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