{ config, lib, pkgs, ensureAccountsWithoutIMAP, ... }: let # Import nodes nodes = import ./../../nodes.nix; myName = config.hostName; myNode = nodes."${myName}"; # And mapping mapping = import ./../../mapping.nix; acct = ensureAccountsWithoutIMAP; generatedCredsCommand = mbox: '' ${pkgs.maddy}/bin/maddyctl creds remove --yes ${mbox} ${pkgs.maddy}/bin/maddyctl creds create --password $(cat ${config.age.secrets."mbox-${lib.lists.head (lib.strings.splitString "@" mbox)}".path}) ${mbox} ''; maddyDeployCreds = pkgs.writeShellScriptBin "maddyDeployCreds" '' ${lib.strings.concatStringsSep "\n" (map generatedCredsCommand acct)} ''; hostToDomain = hostname: "${lib.replaceStrings ["-"] ["."] hostname}.net"; buildSecret = mbox: { "mbox-${lib.lists.head (lib.strings.splitString "@" mbox)}" = { file = ./../../secrets/mail/mbox + ( "/" + lib.lists.head (lib.strings.splitString "@" mbox) + ".age" ); owner = "maddy"; group = "maddy"; }; }; generatedSecrets = lib.foldl' (acc: mbox: acc // buildSecret mbox) {} acct; in { age.secrets = generatedSecrets // { "challenge" = { file = ./../../secrets/dns/challenge.age; owner = "acme"; group = "acme"; }; "dkim-keys" = { file = ./../../secrets/mail/dkim.age; mode = "440"; owner = "maddy"; group = "maddy"; }; }; services.maddy = { enable = true; hostname = "lasuite.federez.net"; user = "maddy"; group = "maddy"; primaryDomain = "lasuite.federez.net"; tls = { loader = "file"; certificates = [{ keyPath = "/var/lib/acme/lasuite.federez.net/key.pem"; certPath = "/var/lib/acme/lasuite.federez.net/fullchain.pem"; }]; extraConfig = '' protocols tls1.0 tls1.3 ''; }; config = '' auth.pass_table local_authdb { table sql_table { driver sqlite3 dsn credentials.db table_name passwords } } table.chain local_rewrites { optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3" optional_step static { entry postmaster postmaster@$(primary_domain) } optional_step file /etc/maddy/aliases } msgpipeline local_routing { default_destination { reject 550 5.1.1 "We don't accept incoming email on lasuite.federez.net, please use federez.net ml" } } smtp tcp://0.0.0.0:25 { limits { all rate 20 1s all concurrency 10 } dmarc yes check { require_mx_record dkim spf } source $(local_domains) { reject 501 5.1.8 "Use Submission for outgoing SMTP" } default_source { destination postmaster $(local_domains) { deliver_to &local_routing } default_destination { reject 550 5.1.1 "User doesn't exist" } } } submission tls://0.0.0.0:465 tcp://0.0.0.0:587 { limits { all rate 50 1s } auth &local_authdb source $(local_domains) { check { authorize_sender { prepare_email &local_rewrites user_to_email identity } } destination postmaster $(local_domains) { deliver_to &local_routing } default_destination { modify { dkim { debug yes domains $(primary_domain) $(local_domains) selector default key_path ${config.age.secrets.dkim-keys.path} } } deliver_to &remote_queue } } default_source { reject 501 5.1.8 "Non-local sender domain" } } target.remote outbound_delivery { limits { destination rate 20 1s destination concurrency 10 } mx_auth { dane mtasts { cache fs fs_dir mtasts_cache/ } local_policy { min_tls_level encrypted min_mx_level none } } } target.queue remote_queue { target &outbound_delivery autogenerated_msg_domain $(primary_domain) bounce { destination postmaster $(local_domains) { deliver_to &local_routing } default_destination { reject 550 5.0.0 "Refusing to send DSNs to non-local addresses" } } } ''; # ensureAccounts automatically create an IMAP account which we do not wish ensureAccounts = []; ensureCredentials = { # Be careful there: those are deployed at runtime only if they do not already exists # As a result using agenix to deploy them will silently failed # See impl: https://github.com/NixOS/nixpkgs/blob/nixos-25.05/nixos/modules/services/mail/maddy.nix # We will write our own script. # But avoid doing this: # "test@lasuite.federez.net".passwordFile = config.age.secrets.monitoring.path; # "test2@lasuite.federez.net".passwordFile = config.age.secrets.postmaster.path; # ... }; }; # Execute custom password setup script systemd.services.maddyDeployCredsSrv = { wantedBy = [ "multi-user.target" ]; after = [ "maddy.service" ]; description = "Add password to maddy's static account"; serviceConfig = { Type = "oneshot"; User = "maddy"; ExecStart = "${maddyDeployCreds}/bin/maddyDeployCreds"; }; }; security.acme = { acceptTerms = true; defaults.email = "monitoring@federez.net"; certs = { "lasuite.federez.net" = { domain = "lasuite.federez.net"; # Needed for mta-sts compliance extraDomainNames = map hostToDomain mapping.mail.hosts; group = "maddy"; dnsProvider = "rfc2136"; reloadServices = [ "maddy" ]; dnsPropagationCheck = true; enableDebugLogs = true; environmentFile = "${pkgs.writeText "dns-creds" '' RFC2136_NAMESERVER=172.19.${toString nodes.${mapping.dns.master}.zone}.${toString nodes.${mapping.dns.master}.id} RFC2136_TSIG_KEY=letsencrypt RFC2136_TSIG_ALGORITHM=hmac-sha512. RFC2136_TSIG_SECRET_FILE="${config.age.secrets.challenge.path}" ''}"; }; }; }; fwtables.allowedTCPPorts = [ { port = 25; public = true; } { port = 465; public = true; } { port = 587; public = true; } ]; }