Commit | Line | Data |
---|---|---|
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 | ||
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 | ||
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 | ||
54 | def 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 |
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 | ||
0bec96d6 | 112 | |
86663388 | 113 | def 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 | |
133 | def 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 | ||
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 | ||
20f6e7c5 AE |
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 | ||
0bec96d6 AE |
226 | main() |
227 |