{ config, lib, ... }: let cfg = config.vhack.mail; in { options.vhack.mail = { enable = lib.mkEnableOption "sophisticated mail setup with simple-nixos-mailserver"; fqdn = lib.mkOption { type = lib.types.str; description = "The fqdn mailserver should be served on."; }; domains = lib.mkOption { type = lib.types.listOf lib.types.str; example = ["example.com"]; default = []; description = "The domains that this mail server serves."; }; accounts = lib.mkOption { type = lib.types.attrsOf ( lib.types.submodule ( {name, ...}: { options = { name = lib.mkOption { type = lib.types.str; default = name; example = "user1@example.com"; readOnly = true; internal = true; description = '' The login username for this account. ''; }; hashedPassword = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; example = "$y$j9T$vfGrwkAaXCjCEWtVNMQck1$383uIXQmn2z0hnmVAA8kwFQmjNj78.nYbvWeyNLIaP1"; description = '' The hashed login password for this account. Use `mkpasswd` to create password hashes: ``` nix-shell -p mkpasswd --run 'mkpasswd -s' ``` :::{note} This is a convenience option, when your threat model allows storing hashed secrets in the world-readable Nix store. Passing the hash through {option}`mailserver.accounts..hashedPasswordFile` allows relying on filesystem discretionary access control as another security boundary. ::: ''; }; hashedPasswordFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; example = "/run/keys/user1-pw-hash"; description = '' The hashed login password for this account read from a file. Use `mkpasswd to create password hashes: ``` nix-shell -p mkpasswd --run 'mkpasswd -s' ``` ''; }; passwordFile = lib.mkOption { type = with lib.types; nullOr (pathWith { inStore = false; }); default = null; example = "/run/keys/user1-pw"; description = '' The plaintext login password for this account read from a file. :::{note} The password is hashed before it is passed on to Dovecot. ::: ''; }; aliases = lib.mkOption { type = with lib.types; listOf types.str; example = [ "abuse@example.com" "postmaster@example.com" ]; default = []; description = '' List of additional mail addresses (aliases) that get routed to this account. :::{admonition} Catch-all with sending permissions :class: tip Configure `@example.com` to create a catch-all for this domain that also allows sending from all addresses. ::: ''; }; aliasesRegexp = lib.mkOption { type = with lib.types; listOf types.str; example = [''/^tom\..*@domain\.com$/'']; default = []; description = '' Same as {option}`mailserver.accounts..aliases` but using PCRE (Perl compatible regex). ''; }; catchAll = lib.mkOption { type = with lib.types; listOf (enum cfg.domains); example = [ "example.com" "example2.com" ]; default = []; description = '' For which domains should this account act as a catch all? :::{warning} Does not allow sending from all addresses of these domains. Use {option}`mailserver.accounts..aliases` if that is required. ::: ''; }; quota = lib.mkOption { type = with lib.types; nullOr types.str; default = null; example = "2G"; description = '' The quota limit for this user. The value is must use a size format like `500M`, `2G`, `10G`. If unset, will fall back to {option}`mailserver.quota.defaults.perUser` if set. ''; }; sieveScript = lib.mkOption { type = with lib.types; nullOr lines; default = null; example = '' require ["fileinto", "mailbox"]; if address :is "from" "gitlab@mg.gitlab.com" { fileinto :create "GitLab"; stop; } # This must be the last rule, it will check if list-id is set, and # file the message into the Lists folder for further investigation elsif header :matches "list-id" "" { fileinto :create "Lists"; stop; } ''; description = '' Per-user sieve script. ''; }; sendOnly = lib.mkOption { type = lib.types.bool; default = false; description = '' Specifies if the account should be a send-only account. Emails sent to send-only accounts will be rejected with the reason configured in {option}`mailserver.accounts..sendOnlyRejectMessage`. ''; }; sendOnlyRejectMessage = lib.mkOption { type = lib.types.str; default = "This account cannot receive emails."; description = '' The message returned to the sender for a send-only account. See {option}`mailserver.accounts..sendOnly`. ''; }; }; } ) ); example = lib.literalExpression '' { user1 = { # This password hash leaks into the Nix store hashedPassword = "$y$j9T$y6eZ1o.IvVNfdGMAsUEvh1$6K/llP52uw2iDh4iSwtAn54/JYy7FzCcoCHmjmx00H5"; }; user2 = { # Hashed password passed as a file hashedPasswordFile = "/run/keys/user2-pw-hash"; }; user3 = { # Plaintext password file passwordFile = "/run/keys/user3-pw"; }; } ''; description = '' Attribute set of mail accounts. Each entry defines a mailbox and login credentials, where the attribute name is used as the login username and optionally routed mail address. Use `mkpasswd` to generate password hashes. ''; default = {}; }; security = lib.mkOption { type = lib.types.submodule { options = { dkimKeys = lib.mkOption { type = lib.types.nullOr (lib.types.attrsOf (lib.types.submodule { options = { dkimPublicKey = lib.mkOption { type = lib.types.str; description = '' The base 64 encoded representation of the public dkim key. ''; }; dkimPrivateKeyPath = lib.mkOption { type = lib.types.path; description = '' Path to the file for the pre-generated dkim key file. Should not point to the nixos store. ''; }; keyAlgorithm = lib.mkOption { type = lib.types.enum ["ed25519-sha256" "rsa-sha-256" "rsa-sha-1"]; description = "The algorithm of the used key"; }; }; })); description = '' Which key to use for which domain. The attr keys are the domains ''; default = null; }; }; }; description = '' Security options. ''; }; }; config = lib.mkIf cfg.enable { vhack.persist.directories = [ { directory = "/var/lib/mail/backup"; user = "virtualMail"; group = "virtualMail"; mode = "0700"; } { directory = "/var/lib/mail/vmail"; user = "virtualMail"; group = "virtualMail"; mode = "0700"; } { directory = "/var/lib/mail/dkim"; user = "opendkim"; group = "opendkim"; mode = "0700"; } { directory = "/var/lib/postfix/data"; user = "postfix"; group = "postfix"; mode = "0700"; } { directory = "/var/lib/postfix/queue"; user = "postfix"; group = "postfix"; mode = "0700"; } { directory = "/var/lib/rspamd"; user = "rspamd"; group = "rspamd"; mode = "0700"; } ]; vhack.nginx.enable = true; services.nginx.virtualHosts = { "${cfg.fqdn}" = { enableACME = true; forceSSL = true; }; }; mailserver = { enable = true; inherit (cfg) fqdn; stateVersion = 5; storage = { directoryLayout = "fs"; path = "/var/lib/mail/vmail"; }; dkim = let dkimKeys = builtins.mapAttrs (name: value: { selectors."mail".keyFile = value.dkimPrivateKeyPath; }) cfg.security.dkimKeys; in { # domains."${cfg.fqdn}".selectors."mail" = lib.mkIf (cfg.dkimKey != null) { # keyFile = cfg.dkimKey; # }; domains = lib.mkIf (cfg.security.dkimKeys != null) dkimKeys; keyDirectory = lib.mkIf (cfg.security.dkimKeys == null) "/var/lib/mail/dkim"; }; backup.snapshotRoot = "/var/lib/mail/backup"; enableImap = false; enableImapSsl = true; enablePop3 = false; enablePop3Ssl = true; # SMTP enableSubmission = false; enableSubmissionSsl = true; openFirewall = true; # Reference the existing ACME configuration created by nginx x509.useACMEHost = cfg.fqdn; inherit (cfg) domains accounts; }; users = { users = { knot-resolver = { uid = config.vhack.constants.ids.uids.knot-resolver; group = "knot-resolver"; }; redis-rspamd.uid = config.vhack.constants.ids.uids.redis-rspamd; rspamd.uid = config.vhack.constants.ids.uids.rspamd; postfix-tlspol.uid = config.vhack.constants.ids.uids.postfix-tlspol; }; groups = { knot-resolver.gid = lib.mkForce config.vhack.constants.ids.gids.knot-resolver; redis-rspamd.gid = config.vhack.constants.ids.gids.redis-rspamd; rspamd.gid = config.vhack.constants.ids.gids.rspamd; postfix-tlspol.gid = config.vhack.constants.ids.gids.postfix-tlspol; }; }; }; }