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
45 from email
.mime
.multipart
import MIMEMultipart
46 from email
.mime
.application
import MIMEApplication
47 from email
.mime
.nonmultipart
import MIMENonMultipart
51 langs
= ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
53 """This list contains the abbreviated names of reply languages available to
57 match_types
= [('message',
58 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
60 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
62 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
64 """This list of tuples matches query names with re.search() queries used
65 to find GPG data for edward to process."""
68 class EddyMsg (object):
70 The EddyMsg class represents relevant parts of a mime message.
72 The represented message can be single-part or multi-part.
74 'multipart' is set to True if there are multiple mime parts.
76 'subparts' points to a list of mime sub-parts if it is a multi-part
77 message. Otherwise it points to an empty list.
79 'payload_bytes' contains the raw mime-decoded bytes that haven't been
80 encoded into a character set.
82 'payload_pieces' is a list of objects containing strings that when strung
83 together form the fully-decoded string representation of the mime part.
85 The 'charset' describes the character set of payload_bytes.
87 The 'filename', 'content_type' and 'description_list' come from the mime
100 description_list
= None
103 class PayloadPiece (object):
105 PayloadPiece represents a complte or sub-section of a mime part.
107 Instances of this class are often strung together within one or more arrays
108 pointed to by each instance of the EddyMsg class.
110 'piece_type' refers to a string whose value describes the content of
111 'string'. Examples include "pubkey", for public keys, and "message", for
112 encrypted data (or armored signatures until they are known to be such.) The
113 names derive from the header and footer of each of these ascii-encoded gpg
116 'string' contains some string of text, such as non-GPG text, an encrypted
117 block of text, a signature, or a public key.
119 'gpg_data' points to any instances of GPGData that have been created based
120 on the contents of 'string'.
128 class GPGData (object):
130 GPGData holds info from decryption, sig. verification, and/or pub. keys.
132 Instances of this class contain decrypted information, signature
133 fingerprints and/or fingerprints of processed and imported public keys.
135 'decrypted' is set to True if 'plainobj' was created from encrypted data.
137 'plainobj' points to any decrypted, or signed part of, a GPG signature. It
138 is intended to be an instance of the EddyMsg class.
140 'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
142 'keys' is a list of fingerprints of keys obtained in public key blocks.
152 class ReplyInfo (object):
154 ReplyInfo contains details that edward uses in generating its reply.
156 Instances of this class contain information about whether a message was
157 successfully encrypted or signed, and whether a public key was attached, or
158 retrievable, from the local GPG store. It stores the fingerprints of
159 potential encryption key candidates and the message (if any at all) to
160 quote in edward's reply.
162 'replies' points one of the dictionaries of translated replies.
164 'target_key' refers to the fingerprint of a key used to sign encrypted
165 data. This is the preferred key, if it is set, and if is available.
167 'fallback_target_key' referst to the fingerprint of a key used to sign
168 unencrypted data; alternatively it may be a public key attached to the
171 'msg_to_quote' refers to the part of a message which edward should quote in
172 his reply. This should remain as None if there was no encrypted and singed
173 part. This is to avoid making edward a service for decrypting other
174 people's messages to edward.
176 'success_decrypt' is set to True if edward could decrypt part of the
179 'failed_decrypt' is set to True if edward failed to decrypt part of the
182 'publick_key_received' is set to True if edward successfully imported a
185 'no_public_key' is set to True if edward doesn't have a key to encrypt to
186 when replying to the user.
188 'sig_success' is set to True if edward could to some extent verify the
189 signature of a signed part of the message to edward.
191 'sig_failure' is set to True if edward failed to some extent verify the
192 signature of a signed part of the message to edward.
198 fallback_target_key
= None
201 success_decrypt
= False
202 failed_decrypt
= False
203 public_key_received
= False
204 no_public_key
= False
212 This is the main function for edward, a GPG reply bot.
214 Edward responds to GPG-encrypted and signed mail, encrypting and signing
215 the response if the user's public key is, or was, included in the message.
224 Mime or plaintext email passing in through standard input. Portions of
225 the email may be encrypted. If the To: address contains the text
226 "edward-ja", then the reply will contain a reply written in the
227 Japanese language. There are other languages as well. The default
231 A reply email will be printed to standard output. The contents of the
232 reply email depends on whether the original email was encrypted or not,
233 has or doesn't have a signature, whether a public key used in the
234 original message is provided or locally stored, and the language
235 implied by the To: address in the original email.
240 gpgme_ctx
= get_gpg_context(edward_config
.gnupghome
,
241 edward_config
.sign_with_key
)
243 email_text
= sys
.stdin
.read()
244 email_struct
= parse_pgp_mime(email_text
, gpgme_ctx
)
246 email_to
, email_from
, email_subject
= email_to_from_subject(email_text
)
247 lang
= import_lang(email_to
)
249 replyinfo_obj
= ReplyInfo()
250 replyinfo_obj
.replies
= lang
.replies
252 prepare_for_reply(email_struct
, replyinfo_obj
)
253 encrypt_to_key
= get_key_from_fp(replyinfo_obj
, gpgme_ctx
)
254 reply_plaintext
= write_reply(replyinfo_obj
)
256 reply_mime
= generate_encrypted_mime(reply_plaintext
, email_from
, \
257 email_subject
, encrypt_to_key
,
263 def get_gpg_context (gnupghome
, sign_with_key_fp
):
265 This function returns the GPG context needed for encryption and signing.
267 The context is needed by other functions which use GPG functionality.
270 gnupghome: The path to "~/.gnupg/" or its alternative.
271 sign_with_key: The fingerprint of the key to sign with
274 A gpgme context to be used for GPG functions.
277 the 'armor' flag is set to True and the list of signing keys contains
278 the single specified key
281 os
.environ
['GNUPGHOME'] = gnupghome
283 gpgme_ctx
= gpgme
.Context()
284 gpgme_ctx
.armor
= True
287 sign_with_key
= gpgme_ctx
.get_key(sign_with_key_fp
)
289 error("unable to load signing key. is the gnupghome "
290 + "and signing key properly set in the edward_config.py?")
293 gpgme_ctx
.signers
= [sign_with_key
]
298 def parse_pgp_mime (email_text
, gpgme_ctx
):
299 """Parses the email for mime payloads and decrypts/verfies signatures.
301 This function creates a representation of a mime or plaintext email with
302 the EddyMsg class. It then splits each mime payload into one or more pieces
303 which may be plain text or GPG data. It then decrypts encrypted parts and
304 does some very basic signature verification on those parts.
307 email_text: an email message in string format
308 gpgme_ctx: a gpgme context
311 A message as an instance of EddyMsg
314 the returned EddyMsg instance has split, decrypted, verified and pubkey
318 email_struct
= email
.parser
.Parser().parsestr(email_text
)
320 eddymsg_obj
= parse_mime(email_struct
)
321 split_payloads(eddymsg_obj
)
322 gpg_on_payloads(eddymsg_obj
, gpgme_ctx
)
327 def parse_mime(msg_struct
):
328 """Translates python's email.parser format into an EddyMsg format
330 If the message is multi-part, then a recursive object is created, where
331 each sub-part is also a EddyMsg instance.
334 msg_struct: an email parsed with email.parser.Parser(), which can be
338 an instance of EddyMsg, potentially a recursive one.
341 eddymsg_obj
= EddyMsg()
343 if msg_struct
.is_multipart() == True:
344 payloads
= msg_struct
.get_payload()
346 eddymsg_obj
.multipart
= True
347 eddymsg_obj
.subparts
= list(map(parse_mime
, payloads
))
350 eddymsg_obj
= get_subpart_data(msg_struct
)
355 def scan_and_split (payload_piece
, match_type
, pattern
):
356 """This splits the payloads of an EddyMsg object into GPG and text parts.
358 An EddyMsg object's payload_pieces starts off as a list containing a single
359 PayloadPiece object. This function returns a list of these objects which
360 have been split into GPG data and regular text, if such splits need to be/
364 payload_piece: a single payload or a split part of a payload
365 match_type: the type of data to try to spit out from the payload piece
366 pattern: the search pattern to be used for finding that type of data
369 a list of objects of the PayloadPiece class, in the order that the
370 string part of payload_piece originally was, broken up according to
371 matches specified by 'pattern'.
374 # don't try to re-split pieces containing gpg data
375 if payload_piece
.piece_type
!= "text":
376 return [payload_piece
]
378 flags
= re
.DOTALL | re
.MULTILINE
379 matches
= re
.search("(?P<beginning>.*?)(?P<match>" + pattern
+
380 ")(?P<rest>.*)", payload_piece
.string
, flags
=flags
)
383 pieces
= [payload_piece
]
387 beginning
= PayloadPiece()
388 beginning
.string
= matches
.group('beginning')
389 beginning
.piece_type
= payload_piece
.piece_type
391 match
= PayloadPiece()
392 match
.string
= matches
.group('match')
393 match
.piece_type
= match_type
395 rest
= PayloadPiece()
396 rest
.string
= matches
.group('rest')
397 rest
.piece_type
= payload_piece
.piece_type
399 more_pieces
= scan_and_split(rest
, match_type
, pattern
)
400 pieces
= [beginning
, match
] + more_pieces
405 def get_subpart_data (part
):
406 """This function grabs information from a single part mime object.
408 It copies needed data from a single part email.parser.Parser() object over
409 to an EddyMsg object.
412 part: a non-multi-part mime.parser.Parser() object
415 a single-part EddyMsg() object
420 obj
.charset
= part
.get_content_charset()
421 obj
.payload_bytes
= part
.get_payload(decode
=True)
423 obj
.filename
= part
.get_filename()
424 obj
.content_type
= part
.get_content_type()
425 obj
.description_list
= part
['content-description']
427 # your guess is as good as a-myy-ee-ine...
428 if obj
.charset
== None:
429 obj
.charset
= 'utf-8'
431 if obj
.payload_bytes
!= None:
433 payload
= PayloadPiece()
434 payload
.string
= obj
.payload_bytes
.decode(obj
.charset
)
435 payload
.piece_type
= 'text'
437 obj
.payload_pieces
= [payload
]
438 except UnicodeDecodeError:
444 def do_to_eddys_pieces (function_to_do
, eddymsg_obj
, data
):
445 """A function which maps another function onto a message's subparts.
447 This is a higer-order function which recursively performs a specified
448 function on each subpart of a multi-part message. Each single-part sub-part
449 has the function applied to it. This function also works if the part passed
453 function_to_do: function to perform on sub-parts
454 eddymsg_obj: a single part or multi-part EddyMsg object
455 data: a second argument to pass to 'function_to_do'
461 The passed-in EddyMsg object is transformed recursively on its
462 sub-parts according to 'function_to_do'.
465 if eddymsg_obj
.multipart
== True:
466 for sub
in eddymsg_obj
.subparts
:
467 do_to_eddys_pieces(function_to_do
, sub
, data
)
469 function_to_do(eddymsg_obj
, data
)
472 def split_payloads (eddymsg_obj
):
473 """Splits all (sub-)payloads of a message into GPG data and regular text.
475 Recursively performs payload splitting on all sub-parts of an EddyMsg
476 object into the various GPG data types, such as GPG messages, public key
477 blocks and signed text.
480 eddymsg_obj: an instance of EddyMsg
486 The EddyMsg object has payloads that are unsplit (by may be split)..
489 The EddyMsg object's payloads are all split into GPG and non-GPG parts.
492 for match_type
in match_types
:
493 do_to_eddys_pieces(split_payload_pieces
, eddymsg_obj
, match_type
)
496 def split_payload_pieces (eddymsg_obj
, match_type
):
497 """A helper function for split_payloads(); works on PayloadPiece objects.
499 This function splits up PayloadPiece objects into multipe PayloadPiece
500 objects and replaces the EddyMsg object's previous list of payload pieces
501 with the new split up one.
504 eddymsg_obj: a single-part EddyMsg object.
505 match_type: a tuple from the match_types list, which specifies a match
506 name and a match pattern.
512 The payload piece(s) of an EddyMsg object may be already split or
516 The EddyMsg object's payload piece(s) are split into a list of pieces
517 if matches of the match_type are found.
520 (match_name
, pattern
) = match_type
523 for piece
in eddymsg_obj
.payload_pieces
:
524 new_pieces_list
+= scan_and_split(piece
, match_name
, pattern
)
526 eddymsg_obj
.payload_pieces
= new_pieces_list
529 def gpg_on_payloads (eddymsg_obj
, gpgme_ctx
, prev_parts
=[]):
530 """Performs GPG operations on the GPG parts of the message
532 This function decrypts text, verifies signatures, and imports public keys
533 included in an email.
536 eddymsg_obj: an EddyMsg object with its payload_pieces split into GPG
537 and non-GPG sections by split_payloads()
538 gpgme_ctx: a gpgme context
540 prev_parts: a list of mime parts that occur before the eddymsg_obj
541 part, under the same multi-part mime part. This is used for
542 verifying detached signatures. For the root mime part, this should
543 be an empty list, which is the default value if this paramater is
550 eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
553 Decryption, verification and key imports occur. the gpg_data member of
554 PayloadPiece objects get filled in with GPGData objects.
557 if eddymsg_obj
.multipart
== True:
559 for sub
in eddymsg_obj
.subparts
:
560 gpg_on_payloads (sub
, gpgme_ctx
, prev_parts
)
565 for piece
in eddymsg_obj
.payload_pieces
:
567 if piece
.piece_type
== "text":
568 # don't transform the plaintext.
571 elif piece
.piece_type
== "message":
572 (plaintext
, sigs
) = decrypt_block(piece
.string
, gpgme_ctx
)
575 piece
.gpg_data
= GPGData()
576 piece
.gpg_data
.decrypted
= True
577 piece
.gpg_data
.sigs
= sigs
579 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
582 # if not encrypted, check to see if this is an armored signature.
583 (plaintext
, sigs
) = verify_sig_message(piece
.string
, gpgme_ctx
)
586 piece
.piece_type
= "signature"
587 piece
.gpg_data
= GPGData()
588 piece
.gpg_data
.sigs
= sigs
590 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext
, gpgme_ctx
)
592 # FIXME: handle pubkeys first, so that signatures can be validated
593 # on freshly imported keys
594 elif piece
.piece_type
== "pubkey":
595 key_fps
= add_gpg_key(piece
.string
, gpgme_ctx
)
598 piece
.gpg_data
= GPGData()
599 piece
.gpg_data
.keys
= key_fps
601 elif piece
.piece_type
== "detachedsig":
602 for prev
in prev_parts
:
603 payload_bytes
= prev
.payload_bytes
604 sig_fps
= verify_detached_signature(piece
.string
, payload_bytes
, gpgme_ctx
)
607 piece
.gpg_data
= GPGData()
608 piece
.gpg_data
.sigs
= sig_fps
609 piece
.gpg_data
.plainobj
= prev
616 def prepare_for_reply (eddymsg_obj
, replyinfo_obj
):
617 """Updates replyinfo_obj with info on the message's GPG success/failures
619 This function marks replyinfo_obj with information about whether encrypted
620 text in eddymsg_obj was successfully decrypted, signatures were verified
621 and whether a public key was found or not.
624 eddymsg_obj: a message in the EddyMsg format
625 replyinfo_obj: an instance of ReplyInfo
631 eddymsg_obj has had its gpg_data created by gpg_on_payloads
634 replyinfo_obj has been updated with info about decryption/sig
635 verififcation status, etc. However the desired key isn't imported until
636 later, so the success or failure of that updates the values set here.
639 do_to_eddys_pieces(prepare_for_reply_pieces
, eddymsg_obj
, replyinfo_obj
)
641 def prepare_for_reply_pieces (eddymsg_obj
, replyinfo_obj
):
642 """A helper function for prepare_for_reply
644 It updates replyinfo_obj with GPG success/failure information, when
645 supplied a single-part EddyMsg object.
648 eddymsg_obj: a single-part message in the EddyMsg format
649 replyinfo_obj: an object which holds information about the message's
656 eddymsg_obj is a single-part message. (it may be a part of a multi-part
657 message.) It has had its gpg_data created by gpg_on_payloads if it has
661 replyinfo_obj has been updated with gpg success/failure information
664 for piece
in eddymsg_obj
.payload_pieces
:
665 if piece
.piece_type
== "text":
666 # don't quote the plaintext part.
669 elif piece
.piece_type
== "message":
670 prepare_for_reply_message(piece
, replyinfo_obj
)
672 elif piece
.piece_type
== "pubkey":
673 prepare_for_reply_pubkey(piece
, replyinfo_obj
)
675 elif (piece
.piece_type
== "detachedsig") \
676 or (piece
.piece_type
== "signature"):
677 prepare_for_reply_sig(piece
, replyinfo_obj
)
680 def prepare_for_reply_message (piece
, replyinfo_obj
):
681 """Helper function for prepare_for_reply()
683 This function is called when the piece_type of a payload piece is
684 "message", or GPG Message block. This should be encrypted text. If the
685 encryted block is signed, a sig will be attached to .target_key unless
686 there is already one there.
689 piece: a PayloadPiece object.
690 replyinfo_obj: object which gets updated with decryption status, etc.
697 the piece.payload_piece value should be "message".
700 replyinfo_obj gets updated with decryption status, signing status and a
701 potential signing key.
704 if piece
.gpg_data
== None:
705 replyinfo_obj
.failed_decrypt
= True
708 replyinfo_obj
.success_decrypt
= True
710 # we already have a key (and a message)
711 if replyinfo_obj
.target_key
!= None:
714 if piece
.gpg_data
.sigs
!= []:
715 replyinfo_obj
.target_key
= piece
.gpg_data
.sigs
[0]
716 get_signed_part
= False
718 # only include a signed message in the reply.
719 get_signed_part
= True
721 flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, replyinfo_obj
, get_signed_part
)
723 # to catch public keys in encrypted blocks
724 prepare_for_reply(piece
.gpg_data
.plainobj
, replyinfo_obj
)
727 def prepare_for_reply_pubkey (piece
, replyinfo_obj
):
728 """Helper function for prepare_for_reply(). Marks pubkey import status.
730 Marks replyinfo_obj with pub key import status.
733 piece: a PayloadPiece object
734 replyinfo_obj: a ReplyInfo object
737 piece.piece_type should be set to "pubkey".
740 replyinfo_obj has its fields updated.
743 if piece
.gpg_data
== None or piece
.gpg_data
.keys
== []:
744 replyinfo_obj
.no_public_key
= True
746 replyinfo_obj
.public_key_received
= True
748 if replyinfo_obj
.fallback_target_key
== None:
749 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.keys
[0]
752 def prepare_for_reply_sig (piece
, replyinfo_obj
):
753 """Helper function for prepare_for_reply(). Marks sig verification status.
755 Marks replyinfo_obj with signature verification status.
758 piece: a PayloadPiece object
759 replyinfo_obj: a ReplyInfo object
762 piece.piece_type should be set to "signature", or "detachedsig".
765 replyinfo_obj has its fields updated.
768 if piece
.gpg_data
== None or piece
.gpg_data
.sigs
== []:
769 replyinfo_obj
.sig_failure
= True
771 replyinfo_obj
.sig_success
= True
773 if replyinfo_obj
.fallback_target_key
== None:
774 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.sigs
[0]
777 def flatten_decrypted_payloads (eddymsg_obj
, replyinfo_obj
, get_signed_part
):
778 """For creating a string representation of a signed, encrypted part.
780 When given a decrypted payload, it will add either the plaintext or signed
781 plaintext to the reply message, depeding on 'get_signed_part'. This is
782 useful for ensuring that the reply message only comes from a signed and
783 ecrypted GPG message. It also sets the target_key for encrypting the reply
784 if it's told to get signed text only.
787 eddymsg_obj: the message in EddyMsg format created by decrypting GPG
789 replyinfo_obj: a ReplyInfo object for holding the message to quote and
790 the target_key to encrypt to.
791 get_signed_part: True if we should only include text that contains a
792 further signature. If False, then include plain text.
798 The EddyMsg instance passed in should be a piece.gpg_data.plainobj
799 which represents decrypted text. It may or may not be signed on that
803 the ReplyInfo instance may have a new 'target_key' set and its
804 'msg_to_quote' will be updated with (possibly signed) plaintext, if any
808 if eddymsg_obj
== None:
811 # recurse on multi-part mime
812 if eddymsg_obj
.multipart
== True:
813 for sub
in eddymsg_obj
.subparts
:
814 flatten_decrypted_payloads(sub
, replyinfo_obj
, get_signed_part
)
816 for piece
in eddymsg_obj
.payload_pieces
:
817 if (get_signed_part
):
818 if ((piece
.piece_type
== "detachedsig") \
819 or (piece
.piece_type
== "signature")) \
820 and (piece
.gpg_data
!= None):
821 flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, replyinfo_obj
, False)
822 replyinfo_obj
.target_key
= piece
.gpg_data
.sigs
[0]
825 if piece
.piece_type
== "text":
826 replyinfo_obj
.msg_to_quote
+= piece
.string
829 def get_key_from_fp (replyinfo_obj
, gpgme_ctx
):
830 """Obtains a public key object from a key fingerprint
832 If the .target_key is not set, then we use .fallback_target_key.
835 replyinfo_obj: ReplyInfo instance
836 gpgme_ctx: the gpgme context
839 The key object of the key of either the target_key or the fallback one
840 if .target_key is not set. If the key cannot be loaded, then return
844 Loading a key requires that we have the public key imported. This
845 requires that they email contains the pub key block, or that it was
846 previously sent to edward.
849 If the key cannot be loaded, then the replyinfo_obj is marked for
850 having no public key available.
853 if replyinfo_obj
.target_key
== None:
854 replyinfo_obj
.target_key
= replyinfo_obj
.fallback_target_key
856 if replyinfo_obj
.target_key
!= None:
858 encrypt_to_key
= gpgme_ctx
.get_key(replyinfo_obj
.target_key
)
859 return encrypt_to_key
864 # no available key to use
865 replyinfo_obj
.target_key
= None
866 replyinfo_obj
.fallback_target_key
= None
868 replyinfo_obj
.no_public_key
= True
869 replyinfo_obj
.public_key_received
= False
874 def write_reply (replyinfo_obj
):
875 """Write the reply email body about the GPG successes/failures.
877 The reply is about whether decryption, sig verification and key
878 import/loading was successful or failed. If text was successfully decrypted
879 and verified, then the first instance of such text will be included in
883 replyinfo_obj: contains details of GPG processing status
886 the plaintext message to be sent to the user
889 replyinfo_obj should be populated with info about GPG processing status.
894 if replyinfo_obj
.success_decrypt
== True:
895 reply_plain
+= replyinfo_obj
.replies
['success_decrypt']
897 if replyinfo_obj
.no_public_key
== False:
898 quoted_text
= email_quote_text(replyinfo_obj
.msg_to_quote
)
899 reply_plain
+= quoted_text
901 elif replyinfo_obj
.failed_decrypt
== True:
902 reply_plain
+= replyinfo_obj
.replies
['failed_decrypt']
905 if replyinfo_obj
.sig_success
== True:
906 reply_plain
+= "\n\n"
907 reply_plain
+= replyinfo_obj
.replies
['sig_success']
909 elif replyinfo_obj
.sig_failure
== True:
910 reply_plain
+= "\n\n"
911 reply_plain
+= replyinfo_obj
.replies
['sig_failure']
914 if replyinfo_obj
.public_key_received
== True:
915 reply_plain
+= "\n\n"
916 reply_plain
+= replyinfo_obj
.replies
['public_key_received']
918 elif replyinfo_obj
.no_public_key
== True:
919 reply_plain
+= "\n\n"
920 reply_plain
+= replyinfo_obj
.replies
['no_public_key']
923 reply_plain
+= "\n\n"
924 reply_plain
+= replyinfo_obj
.replies
['signature']
929 def add_gpg_key (key_block
, gpgme_ctx
):
930 """Adds a GPG pubkey to the local keystore
932 This adds keys received through email into the key store so they can be
936 key_block: the string form of the ascii-armored public key block
937 gpgme_ctx: the gpgme context
940 the fingerprint(s) of the imported key(s)
943 fp
= io
.BytesIO(key_block
.encode('ascii'))
945 result
= gpgme_ctx
.import_(fp
)
946 imports
= result
.imports
948 key_fingerprints
= []
951 for import_
in imports
:
952 fingerprint
= import_
[0]
953 key_fingerprints
+= [fingerprint
]
955 debug("added gpg key: " + fingerprint
)
957 return key_fingerprints
960 def verify_sig_message (msg_block
, gpgme_ctx
):
961 """Verifies the signature of a signed, ascii-armored block of text.
963 It encodes the string into ascii, since binary GPG files are currently
964 unsupported, and alternative, the ascii-armored format is encodable into
968 msg_block: a GPG Message block in string form. It may be encrypted or
969 not. If it is encrypted, it will return empty results.
970 gpgme_ctx: the gpgme context
973 A tuple of the plaintext of the signed part and the list of
974 fingerprints of keys signing the data. If verification failed, perhaps
975 because the message was also encrypted, then empty results are
979 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
980 plain_b
= io
.BytesIO()
983 sigs
= gpgme_ctx
.verify(block_b
, None, plain_b
)
987 plaintext
= plain_b
.getvalue().decode('utf-8')
991 fingerprints
+= [sig
.fpr
]
992 return (plaintext
, fingerprints
)
995 def verify_detached_signature (detached_sig
, plaintext_bytes
, gpgme_ctx
):
996 """Verifies the signature of a detached signature.
998 This requires the signature part and the signed part as separate arguments.
1001 detached_sig: the signature part of the detached signature
1002 plaintext_bytes: the byte form of the message being signed.
1003 gpgme_ctx: the gpgme context
1006 A list of signing fingerprints if the signature verification was
1007 sucessful. Otherwise, an empty list is returned.
1010 detached_sig_fp
= io
.BytesIO(detached_sig
.encode('ascii'))
1011 plaintext_fp
= io
.BytesIO(plaintext_bytes
)
1012 ptxt_fp
= io
.BytesIO()
1014 result
= gpgme_ctx
.verify(detached_sig_fp
, plaintext_fp
, None)
1016 sig_fingerprints
= []
1018 sig_fingerprints
+= [res_
.fpr
]
1020 return sig_fingerprints
1023 def decrypt_block (msg_block
, gpgme_ctx
):
1024 """Decrypts a block of GPG text, and verifies any included sigatures.
1026 Some encypted messages have embeded signatures, so those are verified too.
1029 msg_block: the encrypted(/signed) text
1030 gpgme_ctx: the gpgme context
1033 A tuple of plaintext and signatures, if the decryption and signature
1034 verification were successful, respectively.
1037 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
1038 plain_b
= io
.BytesIO()
1041 sigs
= gpgme_ctx
.decrypt_verify(block_b
, plain_b
)
1045 plaintext
= plain_b
.getvalue().decode('utf-8')
1049 fingerprints
+= [sig
.fpr
]
1050 return (plaintext
, fingerprints
)
1053 def email_to_from_subject (email_text
):
1054 """Returns the values of the email's To:, From: and Subject: fields
1056 Returns this information from an email.
1059 email_text: the string form of the email
1062 the email To:, From:, and Subject: fields as strings
1065 email_struct
= email
.parser
.Parser().parsestr(email_text
)
1067 email_to
= email_struct
['To']
1068 email_from
= email_struct
['From']
1069 email_subject
= email_struct
['Subject']
1071 return email_to
, email_from
, email_subject
1074 def import_lang(email_to
):
1075 """Imports appropriate language file for basic i18n support
1077 The language imported depends on the To: address of the email received by
1078 edward. an -en ending implies the English language, whereas a -ja ending
1079 implies Japanese. The list of supported languages is listed in the 'langs'
1080 list at the beginning of the program.
1083 email_to: the string containing the email address that the mail was
1087 the reference to the imported language module. The only variable in
1088 this file is the 'replies' dictionary.
1091 if email_to
!= None:
1093 if "edward-" + lang
in email_to
:
1094 lang
= "lang." + re
.sub('-', '_', lang
)
1095 language
= importlib
.import_module(lang
)
1099 return importlib
.import_module("lang.en")
1102 def generate_encrypted_mime (plaintext
, email_to
, email_subject
, encrypt_to_key
,
1104 """This function creates the mime email reply. It can encrypt the email.
1106 If the encrypt_key is included, then the email is encrypted and signed.
1107 Otherwise it is unencrypted.
1110 plaintext: the plaintext body of the message to create.
1111 email_to: the email address to reply to
1112 email_subject: the subject to use in reply
1113 encrypt_to_key: the key object to use for encrypting the email. (or
1115 gpgme_ctx: the gpgme context
1118 A string version of the mime message, possibly encrypted and signed.
1121 # quoted printable encoding lets most ascii characters look normal
1122 # before the mime message is decoded.
1123 char_set
= email
.charset
.Charset("utf-8")
1124 char_set
.body_encoding
= email
.charset
.QP
1126 # MIMEText doesn't allow setting the text encoding
1127 # so we use MIMENonMultipart.
1128 plaintext_mime
= MIMENonMultipart('text', 'plain')
1129 plaintext_mime
.set_payload(plaintext
, charset
=char_set
)
1131 if (encrypt_to_key
!= None):
1133 encrypted_text
= encrypt_sign_message(plaintext_mime
.as_string(),
1136 gpg_payload
= encrypted_text
1139 signed_text
= sign_message(plaintext_mime
.as_string(), gpgme_ctx
)
1140 gpg_payload
= signed_text
1142 control_mime
= MIMEApplication("Version: 1",
1143 _subtype
='pgp-encrypted',
1144 _encoder
=email
.encoders
.encode_7or8bit
)
1145 control_mime
['Content-Description'] = 'PGP/MIME version identification'
1146 control_mime
.set_charset('us-ascii')
1148 encoded_mime
= MIMEApplication(gpg_payload
,
1149 _subtype
='octet-stream; name="encrypted.asc"',
1150 _encoder
=email
.encoders
.encode_7or8bit
)
1151 encoded_mime
['Content-Description'] = 'OpenPGP encrypted message'
1152 encoded_mime
['Content-Disposition'] = 'inline; filename="encrypted.asc"'
1153 encoded_mime
.set_charset('us-ascii')
1155 message_mime
= MIMEMultipart(_subtype
="encrypted", protocol
="application/pgp-encrypted")
1156 message_mime
.attach(control_mime
)
1157 message_mime
.attach(encoded_mime
)
1158 message_mime
['Content-Disposition'] = 'inline'
1161 message_mime
['To'] = email_to
1162 message_mime
['Subject'] = email_subject
1164 reply
= message_mime
.as_string()
1169 def email_quote_text (text
):
1170 """Quotes input text by inserting "> "s
1172 This is useful for quoting a text for the reply message. It inserts "> "
1173 strings at the beginning of lines.
1176 text: plain text to quote
1182 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
1184 return quoted_message
1187 def encrypt_sign_message (plaintext
, encrypt_to_key
, gpgme_ctx
):
1188 """Encrypts and signs plaintext
1190 This encrypts and signs a message.
1193 plaintext: text to sign and ecrypt
1194 encrypt_to_key: the key object to encrypt to
1195 gpgme_ctx: the gpgme context
1198 An encrypted and signed string of text
1201 # the plaintext should be mime encoded in an ascii-compatible form
1202 plaintext_bytes
= io
.BytesIO(plaintext
.encode('ascii'))
1203 encrypted_bytes
= io
.BytesIO()
1205 gpgme_ctx
.encrypt_sign([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
1206 plaintext_bytes
, encrypted_bytes
)
1208 encrypted_txt
= encrypted_bytes
.getvalue().decode('ascii')
1209 return encrypted_txt
1212 def sign_message (plaintext
, gpgme_ctx
):
1215 This signs a message.
1218 plaintext: text to sign
1219 gpgme_ctx: the gpgme context
1222 An armored signature as a string of text
1225 # the plaintext should be mime encoded in an ascii-compatible form
1226 plaintext_bytes
= io
.BytesIO(plaintext
.encode('ascii'))
1227 signed_bytes
= io
.BytesIO()
1229 gpgme_ctx
.sign(plaintext_bytes
, signed_bytes
, gpgme
.SIG_MODE_NORMAL
)
1231 signed_txt
= signed_bytes
.getvalue().decode('ascii')
1235 def error (error_msg
):
1236 """Write an error message to stdout
1238 The error message includes the program name.
1241 error_msg: the message to print
1247 An error message is printed to stdout
1250 sys
.stderr
.write(progname
+ ": " + str(error_msg
) + "\n")
1253 def debug (debug_msg
):
1254 """Writes a debug message to stdout if debug == True
1256 If the debug option is set in edward_config.py, then the passed message
1257 gets printed to stdout.
1260 debug_msg: the message to print to stdout
1266 A debug message is printed to stdout
1269 if edward_config
.debug
== True:
1274 """Sets the progname variable and complains about any arguments
1276 If there are any arguments, then edward complains and quits, because input
1286 Exits with error 1 if there are arguments, otherwise returns to the
1287 calling function, such as main().
1291 progname
= sys
.argv
[0]
1293 if len(sys
.argv
) > 1:
1294 print(progname
+ ": error, this program doesn't " \
1295 "need any arguments.", file=sys
.stderr
)
1299 if __name__
== "__main__":
1300 """Executes main if this file is not loaded interactively"""