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