Commit | Line | Data |
---|---|---|
0bec96d6 | 1 | #! /usr/bin/env python3 |
ff4136c7 | 2 | # -*- coding: utf-8 -*- |
0bec96d6 AE |
3 | """********************************************************************* |
4 | * Edward is free software: you can redistribute it and/or modify * | |
5 | * it under the terms of the GNU Affero Public License as published by * | |
6 | * the Free Software Foundation, either version 3 of the License, or * | |
7 | * (at your option) any later version. * | |
8 | * * | |
9 | * Edward is distributed in the hope that it will be useful, * | |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * | |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | |
12 | * GNU Affero Public License for more details. * | |
13 | * * | |
14 | * You should have received a copy of the GNU Affero Public License * | |
15 | * along with Edward. If not, see <http://www.gnu.org/licenses/>. * | |
16 | * * | |
17 | * Copyright (C) 2014-2015 Andrew Engelbrecht (AGPLv3+) * | |
18 | * Copyright (C) 2014 Josh Drake (AGPLv3+) * | |
19 | * Copyright (C) 2014 Lisa Marie Maginnis (AGPLv3+) * | |
8bdfb6d4 AE |
20 | * Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) * |
21 | * Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) * | |
0bec96d6 AE |
22 | * * |
23 | * Special thanks to Josh Drake for writing the original edward bot! :) * | |
24 | * * | |
25 | ************************************************************************ | |
26 | ||
a5385c04 | 27 | Code sourced from these projects: |
0bec96d6 | 28 | |
8bdfb6d4 AE |
29 | * http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz |
30 | * https://git-tails.immerda.ch/whisperback/tree/whisperBack/encryption.py?h=feature/python3 | |
31 | * http://www.physics.drexel.edu/~wking/code/python/send_pgp_mime | |
0bec96d6 AE |
32 | """ |
33 | ||
34 | import sys | |
0bec96d6 AE |
35 | import gpgme |
36 | import re | |
37 | import io | |
40c37ab3 | 38 | import os |
adcef2f7 | 39 | import importlib |
0bec96d6 | 40 | |
8bdfb6d4 AE |
41 | import email.parser |
42 | import email.message | |
43 | import email.encoders | |
44 | ||
45 | from email.mime.multipart import MIMEMultipart | |
46 | from email.mime.application import MIMEApplication | |
47 | from email.mime.nonmultipart import MIMENonMultipart | |
48 | ||
40c37ab3 | 49 | import edward_config |
c96f3837 | 50 | |
5b0e37d3 | 51 | langs = ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"] |
adcef2f7 | 52 | |
def9196c AE |
53 | """This list contains the abbreviated names of reply languages available to |
54 | edward.""" | |
55 | ||
56 | ||
38738401 AE |
57 | match_types = [('clearsign', |
58 | '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'), | |
59 | ('message', | |
56578eaf AE |
60 | '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'), |
61 | ('pubkey', | |
62 | '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'), | |
63 | ('detachedsig', | |
38738401 | 64 | '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')] |
56578eaf | 65 | |
def9196c AE |
66 | """This list of tuples matches query names with re.search() queries used |
67 | to find GPG data for edward to process.""" | |
68 | ||
56578eaf AE |
69 | |
70 | class EddyMsg (object): | |
def9196c AE |
71 | """ |
72 | The EddyMsg class represents relevant parts of a mime message. | |
73 | ||
74 | The represented message can be single-part or multi-part. | |
75 | ||
76 | 'multipart' is set to True if there are multiple mime parts. | |
77 | ||
78 | 'subparts' points to a list of mime sub-parts if it is a multi-part | |
79 | message. Otherwise it points to an empty list. | |
80 | ||
81 | 'payload_bytes' contains the raw mime-decoded bytes that haven't been | |
82 | encoded into a character set. | |
83 | ||
84 | 'payload_pieces' is a list of objects containing strings that when strung | |
85 | together form the fully-decoded string representation of the mime part. | |
86 | ||
87 | The 'charset' describes the character set of payload_bytes. | |
88 | ||
89 | The 'filename', 'content_type' and 'description_list' come from the mime | |
90 | part parameters. | |
91 | """ | |
92 | ||
7d0b91d2 AE |
93 | multipart = False |
94 | subparts = [] | |
56578eaf | 95 | |
7d0b91d2 AE |
96 | payload_bytes = None |
97 | payload_pieces = [] | |
56578eaf | 98 | |
7d0b91d2 AE |
99 | charset = None |
100 | filename = None | |
101 | content_type = None | |
102 | description_list = None | |
56578eaf AE |
103 | |
104 | ||
105 | class PayloadPiece (object): | |
def9196c AE |
106 | """ |
107 | PayloadPiece represents a complte or sub-section of a mime part. | |
108 | ||
109 | Instances of this class are often strung together within one or more arrays | |
110 | pointed to by each instance of the EddyMsg class. | |
111 | ||
112 | 'piece_type' refers to a string whose value describes the content of | |
113 | 'string'. Examples include "pubkey", for public keys, and "message", for | |
114 | encrypted data (or armored signatures until they are known to be such.) The | |
115 | names derive from the header and footer of each of these ascii-encoded gpg | |
116 | blocks. | |
117 | ||
118 | 'string' contains some string of text, such as non-GPG text, an encrypted | |
119 | block of text, a signature, or a public key. | |
120 | ||
121 | 'gpg_data' points to any instances of GPGData that have been created based | |
122 | on the contents of 'string'. | |
123 | """ | |
124 | ||
7d0b91d2 AE |
125 | piece_type = None |
126 | string = None | |
127 | gpg_data = None | |
56578eaf AE |
128 | |
129 | ||
38738401 | 130 | class GPGData (object): |
def9196c AE |
131 | """ |
132 | GPGData holds info from decryption, sig. verification, and/or pub. keys. | |
133 | ||
134 | Instances of this class contain decrypted information, signature | |
135 | fingerprints and/or fingerprints of processed and imported public keys. | |
136 | ||
137 | 'decrypted' is set to True if 'plainobj' was created from encrypted data. | |
138 | ||
139 | 'plainobj' points to any decrypted, or signed part of, a GPG signature. It | |
140 | is intended to be an instance of the EddyMsg class. | |
141 | ||
142 | 'sigs' is a list of fingerprints of keys used to sign the data in plainobj. | |
143 | ||
144 | 'keys' is a list of fingerprints of keys obtained in public key blocks. | |
145 | """ | |
146 | ||
7d0b91d2 AE |
147 | decrypted = False |
148 | ||
149 | plainobj = None | |
150 | sigs = [] | |
151 | keys = [] | |
38738401 | 152 | |
38738401 | 153 | |
d873ff48 | 154 | class ReplyInfo (object): |
def9196c AE |
155 | """ |
156 | ReplyInfo contains details that edward uses in generating its reply. | |
157 | ||
158 | Instances of this class contain information about whether a message was | |
159 | successfully encrypted or signed, and whether a public key was attached, or | |
160 | retrievable, from the local GPG store. It stores the fingerprints of | |
161 | potential encryption key candidates and the message (if any at all) to | |
162 | quote in edward's reply. | |
163 | ||
164 | 'replies' points one of the dictionaries of translated replies. | |
165 | ||
166 | 'target_key' refers to the fingerprint of a key used to sign encrypted | |
167 | data. This is the preferred key, if it is set, and if is available. | |
168 | ||
169 | 'fallback_target_key' referst to the fingerprint of a key used to sign | |
170 | unencrypted data; alternatively it may be a public key attached to the | |
171 | message. | |
172 | ||
173 | 'msg_to_quote' refers to the part of a message which edward should quote in | |
174 | his reply. This should remain as None if there was no encrypted and singed | |
175 | part. This is to avoid making edward a service for decrypting other | |
176 | people's messages to edward. | |
177 | ||
178 | 'success_decrypt' is set to True if edward could decrypt part of the | |
179 | message. | |
180 | ||
181 | 'failed_decrypt' is set to True if edward failed to decrypt part of the | |
182 | message. | |
183 | ||
184 | 'publick_key_received' is set to True if edward successfully imported a | |
185 | public key. | |
186 | ||
187 | 'no_public_key' is set to True if edward doesn't have a key to encrypt to | |
188 | when replying to the user. | |
189 | ||
190 | 'sig_success' is set to True if edward could to some extent verify the | |
191 | signature of a signed part of the message to edward. | |
192 | ||
193 | 'sig_failure' is set to True if edward failed to some extent verify the | |
194 | signature of a signed part of the message to edward. | |
195 | """ | |
196 | ||
7d0b91d2 | 197 | replies = None |
6906d46d | 198 | |
7d0b91d2 AE |
199 | target_key = None |
200 | fallback_target_key = None | |
201 | msg_to_quote = "" | |
d873ff48 | 202 | |
7d0b91d2 AE |
203 | success_decrypt = False |
204 | failed_decrypt = False | |
205 | public_key_received = False | |
206 | no_public_key = False | |
207 | sig_success = False | |
208 | sig_failure = False | |
d873ff48 | 209 | |
38738401 | 210 | |
0bec96d6 AE |
211 | def main (): |
212 | ||
def9196c AE |
213 | """ |
214 | This is the main function for edward, a GPG reply bot. | |
215 | ||
216 | Edward responds to GPG-encrypted and signed mail, encrypting and signing | |
217 | the response if the user's public key is, or was, included in the message. | |
218 | ||
219 | Args: | |
220 | None | |
221 | ||
222 | Returns: | |
a3d6aff8 | 223 | Nothing |
def9196c AE |
224 | |
225 | Pre: | |
226 | Mime or plaintext email passing in through standard input. Portions of | |
227 | the email may be encrypted. If the To: address contains the text | |
228 | "edward-ja", then the reply will contain a reply written in the | |
229 | Japanese language. There are other languages as well. The default | |
230 | language is English. | |
231 | ||
232 | Post: | |
233 | A reply email will be printed to standard output. The contents of the | |
234 | reply email depends on whether the original email was encrypted or not, | |
235 | has or doesn't have a signature, whether a public key used in the | |
236 | original message is provided or locally stored, and the language | |
237 | implied by the To: address in the original email. | |
238 | """ | |
239 | ||
20f6e7c5 | 240 | handle_args() |
0bec96d6 | 241 | |
0a064403 AE |
242 | gpgme_ctx = get_gpg_context(edward_config.gnupghome, |
243 | edward_config.sign_with_key) | |
244 | ||
c96f3837 | 245 | email_text = sys.stdin.read() |
adcef2f7 AE |
246 | email_struct = parse_pgp_mime(email_text, gpgme_ctx) |
247 | ||
248 | email_to, email_from, email_subject = email_to_from_subject(email_text) | |
249 | lang = import_lang(email_to) | |
fafa21c3 | 250 | |
d873ff48 AE |
251 | replyinfo_obj = ReplyInfo() |
252 | replyinfo_obj.replies = lang.replies | |
253 | ||
254 | prepare_for_reply(email_struct, replyinfo_obj) | |
013f7cb8 | 255 | encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx) |
d873ff48 | 256 | reply_plaintext = write_reply(replyinfo_obj) |
adcef2f7 | 257 | |
2007103e AE |
258 | reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \ |
259 | email_subject, encrypt_to_key, | |
260 | gpgme_ctx) | |
1fccb295 | 261 | |
2007103e | 262 | print(reply_mime) |
c96f3837 | 263 | |
0bec96d6 | 264 | |
0a064403 | 265 | def get_gpg_context (gnupghome, sign_with_key_fp): |
a3d6aff8 AE |
266 | """ |
267 | This function returns the GPG context needed for encryption and signing. | |
268 | ||
269 | The context is needed by other functions which use GPG functionality. | |
270 | ||
271 | Args: | |
272 | gnupghome: The path to "~/.gnupg/" or its alternative. | |
273 | sign_with_key: The fingerprint of the key to sign with | |
274 | ||
275 | Returns: | |
276 | A gpgme context to be used for GPG functions. | |
277 | ||
278 | Post: | |
279 | the 'armor' flag is set to True and the list of signing keys contains | |
280 | the single specified key | |
281 | """ | |
0a064403 AE |
282 | |
283 | os.environ['GNUPGHOME'] = gnupghome | |
284 | ||
285 | gpgme_ctx = gpgme.Context() | |
286 | gpgme_ctx.armor = True | |
287 | ||
288 | try: | |
289 | sign_with_key = gpgme_ctx.get_key(sign_with_key_fp) | |
290 | except: | |
291 | error("unable to load signing key. is the gnupghome " | |
292 | + "and signing key properly set in the edward_config.py?") | |
293 | exit(1) | |
294 | ||
295 | gpgme_ctx.signers = [sign_with_key] | |
296 | ||
297 | return gpgme_ctx | |
298 | ||
299 | ||
38738401 | 300 | def parse_pgp_mime (email_text, gpgme_ctx): |
a3d6aff8 AE |
301 | """Parses the email for mime payloads and decrypts/verfies signatures. |
302 | ||
303 | This function creates a representation of a mime or plaintext email with | |
304 | the EddyMsg class. It then splits each mime payload into one or more pieces | |
305 | which may be plain text or GPG data. It then decrypts encrypted parts and | |
306 | does some very basic signature verification on those parts. | |
307 | ||
308 | Args: | |
309 | email_text: an email message in string format | |
310 | gpgme_ctx: a gpgme context | |
311 | ||
312 | Returns: | |
313 | A message as an instance of EddyMsg | |
314 | ||
315 | Post: | |
316 | the returned EddyMsg instance has split, decrypted, verified and pubkey | |
317 | imported payloads | |
318 | """ | |
394a1476 AE |
319 | |
320 | email_struct = email.parser.Parser().parsestr(email_text) | |
321 | ||
928e3819 AE |
322 | eddymsg_obj = parse_mime(email_struct) |
323 | split_payloads(eddymsg_obj) | |
324 | gpg_on_payloads(eddymsg_obj, gpgme_ctx) | |
8bb4b0d5 | 325 | |
928e3819 | 326 | return eddymsg_obj |
0bec96d6 | 327 | |
0bec96d6 | 328 | |
56578eaf | 329 | def parse_mime(msg_struct): |
a3d6aff8 AE |
330 | """Translates python's email.parser format into an EddyMsg format |
331 | ||
332 | If the message is multi-part, then a recursive object is created, where | |
333 | each sub-part is also a EddyMsg instance. | |
334 | ||
335 | Args: | |
336 | msg_struct: an email parsed with email.parser.Parser(), which can be | |
337 | multi-part | |
338 | ||
339 | Returns: | |
340 | an instance of EddyMsg, potentially a recursive one. | |
341 | """ | |
0bec96d6 | 342 | |
928e3819 | 343 | eddymsg_obj = EddyMsg() |
8bb4b0d5 | 344 | |
56578eaf AE |
345 | if msg_struct.is_multipart() == True: |
346 | payloads = msg_struct.get_payload() | |
0bec96d6 | 347 | |
928e3819 AE |
348 | eddymsg_obj.multipart = True |
349 | eddymsg_obj.subparts = list(map(parse_mime, payloads)) | |
dd11a483 | 350 | |
56578eaf | 351 | else: |
928e3819 | 352 | eddymsg_obj = get_subpart_data(msg_struct) |
394a1476 | 353 | |
928e3819 | 354 | return eddymsg_obj |
c267c233 | 355 | |
80119cab | 356 | |
56578eaf | 357 | def scan_and_split (payload_piece, match_type, pattern): |
a3d6aff8 AE |
358 | """This splits the payloads of an EddyMsg object into GPG and text parts. |
359 | ||
360 | An EddyMsg object's payload_pieces starts off as a list containing a single | |
361 | PayloadPiece object. This function returns a list of these objects which | |
362 | have been split into GPG data and regular text, if such splits need to be/ | |
363 | can be made. | |
364 | ||
365 | Args: | |
366 | payload_piece: a single payload or a split part of a payload | |
367 | match_type: the type of data to try to spit out from the payload piece | |
368 | pattern: the search pattern to be used for finding that type of data | |
369 | ||
370 | Returns: | |
371 | a list of objects of the PayloadPiece class, in the order that the | |
372 | string part of payload_piece originally was, broken up according to | |
373 | matches specified by 'pattern'. | |
374 | """ | |
cf75de65 | 375 | |
a5d37d44 AE |
376 | # don't try to re-split pieces containing gpg data |
377 | if payload_piece.piece_type != "text": | |
378 | return [payload_piece] | |
379 | ||
56578eaf AE |
380 | flags = re.DOTALL | re.MULTILINE |
381 | matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern + | |
382 | ")(?P<rest>.*)", payload_piece.string, flags=flags) | |
86663388 | 383 | |
56578eaf AE |
384 | if matches == None: |
385 | pieces = [payload_piece] | |
c96f3837 | 386 | |
56578eaf | 387 | else: |
d437f8b2 | 388 | |
56578eaf AE |
389 | beginning = PayloadPiece() |
390 | beginning.string = matches.group('beginning') | |
391 | beginning.piece_type = payload_piece.piece_type | |
d437f8b2 | 392 | |
56578eaf AE |
393 | match = PayloadPiece() |
394 | match.string = matches.group('match') | |
395 | match.piece_type = match_type | |
d437f8b2 | 396 | |
56578eaf AE |
397 | rest = PayloadPiece() |
398 | rest.string = matches.group('rest') | |
399 | rest.piece_type = payload_piece.piece_type | |
d437f8b2 | 400 | |
56578eaf | 401 | more_pieces = scan_and_split(rest, match_type, pattern) |
4615b156 | 402 | pieces = [beginning, match ] + more_pieces |
d437f8b2 | 403 | |
56578eaf | 404 | return pieces |
d437f8b2 | 405 | |
d437f8b2 | 406 | |
56578eaf | 407 | def get_subpart_data (part): |
a3d6aff8 AE |
408 | """This function grabs information from a single part mime object. |
409 | ||
410 | It copies needed data from a single part email.parser.Parser() object over | |
411 | to an EddyMsg object. | |
412 | ||
413 | Args: | |
414 | part: a non-multi-part mime.parser.Parser() object | |
415 | ||
416 | Returns: | |
417 | a single-part EddyMsg() object | |
418 | """ | |
0bec96d6 | 419 | |
56578eaf | 420 | obj = EddyMsg() |
0bec96d6 | 421 | |
56578eaf AE |
422 | obj.charset = part.get_content_charset() |
423 | obj.payload_bytes = part.get_payload(decode=True) | |
424 | ||
425 | obj.filename = part.get_filename() | |
426 | obj.content_type = part.get_content_type() | |
427 | obj.description_list = part['content-description'] | |
428 | ||
429 | # your guess is as good as a-myy-ee-ine... | |
430 | if obj.charset == None: | |
431 | obj.charset = 'utf-8' | |
432 | ||
433 | if obj.payload_bytes != None: | |
0eb75d9c AE |
434 | try: |
435 | payload = PayloadPiece() | |
436 | payload.string = obj.payload_bytes.decode(obj.charset) | |
437 | payload.piece_type = 'text' | |
438 | ||
439 | obj.payload_pieces = [payload] | |
440 | except UnicodeDecodeError: | |
441 | pass | |
56578eaf AE |
442 | |
443 | return obj | |
444 | ||
445 | ||
928e3819 | 446 | def do_to_eddys_pieces (function_to_do, eddymsg_obj, data): |
a3d6aff8 AE |
447 | """A function which maps another function onto a message's subparts. |
448 | ||
449 | This is a higer-order function which recursively performs a specified | |
450 | function on each subpart of a multi-part message. Each single-part sub-part | |
451 | has the function applied to it. This function also works if the part passed | |
452 | in is single-part. | |
453 | ||
454 | Args: | |
455 | function_to_do: function to perform on sub-parts | |
456 | eddymsg_obj: a single part or multi-part EddyMsg object | |
457 | data: a second argument to pass to 'function_to_do' | |
458 | ||
459 | Returns: | |
460 | Nothing | |
461 | ||
462 | Post: | |
463 | The passed-in EddyMsg object is transformed recursively on its | |
464 | sub-parts according to 'function_to_do'. | |
465 | """ | |
56578eaf | 466 | |
928e3819 AE |
467 | if eddymsg_obj.multipart == True: |
468 | for sub in eddymsg_obj.subparts: | |
d873ff48 | 469 | do_to_eddys_pieces(function_to_do, sub, data) |
394a1476 | 470 | else: |
928e3819 | 471 | function_to_do(eddymsg_obj, data) |
dd11a483 AE |
472 | |
473 | ||
928e3819 | 474 | def split_payloads (eddymsg_obj): |
a3d6aff8 AE |
475 | """Splits all (sub-)payloads of a message into GPG data and regular text. |
476 | ||
477 | Recursively performs payload splitting on all sub-parts of an EddyMsg | |
478 | object into the various GPG data types, such as GPG messages, public key | |
479 | blocks and signed text. | |
480 | ||
481 | Args: | |
482 | eddymsg_obj: an instance of EddyMsg | |
483 | ||
484 | Returns: | |
485 | Nothing | |
486 | ||
487 | Pre: | |
488 | The EddyMsg object has payloads that are unsplit (by may be split).. | |
489 | ||
490 | Post: | |
491 | The EddyMsg object's payloads are all split into GPG and non-GPG parts. | |
492 | """ | |
a5d37d44 AE |
493 | |
494 | for match_type in match_types: | |
928e3819 | 495 | do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type) |
a5d37d44 | 496 | |
a5d37d44 | 497 | |
928e3819 | 498 | def split_payload_pieces (eddymsg_obj, match_type): |
a3d6aff8 AE |
499 | """A helper function for split_payloads(); works on PayloadPiece objects. |
500 | ||
501 | This function splits up PayloadPiece objects into multipe PayloadPiece | |
502 | objects and replaces the EddyMsg object's previous list of payload pieces | |
503 | with the new split up one. | |
504 | ||
505 | Args: | |
506 | eddymsg_obj: a single-part EddyMsg object. | |
507 | match_type: a tuple from the match_types list, which specifies a match | |
508 | name and a match pattern. | |
509 | ||
510 | Returns: | |
511 | Nothing | |
512 | ||
513 | Pre: | |
514 | The payload piece(s) of an EddyMsg object may be already split or | |
515 | unsplit. | |
516 | ||
517 | Post: | |
518 | The EddyMsg object's payload piece(s) are split into a list of pieces | |
519 | if matches of the match_type are found. | |
520 | """ | |
a5d37d44 AE |
521 | |
522 | (match_name, pattern) = match_type | |
523 | ||
524 | new_pieces_list = [] | |
928e3819 | 525 | for piece in eddymsg_obj.payload_pieces: |
a5d37d44 AE |
526 | new_pieces_list += scan_and_split(piece, match_name, pattern) |
527 | ||
928e3819 | 528 | eddymsg_obj.payload_pieces = new_pieces_list |
a5d37d44 AE |
529 | |
530 | ||
928e3819 | 531 | def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): |
a3d6aff8 AE |
532 | """Performs GPG operations on the GPG parts of the message |
533 | ||
534 | This function decrypts text, verifies signatures, and imports public keys | |
535 | included in an email. | |
536 | ||
537 | Args: | |
538 | eddymsg_obj: an EddyMsg object with its payload_pieces split into GPG | |
539 | and non-GPG sections by split_payloads() | |
540 | gpgme_ctx: a gpgme context | |
541 | ||
542 | prev_parts: a list of mime parts that occur before the eddymsg_obj | |
543 | part, under the same multi-part mime part. This is used for | |
544 | verifying detached signatures. For the root mime part, this should | |
545 | be an empty list, which is the default value if this paramater is | |
546 | omitted. | |
547 | ||
548 | Return: | |
549 | Nothing | |
550 | ||
551 | Pre: | |
552 | eddymsg_obj should have its payloads split into gpg and non-gpg pieces. | |
553 | ||
554 | Post: | |
555 | Decryption, verification and key imports occur. the gpg_data member of | |
556 | PayloadPiece objects get filled in with GPGData objects. | |
557 | """ | |
38738401 | 558 | |
928e3819 | 559 | if eddymsg_obj.multipart == True: |
101d54a8 | 560 | prev_parts=[] |
928e3819 | 561 | for sub in eddymsg_obj.subparts: |
101d54a8 AE |
562 | gpg_on_payloads (sub, gpgme_ctx, prev_parts) |
563 | prev_parts += [sub] | |
38738401 | 564 | |
d873ff48 | 565 | return |
38738401 | 566 | |
928e3819 | 567 | for piece in eddymsg_obj.payload_pieces: |
38738401 AE |
568 | |
569 | if piece.piece_type == "text": | |
570 | # don't transform the plaintext. | |
571 | pass | |
572 | ||
573 | elif piece.piece_type == "message": | |
7e18f14d | 574 | (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx) |
38738401 AE |
575 | |
576 | if plaintext: | |
577 | piece.gpg_data = GPGData() | |
b23e171d | 578 | piece.gpg_data.decrypted = True |
38738401 AE |
579 | piece.gpg_data.sigs = sigs |
580 | # recurse! | |
581 | piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx) | |
3a753fd0 AE |
582 | continue |
583 | ||
584 | # if not encrypted, check to see if this is an armored signature. | |
585 | (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx) | |
586 | ||
587 | if plaintext: | |
588 | piece.piece_type = "signature" | |
589 | piece.gpg_data = GPGData() | |
590 | piece.gpg_data.sigs = sigs | |
591 | # recurse! | |
592 | piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx) | |
129543c3 | 593 | |
8f61c66a | 594 | elif piece.piece_type == "pubkey": |
f8ee6bd3 | 595 | key_fps = add_gpg_key(piece.string, gpgme_ctx) |
8f61c66a | 596 | |
f8ee6bd3 | 597 | if key_fps != []: |
8f61c66a | 598 | piece.gpg_data = GPGData() |
f8ee6bd3 | 599 | piece.gpg_data.keys = key_fps |
129543c3 AE |
600 | |
601 | elif piece.piece_type == "clearsign": | |
f8ee6bd3 | 602 | (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx) |
129543c3 | 603 | |
f8ee6bd3 | 604 | if sig_fps != []: |
129543c3 | 605 | piece.gpg_data = GPGData() |
f8ee6bd3 | 606 | piece.gpg_data.sigs = sig_fps |
129543c3 AE |
607 | piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx) |
608 | ||
101d54a8 AE |
609 | elif piece.piece_type == "detachedsig": |
610 | for prev in prev_parts: | |
611 | payload_bytes = prev.payload_bytes | |
6d240f68 | 612 | sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx) |
101d54a8 | 613 | |
6d240f68 AE |
614 | if sig_fps != []: |
615 | piece.gpg_data = GPGData() | |
616 | piece.gpg_data.sigs = sig_fps | |
617 | piece.gpg_data.plainobj = prev | |
618 | break | |
7e18f14d | 619 | |
38738401 AE |
620 | else: |
621 | pass | |
622 | ||
623 | ||
928e3819 | 624 | def prepare_for_reply (eddymsg_obj, replyinfo_obj): |
67691346 AE |
625 | """Updates replyinfo_obj with info on the message's GPG success/failures |
626 | ||
627 | This function marks replyinfo_obj with information about whether encrypted | |
628 | text in eddymsg_obj was successfully decrypted, signatures were verified | |
629 | and whether a public key was found or not. | |
630 | ||
631 | Args: | |
632 | eddymsg_obj: a message in the EddyMsg format | |
633 | replyinfo_obj: an instance of ReplyInfo | |
634 | ||
635 | Returns: | |
636 | Nothing | |
637 | ||
638 | Pre: | |
639 | eddymsg_obj has had its gpg_data created by gpg_on_payloads | |
640 | ||
641 | Post: | |
642 | replyinfo_obj has been updated with info about decryption/sig | |
643 | verififcation status, etc. However the desired key isn't imported until | |
644 | later, so the success or failure of that updates the values set here. | |
645 | """ | |
dd11a483 | 646 | |
928e3819 | 647 | do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj) |
56578eaf | 648 | |
928e3819 | 649 | def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj): |
67691346 AE |
650 | """A helper function for prepare_for_reply |
651 | ||
652 | It updates replyinfo_obj with GPG success/failure information, when | |
653 | supplied a single-part EddyMsg object. | |
654 | ||
655 | Args: | |
656 | eddymsg_obj: a single-part message in the EddyMsg format | |
657 | replyinfo_obj: an object which holds information about the message's | |
658 | GPG status | |
659 | ||
660 | Returns: | |
661 | Nothing | |
662 | ||
663 | Pre: | |
664 | eddymsg_obj is a single-part message. (it may be a part of a multi-part | |
665 | message.) It has had its gpg_data created by gpg_on_payloads if it has | |
666 | gpg data. | |
667 | ||
668 | Post: | |
669 | replyinfo_obj has been updated with gpg success/failure information | |
670 | """ | |
56578eaf | 671 | |
928e3819 | 672 | for piece in eddymsg_obj.payload_pieces: |
38738401 | 673 | if piece.piece_type == "text": |
d873ff48 AE |
674 | # don't quote the plaintext part. |
675 | pass | |
676 | ||
38738401 | 677 | elif piece.piece_type == "message": |
05683768 | 678 | prepare_for_reply_message(piece, replyinfo_obj) |
d873ff48 | 679 | |
05683768 AE |
680 | elif piece.piece_type == "pubkey": |
681 | prepare_for_reply_pubkey(piece, replyinfo_obj) | |
6906d46d | 682 | |
05683768 | 683 | elif (piece.piece_type == "clearsign") \ |
3a753fd0 AE |
684 | or (piece.piece_type == "detachedsig") \ |
685 | or (piece.piece_type == "signature"): | |
05683768 | 686 | prepare_for_reply_sig(piece, replyinfo_obj) |
d873ff48 | 687 | |
d873ff48 | 688 | |
05683768 | 689 | def prepare_for_reply_message (piece, replyinfo_obj): |
67691346 AE |
690 | """Helper function for prepare_for_reply() |
691 | ||
692 | This function is called when the piece_type of a payload piece is | |
693 | "message", or GPG Message block. This should be encrypted text. If the | |
694 | encryted block is signed, a sig will be attached to .target_key unless | |
695 | there is already one there. | |
696 | ||
697 | Args: | |
698 | piece: a PayloadPiece object. | |
699 | replyinfo_obj: object which gets updated with decryption status, etc. | |
700 | ||
701 | ||
702 | Returns: | |
703 | Nothing | |
704 | ||
705 | Pre: | |
706 | the piece.payload_piece value should be "message". | |
707 | ||
708 | Post: | |
709 | replyinfo_obj gets updated with decryption status, signing status and a | |
710 | potential signing key. | |
711 | """ | |
44fe39b7 | 712 | |
05683768 AE |
713 | if piece.gpg_data == None: |
714 | replyinfo_obj.failed_decrypt = True | |
715 | return | |
716 | ||
5c40e676 | 717 | replyinfo_obj.success_decrypt = True |
05683768 AE |
718 | |
719 | # we already have a key (and a message) | |
720 | if replyinfo_obj.target_key != None: | |
721 | return | |
722 | ||
723 | if piece.gpg_data.sigs != []: | |
724 | replyinfo_obj.target_key = piece.gpg_data.sigs[0] | |
725 | get_signed_part = False | |
726 | else: | |
727 | # only include a signed message in the reply. | |
728 | get_signed_part = True | |
729 | ||
730 | replyinfo_obj.msg_to_quote = flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part) | |
731 | ||
732 | # to catch public keys in encrypted blocks | |
733 | prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj) | |
734 | ||
735 | ||
736 | def prepare_for_reply_pubkey (piece, replyinfo_obj): | |
67691346 AE |
737 | """Helper function for prepare_for_reply(). Marks pubkey import status. |
738 | ||
739 | Marks replyinfo_obj with pub key import status. | |
740 | ||
741 | Args: | |
742 | piece: a PayloadPiece object | |
743 | replyinfo_obj: a ReplyInfo object | |
744 | ||
745 | Pre: | |
746 | piece.piece_type should be set to "pubkey". | |
747 | ||
748 | Post: | |
749 | replyinfo_obj has its fields updated. | |
750 | """ | |
05683768 AE |
751 | |
752 | if piece.gpg_data == None or piece.gpg_data.keys == []: | |
753 | replyinfo_obj.no_public_key = True | |
754 | else: | |
755 | replyinfo_obj.public_key_received = True | |
756 | ||
757 | if replyinfo_obj.fallback_target_key == None: | |
758 | replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0] | |
759 | ||
760 | ||
761 | def prepare_for_reply_sig (piece, replyinfo_obj): | |
67691346 AE |
762 | """Helper function for prepare_for_reply(). Marks sig verification status. |
763 | ||
764 | Marks replyinfo_obj with signature verification status. | |
765 | ||
766 | Args: | |
767 | piece: a PayloadPiece object | |
768 | replyinfo_obj: a ReplyInfo object | |
769 | ||
770 | Pre: | |
771 | piece.piece_type should be set to "clearsign", "signature", or | |
772 | "detachedsig". | |
773 | ||
774 | Post: | |
775 | replyinfo_obj has its fields updated. | |
776 | """ | |
05683768 AE |
777 | |
778 | if piece.gpg_data == None or piece.gpg_data.sigs == []: | |
779 | replyinfo_obj.sig_failure = True | |
780 | else: | |
781 | replyinfo_obj.sig_success = True | |
782 | ||
783 | if replyinfo_obj.fallback_target_key == None: | |
784 | replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0] | |
6906d46d AE |
785 | |
786 | ||
d873ff48 | 787 | |
0402995a | 788 | def flatten_decrypted_payloads (eddymsg_obj, get_signed_part): |
d873ff48 AE |
789 | |
790 | flat_string = "" | |
791 | ||
0aa88c27 AE |
792 | if eddymsg_obj == None: |
793 | return "" | |
794 | ||
0402995a | 795 | # recurse on multi-part mime |
928e3819 AE |
796 | if eddymsg_obj.multipart == True: |
797 | for sub in eddymsg_obj.subparts: | |
0d553ce9 | 798 | flat_string += flatten_decrypted_payloads (sub, get_signed_part) |
d873ff48 AE |
799 | |
800 | return flat_string | |
801 | ||
928e3819 | 802 | for piece in eddymsg_obj.payload_pieces: |
0402995a AE |
803 | if (get_signed_part): |
804 | # don't include nested encryption | |
c3d417bf | 805 | if ((piece.piece_type == "clearsign") \ |
3a753fd0 AE |
806 | or (piece.piece_type == "detachedsig") \ |
807 | or (piece.piece_type == "signature")) \ | |
0402995a AE |
808 | and (piece.gpg_data != None): |
809 | # FIXME: the key used to sign this message needs to be the one that is used for the encrypted reply. | |
c3d417bf AE |
810 | flat_string += flatten_decrypted_payloads (piece.gpg_data.plainobj, False) |
811 | break | |
812 | else: | |
813 | if piece.piece_type == "text": | |
814 | flat_string += piece.string | |
d873ff48 AE |
815 | |
816 | return flat_string | |
817 | ||
818 | ||
013f7cb8 AE |
819 | def get_key_from_fp (replyinfo_obj, gpgme_ctx): |
820 | ||
dc24daf1 AE |
821 | if replyinfo_obj.target_key == None: |
822 | replyinfo_obj.target_key = replyinfo_obj.fallback_target_key | |
823 | ||
013f7cb8 AE |
824 | if replyinfo_obj.target_key != None: |
825 | try: | |
826 | encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key) | |
827 | return encrypt_to_key | |
828 | ||
829 | except: | |
830 | pass | |
831 | ||
832 | # no available key to use | |
833 | replyinfo_obj.target_key = None | |
834 | replyinfo_obj.fallback_target_key = None | |
835 | ||
836 | replyinfo_obj.no_public_key = True | |
837 | replyinfo_obj.public_key_received = False | |
838 | ||
839 | return None | |
840 | ||
841 | ||
d873ff48 AE |
842 | def write_reply (replyinfo_obj): |
843 | ||
844 | reply_plain = "" | |
845 | ||
846 | if replyinfo_obj.success_decrypt == True: | |
d873ff48 | 847 | reply_plain += replyinfo_obj.replies['success_decrypt'] |
2694709a AE |
848 | |
849 | if replyinfo_obj.no_public_key == False: | |
850 | quoted_text = email_quote_text(replyinfo_obj.msg_to_quote) | |
851 | reply_plain += quoted_text | |
d873ff48 AE |
852 | |
853 | elif replyinfo_obj.failed_decrypt == True: | |
854 | reply_plain += replyinfo_obj.replies['failed_decrypt'] | |
855 | ||
856 | ||
857 | if replyinfo_obj.sig_success == True: | |
858 | reply_plain += "\n\n" | |
859 | reply_plain += replyinfo_obj.replies['sig_success'] | |
860 | ||
861 | elif replyinfo_obj.sig_failure == True: | |
862 | reply_plain += "\n\n" | |
863 | reply_plain += replyinfo_obj.replies['sig_failure'] | |
864 | ||
865 | ||
866 | if replyinfo_obj.public_key_received == True: | |
867 | reply_plain += "\n\n" | |
868 | reply_plain += replyinfo_obj.replies['public_key_received'] | |
869 | ||
870 | elif replyinfo_obj.no_public_key == True: | |
871 | reply_plain += "\n\n" | |
872 | reply_plain += replyinfo_obj.replies['no_public_key'] | |
873 | ||
874 | ||
875 | reply_plain += "\n\n" | |
876 | reply_plain += replyinfo_obj.replies['signature'] | |
56578eaf | 877 | |
d873ff48 | 878 | return reply_plain |
56578eaf AE |
879 | |
880 | ||
8f61c66a | 881 | def add_gpg_key (key_block, gpgme_ctx): |
c267c233 | 882 | |
8f61c66a | 883 | fp = io.BytesIO(key_block.encode('ascii')) |
c267c233 | 884 | |
8f61c66a AE |
885 | result = gpgme_ctx.import_(fp) |
886 | imports = result.imports | |
c267c233 | 887 | |
f8ee6bd3 | 888 | key_fingerprints = [] |
c267c233 | 889 | |
8f61c66a AE |
890 | if imports != []: |
891 | for import_ in imports: | |
892 | fingerprint = import_[0] | |
f8ee6bd3 | 893 | key_fingerprints += [fingerprint] |
c267c233 | 894 | |
e49673aa | 895 | debug("added gpg key: " + fingerprint) |
ec1e779a | 896 | |
f8ee6bd3 | 897 | return key_fingerprints |
ec1e779a AE |
898 | |
899 | ||
3a753fd0 AE |
900 | def verify_sig_message (msg_block, gpgme_ctx): |
901 | ||
902 | block_b = io.BytesIO(msg_block.encode('ascii')) | |
903 | plain_b = io.BytesIO() | |
904 | ||
905 | try: | |
906 | sigs = gpgme_ctx.verify(block_b, None, plain_b) | |
907 | except: | |
908 | return ("",[]) | |
909 | ||
910 | plaintext = plain_b.getvalue().decode('utf-8') | |
911 | ||
912 | fingerprints = [] | |
913 | for sig in sigs: | |
914 | fingerprints += [sig.fpr] | |
915 | return (plaintext, fingerprints) | |
916 | ||
917 | ||
129543c3 | 918 | def verify_clear_signature (sig_block, gpgme_ctx): |
cf75de65 | 919 | |
129543c3 AE |
920 | # FIXME: this might require the un-decoded bytes |
921 | # or the correct re-encoding with the carset of the mime part. | |
922 | msg_fp = io.BytesIO(sig_block.encode('utf-8')) | |
923 | ptxt_fp = io.BytesIO() | |
cf75de65 | 924 | |
129543c3 | 925 | result = gpgme_ctx.verify(msg_fp, None, ptxt_fp) |
cf75de65 | 926 | |
129543c3 AE |
927 | # FIXME: this might require using the charset of the mime part. |
928 | plaintext = ptxt_fp.getvalue().decode('utf-8') | |
cf75de65 | 929 | |
f8ee6bd3 | 930 | sig_fingerprints = [] |
129543c3 | 931 | for res_ in result: |
f8ee6bd3 | 932 | sig_fingerprints += [res_.fpr] |
cf75de65 | 933 | |
f8ee6bd3 | 934 | return plaintext, sig_fingerprints |
cf75de65 AE |
935 | |
936 | ||
101d54a8 AE |
937 | def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx): |
938 | ||
939 | detached_sig_fp = io.BytesIO(detached_sig.encode('ascii')) | |
940 | plaintext_fp = io.BytesIO(plaintext_bytes) | |
941 | ptxt_fp = io.BytesIO() | |
942 | ||
943 | result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None) | |
944 | ||
945 | sig_fingerprints = [] | |
946 | for res_ in result: | |
947 | sig_fingerprints += [res_.fpr] | |
948 | ||
949 | return sig_fingerprints | |
950 | ||
951 | ||
5b3053c1 | 952 | def decrypt_block (msg_block, gpgme_ctx): |
0bec96d6 | 953 | |
5b3053c1 | 954 | block_b = io.BytesIO(msg_block.encode('ascii')) |
0bec96d6 AE |
955 | plain_b = io.BytesIO() |
956 | ||
afc1f64c AE |
957 | try: |
958 | sigs = gpgme_ctx.decrypt_verify(block_b, plain_b) | |
959 | except: | |
960 | return ("",[]) | |
0bec96d6 | 961 | |
6aa41372 | 962 | plaintext = plain_b.getvalue().decode('utf-8') |
cbdf22c1 AE |
963 | |
964 | fingerprints = [] | |
965 | for sig in sigs: | |
966 | fingerprints += [sig.fpr] | |
967 | return (plaintext, fingerprints) | |
0bec96d6 AE |
968 | |
969 | ||
d0489345 | 970 | def choose_reply_encryption_key (gpgme_ctx, fingerprints): |
fafa21c3 AE |
971 | |
972 | reply_key = None | |
d0489345 AE |
973 | for fp in fingerprints: |
974 | try: | |
975 | key = gpgme_ctx.get_key(fp) | |
976 | ||
977 | if (key.can_encrypt == True): | |
978 | reply_key = key | |
979 | break | |
980 | except: | |
981 | continue | |
982 | ||
fafa21c3 | 983 | |
216708e9 | 984 | return reply_key |
fafa21c3 AE |
985 | |
986 | ||
d65993b8 AE |
987 | def email_to_from_subject (email_text): |
988 | ||
989 | email_struct = email.parser.Parser().parsestr(email_text) | |
990 | ||
991 | email_to = email_struct['To'] | |
992 | email_from = email_struct['From'] | |
993 | email_subject = email_struct['Subject'] | |
994 | ||
995 | return email_to, email_from, email_subject | |
996 | ||
997 | ||
adcef2f7 AE |
998 | def import_lang(email_to): |
999 | ||
5250b3b8 AE |
1000 | if email_to != None: |
1001 | for lang in langs: | |
1002 | if "edward-" + lang in email_to: | |
1003 | lang = "lang." + re.sub('-', '_', lang) | |
1004 | language = importlib.import_module(lang) | |
adcef2f7 | 1005 | |
5250b3b8 | 1006 | return language |
adcef2f7 AE |
1007 | |
1008 | return importlib.import_module("lang.en") | |
1009 | ||
1010 | ||
bf79a93e | 1011 | def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key, |
0a064403 | 1012 | gpgme_ctx): |
1da9b527 | 1013 | |
2007103e | 1014 | # quoted printable encoding lets most ascii characters look normal |
2a2359a4 | 1015 | # before the mime message is decoded. |
2007103e AE |
1016 | char_set = email.charset.Charset("utf-8") |
1017 | char_set.body_encoding = email.charset.QP | |
8bdfb6d4 | 1018 | |
2007103e AE |
1019 | # MIMEText doesn't allow setting the text encoding |
1020 | # so we use MIMENonMultipart. | |
1021 | plaintext_mime = MIMENonMultipart('text', 'plain') | |
1022 | plaintext_mime.set_payload(plaintext, charset=char_set) | |
216708e9 AE |
1023 | |
1024 | if (encrypt_to_key != None): | |
8bdfb6d4 AE |
1025 | |
1026 | encrypted_text = encrypt_sign_message(plaintext_mime.as_string(), | |
1027 | encrypt_to_key, | |
40c37ab3 | 1028 | gpgme_ctx) |
8bdfb6d4 AE |
1029 | |
1030 | control_mime = MIMEApplication("Version: 1", | |
1031 | _subtype='pgp-encrypted', | |
1032 | _encoder=email.encoders.encode_7or8bit) | |
1033 | control_mime['Content-Description'] = 'PGP/MIME version identification' | |
1034 | control_mime.set_charset('us-ascii') | |
1035 | ||
1036 | encoded_mime = MIMEApplication(encrypted_text, | |
1037 | _subtype='octet-stream; name="encrypted.asc"', | |
1038 | _encoder=email.encoders.encode_7or8bit) | |
1039 | encoded_mime['Content-Description'] = 'OpenPGP encrypted message' | |
1040 | encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"' | |
1041 | encoded_mime.set_charset('us-ascii') | |
1042 | ||
1043 | message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted") | |
1044 | message_mime.attach(control_mime) | |
1045 | message_mime.attach(encoded_mime) | |
1046 | message_mime['Content-Disposition'] = 'inline' | |
216708e9 | 1047 | |
216708e9 | 1048 | else: |
2007103e AE |
1049 | message_mime = plaintext_mime |
1050 | ||
1051 | message_mime['To'] = email_from | |
1052 | message_mime['Subject'] = email_subject | |
1053 | ||
1054 | reply = message_mime.as_string() | |
1da9b527 AE |
1055 | |
1056 | return reply | |
1057 | ||
1058 | ||
f87041f8 AE |
1059 | def email_quote_text (text): |
1060 | ||
1061 | quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE) | |
1062 | ||
1063 | return quoted_message | |
1064 | ||
1065 | ||
0a064403 | 1066 | def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx): |
897cbaf6 | 1067 | |
6aa41372 | 1068 | plaintext_bytes = io.BytesIO(plaintext.encode('ascii')) |
1da9b527 AE |
1069 | encrypted_bytes = io.BytesIO() |
1070 | ||
897cbaf6 | 1071 | gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST, |
1da9b527 AE |
1072 | plaintext_bytes, encrypted_bytes) |
1073 | ||
6aa41372 | 1074 | encrypted_txt = encrypted_bytes.getvalue().decode('ascii') |
1da9b527 AE |
1075 | return encrypted_txt |
1076 | ||
1077 | ||
0a064403 AE |
1078 | def error (error_msg): |
1079 | ||
e4fb2ab2 | 1080 | sys.stderr.write(progname + ": " + str(error_msg) + "\n") |
0a064403 AE |
1081 | |
1082 | ||
5e8f9094 AE |
1083 | def debug (debug_msg): |
1084 | ||
1085 | if edward_config.debug == True: | |
0a064403 | 1086 | error(debug_msg) |
5e8f9094 AE |
1087 | |
1088 | ||
20f6e7c5 | 1089 | def handle_args (): |
20f6e7c5 | 1090 | |
a51cdb72 AE |
1091 | global progname |
1092 | progname = sys.argv[0] | |
1093 | ||
1094 | if len(sys.argv) > 1: | |
1095 | print(progname + ": error, this program doesn't " \ | |
1096 | "need any arguments.", file=sys.stderr) | |
1097 | exit(1) | |
20f6e7c5 | 1098 | |
20f6e7c5 | 1099 | |
a51cdb72 | 1100 | if __name__ == "__main__": |
20f6e7c5 | 1101 | |
a51cdb72 | 1102 | main() |
0bec96d6 | 1103 |