hostesty about sig validity certainty
[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
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 104def 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
124def 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
164def 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
179def 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
193def 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
205def 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
217main()
218