From 1a7f3d718c5a6406b7d5b54f10f5c9c69ed90ef9 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 15 Jun 2025 03:39:44 +0200 Subject: [PATCH] treewide: reformat with nixfmt-rfc-style --- .hydra/declarative-jobsets.nix | 32 +- default.nix | 432 ++++++++++++++---------- flake.nix | 348 +++++++++++--------- mail-server/borgbackup.nix | 41 ++- mail-server/common.nix | 95 +++--- mail-server/dovecot.nix | 580 ++++++++++++++++++--------------- mail-server/environment.nix | 25 +- mail-server/kresd.nix | 1 - mail-server/networking.nix | 27 +- mail-server/nginx.nix | 40 ++- mail-server/postfix.nix | 476 +++++++++++++++------------ mail-server/rsnapshot.nix | 10 +- mail-server/rspamd.nix | 406 ++++++++++++----------- mail-server/systemd.nix | 115 ++++--- mail-server/users.nix | 71 ++-- shell.nix | 19 +- tests/clamav.nix | 239 +++++++------- tests/external.nix | 459 +++++++++++++------------- tests/internal.nix | 104 +++--- tests/ldap.nix | 183 ++++++----- tests/multiple.nix | 63 ++-- 21 files changed, 2086 insertions(+), 1680 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 7b99844..6877235 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -1,22 +1,21 @@ { nixpkgs, pulls, ... }: let - pkgs = import nixpkgs {}; + pkgs = import nixpkgs { }; prs = builtins.fromJSON (builtins.readFile pulls); - prJobsets = pkgs.lib.mapAttrs (num: info: - { enabled = 1; - hidden = false; - description = "PR ${num}: ${info.title}"; - checkinterval = 300; - schedulingshares = 20; - enableemail = false; - emailoverride = ""; - keepnr = 1; - type = 1; - flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head"; - } - ) prs; + prJobsets = pkgs.lib.mapAttrs (num: info: { + enabled = 1; + hidden = false; + description = "PR ${num}: ${info.title}"; + checkinterval = 300; + schedulingshares = 20; + enableemail = false; + emailoverride = ""; + keepnr = 1; + type = 1; + flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head"; + }) prs; mkFlakeJobset = branch: { description = "Build ${branch} branch of Simple NixOS MailServer"; checkinterval = 300; @@ -41,8 +40,9 @@ let jobsets = desc; }; -in { - jobsets = pkgs.runCommand "spec-jobsets.json" {} '' +in +{ + jobsets = pkgs.runCommand "spec-jobsets.json" { } '' cat >$out < -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: with lib; @@ -56,14 +61,17 @@ in domains = mkOption { type = types.listOf types.str; example = [ "example.com" ]; - default = []; + default = [ ]; description = "The domains that this mail server serves."; }; certificateDomains = mkOption { type = types.listOf types.str; - example = [ "imap.example.com" "pop3.example.com" ]; - default = []; + example = [ + "imap.example.com" + "pop3.example.com" + ]; + default = [ ]; description = '' ({option}`mailserver.certificateScheme` == `acme-nginx`) @@ -79,130 +87,141 @@ in }; loginAccounts = mkOption { - type = types.attrsOf (types.submodule ({ name, ... }: { - options = { - name = mkOption { - type = types.str; - example = "user1@example.com"; - description = "Username"; - }; + type = types.attrsOf ( + types.submodule ( + { name, ... }: + { + options = { + name = mkOption { + type = types.str; + example = "user1@example.com"; + description = "Username"; + }; - hashedPassword = mkOption { - type = with types; nullOr str; - default = null; - example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; - description = '' - The user's hashed password. Use `mkpasswd` as follows + hashedPassword = mkOption { + type = with types; nullOr str; + default = null; + example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; + description = '' + The user's hashed password. Use `mkpasswd` as follows - ``` - nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' - ``` + ``` + nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' + ``` - Warning: this is stored in plaintext in the Nix store! - Use {option}`mailserver.loginAccounts..hashedPasswordFile` instead. - ''; - }; + Warning: this is stored in plaintext in the Nix store! + Use {option}`mailserver.loginAccounts..hashedPasswordFile` instead. + ''; + }; - hashedPasswordFile = mkOption { - type = with types; nullOr path; - default = null; - example = "/run/keys/user1-passwordhash"; - description = '' - A file containing the user's hashed password. Use `mkpasswd` as follows + hashedPasswordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/user1-passwordhash"; + description = '' + A file containing the user's hashed password. Use `mkpasswd` as follows - ``` - nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' - ``` - ''; - }; + ``` + nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' + ``` + ''; + }; - aliases = mkOption { - type = with types; listOf types.str; - example = ["abuse@example.com" "postmaster@example.com"]; - default = []; - description = '' - A list of aliases of this login account. - Note: Use list entries like "@example.com" to create a catchAll - that allows sending from all email addresses in these domain. - ''; - }; + aliases = mkOption { + type = with types; listOf types.str; + example = [ + "abuse@example.com" + "postmaster@example.com" + ]; + default = [ ]; + description = '' + A list of aliases of this login account. + Note: Use list entries like "@example.com" to create a catchAll + that allows sending from all email addresses in these domain. + ''; + }; - aliasesRegexp = mkOption { - type = with types; listOf types.str; - example = [''/^tom\..*@domain\.com$/'']; - default = []; - description = '' - Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex). - ''; - }; + aliasesRegexp = mkOption { + type = with types; listOf types.str; + example = [ ''/^tom\..*@domain\.com$/'' ]; + default = [ ]; + description = '' + Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex). + ''; + }; - catchAll = mkOption { - type = with types; listOf (enum cfg.domains); - example = ["example.com" "example2.com"]; - default = []; - description = '' - For which domains should this account act as a catch all? - Note: Does not allow sending from all addresses of these domains. - ''; - }; + catchAll = mkOption { + type = with types; listOf (enum cfg.domains); + example = [ + "example.com" + "example2.com" + ]; + default = [ ]; + description = '' + For which domains should this account act as a catch all? + Note: Does not allow sending from all addresses of these domains. + ''; + }; - quota = mkOption { - type = with types; nullOr types.str; - default = null; - example = "2G"; - description = '' - Per user quota rules. Accepted sizes are `xx k/M/G/T` with the - obvious meaning. Leave blank for the standard quota `100G`. - ''; - }; + quota = mkOption { + type = with types; nullOr types.str; + default = null; + example = "2G"; + description = '' + Per user quota rules. Accepted sizes are `xx k/M/G/T` with the + obvious meaning. Leave blank for the standard quota `100G`. + ''; + }; - sieveScript = mkOption { - type = with types; nullOr lines; - default = null; - example = '' - require ["fileinto", "mailbox"]; + sieveScript = mkOption { + type = with types; nullOr lines; + default = null; + example = '' + require ["fileinto", "mailbox"]; - if address :is "from" "gitlab@mg.gitlab.com" { - fileinto :create "GitLab"; - stop; - } + 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. - ''; - }; + # 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 = mkOption { - type = types.bool; - default = false; - description = '' - Specifies if the account should be a send-only account. - Emails sent to send-only accounts will be rejected from - unauthorized senders with the `sendOnlyRejectMessage` - stating the reason. - ''; - }; + sendOnly = mkOption { + type = types.bool; + default = false; + description = '' + Specifies if the account should be a send-only account. + Emails sent to send-only accounts will be rejected from + unauthorized senders with the `sendOnlyRejectMessage` + stating the reason. + ''; + }; - sendOnlyRejectMessage = mkOption { - type = types.str; - default = "This account cannot receive emails."; - description = '' - The message that will be returned to the sender when an email is - sent to a send-only account. Only used if the account is marked - as send-only. - ''; - }; - }; + sendOnlyRejectMessage = mkOption { + type = types.str; + default = "This account cannot receive emails."; + description = '' + The message that will be returned to the sender when an email is + sent to a send-only account. Only used if the account is marked + as send-only. + ''; + }; + }; - config.name = mkDefault name; - })); + config.name = mkDefault name; + } + ) + ); example = { user1 = { hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; @@ -220,13 +239,13 @@ in nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` ''; - default = {}; + default = { }; }; ldap = { enable = mkEnableOption "LDAP support"; - uris = mkOption { + uris = mkOption { type = types.listOf types.str; example = literalExpression '' [ @@ -284,7 +303,11 @@ in }; searchScope = mkOption { - type = types.enum [ "sub" "base" "one" ]; + type = types.enum [ + "sub" + "base" + "one" + ]; default = "sub"; description = '' Search scope below which users accounts are looked for. @@ -419,14 +442,22 @@ in autoIndexExclude = mkOption { type = types.listOf types.str; default = [ ]; - example = [ "\\Trash" "SomeFolder" "Other/*" ]; + example = [ + "\\Trash" + "SomeFolder" + "Other/*" + ]; description = '' Mailboxes to exclude from automatic indexing. ''; }; enforced = mkOption { - type = types.enum [ "yes" "no" "body" ]; + type = types.enum [ + "yes" + "no" + "body" + ]; default = "no"; description = '' Fail searches when no index is available. If set to @@ -439,7 +470,10 @@ in languages = mkOption { type = types.nonEmptyListOf types.str; default = [ "en" ]; - example = [ "en" "de" ]; + example = [ + "en" + "de" + ]; description = '' A list of languages that the full text search should detect. At least one language must be specified. @@ -488,7 +522,10 @@ in }; lmtpSaveToDetailMailbox = mkOption { - type = types.enum ["yes" "no"]; + type = types.enum [ + "yes" + "no" + ]; default = "yes"; description = '' If an email address is delimited by a "+", should it be filed into a @@ -514,17 +551,23 @@ in }; extraVirtualAliases = mkOption { - type = let - loginAccount = mkOptionType { - name = "Login Account"; - check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts); - }; - in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount)); + type = + let + loginAccount = mkOptionType { + name = "Login Account"; + check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts); + }; + in + with types; + attrsOf (either loginAccount (nonEmptyListOf loginAccount)); example = { "info@example.com" = "user1@example.com"; "postmaster@example.com" = "user1@example.com"; "abuse@example.com" = "user1@example.com"; - "multi@example.com" = [ "user1@example.com" "user2@example.com" ]; + "multi@example.com" = [ + "user1@example.com" + "user2@example.com" + ]; }; description = '' Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that @@ -537,7 +580,7 @@ in example all mails for `multi@example.com` will be forwarded to both `user1@example.com` and `user2@example.com`. ''; - default = {}; + default = { }; }; forwards = mkOption { @@ -554,28 +597,34 @@ in can't send mail as `user@example.com`. Also, this option allows to forward mails to external addresses. ''; - default = {}; + default = { }; }; rejectSender = mkOption { type = types.listOf types.str; - example = [ "example.com" "spammer@example.net" ]; + example = [ + "example.com" + "spammer@example.net" + ]; description = '' Reject emails from these addresses from unauthorized senders. Use if a spammer is using the same domain or the same sender over and over. ''; - default = []; + default = [ ]; }; rejectRecipients = mkOption { type = types.listOf types.str; - example = [ "sales@example.com" "info@example.com" ]; + example = [ + "sales@example.com" + "info@example.com" + ]; description = '' Reject emails addressed to these local addresses from unauthorized senders. Use if a spammer has found email addresses in a catchall domain but you do not want to disable the catchall. ''; - default = []; + default = [ ]; }; vmailUID = mkOption { @@ -673,28 +722,46 @@ in }; }; - certificateScheme = let - schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ]; - translate = i: warn "Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${(builtins.elemAt schemes (i - 1))}\"'." - (builtins.elemAt schemes (i - 1)); - in mkOption { - type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes); - default = "selfsigned"; - description = '' - The scheme to use for managing TLS certificates: + certificateScheme = + let + schemes = [ + "manual" + "selfsigned" + "acme-nginx" + "acme" + ]; + translate = + i: + warn + "Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${ + (builtins.elemAt schemes (i - 1)) + }\"'." + (builtins.elemAt schemes (i - 1)); + in + mkOption { + type = + with types; + coercedTo (enum [ + 1 + 2 + 3 + ]) translate (enum schemes); + default = "selfsigned"; + description = '' + The scheme to use for managing TLS certificates: - 1. `manual`: you specify locations via {option}`mailserver.certificateFile` and - {option}`mailserver.keyFile` and manually copy certificates there. - 2. `selfsigned`: you let the server create new (self-signed) certificates on the fly. - 3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org) - via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for - {option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly - configured to point to your server (see the [setup guide](setup-guide.rst) for more information). - 4. `acme`: you already have an ACME certificate set up (for example, you're already running a TLS-enabled - Nginx server on the FQDN). This is better than `manual` because the appropriate services will be reloaded - when the certificate is renewed. - ''; - }; + 1. `manual`: you specify locations via {option}`mailserver.certificateFile` and + {option}`mailserver.keyFile` and manually copy certificates there. + 2. `selfsigned`: you let the server create new (self-signed) certificates on the fly. + 3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org) + via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for + {option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly + configured to point to your server (see the [setup guide](setup-guide.rst) for more information). + 4. `acme`: you already have an ACME certificate set up (for example, you're already running a TLS-enabled + Nginx server on the FQDN). This is better than `manual` because the appropriate services will be reloaded + when the certificate is renewed. + ''; + }; certificateFile = mkOption { type = types.path; @@ -851,7 +918,10 @@ in }; dkimKeyType = mkOption { - type = types.enum [ "rsa" "ed25519" ]; + type = types.enum [ + "rsa" + "ed25519" + ]; default = "rsa"; description = '' The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018). @@ -864,16 +934,16 @@ in }; dkimKeyBits = mkOption { - type = types.int; - default = 1024; - description = '' - How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys. + type = types.int; + default = 1024; + description = '' + How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys. - If you have already deployed a key with a different number of bits than specified - here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get - this package to generate a key with the new number of bits, you will either have to - change the selector or delete the old key file. - ''; + If you have already deployed a key with a different number of bits than specified + here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get + this package to generate a key with the new number of bits, you will either have to + change the selector or delete the old key file. + ''; }; dmarcReporting = { @@ -938,7 +1008,7 @@ in excludeDomains = mkOption { type = types.listOf types.str; - default = []; + default = [ ]; description = '' List of domains or eSLDs to be excluded from DMARC reports. ''; @@ -1150,7 +1220,15 @@ in compression = { method = mkOption { - type = types.nullOr (types.enum ["none" "lz4" "zstd" "zlib" "lzma"]); + type = types.nullOr ( + types.enum [ + "none" + "lz4" + "zstd" + "zlib" + "lzma" + ] + ); default = null; description = "Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4."; }; @@ -1208,14 +1286,14 @@ in locations = mkOption { type = types.listOf types.path; - default = [cfg.mailDirectory]; + default = [ cfg.mailDirectory ]; defaultText = lib.literalExpression "[ config.mailserver.mailDirectory ]"; description = "The locations that are to be backed up by borg."; }; extraArgumentsForInit = mkOption { type = types.listOf types.str; - default = ["--critical"]; + default = [ "--critical" ]; description = "Additional arguments to add to the borg init command line."; }; @@ -1295,9 +1373,9 @@ in cronIntervals = mkOption { type = types.attrsOf types.str; default = { - # minute, hour, day-in-month, month, weekday (0 = sunday) + # minute, hour, day-in-month, month, weekday (0 = sunday) hourly = " 0 * * * *"; # Every full hour - daily = "30 3 * * *"; # Every day at 3:30 + daily = "30 3 * * *"; # Every day at 3:30 weekly = " 0 5 * * 0"; # Every sunday at 5:00 AM }; description = '' @@ -1311,29 +1389,29 @@ in imports = [ (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] '' - This option is not needed for fts-flatcurve + This option is not needed for fts-flatcurve '') (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] '' - This option is not needed for fts-flatcurve + This option is not needed for fts-flatcurve '') (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] '' - This option is not needed for fts-flatcurve + This option is not needed for fts-flatcurve '') (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] '' - This option is not supported by fts-flatcurve + This option is not supported by fts-flatcurve '') (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] '' - This option is not needed since fts-xapian 1.8.3 + This option is not needed since fts-xapian 1.8.3 '') (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] '' - Text attachments are always indexed since fts-xapian 1.4.8 + Text attachments are always indexed since fts-xapian 1.4.8 '') (lib.mkRenamedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "enable" ] [ "system" "autoUpgrade" "allowReboot" ] ) (lib.mkRemovedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "method" ] '' - Use `system.autoUpgrade` instead. + Use `system.autoUpgrade` instead. '') ./mail-server/assertions.nix ./mail-server/borgbackup.nix diff --git a/flake.nix b/flake.nix index 641a858..18757c5 100644 --- a/flake.nix +++ b/flake.nix @@ -20,177 +20,205 @@ }; }; - outputs = { self, blobs, git-hooks, nixpkgs, nixpkgs-25_05, ... }: let - lib = nixpkgs.lib; - system = "x86_64-linux"; - pkgs = nixpkgs.legacyPackages.${system}; - releases = [ - { - name = "unstable"; - nixpkgs = nixpkgs; - pkgs = nixpkgs.legacyPackages.${system}; - } - { - name = "25.05"; - nixpkgs = nixpkgs-25_05; - pkgs = nixpkgs-25_05.legacyPackages.${system}; - } - ]; - testNames = [ - "clamav" - "external" - "internal" - "ldap" - "multiple" - ]; + outputs = + { + self, + blobs, + git-hooks, + nixpkgs, + nixpkgs-25_05, + ... + }: + let + lib = nixpkgs.lib; + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + releases = [ + { + name = "unstable"; + nixpkgs = nixpkgs; + pkgs = nixpkgs.legacyPackages.${system}; + } + { + name = "25.05"; + nixpkgs = nixpkgs-25_05; + pkgs = nixpkgs-25_05.legacyPackages.${system}; + } + ]; + testNames = [ + "clamav" + "external" + "internal" + "ldap" + "multiple" + ]; - genTest = testName: release: let - pkgs = release.pkgs; - nixos-lib = import (release.nixpkgs + "/nixos/lib") { - inherit (pkgs) lib; - }; - in { - name = "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}"; - value = nixos-lib.runTest { - hostPkgs = pkgs; - imports = [ ./tests/${testName}.nix ]; - _module.args = { inherit blobs; }; - extraBaseModules.imports = [ ./default.nix ]; - }; - }; - - # Generate an attribute set such as - # { - # external-unstable = ; - # external-21_05 = ; - # ... - # } - allTests = lib.listToAttrs ( - lib.flatten (map (t: map (r: genTest t r) releases) testNames)); - - mailserverModule = import ./.; - - # Generate a MarkDown file describing the options of the NixOS mailserver module - optionsDoc = let - eval = lib.evalModules { - modules = [ - mailserverModule - { - _module.check = false; - mailserver = { - fqdn = "mx.example.com"; - domains = [ - "example.com" - ]; - dmarcReporting = { - organizationName = "Example Corp"; - domain = "example.com"; - }; - }; - } - ]; - }; - options = builtins.toFile "options.json" (builtins.toJSON - (lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") - (lib.optionAttrSetToDocList eval.options))); - in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } '' - echo "Generating options.md from ${options}" - python ${./scripts/generate-options.py} ${options} > $out - echo $out - ''; - - documentation = pkgs.stdenv.mkDerivation { - name = "documentation"; - src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"]; - buildInputs = [( - pkgs.python3.withPackages (p: with p; [ - sphinx - sphinx_rtd_theme - myst-parser - linkify-it-py - ]) - )]; - buildPhase = '' - cp ${optionsDoc} options.md - # Workaround for https://github.com/sphinx-doc/sphinx/issues/3451 - unset SOURCE_DATE_EPOCH - make html - ''; - installPhase = '' - cp -Tr _build/html $out - ''; - }; - - in { - nixosModules = rec { - mailserver = mailserverModule; - default = mailserver; - }; - nixosModule = self.nixosModules.default; # compatibility - hydraJobs.${system} = allTests // { - inherit documentation; - inherit (self.checks.${system}) pre-commit; - }; - checks.${system} = allTests // { - pre-commit = git-hooks.lib.${system}.run { - src = ./.; - hooks = { - # docs - markdownlint = { - enable = true; - settings.configuration = { - # Max line length, doesn't seem to correclty account for lines containing links - # https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md - MD013 = false; - }; + genTest = + testName: release: + let + pkgs = release.pkgs; + nixos-lib = import (release.nixpkgs + "/nixos/lib") { + inherit (pkgs) lib; }; - rstcheck = { - enable = true; - package = pkgs.rstcheckWithSphinx; - entry = lib.getExe pkgs.rstcheckWithSphinx; - files = "\\.rst$"; + in + { + name = "${testName}-${builtins.replaceStrings [ "." ] [ "_" ] release.name}"; + value = nixos-lib.runTest { + hostPkgs = pkgs; + imports = [ ./tests/${testName}.nix ]; + _module.args = { inherit blobs; }; + extraBaseModules.imports = [ ./default.nix ]; }; + }; - # nix - deadnix.enable = true; - nixfmt-rfc-style.enable = true; + # Generate an attribute set such as + # { + # external-unstable = ; + # external-21_05 = ; + # ... + # } + allTests = lib.listToAttrs (lib.flatten (map (t: map (r: genTest t r) releases) testNames)); - # python - pyright.enable = true; - ruff = { - enable = true; - args = [ - "--extend-select" - "I" + mailserverModule = import ./.; + + # Generate a MarkDown file describing the options of the NixOS mailserver module + optionsDoc = + let + eval = lib.evalModules { + modules = [ + mailserverModule + { + _module.check = false; + mailserver = { + fqdn = "mx.example.com"; + domains = [ + "example.com" + ]; + dmarcReporting = { + organizationName = "Example Corp"; + domain = "example.com"; + }; + }; + } ]; }; - ruff-format.enable = true; + options = builtins.toFile "options.json" ( + builtins.toJSON ( + lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") ( + lib.optionAttrSetToDocList eval.options + ) + ) + ); + in + pkgs.runCommand "options.md" { buildInputs = [ pkgs.python3Minimal ]; } '' + echo "Generating options.md from ${options}" + python ${./scripts/generate-options.py} ${options} > $out + echo $out + ''; - # scripts - shellcheck.enable = true; + documentation = pkgs.stdenv.mkDerivation { + name = "documentation"; + src = lib.sourceByRegex ./docs [ + "logo\\.png" + "conf\\.py" + "Makefile" + ".*\\.rst" + ]; + buildInputs = [ + (pkgs.python3.withPackages ( + p: with p; [ + sphinx + sphinx_rtd_theme + myst-parser + linkify-it-py + ] + )) + ]; + buildPhase = '' + cp ${optionsDoc} options.md + # Workaround for https://github.com/sphinx-doc/sphinx/issues/3451 + unset SOURCE_DATE_EPOCH + make html + ''; + installPhase = '' + cp -Tr _build/html $out + ''; + }; - # sieve - check-sieve = { - enable = true; - package = pkgs.check-sieve; - entry = lib.getExe pkgs.check-sieve; - files = "\\.sieve$"; + in + { + nixosModules = rec { + mailserver = mailserverModule; + default = mailserver; + }; + nixosModule = self.nixosModules.default; # compatibility + hydraJobs.${system} = allTests // { + inherit documentation; + inherit (self.checks.${system}) pre-commit; + }; + checks.${system} = allTests // { + pre-commit = git-hooks.lib.${system}.run { + src = ./.; + hooks = { + # docs + markdownlint = { + enable = true; + settings.configuration = { + # Max line length, doesn't seem to correclty account for lines containing links + # https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md + MD013 = false; + }; + }; + rstcheck = { + enable = true; + package = pkgs.rstcheckWithSphinx; + entry = lib.getExe pkgs.rstcheckWithSphinx; + files = "\\.rst$"; + }; + + # nix + deadnix.enable = true; + nixfmt-rfc-style.enable = true; + + # python + pyright.enable = true; + ruff = { + enable = true; + args = [ + "--extend-select" + "I" + ]; + }; + ruff-format.enable = true; + + # scripts + shellcheck.enable = true; + + # sieve + check-sieve = { + enable = true; + package = pkgs.check-sieve; + entry = lib.getExe pkgs.check-sieve; + files = "\\.sieve$"; + }; }; }; }; - }; - packages.${system} = { - inherit optionsDoc documentation; - }; - devShells.${system}.default = pkgs.mkShellNoCC { - inputsFrom = [ documentation ]; - packages = with pkgs; [ - glab - ] ++ self.checks.${system}.pre-commit.enabledPackages; - shellHook = self.checks.${system}.pre-commit.shellHook; - }; - devShell.${system} = self.devShells.${system}.default; # compatibility + packages.${system} = { + inherit optionsDoc documentation; + }; + devShells.${system}.default = pkgs.mkShellNoCC { + inputsFrom = [ documentation ]; + packages = + with pkgs; + [ + glab + ] + ++ self.checks.${system}.pre-commit.enabledPackages; + shellHook = self.checks.${system}.pre-commit.shellHook; + }; + devShell.${system} = self.devShells.${system}.default; # compatibility - formatter.${system} = pkgs.nixfmt-tree; - }; + formatter.${system} = pkgs.nixfmt-tree; + }; } diff --git a/mail-server/borgbackup.nix b/mail-server/borgbackup.nix index ef83b0d..51ae986 100644 --- a/mail-server/borgbackup.nix +++ b/mail-server/borgbackup.nix @@ -14,28 +14,44 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.mailserver.borgbackup; methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method; autoFragment = - if cfg.compression.auto && cfg.compression.method == null - then throw "compression.method must be set when using auto." - else lib.optional cfg.compression.auto "auto"; + if cfg.compression.auto && cfg.compression.method == null then + throw "compression.method must be set when using auto." + else + lib.optional cfg.compression.auto "auto"; levelFragment = - if cfg.compression.level != null && cfg.compression.method == null - then throw "compression.method must be set when using compression.level." - else lib.optional (cfg.compression.level != null) (toString cfg.compression.level); - compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]); + if cfg.compression.level != null && cfg.compression.method == null then + throw "compression.method must be set when using compression.level." + else + lib.optional (cfg.compression.level != null) (toString cfg.compression.level); + compressionFragment = lib.concatStringsSep "," ( + lib.flatten [ + autoFragment + methodFragment + levelFragment + ] + ); compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}"; encryptionFragment = cfg.encryption.method; passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile; - passphraseFragment = lib.optionalString (cfg.encryption.method != "none") - (if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"'' - else throw "passphraseFile must be set when using encryption."); + passphraseFragment = lib.optionalString (cfg.encryption.method != "none") ( + if cfg.encryption.passphraseFile != null then + ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"'' + else + throw "passphraseFile must be set when using encryption." + ); locations = lib.escapeShellArgs cfg.locations; name = lib.escapeShellArg cfg.name; @@ -55,7 +71,8 @@ let ${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations} ${cmdPostexec} ''; -in { +in +{ config = lib.mkIf (config.mailserver.enable && cfg.enable) { environment.systemPackages = with pkgs; [ borgbackup diff --git a/mail-server/common.nix b/mail-server/common.nix index 813a5f4..cb044b6 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -14,57 +14,76 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib }: +{ + config, + pkgs, + lib, +}: let cfg = config.mailserver; in { # cert :: PATH - certificatePath = if cfg.certificateScheme == "manual" - then cfg.certificateFile - else if cfg.certificateScheme == "selfsigned" - then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" - else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" - then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem" - else throw "unknown certificate scheme"; + certificatePath = + if cfg.certificateScheme == "manual" then + cfg.certificateFile + else if cfg.certificateScheme == "selfsigned" then + "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" + else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then + "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem" + else + throw "unknown certificate scheme"; # key :: PATH - keyPath = if cfg.certificateScheme == "manual" - then cfg.keyFile - else if cfg.certificateScheme == "selfsigned" - then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" - else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" - then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem" - else throw "unknown certificate scheme"; + keyPath = + if cfg.certificateScheme == "manual" then + cfg.keyFile + else if cfg.certificateScheme == "selfsigned" then + "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" + else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then + "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem" + else + throw "unknown certificate scheme"; - passwordFiles = let - mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; - in - lib.mapAttrs (name: value: - if value.hashedPasswordFile == null then - builtins.toString (mkHashFile name value.hashedPassword) - else value.hashedPasswordFile) cfg.loginAccounts; + passwordFiles = + let + mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; + in + lib.mapAttrs ( + name: value: + if value.hashedPasswordFile == null then + builtins.toString (mkHashFile name value.hashedPassword) + else + value.hashedPasswordFile + ) cfg.loginAccounts; # Appends the LDAP bind password to files to avoid writing this # password into the Nix store. - appendLdapBindPwd = { - name, file, prefix, suffix ? "", passwordFile, destination - }: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' - #!${pkgs.stdenv.shell} - set -euo pipefail + appendLdapBindPwd = + { + name, + file, + prefix, + suffix ? "", + passwordFile, + destination, + }: + pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' + #!${pkgs.stdenv.shell} + set -euo pipefail - baseDir=$(dirname ${destination}) - if (! test -d "$baseDir"); then - mkdir -p $baseDir - chmod 755 $baseDir - fi + baseDir=$(dirname ${destination}) + if (! test -d "$baseDir"); then + mkdir -p $baseDir + chmod 755 $baseDir + fi - cat ${file} > ${destination} - echo -n '${prefix}' >> ${destination} - cat ${passwordFile} | tr -d '\n' >> ${destination} - echo -n '${suffix}' >> ${destination} - chmod 600 ${destination} - ''; + cat ${file} > ${destination} + echo -n '${prefix}' >> ${destination} + cat ${passwordFile} | tr -d '\n' >> ${destination} + echo -n '${suffix}' >> ${destination} + chmod 600 ${destination} + ''; } diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index c6e5587..148befc 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -14,7 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: with (import ./common.nix { inherit config pkgs lib; }); @@ -28,10 +33,14 @@ let ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext"; boolToYesNo = x: if x then "yes" else "no"; listToLine = lib.concatStringsSep " "; - listToMultiAttrs = keyPrefix: attrs: lib.listToAttrs (lib.imap1 (n: x: { - name = "${keyPrefix}${if n==1 then "" else toString n}"; - value = x; - }) attrs); + listToMultiAttrs = + keyPrefix: attrs: + lib.listToAttrs ( + lib.imap1 (n: x: { + name = "${keyPrefix}${if n == 1 then "" else toString n}"; + value = x; + }) attrs + ); maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8"; @@ -39,9 +48,7 @@ let # maildir in format "/${domain}/${user}" dovecotMaildir = "maildir:${cfg.mailDirectory}/%{domain}/%{username}${maildirLayoutAppendix}${maildirUTF8FolderNames}" - + (lib.optionalString (cfg.indexDir != null) - ":INDEX=${cfg.indexDir}/%{domain}/%{username}" - ); + + (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}"); postfixCfg = config.services.postfix; @@ -51,7 +58,7 @@ let ldap_version = 3 uris = ${lib.concatStringsSep " " cfg.ldap.uris} ${lib.optionalString cfg.ldap.startTls '' - tls = yes + tls = yes ''} tls_require_cert = hard tls_ca_cert_file = ${cfg.ldap.tlsCAFile} @@ -61,11 +68,11 @@ let base = ${cfg.ldap.searchBase} scope = ${mkLdapSearchScope cfg.ldap.searchScope} ${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) '' - user_attrs = ${cfg.ldap.dovecot.userAttrs} + user_attrs = ${cfg.ldap.dovecot.userAttrs} ''} user_filter = ${cfg.ldap.dovecot.userFilter} ${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") '' - pass_attrs = ${cfg.ldap.dovecot.passAttrs} + pass_attrs = ${cfg.ldap.dovecot.passAttrs} ''} pass_filter = ${cfg.ldap.dovecot.passFilter} ''; @@ -93,7 +100,9 @@ let # Prevent world-readable password files, even temporarily. umask 077 - for f in ${builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts)}; do + for f in ${ + builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts) + }; do if [ ! -f "$f" ]; then echo "Expected password hash file $f does not exist!" exit 1 @@ -101,34 +110,49 @@ let done cat < ${passwdFile} - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: _: - "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" - ) cfg.loginAccounts)} + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList ( + name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" + ) cfg.loginAccounts + )} EOF cat < ${userdbFile} - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: - "${name}:::::::" + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList ( + name: value: + "${name}:::::::" + lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}" - ) cfg.loginAccounts)} + ) cfg.loginAccounts + )} EOF ''; - junkMailboxes = builtins.attrNames (lib.filterAttrs (_: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes); + junkMailboxes = builtins.attrNames ( + lib.filterAttrs (_: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes + ); junkMailboxNumber = builtins.length junkMailboxes; # The assertion garantees there is exactly one Junk mailbox. junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else ""; - mkLdapSearchScope = scope: ( - if scope == "sub" then "subtree" - else if scope == "one" then "onelevel" - else scope - ); + mkLdapSearchScope = + scope: + ( + if scope == "sub" then + "subtree" + else if scope == "one" then + "onelevel" + else + scope + ); ftsPluginSettings = { fts = "flatcurve"; fts_languages = listToLine cfg.fullTextSearch.languages; - fts_tokenizers = listToLine [ "generic" "email-address" ]; + fts_tokenizers = listToLine [ + "generic" + "email-address" + ]; fts_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian fts_flatcurve_substring_search = boolToYesNo cfg.fullTextSearch.substringSearch; fts_filters = listToLine cfg.fullTextSearch.filters; @@ -139,255 +163,283 @@ let in { - config = with cfg; lib.mkIf enable { - assertions = [ - { - assertion = junkMailboxNumber == 1; - message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)"; - } - ]; - - warnings = - lib.optional ( - (builtins.length cfg.fullTextSearch.languages > 1) && - (builtins.elem "stopwords" cfg.fullTextSearch.filters) - ) '' - Using stopwords in `mailserver.fullTextSearch.filters` with multiple - languages in `mailserver.fullTextSearch.languages` configured WILL - cause some searches to fail. - - The recommended solution is to NOT use the stopword filter when - multiple languages are present in the configuration. - '' - ; - - # for sieve-test. Shelling it in on demand usually doesnt' work, as it reads - # the global config and tries to open shared libraries configured in there, - # which are usually not compatible. - environment.systemPackages = [ - pkgs.dovecot_pigeonhole - ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve; - - # For compatibility with python imaplib - environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; - - services.dovecot2 = { - enable = true; - enableImap = enableImap || enableImapSsl; - enablePop3 = enablePop3 || enablePop3Ssl; - enablePAM = false; - enableQuota = true; - mailGroup = vmailGroupName; - mailUser = vmailUserName; - mailLocation = dovecotMaildir; - sslServerCert = certificatePath; - sslServerKey = keyPath; - enableDHE = lib.mkDefault false; - enableLmtp = true; - mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ - "fts" - "fts_flatcurve" + config = + with cfg; + lib.mkIf enable { + assertions = [ + { + assertion = junkMailboxNumber == 1; + message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)"; + } ]; - protocols = lib.optional cfg.enableManageSieve "sieve"; - pluginSettings = { - sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; - sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; - sieve_default_name = "default"; - } // (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings); + warnings = + lib.optional + ( + (builtins.length cfg.fullTextSearch.languages > 1) + && (builtins.elem "stopwords" cfg.fullTextSearch.filters) + ) + '' + Using stopwords in `mailserver.fullTextSearch.filters` with multiple + languages in `mailserver.fullTextSearch.languages` configured WILL + cause some searches to fail. - sieve = { - extensions = [ - "fileinto" + The recommended solution is to NOT use the stopword filter when + multiple languages are present in the configuration. + ''; + + # for sieve-test. Shelling it in on demand usually doesnt' work, as it reads + # the global config and tries to open shared libraries configured in there, + # which are usually not compatible. + environment.systemPackages = [ + pkgs.dovecot_pigeonhole + ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve; + + # For compatibility with python imaplib + environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; + + services.dovecot2 = { + enable = true; + enableImap = enableImap || enableImapSsl; + enablePop3 = enablePop3 || enablePop3Ssl; + enablePAM = false; + enableQuota = true; + mailGroup = vmailGroupName; + mailUser = vmailUserName; + mailLocation = dovecotMaildir; + sslServerCert = certificatePath; + sslServerKey = keyPath; + enableDHE = lib.mkDefault false; + enableLmtp = true; + mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ + "fts" + "fts_flatcurve" ]; + protocols = lib.optional cfg.enableManageSieve "sieve"; - scripts.after = builtins.toFile "spam.sieve" '' - require "fileinto"; + pluginSettings = { + sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; + sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; + sieve_default_name = "default"; + } // (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings); - if header :is "X-Spam" "Yes" { - fileinto "${junkMailboxName}"; - stop; + sieve = { + extensions = [ + "fileinto" + ]; + + scripts.after = builtins.toFile "spam.sieve" '' + require "fileinto"; + + if header :is "X-Spam" "Yes" { + fileinto "${junkMailboxName}"; + stop; + } + ''; + + pipeBins = map lib.getExe [ + (pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham") + (pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam") + ]; + }; + + imapsieve.mailbox = [ + { + name = junkMailboxName; + causes = [ + "COPY" + "APPEND" + ]; + before = ./dovecot/imap_sieve/report-spam.sieve; + } + { + name = "*"; + from = junkMailboxName; + causes = [ "COPY" ]; + before = ./dovecot/imap_sieve/report-ham.sieve; } - ''; - - pipeBins = map lib.getExe [ - (pkgs.writeShellScriptBin "rspamd-learn-ham.sh" - "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham") - (pkgs.writeShellScriptBin "rspamd-learn-spam.sh" - "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam") ]; + + mailboxes = cfg.mailboxes; + + extraConfig = '' + #Extra Config + ${lib.optionalString debug '' + mail_debug = yes + auth_debug = yes + verbose_ssl = yes + ''} + + ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) '' + service imap-login { + inet_listener imap { + ${ + if cfg.enableImap then + '' + port = 143 + '' + else + '' + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = 0 + '' + } + } + inet_listener imaps { + ${ + if cfg.enableImapSsl then + '' + port = 993 + ssl = yes + '' + else + '' + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = 0 + '' + } + } + } + ''} + ${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) '' + service pop3-login { + inet_listener pop3 { + ${ + if cfg.enablePop3 then + '' + port = 110 + '' + else + '' + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = 0 + '' + } + } + inet_listener pop3s { + ${ + if cfg.enablePop3Ssl then + '' + port = 995 + ssl = yes + '' + else + '' + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = 0 + '' + } + } + } + ''} + + protocol imap { + mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} + mail_plugins = $mail_plugins imap_sieve + } + + service imap { + vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB + } + + protocol pop3 { + mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} + } + + mail_access_groups = ${vmailGroupName} + + # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7 + ssl = required + ssl_min_protocol = TLSv1.2 + ssl_prefer_server_ciphers = no + ssl_curve_list = X25519:prime256v1:secp384r1 + + service lmtp { + unix_listener dovecot-lmtp { + group = ${postfixCfg.group} + mode = 0600 + user = ${postfixCfg.user} + } + vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB + } + + service quota-status { + inet_listener { + port = 0 + } + unix_listener quota-status { + user = postfix + } + vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB + } + + recipient_delimiter = ${cfg.recipientDelimiter} + lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} + + protocol lmtp { + mail_plugins = $mail_plugins sieve + } + + passdb { + driver = passwd-file + args = ${passwdFile} + } + + userdb { + driver = passwd-file + args = ${userdbFile} + default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} + } + + ${lib.optionalString cfg.ldap.enable '' + passdb { + driver = ldap + args = ${ldapConfFile} + } + + userdb { + driver = ldap + args = ${ldapConfFile} + default_fields = home=${cfg.mailDirectory}/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} + } + ''} + + service auth { + unix_listener auth { + mode = 0660 + user = ${postfixCfg.user} + group = ${postfixCfg.group} + } + } + + auth_mechanisms = plain login + + namespace inbox { + separator = ${cfg.hierarchySeparator} + inbox = yes + } + + service indexer-worker { + ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' + vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)} + ''} + } + + lda_mailbox_autosubscribe = yes + lda_mailbox_autocreate = yes + ''; }; - imapsieve.mailbox = [ - { - name = junkMailboxName; - causes = [ "COPY" "APPEND" ]; - before = ./dovecot/imap_sieve/report-spam.sieve; - } - { - name = "*"; - from = junkMailboxName; - causes = [ "COPY" ]; - before = ./dovecot/imap_sieve/report-ham.sieve; - } - ]; + systemd.services.dovecot2 = { + preStart = + '' + ${genPasswdScript} + '' + + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); + }; - mailboxes = cfg.mailboxes; - - extraConfig = '' - #Extra Config - ${lib.optionalString debug '' - mail_debug = yes - auth_debug = yes - verbose_ssl = yes - ''} - - ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) '' - service imap-login { - inet_listener imap { - ${if cfg.enableImap then '' - port = 143 - '' else '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - ''} - } - inet_listener imaps { - ${if cfg.enableImapSsl then '' - port = 993 - ssl = yes - '' else '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - ''} - } - } - ''} - ${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) '' - service pop3-login { - inet_listener pop3 { - ${if cfg.enablePop3 then '' - port = 110 - '' else '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - ''} - } - inet_listener pop3s { - ${if cfg.enablePop3Ssl then '' - port = 995 - ssl = yes - '' else '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - ''} - } - } - ''} - - protocol imap { - mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} - mail_plugins = $mail_plugins imap_sieve - } - - service imap { - vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB - } - - protocol pop3 { - mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} - } - - mail_access_groups = ${vmailGroupName} - - # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7 - ssl = required - ssl_min_protocol = TLSv1.2 - ssl_prefer_server_ciphers = no - ssl_curve_list = X25519:prime256v1:secp384r1 - - service lmtp { - unix_listener dovecot-lmtp { - group = ${postfixCfg.group} - mode = 0600 - user = ${postfixCfg.user} - } - vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB - } - - service quota-status { - inet_listener { - port = 0 - } - unix_listener quota-status { - user = postfix - } - vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB - } - - recipient_delimiter = ${cfg.recipientDelimiter} - lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} - - protocol lmtp { - mail_plugins = $mail_plugins sieve - } - - passdb { - driver = passwd-file - args = ${passwdFile} - } - - userdb { - driver = passwd-file - args = ${userdbFile} - default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} - } - - ${lib.optionalString cfg.ldap.enable '' - passdb { - driver = ldap - args = ${ldapConfFile} - } - - userdb { - driver = ldap - args = ${ldapConfFile} - default_fields = home=${cfg.mailDirectory}/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} - } - ''} - - service auth { - unix_listener auth { - mode = 0660 - user = ${postfixCfg.user} - group = ${postfixCfg.group} - } - } - - auth_mechanisms = plain login - - namespace inbox { - separator = ${cfg.hierarchySeparator} - inbox = yes - } - - service indexer-worker { - ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' - vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)} - ''} - } - - lda_mailbox_autosubscribe = yes - lda_mailbox_autocreate = yes - ''; + systemd.services.postfix.restartTriggers = [ + genPasswdScript + ] ++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]); }; - - systemd.services.dovecot2 = { - preStart = '' - ${genPasswdScript} - '' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); - }; - - systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]); - }; } diff --git a/mail-server/environment.nix b/mail-server/environment.nix index b4326a1..b853211 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -14,15 +14,28 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.mailserver; in { - config = with cfg; lib.mkIf enable { - environment.systemPackages = with pkgs; [ - dovecot openssh postfix rspamd - ] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []); - }; + config = + with cfg; + lib.mkIf enable { + environment.systemPackages = + with pkgs; + [ + dovecot + openssh + postfix + rspamd + ] + ++ (if certificateScheme == "selfsigned" then [ openssl ] else [ ]); + }; } diff --git a/mail-server/kresd.nix b/mail-server/kresd.nix index 230bdea..3920534 100644 --- a/mail-server/kresd.nix +++ b/mail-server/kresd.nix @@ -24,4 +24,3 @@ in services.kresd.enable = true; }; } - diff --git a/mail-server/networking.nix b/mail-server/networking.nix index 6af186a..587a8ae 100644 --- a/mail-server/networking.nix +++ b/mail-server/networking.nix @@ -20,18 +20,21 @@ let cfg = config.mailserver; in { - config = with cfg; lib.mkIf (enable && openFirewall) { + config = + with cfg; + lib.mkIf (enable && openFirewall) { - networking.firewall = { - allowedTCPPorts = [ 25 ] - ++ lib.optional enableSubmission 587 - ++ lib.optional enableSubmissionSsl 465 - ++ lib.optional enableImap 143 - ++ lib.optional enableImapSsl 993 - ++ lib.optional enablePop3 110 - ++ lib.optional enablePop3Ssl 995 - ++ lib.optional enableManageSieve 4190 - ++ lib.optional (certificateScheme == "acme-nginx") 80; + networking.firewall = { + allowedTCPPorts = + [ 25 ] + ++ lib.optional enableSubmission 587 + ++ lib.optional enableSubmissionSsl 465 + ++ lib.optional enableImap 143 + ++ lib.optional enableImapSsl 993 + ++ lib.optional enablePop3 110 + ++ lib.optional enablePop3Ssl 995 + ++ lib.optional enableManageSieve 4190 + ++ lib.optional (certificateScheme == "acme-nginx") 80; + }; }; - }; } diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index a037f56..27de2fe 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -14,8 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see - -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: with (import ./common.nix { inherit config lib pkgs; }); @@ -23,20 +27,22 @@ let cfg = config.mailserver; in { - config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) { - services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") { - enable = true; - virtualHosts."${cfg.fqdn}" = { - serverName = cfg.fqdn; - serverAliases = cfg.certificateDomains; - forceSSL = true; - enableACME = true; - }; - }; + config = + lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) + { + services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") { + enable = true; + virtualHosts."${cfg.fqdn}" = { + serverName = cfg.fqdn; + serverAliases = cfg.certificateDomains; + forceSSL = true; + enableACME = true; + }; + }; - security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [ - "postfix.service" - "dovecot2.service" - ]; - }; + security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [ + "postfix.service" + "dovecot2.service" + ]; + }; } diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 9fb316f..4237efc 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -14,7 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: with (import ./common.nix { inherit config pkgs lib; }); @@ -28,31 +33,55 @@ let mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables; # valiases_postfix :: Map String [String] - valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList - (name: value: - let to = name; - in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name)) - cfg.loginAccounts)); - regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList - (name: value: - let to = name; - in map (from: {"${from}" = to;}) value.aliasesRegexp) - cfg.loginAccounts)); + valiases_postfix = mergeLookupTables ( + lib.flatten ( + lib.mapAttrsToList ( + name: value: + let + to = name; + in + map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name) + ) cfg.loginAccounts + ) + ); + regex_valiases_postfix = mergeLookupTables ( + lib.flatten ( + lib.mapAttrsToList ( + name: value: + let + to = name; + in + map (from: { "${from}" = to; }) value.aliasesRegexp + ) cfg.loginAccounts + ) + ); # catchAllPostfix :: Map String [String] - catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList - (name: value: - let to = name; - in map (from: {"@${from}" = to;}) value.catchAll) - cfg.loginAccounts)); + catchAllPostfix = mergeLookupTables ( + lib.flatten ( + lib.mapAttrsToList ( + name: value: + let + to = name; + in + map (from: { "@${from}" = to; }) value.catchAll + ) cfg.loginAccounts + ) + ); # all_valiases_postfix :: Map String [String] - all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix]; + all_valiases_postfix = mergeLookupTables [ + valiases_postfix + extra_valiases_postfix + ]; # attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String] - attrsToLookupTable = aliases: let - lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases; - in mergeLookupTables lookupTables; + attrsToLookupTable = + aliases: + let + lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases; + in + mergeLookupTables lookupTables; # extra_valiases_postfix :: Map String [String] extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases; @@ -61,37 +90,49 @@ let forwards = attrsToLookupTable cfg.forwards; # lookupTableToString :: Map String [String] -> String - lookupTableToString = attrs: let - valueToString = value: lib.concatStringsSep ", " value; - in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs); + lookupTableToString = + attrs: + let + valueToString = value: lib.concatStringsSep ", " value; + in + lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs + ); # valiases_file :: Path - valiases_file = let - content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]); - in builtins.toFile "valias" content; + valiases_file = + let + content = lookupTableToString (mergeLookupTables [ + all_valiases_postfix + catchAllPostfix + ]); + in + builtins.toFile "valias" content; - regex_valiases_file = let - content = lookupTableToString regex_valiases_postfix; - in builtins.toFile "regex_valias" content; + regex_valiases_file = + let + content = lookupTableToString regex_valiases_postfix; + in + builtins.toFile "regex_valias" content; # denied_recipients_postfix :: [ String ] - denied_recipients_postfix = map - (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") - (lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)); - denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix); + denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") ( + lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts) + ); + denied_recipients_file = builtins.toFile "denied_recipients" ( + lib.concatStringsSep "\n" denied_recipients_postfix + ); - reject_senders_postfix = map - (sender: - "${sender} REJECT") - cfg.rejectSender; - reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" reject_senders_postfix) ; + reject_senders_postfix = map (sender: "${sender} REJECT") cfg.rejectSender; + reject_senders_file = builtins.toFile "reject_senders" ( + lib.concatStringsSep "\n" reject_senders_postfix + ); - reject_recipients_postfix = map - (recipient: - "${recipient} REJECT") - cfg.rejectRecipients; + reject_recipients_postfix = map (recipient: "${recipient} REJECT") cfg.rejectRecipients; # rejectRecipients :: [ Path ] - reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" reject_recipients_postfix) ; + reject_recipients_file = builtins.toFile "reject_recipients" ( + lib.concatStringsSep "\n" reject_recipients_postfix + ); # vhosts_file :: Path vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains); @@ -103,45 +144,51 @@ let # every alias is owned (uniquely) by its user. # The user's own address is already in all_valiases_postfix. vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix); - regex_vaccounts_file = builtins.toFile "regex_vaccounts" (lookupTableToString regex_valiases_postfix); + regex_vaccounts_file = builtins.toFile "regex_vaccounts" ( + lookupTableToString regex_valiases_postfix + ); - submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ('' - # Removes sensitive headers from mails handed in via the submission port. - # See https://thomas-leister.de/mailserver-debian-stretch/ - # Uses "pcre" style regex. + submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ( + '' + # Removes sensitive headers from mails handed in via the submission port. + # See https://thomas-leister.de/mailserver-debian-stretch/ + # Uses "pcre" style regex. - /^Received:/ IGNORE - /^X-Originating-IP:/ IGNORE - /^X-Mailer:/ IGNORE - /^User-Agent:/ IGNORE - /^X-Enigmail:/ IGNORE - '' + lib.optionalString cfg.rewriteMessageId '' + /^Received:/ IGNORE + /^X-Originating-IP:/ IGNORE + /^X-Mailer:/ IGNORE + /^User-Agent:/ IGNORE + /^X-Enigmail:/ IGNORE + '' + + lib.optionalString cfg.rewriteMessageId '' - # Replaces the user submitted hostname with the server's FQDN to hide the - # user's host or network. + # Replaces the user submitted hostname with the server's FQDN to hide the + # user's host or network. - /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}> - ''); + /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}> + '' + ); smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ]; mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}"; - submissionOptions = - { - smtpd_tls_security_level = "encrypt"; - smtpd_sasl_auth_enable = "yes"; - smtpd_sasl_type = "dovecot"; - smtpd_sasl_path = "/run/dovecot2/auth"; - smtpd_sasl_security_options = "noanonymous"; - smtpd_sasl_local_domain = "$myhostname"; - smtpd_client_restrictions = "permit_sasl_authenticated,reject"; - smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${lib.optionalString (regex_valiases_postfix != {}) ",pcre:/etc/postfix/regex_vaccounts"}"; - smtpd_sender_restrictions = "reject_sender_login_mismatch"; - smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; - cleanup_service_name = "submission-header-cleanup"; - }; + submissionOptions = { + smtpd_tls_security_level = "encrypt"; + smtpd_sasl_auth_enable = "yes"; + smtpd_sasl_type = "dovecot"; + smtpd_sasl_path = "/run/dovecot2/auth"; + smtpd_sasl_security_options = "noanonymous"; + smtpd_sasl_local_domain = "$myhostname"; + smtpd_client_restrictions = "permit_sasl_authenticated,reject"; + smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${ + lib.optionalString (regex_valiases_postfix != { }) ",pcre:/etc/postfix/regex_vaccounts" + }"; + smtpd_sender_restrictions = "reject_sender_login_mismatch"; + smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; + cleanup_service_name = "submission-header-cleanup"; + }; commonLdapConfig = '' server_host = ${lib.concatStringsSep " " cfg.ldap.uris} @@ -186,164 +233,183 @@ let }; in { - config = with cfg; lib.mkIf enable { + config = + with cfg; + lib.mkIf enable { - systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { - preStart = '' - ${appendPwdInVirtualMailboxMap} - ${appendPwdInSenderLoginMap} - ''; - restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ]; - }; - - services.postfix = { - enable = true; - hostname = "${sendingFqdn}"; - networksStyle = "host"; - mapFiles."valias" = valiases_file; - mapFiles."regex_valias" = regex_valiases_file; - mapFiles."vaccounts" = vaccounts_file; - mapFiles."regex_vaccounts" = regex_vaccounts_file; - mapFiles."denied_recipients" = denied_recipients_file; - mapFiles."reject_senders" = reject_senders_file; - mapFiles."reject_recipients" = reject_recipients_file; - enableSubmission = cfg.enableSubmission; - enableSubmissions = cfg.enableSubmissionSsl; - virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]); - - config = { - smtpd_tls_chain_files = [ - "${keyPath}" - "${certificatePath}" + systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { + preStart = '' + ${appendPwdInVirtualMailboxMap} + ${appendPwdInSenderLoginMap} + ''; + restartTriggers = [ + appendPwdInVirtualMailboxMap + appendPwdInSenderLoginMap ]; + }; - # Extra Config - mydestination = ""; - recipient_delimiter = cfg.recipientDelimiter; - smtpd_banner = "${fqdn} ESMTP NO UCE"; - disable_vrfy_command = true; - message_size_limit = toString cfg.messageSizeLimit; - - # virtual mail system - virtual_uid_maps = "static:5000"; - virtual_gid_maps = "static:5000"; - virtual_mailbox_base = mailDirectory; - virtual_mailbox_domains = vhosts_file; - virtual_mailbox_maps = [ - (mappedFile "valias") - ] ++ lib.optionals cfg.ldap.enable [ - "ldap:${ldapVirtualMailboxMapFile}" - ] ++ lib.optionals (regex_valiases_postfix != {}) [ - (mappedRegexFile "regex_valias") - ]; - virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [ - (mappedRegexFile "regex_valias") + services.postfix = { + enable = true; + hostname = "${sendingFqdn}"; + networksStyle = "host"; + mapFiles."valias" = valiases_file; + mapFiles."regex_valias" = regex_valiases_file; + mapFiles."vaccounts" = vaccounts_file; + mapFiles."regex_vaccounts" = regex_vaccounts_file; + mapFiles."denied_recipients" = denied_recipients_file; + mapFiles."reject_senders" = reject_senders_file; + mapFiles."reject_recipients" = reject_recipients_file; + enableSubmission = cfg.enableSubmission; + enableSubmissions = cfg.enableSubmissionSsl; + virtual = lookupTableToString (mergeLookupTables [ + all_valiases_postfix + catchAllPostfix + forwards ]); - virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; - # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients - lmtp_destination_recipient_limit = "1"; - # sasl with dovecot - smtpd_sasl_type = "dovecot"; - smtpd_sasl_path = "/run/dovecot2/auth"; - smtpd_sasl_auth_enable = true; - smtpd_relay_restrictions = [ - "permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination" - ]; + config = { + smtpd_tls_chain_files = [ + "${keyPath}" + "${certificatePath}" + ]; - # reject selected senders - smtpd_sender_restrictions = [ - "check_sender_access ${mappedFile "reject_senders"}" - ]; + # Extra Config + mydestination = ""; + recipient_delimiter = cfg.recipientDelimiter; + smtpd_banner = "${fqdn} ESMTP NO UCE"; + disable_vrfy_command = true; + message_size_limit = toString cfg.messageSizeLimit; - smtpd_recipient_restrictions = [ - # reject selected recipients - "check_recipient_access ${mappedFile "denied_recipients"}" - "check_recipient_access ${mappedFile "reject_recipients"}" - # quota checking - "check_policy_service unix:/run/dovecot2/quota-status" - ]; + # virtual mail system + virtual_uid_maps = "static:5000"; + virtual_gid_maps = "static:5000"; + virtual_mailbox_base = mailDirectory; + virtual_mailbox_domains = vhosts_file; + virtual_mailbox_maps = + [ + (mappedFile "valias") + ] + ++ lib.optionals cfg.ldap.enable [ + "ldap:${ldapVirtualMailboxMapFile}" + ] + ++ lib.optionals (regex_valiases_postfix != { }) [ + (mappedRegexFile "regex_valias") + ]; + virtual_alias_maps = lib.mkAfter ( + lib.optionals (regex_valiases_postfix != { }) [ + (mappedRegexFile "regex_valias") + ] + ); + virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; + # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients + lmtp_destination_recipient_limit = "1"; - # TLS for incoming mail is optional - smtpd_tls_security_level = "may"; + # sasl with dovecot + smtpd_sasl_type = "dovecot"; + smtpd_sasl_path = "/run/dovecot2/auth"; + smtpd_sasl_auth_enable = true; + smtpd_relay_restrictions = [ + "permit_mynetworks" + "permit_sasl_authenticated" + "reject_unauth_destination" + ]; - # But required for authentication attempts - smtpd_tls_auth_only = true; + # reject selected senders + smtpd_sender_restrictions = [ + "check_sender_access ${mappedFile "reject_senders"}" + ]; - # TLS versions supported for the SMTP server - smtpd_tls_protocols = ">=TLSv1.2"; - smtpd_tls_mandatory_protocols = ">=TLSv1.2"; + smtpd_recipient_restrictions = [ + # reject selected recipients + "check_recipient_access ${mappedFile "denied_recipients"}" + "check_recipient_access ${mappedFile "reject_recipients"}" + # quota checking + "check_policy_service unix:/run/dovecot2/quota-status" + ]; - # Require ciphersuites that OpenSSL classifies as "High" - smtpd_tls_ciphers = "high"; - smtpd_tls_mandatory_ciphers = "high"; + # TLS for incoming mail is optional + smtpd_tls_security_level = "may"; - # Exclude cipher suites with undesirable properties - smtpd_tls_exclude_ciphers = "eNULL, aNULL"; - smtpd_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; + # But required for authentication attempts + smtpd_tls_auth_only = true; - # Opportunistic DANE support when delivering mail to other servers - # https://www.postfix.org/postconf.5.html#smtp_tls_security_level - smtp_dns_support_level = "dnssec"; - smtp_tls_security_level = "dane"; + # TLS versions supported for the SMTP server + smtpd_tls_protocols = ">=TLSv1.2"; + smtpd_tls_mandatory_protocols = ">=TLSv1.2"; - # TLS versions supported for the SMTP client - smtp_tls_protocols = ">=TLSv1.2"; - smtp_tls_mandatory_protocols = ">=TLSv1.2"; + # Require ciphersuites that OpenSSL classifies as "High" + smtpd_tls_ciphers = "high"; + smtpd_tls_mandatory_ciphers = "high"; - # Require ciphersuites that OpenSSL classifies as "High" - smtp_tls_ciphers = "high"; - smtp_tls_mandatory_ciphers = "high"; + # Exclude cipher suites with undesirable properties + smtpd_tls_exclude_ciphers = "eNULL, aNULL"; + smtpd_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; - # Exclude ciphersuites with undesirable properties - smtp_tls_exclude_ciphers = "eNULL, aNULL"; - smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; + # Opportunistic DANE support when delivering mail to other servers + # https://www.postfix.org/postconf.5.html#smtp_tls_security_level + smtp_dns_support_level = "dnssec"; + smtp_tls_security_level = "dane"; - # Restrict and prioritize the following curves in the given order - # Excludes curves that have no widespread support, so we don't bloat the handshake needlessly. - # https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves - # https://ssl-config.mozilla.org/#server=postfix&version=3.10&config=intermediate&openssl=3.4.1&guideline=5.7 - tls_eecdh_auto_curves = [ - "X25519" - "prime256v1" - "secp384r1" - ]; + # TLS versions supported for the SMTP client + smtp_tls_protocols = ">=TLSv1.2"; + smtp_tls_mandatory_protocols = ">=TLSv1.2"; - # Disable FFDHE on TLSv1.3 because it is slower than elliptic curves - # https://www.postfix.org/postconf.5.html#tls_ffdhe_auto_groups - tls_ffdhe_auto_groups = [ ]; + # Require ciphersuites that OpenSSL classifies as "High" + smtp_tls_ciphers = "high"; + smtp_tls_mandatory_ciphers = "high"; - # As long as all cipher suites are considered safe, let the client use its preferred cipher - tls_preempt_cipherlist = false; + # Exclude ciphersuites with undesirable properties + smtp_tls_exclude_ciphers = "eNULL, aNULL"; + smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; - # Log only a summary message on TLS handshake completion - smtp_tls_loglevel = "1"; - smtpd_tls_loglevel = "1"; + # Restrict and prioritize the following curves in the given order + # Excludes curves that have no widespread support, so we don't bloat the handshake needlessly. + # https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves + # https://ssl-config.mozilla.org/#server=postfix&version=3.10&config=intermediate&openssl=3.4.1&guideline=5.7 + tls_eecdh_auto_curves = [ + "X25519" + "prime256v1" + "secp384r1" + ]; - smtpd_milters = smtpdMilters; - non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ]; - milter_protocol = "6"; - milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}"; - }; + # Disable FFDHE on TLSv1.3 because it is slower than elliptic curves + # https://www.postfix.org/postconf.5.html#tls_ffdhe_auto_groups + tls_ffdhe_auto_groups = [ ]; - submissionOptions = submissionOptions; - submissionsOptions = submissionOptions; + # As long as all cipher suites are considered safe, let the client use its preferred cipher + tls_preempt_cipherlist = false; - masterConfig = { - "lmtp" = { - # Add headers when delivering, see http://www.postfix.org/smtp.8.html - # D => Delivered-To, O => X-Original-To, R => Return-Path - args = [ "flags=O" ]; + # Log only a summary message on TLS handshake completion + smtp_tls_loglevel = "1"; + smtpd_tls_loglevel = "1"; + + smtpd_milters = smtpdMilters; + non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ]; + milter_protocol = "6"; + milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}"; }; - "submission-header-cleanup" = { - type = "unix"; - private = false; - chroot = false; - maxproc = 0; - command = "cleanup"; - args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"]; + + submissionOptions = submissionOptions; + submissionsOptions = submissionOptions; + + masterConfig = { + "lmtp" = { + # Add headers when delivering, see http://www.postfix.org/smtp.8.html + # D => Delivered-To, O => X-Original-To, R => Return-Path + args = [ "flags=O" ]; + }; + "submission-header-cleanup" = { + type = "unix"; + private = false; + chroot = false; + maxproc = 0; + command = "cleanup"; + args = [ + "-o" + "header_checks=pcre:${submissionHeaderCleanupRules}" + ]; + }; }; }; }; - }; } diff --git a/mail-server/rsnapshot.nix b/mail-server/rsnapshot.nix index a801c24..de4f13e 100644 --- a/mail-server/rsnapshot.nix +++ b/mail-server/rsnapshot.nix @@ -14,7 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: with lib; @@ -38,7 +43,8 @@ let ${cfg.backup.cmdPostexec} ''; postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}"; -in { +in +{ config = mkIf (cfg.enable && cfg.backup.enable) { services.rsnapshot = { enable = true; diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index ab5113a..a4fcdce 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -14,7 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.mailserver; @@ -26,56 +31,74 @@ let rspamdUser = config.services.rspamd.user; rspamdGroup = config.services.rspamd.group; - createDkimKeypair = domain: let - privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key"; - publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt"; - in pkgs.writeShellScript "dkim-keygen-${domain}" '' - if [ ! -f "${privateKey}" ] - then - ${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \ - --domain "${domain}" \ - --selector "${cfg.dkimSelector}" \ - --type "${cfg.dkimKeyType}" \ - --bits ${toString cfg.dkimKeyBits} \ - --privkey "${privateKey}" > "${publicKey}" - chmod 0644 "${publicKey}" - echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}" - fi - ''; + createDkimKeypair = + domain: + let + privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key"; + publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt"; + in + pkgs.writeShellScript "dkim-keygen-${domain}" '' + if [ ! -f "${privateKey}" ] + then + ${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \ + --domain "${domain}" \ + --selector "${cfg.dkimSelector}" \ + --type "${cfg.dkimKeyType}" \ + --bits ${toString cfg.dkimKeyBits} \ + --privkey "${privateKey}" > "${publicKey}" + chmod 0644 "${publicKey}" + echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}" + fi + ''; in { - config = with cfg; lib.mkIf enable { - environment.systemPackages = lib.mkBefore [ - (pkgs.runCommand "rspamc-wrapped" { - nativeBuildInputs = with pkgs; [ makeWrapper ]; - }'' - makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \ - --add-flags "-h /run/rspamd/worker-controller.sock" - '') - ]; + config = + with cfg; + lib.mkIf enable { + environment.systemPackages = lib.mkBefore [ + (pkgs.runCommand "rspamc-wrapped" + { + nativeBuildInputs = with pkgs; [ makeWrapper ]; + } + '' + makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \ + --add-flags "-h /run/rspamd/worker-controller.sock" + '' + ) + ]; - services.rspamd = { - enable = true; - inherit debug; - locals = { - "milter_headers.conf" = { text = '' + services.rspamd = { + enable = true; + inherit debug; + locals = { + "milter_headers.conf" = { + text = '' extended_spam_headers = true; - ''; }; - "redis.conf" = { text = '' - servers = "${if cfg.redis.port == null - then - cfg.redis.address - else - "${cfg.redis.address}:${toString cfg.redis.port}"}"; - '' + (lib.optionalString (cfg.redis.password != null) '' - password = "${cfg.redis.password}"; - ''); }; - "classifier-bayes.conf" = { text = '' + ''; + }; + "redis.conf" = { + text = + '' + servers = "${ + if cfg.redis.port == null then + cfg.redis.address + else + "${cfg.redis.address}:${toString cfg.redis.port}" + }"; + '' + + (lib.optionalString (cfg.redis.password != null) '' + password = "${cfg.redis.password}"; + ''); + }; + "classifier-bayes.conf" = { + text = '' cache { backend = "redis"; } - ''; }; - "antivirus.conf" = lib.mkIf cfg.virusScanning { text = '' + ''; + }; + "antivirus.conf" = lib.mkIf cfg.virusScanning { + text = '' clamav { action = "reject"; symbol = "CLAM_VIRUS"; @@ -84,157 +107,168 @@ in servers = "/run/clamav/clamd.ctl"; scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all } - ''; }; - "dkim_signing.conf" = { text = '' - enabled = ${lib.boolToString cfg.dkimSigning}; - path = "${cfg.dkimKeyDirectory}/$domain.$selector.key"; - selector = "${cfg.dkimSelector}"; - # Allow for usernames w/o domain part - allow_username_mismatch = true - ''; }; - "dmarc.conf" = { text = '' + ''; + }; + "dkim_signing.conf" = { + text = '' + enabled = ${lib.boolToString cfg.dkimSigning}; + path = "${cfg.dkimKeyDirectory}/$domain.$selector.key"; + selector = "${cfg.dkimSelector}"; + # Allow for usernames w/o domain part + allow_username_mismatch = true + ''; + }; + "dmarc.conf" = { + text = '' ${lib.optionalString cfg.dmarcReporting.enable '' - reporting { - enabled = true; - email = "${cfg.dmarcReporting.email}"; - domain = "${cfg.dmarcReporting.domain}"; - org_name = "${cfg.dmarcReporting.organizationName}"; - from_name = "${cfg.dmarcReporting.fromName}"; - msgid_from = "${cfg.dmarcReporting.domain}"; - ${lib.optionalString (cfg.dmarcReporting.excludeDomains != []) '' - exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains}; - ''} - }''} - ''; }; + reporting { + enabled = true; + email = "${cfg.dmarcReporting.email}"; + domain = "${cfg.dmarcReporting.domain}"; + org_name = "${cfg.dmarcReporting.organizationName}"; + from_name = "${cfg.dmarcReporting.fromName}"; + msgid_from = "${cfg.dmarcReporting.domain}"; + ${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) '' + exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains}; + ''} + }''} + ''; + }; + }; + + workers.rspamd_proxy = { + type = "rspamd_proxy"; + bindSockets = [ + { + socket = "/run/rspamd/rspamd-milter.sock"; + mode = "0664"; + } + ]; + count = 1; # Do not spawn too many processes of this type + extraConfig = '' + milter = yes; # Enable milter mode + timeout = 120s; # Needed for Milter usually + + upstream "local" { + default = yes; # Self-scan upstreams are always default + self_scan = yes; # Enable self-scan + } + ''; + }; + workers.controller = { + type = "controller"; + count = 1; + bindSockets = [ + { + socket = "/run/rspamd/worker-controller.sock"; + mode = "0666"; + } + ]; + includes = [ ]; + extraConfig = '' + static_dir = "''${WWWDIR}"; # Serve the web UI static assets + ''; + }; + }; - workers.rspamd_proxy = { - type = "rspamd_proxy"; - bindSockets = [{ - socket = "/run/rspamd/rspamd-milter.sock"; - mode = "0664"; - }]; - count = 1; # Do not spawn too many processes of this type - extraConfig = '' - milter = yes; # Enable milter mode - timeout = 120s; # Needed for Milter usually + services.redis.servers.rspamd.enable = lib.mkDefault true; - upstream "local" { - default = yes; # Self-scan upstreams are always default - self_scan = yes; # Enable self-scan + systemd.tmpfiles.settings."10-rspamd.conf" = { + "${cfg.dkimKeyDirectory}" = { + d = { + # Create /var/dkim owned by rspamd user/group + user = rspamdUser; + group = rspamdGroup; + }; + Z = { + # Recursively adjust permissions in /var/dkim + user = rspamdUser; + group = rspamdGroup; + }; + }; + }; + + systemd.services.rspamd = { + requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); + after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); + serviceConfig = lib.mkMerge [ + { + SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; } - ''; - }; - workers.controller = { - type = "controller"; - count = 1; - bindSockets = [{ - socket = "/run/rspamd/worker-controller.sock"; - mode = "0666"; - }]; - includes = []; - extraConfig = '' - static_dir = "''${WWWDIR}"; # Serve the web UI static assets - ''; - }; - - }; - - services.redis.servers.rspamd.enable = lib.mkDefault true; - - systemd.tmpfiles.settings."10-rspamd.conf" = { - "${cfg.dkimKeyDirectory}" = { - d = { - # Create /var/dkim owned by rspamd user/group - user = rspamdUser; - group = rspamdGroup; - }; - Z = { - # Recursively adjust permissions in /var/dkim - user = rspamdUser; - group = rspamdGroup; - }; - }; - }; - - systemd.services.rspamd = { - requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); - after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); - serviceConfig = lib.mkMerge [ - { - SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; - } - (lib.optionalAttrs cfg.dkimSigning { - ExecStartPre = map createDkimKeypair cfg.domains; - ReadWritePaths = [ cfg.dkimKeyDirectory ]; - }) - ]; - }; - - systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { - # Explicitly select yesterday's date to work around broken - # default behaviour when called without a date. - # https://github.com/rspamd/rspamd/issues/4062 - script = '' - ${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d") - ''; - serviceConfig = { - User = "${config.services.rspamd.user}"; - Group = "${config.services.rspamd.group}"; - - AmbientCapabilities = []; - CapabilityBoundingSet = ""; - DevicePolicy = "closed"; - IPAddressAllow = "localhost"; - LockPersonality = true; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateMounts = true; - PrivateTmp = true; - PrivateUsers = true; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectProc = "invisible"; - ProcSubset = "pid"; - ProtectSystem = "strict"; - RemoveIPC = true; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - SystemCallFilter = [ - "@system-service" - "~@privileged" + (lib.optionalAttrs cfg.dkimSigning { + ExecStartPre = map createDkimKeypair cfg.domains; + ReadWritePaths = [ cfg.dkimKeyDirectory ]; + }) ]; - UMask = "0077"; }; - }; - systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { - description = "Daily delivery of aggregated DMARC reports"; - wantedBy = [ - "timers.target" - ]; - timerConfig = { - OnCalendar = "daily"; - Persistent = true; - RandomizedDelaySec = 86400; - FixedRandomDelay = true; + systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { + # Explicitly select yesterday's date to work around broken + # default behaviour when called without a date. + # https://github.com/rspamd/rspamd/issues/4062 + script = '' + ${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d") + ''; + serviceConfig = { + User = "${config.services.rspamd.user}"; + Group = "${config.services.rspamd.group}"; + + AmbientCapabilities = [ ]; + CapabilityBoundingSet = ""; + DevicePolicy = "closed"; + IPAddressAllow = "localhost"; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; }; - }; - systemd.services.postfix = { - after = [ rspamdSocket ]; - requires = [ rspamdSocket ]; - }; + systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { + description = "Daily delivery of aggregated DMARC reports"; + wantedBy = [ + "timers.target" + ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = true; + RandomizedDelaySec = 86400; + FixedRandomDelay = true; + }; + }; - users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ]; - }; + systemd.services.postfix = { + after = [ rspamdSocket ]; + requires = [ rspamdSocket ]; + }; + + users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ]; + }; } - diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index c411441..dd4bd63 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -14,72 +14,79 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let cfg = config.mailserver; certificatesDeps = if cfg.certificateScheme == "manual" then - [] + [ ] else if cfg.certificateScheme == "selfsigned" then [ "mailserver-selfsigned-certificate.service" ] else [ "acme-finished-${cfg.fqdn}.target" ]; in { - config = with cfg; lib.mkIf enable { - # Create self signed certificate - systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") { - after = [ "local-fs.target" ]; - script = '' - # Create certificates if they do not exist yet - dir="${cfg.certificateDirectory}" - fqdn="${cfg.fqdn}" - [[ $fqdn == /* ]] && fqdn=$(< "$fqdn") - key="$dir/key-${cfg.fqdn}.pem"; - cert="$dir/cert-${cfg.fqdn}.pem"; + config = + with cfg; + lib.mkIf enable { + # Create self signed certificate + systemd.services.mailserver-selfsigned-certificate = + lib.mkIf (cfg.certificateScheme == "selfsigned") + { + after = [ "local-fs.target" ]; + script = '' + # Create certificates if they do not exist yet + dir="${cfg.certificateDirectory}" + fqdn="${cfg.fqdn}" + [[ $fqdn == /* ]] && fqdn=$(< "$fqdn") + key="$dir/key-${cfg.fqdn}.pem"; + cert="$dir/cert-${cfg.fqdn}.pem"; - if [[ ! -f $key || ! -f $cert ]]; then - mkdir -p "${cfg.certificateDirectory}" - (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) && - "${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \ - -days 3650 -out "$cert" - fi - ''; - serviceConfig = { - Type = "oneshot"; - PrivateTmp = true; + if [[ ! -f $key || ! -f $cert ]]; then + mkdir -p "${cfg.certificateDirectory}" + (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) && + "${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \ + -days 3650 -out "$cert" + fi + ''; + serviceConfig = { + Type = "oneshot"; + PrivateTmp = true; + }; + }; + + # Create maildir folder before dovecot startup + systemd.services.dovecot2 = { + wants = certificatesDeps; + after = certificatesDeps; + preStart = + let + directories = lib.strings.escapeShellArgs ( + [ mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir + ); + in + '' + # Create mail directory and set permissions. See + # . + # Prevent world-readable paths, even temporarily. + umask 007 + mkdir -p ${directories} + chgrp "${vmailGroupName}" ${directories} + chmod 02770 ${directories} + ''; + }; + + # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work + systemd.services.postfix = { + wants = certificatesDeps; + after = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service" ++ certificatesDeps; + requires = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service"; }; }; - - # Create maildir folder before dovecot startup - systemd.services.dovecot2 = { - wants = certificatesDeps; - after = certificatesDeps; - preStart = let - directories = lib.strings.escapeShellArgs ( - [ mailDirectory ] - ++ lib.optional (cfg.indexDir != null) cfg.indexDir - ); - in '' - # Create mail directory and set permissions. See - # . - # Prevent world-readable paths, even temporarily. - umask 007 - mkdir -p ${directories} - chgrp "${vmailGroupName}" ${directories} - chmod 02770 ${directories} - ''; - }; - - # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work - systemd.services.postfix = { - wants = certificatesDeps; - after = [ "dovecot2.service" ] - ++ lib.optional cfg.dkimSigning "rspamd.service" - ++ certificatesDeps; - requires = [ "dovecot2.service" ] - ++ lib.optional cfg.dkimSigning "rspamd.service"; - }; - }; } diff --git a/mail-server/users.nix b/mail-server/users.nix index bf654c3..e9af05a 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -14,7 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: with config.mailserver; @@ -28,7 +33,6 @@ let group = vmailGroupName; }; - virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" '' #!${pkgs.stdenv.shell} @@ -46,28 +50,33 @@ let # Copy user's sieve script to the correct location (if it exists). If it # is null, remove the file. - ${lib.concatMapStringsSep "\n" ({ name, sieveScript }: - if lib.isString sieveScript then '' - if (! test -d "${sieveDirectory}/${name}"); then - mkdir -p "${sieveDirectory}/${name}" - chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}" - chmod 770 "${sieveDirectory}/${name}" - fi - cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve" - ${sieveScript} - EOF - chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve" - '' else '' - if (test -f "${sieveDirectory}/${name}/default.sieve"); then - rm "${sieveDirectory}/${name}/default.sieve" - fi - if (test -f "${sieveDirectory}/${name}.svbin"); then - rm "${sieveDirectory}/${name}/default.svbin" - fi - '') (map (user: { inherit (user) name sieveScript; }) - (lib.attrValues loginAccounts))} + ${lib.concatMapStringsSep "\n" ( + { name, sieveScript }: + if lib.isString sieveScript then + '' + if (! test -d "${sieveDirectory}/${name}"); then + mkdir -p "${sieveDirectory}/${name}" + chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}" + chmod 770 "${sieveDirectory}/${name}" + fi + cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve" + ${sieveScript} + EOF + chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve" + '' + else + '' + if (test -f "${sieveDirectory}/${name}/default.sieve"); then + rm "${sieveDirectory}/${name}/default.sieve" + fi + if (test -f "${sieveDirectory}/${name}.svbin"); then + rm "${sieveDirectory}/${name}/default.svbin" + fi + '' + ) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))} ''; -in { +in +{ config = lib.mkIf enable { # assert that all accounts provide a password assertions = map (acct: { @@ -76,15 +85,19 @@ in { }) (lib.attrValues loginAccounts); # warn for accounts that specify both password and file - warnings = map - (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used") - (lib.filter - (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) - (lib.attrValues loginAccounts)); + warnings = + map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used") + ( + lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) ( + lib.attrValues loginAccounts + ) + ); # set the vmail gid to a specific value users.groups = { - "${vmailGroupName}" = { gid = vmailUID; }; + "${vmailGroupName}" = { + gid = vmailUID; + }; }; # define all users diff --git a/shell.nix b/shell.nix index 6234bb4..493783d 100644 --- a/shell.nix +++ b/shell.nix @@ -1,10 +1,9 @@ -(import - ( - let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in - fetchTarball { - url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; - sha256 = lock.nodes.flake-compat.locked.narHash; - } - ) - { src = ./.; } -).shellNix +(import ( + let + lock = builtins.fromJSON (builtins.readFile ./flake.lock); + in + fetchTarball { + url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } +) { src = ./.; }).shellNix diff --git a/tests/clamav.nix b/tests/clamav.nix index 19b799f..209e91e 100644 --- a/tests/clamav.nix +++ b/tests/clamav.nix @@ -24,73 +24,79 @@ name = "clamav"; nodes = { - server = { pkgs, ... }: - { - imports = [ - ../default.nix - ./lib/config.nix - ]; + server = + { pkgs, ... }: + { + imports = [ + ../default.nix + ./lib/config.nix + ]; - virtualisation.memorySize = 1500; + virtualisation.memorySize = 1500; - environment.systemPackages = with pkgs; [ netcat ]; + environment.systemPackages = with pkgs; [ netcat ]; - services.rsyslogd = { - enable = true; - defaultConfig = '' - *.* /dev/console - ''; - }; - - services.clamav.updater.enable = lib.mkForce false; - systemd.services.old-clam = { - before = [ "clamav-daemon.service" ]; - requiredBy = [ "clamav-daemon.service" ]; - description = "ClamAV virus database"; - - preStart = '' - mkdir -m 0755 -p /var/lib/clamav - chown clamav:clamav /var/lib/clamav - ''; - - script = '' - cp ${blobs}/clamav/main.cvd /var/lib/clamav/ - cp ${blobs}/clamav/daily.cvd /var/lib/clamav/ - cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/ - chown clamav:clamav /var/lib/clamav/* - ''; - - serviceConfig = { - Type = "oneshot"; - PrivateTmp = "yes"; - PrivateDevices = "yes"; - }; - }; - - mailserver = { - enable = true; - fqdn = "mail.example.com"; - domains = [ "example.com" "example2.com" ]; - virusScanning = true; - - loginAccounts = { - "user1@example.com" = { - hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; - aliases = [ "postmaster@example.com" ]; - catchAll = [ "example.com" ]; - }; - "user@example2.com" = { - hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; - }; - }; - enableImap = true; - }; - - environment.etc = { - "root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; - }; + services.rsyslogd = { + enable = true; + defaultConfig = '' + *.* /dev/console + ''; }; - client = { nodes, pkgs, ... }: let + + services.clamav.updater.enable = lib.mkForce false; + systemd.services.old-clam = { + before = [ "clamav-daemon.service" ]; + requiredBy = [ "clamav-daemon.service" ]; + description = "ClamAV virus database"; + + preStart = '' + mkdir -m 0755 -p /var/lib/clamav + chown clamav:clamav /var/lib/clamav + ''; + + script = '' + cp ${blobs}/clamav/main.cvd /var/lib/clamav/ + cp ${blobs}/clamav/daily.cvd /var/lib/clamav/ + cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/ + chown clamav:clamav /var/lib/clamav/* + ''; + + serviceConfig = { + Type = "oneshot"; + PrivateTmp = "yes"; + PrivateDevices = "yes"; + }; + }; + + mailserver = { + enable = true; + fqdn = "mail.example.com"; + domains = [ + "example.com" + "example2.com" + ]; + virusScanning = true; + + loginAccounts = { + "user1@example.com" = { + hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; + aliases = [ "postmaster@example.com" ]; + catchAll = [ "example.com" ]; + }; + "user@example2.com" = { + hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; + }; + }; + enableImap = true; + }; + + environment.etc = { + "root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; + }; + }; + client = + { nodes, pkgs, ... }: + let serverIP = nodes.server.networking.primaryIPAddress; clientIP = nodes.client.networking.primaryIPAddress; grep-ip = pkgs.writeScriptBin "grep-ip" '' @@ -98,20 +104,25 @@ echo grep '${clientIP}' "$@" >&2 exec grep '${clientIP}' "$@" ''; - in { + in + { imports = [ - ./lib/config.nix + ./lib/config.nix ]; environment.systemPackages = with pkgs; [ - fetchmail msmtp procmail findutils grep-ip + fetchmail + msmtp + procmail + findutils + grep-ip ]; environment.etc = { "root/.fetchmailrc" = { text = '' - poll ${serverIP} with proto IMAP - user 'user1@example.com' there with password 'user1' is 'root' here - mda procmail + poll ${serverIP} with proto IMAP + user 'user1@example.com' there with password 'user1' is 'root' here + mda procmail ''; mode = "0700"; }; @@ -185,59 +196,59 @@ ''; }; }; - }; + }; testScript = '' - start_all() + start_all() - server.wait_for_unit("multi-user.target") - client.wait_for_unit("multi-user.target") + server.wait_for_unit("multi-user.target") + client.wait_for_unit("multi-user.target") - # TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket. - server.wait_until_succeeds( - "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" - ) - server.wait_until_succeeds( - "set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" - ) + # TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket. + server.wait_until_succeeds( + "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + ) + server.wait_until_succeeds( + "set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" + ) - client.execute("cp -p /etc/root/.* ~/") - client.succeed("mkdir -p ~/mail") - client.succeed("ls -la ~/ >&2") - client.succeed("cat ~/.fetchmailrc >&2") - client.succeed("cat ~/.procmailrc >&2") - client.succeed("cat ~/.msmtprc >&2") + client.execute("cp -p /etc/root/.* ~/") + client.succeed("mkdir -p ~/mail") + client.succeed("ls -la ~/ >&2") + client.succeed("cat ~/.fetchmailrc >&2") + client.succeed("cat ~/.procmailrc >&2") + client.succeed("cat ~/.msmtprc >&2") - # fetchmail returns EXIT_CODE 1 when no new mail - client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2") + # fetchmail returns EXIT_CODE 1 when no new mail + client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2") - # Verify that mail can be sent and received before testing virus scanner - client.execute("rm ~/mail/*") - client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2") - # give the mail server some time to process the mail - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - client.execute("rm ~/mail/*") - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v >&2") - client.execute("rm ~/mail/*") + # Verify that mail can be sent and received before testing virus scanner + client.execute("rm ~/mail/*") + client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2") + # give the mail server some time to process the mail + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + client.execute("rm ~/mail/*") + # fetchmail returns EXIT_CODE 0 when it retrieves mail + client.succeed("fetchmail --nosslcertck -v >&2") + client.execute("rm ~/mail/*") - with subtest("virus scan file"): - server.succeed( - 'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2' - ) + with subtest("virus scan file"): + server.succeed( + 'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2' + ) - with subtest("virus scan email"): - client.succeed( - 'set +o pipefail; msmtp -a user2 user1@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2' - ) - server.succeed("journalctl -u rspamd | grep -i eicar") - # give the mail server some time to process the mail - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + with subtest("virus scan email"): + client.succeed( + 'set +o pipefail; msmtp -a user2 user1@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2' + ) + server.succeed("journalctl -u rspamd | grep -i eicar") + # give the mail server some time to process the mail + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - with subtest("no warnings or errors"): - server.fail("journalctl -u postfix | grep -i error >&2") - server.fail("journalctl -u postfix | grep -i warning >&2") - server.fail("journalctl -u dovecot2 | grep -i error >&2") - server.fail("journalctl -u dovecot2 | grep -i warning >&2") - ''; + with subtest("no warnings or errors"): + server.fail("journalctl -u postfix | grep -i error >&2") + server.fail("journalctl -u postfix | grep -i warning >&2") + server.fail("journalctl -u dovecot2 | grep -i error >&2") + server.fail("journalctl -u dovecot2 | grep -i warning >&2") + ''; } diff --git a/tests/external.nix b/tests/external.nix index a65f0ce..82abb65 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -18,74 +18,84 @@ name = "external"; nodes = { - server = { pkgs, ... }: - { - imports = [ - ../default.nix - ./lib/config.nix - ]; + server = + { pkgs, ... }: + { + imports = [ + ../default.nix + ./lib/config.nix + ]; - environment.systemPackages = with pkgs; [ netcat ]; + environment.systemPackages = with pkgs; [ netcat ]; - virtualisation.memorySize = 1024; + virtualisation.memorySize = 1024; - services.rsyslogd = { - enable = true; - defaultConfig = '' - *.* /dev/console - ''; - }; - - - mailserver = { - enable = true; - debug = true; - fqdn = "mail.example.com"; - domains = [ "example.com" "example2.com" ]; - rewriteMessageId = true; - dkimKeyBits = 1535; - dmarcReporting = { - enable = true; - domain = "example.com"; - organizationName = "ACME Corp"; - }; - - loginAccounts = { - "user1@example.com" = { - hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; - aliases = [ "postmaster@example.com" ]; - catchAll = [ "example.com" ]; - }; - "user2@example.com" = { - hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; - aliases = [ "chuck@example.com" ]; - }; - "user@example2.com" = { - hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; - }; - "lowquota@example.com" = { - hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; - quota = "1B"; - }; - }; - - extraVirtualAliases = { - "single-alias@example.com" = "user1@example.com"; - "multi-alias@example.com" = [ "user1@example.com" "user2@example.com" ]; - }; - - enableImap = true; - enableImapSsl = true; - fullTextSearch = { - enable = true; - autoIndex = true; - # special use depends on https://github.com/NixOS/nixpkgs/pull/93201 - autoIndexExclude = [ (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") ]; - enforced = "yes"; - }; - }; + services.rsyslogd = { + enable = true; + defaultConfig = '' + *.* /dev/console + ''; }; - client = { nodes, pkgs, ... }: let + + mailserver = { + enable = true; + debug = true; + fqdn = "mail.example.com"; + domains = [ + "example.com" + "example2.com" + ]; + rewriteMessageId = true; + dkimKeyBits = 1535; + dmarcReporting = { + enable = true; + domain = "example.com"; + organizationName = "ACME Corp"; + }; + + loginAccounts = { + "user1@example.com" = { + hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; + aliases = [ "postmaster@example.com" ]; + catchAll = [ "example.com" ]; + }; + "user2@example.com" = { + hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; + aliases = [ "chuck@example.com" ]; + }; + "user@example2.com" = { + hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; + }; + "lowquota@example.com" = { + hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; + quota = "1B"; + }; + }; + + extraVirtualAliases = { + "single-alias@example.com" = "user1@example.com"; + "multi-alias@example.com" = [ + "user1@example.com" + "user2@example.com" + ]; + }; + + enableImap = true; + enableImapSsl = true; + fullTextSearch = { + enable = true; + autoIndex = true; + # special use depends on https://github.com/NixOS/nixpkgs/pull/93201 + autoIndexExclude = [ + (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") + ]; + enforced = "yes"; + }; + }; + }; + client = + { nodes, pkgs, ... }: + let serverIP = nodes.server.networking.primaryIPAddress; clientIP = nodes.client.networking.primaryIPAddress; grep-ip = pkgs.writeScriptBin "grep-ip" '' @@ -172,27 +182,36 @@ assert needle in repr(response) imap.close() ''; - in { + in + { imports = [ - ./lib/config.nix + ./lib/config.nix ]; environment.systemPackages = with pkgs; [ - fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham search + fetchmail + msmtp + procmail + findutils + grep-ip + check-mail-id + test-imap-spam + test-imap-ham + search ]; environment.etc = { "root/.fetchmailrc" = { text = '' - poll ${serverIP} with proto IMAP - user 'user1@example.com' there with password 'user1' is 'root' here - mda procmail + poll ${serverIP} with proto IMAP + user 'user1@example.com' there with password 'user1' is 'root' here + mda procmail ''; mode = "0700"; }; "root/.fetchmailRcLowQuota" = { text = '' - poll ${serverIP} with proto IMAP - user 'lowquota@example.com' there with password 'user2' is 'root' here - mda procmail + poll ${serverIP} with proto IMAP + user 'lowquota@example.com' there with password 'user2' is 'root' here + mda procmail ''; mode = "0700"; }; @@ -338,176 +357,176 @@ ''; }; }; - }; + }; testScript = '' - start_all() + start_all() - server.wait_for_unit("multi-user.target") - client.wait_for_unit("multi-user.target") + server.wait_for_unit("multi-user.target") + client.wait_for_unit("multi-user.target") - # TODO put this blocking into the systemd units? - server.wait_until_succeeds( - "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" - ) + # TODO put this blocking into the systemd units? + server.wait_until_succeeds( + "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + ) - client.execute("cp -p /etc/root/.* ~/") - client.succeed("mkdir -p ~/mail") - client.succeed("ls -la ~/ >&2") - client.succeed("cat ~/.fetchmailrc >&2") - client.succeed("cat ~/.procmailrc >&2") - client.succeed("cat ~/.msmtprc >&2") + client.execute("cp -p /etc/root/.* ~/") + client.succeed("mkdir -p ~/mail") + client.succeed("ls -la ~/ >&2") + client.succeed("cat ~/.fetchmailrc >&2") + client.succeed("cat ~/.procmailrc >&2") + client.succeed("cat ~/.msmtprc >&2") - with subtest("imap retrieving mail"): - # fetchmail returns EXIT_CODE 1 when no new mail - client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2") + with subtest("imap retrieving mail"): + # fetchmail returns EXIT_CODE 1 when no new mail + client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2") - with subtest("submission port send mail"): - # send email from user2 to user1 - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2" - ) - # give the mail server some time to process the mail - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + with subtest("submission port send mail"): + # send email from user2 to user1 + client.succeed( + "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2" + ) + # give the mail server some time to process the mail + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - with subtest("imap retrieving mail 2"): - client.execute("rm ~/mail/*") - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v >&2") + with subtest("imap retrieving mail 2"): + client.execute("rm ~/mail/*") + # fetchmail returns EXIT_CODE 0 when it retrieves mail + client.succeed("fetchmail --nosslcertck -v >&2") - with subtest("remove sensitive information on submission port"): - client.succeed("cat ~/mail/* >&2") - ## make sure our IP is _not_ in the email header - client.fail("grep-ip ~/mail/*") - client.succeed("check-mail-id ~/mail/*") + with subtest("remove sensitive information on submission port"): + client.succeed("cat ~/mail/* >&2") + ## make sure our IP is _not_ in the email header + client.fail("grep-ip ~/mail/*") + client.succeed("check-mail-id ~/mail/*") - with subtest("have correct fqdn as sender"): - client.succeed("grep 'Received: from mail.example.com' ~/mail/*") + with subtest("have correct fqdn as sender"): + client.succeed("grep 'Received: from mail.example.com' ~/mail/*") - with subtest("dkim has user-specified size"): - server.succeed( - "openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'" - ) + with subtest("dkim has user-specified size"): + server.succeed( + "openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'" + ) - with subtest("dkim singing, multiple domains"): - client.execute("rm ~/mail/*") - # send email from user2 to user1 - client.succeed( - "msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") - client.succeed("cat ~/mail/* >&2") - # make sure it is dkim signed - client.succeed("grep DKIM-Signature: ~/mail/*") + with subtest("dkim singing, multiple domains"): + client.execute("rm ~/mail/*") + # send email from user2 to user1 + client.succeed( + "msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email2 >&2" + ) + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + # fetchmail returns EXIT_CODE 0 when it retrieves mail + client.succeed("fetchmail --nosslcertck -v") + client.succeed("cat ~/mail/* >&2") + # make sure it is dkim signed + client.succeed("grep DKIM-Signature: ~/mail/*") - with subtest("aliases"): - client.execute("rm ~/mail/*") - # send email from chuck to postmaster - client.succeed( - "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") + with subtest("aliases"): + client.execute("rm ~/mail/*") + # send email from chuck to postmaster + client.succeed( + "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster@example.com < /etc/root/email2 >&2" + ) + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + # fetchmail returns EXIT_CODE 0 when it retrieves mail + client.succeed("fetchmail --nosslcertck -v") - with subtest("catchAlls"): - client.execute("rm ~/mail/*") - # send email from chuck to non exsitent account - client.succeed( - "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") + with subtest("catchAlls"): + client.execute("rm ~/mail/*") + # send email from chuck to non exsitent account + client.succeed( + "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol@example.com < /etc/root/email2 >&2" + ) + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + # fetchmail returns EXIT_CODE 0 when it retrieves mail + client.succeed("fetchmail --nosslcertck -v") - client.execute("rm ~/mail/*") - # send email from user1 to chuck - client.succeed( - "msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 1 when no new mail - # if this succeeds, it means that user1 recieved the mail that was intended for chuck. - client.fail("fetchmail --nosslcertck -v") + client.execute("rm ~/mail/*") + # send email from user1 to chuck + client.succeed( + "msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck@example.com < /etc/root/email2 >&2" + ) + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + # fetchmail returns EXIT_CODE 1 when no new mail + # if this succeeds, it means that user1 recieved the mail that was intended for chuck. + client.fail("fetchmail --nosslcertck -v") - with subtest("extraVirtualAliases"): - client.execute("rm ~/mail/*") - # send email from single-alias to user1 - client.succeed( - "msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email4 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") + with subtest("extraVirtualAliases"): + client.execute("rm ~/mail/*") + # send email from single-alias to user1 + client.succeed( + "msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email4 >&2" + ) + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + # fetchmail returns EXIT_CODE 0 when it retrieves mail + client.succeed("fetchmail --nosslcertck -v") - client.execute("rm ~/mail/*") - # send email from user1 to multi-alias (user{1,2}@example.com) - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias@example.com < /etc/root/email5 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") + client.execute("rm ~/mail/*") + # send email from user1 to multi-alias (user{1,2}@example.com) + client.succeed( + "msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias@example.com < /etc/root/email5 >&2" + ) + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + # fetchmail returns EXIT_CODE 0 when it retrieves mail + client.succeed("fetchmail --nosslcertck -v") - with subtest("quota"): - client.execute("rm ~/mail/*") - client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc") + with subtest("quota"): + client.execute("rm ~/mail/*") + client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc") - client.succeed( - "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.fail("fetchmail --nosslcertck -v") + client.succeed( + "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota@example.com < /etc/root/email2 >&2" + ) + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + # fetchmail returns EXIT_CODE 0 when it retrieves mail + client.fail("fetchmail --nosslcertck -v") - with subtest("imap sieve junk trainer"): - # send email from user2 to user1 - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2" - ) - # give the mail server some time to process the mail - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + with subtest("imap sieve junk trainer"): + # send email from user2 to user1 + client.succeed( + "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2" + ) + # give the mail server some time to process the mail + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - client.succeed("imap-mark-spam >&2") - server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-spam.sh >&2") - client.succeed("imap-mark-ham >&2") - server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-ham.sh >&2") + client.succeed("imap-mark-spam >&2") + server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-spam.sh >&2") + client.succeed("imap-mark-ham >&2") + server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-ham.sh >&2") - with subtest("full text search and indexation"): - # send 2 email from user2 to user1 - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email6 >&2" - ) - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email7 >&2" - ) - # give the mail server some time to process the mail - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') + with subtest("full text search and indexation"): + # send 2 email from user2 to user1 + client.succeed( + "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email6 >&2" + ) + client.succeed( + "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email7 >&2" + ) + # give the mail server some time to process the mail + server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # should find exactly one email containing this - client.succeed("search INBOX 576a4565b70f5a4c1a0925cabdb587a6 >&2") - # should fail because this folder is not indexed - client.fail("search Junk a >&2") - # check that search really goes through the indexer - server.succeed("journalctl -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2") - # check that Junk is not indexed - server.fail("journalctl -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2") + # should find exactly one email containing this + client.succeed("search INBOX 576a4565b70f5a4c1a0925cabdb587a6 >&2") + # should fail because this folder is not indexed + client.fail("search Junk a >&2") + # check that search really goes through the indexer + server.succeed("journalctl -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2") + # check that Junk is not indexed + server.fail("journalctl -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2") - with subtest("dmarc reporting"): - server.systemctl("start rspamd-dmarc-reporter.service") + with subtest("dmarc reporting"): + server.systemctl("start rspamd-dmarc-reporter.service") - with subtest("no warnings or errors"): - server.fail("journalctl -u postfix | grep -i error >&2") - server.fail("journalctl -u postfix | grep -i warning >&2") - server.fail("journalctl -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2") - # harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html - server.fail( - "journalctl -u dovecot2 | \ - grep -v 'Expunged message reappeared, giving a new UID' | \ - grep -v 'Time moved forwards' | \ - grep -i warning >&2" - ) - ''; + with subtest("no warnings or errors"): + server.fail("journalctl -u postfix | grep -i error >&2") + server.fail("journalctl -u postfix | grep -i warning >&2") + server.fail("journalctl -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2") + # harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html + server.fail( + "journalctl -u dovecot2 | \ + grep -v 'Expunged message reappeared, giving a new UID' | \ + grep -v 'Time moved forwards' | \ + grep -i warning >&2" + ) + ''; } diff --git a/tests/internal.nix b/tests/internal.nix index 8f47e70..af552c3 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -30,11 +30,16 @@ let ''; }; - hashPassword = password: pkgs.runCommand - "password-${password}-hashed" - { buildInputs = [ pkgs.mkpasswd ]; inherit password; } '' - mkpasswd -sm bcrypt <<<"$password" > $out - ''; + hashPassword = + password: + pkgs.runCommand "password-${password}-hashed" + { + buildInputs = [ pkgs.mkpasswd ]; + inherit password; + } + '' + mkpasswd -sm bcrypt <<<"$password" > $out + ''; hashedPasswordFile = hashPassword "my-password"; passwordFile = pkgs.writeText "password" "my-password"; @@ -43,55 +48,62 @@ in name = "internal"; nodes = { - machine = { pkgs, ... }: { - imports = [ - ./../default.nix - ./lib/config.nix - ]; + machine = + { pkgs, ... }: + { + imports = [ + ./../default.nix + ./lib/config.nix + ]; - virtualisation.memorySize = 1024; + virtualisation.memorySize = 1024; - environment.systemPackages = [ - (pkgs.writeScriptBin "mail-check" '' - ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ - '') - ] ++ (with pkgs; [ - curl - openssl - netcat - ]); + environment.systemPackages = + [ + (pkgs.writeScriptBin "mail-check" '' + ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ + '') + ] + ++ (with pkgs; [ + curl + openssl + netcat + ]); - mailserver = { - enable = true; - fqdn = "mail.example.com"; - domains = [ "example.com" "domain.com" ]; - localDnsResolver = false; + mailserver = { + enable = true; + fqdn = "mail.example.com"; + domains = [ + "example.com" + "domain.com" + ]; + localDnsResolver = false; - loginAccounts = { - "user1@example.com" = { - hashedPasswordFile = hashedPasswordFile; + loginAccounts = { + "user1@example.com" = { + hashedPasswordFile = hashedPasswordFile; + }; + "user2@example.com" = { + hashedPasswordFile = hashedPasswordFile; + aliasesRegexp = [ ''/^user2.*@domain\.com$/'' ]; + }; + "send-only@example.com" = { + hashedPasswordFile = hashPassword "send-only"; + sendOnly = true; + }; }; - "user2@example.com" = { - hashedPasswordFile = hashedPasswordFile; - aliasesRegexp = [''/^user2.*@domain\.com$/'']; - }; - "send-only@example.com" = { - hashedPasswordFile = hashPassword "send-only"; - sendOnly = true; + forwards = { + # user2@example.com is a local account and its mails are + # also forwarded to user1@example.com + "user2@example.com" = "user1@example.com"; }; + + vmailGroupName = "vmail"; + vmailUID = 5000; + + enableImap = false; }; - forwards = { - # user2@example.com is a local account and its mails are - # also forwarded to user1@example.com - "user2@example.com" = "user1@example.com"; - }; - - vmailGroupName = "vmail"; - vmailUID = 5000; - - enableImap = false; }; - }; }; testScript = '' machine.start() diff --git a/tests/ldap.nix b/tests/ldap.nix index 8187d7d..1c92572 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -7,110 +7,113 @@ in name = "ldap"; nodes = { - machine = { pkgs, ... }: { - imports = [ - ./../default.nix - ./lib/config.nix - ]; + machine = + { pkgs, ... }: + { + imports = [ + ./../default.nix + ./lib/config.nix + ]; - virtualisation.memorySize = 1024; + virtualisation.memorySize = 1024; - services.openssh = { - enable = true; - settings.PermitRootLogin = "yes"; - }; + services.openssh = { + enable = true; + settings.PermitRootLogin = "yes"; + }; - environment.systemPackages = [ - (pkgs.writeScriptBin "mail-check" '' - ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ - '')]; + environment.systemPackages = [ + (pkgs.writeScriptBin "mail-check" '' + ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ + '') + ]; - environment.etc.bind-password.text = bindPassword; + environment.etc.bind-password.text = bindPassword; - services.openldap = { - enable = true; - settings = { - children = { - "cn=schema".includes = [ - "${pkgs.openldap}/etc/schema/core.ldif" - "${pkgs.openldap}/etc/schema/cosine.ldif" - "${pkgs.openldap}/etc/schema/inetorgperson.ldif" - "${pkgs.openldap}/etc/schema/nis.ldif" - ]; - "olcDatabase={1}mdb" = { - attrs = { - objectClass = [ - "olcDatabaseConfig" - "olcMdbConfig" - ]; - olcDatabase = "{1}mdb"; - olcDbDirectory = "/var/lib/openldap/example"; - olcSuffix = "dc=example"; + services.openldap = { + enable = true; + settings = { + children = { + "cn=schema".includes = [ + "${pkgs.openldap}/etc/schema/core.ldif" + "${pkgs.openldap}/etc/schema/cosine.ldif" + "${pkgs.openldap}/etc/schema/inetorgperson.ldif" + "${pkgs.openldap}/etc/schema/nis.ldif" + ]; + "olcDatabase={1}mdb" = { + attrs = { + objectClass = [ + "olcDatabaseConfig" + "olcMdbConfig" + ]; + olcDatabase = "{1}mdb"; + olcDbDirectory = "/var/lib/openldap/example"; + olcSuffix = "dc=example"; + }; }; }; }; + declarativeContents."dc=example" = '' + dn: dc=example + objectClass: domain + dc: example + + dn: cn=mail,dc=example + objectClass: organizationalRole + objectClass: simpleSecurityObject + objectClass: top + cn: mail + userPassword: ${bindPassword} + + dn: ou=users,dc=example + objectClass: organizationalUnit + ou: users + + dn: cn=alice,ou=users,dc=example + objectClass: inetOrgPerson + cn: alice + sn: Foo + mail: alice@example.com + userPassword: ${alicePassword} + + dn: cn=bob,ou=users,dc=example + objectClass: inetOrgPerson + cn: bob + sn: Bar + mail: bob@example.com + userPassword: ${bobPassword} + ''; }; - declarativeContents."dc=example" = '' - dn: dc=example - objectClass: domain - dc: example - dn: cn=mail,dc=example - objectClass: organizationalRole - objectClass: simpleSecurityObject - objectClass: top - cn: mail - userPassword: ${bindPassword} - - dn: ou=users,dc=example - objectClass: organizationalUnit - ou: users - - dn: cn=alice,ou=users,dc=example - objectClass: inetOrgPerson - cn: alice - sn: Foo - mail: alice@example.com - userPassword: ${alicePassword} - - dn: cn=bob,ou=users,dc=example - objectClass: inetOrgPerson - cn: bob - sn: Bar - mail: bob@example.com - userPassword: ${bobPassword} - ''; - }; - - mailserver = { - enable = true; - fqdn = "mail.example.com"; - domains = [ "example.com" ]; - localDnsResolver = false; - - ldap = { + mailserver = { enable = true; - uris = [ - "ldap://" - ]; - bind = { - dn = "cn=mail,dc=example"; - passwordFile = "/etc/bind-password"; + fqdn = "mail.example.com"; + domains = [ "example.com" ]; + localDnsResolver = false; + + ldap = { + enable = true; + uris = [ + "ldap://" + ]; + bind = { + dn = "cn=mail,dc=example"; + passwordFile = "/etc/bind-password"; + }; + searchBase = "ou=users,dc=example"; + searchScope = "sub"; }; - searchBase = "ou=users,dc=example"; - searchScope = "sub"; + + forwards = { + "bob_fw@example.com" = "bob@example.com"; + }; + + vmailGroupName = "vmail"; + vmailUID = 5000; + + enableImap = false; }; - - forwards = { - "bob_fw@example.com" = "bob@example.com"; - }; - - vmailGroupName = "vmail"; - vmailUID = 5000; - - enableImap = false; }; - }; }; testScript = '' import sys diff --git a/tests/multiple.nix b/tests/multiple.nix index 3e71cd6..2c6d0fc 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -6,16 +6,23 @@ }: let - hashPassword = password: pkgs.runCommand - "password-${password}-hashed" - { buildInputs = [ pkgs.mkpasswd ]; inherit password; } + hashPassword = + password: + pkgs.runCommand "password-${password}-hashed" + { + buildInputs = [ pkgs.mkpasswd ]; + inherit password; + } '' mkpasswd -sm bcrypt <<<"$password" > $out ''; - password = pkgs.writeText "password" "password"; + password = pkgs.writeText "password" "password"; - domainGenerator = domain: { pkgs, ... }: { + domainGenerator = + domain: + { pkgs, ... }: + { imports = [ ../default.nix ./lib/config.nix @@ -37,7 +44,10 @@ let }; services.dnsmasq = { enable = true; - settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ]; + settings.mx-host = [ + "domain1.com,domain1,10" + "domain2.com,domain2,10" + ]; }; }; @@ -47,23 +57,34 @@ in name = "multiple"; nodes = { - domain1 = {...}: { - imports = [ - ../default.nix - (domainGenerator "domain1.com") - ]; - mailserver.forwards = { - "non-local@domain1.com" = ["user@domain2.com" "user@domain1.com"]; - "non@domain1.com" = ["user@domain2.com" "user@domain1.com"]; + domain1 = + { ... }: + { + imports = [ + ../default.nix + (domainGenerator "domain1.com") + ]; + mailserver.forwards = { + "non-local@domain1.com" = [ + "user@domain2.com" + "user@domain1.com" + ]; + "non@domain1.com" = [ + "user@domain2.com" + "user@domain1.com" + ]; + }; }; - }; domain2 = domainGenerator "domain2.com"; - client = { pkgs, ... }: { - environment.systemPackages = [ - (pkgs.writeScriptBin "mail-check" '' - ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ - '')]; - }; + client = + { pkgs, ... }: + { + environment.systemPackages = [ + (pkgs.writeScriptBin "mail-check" '' + ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ + '') + ]; + }; }; testScript = '' start_all()