refactored a bit
[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
36import time
37
38def main ():
39
20f6e7c5 40 handle_args()
0bec96d6 41
20f6e7c5 42 txt = sys.stdin.read()
0bec96d6
AE
43 msg = email.parser.Parser().parsestr(txt)
44
86663388
AE
45 message = ""
46 message += "From: " + msg['From'] + "\n"
47 message += "Subject: " + msg['Subject'] + "\n\n"
0bec96d6 48
86663388
AE
49 message += msg_walk(msg)
50
51 print(message)
0bec96d6
AE
52
53
54def msg_walk (msg):
55
86663388 56 body = ""
0bec96d6 57 for part in msg.walk():
8bb4b0d5 58
d437f8b2 59 payload, descript, filename, conttype = get_part_info(part)
0bec96d6 60
d437f8b2 61 if payload == None:
8bb4b0d5 62 continue
0bec96d6 63
d437f8b2
AE
64 if conttype == 'multipart':
65 continue
0bec96d6 66
8bb4b0d5
AE
67 if conttype == "application/pgp-encrypted":
68 if descript == 'PGP/MIME version identification':
69 if payload.strip() != "Version: 1":
20f6e7c5
AE
70 print(progname + ": Warning: unknown " + descript + ": " \
71 + payload.strip(), file=sys.stderr)
8bb4b0d5
AE
72 continue
73
d437f8b2
AE
74
75 if (filename == "encrypted.asc") or (conttype == "pgp/mime"):
86663388
AE
76 payload_dec = decrypt_payload(payload)
77 body += payload_dec
0bec96d6 78
8bb4b0d5 79 elif conttype == "text/plain":
86663388 80 body += payload + "\n"
0bec96d6 81
8bb4b0d5 82 else:
86663388
AE
83 body += payload + "\n"
84
85 return body
86
d437f8b2
AE
87def 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
0bec96d6 112
86663388 113def decrypt_payload (payload):
0bec96d6 114
86663388
AE
115 blocks = split_message(payload)
116 decrypted_tree = decrypt_blocks(blocks)
0bec96d6 117
86663388 118 if decrypted_tree == None:
0bec96d6
AE
119 return
120
86663388
AE
121 body = ""
122 for node in decrypted_tree:
123 msg = email.parser.Parser().parsestr(node[0])
124 sigs = node[1]
0bec96d6 125
86663388 126 body += msg_walk(msg)
0bec96d6 127 for sig in sigs:
86663388
AE
128 body += format_sig(sig)
129
130 return body
0bec96d6 131
86663388
AE
132
133def format_sig (sig):
0bec96d6 134
8bb4b0d5
AE
135 fprint = sig.fpr
136 fprint_short = re.search("[0-9A-Fa-f]{32}([0-9A-Fa-f]{8})", fprint).groups()[0]
9eb78301 137
8bb4b0d5
AE
138 timestamp = time.localtime(sig.timestamp)
139 date = time.strftime("%a %d %b %Y %I:%M:%S %p %Z", timestamp)
0bec96d6 140
8bb4b0d5
AE
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:
1c1fbe2c 160 status = "MAYBE-Good Signature "
8bb4b0d5 161 elif validity == gpgme.VALIDITY_MARGINAL:
1c1fbe2c 162 status = "MAYBE-Marginal Signature "
8bb4b0d5 163 else:
1c1fbe2c 164 status = "MAYBE-BAD Signature "
16da8026 165
9eb78301 166
86663388
AE
167 sig_str = "Signature Made " + date + " using key ID " + fprint_short + "\n"
168 sig_str += status + "from " + name + " (" + comment + ") <" + e_addr + ">"
0bec96d6 169
86663388 170 return sig_str
0bec96d6
AE
171
172
173def 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
188def 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
202def 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
20f6e7c5
AE
214def 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
0bec96d6
AE
226main()
227