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