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. *
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. *
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/>. *
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+) *
23 * Special thanks to Josh Drake for writing the original edward bot! :) *
25 ************************************************************************
27 Code sourced from these projects:
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
46 from email
.mime
.multipart
import MIMEMultipart
47 from email
.mime
.application
import MIMEApplication
48 from email
.mime
.nonmultipart
import MIMENonMultipart
52 langs
= ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
54 """This list contains the abbreviated names of reply languages available to
57 class TxtType (enum
.Enum
):
65 match_types
= [(TxtType
.message
,
66 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
68 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
70 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
72 """This list of tuples matches query names with re.search() queries used
73 to find GPG data for edward to process."""
76 class EddyMsg (object):
78 The EddyMsg class represents relevant parts of a mime message.
80 The represented message can be single-part or multi-part.
82 'multipart' is set to True if there are multiple mime parts.
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.
87 'payload_bytes' is a binary representation of the mime part before header
88 removal and message decoding.
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.
93 The 'filename', 'content_type' and 'description_list' come from the mime
105 description_list
= None
108 class PayloadPiece (object):
110 PayloadPiece represents a complte or sub-section of a mime part.
112 Instances of this class are often strung together within one or more arrays
113 pointed to by each instance of the EddyMsg class.
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.
121 'string' contains some string of text, such as non-GPG text, an encrypted
122 block of text, a signature, or a public key.
124 'gpg_data' points to any instances of GPGData that have been created based
125 on the contents of 'string'.
133 class GPGData (object):
135 GPGData holds info from decryption, sig. verification, and/or pub. keys.
137 Instances of this class contain decrypted information, signature
138 fingerprints and/or fingerprints of processed and imported public keys.
140 'decrypted' is set to True if 'plainobj' was created from encrypted data.
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.
145 'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
147 'keys' is a list of fingerprints of keys obtained in public key blocks.
157 class ReplyInfo (object):
159 ReplyInfo contains details that edward uses in generating its reply.
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.
167 'replies' points one of the dictionaries of translated replies.
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.
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
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.
181 'success_decrypt' is set to True if edward could decrypt part of the
184 'failed_decrypt' is set to True if edward failed to decrypt part of the
187 'publick_key_received' is set to True if edward successfully imported a
190 'no_public_key' is set to True if edward doesn't have a key to encrypt to
191 when replying to the user.
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.
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.
203 fallback_target_key
= None
206 success_decrypt
= False
207 failed_decrypt
= False
208 public_key_received
= False
209 no_public_key
= False
217 This is the main function for edward, a GPG reply bot.
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.
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
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.
245 gpgme_ctx
= get_gpg_context(edward_config
.gnupghome
,
246 edward_config
.sign_with_key
)
248 email_text
= sys
.stdin
.read()
249 email_struct
= parse_pgp_mime(email_text
, gpgme_ctx
)
251 email_to
, email_from
, email_subject
= email_to_from_subject(email_text
)
252 lang
= import_lang(email_to
)
254 replyinfo_obj
= ReplyInfo()
255 replyinfo_obj
.replies
= lang
.replies
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
)
261 reply_mime
= generate_encrypted_mime(reply_plaintext
, email_from
, \
262 email_subject
, encrypt_to_key
,
268 def get_gpg_context (gnupghome
, sign_with_key_fp
):
270 This function returns the GPG context needed for encryption and signing.
272 The context is needed by other functions which use GPG functionality.
275 gnupghome: The path to "~/.gnupg/" or its alternative.
276 sign_with_key: The fingerprint of the key to sign with
279 A gpgme context to be used for GPG functions.
282 the 'armor' flag is set to True and the list of signing keys contains
283 the single specified key
286 os
.environ
['GNUPGHOME'] = gnupghome
288 gpgme_ctx
= gpgme
.Context()
289 gpgme_ctx
.armor
= True
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?")
298 gpgme_ctx
.signers
= [sign_with_key
]
303 def parse_pgp_mime (email_text
, gpgme_ctx
):
304 """Parses the email for mime payloads and decrypts/verfies signatures.
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.
312 email_text: an email message in string format
313 gpgme_ctx: a gpgme context
316 A message as an instance of EddyMsg
319 the returned EddyMsg instance has split, decrypted, verified and pubkey
323 email_struct
= email
.parser
.Parser().parsestr(email_text
)
325 eddymsg_obj
= parse_mime(email_struct
)
326 split_payloads(eddymsg_obj
)
327 gpg_on_payloads(eddymsg_obj
, gpgme_ctx
)
332 def parse_mime(msg_struct
):
333 """Translates python's email.parser format into an EddyMsg format
335 If the message is multi-part, then a recursive object is created, where
336 each sub-part is also a EddyMsg instance.
339 msg_struct: an email parsed with email.parser.Parser(), which can be
343 an instance of EddyMsg, potentially a recursive one.
346 eddymsg_obj
= EddyMsg()
348 if msg_struct
.is_multipart() == True:
349 payloads
= msg_struct
.get_payload()
351 eddymsg_obj
.multipart
= True
352 eddymsg_obj
.subparts
= list(map(parse_mime
, payloads
))
355 eddymsg_obj
= get_subpart_data(msg_struct
)
360 def scan_and_split (payload_piece
, match_type
, pattern
):
361 """This splits the payloads of an EddyMsg object into GPG and text parts.
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/
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
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'.
379 # don't try to re-split pieces containing gpg data
380 if payload_piece
.piece_type
!= TxtType
.text
:
381 return [payload_piece
]
383 flags
= re
.DOTALL | re
.MULTILINE
384 matches
= re
.search("(?P<beginning>.*?)(?P<match>" + pattern
+
385 ")(?P<rest>.*)", payload_piece
.string
, flags
=flags
)
388 pieces
= [payload_piece
]
392 beginning
= PayloadPiece()
393 beginning
.string
= matches
.group('beginning')
394 beginning
.piece_type
= payload_piece
.piece_type
396 match
= PayloadPiece()
397 match
.string
= matches
.group('match')
398 match
.piece_type
= match_type
400 rest
= PayloadPiece()
401 rest
.string
= matches
.group('rest')
402 rest
.piece_type
= payload_piece
.piece_type
404 more_pieces
= scan_and_split(rest
, match_type
, pattern
)
405 pieces
= [beginning
, match
] + more_pieces
410 def get_subpart_data (part
):
411 """This function grabs information from a single part mime object.
413 It copies needed data from a single part email.parser.Parser() object over
414 to an EddyMsg object.
417 part: a non-multi-part mime.parser.Parser() object
420 a single-part EddyMsg() object
423 charset
= part
.get_content_charset()
424 mime_decoded_bytes
= part
.get_payload(decode
=True)
427 obj
.payload_bytes
= part
.as_bytes()
429 obj
.filename
= part
.get_filename()
430 obj
.content_type
= part
.get_content_type()
431 obj
.description_list
= part
['content-description']
433 # your guess is as good as a-myy-ee-ine...
437 if mime_decoded_bytes
!= None:
439 payload
= PayloadPiece()
440 payload
.string
= mime_decoded_bytes
.decode(charset
)
441 payload
.piece_type
= TxtType
.text
443 obj
.payload_pieces
= [payload
]
444 except UnicodeDecodeError:
450 def do_to_eddys_pieces (function_to_do
, eddymsg_obj
, data
):
451 """A function which maps another function onto a message's subparts.
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
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'
467 The passed-in EddyMsg object is transformed recursively on its
468 sub-parts according to 'function_to_do'.
471 if eddymsg_obj
.multipart
== True:
472 for sub
in eddymsg_obj
.subparts
:
473 do_to_eddys_pieces(function_to_do
, sub
, data
)
475 function_to_do(eddymsg_obj
, data
)
478 def split_payloads (eddymsg_obj
):
479 """Splits all (sub-)payloads of a message into GPG data and regular text.
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.
486 eddymsg_obj: an instance of EddyMsg
492 The EddyMsg object has payloads that are unsplit (by may be split)..
495 The EddyMsg object's payloads are all split into GPG and non-GPG parts.
498 for match_type
in match_types
:
499 do_to_eddys_pieces(split_payload_pieces
, eddymsg_obj
, match_type
)
502 def split_payload_pieces (eddymsg_obj
, match_type
):
503 """A helper function for split_payloads(); works on PayloadPiece objects.
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.
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.
518 The payload piece(s) of an EddyMsg object may be already split or
522 The EddyMsg object's payload piece(s) are split into a list of pieces
523 if matches of the match_type are found.
526 (match_name
, pattern
) = match_type
529 for piece
in eddymsg_obj
.payload_pieces
:
530 new_pieces_list
+= scan_and_split(piece
, match_name
, pattern
)
532 eddymsg_obj
.payload_pieces
= new_pieces_list
535 def gpg_on_payloads (eddymsg_obj
, gpgme_ctx
, prev_parts
=[]):
536 """Performs GPG operations on the GPG parts of the message
538 This function decrypts text, verifies signatures, and imports public keys
539 included in an email.
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
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
556 eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
559 Decryption, verification and key imports occur. the gpg_data member of
560 PayloadPiece objects get filled in with GPGData objects.
563 if eddymsg_obj
.multipart
== True:
565 for sub
in eddymsg_obj
.subparts
:
566 gpg_on_payloads (sub
, gpgme_ctx
, prev_parts
)
571 for piece
in eddymsg_obj
.payload_pieces
:
573 if piece
.piece_type
== TxtType
.text
:
574 # don't transform the plaintext.
577 elif piece
.piece_type
== TxtType
.message
:
578 (plaintext
, sigs
) = decrypt_block(piece
.string
, gpgme_ctx
)
581 piece
.gpg_data
= GPGData()
582 piece
.gpg_data
.decrypted
= True
583 piece
.gpg_data
.sigs
= sigs
585 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
588 # if not encrypted, check to see if this is an armored signature.
589 (plaintext
, sigs
) = verify_sig_message(piece
.string
, gpgme_ctx
)
592 piece
.piece_type
= TxtType
.signature
593 piece
.gpg_data
= GPGData()
594 piece
.gpg_data
.sigs
= sigs
596 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
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
)
604 piece
.gpg_data
= GPGData()
605 piece
.gpg_data
.keys
= key_fps
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
)
612 piece
.gpg_data
= GPGData()
613 piece
.gpg_data
.sigs
= sig_fps
614 piece
.gpg_data
.plainobj
= prev
621 def prepare_for_reply (eddymsg_obj
, replyinfo_obj
):
622 """Updates replyinfo_obj with info on the message's GPG success/failures
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.
629 eddymsg_obj: a message in the EddyMsg format
630 replyinfo_obj: an instance of ReplyInfo
636 eddymsg_obj has had its gpg_data created by gpg_on_payloads
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.
644 do_to_eddys_pieces(prepare_for_reply_pieces
, eddymsg_obj
, replyinfo_obj
)
646 def prepare_for_reply_pieces (eddymsg_obj
, replyinfo_obj
):
647 """A helper function for prepare_for_reply
649 It updates replyinfo_obj with GPG success/failure information, when
650 supplied a single-part EddyMsg object.
653 eddymsg_obj: a single-part message in the EddyMsg format
654 replyinfo_obj: an object which holds information about the message's
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
666 replyinfo_obj has been updated with gpg success/failure information
669 for piece
in eddymsg_obj
.payload_pieces
:
670 if piece
.piece_type
== TxtType
.text
:
671 # don't quote the plaintext part.
674 elif piece
.piece_type
== TxtType
.message
:
675 prepare_for_reply_message(piece
, replyinfo_obj
)
677 elif piece
.piece_type
== TxtType
.pubkey
:
678 prepare_for_reply_pubkey(piece
, replyinfo_obj
)
680 elif (piece
.piece_type
== TxtType
.detachedsig
) \
681 or (piece
.piece_type
== TxtType
.signature
):
682 prepare_for_reply_sig(piece
, replyinfo_obj
)
685 def prepare_for_reply_message (piece
, replyinfo_obj
):
686 """Helper function for prepare_for_reply()
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.
694 piece: a PayloadPiece object.
695 replyinfo_obj: object which gets updated with decryption status, etc.
702 the piece.payload_piece value should be TxtType.message.
705 replyinfo_obj gets updated with decryption status, signing status and a
706 potential signing key.
709 if piece
.gpg_data
== None:
710 replyinfo_obj
.failed_decrypt
= True
713 replyinfo_obj
.success_decrypt
= True
715 # we already have a key (and a message)
716 if replyinfo_obj
.target_key
!= None:
719 if piece
.gpg_data
.sigs
!= []:
720 replyinfo_obj
.target_key
= piece
.gpg_data
.sigs
[0]
721 get_signed_part
= False
723 # only include a signed message in the reply.
724 get_signed_part
= True
726 flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, replyinfo_obj
, get_signed_part
)
728 # to catch public keys in encrypted blocks
729 prepare_for_reply(piece
.gpg_data
.plainobj
, replyinfo_obj
)
732 def prepare_for_reply_pubkey (piece
, replyinfo_obj
):
733 """Helper function for prepare_for_reply(). Marks pubkey import status.
735 Marks replyinfo_obj with pub key import status.
738 piece: a PayloadPiece object
739 replyinfo_obj: a ReplyInfo object
742 piece.piece_type should be set to TxtType.pubkey .
745 replyinfo_obj has its fields updated.
748 if piece
.gpg_data
== None or piece
.gpg_data
.keys
== []:
749 replyinfo_obj
.no_public_key
= True
751 replyinfo_obj
.public_key_received
= True
753 # prefer public key as a fallback for the encrypted reply
754 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.keys
[0]
757 def prepare_for_reply_sig (piece
, replyinfo_obj
):
758 """Helper function for prepare_for_reply(). Marks sig verification status.
760 Marks replyinfo_obj with signature verification status.
763 piece: a PayloadPiece object
764 replyinfo_obj: a ReplyInfo object
767 piece.piece_type should be set to TxtType.signature, or
768 TxtType.detachedsig .
771 replyinfo_obj has its fields updated.
774 if piece
.gpg_data
== None or piece
.gpg_data
.sigs
== []:
775 replyinfo_obj
.sig_failure
= True
777 replyinfo_obj
.sig_success
= True
779 if replyinfo_obj
.fallback_target_key
== None:
780 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.sigs
[0]
783 def flatten_decrypted_payloads (eddymsg_obj
, replyinfo_obj
, get_signed_part
):
784 """For creating a string representation of a signed, encrypted part.
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.
793 eddymsg_obj: the message in EddyMsg format created by decrypting GPG
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.
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
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
814 if eddymsg_obj
== None:
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
)
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]
831 if piece
.piece_type
== TxtType
.text
:
832 replyinfo_obj
.msg_to_quote
+= piece
.string
835 def get_key_from_fp (replyinfo_obj
, gpgme_ctx
):
836 """Obtains a public key object from a key fingerprint
838 If the .target_key is not set, then we use .fallback_target_key.
841 replyinfo_obj: ReplyInfo instance
842 gpgme_ctx: the gpgme context
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
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.
855 If the key cannot be loaded, then the replyinfo_obj is marked for
856 having no public key available.
859 if replyinfo_obj
.target_key
== None:
860 replyinfo_obj
.target_key
= replyinfo_obj
.fallback_target_key
862 if replyinfo_obj
.target_key
!= None:
864 encrypt_to_key
= gpgme_ctx
.get_key(replyinfo_obj
.target_key
)
865 return encrypt_to_key
867 except gpgme
.GpgmeError
:
870 # no available key to use
871 replyinfo_obj
.target_key
= None
872 replyinfo_obj
.fallback_target_key
= None
874 replyinfo_obj
.no_public_key
= True
875 replyinfo_obj
.public_key_received
= False
880 def write_reply (replyinfo_obj
):
881 """Write the reply email body about the GPG successes/failures.
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
889 replyinfo_obj: contains details of GPG processing status
892 the plaintext message to be sent to the user
895 replyinfo_obj should be populated with info about GPG processing status.
900 if replyinfo_obj
.success_decrypt
== True:
901 reply_plain
+= replyinfo_obj
.replies
['success_decrypt']
903 if replyinfo_obj
.no_public_key
== False:
904 quoted_text
= email_quote_text(replyinfo_obj
.msg_to_quote
)
905 reply_plain
+= quoted_text
907 elif replyinfo_obj
.failed_decrypt
== True:
908 reply_plain
+= replyinfo_obj
.replies
['failed_decrypt']
911 if replyinfo_obj
.sig_success
== True:
912 reply_plain
+= "\n\n"
913 reply_plain
+= replyinfo_obj
.replies
['sig_success']
915 elif replyinfo_obj
.sig_failure
== True:
916 reply_plain
+= "\n\n"
917 reply_plain
+= replyinfo_obj
.replies
['sig_failure']
920 if replyinfo_obj
.public_key_received
== True:
921 reply_plain
+= "\n\n"
922 reply_plain
+= replyinfo_obj
.replies
['public_key_received']
924 elif replyinfo_obj
.no_public_key
== True:
925 reply_plain
+= "\n\n"
926 reply_plain
+= replyinfo_obj
.replies
['no_public_key']
929 reply_plain
+= "\n\n"
930 reply_plain
+= replyinfo_obj
.replies
['signature']
935 def add_gpg_key (key_block
, gpgme_ctx
):
936 """Adds a GPG pubkey to the local keystore
938 This adds keys received through email into the key store so they can be
942 key_block: the string form of the ascii-armored public key block
943 gpgme_ctx: the gpgme context
946 the fingerprint(s) of the imported key(s)
949 fp
= io
.BytesIO(key_block
.encode('ascii'))
952 result
= gpgme_ctx
.import_(fp
)
953 imports
= result
.imports
954 except gpgme
.GpgmeError
:
957 key_fingerprints
= []
960 for import_
in imports
:
961 fingerprint
= import_
[0]
962 key_fingerprints
+= [fingerprint
]
964 debug("added gpg key: " + fingerprint
)
966 return key_fingerprints
969 def verify_sig_message (msg_block
, gpgme_ctx
):
970 """Verifies the signature of a signed, ascii-armored block of text.
972 It encodes the string into ascii, since binary GPG files are currently
973 unsupported, and alternative, the ascii-armored format is encodable into
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
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
988 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
989 plain_b
= io
.BytesIO()
992 sigs
= gpgme_ctx
.verify(block_b
, None, plain_b
)
993 except gpgme
.GpgmeError
:
996 plaintext
= plain_b
.getvalue().decode('utf-8')
1000 fingerprints
+= [sig
.fpr
]
1001 return (plaintext
, fingerprints
)
1004 def verify_detached_signature (detached_sig
, plaintext_bytes
, gpgme_ctx
):
1005 """Verifies the signature of a detached signature.
1007 This requires the signature part and the signed part as separate arguments.
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
1015 A list of signing fingerprints if the signature verification was
1016 sucessful. Otherwise, an empty list is returned.
1019 detached_sig_fp
= io
.BytesIO(detached_sig
.encode('ascii'))
1020 plaintext_fp
= io
.BytesIO(plaintext_bytes
)
1023 result
= gpgme_ctx
.verify(detached_sig_fp
, plaintext_fp
, None)
1024 except gpgme
.GpgmeError
:
1027 sig_fingerprints
= []
1029 sig_fingerprints
+= [res_
.fpr
]
1031 return sig_fingerprints
1034 def decrypt_block (msg_block
, gpgme_ctx
):
1035 """Decrypts a block of GPG text, and verifies any included sigatures.
1037 Some encypted messages have embeded signatures, so those are verified too.
1040 msg_block: the encrypted(/signed) text
1041 gpgme_ctx: the gpgme context
1044 A tuple of plaintext and signatures, if the decryption and signature
1045 verification were successful, respectively.
1048 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
1049 plain_b
= io
.BytesIO()
1052 sigs
= gpgme_ctx
.decrypt_verify(block_b
, plain_b
)
1053 except gpgme
.GpgmeError
:
1056 plaintext
= plain_b
.getvalue().decode('utf-8')
1060 fingerprints
+= [sig
.fpr
]
1061 return (plaintext
, fingerprints
)
1064 def email_to_from_subject (email_text
):
1065 """Returns the values of the email's To:, From: and Subject: fields
1067 Returns this information from an email.
1070 email_text: the string form of the email
1073 the email To:, From:, and Subject: fields as strings
1076 email_struct
= email
.parser
.Parser().parsestr(email_text
)
1078 email_to
= email_struct
['To']
1079 email_from
= email_struct
['From']
1080 email_subject
= email_struct
['Subject']
1082 return email_to
, email_from
, email_subject
1085 def import_lang(email_to
):
1086 """Imports appropriate language file for basic i18n support
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.
1094 email_to: the string containing the email address that the mail was
1098 the reference to the imported language module. The only variable in
1099 this file is the 'replies' dictionary.
1102 lang_module
= "lang.en"
1104 if email_to
!= None:
1106 if "edward-" + lang
in email_to
:
1107 lang_module
= "lang." + re
.sub('-', '_', lang
)
1109 return importlib
.import_module(lang_module
)
1112 def generate_encrypted_mime (plaintext
, email_to
, email_subject
, encrypt_to_key
,
1114 """This function creates the mime email reply. It can encrypt the email.
1116 If the encrypt_key is included, then the email is encrypted and signed.
1117 Otherwise it is unencrypted.
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
1125 gpgme_ctx: the gpgme context
1128 A string version of the mime message, possibly encrypted and signed.
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
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
)
1141 if (encrypt_to_key
!= None):
1143 encrypted_text
= encrypt_sign_message(plaintext_mime
.as_string(),
1146 gpg_payload
= encrypted_text
1149 signed_text
= sign_message(plaintext_mime
.as_string(), gpgme_ctx
)
1150 gpg_payload
= signed_text
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')
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')
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'
1171 message_mime
['To'] = email_to
1172 message_mime
['Subject'] = email_subject
1174 reply
= message_mime
.as_string()
1179 def email_quote_text (text
):
1180 """Quotes input text by inserting "> "s
1182 This is useful for quoting a text for the reply message. It inserts "> "
1183 strings at the beginning of lines.
1186 text: plain text to quote
1192 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
1194 return quoted_message
1197 def encrypt_sign_message (plaintext
, encrypt_to_key
, gpgme_ctx
):
1198 """Encrypts and signs plaintext
1200 This encrypts and signs a message.
1203 plaintext: text to sign and ecrypt
1204 encrypt_to_key: the key object to encrypt to
1205 gpgme_ctx: the gpgme context
1208 An encrypted and signed string of text
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()
1215 gpgme_ctx
.encrypt_sign([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
1216 plaintext_bytes
, encrypted_bytes
)
1218 encrypted_txt
= encrypted_bytes
.getvalue().decode('ascii')
1219 return encrypted_txt
1222 def sign_message (plaintext
, gpgme_ctx
):
1225 This signs a message.
1228 plaintext: text to sign
1229 gpgme_ctx: the gpgme context
1232 An armored signature as a string of text
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()
1239 gpgme_ctx
.sign(plaintext_bytes
, signed_bytes
, gpgme
.SIG_MODE_NORMAL
)
1241 signed_txt
= signed_bytes
.getvalue().decode('ascii')
1245 def error (error_msg
):
1246 """Write an error message to stdout
1248 The error message includes the program name.
1251 error_msg: the message to print
1257 An error message is printed to stdout
1260 sys
.stderr
.write(progname
+ ": " + str(error_msg
) + "\n")
1263 def debug (debug_msg
):
1264 """Writes a debug message to stdout if debug == True
1266 If the debug option is set in edward_config.py, then the passed message
1267 gets printed to stdout.
1270 debug_msg: the message to print to stdout
1276 A debug message is printed to stdout
1279 if edward_config
.debug
== True:
1284 """Sets the progname variable and complains about any arguments
1286 If there are any arguments, then edward complains and quits, because input
1296 Exits with error 1 if there are arguments, otherwise returns to the
1297 calling function, such as main().
1301 progname
= sys
.argv
[0]
1303 if len(sys
.argv
) > 1:
1304 print(progname
+ ": error, this program doesn't " \
1305 "need any arguments.", file=sys
.stderr
)
1309 if __name__
== "__main__":
1310 """Executes main if this file is not loaded interactively"""