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
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()
|
|
|