renamed edward-bot to edward
[edward.git] / edward
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+) *
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 used from:
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
40 import email.parser
41 import email.message
42 import email.encoders
43
44 from email.mime.multipart import MIMEMultipart
45 from email.mime.application import MIMEApplication
46 from email.mime.nonmultipart import MIMENonMultipart
47
48
49 def main ():
50
51 handle_args()
52
53 email_text = sys.stdin.read()
54
55 email_from, email_subject = email_from_subject(email_text)
56
57 plaintext, keys = email_decode_flatten(email_text)
58 encrypt_to_key = choose_reply_encryption_key(keys)
59
60 reply_message = generate_reply(plaintext, email_from, \
61 email_subject, encrypt_to_key,
62 "DAB4F989E2788B8DF058E0EFEF1EC52039B36E58")
63
64 print(reply_message)
65
66
67 def email_decode_flatten (email_text):
68
69 body = ""
70 keys = []
71
72 email_struct = email.parser.Parser().parsestr(email_text)
73
74 for subpart in email_struct.walk():
75
76 payload, description, filename, content_type \
77 = get_email_subpart_info(subpart)
78
79 if payload == "":
80 continue
81
82 if content_type == "multipart":
83 continue
84
85 if content_type == "application/pgp-encrypted":
86 if description == "PGP/MIME version identification":
87 if payload.strip() != "Version: 1":
88 print(progname + ": Warning: unknown " \
89 + description + ": " \
90 + payload.strip(), file=sys.stderr)
91 continue
92
93
94 if (filename == "encrypted.asc") or (content_type == "pgp/mime"):
95 plaintext, more_keys = decrypt_text(payload)
96
97 body += plaintext
98 keys += more_keys
99
100 elif content_type == "text/plain":
101 body += payload + "\n"
102
103 else:
104 body += payload + "\n"
105
106 return body, keys
107
108
109 def email_from_subject (email_text):
110
111 email_struct = email.parser.Parser().parsestr(email_text)
112
113 email_from = email_struct['From']
114 email_subject = email_struct['Subject']
115
116 return email_from, email_subject
117
118
119 def get_email_subpart_info (part):
120
121 charset = part.get_content_charset()
122 payload_bytes = part.get_payload(decode=True)
123
124 filename = part.get_filename()
125 content_type = part.get_content_type()
126 description_list = part.get_params(header='content-description')
127
128 if charset == None:
129 charset = 'utf-8'
130
131 if payload_bytes != None:
132 payload = payload_bytes.decode(charset)
133 else:
134 payload = ""
135
136 if description_list != None:
137 description = description_list[0][0]
138 else:
139 description = ""
140
141 return payload, description, filename, content_type
142
143
144 def decrypt_text (gpg_text):
145
146 body = ""
147 keys = []
148
149 gpg_chunks = split_message(gpg_text)
150
151 plaintext_and_sigs_chunks = decrypt_chunks(gpg_chunks)
152
153 for chunk in plaintext_and_sigs_chunks:
154 plaintext = chunk[0]
155 sigs = chunk[1]
156
157 for sig in sigs:
158 key = get_pub_key(sig)
159 keys += [key]
160
161 # recursive for nested layers of mime and/or gpg
162 plaintext, more_keys = email_decode_flatten(plaintext)
163
164 body += plaintext
165 keys += more_keys
166
167 return body, keys
168
169
170 def get_pub_key (sig):
171
172 gpgme_ctx = gpgme.Context()
173
174 fingerprint = sig.fpr
175 key = gpgme_ctx.get_key(fingerprint)
176
177 return key
178
179
180 def split_message (text):
181
182 gpg_matches = re.search( \
183 '(-----BEGIN PGP MESSAGE-----' + \
184 '.*' + \
185 '-----END PGP MESSAGE-----)', \
186 text, \
187 flags=re.DOTALL)
188
189 if gpg_matches != None:
190 gpg_chunks = gpg_matches.groups()
191 else:
192 gpg_chunks = ()
193
194 return gpg_chunks
195
196
197 def decrypt_chunks (gpg_chunks):
198
199 return map(decrypt_chunk, gpg_chunks)
200
201
202 def decrypt_chunk (gpg_chunk):
203
204 gpgme_ctx = gpgme.Context()
205
206 chunk_b = io.BytesIO(gpg_chunk.encode('ascii'))
207 plain_b = io.BytesIO()
208
209 sigs = gpgme_ctx.decrypt_verify(chunk_b, plain_b)
210
211 plaintext = plain_b.getvalue().decode('utf-8')
212 return (plaintext, sigs)
213
214
215 def 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
223 return reply_key
224
225
226 def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
227 sign_with_fingerprint):
228
229
230 reply = "To: " + email_from + "\n"
231 reply += "Subject: " + email_subject + "\n"
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
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'
268
269 reply += message_mime.as_string()
270
271 else:
272 reply += "\n"
273 reply += "Sorry, i couldn't find your key.\n"
274 reply += "I'll need that to encrypt a message to you."
275
276 return reply
277
278
279 def email_quote_text (text):
280
281 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
282
283 return quoted_message
284
285
286 def encrypt_sign_message (plaintext, encrypt_to_key, sign_with_fingerprint):
287
288 gpgme_ctx = gpgme.Context()
289 gpgme_ctx.armor = True
290
291 sign_with_key = gpgme_ctx.get_key(sign_with_fingerprint)
292 gpgme_ctx.signers = [sign_with_key]
293
294 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
295 encrypted_bytes = io.BytesIO()
296
297 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
298 plaintext_bytes, encrypted_bytes)
299
300 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
301 return encrypted_txt
302
303
304 def 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
316 main()
317