use a settings file to manage configurations
[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
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
62 plaintext, keys = email_decode_flatten(email_text, gpgme_ctx)
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
40c37ab3 73def email_decode_flatten (email_text, gpgme_ctx):
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
106 elif content_type == "text/plain":
86663388 107 body += payload + "\n"
0bec96d6 108
8bb4b0d5 109 else:
86663388
AE
110 body += payload + "\n"
111
394a1476 112 return body, keys
86663388 113
c96f3837 114
65ed3800 115def email_from_subject (email_text):
d437f8b2 116
394a1476 117 email_struct = email.parser.Parser().parsestr(email_text)
d437f8b2 118
394a1476
AE
119 email_from = email_struct['From']
120 email_subject = email_struct['Subject']
d437f8b2 121
394a1476 122 return email_from, email_subject
d437f8b2 123
d437f8b2 124
394a1476 125def get_email_subpart_info (part):
d437f8b2 126
394a1476
AE
127 charset = part.get_content_charset()
128 payload_bytes = part.get_payload(decode=True)
d437f8b2 129
394a1476
AE
130 filename = part.get_filename()
131 content_type = part.get_content_type()
132 description_list = part.get_params(header='content-description')
d437f8b2 133
394a1476
AE
134 if charset == None:
135 charset = 'utf-8'
0bec96d6 136
394a1476
AE
137 if payload_bytes != None:
138 payload = payload_bytes.decode(charset)
139 else:
140 payload = ""
0bec96d6 141
394a1476
AE
142 if description_list != None:
143 description = description_list[0][0]
144 else:
145 description = ""
0bec96d6 146
394a1476 147 return payload, description, filename, content_type
0bec96d6 148
0bec96d6 149
40c37ab3 150def decrypt_text (gpg_text, gpgme_ctx):
86663388 151
394a1476
AE
152 body = ""
153 keys = []
0bec96d6 154
394a1476 155 gpg_chunks = split_message(gpg_text)
86663388 156
40c37ab3 157 plaintext_and_sigs_chunks = decrypt_chunks(gpg_chunks, gpgme_ctx)
0bec96d6 158
394a1476
AE
159 for chunk in plaintext_and_sigs_chunks:
160 plaintext = chunk[0]
161 sigs = chunk[1]
9eb78301 162
394a1476 163 for sig in sigs:
40c37ab3 164 key = get_pub_key(sig, gpgme_ctx)
394a1476 165 keys += [key]
0bec96d6 166
394a1476 167 # recursive for nested layers of mime and/or gpg
40c37ab3 168 plaintext, more_keys = email_decode_flatten(plaintext, gpgme_ctx)
8bb4b0d5 169
394a1476
AE
170 body += plaintext
171 keys += more_keys
8bb4b0d5 172
394a1476 173 return body, keys
8bb4b0d5 174
8bb4b0d5 175
40c37ab3 176def get_pub_key (sig, gpgme_ctx):
9eb78301 177
7b06b980
AE
178 fingerprint = sig.fpr
179 key = gpgme_ctx.get_key(fingerprint)
0bec96d6 180
394a1476 181 return key
0bec96d6
AE
182
183
184def split_message (text):
185
7b06b980 186 gpg_matches = re.search( \
394a1476
AE
187 '(-----BEGIN PGP MESSAGE-----' + \
188 '.*' + \
0bec96d6
AE
189 '-----END PGP MESSAGE-----)', \
190 text, \
f31ffe2f 191 flags=re.DOTALL)
0bec96d6 192
7b06b980
AE
193 if gpg_matches != None:
194 gpg_chunks = gpg_matches.groups()
0bec96d6 195 else:
7b06b980
AE
196 gpg_chunks = ()
197
198 return gpg_chunks
0bec96d6
AE
199
200
40c37ab3 201def decrypt_chunks (gpg_chunks, gpgme_ctx):
0bec96d6 202
40c37ab3 203 return [decrypt_chunk(chunk, gpgme_ctx) for chunk in gpg_chunks]
0bec96d6 204
0bec96d6 205
40c37ab3 206def decrypt_chunk (gpg_chunk, gpgme_ctx):
0bec96d6 207
6aa41372 208 chunk_b = io.BytesIO(gpg_chunk.encode('ascii'))
0bec96d6
AE
209 plain_b = io.BytesIO()
210
394a1476 211 sigs = gpgme_ctx.decrypt_verify(chunk_b, plain_b)
0bec96d6 212
6aa41372 213 plaintext = plain_b.getvalue().decode('utf-8')
394a1476 214 return (plaintext, sigs)
0bec96d6
AE
215
216
fafa21c3
AE
217def choose_reply_encryption_key (keys):
218
219 reply_key = None
220 for key in keys:
221 if (key.can_encrypt == True):
222 reply_key = key
223 break
224
216708e9 225 return reply_key
fafa21c3
AE
226
227
897cbaf6 228def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
40c37ab3 229 sign_with_fingerprint, gpgme_ctx):
1da9b527 230
8bdfb6d4 231
1da9b527
AE
232 reply = "To: " + email_from + "\n"
233 reply += "Subject: " + email_subject + "\n"
216708e9
AE
234
235 if (encrypt_to_key != None):
236 plaintext_reply = "thanks for the message!\n\n\n"
237 plaintext_reply += email_quote_text(plaintext)
238
8bdfb6d4
AE
239 # quoted printable encoding lets most ascii characters look normal
240 # before the decrypted mime message is decoded.
241 char_set = email.charset.Charset("utf-8")
242 char_set.body_encoding = email.charset.QP
243
244 # MIMEText doesn't allow setting the text encoding
245 # so we use MIMENonMultipart.
246 plaintext_mime = MIMENonMultipart('text', 'plain')
247 plaintext_mime.set_payload(plaintext_reply, charset=char_set)
248
249 encrypted_text = encrypt_sign_message(plaintext_mime.as_string(),
250 encrypt_to_key,
40c37ab3
AE
251 sign_with_fingerprint,
252 gpgme_ctx)
8bdfb6d4
AE
253
254 control_mime = MIMEApplication("Version: 1",
255 _subtype='pgp-encrypted',
256 _encoder=email.encoders.encode_7or8bit)
257 control_mime['Content-Description'] = 'PGP/MIME version identification'
258 control_mime.set_charset('us-ascii')
259
260 encoded_mime = MIMEApplication(encrypted_text,
261 _subtype='octet-stream; name="encrypted.asc"',
262 _encoder=email.encoders.encode_7or8bit)
263 encoded_mime['Content-Description'] = 'OpenPGP encrypted message'
264 encoded_mime['Content-Disposition'] = 'inline; filename="encrypted.asc"'
265 encoded_mime.set_charset('us-ascii')
266
267 message_mime = MIMEMultipart(_subtype="encrypted", protocol="application/pgp-encrypted")
268 message_mime.attach(control_mime)
269 message_mime.attach(encoded_mime)
270 message_mime['Content-Disposition'] = 'inline'
216708e9 271
8bdfb6d4 272 reply += message_mime.as_string()
216708e9
AE
273
274 else:
8bdfb6d4 275 reply += "\n"
216708e9
AE
276 reply += "Sorry, i couldn't find your key.\n"
277 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
278
279 return reply
280
281
f87041f8
AE
282def email_quote_text (text):
283
284 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
285
286 return quoted_message
287
288
40c37ab3 289def encrypt_sign_message (plaintext, encrypt_to_key, sign_with_fingerprint, gpgme_ctx):
1da9b527 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