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 | ||
1e98a070 | 57 | match_types = [('message', |
56578eaf AE |
58 | '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'), |
59 | ('pubkey', | |
60 | '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'), | |
61 | ('detachedsig', | |
38738401 | 62 | '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')] |
56578eaf | 63 | |
def9196c AE |
64 | """This list of tuples matches query names with re.search() queries used |
65 | to find GPG data for edward to process.""" | |
66 | ||
56578eaf AE |
67 | |
68 | class EddyMsg (object): | |
def9196c AE |
69 | """ |
70 | The EddyMsg class represents relevant parts of a mime message. | |
71 | ||
72 | The represented message can be single-part or multi-part. | |
73 | ||
74 | 'multipart' is set to True if there are multiple mime parts. | |
75 | ||
76 | 'subparts' points to a list of mime sub-parts if it is a multi-part | |
77 | message. Otherwise it points to an empty list. | |
78 | ||
6d8faaa7 AE |
79 | 'payload_bytes' is a binary representation of the mime part before header |
80 | removal and message decoding. | |
def9196c AE |
81 | |
82 | 'payload_pieces' is a list of objects containing strings that when strung | |
83 | together form the fully-decoded string representation of the mime part. | |
84 | ||
def9196c AE |
85 | The 'filename', 'content_type' and 'description_list' come from the mime |
86 | part parameters. | |
87 | """ | |
88 | ||
7d0b91d2 AE |
89 | multipart = False |
90 | subparts = [] | |
56578eaf | 91 | |
7d0b91d2 AE |
92 | payload_bytes = None |
93 | payload_pieces = [] | |
56578eaf | 94 | |
7d0b91d2 AE |
95 | filename = None |
96 | content_type = None | |
97 | description_list = None | |
56578eaf AE |
98 | |
99 | ||
100 | class PayloadPiece (object): | |
def9196c AE |
101 | """ |
102 | PayloadPiece represents a complte or sub-section of a mime part. | |
103 | ||
104 | Instances of this class are often strung together within one or more arrays | |
105 | pointed to by each instance of the EddyMsg class. | |
106 | ||
107 | 'piece_type' refers to a string whose value describes the content of | |
108 | 'string'. Examples include "pubkey", for public keys, and "message", for | |
109 | encrypted data (or armored signatures until they are known to be such.) The | |
110 | names derive from the header and footer of each of these ascii-encoded gpg | |
111 | blocks. | |
112 | ||
113 | 'string' contains some string of text, such as non-GPG text, an encrypted | |
114 | block of text, a signature, or a public key. | |
115 | ||
116 | 'gpg_data' points to any instances of GPGData that have been created based | |
117 | on the contents of 'string'. | |
118 | """ | |
119 | ||
7d0b91d2 AE |
120 | piece_type = None |
121 | string = None | |
122 | gpg_data = None | |
56578eaf AE |
123 | |
124 | ||
38738401 | 125 | class GPGData (object): |
def9196c AE |
126 | """ |
127 | GPGData holds info from decryption, sig. verification, and/or pub. keys. | |
128 | ||
129 | Instances of this class contain decrypted information, signature | |
130 | fingerprints and/or fingerprints of processed and imported public keys. | |
131 | ||
132 | 'decrypted' is set to True if 'plainobj' was created from encrypted data. | |
133 | ||
134 | 'plainobj' points to any decrypted, or signed part of, a GPG signature. It | |
135 | is intended to be an instance of the EddyMsg class. | |
136 | ||
137 | 'sigs' is a list of fingerprints of keys used to sign the data in plainobj. | |
138 | ||
139 | 'keys' is a list of fingerprints of keys obtained in public key blocks. | |
140 | """ | |
141 | ||
7d0b91d2 AE |
142 | decrypted = False |
143 | ||
144 | plainobj = None | |
145 | sigs = [] | |
146 | keys = [] | |
38738401 | 147 | |
38738401 | 148 | |
d873ff48 | 149 | class ReplyInfo (object): |
def9196c AE |
150 | """ |
151 | ReplyInfo contains details that edward uses in generating its reply. | |
152 | ||
153 | Instances of this class contain information about whether a message was | |
154 | successfully encrypted or signed, and whether a public key was attached, or | |
155 | retrievable, from the local GPG store. It stores the fingerprints of | |
156 | potential encryption key candidates and the message (if any at all) to | |
157 | quote in edward's reply. | |
158 | ||
159 | 'replies' points one of the dictionaries of translated replies. | |
160 | ||
161 | 'target_key' refers to the fingerprint of a key used to sign encrypted | |
162 | data. This is the preferred key, if it is set, and if is available. | |
163 | ||
164 | 'fallback_target_key' referst to the fingerprint of a key used to sign | |
165 | unencrypted data; alternatively it may be a public key attached to the | |
166 | message. | |
167 | ||
168 | 'msg_to_quote' refers to the part of a message which edward should quote in | |
169 | his reply. This should remain as None if there was no encrypted and singed | |
170 | part. This is to avoid making edward a service for decrypting other | |
171 | people's messages to edward. | |
172 | ||
173 | 'success_decrypt' is set to True if edward could decrypt part of the | |
174 | message. | |
175 | ||
176 | 'failed_decrypt' is set to True if edward failed to decrypt part of the | |
177 | message. | |
178 | ||
179 | 'publick_key_received' is set to True if edward successfully imported a | |
180 | public key. | |
181 | ||
182 | 'no_public_key' is set to True if edward doesn't have a key to encrypt to | |
183 | when replying to the user. | |
184 | ||
185 | 'sig_success' is set to True if edward could to some extent verify the | |
186 | signature of a signed part of the message to edward. | |
187 | ||
188 | 'sig_failure' is set to True if edward failed to some extent verify the | |
189 | signature of a signed part of the message to edward. | |
190 | """ | |
191 | ||
7d0b91d2 | 192 | replies = None |
6906d46d | 193 | |
7d0b91d2 AE |
194 | target_key = None |
195 | fallback_target_key = None | |
196 | msg_to_quote = "" | |
d873ff48 | 197 | |
7d0b91d2 AE |
198 | success_decrypt = False |
199 | failed_decrypt = False | |
200 | public_key_received = False | |
201 | no_public_key = False | |
202 | sig_success = False | |
203 | sig_failure = False | |
d873ff48 | 204 | |
38738401 | 205 | |
0bec96d6 AE |
206 | def main (): |
207 | ||
def9196c AE |
208 | """ |
209 | This is the main function for edward, a GPG reply bot. | |
210 | ||
211 | Edward responds to GPG-encrypted and signed mail, encrypting and signing | |
212 | the response if the user's public key is, or was, included in the message. | |
213 | ||
214 | Args: | |
215 | None | |
216 | ||
217 | Returns: | |
a3d6aff8 | 218 | Nothing |
def9196c AE |
219 | |
220 | Pre: | |
221 | Mime or plaintext email passing in through standard input. Portions of | |
222 | the email may be encrypted. If the To: address contains the text | |
223 | "edward-ja", then the reply will contain a reply written in the | |
224 | Japanese language. There are other languages as well. The default | |
225 | language is English. | |
226 | ||
227 | Post: | |
228 | A reply email will be printed to standard output. The contents of the | |
229 | reply email depends on whether the original email was encrypted or not, | |
230 | has or doesn't have a signature, whether a public key used in the | |
231 | original message is provided or locally stored, and the language | |
232 | implied by the To: address in the original email. | |
233 | """ | |
234 | ||
20f6e7c5 | 235 | handle_args() |
0bec96d6 | 236 | |
0a064403 AE |
237 | gpgme_ctx = get_gpg_context(edward_config.gnupghome, |
238 | edward_config.sign_with_key) | |
239 | ||
c96f3837 | 240 | email_text = sys.stdin.read() |
adcef2f7 AE |
241 | email_struct = parse_pgp_mime(email_text, gpgme_ctx) |
242 | ||
243 | email_to, email_from, email_subject = email_to_from_subject(email_text) | |
244 | lang = import_lang(email_to) | |
fafa21c3 | 245 | |
d873ff48 AE |
246 | replyinfo_obj = ReplyInfo() |
247 | replyinfo_obj.replies = lang.replies | |
248 | ||
249 | prepare_for_reply(email_struct, replyinfo_obj) | |
013f7cb8 | 250 | encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx) |
d873ff48 | 251 | reply_plaintext = write_reply(replyinfo_obj) |
adcef2f7 | 252 | |
2007103e AE |
253 | reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \ |
254 | email_subject, encrypt_to_key, | |
255 | gpgme_ctx) | |
1fccb295 | 256 | |
2007103e | 257 | print(reply_mime) |
c96f3837 | 258 | |
0bec96d6 | 259 | |
0a064403 | 260 | def get_gpg_context (gnupghome, sign_with_key_fp): |
a3d6aff8 AE |
261 | """ |
262 | This function returns the GPG context needed for encryption and signing. | |
263 | ||
264 | The context is needed by other functions which use GPG functionality. | |
265 | ||
266 | Args: | |
267 | gnupghome: The path to "~/.gnupg/" or its alternative. | |
268 | sign_with_key: The fingerprint of the key to sign with | |
269 | ||
270 | Returns: | |
271 | A gpgme context to be used for GPG functions. | |
272 | ||
273 | Post: | |
274 | the 'armor' flag is set to True and the list of signing keys contains | |
275 | the single specified key | |
276 | """ | |
0a064403 AE |
277 | |
278 | os.environ['GNUPGHOME'] = gnupghome | |
279 | ||
280 | gpgme_ctx = gpgme.Context() | |
281 | gpgme_ctx.armor = True | |
282 | ||
283 | try: | |
284 | sign_with_key = gpgme_ctx.get_key(sign_with_key_fp) | |
5f6f32b1 | 285 | except gpgme.GpgmeError: |
0a064403 AE |
286 | error("unable to load signing key. is the gnupghome " |
287 | + "and signing key properly set in the edward_config.py?") | |
288 | exit(1) | |
289 | ||
290 | gpgme_ctx.signers = [sign_with_key] | |
291 | ||
292 | return gpgme_ctx | |
293 | ||
294 | ||
38738401 | 295 | def parse_pgp_mime (email_text, gpgme_ctx): |
a3d6aff8 AE |
296 | """Parses the email for mime payloads and decrypts/verfies signatures. |
297 | ||
298 | This function creates a representation of a mime or plaintext email with | |
299 | the EddyMsg class. It then splits each mime payload into one or more pieces | |
300 | which may be plain text or GPG data. It then decrypts encrypted parts and | |
301 | does some very basic signature verification on those parts. | |
302 | ||
303 | Args: | |
304 | email_text: an email message in string format | |
305 | gpgme_ctx: a gpgme context | |
306 | ||
307 | Returns: | |
308 | A message as an instance of EddyMsg | |
309 | ||
310 | Post: | |
311 | the returned EddyMsg instance has split, decrypted, verified and pubkey | |
312 | imported payloads | |
313 | """ | |
394a1476 AE |
314 | |
315 | email_struct = email.parser.Parser().parsestr(email_text) | |
316 | ||
928e3819 AE |
317 | eddymsg_obj = parse_mime(email_struct) |
318 | split_payloads(eddymsg_obj) | |
319 | gpg_on_payloads(eddymsg_obj, gpgme_ctx) | |
8bb4b0d5 | 320 | |
928e3819 | 321 | return eddymsg_obj |
0bec96d6 | 322 | |
0bec96d6 | 323 | |
56578eaf | 324 | def parse_mime(msg_struct): |
a3d6aff8 AE |
325 | """Translates python's email.parser format into an EddyMsg format |
326 | ||
327 | If the message is multi-part, then a recursive object is created, where | |
328 | each sub-part is also a EddyMsg instance. | |
329 | ||
330 | Args: | |
331 | msg_struct: an email parsed with email.parser.Parser(), which can be | |
332 | multi-part | |
333 | ||
334 | Returns: | |
335 | an instance of EddyMsg, potentially a recursive one. | |
336 | """ | |
0bec96d6 | 337 | |
928e3819 | 338 | eddymsg_obj = EddyMsg() |
8bb4b0d5 | 339 | |
56578eaf AE |
340 | if msg_struct.is_multipart() == True: |
341 | payloads = msg_struct.get_payload() | |
0bec96d6 | 342 | |
928e3819 AE |
343 | eddymsg_obj.multipart = True |
344 | eddymsg_obj.subparts = list(map(parse_mime, payloads)) | |
dd11a483 | 345 | |
56578eaf | 346 | else: |
928e3819 | 347 | eddymsg_obj = get_subpart_data(msg_struct) |
394a1476 | 348 | |
928e3819 | 349 | return eddymsg_obj |
c267c233 | 350 | |
80119cab | 351 | |
56578eaf | 352 | def scan_and_split (payload_piece, match_type, pattern): |
a3d6aff8 AE |
353 | """This splits the payloads of an EddyMsg object into GPG and text parts. |
354 | ||
355 | An EddyMsg object's payload_pieces starts off as a list containing a single | |
356 | PayloadPiece object. This function returns a list of these objects which | |
357 | have been split into GPG data and regular text, if such splits need to be/ | |
358 | can be made. | |
359 | ||
360 | Args: | |
361 | payload_piece: a single payload or a split part of a payload | |
362 | match_type: the type of data to try to spit out from the payload piece | |
363 | pattern: the search pattern to be used for finding that type of data | |
364 | ||
365 | Returns: | |
366 | a list of objects of the PayloadPiece class, in the order that the | |
367 | string part of payload_piece originally was, broken up according to | |
368 | matches specified by 'pattern'. | |
369 | """ | |
cf75de65 | 370 | |
a5d37d44 AE |
371 | # don't try to re-split pieces containing gpg data |
372 | if payload_piece.piece_type != "text": | |
373 | return [payload_piece] | |
374 | ||
56578eaf AE |
375 | flags = re.DOTALL | re.MULTILINE |
376 | matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern + | |
377 | ")(?P<rest>.*)", payload_piece.string, flags=flags) | |
86663388 | 378 | |
56578eaf AE |
379 | if matches == None: |
380 | pieces = [payload_piece] | |
c96f3837 | 381 | |
56578eaf | 382 | else: |
d437f8b2 | 383 | |
56578eaf AE |
384 | beginning = PayloadPiece() |
385 | beginning.string = matches.group('beginning') | |
386 | beginning.piece_type = payload_piece.piece_type | |
d437f8b2 | 387 | |
56578eaf AE |
388 | match = PayloadPiece() |
389 | match.string = matches.group('match') | |
390 | match.piece_type = match_type | |
d437f8b2 | 391 | |
56578eaf AE |
392 | rest = PayloadPiece() |
393 | rest.string = matches.group('rest') | |
394 | rest.piece_type = payload_piece.piece_type | |
d437f8b2 | 395 | |
56578eaf | 396 | more_pieces = scan_and_split(rest, match_type, pattern) |
4615b156 | 397 | pieces = [beginning, match ] + more_pieces |
d437f8b2 | 398 | |
56578eaf | 399 | return pieces |
d437f8b2 | 400 | |
d437f8b2 | 401 | |
56578eaf | 402 | def get_subpart_data (part): |
a3d6aff8 AE |
403 | """This function grabs information from a single part mime object. |
404 | ||
405 | It copies needed data from a single part email.parser.Parser() object over | |
406 | to an EddyMsg object. | |
407 | ||
408 | Args: | |
409 | part: a non-multi-part mime.parser.Parser() object | |
410 | ||
411 | Returns: | |
412 | a single-part EddyMsg() object | |
413 | """ | |
0bec96d6 | 414 | |
6d8faaa7 AE |
415 | charset = part.get_content_charset() |
416 | mime_decoded_bytes = part.get_payload(decode=True) | |
0bec96d6 | 417 | |
6d8faaa7 AE |
418 | obj = EddyMsg() |
419 | obj.payload_bytes = part.as_bytes() | |
56578eaf AE |
420 | |
421 | obj.filename = part.get_filename() | |
422 | obj.content_type = part.get_content_type() | |
423 | obj.description_list = part['content-description'] | |
424 | ||
425 | # your guess is as good as a-myy-ee-ine... | |
6d8faaa7 AE |
426 | if charset == None: |
427 | charset = 'utf-8' | |
56578eaf | 428 | |
6d8faaa7 | 429 | if mime_decoded_bytes != None: |
0eb75d9c AE |
430 | try: |
431 | payload = PayloadPiece() | |
6d8faaa7 | 432 | payload.string = mime_decoded_bytes.decode(charset) |
0eb75d9c AE |
433 | payload.piece_type = 'text' |
434 | ||
435 | obj.payload_pieces = [payload] | |
436 | except UnicodeDecodeError: | |
437 | pass | |
56578eaf AE |
438 | |
439 | return obj | |
440 | ||
441 | ||
928e3819 | 442 | def do_to_eddys_pieces (function_to_do, eddymsg_obj, data): |
a3d6aff8 AE |
443 | """A function which maps another function onto a message's subparts. |
444 | ||
445 | This is a higer-order function which recursively performs a specified | |
446 | function on each subpart of a multi-part message. Each single-part sub-part | |
447 | has the function applied to it. This function also works if the part passed | |
448 | in is single-part. | |
449 | ||
450 | Args: | |
451 | function_to_do: function to perform on sub-parts | |
452 | eddymsg_obj: a single part or multi-part EddyMsg object | |
453 | data: a second argument to pass to 'function_to_do' | |
454 | ||
455 | Returns: | |
456 | Nothing | |
457 | ||
458 | Post: | |
459 | The passed-in EddyMsg object is transformed recursively on its | |
460 | sub-parts according to 'function_to_do'. | |
461 | """ | |
56578eaf | 462 | |
928e3819 AE |
463 | if eddymsg_obj.multipart == True: |
464 | for sub in eddymsg_obj.subparts: | |
d873ff48 | 465 | do_to_eddys_pieces(function_to_do, sub, data) |
394a1476 | 466 | else: |
928e3819 | 467 | function_to_do(eddymsg_obj, data) |
dd11a483 AE |
468 | |
469 | ||
928e3819 | 470 | def split_payloads (eddymsg_obj): |
a3d6aff8 AE |
471 | """Splits all (sub-)payloads of a message into GPG data and regular text. |
472 | ||
473 | Recursively performs payload splitting on all sub-parts of an EddyMsg | |
474 | object into the various GPG data types, such as GPG messages, public key | |
475 | blocks and signed text. | |
476 | ||
477 | Args: | |
478 | eddymsg_obj: an instance of EddyMsg | |
479 | ||
480 | Returns: | |
481 | Nothing | |
482 | ||
483 | Pre: | |
484 | The EddyMsg object has payloads that are unsplit (by may be split).. | |
485 | ||
486 | Post: | |
487 | The EddyMsg object's payloads are all split into GPG and non-GPG parts. | |
488 | """ | |
a5d37d44 AE |
489 | |
490 | for match_type in match_types: | |
928e3819 | 491 | do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type) |
a5d37d44 | 492 | |
a5d37d44 | 493 | |
928e3819 | 494 | def split_payload_pieces (eddymsg_obj, match_type): |
a3d6aff8 AE |
495 | """A helper function for split_payloads(); works on PayloadPiece objects. |
496 | ||
497 | This function splits up PayloadPiece objects into multipe PayloadPiece | |
498 | objects and replaces the EddyMsg object's previous list of payload pieces | |
499 | with the new split up one. | |
500 | ||
501 | Args: | |
502 | eddymsg_obj: a single-part EddyMsg object. | |
503 | match_type: a tuple from the match_types list, which specifies a match | |
504 | name and a match pattern. | |
505 | ||
506 | Returns: | |
507 | Nothing | |
508 | ||
509 | Pre: | |
510 | The payload piece(s) of an EddyMsg object may be already split or | |
511 | unsplit. | |
512 | ||
513 | Post: | |
514 | The EddyMsg object's payload piece(s) are split into a list of pieces | |
515 | if matches of the match_type are found. | |
516 | """ | |
a5d37d44 AE |
517 | |
518 | (match_name, pattern) = match_type | |
519 | ||
520 | new_pieces_list = [] | |
928e3819 | 521 | for piece in eddymsg_obj.payload_pieces: |
a5d37d44 AE |
522 | new_pieces_list += scan_and_split(piece, match_name, pattern) |
523 | ||
928e3819 | 524 | eddymsg_obj.payload_pieces = new_pieces_list |
a5d37d44 AE |
525 | |
526 | ||
928e3819 | 527 | def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]): |
a3d6aff8 AE |
528 | """Performs GPG operations on the GPG parts of the message |
529 | ||
530 | This function decrypts text, verifies signatures, and imports public keys | |
531 | included in an email. | |
532 | ||
533 | Args: | |
534 | eddymsg_obj: an EddyMsg object with its payload_pieces split into GPG | |
535 | and non-GPG sections by split_payloads() | |
536 | gpgme_ctx: a gpgme context | |
537 | ||
538 | prev_parts: a list of mime parts that occur before the eddymsg_obj | |
539 | part, under the same multi-part mime part. This is used for | |
540 | verifying detached signatures. For the root mime part, this should | |
541 | be an empty list, which is the default value if this paramater is | |
542 | omitted. | |
543 | ||
544 | Return: | |
545 | Nothing | |
546 | ||
547 | Pre: | |
548 | eddymsg_obj should have its payloads split into gpg and non-gpg pieces. | |
549 | ||
550 | Post: | |
551 | Decryption, verification and key imports occur. the gpg_data member of | |
552 | PayloadPiece objects get filled in with GPGData objects. | |
553 | """ | |
38738401 | 554 | |
928e3819 | 555 | if eddymsg_obj.multipart == True: |
101d54a8 | 556 | prev_parts=[] |
928e3819 | 557 | for sub in eddymsg_obj.subparts: |
101d54a8 AE |
558 | gpg_on_payloads (sub, gpgme_ctx, prev_parts) |
559 | prev_parts += [sub] | |
38738401 | 560 | |
d873ff48 | 561 | return |
38738401 | 562 | |
928e3819 | 563 | for piece in eddymsg_obj.payload_pieces: |
38738401 AE |
564 | |
565 | if piece.piece_type == "text": | |
566 | # don't transform the plaintext. | |
567 | pass | |
568 | ||
569 | elif piece.piece_type == "message": | |
7e18f14d | 570 | (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx) |
38738401 AE |
571 | |
572 | if plaintext: | |
573 | piece.gpg_data = GPGData() | |
b23e171d | 574 | piece.gpg_data.decrypted = True |
38738401 AE |
575 | piece.gpg_data.sigs = sigs |
576 | # recurse! | |
577 | piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx) | |
3a753fd0 AE |
578 | continue |
579 | ||
580 | # if not encrypted, check to see if this is an armored signature. | |
581 | (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx) | |
582 | ||
583 | if plaintext: | |
584 | piece.piece_type = "signature" | |
585 | piece.gpg_data = GPGData() | |
586 | piece.gpg_data.sigs = sigs | |
587 | # recurse! | |
588 | piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx) | |
129543c3 | 589 | |
d7d8bf7c AE |
590 | # FIXME: consider handling pubkeys first, so that signatures can be |
591 | # validated on freshly imported keys | |
8f61c66a | 592 | elif piece.piece_type == "pubkey": |
f8ee6bd3 | 593 | key_fps = add_gpg_key(piece.string, gpgme_ctx) |
8f61c66a | 594 | |
f8ee6bd3 | 595 | if key_fps != []: |
8f61c66a | 596 | piece.gpg_data = GPGData() |
f8ee6bd3 | 597 | piece.gpg_data.keys = key_fps |
129543c3 | 598 | |
101d54a8 AE |
599 | elif piece.piece_type == "detachedsig": |
600 | for prev in prev_parts: | |
6d8faaa7 | 601 | sig_fps = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx) |
101d54a8 | 602 | |
6d240f68 AE |
603 | if sig_fps != []: |
604 | piece.gpg_data = GPGData() | |
605 | piece.gpg_data.sigs = sig_fps | |
606 | piece.gpg_data.plainobj = prev | |
607 | break | |
7e18f14d | 608 | |
38738401 AE |
609 | else: |
610 | pass | |
611 | ||
612 | ||
928e3819 | 613 | def prepare_for_reply (eddymsg_obj, replyinfo_obj): |
67691346 AE |
614 | """Updates replyinfo_obj with info on the message's GPG success/failures |
615 | ||
616 | This function marks replyinfo_obj with information about whether encrypted | |
617 | text in eddymsg_obj was successfully decrypted, signatures were verified | |
618 | and whether a public key was found or not. | |
619 | ||
620 | Args: | |
621 | eddymsg_obj: a message in the EddyMsg format | |
622 | replyinfo_obj: an instance of ReplyInfo | |
623 | ||
624 | Returns: | |
625 | Nothing | |
626 | ||
627 | Pre: | |
628 | eddymsg_obj has had its gpg_data created by gpg_on_payloads | |
629 | ||
630 | Post: | |
631 | replyinfo_obj has been updated with info about decryption/sig | |
632 | verififcation status, etc. However the desired key isn't imported until | |
633 | later, so the success or failure of that updates the values set here. | |
634 | """ | |
dd11a483 | 635 | |
928e3819 | 636 | do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj) |
56578eaf | 637 | |
928e3819 | 638 | def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj): |
67691346 AE |
639 | """A helper function for prepare_for_reply |
640 | ||
641 | It updates replyinfo_obj with GPG success/failure information, when | |
642 | supplied a single-part EddyMsg object. | |
643 | ||
644 | Args: | |
645 | eddymsg_obj: a single-part message in the EddyMsg format | |
646 | replyinfo_obj: an object which holds information about the message's | |
647 | GPG status | |
648 | ||
649 | Returns: | |
650 | Nothing | |
651 | ||
652 | Pre: | |
653 | eddymsg_obj is a single-part message. (it may be a part of a multi-part | |
654 | message.) It has had its gpg_data created by gpg_on_payloads if it has | |
655 | gpg data. | |
656 | ||
657 | Post: | |
658 | replyinfo_obj has been updated with gpg success/failure information | |
659 | """ | |
56578eaf | 660 | |
928e3819 | 661 | for piece in eddymsg_obj.payload_pieces: |
38738401 | 662 | if piece.piece_type == "text": |
d873ff48 AE |
663 | # don't quote the plaintext part. |
664 | pass | |
665 | ||
38738401 | 666 | elif piece.piece_type == "message": |
05683768 | 667 | prepare_for_reply_message(piece, replyinfo_obj) |
d873ff48 | 668 | |
05683768 AE |
669 | elif piece.piece_type == "pubkey": |
670 | prepare_for_reply_pubkey(piece, replyinfo_obj) | |
6906d46d | 671 | |
1e98a070 | 672 | elif (piece.piece_type == "detachedsig") \ |
3a753fd0 | 673 | or (piece.piece_type == "signature"): |
05683768 | 674 | prepare_for_reply_sig(piece, replyinfo_obj) |
d873ff48 | 675 | |
d873ff48 | 676 | |
05683768 | 677 | def prepare_for_reply_message (piece, replyinfo_obj): |
67691346 AE |
678 | """Helper function for prepare_for_reply() |
679 | ||
680 | This function is called when the piece_type of a payload piece is | |
681 | "message", or GPG Message block. This should be encrypted text. If the | |
682 | encryted block is signed, a sig will be attached to .target_key unless | |
683 | there is already one there. | |
684 | ||
685 | Args: | |
686 | piece: a PayloadPiece object. | |
687 | replyinfo_obj: object which gets updated with decryption status, etc. | |
688 | ||
689 | ||
690 | Returns: | |
691 | Nothing | |
692 | ||
693 | Pre: | |
694 | the piece.payload_piece value should be "message". | |
695 | ||
696 | Post: | |
697 | replyinfo_obj gets updated with decryption status, signing status and a | |
698 | potential signing key. | |
699 | """ | |
44fe39b7 | 700 | |
05683768 AE |
701 | if piece.gpg_data == None: |
702 | replyinfo_obj.failed_decrypt = True | |
703 | return | |
704 | ||
5c40e676 | 705 | replyinfo_obj.success_decrypt = True |
05683768 AE |
706 | |
707 | # we already have a key (and a message) | |
708 | if replyinfo_obj.target_key != None: | |
709 | return | |
710 | ||
711 | if piece.gpg_data.sigs != []: | |
712 | replyinfo_obj.target_key = piece.gpg_data.sigs[0] | |
713 | get_signed_part = False | |
714 | else: | |
715 | # only include a signed message in the reply. | |
716 | get_signed_part = True | |
717 | ||
8c3e5397 | 718 | flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part) |
05683768 AE |
719 | |
720 | # to catch public keys in encrypted blocks | |
721 | prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj) | |
722 | ||
723 | ||
724 | def prepare_for_reply_pubkey (piece, replyinfo_obj): | |
67691346 AE |
725 | """Helper function for prepare_for_reply(). Marks pubkey import status. |
726 | ||
727 | Marks replyinfo_obj with pub key import status. | |
728 | ||
729 | Args: | |
730 | piece: a PayloadPiece object | |
731 | replyinfo_obj: a ReplyInfo object | |
732 | ||
733 | Pre: | |
734 | piece.piece_type should be set to "pubkey". | |
735 | ||
736 | Post: | |
737 | replyinfo_obj has its fields updated. | |
738 | """ | |
05683768 AE |
739 | |
740 | if piece.gpg_data == None or piece.gpg_data.keys == []: | |
741 | replyinfo_obj.no_public_key = True | |
742 | else: | |
743 | replyinfo_obj.public_key_received = True | |
744 | ||
3ceb92e5 AE |
745 | # prefer public key as a fallback for the encrypted reply |
746 | replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0] | |
05683768 AE |
747 | |
748 | ||
749 | def prepare_for_reply_sig (piece, replyinfo_obj): | |
67691346 AE |
750 | """Helper function for prepare_for_reply(). Marks sig verification status. |
751 | ||
752 | Marks replyinfo_obj with signature verification status. | |
753 | ||
754 | Args: | |
755 | piece: a PayloadPiece object | |
756 | replyinfo_obj: a ReplyInfo object | |
757 | ||
758 | Pre: | |
1e98a070 | 759 | piece.piece_type should be set to "signature", or "detachedsig". |
67691346 AE |
760 | |
761 | Post: | |
762 | replyinfo_obj has its fields updated. | |
763 | """ | |
05683768 AE |
764 | |
765 | if piece.gpg_data == None or piece.gpg_data.sigs == []: | |
766 | replyinfo_obj.sig_failure = True | |
767 | else: | |
768 | replyinfo_obj.sig_success = True | |
769 | ||
770 | if replyinfo_obj.fallback_target_key == None: | |
771 | replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0] | |
6906d46d AE |
772 | |
773 | ||
8c3e5397 AE |
774 | def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part): |
775 | """For creating a string representation of a signed, encrypted part. | |
39e2c525 | 776 | |
8c3e5397 AE |
777 | When given a decrypted payload, it will add either the plaintext or signed |
778 | plaintext to the reply message, depeding on 'get_signed_part'. This is | |
779 | useful for ensuring that the reply message only comes from a signed and | |
780 | ecrypted GPG message. It also sets the target_key for encrypting the reply | |
781 | if it's told to get signed text only. | |
39e2c525 AE |
782 | |
783 | Args: | |
784 | eddymsg_obj: the message in EddyMsg format created by decrypting GPG | |
785 | text | |
8c3e5397 AE |
786 | replyinfo_obj: a ReplyInfo object for holding the message to quote and |
787 | the target_key to encrypt to. | |
39e2c525 AE |
788 | get_signed_part: True if we should only include text that contains a |
789 | further signature. If False, then include plain text. | |
790 | ||
791 | Returns: | |
8c3e5397 | 792 | Nothing |
39e2c525 AE |
793 | |
794 | Pre: | |
795 | The EddyMsg instance passed in should be a piece.gpg_data.plainobj | |
796 | which represents decrypted text. It may or may not be signed on that | |
797 | level. | |
d873ff48 | 798 | |
8c3e5397 AE |
799 | Post: |
800 | the ReplyInfo instance may have a new 'target_key' set and its | |
801 | 'msg_to_quote' will be updated with (possibly signed) plaintext, if any | |
802 | could be found. | |
803 | """ | |
d873ff48 | 804 | |
0aa88c27 | 805 | if eddymsg_obj == None: |
8c3e5397 | 806 | return |
0aa88c27 | 807 | |
0402995a | 808 | # recurse on multi-part mime |
928e3819 AE |
809 | if eddymsg_obj.multipart == True: |
810 | for sub in eddymsg_obj.subparts: | |
8c3e5397 | 811 | flatten_decrypted_payloads(sub, replyinfo_obj, get_signed_part) |
d873ff48 | 812 | |
928e3819 | 813 | for piece in eddymsg_obj.payload_pieces: |
0402995a | 814 | if (get_signed_part): |
1e98a070 | 815 | if ((piece.piece_type == "detachedsig") \ |
3a753fd0 | 816 | or (piece.piece_type == "signature")) \ |
0402995a | 817 | and (piece.gpg_data != None): |
8c3e5397 AE |
818 | flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, False) |
819 | replyinfo_obj.target_key = piece.gpg_data.sigs[0] | |
c3d417bf AE |
820 | break |
821 | else: | |
822 | if piece.piece_type == "text": | |
8c3e5397 | 823 | replyinfo_obj.msg_to_quote += piece.string |
d873ff48 AE |
824 | |
825 | ||
013f7cb8 | 826 | def get_key_from_fp (replyinfo_obj, gpgme_ctx): |
39e2c525 AE |
827 | """Obtains a public key object from a key fingerprint |
828 | ||
829 | If the .target_key is not set, then we use .fallback_target_key. | |
830 | ||
831 | Args: | |
832 | replyinfo_obj: ReplyInfo instance | |
833 | gpgme_ctx: the gpgme context | |
834 | ||
835 | Return: | |
836 | The key object of the key of either the target_key or the fallback one | |
837 | if .target_key is not set. If the key cannot be loaded, then return | |
838 | None. | |
839 | ||
840 | Pre: | |
841 | Loading a key requires that we have the public key imported. This | |
842 | requires that they email contains the pub key block, or that it was | |
843 | previously sent to edward. | |
844 | ||
845 | Post: | |
846 | If the key cannot be loaded, then the replyinfo_obj is marked for | |
847 | having no public key available. | |
848 | """ | |
013f7cb8 | 849 | |
dc24daf1 AE |
850 | if replyinfo_obj.target_key == None: |
851 | replyinfo_obj.target_key = replyinfo_obj.fallback_target_key | |
852 | ||
013f7cb8 AE |
853 | if replyinfo_obj.target_key != None: |
854 | try: | |
855 | encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key) | |
856 | return encrypt_to_key | |
857 | ||
5f6f32b1 | 858 | except gpgme.GpgmeError: |
013f7cb8 AE |
859 | pass |
860 | ||
861 | # no available key to use | |
862 | replyinfo_obj.target_key = None | |
863 | replyinfo_obj.fallback_target_key = None | |
864 | ||
865 | replyinfo_obj.no_public_key = True | |
866 | replyinfo_obj.public_key_received = False | |
867 | ||
868 | return None | |
869 | ||
870 | ||
d873ff48 | 871 | def write_reply (replyinfo_obj): |
39e2c525 AE |
872 | """Write the reply email body about the GPG successes/failures. |
873 | ||
874 | The reply is about whether decryption, sig verification and key | |
875 | import/loading was successful or failed. If text was successfully decrypted | |
876 | and verified, then the first instance of such text will be included in | |
877 | quoted form. | |
878 | ||
879 | Args: | |
880 | replyinfo_obj: contains details of GPG processing status | |
881 | ||
882 | Returns: | |
883 | the plaintext message to be sent to the user | |
884 | ||
885 | Pre: | |
886 | replyinfo_obj should be populated with info about GPG processing status. | |
887 | """ | |
d873ff48 AE |
888 | |
889 | reply_plain = "" | |
890 | ||
891 | if replyinfo_obj.success_decrypt == True: | |
d873ff48 | 892 | reply_plain += replyinfo_obj.replies['success_decrypt'] |
2694709a AE |
893 | |
894 | if replyinfo_obj.no_public_key == False: | |
895 | quoted_text = email_quote_text(replyinfo_obj.msg_to_quote) | |
896 | reply_plain += quoted_text | |
d873ff48 AE |
897 | |
898 | elif replyinfo_obj.failed_decrypt == True: | |
899 | reply_plain += replyinfo_obj.replies['failed_decrypt'] | |
900 | ||
901 | ||
902 | if replyinfo_obj.sig_success == True: | |
903 | reply_plain += "\n\n" | |
904 | reply_plain += replyinfo_obj.replies['sig_success'] | |
905 | ||
906 | elif replyinfo_obj.sig_failure == True: | |
907 | reply_plain += "\n\n" | |
908 | reply_plain += replyinfo_obj.replies['sig_failure'] | |
909 | ||
910 | ||
911 | if replyinfo_obj.public_key_received == True: | |
912 | reply_plain += "\n\n" | |
913 | reply_plain += replyinfo_obj.replies['public_key_received'] | |
914 | ||
915 | elif replyinfo_obj.no_public_key == True: | |
916 | reply_plain += "\n\n" | |
917 | reply_plain += replyinfo_obj.replies['no_public_key'] | |
918 | ||
919 | ||
920 | reply_plain += "\n\n" | |
921 | reply_plain += replyinfo_obj.replies['signature'] | |
56578eaf | 922 | |
d873ff48 | 923 | return reply_plain |
56578eaf AE |
924 | |
925 | ||
8f61c66a | 926 | def add_gpg_key (key_block, gpgme_ctx): |
39e2c525 AE |
927 | """Adds a GPG pubkey to the local keystore |
928 | ||
929 | This adds keys received through email into the key store so they can be | |
930 | used later. | |
931 | ||
932 | Args: | |
933 | key_block: the string form of the ascii-armored public key block | |
934 | gpgme_ctx: the gpgme context | |
935 | ||
936 | Returns: | |
937 | the fingerprint(s) of the imported key(s) | |
938 | """ | |
c267c233 | 939 | |
8f61c66a | 940 | fp = io.BytesIO(key_block.encode('ascii')) |
c267c233 | 941 | |
89c08329 AE |
942 | try: |
943 | result = gpgme_ctx.import_(fp) | |
944 | imports = result.imports | |
945 | except gpgme.GpgmeError: | |
946 | imports = [] | |
c267c233 | 947 | |
f8ee6bd3 | 948 | key_fingerprints = [] |
c267c233 | 949 | |
8f61c66a AE |
950 | if imports != []: |
951 | for import_ in imports: | |
952 | fingerprint = import_[0] | |
f8ee6bd3 | 953 | key_fingerprints += [fingerprint] |
c267c233 | 954 | |
e49673aa | 955 | debug("added gpg key: " + fingerprint) |
ec1e779a | 956 | |
f8ee6bd3 | 957 | return key_fingerprints |
ec1e779a AE |
958 | |
959 | ||
3a753fd0 | 960 | def verify_sig_message (msg_block, gpgme_ctx): |
39e2c525 AE |
961 | """Verifies the signature of a signed, ascii-armored block of text. |
962 | ||
963 | It encodes the string into ascii, since binary GPG files are currently | |
964 | unsupported, and alternative, the ascii-armored format is encodable into | |
965 | ascii. | |
966 | ||
967 | Args: | |
968 | msg_block: a GPG Message block in string form. It may be encrypted or | |
969 | not. If it is encrypted, it will return empty results. | |
970 | gpgme_ctx: the gpgme context | |
971 | ||
972 | Returns: | |
973 | A tuple of the plaintext of the signed part and the list of | |
974 | fingerprints of keys signing the data. If verification failed, perhaps | |
975 | because the message was also encrypted, then empty results are | |
976 | returned. | |
977 | """ | |
3a753fd0 AE |
978 | |
979 | block_b = io.BytesIO(msg_block.encode('ascii')) | |
980 | plain_b = io.BytesIO() | |
981 | ||
982 | try: | |
983 | sigs = gpgme_ctx.verify(block_b, None, plain_b) | |
5f6f32b1 | 984 | except gpgme.GpgmeError: |
3a753fd0 AE |
985 | return ("",[]) |
986 | ||
987 | plaintext = plain_b.getvalue().decode('utf-8') | |
988 | ||
989 | fingerprints = [] | |
990 | for sig in sigs: | |
991 | fingerprints += [sig.fpr] | |
992 | return (plaintext, fingerprints) | |
993 | ||
994 | ||
101d54a8 | 995 | def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx): |
39e2c525 AE |
996 | """Verifies the signature of a detached signature. |
997 | ||
998 | This requires the signature part and the signed part as separate arguments. | |
999 | ||
1000 | Args: | |
1001 | detached_sig: the signature part of the detached signature | |
1002 | plaintext_bytes: the byte form of the message being signed. | |
1003 | gpgme_ctx: the gpgme context | |
1004 | ||
1005 | Returns: | |
1006 | A list of signing fingerprints if the signature verification was | |
1007 | sucessful. Otherwise, an empty list is returned. | |
1008 | """ | |
101d54a8 AE |
1009 | |
1010 | detached_sig_fp = io.BytesIO(detached_sig.encode('ascii')) | |
1011 | plaintext_fp = io.BytesIO(plaintext_bytes) | |
101d54a8 | 1012 | |
ff7aeb3d AE |
1013 | try: |
1014 | result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None) | |
1015 | except gpgme.GpgmeError: | |
1016 | return [] | |
101d54a8 AE |
1017 | |
1018 | sig_fingerprints = [] | |
1019 | for res_ in result: | |
1020 | sig_fingerprints += [res_.fpr] | |
1021 | ||
1022 | return sig_fingerprints | |
1023 | ||
1024 | ||
5b3053c1 | 1025 | def decrypt_block (msg_block, gpgme_ctx): |
39e2c525 AE |
1026 | """Decrypts a block of GPG text, and verifies any included sigatures. |
1027 | ||
1028 | Some encypted messages have embeded signatures, so those are verified too. | |
1029 | ||
1030 | Args: | |
1031 | msg_block: the encrypted(/signed) text | |
1032 | gpgme_ctx: the gpgme context | |
1033 | ||
1034 | Returns: | |
1035 | A tuple of plaintext and signatures, if the decryption and signature | |
1036 | verification were successful, respectively. | |
1037 | """ | |
0bec96d6 | 1038 | |
5b3053c1 | 1039 | block_b = io.BytesIO(msg_block.encode('ascii')) |
0bec96d6 AE |
1040 | plain_b = io.BytesIO() |
1041 | ||
afc1f64c AE |
1042 | try: |
1043 | sigs = gpgme_ctx.decrypt_verify(block_b, plain_b) | |
5f6f32b1 | 1044 | except gpgme.GpgmeError: |
afc1f64c | 1045 | return ("",[]) |
0bec96d6 | 1046 | |
6aa41372 | 1047 | plaintext = plain_b.getvalue().decode('utf-8') |
cbdf22c1 AE |
1048 | |
1049 | fingerprints = [] | |
1050 | for sig in sigs: | |
1051 | fingerprints += [sig.fpr] | |
1052 | return (plaintext, fingerprints) | |
0bec96d6 AE |
1053 | |
1054 | ||
dd2b62d7 AE |
1055 | def email_to_from_subject (email_text): |
1056 | """Returns the values of the email's To:, From: and Subject: fields | |
fafa21c3 | 1057 | |
dd2b62d7 | 1058 | Returns this information from an email. |
fafa21c3 | 1059 | |
dd2b62d7 AE |
1060 | Args: |
1061 | email_text: the string form of the email | |
fafa21c3 | 1062 | |
dd2b62d7 AE |
1063 | Returns: |
1064 | the email To:, From:, and Subject: fields as strings | |
1065 | """ | |
d65993b8 AE |
1066 | |
1067 | email_struct = email.parser.Parser().parsestr(email_text) | |
1068 | ||
1069 | email_to = email_struct['To'] | |
1070 | email_from = email_struct['From'] | |
1071 | email_subject = email_struct['Subject'] | |
1072 | ||
1073 | return email_to, email_from, email_subject | |
1074 | ||
1075 | ||
adcef2f7 | 1076 | def import_lang(email_to): |
dd2b62d7 AE |
1077 | """Imports appropriate language file for basic i18n support |
1078 | ||
1079 | The language imported depends on the To: address of the email received by | |
1080 | edward. an -en ending implies the English language, whereas a -ja ending | |
1081 | implies Japanese. The list of supported languages is listed in the 'langs' | |
1082 | list at the beginning of the program. | |
1083 | ||
1084 | Args: | |
1085 | email_to: the string containing the email address that the mail was | |
1086 | sent to. | |
1087 | ||
1088 | Returns: | |
1089 | the reference to the imported language module. The only variable in | |
1090 | this file is the 'replies' dictionary. | |
1091 | """ | |
adcef2f7 | 1092 | |
daed5f10 AE |
1093 | lang_module = "lang.en" |
1094 | ||
5250b3b8 AE |
1095 | if email_to != None: |
1096 | for lang in langs: | |
1097 | if "edward-" + lang in email_to: | |
daed5f10 | 1098 | lang_module = "lang." + re.sub('-', '_', lang) |
adcef2f7 | 1099 | |
daed5f10 | 1100 | return importlib.import_module(lang_module) |
adcef2f7 AE |
1101 | |
1102 | ||
cfb03389 | 1103 | def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key, |
0a064403 | 1104 | gpgme_ctx): |
dd2b62d7 AE |
1105 | """This function creates the mime email reply. It can encrypt the email. |
1106 | ||
1107 | If the encrypt_key is included, then the email is encrypted and signed. | |
1108 | Otherwise it is unencrypted. | |
1109 | ||
1110 | Args: | |
1111 | plaintext: the plaintext body of the message to create. | |
cfb03389 | 1112 | email_to: the email address to reply to |
dd2b62d7 AE |
1113 | email_subject: the subject to use in reply |
1114 | encrypt_to_key: the key object to use for encrypting the email. (or | |
1115 | None) | |
1116 | gpgme_ctx: the gpgme context | |
1117 | ||
1118 | Returns | |
1119 | A string version of the mime message, possibly encrypted and signed. | |
1120 | """ | |
1da9b527 | 1121 | |
2007103e | 1122 | # quoted printable encoding lets most ascii characters look normal |
2a2359a4 | 1123 | # before the mime message is decoded. |
2007103e AE |
1124 | char_set = email.charset.Charset("utf-8") |
1125 | char_set.body_encoding = email.charset.QP | |
8bdfb6d4 | 1126 | |
2007103e AE |
1127 | # MIMEText doesn't allow setting the text encoding |
1128 | # so we use MIMENonMultipart. | |
1129 | plaintext_mime = MIMENonMultipart('text', 'plain') | |
1130 | plaintext_mime.set_payload(plaintext, charset=char_set) | |
216708e9 AE |
1131 | |
1132 | if (encrypt_to_key != None): | |
8bdfb6d4 AE |
1133 | |
1134 | encrypted_text = encrypt_sign_message(plaintext_mime.as_string(), | |
1135 | encrypt_to_key, | |
40c37ab3 | 1136 | gpgme_ctx) |
b33f601b | 1137 | gpg_payload = encrypted_text |
8bdfb6d4 | 1138 | |
b33f601b AE |
1139 | else: |
1140 | signed_text = sign_message(plaintext_mime.as_string(), gpgme_ctx) | |
1141 | gpg_payload = signed_text | |
8bdfb6d4 | 1142 | |
b33f601b AE |
1143 | control_mime = MIMEApplication("Version: 1", |
1144 | _subtype='pgp-encrypted', | |
1145 | _encoder=email.encoders.encode_7or8bit) | |
1146 | control_mime['Content-Description'] = 'PGP/MIME version identification' | |
1147 | control_mime.set_charset('us-ascii') | |
8bdfb6d4 | 1148 | |
b33f601b AE |
1149 | encoded_mime = MIMEApplication(gpg_payload, |
1150 | _subtype='octet-stream; name="encrypted.asc"', | |
1151 | _encoder=email.encoders.encode_7or8bit) | |
1152 | encoded_mime['Content-Description'] = 'OpenPGP encrypted message' | |
1153 | encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"' | |
1154 | encoded_mime.set_charset('us-ascii') | |
1155 | ||
1156 | message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted") | |
1157 | message_mime.attach(control_mime) | |
1158 | message_mime.attach(encoded_mime) | |
1159 | message_mime['Content-Disposition'] = 'inline' | |
216708e9 | 1160 | |
2007103e | 1161 | |
cfb03389 | 1162 | message_mime['To'] = email_to |
2007103e AE |
1163 | message_mime['Subject'] = email_subject |
1164 | ||
1165 | reply = message_mime.as_string() | |
1da9b527 AE |
1166 | |
1167 | return reply | |
1168 | ||
1169 | ||
f87041f8 | 1170 | def email_quote_text (text): |
dd2b62d7 AE |
1171 | """Quotes input text by inserting "> "s |
1172 | ||
1173 | This is useful for quoting a text for the reply message. It inserts "> " | |
1174 | strings at the beginning of lines. | |
1175 | ||
1176 | Args: | |
1177 | text: plain text to quote | |
1178 | ||
1179 | Returns: | |
1180 | Quoted text | |
1181 | """ | |
f87041f8 AE |
1182 | |
1183 | quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE) | |
1184 | ||
1185 | return quoted_message | |
1186 | ||
1187 | ||
0a064403 | 1188 | def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx): |
dd2b62d7 AE |
1189 | """Encrypts and signs plaintext |
1190 | ||
1191 | This encrypts and signs a message. | |
1192 | ||
1193 | Args: | |
1194 | plaintext: text to sign and ecrypt | |
1195 | encrypt_to_key: the key object to encrypt to | |
1196 | gpgme_ctx: the gpgme context | |
1197 | ||
1198 | Returns: | |
1199 | An encrypted and signed string of text | |
1200 | """ | |
897cbaf6 | 1201 | |
b33f601b | 1202 | # the plaintext should be mime encoded in an ascii-compatible form |
6aa41372 | 1203 | plaintext_bytes = io.BytesIO(plaintext.encode('ascii')) |
1da9b527 AE |
1204 | encrypted_bytes = io.BytesIO() |
1205 | ||
897cbaf6 | 1206 | gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST, |
1da9b527 AE |
1207 | plaintext_bytes, encrypted_bytes) |
1208 | ||
6aa41372 | 1209 | encrypted_txt = encrypted_bytes.getvalue().decode('ascii') |
1da9b527 AE |
1210 | return encrypted_txt |
1211 | ||
1212 | ||
b33f601b AE |
1213 | def sign_message (plaintext, gpgme_ctx): |
1214 | """Signs plaintext | |
1215 | ||
1216 | This signs a message. | |
1217 | ||
1218 | Args: | |
1219 | plaintext: text to sign | |
1220 | gpgme_ctx: the gpgme context | |
1221 | ||
1222 | Returns: | |
1223 | An armored signature as a string of text | |
1224 | """ | |
1225 | ||
1226 | # the plaintext should be mime encoded in an ascii-compatible form | |
1227 | plaintext_bytes = io.BytesIO(plaintext.encode('ascii')) | |
1228 | signed_bytes = io.BytesIO() | |
1229 | ||
1230 | gpgme_ctx.sign(plaintext_bytes, signed_bytes, gpgme.SIG_MODE_NORMAL) | |
1231 | ||
1232 | signed_txt = signed_bytes.getvalue().decode('ascii') | |
1233 | return signed_txt | |
1234 | ||
1235 | ||
0a064403 | 1236 | def error (error_msg): |
dd2b62d7 AE |
1237 | """Write an error message to stdout |
1238 | ||
1239 | The error message includes the program name. | |
1240 | ||
1241 | Args: | |
1242 | error_msg: the message to print | |
1243 | ||
1244 | Returns: | |
1245 | Nothing | |
1246 | ||
1247 | Post: | |
1248 | An error message is printed to stdout | |
1249 | """ | |
0a064403 | 1250 | |
e4fb2ab2 | 1251 | sys.stderr.write(progname + ": " + str(error_msg) + "\n") |
0a064403 AE |
1252 | |
1253 | ||
5e8f9094 | 1254 | def debug (debug_msg): |
dd2b62d7 AE |
1255 | """Writes a debug message to stdout if debug == True |
1256 | ||
1257 | If the debug option is set in edward_config.py, then the passed message | |
1258 | gets printed to stdout. | |
1259 | ||
1260 | Args: | |
1261 | debug_msg: the message to print to stdout | |
1262 | ||
1263 | Returns: | |
1264 | Nothing | |
1265 | ||
1266 | Post: | |
1267 | A debug message is printed to stdout | |
1268 | """ | |
5e8f9094 AE |
1269 | |
1270 | if edward_config.debug == True: | |
0a064403 | 1271 | error(debug_msg) |
5e8f9094 AE |
1272 | |
1273 | ||
20f6e7c5 | 1274 | def handle_args (): |
dd2b62d7 AE |
1275 | """Sets the progname variable and complains about any arguments |
1276 | ||
1277 | If there are any arguments, then edward complains and quits, because input | |
1278 | is read from stdin. | |
1279 | ||
1280 | Args: | |
1281 | None | |
1282 | ||
1283 | Returns: | |
1284 | None | |
1285 | ||
1286 | Post: | |
1287 | Exits with error 1 if there are arguments, otherwise returns to the | |
1288 | calling function, such as main(). | |
1289 | """ | |
20f6e7c5 | 1290 | |
a51cdb72 AE |
1291 | global progname |
1292 | progname = sys.argv[0] | |
1293 | ||
1294 | if len(sys.argv) > 1: | |
1295 | print(progname + ": error, this program doesn't " \ | |
1296 | "need any arguments.", file=sys.stderr) | |
1297 | exit(1) | |
20f6e7c5 | 1298 | |
20f6e7c5 | 1299 | |
a51cdb72 | 1300 | if __name__ == "__main__": |
dd2b62d7 | 1301 | """Executes main if this file is not loaded interactively""" |
20f6e7c5 | 1302 | |
a51cdb72 | 1303 | main() |
0bec96d6 | 1304 |