{ config, inputs, lib, pkgs, ... }: let domain = "hexname.com"; stalwartDomain = "mx.${domain}"; roundcubeDomain = "email.${domain}"; dataDir = "/var/lib/stalwart-mail"; credPath = "/run/credentials/stalwart-mail.service"; in { services.stalwart-mail = { enable = true; package = pkgs.stalwart-mail; openFirewall = true; settings = { server = { hostname = stalwartDomain; tls = { enable = true; implicit = true; }; listener = { smtp = { bind = [ "[::]:25" ]; protocol = "smtp"; }; submission = { bind = [ "[::]:587" ]; protocol = "smtp"; }; submissions = { bind = [ "[::]:465" ]; protocol = "smtp"; tls.implicit = true; }; imap = { bind = [ "[::]:143" ]; protocol = "imap"; }; imaps = { bind = [ "[::]:993" ]; protocol = "imap"; tls.implicit = true; }; http = { bind = [ "[::]:51020" ]; protocol = "http"; url = "https://${stalwartDomain}"; }; }; }; directory."in-memory" = { type = "memory"; # Generate hashes with: # $ openssl passwd -6 principals = [ { name = "contact-us@${domain}"; email = [ "contact-us@${domain}" "privacy@${domain}" ]; secret = "$6$iyUwAnKuGTz31jeu$QPfoaUQPccVDWjCWs4PY43dBI6oG4eNb7buNlGBlnNJrvQOePYKyF8RXN8FI5H6y2x191kOa4U8aDD4K/ssKn/"; class = "individual"; } { name = "no-reply@${domain}"; email = [ "no-reply@${domain}" ]; secret = "$6$FpTIF6mjoBRXyZAO$9lqf/u3NyJNHYNutFY0WmPkbfkq8J.SIkhzya3izl7AbCRE72TlyKeGx/OOyPuI1QTMV10NgOEGzL8jboOWhZ1"; class = "individual"; } ]; }; authentication.fallback-admin = { user = "unguessable-username"; secret = "$6$1sRTqTbiXuGNE3zt$oLcXi.kPsy72W5SDMwWSitpJyKlZSKSzhr1QO3DBn6Q9LSE.YpWUbT2Thu5Kbs0bmTMvqAPFI7x/qa1wm9Bj91"; }; email.folders = let mkFolder = name: { inherit name; create = true; subscribe = true; }; in { inbox = mkFolder "Inbox"; sent = mkFolder "Sent"; drafts = mkFolder "Drafts"; archive = mkFolder "Archive"; junk = mkFolder "Spam"; trash = mkFolder "Trash"; }; # Change the DNS records manually to these addresses to # keep postmaster free for non-automated emails # https://github.com/stalwartlabs/mail-server/discussions/877 report.analysis = { addresses = [ "dmarc-reports@*" "tls-reports@*" "spf-reports@*" ]; forward = false; }; # Stop warnings about what's managed where config.local-keys = [ "authentication.fallback-admin.*" "certificate.*" "cluster.node-id" "directory.*" "email.folders.*" "lookup.default.domain" "lookup.default.hostname" "report.analysis.*" "resolver.*" "server.*" "!server.blocked-ip.*" "session.mta-sts.*" "session.rcpt.catch-all" "session.rcpt.script" "sieve.trusted.scripts.*" "spam-filter.resource" "storage.blob" "storage.data" "storage.directory" "storage.fts" "storage.lookup" "store.*" "tracer.*" "webadmin.*" ]; # Store blobs in the file system for easier backups. # Since the database is backed up to /tmp, it would not fit in RAM # with all the blobs. store.fs = { type = "fs"; path = "${dataDir}/blobs"; }; storage.blob = "fs"; # We have DANE and don't want a certificate for each domain session.mta-sts.mode = "none"; certificate.default = { cert = "%{file:${credPath}/cert.pem}%"; private-key = "%{file:${credPath}/key.pem}%"; default = true; }; lookup.default = { inherit domain; hostname = stalwartDomain; }; tracer.stdout.level = "info"; }; }; networking.firewall.allowedTCPPorts = [ 25 143 465 587 993 ]; systemd.services.stalwart-mail = { wants = [ "acme-${stalwartDomain}.service" ]; after = [ "acme-${stalwartDomain}.service" ]; preStart = '' mkdir -p ${dataDir}/db ''; serviceConfig = { LogsDirectory = "stalwart-mail"; LoadCredential = [ "cert.pem:${config.security.acme.certs.${stalwartDomain}.directory}/cert.pem" "key.pem:${config.security.acme.certs.${stalwartDomain}.directory}/key.pem" ]; }; }; services.roundcube = { enable = true; package = pkgs.roundcube; dicts = with pkgs.aspellDicts; [ en de ]; hostName = roundcubeDomain; plugins = [ "archive" "zipdownload" "acl" ]; extraConfig = '' $config['imap_host'] = 'ssl://${stalwartDomain}:993'; $config['smtp_host'] = 'ssl://%h:465'; $config['mail_domain'] = '%z'; ''; }; services.nginx.virtualHosts = let proxy = "http://localhost:51020"; in { ${stalwartDomain} = { enableACME = true; forceSSL = true; locations."/".proxyPass = proxy; }; ${roundcubeDomain}.locations."/".extraConfig = '' add_header Cache-Control "public, max-age=604800, must-revalidate" always; add_header Referrer-Policy "origin-when-cross-origin" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; ''; "mta-sts.${domain}" = { enableACME = true; forceSSL = true; locations."/".root = pkgs.writeTextFile { name = "mta-sts.txt"; text = '' version: STSv1 mode: enforce mx: ${stalwartDomain} max_age: 86400 ''; destination = "/.well-known/mta-sts.txt"; }; }; }; security.acme.certs.${stalwartDomain} = { # Keep a stable private key for TLSA records (DANE) # https://community.letsencrypt.org/t/please-avoid-3-0-1-and-3-0-2-dane-tlsa-records-with-le-certificates/7022/14 extraLegoRenewFlags = [ "--reuse-key" ]; # Restart Stalwart to apply new certificates reloadServices = [ "stalwart-mail.service" ]; }; }