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