simplified unattractive code
[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
35 import sys
36 import gpgme
37 import re
38 import io
39 import os
40
41 import email.parser
42 import email.message
43 import email.encoders
44
45 from email.mime.multipart import MIMEMultipart
46 from email.mime.application import MIMEApplication
47 from email.mime.nonmultipart import MIMENonMultipart
48
49 import edward_config
50
51 def main ():
52
53 handle_args()
54
55 email_text = sys.stdin.read()
56 email_from, email_subject = email_from_subject(email_text)
57
58 os.environ['GNUPGHOME'] = edward_config.gnupghome
59 gpgme_ctx = gpgme.Context()
60 gpgme_ctx.armor = True
61
62 plaintext, keys = email_decode_flatten(email_text, gpgme_ctx, False)
63 encrypt_to_key = choose_reply_encryption_key(keys)
64
65 reply_message = generate_reply(plaintext, email_from, \
66 email_subject, encrypt_to_key,
67 edward_config.sign_with_key,
68 gpgme_ctx)
69
70 print(reply_message)
71
72
73 def email_decode_flatten (email_text, gpgme_ctx, from_decryption):
74
75 body = ""
76 keys = []
77
78 email_struct = email.parser.Parser().parsestr(email_text)
79
80 for subpart in email_struct.walk():
81
82 payload, description, filename, content_type \
83 = get_email_subpart_info(subpart)
84
85 if payload == "":
86 continue
87
88 if content_type == "multipart":
89 continue
90
91 if content_type == "application/pgp-encrypted":
92 if ((description == "PGP/MIME version identification")
93 and (payload.strip() != "Version: 1")):
94 debug("Warning: unknown " + description
95 + ": " + payload.strip())
96 # ignore the version number
97 continue
98
99 if (filename == "encrypted.asc") or (content_type == "pgp/mime"):
100 plaintext, more_keys = decrypt_text(payload, gpgme_ctx)
101
102 body += plaintext
103 keys += more_keys
104
105 elif content_type == "application/pgp-keys":
106 keys += add_gpg_keys(payload, gpgme_ctx)
107
108 elif content_type == "text/plain":
109 if from_decryption == True:
110 body += payload + "\n"
111
112 else:
113 plaintext, more_keys = decrypt_text(payload, gpgme_ctx)
114
115 body += plaintext
116 keys += more_keys
117
118 keys += add_gpg_keys(payload, gpgme_ctx)
119
120 return body, keys
121
122
123 def email_from_subject (email_text):
124
125 email_struct = email.parser.Parser().parsestr(email_text)
126
127 email_from = email_struct['From']
128 email_subject = email_struct['Subject']
129
130 return email_from, email_subject
131
132
133 def get_email_subpart_info (part):
134
135 charset = part.get_content_charset()
136 payload_bytes = part.get_payload(decode=True)
137
138 filename = part.get_filename()
139 content_type = part.get_content_type()
140 description_list = part.get_params(header='content-description')
141
142 if charset == None:
143 charset = 'utf-8'
144
145 if payload_bytes != None:
146 payload = payload_bytes.decode(charset)
147 else:
148 payload = ""
149
150 if description_list != None:
151 description = description_list[0][0]
152 else:
153 description = ""
154
155 return payload, description, filename, content_type
156
157
158 def add_gpg_keys (text, gpgme_ctx):
159
160 key_blocks = scan_and_grab(text,
161 '-----BEGIN PGP PUBLIC KEY BLOCK-----',
162 '-----END PGP PUBLIC KEY BLOCK-----')
163
164 keys = []
165 for key_block in key_blocks:
166 fp = io.BytesIO(key_block.encode('ascii'))
167
168 result = gpgme_ctx.import_(fp)
169 imports = result.imports
170
171 if imports != []:
172 fingerprint = imports[0][0]
173 keys += [gpgme_ctx.get_key(fingerprint)]
174
175 debug("added gpg key: " + fingerprint)
176
177 return keys
178
179
180 def decrypt_text (gpg_text, gpgme_ctx):
181
182 body = ""
183 keys = []
184
185 msg_blocks = scan_and_grab(gpg_text,
186 '-----BEGIN PGP MESSAGE-----',
187 '-----END PGP MESSAGE-----')
188
189 plaintexts_and_sigs = decrypt_blocks(msg_blocks, gpgme_ctx)
190
191 for pair in plaintexts_and_sigs:
192 plaintext = pair[0]
193 sigs = pair[1]
194
195 for sig in sigs:
196 keys += [gpgme_ctx.get_key(sig.fpr)]
197
198 # recursive for nested layers of mime and/or gpg
199 plaintext, more_keys = email_decode_flatten(plaintext, gpgme_ctx, True)
200
201 body += plaintext
202 keys += more_keys
203
204 return body, keys
205
206
207 def scan_and_grab (text, start_text, end_text):
208
209 matches = re.search('(' + start_text + '.*' + end_text + ')',
210 text, flags=re.DOTALL)
211
212 if matches != None:
213 match_tuple = matches.groups()
214 else:
215 match_tuple = ()
216
217 return match_tuple
218
219
220 def decrypt_blocks (msg_blocks, gpgme_ctx):
221
222 return [decrypt_block(block, gpgme_ctx) for block in msg_blocks]
223
224
225 def decrypt_block (msg_block, gpgme_ctx):
226
227 block_b = io.BytesIO(msg_block.encode('ascii'))
228 plain_b = io.BytesIO()
229
230 try:
231 sigs = gpgme_ctx.decrypt_verify(block_b, plain_b)
232 except:
233 return ("",[])
234
235 plaintext = plain_b.getvalue().decode('utf-8')
236 return (plaintext, sigs)
237
238
239 def choose_reply_encryption_key (keys):
240
241 reply_key = None
242 for key in keys:
243 if (key.can_encrypt == True):
244 reply_key = key
245 break
246
247 return reply_key
248
249
250 def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
251 sign_with_fingerprint, gpgme_ctx):
252
253
254 reply = "To: " + email_from + "\n"
255 reply += "Subject: " + email_subject + "\n"
256
257 if (encrypt_to_key != None):
258 plaintext_reply = "thanks for the message!\n\n\n"
259 plaintext_reply += email_quote_text(plaintext)
260
261 # quoted printable encoding lets most ascii characters look normal
262 # before the decrypted mime message is decoded.
263 char_set = email.charset.Charset("utf-8")
264 char_set.body_encoding = email.charset.QP
265
266 # MIMEText doesn't allow setting the text encoding
267 # so we use MIMENonMultipart.
268 plaintext_mime = MIMENonMultipart('text', 'plain')
269 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
270
271 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
272 encrypt_to_key,
273 sign_with_fingerprint,
274 gpgme_ctx)
275
276 control_mime = MIMEApplication("Version: 1",
277 _subtype='pgp-encrypted',
278 _encoder=email.encoders.encode_7or8bit)
279 control_mime['Content-Description'] = 'PGP/MIME version identification'
280 control_mime.set_charset('us-ascii')
281
282 encoded_mime = MIMEApplication(encrypted_text,
283 _subtype='octet-stream; name="encrypted.asc"',
284 _encoder=email.encoders.encode_7or8bit)
285 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
286 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
287 encoded_mime.set_charset('us-ascii')
288
289 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
290 message_mime.attach(control_mime)
291 message_mime.attach(encoded_mime)
292 message_mime['Content-Disposition'] = 'inline'
293
294 reply += message_mime.as_string()
295
296 else:
297 reply += "\n"
298 reply += "Sorry, i couldn't find your key.\n"
299 reply += "I'll need that to encrypt a message to you."
300
301 return reply
302
303
304 def email_quote_text (text):
305
306 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
307
308 return quoted_message
309
310
311 def encrypt_sign_message (plaintext, encrypt_to_key, sign_with_fingerprint, gpgme_ctx):
312
313 sign_with_key = gpgme_ctx.get_key(sign_with_fingerprint)
314 gpgme_ctx.signers = [sign_with_key]
315
316 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
317 encrypted_bytes = io.BytesIO()
318
319 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
320 plaintext_bytes, encrypted_bytes)
321
322 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
323 return encrypted_txt
324
325
326 def debug (debug_msg):
327
328 if edward_config.debug == True:
329 sys.stderr.write(progname + ": " + debug_msg + "\n")
330
331
332 def handle_args ():
333 if __name__ == "__main__":
334
335 global progname
336 progname = sys.argv[0]
337
338 if len(sys.argv) > 1:
339 print(progname + ": error, this program doesn't " \
340 "need any arguments.", file=sys.stderr)
341 exit(1)
342
343
344 main()
345