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