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