{ name, pkgs, lib, hostInfo, dsl, ... }: let inherit (dsl) mkContainer mkService; inherit (hostInfo.paths) data secrets; domain = "mail.floweytf.com"; accountName = "flowey"; stateDir = "/var/lib/stalwart"; acmeDir = "/etc/mail-acme"; taggingScript = '' require ["envelope", "variables", "imap4flags", "fileinto"]; if envelope :localpart :matches "to" "*" { addflag "''${1}"; } fileinto "INBOX"; ''; cli = lib.getExe' pkgs.stalwart-cli "stalwart-cli"; jq = lib.getExe pkgs.jq; curl = lib.getExe pkgs.curl; grep = lib.getExe' pkgs.gnugrep "grep"; paste = lib.getExe' pkgs.coreutils "paste"; # use sqlite b/c rocksdb segfaults configJsonTemplate = pkgs.writeText "stalwart-config.json" ( builtins.toJSON { "@type" = "Sqlite"; path = "${stateDir}/data.sqlite3"; } ); staticPlan = pkgs.writeText "mail-static-plan.json" ( builtins.concatStringsSep "\n" ( map builtins.toJSON [ { "@type" = "upsert"; object = "Domain"; matchOn = [ "name" ]; value."domain-mail" = { name = domain; catchAllAddress = "${accountName}@${domain}"; isEnabled = true; }; } { "@type" = "upsert"; object = "SieveSystemScript"; matchOn = [ "name" ]; value."sieve-tag" = { name = "tag-by-recipient"; isActive = true; contents = taggingScript; }; } ] ) + "\n" ); stalwartAuthEnv = '' export STALWART_URL="http://127.0.0.1:8080" export STALWART_USER="admin" export STALWART_PASSWORD="$(cat "$CREDENTIALS_DIRECTORY/admin_password")" ''; readyGate = pkgs.writeShellScript "mail-ready-gate" '' set -uo pipefail until ${curl} -sS -o /dev/null "http://127.0.0.1:8080/api/account"; do if ! systemctl -q is-active stalwart.service; then echo "mail: stalwart is not running" >&2 exit 1 fi sleep 1 done ''; bootstrapScript = pkgs.writeShellScript "mail-bootstrap" '' set -uo pipefail ${stalwartAuthEnv} out="$(${cli} update Bootstrap singleton \ --field defaultDomain=${domain} \ --field serverHostname=${domain} \ --field generateDkimKeys=true \ --field requestTlsCertificate=false \ --field 'directory={"@type":"Internal"}' 2>&1)" status=$? if [ "$status" -eq 0 ]; then echo "mail-bootstrap: first-time setup complete, restarting stalwart into normal mode" >&2 systemctl restart stalwart.service exit 0 fi if ${grep} -qi 'bootstrap mode' <<<"$out"; then exit 0 fi echo "mail-bootstrap: $out" >&2 exit 1 ''; certCredCert = "/run/credentials/stalwart.service/mail_tls_cert"; certCredKey = "/run/credentials/stalwart.service/mail_tls_key"; retryOnFailure = { Restart = "on-failure"; RestartSec = 5; }; retryLimit = { StartLimitBurst = 5; StartLimitIntervalSec = 120; }; provisionScript = pkgs.writeShellScript "mail-provision" '' set -euo pipefail ${stalwartAuthEnv} export account_pass="$(cat "$CREDENTIALS_DIRECTORY/account_password")" account_entry="$(${jq} -nc --arg name "${accountName}" \ '{"@type":"upsert","object":"Account","matchOn":["name"],"value":{("account-"+$name):{"@type":"User","name":$name,"domainId":"#domain-mail","credentials":{"0":{"@type":"Password","secret":env.account_pass}}}}}')" { cat ${staticPlan}; echo "$account_entry"; } | ${cli} apply --stdin existing_cert_ids="$(${cli} query Certificate --json --fields id | ${jq} -r .id | ${paste} -sd, -)" if [ -n "$existing_cert_ids" ]; then ${cli} delete Certificate --ids "$existing_cert_ids" fi ${cli} create Certificate \ --field 'certificate={"@type":"File","filePath":"${certCredCert}"}' \ --field 'privateKey={"@type":"File","filePath":"${certCredKey}"}' systemctl restart stalwart.service ''; in mkContainer { inherit name; net.access = [ { to = "@internet"; } ]; svc.autoStart = true; net.forward.tcp = [ 25 993 ]; resources.fs = { "${stateDir}" = { hostPath = "${data}/core/mail"; isReadOnly = false; }; "${acmeDir}" = { hostPath = "${data}/core/nginx/acme/${domain}"; isReadOnly = true; }; "/run/secrets/mail-admin-password" = { hostPath = "${secrets}/mail-admin-password"; isReadOnly = true; }; "/run/secrets/mail-account-password" = { hostPath = "${secrets}/mail-account-password"; isReadOnly = true; }; }; users = [ { name = "stalwart"; uid = 1000; home = stateDir; } ]; containerConfig = _: { systemd.tmpfiles.rules = [ "Z ${stateDir} 0750 stalwart stalwart -" "C ${stateDir}/config.json 0640 stalwart stalwart - ${configJsonTemplate}" ]; systemd.services.stalwart = mkService { description = "Stalwart Mail Server"; execStart = pkgs.writeShellScript "stalwart-run" '' set -euo pipefail export STALWART_RECOVERY_ADMIN="admin:$(cat "$CREDENTIALS_DIRECTORY/admin_password")" exec ${lib.getExe' pkgs.stalwart_0_16 "stalwart"} -c ${stateDir}/config.json ''; serviceConfig = { User = "stalwart"; Group = "stalwart"; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; ReadWritePaths = [ stateDir ]; LoadCredential = [ "admin_password:/run/secrets/mail-admin-password" "mail_tls_cert:${acmeDir}/fullchain.pem" "mail_tls_key:${acmeDir}/key.pem" ]; Restart = "on-failure"; RestartSec = 5; }; }; systemd.paths.mail-cert-renew = { wantedBy = [ "multi-user.target" ]; pathConfig = { PathChanged = "${acmeDir}/key.pem"; Unit = "mail-provision.service"; }; }; systemd.services.mail-ready = mkService { description = "Wait for the Stalwart management API"; execStart = readyGate; after = [ "stalwart.service" ]; wants = [ "stalwart.service" ]; wantedBy = [ ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; TimeoutStartSec = 60; }; }; systemd.services.mail-bootstrap = mkService { description = "One-time Stalwart bootstrap (server identity + admin account)"; execStart = bootstrapScript; after = [ "mail-ready.service" ]; wants = [ "mail-ready.service" ]; unitConfig = retryLimit; serviceConfig = { Type = "oneshot"; LoadCredential = [ "admin_password:/run/secrets/mail-admin-password" ]; } // retryOnFailure; }; systemd.services.mail-provision = mkService { description = "Provision mail.floweytf.com domain + catch-all account in Stalwart"; execStart = provisionScript; after = [ "mail-ready.service" "mail-bootstrap.service" ]; wants = [ "mail-ready.service" "mail-bootstrap.service" ]; serviceConfig = { Type = "oneshot"; LoadCredential = [ "admin_password:/run/secrets/mail-admin-password" "account_password:/run/secrets/mail-account-password" ]; } // retryOnFailure; unitConfig = retryLimit; }; }; }