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