{ config, pkgs, lib, ... }: let choose = paths: lib.findFirst builtins.pathExists null paths; # Read secrets from sops-managed files and trim trailing newlines so the # generated stalwart config contains the actual secret values (not the # literal "$FOO" placeholders). Using builtins.readFile here ensures the # values are placed into the TOML at build time. # Use a safe helper that handles missing secret attributes or missing files # during a dry-run (returns empty string if absent). getSecret = secretAttr: if secretAttr != null && builtins.pathExists secretAttr.path then builtins.replaceStrings ["\n"] [""] (builtins.readFile secretAttr.path) else ""; adminPassword = getSecret config.sops.secrets."admin-password"; boardPassword = getSecret config.sops.secrets."board-member-password"; utilityPassword = getSecret config.sops.secrets."utility-password"; cloudflareUsername = getSecret config.sops.secrets."cloudflare-username"; cloudflareToken = getSecret config.sops.secrets."cloudflare-dns-token"; in { options.local = { hostname = lib.mkOption { type = lib.types.str; default = "nixos-default"; description = "System hostname"; }; username = lib.mkOption { type = lib.types.str; default = "user"; description = "Primary user username"; }; userDescription = lib.mkOption { type = lib.types.str; default = "NixOS User"; description = "Primary user description"; }; address = lib.mkOption { type = lib.types.str; default = "10.1.1.100"; description = "Static IP address"; }; }; imports = lib.filter (x: x != null) [ (choose [./modules/desktop-manager/sway_greetd_homemanager.nix ../desktop-manager/sway_greetd_homemanager.nix]) (choose [./modules/local/hostname_username.nix ../local/hostname_username.nix]) (choose [./modules/local/networking_local.nix ../local/networking_local.nix]) (choose [./modules/bootloader/seabios-assigned-proxmox-at-birth.nix ../bootloader/seabios-assigned-proxmox-at-birth.nix]) (choose [./modules/lix-default.nix ../lix-default.nix]) (choose [./modules/secrets-config/sops-the-blank-system.nix ../secrets-config/sops-the-blank-system.nix]) (choose [./modules/toolsets/grafana_metric.nix ../toolsets/grafana_metric.nix]) (choose [./modules/secrets-config/sops-mail.nix ../secrets-config/sops-mail.nix]) ]; config = { # Create /etc/stalwart secret files so the generated TOML can reference them environment.etc = { "stalwart/mail-pw1".text = boardPassword; # principal password (board) "stalwart/mail-pw1".mode = "0777"; "stalwart/mail-pw2".text = utilityPassword; # principal password (utility) "stalwart/mail-pw2".mode = "0777"; "stalwart/admin-pw".text = adminPassword; # admin fallback password "stalwart/admin-pw".mode = "0777"; "stalwart/acme-secret".text = cloudflareToken; # API token for ACME (Cloudflare) "stalwart/acme-secret".mode = "0777"; "stalwart/cloudflare-username".text = cloudflareUsername; # contact email for ACME "stalwart/cloudflare-username".mode = "0777"; }; systemd.tmpfiles.rules = [ # z = create/modify file or directory, set mode and owner "z /etc/stalwart 0555 root root - -" ]; # Enable Tailscale for remote access to Traefik dashboard and configuration services.tailscale.enable = true; local.hostname = "nixos-mailserver"; local.username = "mailprg"; local.userDescription = "NixOS PRG Mailing Service"; local.address = "10.1.1.15"; system.stateVersion = "25.11"; services.stalwart-mail = { enable = true; openFirewall = true; settings = { server = { hostname = "mail.prg-radio.org"; tls = { enable = true; implicit = true; }; listener = { smtp = { protocol = "smtp"; # Use port 587 for SMTP submission with STARTTLS, as port 25 is often blocked by ISPs for outgoing mail to prevent spam. # This way, you can still receive mail on port 25 but require authenticated submission on port 587. bind = "[::]:25"; # I am so fucking fed up this shit, this fucking ISP is sending me to early grave. }; submissions = { bind = "[::]:465"; protocol = "smtp"; tls.implicit = true; }; imaps = { bind = "[::]:993"; protocol = "imap"; tls.implicit = true; }; jmap = { bind = "[::]:8080"; url = "https://mail.prg-radio.org"; protocol = "http"; }; management = { bind = ["127.0.0.1:8080" "[::]:8081"]; protocol = "http"; }; }; }; lookup.default = { hostname = "mail.prg-radio.org"; domain = "prg-radio.org"; }; acme."letsencrypt" = { directory = "https://acme-v02.api.letsencrypt.org/directory"; challenge = "dns-01"; # reference the contact and secret via files under /etc/stalwart contact = "%{file:/etc/stalwart/cloudflare-username}%"; domains = ["prg-radio.org" "mailadmin.prg-radio.org" "mail.prg-radio.org"]; provider = "cloudflare"; secret = "%{file:/etc/stalwart/acme-secret}%"; }; session.auth = { mechanisms = ["plain"]; directory = "in-memory"; }; storage.directory = "in-memory"; session.rcpt.directory = "in-memory"; directory."imap".lookup.domains = ["prg-radio.org"]; directory."in-memory" = { type = "memory"; principals = [ { class = "individual"; name = "Polyteknisk Radiogruppe Board Member"; secret = "%{file:/etc/stalwart/mail-pw1}%"; email = ["board@prg-radio.org"]; } { class = "individual"; name = "postmaster"; secret = "%{file:/etc/stalwart/mail-pw1}%"; email = ["postmaster@prg-radio.org"]; } { class = "individual"; name = "no-reply"; secret = "%{file:/etc/stalwart/mail-pw2}%"; email = ["no-reply@prg-radio.org"]; } { class = "individual"; name = "service"; secret = "%{file:/etc/stalwart/mail-pw2}%"; email = ["service@prg-radio.org"]; } ]; }; authentication.fallback-admin = { user = "admin"; secret = "%{file:/etc/stalwart/admin-pw}%"; }; }; }; networking.firewall.allowedTCPPorts = [ 25 ]; networking.firewall.allowedUDPPorts = [ 25 ]; }; }