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