1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
# -*- coding: utf-8 -*-
# Copyright (C) 2019 Daniel Kahn Gillmor
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
This script reads a MIME message from stdin and produces a treelike
representation on it stdout.
Example:
0 dkg@alice:~$ printmimestructure < 'Maildir/cur/1269025522.M338697P12023.monkey,S=6459,W=6963:2,Sa'
└┬╴multipart/signed 6546 bytes
├─╴text/plain inline 895 bytes
└─╴application/pgp-signature inline [signature.asc] 836 bytes
0 dkg@alice:~$
If you want to number the parts, i suggest piping the output through
something like "cat -n"
'''
import os
import sys
import enum
import email
import logging
import subprocess
from argparse import ArgumentParser, Namespace
from typing import Optional, Union, List, Tuple, Any
from email.charset import Charset
from email.message import Message
try:
import pgpy #type: ignore
except ImportError:
pgpy = None
try:
import argcomplete #type: ignore
except ImportError:
argcomplete = None
EncType = enum.Enum('EncType', ['PGPMIME', 'SMIME'])
class MimePrinter(object):
def __init__(self, args:Namespace):
self.args = args
def print_part(self, z:Message, prefix:str, parent:Optional[Message], num:int) -> None:
ofname:Optional[str] = z.get_filename()
fname:str = '' if ofname is None else f' [{ofname}]'
ocharset:Union[Charset, str, None] = z.get_charset()
cset:str = '' if ocharset is None else f' ({ocharset})'
disp:Union[List[Tuple[str,str]], List[str], None] = z.get_params(None, header='Content-Disposition')
disposition:str = ''
if (disp is not None):
for d in disp:
if d[0] in [ 'attachment', 'inline' ]:
disposition = ' ' + d[0]
nbytes:int
if z.is_multipart():
# FIXME: it looks like we are counting chars here, not bytes:
nbytes = len(z.as_string())
else:
payload:Union[List[Message], str, bytes, None] = z.get_payload()
if not isinstance(payload, (str,bytes)):
raise TypeError(f'expected payload to be either str or bytes, got {type(payload)}')
# FIXME: it looks like we are counting chars here, not bytes:
nbytes = len(payload)
print(f'{prefix}{z.get_content_type()}{cset}{disposition}{fname} {nbytes:d} bytes')
cryptopayload:Optional[Message] = None
try_pgp_decrypt:bool = self.args.pgpkey or self.args.use_gpg_agent
try_cms_decrypt:bool = self.args.cmskey or self.args.use_gpg_agent
if try_pgp_decrypt and \
(parent is not None) and \
(parent.get_content_type().lower() == 'multipart/encrypted') and \
(str(parent.get_param('protocol')).lower() == 'application/pgp-encrypted') and \
(num == 2):
cryptopayload = self.decrypt_part(z, EncType.PGPMIME)
if try_cms_decrypt and \
cryptopayload is None and \
z.get_content_type().lower() == 'application/pkcs7-mime' and \
str(z.get_param('smime-type')).lower() in ['authenveloped-data',
'enveloped-data']:
cryptopayload = self.decrypt_part(z, EncType.SMIME)
if cryptopayload is not None:
newprefix = prefix[:-3] + ' '
print(f'{newprefix}↧ (decrypts to)')
self.print_tree(cryptopayload, newprefix + '└', z, 0)
else:
if z.get_content_type().lower() == 'application/pkcs7-mime' and \
str(z.get_param('smime-type')).lower() == 'signed-data':
bodypart:Union[List[Message],str,bytes,None] = z.get_payload(decode=True)
if isinstance(bodypart, bytes):
unwrapped = self.pipe_transform(bodypart, ['certtool', '--p7-show-data', '--p7-info', '--inder'])
if unwrapped:
newprefix = prefix[:-3] + ' '
print(f'{newprefix}⇩ (unwraps to)')
self.print_tree(unwrapped, newprefix + '└', z, 0)
else:
logging.warning(f'Unable to unwrap one-part PKCS#7 signed message (maybe try "apt install gnutls-bin")')
def decrypt_part(self, msg:Message, flavor:EncType) -> Optional[Message]:
ciphertext:Union[List[Message],str,bytes,None] = msg.get_payload(decode=True)
cryptopayload:Optional[Message] = None
if not isinstance(ciphertext, bytes):
logging.warning('encrypted part was not a leaf mime part somehow')
return None
if flavor == EncType.PGPMIME:
if self.args.pgpkey:
cryptopayload = self.pgpy_decrypt(self.args.pgpkey, ciphertext)
if cryptopayload is None and self.args.use_gpg_agent:
cryptopayload = self.pipe_transform(ciphertext, ['gpg', '--batch', '--decrypt'])
elif flavor == EncType.SMIME:
if self.args.cmskey:
for keyname in self.args.cmskey:
cmd = ['openssl', 'smime', '-decrypt', '-inform', 'DER', '-inkey', keyname]
cryptopayload = self.pipe_transform(ciphertext, cmd)
if cryptopayload:
return cryptopayload
if self.args.use_gpg_agent:
cryptopayload = self.pipe_transform(ciphertext, ['gpgsm', '--batch', '--decrypt'])
if cryptopayload is None:
logging.warning(f'Unable to decrypt')
return cryptopayload
def pgpy_decrypt(self, keys:List[str], ciphertext:bytes) -> Optional[Message]:
if pgpy is None:
logging.warning(f'Python module pgpy is not available, not decrypting (try "apt install python3-pgpy")')
return None
keyname:str
ret:Optional[Message] = None
for keyname in keys:
try:
key:pgpy.PGPKey
key, _ = pgpy.PGPKey.from_file(keyname)
msg:pgpy.PGPMessage = pgpy.PGPMessage.from_blob(ciphertext)
msg = key.decrypt(msg)
return email.message_from_bytes(msg.message)
except:
pass
return None
def pipe_transform(self, ciphertext:bytes, cmd:List[str]) -> Optional[Message]:
inp:int
outp:int
inp, outp = os.pipe()
with open(outp, 'wb') as outf:
outf.write(ciphertext)
out:subprocess.CompletedProcess[bytes] = subprocess.run(cmd,
stdin=inp,
capture_output=True)
if out.returncode == 0:
return email.message_from_bytes(out.stdout)
return None
def print_tree(self, z:Message, prefix:str, parent:Optional[Message], num:int) -> None:
if (z.is_multipart()):
self.print_part(z, prefix+'┬╴', parent, num)
if prefix.endswith('└'):
prefix = prefix.rpartition('└')[0] + ' '
if prefix.endswith('├'):
prefix = prefix.rpartition('├')[0] + '│'
parts:Union[List[Message], str, bytes, None] = z.get_payload()
if not isinstance(parts, list):
raise TypeError(f'parts was {type(parts)}, expected List[Message]')
i = 0
while (i < len(parts)-1):
self.print_tree(parts[i], prefix + '├', z, i+1)
i += 1
self.print_tree(parts[i], prefix + '└', z, i+1)
# FIXME: show epilogue?
else:
self.print_part(z, prefix+'─╴', parent, num)
def main() -> None:
parser:ArgumentParser = ArgumentParser(description='Read RFC2822 MIME message from stdin and emit a tree diagram to stdout.',
epilog="Example: email-print-mime-structure <message.eml")
parser.add_argument('--pgpkey', metavar='KEYFILE', action='append',
help='OpenPGP Transferable Secret Key for decrypting PGP/MIME')
parser.add_argument('--cmskey', metavar='KEYFILE', action='append',
help='X.509 Private Key for decrypting S/MIME')
parser.add_argument('--use-gpg-agent', action='store_true',
help='Ask local GnuPG installation for decryption')
parser.add_argument('--no-use-gpg-agent', action='store_false',
help='Don\'t ask local GnuPG installation for decryption')
parser.set_defaults(use_gpg_agent=False)
if argcomplete:
argcomplete.autocomplete(parser)
elif '_ARGCOMPLETE' in os.environ:
logging.error('Argument completion requested but the "argcomplete" '
'module is not installed. '
'Maybe you want to "apt install python3-argcomplete"')
sys.exit(1)
args:Namespace = parser.parse_args()
msg:Union[Message, str, int, Any] = email.message_from_file(sys.stdin)
if isinstance(msg, Message):
printer:MimePrinter = MimePrinter(args)
printer.print_tree(msg, '└', None, 0)
else:
logging.error('Input was not an e-mail message')
if __name__ == '__main__':
main()
|