tachibana/demo/fw/tools/create_age_email.py
2021-07-21 17:28:47 +02:00

141 lines
5.1 KiB
Python

#!/usr/bin/env python3
import os, sys
from pathlib import Path
import math
import subprocess
from email.message import EmailMessage
from email.utils import make_msgid
import smtplib
import textwrap
import io
import binascii
import base64
import click
import bdfparser
from PIL import Image
import numpy as np
import tb_data_encoder
def age_encrypt(content, keys):
age_binary = os.environ.get('AGE', 'age')
proc = subprocess.run([age_binary, '-e', *(elem for key in keys for elem in ('-r', key))],
check=True,
capture_output=True,
text=False,
input=content.encode('utf-8'))
return proc.stdout
def data_url(data):
return f'data:image/png;base64,{base64.b64encode(data).decode("utf-8")}'
BORDER_COLOR = '#000000'
BACKGROUND_COLOR = '#d020a0'
FONT_FG_COLOR = '#e0e010'
def pack_html(payload_img_cid, hint_img_data):
return textwrap.dedent(f'''\
<html style="margin: 0; padding: 0; border: 0; width: 100%; height: 100%">
<body style="margin: 0; padding: 0; border: 0; width: 100%; \
background-color: {BACKGROUND_COLOR}; background-image: url({data_url(hint_img_data)})">
<div style="margin: 0; padding: 2px; width: calc(100% - 4px)">
<img src="cid:{payload_img_cid}" style="margin: 0; padding: 0"/>
</div>
<div style="border-left: 2px; border-top: 2px; border-bottom: 2px; border-right: 20px; \
border-style: solid; border-color: {BORDER_COLOR}; margin: 0; padding: 0; \
width: calc(100% - 2px - 20px); height: calc(100% - 4px); position: fixed; top: 0; left: 0">
</div>
</body>
</html>
''').strip()
def create_hint_img(text, max_w=200, fgcolor=FONT_FG_COLOR, bgcolor=BACKGROUND_COLOR, border=10):
relative_path = (Path(__file__).parent / '../../../upstream/terminus-font-4.49.1/ter-u16b.bdf').resolve()
font_file = os.getenv('HINT_FONT', str(relative_path))
font = bdfparser.Font(font_file)
hex_to_np = lambda s: np.frombuffer(binascii.unhexlify(s.lstrip('#')), dtype=np.uint8)
colors = np.array([hex_to_np(bgcolor), hex_to_np(fgcolor)])
imgs = []
for line in text.splitlines():
img = colors[np.array(font.draw(line, linelimit=max_w).todata(2))]
bg = np.tile(colors[np.newaxis, np.newaxis, 0], (img.shape[0], max_w, 1))
w = img.shape[1]
bg[:, (max_w-w)//2 : (max_w-w)//2 + w, :] = img
imgs.append(bg)
img = np.vstack(imgs)
bg = np.tile(colors[np.newaxis, np.newaxis, 0], (img.shape[0]+2*border, img.shape[1]+2*border, 1))
bg[border:-border, border:-border, :] = img
return Image.fromarray(bg)
# because inexplicably, pillow doesn't have this already
def img_to_bytes(img, fmt='png'):
bio = io.BytesIO()
img.save(bio, fmt)
return bio.getvalue()
@click.command()
@click.option('-f', '--from', 'sender')
@click.option('-t', '--to', 'recipients', multiple=True)
@click.option('-c', '--cc', 'cc', multiple=True)
@click.option('-b', '--bcc', 'bcc', multiple=True)
@click.option('--smtp-server')
@click.option('--smtp-user')
@click.option('--smtp-password')
@click.option('--smtp-port', type=int, default=465)
@click.option('-k', '--key', 'keys', multiple=True, required=True)
@click.option('-s', '--subject', default='tachibana/age encrypted message')
@click.option('--payload-width', type=int, default=300)
@click.option('--payload-height', type=int)
@click.argument('content', type=click.File('r'))
def create_age_email(content, sender, recipients, cc, bcc, keys, subject, smtp_server, smtp_port, smtp_user,
smtp_password, payload_width, payload_height):
encrypted = age_encrypt(content.read(), keys)
msg = EmailMessage()
msg['Subject'] = subject
if sender:
msg['From'] = sender
if recipients:
msg['To'] = ', '.join(recipients)
if cc:
msg['Cc'] = ', '.join(cc)
if bcc:
msg['Bcc'] = ', '.join(bcc)
hint_img_data = img_to_bytes(create_hint_img('tachibana/age\nencrypted email', max_w=200))
payload_img_cid = make_msgid()
# remove <...> angle brackets from cids in <img> tags
msg.set_content(pack_html(payload_img_cid[1:-1], hint_img_data), subtype='html')
if payload_height is None:
payload_height = len(encrypted) // 16 * 16
img_bytes = img_to_bytes(tb_data_encoder.data_encode(encrypted, payload_width, payload_height))
#with open('/tmp/test.png', 'wb') as f:
# f.write(img_bytes)
msg.add_related(img_bytes, 'image', 'png', cid=payload_img_cid)
if not smtp_server:
sys.stdout.buffer.write(bytes(msg))
else:
smtp = smtplib.SMTP_SSL(smtp_server, smtp_port)
if smtp_user:
if smtp_password is None:
smtp_password = click.prompt('Please input SMTP password', hide_input=True)
smtp.login(smtp_user, smtp_password)
smtp.send_message(msg)
smtp.quit()
print('Message sent!', file=sys.stderr)
if __name__ == '__main__':
#print(pack_html('foobar'))
#with open('/tmp/test.png', 'wb') as f:
# f.write(img_to_bytes(create_hint_img('tachibana/age\nencrypted email', max_w=200)))
create_age_email()