stylistic changes
[edward.git] / edward-bot
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+) *
20* *
21* Special thanks to Josh Drake for writing the original edward bot! :) *
22* *
23************************************************************************
24
25Code used from:
26
27* http://agpl.fsf.org/emailselfdefense.fsf.org/edward/CURRENT/edward.tar.gz
28
29"""
30
31import sys
32import email.parser
33import gpgme
34import re
35import io
0bec96d6 36
c96f3837 37
0bec96d6
AE
38def main ():
39
20f6e7c5 40 handle_args()
0bec96d6 41
c96f3837 42 email_text = sys.stdin.read()
c96f3837 43
65ed3800
AE
44 email_from, email_subject = email_from_subject(email_text)
45
955eaaa6 46 plaintext, keys = email_decode_flatten(email_text)
1da9b527 47 encrypt_to_key = choose_reply_encryption_key(keys)
fafa21c3 48
1da9b527 49 reply_message = generate_reply(plaintext, email_from, \
897cbaf6
AE
50 email_subject, encrypt_to_key,
51 "DAB4F989E2788B8DF058E0EFEF1EC52039B36E58")
c96f3837 52
1da9b527 53 print(reply_message)
c96f3837 54
0bec96d6 55
7b06b980 56def email_decode_flatten (email_text):
0bec96d6 57
86663388 58 body = ""
394a1476
AE
59 keys = []
60
61 email_struct = email.parser.Parser().parsestr(email_text)
62
63 for subpart in email_struct.walk():
8bb4b0d5 64
394a1476
AE
65 payload, description, filename, content_type \
66 = get_email_subpart_info(subpart)
0bec96d6 67
394a1476 68 if payload == "":
8bb4b0d5 69 continue
0bec96d6 70
394a1476 71 if content_type == "multipart":
d437f8b2 72 continue
0bec96d6 73
394a1476
AE
74 if content_type == "application/pgp-encrypted":
75 if description == "PGP/MIME version identification":
8bb4b0d5 76 if payload.strip() != "Version: 1":
394a1476
AE
77 print(progname + ": Warning: unknown " \
78 + description + ": " \
20f6e7c5 79 + payload.strip(), file=sys.stderr)
8bb4b0d5
AE
80 continue
81
d437f8b2 82
394a1476
AE
83 if (filename == "encrypted.asc") or (content_type == "pgp/mime"):
84 plaintext, more_keys = decrypt_text(payload)
0bec96d6 85
394a1476
AE
86 body += plaintext
87 keys += more_keys
88
89 elif content_type == "text/plain":
86663388 90 body += payload + "\n"
0bec96d6 91
8bb4b0d5 92 else:
86663388
AE
93 body += payload + "\n"
94
394a1476 95 return body, keys
86663388 96
c96f3837 97
65ed3800 98def email_from_subject (email_text):
d437f8b2 99
394a1476 100 email_struct = email.parser.Parser().parsestr(email_text)
d437f8b2 101
394a1476
AE
102 email_from = email_struct['From']
103 email_subject = email_struct['Subject']
d437f8b2 104
394a1476 105 return email_from, email_subject
d437f8b2 106
d437f8b2 107
394a1476 108def get_email_subpart_info (part):
d437f8b2 109
394a1476
AE
110 charset = part.get_content_charset()
111 payload_bytes = part.get_payload(decode=True)
d437f8b2 112
394a1476
AE
113 filename = part.get_filename()
114 content_type = part.get_content_type()
115 description_list = part.get_params(header='content-description')
d437f8b2 116
394a1476
AE
117 if charset == None:
118 charset = 'utf-8'
0bec96d6 119
394a1476
AE
120 if payload_bytes != None:
121 payload = payload_bytes.decode(charset)
122 else:
123 payload = ""
0bec96d6 124
394a1476
AE
125 if description_list != None:
126 description = description_list[0][0]
127 else:
128 description = ""
0bec96d6 129
394a1476 130 return payload, description, filename, content_type
0bec96d6 131
0bec96d6 132
394a1476 133def decrypt_text (gpg_text):
86663388 134
394a1476
AE
135 body = ""
136 keys = []
0bec96d6 137
394a1476 138 gpg_chunks = split_message(gpg_text)
86663388 139
394a1476 140 plaintext_and_sigs_chunks = decrypt_chunks(gpg_chunks)
0bec96d6 141
394a1476
AE
142 for chunk in plaintext_and_sigs_chunks:
143 plaintext = chunk[0]
144 sigs = chunk[1]
9eb78301 145
394a1476
AE
146 for sig in sigs:
147 key = get_pub_key(sig)
148 keys += [key]
0bec96d6 149
394a1476 150 # recursive for nested layers of mime and/or gpg
7b06b980 151 plaintext, more_keys = email_decode_flatten(plaintext)
8bb4b0d5 152
394a1476
AE
153 body += plaintext
154 keys += more_keys
8bb4b0d5 155
394a1476 156 return body, keys
8bb4b0d5 157
8bb4b0d5 158
394a1476 159def get_pub_key (sig):
16da8026 160
394a1476 161 gpgme_ctx = gpgme.Context()
9eb78301 162
7b06b980
AE
163 fingerprint = sig.fpr
164 key = gpgme_ctx.get_key(fingerprint)
0bec96d6 165
394a1476 166 return key
0bec96d6
AE
167
168
169def split_message (text):
170
7b06b980 171 gpg_matches = re.search( \
394a1476
AE
172 '(-----BEGIN PGP MESSAGE-----' + \
173 '.*' + \
0bec96d6
AE
174 '-----END PGP MESSAGE-----)', \
175 text, \
f31ffe2f 176 flags=re.DOTALL)
0bec96d6 177
7b06b980
AE
178 if gpg_matches != None:
179 gpg_chunks = gpg_matches.groups()
0bec96d6 180 else:
7b06b980
AE
181 gpg_chunks = ()
182
183 return gpg_chunks
0bec96d6
AE
184
185
394a1476 186def decrypt_chunks (gpg_chunks):
0bec96d6 187
6f6f3fb0 188 return map(decrypt_chunk, gpg_chunks)
0bec96d6 189
0bec96d6 190
394a1476 191def decrypt_chunk (gpg_chunk):
0bec96d6 192
394a1476 193 gpgme_ctx = gpgme.Context()
0bec96d6 194
6aa41372 195 chunk_b = io.BytesIO(gpg_chunk.encode('ascii'))
0bec96d6
AE
196 plain_b = io.BytesIO()
197
394a1476 198 sigs = gpgme_ctx.decrypt_verify(chunk_b, plain_b)
0bec96d6 199
6aa41372 200 plaintext = plain_b.getvalue().decode('utf-8')
394a1476 201 return (plaintext, sigs)
0bec96d6
AE
202
203
fafa21c3
AE
204def choose_reply_encryption_key (keys):
205
206 reply_key = None
207 for key in keys:
208 if (key.can_encrypt == True):
209 reply_key = key
210 break
211
216708e9 212 return reply_key
fafa21c3
AE
213
214
897cbaf6
AE
215def generate_reply (plaintext, email_from, email_subject, encrypt_to_key,
216 sign_with_fingerprint):
1da9b527 217
1da9b527
AE
218 reply = "To: " + email_from + "\n"
219 reply += "Subject: " + email_subject + "\n"
f87041f8 220 reply += "\n"
216708e9
AE
221
222 if (encrypt_to_key != None):
223 plaintext_reply = "thanks for the message!\n\n\n"
224 plaintext_reply += email_quote_text(plaintext)
225
226 encrypted_reply = encrypt_sign_message(plaintext_reply, encrypt_to_key,
227 sign_with_fingerprint)
228
229 reply += encrypted_reply
230
231 else:
232 reply += "Sorry, i couldn't find your key.\n"
233 reply += "I'll need that to encrypt a message to you."
1da9b527
AE
234
235 return reply
236
237
f87041f8
AE
238def email_quote_text (text):
239
240 quoted_message = re.sub(r'^', r'> ', text, flags=re.MULTILINE)
241
242 return quoted_message
243
244
897cbaf6 245def encrypt_sign_message (plaintext, encrypt_to_key, sign_with_fingerprint):
1da9b527
AE
246
247 gpgme_ctx = gpgme.Context()
248 gpgme_ctx.armor = True
249
897cbaf6
AE
250 sign_with_key = gpgme_ctx.get_key(sign_with_fingerprint)
251 gpgme_ctx.signers = [sign_with_key]
252
6aa41372 253 plaintext_bytes = io.BytesIO(plaintext.encode('ascii'))
1da9b527
AE
254 encrypted_bytes = io.BytesIO()
255
897cbaf6 256 gpgme_ctx.encrypt_sign([encrypt_to_key], gpgme.ENCRYPT_ALWAYS_TRUST,
1da9b527
AE
257 plaintext_bytes, encrypted_bytes)
258
6aa41372 259 encrypted_txt = encrypted_bytes.getvalue().decode('ascii')
1da9b527
AE
260 return encrypted_txt
261
262
20f6e7c5
AE
263def handle_args ():
264 if __name__ == "__main__":
265
266 global progname
267 progname = sys.argv[0]
268
269 if len(sys.argv) > 1:
270 print(progname + ": error, this program doesn't " \
271 "need any arguments.", file=sys.stderr)
272 exit(1)
273
274
0bec96d6
AE
275main()
276