239 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			239 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python
 | |
| 
 | |
| 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()
 |