| 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 | if part.get_content_type() == 'multipart': |
| 60 | continue |
| 61 | |
| 62 | charset = part.get_content_charset() |
| 63 | payload_b = part.get_payload(decode=True) |
| 64 | |
| 65 | filename = part.get_filename() |
| 66 | conttype = part.get_content_type() |
| 67 | descrip_p = part.get_params(header='content-description') |
| 68 | |
| 69 | if charset == None: |
| 70 | charset = 'utf-8' |
| 71 | |
| 72 | if payload_b == None: |
| 73 | continue |
| 74 | else: |
| 75 | payload = payload_b.decode(charset) |
| 76 | |
| 77 | |
| 78 | if descrip_p == None: |
| 79 | decript = "" |
| 80 | else: |
| 81 | descript = descrip_p[0][0] |
| 82 | |
| 83 | |
| 84 | if conttype == "application/pgp-encrypted": |
| 85 | if descript == 'PGP/MIME version identification': |
| 86 | if payload.strip() != "Version: 1": |
| 87 | print(progname + ": Warning: unknown " + descript + ": " \ |
| 88 | + payload.strip(), file=sys.stderr) |
| 89 | continue |
| 90 | |
| 91 | elif (filename == "encrypted.asc") or (conttype == "pgp/mime"): |
| 92 | payload_dec = decrypt_payload(payload) |
| 93 | body += payload_dec |
| 94 | |
| 95 | elif conttype == "text/plain": |
| 96 | body += payload + "\n" |
| 97 | |
| 98 | else: |
| 99 | body += payload + "\n" |
| 100 | |
| 101 | return body |
| 102 | |
| 103 | |
| 104 | def decrypt_payload (payload): |
| 105 | |
| 106 | blocks = split_message(payload) |
| 107 | decrypted_tree = decrypt_blocks(blocks) |
| 108 | |
| 109 | if decrypted_tree == None: |
| 110 | return |
| 111 | |
| 112 | body = "" |
| 113 | for node in decrypted_tree: |
| 114 | msg = email.parser.Parser().parsestr(node[0]) |
| 115 | sigs = node[1] |
| 116 | |
| 117 | body += msg_walk(msg) |
| 118 | for sig in sigs: |
| 119 | body += format_sig(sig) |
| 120 | |
| 121 | return body |
| 122 | |
| 123 | |
| 124 | def format_sig (sig): |
| 125 | |
| 126 | fprint = sig.fpr |
| 127 | fprint_short = re.search("[0-9A-Fa-f]{32}([0-9A-Fa-f]{8})", fprint).groups()[0] |
| 128 | |
| 129 | timestamp = time.localtime(sig.timestamp) |
| 130 | date = time.strftime("%a %d %b %Y %I:%M:%S %p %Z", timestamp) |
| 131 | |
| 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: |
| 151 | status = "MAYBE-Good Signature " |
| 152 | elif validity == gpgme.VALIDITY_MARGINAL: |
| 153 | status = "MAYBE-Marginal Signature " |
| 154 | else: |
| 155 | status = "MAYBE-BAD Signature " |
| 156 | |
| 157 | |
| 158 | sig_str = "Signature Made " + date + " using key ID " + fprint_short + "\n" |
| 159 | sig_str += status + "from " + name + " (" + comment + ") <" + e_addr + ">" |
| 160 | |
| 161 | return sig_str |
| 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 | |
| 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 | |
| 217 | main() |
| 218 | |