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