variable name changes
[edward.git] / edward
CommitLineData
0bec96d6 1#! /usr/bin/env python3
ff4136c7 2# -*- coding: utf-8 -*-
0bec96d6
AE
3"""*********************************************************************
4* Edward is free software: you can redistribute it and/or modify *
5* it under the terms of the GNU Affero Public License as published by *
6* the Free Software Foundation, either version 3 of the License, or *
7* (at your option) any later version. *
8* *
9* Edward is distributed in the hope that it will be useful, *
10* but WITHOUT ANY WARRANTY; without even the implied warranty of *
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12* GNU Affero Public License for more details. *
13* *
14* You should have received a copy of the GNU Affero Public License *
15* along with Edward. If not, see <http://www.gnu.org/licenses/>. *
16* *
17* Copyright (C) 2014-2015 Andrew Engelbrecht (AGPLv3+) *
18* Copyright (C) 2014 Josh Drake (AGPLv3+) *
19* Copyright (C) 2014 Lisa Marie Maginnis (AGPLv3+) *
8bdfb6d4
AE
20* Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) *
21* Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) *
0bec96d6
AE
22* *
23* Special thanks to Josh Drake for writing the original edward bot! :) *
24* *
25************************************************************************
26
a5385c04 27Code sourced from these projects:
0bec96d6 28
8bdfb6d4
AE
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
0bec96d6
AE
32"""
33
0bec96d6
AE
34import re
35import io
40c37ab3 36import os
2f4ce2b2 37import sys
0fd3389f 38import enum
2f4ce2b2 39import gpgme
adcef2f7 40import importlib
0bec96d6 41
8bdfb6d4
AE
42import email.parser
43import email.message
44import email.encoders
45
46from email.mime.multipart import MIMEMultipart
47from email.mime.application import MIMEApplication
48from email.mime.nonmultipart import MIMENonMultipart
49
40c37ab3 50import edward_config
c96f3837 51
5b0e37d3 52langs = ["de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
adcef2f7 53
def9196c
AE
54"""This list contains the abbreviated names of reply languages available to
55edward."""
56
0fd3389f
AE
57class TxtType (enum.Enum):
58 text = 0
59 message = 1
60 pubkey = 2
61 detachedsig = 3
62 signature = 4
def9196c 63
0fd3389f 64
2f4102b9 65match_pairs = [(TxtType.message,
56578eaf 66 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
0fd3389f 67 (TxtType.pubkey,
56578eaf 68 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
0fd3389f 69 (TxtType.detachedsig,
38738401 70 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
56578eaf 71
def9196c
AE
72"""This list of tuples matches query names with re.search() queries used
73to find GPG data for edward to process."""
74
56578eaf
AE
75
76class EddyMsg (object):
def9196c
AE
77 """
78 The EddyMsg class represents relevant parts of a mime message.
79
80 The represented message can be single-part or multi-part.
81
82 'multipart' is set to True if there are multiple mime parts.
83
84 'subparts' points to a list of mime sub-parts if it is a multi-part
85 message. Otherwise it points to an empty list.
86
6d8faaa7
AE
87 'payload_bytes' is a binary representation of the mime part before header
88 removal and message decoding.
def9196c
AE
89
90 'payload_pieces' is a list of objects containing strings that when strung
91 together form the fully-decoded string representation of the mime part.
92
def9196c
AE
93 The 'filename', 'content_type' and 'description_list' come from the mime
94 part parameters.
95 """
96
7d0b91d2
AE
97 multipart = False
98 subparts = []
56578eaf 99
7d0b91d2
AE
100 payload_bytes = None
101 payload_pieces = []
56578eaf 102
7d0b91d2
AE
103 filename = None
104 content_type = None
105 description_list = None
56578eaf
AE
106
107
108class PayloadPiece (object):
def9196c
AE
109 """
110 PayloadPiece represents a complte or sub-section of a mime part.
111
112 Instances of this class are often strung together within one or more arrays
113 pointed to by each instance of the EddyMsg class.
114
0fd3389f
AE
115 'piece_type' refers to an enum whose value describes the content of
116 'string'. Examples include TxtType.pubkey, for public keys, and
117 TxtType.message, for encrypted data (or armored signatures until they are
118 known to be such.) Some of the names derive from the header and footer of
119 each of these ascii-encoded gpg blocks.
def9196c
AE
120
121 'string' contains some string of text, such as non-GPG text, an encrypted
122 block of text, a signature, or a public key.
123
124 'gpg_data' points to any instances of GPGData that have been created based
125 on the contents of 'string'.
126 """
127
7d0b91d2
AE
128 piece_type = None
129 string = None
130 gpg_data = None
56578eaf
AE
131
132
38738401 133class GPGData (object):
def9196c
AE
134 """
135 GPGData holds info from decryption, sig. verification, and/or pub. keys.
136
137 Instances of this class contain decrypted information, signature
138 fingerprints and/or fingerprints of processed and imported public keys.
139
140 'decrypted' is set to True if 'plainobj' was created from encrypted data.
141
142 'plainobj' points to any decrypted, or signed part of, a GPG signature. It
143 is intended to be an instance of the EddyMsg class.
144
145 'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
146
147 'keys' is a list of fingerprints of keys obtained in public key blocks.
148 """
149
7d0b91d2
AE
150 decrypted = False
151
152 plainobj = None
153 sigs = []
154 keys = []
38738401 155
38738401 156
d873ff48 157class ReplyInfo (object):
def9196c
AE
158 """
159 ReplyInfo contains details that edward uses in generating its reply.
160
161 Instances of this class contain information about whether a message was
162 successfully encrypted or signed, and whether a public key was attached, or
163 retrievable, from the local GPG store. It stores the fingerprints of
164 potential encryption key candidates and the message (if any at all) to
165 quote in edward's reply.
166
167 'replies' points one of the dictionaries of translated replies.
168
169 'target_key' refers to the fingerprint of a key used to sign encrypted
170 data. This is the preferred key, if it is set, and if is available.
171
172 'fallback_target_key' referst to the fingerprint of a key used to sign
173 unencrypted data; alternatively it may be a public key attached to the
174 message.
175
176 'msg_to_quote' refers to the part of a message which edward should quote in
177 his reply. This should remain as None if there was no encrypted and singed
178 part. This is to avoid making edward a service for decrypting other
179 people's messages to edward.
180
181 'success_decrypt' is set to True if edward could decrypt part of the
182 message.
183
184 'failed_decrypt' is set to True if edward failed to decrypt part of the
185 message.
186
187 'publick_key_received' is set to True if edward successfully imported a
188 public key.
189
190 'no_public_key' is set to True if edward doesn't have a key to encrypt to
191 when replying to the user.
192
193 'sig_success' is set to True if edward could to some extent verify the
194 signature of a signed part of the message to edward.
195
196 'sig_failure' is set to True if edward failed to some extent verify the
197 signature of a signed part of the message to edward.
198 """
199
7d0b91d2 200 replies = None
6906d46d 201
7d0b91d2
AE
202 target_key = None
203 fallback_target_key = None
204 msg_to_quote = ""
d873ff48 205
7d0b91d2
AE
206 success_decrypt = False
207 failed_decrypt = False
208 public_key_received = False
209 no_public_key = False
210 sig_success = False
211 sig_failure = False
d873ff48 212
38738401 213
0bec96d6
AE
214def main ():
215
def9196c
AE
216 """
217 This is the main function for edward, a GPG reply bot.
218
219 Edward responds to GPG-encrypted and signed mail, encrypting and signing
220 the response if the user's public key is, or was, included in the message.
221
222 Args:
223 None
224
225 Returns:
a3d6aff8 226 Nothing
def9196c
AE
227
228 Pre:
229 Mime or plaintext email passing in through standard input. Portions of
230 the email may be encrypted. If the To: address contains the text
231 "edward-ja", then the reply will contain a reply written in the
232 Japanese language. There are other languages as well. The default
233 language is English.
234
235 Post:
236 A reply email will be printed to standard output. The contents of the
237 reply email depends on whether the original email was encrypted or not,
238 has or doesn't have a signature, whether a public key used in the
239 original message is provided or locally stored, and the language
240 implied by the To: address in the original email.
241 """
242
20f6e7c5 243 handle_args()
0bec96d6 244
0a064403
AE
245 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
246 edward_config.sign_with_key)
247
c96f3837 248 email_text = sys.stdin.read()
adcef2f7
AE
249 email_struct = parse_pgp_mime(email_text, gpgme_ctx)
250
251 email_to, email_from, email_subject = email_to_from_subject(email_text)
252 lang = import_lang(email_to)
fafa21c3 253
d873ff48
AE
254 replyinfo_obj = ReplyInfo()
255 replyinfo_obj.replies = lang.replies
256
257 prepare_for_reply(email_struct, replyinfo_obj)
013f7cb8 258 encrypt_to_key = get_key_from_fp(replyinfo_obj, gpgme_ctx)
d873ff48 259 reply_plaintext = write_reply(replyinfo_obj)
adcef2f7 260
2007103e
AE
261 reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
262 email_subject, encrypt_to_key,
263 gpgme_ctx)
1fccb295 264
2007103e 265 print(reply_mime)
c96f3837 266
0bec96d6 267
0a064403 268def get_gpg_context (gnupghome, sign_with_key_fp):
a3d6aff8
AE
269 """
270 This function returns the GPG context needed for encryption and signing.
271
272 The context is needed by other functions which use GPG functionality.
273
274 Args:
275 gnupghome: The path to "~/.gnupg/" or its alternative.
276 sign_with_key: The fingerprint of the key to sign with
277
278 Returns:
279 A gpgme context to be used for GPG functions.
280
281 Post:
282 the 'armor' flag is set to True and the list of signing keys contains
283 the single specified key
284 """
0a064403
AE
285
286 os.environ['GNUPGHOME'] = gnupghome
287
288 gpgme_ctx = gpgme.Context()
289 gpgme_ctx.armor = True
290
291 try:
292 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
5f6f32b1 293 except gpgme.GpgmeError:
0a064403
AE
294 error("unable to load signing key. is the gnupghome "
295 + "and signing key properly set in the edward_config.py?")
296 exit(1)
297
298 gpgme_ctx.signers = [sign_with_key]
299
300 return gpgme_ctx
301
302
38738401 303def parse_pgp_mime (email_text, gpgme_ctx):
a3d6aff8
AE
304 """Parses the email for mime payloads and decrypts/verfies signatures.
305
306 This function creates a representation of a mime or plaintext email with
307 the EddyMsg class. It then splits each mime payload into one or more pieces
308 which may be plain text or GPG data. It then decrypts encrypted parts and
309 does some very basic signature verification on those parts.
310
311 Args:
312 email_text: an email message in string format
313 gpgme_ctx: a gpgme context
314
315 Returns:
316 A message as an instance of EddyMsg
317
318 Post:
319 the returned EddyMsg instance has split, decrypted, verified and pubkey
320 imported payloads
321 """
394a1476
AE
322
323 email_struct = email.parser.Parser().parsestr(email_text)
324
928e3819
AE
325 eddymsg_obj = parse_mime(email_struct)
326 split_payloads(eddymsg_obj)
327 gpg_on_payloads(eddymsg_obj, gpgme_ctx)
8bb4b0d5 328
928e3819 329 return eddymsg_obj
0bec96d6 330
0bec96d6 331
56578eaf 332def parse_mime(msg_struct):
a3d6aff8
AE
333 """Translates python's email.parser format into an EddyMsg format
334
335 If the message is multi-part, then a recursive object is created, where
336 each sub-part is also a EddyMsg instance.
337
338 Args:
339 msg_struct: an email parsed with email.parser.Parser(), which can be
340 multi-part
341
342 Returns:
343 an instance of EddyMsg, potentially a recursive one.
344 """
0bec96d6 345
928e3819 346 eddymsg_obj = EddyMsg()
8bb4b0d5 347
56578eaf
AE
348 if msg_struct.is_multipart() == True:
349 payloads = msg_struct.get_payload()
0bec96d6 350
928e3819
AE
351 eddymsg_obj.multipart = True
352 eddymsg_obj.subparts = list(map(parse_mime, payloads))
dd11a483 353
56578eaf 354 else:
928e3819 355 eddymsg_obj = get_subpart_data(msg_struct)
394a1476 356
928e3819 357 return eddymsg_obj
c267c233 358
80119cab 359
2f4102b9 360def scan_and_split (payload_piece, match_name, pattern):
a3d6aff8
AE
361 """This splits the payloads of an EddyMsg object into GPG and text parts.
362
363 An EddyMsg object's payload_pieces starts off as a list containing a single
364 PayloadPiece object. This function returns a list of these objects which
365 have been split into GPG data and regular text, if such splits need to be/
366 can be made.
367
368 Args:
369 payload_piece: a single payload or a split part of a payload
2f4102b9 370 match_name: the type of data to try to spit out from the payload piece
a3d6aff8
AE
371 pattern: the search pattern to be used for finding that type of data
372
373 Returns:
374 a list of objects of the PayloadPiece class, in the order that the
375 string part of payload_piece originally was, broken up according to
376 matches specified by 'pattern'.
377 """
cf75de65 378
a5d37d44 379 # don't try to re-split pieces containing gpg data
0fd3389f 380 if payload_piece.piece_type != TxtType.text:
a5d37d44
AE
381 return [payload_piece]
382
56578eaf
AE
383 flags = re.DOTALL | re.MULTILINE
384 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
385 ")(?P<rest>.*)", payload_piece.string, flags=flags)
86663388 386
56578eaf
AE
387 if matches == None:
388 pieces = [payload_piece]
c96f3837 389
56578eaf 390 else:
d437f8b2 391
56578eaf
AE
392 beginning = PayloadPiece()
393 beginning.string = matches.group('beginning')
394 beginning.piece_type = payload_piece.piece_type
d437f8b2 395
56578eaf
AE
396 match = PayloadPiece()
397 match.string = matches.group('match')
2f4102b9 398 match.piece_type = match_name
d437f8b2 399
56578eaf
AE
400 rest = PayloadPiece()
401 rest.string = matches.group('rest')
402 rest.piece_type = payload_piece.piece_type
d437f8b2 403
2f4102b9 404 more_pieces = scan_and_split(rest, match_name, pattern)
4615b156 405 pieces = [beginning, match ] + more_pieces
d437f8b2 406
56578eaf 407 return pieces
d437f8b2 408
d437f8b2 409
56578eaf 410def get_subpart_data (part):
a3d6aff8
AE
411 """This function grabs information from a single part mime object.
412
413 It copies needed data from a single part email.parser.Parser() object over
414 to an EddyMsg object.
415
416 Args:
417 part: a non-multi-part mime.parser.Parser() object
418
419 Returns:
420 a single-part EddyMsg() object
421 """
0bec96d6 422
6d8faaa7
AE
423 charset = part.get_content_charset()
424 mime_decoded_bytes = part.get_payload(decode=True)
0bec96d6 425
6d8faaa7
AE
426 obj = EddyMsg()
427 obj.payload_bytes = part.as_bytes()
56578eaf
AE
428
429 obj.filename = part.get_filename()
430 obj.content_type = part.get_content_type()
431 obj.description_list = part['content-description']
432
433 # your guess is as good as a-myy-ee-ine...
6d8faaa7
AE
434 if charset == None:
435 charset = 'utf-8'
56578eaf 436
6d8faaa7 437 if mime_decoded_bytes != None:
0eb75d9c
AE
438 try:
439 payload = PayloadPiece()
6d8faaa7 440 payload.string = mime_decoded_bytes.decode(charset)
0fd3389f 441 payload.piece_type = TxtType.text
0eb75d9c
AE
442
443 obj.payload_pieces = [payload]
444 except UnicodeDecodeError:
445 pass
56578eaf
AE
446
447 return obj
448
449
928e3819 450def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
a3d6aff8
AE
451 """A function which maps another function onto a message's subparts.
452
453 This is a higer-order function which recursively performs a specified
454 function on each subpart of a multi-part message. Each single-part sub-part
455 has the function applied to it. This function also works if the part passed
456 in is single-part.
457
458 Args:
459 function_to_do: function to perform on sub-parts
460 eddymsg_obj: a single part or multi-part EddyMsg object
461 data: a second argument to pass to 'function_to_do'
462
463 Returns:
464 Nothing
465
466 Post:
467 The passed-in EddyMsg object is transformed recursively on its
468 sub-parts according to 'function_to_do'.
469 """
56578eaf 470
928e3819
AE
471 if eddymsg_obj.multipart == True:
472 for sub in eddymsg_obj.subparts:
d873ff48 473 do_to_eddys_pieces(function_to_do, sub, data)
394a1476 474 else:
928e3819 475 function_to_do(eddymsg_obj, data)
dd11a483
AE
476
477
928e3819 478def split_payloads (eddymsg_obj):
a3d6aff8
AE
479 """Splits all (sub-)payloads of a message into GPG data and regular text.
480
481 Recursively performs payload splitting on all sub-parts of an EddyMsg
482 object into the various GPG data types, such as GPG messages, public key
483 blocks and signed text.
484
485 Args:
486 eddymsg_obj: an instance of EddyMsg
487
488 Returns:
489 Nothing
490
491 Pre:
492 The EddyMsg object has payloads that are unsplit (by may be split)..
493
494 Post:
495 The EddyMsg object's payloads are all split into GPG and non-GPG parts.
496 """
a5d37d44 497
2f4102b9
AE
498 for match_pair in match_pairs:
499 do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_pair)
a5d37d44 500
a5d37d44 501
2f4102b9 502def split_payload_pieces (eddymsg_obj, match_pair):
a3d6aff8
AE
503 """A helper function for split_payloads(); works on PayloadPiece objects.
504
505 This function splits up PayloadPiece objects into multipe PayloadPiece
506 objects and replaces the EddyMsg object's previous list of payload pieces
507 with the new split up one.
508
509 Args:
510 eddymsg_obj: a single-part EddyMsg object.
2f4102b9 511 match_pair: a tuple from the match_pairs list, which specifies a match
a3d6aff8
AE
512 name and a match pattern.
513
514 Returns:
515 Nothing
516
517 Pre:
518 The payload piece(s) of an EddyMsg object may be already split or
519 unsplit.
520
521 Post:
522 The EddyMsg object's payload piece(s) are split into a list of pieces
2f4102b9 523 if matches of the match_pair are found.
a3d6aff8 524 """
a5d37d44 525
2f4102b9 526 (match_name, pattern) = match_pair
a5d37d44
AE
527
528 new_pieces_list = []
928e3819 529 for piece in eddymsg_obj.payload_pieces:
a5d37d44
AE
530 new_pieces_list += scan_and_split(piece, match_name, pattern)
531
928e3819 532 eddymsg_obj.payload_pieces = new_pieces_list
a5d37d44
AE
533
534
928e3819 535def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
a3d6aff8
AE
536 """Performs GPG operations on the GPG parts of the message
537
538 This function decrypts text, verifies signatures, and imports public keys
539 included in an email.
540
541 Args:
542 eddymsg_obj: an EddyMsg object with its payload_pieces split into GPG
543 and non-GPG sections by split_payloads()
544 gpgme_ctx: a gpgme context
545
546 prev_parts: a list of mime parts that occur before the eddymsg_obj
547 part, under the same multi-part mime part. This is used for
548 verifying detached signatures. For the root mime part, this should
549 be an empty list, which is the default value if this paramater is
550 omitted.
551
552 Return:
553 Nothing
554
555 Pre:
556 eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
557
558 Post:
559 Decryption, verification and key imports occur. the gpg_data member of
560 PayloadPiece objects get filled in with GPGData objects.
561 """
38738401 562
928e3819 563 if eddymsg_obj.multipart == True:
101d54a8 564 prev_parts=[]
928e3819 565 for sub in eddymsg_obj.subparts:
101d54a8
AE
566 gpg_on_payloads (sub, gpgme_ctx, prev_parts)
567 prev_parts += [sub]
38738401 568
d873ff48 569 return
38738401 570
928e3819 571 for piece in eddymsg_obj.payload_pieces:
38738401 572
0fd3389f 573 if piece.piece_type == TxtType.text:
38738401
AE
574 # don't transform the plaintext.
575 pass
576
0fd3389f 577 elif piece.piece_type == TxtType.message:
7e18f14d 578 (plaintext, sigs) = decrypt_block(piece.string, gpgme_ctx)
38738401
AE
579
580 if plaintext:
581 piece.gpg_data = GPGData()
b23e171d 582 piece.gpg_data.decrypted = True
38738401
AE
583 piece.gpg_data.sigs = sigs
584 # recurse!
585 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
3a753fd0
AE
586 continue
587
588 # if not encrypted, check to see if this is an armored signature.
589 (plaintext, sigs) = verify_sig_message(piece.string, gpgme_ctx)
590
591 if plaintext:
0fd3389f 592 piece.piece_type = TxtType.signature
3a753fd0
AE
593 piece.gpg_data = GPGData()
594 piece.gpg_data.sigs = sigs
595 # recurse!
596 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
129543c3 597
d7d8bf7c
AE
598 # FIXME: consider handling pubkeys first, so that signatures can be
599 # validated on freshly imported keys
0fd3389f 600 elif piece.piece_type == TxtType.pubkey:
f8ee6bd3 601 key_fps = add_gpg_key(piece.string, gpgme_ctx)
8f61c66a 602
f8ee6bd3 603 if key_fps != []:
8f61c66a 604 piece.gpg_data = GPGData()
f8ee6bd3 605 piece.gpg_data.keys = key_fps
129543c3 606
0fd3389f 607 elif piece.piece_type == TxtType.detachedsig:
101d54a8 608 for prev in prev_parts:
6d8faaa7 609 sig_fps = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx)
101d54a8 610
6d240f68
AE
611 if sig_fps != []:
612 piece.gpg_data = GPGData()
613 piece.gpg_data.sigs = sig_fps
614 piece.gpg_data.plainobj = prev
615 break
7e18f14d 616
38738401
AE
617 else:
618 pass
619
620
928e3819 621def prepare_for_reply (eddymsg_obj, replyinfo_obj):
67691346
AE
622 """Updates replyinfo_obj with info on the message's GPG success/failures
623
624 This function marks replyinfo_obj with information about whether encrypted
625 text in eddymsg_obj was successfully decrypted, signatures were verified
626 and whether a public key was found or not.
627
628 Args:
629 eddymsg_obj: a message in the EddyMsg format
630 replyinfo_obj: an instance of ReplyInfo
631
632 Returns:
633 Nothing
634
635 Pre:
636 eddymsg_obj has had its gpg_data created by gpg_on_payloads
637
638 Post:
639 replyinfo_obj has been updated with info about decryption/sig
640 verififcation status, etc. However the desired key isn't imported until
641 later, so the success or failure of that updates the values set here.
642 """
dd11a483 643
928e3819 644 do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
56578eaf 645
928e3819 646def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
67691346
AE
647 """A helper function for prepare_for_reply
648
649 It updates replyinfo_obj with GPG success/failure information, when
650 supplied a single-part EddyMsg object.
651
652 Args:
653 eddymsg_obj: a single-part message in the EddyMsg format
654 replyinfo_obj: an object which holds information about the message's
655 GPG status
656
657 Returns:
658 Nothing
659
660 Pre:
661 eddymsg_obj is a single-part message. (it may be a part of a multi-part
662 message.) It has had its gpg_data created by gpg_on_payloads if it has
663 gpg data.
664
665 Post:
666 replyinfo_obj has been updated with gpg success/failure information
667 """
56578eaf 668
928e3819 669 for piece in eddymsg_obj.payload_pieces:
0fd3389f 670 if piece.piece_type == TxtType.text:
d873ff48
AE
671 # don't quote the plaintext part.
672 pass
673
0fd3389f 674 elif piece.piece_type == TxtType.message:
05683768 675 prepare_for_reply_message(piece, replyinfo_obj)
d873ff48 676
0fd3389f 677 elif piece.piece_type == TxtType.pubkey:
05683768 678 prepare_for_reply_pubkey(piece, replyinfo_obj)
6906d46d 679
0fd3389f
AE
680 elif (piece.piece_type == TxtType.detachedsig) \
681 or (piece.piece_type == TxtType.signature):
05683768 682 prepare_for_reply_sig(piece, replyinfo_obj)
d873ff48 683
d873ff48 684
05683768 685def prepare_for_reply_message (piece, replyinfo_obj):
67691346
AE
686 """Helper function for prepare_for_reply()
687
688 This function is called when the piece_type of a payload piece is
0fd3389f 689 TxtType.message, or GPG Message block. This should be encrypted text. If the
67691346
AE
690 encryted block is signed, a sig will be attached to .target_key unless
691 there is already one there.
692
693 Args:
694 piece: a PayloadPiece object.
695 replyinfo_obj: object which gets updated with decryption status, etc.
696
697
698 Returns:
699 Nothing
700
701 Pre:
0fd3389f 702 the piece.payload_piece value should be TxtType.message.
67691346
AE
703
704 Post:
705 replyinfo_obj gets updated with decryption status, signing status and a
706 potential signing key.
707 """
44fe39b7 708
05683768
AE
709 if piece.gpg_data == None:
710 replyinfo_obj.failed_decrypt = True
711 return
712
5c40e676 713 replyinfo_obj.success_decrypt = True
05683768
AE
714
715 # we already have a key (and a message)
716 if replyinfo_obj.target_key != None:
717 return
718
719 if piece.gpg_data.sigs != []:
720 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
721 get_signed_part = False
722 else:
723 # only include a signed message in the reply.
724 get_signed_part = True
725
8c3e5397 726 flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part)
05683768
AE
727
728 # to catch public keys in encrypted blocks
729 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
730
731
732def prepare_for_reply_pubkey (piece, replyinfo_obj):
67691346
AE
733 """Helper function for prepare_for_reply(). Marks pubkey import status.
734
735 Marks replyinfo_obj with pub key import status.
736
737 Args:
738 piece: a PayloadPiece object
739 replyinfo_obj: a ReplyInfo object
740
741 Pre:
0fd3389f 742 piece.piece_type should be set to TxtType.pubkey .
67691346
AE
743
744 Post:
745 replyinfo_obj has its fields updated.
746 """
05683768
AE
747
748 if piece.gpg_data == None or piece.gpg_data.keys == []:
749 replyinfo_obj.no_public_key = True
750 else:
751 replyinfo_obj.public_key_received = True
752
3ceb92e5
AE
753 # prefer public key as a fallback for the encrypted reply
754 replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
05683768
AE
755
756
757def prepare_for_reply_sig (piece, replyinfo_obj):
67691346
AE
758 """Helper function for prepare_for_reply(). Marks sig verification status.
759
760 Marks replyinfo_obj with signature verification status.
761
762 Args:
763 piece: a PayloadPiece object
764 replyinfo_obj: a ReplyInfo object
765
766 Pre:
0fd3389f
AE
767 piece.piece_type should be set to TxtType.signature, or
768 TxtType.detachedsig .
67691346
AE
769
770 Post:
771 replyinfo_obj has its fields updated.
772 """
05683768
AE
773
774 if piece.gpg_data == None or piece.gpg_data.sigs == []:
775 replyinfo_obj.sig_failure = True
776 else:
777 replyinfo_obj.sig_success = True
778
779 if replyinfo_obj.fallback_target_key == None:
780 replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
6906d46d
AE
781
782
8c3e5397
AE
783def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
784 """For creating a string representation of a signed, encrypted part.
39e2c525 785
8c3e5397
AE
786 When given a decrypted payload, it will add either the plaintext or signed
787 plaintext to the reply message, depeding on 'get_signed_part'. This is
788 useful for ensuring that the reply message only comes from a signed and
789 ecrypted GPG message. It also sets the target_key for encrypting the reply
790 if it's told to get signed text only.
39e2c525
AE
791
792 Args:
793 eddymsg_obj: the message in EddyMsg format created by decrypting GPG
794 text
8c3e5397
AE
795 replyinfo_obj: a ReplyInfo object for holding the message to quote and
796 the target_key to encrypt to.
39e2c525
AE
797 get_signed_part: True if we should only include text that contains a
798 further signature. If False, then include plain text.
799
800 Returns:
8c3e5397 801 Nothing
39e2c525
AE
802
803 Pre:
804 The EddyMsg instance passed in should be a piece.gpg_data.plainobj
805 which represents decrypted text. It may or may not be signed on that
806 level.
d873ff48 807
8c3e5397
AE
808 Post:
809 the ReplyInfo instance may have a new 'target_key' set and its
810 'msg_to_quote' will be updated with (possibly signed) plaintext, if any
811 could be found.
812 """
d873ff48 813
0aa88c27 814 if eddymsg_obj == None:
8c3e5397 815 return
0aa88c27 816
0402995a 817 # recurse on multi-part mime
928e3819
AE
818 if eddymsg_obj.multipart == True:
819 for sub in eddymsg_obj.subparts:
8c3e5397 820 flatten_decrypted_payloads(sub, replyinfo_obj, get_signed_part)
d873ff48 821
928e3819 822 for piece in eddymsg_obj.payload_pieces:
0402995a 823 if (get_signed_part):
0fd3389f
AE
824 if ((piece.piece_type == TxtType.detachedsig) \
825 or (piece.piece_type == TxtType.signature)) \
0402995a 826 and (piece.gpg_data != None):
8c3e5397
AE
827 flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, False)
828 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
c3d417bf
AE
829 break
830 else:
0fd3389f 831 if piece.piece_type == TxtType.text:
8c3e5397 832 replyinfo_obj.msg_to_quote += piece.string
d873ff48
AE
833
834
013f7cb8 835def get_key_from_fp (replyinfo_obj, gpgme_ctx):
39e2c525
AE
836 """Obtains a public key object from a key fingerprint
837
838 If the .target_key is not set, then we use .fallback_target_key.
839
840 Args:
841 replyinfo_obj: ReplyInfo instance
842 gpgme_ctx: the gpgme context
843
844 Return:
845 The key object of the key of either the target_key or the fallback one
846 if .target_key is not set. If the key cannot be loaded, then return
847 None.
848
849 Pre:
850 Loading a key requires that we have the public key imported. This
851 requires that they email contains the pub key block, or that it was
852 previously sent to edward.
853
854 Post:
855 If the key cannot be loaded, then the replyinfo_obj is marked for
856 having no public key available.
857 """
013f7cb8 858
dc24daf1
AE
859 if replyinfo_obj.target_key == None:
860 replyinfo_obj.target_key = replyinfo_obj.fallback_target_key
861
013f7cb8
AE
862 if replyinfo_obj.target_key != None:
863 try:
864 encrypt_to_key = gpgme_ctx.get_key(replyinfo_obj.target_key)
865 return encrypt_to_key
866
5f6f32b1 867 except gpgme.GpgmeError:
013f7cb8
AE
868 pass
869
870 # no available key to use
871 replyinfo_obj.target_key = None
872 replyinfo_obj.fallback_target_key = None
873
874 replyinfo_obj.no_public_key = True
875 replyinfo_obj.public_key_received = False
876
877 return None
878
879
d873ff48 880def write_reply (replyinfo_obj):
39e2c525
AE
881 """Write the reply email body about the GPG successes/failures.
882
883 The reply is about whether decryption, sig verification and key
884 import/loading was successful or failed. If text was successfully decrypted
885 and verified, then the first instance of such text will be included in
886 quoted form.
887
888 Args:
889 replyinfo_obj: contains details of GPG processing status
890
891 Returns:
892 the plaintext message to be sent to the user
893
894 Pre:
895 replyinfo_obj should be populated with info about GPG processing status.
896 """
d873ff48
AE
897
898 reply_plain = ""
899
900 if replyinfo_obj.success_decrypt == True:
d873ff48 901 reply_plain += replyinfo_obj.replies['success_decrypt']
2694709a
AE
902
903 if replyinfo_obj.no_public_key == False:
904 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
905 reply_plain += quoted_text
d873ff48
AE
906
907 elif replyinfo_obj.failed_decrypt == True:
908 reply_plain += replyinfo_obj.replies['failed_decrypt']
909
910
911 if replyinfo_obj.sig_success == True:
912 reply_plain += "\n\n"
913 reply_plain += replyinfo_obj.replies['sig_success']
914
915 elif replyinfo_obj.sig_failure == True:
916 reply_plain += "\n\n"
917 reply_plain += replyinfo_obj.replies['sig_failure']
918
919
920 if replyinfo_obj.public_key_received == True:
921 reply_plain += "\n\n"
922 reply_plain += replyinfo_obj.replies['public_key_received']
923
924 elif replyinfo_obj.no_public_key == True:
925 reply_plain += "\n\n"
926 reply_plain += replyinfo_obj.replies['no_public_key']
927
928
929 reply_plain += "\n\n"
930 reply_plain += replyinfo_obj.replies['signature']
56578eaf 931
d873ff48 932 return reply_plain
56578eaf
AE
933
934
8f61c66a 935def add_gpg_key (key_block, gpgme_ctx):
39e2c525
AE
936 """Adds a GPG pubkey to the local keystore
937
938 This adds keys received through email into the key store so they can be
939 used later.
940
941 Args:
942 key_block: the string form of the ascii-armored public key block
943 gpgme_ctx: the gpgme context
944
945 Returns:
946 the fingerprint(s) of the imported key(s)
947 """
c267c233 948
8f61c66a 949 fp = io.BytesIO(key_block.encode('ascii'))
c267c233 950
89c08329
AE
951 try:
952 result = gpgme_ctx.import_(fp)
953 imports = result.imports
954 except gpgme.GpgmeError:
955 imports = []
c267c233 956
f8ee6bd3 957 key_fingerprints = []
c267c233 958
8f61c66a
AE
959 if imports != []:
960 for import_ in imports:
961 fingerprint = import_[0]
f8ee6bd3 962 key_fingerprints += [fingerprint]
c267c233 963
e49673aa 964 debug("added gpg key: " + fingerprint)
ec1e779a 965
f8ee6bd3 966 return key_fingerprints
ec1e779a
AE
967
968
3a753fd0 969def verify_sig_message (msg_block, gpgme_ctx):
39e2c525
AE
970 """Verifies the signature of a signed, ascii-armored block of text.
971
972 It encodes the string into ascii, since binary GPG files are currently
973 unsupported, and alternative, the ascii-armored format is encodable into
974 ascii.
975
976 Args:
977 msg_block: a GPG Message block in string form. It may be encrypted or
978 not. If it is encrypted, it will return empty results.
979 gpgme_ctx: the gpgme context
980
981 Returns:
982 A tuple of the plaintext of the signed part and the list of
983 fingerprints of keys signing the data. If verification failed, perhaps
984 because the message was also encrypted, then empty results are
985 returned.
986 """
3a753fd0
AE
987
988 block_b = io.BytesIO(msg_block.encode('ascii'))
989 plain_b = io.BytesIO()
990
991 try:
992 sigs = gpgme_ctx.verify(block_b, None, plain_b)
5f6f32b1 993 except gpgme.GpgmeError:
3a753fd0
AE
994 return ("",[])
995
996 plaintext = plain_b.getvalue().decode('utf-8')
997
998 fingerprints = []
999 for sig in sigs:
1000 fingerprints += [sig.fpr]
1001 return (plaintext, fingerprints)
1002
1003
101d54a8 1004def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
39e2c525
AE
1005 """Verifies the signature of a detached signature.
1006
1007 This requires the signature part and the signed part as separate arguments.
1008
1009 Args:
1010 detached_sig: the signature part of the detached signature
1011 plaintext_bytes: the byte form of the message being signed.
1012 gpgme_ctx: the gpgme context
1013
1014 Returns:
1015 A list of signing fingerprints if the signature verification was
1016 sucessful. Otherwise, an empty list is returned.
1017 """
101d54a8
AE
1018
1019 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
1020 plaintext_fp = io.BytesIO(plaintext_bytes)
101d54a8 1021
ff7aeb3d
AE
1022 try:
1023 result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
1024 except gpgme.GpgmeError:
1025 return []
101d54a8
AE
1026
1027 sig_fingerprints = []
1028 for res_ in result:
1029 sig_fingerprints += [res_.fpr]
1030
1031 return sig_fingerprints
1032
1033
5b3053c1 1034def decrypt_block (msg_block, gpgme_ctx):
39e2c525
AE
1035 """Decrypts a block of GPG text, and verifies any included sigatures.
1036
1037 Some encypted messages have embeded signatures, so those are verified too.
1038
1039 Args:
1040 msg_block: the encrypted(/signed) text
1041 gpgme_ctx: the gpgme context
1042
1043 Returns:
1044 A tuple of plaintext and signatures, if the decryption and signature
1045 verification were successful, respectively.
1046 """
0bec96d6 1047
5b3053c1 1048 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
1049 plain_b = io.BytesIO()
1050
afc1f64c
AE
1051 try:
1052 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
5f6f32b1 1053 except gpgme.GpgmeError:
afc1f64c 1054 return ("",[])
0bec96d6 1055
6aa41372 1056 plaintext = plain_b.getvalue().decode('utf-8')
cbdf22c1
AE
1057
1058 fingerprints = []
1059 for sig in sigs:
1060 fingerprints += [sig.fpr]
1061 return (plaintext, fingerprints)
0bec96d6
AE
1062
1063
dd2b62d7
AE
1064def email_to_from_subject (email_text):
1065 """Returns the values of the email's To:, From: and Subject: fields
fafa21c3 1066
dd2b62d7 1067 Returns this information from an email.
fafa21c3 1068
dd2b62d7
AE
1069 Args:
1070 email_text: the string form of the email
fafa21c3 1071
dd2b62d7
AE
1072 Returns:
1073 the email To:, From:, and Subject: fields as strings
1074 """
d65993b8
AE
1075
1076 email_struct = email.parser.Parser().parsestr(email_text)
1077
1078 email_to = email_struct['To']
1079 email_from = email_struct['From']
1080 email_subject = email_struct['Subject']
1081
1082 return email_to, email_from, email_subject
1083
1084
adcef2f7 1085def import_lang(email_to):
dd2b62d7
AE
1086 """Imports appropriate language file for basic i18n support
1087
1088 The language imported depends on the To: address of the email received by
1089 edward. an -en ending implies the English language, whereas a -ja ending
1090 implies Japanese. The list of supported languages is listed in the 'langs'
1091 list at the beginning of the program.
1092
1093 Args:
1094 email_to: the string containing the email address that the mail was
1095 sent to.
1096
1097 Returns:
1098 the reference to the imported language module. The only variable in
1099 this file is the 'replies' dictionary.
1100 """
adcef2f7 1101
daed5f10
AE
1102 lang_module = "lang.en"
1103
5250b3b8
AE
1104 if email_to != None:
1105 for lang in langs:
1106 if "edward-" + lang in email_to:
daed5f10 1107 lang_module = "lang." + re.sub('-', '_', lang)
adcef2f7 1108
daed5f10 1109 return importlib.import_module(lang_module)
adcef2f7
AE
1110
1111
cfb03389 1112def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key,
0a064403 1113 gpgme_ctx):
dd2b62d7
AE
1114 """This function creates the mime email reply. It can encrypt the email.
1115
1116 If the encrypt_key is included, then the email is encrypted and signed.
1117 Otherwise it is unencrypted.
1118
1119 Args:
1120 plaintext: the plaintext body of the message to create.
cfb03389 1121 email_to: the email address to reply to
dd2b62d7
AE
1122 email_subject: the subject to use in reply
1123 encrypt_to_key: the key object to use for encrypting the email. (or
1124 None)
1125 gpgme_ctx: the gpgme context
1126
1127 Returns
1128 A string version of the mime message, possibly encrypted and signed.
1129 """
1da9b527 1130
2007103e 1131 # quoted printable encoding lets most ascii characters look normal
2a2359a4 1132 # before the mime message is decoded.
2007103e
AE
1133 char_set = email.charset.Charset("utf-8")
1134 char_set.body_encoding = email.charset.QP
8bdfb6d4 1135
2007103e
AE
1136 # MIMEText doesn't allow setting the text encoding
1137 # so we use MIMENonMultipart.
1138 plaintext_mime = MIMENonMultipart('text', 'plain')
1139 plaintext_mime.set_payload(plaintext, charset=char_set)
216708e9
AE
1140
1141 if (encrypt_to_key != None):
8bdfb6d4
AE
1142
1143 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
1144 encrypt_to_key,
40c37ab3 1145 gpgme_ctx)
b33f601b 1146 gpg_payload = encrypted_text
8bdfb6d4 1147
b33f601b
AE
1148 else:
1149 signed_text = sign_message(plaintext_mime.as_string(), gpgme_ctx)
1150 gpg_payload = signed_text
8bdfb6d4 1151
b33f601b
AE
1152 control_mime = MIMEApplication("Version: 1",
1153 _subtype='pgp-encrypted',
1154 _encoder=email.encoders.encode_7or8bit)
1155 control_mime['Content-Description'] = 'PGP/MIME version identification'
1156 control_mime.set_charset('us-ascii')
8bdfb6d4 1157
b33f601b
AE
1158 encoded_mime = MIMEApplication(gpg_payload,
1159 _subtype='octet-stream; name="encrypted.asc"',
1160 _encoder=email.encoders.encode_7or8bit)
1161 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
1162 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
1163 encoded_mime.set_charset('us-ascii')
1164
1165 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
1166 message_mime.attach(control_mime)
1167 message_mime.attach(encoded_mime)
1168 message_mime['Content-Disposition'] = 'inline'
216708e9 1169
2007103e 1170
cfb03389 1171 message_mime['To'] = email_to
2007103e
AE
1172 message_mime['Subject'] = email_subject
1173
1174 reply = message_mime.as_string()
1da9b527
AE
1175
1176 return reply
1177
1178
f87041f8 1179def email_quote_text (text):
dd2b62d7
AE
1180 """Quotes input text by inserting "> "s
1181
1182 This is useful for quoting a text for the reply message. It inserts "> "
1183 strings at the beginning of lines.
1184
1185 Args:
1186 text: plain text to quote
1187
1188 Returns:
1189 Quoted text
1190 """
f87041f8
AE
1191
1192 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
1193
1194 return quoted_message
1195
1196
0a064403 1197def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
dd2b62d7
AE
1198 """Encrypts and signs plaintext
1199
1200 This encrypts and signs a message.
1201
1202 Args:
1203 plaintext: text to sign and ecrypt
1204 encrypt_to_key: the key object to encrypt to
1205 gpgme_ctx: the gpgme context
1206
1207 Returns:
1208 An encrypted and signed string of text
1209 """
897cbaf6 1210
b33f601b 1211 # the plaintext should be mime encoded in an ascii-compatible form
6aa41372 1212 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
1213 encrypted_bytes = io.BytesIO()
1214
897cbaf6 1215 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
1216 plaintext_bytes, encrypted_bytes)
1217
6aa41372 1218 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
1219 return encrypted_txt
1220
1221
b33f601b
AE
1222def sign_message (plaintext, gpgme_ctx):
1223 """Signs plaintext
1224
1225 This signs a message.
1226
1227 Args:
1228 plaintext: text to sign
1229 gpgme_ctx: the gpgme context
1230
1231 Returns:
1232 An armored signature as a string of text
1233 """
1234
1235 # the plaintext should be mime encoded in an ascii-compatible form
1236 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1237 signed_bytes = io.BytesIO()
1238
1239 gpgme_ctx.sign(plaintext_bytes, signed_bytes, gpgme.SIG_MODE_NORMAL)
1240
1241 signed_txt = signed_bytes.getvalue().decode('ascii')
1242 return signed_txt
1243
1244
0a064403 1245def error (error_msg):
dd2b62d7
AE
1246 """Write an error message to stdout
1247
1248 The error message includes the program name.
1249
1250 Args:
1251 error_msg: the message to print
1252
1253 Returns:
1254 Nothing
1255
1256 Post:
1257 An error message is printed to stdout
1258 """
0a064403 1259
e4fb2ab2 1260 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
0a064403
AE
1261
1262
5e8f9094 1263def debug (debug_msg):
dd2b62d7
AE
1264 """Writes a debug message to stdout if debug == True
1265
1266 If the debug option is set in edward_config.py, then the passed message
1267 gets printed to stdout.
1268
1269 Args:
1270 debug_msg: the message to print to stdout
1271
1272 Returns:
1273 Nothing
1274
1275 Post:
1276 A debug message is printed to stdout
1277 """
5e8f9094
AE
1278
1279 if edward_config.debug == True:
0a064403 1280 error(debug_msg)
5e8f9094
AE
1281
1282
20f6e7c5 1283def handle_args ():
dd2b62d7
AE
1284 """Sets the progname variable and complains about any arguments
1285
1286 If there are any arguments, then edward complains and quits, because input
1287 is read from stdin.
1288
1289 Args:
1290 None
1291
1292 Returns:
1293 None
1294
1295 Post:
1296 Exits with error 1 if there are arguments, otherwise returns to the
1297 calling function, such as main().
1298 """
20f6e7c5 1299
a51cdb72
AE
1300 global progname
1301 progname = sys.argv[0]
1302
1303 if len(sys.argv) > 1:
1304 print(progname + ": error, this program doesn't " \
1305 "need any arguments.", file=sys.stderr)
1306 exit(1)
20f6e7c5 1307
20f6e7c5 1308
a51cdb72 1309if __name__ == "__main__":
dd2b62d7 1310 """Executes main if this file is not loaded interactively"""
20f6e7c5 1311
a51cdb72 1312 main()
0bec96d6 1313