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