progress made toward implementing a better parser
[edward.git] / edward
1 #! /usr/bin/env python3
2 # -*- coding: utf-8 -*-
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+) *
20 * Copyright (C) 2009-2015 Tails developers <tails@boum.org> ( GPLv3+) *
21 * Copyright (C) 2009 W. Trevor King <wking@drexel.edu> ( GPLv2+) *
22 * *
23 * Special thanks to Josh Drake for writing the original edward bot! :) *
24 * *
25 ************************************************************************
26
27 Code sourced from these projects:
28
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
32 """
33
34 import sys
35 import gpgme
36 import re
37 import io
38 import os
39
40 import email.parser
41 import email.message
42 import email.encoders
43
44 from email.mime.multipart import MIMEMultipart
45 from email.mime.application import MIMEApplication
46 from email.mime.nonmultipart import MIMENonMultipart
47
48 import edward_config
49
50 match_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
60 class 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
74 class PayloadPiece (object):
75 def __init__(self):
76 self.piece_type = None
77 self.string = None
78 self.gpg_data = None
79
80
81 def main ():
82
83 handle_args()
84
85 gpgme_ctx = get_gpg_context(edward_config.gnupghome,
86 edward_config.sign_with_key)
87
88 email_text = sys.stdin.read()
89 result = parse_pgp_mime(email_text)
90
91 email_from, email_subject = email_from_subject(email_text)
92
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)
99
100 print(flatten_eddy(result))
101
102
103 def 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
122 def parse_pgp_mime (email_text):
123
124 email_struct = email.parser.Parser().parsestr(email_text)
125
126 eddy_obj = parse_mime(email_struct)
127 eddy_obj = split_payloads(eddy_obj)
128
129 return eddy_obj
130
131
132 def parse_mime(msg_struct):
133
134 eddy_obj = EddyMsg()
135
136 if msg_struct.is_multipart() == True:
137 payloads = msg_struct.get_payload()
138
139 eddy_obj.multipart = True
140 eddy_obj.subparts = map(parse_mime, payloads)
141 else:
142 eddy_obj = get_subpart_data(msg_struct)
143
144 return eddy_obj
145
146
147 def split_payloads (eddy_obj):
148
149 if eddy_obj.multipart == True:
150 eddy_obj.subparts = map(split_payloads, eddy_obj.subparts)
151
152 else:
153 for (match_type, pattern) in match_types:
154
155 new_pieces_list = []
156 for payload_piece in eddy_obj.payload_pieces:
157 new_pieces_list += scan_and_split(payload_piece,
158 match_type, pattern)
159 eddy_obj.payload_pieces = new_pieces_list
160
161 return eddy_obj
162
163
164 def scan_and_split (payload_piece, match_type, pattern):
165
166 flags = re.DOTALL | re.MULTILINE
167 matches = re.search("(?P<beginning>.*?)(?P<match>" + pattern +
168 ")(?P<rest>.*)", payload_piece.string, flags=flags)
169
170 if matches == None:
171 pieces = [payload_piece]
172
173 else:
174
175 beginning = PayloadPiece()
176 beginning.string = matches.group('beginning')
177 beginning.piece_type = payload_piece.piece_type
178
179 match = PayloadPiece()
180 match.string = matches.group('match')
181 match.piece_type = match_type
182
183 rest = PayloadPiece()
184 rest.string = matches.group('rest')
185 rest.piece_type = payload_piece.piece_type
186
187 more_pieces = scan_and_split(rest, match_type, pattern)
188
189 if more_pieces == None:
190 pieces = [beginning, match, rest]
191 else:
192 pieces = [beginning, match] + more_pieces
193
194 return pieces
195
196
197 def get_subpart_data (part):
198
199 obj = EddyMsg()
200
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:
213 # this belongs in a specific try statement.
214 payload = PayloadPiece()
215 payload.string = obj.payload_bytes.decode(obj.charset)
216 payload.piece_type = 'text'
217
218 obj.payload_pieces = [payload]
219
220 return obj
221
222
223 def flatten_eddy (eddy_obj):
224
225 if eddy_obj.multipart == True:
226 string = "\n".join(map(flatten_eddy, eddy_obj.subparts))
227 else:
228 string = flatten_payload_piece(eddy_obj.payload_pieces)
229
230 return string
231
232
233 def flatten_payload_piece (payload_pieces):
234
235 string = ""
236 for piece in payload_pieces:
237 string += piece.string
238
239 return string
240
241
242 def email_from_subject (email_text):
243
244 email_struct = email.parser.Parser().parsestr(email_text)
245
246 email_from = email_struct['From']
247 email_subject = email_struct['Subject']
248
249 return email_from, email_subject
250
251
252 def add_gpg_keys (text, gpgme_ctx):
253
254 key_blocks = scan_and_grab(text,
255 '-----BEGIN PGP PUBLIC KEY BLOCK-----',
256 '-----END PGP PUBLIC KEY BLOCK-----')
257
258 fingerprints = []
259 for key_block in key_blocks:
260 fp = io.BytesIO(key_block.encode('ascii'))
261
262 result = gpgme_ctx.import_(fp)
263 imports = result.imports
264
265 if imports != []:
266 fingerprint = imports[0][0]
267 fingerprints += [fingerprint]
268
269 debug("added gpg key: " + fingerprint)
270
271 return fingerprints
272
273
274 def decrypt_text (gpg_text, gpgme_ctx):
275
276 body = ""
277 fingerprints = []
278
279 msg_blocks = scan_and_grab(gpg_text,
280 '-----BEGIN PGP MESSAGE-----',
281 '-----END PGP MESSAGE-----')
282
283 plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx)
284
285 for pair in plaintexts_and_sigs:
286 plaintext = pair[0]
287 sigs = pair[1]
288
289 for sig in sigs:
290 fingerprints += [sig.fpr]
291
292 # recursive for nested layers of mime and/or gpg
293 plaintext, more_fps = email_decode_flatten(plaintext, gpgme_ctx, True)
294
295 body += plaintext
296 fingerprints += more_fps
297
298 return body, fingerprints
299
300
301 def verify_clear_signature (text, gpgme_ctx):
302
303 sig_blocks = scan_and_grab(text,
304 '-----BEGIN PGP SIGNED MESSAGE-----',
305 '-----END PGP SIGNATURE-----')
306
307 fingerprints = []
308 plaintext = ""
309
310 for sig_block in sig_blocks:
311 msg_fp = io.BytesIO(sig_block.encode('utf-8'))
312 ptxt_fp = io.BytesIO()
313
314 result = gpgme_ctx.verify(msg_fp, None, ptxt_fp)
315
316 plaintext += ptxt_fp.getvalue().decode('utf-8')
317 fingerprint = result[0].fpr
318
319 fingerprints += [fingerprint]
320
321 return plaintext, fingerprints
322
323
324 def scan_and_grab (text, start_text, end_text):
325
326 matches = re.search('(' + start_text + '.*' + end_text + ')',
327 text, flags=re.DOTALL)
328
329 if matches != None:
330 match_tuple = matches.groups()
331 else:
332 match_tuple = ()
333
334 return match_tuple
335
336
337 def decrypt_blocks (msg_blocks, gpgme_ctx):
338
339 return [decrypt_block(block, gpgme_ctx) for block in msg_blocks]
340
341
342 def decrypt_block (msg_block, gpgme_ctx):
343
344 block_b = io.BytesIO(msg_block.encode('ascii'))
345 plain_b = io.BytesIO()
346
347 try:
348 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
349 except:
350 return ("",[])
351
352 plaintext = plain_b.getvalue().decode('utf-8')
353 return (plaintext, sigs)
354
355
356 def choose_reply_encryption_key (gpgme_ctx, fingerprints):
357
358 reply_key = None
359 for fp in fingerprints:
360 try:
361 key = gpgme_ctx.get_key(fp)
362
363 if (key.can_encrypt == True):
364 reply_key = key
365 break
366 except:
367 continue
368
369
370 return reply_key
371
372
373 def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
374 gpgme_ctx):
375
376
377 reply = "To: " + email_from + "\n"
378 reply += "Subject: " + email_subject + "\n"
379
380 if (encrypt_to_key != None):
381 plaintext_reply = "thanks for the message!\n\n\n"
382 plaintext_reply += email_quote_text(plaintext)
383
384 # quoted printable encoding lets most ascii characters look normal
385 # before the decrypted mime message is decoded.
386 char_set = email.charset.Charset("utf-8")
387 char_set.body_encoding = email.charset.QP
388
389 # MIMEText doesn't allow setting the text encoding
390 # so we use MIMENonMultipart.
391 plaintext_mime = MIMENonMultipart('text', 'plain')
392 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
393
394 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
395 encrypt_to_key,
396 gpgme_ctx)
397
398 control_mime = MIMEApplication("Version: 1",
399 _subtype='pgp-encrypted',
400 _encoder=email.encoders.encode_7or8bit)
401 control_mime['Content-Description'] = 'PGP/MIME version identification'
402 control_mime.set_charset('us-ascii')
403
404 encoded_mime = MIMEApplication(encrypted_text,
405 _subtype='octet-stream; name="encrypted.asc"',
406 _encoder=email.encoders.encode_7or8bit)
407 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
408 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
409 encoded_mime.set_charset('us-ascii')
410
411 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
412 message_mime.attach(control_mime)
413 message_mime.attach(encoded_mime)
414 message_mime['Content-Disposition'] = 'inline'
415
416 reply += message_mime.as_string()
417
418 else:
419 reply += "\n"
420 reply += "Sorry, i couldn't find your key.\n"
421 reply += "I'll need that to encrypt a message to you."
422
423 return reply
424
425
426 def email_quote_text (text):
427
428 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
429
430 return quoted_message
431
432
433 def encrypt_sign_message (plaintext, encrypt_to_key, gpgme_ctx):
434
435 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
436 encrypted_bytes = io.BytesIO()
437
438 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
439 plaintext_bytes, encrypted_bytes)
440
441 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
442 return encrypted_txt
443
444
445 def error (error_msg):
446
447 sys.stderr.write(progname + ": " + error_msg + "\n")
448
449
450 def debug (debug_msg):
451
452 if edward_config.debug == True:
453 error(debug_msg)
454
455
456 def handle_args ():
457 if __name__ == "__main__":
458
459 global progname
460 progname = sys.argv[0]
461
462 if len(sys.argv) > 1:
463 print(progname + ": error, this program doesn't " \
464 "need any arguments.", file=sys.stderr)
465 exit(1)
466
467
468 main()
469