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/PREVIOUS-20150530/edward.tgz
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
47 from email
.mime
.text
import MIMEText
48 from email
.mime
.multipart
import MIMEMultipart
49 from email
.mime
.application
import MIMEApplication
50 from email
.mime
.nonmultipart
import MIMENonMultipart
54 langs
= ["de", "el", "en", "es", "fr", "it", "ja", "pt-br", "ro", "ru", "tr"]
56 """This list contains the abbreviated names of reply languages available to
59 class TxtType (enum
.Enum
):
67 match_pairs
= [(TxtType
.message
,
68 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
70 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
72 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
74 """This list of tuples matches query names with re.search() queries used
75 to find GPG data for edward to process."""
78 class EddyMsg (object):
80 The EddyMsg class represents relevant parts of a mime message.
82 The represented message can be single-part or multi-part.
84 'multipart' is set to True if there are multiple mime parts.
86 'subparts' points to a list of mime sub-parts if it is a multi-part
87 message. Otherwise it points to an empty list.
89 'payload_bytes' is a binary representation of the mime part before header
90 removal and message decoding.
92 'payload_pieces' is a list of objects containing strings that when strung
93 together form the fully-decoded string representation of the mime part.
95 The 'filename', 'content_type' and 'description_list' come from the mime
107 description_list
= None
110 class PayloadPiece (object):
112 PayloadPiece represents a complte or sub-section of a mime part.
114 Instances of this class are often strung together within one or more arrays
115 pointed to by each instance of the EddyMsg class.
117 'piece_type' refers to an enum whose value describes the content of
118 'string'. Examples include TxtType.pubkey, for public keys, and
119 TxtType.message, for encrypted data (or armored signatures until they are
120 known to be such.) Some of the names derive from the header and footer of
121 each of these ascii-encoded gpg blocks.
123 'string' contains some string of text, such as non-GPG text, an encrypted
124 block of text, a signature, or a public key.
126 'gpg_data' points to any instances of GPGData that have been created based
127 on the contents of 'string'.
135 class GPGData (object):
137 GPGData holds info from decryption, sig. verification, and/or pub. keys.
139 Instances of this class contain decrypted information, signature
140 fingerprints and/or fingerprints of processed and imported public keys.
142 'decrypted' is set to True if 'plainobj' was created from encrypted data.
144 'plainobj' points to any decrypted, or signed part of, a GPG signature. It
145 is intended to be an instance of the EddyMsg class.
147 'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
149 'sigkey_missing' is set to True if edward doesn't have the key it needs to
150 verify the signature on a block of text.
152 'key_cannot_encrypt' is set to True if pubkeys or sigs' keys in the payload
153 piece are not capable of encryption, are revoked or expired, for instance.
155 'keys' is a list of fingerprints of keys obtained in public key blocks.
162 sigkey_missing
= False
163 key_cannot_encrypt
= False
167 class ReplyInfo (object):
169 ReplyInfo contains details that edward uses in generating its reply.
171 Instances of this class contain information about whether a message was
172 successfully encrypted or signed, and whether a public key was attached, or
173 retrievable, from the local GPG store. It stores the fingerprints of
174 potential encryption key candidates and the message (if any at all) to
175 quote in edward's reply.
177 'replies' points one of the dictionaries of translated replies.
179 'target_key' refers to the fingerprint of a key used to sign encrypted
180 data. This is the preferred key, if it is set, and if is available.
182 'fallback_target_key' referst to the fingerprint of a key used to sign
183 unencrypted data; alternatively it may be a public key attached to the
186 'encrypt_to_key' the key object to use when encrypting edward's reply
188 'msg_to_quote' refers to the part of a message which edward should quote in
189 his reply. This should remain as None if there was no encrypted and singed
190 part. This is to avoid making edward a service for decrypting other
191 people's messages to edward.
193 'decrypt_success' is set to True if edward could decrypt part of the
196 'sig_success' is set to True if edward could to some extent verify the
197 signature of a signed part of the message to edward.
199 'key_can_encrypt' is set to True if a key which can be encrypted to has
202 'sig_failure' is set to True if edward could not verify a siganture.
204 'pubkey_success' is set to True if edward successfully imported a public
207 'sigkey_missing' is set to True if edward doesn't have the public key
208 needed for signature verification.
210 'key_cannot_encrypt' is set to True if pubkeys or sig's keys in a payload
211 piece of the message are not capable of encryption.
213 'have_repy_key' is set to True if edward has a public key to encrypt its
220 fallback_target_key
= None
221 encrypt_to_key
= None
224 decrypt_success
= False
226 pubkey_success
= False
227 key_can_encrypt
= False
229 decrypt_failure
= False
231 sigkey_missing
= False
232 key_cannot_encrypt
= False
234 have_reply_key
= False
240 This is the main function for edward, a GPG reply bot.
242 Edward responds to GPG-encrypted and signed mail, encrypting and signing
243 the response if the user's public key is, or was, included in the message.
252 Mime or plaintext email passing in through standard input. Portions of
253 the email may be encrypted. If the To: address contains the text
254 "edward-ja", then the reply will contain a reply written in the
255 Japanese language. There are other languages as well. The default
259 A reply email will be printed to standard output. The contents of the
260 reply email depends on whether the original email was encrypted or not,
261 has or doesn't have a signature, whether a public key used in the
262 original message is provided or locally stored, and the language
263 implied by the To: address in the original email.
266 print_reply_only
= handle_args()
268 gpgme_ctx
= get_gpg_context(edward_config
.gnupghome
,
269 edward_config
.sign_with_key
)
271 email_bytes
= sys
.stdin
.buffer.read()
272 email_struct
= parse_pgp_mime(email_bytes
, gpgme_ctx
)
274 email_to
, email_from
, email_subject
= email_to_from_subject(email_bytes
)
275 lang
, reply_from
= import_lang_pick_address(email_to
, edward_config
.hostname
)
277 replyinfo_obj
= ReplyInfo()
278 replyinfo_obj
.replies
= lang
.replies
280 prepare_for_reply(email_struct
, replyinfo_obj
)
281 get_key_from_fp(replyinfo_obj
, gpgme_ctx
)
282 reply_plaintext
= write_reply(replyinfo_obj
)
284 reply_mime
= generate_encrypted_mime(reply_plaintext
, email_from
, \
285 email_subject
, replyinfo_obj
.encrypt_to_key
,
288 if print_reply_only
== True:
291 send_reply(reply_mime
, email_subject
, email_from
, reply_from
)
294 def get_gpg_context (gnupghome
, sign_with_key_fp
):
296 This function returns the GPG context needed for encryption and signing.
298 The context is needed by other functions which use GPG functionality.
301 gnupghome: The path to "~/.gnupg/" or its alternative.
302 sign_with_key: The fingerprint of the key to sign with
305 A gpgme context to be used for GPG functions.
308 the 'armor' flag is set to True and the list of signing keys contains
309 the single specified key
312 os
.environ
['GNUPGHOME'] = gnupghome
314 gpgme_ctx
= gpgme
.Context()
315 gpgme_ctx
.armor
= True
318 sign_with_key
= gpgme_ctx
.get_key(sign_with_key_fp
)
319 except gpgme
.GpgmeError
:
320 error("unable to load signing key. is the gnupghome "
321 + "and signing key properly set in the edward_config.py?")
324 gpgme_ctx
.signers
= [sign_with_key
]
329 def parse_pgp_mime (email_bytes
, gpgme_ctx
):
330 """Parses the email for mime payloads and decrypts/verfies signatures.
332 This function creates a representation of a mime or plaintext email with
333 the EddyMsg class. It then splits each mime payload into one or more pieces
334 which may be plain text or GPG data. It then decrypts encrypted parts and
335 does some very basic signature verification on those parts.
338 email_bytes: an email message in byte string format
339 gpgme_ctx: a gpgme context
342 A message as an instance of EddyMsg
345 the returned EddyMsg instance has split, decrypted, verified and pubkey
349 email_struct
= email
.parser
.BytesParser().parsebytes(email_bytes
)
351 eddymsg_obj
= parse_mime(email_struct
)
352 split_payloads(eddymsg_obj
)
353 gpg_on_payloads(eddymsg_obj
, gpgme_ctx
)
358 def parse_mime(msg_struct
):
359 """Translates python's email.parser format into an EddyMsg format
361 If the message is multi-part, then a recursive object is created, where
362 each sub-part is also a EddyMsg instance.
365 msg_struct: an email parsed with email.parser.Parser(), which can be
369 an instance of EddyMsg, potentially a recursive one.
372 eddymsg_obj
= get_subpart_data(msg_struct
)
374 if msg_struct
.is_multipart() == True:
375 payloads
= msg_struct
.get_payload()
377 eddymsg_obj
.multipart
= True
378 eddymsg_obj
.subparts
= list(map(parse_mime
, payloads
))
383 def scan_and_split (payload_piece
, match_name
, pattern
):
384 """This splits the payloads of an EddyMsg object into GPG and text parts.
386 An EddyMsg object's payload_pieces starts off as a list containing a single
387 PayloadPiece object. This function returns a list of these objects which
388 have been split into GPG data and regular text, if such splits need to be/
392 payload_piece: a single payload or a split part of a payload
393 match_name: the type of data to try to spit out from the payload piece
394 pattern: the search pattern to be used for finding that type of data
397 a list of objects of the PayloadPiece class, in the order that the
398 string part of payload_piece originally was, broken up according to
399 matches specified by 'pattern'.
402 # don't try to re-split pieces containing gpg data
403 if payload_piece
.piece_type
!= TxtType
.text
:
404 return [payload_piece
]
406 flags
= re
.DOTALL | re
.MULTILINE
407 matches
= re
.search("(?P<beginning>.*?)(?P<match>" + pattern
+
408 ")(?P<rest>.*)", payload_piece
.string
, flags
=flags
)
411 pieces
= [payload_piece
]
415 beginning
= PayloadPiece()
416 beginning
.string
= matches
.group('beginning')
417 beginning
.piece_type
= payload_piece
.piece_type
419 match
= PayloadPiece()
420 match
.string
= matches
.group('match')
421 match
.piece_type
= match_name
423 rest
= PayloadPiece()
424 rest
.string
= matches
.group('rest')
425 rest
.piece_type
= payload_piece
.piece_type
427 more_pieces
= scan_and_split(rest
, match_name
, pattern
)
428 pieces
= [beginning
, match
] + more_pieces
433 def get_subpart_data (part
):
434 """This function grabs information from a single part mime object.
436 It copies needed data from a single part email.parser.Parser() object over
437 to an EddyMsg object.
440 part: a non-multi-part mime.parser.Parser() object
443 a single-part EddyMsg() object
448 mime_decoded_bytes
= part
.get_payload(decode
=True)
449 charset
= part
.get_content_charset()
451 # your guess is as good as a-myy-ee-ine...
455 payload_string
= part
.as_string()
456 if payload_string
!= None:
457 obj
.payload_bytes
= payload_string
.encode(charset
)
459 obj
.filename
= part
.get_filename()
460 obj
.content_type
= part
.get_content_type()
461 obj
.description_list
= part
['content-description']
463 if mime_decoded_bytes
!= None:
465 payload
= PayloadPiece()
466 payload
.string
= mime_decoded_bytes
.decode(charset
)
467 payload
.piece_type
= TxtType
.text
469 obj
.payload_pieces
= [payload
]
470 except UnicodeDecodeError:
476 def do_to_eddys_pieces (function_to_do
, eddymsg_obj
, data
):
477 """A function which maps another function onto a message's subparts.
479 This is a higer-order function which recursively performs a specified
480 function on each subpart of a multi-part message. Each single-part sub-part
481 has the function applied to it. This function also works if the part passed
485 function_to_do: function to perform on sub-parts
486 eddymsg_obj: a single part or multi-part EddyMsg object
487 data: a second argument to pass to 'function_to_do'
493 The passed-in EddyMsg object is transformed recursively on its
494 sub-parts according to 'function_to_do'.
497 if eddymsg_obj
.multipart
== True:
498 for sub
in eddymsg_obj
.subparts
:
499 do_to_eddys_pieces(function_to_do
, sub
, data
)
501 function_to_do(eddymsg_obj
, data
)
504 def split_payloads (eddymsg_obj
):
505 """Splits all (sub-)payloads of a message into GPG data and regular text.
507 Recursively performs payload splitting on all sub-parts of an EddyMsg
508 object into the various GPG data types, such as GPG messages, public key
509 blocks and signed text.
512 eddymsg_obj: an instance of EddyMsg
518 The EddyMsg object has payloads that are unsplit (by may be split)..
521 The EddyMsg object's payloads are all split into GPG and non-GPG parts.
524 for match_pair
in match_pairs
:
525 do_to_eddys_pieces(split_payload_pieces
, eddymsg_obj
, match_pair
)
528 def split_payload_pieces (eddymsg_obj
, match_pair
):
529 """A helper function for split_payloads(); works on PayloadPiece objects.
531 This function splits up PayloadPiece objects into multipe PayloadPiece
532 objects and replaces the EddyMsg object's previous list of payload pieces
533 with the new split up one.
536 eddymsg_obj: a single-part EddyMsg object.
537 match_pair: a tuple from the match_pairs list, which specifies a match
538 name and a match pattern.
544 The payload piece(s) of an EddyMsg object may be already split or
548 The EddyMsg object's payload piece(s) are split into a list of pieces
549 if matches of the match_pair are found.
552 (match_name
, pattern
) = match_pair
555 for piece
in eddymsg_obj
.payload_pieces
:
556 new_pieces_list
+= scan_and_split(piece
, match_name
, pattern
)
558 eddymsg_obj
.payload_pieces
= new_pieces_list
561 def gpg_on_payloads (eddymsg_obj
, gpgme_ctx
, prev_parts
=[]):
562 """Performs GPG operations on the GPG parts of the message
564 This function decrypts text, verifies signatures, and imports public keys
565 included in an email.
568 eddymsg_obj: an EddyMsg object with its payload_pieces split into GPG
569 and non-GPG sections by split_payloads()
570 gpgme_ctx: a gpgme context
572 prev_parts: a list of mime parts that occur before the eddymsg_obj
573 part, under the same multi-part mime part. This is used for
574 verifying detached signatures. For the root mime part, this should
575 be an empty list, which is the default value if this paramater is
582 eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
585 Decryption, verification and key imports occur. the gpg_data members of
586 PayloadPiece objects get filled in with GPGData objects with some of
587 their attributes set.
590 if eddymsg_obj
.multipart
== True:
592 for sub
in eddymsg_obj
.subparts
:
593 gpg_on_payloads (sub
, gpgme_ctx
, prev_parts
)
598 for piece
in eddymsg_obj
.payload_pieces
:
600 if piece
.piece_type
== TxtType
.text
:
601 # don't transform the plaintext.
604 elif piece
.piece_type
== TxtType
.message
:
605 piece
.gpg_data
= GPGData()
607 (plaintext_b
, sigs
, sigkey_missing
, key_cannot_encrypt
) = decrypt_block(piece
.string
, gpgme_ctx
)
609 piece
.gpg_data
.sigkey_missing
= sigkey_missing
610 piece
.gpg_data
.key_cannot_encrypt
= key_cannot_encrypt
613 piece
.gpg_data
.decrypted
= True
614 piece
.gpg_data
.sigs
= sigs
616 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext_b
, gpgme_ctx
)
619 # if not encrypted, check to see if this is an armored signature.
620 (plaintext_b
, sigs
, sigkey_missing
, key_cannot_encrypt
) = verify_sig_message(piece
.string
, gpgme_ctx
)
622 piece
.gpg_data
.sigkey_missing
= sigkey_missing
623 piece
.gpg_data
.key_cannot_encrypt
= key_cannot_encrypt
626 piece
.piece_type
= TxtType
.signature
627 piece
.gpg_data
.sigs
= sigs
629 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext_b
, gpgme_ctx
)
631 elif piece
.piece_type
== TxtType
.pubkey
:
632 piece
.gpg_data
= GPGData()
634 (key_fps
, key_cannot_encrypt
) = add_gpg_key(piece
.string
, gpgme_ctx
)
636 piece
.gpg_data
.key_cannot_encrypt
= key_cannot_encrypt
639 piece
.gpg_data
.keys
= key_fps
641 elif piece
.piece_type
== TxtType
.detachedsig
:
642 piece
.gpg_data
= GPGData()
644 for prev
in prev_parts
:
645 (sig_fps
, sigkey_missing
, key_cannot_encrypt
) = verify_detached_signature(piece
.string
, prev
.payload_bytes
, gpgme_ctx
)
647 piece
.gpg_data
.sigkey_missing
= sigkey_missing
648 piece
.gpg_data
.key_cannot_encrypt
= key_cannot_encrypt
651 piece
.gpg_data
.sigs
= sig_fps
652 piece
.gpg_data
.plainobj
= prev
659 def prepare_for_reply (eddymsg_obj
, replyinfo_obj
):
660 """Updates replyinfo_obj with info on the message's GPG success/failures
662 This function marks replyinfo_obj with information about whether encrypted
663 text in eddymsg_obj was successfully decrypted, signatures were verified
664 and whether a public key was found or not.
667 eddymsg_obj: a message in the EddyMsg format
668 replyinfo_obj: an instance of ReplyInfo
674 eddymsg_obj has had its gpg_data created by gpg_on_payloads
677 replyinfo_obj has been updated with info about decryption/sig
678 verififcation status, etc. However the desired key isn't imported until
679 later, so the success or failure of that updates the values set here.
682 do_to_eddys_pieces(prepare_for_reply_pieces
, eddymsg_obj
, replyinfo_obj
)
684 def prepare_for_reply_pieces (eddymsg_obj
, replyinfo_obj
):
685 """A helper function for prepare_for_reply
687 It updates replyinfo_obj with GPG success/failure information, when
688 supplied a single-part EddyMsg object.
691 eddymsg_obj: a single-part message in the EddyMsg format
692 replyinfo_obj: an object which holds information about the message's
699 eddymsg_obj is a single-part message. (it may be a part of a multi-part
700 message.) It has had its gpg_data created by gpg_on_payloads if it has
704 replyinfo_obj has been updated with gpg success/failure information
707 for piece
in eddymsg_obj
.payload_pieces
:
708 if piece
.piece_type
== TxtType
.text
:
709 # don't quote the plaintext part.
712 elif piece
.piece_type
== TxtType
.message
:
713 prepare_for_reply_message(piece
, replyinfo_obj
)
715 elif piece
.piece_type
== TxtType
.pubkey
:
716 prepare_for_reply_pubkey(piece
, replyinfo_obj
)
718 elif (piece
.piece_type
== TxtType
.detachedsig
) \
719 or (piece
.piece_type
== TxtType
.signature
):
720 prepare_for_reply_sig(piece
, replyinfo_obj
)
723 def prepare_for_reply_message (piece
, replyinfo_obj
):
724 """Helper function for prepare_for_reply()
726 This function is called when the piece_type of a payload piece is
727 TxtType.message, or GPG Message block. This should be encrypted text. If
728 the encryted block is correclty signed, a sig will be attached to
729 .target_key unless there is already one there.
732 piece: a PayloadPiece object.
733 replyinfo_obj: object which gets updated with decryption status, etc.
739 the piece.payload_piece value should be TxtType.message.
742 replyinfo_obj gets updated with decryption status, signing status, a
743 potential signing key, posession status of the public key for the
744 signature and encryption capability status if that key is missing.
747 if piece
.gpg_data
.plainobj
== None:
748 replyinfo_obj
.decrypt_failure
= True
751 replyinfo_obj
.decrypt_success
= True
753 # we already have a key (and a message)
754 if replyinfo_obj
.target_key
!= None:
757 if piece
.gpg_data
.sigs
== []:
758 if piece
.gpg_data
.sigkey_missing
== True:
759 replyinfo_obj
.sigkey_missing
= True
761 if piece
.gpg_data
.key_cannot_encrypt
== True:
762 replyinfo_obj
.key_cannot_encrypt
= True
764 # only include a signed message in the reply.
765 get_signed_part
= True
768 replyinfo_obj
.target_key
= piece
.gpg_data
.sigs
[0]
769 replyinfo_obj
.sig_success
= True
770 get_signed_part
= False
772 flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, replyinfo_obj
, get_signed_part
)
774 # to catch public keys in encrypted blocks
775 prepare_for_reply(piece
.gpg_data
.plainobj
, replyinfo_obj
)
778 def prepare_for_reply_pubkey (piece
, replyinfo_obj
):
779 """Helper function for prepare_for_reply(). Marks pubkey import status.
781 Marks replyinfo_obj with pub key import status.
784 piece: a PayloadPiece object
785 replyinfo_obj: a ReplyInfo object
788 piece.piece_type should be set to TxtType.pubkey .
791 replyinfo_obj has its fields updated.
794 if piece
.gpg_data
.keys
== []:
795 if piece
.gpg_data
.key_cannot_encrypt
== True:
796 replyinfo_obj
.key_cannot_encrypt
= True
798 replyinfo_obj
.pubkey_success
= True
800 # prefer public key as a fallback for the encrypted reply
801 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.keys
[0]
804 def prepare_for_reply_sig (piece
, replyinfo_obj
):
805 """Helper function for prepare_for_reply(). Marks sig verification status.
807 Marks replyinfo_obj with signature verification status.
810 piece: a PayloadPiece object
811 replyinfo_obj: a ReplyInfo object
814 piece.piece_type should be set to TxtType.signature, or
815 TxtType.detachedsig .
818 replyinfo_obj has its fields updated.
821 if piece
.gpg_data
.sigs
== []:
822 replyinfo_obj
.sig_failure
= True
824 if piece
.gpg_data
.sigkey_missing
== True:
825 replyinfo_obj
.sigkey_missing
= True
827 if piece
.gpg_data
.key_cannot_encrypt
== True:
828 replyinfo_obj
.key_cannot_encrypt
= True
831 replyinfo_obj
.sig_success
= True
833 if replyinfo_obj
.fallback_target_key
== None:
834 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.sigs
[0]
836 if (piece
.piece_type
== TxtType
.signature
):
837 # to catch public keys in signature blocks
838 prepare_for_reply(piece
.gpg_data
.plainobj
, replyinfo_obj
)
841 def flatten_decrypted_payloads (eddymsg_obj
, replyinfo_obj
, get_signed_part
):
842 """For creating a string representation of a signed, encrypted part.
844 When given a decrypted payload, it will add either the plaintext or signed
845 plaintext to the reply message, depeding on 'get_signed_part'. This is
846 useful for ensuring that the reply message only comes from a signed and
847 ecrypted GPG message. It also sets the target_key for encrypting the reply
848 if it's told to get signed text only.
851 eddymsg_obj: the message in EddyMsg format created by decrypting GPG
853 replyinfo_obj: a ReplyInfo object for holding the message to quote and
854 the target_key to encrypt to.
855 get_signed_part: True if we should only include text that contains a
856 further signature. If False, then include plain text.
862 The EddyMsg instance passed in should be a piece.gpg_data.plainobj
863 which represents decrypted text. It may or may not be signed on that
867 the ReplyInfo instance may have a new 'target_key' set and its
868 'msg_to_quote' will be updated with (possibly signed) plaintext, if any
872 if eddymsg_obj
== None:
875 # recurse on multi-part mime
876 if eddymsg_obj
.multipart
== True:
877 for sub
in eddymsg_obj
.subparts
:
878 flatten_decrypted_payloads(sub
, replyinfo_obj
, get_signed_part
)
880 for piece
in eddymsg_obj
.payload_pieces
:
881 if (get_signed_part
):
882 if ((piece
.piece_type
== TxtType
.detachedsig
) \
883 or (piece
.piece_type
== TxtType
.signature
)) \
884 and (piece
.gpg_data
!= None) \
885 and (piece
.gpg_data
.plainobj
!= None):
886 flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, replyinfo_obj
, False)
887 replyinfo_obj
.target_key
= piece
.gpg_data
.sigs
[0]
890 if piece
.piece_type
== TxtType
.text
:
891 replyinfo_obj
.msg_to_quote
+= piece
.string
894 def get_key_from_fp (replyinfo_obj
, gpgme_ctx
):
895 """Obtains a public key object from a key fingerprint
897 If the .target_key is not set, then we use .fallback_target_key, if
901 replyinfo_obj: ReplyInfo instance
902 gpgme_ctx: the gpgme context
908 Loading a key requires that we have the public key imported. This
909 requires that they email contains the pub key block, or that it was
910 previously sent to edward.
913 If the key can be loaded, then replyinfo_obj.reply_to_key points to the
914 public key object. If the key cannot be loaded, then the replyinfo_obj
915 is marked as having no public key available. If the key is not capable
916 of encryption, it will not be used, and replyinfo_obj will be marked
920 for key
in (replyinfo_obj
.target_key
, replyinfo_obj
.fallback_target_key
):
923 encrypt_to_key
= gpgme_ctx
.get_key(key
)
925 except gpgme
.GpgmeError
:
928 if is_key_usable(encrypt_to_key
):
929 replyinfo_obj
.encrypt_to_key
= encrypt_to_key
930 replyinfo_obj
.have_reply_key
= True
931 replyinfo_obj
.key_can_encrypt
= True
935 replyinfo_obj
.key_cannot_encrypt
= True
939 def write_reply (replyinfo_obj
):
940 """Write the reply email body about the GPG successes/failures.
942 The reply is about whether decryption, sig verification and key
943 import/loading was successful or failed. If text was successfully decrypted
944 and verified, then the first instance of such text will be included in
948 replyinfo_obj: contains details of GPG processing status
951 the plaintext message to be sent to the user
954 replyinfo_obj should be populated with info about GPG processing status.
959 if (replyinfo_obj
.pubkey_success
== True):
960 reply_plain
+= replyinfo_obj
.replies
['greeting']
961 reply_plain
+= "\n\n"
964 if replyinfo_obj
.decrypt_success
== True:
965 debug('decrypt success')
966 reply_plain
+= replyinfo_obj
.replies
['success_decrypt']
968 if (replyinfo_obj
.sig_success
== True) and (replyinfo_obj
.have_reply_key
== True):
969 debug('message quoted')
970 reply_plain
+= replyinfo_obj
.replies
['space']
971 reply_plain
+= replyinfo_obj
.replies
['quote_follows']
972 reply_plain
+= "\n\n"
973 quoted_text
= email_quote_text(replyinfo_obj
.msg_to_quote
)
974 reply_plain
+= quoted_text
976 reply_plain
+= "\n\n"
978 elif replyinfo_obj
.decrypt_failure
== True:
979 debug('decrypt failure')
980 reply_plain
+= replyinfo_obj
.replies
['failed_decrypt']
981 reply_plain
+= "\n\n"
984 if replyinfo_obj
.sig_success
== True:
985 debug('signature success')
986 reply_plain
+= replyinfo_obj
.replies
['sig_success']
987 reply_plain
+= "\n\n"
989 elif replyinfo_obj
.sig_failure
== True:
990 debug('signature failure')
991 reply_plain
+= replyinfo_obj
.replies
['sig_failure']
992 reply_plain
+= "\n\n"
995 if (replyinfo_obj
.pubkey_success
== True):
996 debug('public key received')
997 reply_plain
+= replyinfo_obj
.replies
['public_key_received']
998 reply_plain
+= "\n\n"
1000 elif (replyinfo_obj
.sigkey_missing
== True):
1001 debug('no public key')
1002 reply_plain
+= replyinfo_obj
.replies
['no_public_key']
1003 reply_plain
+= "\n\n"
1005 elif (replyinfo_obj
.key_can_encrypt
== False) \
1006 and (replyinfo_obj
.key_cannot_encrypt
== True):
1007 debug('bad public key')
1008 reply_plain
+= replyinfo_obj
.replies
['no_public_key']
1009 reply_plain
+= "\n\n"
1012 if (reply_plain
== ""):
1013 debug('plaintext message')
1014 reply_plain
+= replyinfo_obj
.replies
['failed_decrypt']
1015 reply_plain
+= "\n\n"
1018 reply_plain
+= replyinfo_obj
.replies
['signature']
1019 reply_plain
+= "\n\n"
1024 def add_gpg_key (key_block
, gpgme_ctx
):
1025 """Adds a GPG pubkey to the local keystore
1027 This adds keys received through email into the key store so they can be
1031 key_block: the string form of the ascii-armored public key block
1032 gpgme_ctx: the gpgme context
1035 the fingerprint(s) of the imported key(s) which can be used for
1036 encryption, and a boolean marking whether none of the keys are capable
1040 fp
= io
.BytesIO(key_block
.encode('ascii'))
1043 result
= gpgme_ctx
.import_(fp
)
1044 imports
= result
.imports
1045 except gpgme
.GpgmeError
:
1048 key_fingerprints
= []
1049 key_cannot_encrypt
= False
1051 for import_res
in imports
:
1052 fingerprint
= import_res
[0]
1055 key_obj
= gpgme_ctx
.get_key(fingerprint
)
1059 if is_key_usable(key_obj
):
1060 key_fingerprints
+= [fingerprint
]
1061 key_cannot_encrypt
= False
1063 debug("added gpg key: " + fingerprint
)
1065 elif key_fingerprints
== []:
1066 key_cannot_encrypt
= True
1068 return (key_fingerprints
, key_cannot_encrypt
)
1071 def verify_sig_message (msg_block
, gpgme_ctx
):
1072 """Verifies the signature of a signed, ascii-armored block of text.
1074 It encodes the string into ascii, since binary GPG files are currently
1075 unsupported, and alternative, the ascii-armored format is encodable into
1079 msg_block: a GPG Message block in string form. It may be encrypted or
1080 not. If it is encrypted, it will return empty results.
1081 gpgme_ctx: the gpgme context
1084 A tuple containing the plaintext bytes of the signed part, the list of
1085 fingerprints of encryption-capable keys signing the data, a boolean
1086 marking whether edward is missing all public keys for validating any of
1087 the signatures, and a boolean marking whether all sigs' keys are
1088 incapable of encryption. If verification failed, perhaps because the
1089 message was also encrypted, sensible default values are returned.
1092 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
1093 plain_b
= io
.BytesIO()
1096 sigs
= gpgme_ctx
.verify(block_b
, None, plain_b
)
1097 except gpgme
.GpgmeError
:
1098 return ("",[],False,False)
1100 plaintext_b
= plain_b
.getvalue()
1102 (fingerprints
, sigkey_missing
, key_cannot_encrypt
) = get_signature_fp(sigs
, gpgme_ctx
)
1104 return (plaintext_b
, fingerprints
, sigkey_missing
, key_cannot_encrypt
)
1107 def verify_detached_signature (detached_sig
, plaintext_bytes
, gpgme_ctx
):
1108 """Verifies the signature of a detached signature.
1110 This requires the signature part and the signed part as separate arguments.
1113 detached_sig: the signature part of the detached signature
1114 plaintext_bytes: the byte form of the message being signed.
1115 gpgme_ctx: the gpgme context
1118 A tuple containging a list of encryption capable signing fingerprints
1119 if the signature verification was sucessful, a boolean marking whether
1120 edward is missing all public keys for validating any of the signatures,
1121 and a boolean marking whether all signing keys are incapable of
1122 encryption. Otherwise, a tuple containing an empty list and True are
1126 detached_sig_fp
= io
.BytesIO(detached_sig
.encode('ascii'))
1127 plaintext_fp
= io
.BytesIO(plaintext_bytes
)
1130 sigs
= gpgme_ctx
.verify(detached_sig_fp
, plaintext_fp
, None)
1131 except gpgme
.GpgmeError
:
1132 return ([],False,False)
1134 (fingerprints
, sigkey_missing
, key_cannot_encrypt
) = get_signature_fp(sigs
, gpgme_ctx
)
1136 return (fingerprints
, sigkey_missing
, key_cannot_encrypt
)
1139 def decrypt_block (msg_block
, gpgme_ctx
):
1140 """Decrypts a block of GPG text and verifies any included sigatures.
1142 Some encypted messages have embeded signatures, so those are verified too.
1145 msg_block: the encrypted(/signed) text
1146 gpgme_ctx: the gpgme context
1149 A tuple containing plaintext bytes, encryption-capable signatures (if
1150 decryption and signature verification were successful, respectively), a
1151 boolean marking whether edward is missing all public keys for
1152 validating any of the signatures, and a boolean marking whether all
1153 signature keys are incapable of encryption.
1156 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
1157 plain_b
= io
.BytesIO()
1160 sigs
= gpgme_ctx
.decrypt_verify(block_b
, plain_b
)
1161 except gpgme
.GpgmeError
:
1162 return ("",[],False,False)
1164 plaintext_b
= plain_b
.getvalue()
1166 (fingerprints
, sigkey_missing
, key_cannot_encrypt
) = get_signature_fp(sigs
, gpgme_ctx
)
1168 return (plaintext_b
, fingerprints
, sigkey_missing
, key_cannot_encrypt
)
1171 def get_signature_fp (sigs
, gpgme_ctx
):
1172 """Selects valid signatures from output of gpgme signature verifying functions
1174 get_signature_fp returns a list of valid signature fingerprints if those
1175 fingerprints are associated with available keys capable of encryption.
1178 sigs: a signature verification result object list
1179 gpgme_ctx: a gpgme context
1182 fingerprints: a list of fingerprints
1183 sigkey_missing: a boolean marking whether public keys are missing for
1184 all available signatures.
1185 key_cannot_encrypt: a boolearn marking whether available public keys are
1186 incapable of encryption.
1189 sigkey_missing
= False
1190 key_cannot_encrypt
= False
1194 if (sig
.summary
== 0) or (sig
.summary
& gpgme
.SIGSUM_VALID
!= 0) or (sig
.summary
& gpgme
.SIGSUM_GREEN
!= 0):
1196 key_obj
= gpgme_ctx
.get_key(sig
.fpr
)
1198 if fingerprints
== []:
1199 sigkey_missing
= True
1202 if is_key_usable(key_obj
):
1203 fingerprints
+= [sig
.fpr
]
1204 key_cannot_encrypt
= False
1205 sigkey_missing
= False
1207 elif fingerprints
== []:
1208 key_cannot_encrypt
= True
1210 elif fingerprints
== []:
1211 if (sig
.summary
& gpgme
.SIGSUM_KEY_MISSING
!= 0):
1212 sigkey_missing
= True
1214 return (fingerprints
, sigkey_missing
, key_cannot_encrypt
)
1217 def is_key_usable (key_obj
):
1218 """Returns boolean representing key usability regarding encryption
1220 Tests various feature of key and returns usability
1223 key_obj: a gpgme key object
1226 A boolean representing key usability
1228 if key_obj
.can_encrypt
and not key_obj
.invalid
and not key_obj
.expired \
1229 and not key_obj
.revoked
and not key_obj
.disabled
:
1235 def email_to_from_subject (email_bytes
):
1236 """Returns the values of the email's To:, From: and Subject: fields
1238 Returns this information from an email.
1241 email_bytes: the byte string form of the email
1244 the email To:, From:, and Subject: fields as strings
1247 email_struct
= email
.parser
.BytesParser().parsebytes(email_bytes
)
1249 email_to
= email_struct
['To']
1250 email_from
= email_struct
['From']
1251 email_subject
= email_struct
['Subject']
1253 return email_to
, email_from
, email_subject
1256 def import_lang_pick_address(email_to
, hostname
):
1257 """Imports language file for i18n support; makes reply from address
1259 The language imported depends on the To: address of the email received by
1260 edward. an -en ending implies the English language, whereas a -ja ending
1261 implies Japanese. The list of supported languages is listed in the 'langs'
1262 list at the beginning of the program. This function also chooses the
1263 language-dependent address which can be used as the From address in the
1267 email_to: the string containing the email address that the mail was
1269 hostname: the hostname part of the reply email's from address
1272 the reference to the imported language module. The only variable in
1273 this file is the 'replies' dictionary.
1279 if email_to
!= None:
1281 if "edward-" + lang
in email_to
:
1285 lang_mod_name
= "lang." + re
.sub('-', '_', use_lang
)
1286 lang_module
= importlib
.import_module(lang_mod_name
)
1288 reply_from
= "edward-" + use_lang
+ "@" + hostname
1290 return lang_module
, reply_from
1293 def generate_encrypted_mime (plaintext
, email_to
, email_subject
, encrypt_to_key
,
1295 """This function creates the mime email reply. It can encrypt the email.
1297 If the encrypt_key is included, then the email is encrypted and signed.
1298 Otherwise it is unencrypted.
1301 plaintext: the plaintext body of the message to create.
1302 email_to: the email address to reply to
1303 email_subject: the subject to use in reply
1304 encrypt_to_key: the key object to use for encrypting the email. (or
1306 gpgme_ctx: the gpgme context
1309 A string version of the mime message, possibly encrypted and signed.
1312 plaintext_mime
= MIMEText(plaintext
)
1313 plaintext_mime
.set_charset('utf-8')
1315 if (encrypt_to_key
!= None):
1317 encrypted_text
= encrypt_sign_message(plaintext_mime
.as_string(),
1321 control_mime
= MIMEApplication("Version: 1",
1322 _subtype
='pgp-encrypted',
1323 _encoder
=email
.encoders
.encode_7or8bit
)
1324 control_mime
['Content-Description'] = 'PGP/MIME version identification'
1325 control_mime
.set_charset('us-ascii')
1327 encoded_mime
= MIMEApplication(encrypted_text
,
1328 _subtype
='octet-stream; name="encrypted.asc"',
1329 _encoder
=email
.encoders
.encode_7or8bit
)
1330 encoded_mime
['Content-Description'] = 'OpenPGP encrypted message'
1331 encoded_mime
['Content-Disposition'] = 'inline; filename="encrypted.asc"'
1332 encoded_mime
.set_charset('us-ascii')
1334 message_mime
= MIMEMultipart(_subtype
="encrypted", protocol
="application/pgp-encrypted")
1335 message_mime
.attach(control_mime
)
1336 message_mime
.attach(encoded_mime
)
1337 message_mime
['Content-Disposition'] = 'inline'
1340 message_mime
= plaintext_mime
1342 message_mime
['To'] = email_to
1343 message_mime
['Subject'] = email_subject
1345 reply
= message_mime
.as_string()
1350 def send_reply(email_txt
, subject
, reply_to
, reply_from
):
1352 email_bytes
= email_txt
.encode('ascii')
1354 p
= subprocess
.Popen(["/usr/sbin/sendmail", "-f", reply_from
, "-F", "Edward, GPG Bot", "-i", reply_to
], stdin
=subprocess
.PIPE
)
1356 (stdout
, stderr
) = p
.communicate(email_bytes
)
1359 debug("sendmail stdout: " + str(stdout
))
1361 error("sendmail stderr: " + str(stderr
))
1364 def email_quote_text (text
):
1365 """Quotes input text by inserting "> "s
1367 This is useful for quoting a text for the reply message. It inserts "> "
1368 strings at the beginning of lines.
1371 text: plain text to quote
1377 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
1379 return quoted_message
1382 def encrypt_sign_message (plaintext
, encrypt_to_key
, gpgme_ctx
):
1383 """Encrypts and signs plaintext
1385 This encrypts and signs a message.
1388 plaintext: text to sign and ecrypt
1389 encrypt_to_key: the key object to encrypt to
1390 gpgme_ctx: the gpgme context
1393 An encrypted and signed string of text
1396 # the plaintext should be mime encoded in an ascii-compatible form
1397 plaintext_bytes
= io
.BytesIO(plaintext
.encode('ascii'))
1398 encrypted_bytes
= io
.BytesIO()
1400 gpgme_ctx
.encrypt_sign([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
1401 plaintext_bytes
, encrypted_bytes
)
1403 encrypted_txt
= encrypted_bytes
.getvalue().decode('ascii')
1404 return encrypted_txt
1407 def error (error_msg
):
1408 """Write an error message to stdout
1410 The error message includes the program name.
1413 error_msg: the message to print
1419 An error message is printed to stdout
1422 sys
.stderr
.write(progname
+ ": " + str(error_msg
) + "\n")
1425 def debug (debug_msg
):
1426 """Writes a debug message to stdout if debug == True
1428 If the debug option is set in edward_config.py, then the passed message
1429 gets printed to stdout.
1432 debug_msg: the message to print to stdout
1438 A debug message is printed to stdout
1441 if edward_config
.debug
== True:
1446 """Sets the progname variable and processes optional argument
1448 If there are more than two arguments then edward complains and quits. An
1449 single "-p" argument sets the print_reply_only option, which makes edward
1450 print email replies instead of mailing them.
1456 True if edward should print arguments instead of mailing them,
1457 otherwise it returns False.
1460 Exits with error 1 if there are more than two arguments, otherwise
1461 returns the print_reply_only option.
1465 progname
= sys
.argv
[0]
1467 print_reply_only
= False
1469 if len(sys
.argv
) > 2:
1470 print(progname
+ " usage: " + progname
+ " [-p]\n\n" \
1471 + " -p print reply message to stdout, do not mail it\n", \
1475 elif (len(sys
.argv
) == 2) and (sys
.argv
[1] == "-p"):
1476 print_reply_only
= True
1478 return print_reply_only
1481 if __name__
== "__main__":
1482 """Executes main if this file is not loaded interactively"""