{ 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. adminPassword = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."admin-password".path); boardPassword = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."board-member-password".path); cloudflareUsername = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."cloudflare-username".path); cloudflareToken = builtins.replaceStrings ["\n"] [""] (builtins.readFile config.sops.secrets."cloudflare-dns-token".path); 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/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"; bind = "[::]:25"; proxy.trusted-networks = [ "10.1.1.250/32" ]; }; submissions = { bind = "[::]:465"; protocol = "smtp"; tls.implicit = true; # Also trust proxy for SMTPS proxy.trusted-networks = ["10.1.1.250/32"]; }; imaps = { bind = "[::]:993"; protocol = "imap"; tls.implicit = true; proxy.trusted-networks = ["10.1.1.250/32"]; }; 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" "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"]; } ]; }; authentication.fallback-admin = { user = "admin"; secret = "%{file:/etc/stalwart/admin-pw}%"; }; }; }; }; }