""" Chat Mail pyinfra deploy. """ import importlib.resources import configparser import textwrap from pathlib import Path from pyinfra import host from pyinfra.operations import apt, files, server, systemd from pyinfra.facts.files import File from pyinfra.facts.systemd import SystemdEnabled from .acmetool import deploy_acmetool import markdown from jinja2 import Template from .genqr import gen_qr_png_data def _install_chatmaild() -> None: chatmaild_filename = "chatmaild-0.1.tar.gz" chatmaild_path = importlib.resources.files(__package__).joinpath( f"../../../dist/{chatmaild_filename}" ) remote_path = f"/tmp/{chatmaild_filename}" if Path(str(chatmaild_path)).exists(): files.put( name="Upload chatmaild source package", src=chatmaild_path.open("rb"), dest=remote_path, ) apt.packages( name="apt install python3-aiosmtpd python3-pip python3-venv", packages=["python3-aiosmtpd", "python3-pip", "python3-venv"], ) # --no-deps because aiosmtplib is installed with `apt`. server.shell( name="install chatmaild with pip", commands=[f"pip install --break-system-packages {remote_path}"], ) # disable legacy doveauth-dictproxy.service if host.get_fact(SystemdEnabled).get("doveauth-dictproxy.service"): systemd.service( name="Disable legacy doveauth-dictproxy.service", service="doveauth-dictproxy.service", running=False, enabled=False, ) # install systemd units for fn in ( "doveauth", "filtermail", ): files.put( name=f"Upload {fn}.service", src=importlib.resources.files("chatmaild") .joinpath(f"{fn}.service") .open("rb"), dest=f"/etc/systemd/system/{fn}.service", user="root", group="root", mode="644", ) systemd.service( name=f"Setup {fn} service", service=f"{fn}.service", running=True, enabled=True, restarted=True, daemon_reload=True, ) def _configure_opendkim(domain: str, dkim_selector: str) -> bool: """Configures OpenDKIM""" need_restart = False main_config = files.template( src=importlib.resources.files(__package__).joinpath("opendkim/opendkim.conf"), dest="/etc/opendkim.conf", user="root", group="root", mode="644", config={"domain_name": domain, "opendkim_selector": dkim_selector}, ) need_restart |= main_config.changed files.directory( name="Add opendkim directory to /etc", path="/etc/opendkim", user="opendkim", group="opendkim", mode="750", present=True, ) keytable = files.template( src=importlib.resources.files(__package__).joinpath("opendkim/KeyTable"), dest="/etc/dkimkeys/KeyTable", user="opendkim", group="opendkim", mode="644", config={"domain_name": domain, "opendkim_selector": dkim_selector}, ) need_restart |= keytable.changed signing_table = files.template( src=importlib.resources.files(__package__).joinpath("opendkim/SigningTable"), dest="/etc/dkimkeys/SigningTable", user="opendkim", group="opendkim", mode="644", config={"domain_name": domain, "opendkim_selector": dkim_selector}, ) need_restart |= signing_table.changed files.directory( name="Add opendkim socket directory to /var/spool/postfix", path="/var/spool/postfix/opendkim", user="opendkim", group="opendkim", mode="750", present=True, ) if not host.get_fact(File, f"/etc/dkimkeys/{dkim_selector}.private"): server.shell( name="Generate OpenDKIM domain keys", commands=[ f"opendkim-genkey -D /etc/dkimkeys -d {domain} -s {dkim_selector}" ], _sudo=True, _sudo_user="opendkim", ) return need_restart def _install_mta_sts_daemon() -> bool: need_restart = False config = files.put( name="upload postfix-mta-sts-resolver config", src=importlib.resources.files(__package__).joinpath( "postfix/mta-sts-daemon.yml" ), dest="/etc/mta-sts-daemon.yml", user="root", group="root", mode="644", ) need_restart |= config.changed server.shell( name="install postfix-mta-sts-resolver with pip", commands=[ "python3 -m venv /usr/local/lib/postfix-mta-sts-resolver", "/usr/local/lib/postfix-mta-sts-resolver/bin/pip install postfix-mta-sts-resolver", ], ) systemd_unit = files.put( name="upload mta-sts-daemon systemd unit", src=importlib.resources.files(__package__).joinpath( "postfix/mta-sts-daemon.service" ), dest="/etc/systemd/system/mta-sts-daemon.service", user="root", group="root", mode="644", ) need_restart |= systemd_unit.changed return need_restart def _configure_postfix(domain: str, debug: bool = False) -> bool: """Configures Postfix SMTP server.""" need_restart = False main_config = files.template( src=importlib.resources.files(__package__).joinpath("postfix/main.cf.j2"), dest="/etc/postfix/main.cf", user="root", group="root", mode="644", config={"domain_name": domain}, ) need_restart |= main_config.changed master_config = files.template( src=importlib.resources.files(__package__).joinpath("postfix/master.cf.j2"), dest="/etc/postfix/master.cf", user="root", group="root", mode="644", debug=debug, ) need_restart |= master_config.changed return need_restart def _configure_dovecot(mail_server: str, debug: bool = False) -> bool: """Configures Dovecot IMAP server.""" need_restart = False main_config = files.template( src=importlib.resources.files(__package__).joinpath("dovecot/dovecot.conf.j2"), dest="/etc/dovecot/dovecot.conf", user="root", group="root", mode="644", config={"hostname": mail_server}, debug=debug, ) need_restart |= main_config.changed auth_config = files.put( src=importlib.resources.files(__package__).joinpath("dovecot/auth.conf"), dest="/etc/dovecot/auth.conf", user="root", group="root", mode="644", ) need_restart |= auth_config.changed files.put( src=importlib.resources.files(__package__) .joinpath("dovecot/expunge.cron") .open("rb"), dest="/etc/cron.d/expunge", user="root", group="root", mode="644", ) # as per https://doc.dovecot.org/configuration_manual/os/ # it is recommended to set the following inotify limits for name in ("max_user_instances", "max_user_watches"): key = f"fs.inotify.{name}" server.sysctl( name=f"Change {key}", key=key, value=65535, persist=True, ) return need_restart def _configure_nginx(domain: str, debug: bool = False) -> bool: """Configures nginx HTTP server.""" need_restart = False main_config = files.template( src=importlib.resources.files(__package__).joinpath("nginx/nginx.conf.j2"), dest="/etc/nginx/nginx.conf", user="root", group="root", mode="644", config={"domain_name": domain}, ) need_restart |= main_config.changed autoconfig = files.template( src=importlib.resources.files(__package__).joinpath("nginx/autoconfig.xml.j2"), dest="/var/www/html/.well-known/autoconfig/mail/config-v1.1.xml", user="root", group="root", mode="644", config={"domain_name": domain}, ) need_restart |= autoconfig.changed mta_sts_config = files.template( src=importlib.resources.files(__package__).joinpath("nginx/mta-sts.txt.j2"), dest="/var/www/html/.well-known/mta-sts.txt", user="root", group="root", mode="644", config={"domain_name": domain}, ) 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="Upload cgi newemail.py script", src=importlib.resources.files("chatmaild").joinpath("newemail.py").open("rb"), dest=f"{cgi_dir}/newemail.py", user="root", group="root", mode="755", ) return need_restart def get_ini_settings(mail_domain, inipath): parser = configparser.ConfigParser() parser.read(inipath) settings = {key: value.strip() for (key, value) in parser["config"].items()} if mail_domain != "testrun.org" and not mail_domain.endswith(".testrun.org"): for value in settings.values(): value = value.lower() if "merlinux" in value or "schmieder" in value or "@testrun.org" in value: raise ValueError( f"please set your own privacy contacts/addresses in {inipath}" ) settings["mail_domain"] = mail_domain return settings def build_htmlj2_from_markdown(source): assert source.exists(), source template_content = open(source).read() if source.stem == "privacy": title = "privacy {{ config.mail_domain }}" elif source.stem == "index": title = "home {{ config.mail_domain }}" elif source.stem == "info": title = "info {{ config.mail_domain }}" html = markdown.markdown(template_content) html = ( textwrap.dedent( f"""\