don't process 'None' EddyMsg objects
[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
92
0bec96d6
AE
93def main ():
94
20f6e7c5 95 handle_args()
0bec96d6 96
0a064403
AE
97 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
98 edward_config.sign_with_key)
99
c96f3837 100 email_text = sys.stdin.read()
adcef2f7
AE
101 email_struct = parse_pgp_mime(email_text, gpgme_ctx)
102
103 email_to, email_from, email_subject = email_to_from_subject(email_text)
104 lang = import_lang(email_to)
fafa21c3 105
adcef2f7
AE
106 reply_plaintext = build_reply(email_struct)
107
108 debug(lang.replies['success_decrypt'])
109 print(reply_plaintext)
1fccb295 110
56578eaf
AE
111# encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints)
112#
bf79a93e
AE
113# reply_mime = generate_encrypted_mime(plaintext, email_from, \
114# email_subject, encrypt_to_key,
115# gpgme_ctx)
c96f3837 116
0bec96d6 117
0a064403
AE
118def get_gpg_context (gnupghome, sign_with_key_fp):
119
120 os.environ['GNUPGHOME'] = gnupghome
121
122 gpgme_ctx = gpgme.Context()
123 gpgme_ctx.armor = True
124
125 try:
126 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
127 except:
128 error("unable to load signing key. is the gnupghome "
129 + "and signing key properly set in the edward_config.py?")
130 exit(1)
131
132 gpgme_ctx.signers = [sign_with_key]
133
134 return gpgme_ctx
135
136
38738401 137def parse_pgp_mime (email_text, gpgme_ctx):
394a1476
AE
138
139 email_struct = email.parser.Parser().parsestr(email_text)
140
56578eaf 141 eddy_obj = parse_mime(email_struct)
80aec7fb
AE
142 split_payloads(eddy_obj)
143 gpg_on_payloads(eddy_obj, gpgme_ctx)
8bb4b0d5 144
56578eaf 145 return eddy_obj
0bec96d6 146
0bec96d6 147
56578eaf 148def parse_mime(msg_struct):
0bec96d6 149
56578eaf 150 eddy_obj = EddyMsg()
8bb4b0d5 151
56578eaf
AE
152 if msg_struct.is_multipart() == True:
153 payloads = msg_struct.get_payload()
0bec96d6 154
56578eaf 155 eddy_obj.multipart = True
dd11a483
AE
156 eddy_obj.subparts = list(map(parse_mime, payloads))
157
56578eaf
AE
158 else:
159 eddy_obj = get_subpart_data(msg_struct)
394a1476 160
56578eaf 161 return eddy_obj
c267c233 162
80119cab 163
56578eaf 164def scan_and_split (payload_piece, match_type, pattern):
cf75de65 165
a5d37d44
AE
166 # don't try to re-split pieces containing gpg data
167 if payload_piece.piece_type != "text":
168 return [payload_piece]
169
56578eaf
AE
170 flags = re.DOTALL | re.MULTILINE
171 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
172 ")(?P<rest>.*)", payload_piece.string, flags=flags)
86663388 173
56578eaf
AE
174 if matches == None:
175 pieces = [payload_piece]
c96f3837 176
56578eaf 177 else:
d437f8b2 178
56578eaf
AE
179 beginning = PayloadPiece()
180 beginning.string = matches.group('beginning')
181 beginning.piece_type = payload_piece.piece_type
d437f8b2 182
56578eaf
AE
183 match = PayloadPiece()
184 match.string = matches.group('match')
185 match.piece_type = match_type
d437f8b2 186
56578eaf
AE
187 rest = PayloadPiece()
188 rest.string = matches.group('rest')
189 rest.piece_type = payload_piece.piece_type
d437f8b2 190
56578eaf 191 more_pieces = scan_and_split(rest, match_type, pattern)
4615b156 192 pieces = [beginning, match ] + more_pieces
d437f8b2 193
56578eaf 194 return pieces
d437f8b2 195
d437f8b2 196
56578eaf 197def get_subpart_data (part):
0bec96d6 198
56578eaf 199 obj = EddyMsg()
0bec96d6 200
56578eaf
AE
201 obj.charset = part.get_content_charset()
202 obj.payload_bytes = part.get_payload(decode=True)
203
204 obj.filename = part.get_filename()
205 obj.content_type = part.get_content_type()
206 obj.description_list = part['content-description']
207
208 # your guess is as good as a-myy-ee-ine...
209 if obj.charset == None:
210 obj.charset = 'utf-8'
211
212 if obj.payload_bytes != None:
0eb75d9c
AE
213 try:
214 payload = PayloadPiece()
215 payload.string = obj.payload_bytes.decode(obj.charset)
216 payload.piece_type = 'text'
217
218 obj.payload_pieces = [payload]
219 except UnicodeDecodeError:
220 pass
56578eaf
AE
221
222 return obj
223
224
dd11a483 225def do_to_eddys_pieces (function_to_do, eddy_obj, data):
56578eaf
AE
226
227 if eddy_obj.multipart == True:
dd11a483
AE
228 result_list = []
229 for sub in eddy_obj.subparts:
230 result_list += do_to_eddys_pieces(function_to_do, sub, data)
394a1476 231 else:
a5d37d44 232 result_list = [function_to_do(eddy_obj, data)]
dd11a483
AE
233
234 return result_list
235
236
a5d37d44
AE
237def split_payloads (eddy_obj):
238
239 for match_type in match_types:
240 do_to_eddys_pieces(split_payload_pieces, eddy_obj, match_type)
241
a5d37d44
AE
242
243def split_payload_pieces (eddy_obj, match_type):
244
245 (match_name, pattern) = match_type
246
247 new_pieces_list = []
248 for piece in eddy_obj.payload_pieces:
249 new_pieces_list += scan_and_split(piece, match_name, pattern)
250
251 eddy_obj.payload_pieces = new_pieces_list
252
253
8f61c66a 254def gpg_on_payloads (eddy_obj, gpgme_ctx):
38738401 255
8f61c66a 256 do_to_eddys_pieces(gpg_on_payload_pieces, eddy_obj, gpgme_ctx)
38738401 257
38738401 258
8f61c66a 259def gpg_on_payload_pieces (eddy_obj, gpgme_ctx):
38738401 260
a5d37d44 261 for piece in eddy_obj.payload_pieces:
38738401
AE
262
263 if piece.piece_type == "text":
264 # don't transform the plaintext.
265 pass
266
267 elif piece.piece_type == "message":
268 (plaintext, sigs) = decrypt_block (piece.string, gpgme_ctx)
269
270 if plaintext:
271 piece.gpg_data = GPGData()
272 piece.gpg_data.sigs = sigs
273 # recurse!
274 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
129543c3 275
8f61c66a
AE
276 elif piece.piece_type == "pubkey":
277 fingerprints = add_gpg_key(piece.string, gpgme_ctx)
278
279 if fingerprints != []:
280 piece.gpg_data = GPGData()
281 piece.gpg_data.keys = fingerprints
129543c3
AE
282
283 elif piece.piece_type == "clearsign":
284 (plaintext, fingerprints) = verify_clear_signature(piece.string, gpgme_ctx)
285
286 if fingerprints != []:
287 piece.gpg_data = GPGData()
288 piece.gpg_data.sigs = fingerprints
289 piece.gpg_data.plainobj = parse_pgp_mime(plaintext, gpgme_ctx)
290
38738401
AE
291 else:
292 pass
293
294
adcef2f7 295def build_reply (eddy_obj):
dd11a483 296
adcef2f7 297 string = "\n".join(do_to_eddys_pieces(build_reply_pieces, eddy_obj, None))
56578eaf
AE
298
299 return string
300
301
adcef2f7 302def build_reply_pieces (eddy_obj, _ignore):
0bec96d6 303
56578eaf 304 string = ""
a5d37d44 305 for piece in eddy_obj.payload_pieces:
38738401
AE
306 if piece.piece_type == "text":
307 string += piece.string
efe7336d
AE
308 elif piece.gpg_data == None:
309 string += "Hmmm... I wasn't able to get that part.\n"
38738401
AE
310 elif piece.piece_type == "message":
311 # recursive!
adcef2f7 312 string += build_reply(piece.gpg_data.plainobj)
8f61c66a
AE
313 elif piece.piece_type == "pubkey":
314 string += "thanks for your public key:"
315 for key in piece.gpg_data.keys:
316 string += "\n" + key
129543c3
AE
317 elif piece.piece_type == "clearsign":
318 string += "*** Begin signed part ***\n"
adcef2f7 319 string += build_reply(piece.gpg_data.plainobj)
129543c3 320 string += "\n*** End signed part ***"
56578eaf
AE
321
322 return string
323
324
8f61c66a 325def add_gpg_key (key_block, gpgme_ctx):
c267c233 326
8f61c66a 327 fp = io.BytesIO(key_block.encode('ascii'))
c267c233 328
8f61c66a
AE
329 result = gpgme_ctx.import_(fp)
330 imports = result.imports
c267c233 331
8f61c66a 332 fingerprints = []
c267c233 333
8f61c66a
AE
334 if imports != []:
335 for import_ in imports:
336 fingerprint = import_[0]
d0489345 337 fingerprints += [fingerprint]
c267c233 338
e49673aa 339 debug("added gpg key: " + fingerprint)
ec1e779a 340
d0489345 341 return fingerprints
ec1e779a
AE
342
343
129543c3 344def verify_clear_signature (sig_block, gpgme_ctx):
cf75de65 345
129543c3
AE
346 # FIXME: this might require the un-decoded bytes
347 # or the correct re-encoding with the carset of the mime part.
348 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
349 ptxt_fp = io.BytesIO()
cf75de65 350
129543c3 351 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
cf75de65 352
129543c3
AE
353 # FIXME: this might require using the charset of the mime part.
354 plaintext = ptxt_fp.getvalue().decode('utf-8')
cf75de65 355
129543c3
AE
356 fingerprints = []
357 for res_ in result:
358 fingerprints += [res_.fpr]
cf75de65
AE
359
360 return plaintext, fingerprints
361
362
5b3053c1 363def decrypt_block (msg_block, gpgme_ctx):
0bec96d6 364
5b3053c1 365 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
366 plain_b = io.BytesIO()
367
afc1f64c
AE
368 try:
369 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
370 except:
371 return ("",[])
0bec96d6 372
6aa41372 373 plaintext = plain_b.getvalue().decode('utf-8')
394a1476 374 return (plaintext, sigs)
0bec96d6
AE
375
376
d0489345 377def choose_reply_encryption_key (gpgme_ctx, fingerprints):
fafa21c3
AE
378
379 reply_key = None
d0489345
AE
380 for fp in fingerprints:
381 try:
382 key = gpgme_ctx.get_key(fp)
383
384 if (key.can_encrypt == True):
385 reply_key = key
386 break
387 except:
388 continue
389
fafa21c3 390
216708e9 391 return reply_key
fafa21c3
AE
392
393
d65993b8
AE
394def email_to_from_subject (email_text):
395
396 email_struct = email.parser.Parser().parsestr(email_text)
397
398 email_to = email_struct['To']
399 email_from = email_struct['From']
400 email_subject = email_struct['Subject']
401
402 return email_to, email_from, email_subject
403
404
adcef2f7
AE
405def import_lang(email_to):
406
5250b3b8
AE
407 if email_to != None:
408 for lang in langs:
409 if "edward-" + lang in email_to:
410 lang = "lang." + re.sub('-', '_', lang)
411 language = importlib.import_module(lang)
adcef2f7 412
5250b3b8 413 return language
adcef2f7
AE
414
415 return importlib.import_module("lang.en")
416
417
bf79a93e 418def generate_encrypted_mime (plaintext, email_from, email_subject, encrypt_to_key,
0a064403 419 gpgme_ctx):
1da9b527 420
8bdfb6d4 421
1da9b527
AE
422 reply = "To: " + email_from + "\n"
423 reply += "Subject: " + email_subject + "\n"
216708e9
AE
424
425 if (encrypt_to_key != None):
426 plaintext_reply = "thanks for the message!\n\n\n"
427 plaintext_reply += email_quote_text(plaintext)
428
8bdfb6d4
AE
429 # quoted printable encoding lets most ascii characters look normal
430 # before the decrypted mime message is decoded.
431 char_set = email.charset.Charset("utf-8")
432 char_set.body_encoding = email.charset.QP
433
434 # MIMEText doesn't allow setting the text encoding
435 # so we use MIMENonMultipart.
436 plaintext_mime = MIMENonMultipart('text', 'plain')
437 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
438
439 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
440 encrypt_to_key,
40c37ab3 441 gpgme_ctx)
8bdfb6d4
AE
442
443 control_mime = MIMEApplication("Version: 1",
444 _subtype='pgp-encrypted',
445 _encoder=email.encoders.encode_7or8bit)
446 control_mime['Content-Description'] = 'PGP/MIME version identification'
447 control_mime.set_charset('us-ascii')
448
449 encoded_mime = MIMEApplication(encrypted_text,
450 _subtype='octet-stream; name="encrypted.asc"',
451 _encoder=email.encoders.encode_7or8bit)
452 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
453 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
454 encoded_mime.set_charset('us-ascii')
455
456 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
457 message_mime.attach(control_mime)
458 message_mime.attach(encoded_mime)
459 message_mime['Content-Disposition'] = 'inline'
216708e9 460
8bdfb6d4 461 reply += message_mime.as_string()
216708e9
AE
462
463 else:
8bdfb6d4 464 reply += "\n"
216708e9
AE
465 reply += "Sorry, i couldn't find your key.\n"
466 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
467
468 return reply
469
470
f87041f8
AE
471def email_quote_text (text):
472
473 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
474
475 return quoted_message
476
477
0a064403 478def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
897cbaf6 479
6aa41372 480 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
481 encrypted_bytes = io.BytesIO()
482
897cbaf6 483 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
484 plaintext_bytes, encrypted_bytes)
485
6aa41372 486 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
487 return encrypted_txt
488
489
0a064403
AE
490def error (error_msg):
491
e4fb2ab2 492 sys.stderr.write(progname + ": " + str(error_msg) + "\n")
0a064403
AE
493
494
5e8f9094
AE
495def debug (debug_msg):
496
497 if edward_config.debug == True:
0a064403 498 error(debug_msg)
5e8f9094
AE
499
500
20f6e7c5
AE
501def handle_args ():
502 if __name__ == "__main__":
503
504 global progname
505 progname = sys.argv[0]
506
507 if len(sys.argv) > 1:
508 print(progname + ": error, this program doesn't " \
509 "need any arguments.", file=sys.stderr)
510 exit(1)
511
512
0bec96d6
AE
513main()
514