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