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
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. This could happen if a key is revoked
154 or expired, for instance.
156 'keys' is a list of fingerprints of keys obtained in public key blocks.
163 sigkey_missing
= False
164 key_cannot_encrypt
= False
168 class ReplyInfo (object):
170 ReplyInfo contains details that edward uses in generating its reply.
172 Instances of this class contain information about whether a message was
173 successfully encrypted or signed, and whether a public key was attached, or
174 retrievable, from the local GPG store. It stores the fingerprints of
175 potential encryption key candidates and the message (if any at all) to
176 quote in edward's reply.
178 'replies' points one of the dictionaries of translated replies.
180 'target_key' refers to the fingerprint of a key used to sign encrypted
181 data. This is the preferred key, if it is set, and if is available.
183 'fallback_target_key' referst to the fingerprint of a key used to sign
184 unencrypted data; alternatively it may be a public key attached to the
187 'encrypt_to_key' the key object to use when encrypting edward's reply
189 'msg_to_quote' refers to the part of a message which edward should quote in
190 his reply. This should remain as None if there was no encrypted and singed
191 part. This is to avoid making edward a service for decrypting other
192 people's messages to edward.
194 'decrypt_success' is set to True if edward could decrypt part of the
197 'sig_success' is set to True if edward could to some extent verify the
198 signature of a signed part of the message to edward.
200 'key_can_encrypt' is set to True if a key which can be encrypted to has
203 'sig_failure' is set to True if edward could not verify a siganture.
205 'pubkey_success' is set to True if edward successfully imported a public
208 'sigkey_missing' is set to True if edward doesn't have the public key
209 needed for signature verification.
211 'key_cannot_encrypt' is set to True if pubkeys or sig's keys in a payload
212 piece of the message are not capable of encryption.
214 'have_repy_key' is set to True if edward has a public key to encrypt its
221 fallback_target_key
= None
222 encrypt_to_key
= None
225 decrypt_success
= False
227 pubkey_success
= False
228 key_can_encrypt
= False
230 decrypt_failure
= False
232 sigkey_missing
= False
233 key_cannot_encrypt
= False
235 have_reply_key
= False
241 This is the main function for edward, a GPG reply bot.
243 Edward responds to GPG-encrypted and signed mail, encrypting and signing
244 the response if the user's public key is, or was, included in the message.
253 Mime or plaintext email passing in through standard input. Portions of
254 the email may be encrypted. If the To: address contains the text
255 "edward-ja", then the reply will contain a reply written in the
256 Japanese language. There are other languages as well. The default
260 A reply email will be printed to standard output. The contents of the
261 reply email depends on whether the original email was encrypted or not,
262 has or doesn't have a signature, whether a public key used in the
263 original message is provided or locally stored, and the language
264 implied by the To: address in the original email.
267 print_reply_only
= handle_args()
269 gpgme_ctx
= get_gpg_context(edward_config
.gnupghome
,
270 edward_config
.sign_with_key
)
272 email_bytes
= sys
.stdin
.buffer.read()
273 email_struct
= parse_pgp_mime(email_bytes
, gpgme_ctx
)
275 email_to
, email_from
, email_subject
= email_to_from_subject(email_bytes
)
276 lang
, reply_from
= import_lang_pick_address(email_to
, edward_config
.hostname
)
278 replyinfo_obj
= ReplyInfo()
279 replyinfo_obj
.replies
= lang
.replies
281 prepare_for_reply(email_struct
, replyinfo_obj
)
282 get_key_from_fp(replyinfo_obj
, gpgme_ctx
)
283 reply_plaintext
= write_reply(replyinfo_obj
)
285 reply_mime
= generate_encrypted_mime(reply_plaintext
, email_from
, \
286 email_subject
, replyinfo_obj
.encrypt_to_key
,
289 if print_reply_only
== True:
292 send_reply(reply_mime
, email_subject
, email_from
, reply_from
)
295 def get_gpg_context (gnupghome
, sign_with_key_fp
):
297 This function returns the GPG context needed for encryption and signing.
299 The context is needed by other functions which use GPG functionality.
302 gnupghome: The path to "~/.gnupg/" or its alternative.
303 sign_with_key: The fingerprint of the key to sign with
306 A gpgme context to be used for GPG functions.
309 the 'armor' flag is set to True and the list of signing keys contains
310 the single specified key
313 os
.environ
['GNUPGHOME'] = gnupghome
315 gpgme_ctx
= gpgme
.Context()
316 gpgme_ctx
.armor
= True
319 sign_with_key
= gpgme_ctx
.get_key(sign_with_key_fp
)
320 except gpgme
.GpgmeError
:
321 error("unable to load signing key. is the gnupghome "
322 + "and signing key properly set in the edward_config.py?")
325 gpgme_ctx
.signers
= [sign_with_key
]
330 def parse_pgp_mime (email_bytes
, gpgme_ctx
):
331 """Parses the email for mime payloads and decrypts/verfies signatures.
333 This function creates a representation of a mime or plaintext email with
334 the EddyMsg class. It then splits each mime payload into one or more pieces
335 which may be plain text or GPG data. It then decrypts encrypted parts and
336 does some very basic signature verification on those parts.
339 email_bytes: an email message in byte string format
340 gpgme_ctx: a gpgme context
343 A message as an instance of EddyMsg
346 the returned EddyMsg instance has split, decrypted, verified and pubkey
350 email_struct
= email
.parser
.BytesParser().parsebytes(email_bytes
)
352 eddymsg_obj
= parse_mime(email_struct
)
353 split_payloads(eddymsg_obj
)
354 gpg_on_payloads(eddymsg_obj
, gpgme_ctx
)
359 def parse_mime(msg_struct
):
360 """Translates python's email.parser format into an EddyMsg format
362 If the message is multi-part, then a recursive object is created, where
363 each sub-part is also a EddyMsg instance.
366 msg_struct: an email parsed with email.parser.Parser(), which can be
370 an instance of EddyMsg, potentially a recursive one.
373 eddymsg_obj
= EddyMsg()
375 if msg_struct
.is_multipart() == True:
376 payloads
= msg_struct
.get_payload()
378 eddymsg_obj
.multipart
= True
379 eddymsg_obj
.subparts
= list(map(parse_mime
, payloads
))
382 eddymsg_obj
= get_subpart_data(msg_struct
)
387 def scan_and_split (payload_piece
, match_name
, pattern
):
388 """This splits the payloads of an EddyMsg object into GPG and text parts.
390 An EddyMsg object's payload_pieces starts off as a list containing a single
391 PayloadPiece object. This function returns a list of these objects which
392 have been split into GPG data and regular text, if such splits need to be/
396 payload_piece: a single payload or a split part of a payload
397 match_name: the type of data to try to spit out from the payload piece
398 pattern: the search pattern to be used for finding that type of data
401 a list of objects of the PayloadPiece class, in the order that the
402 string part of payload_piece originally was, broken up according to
403 matches specified by 'pattern'.
406 # don't try to re-split pieces containing gpg data
407 if payload_piece
.piece_type
!= TxtType
.text
:
408 return [payload_piece
]
410 flags
= re
.DOTALL | re
.MULTILINE
411 matches
= re
.search("(?P<beginning>.*?)(?P<match>" + pattern
+
412 ")(?P<rest>.*)", payload_piece
.string
, flags
=flags
)
415 pieces
= [payload_piece
]
419 beginning
= PayloadPiece()
420 beginning
.string
= matches
.group('beginning')
421 beginning
.piece_type
= payload_piece
.piece_type
423 match
= PayloadPiece()
424 match
.string
= matches
.group('match')
425 match
.piece_type
= match_name
427 rest
= PayloadPiece()
428 rest
.string
= matches
.group('rest')
429 rest
.piece_type
= payload_piece
.piece_type
431 more_pieces
= scan_and_split(rest
, match_name
, pattern
)
432 pieces
= [beginning
, match
] + more_pieces
437 def get_subpart_data (part
):
438 """This function grabs information from a single part mime object.
440 It copies needed data from a single part email.parser.Parser() object over
441 to an EddyMsg object.
444 part: a non-multi-part mime.parser.Parser() object
447 a single-part EddyMsg() object
452 mime_decoded_bytes
= part
.get_payload(decode
=True)
453 charset
= part
.get_content_charset()
455 # your guess is as good as a-myy-ee-ine...
459 payload_string
= part
.as_string()
460 if payload_string
!= None:
461 obj
.payload_bytes
= payload_string
.encode(charset
)
463 obj
.filename
= part
.get_filename()
464 obj
.content_type
= part
.get_content_type()
465 obj
.description_list
= part
['content-description']
467 if mime_decoded_bytes
!= None:
469 payload
= PayloadPiece()
470 payload
.string
= mime_decoded_bytes
.decode(charset
)
471 payload
.piece_type
= TxtType
.text
473 obj
.payload_pieces
= [payload
]
474 except UnicodeDecodeError:
480 def do_to_eddys_pieces (function_to_do
, eddymsg_obj
, data
):
481 """A function which maps another function onto a message's subparts.
483 This is a higer-order function which recursively performs a specified
484 function on each subpart of a multi-part message. Each single-part sub-part
485 has the function applied to it. This function also works if the part passed
489 function_to_do: function to perform on sub-parts
490 eddymsg_obj: a single part or multi-part EddyMsg object
491 data: a second argument to pass to 'function_to_do'
497 The passed-in EddyMsg object is transformed recursively on its
498 sub-parts according to 'function_to_do'.
501 if eddymsg_obj
.multipart
== True:
502 for sub
in eddymsg_obj
.subparts
:
503 do_to_eddys_pieces(function_to_do
, sub
, data
)
505 function_to_do(eddymsg_obj
, data
)
508 def split_payloads (eddymsg_obj
):
509 """Splits all (sub-)payloads of a message into GPG data and regular text.
511 Recursively performs payload splitting on all sub-parts of an EddyMsg
512 object into the various GPG data types, such as GPG messages, public key
513 blocks and signed text.
516 eddymsg_obj: an instance of EddyMsg
522 The EddyMsg object has payloads that are unsplit (by may be split)..
525 The EddyMsg object's payloads are all split into GPG and non-GPG parts.
528 for match_pair
in match_pairs
:
529 do_to_eddys_pieces(split_payload_pieces
, eddymsg_obj
, match_pair
)
532 def split_payload_pieces (eddymsg_obj
, match_pair
):
533 """A helper function for split_payloads(); works on PayloadPiece objects.
535 This function splits up PayloadPiece objects into multipe PayloadPiece
536 objects and replaces the EddyMsg object's previous list of payload pieces
537 with the new split up one.
540 eddymsg_obj: a single-part EddyMsg object.
541 match_pair: a tuple from the match_pairs list, which specifies a match
542 name and a match pattern.
548 The payload piece(s) of an EddyMsg object may be already split or
552 The EddyMsg object's payload piece(s) are split into a list of pieces
553 if matches of the match_pair are found.
556 (match_name
, pattern
) = match_pair
559 for piece
in eddymsg_obj
.payload_pieces
:
560 new_pieces_list
+= scan_and_split(piece
, match_name
, pattern
)
562 eddymsg_obj
.payload_pieces
= new_pieces_list
565 def gpg_on_payloads (eddymsg_obj
, gpgme_ctx
, prev_parts
=[]):
566 """Performs GPG operations on the GPG parts of the message
568 This function decrypts text, verifies signatures, and imports public keys
569 included in an email.
572 eddymsg_obj: an EddyMsg object with its payload_pieces split into GPG
573 and non-GPG sections by split_payloads()
574 gpgme_ctx: a gpgme context
576 prev_parts: a list of mime parts that occur before the eddymsg_obj
577 part, under the same multi-part mime part. This is used for
578 verifying detached signatures. For the root mime part, this should
579 be an empty list, which is the default value if this paramater is
586 eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
589 Decryption, verification and key imports occur. the gpg_data members of
590 PayloadPiece objects get filled in with GPGData objects with some of
591 their attributes set.
594 if eddymsg_obj
.multipart
== True:
596 for sub
in eddymsg_obj
.subparts
:
597 gpg_on_payloads (sub
, gpgme_ctx
, prev_parts
)
602 for piece
in eddymsg_obj
.payload_pieces
:
604 if piece
.piece_type
== TxtType
.text
:
605 # don't transform the plaintext.
608 elif piece
.piece_type
== TxtType
.message
:
609 piece
.gpg_data
= GPGData()
611 (plaintext_b
, sigs
, sigkey_missing
, key_cannot_encrypt
) = decrypt_block(piece
.string
, gpgme_ctx
)
613 piece
.gpg_data
.sigkey_missing
= sigkey_missing
614 piece
.gpg_data
.key_cannot_encrypt
= key_cannot_encrypt
617 piece
.gpg_data
.decrypted
= True
618 piece
.gpg_data
.sigs
= sigs
620 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext_b
, gpgme_ctx
)
623 # if not encrypted, check to see if this is an armored signature.
624 (plaintext_b
, sigs
, sigkey_missing
, key_cannot_encrypt
) = verify_sig_message(piece
.string
, gpgme_ctx
)
626 piece
.gpg_data
.sigkey_missing
= sigkey_missing
627 piece
.gpg_data
.key_cannot_encrypt
= key_cannot_encrypt
630 piece
.piece_type
= TxtType
.signature
631 piece
.gpg_data
.sigs
= sigs
633 piece
.gpg_data
.plainobj
= parse_pgp_mime(plaintext_b
, gpgme_ctx
)
635 elif piece
.piece_type
== TxtType
.pubkey
:
636 piece
.gpg_data
= GPGData()
638 (key_fps
, key_cannot_encrypt
) = add_gpg_key(piece
.string
, gpgme_ctx
)
640 piece
.gpg_data
.key_cannot_encrypt
= key_cannot_encrypt
643 piece
.gpg_data
.keys
= key_fps
645 elif piece
.piece_type
== TxtType
.detachedsig
:
646 piece
.gpg_data
= GPGData()
648 for prev
in prev_parts
:
649 (sig_fps
, sigkey_missing
, key_cannot_encrypt
) = verify_detached_signature(piece
.string
, prev
.payload_bytes
, gpgme_ctx
)
651 piece
.gpg_data
.sigkey_missing
= sigkey_missing
652 piece
.gpg_data
.key_cannot_encrypt
= key_cannot_encrypt
655 piece
.gpg_data
.sigs
= sig_fps
656 piece
.gpg_data
.plainobj
= prev
663 def prepare_for_reply (eddymsg_obj
, replyinfo_obj
):
664 """Updates replyinfo_obj with info on the message's GPG success/failures
666 This function marks replyinfo_obj with information about whether encrypted
667 text in eddymsg_obj was successfully decrypted, signatures were verified
668 and whether a public key was found or not.
671 eddymsg_obj: a message in the EddyMsg format
672 replyinfo_obj: an instance of ReplyInfo
678 eddymsg_obj has had its gpg_data created by gpg_on_payloads
681 replyinfo_obj has been updated with info about decryption/sig
682 verififcation status, etc. However the desired key isn't imported until
683 later, so the success or failure of that updates the values set here.
686 do_to_eddys_pieces(prepare_for_reply_pieces
, eddymsg_obj
, replyinfo_obj
)
688 def prepare_for_reply_pieces (eddymsg_obj
, replyinfo_obj
):
689 """A helper function for prepare_for_reply
691 It updates replyinfo_obj with GPG success/failure information, when
692 supplied a single-part EddyMsg object.
695 eddymsg_obj: a single-part message in the EddyMsg format
696 replyinfo_obj: an object which holds information about the message's
703 eddymsg_obj is a single-part message. (it may be a part of a multi-part
704 message.) It has had its gpg_data created by gpg_on_payloads if it has
708 replyinfo_obj has been updated with gpg success/failure information
711 for piece
in eddymsg_obj
.payload_pieces
:
712 if piece
.piece_type
== TxtType
.text
:
713 # don't quote the plaintext part.
716 elif piece
.piece_type
== TxtType
.message
:
717 prepare_for_reply_message(piece
, replyinfo_obj
)
719 elif piece
.piece_type
== TxtType
.pubkey
:
720 prepare_for_reply_pubkey(piece
, replyinfo_obj
)
722 elif (piece
.piece_type
== TxtType
.detachedsig
) \
723 or (piece
.piece_type
== TxtType
.signature
):
724 prepare_for_reply_sig(piece
, replyinfo_obj
)
727 def prepare_for_reply_message (piece
, replyinfo_obj
):
728 """Helper function for prepare_for_reply()
730 This function is called when the piece_type of a payload piece is
731 TxtType.message, or GPG Message block. This should be encrypted text. If
732 the encryted block is correclty signed, a sig will be attached to
733 .target_key unless there is already one there.
736 piece: a PayloadPiece object.
737 replyinfo_obj: object which gets updated with decryption status, etc.
743 the piece.payload_piece value should be TxtType.message.
746 replyinfo_obj gets updated with decryption status, signing status, a
747 potential signing key, posession status of the public key for the
748 signature and encryption capability status if that key is missing.
751 if piece
.gpg_data
.plainobj
== None:
752 replyinfo_obj
.decrypt_failure
= True
755 replyinfo_obj
.decrypt_success
= True
757 # we already have a key (and a message)
758 if replyinfo_obj
.target_key
!= None:
761 if piece
.gpg_data
.sigs
== []:
762 if piece
.gpg_data
.sigkey_missing
== True:
763 replyinfo_obj
.sigkey_missing
= True
765 if piece
.gpg_data
.key_cannot_encrypt
== True:
766 replyinfo_obj
.key_cannot_encrypt
= True
768 # only include a signed message in the reply.
769 get_signed_part
= True
772 replyinfo_obj
.target_key
= piece
.gpg_data
.sigs
[0]
773 replyinfo_obj
.sig_success
= True
774 get_signed_part
= False
776 flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, replyinfo_obj
, get_signed_part
)
778 # to catch public keys in encrypted blocks
779 prepare_for_reply(piece
.gpg_data
.plainobj
, replyinfo_obj
)
782 def prepare_for_reply_pubkey (piece
, replyinfo_obj
):
783 """Helper function for prepare_for_reply(). Marks pubkey import status.
785 Marks replyinfo_obj with pub key import status.
788 piece: a PayloadPiece object
789 replyinfo_obj: a ReplyInfo object
792 piece.piece_type should be set to TxtType.pubkey .
795 replyinfo_obj has its fields updated.
798 if piece
.gpg_data
.keys
== []:
799 if piece
.gpg_data
.key_cannot_encrypt
== True:
800 replyinfo_obj
.key_cannot_encrypt
= True
802 replyinfo_obj
.pubkey_success
= True
804 # prefer public key as a fallback for the encrypted reply
805 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.keys
[0]
808 def prepare_for_reply_sig (piece
, replyinfo_obj
):
809 """Helper function for prepare_for_reply(). Marks sig verification status.
811 Marks replyinfo_obj with signature verification status.
814 piece: a PayloadPiece object
815 replyinfo_obj: a ReplyInfo object
818 piece.piece_type should be set to TxtType.signature, or
819 TxtType.detachedsig .
822 replyinfo_obj has its fields updated.
825 if piece
.gpg_data
.sigs
== []:
826 replyinfo_obj
.sig_failure
= True
828 if piece
.gpg_data
.sigkey_missing
== True:
829 replyinfo_obj
.sigkey_missing
= True
831 if piece
.gpg_data
.key_cannot_encrypt
== True:
832 replyinfo_obj
.key_cannot_encrypt
= True
835 replyinfo_obj
.sig_success
= True
837 if replyinfo_obj
.fallback_target_key
== None:
838 replyinfo_obj
.fallback_target_key
= piece
.gpg_data
.sigs
[0]
840 if (piece
.piece_type
== TxtType
.signature
):
841 # to catch public keys in signature blocks
842 prepare_for_reply(piece
.gpg_data
.plainobj
, replyinfo_obj
)
845 def flatten_decrypted_payloads (eddymsg_obj
, replyinfo_obj
, get_signed_part
):
846 """For creating a string representation of a signed, encrypted part.
848 When given a decrypted payload, it will add either the plaintext or signed
849 plaintext to the reply message, depeding on 'get_signed_part'. This is
850 useful for ensuring that the reply message only comes from a signed and
851 ecrypted GPG message. It also sets the target_key for encrypting the reply
852 if it's told to get signed text only.
855 eddymsg_obj: the message in EddyMsg format created by decrypting GPG
857 replyinfo_obj: a ReplyInfo object for holding the message to quote and
858 the target_key to encrypt to.
859 get_signed_part: True if we should only include text that contains a
860 further signature. If False, then include plain text.
866 The EddyMsg instance passed in should be a piece.gpg_data.plainobj
867 which represents decrypted text. It may or may not be signed on that
871 the ReplyInfo instance may have a new 'target_key' set and its
872 'msg_to_quote' will be updated with (possibly signed) plaintext, if any
876 if eddymsg_obj
== None:
879 # recurse on multi-part mime
880 if eddymsg_obj
.multipart
== True:
881 for sub
in eddymsg_obj
.subparts
:
882 flatten_decrypted_payloads(sub
, replyinfo_obj
, get_signed_part
)
884 for piece
in eddymsg_obj
.payload_pieces
:
885 if (get_signed_part
):
886 if ((piece
.piece_type
== TxtType
.detachedsig
) \
887 or (piece
.piece_type
== TxtType
.signature
)) \
888 and (piece
.gpg_data
!= None) \
889 and (piece
.gpg_data
.plainobj
!= None):
890 flatten_decrypted_payloads(piece
.gpg_data
.plainobj
, replyinfo_obj
, False)
891 replyinfo_obj
.target_key
= piece
.gpg_data
.sigs
[0]
894 if piece
.piece_type
== TxtType
.text
:
895 replyinfo_obj
.msg_to_quote
+= piece
.string
898 def get_key_from_fp (replyinfo_obj
, gpgme_ctx
):
899 """Obtains a public key object from a key fingerprint
901 If the .target_key is not set, then we use .fallback_target_key, if
905 replyinfo_obj: ReplyInfo instance
906 gpgme_ctx: the gpgme context
912 Loading a key requires that we have the public key imported. This
913 requires that they email contains the pub key block, or that it was
914 previously sent to edward.
917 If the key can be loaded, then replyinfo_obj.reply_to_key points to the
918 public key object. If the key cannot be loaded, then the replyinfo_obj
919 is marked as having no public key available. If the key is not capable
920 of encryption, it will not be used, and replyinfo_obj will be marked
924 for key
in (replyinfo_obj
.target_key
, replyinfo_obj
.fallback_target_key
):
927 encrypt_to_key
= gpgme_ctx
.get_key(key
)
929 except gpgme
.GpgmeError
:
932 if encrypt_to_key
.can_encrypt
== True:
933 replyinfo_obj
.encrypt_to_key
= encrypt_to_key
934 replyinfo_obj
.have_reply_key
= True
935 replyinfo_obj
.key_can_encrypt
= True
939 replyinfo_obj
.key_cannot_encrypt
= True
943 def write_reply (replyinfo_obj
):
944 """Write the reply email body about the GPG successes/failures.
946 The reply is about whether decryption, sig verification and key
947 import/loading was successful or failed. If text was successfully decrypted
948 and verified, then the first instance of such text will be included in
952 replyinfo_obj: contains details of GPG processing status
955 the plaintext message to be sent to the user
958 replyinfo_obj should be populated with info about GPG processing status.
963 if (replyinfo_obj
.pubkey_success
== True):
964 reply_plain
+= replyinfo_obj
.replies
['greeting']
965 reply_plain
+= "\n\n"
968 if replyinfo_obj
.decrypt_success
== True:
969 debug('decrypt success')
970 reply_plain
+= replyinfo_obj
.replies
['success_decrypt']
972 if (replyinfo_obj
.sig_success
== True) and (replyinfo_obj
.have_reply_key
== True):
973 debug('message quoted')
974 reply_plain
+= replyinfo_obj
.replies
['space']
975 reply_plain
+= replyinfo_obj
.replies
['quote_follows']
976 reply_plain
+= "\n\n"
977 quoted_text
= email_quote_text(replyinfo_obj
.msg_to_quote
)
978 reply_plain
+= quoted_text
980 reply_plain
+= "\n\n"
982 elif replyinfo_obj
.decrypt_failure
== True:
983 debug('decrypt failure')
984 reply_plain
+= replyinfo_obj
.replies
['failed_decrypt']
985 reply_plain
+= "\n\n"
988 if replyinfo_obj
.sig_success
== True:
989 debug('signature success')
990 reply_plain
+= replyinfo_obj
.replies
['sig_success']
991 reply_plain
+= "\n\n"
993 elif replyinfo_obj
.sig_failure
== True:
994 debug('signature failure')
995 reply_plain
+= replyinfo_obj
.replies
['sig_failure']
996 reply_plain
+= "\n\n"
999 if (replyinfo_obj
.pubkey_success
== True):
1000 debug('public key received')
1001 reply_plain
+= replyinfo_obj
.replies
['public_key_received']
1002 reply_plain
+= "\n\n"
1004 elif (replyinfo_obj
.sigkey_missing
== True):
1005 debug('no public key')
1006 reply_plain
+= replyinfo_obj
.replies
['no_public_key']
1007 reply_plain
+= "\n\n"
1009 elif (replyinfo_obj
.key_can_encrypt
== False) \
1010 and (replyinfo_obj
.key_cannot_encrypt
== True):
1011 debug('bad public key')
1012 reply_plain
+= replyinfo_obj
.replies
['no_public_key']
1013 reply_plain
+= "\n\n"
1016 if (reply_plain
== ""):
1017 debug('plaintext message')
1018 reply_plain
+= replyinfo_obj
.replies
['failed_decrypt']
1019 reply_plain
+= "\n\n"
1022 reply_plain
+= replyinfo_obj
.replies
['signature']
1023 reply_plain
+= "\n\n"
1028 def add_gpg_key (key_block
, gpgme_ctx
):
1029 """Adds a GPG pubkey to the local keystore
1031 This adds keys received through email into the key store so they can be
1035 key_block: the string form of the ascii-armored public key block
1036 gpgme_ctx: the gpgme context
1039 the fingerprint(s) of the imported key(s) which can be used for
1040 encryption, and a boolean marking whether none of the keys are capable
1044 fp
= io
.BytesIO(key_block
.encode('ascii'))
1047 result
= gpgme_ctx
.import_(fp
)
1048 imports
= result
.imports
1049 except gpgme
.GpgmeError
:
1052 key_fingerprints
= []
1053 key_cannot_encrypt
= False
1055 for import_res
in imports
:
1056 fingerprint
= import_res
[0]
1059 key_obj
= gpgme_ctx
.get_key(fingerprint
)
1063 if key_obj
.can_encrypt
== True:
1064 key_fingerprints
+= [fingerprint
]
1065 key_cannot_encrypt
= False
1067 debug("added gpg key: " + fingerprint
)
1069 elif key_fingerprints
== []:
1070 key_cannot_encrypt
= True
1072 return (key_fingerprints
, key_cannot_encrypt
)
1075 def verify_sig_message (msg_block
, gpgme_ctx
):
1076 """Verifies the signature of a signed, ascii-armored block of text.
1078 It encodes the string into ascii, since binary GPG files are currently
1079 unsupported, and alternative, the ascii-armored format is encodable into
1083 msg_block: a GPG Message block in string form. It may be encrypted or
1084 not. If it is encrypted, it will return empty results.
1085 gpgme_ctx: the gpgme context
1088 A tuple containing the plaintext bytes of the signed part, the list of
1089 fingerprints of encryption-capable keys signing the data, a boolean
1090 marking whether edward is missing all public keys for validating any of
1091 the signatures, and a boolean marking whether all sigs' keys are
1092 incapable of encryption. If verification failed, perhaps because the
1093 message was also encrypted, sensible default values are returned.
1096 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
1097 plain_b
= io
.BytesIO()
1100 sigs
= gpgme_ctx
.verify(block_b
, None, plain_b
)
1101 except gpgme
.GpgmeError
:
1102 return ("",[],False,False)
1104 plaintext_b
= plain_b
.getvalue()
1106 (fingerprints
, sigkey_missing
, key_cannot_encrypt
) = get_signature_fp(sigs
, gpgme_ctx
)
1108 return (plaintext_b
, fingerprints
, sigkey_missing
, key_cannot_encrypt
)
1111 def verify_detached_signature (detached_sig
, plaintext_bytes
, gpgme_ctx
):
1112 """Verifies the signature of a detached signature.
1114 This requires the signature part and the signed part as separate arguments.
1117 detached_sig: the signature part of the detached signature
1118 plaintext_bytes: the byte form of the message being signed.
1119 gpgme_ctx: the gpgme context
1122 A tuple containging a list of encryption capable signing fingerprints
1123 if the signature verification was sucessful, a boolean marking whether
1124 edward is missing all public keys for validating any of the signatures,
1125 and a boolean marking whether all signing keys are incapable of
1126 encryption. Otherwise, a tuple containing an empty list and True are
1130 detached_sig_fp
= io
.BytesIO(detached_sig
.encode('ascii'))
1131 plaintext_fp
= io
.BytesIO(plaintext_bytes
)
1134 sigs
= gpgme_ctx
.verify(detached_sig_fp
, plaintext_fp
, None)
1135 except gpgme
.GpgmeError
:
1136 return ([],False,False)
1138 (fingerprints
, sigkey_missing
, key_cannot_encrypt
) = get_signature_fp(sigs
, gpgme_ctx
)
1140 return (fingerprints
, sigkey_missing
, key_cannot_encrypt
)
1143 def decrypt_block (msg_block
, gpgme_ctx
):
1144 """Decrypts a block of GPG text and verifies any included sigatures.
1146 Some encypted messages have embeded signatures, so those are verified too.
1149 msg_block: the encrypted(/signed) text
1150 gpgme_ctx: the gpgme context
1153 A tuple containing plaintext bytes, encryption-capable signatures (if
1154 decryption and signature verification were successful, respectively), a
1155 boolean marking whether edward is missing all public keys for
1156 validating any of the signatures, and a boolean marking whether all
1157 signature keys are incapable of encryption.
1160 block_b
= io
.BytesIO(msg_block
.encode('ascii'))
1161 plain_b
= io
.BytesIO()
1164 sigs
= gpgme_ctx
.decrypt_verify(block_b
, plain_b
)
1165 except gpgme
.GpgmeError
:
1166 return ("",[],False,False)
1168 plaintext_b
= plain_b
.getvalue()
1170 (fingerprints
, sigkey_missing
, key_cannot_encrypt
) = get_signature_fp(sigs
, gpgme_ctx
)
1172 return (plaintext_b
, fingerprints
, sigkey_missing
, key_cannot_encrypt
)
1175 def get_signature_fp (sigs
, gpgme_ctx
):
1176 """Selects valid signatures from output of gpgme signature verifying functions
1178 get_signature_fp returns a list of valid signature fingerprints if those
1179 fingerprints are associated with available keys capable of encryption.
1182 sigs: a signature verification result object list
1183 gpgme_ctx: a gpgme context
1186 fingerprints: a list of fingerprints
1187 sigkey_missing: a boolean marking whether public keys are missing for
1188 all available signatures.
1189 key_cannot_encrypt: a boolearn marking whether available public keys are
1190 incapable of encryption.
1193 sigkey_missing
= False
1194 key_cannot_encrypt
= False
1198 if (sig
.summary
== 0) or (sig
.summary
& gpgme
.SIGSUM_VALID
!= 0) or (sig
.summary
& gpgme
.SIGSUM_GREEN
!= 0):
1200 key_obj
= gpgme_ctx
.get_key(sig
.fpr
)
1202 if fingerprints
== []:
1203 sigkey_missing
= True
1206 if key_obj
.can_encrypt
== True:
1207 fingerprints
+= [sig
.fpr
]
1208 key_cannot_encrypt
= False
1209 sigkey_missing
= False
1211 elif fingerprints
== []:
1212 key_cannot_encrypt
= True
1214 elif fingerprints
== []:
1215 if (sig
.summary
& gpgme
.SIGSUM_KEY_MISSING
!= 0):
1216 sigkey_missing
= True
1218 return (fingerprints
, sigkey_missing
, key_cannot_encrypt
)
1221 def email_to_from_subject (email_bytes
):
1222 """Returns the values of the email's To:, From: and Subject: fields
1224 Returns this information from an email.
1227 email_bytes: the byte string form of the email
1230 the email To:, From:, and Subject: fields as strings
1233 email_struct
= email
.parser
.BytesParser().parsebytes(email_bytes
)
1235 email_to
= email_struct
['To']
1236 email_from
= email_struct
['From']
1237 email_subject
= email_struct
['Subject']
1239 return email_to
, email_from
, email_subject
1242 def import_lang_pick_address(email_to
, hostname
):
1243 """Imports language file for i18n support; makes reply from address
1245 The language imported depends on the To: address of the email received by
1246 edward. an -en ending implies the English language, whereas a -ja ending
1247 implies Japanese. The list of supported languages is listed in the 'langs'
1248 list at the beginning of the program. This function also chooses the
1249 language-dependent address which can be used as the From address in the
1253 email_to: the string containing the email address that the mail was
1255 hostname: the hostname part of the reply email's from address
1258 the reference to the imported language module. The only variable in
1259 this file is the 'replies' dictionary.
1265 if email_to
!= None:
1267 if "edward-" + lang
in email_to
:
1271 lang_mod_name
= "lang." + re
.sub('-', '_', use_lang
)
1272 lang_module
= importlib
.import_module(lang_mod_name
)
1274 reply_from
= "edward-" + use_lang
+ "@" + hostname
1276 return lang_module
, reply_from
1279 def generate_encrypted_mime (plaintext
, email_to
, email_subject
, encrypt_to_key
,
1281 """This function creates the mime email reply. It can encrypt the email.
1283 If the encrypt_key is included, then the email is encrypted and signed.
1284 Otherwise it is unencrypted.
1287 plaintext: the plaintext body of the message to create.
1288 email_to: the email address to reply to
1289 email_subject: the subject to use in reply
1290 encrypt_to_key: the key object to use for encrypting the email. (or
1292 gpgme_ctx: the gpgme context
1295 A string version of the mime message, possibly encrypted and signed.
1298 plaintext_mime
= MIMEText(plaintext
)
1299 plaintext_mime
.set_charset('utf-8')
1301 if (encrypt_to_key
!= None):
1303 encrypted_text
= encrypt_sign_message(plaintext_mime
.as_string(),
1307 control_mime
= MIMEApplication("Version: 1",
1308 _subtype
='pgp-encrypted',
1309 _encoder
=email
.encoders
.encode_7or8bit
)
1310 control_mime
['Content-Description'] = 'PGP/MIME version identification'
1311 control_mime
.set_charset('us-ascii')
1313 encoded_mime
= MIMEApplication(encrypted_text
,
1314 _subtype
='octet-stream; name="encrypted.asc"',
1315 _encoder
=email
.encoders
.encode_7or8bit
)
1316 encoded_mime
['Content-Description'] = 'OpenPGP encrypted message'
1317 encoded_mime
['Content-Disposition'] = 'inline; filename="encrypted.asc"'
1318 encoded_mime
.set_charset('us-ascii')
1320 message_mime
= MIMEMultipart(_subtype
="encrypted", protocol
="application/pgp-encrypted")
1321 message_mime
.attach(control_mime
)
1322 message_mime
.attach(encoded_mime
)
1323 message_mime
['Content-Disposition'] = 'inline'
1326 message_mime
= plaintext_mime
1328 message_mime
['To'] = email_to
1329 message_mime
['Subject'] = email_subject
1331 reply
= message_mime
.as_string()
1336 def send_reply(email_txt
, subject
, reply_to
, reply_from
):
1338 email_bytes
= email_txt
.encode('ascii')
1340 p
= subprocess
.Popen(["/usr/sbin/sendmail", "-f", reply_from
, "-F", "Edward, GPG Bot", "-i", reply_to
], stdin
=subprocess
.PIPE
)
1342 (stdout
, stderr
) = p
.communicate(email_bytes
)
1345 debug("sendmail stdout: " + str(stdout
))
1347 error("sendmail stderr: " + str(stderr
))
1350 def email_quote_text (text
):
1351 """Quotes input text by inserting "> "s
1353 This is useful for quoting a text for the reply message. It inserts "> "
1354 strings at the beginning of lines.
1357 text: plain text to quote
1363 quoted_message
= re
.sub(r
'^', r
'> ', text
, flags
=re
.MULTILINE
)
1365 return quoted_message
1368 def encrypt_sign_message (plaintext
, encrypt_to_key
, gpgme_ctx
):
1369 """Encrypts and signs plaintext
1371 This encrypts and signs a message.
1374 plaintext: text to sign and ecrypt
1375 encrypt_to_key: the key object to encrypt to
1376 gpgme_ctx: the gpgme context
1379 An encrypted and signed string of text
1382 # the plaintext should be mime encoded in an ascii-compatible form
1383 plaintext_bytes
= io
.BytesIO(plaintext
.encode('ascii'))
1384 encrypted_bytes
= io
.BytesIO()
1386 gpgme_ctx
.encrypt_sign([encrypt_to_key
], gpgme
.ENCRYPT_ALWAYS_TRUST
,
1387 plaintext_bytes
, encrypted_bytes
)
1389 encrypted_txt
= encrypted_bytes
.getvalue().decode('ascii')
1390 return encrypted_txt
1393 def error (error_msg
):
1394 """Write an error message to stdout
1396 The error message includes the program name.
1399 error_msg: the message to print
1405 An error message is printed to stdout
1408 sys
.stderr
.write(progname
+ ": " + str(error_msg
) + "\n")
1411 def debug (debug_msg
):
1412 """Writes a debug message to stdout if debug == True
1414 If the debug option is set in edward_config.py, then the passed message
1415 gets printed to stdout.
1418 debug_msg: the message to print to stdout
1424 A debug message is printed to stdout
1427 if edward_config
.debug
== True:
1432 """Sets the progname variable and processes optional argument
1434 If there are more than two arguments then edward complains and quits. An
1435 single "-p" argument sets the print_reply_only option, which makes edward
1436 print email replies instead of mailing them.
1442 True if edward should print arguments instead of mailing them,
1443 otherwise it returns False.
1446 Exits with error 1 if there are more than two arguments, otherwise
1447 returns the print_reply_only option.
1451 progname
= sys
.argv
[0]
1453 print_reply_only
= False
1455 if len(sys
.argv
) > 2:
1456 print(progname
+ " usage: " + progname
+ " [-p]\n\n" \
1457 + " -p print reply message to stdout, do not mail it\n", \
1461 elif (len(sys
.argv
) == 2) and (sys
.argv
[1] == "-p"):
1462 print_reply_only
= True
1464 return print_reply_only
1467 if __name__
== "__main__":
1468 """Executes main if this file is not loaded interactively"""