#!/usr/local/bin/python3.6 import re import sys import email import shlex import mimetypes import subprocess from copy import copy from hashlib import md5 from email import charset from email import encoders from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.nonmultipart import MIMENonMultipart from os.path import basename, splitext, expanduser charset.add_charset('utf-8', charset.SHORTEST, '8bit') def pandoc(from_format, to_format='markdown', plain='markdown', title=None): markdown = ('markdown' '-blank_before_blockquote') if from_format == 'plain': from_format = plain if from_format == 'markdown': from_format = markdown if to_format == 'markdown': to_format = markdown command = 'pandoc -f {} -t {} --standalone --highlight-style=tango' if to_format in ('html', 'html5'): if title is not None: command += ' --variable=pagetitle:{}'.format(shlex.quote(title)) command += ' --webtex --template={}'.format( expanduser('~/.pandoc/templates/email.html')) return command.format(from_format, to_format) def gmailfy(payload): return payload.replace('
', '
') def make_alternative(message, part): alternative = convert(part, 'html', pandoc(part.get_content_subtype(), to_format='html', title=message.get('Subject'))) alternative.set_payload(gmailfy(alternative.get_payload())) return alternative def make_replacement(message, part): return convert(part, 'plain', pandoc(part.get_content_subtype())) def convert(part, to_subtype, command): payload = part.get_payload() if isinstance(payload, str): payload = payload.encode('utf-8') else: payload = part.get_payload(None, True) if not isinstance(payload, bytes): payload = payload.encode('utf-8') process = subprocess.run( shlex.split(command), input=payload, stdout=subprocess.PIPE, check=True) return MIMEText(process.stdout, to_subtype, 'utf-8') def with_alternative(parent, part, from_signed, make_alternative=make_alternative, make_replacement=None): try: alternative = make_alternative(parent or part, from_signed or part) replacement = (make_replacement(parent or part, part) if from_signed is None and make_replacement is not None else part) except: return parent or part envelope = MIMEMultipart('alternative') if parent is None: for k, v in part.items(): if (k.lower() != 'mime-version' and not k.lower().startswith('content-')): envelope.add_header(k, v) del part[k] envelope.attach(replacement) envelope.attach(alternative) if parent is None: return envelope payload = parent.get_payload() payload[payload.index(part)] = envelope return parent def tag_attachments(message): if message.get_content_type() == 'multipart/mixed': for part in message.get_payload(): if (part.get_content_maintype() in ['image'] and 'Content-ID' not in part): filename = part.get_param('filename', header='Content-Disposition') if isinstance(filename, tuple): filename = str(filename[2], filename[0] or 'us-ascii') if filename: filename = splitext(basename(filename))[0] if filename: part.add_header('Content-ID', '<{}>'.format(filename)) return message def attachment_from_file_path(attachment_path): try: mime, encoding = mimetypes.guess_type(attachment_path, strict=False) maintype, subtype = mime.split('/') with open(attachment_path, 'rb') as payload: attachment = MIMENonMultipart(maintype, subtype) attachment.set_payload(payload.read()) encoders.encode_base64(attachment) if encoding: attachment.add_header('Content-Encoding', encoding) return attachment except: return None attachment_path_pattern = re.compile(r'\]\s*\(\s*file://(/[^)]*\S)\s*\)|' r'\]\s*:\s*file://(/.*\S)\s*$', re.MULTILINE) def link_attachments(payload): attached = [] attachments = [] def on_match(match): if match.group(1): attachment_path = match.group(1) cid_fmt = '](cid:{})' else: attachment_path = match.group(2) cid_fmt = ']: cid:{}' attachment_id = md5(attachment_path.encode()).hexdigest() if attachment_id in attached: return cid_fmt.format(attachment_id) attachment = attachment_from_file_path(attachment_path) if attachment: attachment.add_header('Content-ID', '<{}>'.format(attachment_id)) attachments.append(attachment) attached.append(attachment_id) return cid_fmt.format(attachment_id) return match.group() return attachments, attachment_path_pattern.sub(on_match, payload) def with_local_attachments(parent, part, from_signed, link_attachments=link_attachments): if from_signed is None: attachments, payload = link_attachments(part.get_payload()) part.set_payload(payload) else: attachments, payload = link_attachments(from_signed.get_payload()) from_signed = copy(from_signed) from_signed.set_payload(payload) if not attachments: return parent, part, from_signed if parent is None: parent = MIMEMultipart('mixed') for k, v in part.items(): if (k.lower() != 'mime-version' and not k.lower().startswith('content-')): parent.add_header(k, v) del part[k] parent.attach(part) for attachment in attachments: parent.attach(attachment) return parent, part, from_signed def is_target(part, target_subtypes): return (part.get('Content-Disposition', 'inline') == 'inline' and part.get_content_maintype() == 'text' and part.get_content_subtype() in target_subtypes) def pick_from_signed(part, target_subtypes): for from_signed in part.get_payload(): if is_target(from_signed, target_subtypes): return from_signed def seek_target(message, target_subtypes=['plain', 'markdown']): if message.is_multipart(): if message.get_content_type() == 'multipart/signed': part = pick_from_signed(message, target_subtypes) if part is not None: return None, message, part elif message.get_content_type() == 'multipart/mixed': for part in message.get_payload(): if part.is_multipart(): if part.get_content_type() == 'multipart/signed': from_signed = pick_from_signed(part, target_subtypes) if from_signed is not None: return message, part, from_signed elif is_target(part, target_subtypes): return message, part, None else: if is_target(message, target_subtypes): return None, message, None return None, None, None def main(): try: message = email.message_from_file(sys.stdin) parent, part, from_signed = seek_target(message) if (parent, part, from_signed) == (None, None, None): print(message) return tag_attachments(message) print(with_alternative( *with_local_attachments(parent, part, from_signed))) except (BrokenPipeError, KeyboardInterrupt): pass if __name__ == '__main__': main()