Add email encoder

This commit is contained in:
jaseg 2021-07-20 15:09:10 +02:00
parent a02e16fd47
commit c63beef741
4 changed files with 110 additions and 9 deletions

View file

@ -0,0 +1,98 @@
#!/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 textwrap
import io
import binascii
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 pack_html(payload_img_cid, hint_img_cid):
return textwrap.dedent(f'''\
<html style="margin: 0; padding: 0; border: 0; width: 100%; height: 100%">
<body style="border-style: solid; border-width: 2px; border-color: #162329; margin: 0; padding: 0; background-color: #d61ca1; width: calc(100% - 4px); height: calc(100% - 4px); overflow: hidden">
<img src="cid:{payload_img_cid}" style="margin: 0; padding: 0"/>
<br/>
<img src="cid:{hint_img_cid}" style="margin: 0; padding: 0"/>
</body>
</html>
''').strip()
def create_hint_img(text, max_w=200, fgcolor='#e1e31b', bgcolor='#d61ca1'):
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)])
img = font.draw(text, linelimit=max_w)
return Image.fromarray(colors[img.todata(2)])
# 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')
@click.option('-c', '--cc', 'cc')
@click.option('-b', '--bcc', 'bcc')
@click.option('-k', '--key', 'keys', multiple=True, required=True)
@click.option('-s', '--subject', default='tachibana/age encrypted message')
@click.argument('content', type=click.File('r'))
def create_age_email(content, sender, recipients, cc, bcc, keys, subject):
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)
payload_img_cid, hint_img_cid = make_msgid(), make_msgid()
# remove <...> angle brackets from cids in <img> tags
msg.set_content(pack_html(payload_img_cid[1:-1], hint_img_cid[1:-1]), subtype='html')
w = max(200, math.ceil(math.sqrt(len(encrypted))))
h = math.ceil((len(encrypted) + 64) / w)
img_bytes = img_to_bytes(tb_data_encoder.data_encode(encrypted, w, h))
msg.add_related(img_bytes, 'image', 'png', cid=payload_img_cid)
img_bytes = img_to_bytes(create_hint_img('tachibana/age\nencrypted email', max_w=200))
msg.add_related(img_bytes, 'image', 'png', cid=hint_img_cid)
sys.stdout.buffer.write(bytes(msg))
if __name__ == '__main__':
#print(pack_html('foobar'))
create_age_email()

View file

@ -21,13 +21,7 @@ WINDOW_MAGIC = np.array([
def tb_int_to_px(val): def tb_int_to_px(val):
return np.array([(val//0x10000)&0xff, (val//0x100)&0xff, val&0xff], dtype=np.uint8) return np.array([(val//0x10000)&0xff, (val//0x100)&0xff, val&0xff], dtype=np.uint8)
@click.command() def data_encode(data, width, height):
@click.option('-w', '--width', type=int, default=400)
@click.option('-h', '--height', type=int, default=300)
@click.argument('input_file', type=click.File('rb'))
@click.argument('output_file', type=click.Path(dir_okay=False, writable=True))
def tb_data_encode(width, height, input_file, output_file):
data = input_file.read()
data = struct.pack('>I', len(data)) + data data = struct.pack('>I', len(data)) + data
data = np.frombuffer(data, dtype=np.uint8) data = np.frombuffer(data, dtype=np.uint8)
@ -46,9 +40,18 @@ def tb_data_encode(width, height, input_file, output_file):
win[0, 10] = tb_int_to_px(width) win[0, 10] = tb_int_to_px(width)
win[0, 11] = tb_int_to_px(height) win[0, 11] = tb_int_to_px(height)
print('out:', ' '.join(f'{x:02x}' for x in win.flatten()[:256])) #print('out:', ' '.join(f'{x:02x}' for x in win.flatten()[:256]))
return Image.fromarray(win)
Image.fromarray(win).save(output_file) @click.command()
@click.option('-w', '--width', type=int, default=400)
@click.option('-h', '--height', type=int, default=300)
@click.argument('input_file', type=click.File('rb'))
@click.argument('output_file', type=click.Path(dir_okay=False, writable=True))
def tb_data_encode(width, height, input_file, output_file):
data = input_file.read()
img = data_encode(data, width, height)
img.save(output_file)
if __name__ == '__main__': if __name__ == '__main__':
tb_data_encode() tb_data_encode()