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