diff --git a/chatmaild/pyproject.toml b/chatmaild/pyproject.toml index 5e74f5c7..3f1e1fc0 100644 --- a/chatmaild/pyproject.toml +++ b/chatmaild/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" name = "chatmaild" version = "0.1" dependencies = [ - "aiosmtpd" + "aiosmtpd", ] [project.scripts] diff --git a/chatmaild/src/chatmaild/newemail.py b/chatmaild/src/chatmaild/newemail.py new file mode 100644 index 00000000..4fbc0df6 --- /dev/null +++ b/chatmaild/src/chatmaild/newemail.py @@ -0,0 +1,28 @@ +#!/usr/bin/python3 + +""" CGI script for creating new accounts. """ + +import json +import random + +mailname_path = "/etc/mailname" + + +def create_newemail_dict(domain): + alphanumeric = "abcdefghijklmnopqrstuvwxyz1234567890" + user = "".join(random.choices(alphanumeric, k=9)) + password = "".join(random.choices(alphanumeric, k=12)) + return dict(email=f"{user}@{domain}", password=f"{password}") + + +def print_new_account(): + domain = open(mailname_path).read().strip() + creds = create_newemail_dict(domain=domain) + + print("Content-Type: application/json") + print("") + print(json.dumps(creds)) + + +if __name__ == "__main__": + print_new_account() diff --git a/deploy-chatmail/pyproject.toml b/deploy-chatmail/pyproject.toml index 3d98b8a4..27ef46d0 100644 --- a/deploy-chatmail/pyproject.toml +++ b/deploy-chatmail/pyproject.toml @@ -7,6 +7,7 @@ name = "deploy-chatmail" version = "0.1" dependencies = [ "pyinfra", + "qrcode", ] [tool.pytest.ini_options] diff --git a/deploy-chatmail/src/deploy_chatmail/__init__.py b/deploy-chatmail/src/deploy_chatmail/__init__.py index 7b80fb8b..072d0d53 100644 --- a/deploy-chatmail/src/deploy_chatmail/__init__.py +++ b/deploy-chatmail/src/deploy_chatmail/__init__.py @@ -10,6 +10,8 @@ from pyinfra.facts.files import File from pyinfra.facts.systemd import SystemdEnabled from .acmetool import deploy_acmetool +from .genqr import gen_qr_png_data + def _install_chatmaild() -> None: chatmaild_filename = "chatmaild-0.1.tar.gz" @@ -44,6 +46,8 @@ def _install_chatmaild() -> None: enabled=False, ) + # install systemd units + for fn in ( "doveauth", "filtermail", @@ -279,6 +283,34 @@ def _configure_nginx(domain: str, debug: bool = False) -> bool: ) need_restart |= mta_sts_config.changed + # install CGI newemail script + # + cgi_dir = "/usr/lib/cgi-bin" + files.directory( + name=f"Ensure {cgi_dir} exists", + path=cgi_dir, + user="root", + group="root", + ) + + files.put( + name=f"Upload cgi newemail.py script", + src=importlib.resources.files("chatmaild").joinpath(f"newemail.py").open("rb"), + dest=f"{cgi_dir}/newemail.py", + user="root", + group="root", + mode="755", + ) + + files.put( + name=f"Upload QR code for account creation", + src=gen_qr_png_data(domain), + dest=f"/var/www/html/qrcode.png", + user="root", + group="root", + mode="644", + ) + return need_restart @@ -328,19 +360,26 @@ def deploy_chatmail(mail_domain: str, mail_server: str, dkim_selector: str) -> N packages=["nginx"], ) + apt.packages( + name="Install fcgiwrap", + packages=["fcgiwrap"], + ) + _install_chatmaild() debug = False dovecot_need_restart = _configure_dovecot(mail_server, debug=debug) postfix_need_restart = _configure_postfix(mail_domain, debug=debug) opendkim_need_restart = _configure_opendkim(mail_domain, dkim_selector) - nginx_need_restart = _configure_nginx(mail_domain) mta_sts_need_restart = _install_mta_sts_daemon() + nginx_need_restart = _configure_nginx(mail_domain) # deploy web pages and info if we have them pkg_root = importlib.resources.files(__package__) www_path = pkg_root.joinpath(f"../../../www/{mail_domain}").resolve() - if www_path.is_dir(): - files.rsync(f"{www_path}/", "/var/www/html", flags=["-avz"]) + if not www_path.is_dir(): + www_path = pkg_root.joinpath(f"../../../www/default").resolve() + + files.rsync(f"{www_path}/", "/var/www/html", flags=["-avz"]) systemd.service( name="Start and enable OpenDKIM", diff --git a/deploy-chatmail/src/deploy_chatmail/data/delta-chat-bw.png b/deploy-chatmail/src/deploy_chatmail/data/delta-chat-bw.png new file mode 100644 index 00000000..26775ae2 Binary files /dev/null and b/deploy-chatmail/src/deploy_chatmail/data/delta-chat-bw.png differ diff --git a/deploy-chatmail/src/deploy_chatmail/data/delta-chat-red.png b/deploy-chatmail/src/deploy_chatmail/data/delta-chat-red.png new file mode 100644 index 00000000..ebc1cdef Binary files /dev/null and b/deploy-chatmail/src/deploy_chatmail/data/delta-chat-red.png differ diff --git a/deploy-chatmail/src/deploy_chatmail/data/opensans-regular.ttf b/deploy-chatmail/src/deploy_chatmail/data/opensans-regular.ttf new file mode 100644 index 00000000..29bfd35a Binary files /dev/null and b/deploy-chatmail/src/deploy_chatmail/data/opensans-regular.ttf differ diff --git a/deploy-chatmail/src/deploy_chatmail/genqr.py b/deploy-chatmail/src/deploy_chatmail/genqr.py new file mode 100644 index 00000000..605c14ec --- /dev/null +++ b/deploy-chatmail/src/deploy_chatmail/genqr.py @@ -0,0 +1,94 @@ +import importlib +import qrcode +import os +from PIL import ImageFont, ImageDraw, Image +import io + + +def gen_qr_png_data(maildomain): + url = f"DCACCOUNT:https://{maildomain}/cgi-bin/newemail.py" + image = gen_qr(maildomain, url) + temp = io.BytesIO() + image.save(temp, format="png") + temp.seek(0) + return temp + + +def gen_qr(maildomain, url): + info = f"{maildomain} invite code" + + steps = ( + "1. Install https://get.delta.chat\n" + "2. On setup screen scan above invite QR code\n" + "3. Choose nickname & avatar\n" + "+ chat with any e-mail address ...\n" + ) + + # load QR code + qr = qrcode.QRCode( + version=1, + error_correction=qrcode.constants.ERROR_CORRECT_H, + box_size=1, + border=1, + ) + qr.add_data(url) + qr.make(fit=True) + qr_img = qr.make_image(fill_color="black", back_color="white") + + # paint all elements + ttf_path = str( + importlib.resources.files(__package__).joinpath("data/opensans-regular.ttf") + ) + logo_red_path = str( + importlib.resources.files(__package__).joinpath("data/delta-chat-bw.png") + ) + + assert os.path.exists(ttf_path), ttf_path + font_size = 16 + font = ImageFont.truetype(font=ttf_path, size=font_size) + + num_lines = (info + steps).count("\n") + 3 + + size = width = 384 + qr_padding = 6 + text_margin_right = 12 + text_height = font_size * num_lines + height = size + text_height + qr_padding * 2 + + image = Image.new("RGBA", (width, height), "white") + + draw = ImageDraw.Draw(image) + + qr_final_size = width - (qr_padding * 2) + + # draw text + if hasattr(font, "getsize"): + info_pos = (width - font.getsize(info.strip())[0]) // 2 + else: + info_pos = (width - font.getbbox(info.strip())[3]) // 2 + + draw.multiline_text( + (info_pos, size - qr_padding // 2), info, font=font, fill="black", align="right" + ) + draw.multiline_text( + (text_margin_right, height - text_height + font_size * 1.0), + steps, + font=font, + fill="black", + align="left", + ) + + # paste QR code + image.paste( + qr_img.resize((qr_final_size, qr_final_size), resample=Image.NEAREST), + (qr_padding, qr_padding), + ) + + # background delta logo + logo2_img = Image.open(logo_red_path) + logo2_width = int(size / 6) + logo2 = logo2_img.resize((logo2_width, logo2_width), resample=Image.NEAREST) + pos = int((size / 2) - (logo2_width / 2)) + image.paste(logo2, (pos, pos), mask=logo2) + + return image diff --git a/deploy-chatmail/src/deploy_chatmail/nginx/nginx.conf.j2 b/deploy-chatmail/src/deploy_chatmail/nginx/nginx.conf.j2 index 7a2f8265..e2a71097 100644 --- a/deploy-chatmail/src/deploy_chatmail/nginx/nginx.conf.j2 +++ b/deploy-chatmail/src/deploy_chatmail/nginx/nginx.conf.j2 @@ -40,6 +40,21 @@ http { # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } + + location /cgi-bin/ { + # Set the root to /usr/lib (inside this location this means that we are + # giving access to the files under /usr/lib/cgi-bin) + root /usr/lib; + + # Fastcgi socket + fastcgi_pass unix:/var/run/fcgiwrap.socket; + + # Fastcgi parameters, include the standard ones + include /etc/nginx/fastcgi_params; + + # Adjust non standard parameters (SCRIPT_FILENAME) + # fastcgi_param SCRIPT_FILENAME /usr/lib$fastcgi_script_name; + } } server { listen 80 default_server; @@ -48,5 +63,8 @@ http { return 301 https://$host$request_uri; } + + + } diff --git a/tests/chatmaild/test_newmail.py b/tests/chatmaild/test_newmail.py new file mode 100644 index 00000000..5b6dad1f --- /dev/null +++ b/tests/chatmaild/test_newmail.py @@ -0,0 +1,28 @@ +import json + +import chatmaild +from chatmaild.newemail import create_newemail_dict, print_new_account + +def test_create_newemail_dict(): + ac1 = create_newemail_dict(domain="example.org") + assert "@" in ac1["email"] + assert len(ac1["password"]) >= 10 + + ac2 = create_newemail_dict(domain="example.org") + + assert ac1["email"] != ac2["email"] + assert ac1["password"] != ac2["password"] + + +def test_print_new_account(capsys, monkeypatch, maildomain, tmpdir): + p = tmpdir.join("mailname") + p.write(maildomain) + monkeypatch.setattr(chatmaild.newemail, "mailname_path", str(p)) + print_new_account() + out, err = capsys.readouterr() + lines = out.split("\n") + assert lines[0] == "Content-Type: application/json" + assert not lines[1] + dic = json.loads(lines[2]) + assert dic["email"].endswith(f"@{maildomain}") + assert len(dic["password"]) >= 10 diff --git a/www/default/index.html b/www/default/index.html new file mode 100644 index 00000000..de66cbbe --- /dev/null +++ b/www/default/index.html @@ -0,0 +1,25 @@ + + + + + chatmail instance + + + +

Welcome to Chatmail!

+

Scan this invite QR code from any Delta Chat app

+ + +

Properties / Constraints

+ + +