refactored a bit
[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 def main ():
39
40 handle_args()
41
42 txt = sys.stdin.read()
43 msg = email.parser.Parser().parsestr(txt)
44
45 message = ""
46 message += "From: " + msg['From'] + "\n"
47 message += "Subject: " + msg['Subject'] + "\n\n"
48
49 message += msg_walk(msg)
50
51 print(message)
52
53
54 def msg_walk (msg):
55
56 body = ""
57 for part in msg.walk():
58
59 payload, descript, filename, conttype = get_part_info(part)
60
61 if payload == None:
62 continue
63
64 if conttype == 'multipart':
65 continue
66
67 if conttype == "application/pgp-encrypted":
68 if descript == 'PGP/MIME version identification':
69 if payload.strip() != "Version: 1":
70 print(progname + ": Warning: unknown " + descript + ": " \
71 + payload.strip(), file=sys.stderr)
72 continue
73
74
75 if (filename == "encrypted.asc") or (conttype == "pgp/mime"):
76 payload_dec = decrypt_payload(payload)
77 body += payload_dec
78
79 elif conttype == "text/plain":
80 body += payload + "\n"
81
82 else:
83 body += payload + "\n"
84
85 return body
86
87 def get_part_info (part):
88
89 charset = part.get_content_charset()
90 payload_b = part.get_payload(decode=True)
91
92 filename = part.get_filename()
93 conttype = part.get_content_type()
94 descrip_p = part.get_params(header='content-description')
95
96 if charset == None:
97 charset = 'utf-8'
98
99 if payload_b == None:
100 payload = None
101 else:
102 payload = payload_b.decode(charset)
103
104 if descrip_p == None:
105 descript = None
106 else:
107 descript = descrip_p[0][0]
108
109
110 return payload, descript, filename, conttype
111
112
113 def decrypt_payload (payload):
114
115 blocks = split_message(payload)
116 decrypted_tree = decrypt_blocks(blocks)
117
118 if decrypted_tree == None:
119 return
120
121 body = ""
122 for node in decrypted_tree:
123 msg = email.parser.Parser().parsestr(node[0])
124 sigs = node[1]
125
126 body += msg_walk(msg)
127 for sig in sigs:
128 body += format_sig(sig)
129
130 return body
131
132
133 def format_sig (sig):
134
135 fprint = sig.fpr
136 fprint_short = re.search("[0-9A-Fa-f]{32}([0-9A-Fa-f]{8})", fprint).groups()[0]
137
138 timestamp = time.localtime(sig.timestamp)
139 date = time.strftime("%a %d %b %Y %I:%M:%S %p %Z", timestamp)
140
141 g = gpgme.Context()
142 key = g.get_key(fprint)
143
144 # right now i'm just choosing the first user id, even if that id isn't
145 # signed by the user yet another is. if a user id is printed, it should
146 # at least be one that is signed, and/or correspond to the From:
147 # field's email address and full name.
148
149 name = key.uids[0].name
150 e_addr = key.uids[0].email
151 comment = key.uids[0].comment
152
153 # this section needs some work. signature summary, validity, status,
154 # and wrong_key_usage all complicate the picture. their enum/#define
155 # values overlap, which makes things more complicated.
156
157 validity = sig.validity
158 if validity == gpgme.VALIDITY_ULTIMATE \
159 or validity == gpgme.VALIDITY_FULL:
160 status = "MAYBE-Good Signature "
161 elif validity == gpgme.VALIDITY_MARGINAL:
162 status = "MAYBE-Marginal Signature "
163 else:
164 status = "MAYBE-BAD Signature "
165
166
167 sig_str = "Signature Made " + date + " using key ID " + fprint_short + "\n"
168 sig_str += status + "from " + name + " (" + comment + ") <" + e_addr + ">"
169
170 return sig_str
171
172
173 def split_message (text):
174
175 pgp_matches = re.search( \
176 '(-----BEGIN PGP MESSAGE-----' \
177 '.*' \
178 '-----END PGP MESSAGE-----)', \
179 text, \
180 re.DOTALL)
181
182 if pgp_matches == None:
183 return None
184 else:
185 return pgp_matches.groups()
186
187
188 def decrypt_blocks (blocks):
189
190 if blocks == None:
191 return None
192
193 message = []
194 for block in blocks:
195 plain, sigs = decrypt_block(block)
196
197 message = message + [(plain, sigs)]
198
199 return message
200
201
202 def decrypt_block (block):
203
204 block_b = io.BytesIO(block.encode('ASCII'))
205 plain_b = io.BytesIO()
206
207 g = gpgme.Context()
208 sigs = g.decrypt_verify(block_b, plain_b)
209
210 plain = plain_b.getvalue().decode('ASCII')
211 return (plain, sigs)
212
213
214 def handle_args ():
215 if __name__ == "__main__":
216
217 global progname
218 progname = sys.argv[0]
219
220 if len(sys.argv) > 1:
221 print(progname + ": error, this program doesn't " \
222 "need any arguments.", file=sys.stderr)
223 exit(1)
224
225
226 main()
227