You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

239 lines
8.3 KiB

#!/usr/bin/env python3
# Original Source: https://github.com/oblitum/dotfiles/blob/ArchLinux/.local/bin/MIMEmbellish
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('<blockquote>',
'<blockquote class="gmail_quote" style="'
'padding: 0 7px 0 7px;'
'border-left: 2px solid #cccccc;'
'font-style: italic;'
'margin: 0 0 7px 3px;'
'">')
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()