created higher order function used by flatten_eddy
[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
0bec96d6 39
8bdfb6d4
AE
40import email.parser
41import email.message
42import email.encoders
43
44from email.mime.multipart import MIMEMultipart
45from email.mime.application import MIMEApplication
46from email.mime.nonmultipart import MIMENonMultipart
47
40c37ab3 48import edward_config
c96f3837 49
56578eaf
AE
50match_types = [('encrypted',
51 '-----BEGIN PGP MESSAGE-----.*?-----END PGP MESSAGE-----'),
52 ('pubkey',
53 '-----BEGIN PGP PUBLIC KEY BLOCK-----.*?-----END PGP PUBLIC KEY BLOCK-----'),
54 ('detachedsig',
55 '-----END PGP SIGNATURE-----.*?-----BEGIN PGP SIGNATURE-----'),
56 ('clearsign',
57 '-----BEGIN PGP SIGNED MESSAGE-----.*?-----BEGIN PGP SIGNATURE-----.*?-----END PGP SIGNATURE-----')]
58
59
60class EddyMsg (object):
61 def __init__(self):
62 self.multipart = False
63 self.subparts = []
64
65 self.charset = None
66 self.payload_bytes = None
67 self.payload_pieces = []
68
69 self.filename = None
70 self.content_type = None
71 self.description_list = None
72
73
74class PayloadPiece (object):
75 def __init__(self):
76 self.piece_type = None
77 self.string = None
78 self.gpg_data = None
79
80
0bec96d6
AE
81def main ():
82
20f6e7c5 83 handle_args()
0bec96d6 84
0a064403
AE
85 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
86 edward_config.sign_with_key)
87
c96f3837 88 email_text = sys.stdin.read()
56578eaf 89 result = parse_pgp_mime(email_text)
65ed3800 90
56578eaf 91 email_from, email_subject = email_from_subject(email_text)
fafa21c3 92
56578eaf
AE
93# plaintext, fingerprints = email_decode_flatten(email_text, gpgme_ctx, False)
94# encrypt_to_key = choose_reply_encryption_key(gpgme_ctx, fingerprints)
95#
96# reply_message = generate_reply(plaintext, email_from, \
97# email_subject, encrypt_to_key,
98# gpgme_ctx)
c96f3837 99
56578eaf 100 print(flatten_eddy(result))
c96f3837 101
0bec96d6 102
0a064403
AE
103def get_gpg_context (gnupghome, sign_with_key_fp):
104
105 os.environ['GNUPGHOME'] = gnupghome
106
107 gpgme_ctx = gpgme.Context()
108 gpgme_ctx.armor = True
109
110 try:
111 sign_with_key = gpgme_ctx.get_key(sign_with_key_fp)
112 except:
113 error("unable to load signing key. is the gnupghome "
114 + "and signing key properly set in the edward_config.py?")
115 exit(1)
116
117 gpgme_ctx.signers = [sign_with_key]
118
119 return gpgme_ctx
120
121
56578eaf 122def parse_pgp_mime (email_text):
394a1476
AE
123
124 email_struct = email.parser.Parser().parsestr(email_text)
125
56578eaf
AE
126 eddy_obj = parse_mime(email_struct)
127 eddy_obj = split_payloads(eddy_obj)
8bb4b0d5 128
56578eaf 129 return eddy_obj
0bec96d6 130
0bec96d6 131
56578eaf 132def parse_mime(msg_struct):
0bec96d6 133
56578eaf 134 eddy_obj = EddyMsg()
8bb4b0d5 135
56578eaf
AE
136 if msg_struct.is_multipart() == True:
137 payloads = msg_struct.get_payload()
0bec96d6 138
56578eaf 139 eddy_obj.multipart = True
dd11a483
AE
140 eddy_obj.subparts = list(map(parse_mime, payloads))
141
56578eaf
AE
142 else:
143 eddy_obj = get_subpart_data(msg_struct)
394a1476 144
56578eaf 145 return eddy_obj
c267c233 146
80119cab 147
56578eaf 148def split_payloads (eddy_obj):
3a9d1426 149
56578eaf 150 if eddy_obj.multipart == True:
dd11a483 151 eddy_obj.subparts = list(map(split_payloads, eddy_obj.subparts))
cf75de65 152
56578eaf
AE
153 else:
154 for (match_type, pattern) in match_types:
0bec96d6 155
56578eaf
AE
156 new_pieces_list = []
157 for payload_piece in eddy_obj.payload_pieces:
158 new_pieces_list += scan_and_split(payload_piece,
159 match_type, pattern)
160 eddy_obj.payload_pieces = new_pieces_list
86663388 161
56578eaf 162 return eddy_obj
cf75de65
AE
163
164
56578eaf 165def scan_and_split (payload_piece, match_type, pattern):
cf75de65 166
56578eaf
AE
167 flags = re.DOTALL | re.MULTILINE
168 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
169 ")(?P<rest>.*)", payload_piece.string, flags=flags)
86663388 170
56578eaf
AE
171 if matches == None:
172 pieces = [payload_piece]
c96f3837 173
56578eaf 174 else:
d437f8b2 175
56578eaf
AE
176 beginning = PayloadPiece()
177 beginning.string = matches.group('beginning')
178 beginning.piece_type = payload_piece.piece_type
d437f8b2 179
56578eaf
AE
180 match = PayloadPiece()
181 match.string = matches.group('match')
182 match.piece_type = match_type
d437f8b2 183
56578eaf
AE
184 rest = PayloadPiece()
185 rest.string = matches.group('rest')
186 rest.piece_type = payload_piece.piece_type
d437f8b2 187
56578eaf 188 more_pieces = scan_and_split(rest, match_type, pattern)
d437f8b2 189
56578eaf
AE
190 if more_pieces == None:
191 pieces = [beginning, match, rest]
192 else:
193 pieces = [beginning, match] + more_pieces
d437f8b2 194
56578eaf 195 return pieces
d437f8b2 196
d437f8b2 197
56578eaf 198def get_subpart_data (part):
0bec96d6 199
56578eaf 200 obj = EddyMsg()
0bec96d6 201
56578eaf
AE
202 obj.charset = part.get_content_charset()
203 obj.payload_bytes = part.get_payload(decode=True)
204
205 obj.filename = part.get_filename()
206 obj.content_type = part.get_content_type()
207 obj.description_list = part['content-description']
208
209 # your guess is as good as a-myy-ee-ine...
210 if obj.charset == None:
211 obj.charset = 'utf-8'
212
213 if obj.payload_bytes != None:
0eb75d9c
AE
214 try:
215 payload = PayloadPiece()
216 payload.string = obj.payload_bytes.decode(obj.charset)
217 payload.piece_type = 'text'
218
219 obj.payload_pieces = [payload]
220 except UnicodeDecodeError:
221 pass
56578eaf
AE
222
223 return obj
224
225
dd11a483 226def do_to_eddys_pieces (function_to_do, eddy_obj, data):
56578eaf
AE
227
228 if eddy_obj.multipart == True:
dd11a483
AE
229 result_list = []
230 for sub in eddy_obj.subparts:
231 result_list += do_to_eddys_pieces(function_to_do, sub, data)
394a1476 232 else:
dd11a483
AE
233 result_list = [function_to_do(eddy_obj.payload_pieces, data)]
234
235 return result_list
236
237
238def flatten_eddy (eddy_obj):
239
240 string = "\n".join(do_to_eddys_pieces(flatten_payload_pieces, eddy_obj, None))
56578eaf
AE
241
242 return string
243
244
dd11a483 245def flatten_payload_pieces (payload_pieces, _ignore):
0bec96d6 246
56578eaf
AE
247 string = ""
248 for piece in payload_pieces:
249 string += piece.string
250
251 return string
252
253
254def email_from_subject (email_text):
255
256 email_struct = email.parser.Parser().parsestr(email_text)
257
258 email_from = email_struct['From']
259 email_subject = email_struct['Subject']
260
261 return email_from, email_subject
0bec96d6 262
0bec96d6 263
c267c233
AE
264def add_gpg_keys (text, gpgme_ctx):
265
83210634
AE
266 key_blocks = scan_and_grab(text,
267 '-----BEGIN PGP PUBLIC KEY BLOCK-----',
268 '-----END PGP PUBLIC KEY BLOCK-----')
c267c233 269
d0489345 270 fingerprints = []
83210634
AE
271 for key_block in key_blocks:
272 fp = io.BytesIO(key_block.encode('ascii'))
c267c233
AE
273
274 result = gpgme_ctx.import_(fp)
e49673aa 275 imports = result.imports
c267c233 276
e49673aa
AE
277 if imports != []:
278 fingerprint = imports[0][0]
d0489345 279 fingerprints += [fingerprint]
c267c233 280
e49673aa 281 debug("added gpg key: " + fingerprint)
ec1e779a 282
d0489345 283 return fingerprints
ec1e779a
AE
284
285
40c37ab3 286def decrypt_text (gpg_text, gpgme_ctx):
86663388 287
394a1476 288 body = ""
d0489345 289 fingerprints = []
0bec96d6 290
5b3053c1 291 msg_blocks = scan_and_grab(gpg_text,
c267c233
AE
292 '-----BEGIN PGP MESSAGE-----',
293 '-----END PGP MESSAGE-----')
86663388 294
5b3053c1 295 plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx)
0bec96d6 296
5b3053c1
AE
297 for pair in plaintexts_and_sigs:
298 plaintext = pair[0]
299 sigs = pair[1]
9eb78301 300
394a1476 301 for sig in sigs:
d0489345 302 fingerprints += [sig.fpr]
0bec96d6 303
394a1476 304 # recursive for nested layers of mime and/or gpg
d0489345 305 plaintext, more_fps = email_decode_flatten(plaintext, gpgme_ctx, True)
8bb4b0d5 306
394a1476 307 body += plaintext
d0489345 308 fingerprints += more_fps
8bb4b0d5 309
d0489345 310 return body, fingerprints
8bb4b0d5 311
8bb4b0d5 312
cf75de65
AE
313def verify_clear_signature (text, gpgme_ctx):
314
315 sig_blocks = scan_and_grab(text,
316 '-----BEGIN PGP SIGNED MESSAGE-----',
317 '-----END PGP SIGNATURE-----')
318
319 fingerprints = []
320 plaintext = ""
321
322 for sig_block in sig_blocks:
323 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
324 ptxt_fp = io.BytesIO()
325
326 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
327
328 plaintext += ptxt_fp.getvalue().decode('utf-8')
329 fingerprint = result[0].fpr
330
331 fingerprints += [fingerprint]
332
333 return plaintext, fingerprints
334
335
c267c233 336def scan_and_grab (text, start_text, end_text):
0bec96d6 337
c267c233
AE
338 matches = re.search('(' + start_text + '.*' + end_text + ')',
339 text, flags=re.DOTALL)
0bec96d6 340
c267c233
AE
341 if matches != None:
342 match_tuple = matches.groups()
0bec96d6 343 else:
c267c233 344 match_tuple = ()
7b06b980 345
c267c233 346 return match_tuple
0bec96d6
AE
347
348
5b3053c1 349def decrypt_blocks (msg_blocks, gpgme_ctx):
0bec96d6 350
5b3053c1 351 return [decrypt_block(block, gpgme_ctx) for block in msg_blocks]
0bec96d6 352
0bec96d6 353
5b3053c1 354def decrypt_block (msg_block, gpgme_ctx):
0bec96d6 355
5b3053c1 356 block_b = io.BytesIO(msg_block.encode('ascii'))
0bec96d6
AE
357 plain_b = io.BytesIO()
358
afc1f64c
AE
359 try:
360 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
361 except:
362 return ("",[])
0bec96d6 363
6aa41372 364 plaintext = plain_b.getvalue().decode('utf-8')
394a1476 365 return (plaintext, sigs)
0bec96d6
AE
366
367
d0489345 368def choose_reply_encryption_key (gpgme_ctx, fingerprints):
fafa21c3
AE
369
370 reply_key = None
d0489345
AE
371 for fp in fingerprints:
372 try:
373 key = gpgme_ctx.get_key(fp)
374
375 if (key.can_encrypt == True):
376 reply_key = key
377 break
378 except:
379 continue
380
fafa21c3 381
216708e9 382 return reply_key
fafa21c3
AE
383
384
897cbaf6 385def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
0a064403 386 gpgme_ctx):
1da9b527 387
8bdfb6d4 388
1da9b527
AE
389 reply = "To: " + email_from + "\n"
390 reply += "Subject: " + email_subject + "\n"
216708e9
AE
391
392 if (encrypt_to_key != None):
393 plaintext_reply = "thanks for the message!\n\n\n"
394 plaintext_reply += email_quote_text(plaintext)
395
8bdfb6d4
AE
396 # quoted printable encoding lets most ascii characters look normal
397 # before the decrypted mime message is decoded.
398 char_set = email.charset.Charset("utf-8")
399 char_set.body_encoding = email.charset.QP
400
401 # MIMEText doesn't allow setting the text encoding
402 # so we use MIMENonMultipart.
403 plaintext_mime = MIMENonMultipart('text', 'plain')
404 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
405
406 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
407 encrypt_to_key,
40c37ab3 408 gpgme_ctx)
8bdfb6d4
AE
409
410 control_mime = MIMEApplication("Version: 1",
411 _subtype='pgp-encrypted',
412 _encoder=email.encoders.encode_7or8bit)
413 control_mime['Content-Description'] = 'PGP/MIME version identification'
414 control_mime.set_charset('us-ascii')
415
416 encoded_mime = MIMEApplication(encrypted_text,
417 _subtype='octet-stream; name="encrypted.asc"',
418 _encoder=email.encoders.encode_7or8bit)
419 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
420 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
421 encoded_mime.set_charset('us-ascii')
422
423 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
424 message_mime.attach(control_mime)
425 message_mime.attach(encoded_mime)
426 message_mime['Content-Disposition'] = 'inline'
216708e9 427
8bdfb6d4 428 reply += message_mime.as_string()
216708e9
AE
429
430 else:
8bdfb6d4 431 reply += "\n"
216708e9
AE
432 reply += "Sorry, i couldn't find your key.\n"
433 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
434
435 return reply
436
437
f87041f8
AE
438def email_quote_text (text):
439
440 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
441
442 return quoted_message
443
444
0a064403 445def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
897cbaf6 446
6aa41372 447 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
448 encrypted_bytes = io.BytesIO()
449
897cbaf6 450 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
451 plaintext_bytes, encrypted_bytes)
452
6aa41372 453 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
454 return encrypted_txt
455
456
0a064403
AE
457def error (error_msg):
458
459 sys.stderr.write(progname + ": " + error_msg + "\n")
460
461
5e8f9094
AE
462def debug (debug_msg):
463
464 if edward_config.debug == True:
0a064403 465 error(debug_msg)
5e8f9094
AE
466
467
20f6e7c5
AE
468def handle_args ():
469 if __name__ == "__main__":
470
471 global progname
472 progname = sys.argv[0]
473
474 if len(sys.argv) > 1:
475 print(progname + ": error, this program doesn't " \
476 "need any arguments.", file=sys.stderr)
477 exit(1)
478
479
0bec96d6
AE
480main()
481