added basic mutli-language reply generation
[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
34import sys
0bec96d6
AE
35import gpgme
36import re
37import io
40c37ab3 38import os
adcef2f7 39import importlib
0bec96d6 40
8bdfb6d4
AE
41import email.parser
42import email.message
43import email.encoders
44
45from email.mime.multipart import MIMEMultipart
46from email.mime.application import MIMEApplication
47from email.mime.nonmultipart import MIMENonMultipart
48
40c37ab3 49import edward_config
c96f3837 50
adcef2f7
AE
51langs = ["an", "de", "el", "en", "fr", "ja", "pt-br", "ro", "ru", "tr"]
52
38738401
AE
53match_types = [('clearsign',
54 '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----'),
55 ('message',
56578eaf
AE
56 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
57 ('pubkey',
58 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
59 ('detachedsig',
38738401 60 '-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
56578eaf
AE
61
62
63class EddyMsg (object):
64 def __init__(self):
65 self.multipart = False
66 self.subparts = []
67
68 self.charset = None
69 self.payload_bytes = None
70 self.payload_pieces = []
71
72 self.filename = None
73 self.content_type = None
74 self.description_list = None
75
76
77class PayloadPiece (object):
78 def __init__(self):
79 self.piece_type = None
80 self.string = None
81 self.gpg_data = None
82
83
38738401
AE
84class GPGData (object):
85 def __init__(self):
86 self.decrypted = False
87
88 self.plainobj = None
89 self.sigs = []
90 self.keys = []
91
d873ff48
AE
92class ReplyInfo (object):
93 def __init__(self):
94 self.replies = None
95 self.msg_to_quote = ""
96
97 self.success_decrypt = False
98 self.failed_decrypt = False
99 self.public_key_received = False
100 self.no_public_key = False
101 self.sig_success = False
102 self.sig_failure = False
103
38738401 104
0bec96d6
AE
105def main ():
106
20f6e7c5 107 handle_args()
0bec96d6 108
0a064403
AE
109 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
110 edward_config.sign_with_key)
111
c96f3837 112 email_text = sys.stdin.read()
adcef2f7
AE
113 email_struct = parse_pgp_mime(email_text, gpgme_ctx)
114
115 email_to, email_from, email_subject = email_to_from_subject(email_text)
116 lang = import_lang(email_to)
fafa21c3 117
d873ff48
AE
118 replyinfo_obj = ReplyInfo()
119 replyinfo_obj.replies = lang.replies
120
121 prepare_for_reply(email_struct, replyinfo_obj)
122 reply_plaintext = write_reply(replyinfo_obj)
adcef2f7 123
adcef2f7 124 print(reply_plaintext)
1fccb295 125
56578eaf
AE
126# encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints)
127#
bf79a93e
AE
128# reply_mime = generate_encrypted_mime(plaintext, email_from, \
129# email_subject, encrypt_to_key,
130# gpgme_ctx)
c96f3837 131
0bec96d6 132
0a064403
AE
133def get_gpg_context (gnupghome, sign_with_key_fp):
134
135 os.environ['GNUPGHOME'] = gnupghome
136
137 gpgme_ctx = gpgme.Context()
138 gpgme_ctx.armor = True
139
140 try:
141 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
142 except:
143 error("unable to load signing key. is the gnupghome "
144 + "and signing key properly set in the edward_config.py?")
145 exit(1)
146
147 gpgme_ctx.signers = [sign_with_key]
148
149 return gpgme_ctx
150
151
38738401 152def parse_pgp_mime (email_text, gpgme_ctx):
394a1476
AE
153
154 email_struct = email.parser.Parser().parsestr(email_text)
155
56578eaf 156 eddy_obj = parse_mime(email_struct)
80aec7fb
AE
157 split_payloads(eddy_obj)
158 gpg_on_payloads(eddy_obj, gpgme_ctx)
8bb4b0d5 159
56578eaf 160 return eddy_obj
0bec96d6 161
0bec96d6 162
56578eaf 163def parse_mime(msg_struct):
0bec96d6 164
56578eaf 165 eddy_obj = EddyMsg()
8bb4b0d5 166
56578eaf
AE
167 if msg_struct.is_multipart() == True:
168 payloads = msg_struct.get_payload()
0bec96d6 169
56578eaf 170 eddy_obj.multipart = True
dd11a483
AE
171 eddy_obj.subparts = list(map(parse_mime, payloads))
172
56578eaf
AE
173 else:
174 eddy_obj = get_subpart_data(msg_struct)
394a1476 175
56578eaf 176 return eddy_obj
c267c233 177
80119cab 178
56578eaf 179def scan_and_split (payload_piece, match_type, pattern):
cf75de65 180
a5d37d44
AE
181 # don't try to re-split pieces containing gpg data
182 if payload_piece.piece_type != "text":
183 return [payload_piece]
184
56578eaf
AE
185 flags = re.DOTALL | re.MULTILINE
186 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
187 ")(?P<rest>.*)", payload_piece.string, flags=flags)
86663388 188
56578eaf
AE
189 if matches == None:
190 pieces = [payload_piece]
c96f3837 191
56578eaf 192 else:
d437f8b2 193
56578eaf
AE
194 beginning = PayloadPiece()
195 beginning.string = matches.group('beginning')
196 beginning.piece_type = payload_piece.piece_type
d437f8b2 197
56578eaf
AE
198 match = PayloadPiece()
199 match.string = matches.group('match')
200 match.piece_type = match_type
d437f8b2 201
56578eaf
AE
202 rest = PayloadPiece()
203 rest.string = matches.group('rest')
204 rest.piece_type = payload_piece.piece_type
d437f8b2 205
56578eaf 206 more_pieces = scan_and_split(rest, match_type, pattern)
4615b156 207 pieces = [beginning, match ] + more_pieces
d437f8b2 208
56578eaf 209 return pieces
d437f8b2 210
d437f8b2 211
56578eaf 212def get_subpart_data (part):
0bec96d6 213
56578eaf 214 obj = EddyMsg()
0bec96d6 215
56578eaf
AE
216 obj.charset = part.get_content_charset()
217 obj.payload_bytes = part.get_payload(decode=True)
218
219 obj.filename = part.get_filename()
220 obj.content_type = part.get_content_type()
221 obj.description_list = part['content-description']
222
223 # your guess is as good as a-myy-ee-ine...
224 if obj.charset == None:
225 obj.charset = 'utf-8'
226
227 if obj.payload_bytes != None:
0eb75d9c
AE
228 try:
229 payload = PayloadPiece()
230 payload.string = obj.payload_bytes.decode(obj.charset)
231 payload.piece_type = 'text'
232
233 obj.payload_pieces = [payload]
234 except UnicodeDecodeError:
235 pass
56578eaf
AE
236
237 return obj
238
239
dd11a483 240def do_to_eddys_pieces (function_to_do, eddy_obj, data):
56578eaf
AE
241
242 if eddy_obj.multipart == True:
dd11a483 243 for sub in eddy_obj.subparts:
d873ff48 244 do_to_eddys_pieces(function_to_do, sub, data)
394a1476 245 else:
d873ff48 246 function_to_do(eddy_obj, data)
dd11a483
AE
247
248
a5d37d44
AE
249def split_payloads (eddy_obj):
250
251 for match_type in match_types:
252 do_to_eddys_pieces(split_payload_pieces, eddy_obj, match_type)
253
a5d37d44
AE
254
255def split_payload_pieces (eddy_obj, match_type):
256
257 (match_name, pattern) = match_type
258
259 new_pieces_list = []
260 for piece in eddy_obj.payload_pieces:
261 new_pieces_list += scan_and_split(piece, match_name, pattern)
262
263 eddy_obj.payload_pieces = new_pieces_list
264
265
101d54a8 266def gpg_on_payloads (eddy_obj, gpgme_ctx, prev_parts=[]):
38738401 267
101d54a8
AE
268 if eddy_obj.multipart == True:
269 prev_parts=[]
270 for sub in eddy_obj.subparts:
271 gpg_on_payloads (sub, gpgme_ctx, prev_parts)
272 prev_parts += [sub]
38738401 273
d873ff48 274 return
38738401 275
a5d37d44 276 for piece in eddy_obj.payload_pieces:
38738401
AE
277
278 if piece.piece_type == "text":
279 # don't transform the plaintext.
280 pass
281
282 elif piece.piece_type == "message":
283 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
284
285 if plaintext:
286 piece.gpg_data = GPGData()
287 piece.gpg_data.sigs = sigs
288 # recurse!
289 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
129543c3 290
8f61c66a 291 elif piece.piece_type == "pubkey":
f8ee6bd3 292 key_fps = add_gpg_key(piece.string, gpgme_ctx)
8f61c66a 293
f8ee6bd3 294 if key_fps != []:
8f61c66a 295 piece.gpg_data = GPGData()
f8ee6bd3 296 piece.gpg_data.keys = key_fps
129543c3
AE
297
298 elif piece.piece_type == "clearsign":
f8ee6bd3 299 (plaintext, sig_fps) = verify_clear_signature(piece.string, gpgme_ctx)
129543c3 300
f8ee6bd3 301 if sig_fps != []:
129543c3 302 piece.gpg_data = GPGData()
f8ee6bd3 303 piece.gpg_data.sigs = sig_fps
129543c3
AE
304 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
305
101d54a8
AE
306 elif piece.piece_type == "detachedsig":
307 for prev in prev_parts:
308 payload_bytes = prev.payload_bytes
6d240f68 309 sig_fps = verify_detached_signature(piece.string, payload_bytes, gpgme_ctx)
101d54a8 310
6d240f68
AE
311 if sig_fps != []:
312 piece.gpg_data = GPGData()
313 piece.gpg_data.sigs = sig_fps
314 piece.gpg_data.plainobj = prev
315 break
38738401
AE
316 else:
317 pass
318
319
d873ff48 320def prepare_for_reply (eddy_obj, replyinfo_obj):
dd11a483 321
d873ff48 322 do_to_eddys_pieces(prepare_for_reply_pieces, eddy_obj, replyinfo_obj)
56578eaf 323
56578eaf 324
d873ff48 325def prepare_for_reply_pieces (eddy_obj, replyinfo_obj):
56578eaf 326
a5d37d44 327 for piece in eddy_obj.payload_pieces:
38738401 328 if piece.piece_type == "text":
d873ff48
AE
329 # don't quote the plaintext part.
330 pass
331
38738401 332 elif piece.piece_type == "message":
d873ff48
AE
333 if piece.gpg_data == None:
334 replyinfo_obj.failed_decrypt = True
335 else:
336 replyinfo_obj.success_decrypt = True
337 # TODO: only quote it if it is also signed by the encrypter.
338 replyinfo_obj.msg_to_quote += flatten_payloads(piece.gpg_data.plainobj)
339
340 prepare_for_reply(piece.gpg_data.plainobj, replyinfo_obj)
341
8f61c66a 342 elif piece.piece_type == "pubkey":
d873ff48
AE
343 if piece.gpg_data == None:
344 replyinfo_obj.no_public_key = True
345 else:
346 replyinfo_obj.public_key_received = True
347
348 elif (piece.piece_type == "clearsign") \
349 or (piece.piece_type == "detachedsig"):
350 if piece.gpg_data == None:
351 replyinfo_obj.sig_failure = True
352 else:
353 replyinfo_obj.sig_success = True
354
355
356def flatten_payloads (eddy_obj):
357
358 flat_string = ""
359
360 if eddy_obj.multipart == True:
361 for sub in eddy_obj.subparts:
362 flat_string += flatten_payloads (sub)
363
364 return flat_string
365
366 for piece in eddy_obj.payload_pieces:
367 if piece.piece_type == "text":
368 flat_string += piece.string
369
370 return flat_string
371
372
373def write_reply (replyinfo_obj):
374
375 reply_plain = ""
376
377 if replyinfo_obj.success_decrypt == True:
378 quoted_text = email_quote_text(replyinfo_obj.msg_to_quote)
379 reply_plain += replyinfo_obj.replies['success_decrypt']
380 reply_plain += quoted_text
381
382 elif replyinfo_obj.failed_decrypt == True:
383 reply_plain += replyinfo_obj.replies['failed_decrypt']
384
385
386 if replyinfo_obj.sig_success == True:
387 reply_plain += "\n\n"
388 reply_plain += replyinfo_obj.replies['sig_success']
389
390 elif replyinfo_obj.sig_failure == True:
391 reply_plain += "\n\n"
392 reply_plain += replyinfo_obj.replies['sig_failure']
393
394
395 if replyinfo_obj.public_key_received == True:
396 reply_plain += "\n\n"
397 reply_plain += replyinfo_obj.replies['public_key_received']
398
399 elif replyinfo_obj.no_public_key == True:
400 reply_plain += "\n\n"
401 reply_plain += replyinfo_obj.replies['no_public_key']
402
403
404 reply_plain += "\n\n"
405 reply_plain += replyinfo_obj.replies['signature']
56578eaf 406
d873ff48 407 return reply_plain
56578eaf
AE
408
409
8f61c66a 410def add_gpg_key (key_block, gpgme_ctx):
c267c233 411
8f61c66a 412 fp = io.BytesIO(key_block.encode('ascii'))
c267c233 413
8f61c66a
AE
414 result = gpgme_ctx.import_(fp)
415 imports = result.imports
c267c233 416
f8ee6bd3 417 key_fingerprints = []
c267c233 418
8f61c66a
AE
419 if imports != []:
420 for import_ in imports:
421 fingerprint = import_[0]
f8ee6bd3 422 key_fingerprints += [fingerprint]
c267c233 423
e49673aa 424 debug("added gpg key: " + fingerprint)
ec1e779a 425
f8ee6bd3 426 return key_fingerprints
ec1e779a
AE
427
428
129543c3 429def verify_clear_signature (sig_block, gpgme_ctx):
cf75de65 430
129543c3
AE
431 # FIXME: this might require the un-decoded bytes
432 # or the correct re-encoding with the carset of the mime part.
433 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
434 ptxt_fp = io.BytesIO()
cf75de65 435
129543c3 436 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
cf75de65 437
129543c3
AE
438 # FIXME: this might require using the charset of the mime part.
439 plaintext = ptxt_fp.getvalue().decode('utf-8')
cf75de65 440
f8ee6bd3 441 sig_fingerprints = []
129543c3 442 for res_ in result:
f8ee6bd3 443 sig_fingerprints += [res_.fpr]
cf75de65 444
f8ee6bd3 445 return plaintext, sig_fingerprints
cf75de65
AE
446
447
101d54a8
AE
448def verify_detached_signature (detached_sig, plaintext_bytes, gpgme_ctx):
449
450 detached_sig_fp = io.BytesIO(detached_sig.encode('ascii'))
451 plaintext_fp = io.BytesIO(plaintext_bytes)
452 ptxt_fp = io.BytesIO()
453
454 result = gpgme_ctx.verify(detached_sig_fp, plaintext_fp, None)
455
456 sig_fingerprints = []
457 for res_ in result:
458 sig_fingerprints += [res_.fpr]
459
460 return sig_fingerprints
461
462
5b3053c1 463def decrypt_block (msg_block, gpgme_ctx):
0bec96d6 464
5b3053c1 465 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
466 plain_b = io.BytesIO()
467
afc1f64c
AE
468 try:
469 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
470 except:
471 return ("",[])
0bec96d6 472
6aa41372 473 plaintext = plain_b.getvalue().decode('utf-8')
394a1476 474 return (plaintext, sigs)
0bec96d6
AE
475
476
d0489345 477def choose_reply_encryption_key (gpgme_ctx, fingerprints):
fafa21c3
AE
478
479 reply_key = None
d0489345
AE
480 for fp in fingerprints:
481 try:
482 key = gpgme_ctx.get_key(fp)
483
484 if (key.can_encrypt == True):
485 reply_key = key
486 break
487 except:
488 continue
489
fafa21c3 490
216708e9 491 return reply_key
fafa21c3
AE
492
493
d65993b8
AE
494def email_to_from_subject (email_text):
495
496 email_struct = email.parser.Parser().parsestr(email_text)
497
498 email_to = email_struct['To']
499 email_from = email_struct['From']
500 email_subject = email_struct['Subject']
501
502 return email_to, email_from, email_subject
503
504
adcef2f7
AE
505def import_lang(email_to):
506
5250b3b8
AE
507 if email_to != None:
508 for lang in langs:
509 if "edward-" + lang in email_to:
510 lang = "lang." + re.sub('-', '_', lang)
511 language = importlib.import_module(lang)
adcef2f7 512
5250b3b8 513 return language
adcef2f7
AE
514
515 return importlib.import_module("lang.en")
516
517
bf79a93e 518def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
0a064403 519 gpgme_ctx):
1da9b527 520
8bdfb6d4 521
1da9b527
AE
522 reply = "To: " + email_from + "\n"
523 reply += "Subject: " + email_subject + "\n"
216708e9
AE
524
525 if (encrypt_to_key != None):
526 plaintext_reply = "thanks for the message!\n\n\n"
527 plaintext_reply += email_quote_text(plaintext)
528
8bdfb6d4
AE
529 # quoted printable encoding lets most ascii characters look normal
530 # before the decrypted mime message is decoded.
531 char_set = email.charset.Charset("utf-8")
532 char_set.body_encoding = email.charset.QP
533
534 # MIMEText doesn't allow setting the text encoding
535 # so we use MIMENonMultipart.
536 plaintext_mime = MIMENonMultipart('text', 'plain')
537 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
538
539 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
540 encrypt_to_key,
40c37ab3 541 gpgme_ctx)
8bdfb6d4
AE
542
543 control_mime = MIMEApplication("Version: 1",
544 _subtype='pgp-encrypted',
545 _encoder=email.encoders.encode_7or8bit)
546 control_mime['Content-Description'] = 'PGP/MIME version identification'
547 control_mime.set_charset('us-ascii')
548
549 encoded_mime = MIMEApplication(encrypted_text,
550 _subtype='octet-stream; name="encrypted.asc"',
551 _encoder=email.encoders.encode_7or8bit)
552 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
553 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
554 encoded_mime.set_charset('us-ascii')
555
556 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
557 message_mime.attach(control_mime)
558 message_mime.attach(encoded_mime)
559 message_mime['Content-Disposition'] = 'inline'
216708e9 560
8bdfb6d4 561 reply += message_mime.as_string()
216708e9
AE
562
563 else:
8bdfb6d4 564 reply += "\n"
216708e9
AE
565 reply += "Sorry, i couldn't find your key.\n"
566 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
567
568 return reply
569
570
f87041f8
AE
571def email_quote_text (text):
572
573 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
574
575 return quoted_message
576
577
0a064403 578def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
897cbaf6 579
6aa41372 580 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
581 encrypted_bytes = io.BytesIO()
582
897cbaf6 583 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
584 plaintext_bytes, encrypted_bytes)
585
6aa41372 586 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
587 return encrypted_txt
588
589
0a064403
AE
590def error (error_msg):
591
e4fb2ab2 592 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
0a064403
AE
593
594
5e8f9094
AE
595def debug (debug_msg):
596
597 if edward_config.debug == True:
0a064403 598 error(debug_msg)
5e8f9094
AE
599
600
20f6e7c5
AE
601def handle_args ():
602 if __name__ == "__main__":
603
604 global progname
605 progname = sys.argv[0]
606
607 if len(sys.argv) > 1:
608 print(progname + ": error, this program doesn't " \
609 "need any arguments.", file=sys.stderr)
610 exit(1)
611
612
0bec96d6
AE
613main()
614