141 lines
5.1 KiB
Python
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()
|