added comments to the classes and main()
[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 importlib
40
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
49 import edward_config
50
51 langs = ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
52
53 """This list contains the abbreviated names of reply languages available to
54 edward."""
55
56
57 match_types = [('clearsign',
58 '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
59 ('message',
60 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
61 ('pubkey',
62 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
63 ('detachedsig',
64 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
65
66 """This list of tuples matches query names with re.search() queries used
67 to find GPG data for edward to process."""
68
69
70 class EddyMsg (object):
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
93 def __init__(self):
94 self.multipart = False
95 self.subparts = []
96
97 self.charset = None
98 self.payload_bytes = None
99 self.payload_pieces = []
100
101 self.filename = None
102 self.content_type = None
103 self.description_list = None
104
105
106 class PayloadPiece (object):
107 """
108 PayloadPiece represents a complte or sub-section of a mime part.
109
110 Instances of this class are often strung together within one or more arrays
111 pointed to by each instance of the EddyMsg class.
112
113 'piece_type' refers to a string whose value describes the content of
114 'string'. Examples include "pubkey", for public keys, and "message", for
115 encrypted data (or armored signatures until they are known to be such.) The
116 names derive from the header and footer of each of these ascii-encoded gpg
117 blocks.
118
119 'string' contains some string of text, such as non-GPG text, an encrypted
120 block of text, a signature, or a public key.
121
122 'gpg_data' points to any instances of GPGData that have been created based
123 on the contents of 'string'.
124 """
125
126 def __init__(self):
127 self.piece_type = None
128 self.string = None
129 self.gpg_data = None
130
131
132 class GPGData (object):
133 """
134 GPGData holds info from decryption, sig. verification, and/or pub. keys.
135
136 Instances of this class contain decrypted information, signature
137 fingerprints and/or fingerprints of processed and imported public keys.
138
139 'decrypted' is set to True if 'plainobj' was created from encrypted data.
140
141 'plainobj' points to any decrypted, or signed part of, a GPG signature. It
142 is intended to be an instance of the EddyMsg class.
143
144 'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
145
146 'keys' is a list of fingerprints of keys obtained in public key blocks.
147 """
148
149 def __init__(self):
150 self.decrypted = False
151
152 self.plainobj = None
153 self.sigs = []
154 self.keys = []
155
156 class ReplyInfo (object):
157 """
158 ReplyInfo contains details that edward uses in generating its reply.
159
160 Instances of this class contain information about whether a message was
161 successfully encrypted or signed, and whether a public key was attached, or
162 retrievable, from the local GPG store. It stores the fingerprints of
163 potential encryption key candidates and the message (if any at all) to
164 quote in edward's reply.
165
166 'replies' points one of the dictionaries of translated replies.
167
168 'target_key' refers to the fingerprint of a key used to sign encrypted
169 data. This is the preferred key, if it is set, and if is available.
170
171 'fallback_target_key' referst to the fingerprint of a key used to sign
172 unencrypted data; alternatively it may be a public key attached to the
173 message.
174
175 'msg_to_quote' refers to the part of a message which edward should quote in
176 his reply. This should remain as None if there was no encrypted and singed
177 part. This is to avoid making edward a service for decrypting other
178 people's messages to edward.
179
180 'success_decrypt' is set to True if edward could decrypt part of the
181 message.
182
183 'failed_decrypt' is set to True if edward failed to decrypt part of the
184 message.
185
186 'publick_key_received' is set to True if edward successfully imported a
187 public key.
188
189 'no_public_key' is set to True if edward doesn't have a key to encrypt to
190 when replying to the user.
191
192 'sig_success' is set to True if edward could to some extent verify the
193 signature of a signed part of the message to edward.
194
195 'sig_failure' is set to True if edward failed to some extent verify the
196 signature of a signed part of the message to edward.
197 """
198
199 def __init__(self):
200 self.replies = None
201
202 self.target_key = None
203 self.fallback_target_key = None
204 self.msg_to_quote = ""
205
206 self.success_decrypt = False
207 self.failed_decrypt = False
208 self.public_key_received = False
209 self.no_public_key = False
210 self.sig_success = False
211 self.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 None
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 os.environ['GNUPGHOME'] = gnupghome
271
272 gpgme_ctx = gpgme.Context()
273 gpgme_ctx.armor = True
274
275 try:
276 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
277 except:
278 error("unable to load signing key. is the gnupghome "
279 + "and signing key properly set in the edward_config.py?")
280 exit(1)
281
282 gpgme_ctx.signers = [sign_with_key]
283
284 return gpgme_ctx
285
286
287 def parse_pgp_mime (email_text, gpgme_ctx):
288
289 email_struct = email.parser.Parser().parsestr(email_text)
290
291 eddymsg_obj = parse_mime(email_struct)
292 split_payloads(eddymsg_obj)
293 gpg_on_payloads(eddymsg_obj, gpgme_ctx)
294
295 return eddymsg_obj
296
297
298 def parse_mime(msg_struct):
299
300 eddymsg_obj = EddyMsg()
301
302 if msg_struct.is_multipart() == True:
303 payloads = msg_struct.get_payload()
304
305 eddymsg_obj.multipart = True
306 eddymsg_obj.subparts = list(map(parse_mime, payloads))
307
308 else:
309 eddymsg_obj = get_subpart_data(msg_struct)
310
311 return eddymsg_obj
312
313
314 def scan_and_split (payload_piece, match_type, pattern):
315
316 # don't try to re-split pieces containing gpg data
317 if payload_piece.piece_type != "text":
318 return [payload_piece]
319
320 flags = re.DOTALL | re.MULTILINE
321 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
322 ")(?P<rest>.*)", payload_piece.string, flags=flags)
323
324 if matches == None:
325 pieces = [payload_piece]
326
327 else:
328
329 beginning = PayloadPiece()
330 beginning.string = matches.group('beginning')
331 beginning.piece_type = payload_piece.piece_type
332
333 match = PayloadPiece()
334 match.string = matches.group('match')
335 match.piece_type = match_type
336
337 rest = PayloadPiece()
338 rest.string = matches.group('rest')
339 rest.piece_type = payload_piece.piece_type
340
341 more_pieces = scan_and_split(rest, match_type, pattern)
342 pieces = [beginning, match ] + more_pieces
343
344 return pieces
345
346
347 def get_subpart_data (part):
348
349 obj = EddyMsg()
350
351 obj.charset = part.get_content_charset()
352 obj.payload_bytes = part.get_payload(decode=True)
353
354 obj.filename = part.get_filename()
355 obj.content_type = part.get_content_type()
356 obj.description_list = part['content-description']
357
358 # your guess is as good as a-myy-ee-ine...
359 if obj.charset == None:
360 obj.charset = 'utf-8'
361
362 if obj.payload_bytes != None:
363 try:
364 payload = PayloadPiece()
365 payload.string = obj.payload_bytes.decode(obj.charset)
366 payload.piece_type = 'text'
367
368 obj.payload_pieces = [payload]
369 except UnicodeDecodeError:
370 pass
371
372 return obj
373
374
375 def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
376
377 if eddymsg_obj.multipart == True:
378 for sub in eddymsg_obj.subparts:
379 do_to_eddys_pieces(function_to_do, sub, data)
380 else:
381 function_to_do(eddymsg_obj, data)
382
383
384 def split_payloads (eddymsg_obj):
385
386 for match_type in match_types:
387 do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_type)
388
389
390 def split_payload_pieces (eddymsg_obj, match_type):
391
392 (match_name, pattern) = match_type
393
394 new_pieces_list = []
395 for piece in eddymsg_obj.payload_pieces:
396 new_pieces_list += scan_and_split(piece, match_name, pattern)
397
398 eddymsg_obj.payload_pieces = new_pieces_list
399
400
401 def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
402
403 if eddymsg_obj.multipart == True:
404 prev_parts=[]
405 for sub in eddymsg_obj.subparts:
406 gpg_on_payloads (sub, gpgme_ctx, prev_parts)
407 prev_parts += [sub]
408
409 return
410
411 for piece in eddymsg_obj.payload_pieces:
412
413 if piece.piece_type == "text":
414 # don't transform the plaintext.
415 pass
416
417 elif piece.piece_type == "message":
418 (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
419
420 if plaintext:
421 piece.gpg_data = GPGData()
422 piece.gpg_data.decrypted = True
423 piece.gpg_data.sigs = sigs
424 # recurse!
425 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
426 continue
427
428 # if not encrypted, check to see if this is an armored signature.
429 (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx)
430
431 if plaintext:
432 piece.piece_type = "signature"
433 piece.gpg_data = GPGData()
434 piece.gpg_data.sigs = sigs
435 # recurse!
436 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
437
438 elif piece.piece_type == "pubkey":
439 key_fps = add_gpg_key(piece.string, gpgme_ctx)
440
441 if key_fps != []:
442 piece.gpg_data = GPGData()
443 piece.gpg_data.keys = key_fps
444
445 elif piece.piece_type == "clearsign":
446 (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
447
448 if sig_fps != []:
449 piece.gpg_data = GPGData()
450 piece.gpg_data.sigs = sig_fps
451 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
452
453 elif piece.piece_type == "detachedsig":
454 for prev in prev_parts:
455 payload_bytes = prev.payload_bytes
456 sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
457
458 if sig_fps != []:
459 piece.gpg_data = GPGData()
460 piece.gpg_data.sigs = sig_fps
461 piece.gpg_data.plainobj = prev
462 break
463
464 else:
465 pass
466
467
468 def prepare_for_reply (eddymsg_obj, replyinfo_obj):
469
470 do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
471
472 def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
473
474 for piece in eddymsg_obj.payload_pieces:
475 if piece.piece_type == "text":
476 # don't quote the plaintext part.
477 pass
478
479 elif piece.piece_type == "message":
480 prepare_for_reply_message(piece, replyinfo_obj)
481
482 elif piece.piece_type == "pubkey":
483 prepare_for_reply_pubkey(piece, replyinfo_obj)
484
485 elif (piece.piece_type == "clearsign") \
486 or (piece.piece_type == "detachedsig") \
487 or (piece.piece_type == "signature"):
488 prepare_for_reply_sig(piece, replyinfo_obj)
489
490
491 def prepare_for_reply_message (piece, replyinfo_obj):
492
493 if piece.gpg_data == None:
494 replyinfo_obj.failed_decrypt = True
495 return
496
497 replyinfo_obj.success_decrypt = True
498
499 # we already have a key (and a message)
500 if replyinfo_obj.target_key != None:
501 return
502
503 if piece.gpg_data.sigs != []:
504 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
505 get_signed_part = False
506 else:
507 # only include a signed message in the reply.
508 get_signed_part = True
509
510 replyinfo_obj.msg_to_quote = flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
511
512 # to catch public keys in encrypted blocks
513 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
514
515
516 def prepare_for_reply_pubkey (piece, replyinfo_obj):
517
518 if piece.gpg_data == None or piece.gpg_data.keys == []:
519 replyinfo_obj.no_public_key = True
520 else:
521 replyinfo_obj.public_key_received = True
522
523 if replyinfo_obj.fallback_target_key == None:
524 replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
525
526
527 def prepare_for_reply_sig (piece, replyinfo_obj):
528
529 if piece.gpg_data == None or piece.gpg_data.sigs == []:
530 replyinfo_obj.sig_failure = True
531 else:
532 replyinfo_obj.sig_success = True
533
534 if replyinfo_obj.fallback_target_key == None:
535 replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
536
537
538
539 def flatten_decrypted_payloads (eddymsg_obj, get_signed_part):
540
541 flat_string = ""
542
543 if eddymsg_obj == None:
544 return ""
545
546 # recurse on multi-part mime
547 if eddymsg_obj.multipart == True:
548 for sub in eddymsg_obj.subparts:
549 flat_string += flatten_decrypted_payloads (sub, get_signed_part)
550
551 return flat_string
552
553 for piece in eddymsg_obj.payload_pieces:
554 if piece.piece_type == "text":
555 flat_string += piece.string
556
557 if (get_signed_part):
558 # don't include nested encryption
559 if (piece.piece_type == "message") \
560 and (piece.gpg_data != None) \
561 and (piece.gpg_data.decrypted == False):
562 flat_string += flatten_decrypted_payloads(piece.gpg_data.plainobj, get_signed_part)
563
564 elif ((piece.piece_type == "clearsign") \
565 or (piece.piece_type == "detachedsig") \
566 or (piece.piece_type == "signature")) \
567 and (piece.gpg_data != None):
568 # FIXME: the key used to sign this message needs to be the one that is used for the encrypted reply.
569 flat_string += flatten_decrypted_payloads (piece.gpg_data.plainobj, get_signed_part)
570
571 return flat_string
572
573
574 def get_key_from_fp (replyinfo_obj, gpgme_ctx):
575
576 if replyinfo_obj.target_key == None:
577 replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
578
579 if replyinfo_obj.target_key != None:
580 try:
581 encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
582 return encrypt_to_key
583
584 except:
585 pass
586
587 # no available key to use
588 replyinfo_obj.target_key = None
589 replyinfo_obj.fallback_target_key = None
590
591 replyinfo_obj.no_public_key = True
592 replyinfo_obj.public_key_received = False
593
594 return None
595
596
597 def write_reply (replyinfo_obj):
598
599 reply_plain = ""
600
601 if replyinfo_obj.success_decrypt == True:
602 reply_plain += replyinfo_obj.replies['success_decrypt']
603
604 if replyinfo_obj.no_public_key == False:
605 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
606 reply_plain += quoted_text
607
608 elif replyinfo_obj.failed_decrypt == True:
609 reply_plain += replyinfo_obj.replies['failed_decrypt']
610
611
612 if replyinfo_obj.sig_success == True:
613 reply_plain += "\n\n"
614 reply_plain += replyinfo_obj.replies['sig_success']
615
616 elif replyinfo_obj.sig_failure == True:
617 reply_plain += "\n\n"
618 reply_plain += replyinfo_obj.replies['sig_failure']
619
620
621 if replyinfo_obj.public_key_received == True:
622 reply_plain += "\n\n"
623 reply_plain += replyinfo_obj.replies['public_key_received']
624
625 elif replyinfo_obj.no_public_key == True:
626 reply_plain += "\n\n"
627 reply_plain += replyinfo_obj.replies['no_public_key']
628
629
630 reply_plain += "\n\n"
631 reply_plain += replyinfo_obj.replies['signature']
632
633 return reply_plain
634
635
636 def add_gpg_key (key_block, gpgme_ctx):
637
638 fp = io.BytesIO(key_block.encode('ascii'))
639
640 result = gpgme_ctx.import_(fp)
641 imports = result.imports
642
643 key_fingerprints = []
644
645 if imports != []:
646 for import_ in imports:
647 fingerprint = import_[0]
648 key_fingerprints += [fingerprint]
649
650 debug("added gpg key: " + fingerprint)
651
652 return key_fingerprints
653
654
655 def verify_sig_message (msg_block, gpgme_ctx):
656
657 block_b = io.BytesIO(msg_block.encode('ascii'))
658 plain_b = io.BytesIO()
659
660 try:
661 sigs = gpgme_ctx.verify(block_b, None, plain_b)
662 except:
663 return ("",[])
664
665 plaintext = plain_b.getvalue().decode('utf-8')
666
667 fingerprints = []
668 for sig in sigs:
669 fingerprints += [sig.fpr]
670 return (plaintext, fingerprints)
671
672
673 def verify_clear_signature (sig_block, gpgme_ctx):
674
675 # FIXME: this might require the un-decoded bytes
676 # or the correct re-encoding with the carset of the mime part.
677 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
678 ptxt_fp = io.BytesIO()
679
680 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
681
682 # FIXME: this might require using the charset of the mime part.
683 plaintext = ptxt_fp.getvalue().decode('utf-8')
684
685 sig_fingerprints = []
686 for res_ in result:
687 sig_fingerprints += [res_.fpr]
688
689 return plaintext, sig_fingerprints
690
691
692 def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
693
694 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
695 plaintext_fp = io.BytesIO(plaintext_bytes)
696 ptxt_fp = io.BytesIO()
697
698 result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
699
700 sig_fingerprints = []
701 for res_ in result:
702 sig_fingerprints += [res_.fpr]
703
704 return sig_fingerprints
705
706
707 def decrypt_block (msg_block, gpgme_ctx):
708
709 block_b = io.BytesIO(msg_block.encode('ascii'))
710 plain_b = io.BytesIO()
711
712 try:
713 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
714 except:
715 return ("",[])
716
717 plaintext = plain_b.getvalue().decode('utf-8')
718
719 fingerprints = []
720 for sig in sigs:
721 fingerprints += [sig.fpr]
722 return (plaintext, fingerprints)
723
724
725 def choose_reply_encryption_key (gpgme_ctx, fingerprints):
726
727 reply_key = None
728 for fp in fingerprints:
729 try:
730 key = gpgme_ctx.get_key(fp)
731
732 if (key.can_encrypt == True):
733 reply_key = key
734 break
735 except:
736 continue
737
738
739 return reply_key
740
741
742 def email_to_from_subject (email_text):
743
744 email_struct = email.parser.Parser().parsestr(email_text)
745
746 email_to = email_struct['To']
747 email_from = email_struct['From']
748 email_subject = email_struct['Subject']
749
750 return email_to, email_from, email_subject
751
752
753 def import_lang(email_to):
754
755 if email_to != None:
756 for lang in langs:
757 if "edward-" + lang in email_to:
758 lang = "lang." + re.sub('-', '_', lang)
759 language = importlib.import_module(lang)
760
761 return language
762
763 return importlib.import_module("lang.en")
764
765
766 def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
767 gpgme_ctx):
768
769 # quoted printable encoding lets most ascii characters look normal
770 # before the mime message is decoded.
771 char_set = email.charset.Charset("utf-8")
772 char_set.body_encoding = email.charset.QP
773
774 # MIMEText doesn't allow setting the text encoding
775 # so we use MIMENonMultipart.
776 plaintext_mime = MIMENonMultipart('text', 'plain')
777 plaintext_mime.set_payload(plaintext, charset=char_set)
778
779 if (encrypt_to_key != None):
780
781 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
782 encrypt_to_key,
783 gpgme_ctx)
784
785 control_mime = MIMEApplication("Version: 1",
786 _subtype='pgp-encrypted',
787 _encoder=email.encoders.encode_7or8bit)
788 control_mime['Content-Description'] = 'PGP/MIME version identification'
789 control_mime.set_charset('us-ascii')
790
791 encoded_mime = MIMEApplication(encrypted_text,
792 _subtype='octet-stream; name="encrypted.asc"',
793 _encoder=email.encoders.encode_7or8bit)
794 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
795 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
796 encoded_mime.set_charset('us-ascii')
797
798 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
799 message_mime.attach(control_mime)
800 message_mime.attach(encoded_mime)
801 message_mime['Content-Disposition'] = 'inline'
802
803 else:
804 message_mime = plaintext_mime
805
806 message_mime['To'] = email_from
807 message_mime['Subject'] = email_subject
808
809 reply = message_mime.as_string()
810
811 return reply
812
813
814 def email_quote_text (text):
815
816 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
817
818 return quoted_message
819
820
821 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
822
823 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
824 encrypted_bytes = io.BytesIO()
825
826 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
827 plaintext_bytes, encrypted_bytes)
828
829 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
830 return encrypted_txt
831
832
833 def error (error_msg):
834
835 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
836
837
838 def debug (debug_msg):
839
840 if edward_config.debug == True:
841 error(debug_msg)
842
843
844 def handle_args ():
845 if __name__ == "__main__":
846
847 global progname
848 progname = sys.argv[0]
849
850 if len(sys.argv) > 1:
851 print(progname + ": error, this program doesn't " \
852 "need any arguments.", file=sys.stderr)
853 exit(1)
854
855
856 main()
857