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