check to see if a reply encryption key can encrypt
[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
60ccbaf4 41import subprocess
0bec96d6 42
8bdfb6d4
AE
43import email.parser
44import email.message
45import email.encoders
46
e5dd6f23 47from email.mime.text import MIMEText
8bdfb6d4
AE
48from email.mime.multipart import MIMEMultipart
49from email.mime.application import MIMEApplication
50from email.mime.nonmultipart import MIMENonMultipart
51
40c37ab3 52import edward_config
c96f3837 53
01ceaec3 54langs = ["de", "el", "en", "es", "fr", "it", "ja", "pt-br", "ro", "ru", "tr"]
adcef2f7 55
def9196c
AE
56"""This list contains the abbreviated names of reply languages available to
57edward."""
58
0fd3389f
AE
59class TxtType (enum.Enum):
60 text = 0
61 message = 1
62 pubkey = 2
63 detachedsig = 3
64 signature = 4
def9196c 65
0fd3389f 66
2f4102b9 67match_pairs = [(TxtType.message,
56578eaf 68 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
0fd3389f 69 (TxtType.pubkey,
56578eaf 70 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
0fd3389f 71 (TxtType.detachedsig,
38738401 72 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
56578eaf 73
def9196c
AE
74"""This list of tuples matches query names with re.search() queries used
75to find GPG data for edward to process."""
76
56578eaf
AE
77
78class EddyMsg (object):
def9196c
AE
79 """
80 The EddyMsg class represents relevant parts of a mime message.
81
82 The represented message can be single-part or multi-part.
83
84 'multipart' is set to True if there are multiple mime parts.
85
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.
88
6d8faaa7
AE
89 'payload_bytes' is a binary representation of the mime part before header
90 removal and message decoding.
def9196c
AE
91
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.
94
def9196c
AE
95 The 'filename', 'content_type' and 'description_list' come from the mime
96 part parameters.
97 """
98
7d0b91d2
AE
99 multipart = False
100 subparts = []
56578eaf 101
7d0b91d2
AE
102 payload_bytes = None
103 payload_pieces = []
56578eaf 104
7d0b91d2
AE
105 filename = None
106 content_type = None
107 description_list = None
56578eaf
AE
108
109
110class PayloadPiece (object):
def9196c
AE
111 """
112 PayloadPiece represents a complte or sub-section of a mime part.
113
114 Instances of this class are often strung together within one or more arrays
115 pointed to by each instance of the EddyMsg class.
116
0fd3389f
AE
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.
def9196c
AE
122
123 'string' contains some string of text, such as non-GPG text, an encrypted
124 block of text, a signature, or a public key.
125
126 'gpg_data' points to any instances of GPGData that have been created based
127 on the contents of 'string'.
128 """
129
7d0b91d2
AE
130 piece_type = None
131 string = None
132 gpg_data = None
56578eaf
AE
133
134
38738401 135class GPGData (object):
def9196c
AE
136 """
137 GPGData holds info from decryption, sig. verification, and/or pub. keys.
138
139 Instances of this class contain decrypted information, signature
140 fingerprints and/or fingerprints of processed and imported public keys.
141
142 'decrypted' is set to True if 'plainobj' was created from encrypted data.
143
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.
146
147 'sigs' is a list of fingerprints of keys used to sign the data in plainobj.
148
b4c116d5
AE
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.
9af26cb8 151
c035a008
AE
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.
155
def9196c
AE
156 'keys' is a list of fingerprints of keys obtained in public key blocks.
157 """
158
7d0b91d2
AE
159 decrypted = False
160
161 plainobj = None
162 sigs = []
b4c116d5 163 sigkey_missing = False
c035a008 164 key_cannot_encrypt = False
7d0b91d2 165 keys = []
38738401 166
38738401 167
d873ff48 168class ReplyInfo (object):
def9196c
AE
169 """
170 ReplyInfo contains details that edward uses in generating its reply.
171
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.
177
178 'replies' points one of the dictionaries of translated replies.
179
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.
182
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
185 message.
186
ca37d0cb
AE
187 'encrypt_to_key' the key object to use when encrypting edward's reply
188
def9196c
AE
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.
193
b4c116d5 194 'decrypt_success' is set to True if edward could decrypt part of the
def9196c
AE
195 message.
196
def9196c
AE
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.
199
c035a008
AE
200 'key_can_encrypt' is set to True if a key which can be encrypted to has
201 been found.
202
32a8996f
AE
203 'sig_failure' is set to True if edward could not verify a siganture.
204
b4c116d5
AE
205 'pubkey_success' is set to True if edward successfully imported a public
206 key.
207
208 'sigkey_missing' is set to True if edward doesn't have the public key
209 needed for signature verification.
210
c035a008
AE
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.
213
b4c116d5
AE
214 'have_repy_key' is set to True if edward has a public key to encrypt its
215 reply to.
def9196c
AE
216 """
217
7d0b91d2 218 replies = None
6906d46d 219
7d0b91d2
AE
220 target_key = None
221 fallback_target_key = None
ca37d0cb 222 encrypt_to_key = None
7d0b91d2 223 msg_to_quote = ""
d873ff48 224
b4c116d5 225 decrypt_success = False
7d0b91d2 226 sig_success = False
b4c116d5 227 pubkey_success = False
c035a008 228 key_can_encrypt = False
b4c116d5 229
4e2c66f7 230 decrypt_failure = False
32a8996f 231 sig_failure = False
b4c116d5 232 sigkey_missing = False
c035a008 233 key_cannot_encrypt = False
371911ad 234
b4c116d5 235 have_reply_key = False
d873ff48 236
38738401 237
0bec96d6
AE
238def main ():
239
def9196c
AE
240 """
241 This is the main function for edward, a GPG reply bot.
242
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.
245
246 Args:
247 None
248
249 Returns:
a3d6aff8 250 Nothing
def9196c
AE
251
252 Pre:
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
257 language is English.
258
259 Post:
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.
265 """
266
60ccbaf4 267 print_reply_only = handle_args()
0bec96d6 268
0a064403
AE
269 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
270 edward_config.sign_with_key)
271
85a829fc
AE
272 email_bytes = sys.stdin.buffer.read()
273 email_struct = parse_pgp_mime(email_bytes, gpgme_ctx)
adcef2f7 274
85a829fc 275 email_to, email_from, email_subject = email_to_from_subject(email_bytes)
60ccbaf4 276 lang, reply_from = import_lang_pick_address(email_to, edward_config.hostname)
fafa21c3 277
d873ff48
AE
278 replyinfo_obj = ReplyInfo()
279 replyinfo_obj.replies = lang.replies
280
281 prepare_for_reply(email_struct, replyinfo_obj)
ca37d0cb 282 get_key_from_fp(replyinfo_obj, gpgme_ctx)
d873ff48 283 reply_plaintext = write_reply(replyinfo_obj)
adcef2f7 284
2007103e 285 reply_mime = generate_encrypted_mime(reply_plaintext, email_from, \
ca37d0cb 286 email_subject, replyinfo_obj.encrypt_to_key,
2007103e 287 gpgme_ctx)
1fccb295 288
60ccbaf4
AE
289 if print_reply_only == True:
290 print(reply_mime)
291 else:
292 send_reply(reply_mime, email_subject, email_from, reply_from)
c96f3837 293
0bec96d6 294
0a064403 295def get_gpg_context (gnupghome, sign_with_key_fp):
a3d6aff8
AE
296 """
297 This function returns the GPG context needed for encryption and signing.
298
299 The context is needed by other functions which use GPG functionality.
300
301 Args:
302 gnupghome: The path to "~/.gnupg/" or its alternative.
303 sign_with_key: The fingerprint of the key to sign with
304
305 Returns:
306 A gpgme context to be used for GPG functions.
307
308 Post:
309 the 'armor' flag is set to True and the list of signing keys contains
310 the single specified key
311 """
0a064403
AE
312
313 os.environ['GNUPGHOME'] = gnupghome
314
315 gpgme_ctx = gpgme.Context()
316 gpgme_ctx.armor = True
317
318 try:
319 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
5f6f32b1 320 except gpgme.GpgmeError:
0a064403
AE
321 error("unable to load signing key. is the gnupghome "
322 + "and signing key properly set in the edward_config.py?")
323 exit(1)
324
325 gpgme_ctx.signers = [sign_with_key]
326
327 return gpgme_ctx
328
329
85a829fc 330def parse_pgp_mime (email_bytes, gpgme_ctx):
a3d6aff8
AE
331 """Parses the email for mime payloads and decrypts/verfies signatures.
332
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.
337
338 Args:
85a829fc 339 email_bytes: an email message in byte string format
a3d6aff8
AE
340 gpgme_ctx: a gpgme context
341
342 Returns:
343 A message as an instance of EddyMsg
344
345 Post:
346 the returned EddyMsg instance has split, decrypted, verified and pubkey
347 imported payloads
348 """
394a1476 349
85a829fc 350 email_struct = email.parser.BytesParser().parsebytes(email_bytes)
394a1476 351
928e3819
AE
352 eddymsg_obj = parse_mime(email_struct)
353 split_payloads(eddymsg_obj)
354 gpg_on_payloads(eddymsg_obj, gpgme_ctx)
8bb4b0d5 355
928e3819 356 return eddymsg_obj
0bec96d6 357
0bec96d6 358
56578eaf 359def parse_mime(msg_struct):
a3d6aff8
AE
360 """Translates python's email.parser format into an EddyMsg format
361
362 If the message is multi-part, then a recursive object is created, where
363 each sub-part is also a EddyMsg instance.
364
365 Args:
366 msg_struct: an email parsed with email.parser.Parser(), which can be
367 multi-part
368
369 Returns:
370 an instance of EddyMsg, potentially a recursive one.
371 """
0bec96d6 372
928e3819 373 eddymsg_obj = EddyMsg()
8bb4b0d5 374
56578eaf
AE
375 if msg_struct.is_multipart() == True:
376 payloads = msg_struct.get_payload()
0bec96d6 377
928e3819
AE
378 eddymsg_obj.multipart = True
379 eddymsg_obj.subparts = list(map(parse_mime, payloads))
dd11a483 380
56578eaf 381 else:
928e3819 382 eddymsg_obj = get_subpart_data(msg_struct)
394a1476 383
928e3819 384 return eddymsg_obj
c267c233 385
80119cab 386
2f4102b9 387def scan_and_split (payload_piece, match_name, pattern):
a3d6aff8
AE
388 """This splits the payloads of an EddyMsg object into GPG and text parts.
389
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/
393 can be made.
394
395 Args:
396 payload_piece: a single payload or a split part of a payload
2f4102b9 397 match_name: the type of data to try to spit out from the payload piece
a3d6aff8
AE
398 pattern: the search pattern to be used for finding that type of data
399
400 Returns:
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'.
404 """
cf75de65 405
a5d37d44 406 # don't try to re-split pieces containing gpg data
0fd3389f 407 if payload_piece.piece_type != TxtType.text:
a5d37d44
AE
408 return [payload_piece]
409
56578eaf
AE
410 flags = re.DOTALL | re.MULTILINE
411 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
412 ")(?P<rest>.*)", payload_piece.string, flags=flags)
86663388 413
56578eaf
AE
414 if matches == None:
415 pieces = [payload_piece]
c96f3837 416
56578eaf 417 else:
d437f8b2 418
56578eaf
AE
419 beginning = PayloadPiece()
420 beginning.string = matches.group('beginning')
421 beginning.piece_type = payload_piece.piece_type
d437f8b2 422
56578eaf
AE
423 match = PayloadPiece()
424 match.string = matches.group('match')
2f4102b9 425 match.piece_type = match_name
d437f8b2 426
56578eaf
AE
427 rest = PayloadPiece()
428 rest.string = matches.group('rest')
429 rest.piece_type = payload_piece.piece_type
d437f8b2 430
2f4102b9 431 more_pieces = scan_and_split(rest, match_name, pattern)
4615b156 432 pieces = [beginning, match ] + more_pieces
d437f8b2 433
56578eaf 434 return pieces
d437f8b2 435
d437f8b2 436
56578eaf 437def get_subpart_data (part):
a3d6aff8
AE
438 """This function grabs information from a single part mime object.
439
440 It copies needed data from a single part email.parser.Parser() object over
441 to an EddyMsg object.
442
443 Args:
444 part: a non-multi-part mime.parser.Parser() object
445
446 Returns:
447 a single-part EddyMsg() object
448 """
0bec96d6 449
6d8faaa7 450 obj = EddyMsg()
56578eaf 451
84ff9773
AE
452 mime_decoded_bytes = part.get_payload(decode=True)
453 charset = part.get_content_charset()
56578eaf
AE
454
455 # your guess is as good as a-myy-ee-ine...
6d8faaa7
AE
456 if charset == None:
457 charset = 'utf-8'
56578eaf 458
84ff9773
AE
459 payload_string = part.as_string()
460 if payload_string != None:
461 obj.payload_bytes = payload_string.encode(charset)
462
463 obj.filename = part.get_filename()
464 obj.content_type = part.get_content_type()
465 obj.description_list = part['content-description']
466
6d8faaa7 467 if mime_decoded_bytes != None:
0eb75d9c
AE
468 try:
469 payload = PayloadPiece()
6d8faaa7 470 payload.string = mime_decoded_bytes.decode(charset)
0fd3389f 471 payload.piece_type = TxtType.text
0eb75d9c
AE
472
473 obj.payload_pieces = [payload]
474 except UnicodeDecodeError:
475 pass
56578eaf
AE
476
477 return obj
478
479
928e3819 480def do_to_eddys_pieces (function_to_do, eddymsg_obj, data):
a3d6aff8
AE
481 """A function which maps another function onto a message's subparts.
482
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
486 in is single-part.
487
488 Args:
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'
492
493 Returns:
494 Nothing
495
496 Post:
497 The passed-in EddyMsg object is transformed recursively on its
498 sub-parts according to 'function_to_do'.
499 """
56578eaf 500
928e3819
AE
501 if eddymsg_obj.multipart == True:
502 for sub in eddymsg_obj.subparts:
d873ff48 503 do_to_eddys_pieces(function_to_do, sub, data)
394a1476 504 else:
928e3819 505 function_to_do(eddymsg_obj, data)
dd11a483
AE
506
507
928e3819 508def split_payloads (eddymsg_obj):
a3d6aff8
AE
509 """Splits all (sub-)payloads of a message into GPG data and regular text.
510
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.
514
515 Args:
516 eddymsg_obj: an instance of EddyMsg
517
518 Returns:
519 Nothing
520
521 Pre:
522 The EddyMsg object has payloads that are unsplit (by may be split)..
523
524 Post:
525 The EddyMsg object's payloads are all split into GPG and non-GPG parts.
526 """
a5d37d44 527
2f4102b9
AE
528 for match_pair in match_pairs:
529 do_to_eddys_pieces(split_payload_pieces, eddymsg_obj, match_pair)
a5d37d44 530
a5d37d44 531
2f4102b9 532def split_payload_pieces (eddymsg_obj, match_pair):
a3d6aff8
AE
533 """A helper function for split_payloads(); works on PayloadPiece objects.
534
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.
538
539 Args:
540 eddymsg_obj: a single-part EddyMsg object.
2f4102b9 541 match_pair: a tuple from the match_pairs list, which specifies a match
a3d6aff8
AE
542 name and a match pattern.
543
544 Returns:
545 Nothing
546
547 Pre:
548 The payload piece(s) of an EddyMsg object may be already split or
549 unsplit.
550
551 Post:
552 The EddyMsg object's payload piece(s) are split into a list of pieces
2f4102b9 553 if matches of the match_pair are found.
a3d6aff8 554 """
a5d37d44 555
2f4102b9 556 (match_name, pattern) = match_pair
a5d37d44
AE
557
558 new_pieces_list = []
928e3819 559 for piece in eddymsg_obj.payload_pieces:
a5d37d44
AE
560 new_pieces_list += scan_and_split(piece, match_name, pattern)
561
928e3819 562 eddymsg_obj.payload_pieces = new_pieces_list
a5d37d44
AE
563
564
928e3819 565def gpg_on_payloads (eddymsg_obj, gpgme_ctx, prev_parts=[]):
a3d6aff8
AE
566 """Performs GPG operations on the GPG parts of the message
567
568 This function decrypts text, verifies signatures, and imports public keys
569 included in an email.
570
571 Args:
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
575
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
580 omitted.
581
582 Return:
583 Nothing
584
585 Pre:
586 eddymsg_obj should have its payloads split into gpg and non-gpg pieces.
587
588 Post:
c035a008
AE
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.
a3d6aff8 592 """
38738401 593
928e3819 594 if eddymsg_obj.multipart == True:
101d54a8 595 prev_parts=[]
928e3819 596 for sub in eddymsg_obj.subparts:
101d54a8
AE
597 gpg_on_payloads (sub, gpgme_ctx, prev_parts)
598 prev_parts += [sub]
38738401 599
d873ff48 600 return
38738401 601
928e3819 602 for piece in eddymsg_obj.payload_pieces:
38738401 603
0fd3389f 604 if piece.piece_type == TxtType.text:
38738401
AE
605 # don't transform the plaintext.
606 pass
607
0fd3389f 608 elif piece.piece_type == TxtType.message:
b4c116d5
AE
609 piece.gpg_data = GPGData()
610
c035a008 611 (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = decrypt_block(piece.string, gpgme_ctx)
b4c116d5
AE
612
613 piece.gpg_data.sigkey_missing = sigkey_missing
c035a008 614 piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
38738401 615
85a829fc 616 if plaintext_b:
b23e171d 617 piece.gpg_data.decrypted = True
38738401
AE
618 piece.gpg_data.sigs = sigs
619 # recurse!
85a829fc 620 piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx)
3a753fd0
AE
621 continue
622
623 # if not encrypted, check to see if this is an armored signature.
c035a008 624 (plaintext_b, sigs, sigkey_missing, key_cannot_encrypt) = verify_sig_message(piece.string, gpgme_ctx)
b4c116d5
AE
625
626 piece.gpg_data.sigkey_missing = sigkey_missing
c035a008 627 piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
3a753fd0 628
85a829fc 629 if plaintext_b:
0fd3389f 630 piece.piece_type = TxtType.signature
3a753fd0
AE
631 piece.gpg_data.sigs = sigs
632 # recurse!
85a829fc 633 piece.gpg_data.plainobj = parse_pgp_mime(plaintext_b, gpgme_ctx)
129543c3 634
0fd3389f 635 elif piece.piece_type == TxtType.pubkey:
c035a008
AE
636 piece.gpg_data = GPGData()
637
638 (key_fps, key_cannot_encrypt) = add_gpg_key(piece.string, gpgme_ctx)
639
640 piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
8f61c66a 641
f8ee6bd3 642 if key_fps != []:
f8ee6bd3 643 piece.gpg_data.keys = key_fps
129543c3 644
0fd3389f 645 elif piece.piece_type == TxtType.detachedsig:
b4c116d5
AE
646 piece.gpg_data = GPGData()
647
101d54a8 648 for prev in prev_parts:
c035a008 649 (sig_fps, sigkey_missing, key_cannot_encrypt) = verify_detached_signature(piece.string, prev.payload_bytes, gpgme_ctx)
b4c116d5
AE
650
651 piece.gpg_data.sigkey_missing = sigkey_missing
c035a008 652 piece.gpg_data.key_cannot_encrypt = key_cannot_encrypt
101d54a8 653
6d240f68 654 if sig_fps != []:
6d240f68
AE
655 piece.gpg_data.sigs = sig_fps
656 piece.gpg_data.plainobj = prev
657 break
7e18f14d 658
38738401
AE
659 else:
660 pass
661
662
928e3819 663def prepare_for_reply (eddymsg_obj, replyinfo_obj):
67691346
AE
664 """Updates replyinfo_obj with info on the message's GPG success/failures
665
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.
669
670 Args:
671 eddymsg_obj: a message in the EddyMsg format
672 replyinfo_obj: an instance of ReplyInfo
673
674 Returns:
675 Nothing
676
677 Pre:
678 eddymsg_obj has had its gpg_data created by gpg_on_payloads
679
680 Post:
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.
684 """
dd11a483 685
928e3819 686 do_to_eddys_pieces(prepare_for_reply_pieces, eddymsg_obj, replyinfo_obj)
56578eaf 687
928e3819 688def prepare_for_reply_pieces (eddymsg_obj, replyinfo_obj):
67691346
AE
689 """A helper function for prepare_for_reply
690
691 It updates replyinfo_obj with GPG success/failure information, when
692 supplied a single-part EddyMsg object.
693
694 Args:
695 eddymsg_obj: a single-part message in the EddyMsg format
696 replyinfo_obj: an object which holds information about the message's
697 GPG status
698
699 Returns:
700 Nothing
701
702 Pre:
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
705 gpg data.
706
707 Post:
708 replyinfo_obj has been updated with gpg success/failure information
709 """
56578eaf 710
928e3819 711 for piece in eddymsg_obj.payload_pieces:
0fd3389f 712 if piece.piece_type == TxtType.text:
d873ff48
AE
713 # don't quote the plaintext part.
714 pass
715
0fd3389f 716 elif piece.piece_type == TxtType.message:
05683768 717 prepare_for_reply_message(piece, replyinfo_obj)
d873ff48 718
0fd3389f 719 elif piece.piece_type == TxtType.pubkey:
05683768 720 prepare_for_reply_pubkey(piece, replyinfo_obj)
6906d46d 721
0fd3389f
AE
722 elif (piece.piece_type == TxtType.detachedsig) \
723 or (piece.piece_type == TxtType.signature):
05683768 724 prepare_for_reply_sig(piece, replyinfo_obj)
d873ff48 725
d873ff48 726
05683768 727def prepare_for_reply_message (piece, replyinfo_obj):
67691346
AE
728 """Helper function for prepare_for_reply()
729
730 This function is called when the piece_type of a payload piece is
9af26cb8
AE
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.
67691346
AE
734
735 Args:
736 piece: a PayloadPiece object.
737 replyinfo_obj: object which gets updated with decryption status, etc.
738
67691346
AE
739 Returns:
740 Nothing
741
742 Pre:
0fd3389f 743 the piece.payload_piece value should be TxtType.message.
67691346
AE
744
745 Post:
b4c116d5 746 replyinfo_obj gets updated with decryption status, signing status, a
c035a008
AE
747 potential signing key, posession status of the public key for the
748 signature and encryption capability status if that key is missing.
67691346 749 """
44fe39b7 750
c035a008 751 if piece.gpg_data.plainobj == None:
4e2c66f7 752 replyinfo_obj.decrypt_failure = True
05683768
AE
753 return
754
b4c116d5 755 replyinfo_obj.decrypt_success = True
05683768
AE
756
757 # we already have a key (and a message)
758 if replyinfo_obj.target_key != None:
759 return
760
c035a008 761 if piece.gpg_data.sigs == []:
b4c116d5
AE
762 if piece.gpg_data.sigkey_missing == True:
763 replyinfo_obj.sigkey_missing = True
9af26cb8 764
c035a008
AE
765 if piece.gpg_data.key_cannot_encrypt == True:
766 replyinfo_obj.key_cannot_encrypt = True
767
05683768
AE
768 # only include a signed message in the reply.
769 get_signed_part = True
770
c035a008
AE
771 else:
772 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
773 replyinfo_obj.sig_success = True
774 get_signed_part = False
775
8c3e5397 776 flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, get_signed_part)
05683768
AE
777
778 # to catch public keys in encrypted blocks
779 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
780
781
782def prepare_for_reply_pubkey (piece, replyinfo_obj):
67691346
AE
783 """Helper function for prepare_for_reply(). Marks pubkey import status.
784
785 Marks replyinfo_obj with pub key import status.
786
787 Args:
788 piece: a PayloadPiece object
789 replyinfo_obj: a ReplyInfo object
790
791 Pre:
0fd3389f 792 piece.piece_type should be set to TxtType.pubkey .
67691346
AE
793
794 Post:
795 replyinfo_obj has its fields updated.
796 """
05683768 797
c035a008
AE
798 if piece.gpg_data.keys == []:
799 if piece.gpg_data.key_cannot_encrypt == True:
800 replyinfo_obj.key_cannot_encrypt = True
05683768 801 else:
b4c116d5 802 replyinfo_obj.pubkey_success = True
05683768 803
3ceb92e5
AE
804 # prefer public key as a fallback for the encrypted reply
805 replyinfo_obj.fallback_target_key = piece.gpg_data.keys[0]
05683768
AE
806
807
808def prepare_for_reply_sig (piece, replyinfo_obj):
67691346
AE
809 """Helper function for prepare_for_reply(). Marks sig verification status.
810
811 Marks replyinfo_obj with signature verification status.
812
813 Args:
814 piece: a PayloadPiece object
815 replyinfo_obj: a ReplyInfo object
816
817 Pre:
0fd3389f
AE
818 piece.piece_type should be set to TxtType.signature, or
819 TxtType.detachedsig .
67691346
AE
820
821 Post:
822 replyinfo_obj has its fields updated.
823 """
05683768 824
c035a008 825 if piece.gpg_data.sigs == []:
32a8996f 826 replyinfo_obj.sig_failure = True
b4c116d5
AE
827
828 if piece.gpg_data.sigkey_missing == True:
829 replyinfo_obj.sigkey_missing = True
830
c035a008
AE
831 if piece.gpg_data.key_cannot_encrypt == True:
832 replyinfo_obj.key_cannot_encrypt = True
833
05683768
AE
834 else:
835 replyinfo_obj.sig_success = True
836
837 if replyinfo_obj.fallback_target_key == None:
838 replyinfo_obj.fallback_target_key = piece.gpg_data.sigs[0]
6906d46d 839
c230113e
AE
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)
843
6906d46d 844
8c3e5397
AE
845def flatten_decrypted_payloads (eddymsg_obj, replyinfo_obj, get_signed_part):
846 """For creating a string representation of a signed, encrypted part.
39e2c525 847
8c3e5397
AE
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.
39e2c525
AE
853
854 Args:
855 eddymsg_obj: the message in EddyMsg format created by decrypting GPG
856 text
8c3e5397
AE
857 replyinfo_obj: a ReplyInfo object for holding the message to quote and
858 the target_key to encrypt to.
39e2c525
AE
859 get_signed_part: True if we should only include text that contains a
860 further signature. If False, then include plain text.
861
862 Returns:
8c3e5397 863 Nothing
39e2c525
AE
864
865 Pre:
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
868 level.
d873ff48 869
8c3e5397
AE
870 Post:
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
873 could be found.
874 """
d873ff48 875
0aa88c27 876 if eddymsg_obj == None:
8c3e5397 877 return
0aa88c27 878
0402995a 879 # recurse on multi-part mime
928e3819
AE
880 if eddymsg_obj.multipart == True:
881 for sub in eddymsg_obj.subparts:
8c3e5397 882 flatten_decrypted_payloads(sub, replyinfo_obj, get_signed_part)
d873ff48 883
928e3819 884 for piece in eddymsg_obj.payload_pieces:
0402995a 885 if (get_signed_part):
0fd3389f
AE
886 if ((piece.piece_type == TxtType.detachedsig) \
887 or (piece.piece_type == TxtType.signature)) \
b4c116d5
AE
888 and (piece.gpg_data != None) \
889 and (piece.gpg_data.plainobj != None):
8c3e5397
AE
890 flatten_decrypted_payloads(piece.gpg_data.plainobj, replyinfo_obj, False)
891 replyinfo_obj.target_key = piece.gpg_data.sigs[0]
c3d417bf
AE
892 break
893 else:
0fd3389f 894 if piece.piece_type == TxtType.text:
8c3e5397 895 replyinfo_obj.msg_to_quote += piece.string
d873ff48
AE
896
897
013f7cb8 898def get_key_from_fp (replyinfo_obj, gpgme_ctx):
39e2c525
AE
899 """Obtains a public key object from a key fingerprint
900
ca37d0cb
AE
901 If the .target_key is not set, then we use .fallback_target_key, if
902 available.
39e2c525
AE
903
904 Args:
905 replyinfo_obj: ReplyInfo instance
906 gpgme_ctx: the gpgme context
907
908 Return:
c035a008 909 Nothing
39e2c525
AE
910
911 Pre:
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.
915
916 Post:
ca37d0cb
AE
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
c035a008
AE
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
921 accordingly.
39e2c525 922 """
013f7cb8 923
ca37d0cb
AE
924 for key in (replyinfo_obj.target_key, replyinfo_obj.fallback_target_key):
925 if key != None:
926 try:
927 encrypt_to_key = gpgme_ctx.get_key(key)
c035a008
AE
928
929 except gpgme.GpgmeError:
930 continue
931
932 if encrypt_to_key.can_encrypt == True:
ca37d0cb 933 replyinfo_obj.encrypt_to_key = encrypt_to_key
b4c116d5 934 replyinfo_obj.have_reply_key = True
c035a008 935 replyinfo_obj.key_can_encrypt = True
ca37d0cb 936 return
013f7cb8 937
c035a008
AE
938 else:
939 replyinfo_obj.key_cannot_encrypt = True
940
013f7cb8 941
013f7cb8 942
d873ff48 943def write_reply (replyinfo_obj):
39e2c525
AE
944 """Write the reply email body about the GPG successes/failures.
945
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
949 quoted form.
950
951 Args:
952 replyinfo_obj: contains details of GPG processing status
953
954 Returns:
955 the plaintext message to be sent to the user
956
957 Pre:
958 replyinfo_obj should be populated with info about GPG processing status.
959 """
d873ff48
AE
960
961 reply_plain = ""
962
cfcc211c
AE
963 if (replyinfo_obj.pubkey_success == True):
964 reply_plain += replyinfo_obj.replies['greeting']
965 reply_plain += "\n\n"
966
371911ad 967
b4c116d5 968 if replyinfo_obj.decrypt_success == True:
234f607b 969 debug('decrypt success')
d873ff48 970 reply_plain += replyinfo_obj.replies['success_decrypt']
2694709a 971
b4c116d5 972 if (replyinfo_obj.sig_success == True) and (replyinfo_obj.have_reply_key == True):
234f607b 973 debug('message quoted')
cfcc211c
AE
974 reply_plain += replyinfo_obj.replies['space']
975 reply_plain += replyinfo_obj.replies['quote_follows']
976 reply_plain += "\n\n"
2694709a
AE
977 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
978 reply_plain += quoted_text
cfcc211c
AE
979
980 reply_plain += "\n\n"
d873ff48 981
4e2c66f7 982 elif replyinfo_obj.decrypt_failure == True:
234f607b 983 debug('decrypt failure')
d873ff48 984 reply_plain += replyinfo_obj.replies['failed_decrypt']
b4c116d5 985 reply_plain += "\n\n"
d873ff48 986
371911ad 987
d873ff48 988 if replyinfo_obj.sig_success == True:
234f607b 989 debug('signature success')
d873ff48 990 reply_plain += replyinfo_obj.replies['sig_success']
b4c116d5 991 reply_plain += "\n\n"
d873ff48 992
32a8996f 993 elif replyinfo_obj.sig_failure == True:
234f607b 994 debug('signature failure')
d873ff48 995 reply_plain += replyinfo_obj.replies['sig_failure']
b4c116d5 996 reply_plain += "\n\n"
d873ff48 997
371911ad 998
b4c116d5 999 if (replyinfo_obj.pubkey_success == True):
234f607b 1000 debug('public key received')
b4c116d5 1001 reply_plain += replyinfo_obj.replies['public_key_received']
d873ff48 1002 reply_plain += "\n\n"
d873ff48 1003
b4c116d5 1004 elif (replyinfo_obj.sigkey_missing == True):
234f607b 1005 debug('no public key')
b4c116d5 1006 reply_plain += replyinfo_obj.replies['no_public_key']
d873ff48 1007 reply_plain += "\n\n"
d873ff48 1008
c035a008
AE
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"
1014
d873ff48 1015
4e2c66f7
AE
1016 if (reply_plain == ""):
1017 debug('plaintext message')
1018 reply_plain += replyinfo_obj.replies['failed_decrypt']
1019 reply_plain += "\n\n"
1020
1021
d873ff48 1022 reply_plain += replyinfo_obj.replies['signature']
b4c116d5 1023 reply_plain += "\n\n"
56578eaf 1024
d873ff48 1025 return reply_plain
56578eaf
AE
1026
1027
8f61c66a 1028def add_gpg_key (key_block, gpgme_ctx):
39e2c525
AE
1029 """Adds a GPG pubkey to the local keystore
1030
1031 This adds keys received through email into the key store so they can be
1032 used later.
1033
1034 Args:
1035 key_block: the string form of the ascii-armored public key block
1036 gpgme_ctx: the gpgme context
1037
1038 Returns:
c035a008
AE
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
1041 of encryption.
39e2c525 1042 """
c267c233 1043
8f61c66a 1044 fp = io.BytesIO(key_block.encode('ascii'))
c267c233 1045
89c08329
AE
1046 try:
1047 result = gpgme_ctx.import_(fp)
1048 imports = result.imports
1049 except gpgme.GpgmeError:
1050 imports = []
c267c233 1051
f8ee6bd3 1052 key_fingerprints = []
c035a008
AE
1053 key_cannot_encrypt = False
1054
1055 for import_res in imports:
1056 fingerprint = import_res[0]
1057
1058 try:
1059 key_obj = gpgme_ctx.get_key(fingerprint)
1060 except:
1061 pass
c267c233 1062
c035a008 1063 if key_obj.can_encrypt == True:
f8ee6bd3 1064 key_fingerprints += [fingerprint]
c035a008 1065 key_cannot_encrypt = False
c267c233 1066
e49673aa 1067 debug("added gpg key: " + fingerprint)
ec1e779a 1068
c035a008
AE
1069 elif key_fingerprints == []:
1070 key_cannot_encrypt = True
1071
1072 return (key_fingerprints, key_cannot_encrypt)
ec1e779a
AE
1073
1074
3a753fd0 1075def verify_sig_message (msg_block, gpgme_ctx):
39e2c525
AE
1076 """Verifies the signature of a signed, ascii-armored block of text.
1077
1078 It encodes the string into ascii, since binary GPG files are currently
1079 unsupported, and alternative, the ascii-armored format is encodable into
1080 ascii.
1081
1082 Args:
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
1086
1087 Returns:
85a829fc 1088 A tuple containing the plaintext bytes of the signed part, the list of
c035a008
AE
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.
39e2c525 1094 """
3a753fd0
AE
1095
1096 block_b = io.BytesIO(msg_block.encode('ascii'))
1097 plain_b = io.BytesIO()
1098
1099 try:
1100 sigs = gpgme_ctx.verify(block_b, None, plain_b)
5f6f32b1 1101 except gpgme.GpgmeError:
c035a008 1102 return ("",[],False,False)
3a753fd0 1103
85a829fc 1104 plaintext_b = plain_b.getvalue()
3a753fd0 1105
c035a008 1106 (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
9af26cb8 1107
c035a008 1108 return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt)
3a753fd0
AE
1109
1110
101d54a8 1111def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
39e2c525
AE
1112 """Verifies the signature of a detached signature.
1113
1114 This requires the signature part and the signed part as separate arguments.
1115
1116 Args:
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
1120
1121 Returns:
c035a008
AE
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
1127 returned.
39e2c525 1128 """
101d54a8
AE
1129
1130 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
1131 plaintext_fp = io.BytesIO(plaintext_bytes)
101d54a8 1132
ff7aeb3d 1133 try:
c035a008 1134 sigs = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
ff7aeb3d 1135 except gpgme.GpgmeError:
c035a008 1136 return ([],False,False)
101d54a8 1137
c035a008 1138 (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
101d54a8 1139
c035a008 1140 return (fingerprints, sigkey_missing, key_cannot_encrypt)
101d54a8
AE
1141
1142
5b3053c1 1143def decrypt_block (msg_block, gpgme_ctx):
9af26cb8 1144 """Decrypts a block of GPG text and verifies any included sigatures.
39e2c525
AE
1145
1146 Some encypted messages have embeded signatures, so those are verified too.
1147
1148 Args:
1149 msg_block: the encrypted(/signed) text
1150 gpgme_ctx: the gpgme context
1151
1152 Returns:
c035a008
AE
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.
39e2c525 1158 """
0bec96d6 1159
5b3053c1 1160 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
1161 plain_b = io.BytesIO()
1162
afc1f64c
AE
1163 try:
1164 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
5f6f32b1 1165 except gpgme.GpgmeError:
c035a008 1166 return ("",[],False,False)
0bec96d6 1167
85a829fc 1168 plaintext_b = plain_b.getvalue()
cbdf22c1 1169
c035a008
AE
1170 (fingerprints, sigkey_missing, key_cannot_encrypt) = get_signature_fp(sigs, gpgme_ctx)
1171
1172 return (plaintext_b, fingerprints, sigkey_missing, key_cannot_encrypt)
1173
1174
1175def get_signature_fp (sigs, gpgme_ctx):
1176 """Selects valid signatures from output of gpgme signature verifying functions
1177
1178 get_signature_fp returns a list of valid signature fingerprints if those
1179 fingerprints are associated with available keys capable of encryption.
1180
1181 Args:
1182 sigs: a signature verification result object list
1183 gpgme_ctx: a gpgme context
1184
1185 Returns:
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.
1191 """
1192
b4c116d5 1193 sigkey_missing = False
c035a008 1194 key_cannot_encrypt = False
cbdf22c1 1195 fingerprints = []
c035a008 1196
cbdf22c1 1197 for sig in sigs:
8f011cc9 1198 if (sig.summary == 0) or (sig.summary & gpgme.SIGSUM_VALID != 0) or (sig.summary & gpgme.SIGSUM_GREEN != 0):
c035a008
AE
1199 try:
1200 key_obj = gpgme_ctx.get_key(sig.fpr)
1201 except:
1202 if fingerprints == []:
1203 sigkey_missing = True
1204 continue
1205
1206 if key_obj.can_encrypt == True:
1207 fingerprints += [sig.fpr]
1208 key_cannot_encrypt = False
1209 sigkey_missing = False
1210
1211 elif fingerprints == []:
1212 key_cannot_encrypt = True
1213
1214 elif fingerprints == []:
b4c116d5
AE
1215 if (sig.summary & gpgme.SIGSUM_KEY_MISSING != 0):
1216 sigkey_missing = True
9af26cb8 1217
c035a008 1218 return (fingerprints, sigkey_missing, key_cannot_encrypt)
0bec96d6
AE
1219
1220
85a829fc 1221def email_to_from_subject (email_bytes):
dd2b62d7 1222 """Returns the values of the email's To:, From: and Subject: fields
fafa21c3 1223
dd2b62d7 1224 Returns this information from an email.
fafa21c3 1225
dd2b62d7 1226 Args:
85a829fc 1227 email_bytes: the byte string form of the email
fafa21c3 1228
dd2b62d7
AE
1229 Returns:
1230 the email To:, From:, and Subject: fields as strings
1231 """
d65993b8 1232
85a829fc 1233 email_struct = email.parser.BytesParser().parsebytes(email_bytes)
d65993b8
AE
1234
1235 email_to = email_struct['To']
1236 email_from = email_struct['From']
1237 email_subject = email_struct['Subject']
1238
1239 return email_to, email_from, email_subject
1240
1241
bb27d257
AE
1242def import_lang_pick_address(email_to, hostname):
1243 """Imports language file for i18n support; makes reply from address
dd2b62d7
AE
1244
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'
bb27d257
AE
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
1250 reply email.
dd2b62d7
AE
1251
1252 Args:
1253 email_to: the string containing the email address that the mail was
bb27d257
AE
1254 sent to.
1255 hostname: the hostname part of the reply email's from address
dd2b62d7
AE
1256
1257 Returns:
1258 the reference to the imported language module. The only variable in
1259 this file is the 'replies' dictionary.
1260 """
adcef2f7 1261
bb27d257
AE
1262 # default
1263 use_lang = "en"
daed5f10 1264
5250b3b8
AE
1265 if email_to != None:
1266 for lang in langs:
1267 if "edward-" + lang in email_to:
bb27d257
AE
1268 use_lang = lang
1269 break
adcef2f7 1270
bb27d257
AE
1271 lang_mod_name = "lang." + re.sub('-', '_', use_lang)
1272 lang_module = importlib.import_module(lang_mod_name)
1273
60ccbaf4 1274 reply_from = "edward-" + use_lang + "@" + hostname
bb27d257 1275
60ccbaf4 1276 return lang_module, reply_from
adcef2f7
AE
1277
1278
cfb03389 1279def generate_encrypted_mime (plaintext, email_to, email_subject, encrypt_to_key,
0a064403 1280 gpgme_ctx):
dd2b62d7
AE
1281 """This function creates the mime email reply. It can encrypt the email.
1282
1283 If the encrypt_key is included, then the email is encrypted and signed.
1284 Otherwise it is unencrypted.
1285
1286 Args:
1287 plaintext: the plaintext body of the message to create.
cfb03389 1288 email_to: the email address to reply to
dd2b62d7
AE
1289 email_subject: the subject to use in reply
1290 encrypt_to_key: the key object to use for encrypting the email. (or
1291 None)
1292 gpgme_ctx: the gpgme context
1293
1294 Returns
1295 A string version of the mime message, possibly encrypted and signed.
1296 """
1da9b527 1297
e5dd6f23 1298 if (encrypt_to_key != None):
8bdfb6d4 1299
66a125ef
AE
1300 plaintext_mime = MIMEText(plaintext)
1301 plaintext_mime.set_charset('utf-8')
8bdfb6d4
AE
1302
1303 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
1304 encrypt_to_key,
40c37ab3 1305 gpgme_ctx)
8bdfb6d4 1306
e5dd6f23
AE
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')
8bdfb6d4 1312
e5dd6f23
AE
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')
8bdfb6d4 1319
e5dd6f23
AE
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'
216708e9 1324
e5dd6f23
AE
1325 else:
1326 message_mime = MIMEText(plaintext)
66a125ef 1327 message_mime.set_charset('utf-8')
2007103e 1328
cfb03389 1329 message_mime['To'] = email_to
2007103e
AE
1330 message_mime['Subject'] = email_subject
1331
1332 reply = message_mime.as_string()
1da9b527
AE
1333
1334 return reply
1335
1336
60ccbaf4
AE
1337def send_reply(email_txt, subject, reply_to, reply_from):
1338
1339 email_bytes = email_txt.encode('ascii')
1340
1341 p = subprocess.Popen(["/usr/sbin/sendmail", "-f", reply_from, "-F", "Edward, GPG Bot", "-i", reply_to], stdin=subprocess.PIPE)
1342
1343 (stdout, stderr) = p.communicate(email_bytes)
1344
1345 if stdout != None:
1346 debug("sendmail stdout: " + str(stdout))
1347 if stderr != None:
1348 error("sendmail stderr: " + str(stderr))
1349
1350
f87041f8 1351def email_quote_text (text):
dd2b62d7
AE
1352 """Quotes input text by inserting "> "s
1353
1354 This is useful for quoting a text for the reply message. It inserts "> "
1355 strings at the beginning of lines.
1356
1357 Args:
1358 text: plain text to quote
1359
1360 Returns:
1361 Quoted text
1362 """
f87041f8
AE
1363
1364 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
1365
1366 return quoted_message
1367
1368
0a064403 1369def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
dd2b62d7
AE
1370 """Encrypts and signs plaintext
1371
1372 This encrypts and signs a message.
1373
1374 Args:
1375 plaintext: text to sign and ecrypt
1376 encrypt_to_key: the key object to encrypt to
1377 gpgme_ctx: the gpgme context
1378
1379 Returns:
1380 An encrypted and signed string of text
1381 """
897cbaf6 1382
b33f601b 1383 # the plaintext should be mime encoded in an ascii-compatible form
6aa41372 1384 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
1385 encrypted_bytes = io.BytesIO()
1386
897cbaf6 1387 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
1388 plaintext_bytes, encrypted_bytes)
1389
6aa41372 1390 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
1391 return encrypted_txt
1392
1393
0a064403 1394def error (error_msg):
dd2b62d7
AE
1395 """Write an error message to stdout
1396
1397 The error message includes the program name.
1398
1399 Args:
1400 error_msg: the message to print
1401
1402 Returns:
1403 Nothing
1404
1405 Post:
1406 An error message is printed to stdout
1407 """
0a064403 1408
e4fb2ab2 1409 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
0a064403
AE
1410
1411
5e8f9094 1412def debug (debug_msg):
dd2b62d7
AE
1413 """Writes a debug message to stdout if debug == True
1414
1415 If the debug option is set in edward_config.py, then the passed message
1416 gets printed to stdout.
1417
1418 Args:
1419 debug_msg: the message to print to stdout
1420
1421 Returns:
1422 Nothing
1423
1424 Post:
1425 A debug message is printed to stdout
1426 """
5e8f9094
AE
1427
1428 if edward_config.debug == True:
0a064403 1429 error(debug_msg)
5e8f9094
AE
1430
1431
20f6e7c5 1432def handle_args ():
60ccbaf4 1433 """Sets the progname variable and processes optional argument
dd2b62d7 1434
60ccbaf4
AE
1435 If there are more than two arguments then edward complains and quits. An
1436 single "-p" argument sets the print_reply_only option, which makes edward
1437 print email replies instead of mailing them.
dd2b62d7
AE
1438
1439 Args:
1440 None
1441
1442 Returns:
60ccbaf4
AE
1443 True if edward should print arguments instead of mailing them,
1444 otherwise it returns False.
dd2b62d7
AE
1445
1446 Post:
60ccbaf4
AE
1447 Exits with error 1 if there are more than two arguments, otherwise
1448 returns the print_reply_only option.
dd2b62d7 1449 """
20f6e7c5 1450
a51cdb72
AE
1451 global progname
1452 progname = sys.argv[0]
1453
60ccbaf4
AE
1454 print_reply_only = False
1455
1456 if len(sys.argv) > 2:
1457 print(progname + " usage: " + progname + " [-p]\n\n" \
1458 + " -p print reply message to stdout, do not mail it\n", \
1459 file=sys.stderr)
a51cdb72 1460 exit(1)
20f6e7c5 1461
60ccbaf4
AE
1462 elif (len(sys.argv) == 2) and (sys.argv[1] == "-p"):
1463 print_reply_only = True
1464
1465 return print_reply_only
1466
20f6e7c5 1467
a51cdb72 1468if __name__ == "__main__":
dd2b62d7 1469 """Executes main if this file is not loaded interactively"""
20f6e7c5 1470
a51cdb72 1471 main()
0bec96d6 1472