From 37376efbbf44ac5accaef84ccbbebbdfdc02e95d Mon Sep 17 00:00:00 2001 From: Luflosi Date: Sun, 23 May 2021 20:15:38 +0200 Subject: [PATCH 001/225] docs: link to an english Wikipedia article instead of a french one --- docs/setup-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index c2c3ca0..7e6137e 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -131,7 +131,7 @@ Note that it can take a while until a DNS entry is propagated. Set a ``SPF`` record ^^^^^^^^^^^^^^^^^^^^ -Add a `SPF `_ +Add a `SPF `_ record to the domain ``example.com``. ================ ===== ==== ================================ From 2eab26e05c13aa4274daa21f98ddef8366627056 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Thu, 27 May 2021 08:59:38 +0200 Subject: [PATCH 002/225] Switch from Freenode to Libera --- docs/howto-develop.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index 57afee3..b12c9b9 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -4,7 +4,7 @@ Contribute or troubleshoot To report an issue, please go to ``_. -You can also chat with us on the Freenode IRC channel ``#nixos-mailserver``. +You can also chat with us on the Libera IRC channel ``#nixos-mailserver``. Run NixOS tests --------------- From 500685bc38507a4e5c19f86f0a9782191f5148e4 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Thu, 27 May 2021 23:03:20 +0200 Subject: [PATCH 003/225] hydra: remove useless declInput argument --- .hydra/declarative-jobsets.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 29750a4..7a09a07 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -1,4 +1,4 @@ -{ nixpkgs, declInput, pulls }: +{ nixpkgs, pulls, ... }: let pkgs = import nixpkgs {}; From 190ac7ca60c6baed47c966d207874171ab112ce6 Mon Sep 17 00:00:00 2001 From: Evan Hanson Date: Mon, 31 May 2021 18:29:11 +1200 Subject: [PATCH 004/225] Remove duplicate `default` attribute on mailserver.forwards option --- default.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/default.nix b/default.nix index 8905c5a..dd800b1 100644 --- a/default.nix +++ b/default.nix @@ -318,7 +318,6 @@ in forwards = mkOption { type = with types; attrsOf (either (listOf str) str); - default = {}; example = { "user@example.com" = "user@elsewhere.com"; }; From 2ca02f32c828c3b1dd27f3e290102b283edfdd26 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Mon, 31 May 2021 09:53:52 +0200 Subject: [PATCH 005/225] hydra: provide nixpkgs to allow Niv to use pkgs.fetchzip --- .hydra/declarative-jobsets.nix | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 7a09a07..b1d0628 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -17,6 +17,13 @@ let keepnr = 1; type = 0; inputs = { + # This is only used to allow Niv to use pkgs.fetchzip which is + # required because of Hydra restricted evaluation mode. + nixpkgs = { + value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; + type = "git"; + emailresponsible = false; + }; snm = { type = "git"; value = "${info.target_repo_url} merge-requests/${info.iid}/head"; @@ -40,6 +47,13 @@ let hidden = false; type = 0; inputs = { + # This is only used to allow Niv to use pkgs.fetchzip which is + # required because of Hydra restricted evaluation mode. + nixpkgs = { + value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; + type = "git"; + emailresponsible = false; + }; snm = { value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver master"; type = "git"; @@ -80,6 +94,13 @@ let hidden = false; type = 0; inputs = { + # This is only used to allow Niv to use pkgs.fetchzip which is + # required because of Hydra restricted evaluation mode. + nixpkgs = { + value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; + type = "git"; + emailresponsible = false; + }; snm = { value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver nixos-20.09"; type = "git"; From 49074b7835c6d40846cb23953c04a03e1d2a4a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Tue, 25 May 2021 18:35:14 +0200 Subject: [PATCH 006/225] kresd: no need to explicitly set nameserver Since https://github.com/NixOS/nixpkgs/pull/124391, enabling kreasd also sets `networking.resolvconf.useLocalResolver = true`. --- mail-server/kresd.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/mail-server/kresd.nix b/mail-server/kresd.nix index 1694eca..e3baa07 100644 --- a/mail-server/kresd.nix +++ b/mail-server/kresd.nix @@ -22,7 +22,6 @@ in { config = lib.mkIf (cfg.enable && cfg.localDnsResolver) { services.kresd.enable = true; - networking.nameservers = [ "127.0.0.1" ]; }; } From 3fc047bc64bee2a16e34393b7a06be788d13c37f Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 30 May 2021 21:43:05 +0200 Subject: [PATCH 007/225] Remove nixos-20.03 job We only support 2 releases. --- .hydra/declarative-jobsets.nix | 20 -------------------- README.md | 3 --- 2 files changed, 23 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index b1d0628..f96d5ba 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -61,26 +61,6 @@ let }; }; }; - "nixos-20.03" = { - description = "Build the nixos-20.03 branch of Simple NixOS MailServer"; - checkinterval = "60"; - enabled = "1"; - nixexprinput = "snm"; - nixexprpath = ".hydra/default.nix"; - schedulingshares = 100; - enableemail = false; - emailoverride = ""; - keepnr = 3; - hidden = false; - type = 0; - inputs = { - snm = { - value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver nixos-20.03"; - type = "git"; - emailresponsible = false; - }; - }; - }; "nixos-20.09" = { description = "Build the nixos-20.09 branch of Simple NixOS MailServer"; checkinterval = "60"; diff --git a/README.md b/README.md index 561b55b..e680154 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,6 @@ SNM branch corresponding to your NixOS version. - Use the [SNM branch `nixos-20.09`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.09) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/release-notes.html#nixos-20-09) -* For NixOS 20.03 - - Use the [SNM branch `nixos-20.03`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.03) - - [Release notes](#nixos-2003) * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) From ddafdfbde7f24a79cde2dae78a187b294e93f253 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Mon, 31 May 2021 09:48:15 +0200 Subject: [PATCH 008/225] Make Niv working in restricted evaluation mode --- nix/sources.nix | 3 --- 1 file changed, 3 deletions(-) diff --git a/nix/sources.nix b/nix/sources.nix index 8a725cb..ea0163b 100644 --- a/nix/sources.nix +++ b/nix/sources.nix @@ -13,9 +13,6 @@ let pkgs.fetchurl { inherit (spec) url sha256; }; fetch_tarball = pkgs: spec: - if spec.builtin or true then - builtins_fetchTarball { inherit (spec) url sha256; } - else pkgs.fetchzip { inherit (spec) url sha256; }; fetch_git = spec: From bbcc6863b549b5e0348d826544c52d035d26acf4 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 30 May 2021 22:07:53 +0200 Subject: [PATCH 009/225] Release nixos-21.05 --- .hydra/declarative-jobsets.nix | 27 +++++++++++++++++++++++++++ README.md | 8 ++++++-- docs/release-notes.rst | 10 ++++++++++ nix/sources.json | 12 ++++++++++++ tests/default.nix | 1 + 5 files changed, 56 insertions(+), 2 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index f96d5ba..43c9fa8 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -88,6 +88,33 @@ let }; }; }; + "nixos-21.05" = { + description = "Build the nixos-21.05 branch of Simple NixOS MailServer"; + checkinterval = "60"; + enabled = "1"; + nixexprinput = "snm"; + nixexprpath = ".hydra/default.nix"; + schedulingshares = 100; + enableemail = false; + emailoverride = ""; + keepnr = 3; + hidden = false; + type = 0; + inputs = { + # This is only used to allow Niv to use pkgs.fetchzip which is + # required because of Hydra restricted evaluation mode. + nixpkgs = { + value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; + type = "git"; + emailresponsible = false; + }; + snm = { + value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver master"; + type = "git"; + emailresponsible = false; + }; + }; + }; }; in { diff --git a/README.md b/README.md index e680154..4ef7d2a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. +* For NixOS 21.05 + - Use the [SNM branch `nixos-21.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-21.05) + - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-21.05/) + - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-21.05/release-notes.html#nixos-21-05) * For NixOS 20.09 - Use the [SNM branch `nixos-20.09`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.09) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/) @@ -15,7 +19,7 @@ SNM branch corresponding to your NixOS version. * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) - - This branch is currently supporting the NixOS release 20.09 but + - This branch is currently supporting the NixOS release 21.05 but we could remove this support on any NixOS unstable breaking change. @@ -79,7 +83,7 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A ```nix { config, pkgs, ... }: - let release = "nixos-20.09"; + let release = "nixos-21.05"; in { imports = [ (builtins.fetchTarball { diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 561a40c..fce2abf 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,6 +1,16 @@ Release Notes ============= +NixOS 21.05 +----------- + +- New `fullTextSearch` option to search in messages (based on Xapian) + (`Merge Request `__) +- Flake support + (`Merge Request `__) +- New `openFirewall` option defaulting to `true` +- We moved from Freenode to Libera Chat + NixOS 20.09 ----------- diff --git a/nix/sources.json b/nix/sources.json index 2d2d79a..1edeea8 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -18,6 +18,18 @@ "url": "https://github.com/NixOS/nixpkgs/archive/b6eefa48d8e10491e43c0c6155ac12b463f6fed3.tar.gz", "url_template": "https://github.com///archive/.tar.gz" }, + "nixpkgs-21.05": { + "branch": "release-21.05", + "description": "Nix Packages collection", + "homepage": "", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "3776ceb792907ddebd7d2603778c7f106fabfb08", + "sha256": "10wmmnf5scmrnf1knc31yhwlrq720n3k26j44daqal4shqxhr2vk", + "type": "tarball", + "url": "https://github.com/nixos/nixpkgs/archive/3776ceb792907ddebd7d2603778c7f106fabfb08.tar.gz", + "url_template": "https://github.com///archive/.tar.gz" + }, "nixpkgs-unstable": { "branch": "nixos-unstable", "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", diff --git a/tests/default.nix b/tests/default.nix index 10aba18..68f2053 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -29,6 +29,7 @@ let releaseNames = [ "nixpkgs-unstable" "nixpkgs-20.09" + "nixpkgs-21.05" ]; testNames = [ From 92a09398964f773d313d9e2a0e02a71ef5811fab Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 6 Jun 2021 09:52:00 +0200 Subject: [PATCH 010/225] ci: simplify declarative-jobsets.nix --- .hydra/declarative-jobsets.nix | 111 +++++++++------------------------ 1 file changed, 30 insertions(+), 81 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 43c9fa8..7d36ea6 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -32,89 +32,38 @@ let }; } ) prs; + mkJobset = branch: { + description = "Build ${branch} branch of Simple NixOS MailServer"; + checkinterval = "60"; + enabled = "1"; + nixexprinput = "snm"; + nixexprpath = ".hydra/default.nix"; + schedulingshares = 100; + enableemail = false; + emailoverride = ""; + keepnr = 3; + hidden = false; + type = 0; + inputs = { + # This is only used to allow Niv to use pkgs.fetchzip which is + # required because of Hydra restricted evaluation mode. + nixpkgs = { + value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; + type = "git"; + emailresponsible = false; + }; + snm = { + value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver ${branch}"; + type = "git"; + emailresponsible = false; + }; + }; + }; desc = prJobsets // { - master = { - description = "Build master branch of Simple NixOS MailServer"; - checkinterval = "60"; - enabled = "1"; - nixexprinput = "snm"; - nixexprpath = ".hydra/default.nix"; - schedulingshares = 100; - enableemail = false; - emailoverride = ""; - keepnr = 3; - hidden = false; - type = 0; - inputs = { - # This is only used to allow Niv to use pkgs.fetchzip which is - # required because of Hydra restricted evaluation mode. - nixpkgs = { - value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; - type = "git"; - emailresponsible = false; - }; - snm = { - value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver master"; - type = "git"; - emailresponsible = false; - }; - }; - }; - "nixos-20.09" = { - description = "Build the nixos-20.09 branch of Simple NixOS MailServer"; - checkinterval = "60"; - enabled = "1"; - nixexprinput = "snm"; - nixexprpath = ".hydra/default.nix"; - schedulingshares = 100; - enableemail = false; - emailoverride = ""; - keepnr = 3; - hidden = false; - type = 0; - inputs = { - # This is only used to allow Niv to use pkgs.fetchzip which is - # required because of Hydra restricted evaluation mode. - nixpkgs = { - value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; - type = "git"; - emailresponsible = false; - }; - snm = { - value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver nixos-20.09"; - type = "git"; - emailresponsible = false; - }; - }; - }; - "nixos-21.05" = { - description = "Build the nixos-21.05 branch of Simple NixOS MailServer"; - checkinterval = "60"; - enabled = "1"; - nixexprinput = "snm"; - nixexprpath = ".hydra/default.nix"; - schedulingshares = 100; - enableemail = false; - emailoverride = ""; - keepnr = 3; - hidden = false; - type = 0; - inputs = { - # This is only used to allow Niv to use pkgs.fetchzip which is - # required because of Hydra restricted evaluation mode. - nixpkgs = { - value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; - type = "git"; - emailresponsible = false; - }; - snm = { - value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver master"; - type = "git"; - emailresponsible = false; - }; - }; - }; + "master" = mkJobset "master"; + "nixos-20.09" = mkJobset "nixos-20.09"; + "nixos-21.05" = mkJobset "nixos-21.05"; }; in { From 5675b122a947b40e551438df6a623efad19fd2e7 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 6 Jun 2021 10:21:14 +0200 Subject: [PATCH 011/225] readme: switch from freenode to libera --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ef7d2a..13df600 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A ### Get in touch - Subscribe to the [mailing list](https://www.freelists.org/archive/snm/) -- Join the Freenode IRC channel `#nixos-mailserver` +- Join the Libera Chat IRC channel `#nixos-mailserver` ### Quick Start From a9f87ca461a1f0e23e85a1ba8c696672ed9e11e2 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Thu, 24 Jun 2021 21:29:23 +0200 Subject: [PATCH 012/225] Update nixpkgs-unstable Because of https://github.com/NixOS/nixpkgs/commit/b7749c76715ba96727f7a12bc2514ddfa6847813 we need to `set +o pipefail` several asserts. --- nix/sources.json | 6 +++--- tests/clamav.nix | 8 ++++---- tests/external.nix | 6 +++--- tests/internal.nix | 4 ++-- tests/multiple.nix | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/nix/sources.json b/nix/sources.json index 1edeea8..a669d85 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -36,10 +36,10 @@ "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e5cc06a1e806070693add4f231060a62b962fc44", - "sha256": "04543i332fx9m7jf6167ac825s4qb8is0d0x0pz39il979mlc87v", + "rev": "1905f5f2e55e0db0bb6244cfe62cb6c0dbda391d", + "sha256": "148f79hhya66qj8v5gn7bs6zrfjy1nbvdciyxdm4yd5p8r6ayzv6", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/e5cc06a1e806070693add4f231060a62b962fc44.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/1905f5f2e55e0db0bb6244cfe62cb6c0dbda391d.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/tests/clamav.nix b/tests/clamav.nix index f62df58..f818991 100644 --- a/tests/clamav.nix +++ b/tests/clamav.nix @@ -196,10 +196,10 @@ pkgs.nixosTest { # TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket. server.wait_until_succeeds( - "timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) server.wait_until_succeeds( - "timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" ) client.execute("cp -p /etc/root/.* ~/") @@ -224,12 +224,12 @@ pkgs.nixosTest { with subtest("virus scan file"): server.succeed( - 'clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2' + '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( - 'msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2' + '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 diff --git a/tests/external.nix b/tests/external.nix index f453608..55d9eb5 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -345,7 +345,7 @@ pkgs.nixosTest { # TODO put this blocking into the systemd units? server.wait_until_succeeds( - "timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) client.execute("cp -p /etc/root/.* ~/") @@ -489,11 +489,11 @@ pkgs.nixosTest { client.fail("search Junk a >&2") # check that search really goes through the indexer server.succeed( - "journalctl -u dovecot2 | grep -E 'indexer-worker.*Indexed . messages in INBOX' >&2" + "journalctl -u dovecot2 | grep -E 'indexer-worker.* Mailbox INBOX: Indexed' >&2" ) # check that Junk is not indexed server.fail( - "journalctl -u dovecot2 | grep -E 'indexer-worker.*Indexed . messages in Junk' >&2" + "journalctl -u dovecot2 | grep -E 'indexer-worker.* Mailbox JUNK: Indexed' >&2" ) with subtest("no warnings or errors"): diff --git a/tests/internal.nix b/tests/internal.nix index 12590c0..c64339a 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -133,7 +133,7 @@ pkgs.nixosTest { machine.wait_for_open_port(25) # TODO put this blocking into the systemd units machine.wait_until_succeeds( - "timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) machine.succeed( "cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q 'This account cannot receive emails'" @@ -141,7 +141,7 @@ pkgs.nixosTest { with subtest("rspamd controller serves web ui"): machine.succeed( - "${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q ''" + "set +o pipefail; ${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q ''" ) with subtest("imap port 143 is closed and imaps is serving SSL"): diff --git a/tests/multiple.nix b/tests/multiple.nix index 9f54cff..3377906 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -68,10 +68,10 @@ pkgs.nixosTest { # TODO put this blocking into the systemd units? domain1.wait_until_succeeds( - "timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) domain2.wait_until_succeeds( - "timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) # user@domain1.com sends a mail to user@domain2.com From a0f9688a31dd9f4b6f9d252ba06f8d1acef914af Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sat, 10 Jul 2021 16:05:25 +0200 Subject: [PATCH 013/225] Switch CI to Nix flakes We also move tests to Flakes. This would allow users to submit PRs with a fork of nixpkgs when they want to test nixpkgs PRs against SNM. --- .hydra/declarative-jobsets.nix | 48 +++++++++---------------------- docs/howto-develop.rst | 18 +++++++++--- flake.lock | 33 +++++++++++++++++++++ flake.nix | 52 ++++++++++++++++++++++++++++++---- tests/clamav.nix | 9 +----- tests/default.nix | 48 ------------------------------- tests/external.nix | 2 +- tests/internal.nix | 2 +- tests/multiple.nix | 2 +- 9 files changed, 111 insertions(+), 103 deletions(-) delete mode 100644 tests/default.nix diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 7d36ea6..2159de7 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -8,56 +8,26 @@ let { enabled = 1; hidden = false; description = "PR ${num}: ${info.title}"; - nixexprinput = "snm"; - nixexprpath = ".hydra/default.nix"; checkinterval = 30; schedulingshares = 20; enableemail = false; emailoverride = ""; keepnr = 1; - type = 0; - inputs = { - # This is only used to allow Niv to use pkgs.fetchzip which is - # required because of Hydra restricted evaluation mode. - nixpkgs = { - value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; - type = "git"; - emailresponsible = false; - }; - snm = { - type = "git"; - value = "${info.target_repo_url} merge-requests/${info.iid}/head"; - emailresponsible = false; - }; - }; + type = 1; + flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head"; } ) prs; mkJobset = branch: { description = "Build ${branch} branch of Simple NixOS MailServer"; checkinterval = "60"; enabled = "1"; - nixexprinput = "snm"; - nixexprpath = ".hydra/default.nix"; schedulingshares = 100; enableemail = false; emailoverride = ""; keepnr = 3; hidden = false; - type = 0; - inputs = { - # This is only used to allow Niv to use pkgs.fetchzip which is - # required because of Hydra restricted evaluation mode. - nixpkgs = { - value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; - type = "git"; - emailresponsible = false; - }; - snm = { - value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver ${branch}"; - type = "git"; - emailresponsible = false; - }; - }; + type = 1; + flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/${branch}"; }; desc = prJobsets // { @@ -66,10 +36,20 @@ let "nixos-21.05" = mkJobset "nixos-21.05"; }; + log = { + pulls = prs; + jobsets = desc; + }; + in { jobsets = pkgs.runCommand "spec-jobsets.json" {} '' cat >$out <tmp <`. + +You can then run the testsuite via :: - $ nix-build tests -A external.nixpkgs_20_03 - $ nix-build tests -A internal.nixpkgs_unstable - ... + $ nix flake check -L + +Since Nix doesn't garantee your machine have enough resources to run +all test VMs in parallel, some tests can fail. You would then haev to +run tests manually. For instance: + +:: + + $ nix build .#hydraJobs.x86_64-linux.external-unstable -L + Contributing to the documentation --------------------------------- diff --git a/flake.lock b/flake.lock index a9abdbc..3d49d2e 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "blobs": { + "flake": false, + "locked": { + "lastModified": 1604995301, + "narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=", + "owner": "simple-nixos-mailserver", + "repo": "blobs", + "rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265", + "type": "gitlab" + }, + "original": { + "owner": "simple-nixos-mailserver", + "repo": "blobs", + "type": "gitlab" + } + }, "nixpkgs": { "locked": { "lastModified": 1607522989, @@ -15,9 +31,26 @@ "type": "indirect" } }, + "nixpkgs-21_05": { + "locked": { + "lastModified": 1625692408, + "narHash": "sha256-e9L3TLLDVIJpMnHtiNHJE62oOh6emRtSZ244bgYJUZs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c06613c25df3fe1dd26243847a3c105cf6770627", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-21.05", + "type": "indirect" + } + }, "root": { "inputs": { + "blobs": "blobs", "nixpkgs": "nixpkgs", + "nixpkgs-21_05": "nixpkgs-21_05", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index 3d8cfbe..f58b6fa 100644 --- a/flake.nix +++ b/flake.nix @@ -4,15 +4,55 @@ inputs = { utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; + nixpkgs-21_05.url = "flake:nixpkgs/nixos-21.05"; + blobs = { + url = "gitlab:simple-nixos-mailserver/blobs"; + flake = false; + }; }; - outputs = { self, utils, nixpkgs }: { + outputs = { self, utils, blobs, nixpkgs, nixpkgs-21_05 }: let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + # We want to test nixos-mailserver on several nixos releases + releases = [ + { + name = "unstable"; + pkgs = nixpkgs.legacyPackages.${system}; + } + { + name = "21_05"; + pkgs = nixpkgs-21_05.legacyPackages.${system}; + } + ]; + testNames = [ + "internal" + "external" + "clamav" + "multiple" + ]; + genTest = testName: release: { + "name"= "${testName}-${release.name}"; + "value"= import (./tests/. + "/${testName}.nix") { + pkgs = release.pkgs; + inherit blobs; + }; + }; + # Generate an attribute set such as + # { + # external-unstable = ; + # external-21_05 = ; + # ... + # } + allTests = pkgs.lib.listToAttrs ( + pkgs.lib.flatten (map (t: map (r: genTest t r) releases) testNames)); + + in { nixosModules.mailserver = import ./.; nixosModule = self.nixosModules.mailserver; - } // utils.lib.eachDefaultSystem (system: let - pkgs = nixpkgs.legacyPackages.${system}; - in { - devShell = pkgs.mkShell { + hydraJobs.${system} = allTests; + checks.${system} = allTests; + devShell.${system} = pkgs.mkShell { buildInputs = with pkgs; [ (python3.withPackages (p: with p; [ sphinx @@ -22,5 +62,5 @@ clamav ]; }; - }); + }; } diff --git a/tests/clamav.nix b/tests/clamav.nix index f818991..4f59d64 100644 --- a/tests/clamav.nix +++ b/tests/clamav.nix @@ -14,19 +14,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ pkgs ? import {}}: +{ pkgs ? import {}, blobs}: pkgs.nixosTest { name = "clamav"; nodes = { server = { config, pkgs, lib, ... }: - let - sources = import ../nix/sources.nix; - blobs = pkgs.fetchzip { - url = sources.blobs.url; - sha256 = sources.blobs.sha256; - }; - in { imports = [ ../default.nix diff --git a/tests/default.nix b/tests/default.nix deleted file mode 100644 index 68f2053..0000000 --- a/tests/default.nix +++ /dev/null @@ -1,48 +0,0 @@ -# Generate an attribute sets containing all tests for all releaeses -# It looks like: -# - external.nixpkgs_20.03 -# - external.nixpkgs_unstable -# - internal.nixpkgs_20.03 -# - internal.nixpkgs_unstable - -with builtins; - -let - sources = import ../nix/sources.nix; - - releases = listToAttrs (map genRelease releaseNames); - - genRelease = name: { - name = name; - value = import sources."${name}" {}; - }; - - genTest = testName: release: - let - pkgs = releases."${release}"; - test = pkgs.callPackage (./. + "/${testName}.nix") { }; - in { - "name"= builtins.replaceStrings ["." "-"] ["_" "_"] release; - "value"= test; - }; - - releaseNames = [ - "nixpkgs-unstable" - "nixpkgs-20.09" - "nixpkgs-21.05" - ]; - - testNames = [ - "internal" - "external" - "clamav" - "multiple" - ]; - - # Generate an attribute set containing one test per releases - genTests = testName: { - name = testName; - value = listToAttrs (map (genTest testName) (builtins.attrNames releases)); - }; - -in listToAttrs (map genTests testNames) diff --git a/tests/external.nix b/tests/external.nix index 55d9eb5..7c87664 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ pkgs ? import {}}: +{ pkgs ? import {}, ...}: pkgs.nixosTest { name = "external"; diff --git a/tests/internal.nix b/tests/internal.nix index c64339a..e8c4227 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ pkgs ? import {}}: +{ pkgs ? import {}, ...}: let sendMail = pkgs.writeTextFile { diff --git a/tests/multiple.nix b/tests/multiple.nix index 3377906..99e1aaf 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -1,6 +1,6 @@ # This tests is used to test features requiring several mail domains. -{ pkgs ? import {}}: +{ pkgs ? import {}, ...}: let hashPassword = password: pkgs.runCommand From 87735ed077e102437760434eb7c51d5563f93b71 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 11 Jul 2021 10:12:40 +0200 Subject: [PATCH 014/225] Remove Niv It is now useless since we are using Nix Flakes --- nix/sources.json | 45 ---------------- nix/sources.nix | 131 ----------------------------------------------- 2 files changed, 176 deletions(-) delete mode 100644 nix/sources.json delete mode 100644 nix/sources.nix diff --git a/nix/sources.json b/nix/sources.json deleted file mode 100644 index a669d85..0000000 --- a/nix/sources.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "blobs": { - "sha256": "1g687x3b2r4ar5i4xyav5qzpy9fp1phx9wf70f4j3scwny0g7hn1", - "type": "tarball", - "url": "https://gitlab.com/simple-nixos-mailserver/blobs/-/archive/2cccdf1ca48316f2cfd1c9a0017e8de5a7156265/blobs-2cccdf1ca48316f2cfd1c9a0017e8de5a7156265.tar.gz", - "url_template": "https://gitlab.com/simple-nixos-mailserver/blobs/-/archive//blobs-.tar.gz", - "version": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265" - }, - "nixpkgs-20.09": { - "branch": "release-20.09", - "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", - "homepage": "https://github.com/NixOS/nixpkgs", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "b6eefa48d8e10491e43c0c6155ac12b463f6fed3", - "sha256": "0hrp7gshy62bsj719xd6hk6z284pzr8ksw1vvxvyfrffq1f7d8k9", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/b6eefa48d8e10491e43c0c6155ac12b463f6fed3.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "nixpkgs-21.05": { - "branch": "release-21.05", - "description": "Nix Packages collection", - "homepage": "", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "3776ceb792907ddebd7d2603778c7f106fabfb08", - "sha256": "10wmmnf5scmrnf1knc31yhwlrq720n3k26j44daqal4shqxhr2vk", - "type": "tarball", - "url": "https://github.com/nixos/nixpkgs/archive/3776ceb792907ddebd7d2603778c7f106fabfb08.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - }, - "nixpkgs-unstable": { - "branch": "nixos-unstable", - "description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to", - "homepage": "https://github.com/NixOS/nixpkgs", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "1905f5f2e55e0db0bb6244cfe62cb6c0dbda391d", - "sha256": "148f79hhya66qj8v5gn7bs6zrfjy1nbvdciyxdm4yd5p8r6ayzv6", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/1905f5f2e55e0db0bb6244cfe62cb6c0dbda391d.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - } -} diff --git a/nix/sources.nix b/nix/sources.nix deleted file mode 100644 index ea0163b..0000000 --- a/nix/sources.nix +++ /dev/null @@ -1,131 +0,0 @@ -# This file has been generated by Niv. - -let - - # - # The fetchers. fetch_ fetches specs of type . - # - - fetch_file = pkgs: spec: - if spec.builtin or true then - builtins_fetchurl { inherit (spec) url sha256; } - else - pkgs.fetchurl { inherit (spec) url sha256; }; - - fetch_tarball = pkgs: spec: - pkgs.fetchzip { inherit (spec) url sha256; }; - - fetch_git = spec: - builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; }; - - fetch_builtin-tarball = spec: - builtins.trace - '' - WARNING: - The niv type "builtin-tarball" will soon be deprecated. You should - instead use `builtin = true`. - - $ niv modify -a type=tarball -a builtin=true - '' - builtins_fetchTarball { inherit (spec) url sha256; }; - - fetch_builtin-url = spec: - builtins.trace - '' - WARNING: - The niv type "builtin-url" will soon be deprecated. You should - instead use `builtin = true`. - - $ niv modify -a type=file -a builtin=true - '' - (builtins_fetchurl { inherit (spec) url sha256; }); - - # - # Various helpers - # - - # The set of packages used when specs are fetched using non-builtins. - mkPkgs = sources: - let - sourcesNixpkgs = - import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {}; - hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; - hasThisAsNixpkgsPath = == ./.; - in - if builtins.hasAttr "nixpkgs" sources - then sourcesNixpkgs - else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then - import {} - else - abort - '' - Please specify either (through -I or NIX_PATH=nixpkgs=...) or - add a package called "nixpkgs" to your sources.json. - ''; - - # The actual fetching function. - fetch = pkgs: name: spec: - - if ! builtins.hasAttr "type" spec then - abort "ERROR: niv spec ${name} does not have a 'type' attribute" - else if spec.type == "file" then fetch_file pkgs spec - else if spec.type == "tarball" then fetch_tarball pkgs spec - else if spec.type == "git" then fetch_git spec - else if spec.type == "builtin-tarball" then fetch_builtin-tarball spec - else if spec.type == "builtin-url" then fetch_builtin-url spec - else - abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; - - # Ports of functions for older nix versions - - # a Nix version of mapAttrs if the built-in doesn't exist - mapAttrs = builtins.mapAttrs or ( - f: set: with builtins; - listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) - ); - - # fetchTarball version that is compatible between all the versions of Nix - builtins_fetchTarball = { url, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchTarball; - in - if lessThan nixVersion "1.12" then - fetchTarball { inherit url; } - else - fetchTarball attrs; - - # fetchurl version that is compatible between all the versions of Nix - builtins_fetchurl = { url, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchurl; - in - if lessThan nixVersion "1.12" then - fetchurl { inherit url; } - else - fetchurl attrs; - - # Create the final "sources" from the config - mkSources = config: - mapAttrs ( - name: spec: - if builtins.hasAttr "outPath" spec - then abort - "The values in sources.json should not have an 'outPath' attribute" - else - spec // { outPath = fetch config.pkgs name spec; } - ) config.sources; - - # The "config" used by the fetchers - mkConfig = - { sourcesFile ? ./sources.json - , sources ? builtins.fromJSON (builtins.readFile sourcesFile) - , pkgs ? mkPkgs sources - }: rec { - # The sources, i.e. the attribute set of spec name to spec - inherit sources; - - # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers - inherit pkgs; - }; -in -mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } From 2fa9c7c4df202fc9f1e40aaad874a71aa9d9b351 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 11 Jul 2021 09:48:56 +0200 Subject: [PATCH 015/225] tests: update fts indexer log messages --- tests/external.nix | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/external.nix b/tests/external.nix index 7c87664..ba97815 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -489,12 +489,10 @@ pkgs.nixosTest { client.fail("search Junk a >&2") # check that search really goes through the indexer server.succeed( - "journalctl -u dovecot2 | grep -E 'indexer-worker.* Mailbox INBOX: Indexed' >&2" + "journalctl -u dovecot2 | grep -E 'indexer-worker.* Mailbox INBOX: Mailbox opened because: indexing' >&2" ) # check that Junk is not indexed - server.fail( - "journalctl -u dovecot2 | grep -E 'indexer-worker.* Mailbox JUNK: Indexed' >&2" - ) + server.fail("journalctl -u dovecot2 | grep 'indexer-worker' | grep -i 'JUNK' >&2") with subtest("no warnings or errors"): server.fail("journalctl -u postfix | grep -i error >&2") From a37dac9d66b0bbfc28ca64e7efbf4ce55be94143 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Mon, 12 Jul 2021 23:28:02 +0200 Subject: [PATCH 016/225] ci: reenable 20.09 and 21.05 jobs :/ They haven't been moved to flake so we still need to keep the non flake Hydra configuration. --- .hydra/declarative-jobsets.nix | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 2159de7..5273cc2 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -17,7 +17,36 @@ let flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head"; } ) prs; + # This could be removed once branch 20.09 and 21.05 would have been + # removed. mkJobset = branch: { + description = "Build ${branch} branch of Simple NixOS MailServer"; + checkinterval = "60"; + enabled = "1"; + schedulingshares = 100; + enableemail = false; + emailoverride = ""; + nixexprinput = "snm"; + nixexprpath = ".hydra/default.nix"; + type = 0; + inputs = { + # This is only used to allow Niv to use pkgs.fetchzip which is + # required because of Hydra restricted evaluation mode. + nixpkgs = { + value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; + type = "git"; + emailresponsible = false; + }; + snm = { + value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver ${branch}"; + type = "git"; + emailresponsible = false; + }; + }; + keepnr = 3; + hidden = false; + }; + mkFlakeJobset = branch: { description = "Build ${branch} branch of Simple NixOS MailServer"; checkinterval = "60"; enabled = "1"; @@ -31,7 +60,7 @@ let }; desc = prJobsets // { - "master" = mkJobset "master"; + "master" = mkFlakeJobset "master"; "nixos-20.09" = mkJobset "nixos-20.09"; "nixos-21.05" = mkJobset "nixos-21.05"; }; From 864ea5bfeff7f9a0380f03b9c325ca0233a77bf5 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sat, 24 Jul 2021 09:42:30 +0200 Subject: [PATCH 017/225] Update nixpkgs-unstable --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 3d49d2e..f3ec998 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1607522989, - "narHash": "sha256-o/jWhOSAlaK7y2M57OIriRt6whuVVocS/T0mG7fd1TI=", + "lastModified": 1626852498, + "narHash": "sha256-lOXUJvi0FJUXHTVSiC5qsMRtEUgqM4mGZpMESLuGhmo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e9158eca70ae59e73fae23be5d13d3fa0cfc78b4", + "rev": "16105403bdd843540cbef9c63fc0f16c1c6eaa70", "type": "github" }, "original": { From 9578dbac69ec3ddf9b2fd38a40d74332b7454dfa Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Fri, 23 Jul 2021 23:21:36 +0200 Subject: [PATCH 018/225] Remove non longer supported configurations (<21.05) --- default.nix | 37 +++++++++++++++++-------------------- mail-server/clamav.nix | 14 +------------- 2 files changed, 18 insertions(+), 33 deletions(-) diff --git a/default.nix b/default.nix index dd800b1..5d94438 100644 --- a/default.nix +++ b/default.nix @@ -421,27 +421,24 @@ in The mailboxes for dovecot. Depending on the mail client used it might be necessary to change some mailbox's name. ''; - default = let - defMailBoxes = { - Trash = { - auto = "no"; - specialUse = "Trash"; - }; - Junk = { - auto = "subscribe"; - specialUse = "Junk"; - }; - Drafts = { - auto = "subscribe"; - specialUse = "Drafts"; - }; - Sent = { - auto = "subscribe"; - specialUse = "Sent"; - }; + default = { + Trash = { + auto = "no"; + specialUse = "Trash"; }; - in if (versionAtLeast version "20.09pre") then defMailBoxes - else (flip mapAttrsToList defMailBoxes (name: options: { inherit name; } // options)); + Junk = { + auto = "subscribe"; + specialUse = "Junk"; + }; + Drafts = { + auto = "subscribe"; + specialUse = "Drafts"; + }; + Sent = { + auto = "subscribe"; + specialUse = "Sent"; + }; + }; }; certificateScheme = mkOption { diff --git a/mail-server/clamav.nix b/mail-server/clamav.nix index a73d4e5..25418f0 100644 --- a/mail-server/clamav.nix +++ b/mail-server/clamav.nix @@ -18,25 +18,13 @@ let cfg = config.mailserver; - clamHasSettings = options.services.clamav.daemon ? settings; in -with lib; { config = lib.mkIf (cfg.enable && cfg.virusScanning) { - - # Remove extraConfig and settings conditional after 20.09 support is removed - services.clamav.daemon = { enable = true; - } // (if clamHasSettings then { settings.PhishingScanURLs = "no"; - } else { - extraConfig = '' - PhishingScanURLs no - ''; - }); - + }; services.clamav.updater.enable = true; }; } - From 4d087532b69c35858b4f72bdcefebe6c49a69c01 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 14 Jul 2021 10:06:58 +0200 Subject: [PATCH 019/225] docs: generate the list of options To generate the list of options, we need to generate and commit a rst file to make all files available for ReadTheDoc. An Hydra test ensures this generated file is up-to-date. If it is not up-to-date, the error message explains the user how to generate it: the user just needs to run `nix-shell --run generate-rst-options`. --- docs/howto-develop.rst | 7 + docs/index.rst | 1 + docs/options.rst | 1132 +++++++++++++++++++++++++++++++ flake.nix | 46 +- scripts/generate-rst-options.py | 64 ++ 5 files changed, 1247 insertions(+), 3 deletions(-) create mode 100644 docs/options.rst create mode 100644 scripts/generate-rst-options.py diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index 5eb4e57..ce74683 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -46,6 +46,13 @@ documentation: $ make html $ firefox ./_build/html/index.html +Note if you modify some NixOS mailserver options, you would also need +to regenerate the ``options.rst`` file: + +:: + + $ nix-shell --run generate-rst-options + Nixops ------ diff --git a/docs/index.rst b/docs/index.rst index 3ef9d5e..29386b4 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ Welcome to NixOS Mailserver's documentation! howto-develop faq release-notes + options .. toctree:: :maxdepth: 1 diff --git a/docs/options.rst b/docs/options.rst new file mode 100644 index 0000000..33616d7 --- /dev/null +++ b/docs/options.rst @@ -0,0 +1,1132 @@ + +Mailserver Options +================== + +mailserver +~~~~~~~~~~ + + + +mailserver.debug +---------------- + +Whether to enable verbose logging for mailserver related services. This +intended be used for development purposes only, you probably don't want +to enable this unless you're hacking on nixos-mailserver. + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.domains +------------------ + +The domains that this mail server serves. + +- Type: ``list of strings`` +- Default: ``[]`` + + +mailserver.enable +----------------- + +Whether to enable nixos-mailserver. + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.enableImap +--------------------- + +Whether to enable IMAP with STARTTLS on port 143. + + +- Type: ``boolean`` +- Default: ``True`` + + +mailserver.enableImapSsl +------------------------ + +Whether to enable IMAP with TLS in wrapper-mode on port 993. + + +- Type: ``boolean`` +- Default: ``True`` + + +mailserver.enableManageSieve +---------------------------- + +Whether to enable ManageSieve, setting this option to true will open +port 4190 in the firewall. + +The ManageSieve protocol allows users to manage their Sieve scripts on +a remote server with a supported client, including Thunderbird. + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.enablePop3 +--------------------- + +Whether to enable POP3 with STARTTLS on port on port 110. + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.enablePop3Ssl +------------------------ + +Whether to enable POP3 with TLS in wrapper-mode on port 995. + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.enableSubmission +--------------------------- + +Whether to enable SMTP with STARTTLS on port 587. + + +- Type: ``boolean`` +- Default: ``True`` + + +mailserver.enableSubmissionSsl +------------------------------ + +Whether to enable SMTP with TLS in wrapper-mode on port 465. + + +- Type: ``boolean`` +- Default: ``True`` + + +mailserver.extraVirtualAliases +------------------------------ + +Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that +all mail to `info@example.com` is forwarded to `user1@example.com`. Note +that it is expected that `postmaster@example.com` and `abuse@example.com` is +forwarded to some valid email address. (Alternatively you can create login +accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows +the user `user1@example.com` to send emails as `info@example.com`. +It's also possible to create an alias for multiple accounts. In this +example all mails for `multi@example.com` will be forwarded to both +`user1@example.com` and `user2@example.com`. + + +- Type: ``attribute set of Login Account or non-empty list of Login Accountss`` +- Default: ``{}`` + + +mailserver.forwards +------------------- + +To forward mails to an external address. For instance, +the value {`"user@example.com" = "user@elsewhere.com";}` +means that mails to `user@example.com` are forwarded to +`user@elsewhere.com`. The difference with the +`extraVirtualAliases` option is that `user@elsewhere.com` +can't send mail as `user@example.com`. Also, this option +allows to forward mails to external addresses. + + +- Type: ``attribute set of list of strings or strings`` +- Default: ``{}`` + + +mailserver.fqdn +--------------- + +The fully qualified domain name of the mail server. + +- Type: ``string`` + + + +mailserver.hierarchySeparator +----------------------------- + +The hierarchy separator for mailboxes used by dovecot for the namespace 'inbox'. +Dovecot defaults to "." but recommends "/". +This affects how mailboxes appear to mail clients and sieve scripts. +For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example". +This does not determine the way your mails are stored on disk. +See https://wiki.dovecot.org/Namespaces for details. + + +- Type: ``string`` +- Default: ``.`` + + +mailserver.indexDir +------------------- + +Folder to store search indices. If null, indices are stored +along with email, which could not necessarily be desirable, +especially when the fullTextSearch option is enable since +indices it creates are voluminous and do not need to be backed +up. + +Be careful when changing this option value since all indices +would be recreated at the new location (and clients would need +to resynchronize). + +Note the some variables can be used in the file path. See +https://doc.dovecot.org/configuration_manual/mail_location/#variables +for details. + + +- Type: ``null or string`` +- Default: ``None`` + + +mailserver.keyFile +------------------ + +Scheme 1) +Location of the key file + + +- Type: ``path`` + + + +mailserver.lmtpSaveToDetailMailbox +---------------------------------- + +If an email address is delimited by a "+", should it be filed into a +mailbox matching the string after the "+"? For example, +user1+test@example.com would be filed into the mailbox "test". + + +- Type: ``one of "yes", "no"`` +- Default: ``yes`` + + +mailserver.localDnsResolver +--------------------------- + +Runs a local DNS resolver (kresd) as recommended when running rspamd. This prevents your log file from filling up with rspamd_monitored_dns_mon entries. + + +- Type: ``boolean`` +- Default: ``True`` + + +mailserver.mailDirectory +------------------------ + +Where to store the mail. + + +- Type: ``path`` +- Default: ``/var/vmail`` + + +mailserver.mailboxes +-------------------- + +The mailboxes for dovecot. +Depending on the mail client used it might be necessary to change some mailbox's name. + + +- Type: ``unspecified`` +- Default: ``{'Drafts': {'auto': 'subscribe', 'specialUse': 'Drafts'}, 'Junk': {'auto': 'subscribe', 'specialUse': 'Junk'}, 'Sent': {'auto': 'subscribe', 'specialUse': 'Sent'}, 'Trash': {'auto': 'no', 'specialUse': 'Trash'}}`` + + +mailserver.maxConnectionsPerUser +-------------------------------- + +Maximum number of IMAP/POP3 connections allowed for a user from each IP address. +E.g. a value of 50 allows for 50 IMAP and 50 POP3 connections at the same +time for a single user. + + +- Type: ``signed integer`` +- Default: ``100`` + + +mailserver.messageSizeLimit +--------------------------- + +Message size limit enforced by Postfix. + +- Type: ``signed integer`` +- Default: ``20971520`` + + +mailserver.openFirewall +----------------------- + +Automatically open ports in the firewall. + +- Type: ``boolean`` +- Default: ``True`` + + +mailserver.policydSPFExtraConfig +-------------------------------- + +Extra configuration options for policyd-spf. This can be use to among +other things skip spf checking for some IP addresses. + + +- Type: ``strings concatenated with "\n"`` +- Default: ``""`` + + +mailserver.rebootAfterKernelUpgrade.enable +------------------------------------------ + +Whether to enable automatic reboot after kernel upgrades. +This is to be used in conjunction with system.autoUpgrade.enable = true" + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.rebootAfterKernelUpgrade.method +------------------------------------------ + +Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot. +It is recommended to use the default value because the quicker kexec reboot has a number of problems. +Also if your server is running in a virtual machine the regular reboot will already be very quick. + + +- Type: ``one of "reboot", "systemctl kexec"`` +- Default: ``reboot`` + + +mailserver.recipientDelimiter +----------------------------- + +Configure the recipient delimiter. + + +- Type: ``string`` +- Default: ``+`` + + +mailserver.rejectRecipients +--------------------------- + +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. + + +- Type: ``list of strings`` +- Default: ``[]`` + + +mailserver.rejectSender +----------------------- + +Reject emails from these addresses from unauthorized senders. +Use if a spammer is using the same domain or the same sender over and over. + + +- Type: ``list of strings`` +- Default: ``[]`` + + +mailserver.rewriteMessageId +--------------------------- + +Rewrites the Message-ID's hostname-part of outgoing emails to the FQDN. +Please be aware that this may cause problems with some mail clients +relying on the original Message-ID. + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.sendingFqdn +---------------------- + +The fully qualified domain name of the mail server used to +identify with remote servers. + +If this server's IP serves purposes other than a mail server, +it may be desirable for the server to have a name other than +that to which the user will connect. For example, the user +might connect to mx.example.com, but the server's IP has +reverse DNS that resolves to myserver.example.com; in this +scenario, some mail servers may reject or penalize the +message. + +This setting allows the server to identify as +myserver.example.com when forwarding mail, independently of +`fqdn` (which, for SSL reasons, should generally be the name +to which the user connects). + +Set this to the name to which the sending IP's reverse DNS +resolves. + + +- Type: ``string`` +- Default: ``config.mailserver.fqdn`` + + +mailserver.sieveDirectory +------------------------- + +Where to store the sieve scripts. + + +- Type: ``path`` +- Default: ``/var/sieve`` + + +mailserver.useFsLayout +---------------------- + +Sets whether dovecot should organize mail in subdirectories: + +- /var/vmail/example.com/user/.folder.subfolder/ (default layout) +- /var/vmail/example.com/user/folder/subfolder/ (FS layout) + +See https://wiki2.dovecot.org/MailboxFormat/Maildir for details. + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.virusScanning +------------------------ + +Whether to activate virus scanning. Note that virus scanning is _very_ +expensive memory wise. + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.vmailGroupName +------------------------- + +The user name and group name of the user that owns the directory where all +the mail is stored. + + +- Type: ``string`` +- Default: ``virtualMail`` + + +mailserver.vmailUID +------------------- + +The unix UID of the virtual mail user. Be mindful that if this is +changed, you will need to manually adjust the permissions of +mailDirectory. + + +- Type: ``signed integer`` +- Default: ``5000`` + + +mailserver.vmailUserName +------------------------ + +The user name and group name of the user that owns the directory where all +the mail is stored. + + +- Type: ``string`` +- Default: ``virtualMail`` + +mailserver.loginAccount +~~~~~~~~~~~~~~~~~~~~~~~ + + +mailserver.loginAccounts +------------------------ + +The login account of the domain. Every account is mapped to a unix user, +e.g. `user1@example.com`. To generate the passwords use `htpasswd` as +follows + +``` +nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 +``` + + +- Type: ``attribute set of submodules`` +- Default: ``{}`` + + +mailserver.loginAccounts..aliases +--------------------------------------- + +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. + + +- Type: ``list of strings`` +- Default: ``[]`` + + +mailserver.loginAccounts..catchAll +---------------------------------------- + +For which domains should this account act as a catch all? +Note: Does not allow sending from all addresses of these domains. + + +- Type: ``list of one of s`` +- Default: ``[]`` + + +mailserver.loginAccounts..hashedPassword +---------------------------------------------- + +The user's hashed password. Use `htpasswd` as follows + +``` +nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 +``` + +Warning: this is stored in plaintext in the Nix store! +Use `hashedPasswordFile` instead. + + +- Type: ``null or string`` +- Default: ``None`` + + +mailserver.loginAccounts..hashedPasswordFile +-------------------------------------------------- + +A file containing the user's hashed password. Use `htpasswd` as follows + +``` +nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 +``` + + +- Type: ``null or path`` +- Default: ``None`` + + +mailserver.loginAccounts..name +------------------------------------ + +Username + +- Type: ``string`` + + + +mailserver.loginAccounts..quota +------------------------------------- + +Per user quota rules. Accepted sizes are `xx k/M/G/T` with the +obvious meaning. Leave blank for the standard quota `100G`. + + +- Type: ``null or string`` +- Default: ``None`` + + +mailserver.loginAccounts..sendOnly +---------------------------------------- + +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. + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.loginAccounts..sendOnlyRejectMessage +----------------------------------------------------- + +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. + + +- Type: ``string`` +- Default: ``This account cannot receive emails.`` + + +mailserver.loginAccounts..sieveScript +------------------------------------------- + +Per-user sieve script. + + +- Type: ``null or strings concatenated with "\n"`` +- Default: ``None`` + +mailserver.certificate +~~~~~~~~~~~~~~~~~~~~~~ + + +mailserver.certificateDirectory +------------------------------- + +Scheme 2) +This is the folder where the certificate will be created. The name is +hardcoded to "cert-.pem" and "key-.pem" and the +certificate is valid for 10 years. + + +- Type: ``path`` +- Default: ``/var/certs`` + + +mailserver.certificateFile +-------------------------- + +Scheme 1) +Location of the certificate + + +- Type: ``path`` + + + +mailserver.certificateScheme +---------------------------- + +Certificate Files. There are three options for these. + +1) You specify locations and manually copy certificates there. +2) You let the server create new (self signed) certificates on the fly. +3) You let the server create a certificate via `Let's Encrypt`. Note that + this implies that a stripped down webserver has to be started. This also + implies that the FQDN must be set as an `A` record to point to the IP of + the server. In particular port 80 on the server will be opened. For details + on how to set up the domain records, see the guide in the readme. + + +- Type: ``one of 1, 2, 3`` +- Default: ``2`` + +mailserver.dkim +~~~~~~~~~~~~~~~ + + +mailserver.dkimKeyBits +---------------------- + +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 (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. + + +- Type: ``signed integer`` +- Default: ``1024`` + + +mailserver.dkimKeyDirectory +--------------------------- + + + + +- Type: ``path`` +- Default: ``/var/dkim`` + + +mailserver.dkimSelector +----------------------- + + + + +- Type: ``string`` +- Default: ``mail`` + + +mailserver.dkimSigning +---------------------- + +Whether to activate dkim signing. + + +- Type: ``boolean`` +- Default: ``True`` + +mailserver.fullTextSearch +~~~~~~~~~~~~~~~~~~~~~~~~~ + + +mailserver.fullTextSearch.autoIndex +----------------------------------- + +Enable automatic indexing of messages as they are received or modified. + +- Type: ``boolean`` +- Default: ``True`` + + +mailserver.fullTextSearch.autoIndexExclude +------------------------------------------ + +Mailboxes to exclude from automatic indexing. + + +- Type: ``list of strings`` +- Default: ``[]`` + + +mailserver.fullTextSearch.enable +-------------------------------- + +Whether to enable Full text search indexing with xapian. This has significant performance and disk space cost.. + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.fullTextSearch.enforced +---------------------------------- + +Fail searches when no index is available. If set to +body, then only body searches (as opposed to +header) are affected. If set to no, searches may +fall back to a very slow brute force search. + + +- Type: ``one of "yes", "no", "body"`` +- Default: ``no`` + + +mailserver.fullTextSearch.indexAttachments +------------------------------------------ + +Also index text-only attachements. Binary attachements are never indexed. + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.fullTextSearch.maintenance.enable +-------------------------------------------- + +Regularly optmize indices, as recommended by upstream. + +- Type: ``boolean`` +- Default: ``True`` + + +mailserver.fullTextSearch.maintenance.onCalendar +------------------------------------------------ + +When to run the maintenance job. See systemd.time(7) for more information about the format. + +- Type: ``string`` +- Default: ``daily`` + + +mailserver.fullTextSearch.maintenance.randomizedDelaySec +-------------------------------------------------------- + +Run the maintenance job not exactly at the time specified with onCalendar, but plus or minus this many seconds. + +- Type: ``signed integer`` +- Default: ``1000`` + + +mailserver.fullTextSearch.maxSize +--------------------------------- + +Size of the largest n-gram to index. + +- Type: ``signed integer`` +- Default: ``20`` + + +mailserver.fullTextSearch.memoryLimit +------------------------------------- + +Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit. + +- Type: ``null or signed integer`` +- Default: ``None`` + + +mailserver.fullTextSearch.minSize +--------------------------------- + +Size of the smallest n-gram to index. + +- Type: ``signed integer`` +- Default: ``2`` + +mailserver.redis +~~~~~~~~~~~~~~~~ + + +mailserver.redis.address +------------------------ + +Address that rspamd should use to contact redis. The default value +is read from config.services.redis.bind. + + +- Type: ``string`` +- Default: ``127.0.0.1`` + + +mailserver.redis.password +------------------------- + +Password that rspamd should use to contact redis, or null if not +required. The default value is read from +config.services.redis.requirePass. + + +- Type: ``null or string`` +- Default: ``None`` + + +mailserver.redis.port +--------------------- + +Port that rspamd should use to contact redis. The default value is +read from config.services.redis.port. + + +- Type: ``16 bit unsigned integer; between 0 and 65535 (both inclusive)`` +- Default: ``6379`` + +mailserver.monitoring +~~~~~~~~~~~~~~~~~~~~~ + + +mailserver.monitoring.alertAddress +---------------------------------- + +The email address to send alerts to. + + +- Type: ``string`` + + + +mailserver.monitoring.config +---------------------------- + +The configuration used for monitoring via monit. +Use a mail address that you actively check and set it via 'set alert ...'. + + +- Type: ``string`` +- Default: ``set daemon 120 with start delay 60 +set mailserver + localhost + +set httpd port 2812 and use address localhost + allow localhost + allow admin:obwjoawijerfoijsiwfj29jf2f2jd + +check filesystem root with path / + if space usage > 80% then alert + if inode usage > 80% then alert + +check system $HOST + if cpu usage > 95% for 10 cycles then alert + if memory usage > 75% for 5 cycles then alert + if swap usage > 20% for 10 cycles then alert + if loadavg (1min) > 90 for 15 cycles then alert + if loadavg (5min) > 80 for 10 cycles then alert + if loadavg (15min) > 70 for 8 cycles then alert + +check process sshd with pidfile /var/run/sshd.pid + start program "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl start sshd" + stop program "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl stop sshd" + if failed port 22 protocol ssh for 2 cycles then restart + +check process postfix with pidfile /var/lib/postfix/queue/pid/master.pid + start program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl start postfix" + stop program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl stop postfix" + if failed port 25 protocol smtp for 5 cycles then restart + +check process dovecot with pidfile /var/run/dovecot2/master.pid + start program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl start dovecot2" + stop program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl stop dovecot2" + if failed host mx.example.com port 993 type tcpssl sslauto protocol imap for 5 cycles then restart + +check process rspamd with pidfile /var/run/rspamd.pid + start program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl start rspamd" + stop program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl stop rspamd" +`` + + +mailserver.monitoring.enable +---------------------------- + +Whether to enable monitoring via monit. + +- Type: ``boolean`` +- Default: ``False`` + +mailserver.backup +~~~~~~~~~~~~~~~~~ + + +mailserver.backup.cmdPostexec +----------------------------- + +The command to be executed after each backup operation. This is wrapped in a shell script to be called by rsnapshot. + +- Type: ``null or string`` +- Default: ``None`` + + +mailserver.backup.cmdPreexec +---------------------------- + +The command to be executed before each backup operation. This is wrapped in a shell script to be called by rsnapshot. + + +- Type: ``null or string`` +- Default: ``None`` + + +mailserver.backup.cronIntervals +------------------------------- + +Periodicity at which intervals should be run by cron. +Note that the intervals also have to exist in configuration +as retain options. + + +- Type: ``attribute set of strings`` +- Default: ``{'daily': '30 3 * * *', 'hourly': ' 0 * * * *', 'weekly': ' 0 5 * * 0'}`` + + +mailserver.backup.enable +------------------------ + +Whether to enable backup via rsnapshot. + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.backup.retain.daily +------------------------------ + +How many daily snapshots are retained. + +- Type: ``signed integer`` +- Default: ``7`` + + +mailserver.backup.retain.hourly +------------------------------- + +How many hourly snapshots are retained. + +- Type: ``signed integer`` +- Default: ``24`` + + +mailserver.backup.retain.weekly +------------------------------- + +How many weekly snapshots are retained. + +- Type: ``signed integer`` +- Default: ``54`` + + +mailserver.backup.snapshotRoot +------------------------------ + +The directory where rsnapshot stores the backup. + + +- Type: ``path`` +- Default: ``/var/rsnapshot`` + +mailserver.borg +~~~~~~~~~~~~~~~ + + +mailserver.borgbackup.cmdPostexec +--------------------------------- + +The command to be executed after each backup operation. +This is called after borg create completed successfully and in the same script that runs +cmdPreexec, borg init and create. + + +- Type: ``null or string`` +- Default: ``None`` + + +mailserver.borgbackup.cmdPreexec +-------------------------------- + +The command to be executed before each backup operation. +This is called prior to borg init in the same script that runs borg init and create and cmdPostexec. +Example: + export BORG_RSH="ssh -i /path/to/private/key" + + +- Type: ``null or string`` +- Default: ``None`` + + +mailserver.borgbackup.compression.auto +-------------------------------------- + +Leaves it to borg to determine whether an individual file should be compressed. + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.borgbackup.compression.level +--------------------------------------- + +Denotes the level of compression used by borg. +Most methods accept levels from 0 to 9 but zstd which accepts values from 1 to 22. +If null the decision is left up to borg. + + +- Type: ``null or signed integer`` +- Default: ``None`` + + +mailserver.borgbackup.compression.method +---------------------------------------- + +Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4. + +- Type: ``null or one of "none", "lz4", "zstd", "zlib", "lzma"`` +- Default: ``None`` + + +mailserver.borgbackup.enable +---------------------------- + +Whether to enable backup via borgbackup. + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.borgbackup.encryption.method +--------------------------------------- + +The backup can be encrypted by choosing any other value than 'none'. +When using encryption the password / passphrase must be provided in passphraseFile. + + +- Type: ``one of "none", "authenticated", "authenticated-blake2", "repokey", "keyfile", "repokey-blake2", "keyfile-blake2"`` +- Default: ``none`` + + +mailserver.borgbackup.encryption.passphraseFile +----------------------------------------------- + +This option has no description. + +- Type: ``null or path`` +- Default: ``None`` + + +mailserver.borgbackup.extraArgumentsForCreate +--------------------------------------------- + +Additional arguments to add to the borg create command line e.g. '--stats'. + +- Type: ``list of strings`` +- Default: ``[]`` + + +mailserver.borgbackup.extraArgumentsForInit +------------------------------------------- + +Additional arguments to add to the borg init command line. + +- Type: ``list of strings`` +- Default: ``['--critical']`` + + +mailserver.borgbackup.group +--------------------------- + +The group borg and its launch script is run as. + +- Type: ``string`` +- Default: ``virtualMail`` + + +mailserver.borgbackup.locations +------------------------------- + +The locations that are to be backed up by borg. + +- Type: ``list of paths`` +- Default: ``['/var/vmail']`` + + +mailserver.borgbackup.name +-------------------------- + +The name of the individual backups as used by borg. +Certain placeholders will be replaced by borg. + + +- Type: ``string`` +- Default: ``{hostname}-{user}-{now}`` + + +mailserver.borgbackup.repoLocation +---------------------------------- + +The location where borg saves the backups. +This can be a local path or a remote location such as user@host:/path/to/repo. +It is exported and thus available as an environment variable to cmdPreexec and cmdPostexec. + + +- Type: ``string`` +- Default: ``/var/borgbackup`` + + +mailserver.borgbackup.startAt +----------------------------- + +When or how often the backup should run. Must be in the format described in systemd.time 7. + +- Type: ``string`` +- Default: ``hourly`` + + +mailserver.borgbackup.user +-------------------------- + +The user borg and its launch script is run as. + +- Type: ``string`` +- Default: ``virtualMail`` + diff --git a/flake.nix b/flake.nix index f58b6fa..f340c2f 100644 --- a/flake.nix +++ b/flake.nix @@ -47,13 +47,53 @@ allTests = pkgs.lib.listToAttrs ( pkgs.lib.flatten (map (t: map (r: genTest t r) releases) testNames)); - in { - nixosModules.mailserver = import ./.; + mailserverModule = import ./.; + + # Generate a rst file describing options of the NixOS mailserver module + generateRstOptions = let + eval = import (pkgs.path + "/nixos/lib/eval-config.nix") { + inherit system; + modules = [ + mailserverModule + { + mailserver.fqdn = "mx.example.com"; + } + ]; + + }; + options = pkgs.nixosOptionsDoc { + options = eval.options; + }; + in pkgs.runCommand "options.rst" { buildInputs = [pkgs.python3]; } '' + echo Generating options.rst from ${options.optionsJSON}/share/doc/nixos/options.json + python ${./scripts/generate-rst-options.py} ${options.optionsJSON}/share/doc/nixos/options.json > $out + ''; + + # This is a script helping users to generate this file in the docs directory + generateRstOptionsScript = pkgs.writeScriptBin "generate-rst-options" '' + cp -v ${generateRstOptions} ./docs/options.rst + ''; + + # This is to ensure we don't forget to update the options.rst file + testRstOptions = pkgs.runCommand "test-rst-options" {} '' + if ! diff -q ${./docs/options.rst} ${generateRstOptions} + then + echo "The file ./docs/options.rst is not up-to-date and needs to be regenerated!" + echo " hint: run 'nix-shell --run generate-rst-options' to generate this file" + fi + echo "test: ok" > $out + ''; + + in rec { + nixosModules.mailserver = mailserverModule ; nixosModule = self.nixosModules.mailserver; - hydraJobs.${system} = allTests; + hydraJobs.${system} = allTests // { + test-rst-options = testRstOptions; + }; checks.${system} = allTests; devShell.${system} = pkgs.mkShell { buildInputs = with pkgs; [ + generateRstOptionsScript (python3.withPackages (p: with p; [ sphinx sphinx_rtd_theme diff --git a/scripts/generate-rst-options.py b/scripts/generate-rst-options.py new file mode 100644 index 0000000..da4e3dd --- /dev/null +++ b/scripts/generate-rst-options.py @@ -0,0 +1,64 @@ +import json +import sys + +header = """ +Mailserver Options +================== + +mailserver +~~~~~~~~~~ + +""" + +template = """ +{key} +{line} + +{description} + +{type} +{default} +""" + +f = open(sys.argv[1]) +options = json.load(f) + +options = { k: v for k, v in options.items() if k.startswith("mailserver.") } + +groups = [ "mailserver.loginAccount", + "mailserver.certificate", + "mailserver.dkim", + "mailserver.fullTextSearch", + "mailserver.redis", + "mailserver.monitoring", + "mailserver.backup", + "mailserver.borg" ] + +def print_option(name, value): + if 'default' in v: + if v['default'] == "": + default = '- Default: ``""``' + else: + default = '- Default: ``{}``'.format(v['default']) + else: + default = "" + print(template.format( + key=k, + line="-"*len(k), + description=v['description'], + type="- Type: ``{}``".format(v['type']), + default=default)) + +print(header) +for k, v in options.items(): + if any([k.startswith(c) for c in groups]): + continue + print_option(k, v) + +for c in groups: + print(c) + print("~"*len(c)) + print() + for k, v in options.items(): + if k.startswith(c): + print_option(k, v) From 68b9397a305c9e2e3c0f1048ee6e4907bc76d2d0 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Fri, 23 Jul 2021 21:24:22 +0200 Subject: [PATCH 020/225] Move the logo --- README.md | 2 +- docs/index.rst | 2 +- {logo => docs}/logo.png | Bin 3 files changed, 2 insertions(+), 2 deletions(-) rename {logo => docs}/logo.png (100%) diff --git a/README.md b/README.md index 13df600..647dcfa 100644 --- a/README.md +++ b/README.md @@ -160,4 +160,4 @@ See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mails -[logo]: logo/logo.png +[logo]: docs/logo.png diff --git a/docs/index.rst b/docs/index.rst index 29386b4..262c664 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,7 +6,7 @@ Welcome to NixOS Mailserver's documentation! ============================================ -.. image:: ../logo/logo.png +.. image:: logo.png :width: 400 :alt: SNM Logo diff --git a/logo/logo.png b/docs/logo.png similarity index 100% rename from logo/logo.png rename to docs/logo.png From 42db23553d433646398ca08e9c61071d01d3350f Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Fri, 23 Jul 2021 21:25:14 +0200 Subject: [PATCH 021/225] Nixify the documentation build --- flake.nix | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/flake.nix b/flake.nix index f340c2f..ef764b5 100644 --- a/flake.nix +++ b/flake.nix @@ -56,6 +56,10 @@ modules = [ mailserverModule { + # Because the blockbook package is currently broken (we + # don't care about this package but it is part of the + # NixOS module evaluation) + nixpkgs.config.allowBroken = true; mailserver.fqdn = "mx.example.com"; } ]; @@ -84,11 +88,33 @@ echo "test: ok" > $out ''; + documentation = pkgs.stdenv.mkDerivation { + name = "documentation"; + src = pkgs.lib.sourceByRegex ./docs ["logo.png" "conf.py" "Makefile" ".*rst$"]; + buildInputs = [( + pkgs.python3.withPackages(p: [ + p.sphinx + p.sphinx_rtd_theme + ]) + )]; + buildPhase = '' + cp ${generateRstOptions} options.rst + mkdir -p _static + # Workaround for https://github.com/sphinx-doc/sphinx/issues/3451 + export SOURCE_DATE_EPOCH=$(${pkgs.coreutils}/bin/date +%s) + make html + ''; + installPhase = '' + cp -r _build/html $out + ''; + }; + in rec { nixosModules.mailserver = mailserverModule ; nixosModule = self.nixosModules.mailserver; hydraJobs.${system} = allTests // { test-rst-options = testRstOptions; + inherit documentation; }; checks.${system} = allTests; devShell.${system} = pkgs.mkShell { From 72748d7b6d706ca17817389952a3a7d9185b0d5c Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Fri, 23 Jul 2021 22:57:39 +0200 Subject: [PATCH 022/225] Use the Junk mailbox name defined in the mailboxes attrs Previously, the static Junk mailbox was used in sieve script to move spam messages. This patch gets the Junk mailbox defined in the dovecot mailboxes attribute instead. Fixes #224 --- mail-server/dovecot.nix | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 3781bbd..bc947ae 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -85,9 +85,22 @@ let chmod 600 ${passwdFile} ''; + + junkMailboxes = builtins.attrNames (lib.filterAttrs (n: 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 ""; + 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)"; + } + ]; + services.dovecot2 = { enable = true; enableImap = enableImap || enableImapSsl; @@ -109,7 +122,7 @@ in require "fileinto"; if header :is "X-Spam" "Yes" { - fileinto "Junk"; + fileinto "${junkMailboxName}"; stop; } ''; @@ -229,13 +242,13 @@ in sieve_default_name = default # From elsewhere to Spam folder - imapsieve_mailbox1_name = Junk + imapsieve_mailbox1_name = ${junkMailboxName} imapsieve_mailbox1_causes = COPY imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve # From Spam folder to elsewhere imapsieve_mailbox2_name = * - imapsieve_mailbox2_from = Junk + imapsieve_mailbox2_from = ${junkMailboxName} imapsieve_mailbox2_causes = COPY imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve From fb85a3fe9e4f673f9c017a23ada00b05d61cdc6a Mon Sep 17 00:00:00 2001 From: Steve Purcell Date: Wed, 11 Aug 2021 09:18:24 +0000 Subject: [PATCH 023/225] Ensure locally-delivered mails have the X-Original-To header See #223 --- mail-server/postfix.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 618d6c5..340122b 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -172,6 +172,8 @@ in virtual_mailbox_domains = vhosts_file; virtual_mailbox_maps = mappedFile "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"; # sasl with dovecot smtpd_sasl_type = "dovecot"; @@ -241,6 +243,11 @@ in 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" ]; + }; "policy-spf" = { type = "unix"; privileged = true; From 74bb22799017b22b64fa4aa1a0ea46b8377d48e6 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Thu, 14 Oct 2021 09:06:14 +0200 Subject: [PATCH 024/225] docs: remove output paths from generated documentation Otherwise, the `testRstOptions` test would fail too often! --- docs/options.rst | 18 ++++++++-------- scripts/generate-rst-options.py | 37 ++++++++++++++++++++------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/docs/options.rst b/docs/options.rst index 33616d7..d198f5e 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -489,7 +489,7 @@ For which domains should this account act as a catch all? Note: Does not allow sending from all addresses of these domains. -- Type: ``list of one of s`` +- Type: ``list of impossible (empty enum)s`` - Default: ``[]`` @@ -858,23 +858,23 @@ check system $HOST if loadavg (15min) > 70 for 8 cycles then alert check process sshd with pidfile /var/run/sshd.pid - start program "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl start sshd" - stop program "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl stop sshd" + start program "/bin/systemctl start sshd" + stop program "/bin/systemctl stop sshd" if failed port 22 protocol ssh for 2 cycles then restart check process postfix with pidfile /var/lib/postfix/queue/pid/master.pid - start program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl start postfix" - stop program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl stop postfix" + start program = "/bin/systemctl start postfix" + stop program = "/bin/systemctl stop postfix" if failed port 25 protocol smtp for 5 cycles then restart check process dovecot with pidfile /var/run/dovecot2/master.pid - start program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl start dovecot2" - stop program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl stop dovecot2" + start program = "/bin/systemctl start dovecot2" + stop program = "/bin/systemctl stop dovecot2" if failed host mx.example.com port 993 type tcpssl sslauto protocol imap for 5 cycles then restart check process rspamd with pidfile /var/run/rspamd.pid - start program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl start rspamd" - stop program = "/nix/store/gyg6zyw1f0d1ahh1yk0pl18sxwx5a3zc-systemd-246.6/bin/systemctl stop rspamd" + start program = "/bin/systemctl start rspamd" + stop program = "/bin/systemctl stop rspamd" `` diff --git a/scripts/generate-rst-options.py b/scripts/generate-rst-options.py index da4e3dd..3a13074 100644 --- a/scripts/generate-rst-options.py +++ b/scripts/generate-rst-options.py @@ -1,5 +1,6 @@ import json import sys +import re header = """ Mailserver Options @@ -23,32 +24,38 @@ template = """ f = open(sys.argv[1]) options = json.load(f) -options = { k: v for k, v in options.items() if k.startswith("mailserver.") } +options = {k: v for k, v in options.items() + if k.startswith("mailserver.")} + +groups = ["mailserver.loginAccount", + "mailserver.certificate", + "mailserver.dkim", + "mailserver.fullTextSearch", + "mailserver.redis", + "mailserver.monitoring", + "mailserver.backup", + "mailserver.borg"] -groups = [ "mailserver.loginAccount", - "mailserver.certificate", - "mailserver.dkim", - "mailserver.fullTextSearch", - "mailserver.redis", - "mailserver.monitoring", - "mailserver.backup", - "mailserver.borg" ] def print_option(name, value): - if 'default' in v: - if v['default'] == "": + if 'default' in value: + if value['default'] == "": default = '- Default: ``""``' else: default = '- Default: ``{}``'.format(v['default']) + # Some default values contains OUTPUTPATHS which make the + # output not stable across nixpkgs updates. + default = re.sub('/nix/store/[\w.-]*/', '/', default) # noqa else: default = "" print(template.format( - key=k, - line="-"*len(k), - description=v['description'], - type="- Type: ``{}``".format(v['type']), + key=name, + line="-"*len(name), + description=value['description'], + type="- Type: ``{}``".format(value['type']), default=default)) + print(header) for k, v in options.items(): if any([k.startswith(c) for c in groups]): From acaba31d8f35f640e21a88f1c0719f74b3146568 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Thu, 14 Oct 2021 09:07:32 +0200 Subject: [PATCH 025/225] docs: fix the test which could never fail --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index ef764b5..7d6dc4c 100644 --- a/flake.nix +++ b/flake.nix @@ -84,6 +84,7 @@ then echo "The file ./docs/options.rst is not up-to-date and needs to be regenerated!" echo " hint: run 'nix-shell --run generate-rst-options' to generate this file" + exit 1 fi echo "test: ok" > $out ''; From 0d9a880c0e41a553c5d9af4efa62169db7ddeb62 Mon Sep 17 00:00:00 2001 From: Ero Sennin Date: Thu, 14 Oct 2021 18:45:21 +0000 Subject: [PATCH 026/225] Set DKIM policy to relaxed/relaxed And make this policy configurable. --- default.nix | 20 ++++++++++++++++++++ docs/options.rst | 24 ++++++++++++++++++++++++ mail-server/opendkim.nix | 2 +- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 5d94438..6bb0c23 100644 --- a/default.nix +++ b/default.nix @@ -600,6 +600,26 @@ in ''; }; + dkimHeaderCanonicalization = mkOption { + type = types.enum ["relaxed" "simple"]; + default = "relaxed"; + description = '' + DKIM canonicalization algorithm for message headers. + + See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. + ''; + }; + + dkimBodyCanonicalization = mkOption { + type = types.enum ["relaxed" "simple"]; + default = "relaxed"; + description = '' + DKIM canonicalization algorithm for message bodies. + + See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. + ''; + }; + debug = mkOption { type = types.bool; default = false; diff --git a/docs/options.rst b/docs/options.rst index d198f5e..253690d 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -627,6 +627,30 @@ mailserver.dkim ~~~~~~~~~~~~~~~ +mailserver.dkimBodyCanonicalization +----------------------------------- + +DKIM canonicalization algorithm for message bodies. + +See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. + + +- Type: ``one of "relaxed", "simple"`` +- Default: ``relaxed`` + + +mailserver.dkimHeaderCanonicalization +------------------------------------- + +DKIM canonicalization algorithm for message headers. + +See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. + + +- Type: ``one of "relaxed", "simple"`` +- Default: ``relaxed`` + + mailserver.dkimKeyBits ---------------------- diff --git a/mail-server/opendkim.nix b/mail-server/opendkim.nix index 6fd0bef..3dd7d57 100644 --- a/mail-server/opendkim.nix +++ b/mail-server/opendkim.nix @@ -59,7 +59,7 @@ in keyPath = cfg.dkimKeyDirectory; domains = "csl:${builtins.concatStringsSep "," cfg.domains}"; configFile = pkgs.writeText "opendkim.conf" ('' - Canonicalization relaxed/simple + Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization} UMask 0002 Socket ${dkim.socket} KeyTable file:${keyTable} From ef8ca96c5d0097a0feaf6059a9b012001a096a7f Mon Sep 17 00:00:00 2001 From: Lionello Date: Mon, 1 Nov 2021 23:18:18 +0000 Subject: [PATCH 027/225] Fix typos in indexDir example --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 6bb0c23..bf86e19 100644 --- a/default.nix +++ b/default.nix @@ -205,7 +205,7 @@ in https://doc.dovecot.org/configuration_manual/mail_location/#variables for details. ''; - example = "/var/lib/docecot/indices/%d/%n"; + example = "/var/lib/dovecot/indices"; }; fullTextSearch = { From 9d3a87905e8476b7624168ff41416465ff794ee6 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 7 Nov 2021 11:13:06 +0100 Subject: [PATCH 028/225] docs: add .readthedocs.yml conf file to pin Python dependencies --- .readthedocs.yaml | 21 +++++++++++++++++++++ docs/requirements.txt | 2 ++ 2 files changed, 23 insertions(+) create mode 100644 .readthedocs.yaml create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..3918d82 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,21 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +build: + os: ubuntu-20.04 + tools: + python: "3.9" + +sphinx: + configuration: docs/conf.py + +formats: + - pdf + - epub + +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..1b9a4f8 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx==4.0.2 +sphinx_rtd_theme==0.5.2 From a13526a6e3128c6a4188e4eb145fc94cd1a1ec63 Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Sun, 24 Oct 2021 12:00:00 +0000 Subject: [PATCH 029/225] nginx.nix: don't reload nginx Fixes #227 Reloading nginx manually is actually not needed (see nginx-config-reload.service) and causes deadlocks. --- mail-server/nginx.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index bdead6c..c9cb454 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -36,7 +36,6 @@ in }; security.acme.certs."${cfg.fqdn}".postRun = '' - systemctl reload nginx systemctl reload postfix systemctl reload dovecot2 ''; From 6e8142862f23ab99e1cc57838c02b733361e8d50 Mon Sep 17 00:00:00 2001 From: Lionello Lunesu Date: Sun, 24 Oct 2021 15:46:44 -0700 Subject: [PATCH 030/225] opendkim: don't recreate keys if private key is present --- mail-server/opendkim.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mail-server/opendkim.nix b/mail-server/opendkim.nix index 3dd7d57..27df835 100644 --- a/mail-server/opendkim.nix +++ b/mail-server/opendkim.nix @@ -29,7 +29,7 @@ let dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt"; in '' - if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ] + if [ ! -f "${dkim_key}" ] then ${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \ -d "${dom}" \ @@ -42,10 +42,10 @@ let ''; createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains); - keyTable = pkgs.writeText "opendkim-KeyTable" - (lib.concatStringsSep "\n" (lib.flip map cfg.domains + keyTable = pkgs.writeText "opendkim-KeyTable" + (lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key"))); - signingTable = pkgs.writeText "opendkim-SigningTable" + signingTable = pkgs.writeText "opendkim-SigningTable" (lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}"))); dkim = config.services.opendkim; From 4f0f0128d8d4115571b3ff0ce2378ddf7de7278e Mon Sep 17 00:00:00 2001 From: DwarfMaster Date: Wed, 17 Nov 2021 17:59:32 +0100 Subject: [PATCH 031/225] rspamd: make sure redis is started over TCP socket --- mail-server/rspamd.nix | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index 371edde..57e9cea 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -99,6 +99,12 @@ in }; services.redis.enable = true; + assertions = [ + { + assertion = isNull config.services.redis.unixSocket; + message = "nixos-mailserver doesn't support redis over UNIX socket"; + } + ]; systemd.services.rspamd = { requires = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); From 822c5f22bd6e43b6b4a772045756ebf6a00cd884 Mon Sep 17 00:00:00 2001 From: Lionello Lunesu Date: Thu, 25 Nov 2021 20:53:00 -0800 Subject: [PATCH 032/225] Fix fullTextSearch.enable=false --- mail-server/dovecot.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index bc947ae..18a9262 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -257,7 +257,7 @@ in sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment } - ${lib.optionalString (cfg.fullTextSearch.enable != null) '' + ${lib.optionalString cfg.fullTextSearch.enable '' plugin { plugin = fts fts_xapian fts = xapian From 7c7ed5ce06621578b72725094cab12d6192a4598 Mon Sep 17 00:00:00 2001 From: Kerstin Humm Date: Wed, 1 Dec 2021 01:00:51 +0100 Subject: [PATCH 033/225] Revert "rspamd: make sure redis is started over TCP socket" This reverts commit 4f0f0128d8d4115571b3ff0ce2378ddf7de7278e. Redis does seem to run fine with both unixSocket and TCP enabled. This broke people's setups. --- mail-server/rspamd.nix | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index 57e9cea..371edde 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -99,12 +99,6 @@ in }; services.redis.enable = true; - assertions = [ - { - assertion = isNull config.services.redis.unixSocket; - message = "nixos-mailserver doesn't support redis over UNIX socket"; - } - ]; systemd.services.rspamd = { requires = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); From f3d967f8308f0f617597ab0b96bfec3fc1aeb325 Mon Sep 17 00:00:00 2001 From: Izorkin Date: Fri, 16 Nov 2018 14:11:31 +0300 Subject: [PATCH 034/225] nginx: generate certificates for custom domains and subdomains --- default.nix | 7 +++++++ docs/options.rst | 9 +++++++++ mail-server/nginx.nix | 1 + 3 files changed, 17 insertions(+) diff --git a/default.nix b/default.nix index bf86e19..f46f1a4 100644 --- a/default.nix +++ b/default.nix @@ -44,6 +44,13 @@ in description = "The domains that this mail server serves."; }; + certificateDomains = mkOption { + type = types.listOf types.str; + example = [ "imap.example.com" "pop3.example.com" ]; + default = []; + description = "Secondary domains and subdomains for which it is necessary to generate a certificate."; + }; + messageSizeLimit = mkOption { type = types.int; example = 52428800; diff --git a/docs/options.rst b/docs/options.rst index 253690d..b3c2ef9 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -595,6 +595,15 @@ certificate is valid for 10 years. - Default: ``/var/certs`` +mailserver.certificateDomains +----------------------------- + +Secondary domains and subdomains for which it is necessary to generate a certificate. + +- Type: ``list of strings`` +- Default: ``[]`` + + mailserver.certificateFile -------------------------- diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index c9cb454..1590c5f 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -29,6 +29,7 @@ in enable = true; virtualHosts."${cfg.fqdn}" = { serverName = cfg.fqdn; + serverAliases = cfg.certificateDomains; forceSSL = true; enableACME = true; acmeRoot = acmeRoot; From 6e3a7b2ea6f0d68b82027b988aa25d3423787303 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 1 Dec 2021 23:08:29 +0100 Subject: [PATCH 035/225] Release nixos-21.11 --- .hydra/declarative-jobsets.nix | 2 +- README.md | 10 +++++----- docs/release-notes.rst | 15 +++++++++++++++ flake.lock | 16 ++++++++++++++++ flake.nix | 7 ++++++- 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 5273cc2..130df05 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -61,8 +61,8 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; - "nixos-20.09" = mkJobset "nixos-20.09"; "nixos-21.05" = mkJobset "nixos-21.05"; + "nixos-21.11" = mkJobset "nixos-21.11"; }; log = { diff --git a/README.md b/README.md index 647dcfa..c77cf24 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,18 @@ For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. +* For NixOS 21.11 + - Use the [SNM branch `nixos-21.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-21.11) + - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-21.11/) + - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-21.11/release-notes.html#nixos-21-11) * For NixOS 21.05 - Use the [SNM branch `nixos-21.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-21.05) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-21.05/) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-21.05/release-notes.html#nixos-21-05) -* For NixOS 20.09 - - Use the [SNM branch `nixos-20.09`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.09) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/) - - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/release-notes.html#nixos-20-09) * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) - - This branch is currently supporting the NixOS release 21.05 but + - This branch is currently supporting the NixOS release 21.11 but we could remove this support on any NixOS unstable breaking change. diff --git a/docs/release-notes.rst b/docs/release-notes.rst index fce2abf..cf9fad2 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,6 +1,21 @@ Release Notes ============= +NixOS 21.11 +----------- + +- Switch default DKIM body policy from simple to relaxed + (`merge request `__) +- Ensure locally-delivered mails have the X-Original-To header + (`merge request `__) +- NixOS Mailserver options are detailed in the `documentation + `__ +- New options ``dkimBodyCanonicalization`` and + ``dkimHeaderCanonicalization`` +- New option ``certificateDomains`` to generate certificate for + additional domains (such as ``imap.example.com``) + + NixOS 21.05 ----------- diff --git a/flake.lock b/flake.lock index f3ec998..3580349 100644 --- a/flake.lock +++ b/flake.lock @@ -46,11 +46,27 @@ "type": "indirect" } }, + "nixpkgs-21_11": { + "locked": { + "lastModified": 1638371214, + "narHash": "sha256-0kE6KhgH7n0vyuX4aUoGsGIQOqjIx2fJavpCWtn73rc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a640d8394f34714578f3e6335fc767d0755d78f9", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-21.11", + "type": "indirect" + } + }, "root": { "inputs": { "blobs": "blobs", "nixpkgs": "nixpkgs", "nixpkgs-21_05": "nixpkgs-21_05", + "nixpkgs-21_11": "nixpkgs-21_11", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index 7d6dc4c..33e433e 100644 --- a/flake.nix +++ b/flake.nix @@ -5,13 +5,14 @@ utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; nixpkgs-21_05.url = "flake:nixpkgs/nixos-21.05"; + nixpkgs-21_11.url = "flake:nixpkgs/nixos-21.11"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, utils, blobs, nixpkgs, nixpkgs-21_05 }: let + outputs = { self, utils, blobs, nixpkgs, nixpkgs-21_05, nixpkgs-21_11 }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; # We want to test nixos-mailserver on several nixos releases @@ -24,6 +25,10 @@ name = "21_05"; pkgs = nixpkgs-21_05.legacyPackages.${system}; } + { + name = "21_11"; + pkgs = nixpkgs-21_11.legacyPackages.${system}; + } ]; testNames = [ "internal" From 665aa181e6a629482f85df46c58a2cc6e3998ded Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 20 Feb 2022 11:29:33 +0100 Subject: [PATCH 036/225] ci: make release-21.11 a flake job --- .hydra/declarative-jobsets.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 130df05..4feaf69 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -57,12 +57,12 @@ let hidden = false; type = 1; flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/${branch}"; - }; + }; desc = prJobsets // { "master" = mkFlakeJobset "master"; "nixos-21.05" = mkJobset "nixos-21.05"; - "nixos-21.11" = mkJobset "nixos-21.11"; + "nixos-21.11" = mkFlakeJobset "nixos-21.11"; }; log = { From 11ad4742aa3c79443d7453fb139d5a3dad4cabfa Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 20 Feb 2022 10:11:05 +0100 Subject: [PATCH 037/225] Fix CI job because of Nix new CLI options --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b74daa0..6c386e4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,11 +3,11 @@ hydra-pr: - merge_requests image: nixos/nix script: - - nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID} + - nix run -f channel:nixos-unstable hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID} hydra-master: only: - master image: nixos/nix script: - - nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master + - nix run -f channel:nixos-unstable hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master From ef03562ebae513b506c58e198038352ee6ae5138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Sun, 26 Dec 2021 19:15:06 +0100 Subject: [PATCH 038/225] make option documentation compatible with nixos-search --- default.nix | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/default.nix b/default.nix index f46f1a4..f142422 100644 --- a/default.nix +++ b/default.nix @@ -243,7 +243,7 @@ in description = '' Fail searches when no index is available. If set to body, then only body searches (as opposed to - header) are affected. If set to no, searches may + header) are affected. If set to no, searches may fall back to a very slow brute force search. ''; }; @@ -488,7 +488,7 @@ in description = '' Scheme 2) This is the folder where the certificate will be created. The name is - hardcoded to "cert-.pem" and "key-.pem" and the + hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the certificate is valid for 10 years. ''; }; @@ -677,28 +677,27 @@ in if (ip == "0.0.0.0" || ip == "::") then "127.0.0.1" else if isIpv6 ip then "[${ip}]" else ip; + defaultText = lib.literalDocBook "computed from "; description = '' - Address that rspamd should use to contact redis. The default value - is read from config.services.redis.bind. + Address that rspamd should use to contact redis. ''; }; port = mkOption { type = types.port; default = config.services.redis.port; + defaultText = lib.literalExpression "config.services.redis.port"; description = '' - Port that rspamd should use to contact redis. The default value is - read from config.services.redis.port. + Port that rspamd should use to contact redis. ''; }; password = mkOption { type = types.nullOr types.str; default = config.services.redis.requirePass; + defaultText = lib.literalExpression "config.services.redis.requirePass"; description = '' - Password that rspamd should use to contact redis, or null if not - required. The default value is read from - config.services.redis.requirePass. + Password that rspamd should use to contact redis, or null if not required. ''; }; }; @@ -804,6 +803,7 @@ in start program = "${pkgs.systemd}/bin/systemctl start rspamd" stop program = "${pkgs.systemd}/bin/systemctl stop rspamd" ''; + defaultText = lib.literalDocBook "see source"; description = '' The configuration used for monitoring via monit. Use a mail address that you actively check and set it via 'set alert ...'. @@ -887,6 +887,7 @@ in passphraseFile = mkOption { type = types.nullOr types.path; default = null; + description = "Path to a file containing the encryption password or passphrase."; }; }; From f4c14572fcd384d2cb3c63be1cd26fbe143803f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Fri, 21 Jan 2022 13:02:15 +0100 Subject: [PATCH 039/225] Drop 21.05 branch --- .hydra/declarative-jobsets.nix | 3 +-- README.md | 2 +- flake.lock | 16 ---------------- flake.nix | 11 +++-------- 4 files changed, 5 insertions(+), 27 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 4feaf69..a71242b 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -17,7 +17,7 @@ let flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head"; } ) prs; - # This could be removed once branch 20.09 and 21.05 would have been + # This could be removed once branch 21.11 would have been # removed. mkJobset = branch: { description = "Build ${branch} branch of Simple NixOS MailServer"; @@ -61,7 +61,6 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; - "nixos-21.05" = mkJobset "nixos-21.05"; "nixos-21.11" = mkFlakeJobset "nixos-21.11"; }; diff --git a/README.md b/README.md index c77cf24..794a12c 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A ```nix { config, pkgs, ... }: - let release = "nixos-21.05"; + let release = "nixos-21.11"; in { imports = [ (builtins.fetchTarball { diff --git a/flake.lock b/flake.lock index 3580349..d78f8a8 100644 --- a/flake.lock +++ b/flake.lock @@ -31,21 +31,6 @@ "type": "indirect" } }, - "nixpkgs-21_05": { - "locked": { - "lastModified": 1625692408, - "narHash": "sha256-e9L3TLLDVIJpMnHtiNHJE62oOh6emRtSZ244bgYJUZs=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "c06613c25df3fe1dd26243847a3c105cf6770627", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "ref": "nixos-21.05", - "type": "indirect" - } - }, "nixpkgs-21_11": { "locked": { "lastModified": 1638371214, @@ -65,7 +50,6 @@ "inputs": { "blobs": "blobs", "nixpkgs": "nixpkgs", - "nixpkgs-21_05": "nixpkgs-21_05", "nixpkgs-21_11": "nixpkgs-21_11", "utils": "utils" } diff --git a/flake.nix b/flake.nix index 33e433e..f7c35c8 100644 --- a/flake.nix +++ b/flake.nix @@ -4,7 +4,6 @@ inputs = { utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; - nixpkgs-21_05.url = "flake:nixpkgs/nixos-21.05"; nixpkgs-21_11.url = "flake:nixpkgs/nixos-21.11"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; @@ -12,7 +11,7 @@ }; }; - outputs = { self, utils, blobs, nixpkgs, nixpkgs-21_05, nixpkgs-21_11 }: let + outputs = { self, utils, blobs, nixpkgs, nixpkgs-21_11 }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; # We want to test nixos-mailserver on several nixos releases @@ -21,10 +20,6 @@ name = "unstable"; pkgs = nixpkgs.legacyPackages.${system}; } - { - name = "21_05"; - pkgs = nixpkgs-21_05.legacyPackages.${system}; - } { name = "21_11"; pkgs = nixpkgs-21_11.legacyPackages.${system}; @@ -54,7 +49,7 @@ mailserverModule = import ./.; - # Generate a rst file describing options of the NixOS mailserver module + # Generate a rst file describing options of the NixOS mailserver module generateRstOptions = let eval = import (pkgs.path + "/nixos/lib/eval-config.nix") { inherit system; @@ -68,7 +63,7 @@ mailserver.fqdn = "mx.example.com"; } ]; - + }; options = pkgs.nixosOptionsDoc { options = eval.options; From 4ed684481b550a802a9c4af97cdc60354d03a615 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Fri, 21 Jan 2022 14:42:39 +0100 Subject: [PATCH 040/225] Update nixos-unstable and drop 21.11 --- .hydra/declarative-jobsets.nix | 30 ------------------------------ README.md | 3 --- default.nix | 12 ++++++------ flake.lock | 22 +++------------------- flake.nix | 8 +------- mail-server/rspamd.nix | 6 +++--- 6 files changed, 13 insertions(+), 68 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index a71242b..b831abe 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -17,35 +17,6 @@ let flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head"; } ) prs; - # This could be removed once branch 21.11 would have been - # removed. - mkJobset = branch: { - description = "Build ${branch} branch of Simple NixOS MailServer"; - checkinterval = "60"; - enabled = "1"; - schedulingshares = 100; - enableemail = false; - emailoverride = ""; - nixexprinput = "snm"; - nixexprpath = ".hydra/default.nix"; - type = 0; - inputs = { - # This is only used to allow Niv to use pkgs.fetchzip which is - # required because of Hydra restricted evaluation mode. - nixpkgs = { - value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3"; - type = "git"; - emailresponsible = false; - }; - snm = { - value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver ${branch}"; - type = "git"; - emailresponsible = false; - }; - }; - keepnr = 3; - hidden = false; - }; mkFlakeJobset = branch: { description = "Build ${branch} branch of Simple NixOS MailServer"; checkinterval = "60"; @@ -61,7 +32,6 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; - "nixos-21.11" = mkFlakeJobset "nixos-21.11"; }; log = { diff --git a/README.md b/README.md index 794a12c..a9494b4 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,6 @@ SNM branch corresponding to your NixOS version. * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) - - This branch is currently supporting the NixOS release 21.11 but - we could remove this support on any NixOS unstable breaking - change. [Subscribe to SNM Announcement List](https://www.freelists.org/list/snm) This is a very low volume list where new releases of SNM are announced, so you diff --git a/default.nix b/default.nix index f142422..c438099 100644 --- a/default.nix +++ b/default.nix @@ -668,7 +668,7 @@ in type = types.str; # read the default from nixos' redis module default = let - cf = config.services.redis.bind; + cf = config.services.redis.servers.rspamd.bind; cfdefault = if cf == null then "127.0.0.1" else cf; ips = lib.strings.splitString " " cfdefault; ip = lib.lists.head (ips ++ [ "127.0.0.1" ]); @@ -677,7 +677,7 @@ in if (ip == "0.0.0.0" || ip == "::") then "127.0.0.1" else if isIpv6 ip then "[${ip}]" else ip; - defaultText = lib.literalDocBook "computed from "; + defaultText = lib.literalDocBook "computed from "; description = '' Address that rspamd should use to contact redis. ''; @@ -685,8 +685,8 @@ in port = mkOption { type = types.port; - default = config.services.redis.port; - defaultText = lib.literalExpression "config.services.redis.port"; + default = config.services.redis.servers.rspamd.port; + defaultText = lib.literalExpression "config.services.redis.servers.rspamd.port"; description = '' Port that rspamd should use to contact redis. ''; @@ -694,8 +694,8 @@ in password = mkOption { type = types.nullOr types.str; - default = config.services.redis.requirePass; - defaultText = lib.literalExpression "config.services.redis.requirePass"; + default = config.services.redis.servers.rspamd.requirePass; + defaultText = lib.literalExpression "config.services.redis.servers.rspamd.requirePass"; description = '' Password that rspamd should use to contact redis, or null if not required. ''; diff --git a/flake.lock b/flake.lock index d78f8a8..eb8f927 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1626852498, - "narHash": "sha256-lOXUJvi0FJUXHTVSiC5qsMRtEUgqM4mGZpMESLuGhmo=", + "lastModified": 1642635915, + "narHash": "sha256-vabPA32j81xBO5m3+qXndWp5aqepe+vu96Wkd9UnngM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "16105403bdd843540cbef9c63fc0f16c1c6eaa70", + "rev": "6d8215281b2f87a5af9ed7425a26ac575da0438f", "type": "github" }, "original": { @@ -31,26 +31,10 @@ "type": "indirect" } }, - "nixpkgs-21_11": { - "locked": { - "lastModified": 1638371214, - "narHash": "sha256-0kE6KhgH7n0vyuX4aUoGsGIQOqjIx2fJavpCWtn73rc=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "a640d8394f34714578f3e6335fc767d0755d78f9", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "ref": "nixos-21.11", - "type": "indirect" - } - }, "root": { "inputs": { "blobs": "blobs", "nixpkgs": "nixpkgs", - "nixpkgs-21_11": "nixpkgs-21_11", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index f7c35c8..35299aa 100644 --- a/flake.nix +++ b/flake.nix @@ -4,26 +4,20 @@ inputs = { utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; - nixpkgs-21_11.url = "flake:nixpkgs/nixos-21.11"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, utils, blobs, nixpkgs, nixpkgs-21_11 }: let + outputs = { self, utils, blobs, nixpkgs }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; - # We want to test nixos-mailserver on several nixos releases releases = [ { name = "unstable"; pkgs = nixpkgs.legacyPackages.${system}; } - { - name = "21_11"; - pkgs = nixpkgs-21_11.legacyPackages.${system}; - } ]; testNames = [ "internal" diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index 371edde..8d22e76 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -98,11 +98,11 @@ in }; - services.redis.enable = true; + services.redis.servers.rspamd.enable = true; systemd.services.rspamd = { - requires = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); - after = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); + requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); + after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); }; systemd.services.postfix = { From 53af8832553bb2f82e210d1ea07040e9371af582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Fri, 21 Jan 2022 15:06:14 +0100 Subject: [PATCH 041/225] Regenerate options.rst --- docs/options.rst | 62 ++++++--------------------------- scripts/generate-rst-options.py | 16 ++++++--- 2 files changed, 21 insertions(+), 57 deletions(-) diff --git a/docs/options.rst b/docs/options.rst index b3c2ef9..32f6d77 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -587,7 +587,7 @@ mailserver.certificateDirectory Scheme 2) This is the folder where the certificate will be created. The name is -hardcoded to "cert-.pem" and "key-.pem" and the +hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the certificate is valid for 10 years. @@ -741,7 +741,7 @@ mailserver.fullTextSearch.enforced Fail searches when no index is available. If set to body, then only body searches (as opposed to -header) are affected. If set to no, searches may +header) are affected. If set to no, searches may fall back to a very slow brute force search. @@ -818,35 +818,31 @@ mailserver.redis mailserver.redis.address ------------------------ -Address that rspamd should use to contact redis. The default value -is read from config.services.redis.bind. +Address that rspamd should use to contact redis. - Type: ``string`` -- Default: ``127.0.0.1`` +- Default: computed from mailserver.redis.password ------------------------- -Password that rspamd should use to contact redis, or null if not -required. The default value is read from -config.services.redis.requirePass. +Password that rspamd should use to contact redis, or null if not required. - Type: ``null or string`` -- Default: ``None`` +- Default: ``config.services.redis.servers.rspamd.requirePass`` mailserver.redis.port --------------------- -Port that rspamd should use to contact redis. The default value is -read from config.services.redis.port. +Port that rspamd should use to contact redis. - Type: ``16 bit unsigned integer; between 0 and 65535 (both inclusive)`` -- Default: ``6379`` +- Default: ``config.services.redis.servers.rspamd.port`` mailserver.monitoring ~~~~~~~~~~~~~~~~~~~~~ @@ -870,45 +866,7 @@ Use a mail address that you actively check and set it via 'set alert ...'. - Type: ``string`` -- Default: ``set daemon 120 with start delay 60 -set mailserver - localhost - -set httpd port 2812 and use address localhost - allow localhost - allow admin:obwjoawijerfoijsiwfj29jf2f2jd - -check filesystem root with path / - if space usage > 80% then alert - if inode usage > 80% then alert - -check system $HOST - if cpu usage > 95% for 10 cycles then alert - if memory usage > 75% for 5 cycles then alert - if swap usage > 20% for 10 cycles then alert - if loadavg (1min) > 90 for 15 cycles then alert - if loadavg (5min) > 80 for 10 cycles then alert - if loadavg (15min) > 70 for 8 cycles then alert - -check process sshd with pidfile /var/run/sshd.pid - start program "/bin/systemctl start sshd" - stop program "/bin/systemctl stop sshd" - if failed port 22 protocol ssh for 2 cycles then restart - -check process postfix with pidfile /var/lib/postfix/queue/pid/master.pid - start program = "/bin/systemctl start postfix" - stop program = "/bin/systemctl stop postfix" - if failed port 25 protocol smtp for 5 cycles then restart - -check process dovecot with pidfile /var/run/dovecot2/master.pid - start program = "/bin/systemctl start dovecot2" - stop program = "/bin/systemctl stop dovecot2" - if failed host mx.example.com port 993 type tcpssl sslauto protocol imap for 5 cycles then restart - -check process rspamd with pidfile /var/run/rspamd.pid - start program = "/bin/systemctl start rspamd" - stop program = "/bin/systemctl stop rspamd" -`` +- Default: see source mailserver.monitoring.enable @@ -1081,7 +1039,7 @@ When using encryption the password / passphrase must be provided in passphraseFi mailserver.borgbackup.encryption.passphraseFile ----------------------------------------------- -This option has no description. +Path to a file containing the encryption password or passphrase. - Type: ``null or path`` - Default: ``None`` diff --git a/scripts/generate-rst-options.py b/scripts/generate-rst-options.py index 3a13074..a7d751e 100644 --- a/scripts/generate-rst-options.py +++ b/scripts/generate-rst-options.py @@ -40,12 +40,18 @@ groups = ["mailserver.loginAccount", def print_option(name, value): if 'default' in value: if value['default'] == "": - default = '- Default: ``""``' + default = '``""``' + elif isinstance(value['default'], dict) and '_type' in value['default']: + if value['default']['_type'] == 'literalExpression': + default = '``{}``'.format(value['default']['text']) + if value['default']['_type'] == 'literalDocBook': + default = value['default']['text'] else: - default = '- Default: ``{}``'.format(v['default']) - # Some default values contains OUTPUTPATHS which make the - # output not stable across nixpkgs updates. - default = re.sub('/nix/store/[\w.-]*/', '/', default) # noqa + default = '``{}``'.format(value['default']) + # Some default values contains OUTPUTPATHS which make the + # output not stable across nixpkgs updates. + default = re.sub('/nix/store/[\w.-]*/', '/', default) # noqa + default = '- Default: ' + default else: default = "" print(template.format( From 46ef908c91cd6917e91fb9ff6c1c94cad8b0ac36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Fri, 21 Jan 2022 15:58:56 +0100 Subject: [PATCH 042/225] rspamd: set default port for redis Since we are now using services.redis.servers.rspamd, the port defaults to 0 (i.e. do not bind a TCP socket). We still want rspamd to connect to redis via TCP, so set a default port that is one above the default redis port. --- mail-server/rspamd.nix | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index 8d22e76..efe3abc 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -98,7 +98,10 @@ in }; - services.redis.servers.rspamd.enable = true; + services.redis.servers.rspamd = { + enable = lib.mkDefault true; + port = lib.mkDefault 6380; + }; systemd.services.rspamd = { requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); From 021b5c8f7346aad37ba7f208408b1fb11b3b5f88 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Fri, 25 Feb 2022 09:06:05 +0100 Subject: [PATCH 043/225] ci: enable the nix-command feature --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6c386e4..0a2cb4b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,11 +3,11 @@ hydra-pr: - merge_requests image: nixos/nix script: - - nix run -f channel:nixos-unstable hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID} + - nix --extra-experimental-features nix-command run -f channel:nixos-unstable hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID} hydra-master: only: - master image: nixos/nix script: - - nix run -f channel:nixos-unstable hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master + - nix --extra-experimental-features nix-command run -f channel:nixos-unstable hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master From 7de138037f62679e2fefa0549af543412dab0d1a Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Fri, 31 Dec 2021 12:00:00 +0000 Subject: [PATCH 044/225] docs: add how-to to setup roundcube --- docs/add-roundcube.rst | 32 ++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 33 insertions(+) create mode 100644 docs/add-roundcube.rst diff --git a/docs/add-roundcube.rst b/docs/add-roundcube.rst new file mode 100644 index 0000000..08e41ed --- /dev/null +++ b/docs/add-roundcube.rst @@ -0,0 +1,32 @@ +Add Roundube, a webmail +======================= + +The NixOS module for roundcube nearly works out of the box with SNM. By +default, it sets up a nginx virtual host to serve the webmail, other web +servers may require more work. + +.. code:: nix + + { config, pkgs, lib, ... }: + + with lib; + + { + services.roundcube = { + enable = true; + # this is the url of the vhost, not necessarily the same as the fqdn of + # the mailserver + hostName = "webmail.example.com"; + extraConfig = '' + # starttls needed for authentication, so the fqdn required to match + # the certificate + $config['smtp_server'] = "tls://${config.mailserver.fqdn}"; + $config['smtp_user'] = "%u"; + $config['smtp_pass'] = "%p"; + ''; + }; + + services.nginx.enable = true; + + networking.firewall.allowedTCPPorts = [ 80 443 ]; + } diff --git a/docs/index.rst b/docs/index.rst index 262c664..b8700ff 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -25,6 +25,7 @@ Welcome to NixOS Mailserver's documentation! backup-guide add-radicale + add-roundcube rspamd-tuning fts flakes From 75728d2686dec5ef2a73cdf80174311587413392 Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Sat, 5 Mar 2022 12:00:00 +0000 Subject: [PATCH 045/225] tests: compatibility with fts xapian 1.5.4 --- tests/external.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/external.nix b/tests/external.nix index ba97815..f30a1e5 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -489,7 +489,7 @@ pkgs.nixosTest { client.fail("search Junk a >&2") # check that search really goes through the indexer server.succeed( - "journalctl -u dovecot2 | grep -E 'indexer-worker.* Mailbox INBOX: Mailbox opened because: indexing' >&2" + "journalctl -u dovecot2 | grep -E 'indexer-worker.* Done indexing .INBOX.' >&2" ) # check that Junk is not indexed server.fail("journalctl -u dovecot2 | grep 'indexer-worker' | grep -i 'JUNK' >&2") From 4ce864f52ae7e1733582a32d66c1f94ee11a52c8 Mon Sep 17 00:00:00 2001 From: Fatih Altinok Date: Sat, 16 Apr 2022 18:17:48 +0000 Subject: [PATCH 046/225] Fix typo in title --- docs/add-roundcube.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/add-roundcube.rst b/docs/add-roundcube.rst index 08e41ed..6d4795c 100644 --- a/docs/add-roundcube.rst +++ b/docs/add-roundcube.rst @@ -1,4 +1,4 @@ -Add Roundube, a webmail +Add Roundcube, a webmail ======================= The NixOS module for roundcube nearly works out of the box with SNM. By From 4396125ebbbaecbaba0f8e0afb25e492b3be5795 Mon Sep 17 00:00:00 2001 From: Ryan Mulligan Date: Sun, 8 May 2022 15:45:24 -0700 Subject: [PATCH 047/225] docs/full text search: fix typo; improve ux docecot -> dovecot Also, `indexDir` is not expecting to see %d/%n being passed to that parameter, so remove that to make it easier to cpy the path into there. --- docs/fts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fts.rst b/docs/fts.rst index aed2cba..5d84eaf 100644 --- a/docs/fts.rst +++ b/docs/fts.rst @@ -42,7 +42,7 @@ Indices created by the full text search feature can take more disk space than the emails themselves. By default, they are kept in the emails location. When enabling the full text search feature, it is recommended to move indices in a different location, such as -(``/var/lib/docecot/indices/%d/%n``) by using the option +(``/var/lib/dovecot/indices``) by using the option ``mailserver.indexDir``. .. warning:: From 6284a20f77268d037823794f36396ab2e6b84a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20Hamb=C3=BCchen?= Date: Tue, 15 Feb 2022 16:42:11 +0100 Subject: [PATCH 048/225] acme: Switch from `postRun` to `reloadServices` to fix hangs. Fixes #232 --- mail-server/nginx.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index 1590c5f..abc6421 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -36,9 +36,9 @@ in }; }; - security.acme.certs."${cfg.fqdn}".postRun = '' - systemctl reload postfix - systemctl reload dovecot2 - ''; + security.acme.certs."${cfg.fqdn}".reloadServices = [ + "postfix.service" + "dovecot2.service" + ]; }; } From 15cf252a0dfffb2420a60fbd6be3467c11026142 Mon Sep 17 00:00:00 2001 From: Ryan Mulligan Date: Sun, 8 May 2022 15:18:13 -0700 Subject: [PATCH 049/225] monit/rspamd: monitor by process name --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index c438099..7ef5cba 100644 --- a/default.nix +++ b/default.nix @@ -799,7 +799,7 @@ in stop program = "${pkgs.systemd}/bin/systemctl stop dovecot2" if failed host ${cfg.fqdn} port 993 type tcpssl sslauto protocol imap for 5 cycles then restart - check process rspamd with pidfile /var/run/rspamd.pid + check process rspamd with matching "rspamd: main process" start program = "${pkgs.systemd}/bin/systemctl start rspamd" stop program = "${pkgs.systemd}/bin/systemctl stop rspamd" ''; From f535d8123c4761b2ed8138f3d202ea710a334a1d Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Tue, 14 Jun 2022 15:44:15 +0200 Subject: [PATCH 050/225] Release 22.05 --- .hydra/declarative-jobsets.nix | 1 + README.md | 8 ++++---- docs/release-notes.rst | 6 ++++++ flake.lock | 16 ++++++++++++++++ flake.nix | 7 ++++++- 5 files changed, 33 insertions(+), 5 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index b831abe..11b2763 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -32,6 +32,7 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; + "nixos-22.05" = mkFlakeJobset "nixos-22.05"; }; log = { diff --git a/README.md b/README.md index a9494b4..0adc7d6 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. +* For NixOS 22.05 + - Use the [SNM branch `nixos-22.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.05) + - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/) + - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/release-notes.html#nixos-22-05) * For NixOS 21.11 - Use the [SNM branch `nixos-21.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-21.11) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-21.11/) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-21.11/release-notes.html#nixos-21-11) -* For NixOS 21.05 - - Use the [SNM branch `nixos-21.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-21.05) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-21.05/) - - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-21.05/release-notes.html#nixos-21-05) * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index cf9fad2..98c96b5 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,6 +1,12 @@ Release Notes ============= +NixOS 22.05 +----------- + +- Make NixOS Mailserver options discoverable from search.nixos.org +- Add a roundcube setup guide in the documentation + NixOS 21.11 ----------- diff --git a/flake.lock b/flake.lock index eb8f927..e98dc26 100644 --- a/flake.lock +++ b/flake.lock @@ -31,10 +31,26 @@ "type": "indirect" } }, + "nixpkgs-22_05": { + "locked": { + "lastModified": 1654936503, + "narHash": "sha256-soKzdhI4jTHv/rSbh89RdlcJmrPgH8oMb/PLqiqIYVQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "dab6df51387c3878cdea09f43589a15729cae9f4", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-22.05", + "type": "indirect" + } + }, "root": { "inputs": { "blobs": "blobs", "nixpkgs": "nixpkgs", + "nixpkgs-22_05": "nixpkgs-22_05", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index 35299aa..e5b85ad 100644 --- a/flake.nix +++ b/flake.nix @@ -4,13 +4,14 @@ inputs = { utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; + nixpkgs-22_05.url = "flake:nixpkgs/nixos-22.05"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, utils, blobs, nixpkgs }: let + outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_05 }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; releases = [ @@ -18,6 +19,10 @@ name = "unstable"; pkgs = nixpkgs.legacyPackages.${system}; } + { + name = "22.05"; + pkgs = nixpkgs-22_05.legacyPackages.${system}; + } ]; testNames = [ "internal" From 004c229ca44c069d93c92abf67ff1619fb508c6a Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 19 Jul 2022 23:54:04 +0200 Subject: [PATCH 051/225] Convert minimal test to python test driver --- tests/minimal.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/minimal.nix b/tests/minimal.nix index 7327f55..88cb276 100644 --- a/tests/minimal.nix +++ b/tests/minimal.nix @@ -14,9 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -import { +import { - machine = + nodes.machine = { config, pkgs, ... }: { imports = [ @@ -26,6 +26,6 @@ import { testScript = '' - $machine->waitForUnit("multi-user.target"); + machine.wait_for_unit("multi-user.target"); ''; } From a40e9c3abb4b12d384df1ff2ca9a537227635e1f Mon Sep 17 00:00:00 2001 From: Linus Heckemann Date: Sun, 27 Nov 2022 19:14:22 +0000 Subject: [PATCH 052/225] htpasswd -> mkpasswd --- .gitlab-ci.yml | 4 ++-- README.md | 2 +- default.nix | 12 ++++++------ docs/add-radicale.rst | 4 ++-- docs/options.rst | 12 ++++++------ docs/setup-guide.rst | 2 +- tests/internal.nix | 4 ++-- tests/multiple.nix | 4 ++-- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0a2cb4b..702c294 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,11 +3,11 @@ hydra-pr: - merge_requests image: nixos/nix script: - - nix --extra-experimental-features nix-command run -f channel:nixos-unstable hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID} + - nix-shell -I nixpkgs=channel:nixos-unstable -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}' hydra-master: only: - master image: nixos/nix script: - - nix --extra-experimental-features nix-command run -f channel:nixos-unstable hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master + - nix-shell -I nixpkgs=channel:nixos-unstable -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master' diff --git a/README.md b/README.md index 0adc7d6..86351d7 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A domains = [ "example.com" "example2.com" ]; loginAccounts = { "user1@example.com" = { - # nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 > /hashed/password/file/location + # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' > /hashed/password/file/location hashedPasswordFile = "/hashed/password/file/location"; aliases = [ diff --git a/default.nix b/default.nix index 7ef5cba..7c9118e 100644 --- a/default.nix +++ b/default.nix @@ -72,10 +72,10 @@ in default = null; example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; description = '' - The user's hashed password. Use `htpasswd` as follows + The user's hashed password. Use `mkpasswd` as follows ``` - nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 + nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` Warning: this is stored in plaintext in the Nix store! @@ -88,10 +88,10 @@ in default = null; example = "/run/keys/user1-passwordhash"; description = '' - A file containing the user's hashed password. Use `htpasswd` as follows + A file containing the user's hashed password. Use `mkpasswd` as follows ``` - nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 + nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` ''; }; @@ -184,11 +184,11 @@ in }; description = '' The login account of the domain. Every account is mapped to a unix user, - e.g. `user1@example.com`. To generate the passwords use `htpasswd` as + e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as follows ``` - nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 + nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` ''; default = {}; diff --git a/docs/add-radicale.rst b/docs/add-radicale.rst index 3f29434..2393f6e 100644 --- a/docs/add-radicale.rst +++ b/docs/add-radicale.rst @@ -4,8 +4,8 @@ Add Radicale Configuration by @dotlambda Starting with Radicale 3 (first introduced in NixOS 20.09) the traditional -crypt passwords, as generated by `mkpasswd`, are no longer supported. Instead -bcrypt passwords have to be used which can be generated using `htpasswd`. +crypt passwords are no longer supported. Instead bcrypt passwords +have to be used. These can still be generated using `mkpasswd -m bcrypt`. .. code:: nix diff --git a/docs/options.rst b/docs/options.rst index 32f6d77..1e0af9a 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -458,11 +458,11 @@ mailserver.loginAccounts ------------------------ The login account of the domain. Every account is mapped to a unix user, -e.g. `user1@example.com`. To generate the passwords use `htpasswd` as +e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as follows ``` -nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 +nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` @@ -496,10 +496,10 @@ Note: Does not allow sending from all addresses of these domains. mailserver.loginAccounts..hashedPassword ---------------------------------------------- -The user's hashed password. Use `htpasswd` as follows +The user's hashed password. Use `mkpasswd` as follows ``` -nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 +nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` Warning: this is stored in plaintext in the Nix store! @@ -513,10 +513,10 @@ Use `hashedPasswordFile` instead. mailserver.loginAccounts..hashedPasswordFile -------------------------------------------------- -A file containing the user's hashed password. Use `htpasswd` as follows +A file containing the user's hashed password. Use `mkpasswd` as follows ``` -nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 +nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 7e6137e..4a39676 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -70,7 +70,7 @@ these should be the most common ones. domains = [ "example.com" ]; # A list of all login accounts. To create the password hashes, use - # nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 + # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' loginAccounts = { "user1@example.com" = { hashedPasswordFile = "/a/file/containing/a/hashed/password"; diff --git a/tests/internal.nix b/tests/internal.nix index e8c4227..02609fd 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -29,8 +29,8 @@ let hashPassword = password: pkgs.runCommand "password-${password}-hashed" - { buildInputs = [ pkgs.apacheHttpd ]; } '' - htpasswd -nbB "" "${password}" | cut -d: -f2 > $out + { buildInputs = [ pkgs.mkpasswd ]; inherit password; } '' + mkpasswd -sm bcrypt <<<"$password" > $out ''; hashedPasswordFile = hashPassword "my-password"; diff --git a/tests/multiple.nix b/tests/multiple.nix index 99e1aaf..cef20e3 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -5,9 +5,9 @@ let hashPassword = password: pkgs.runCommand "password-${password}-hashed" - { buildInputs = [ pkgs.apacheHttpd ]; } + { buildInputs = [ pkgs.mkpasswd ]; inherit password; } '' - htpasswd -nbB "" "${password}" | cut -d: -f2 > $out + mkpasswd -sm bcrypt <<<"$password" > $out ''; password = pkgs.writeText "password" "password"; From 737eb4f3980560605d5c3b118b34ec4fb0b533d4 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 10 Jul 2022 09:25:43 +0200 Subject: [PATCH 053/225] docs: explicitly mention a reverse DNS entry is required Fixes https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/234 --- docs/setup-guide.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 4a39676..8127734 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -96,7 +96,14 @@ Set rDNS (reverse DNS) entry for server Wherever you have rented your server, you should be able to set reverse DNS entries for the IP’s you own. Add an entry resolving ``1.2.3.4`` -to ``mail.example.com`` +to ``mail.example.com``. + +.. warning:: + + We don't recommend setting up a mail server if you are not able to + set a reverse DNS on your public IP because sent emails would be + mostly marked as spam. Note that many residential ISP providers + don't allow you to set a reverse DNS entry. You can check this with From 3f0b7a1b5c63980acddbabdac2e75f83172f36c3 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 27 Nov 2022 20:43:25 +0100 Subject: [PATCH 054/225] ci: pin nixpkgs to 22.05 Because hydra-cli build is currently broken on unstable. --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 702c294..b72b9f9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,11 +3,11 @@ hydra-pr: - merge_requests image: nixos/nix script: - - nix-shell -I nixpkgs=channel:nixos-unstable -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}' + - nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}' hydra-master: only: - master image: nixos/nix script: - - nix-shell -I nixpkgs=channel:nixos-unstable -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master' + - nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master' From fe36e7ae0db34862e5397544c7b01472b0febc3a Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 3 Oct 2021 14:31:43 +0200 Subject: [PATCH 055/225] rspamd: allow configuring dmarc reporting Enabling collects DMARC results in Redis and sends out aggregated reports (RUA) on a daily basis. --- default.nix | 57 ++++++++++++++++++++++++++++++++++ docs/options.rst | 68 ++++++++++++++++++++++++++++++++++++++++- flake.nix | 11 ++++++- mail-server/rspamd.nix | 69 ++++++++++++++++++++++++++++++++++++++++++ tests/external.nix | 9 ++++++ 5 files changed, 212 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index 7c9118e..8584af2 100644 --- a/default.nix +++ b/default.nix @@ -627,6 +627,63 @@ in ''; }; + dmarcReporting = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to send out aggregated, daily DMARC reports in response to incoming + mail, when the sender domain defines a DMARC policy including the RUA tag. + + This is helpful for the mail ecosystem, because it allows third parties to + get notified about SPF/DKIM violations originating from their sender domains. + + See https://rspamd.com/doc/modules/dmarc.html#reporting + ''; + }; + + localpart = mkOption { + type = types.str; + default = "dmarc-noreply"; + example = "dmarc-report"; + description = '' + The local part of the email address used for outgoing DMARC reports. + ''; + }; + + domain = mkOption { + type = types.enum (cfg.domains); + example = "example.com"; + description = '' + The domain from which outgoing DMARC reports are served. + ''; + }; + + email = mkOption { + type = types.str; + default = with cfg.dmarcReporting; "${localpart}@${domain}"; + example = "dmarc-noreply@example.com"; + readOnly = true; + }; + + organizationName = mkOption { + type = types.str; + example = "ACME Corp."; + description = '' + The name of your organization used in the org_name attribute in + DMARC reports. + ''; + }; + + fromName = mkOption { + type = types.str; + default = cfg.dmarcReporting.organizationName; + description = '' + The sender name for DMARC reports. Defaults to the organization name. + ''; + }; + }; + debug = mkOption { type = types.bool; default = false; diff --git a/docs/options.rst b/docs/options.rst index 1e0af9a..0e3f7ab 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -19,6 +19,72 @@ to enable this unless you're hacking on nixos-mailserver. - Default: ``False`` +mailserver.dmarcReporting.domain +-------------------------------- + +The domain from which outgoing DMARC reports are served. + + +- Type: ``value "example.com" (singular enum)`` + + + +mailserver.dmarcReporting.email +------------------------------- + +None + +- Type: ``string`` +- Default: ``dmarc-noreply@example.com`` + + +mailserver.dmarcReporting.enable +-------------------------------- + +Whether to send out aggregated, daily DMARC reports in response to incoming +mail, when the sender domain defines a DMARC policy including the RUA tag. + +This is helpful for the mail ecosystem, because it allows third parties to +get notified about SPF/DKIM violations originating from their sender domains. + +See https://rspamd.com/doc/modules/dmarc.html#reporting + + +- Type: ``boolean`` +- Default: ``False`` + + +mailserver.dmarcReporting.fromName +---------------------------------- + +The sender name for DMARC reports. Defaults to the organization name. + + +- Type: ``string`` +- Default: ``Example Corp`` + + +mailserver.dmarcReporting.localpart +----------------------------------- + +The local part of the email address used for outgoing DMARC reports. + + +- Type: ``string`` +- Default: ``dmarc-noreply`` + + +mailserver.dmarcReporting.organizationName +------------------------------------------ + +The name of your organization used in the org_name attribute in +DMARC reports. + + +- Type: ``string`` + + + mailserver.domains ------------------ @@ -489,7 +555,7 @@ For which domains should this account act as a catch all? Note: Does not allow sending from all addresses of these domains. -- Type: ``list of impossible (empty enum)s`` +- Type: ``list of value "example.com" (singular enum)s`` - Default: ``[]`` diff --git a/flake.nix b/flake.nix index e5b85ad..3897e8c 100644 --- a/flake.nix +++ b/flake.nix @@ -59,7 +59,16 @@ # don't care about this package but it is part of the # NixOS module evaluation) nixpkgs.config.allowBroken = true; - mailserver.fqdn = "mx.example.com"; + mailserver = { + fqdn = "mx.example.com"; + domains = [ + "example.com" + ]; + dmarcReporting = { + organizationName = "Example Corp"; + domain = "example.com"; + }; + }; } ]; diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index efe3abc..a506904 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -56,6 +56,17 @@ in # Disable outbound email signing, we use opendkim for this enabled = false; ''; }; + "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 = "dmarc-rua"; + }''} + ''; }; }; overrides = { @@ -108,6 +119,64 @@ in after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); }; + 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.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.postfix = { after = [ rspamdSocket ]; requires = [ rspamdSocket ]; diff --git a/tests/external.nix b/tests/external.nix index f30a1e5..c14a345 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -43,6 +43,11 @@ pkgs.nixosTest { domains = [ "example.com" "example2.com" ]; rewriteMessageId = true; dkimKeyBits = 1535; + dmarcReporting = { + enable = true; + domain = "example.com"; + organizationName = "ACME Corp"; + }; loginAccounts = { "user1@example.com" = { @@ -494,6 +499,10 @@ pkgs.nixosTest { # check that Junk is not indexed server.fail("journalctl -u dovecot2 | grep 'indexer-worker' | grep -i 'JUNK' >&2") + with subtest("dmarc reporting"): + server.systemctl("start rspamd-dmarc-reporter.service") + server.wait_until_succeeds("journalctl -eu rspamd-dmarc-reporter.service -o cat | grep -q 'No reports for '") + with subtest("no warnings or errors"): server.fail("journalctl -u postfix | grep -i error >&2") server.fail("journalctl -u postfix | grep -i warning >&2") From 694e7d34f60028f4877517e1c7c73c9527fad400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Wed, 30 Nov 2022 12:30:29 +0100 Subject: [PATCH 056/225] docs: option docs improvements - add missing description and defaultText fields - add dmarcReporting option group - render examples --- default.nix | 6 +- docs/options.rst | 642 +++++++++++++++++++------------- scripts/generate-rst-options.py | 48 ++- 3 files changed, 420 insertions(+), 276 deletions(-) diff --git a/default.nix b/default.nix index 8584af2..275ac83 100644 --- a/default.nix +++ b/default.nix @@ -662,8 +662,11 @@ in email = mkOption { type = types.str; default = with cfg.dmarcReporting; "${localpart}@${domain}"; - example = "dmarc-noreply@example.com"; + defaultText = literalExpression ''"''${localpart}@''${domain}"''; readOnly = true; + description = '' + The email address used for outgoing DMARC reports. Read-only. + ''; }; organizationName = mkOption { @@ -678,6 +681,7 @@ in fromName = mkOption { type = types.str; default = cfg.dmarcReporting.organizationName; + defaultText = literalExpression "organizationName"; description = '' The sender name for DMARC reports. Defaults to the organization name. ''; diff --git a/docs/options.rst b/docs/options.rst index 0e3f7ab..5c23c83 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -15,73 +15,8 @@ intended be used for development purposes only, you probably don't want to enable this unless you're hacking on nixos-mailserver. -- Type: ``boolean`` -- Default: ``False`` - - -mailserver.dmarcReporting.domain --------------------------------- - -The domain from which outgoing DMARC reports are served. - - -- Type: ``value "example.com" (singular enum)`` - - - -mailserver.dmarcReporting.email -------------------------------- - -None - -- Type: ``string`` -- Default: ``dmarc-noreply@example.com`` - - -mailserver.dmarcReporting.enable --------------------------------- - -Whether to send out aggregated, daily DMARC reports in response to incoming -mail, when the sender domain defines a DMARC policy including the RUA tag. - -This is helpful for the mail ecosystem, because it allows third parties to -get notified about SPF/DKIM violations originating from their sender domains. - -See https://rspamd.com/doc/modules/dmarc.html#reporting - - -- Type: ``boolean`` -- Default: ``False`` - - -mailserver.dmarcReporting.fromName ----------------------------------- - -The sender name for DMARC reports. Defaults to the organization name. - - -- Type: ``string`` -- Default: ``Example Corp`` - - -mailserver.dmarcReporting.localpart ------------------------------------ - -The local part of the email address used for outgoing DMARC reports. - - -- Type: ``string`` -- Default: ``dmarc-noreply`` - - -mailserver.dmarcReporting.organizationName ------------------------------------------- - -The name of your organization used in the org_name attribute in -DMARC reports. - - -- Type: ``string`` +- type: ``boolean`` +- default: ``False`` @@ -90,8 +25,9 @@ mailserver.domains The domains that this mail server serves. -- Type: ``list of strings`` -- Default: ``[]`` +- type: ``list of strings`` +- default: ``[]`` +- example: ``['example.com']`` mailserver.enable @@ -99,8 +35,9 @@ mailserver.enable Whether to enable nixos-mailserver. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` +- example: ``True`` mailserver.enableImap @@ -109,8 +46,9 @@ mailserver.enableImap Whether to enable IMAP with STARTTLS on port 143. -- Type: ``boolean`` -- Default: ``True`` +- type: ``boolean`` +- default: ``True`` + mailserver.enableImapSsl @@ -119,8 +57,9 @@ mailserver.enableImapSsl Whether to enable IMAP with TLS in wrapper-mode on port 993. -- Type: ``boolean`` -- Default: ``True`` +- type: ``boolean`` +- default: ``True`` + mailserver.enableManageSieve @@ -133,8 +72,9 @@ The ManageSieve protocol allows users to manage their Sieve scripts on a remote server with a supported client, including Thunderbird. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` + mailserver.enablePop3 @@ -143,8 +83,9 @@ mailserver.enablePop3 Whether to enable POP3 with STARTTLS on port on port 110. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` + mailserver.enablePop3Ssl @@ -153,8 +94,9 @@ mailserver.enablePop3Ssl Whether to enable POP3 with TLS in wrapper-mode on port 995. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` + mailserver.enableSubmission @@ -163,8 +105,9 @@ mailserver.enableSubmission Whether to enable SMTP with STARTTLS on port 587. -- Type: ``boolean`` -- Default: ``True`` +- type: ``boolean`` +- default: ``True`` + mailserver.enableSubmissionSsl @@ -173,8 +116,9 @@ mailserver.enableSubmissionSsl Whether to enable SMTP with TLS in wrapper-mode on port 465. -- Type: ``boolean`` -- Default: ``True`` +- type: ``boolean`` +- default: ``True`` + mailserver.extraVirtualAliases @@ -191,8 +135,9 @@ example all mails for `multi@example.com` will be forwarded to both `user1@example.com` and `user2@example.com`. -- Type: ``attribute set of Login Account or non-empty list of Login Accountss`` -- Default: ``{}`` +- type: ``attribute set of Login Account or non-empty list of Login Accountss`` +- default: ``{}`` +- example: ``{'abuse@example.com': 'user1@example.com', 'info@example.com': 'user1@example.com', 'multi@example.com': ['user1@example.com', 'user2@example.com'], 'postmaster@example.com': 'user1@example.com'}`` mailserver.forwards @@ -207,8 +152,9 @@ can't send mail as `user@example.com`. Also, this option allows to forward mails to external addresses. -- Type: ``attribute set of list of strings or strings`` -- Default: ``{}`` +- type: ``attribute set of list of strings or strings`` +- default: ``{}`` +- example: ``{'user@example.com': 'user@elsewhere.com'}`` mailserver.fqdn @@ -216,8 +162,9 @@ mailserver.fqdn The fully qualified domain name of the mail server. -- Type: ``string`` +- type: ``string`` +- example: ``mx.example.com`` mailserver.hierarchySeparator @@ -231,8 +178,9 @@ This does not determine the way your mails are stored on disk. See https://wiki.dovecot.org/Namespaces for details. -- Type: ``string`` -- Default: ``.`` +- type: ``string`` +- default: ``.`` + mailserver.indexDir @@ -253,8 +201,9 @@ https://doc.dovecot.org/configuration_manual/mail_location/#variables for details. -- Type: ``null or string`` -- Default: ``None`` +- type: ``null or string`` +- default: ``None`` +- example: ``/var/lib/dovecot/indices`` mailserver.keyFile @@ -264,8 +213,9 @@ Scheme 1) Location of the key file -- Type: ``path`` +- type: ``path`` +- example: ``/root/mail-server.key`` mailserver.lmtpSaveToDetailMailbox @@ -276,8 +226,9 @@ mailbox matching the string after the "+"? For example, user1+test@example.com would be filed into the mailbox "test". -- Type: ``one of "yes", "no"`` -- Default: ``yes`` +- type: ``one of "yes", "no"`` +- default: ``yes`` + mailserver.localDnsResolver @@ -286,8 +237,9 @@ mailserver.localDnsResolver Runs a local DNS resolver (kresd) as recommended when running rspamd. This prevents your log file from filling up with rspamd_monitored_dns_mon entries. -- Type: ``boolean`` -- Default: ``True`` +- type: ``boolean`` +- default: ``True`` + mailserver.mailDirectory @@ -296,8 +248,9 @@ mailserver.mailDirectory Where to store the mail. -- Type: ``path`` -- Default: ``/var/vmail`` +- type: ``path`` +- default: ``/var/vmail`` + mailserver.mailboxes @@ -307,8 +260,9 @@ The mailboxes for dovecot. Depending on the mail client used it might be necessary to change some mailbox's name. -- Type: ``unspecified`` -- Default: ``{'Drafts': {'auto': 'subscribe', 'specialUse': 'Drafts'}, 'Junk': {'auto': 'subscribe', 'specialUse': 'Junk'}, 'Sent': {'auto': 'subscribe', 'specialUse': 'Sent'}, 'Trash': {'auto': 'no', 'specialUse': 'Trash'}}`` +- type: ``unspecified`` +- default: ``{'Drafts': {'auto': 'subscribe', 'specialUse': 'Drafts'}, 'Junk': {'auto': 'subscribe', 'specialUse': 'Junk'}, 'Sent': {'auto': 'subscribe', 'specialUse': 'Sent'}, 'Trash': {'auto': 'no', 'specialUse': 'Trash'}}`` + mailserver.maxConnectionsPerUser @@ -319,8 +273,9 @@ E.g. a value of 50 allows for 50 IMAP and 50 POP3 connections at the same time for a single user. -- Type: ``signed integer`` -- Default: ``100`` +- type: ``signed integer`` +- default: ``100`` + mailserver.messageSizeLimit @@ -328,8 +283,9 @@ mailserver.messageSizeLimit Message size limit enforced by Postfix. -- Type: ``signed integer`` -- Default: ``20971520`` +- type: ``signed integer`` +- default: ``20971520`` +- example: ``52428800`` mailserver.openFirewall @@ -337,8 +293,9 @@ mailserver.openFirewall Automatically open ports in the firewall. -- Type: ``boolean`` -- Default: ``True`` +- type: ``boolean`` +- default: ``True`` + mailserver.policydSPFExtraConfig @@ -348,8 +305,14 @@ Extra configuration options for policyd-spf. This can be use to among other things skip spf checking for some IP addresses. -- Type: ``strings concatenated with "\n"`` -- Default: ``""`` +- type: ``strings concatenated with "\n"`` +- default: ``""`` +- example: +.. code:: + + skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 + + mailserver.rebootAfterKernelUpgrade.enable @@ -359,8 +322,9 @@ Whether to enable automatic reboot after kernel upgrades. This is to be used in conjunction with system.autoUpgrade.enable = true" -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` +- example: ``True`` mailserver.rebootAfterKernelUpgrade.method @@ -371,8 +335,9 @@ It is recommended to use the default value because the quicker kexec reboot has Also if your server is running in a virtual machine the regular reboot will already be very quick. -- Type: ``one of "reboot", "systemctl kexec"`` -- Default: ``reboot`` +- type: ``one of "reboot", "systemctl kexec"`` +- default: ``reboot`` + mailserver.recipientDelimiter @@ -381,8 +346,9 @@ mailserver.recipientDelimiter Configure the recipient delimiter. -- Type: ``string`` -- Default: ``+`` +- type: ``string`` +- default: ``+`` + mailserver.rejectRecipients @@ -393,8 +359,9 @@ Use if a spammer has found email addresses in a catchall domain but you do not want to disable the catchall. -- Type: ``list of strings`` -- Default: ``[]`` +- type: ``list of strings`` +- default: ``[]`` +- example: ``['sales@example.com', 'info@example.com']`` mailserver.rejectSender @@ -404,8 +371,9 @@ Reject emails from these addresses from unauthorized senders. Use if a spammer is using the same domain or the same sender over and over. -- Type: ``list of strings`` -- Default: ``[]`` +- type: ``list of strings`` +- default: ``[]`` +- example: ``['@example.com', 'spammer@example.net']`` mailserver.rewriteMessageId @@ -416,8 +384,9 @@ Please be aware that this may cause problems with some mail clients relying on the original Message-ID. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` + mailserver.sendingFqdn @@ -443,8 +412,9 @@ Set this to the name to which the sending IP's reverse DNS resolves. -- Type: ``string`` -- Default: ``config.mailserver.fqdn`` +- type: ``string`` +- default: ``config.mailserver.fqdn`` +- example: ``myserver.example.com`` mailserver.sieveDirectory @@ -453,8 +423,9 @@ mailserver.sieveDirectory Where to store the sieve scripts. -- Type: ``path`` -- Default: ``/var/sieve`` +- type: ``path`` +- default: ``/var/sieve`` + mailserver.useFsLayout @@ -468,8 +439,9 @@ Sets whether dovecot should organize mail in subdirectories: See https://wiki2.dovecot.org/MailboxFormat/Maildir for details. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` + mailserver.virusScanning @@ -479,8 +451,9 @@ Whether to activate virus scanning. Note that virus scanning is _very_ expensive memory wise. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` + mailserver.vmailGroupName @@ -490,8 +463,9 @@ The user name and group name of the user that owns the directory where all the mail is stored. -- Type: ``string`` -- Default: ``virtualMail`` +- type: ``string`` +- default: ``virtualMail`` + mailserver.vmailUID @@ -502,8 +476,9 @@ changed, you will need to manually adjust the permissions of mailDirectory. -- Type: ``signed integer`` -- Default: ``5000`` +- type: ``signed integer`` +- default: ``5000`` + mailserver.vmailUserName @@ -513,8 +488,9 @@ The user name and group name of the user that owns the directory where all the mail is stored. -- Type: ``string`` -- Default: ``virtualMail`` +- type: ``string`` +- default: ``virtualMail`` + mailserver.loginAccount ~~~~~~~~~~~~~~~~~~~~~~~ @@ -532,8 +508,9 @@ nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` -- Type: ``attribute set of submodules`` -- Default: ``{}`` +- type: ``attribute set of submodules`` +- default: ``{}`` +- example: ``{'user1': {'hashedPassword': '$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/'}, 'user2': {'hashedPassword': '$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/'}}`` mailserver.loginAccounts..aliases @@ -544,8 +521,9 @@ Note: Use list entries like "@example.com" to create a catchAll that allows sending from all email addresses in these domain. -- Type: ``list of strings`` -- Default: ``[]`` +- type: ``list of strings`` +- default: ``[]`` +- example: ``['abuse@example.com', 'postmaster@example.com']`` mailserver.loginAccounts..catchAll @@ -555,8 +533,9 @@ For which domains should this account act as a catch all? Note: Does not allow sending from all addresses of these domains. -- Type: ``list of value "example.com" (singular enum)s`` -- Default: ``[]`` +- type: ``list of value "example.com" (singular enum)s`` +- default: ``[]`` +- example: ``['example.com', 'example2.com']`` mailserver.loginAccounts..hashedPassword @@ -572,8 +551,9 @@ Warning: this is stored in plaintext in the Nix store! Use `hashedPasswordFile` instead. -- Type: ``null or string`` -- Default: ``None`` +- type: ``null or string`` +- default: ``None`` +- example: ``$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/`` mailserver.loginAccounts..hashedPasswordFile @@ -586,8 +566,9 @@ nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` -- Type: ``null or path`` -- Default: ``None`` +- type: ``null or path`` +- default: ``None`` +- example: ``/run/keys/user1-passwordhash`` mailserver.loginAccounts..name @@ -595,8 +576,9 @@ mailserver.loginAccounts..name Username -- Type: ``string`` +- type: ``string`` +- example: ``user1@example.com`` mailserver.loginAccounts..quota @@ -606,8 +588,9 @@ Per user quota rules. Accepted sizes are `xx k/M/G/T` with the obvious meaning. Leave blank for the standard quota `100G`. -- Type: ``null or string`` -- Default: ``None`` +- type: ``null or string`` +- default: ``None`` +- example: ``2G`` mailserver.loginAccounts..sendOnly @@ -619,8 +602,9 @@ unauthorized senders with the sendOnlyRejectMessage stating the reason. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` + mailserver.loginAccounts..sendOnlyRejectMessage @@ -631,8 +615,9 @@ sent to a send-only account. Only used if the account is marked as send-only. -- Type: ``string`` -- Default: ``This account cannot receive emails.`` +- type: ``string`` +- default: ``This account cannot receive emails.`` + mailserver.loginAccounts..sieveScript @@ -641,8 +626,26 @@ mailserver.loginAccounts..sieveScript Per-user sieve script. -- Type: ``null or strings concatenated with "\n"`` -- Default: ``None`` +- type: ``null or strings concatenated with "\n"`` +- default: ``None`` +- example: +.. code:: + + require ["fileinto", "mailbox"]; + + if address :is "from" "gitlab@mg.gitlab.com" { + fileinto :create "GitLab"; + stop; + } + + # This must be the last rule, it will check if list-id is set, and + # file the message into the Lists folder for further investigation + elsif header :matches "list-id" "" { + fileinto :create "Lists"; + stop; + } + + mailserver.certificate ~~~~~~~~~~~~~~~~~~~~~~ @@ -657,8 +660,9 @@ hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the certificate is valid for 10 years. -- Type: ``path`` -- Default: ``/var/certs`` +- type: ``path`` +- default: ``/var/certs`` + mailserver.certificateDomains @@ -666,8 +670,9 @@ mailserver.certificateDomains Secondary domains and subdomains for which it is necessary to generate a certificate. -- Type: ``list of strings`` -- Default: ``[]`` +- type: ``list of strings`` +- default: ``[]`` +- example: ``['imap.example.com', 'pop3.example.com']`` mailserver.certificateFile @@ -677,8 +682,9 @@ Scheme 1) Location of the certificate -- Type: ``path`` +- type: ``path`` +- example: ``/root/mail-server.crt`` mailserver.certificateScheme @@ -695,8 +701,9 @@ Certificate Files. There are three options for these. on how to set up the domain records, see the guide in the readme. -- Type: ``one of 1, 2, 3`` -- Default: ``2`` +- type: ``one of 1, 2, 3`` +- default: ``2`` + mailserver.dkim ~~~~~~~~~~~~~~~ @@ -710,8 +717,9 @@ DKIM canonicalization algorithm for message bodies. See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. -- Type: ``one of "relaxed", "simple"`` -- Default: ``relaxed`` +- type: ``one of "relaxed", "simple"`` +- default: ``relaxed`` + mailserver.dkimHeaderCanonicalization @@ -722,8 +730,9 @@ DKIM canonicalization algorithm for message headers. See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. -- Type: ``one of "relaxed", "simple"`` -- Default: ``relaxed`` +- type: ``one of "relaxed", "simple"`` +- default: ``relaxed`` + mailserver.dkimKeyBits @@ -737,8 +746,9 @@ this package to generate a key with the new number of bits, you will either have change the selector or delete the old key file. -- Type: ``signed integer`` -- Default: ``1024`` +- type: ``signed integer`` +- default: ``1024`` + mailserver.dkimKeyDirectory @@ -747,8 +757,9 @@ mailserver.dkimKeyDirectory -- Type: ``path`` -- Default: ``/var/dkim`` +- type: ``path`` +- default: ``/var/dkim`` + mailserver.dkimSelector @@ -757,8 +768,9 @@ mailserver.dkimSelector -- Type: ``string`` -- Default: ``mail`` +- type: ``string`` +- default: ``mail`` + mailserver.dkimSigning @@ -767,8 +779,85 @@ mailserver.dkimSigning Whether to activate dkim signing. -- Type: ``boolean`` -- Default: ``True`` +- type: ``boolean`` +- default: ``True`` + + +mailserver.dmarcReporting +~~~~~~~~~~~~~~~~~~~~~~~~~ + + +mailserver.dmarcReporting.domain +-------------------------------- + +The domain from which outgoing DMARC reports are served. + + +- type: ``value "example.com" (singular enum)`` + +- example: ``example.com`` + + +mailserver.dmarcReporting.email +------------------------------- + +The email address used for outgoing DMARC reports. Read-only. + + +- type: ``string`` +- default: ``"${localpart}@${domain}"`` + + + +mailserver.dmarcReporting.enable +-------------------------------- + +Whether to send out aggregated, daily DMARC reports in response to incoming +mail, when the sender domain defines a DMARC policy including the RUA tag. + +This is helpful for the mail ecosystem, because it allows third parties to +get notified about SPF/DKIM violations originating from their sender domains. + +See https://rspamd.com/doc/modules/dmarc.html#reporting + + +- type: ``boolean`` +- default: ``False`` + + + +mailserver.dmarcReporting.fromName +---------------------------------- + +The sender name for DMARC reports. Defaults to the organization name. + + +- type: ``string`` +- default: ``organizationName`` + + + +mailserver.dmarcReporting.localpart +----------------------------------- + +The local part of the email address used for outgoing DMARC reports. + + +- type: ``string`` +- default: ``dmarc-noreply`` +- example: ``dmarc-report`` + + +mailserver.dmarcReporting.organizationName +------------------------------------------ + +The name of your organization used in the org_name attribute in +DMARC reports. + + +- type: ``string`` + +- example: ``ACME Corp.`` mailserver.fullTextSearch ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -779,8 +868,9 @@ mailserver.fullTextSearch.autoIndex Enable automatic indexing of messages as they are received or modified. -- Type: ``boolean`` -- Default: ``True`` +- type: ``boolean`` +- default: ``True`` + mailserver.fullTextSearch.autoIndexExclude @@ -789,8 +879,9 @@ mailserver.fullTextSearch.autoIndexExclude Mailboxes to exclude from automatic indexing. -- Type: ``list of strings`` -- Default: ``[]`` +- type: ``list of strings`` +- default: ``[]`` +- example: ``['\\Trash', 'SomeFolder', 'Other/*']`` mailserver.fullTextSearch.enable @@ -798,8 +889,9 @@ mailserver.fullTextSearch.enable Whether to enable Full text search indexing with xapian. This has significant performance and disk space cost.. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` +- example: ``True`` mailserver.fullTextSearch.enforced @@ -811,8 +903,9 @@ header) are affected. If set to no, searches may fall back to a very slow brute force search. -- Type: ``one of "yes", "no", "body"`` -- Default: ``no`` +- type: ``one of "yes", "no", "body"`` +- default: ``no`` + mailserver.fullTextSearch.indexAttachments @@ -820,8 +913,9 @@ mailserver.fullTextSearch.indexAttachments Also index text-only attachements. Binary attachements are never indexed. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` + mailserver.fullTextSearch.maintenance.enable @@ -829,8 +923,9 @@ mailserver.fullTextSearch.maintenance.enable Regularly optmize indices, as recommended by upstream. -- Type: ``boolean`` -- Default: ``True`` +- type: ``boolean`` +- default: ``True`` + mailserver.fullTextSearch.maintenance.onCalendar @@ -838,8 +933,9 @@ mailserver.fullTextSearch.maintenance.onCalendar When to run the maintenance job. See systemd.time(7) for more information about the format. -- Type: ``string`` -- Default: ``daily`` +- type: ``string`` +- default: ``daily`` + mailserver.fullTextSearch.maintenance.randomizedDelaySec @@ -847,8 +943,9 @@ mailserver.fullTextSearch.maintenance.randomizedDelaySec Run the maintenance job not exactly at the time specified with onCalendar, but plus or minus this many seconds. -- Type: ``signed integer`` -- Default: ``1000`` +- type: ``signed integer`` +- default: ``1000`` + mailserver.fullTextSearch.maxSize @@ -856,8 +953,9 @@ mailserver.fullTextSearch.maxSize Size of the largest n-gram to index. -- Type: ``signed integer`` -- Default: ``20`` +- type: ``signed integer`` +- default: ``20`` + mailserver.fullTextSearch.memoryLimit @@ -865,8 +963,9 @@ mailserver.fullTextSearch.memoryLimit Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit. -- Type: ``null or signed integer`` -- Default: ``None`` +- type: ``null or signed integer`` +- default: ``None`` +- example: ``2000`` mailserver.fullTextSearch.minSize @@ -874,8 +973,9 @@ mailserver.fullTextSearch.minSize Size of the smallest n-gram to index. -- Type: ``signed integer`` -- Default: ``2`` +- type: ``signed integer`` +- default: ``2`` + mailserver.redis ~~~~~~~~~~~~~~~~ @@ -887,8 +987,9 @@ mailserver.redis.address Address that rspamd should use to contact redis. -- Type: ``string`` -- Default: computed from +- type: ``string`` +- default: computed from + mailserver.redis.password @@ -897,8 +998,9 @@ mailserver.redis.password Password that rspamd should use to contact redis, or null if not required. -- Type: ``null or string`` -- Default: ``config.services.redis.servers.rspamd.requirePass`` +- type: ``null or string`` +- default: ``config.services.redis.servers.rspamd.requirePass`` + mailserver.redis.port @@ -907,8 +1009,9 @@ mailserver.redis.port Port that rspamd should use to contact redis. -- Type: ``16 bit unsigned integer; between 0 and 65535 (both inclusive)`` -- Default: ``config.services.redis.servers.rspamd.port`` +- type: ``16 bit unsigned integer; between 0 and 65535 (both inclusive)`` +- default: ``config.services.redis.servers.rspamd.port`` + mailserver.monitoring ~~~~~~~~~~~~~~~~~~~~~ @@ -920,7 +1023,8 @@ mailserver.monitoring.alertAddress The email address to send alerts to. -- Type: ``string`` +- type: ``string`` + @@ -931,8 +1035,9 @@ The configuration used for monitoring via monit. Use a mail address that you actively check and set it via 'set alert ...'. -- Type: ``string`` -- Default: see source +- type: ``string`` +- default: see source + mailserver.monitoring.enable @@ -940,8 +1045,9 @@ mailserver.monitoring.enable Whether to enable monitoring via monit. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` +- example: ``True`` mailserver.backup ~~~~~~~~~~~~~~~~~ @@ -952,8 +1058,9 @@ mailserver.backup.cmdPostexec The command to be executed after each backup operation. This is wrapped in a shell script to be called by rsnapshot. -- Type: ``null or string`` -- Default: ``None`` +- type: ``null or string`` +- default: ``None`` + mailserver.backup.cmdPreexec @@ -962,8 +1069,9 @@ mailserver.backup.cmdPreexec The command to be executed before each backup operation. This is wrapped in a shell script to be called by rsnapshot. -- Type: ``null or string`` -- Default: ``None`` +- type: ``null or string`` +- default: ``None`` + mailserver.backup.cronIntervals @@ -974,8 +1082,9 @@ Note that the intervals also have to exist in configuration as retain options. -- Type: ``attribute set of strings`` -- Default: ``{'daily': '30 3 * * *', 'hourly': ' 0 * * * *', 'weekly': ' 0 5 * * 0'}`` +- type: ``attribute set of strings`` +- default: ``{'daily': '30 3 * * *', 'hourly': ' 0 * * * *', 'weekly': ' 0 5 * * 0'}`` + mailserver.backup.enable @@ -983,8 +1092,9 @@ mailserver.backup.enable Whether to enable backup via rsnapshot. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` +- example: ``True`` mailserver.backup.retain.daily @@ -992,8 +1102,9 @@ mailserver.backup.retain.daily How many daily snapshots are retained. -- Type: ``signed integer`` -- Default: ``7`` +- type: ``signed integer`` +- default: ``7`` + mailserver.backup.retain.hourly @@ -1001,8 +1112,9 @@ mailserver.backup.retain.hourly How many hourly snapshots are retained. -- Type: ``signed integer`` -- Default: ``24`` +- type: ``signed integer`` +- default: ``24`` + mailserver.backup.retain.weekly @@ -1010,8 +1122,9 @@ mailserver.backup.retain.weekly How many weekly snapshots are retained. -- Type: ``signed integer`` -- Default: ``54`` +- type: ``signed integer`` +- default: ``54`` + mailserver.backup.snapshotRoot @@ -1020,8 +1133,9 @@ mailserver.backup.snapshotRoot The directory where rsnapshot stores the backup. -- Type: ``path`` -- Default: ``/var/rsnapshot`` +- type: ``path`` +- default: ``/var/rsnapshot`` + mailserver.borg ~~~~~~~~~~~~~~~ @@ -1035,8 +1149,9 @@ This is called after borg create completed successfully and in the same script t cmdPreexec, borg init and create. -- Type: ``null or string`` -- Default: ``None`` +- type: ``null or string`` +- default: ``None`` + mailserver.borgbackup.cmdPreexec @@ -1048,8 +1163,9 @@ Example: export BORG_RSH="ssh -i /path/to/private/key" -- Type: ``null or string`` -- Default: ``None`` +- type: ``null or string`` +- default: ``None`` + mailserver.borgbackup.compression.auto @@ -1057,8 +1173,9 @@ mailserver.borgbackup.compression.auto Leaves it to borg to determine whether an individual file should be compressed. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` + mailserver.borgbackup.compression.level @@ -1069,8 +1186,9 @@ Most methods accept levels from 0 to 9 but zstd which accepts values from 1 to 2 If null the decision is left up to borg. -- Type: ``null or signed integer`` -- Default: ``None`` +- type: ``null or signed integer`` +- default: ``None`` + mailserver.borgbackup.compression.method @@ -1078,8 +1196,9 @@ mailserver.borgbackup.compression.method Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4. -- Type: ``null or one of "none", "lz4", "zstd", "zlib", "lzma"`` -- Default: ``None`` +- type: ``null or one of "none", "lz4", "zstd", "zlib", "lzma"`` +- default: ``None`` + mailserver.borgbackup.enable @@ -1087,8 +1206,9 @@ mailserver.borgbackup.enable Whether to enable backup via borgbackup. -- Type: ``boolean`` -- Default: ``False`` +- type: ``boolean`` +- default: ``False`` +- example: ``True`` mailserver.borgbackup.encryption.method @@ -1098,8 +1218,9 @@ The backup can be encrypted by choosing any other value than 'none'. When using encryption the password / passphrase must be provided in passphraseFile. -- Type: ``one of "none", "authenticated", "authenticated-blake2", "repokey", "keyfile", "repokey-blake2", "keyfile-blake2"`` -- Default: ``none`` +- type: ``one of "none", "authenticated", "authenticated-blake2", "repokey", "keyfile", "repokey-blake2", "keyfile-blake2"`` +- default: ``none`` + mailserver.borgbackup.encryption.passphraseFile @@ -1107,8 +1228,9 @@ mailserver.borgbackup.encryption.passphraseFile Path to a file containing the encryption password or passphrase. -- Type: ``null or path`` -- Default: ``None`` +- type: ``null or path`` +- default: ``None`` + mailserver.borgbackup.extraArgumentsForCreate @@ -1116,8 +1238,9 @@ mailserver.borgbackup.extraArgumentsForCreate Additional arguments to add to the borg create command line e.g. '--stats'. -- Type: ``list of strings`` -- Default: ``[]`` +- type: ``list of strings`` +- default: ``[]`` + mailserver.borgbackup.extraArgumentsForInit @@ -1125,8 +1248,9 @@ mailserver.borgbackup.extraArgumentsForInit Additional arguments to add to the borg init command line. -- Type: ``list of strings`` -- Default: ``['--critical']`` +- type: ``list of strings`` +- default: ``['--critical']`` + mailserver.borgbackup.group @@ -1134,8 +1258,9 @@ mailserver.borgbackup.group The group borg and its launch script is run as. -- Type: ``string`` -- Default: ``virtualMail`` +- type: ``string`` +- default: ``virtualMail`` + mailserver.borgbackup.locations @@ -1143,8 +1268,9 @@ mailserver.borgbackup.locations The locations that are to be backed up by borg. -- Type: ``list of paths`` -- Default: ``['/var/vmail']`` +- type: ``list of paths`` +- default: ``['/var/vmail']`` + mailserver.borgbackup.name @@ -1154,8 +1280,9 @@ The name of the individual backups as used by borg. Certain placeholders will be replaced by borg. -- Type: ``string`` -- Default: ``{hostname}-{user}-{now}`` +- type: ``string`` +- default: ``{hostname}-{user}-{now}`` + mailserver.borgbackup.repoLocation @@ -1166,8 +1293,9 @@ This can be a local path or a remote location such as user@host:/path/to/repo. It is exported and thus available as an environment variable to cmdPreexec and cmdPostexec. -- Type: ``string`` -- Default: ``/var/borgbackup`` +- type: ``string`` +- default: ``/var/borgbackup`` + mailserver.borgbackup.startAt @@ -1175,8 +1303,9 @@ mailserver.borgbackup.startAt When or how often the backup should run. Must be in the format described in systemd.time 7. -- Type: ``string`` -- Default: ``hourly`` +- type: ``string`` +- default: ``hourly`` + mailserver.borgbackup.user @@ -1184,6 +1313,7 @@ mailserver.borgbackup.user The user borg and its launch script is run as. -- Type: ``string`` -- Default: ``virtualMail`` +- type: ``string`` +- default: ``virtualMail`` + diff --git a/scripts/generate-rst-options.py b/scripts/generate-rst-options.py index a7d751e..8c5ce9c 100644 --- a/scripts/generate-rst-options.py +++ b/scripts/generate-rst-options.py @@ -1,6 +1,7 @@ import json import sys import re +import textwrap header = """ Mailserver Options @@ -19,6 +20,7 @@ template = """ {type} {default} +{example} """ f = open(sys.argv[1]) @@ -30,36 +32,44 @@ options = {k: v for k, v in options.items() groups = ["mailserver.loginAccount", "mailserver.certificate", "mailserver.dkim", + "mailserver.dmarcReporting", "mailserver.fullTextSearch", "mailserver.redis", "mailserver.monitoring", "mailserver.backup", "mailserver.borg"] +def render_option_value(opt, attr): + if attr in opt: + if isinstance(opt[attr], dict) and '_type' in opt[attr]: + if opt[attr]['_type'] == 'literalExpression': + if '\n' in opt[attr]['text']: + res = '\n.. code:: nix\n\n' + textwrap.indent(opt[attr]['text'], ' ') + '\n' + else: + res = '``{}``'.format(opt[attr]['text']) + elif opt[attr]['_type'] == 'literalDocBook': + res = opt[attr]['text'] + else: + s = str(opt[attr]) + if s == "": + res = '``""``' + elif '\n' in s: + res = '\n.. code::\n\n' + textwrap.indent(s, ' ') + '\n' + else: + res = '``{}``'.format(s) + res = '- ' + attr + ': ' + res + else: + res = "" + return res def print_option(name, value): - if 'default' in value: - if value['default'] == "": - default = '``""``' - elif isinstance(value['default'], dict) and '_type' in value['default']: - if value['default']['_type'] == 'literalExpression': - default = '``{}``'.format(value['default']['text']) - if value['default']['_type'] == 'literalDocBook': - default = value['default']['text'] - else: - default = '``{}``'.format(value['default']) - # Some default values contains OUTPUTPATHS which make the - # output not stable across nixpkgs updates. - default = re.sub('/nix/store/[\w.-]*/', '/', default) # noqa - default = '- Default: ' + default - else: - default = "" print(template.format( key=name, line="-"*len(name), - description=value['description'], - type="- Type: ``{}``".format(value['type']), - default=default)) + description=value['description'] or "", + type="- type: ``{}``".format(value['type']), + default=render_option_value(value, 'default'), + example=render_option_value(value, 'example'))) print(header) From 033b3d2a4542f113f3e3fb341cf861f801760b55 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 30 Nov 2022 20:55:24 +0100 Subject: [PATCH 057/225] Removing 22.05 release Because of some incompabilities with the 22.11 release. --- flake.nix | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/flake.nix b/flake.nix index 3897e8c..d9f2fad 100644 --- a/flake.nix +++ b/flake.nix @@ -4,14 +4,13 @@ inputs = { utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; - nixpkgs-22_05.url = "flake:nixpkgs/nixos-22.05"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_05 }: let + outputs = { self, utils, blobs, nixpkgs }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; releases = [ @@ -19,10 +18,6 @@ name = "unstable"; pkgs = nixpkgs.legacyPackages.${system}; } - { - name = "22.05"; - pkgs = nixpkgs-22_05.legacyPackages.${system}; - } ]; testNames = [ "internal" From 31eadb638850824de79cbff9fa1999993537a698 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 30 Nov 2022 21:02:49 +0100 Subject: [PATCH 058/225] doc: regenerate it --- docs/options.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/options.rst b/docs/options.rst index 5c23c83..b8aef82 100644 --- a/docs/options.rst +++ b/docs/options.rst @@ -25,7 +25,7 @@ mailserver.domains The domains that this mail server serves. -- type: ``list of strings`` +- type: ``list of string`` - default: ``[]`` - example: ``['example.com']`` @@ -135,7 +135,7 @@ example all mails for `multi@example.com` will be forwarded to both `user1@example.com` and `user2@example.com`. -- type: ``attribute set of Login Account or non-empty list of Login Accountss`` +- type: ``attribute set of ((Login Account) or non-empty (list of (Login Account)))`` - default: ``{}`` - example: ``{'abuse@example.com': 'user1@example.com', 'info@example.com': 'user1@example.com', 'multi@example.com': ['user1@example.com', 'user2@example.com'], 'postmaster@example.com': 'user1@example.com'}`` @@ -152,7 +152,7 @@ can't send mail as `user@example.com`. Also, this option allows to forward mails to external addresses. -- type: ``attribute set of list of strings or strings`` +- type: ``attribute set of ((list of string) or string)`` - default: ``{}`` - example: ``{'user@example.com': 'user@elsewhere.com'}`` @@ -260,7 +260,7 @@ The mailboxes for dovecot. Depending on the mail client used it might be necessary to change some mailbox's name. -- type: ``unspecified`` +- type: ``unspecified value`` - default: ``{'Drafts': {'auto': 'subscribe', 'specialUse': 'Drafts'}, 'Junk': {'auto': 'subscribe', 'specialUse': 'Junk'}, 'Sent': {'auto': 'subscribe', 'specialUse': 'Sent'}, 'Trash': {'auto': 'no', 'specialUse': 'Trash'}}`` @@ -359,7 +359,7 @@ Use if a spammer has found email addresses in a catchall domain but you do not want to disable the catchall. -- type: ``list of strings`` +- type: ``list of string`` - default: ``[]`` - example: ``['sales@example.com', 'info@example.com']`` @@ -371,7 +371,7 @@ Reject emails from these addresses from unauthorized senders. Use if a spammer is using the same domain or the same sender over and over. -- type: ``list of strings`` +- type: ``list of string`` - default: ``[]`` - example: ``['@example.com', 'spammer@example.net']`` @@ -508,7 +508,7 @@ nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' ``` -- type: ``attribute set of submodules`` +- type: ``attribute set of (submodule)`` - default: ``{}`` - example: ``{'user1': {'hashedPassword': '$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/'}, 'user2': {'hashedPassword': '$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/'}}`` @@ -521,7 +521,7 @@ Note: Use list entries like "@example.com" to create a catchAll that allows sending from all email addresses in these domain. -- type: ``list of strings`` +- type: ``list of string`` - default: ``[]`` - example: ``['abuse@example.com', 'postmaster@example.com']`` @@ -533,7 +533,7 @@ For which domains should this account act as a catch all? Note: Does not allow sending from all addresses of these domains. -- type: ``list of value "example.com" (singular enum)s`` +- type: ``list of value "example.com" (singular enum)`` - default: ``[]`` - example: ``['example.com', 'example2.com']`` @@ -670,7 +670,7 @@ mailserver.certificateDomains Secondary domains and subdomains for which it is necessary to generate a certificate. -- type: ``list of strings`` +- type: ``list of string`` - default: ``[]`` - example: ``['imap.example.com', 'pop3.example.com']`` @@ -879,7 +879,7 @@ mailserver.fullTextSearch.autoIndexExclude Mailboxes to exclude from automatic indexing. -- type: ``list of strings`` +- type: ``list of string`` - default: ``[]`` - example: ``['\\Trash', 'SomeFolder', 'Other/*']`` @@ -1082,7 +1082,7 @@ Note that the intervals also have to exist in configuration as retain options. -- type: ``attribute set of strings`` +- type: ``attribute set of string`` - default: ``{'daily': '30 3 * * *', 'hourly': ' 0 * * * *', 'weekly': ' 0 5 * * 0'}`` @@ -1238,7 +1238,7 @@ mailserver.borgbackup.extraArgumentsForCreate Additional arguments to add to the borg create command line e.g. '--stats'. -- type: ``list of strings`` +- type: ``list of string`` - default: ``[]`` @@ -1248,7 +1248,7 @@ mailserver.borgbackup.extraArgumentsForInit Additional arguments to add to the borg init command line. -- type: ``list of strings`` +- type: ``list of string`` - default: ``['--critical']`` @@ -1268,7 +1268,7 @@ mailserver.borgbackup.locations The locations that are to be backed up by borg. -- type: ``list of paths`` +- type: ``list of path`` - default: ``['/var/vmail']`` From bc667fb6afc45f6cc2d118ab77658faf2227cffd Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 30 Nov 2022 20:56:06 +0100 Subject: [PATCH 059/225] Release 22.11 --- .hydra/declarative-jobsets.nix | 1 + README.md | 8 ++++---- docs/release-notes.rst | 6 ++++++ flake.lock | 18 +++++++++--------- flake.nix | 7 ++++++- tests/clamav.nix | 4 ++-- tests/external.nix | 2 +- 7 files changed, 29 insertions(+), 17 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 11b2763..eeb82d2 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -33,6 +33,7 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; "nixos-22.05" = mkFlakeJobset "nixos-22.05"; + "nixos-22.11" = mkFlakeJobset "nixos-22.11"; }; log = { diff --git a/README.md b/README.md index 86351d7..9aee259 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. +* For NixOS 22.11 + - Use the [SNM branch `nixos-22.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.11) + - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/) + - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/release-notes.html#nixos-22-11) * For NixOS 22.05 - Use the [SNM branch `nixos-22.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.05) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/release-notes.html#nixos-22-05) -* For NixOS 21.11 - - Use the [SNM branch `nixos-21.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-21.11) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-21.11/) - - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-21.11/release-notes.html#nixos-21-11) * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 98c96b5..fa1a87c 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,6 +1,12 @@ Release Notes ============= +NixOS 22.11 +----------- + +- Allow Rspamd to send dmarc reporting + (`merge request `__) + NixOS 22.05 ----------- diff --git a/flake.lock b/flake.lock index e98dc26..0f7f71a 100644 --- a/flake.lock +++ b/flake.lock @@ -18,11 +18,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1642635915, - "narHash": "sha256-vabPA32j81xBO5m3+qXndWp5aqepe+vu96Wkd9UnngM=", + "lastModified": 1669542132, + "narHash": "sha256-DRlg++NJAwPh8io3ExBJdNW7Djs3plVI5jgYQ+iXAZQ=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "6d8215281b2f87a5af9ed7425a26ac575da0438f", + "rev": "a115bb9bd56831941be3776c8a94005867f316a7", "type": "github" }, "original": { @@ -31,18 +31,18 @@ "type": "indirect" } }, - "nixpkgs-22_05": { + "nixpkgs-22_11": { "locked": { - "lastModified": 1654936503, - "narHash": "sha256-soKzdhI4jTHv/rSbh89RdlcJmrPgH8oMb/PLqiqIYVQ=", + "lastModified": 1669558522, + "narHash": "sha256-yqxn+wOiPqe6cxzOo4leeJOp1bXE/fjPEi/3F/bBHv8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dab6df51387c3878cdea09f43589a15729cae9f4", + "rev": "ce5fe99df1f15a09a91a86be9738d68fadfbad82", "type": "github" }, "original": { "id": "nixpkgs", - "ref": "nixos-22.05", + "ref": "nixos-22.11", "type": "indirect" } }, @@ -50,7 +50,7 @@ "inputs": { "blobs": "blobs", "nixpkgs": "nixpkgs", - "nixpkgs-22_05": "nixpkgs-22_05", + "nixpkgs-22_11": "nixpkgs-22_11", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index d9f2fad..4b5c8e6 100644 --- a/flake.nix +++ b/flake.nix @@ -4,13 +4,14 @@ inputs = { utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; + nixpkgs-22_11.url = "flake:nixpkgs/nixos-22.11"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, utils, blobs, nixpkgs }: let + outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_11 }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; releases = [ @@ -18,6 +19,10 @@ name = "unstable"; pkgs = nixpkgs.legacyPackages.${system}; } + { + name = "22.11"; + pkgs = nixpkgs-22_11.legacyPackages.${system}; + } ]; testNames = [ "internal" diff --git a/tests/clamav.nix b/tests/clamav.nix index 4f59d64..7a9f43c 100644 --- a/tests/clamav.nix +++ b/tests/clamav.nix @@ -189,10 +189,10 @@ pkgs.nixosTest { # 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 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) server.wait_until_succeeds( - "set +e; timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" ) client.execute("cp -p /etc/root/.* ~/") diff --git a/tests/external.nix b/tests/external.nix index c14a345..6c03144 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -350,7 +350,7 @@ pkgs.nixosTest { # TODO put this blocking into the systemd units? server.wait_until_succeeds( - "set +e; timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) client.execute("cp -p /etc/root/.* ~/") From 4fcab839d7fe55cb6544ac57e615dd3c09cfeeb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Wed, 30 Nov 2022 22:30:45 +0100 Subject: [PATCH 060/225] docs: use MarkDown for option docs --- .readthedocs.yaml | 4 +- default.nix | 51 +- docs/add-roundcube.rst | 2 +- docs/conf.py | 12 +- docs/howto-develop.rst | 8 +- docs/options.md | 1202 ++++++++++++++++++++++++++++ docs/options.rst | 1319 ------------------------------- docs/requirements.txt | 6 +- flake.lock | 17 + flake.nix | 90 +-- scripts/generate-options.py | 81 ++ scripts/generate-rst-options.py | 87 -- shell.nix | 11 +- 13 files changed, 1403 insertions(+), 1487 deletions(-) create mode 100644 docs/options.md delete mode 100644 docs/options.rst create mode 100644 scripts/generate-options.py delete mode 100644 scripts/generate-rst-options.py diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3918d82..eb6988e 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,9 +5,9 @@ version: 2 build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: - python: "3.9" + python: "3" sphinx: configuration: docs/conf.py diff --git a/default.nix b/default.nix index 275ac83..ef27f43 100644 --- a/default.nix +++ b/default.nix @@ -79,7 +79,7 @@ in ``` Warning: this is stored in plaintext in the Nix store! - Use `hashedPasswordFile` instead. + Use {option}`mailserver.loginAccounts..hashedPasswordFile` instead. ''; }; @@ -156,7 +156,7 @@ in 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 + unauthorized senders with the `sendOnlyRejectMessage` stating the reason. ''; }; @@ -200,7 +200,7 @@ in description = '' Folder to store search indices. If null, indices are stored along with email, which could not necessarily be desirable, - especially when the fullTextSearch option is enable since + especially when {option}`mailserver.fullTextSearch.enable` is `true` since indices it creates are voluminous and do not need to be backed up. @@ -242,8 +242,8 @@ in default = "no"; description = '' Fail searches when no index is available. If set to - body, then only body searches (as opposed to - header) are affected. If set to no, searches may + `body`, then only body searches (as opposed to + header) are affected. If set to `no`, searches may fall back to a very slow brute force search. ''; }; @@ -281,7 +281,7 @@ in randomizedDelaySec = mkOption { type = types.int; default = 1000; - description = "Run the maintenance job not exactly at the time specified with onCalendar, but plus or minus this many seconds."; + description = "Run the maintenance job not exactly at the time specified with `onCalendar`, but plus or minus this many seconds."; }; }; }; @@ -333,7 +333,7 @@ in the value {`"user@example.com" = "user@elsewhere.com";}` means that mails to `user@example.com` are forwarded to `user@elsewhere.com`. The difference with the - `extraVirtualAliases` option is that `user@elsewhere.com` + {option}`mailserver.extraVirtualAliases` option is that `user@elsewhere.com` can't send mail as `user@example.com`. Also, this option allows to forward mails to external addresses. ''; @@ -367,7 +367,7 @@ in description = '' The unix UID of the virtual mail user. Be mindful that if this is changed, you will need to manually adjust the permissions of - mailDirectory. + `mailDirectory`. ''; }; @@ -582,7 +582,7 @@ in type = types.str; default = "mail"; description = '' - + The DKIM selector. ''; }; @@ -590,7 +590,7 @@ in type = types.path; default = "/var/dkim"; description = '' - + The DKIM directory. ''; }; @@ -601,7 +601,7 @@ in 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 (dkimSelector). In order to get + 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. ''; @@ -673,7 +673,7 @@ in type = types.str; example = "ACME Corp."; description = '' - The name of your organization used in the org_name attribute in + The name of your organization used in the `org_name` attribute in DMARC reports. ''; }; @@ -681,7 +681,7 @@ in fromName = mkOption { type = types.str; default = cfg.dmarcReporting.organizationName; - defaultText = literalExpression "organizationName"; + defaultText = literalMD "{option}`mailserver.dmarcReporting.organizationName`"; description = '' The sender name for DMARC reports. Defaults to the organization name. ''; @@ -738,7 +738,7 @@ in if (ip == "0.0.0.0" || ip == "::") then "127.0.0.1" else if isIpv6 ip then "[${ip}]" else ip; - defaultText = lib.literalDocBook "computed from "; + defaultText = lib.literalMD "computed from `config.services.redis.servers.rspamd.bind`"; description = '' Address that rspamd should use to contact redis. ''; @@ -776,7 +776,7 @@ in sendingFqdn = mkOption { type = types.str; default = cfg.fqdn; - defaultText = "config.mailserver.fqdn"; + defaultText = lib.literalMD "{option}`mailserver.fqdn`"; example = "myserver.example.com"; description = '' The fully qualified domain name of the mail server used to @@ -792,7 +792,7 @@ in This setting allows the server to identify as myserver.example.com when forwarding mail, independently of - `fqdn` (which, for SSL reasons, should generally be the name + {option}`mailserver.fqdn` (which, for SSL reasons, should generally be the name to which the user connects). Set this to the name to which the sending IP's reverse DNS @@ -864,7 +864,7 @@ in start program = "${pkgs.systemd}/bin/systemctl start rspamd" stop program = "${pkgs.systemd}/bin/systemctl stop rspamd" ''; - defaultText = lib.literalDocBook "see source"; + defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)"; description = '' The configuration used for monitoring via monit. Use a mail address that you actively check and set it via 'set alert ...'. @@ -881,7 +881,8 @@ in description = '' The location where borg saves the backups. This can be a local path or a remote location such as user@host:/path/to/repo. - It is exported and thus available as an environment variable to cmdPreexec and cmdPostexec. + It is exported and thus available as an environment variable to + {option}`mailserver.borgbackup.cmdPreexec` and {option}`mailserver.borgbackup.cmdPostexec`. ''; }; @@ -941,7 +942,7 @@ in default = "none"; description = '' The backup can be encrypted by choosing any other value than 'none'. - When using encryption the password / passphrase must be provided in passphraseFile. + When using encryption the password/passphrase must be provided in `passphraseFile`. ''; }; @@ -964,6 +965,7 @@ in locations = mkOption { type = types.listOf types.path; default = [cfg.mailDirectory]; + defaultText = lib.literalExpression "[ config.mailserver.mailDirectory ]"; description = "The locations that are to be backed up by borg."; }; @@ -984,9 +986,10 @@ in default = null; description = '' The command to be executed before each backup operation. - This is called prior to borg init in the same script that runs borg init and create and cmdPostexec. - Example: - export BORG_RSH="ssh -i /path/to/private/key" + This is called prior to borg init in the same script that runs borg init and create and `cmdPostexec`. + ''; + example = '' + export BORG_RSH="ssh -i /path/to/private/key" ''; }; @@ -996,7 +999,7 @@ in description = '' The command to be executed after each backup operation. This is called after borg create completed successfully and in the same script that runs - cmdPreexec, borg init and create. + `cmdPreexec`, borg init and create. ''; }; @@ -1009,7 +1012,7 @@ in example = true; description = '' Whether to enable automatic reboot after kernel upgrades. - This is to be used in conjunction with system.autoUpgrade.enable = true" + This is to be used in conjunction with `system.autoUpgrade.enable = true;` ''; }; method = mkOption { diff --git a/docs/add-roundcube.rst b/docs/add-roundcube.rst index 6d4795c..4e6be83 100644 --- a/docs/add-roundcube.rst +++ b/docs/add-roundcube.rst @@ -1,5 +1,5 @@ Add Roundcube, a webmail -======================= +======================== The NixOS module for roundcube nearly works out of the box with SNM. By default, it sets up a nginx virtual host to serve the webmail, other web diff --git a/docs/conf.py b/docs/conf.py index 84eb68b..1845917 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ # -- Project information ----------------------------------------------------- project = 'NixOS Mailserver' -copyright = '2020, NixOS Mailserver Contributors' +copyright = '2022, NixOS Mailserver Contributors' author = 'NixOS Mailserver Contributors' @@ -28,8 +28,16 @@ author = 'NixOS Mailserver Contributors' # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + 'myst_parser' ] +myst_enable_extensions = [ + 'colon_fence', + 'linkify', +] + +smartquotes = False + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -50,4 +58,4 @@ html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = [] diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index ce74683..0ac9009 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -30,8 +30,8 @@ run tests manually. For instance: Contributing to the documentation --------------------------------- -The documentation is written in RST, build with Sphinx and published -by `Read the Docs `_. +The documentation is written in RST (except option documentation which is in MarkDown), +built with Sphinx and published by `Read the Docs `_. For the syntax, see `RST/Sphinx Cheatsheet `_. @@ -47,11 +47,11 @@ documentation: $ firefox ./_build/html/index.html Note if you modify some NixOS mailserver options, you would also need -to regenerate the ``options.rst`` file: +to regenerate the ``options.md`` file: :: - $ nix-shell --run generate-rst-options + $ nix-shell --run generate-options Nixops ------ diff --git a/docs/options.md b/docs/options.md new file mode 100644 index 0000000..0944fa7 --- /dev/null +++ b/docs/options.md @@ -0,0 +1,1202 @@ + +# Mailserver options + +## `mailserver` + + + +`````{option} mailserver.debug +Whether to enable verbose logging for mailserver related services. This +intended be used for development purposes only, you probably don't want +to enable this unless you're hacking on nixos-mailserver. + + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.domains +The domains that this mail server serves. + +- type: ```list of string``` +- default: ```[]``` +- example: ```['example.com']``` +````` + + +`````{option} mailserver.enable +Whether to enable nixos-mailserver. + +- type: ```boolean``` +- default: ```False``` +- example: ```True``` +````` + + +`````{option} mailserver.enableImap +Whether to enable IMAP with STARTTLS on port 143. + + +- type: ```boolean``` +- default: ```True``` + +````` + + +`````{option} mailserver.enableImapSsl +Whether to enable IMAP with TLS in wrapper-mode on port 993. + + +- type: ```boolean``` +- default: ```True``` + +````` + + +`````{option} mailserver.enableManageSieve +Whether to enable ManageSieve, setting this option to true will open +port 4190 in the firewall. + +The ManageSieve protocol allows users to manage their Sieve scripts on +a remote server with a supported client, including Thunderbird. + + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.enablePop3 +Whether to enable POP3 with STARTTLS on port on port 110. + + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.enablePop3Ssl +Whether to enable POP3 with TLS in wrapper-mode on port 995. + + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.enableSubmission +Whether to enable SMTP with STARTTLS on port 587. + + +- type: ```boolean``` +- default: ```True``` + +````` + + +`````{option} mailserver.enableSubmissionSsl +Whether to enable SMTP with TLS in wrapper-mode on port 465. + + +- type: ```boolean``` +- default: ```True``` + +````` + + +`````{option} mailserver.extraVirtualAliases +Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that +all mail to `info@example.com` is forwarded to `user1@example.com`. Note +that it is expected that `postmaster@example.com` and `abuse@example.com` is +forwarded to some valid email address. (Alternatively you can create login +accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows +the user `user1@example.com` to send emails as `info@example.com`. +It's also possible to create an alias for multiple accounts. In this +example all mails for `multi@example.com` will be forwarded to both +`user1@example.com` and `user2@example.com`. + + +- type: ```attribute set of ((Login Account) or non-empty (list of (Login Account)))``` +- default: ```{}``` +- example: ```{'abuse@example.com': 'user1@example.com', 'info@example.com': 'user1@example.com', 'multi@example.com': ['user1@example.com', 'user2@example.com'], 'postmaster@example.com': 'user1@example.com'}``` +````` + + +`````{option} mailserver.forwards +To forward mails to an external address. For instance, +the value {`"user@example.com" = "user@elsewhere.com";}` +means that mails to `user@example.com` are forwarded to +`user@elsewhere.com`. The difference with the +{option}`mailserver.extraVirtualAliases` option is that `user@elsewhere.com` +can't send mail as `user@example.com`. Also, this option +allows to forward mails to external addresses. + + +- type: ```attribute set of ((list of string) or string)``` +- default: ```{}``` +- example: ```{'user@example.com': 'user@elsewhere.com'}``` +````` + + +`````{option} mailserver.fqdn +The fully qualified domain name of the mail server. + +- type: ```string``` + +- example: ```mx.example.com``` +````` + + +`````{option} mailserver.hierarchySeparator +The hierarchy separator for mailboxes used by dovecot for the namespace 'inbox'. +Dovecot defaults to "." but recommends "/". +This affects how mailboxes appear to mail clients and sieve scripts. +For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example". +This does not determine the way your mails are stored on disk. +See https://wiki.dovecot.org/Namespaces for details. + + +- type: ```string``` +- default: ```.``` + +````` + + +`````{option} mailserver.indexDir +Folder to store search indices. If null, indices are stored +along with email, which could not necessarily be desirable, +especially when {option}`mailserver.fullTextSearch.enable` is `true` since +indices it creates are voluminous and do not need to be backed +up. + +Be careful when changing this option value since all indices +would be recreated at the new location (and clients would need +to resynchronize). + +Note the some variables can be used in the file path. See +https://doc.dovecot.org/configuration_manual/mail_location/#variables +for details. + + +- type: ```null or string``` +- default: ```None``` +- example: ```/var/lib/dovecot/indices``` +````` + + +`````{option} mailserver.keyFile +Scheme 1) +Location of the key file + + +- type: ```path``` + +- example: ```/root/mail-server.key``` +````` + + +`````{option} mailserver.lmtpSaveToDetailMailbox +If an email address is delimited by a "+", should it be filed into a +mailbox matching the string after the "+"? For example, +user1+test@example.com would be filed into the mailbox "test". + + +- type: ```one of "yes", "no"``` +- default: ```yes``` + +````` + + +`````{option} mailserver.localDnsResolver +Runs a local DNS resolver (kresd) as recommended when running rspamd. This prevents your log file from filling up with rspamd_monitored_dns_mon entries. + + +- type: ```boolean``` +- default: ```True``` + +````` + + +`````{option} mailserver.mailDirectory +Where to store the mail. + + +- type: ```path``` +- default: ```/var/vmail``` + +````` + + +`````{option} mailserver.mailboxes +The mailboxes for dovecot. +Depending on the mail client used it might be necessary to change some mailbox's name. + + +- type: ```unspecified value``` +- default: ```{'Drafts': {'auto': 'subscribe', 'specialUse': 'Drafts'}, 'Junk': {'auto': 'subscribe', 'specialUse': 'Junk'}, 'Sent': {'auto': 'subscribe', 'specialUse': 'Sent'}, 'Trash': {'auto': 'no', 'specialUse': 'Trash'}}``` + +````` + + +`````{option} mailserver.maxConnectionsPerUser +Maximum number of IMAP/POP3 connections allowed for a user from each IP address. +E.g. a value of 50 allows for 50 IMAP and 50 POP3 connections at the same +time for a single user. + + +- type: ```signed integer``` +- default: ```100``` + +````` + + +`````{option} mailserver.messageSizeLimit +Message size limit enforced by Postfix. + +- type: ```signed integer``` +- default: ```20971520``` +- example: ```52428800``` +````` + + +`````{option} mailserver.openFirewall +Automatically open ports in the firewall. + +- type: ```boolean``` +- default: ```True``` + +````` + + +`````{option} mailserver.policydSPFExtraConfig +Extra configuration options for policyd-spf. This can be use to among +other things skip spf checking for some IP addresses. + + +- type: ```strings concatenated with "\n"``` +- default: `""` +- example: +``` +skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 +``` +````` + + +`````{option} mailserver.rebootAfterKernelUpgrade.enable +Whether to enable automatic reboot after kernel upgrades. +This is to be used in conjunction with `system.autoUpgrade.enable = true;` + + +- type: ```boolean``` +- default: ```False``` +- example: ```True``` +````` + + +`````{option} mailserver.rebootAfterKernelUpgrade.method +Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot. +It is recommended to use the default value because the quicker kexec reboot has a number of problems. +Also if your server is running in a virtual machine the regular reboot will already be very quick. + + +- type: ```one of "reboot", "systemctl kexec"``` +- default: ```reboot``` + +````` + + +`````{option} mailserver.recipientDelimiter +Configure the recipient delimiter. + + +- type: ```string``` +- default: ```+``` + +````` + + +`````{option} mailserver.rejectRecipients +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. + + +- type: ```list of string``` +- default: ```[]``` +- example: ```['sales@example.com', 'info@example.com']``` +````` + + +`````{option} mailserver.rejectSender +Reject emails from these addresses from unauthorized senders. +Use if a spammer is using the same domain or the same sender over and over. + + +- type: ```list of string``` +- default: ```[]``` +- example: ```['@example.com', 'spammer@example.net']``` +````` + + +`````{option} mailserver.rewriteMessageId +Rewrites the Message-ID's hostname-part of outgoing emails to the FQDN. +Please be aware that this may cause problems with some mail clients +relying on the original Message-ID. + + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.sendingFqdn +The fully qualified domain name of the mail server used to +identify with remote servers. + +If this server's IP serves purposes other than a mail server, +it may be desirable for the server to have a name other than +that to which the user will connect. For example, the user +might connect to mx.example.com, but the server's IP has +reverse DNS that resolves to myserver.example.com; in this +scenario, some mail servers may reject or penalize the +message. + +This setting allows the server to identify as +myserver.example.com when forwarding mail, independently of +{option}`mailserver.fqdn` (which, for SSL reasons, should generally be the name +to which the user connects). + +Set this to the name to which the sending IP's reverse DNS +resolves. + + +- type: ```string``` +- default: {option}`mailserver.fqdn` +- example: ```myserver.example.com``` +````` + + +`````{option} mailserver.sieveDirectory +Where to store the sieve scripts. + + +- type: ```path``` +- default: ```/var/sieve``` + +````` + + +`````{option} mailserver.useFsLayout +Sets whether dovecot should organize mail in subdirectories: + +- /var/vmail/example.com/user/.folder.subfolder/ (default layout) +- /var/vmail/example.com/user/folder/subfolder/ (FS layout) + +See https://wiki2.dovecot.org/MailboxFormat/Maildir for details. + + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.virusScanning +Whether to activate virus scanning. Note that virus scanning is _very_ +expensive memory wise. + + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.vmailGroupName +The user name and group name of the user that owns the directory where all +the mail is stored. + + +- type: ```string``` +- default: ```virtualMail``` + +````` + + +`````{option} mailserver.vmailUID +The unix UID of the virtual mail user. Be mindful that if this is +changed, you will need to manually adjust the permissions of +`mailDirectory`. + + +- type: ```signed integer``` +- default: ```5000``` + +````` + + +`````{option} mailserver.vmailUserName +The user name and group name of the user that owns the directory where all +the mail is stored. + + +- type: ```string``` +- default: ```virtualMail``` + +````` + +## `mailserver.loginAccounts` + + +`````{option} mailserver.loginAccounts +The login account of the domain. Every account is mapped to a unix user, +e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as +follows + +``` +nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' +``` + + +- type: ```attribute set of (submodule)``` +- default: ```{}``` +- example: ```{'user1': {'hashedPassword': '$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/'}, 'user2': {'hashedPassword': '$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/'}}``` +````` + + +`````{option} mailserver.loginAccounts..aliases +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. + + +- type: ```list of string``` +- default: ```[]``` +- example: ```['abuse@example.com', 'postmaster@example.com']``` +````` + + +`````{option} mailserver.loginAccounts..catchAll +For which domains should this account act as a catch all? +Note: Does not allow sending from all addresses of these domains. + + +- type: ```list of value "example.com" (singular enum)``` +- default: ```[]``` +- example: ```['example.com', 'example2.com']``` +````` + + +`````{option} mailserver.loginAccounts..hashedPassword +The user's hashed password. Use `mkpasswd` as follows + +``` +nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' +``` + +Warning: this is stored in plaintext in the Nix store! +Use {option}`mailserver.loginAccounts..hashedPasswordFile` instead. + + +- type: ```null or string``` +- default: ```None``` +- example: ```$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/``` +````` + + +`````{option} mailserver.loginAccounts..hashedPasswordFile +A file containing the user's hashed password. Use `mkpasswd` as follows + +``` +nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' +``` + + +- type: ```null or path``` +- default: ```None``` +- example: ```/run/keys/user1-passwordhash``` +````` + + +`````{option} mailserver.loginAccounts..name +Username + +- type: ```string``` + +- example: ```user1@example.com``` +````` + + +`````{option} mailserver.loginAccounts..quota +Per user quota rules. Accepted sizes are `xx k/M/G/T` with the +obvious meaning. Leave blank for the standard quota `100G`. + + +- type: ```null or string``` +- default: ```None``` +- example: ```2G``` +````` + + +`````{option} mailserver.loginAccounts..sendOnly +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. + + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.loginAccounts..sendOnlyRejectMessage +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. + + +- type: ```string``` +- default: ```This account cannot receive emails.``` + +````` + + +`````{option} mailserver.loginAccounts..sieveScript +Per-user sieve script. + + +- type: ```null or strings concatenated with "\n"``` +- default: ```None``` +- example: +``` +require ["fileinto", "mailbox"]; + +if address :is "from" "gitlab@mg.gitlab.com" { + fileinto :create "GitLab"; + stop; +} + +# This must be the last rule, it will check if list-id is set, and +# file the message into the Lists folder for further investigation +elsif header :matches "list-id" "" { + fileinto :create "Lists"; + stop; +} +``` +````` + +## `mailserver.certificate` + + +`````{option} mailserver.certificateDirectory +Scheme 2) +This is the folder where the certificate will be created. The name is +hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the +certificate is valid for 10 years. + + +- type: ```path``` +- default: ```/var/certs``` + +````` + + +`````{option} mailserver.certificateDomains +Secondary domains and subdomains for which it is necessary to generate a certificate. + +- type: ```list of string``` +- default: ```[]``` +- example: ```['imap.example.com', 'pop3.example.com']``` +````` + + +`````{option} mailserver.certificateFile +Scheme 1) +Location of the certificate + + +- type: ```path``` + +- example: ```/root/mail-server.crt``` +````` + + +`````{option} mailserver.certificateScheme +Certificate Files. There are three options for these. + +1) You specify locations and manually copy certificates there. +2) You let the server create new (self signed) certificates on the fly. +3) You let the server create a certificate via `Let's Encrypt`. Note that + this implies that a stripped down webserver has to be started. This also + implies that the FQDN must be set as an `A` record to point to the IP of + the server. In particular port 80 on the server will be opened. For details + on how to set up the domain records, see the guide in the readme. + + +- type: ```one of 1, 2, 3``` +- default: ```2``` + +````` + +## `mailserver.dkim` + + +`````{option} mailserver.dkimBodyCanonicalization +DKIM canonicalization algorithm for message bodies. + +See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. + + +- type: ```one of "relaxed", "simple"``` +- default: ```relaxed``` + +````` + + +`````{option} mailserver.dkimHeaderCanonicalization +DKIM canonicalization algorithm for message headers. + +See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. + + +- type: ```one of "relaxed", "simple"``` +- default: ```relaxed``` + +````` + + +`````{option} mailserver.dkimKeyBits +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. + + +- type: ```signed integer``` +- default: ```1024``` + +````` + + +`````{option} mailserver.dkimKeyDirectory +The DKIM directory. + + +- type: ```path``` +- default: ```/var/dkim``` + +````` + + +`````{option} mailserver.dkimSelector +The DKIM selector. + + +- type: ```string``` +- default: ```mail``` + +````` + + +`````{option} mailserver.dkimSigning +Whether to activate dkim signing. + + +- type: ```boolean``` +- default: ```True``` + +````` + +## `mailserver.dmarcReporting` + + +`````{option} mailserver.dmarcReporting.domain +The domain from which outgoing DMARC reports are served. + + +- type: ```value "example.com" (singular enum)``` + +- example: ```example.com``` +````` + + +`````{option} mailserver.dmarcReporting.email +The email address used for outgoing DMARC reports. Read-only. + + +- type: ```string``` +- default: ```"${localpart}@${domain}"``` + +````` + + +`````{option} mailserver.dmarcReporting.enable +Whether to send out aggregated, daily DMARC reports in response to incoming +mail, when the sender domain defines a DMARC policy including the RUA tag. + +This is helpful for the mail ecosystem, because it allows third parties to +get notified about SPF/DKIM violations originating from their sender domains. + +See https://rspamd.com/doc/modules/dmarc.html#reporting + + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.dmarcReporting.fromName +The sender name for DMARC reports. Defaults to the organization name. + + +- type: ```string``` +- default: {option}`mailserver.dmarcReporting.organizationName` + +````` + + +`````{option} mailserver.dmarcReporting.localpart +The local part of the email address used for outgoing DMARC reports. + + +- type: ```string``` +- default: ```dmarc-noreply``` +- example: ```dmarc-report``` +````` + + +`````{option} mailserver.dmarcReporting.organizationName +The name of your organization used in the `org_name` attribute in +DMARC reports. + + +- type: ```string``` + +- example: ```ACME Corp.``` +````` + +## `mailserver.fullTextSearch` + + +`````{option} mailserver.fullTextSearch.autoIndex +Enable automatic indexing of messages as they are received or modified. + +- type: ```boolean``` +- default: ```True``` + +````` + + +`````{option} mailserver.fullTextSearch.autoIndexExclude +Mailboxes to exclude from automatic indexing. + + +- type: ```list of string``` +- default: ```[]``` +- example: ```['\\Trash', 'SomeFolder', 'Other/*']``` +````` + + +`````{option} mailserver.fullTextSearch.enable +Whether to enable Full text search indexing with xapian. This has significant performance and disk space cost.. + +- type: ```boolean``` +- default: ```False``` +- example: ```True``` +````` + + +`````{option} mailserver.fullTextSearch.enforced +Fail searches when no index is available. If set to +`body`, then only body searches (as opposed to +header) are affected. If set to `no`, searches may +fall back to a very slow brute force search. + + +- type: ```one of "yes", "no", "body"``` +- default: ```no``` + +````` + + +`````{option} mailserver.fullTextSearch.indexAttachments +Also index text-only attachements. Binary attachements are never indexed. + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.fullTextSearch.maintenance.enable +Regularly optmize indices, as recommended by upstream. + +- type: ```boolean``` +- default: ```True``` + +````` + + +`````{option} mailserver.fullTextSearch.maintenance.onCalendar +When to run the maintenance job. See systemd.time(7) for more information about the format. + +- type: ```string``` +- default: ```daily``` + +````` + + +`````{option} mailserver.fullTextSearch.maintenance.randomizedDelaySec +Run the maintenance job not exactly at the time specified with `onCalendar`, but plus or minus this many seconds. + +- type: ```signed integer``` +- default: ```1000``` + +````` + + +`````{option} mailserver.fullTextSearch.maxSize +Size of the largest n-gram to index. + +- type: ```signed integer``` +- default: ```20``` + +````` + + +`````{option} mailserver.fullTextSearch.memoryLimit +Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit. + +- type: ```null or signed integer``` +- default: ```None``` +- example: ```2000``` +````` + + +`````{option} mailserver.fullTextSearch.minSize +Size of the smallest n-gram to index. + +- type: ```signed integer``` +- default: ```2``` + +````` + +## `mailserver.redis` + + +`````{option} mailserver.redis.address +Address that rspamd should use to contact redis. + + +- type: ```string``` +- default: computed from `config.services.redis.servers.rspamd.bind` + +````` + + +`````{option} mailserver.redis.password +Password that rspamd should use to contact redis, or null if not required. + + +- type: ```null or string``` +- default: ```config.services.redis.servers.rspamd.requirePass``` + +````` + + +`````{option} mailserver.redis.port +Port that rspamd should use to contact redis. + + +- type: ```16 bit unsigned integer; between 0 and 65535 (both inclusive)``` +- default: ```config.services.redis.servers.rspamd.port``` + +````` + +## `mailserver.monitoring` + + +`````{option} mailserver.monitoring.alertAddress +The email address to send alerts to. + + +- type: ```string``` + + +````` + + +`````{option} mailserver.monitoring.config +The configuration used for monitoring via monit. +Use a mail address that you actively check and set it via 'set alert ...'. + + +- type: ```string``` +- default: see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix) + +````` + + +`````{option} mailserver.monitoring.enable +Whether to enable monitoring via monit. + +- type: ```boolean``` +- default: ```False``` +- example: ```True``` +````` + +## `mailserver.backup` + + +`````{option} mailserver.backup.cmdPostexec +The command to be executed after each backup operation. This is wrapped in a shell script to be called by rsnapshot. + +- type: ```null or string``` +- default: ```None``` + +````` + + +`````{option} mailserver.backup.cmdPreexec +The command to be executed before each backup operation. This is wrapped in a shell script to be called by rsnapshot. + + +- type: ```null or string``` +- default: ```None``` + +````` + + +`````{option} mailserver.backup.cronIntervals +Periodicity at which intervals should be run by cron. +Note that the intervals also have to exist in configuration +as retain options. + + +- type: ```attribute set of string``` +- default: ```{'daily': '30 3 * * *', 'hourly': ' 0 * * * *', 'weekly': ' 0 5 * * 0'}``` + +````` + + +`````{option} mailserver.backup.enable +Whether to enable backup via rsnapshot. + +- type: ```boolean``` +- default: ```False``` +- example: ```True``` +````` + + +`````{option} mailserver.backup.retain.daily +How many daily snapshots are retained. + +- type: ```signed integer``` +- default: ```7``` + +````` + + +`````{option} mailserver.backup.retain.hourly +How many hourly snapshots are retained. + +- type: ```signed integer``` +- default: ```24``` + +````` + + +`````{option} mailserver.backup.retain.weekly +How many weekly snapshots are retained. + +- type: ```signed integer``` +- default: ```54``` + +````` + + +`````{option} mailserver.backup.snapshotRoot +The directory where rsnapshot stores the backup. + + +- type: ```path``` +- default: ```/var/rsnapshot``` + +````` + +## `mailserver.borgbackup` + + +`````{option} mailserver.borgbackup.cmdPostexec +The command to be executed after each backup operation. +This is called after borg create completed successfully and in the same script that runs +`cmdPreexec`, borg init and create. + + +- type: ```null or string``` +- default: ```None``` + +````` + + +`````{option} mailserver.borgbackup.cmdPreexec +The command to be executed before each backup operation. +This is called prior to borg init in the same script that runs borg init and create and `cmdPostexec`. + + +- type: ```null or string``` +- default: ```None``` +- example: +``` +export BORG_RSH="ssh -i /path/to/private/key" +``` +````` + + +`````{option} mailserver.borgbackup.compression.auto +Leaves it to borg to determine whether an individual file should be compressed. + +- type: ```boolean``` +- default: ```False``` + +````` + + +`````{option} mailserver.borgbackup.compression.level +Denotes the level of compression used by borg. +Most methods accept levels from 0 to 9 but zstd which accepts values from 1 to 22. +If null the decision is left up to borg. + + +- type: ```null or signed integer``` +- default: ```None``` + +````` + + +`````{option} mailserver.borgbackup.compression.method +Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4. + +- type: ```null or one of "none", "lz4", "zstd", "zlib", "lzma"``` +- default: ```None``` + +````` + + +`````{option} mailserver.borgbackup.enable +Whether to enable backup via borgbackup. + +- type: ```boolean``` +- default: ```False``` +- example: ```True``` +````` + + +`````{option} mailserver.borgbackup.encryption.method +The backup can be encrypted by choosing any other value than 'none'. +When using encryption the password/passphrase must be provided in `passphraseFile`. + + +- type: ```one of "none", "authenticated", "authenticated-blake2", "repokey", "keyfile", "repokey-blake2", "keyfile-blake2"``` +- default: ```none``` + +````` + + +`````{option} mailserver.borgbackup.encryption.passphraseFile +Path to a file containing the encryption password or passphrase. + +- type: ```null or path``` +- default: ```None``` + +````` + + +`````{option} mailserver.borgbackup.extraArgumentsForCreate +Additional arguments to add to the borg create command line e.g. '--stats'. + +- type: ```list of string``` +- default: ```[]``` + +````` + + +`````{option} mailserver.borgbackup.extraArgumentsForInit +Additional arguments to add to the borg init command line. + +- type: ```list of string``` +- default: ```['--critical']``` + +````` + + +`````{option} mailserver.borgbackup.group +The group borg and its launch script is run as. + +- type: ```string``` +- default: ```virtualMail``` + +````` + + +`````{option} mailserver.borgbackup.locations +The locations that are to be backed up by borg. + +- type: ```list of path``` +- default: ```[ config.mailserver.mailDirectory ]``` + +````` + + +`````{option} mailserver.borgbackup.name +The name of the individual backups as used by borg. +Certain placeholders will be replaced by borg. + + +- type: ```string``` +- default: ```{hostname}-{user}-{now}``` + +````` + + +`````{option} mailserver.borgbackup.repoLocation +The location where borg saves the backups. +This can be a local path or a remote location such as user@host:/path/to/repo. +It is exported and thus available as an environment variable to +{option}`mailserver.borgbackup.cmdPreexec` and {option}`mailserver.borgbackup.cmdPostexec`. + + +- type: ```string``` +- default: ```/var/borgbackup``` + +````` + + +`````{option} mailserver.borgbackup.startAt +When or how often the backup should run. Must be in the format described in systemd.time 7. + +- type: ```string``` +- default: ```hourly``` + +````` + + +`````{option} mailserver.borgbackup.user +The user borg and its launch script is run as. + +- type: ```string``` +- default: ```virtualMail``` + +````` + diff --git a/docs/options.rst b/docs/options.rst deleted file mode 100644 index b8aef82..0000000 --- a/docs/options.rst +++ /dev/null @@ -1,1319 +0,0 @@ - -Mailserver Options -================== - -mailserver -~~~~~~~~~~ - - - -mailserver.debug ----------------- - -Whether to enable verbose logging for mailserver related services. This -intended be used for development purposes only, you probably don't want -to enable this unless you're hacking on nixos-mailserver. - - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.domains ------------------- - -The domains that this mail server serves. - -- type: ``list of string`` -- default: ``[]`` -- example: ``['example.com']`` - - -mailserver.enable ------------------ - -Whether to enable nixos-mailserver. - -- type: ``boolean`` -- default: ``False`` -- example: ``True`` - - -mailserver.enableImap ---------------------- - -Whether to enable IMAP with STARTTLS on port 143. - - -- type: ``boolean`` -- default: ``True`` - - - -mailserver.enableImapSsl ------------------------- - -Whether to enable IMAP with TLS in wrapper-mode on port 993. - - -- type: ``boolean`` -- default: ``True`` - - - -mailserver.enableManageSieve ----------------------------- - -Whether to enable ManageSieve, setting this option to true will open -port 4190 in the firewall. - -The ManageSieve protocol allows users to manage their Sieve scripts on -a remote server with a supported client, including Thunderbird. - - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.enablePop3 ---------------------- - -Whether to enable POP3 with STARTTLS on port on port 110. - - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.enablePop3Ssl ------------------------- - -Whether to enable POP3 with TLS in wrapper-mode on port 995. - - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.enableSubmission ---------------------------- - -Whether to enable SMTP with STARTTLS on port 587. - - -- type: ``boolean`` -- default: ``True`` - - - -mailserver.enableSubmissionSsl ------------------------------- - -Whether to enable SMTP with TLS in wrapper-mode on port 465. - - -- type: ``boolean`` -- default: ``True`` - - - -mailserver.extraVirtualAliases ------------------------------- - -Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that -all mail to `info@example.com` is forwarded to `user1@example.com`. Note -that it is expected that `postmaster@example.com` and `abuse@example.com` is -forwarded to some valid email address. (Alternatively you can create login -accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows -the user `user1@example.com` to send emails as `info@example.com`. -It's also possible to create an alias for multiple accounts. In this -example all mails for `multi@example.com` will be forwarded to both -`user1@example.com` and `user2@example.com`. - - -- type: ``attribute set of ((Login Account) or non-empty (list of (Login Account)))`` -- default: ``{}`` -- example: ``{'abuse@example.com': 'user1@example.com', 'info@example.com': 'user1@example.com', 'multi@example.com': ['user1@example.com', 'user2@example.com'], 'postmaster@example.com': 'user1@example.com'}`` - - -mailserver.forwards -------------------- - -To forward mails to an external address. For instance, -the value {`"user@example.com" = "user@elsewhere.com";}` -means that mails to `user@example.com` are forwarded to -`user@elsewhere.com`. The difference with the -`extraVirtualAliases` option is that `user@elsewhere.com` -can't send mail as `user@example.com`. Also, this option -allows to forward mails to external addresses. - - -- type: ``attribute set of ((list of string) or string)`` -- default: ``{}`` -- example: ``{'user@example.com': 'user@elsewhere.com'}`` - - -mailserver.fqdn ---------------- - -The fully qualified domain name of the mail server. - -- type: ``string`` - -- example: ``mx.example.com`` - - -mailserver.hierarchySeparator ------------------------------ - -The hierarchy separator for mailboxes used by dovecot for the namespace 'inbox'. -Dovecot defaults to "." but recommends "/". -This affects how mailboxes appear to mail clients and sieve scripts. -For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example". -This does not determine the way your mails are stored on disk. -See https://wiki.dovecot.org/Namespaces for details. - - -- type: ``string`` -- default: ``.`` - - - -mailserver.indexDir -------------------- - -Folder to store search indices. If null, indices are stored -along with email, which could not necessarily be desirable, -especially when the fullTextSearch option is enable since -indices it creates are voluminous and do not need to be backed -up. - -Be careful when changing this option value since all indices -would be recreated at the new location (and clients would need -to resynchronize). - -Note the some variables can be used in the file path. See -https://doc.dovecot.org/configuration_manual/mail_location/#variables -for details. - - -- type: ``null or string`` -- default: ``None`` -- example: ``/var/lib/dovecot/indices`` - - -mailserver.keyFile ------------------- - -Scheme 1) -Location of the key file - - -- type: ``path`` - -- example: ``/root/mail-server.key`` - - -mailserver.lmtpSaveToDetailMailbox ----------------------------------- - -If an email address is delimited by a "+", should it be filed into a -mailbox matching the string after the "+"? For example, -user1+test@example.com would be filed into the mailbox "test". - - -- type: ``one of "yes", "no"`` -- default: ``yes`` - - - -mailserver.localDnsResolver ---------------------------- - -Runs a local DNS resolver (kresd) as recommended when running rspamd. This prevents your log file from filling up with rspamd_monitored_dns_mon entries. - - -- type: ``boolean`` -- default: ``True`` - - - -mailserver.mailDirectory ------------------------- - -Where to store the mail. - - -- type: ``path`` -- default: ``/var/vmail`` - - - -mailserver.mailboxes --------------------- - -The mailboxes for dovecot. -Depending on the mail client used it might be necessary to change some mailbox's name. - - -- type: ``unspecified value`` -- default: ``{'Drafts': {'auto': 'subscribe', 'specialUse': 'Drafts'}, 'Junk': {'auto': 'subscribe', 'specialUse': 'Junk'}, 'Sent': {'auto': 'subscribe', 'specialUse': 'Sent'}, 'Trash': {'auto': 'no', 'specialUse': 'Trash'}}`` - - - -mailserver.maxConnectionsPerUser --------------------------------- - -Maximum number of IMAP/POP3 connections allowed for a user from each IP address. -E.g. a value of 50 allows for 50 IMAP and 50 POP3 connections at the same -time for a single user. - - -- type: ``signed integer`` -- default: ``100`` - - - -mailserver.messageSizeLimit ---------------------------- - -Message size limit enforced by Postfix. - -- type: ``signed integer`` -- default: ``20971520`` -- example: ``52428800`` - - -mailserver.openFirewall ------------------------ - -Automatically open ports in the firewall. - -- type: ``boolean`` -- default: ``True`` - - - -mailserver.policydSPFExtraConfig --------------------------------- - -Extra configuration options for policyd-spf. This can be use to among -other things skip spf checking for some IP addresses. - - -- type: ``strings concatenated with "\n"`` -- default: ``""`` -- example: -.. code:: - - skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 - - - - -mailserver.rebootAfterKernelUpgrade.enable ------------------------------------------- - -Whether to enable automatic reboot after kernel upgrades. -This is to be used in conjunction with system.autoUpgrade.enable = true" - - -- type: ``boolean`` -- default: ``False`` -- example: ``True`` - - -mailserver.rebootAfterKernelUpgrade.method ------------------------------------------- - -Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot. -It is recommended to use the default value because the quicker kexec reboot has a number of problems. -Also if your server is running in a virtual machine the regular reboot will already be very quick. - - -- type: ``one of "reboot", "systemctl kexec"`` -- default: ``reboot`` - - - -mailserver.recipientDelimiter ------------------------------ - -Configure the recipient delimiter. - - -- type: ``string`` -- default: ``+`` - - - -mailserver.rejectRecipients ---------------------------- - -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. - - -- type: ``list of string`` -- default: ``[]`` -- example: ``['sales@example.com', 'info@example.com']`` - - -mailserver.rejectSender ------------------------ - -Reject emails from these addresses from unauthorized senders. -Use if a spammer is using the same domain or the same sender over and over. - - -- type: ``list of string`` -- default: ``[]`` -- example: ``['@example.com', 'spammer@example.net']`` - - -mailserver.rewriteMessageId ---------------------------- - -Rewrites the Message-ID's hostname-part of outgoing emails to the FQDN. -Please be aware that this may cause problems with some mail clients -relying on the original Message-ID. - - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.sendingFqdn ----------------------- - -The fully qualified domain name of the mail server used to -identify with remote servers. - -If this server's IP serves purposes other than a mail server, -it may be desirable for the server to have a name other than -that to which the user will connect. For example, the user -might connect to mx.example.com, but the server's IP has -reverse DNS that resolves to myserver.example.com; in this -scenario, some mail servers may reject or penalize the -message. - -This setting allows the server to identify as -myserver.example.com when forwarding mail, independently of -`fqdn` (which, for SSL reasons, should generally be the name -to which the user connects). - -Set this to the name to which the sending IP's reverse DNS -resolves. - - -- type: ``string`` -- default: ``config.mailserver.fqdn`` -- example: ``myserver.example.com`` - - -mailserver.sieveDirectory -------------------------- - -Where to store the sieve scripts. - - -- type: ``path`` -- default: ``/var/sieve`` - - - -mailserver.useFsLayout ----------------------- - -Sets whether dovecot should organize mail in subdirectories: - -- /var/vmail/example.com/user/.folder.subfolder/ (default layout) -- /var/vmail/example.com/user/folder/subfolder/ (FS layout) - -See https://wiki2.dovecot.org/MailboxFormat/Maildir for details. - - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.virusScanning ------------------------- - -Whether to activate virus scanning. Note that virus scanning is _very_ -expensive memory wise. - - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.vmailGroupName -------------------------- - -The user name and group name of the user that owns the directory where all -the mail is stored. - - -- type: ``string`` -- default: ``virtualMail`` - - - -mailserver.vmailUID -------------------- - -The unix UID of the virtual mail user. Be mindful that if this is -changed, you will need to manually adjust the permissions of -mailDirectory. - - -- type: ``signed integer`` -- default: ``5000`` - - - -mailserver.vmailUserName ------------------------- - -The user name and group name of the user that owns the directory where all -the mail is stored. - - -- type: ``string`` -- default: ``virtualMail`` - - -mailserver.loginAccount -~~~~~~~~~~~~~~~~~~~~~~~ - - -mailserver.loginAccounts ------------------------- - -The login account of the domain. Every account is mapped to a unix user, -e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as -follows - -``` -nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' -``` - - -- type: ``attribute set of (submodule)`` -- default: ``{}`` -- example: ``{'user1': {'hashedPassword': '$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/'}, 'user2': {'hashedPassword': '$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/'}}`` - - -mailserver.loginAccounts..aliases ---------------------------------------- - -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. - - -- type: ``list of string`` -- default: ``[]`` -- example: ``['abuse@example.com', 'postmaster@example.com']`` - - -mailserver.loginAccounts..catchAll ----------------------------------------- - -For which domains should this account act as a catch all? -Note: Does not allow sending from all addresses of these domains. - - -- type: ``list of value "example.com" (singular enum)`` -- default: ``[]`` -- example: ``['example.com', 'example2.com']`` - - -mailserver.loginAccounts..hashedPassword ----------------------------------------------- - -The user's hashed password. Use `mkpasswd` as follows - -``` -nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' -``` - -Warning: this is stored in plaintext in the Nix store! -Use `hashedPasswordFile` instead. - - -- type: ``null or string`` -- default: ``None`` -- example: ``$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/`` - - -mailserver.loginAccounts..hashedPasswordFile --------------------------------------------------- - -A file containing the user's hashed password. Use `mkpasswd` as follows - -``` -nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' -``` - - -- type: ``null or path`` -- default: ``None`` -- example: ``/run/keys/user1-passwordhash`` - - -mailserver.loginAccounts..name ------------------------------------- - -Username - -- type: ``string`` - -- example: ``user1@example.com`` - - -mailserver.loginAccounts..quota -------------------------------------- - -Per user quota rules. Accepted sizes are `xx k/M/G/T` with the -obvious meaning. Leave blank for the standard quota `100G`. - - -- type: ``null or string`` -- default: ``None`` -- example: ``2G`` - - -mailserver.loginAccounts..sendOnly ----------------------------------------- - -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. - - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.loginAccounts..sendOnlyRejectMessage ------------------------------------------------------ - -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. - - -- type: ``string`` -- default: ``This account cannot receive emails.`` - - - -mailserver.loginAccounts..sieveScript -------------------------------------------- - -Per-user sieve script. - - -- type: ``null or strings concatenated with "\n"`` -- default: ``None`` -- example: -.. code:: - - require ["fileinto", "mailbox"]; - - if address :is "from" "gitlab@mg.gitlab.com" { - fileinto :create "GitLab"; - stop; - } - - # This must be the last rule, it will check if list-id is set, and - # file the message into the Lists folder for further investigation - elsif header :matches "list-id" "" { - fileinto :create "Lists"; - stop; - } - - - -mailserver.certificate -~~~~~~~~~~~~~~~~~~~~~~ - - -mailserver.certificateDirectory -------------------------------- - -Scheme 2) -This is the folder where the certificate will be created. The name is -hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the -certificate is valid for 10 years. - - -- type: ``path`` -- default: ``/var/certs`` - - - -mailserver.certificateDomains ------------------------------ - -Secondary domains and subdomains for which it is necessary to generate a certificate. - -- type: ``list of string`` -- default: ``[]`` -- example: ``['imap.example.com', 'pop3.example.com']`` - - -mailserver.certificateFile --------------------------- - -Scheme 1) -Location of the certificate - - -- type: ``path`` - -- example: ``/root/mail-server.crt`` - - -mailserver.certificateScheme ----------------------------- - -Certificate Files. There are three options for these. - -1) You specify locations and manually copy certificates there. -2) You let the server create new (self signed) certificates on the fly. -3) You let the server create a certificate via `Let's Encrypt`. Note that - this implies that a stripped down webserver has to be started. This also - implies that the FQDN must be set as an `A` record to point to the IP of - the server. In particular port 80 on the server will be opened. For details - on how to set up the domain records, see the guide in the readme. - - -- type: ``one of 1, 2, 3`` -- default: ``2`` - - -mailserver.dkim -~~~~~~~~~~~~~~~ - - -mailserver.dkimBodyCanonicalization ------------------------------------ - -DKIM canonicalization algorithm for message bodies. - -See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. - - -- type: ``one of "relaxed", "simple"`` -- default: ``relaxed`` - - - -mailserver.dkimHeaderCanonicalization -------------------------------------- - -DKIM canonicalization algorithm for message headers. - -See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. - - -- type: ``one of "relaxed", "simple"`` -- default: ``relaxed`` - - - -mailserver.dkimKeyBits ----------------------- - -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 (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. - - -- type: ``signed integer`` -- default: ``1024`` - - - -mailserver.dkimKeyDirectory ---------------------------- - - - - -- type: ``path`` -- default: ``/var/dkim`` - - - -mailserver.dkimSelector ------------------------ - - - - -- type: ``string`` -- default: ``mail`` - - - -mailserver.dkimSigning ----------------------- - -Whether to activate dkim signing. - - -- type: ``boolean`` -- default: ``True`` - - -mailserver.dmarcReporting -~~~~~~~~~~~~~~~~~~~~~~~~~ - - -mailserver.dmarcReporting.domain --------------------------------- - -The domain from which outgoing DMARC reports are served. - - -- type: ``value "example.com" (singular enum)`` - -- example: ``example.com`` - - -mailserver.dmarcReporting.email -------------------------------- - -The email address used for outgoing DMARC reports. Read-only. - - -- type: ``string`` -- default: ``"${localpart}@${domain}"`` - - - -mailserver.dmarcReporting.enable --------------------------------- - -Whether to send out aggregated, daily DMARC reports in response to incoming -mail, when the sender domain defines a DMARC policy including the RUA tag. - -This is helpful for the mail ecosystem, because it allows third parties to -get notified about SPF/DKIM violations originating from their sender domains. - -See https://rspamd.com/doc/modules/dmarc.html#reporting - - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.dmarcReporting.fromName ----------------------------------- - -The sender name for DMARC reports. Defaults to the organization name. - - -- type: ``string`` -- default: ``organizationName`` - - - -mailserver.dmarcReporting.localpart ------------------------------------ - -The local part of the email address used for outgoing DMARC reports. - - -- type: ``string`` -- default: ``dmarc-noreply`` -- example: ``dmarc-report`` - - -mailserver.dmarcReporting.organizationName ------------------------------------------- - -The name of your organization used in the org_name attribute in -DMARC reports. - - -- type: ``string`` - -- example: ``ACME Corp.`` - -mailserver.fullTextSearch -~~~~~~~~~~~~~~~~~~~~~~~~~ - - -mailserver.fullTextSearch.autoIndex ------------------------------------ - -Enable automatic indexing of messages as they are received or modified. - -- type: ``boolean`` -- default: ``True`` - - - -mailserver.fullTextSearch.autoIndexExclude ------------------------------------------- - -Mailboxes to exclude from automatic indexing. - - -- type: ``list of string`` -- default: ``[]`` -- example: ``['\\Trash', 'SomeFolder', 'Other/*']`` - - -mailserver.fullTextSearch.enable --------------------------------- - -Whether to enable Full text search indexing with xapian. This has significant performance and disk space cost.. - -- type: ``boolean`` -- default: ``False`` -- example: ``True`` - - -mailserver.fullTextSearch.enforced ----------------------------------- - -Fail searches when no index is available. If set to -body, then only body searches (as opposed to -header) are affected. If set to no, searches may -fall back to a very slow brute force search. - - -- type: ``one of "yes", "no", "body"`` -- default: ``no`` - - - -mailserver.fullTextSearch.indexAttachments ------------------------------------------- - -Also index text-only attachements. Binary attachements are never indexed. - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.fullTextSearch.maintenance.enable --------------------------------------------- - -Regularly optmize indices, as recommended by upstream. - -- type: ``boolean`` -- default: ``True`` - - - -mailserver.fullTextSearch.maintenance.onCalendar ------------------------------------------------- - -When to run the maintenance job. See systemd.time(7) for more information about the format. - -- type: ``string`` -- default: ``daily`` - - - -mailserver.fullTextSearch.maintenance.randomizedDelaySec --------------------------------------------------------- - -Run the maintenance job not exactly at the time specified with onCalendar, but plus or minus this many seconds. - -- type: ``signed integer`` -- default: ``1000`` - - - -mailserver.fullTextSearch.maxSize ---------------------------------- - -Size of the largest n-gram to index. - -- type: ``signed integer`` -- default: ``20`` - - - -mailserver.fullTextSearch.memoryLimit -------------------------------------- - -Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit. - -- type: ``null or signed integer`` -- default: ``None`` -- example: ``2000`` - - -mailserver.fullTextSearch.minSize ---------------------------------- - -Size of the smallest n-gram to index. - -- type: ``signed integer`` -- default: ``2`` - - -mailserver.redis -~~~~~~~~~~~~~~~~ - - -mailserver.redis.address ------------------------- - -Address that rspamd should use to contact redis. - - -- type: ``string`` -- default: computed from - - - -mailserver.redis.password -------------------------- - -Password that rspamd should use to contact redis, or null if not required. - - -- type: ``null or string`` -- default: ``config.services.redis.servers.rspamd.requirePass`` - - - -mailserver.redis.port ---------------------- - -Port that rspamd should use to contact redis. - - -- type: ``16 bit unsigned integer; between 0 and 65535 (both inclusive)`` -- default: ``config.services.redis.servers.rspamd.port`` - - -mailserver.monitoring -~~~~~~~~~~~~~~~~~~~~~ - - -mailserver.monitoring.alertAddress ----------------------------------- - -The email address to send alerts to. - - -- type: ``string`` - - - - -mailserver.monitoring.config ----------------------------- - -The configuration used for monitoring via monit. -Use a mail address that you actively check and set it via 'set alert ...'. - - -- type: ``string`` -- default: see source - - - -mailserver.monitoring.enable ----------------------------- - -Whether to enable monitoring via monit. - -- type: ``boolean`` -- default: ``False`` -- example: ``True`` - -mailserver.backup -~~~~~~~~~~~~~~~~~ - - -mailserver.backup.cmdPostexec ------------------------------ - -The command to be executed after each backup operation. This is wrapped in a shell script to be called by rsnapshot. - -- type: ``null or string`` -- default: ``None`` - - - -mailserver.backup.cmdPreexec ----------------------------- - -The command to be executed before each backup operation. This is wrapped in a shell script to be called by rsnapshot. - - -- type: ``null or string`` -- default: ``None`` - - - -mailserver.backup.cronIntervals -------------------------------- - -Periodicity at which intervals should be run by cron. -Note that the intervals also have to exist in configuration -as retain options. - - -- type: ``attribute set of string`` -- default: ``{'daily': '30 3 * * *', 'hourly': ' 0 * * * *', 'weekly': ' 0 5 * * 0'}`` - - - -mailserver.backup.enable ------------------------- - -Whether to enable backup via rsnapshot. - -- type: ``boolean`` -- default: ``False`` -- example: ``True`` - - -mailserver.backup.retain.daily ------------------------------- - -How many daily snapshots are retained. - -- type: ``signed integer`` -- default: ``7`` - - - -mailserver.backup.retain.hourly -------------------------------- - -How many hourly snapshots are retained. - -- type: ``signed integer`` -- default: ``24`` - - - -mailserver.backup.retain.weekly -------------------------------- - -How many weekly snapshots are retained. - -- type: ``signed integer`` -- default: ``54`` - - - -mailserver.backup.snapshotRoot ------------------------------- - -The directory where rsnapshot stores the backup. - - -- type: ``path`` -- default: ``/var/rsnapshot`` - - -mailserver.borg -~~~~~~~~~~~~~~~ - - -mailserver.borgbackup.cmdPostexec ---------------------------------- - -The command to be executed after each backup operation. -This is called after borg create completed successfully and in the same script that runs -cmdPreexec, borg init and create. - - -- type: ``null or string`` -- default: ``None`` - - - -mailserver.borgbackup.cmdPreexec --------------------------------- - -The command to be executed before each backup operation. -This is called prior to borg init in the same script that runs borg init and create and cmdPostexec. -Example: - export BORG_RSH="ssh -i /path/to/private/key" - - -- type: ``null or string`` -- default: ``None`` - - - -mailserver.borgbackup.compression.auto --------------------------------------- - -Leaves it to borg to determine whether an individual file should be compressed. - -- type: ``boolean`` -- default: ``False`` - - - -mailserver.borgbackup.compression.level ---------------------------------------- - -Denotes the level of compression used by borg. -Most methods accept levels from 0 to 9 but zstd which accepts values from 1 to 22. -If null the decision is left up to borg. - - -- type: ``null or signed integer`` -- default: ``None`` - - - -mailserver.borgbackup.compression.method ----------------------------------------- - -Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4. - -- type: ``null or one of "none", "lz4", "zstd", "zlib", "lzma"`` -- default: ``None`` - - - -mailserver.borgbackup.enable ----------------------------- - -Whether to enable backup via borgbackup. - -- type: ``boolean`` -- default: ``False`` -- example: ``True`` - - -mailserver.borgbackup.encryption.method ---------------------------------------- - -The backup can be encrypted by choosing any other value than 'none'. -When using encryption the password / passphrase must be provided in passphraseFile. - - -- type: ``one of "none", "authenticated", "authenticated-blake2", "repokey", "keyfile", "repokey-blake2", "keyfile-blake2"`` -- default: ``none`` - - - -mailserver.borgbackup.encryption.passphraseFile ------------------------------------------------ - -Path to a file containing the encryption password or passphrase. - -- type: ``null or path`` -- default: ``None`` - - - -mailserver.borgbackup.extraArgumentsForCreate ---------------------------------------------- - -Additional arguments to add to the borg create command line e.g. '--stats'. - -- type: ``list of string`` -- default: ``[]`` - - - -mailserver.borgbackup.extraArgumentsForInit -------------------------------------------- - -Additional arguments to add to the borg init command line. - -- type: ``list of string`` -- default: ``['--critical']`` - - - -mailserver.borgbackup.group ---------------------------- - -The group borg and its launch script is run as. - -- type: ``string`` -- default: ``virtualMail`` - - - -mailserver.borgbackup.locations -------------------------------- - -The locations that are to be backed up by borg. - -- type: ``list of path`` -- default: ``['/var/vmail']`` - - - -mailserver.borgbackup.name --------------------------- - -The name of the individual backups as used by borg. -Certain placeholders will be replaced by borg. - - -- type: ``string`` -- default: ``{hostname}-{user}-{now}`` - - - -mailserver.borgbackup.repoLocation ----------------------------------- - -The location where borg saves the backups. -This can be a local path or a remote location such as user@host:/path/to/repo. -It is exported and thus available as an environment variable to cmdPreexec and cmdPostexec. - - -- type: ``string`` -- default: ``/var/borgbackup`` - - - -mailserver.borgbackup.startAt ------------------------------ - -When or how often the backup should run. Must be in the format described in systemd.time 7. - -- type: ``string`` -- default: ``hourly`` - - - -mailserver.borgbackup.user --------------------------- - -The user borg and its launch script is run as. - -- type: ``string`` -- default: ``virtualMail`` - - diff --git a/docs/requirements.txt b/docs/requirements.txt index 1b9a4f8..2211dd5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,4 @@ -sphinx==4.0.2 -sphinx_rtd_theme==0.5.2 +sphinx ~= 5.3 +sphinx_rtd_theme ~= 1.1 +myst-parser ~= 0.18 +linkify-it-py ~= 2.0 diff --git a/flake.lock b/flake.lock index 0f7f71a..4b4474d 100644 --- a/flake.lock +++ b/flake.lock @@ -16,6 +16,22 @@ "type": "gitlab" } }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1668681692, + "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "009399224d5e398d03b22badca40a37ac85412a1", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1669542132, @@ -49,6 +65,7 @@ "root": { "inputs": { "blobs": "blobs", + "flake-compat": "flake-compat", "nixpkgs": "nixpkgs", "nixpkgs-22_11": "nixpkgs-22_11", "utils": "utils" diff --git a/flake.nix b/flake.nix index 4b5c8e6..9a46ea3 100644 --- a/flake.nix +++ b/flake.nix @@ -2,6 +2,10 @@ description = "A complete and Simple Nixos Mailserver"; inputs = { + flake-compat = { + url = "github:edolstra/flake-compat"; + flake = false; + }; utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; nixpkgs-22_11.url = "flake:nixpkgs/nixos-22.11"; @@ -11,7 +15,8 @@ }; }; - outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_11 }: let + outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_11, ... }: let + lib = nixpkgs.lib; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; releases = [ @@ -43,22 +48,18 @@ # external-21_05 = ; # ... # } - allTests = pkgs.lib.listToAttrs ( - pkgs.lib.flatten (map (t: map (r: genTest t r) releases) testNames)); + allTests = lib.listToAttrs ( + lib.flatten (map (t: map (r: genTest t r) releases) testNames)); mailserverModule = import ./.; - # Generate a rst file describing options of the NixOS mailserver module - generateRstOptions = let - eval = import (pkgs.path + "/nixos/lib/eval-config.nix") { - inherit system; + # Generate a MarkDown file describing the options of the NixOS mailserver module + optionsDoc = let + eval = lib.evalModules { modules = [ mailserverModule { - # Because the blockbook package is currently broken (we - # don't care about this package but it is part of the - # NixOS module evaluation) - nixpkgs.config.allowBroken = true; + _module.check = false; mailserver = { fqdn = "mx.example.com"; domains = [ @@ -71,27 +72,26 @@ }; } ]; - }; - options = pkgs.nixosOptionsDoc { - options = eval.options; - }; - in pkgs.runCommand "options.rst" { buildInputs = [pkgs.python3]; } '' - echo Generating options.rst from ${options.optionsJSON}/share/doc/nixos/options.json - python ${./scripts/generate-rst-options.py} ${options.optionsJSON}/share/doc/nixos/options.json > $out + 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 ''; # This is a script helping users to generate this file in the docs directory - generateRstOptionsScript = pkgs.writeScriptBin "generate-rst-options" '' - cp -v ${generateRstOptions} ./docs/options.rst + generateOptions = pkgs.writeShellScriptBin "generate-options" '' + install -vm644 ${optionsDoc} ./docs/options.md ''; - # This is to ensure we don't forget to update the options.rst file - testRstOptions = pkgs.runCommand "test-rst-options" {} '' - if ! diff -q ${./docs/options.rst} ${generateRstOptions} + # This is to ensure we don't forget to update the options.md file + testOptions = pkgs.runCommand "test-options" {} '' + if ! diff -q ${./docs/options.md} ${optionsDoc} then - echo "The file ./docs/options.rst is not up-to-date and needs to be regenerated!" - echo " hint: run 'nix-shell --run generate-rst-options' to generate this file" + echo "The file ./docs/options.md is not up-to-date and needs to be regenerated!" + echo " hint: run 'nix-shell --run generate-options' to generate this file" exit 1 fi echo "test: ok" > $out @@ -99,43 +99,43 @@ documentation = pkgs.stdenv.mkDerivation { name = "documentation"; - src = pkgs.lib.sourceByRegex ./docs ["logo.png" "conf.py" "Makefile" ".*rst$"]; + src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"]; buildInputs = [( - pkgs.python3.withPackages(p: [ - p.sphinx - p.sphinx_rtd_theme + pkgs.python3.withPackages (p: with p; [ + sphinx + sphinx_rtd_theme + myst-parser ]) )]; buildPhase = '' - cp ${generateRstOptions} options.rst - mkdir -p _static + cp ${optionsDoc} options.md # Workaround for https://github.com/sphinx-doc/sphinx/issues/3451 - export SOURCE_DATE_EPOCH=$(${pkgs.coreutils}/bin/date +%s) + unset SOURCE_DATE_EPOCH make html ''; installPhase = '' - cp -r _build/html $out + cp -Tr _build/html $out ''; }; - in rec { - nixosModules.mailserver = mailserverModule ; - nixosModule = self.nixosModules.mailserver; + in { + nixosModules = rec { + mailserver = mailserverModule; + default = mailserver; + }; + nixosModule = self.nixosModules.default; # compatibility hydraJobs.${system} = allTests // { - test-rst-options = testRstOptions; + test-options = testOptions; inherit documentation; }; checks.${system} = allTests; - devShell.${system} = pkgs.mkShell { - buildInputs = with pkgs; [ - generateRstOptionsScript - (python3.withPackages (p: with p; [ - sphinx - sphinx_rtd_theme - ])) - jq + devShells.${system}.default = pkgs.mkShell { + inputsFrom = [ documentation ]; + packages = with pkgs; [ + generateOptions clamav ]; }; + devShell.${system} = self.devShells.${system}.default; # compatibility }; } diff --git a/scripts/generate-options.py b/scripts/generate-options.py new file mode 100644 index 0000000..a4973b1 --- /dev/null +++ b/scripts/generate-options.py @@ -0,0 +1,81 @@ +import json +import sys + +header = """ +# Mailserver options + +## `mailserver` + +""" + +template = """ +`````{{option}} {key} +{description} + +{type} +{default} +{example} +````` +""" + +f = open(sys.argv[1]) +options = json.load(f) + +groups = ["mailserver.loginAccounts", + "mailserver.certificate", + "mailserver.dkim", + "mailserver.dmarcReporting", + "mailserver.fullTextSearch", + "mailserver.redis", + "mailserver.monitoring", + "mailserver.backup", + "mailserver.borgbackup"] + +def render_option_value(opt, attr): + if attr in opt: + if isinstance(opt[attr], dict) and '_type' in opt[attr]: + if opt[attr]['_type'] == 'literalExpression': + if '\n' in opt[attr]['text']: + res = '\n```nix\n' + opt[attr]['text'].rstrip('\n') + '\n```' + else: + res = '```{}```'.format(opt[attr]['text']) + elif opt[attr]['_type'] == 'literalMD': + res = opt[attr]['text'] + else: + s = str(opt[attr]) + if s == "": + res = '`""`' + elif '\n' in s: + res = '\n```\n' + s.rstrip('\n') + '\n```' + else: + res = '```{}```'.format(s) + res = '- ' + attr + ': ' + res + else: + res = "" + return res + +def print_option(opt): + if isinstance(opt['description'], dict) and '_type' in opt['description']: # mdDoc + description = opt['description']['text'] + else: + description = opt['description'] + print(template.format( + key=opt['name'], + description=description or "", + type="- type: ```{}```".format(opt['type']), + default=render_option_value(opt, 'default'), + example=render_option_value(opt, 'example'))) + + +print(header) +for opt in options: + if any([opt['name'].startswith(c) for c in groups]): + continue + print_option(opt) + +for c in groups: + print('## `{}`'.format(c)) + print() + for opt in options: + if opt['name'].startswith(c): + print_option(opt) diff --git a/scripts/generate-rst-options.py b/scripts/generate-rst-options.py deleted file mode 100644 index 8c5ce9c..0000000 --- a/scripts/generate-rst-options.py +++ /dev/null @@ -1,87 +0,0 @@ -import json -import sys -import re -import textwrap - -header = """ -Mailserver Options -================== - -mailserver -~~~~~~~~~~ - -""" - -template = """ -{key} -{line} - -{description} - -{type} -{default} -{example} -""" - -f = open(sys.argv[1]) -options = json.load(f) - -options = {k: v for k, v in options.items() - if k.startswith("mailserver.")} - -groups = ["mailserver.loginAccount", - "mailserver.certificate", - "mailserver.dkim", - "mailserver.dmarcReporting", - "mailserver.fullTextSearch", - "mailserver.redis", - "mailserver.monitoring", - "mailserver.backup", - "mailserver.borg"] - -def render_option_value(opt, attr): - if attr in opt: - if isinstance(opt[attr], dict) and '_type' in opt[attr]: - if opt[attr]['_type'] == 'literalExpression': - if '\n' in opt[attr]['text']: - res = '\n.. code:: nix\n\n' + textwrap.indent(opt[attr]['text'], ' ') + '\n' - else: - res = '``{}``'.format(opt[attr]['text']) - elif opt[attr]['_type'] == 'literalDocBook': - res = opt[attr]['text'] - else: - s = str(opt[attr]) - if s == "": - res = '``""``' - elif '\n' in s: - res = '\n.. code::\n\n' + textwrap.indent(s, ' ') + '\n' - else: - res = '``{}``'.format(s) - res = '- ' + attr + ': ' + res - else: - res = "" - return res - -def print_option(name, value): - print(template.format( - key=name, - line="-"*len(name), - description=value['description'] or "", - type="- type: ``{}``".format(value['type']), - default=render_option_value(value, 'default'), - example=render_option_value(value, 'example'))) - - -print(header) -for k, v in options.items(): - if any([k.startswith(c) for c in groups]): - continue - print_option(k, v) - -for c in groups: - print(c) - print("~"*len(c)) - print() - for k, v in options.items(): - if k.startswith(c): - print_option(k, v) diff --git a/shell.nix b/shell.nix index d32886e..6234bb4 100644 --- a/shell.nix +++ b/shell.nix @@ -1 +1,10 @@ -(import (builtins.fetchGit "https://github.com/edolstra/flake-compat") { 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 From 0bbb2ac74e8a1f968d44dcfa163926c9b866a3b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Thu, 1 Dec 2022 14:22:27 +0100 Subject: [PATCH 061/225] docs: drop options.md from the repository Generate the file on the readthedocs builder using Nix. Since there is no root access or user namespaces, we have to use proot (see https://nixos.wiki/wiki/Nix_Installation_Guide#PRoot). --- .readthedocs.yaml | 8 + docs/howto-develop.rst | 26 +- docs/options.md | 1202 ---------------------------------------- flake.nix | 21 +- 4 files changed, 20 insertions(+), 1237 deletions(-) delete mode 100644 docs/options.md diff --git a/.readthedocs.yaml b/.readthedocs.yaml index eb6988e..99a66d4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -8,6 +8,14 @@ build: os: ubuntu-22.04 tools: python: "3" + apt_packages: + - nix + - proot + jobs: + pre_install: + - mkdir -p ~/.nix ~/.config/nix + - echo "experimental-features = nix-command flakes" > ~/.config/nix/nix.conf + - proot -b ~/.nix:/nix /bin/sh -c "nix build -L .#optionsDoc && cp -v result docs/options.md" sphinx: configuration: docs/conf.py diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index 0ac9009..ded90b9 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -10,7 +10,7 @@ Run NixOS tests --------------- To run the test suite, you need to enable `Nix Flakes -`. +`_. You can then run the testsuite via @@ -30,28 +30,20 @@ run tests manually. For instance: Contributing to the documentation --------------------------------- -The documentation is written in RST (except option documentation which is in MarkDown), +The documentation is written in RST (except option documentation which is in CommonMark), built with Sphinx and published by `Read the Docs `_. -For the syntax, see `RST/Sphinx Cheatsheet -`_. +For the syntax, see the `RST/Sphinx primer +`_. + +To build the documentation, you need to enable `Nix Flakes +`_. -The ``shell.nix`` provides all the tooling required to build the -documentation: :: - $ nix-shell - $ cd docs - $ make html - $ firefox ./_build/html/index.html - -Note if you modify some NixOS mailserver options, you would also need -to regenerate the ``options.md`` file: - -:: - - $ nix-shell --run generate-options + $ nix build .#documentation + $ xdg-open result/index.html Nixops ------ diff --git a/docs/options.md b/docs/options.md deleted file mode 100644 index 0944fa7..0000000 --- a/docs/options.md +++ /dev/null @@ -1,1202 +0,0 @@ - -# Mailserver options - -## `mailserver` - - - -`````{option} mailserver.debug -Whether to enable verbose logging for mailserver related services. This -intended be used for development purposes only, you probably don't want -to enable this unless you're hacking on nixos-mailserver. - - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.domains -The domains that this mail server serves. - -- type: ```list of string``` -- default: ```[]``` -- example: ```['example.com']``` -````` - - -`````{option} mailserver.enable -Whether to enable nixos-mailserver. - -- type: ```boolean``` -- default: ```False``` -- example: ```True``` -````` - - -`````{option} mailserver.enableImap -Whether to enable IMAP with STARTTLS on port 143. - - -- type: ```boolean``` -- default: ```True``` - -````` - - -`````{option} mailserver.enableImapSsl -Whether to enable IMAP with TLS in wrapper-mode on port 993. - - -- type: ```boolean``` -- default: ```True``` - -````` - - -`````{option} mailserver.enableManageSieve -Whether to enable ManageSieve, setting this option to true will open -port 4190 in the firewall. - -The ManageSieve protocol allows users to manage their Sieve scripts on -a remote server with a supported client, including Thunderbird. - - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.enablePop3 -Whether to enable POP3 with STARTTLS on port on port 110. - - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.enablePop3Ssl -Whether to enable POP3 with TLS in wrapper-mode on port 995. - - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.enableSubmission -Whether to enable SMTP with STARTTLS on port 587. - - -- type: ```boolean``` -- default: ```True``` - -````` - - -`````{option} mailserver.enableSubmissionSsl -Whether to enable SMTP with TLS in wrapper-mode on port 465. - - -- type: ```boolean``` -- default: ```True``` - -````` - - -`````{option} mailserver.extraVirtualAliases -Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that -all mail to `info@example.com` is forwarded to `user1@example.com`. Note -that it is expected that `postmaster@example.com` and `abuse@example.com` is -forwarded to some valid email address. (Alternatively you can create login -accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows -the user `user1@example.com` to send emails as `info@example.com`. -It's also possible to create an alias for multiple accounts. In this -example all mails for `multi@example.com` will be forwarded to both -`user1@example.com` and `user2@example.com`. - - -- type: ```attribute set of ((Login Account) or non-empty (list of (Login Account)))``` -- default: ```{}``` -- example: ```{'abuse@example.com': 'user1@example.com', 'info@example.com': 'user1@example.com', 'multi@example.com': ['user1@example.com', 'user2@example.com'], 'postmaster@example.com': 'user1@example.com'}``` -````` - - -`````{option} mailserver.forwards -To forward mails to an external address. For instance, -the value {`"user@example.com" = "user@elsewhere.com";}` -means that mails to `user@example.com` are forwarded to -`user@elsewhere.com`. The difference with the -{option}`mailserver.extraVirtualAliases` option is that `user@elsewhere.com` -can't send mail as `user@example.com`. Also, this option -allows to forward mails to external addresses. - - -- type: ```attribute set of ((list of string) or string)``` -- default: ```{}``` -- example: ```{'user@example.com': 'user@elsewhere.com'}``` -````` - - -`````{option} mailserver.fqdn -The fully qualified domain name of the mail server. - -- type: ```string``` - -- example: ```mx.example.com``` -````` - - -`````{option} mailserver.hierarchySeparator -The hierarchy separator for mailboxes used by dovecot for the namespace 'inbox'. -Dovecot defaults to "." but recommends "/". -This affects how mailboxes appear to mail clients and sieve scripts. -For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example". -This does not determine the way your mails are stored on disk. -See https://wiki.dovecot.org/Namespaces for details. - - -- type: ```string``` -- default: ```.``` - -````` - - -`````{option} mailserver.indexDir -Folder to store search indices. If null, indices are stored -along with email, which could not necessarily be desirable, -especially when {option}`mailserver.fullTextSearch.enable` is `true` since -indices it creates are voluminous and do not need to be backed -up. - -Be careful when changing this option value since all indices -would be recreated at the new location (and clients would need -to resynchronize). - -Note the some variables can be used in the file path. See -https://doc.dovecot.org/configuration_manual/mail_location/#variables -for details. - - -- type: ```null or string``` -- default: ```None``` -- example: ```/var/lib/dovecot/indices``` -````` - - -`````{option} mailserver.keyFile -Scheme 1) -Location of the key file - - -- type: ```path``` - -- example: ```/root/mail-server.key``` -````` - - -`````{option} mailserver.lmtpSaveToDetailMailbox -If an email address is delimited by a "+", should it be filed into a -mailbox matching the string after the "+"? For example, -user1+test@example.com would be filed into the mailbox "test". - - -- type: ```one of "yes", "no"``` -- default: ```yes``` - -````` - - -`````{option} mailserver.localDnsResolver -Runs a local DNS resolver (kresd) as recommended when running rspamd. This prevents your log file from filling up with rspamd_monitored_dns_mon entries. - - -- type: ```boolean``` -- default: ```True``` - -````` - - -`````{option} mailserver.mailDirectory -Where to store the mail. - - -- type: ```path``` -- default: ```/var/vmail``` - -````` - - -`````{option} mailserver.mailboxes -The mailboxes for dovecot. -Depending on the mail client used it might be necessary to change some mailbox's name. - - -- type: ```unspecified value``` -- default: ```{'Drafts': {'auto': 'subscribe', 'specialUse': 'Drafts'}, 'Junk': {'auto': 'subscribe', 'specialUse': 'Junk'}, 'Sent': {'auto': 'subscribe', 'specialUse': 'Sent'}, 'Trash': {'auto': 'no', 'specialUse': 'Trash'}}``` - -````` - - -`````{option} mailserver.maxConnectionsPerUser -Maximum number of IMAP/POP3 connections allowed for a user from each IP address. -E.g. a value of 50 allows for 50 IMAP and 50 POP3 connections at the same -time for a single user. - - -- type: ```signed integer``` -- default: ```100``` - -````` - - -`````{option} mailserver.messageSizeLimit -Message size limit enforced by Postfix. - -- type: ```signed integer``` -- default: ```20971520``` -- example: ```52428800``` -````` - - -`````{option} mailserver.openFirewall -Automatically open ports in the firewall. - -- type: ```boolean``` -- default: ```True``` - -````` - - -`````{option} mailserver.policydSPFExtraConfig -Extra configuration options for policyd-spf. This can be use to among -other things skip spf checking for some IP addresses. - - -- type: ```strings concatenated with "\n"``` -- default: `""` -- example: -``` -skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 -``` -````` - - -`````{option} mailserver.rebootAfterKernelUpgrade.enable -Whether to enable automatic reboot after kernel upgrades. -This is to be used in conjunction with `system.autoUpgrade.enable = true;` - - -- type: ```boolean``` -- default: ```False``` -- example: ```True``` -````` - - -`````{option} mailserver.rebootAfterKernelUpgrade.method -Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot. -It is recommended to use the default value because the quicker kexec reboot has a number of problems. -Also if your server is running in a virtual machine the regular reboot will already be very quick. - - -- type: ```one of "reboot", "systemctl kexec"``` -- default: ```reboot``` - -````` - - -`````{option} mailserver.recipientDelimiter -Configure the recipient delimiter. - - -- type: ```string``` -- default: ```+``` - -````` - - -`````{option} mailserver.rejectRecipients -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. - - -- type: ```list of string``` -- default: ```[]``` -- example: ```['sales@example.com', 'info@example.com']``` -````` - - -`````{option} mailserver.rejectSender -Reject emails from these addresses from unauthorized senders. -Use if a spammer is using the same domain or the same sender over and over. - - -- type: ```list of string``` -- default: ```[]``` -- example: ```['@example.com', 'spammer@example.net']``` -````` - - -`````{option} mailserver.rewriteMessageId -Rewrites the Message-ID's hostname-part of outgoing emails to the FQDN. -Please be aware that this may cause problems with some mail clients -relying on the original Message-ID. - - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.sendingFqdn -The fully qualified domain name of the mail server used to -identify with remote servers. - -If this server's IP serves purposes other than a mail server, -it may be desirable for the server to have a name other than -that to which the user will connect. For example, the user -might connect to mx.example.com, but the server's IP has -reverse DNS that resolves to myserver.example.com; in this -scenario, some mail servers may reject or penalize the -message. - -This setting allows the server to identify as -myserver.example.com when forwarding mail, independently of -{option}`mailserver.fqdn` (which, for SSL reasons, should generally be the name -to which the user connects). - -Set this to the name to which the sending IP's reverse DNS -resolves. - - -- type: ```string``` -- default: {option}`mailserver.fqdn` -- example: ```myserver.example.com``` -````` - - -`````{option} mailserver.sieveDirectory -Where to store the sieve scripts. - - -- type: ```path``` -- default: ```/var/sieve``` - -````` - - -`````{option} mailserver.useFsLayout -Sets whether dovecot should organize mail in subdirectories: - -- /var/vmail/example.com/user/.folder.subfolder/ (default layout) -- /var/vmail/example.com/user/folder/subfolder/ (FS layout) - -See https://wiki2.dovecot.org/MailboxFormat/Maildir for details. - - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.virusScanning -Whether to activate virus scanning. Note that virus scanning is _very_ -expensive memory wise. - - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.vmailGroupName -The user name and group name of the user that owns the directory where all -the mail is stored. - - -- type: ```string``` -- default: ```virtualMail``` - -````` - - -`````{option} mailserver.vmailUID -The unix UID of the virtual mail user. Be mindful that if this is -changed, you will need to manually adjust the permissions of -`mailDirectory`. - - -- type: ```signed integer``` -- default: ```5000``` - -````` - - -`````{option} mailserver.vmailUserName -The user name and group name of the user that owns the directory where all -the mail is stored. - - -- type: ```string``` -- default: ```virtualMail``` - -````` - -## `mailserver.loginAccounts` - - -`````{option} mailserver.loginAccounts -The login account of the domain. Every account is mapped to a unix user, -e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as -follows - -``` -nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' -``` - - -- type: ```attribute set of (submodule)``` -- default: ```{}``` -- example: ```{'user1': {'hashedPassword': '$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/'}, 'user2': {'hashedPassword': '$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/'}}``` -````` - - -`````{option} mailserver.loginAccounts..aliases -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. - - -- type: ```list of string``` -- default: ```[]``` -- example: ```['abuse@example.com', 'postmaster@example.com']``` -````` - - -`````{option} mailserver.loginAccounts..catchAll -For which domains should this account act as a catch all? -Note: Does not allow sending from all addresses of these domains. - - -- type: ```list of value "example.com" (singular enum)``` -- default: ```[]``` -- example: ```['example.com', 'example2.com']``` -````` - - -`````{option} mailserver.loginAccounts..hashedPassword -The user's hashed password. Use `mkpasswd` as follows - -``` -nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' -``` - -Warning: this is stored in plaintext in the Nix store! -Use {option}`mailserver.loginAccounts..hashedPasswordFile` instead. - - -- type: ```null or string``` -- default: ```None``` -- example: ```$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/``` -````` - - -`````{option} mailserver.loginAccounts..hashedPasswordFile -A file containing the user's hashed password. Use `mkpasswd` as follows - -``` -nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' -``` - - -- type: ```null or path``` -- default: ```None``` -- example: ```/run/keys/user1-passwordhash``` -````` - - -`````{option} mailserver.loginAccounts..name -Username - -- type: ```string``` - -- example: ```user1@example.com``` -````` - - -`````{option} mailserver.loginAccounts..quota -Per user quota rules. Accepted sizes are `xx k/M/G/T` with the -obvious meaning. Leave blank for the standard quota `100G`. - - -- type: ```null or string``` -- default: ```None``` -- example: ```2G``` -````` - - -`````{option} mailserver.loginAccounts..sendOnly -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. - - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.loginAccounts..sendOnlyRejectMessage -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. - - -- type: ```string``` -- default: ```This account cannot receive emails.``` - -````` - - -`````{option} mailserver.loginAccounts..sieveScript -Per-user sieve script. - - -- type: ```null or strings concatenated with "\n"``` -- default: ```None``` -- example: -``` -require ["fileinto", "mailbox"]; - -if address :is "from" "gitlab@mg.gitlab.com" { - fileinto :create "GitLab"; - stop; -} - -# This must be the last rule, it will check if list-id is set, and -# file the message into the Lists folder for further investigation -elsif header :matches "list-id" "" { - fileinto :create "Lists"; - stop; -} -``` -````` - -## `mailserver.certificate` - - -`````{option} mailserver.certificateDirectory -Scheme 2) -This is the folder where the certificate will be created. The name is -hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the -certificate is valid for 10 years. - - -- type: ```path``` -- default: ```/var/certs``` - -````` - - -`````{option} mailserver.certificateDomains -Secondary domains and subdomains for which it is necessary to generate a certificate. - -- type: ```list of string``` -- default: ```[]``` -- example: ```['imap.example.com', 'pop3.example.com']``` -````` - - -`````{option} mailserver.certificateFile -Scheme 1) -Location of the certificate - - -- type: ```path``` - -- example: ```/root/mail-server.crt``` -````` - - -`````{option} mailserver.certificateScheme -Certificate Files. There are three options for these. - -1) You specify locations and manually copy certificates there. -2) You let the server create new (self signed) certificates on the fly. -3) You let the server create a certificate via `Let's Encrypt`. Note that - this implies that a stripped down webserver has to be started. This also - implies that the FQDN must be set as an `A` record to point to the IP of - the server. In particular port 80 on the server will be opened. For details - on how to set up the domain records, see the guide in the readme. - - -- type: ```one of 1, 2, 3``` -- default: ```2``` - -````` - -## `mailserver.dkim` - - -`````{option} mailserver.dkimBodyCanonicalization -DKIM canonicalization algorithm for message bodies. - -See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. - - -- type: ```one of "relaxed", "simple"``` -- default: ```relaxed``` - -````` - - -`````{option} mailserver.dkimHeaderCanonicalization -DKIM canonicalization algorithm for message headers. - -See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. - - -- type: ```one of "relaxed", "simple"``` -- default: ```relaxed``` - -````` - - -`````{option} mailserver.dkimKeyBits -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. - - -- type: ```signed integer``` -- default: ```1024``` - -````` - - -`````{option} mailserver.dkimKeyDirectory -The DKIM directory. - - -- type: ```path``` -- default: ```/var/dkim``` - -````` - - -`````{option} mailserver.dkimSelector -The DKIM selector. - - -- type: ```string``` -- default: ```mail``` - -````` - - -`````{option} mailserver.dkimSigning -Whether to activate dkim signing. - - -- type: ```boolean``` -- default: ```True``` - -````` - -## `mailserver.dmarcReporting` - - -`````{option} mailserver.dmarcReporting.domain -The domain from which outgoing DMARC reports are served. - - -- type: ```value "example.com" (singular enum)``` - -- example: ```example.com``` -````` - - -`````{option} mailserver.dmarcReporting.email -The email address used for outgoing DMARC reports. Read-only. - - -- type: ```string``` -- default: ```"${localpart}@${domain}"``` - -````` - - -`````{option} mailserver.dmarcReporting.enable -Whether to send out aggregated, daily DMARC reports in response to incoming -mail, when the sender domain defines a DMARC policy including the RUA tag. - -This is helpful for the mail ecosystem, because it allows third parties to -get notified about SPF/DKIM violations originating from their sender domains. - -See https://rspamd.com/doc/modules/dmarc.html#reporting - - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.dmarcReporting.fromName -The sender name for DMARC reports. Defaults to the organization name. - - -- type: ```string``` -- default: {option}`mailserver.dmarcReporting.organizationName` - -````` - - -`````{option} mailserver.dmarcReporting.localpart -The local part of the email address used for outgoing DMARC reports. - - -- type: ```string``` -- default: ```dmarc-noreply``` -- example: ```dmarc-report``` -````` - - -`````{option} mailserver.dmarcReporting.organizationName -The name of your organization used in the `org_name` attribute in -DMARC reports. - - -- type: ```string``` - -- example: ```ACME Corp.``` -````` - -## `mailserver.fullTextSearch` - - -`````{option} mailserver.fullTextSearch.autoIndex -Enable automatic indexing of messages as they are received or modified. - -- type: ```boolean``` -- default: ```True``` - -````` - - -`````{option} mailserver.fullTextSearch.autoIndexExclude -Mailboxes to exclude from automatic indexing. - - -- type: ```list of string``` -- default: ```[]``` -- example: ```['\\Trash', 'SomeFolder', 'Other/*']``` -````` - - -`````{option} mailserver.fullTextSearch.enable -Whether to enable Full text search indexing with xapian. This has significant performance and disk space cost.. - -- type: ```boolean``` -- default: ```False``` -- example: ```True``` -````` - - -`````{option} mailserver.fullTextSearch.enforced -Fail searches when no index is available. If set to -`body`, then only body searches (as opposed to -header) are affected. If set to `no`, searches may -fall back to a very slow brute force search. - - -- type: ```one of "yes", "no", "body"``` -- default: ```no``` - -````` - - -`````{option} mailserver.fullTextSearch.indexAttachments -Also index text-only attachements. Binary attachements are never indexed. - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.fullTextSearch.maintenance.enable -Regularly optmize indices, as recommended by upstream. - -- type: ```boolean``` -- default: ```True``` - -````` - - -`````{option} mailserver.fullTextSearch.maintenance.onCalendar -When to run the maintenance job. See systemd.time(7) for more information about the format. - -- type: ```string``` -- default: ```daily``` - -````` - - -`````{option} mailserver.fullTextSearch.maintenance.randomizedDelaySec -Run the maintenance job not exactly at the time specified with `onCalendar`, but plus or minus this many seconds. - -- type: ```signed integer``` -- default: ```1000``` - -````` - - -`````{option} mailserver.fullTextSearch.maxSize -Size of the largest n-gram to index. - -- type: ```signed integer``` -- default: ```20``` - -````` - - -`````{option} mailserver.fullTextSearch.memoryLimit -Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit. - -- type: ```null or signed integer``` -- default: ```None``` -- example: ```2000``` -````` - - -`````{option} mailserver.fullTextSearch.minSize -Size of the smallest n-gram to index. - -- type: ```signed integer``` -- default: ```2``` - -````` - -## `mailserver.redis` - - -`````{option} mailserver.redis.address -Address that rspamd should use to contact redis. - - -- type: ```string``` -- default: computed from `config.services.redis.servers.rspamd.bind` - -````` - - -`````{option} mailserver.redis.password -Password that rspamd should use to contact redis, or null if not required. - - -- type: ```null or string``` -- default: ```config.services.redis.servers.rspamd.requirePass``` - -````` - - -`````{option} mailserver.redis.port -Port that rspamd should use to contact redis. - - -- type: ```16 bit unsigned integer; between 0 and 65535 (both inclusive)``` -- default: ```config.services.redis.servers.rspamd.port``` - -````` - -## `mailserver.monitoring` - - -`````{option} mailserver.monitoring.alertAddress -The email address to send alerts to. - - -- type: ```string``` - - -````` - - -`````{option} mailserver.monitoring.config -The configuration used for monitoring via monit. -Use a mail address that you actively check and set it via 'set alert ...'. - - -- type: ```string``` -- default: see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix) - -````` - - -`````{option} mailserver.monitoring.enable -Whether to enable monitoring via monit. - -- type: ```boolean``` -- default: ```False``` -- example: ```True``` -````` - -## `mailserver.backup` - - -`````{option} mailserver.backup.cmdPostexec -The command to be executed after each backup operation. This is wrapped in a shell script to be called by rsnapshot. - -- type: ```null or string``` -- default: ```None``` - -````` - - -`````{option} mailserver.backup.cmdPreexec -The command to be executed before each backup operation. This is wrapped in a shell script to be called by rsnapshot. - - -- type: ```null or string``` -- default: ```None``` - -````` - - -`````{option} mailserver.backup.cronIntervals -Periodicity at which intervals should be run by cron. -Note that the intervals also have to exist in configuration -as retain options. - - -- type: ```attribute set of string``` -- default: ```{'daily': '30 3 * * *', 'hourly': ' 0 * * * *', 'weekly': ' 0 5 * * 0'}``` - -````` - - -`````{option} mailserver.backup.enable -Whether to enable backup via rsnapshot. - -- type: ```boolean``` -- default: ```False``` -- example: ```True``` -````` - - -`````{option} mailserver.backup.retain.daily -How many daily snapshots are retained. - -- type: ```signed integer``` -- default: ```7``` - -````` - - -`````{option} mailserver.backup.retain.hourly -How many hourly snapshots are retained. - -- type: ```signed integer``` -- default: ```24``` - -````` - - -`````{option} mailserver.backup.retain.weekly -How many weekly snapshots are retained. - -- type: ```signed integer``` -- default: ```54``` - -````` - - -`````{option} mailserver.backup.snapshotRoot -The directory where rsnapshot stores the backup. - - -- type: ```path``` -- default: ```/var/rsnapshot``` - -````` - -## `mailserver.borgbackup` - - -`````{option} mailserver.borgbackup.cmdPostexec -The command to be executed after each backup operation. -This is called after borg create completed successfully and in the same script that runs -`cmdPreexec`, borg init and create. - - -- type: ```null or string``` -- default: ```None``` - -````` - - -`````{option} mailserver.borgbackup.cmdPreexec -The command to be executed before each backup operation. -This is called prior to borg init in the same script that runs borg init and create and `cmdPostexec`. - - -- type: ```null or string``` -- default: ```None``` -- example: -``` -export BORG_RSH="ssh -i /path/to/private/key" -``` -````` - - -`````{option} mailserver.borgbackup.compression.auto -Leaves it to borg to determine whether an individual file should be compressed. - -- type: ```boolean``` -- default: ```False``` - -````` - - -`````{option} mailserver.borgbackup.compression.level -Denotes the level of compression used by borg. -Most methods accept levels from 0 to 9 but zstd which accepts values from 1 to 22. -If null the decision is left up to borg. - - -- type: ```null or signed integer``` -- default: ```None``` - -````` - - -`````{option} mailserver.borgbackup.compression.method -Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4. - -- type: ```null or one of "none", "lz4", "zstd", "zlib", "lzma"``` -- default: ```None``` - -````` - - -`````{option} mailserver.borgbackup.enable -Whether to enable backup via borgbackup. - -- type: ```boolean``` -- default: ```False``` -- example: ```True``` -````` - - -`````{option} mailserver.borgbackup.encryption.method -The backup can be encrypted by choosing any other value than 'none'. -When using encryption the password/passphrase must be provided in `passphraseFile`. - - -- type: ```one of "none", "authenticated", "authenticated-blake2", "repokey", "keyfile", "repokey-blake2", "keyfile-blake2"``` -- default: ```none``` - -````` - - -`````{option} mailserver.borgbackup.encryption.passphraseFile -Path to a file containing the encryption password or passphrase. - -- type: ```null or path``` -- default: ```None``` - -````` - - -`````{option} mailserver.borgbackup.extraArgumentsForCreate -Additional arguments to add to the borg create command line e.g. '--stats'. - -- type: ```list of string``` -- default: ```[]``` - -````` - - -`````{option} mailserver.borgbackup.extraArgumentsForInit -Additional arguments to add to the borg init command line. - -- type: ```list of string``` -- default: ```['--critical']``` - -````` - - -`````{option} mailserver.borgbackup.group -The group borg and its launch script is run as. - -- type: ```string``` -- default: ```virtualMail``` - -````` - - -`````{option} mailserver.borgbackup.locations -The locations that are to be backed up by borg. - -- type: ```list of path``` -- default: ```[ config.mailserver.mailDirectory ]``` - -````` - - -`````{option} mailserver.borgbackup.name -The name of the individual backups as used by borg. -Certain placeholders will be replaced by borg. - - -- type: ```string``` -- default: ```{hostname}-{user}-{now}``` - -````` - - -`````{option} mailserver.borgbackup.repoLocation -The location where borg saves the backups. -This can be a local path or a remote location such as user@host:/path/to/repo. -It is exported and thus available as an environment variable to -{option}`mailserver.borgbackup.cmdPreexec` and {option}`mailserver.borgbackup.cmdPostexec`. - - -- type: ```string``` -- default: ```/var/borgbackup``` - -````` - - -`````{option} mailserver.borgbackup.startAt -When or how often the backup should run. Must be in the format described in systemd.time 7. - -- type: ```string``` -- default: ```hourly``` - -````` - - -`````{option} mailserver.borgbackup.user -The user borg and its launch script is run as. - -- type: ```string``` -- default: ```virtualMail``` - -````` - diff --git a/flake.nix b/flake.nix index 9a46ea3..f002909 100644 --- a/flake.nix +++ b/flake.nix @@ -81,22 +81,6 @@ python ${./scripts/generate-options.py} ${options} > $out ''; - # This is a script helping users to generate this file in the docs directory - generateOptions = pkgs.writeShellScriptBin "generate-options" '' - install -vm644 ${optionsDoc} ./docs/options.md - ''; - - # This is to ensure we don't forget to update the options.md file - testOptions = pkgs.runCommand "test-options" {} '' - if ! diff -q ${./docs/options.md} ${optionsDoc} - then - echo "The file ./docs/options.md is not up-to-date and needs to be regenerated!" - echo " hint: run 'nix-shell --run generate-options' to generate this file" - exit 1 - fi - echo "test: ok" > $out - ''; - documentation = pkgs.stdenv.mkDerivation { name = "documentation"; src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"]; @@ -125,14 +109,15 @@ }; nixosModule = self.nixosModules.default; # compatibility hydraJobs.${system} = allTests // { - test-options = testOptions; inherit documentation; }; checks.${system} = allTests; + packages.${system} = { + inherit optionsDoc documentation; + }; devShells.${system}.default = pkgs.mkShell { inputsFrom = [ documentation ]; packages = with pkgs; [ - generateOptions clamav ]; }; From 6d0d9fb966cc565a3df74d3b686f924c7615118c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Sat, 10 Dec 2022 17:19:39 +0100 Subject: [PATCH 062/225] Update nixpkgs Option values are now rendered correctly as Nix thanks to https://github.com/NixOS/nixpkgs/pull/199363 --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 4b4474d..4c8b160 100644 --- a/flake.lock +++ b/flake.lock @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1669542132, - "narHash": "sha256-DRlg++NJAwPh8io3ExBJdNW7Djs3plVI5jgYQ+iXAZQ=", + "lastModified": 1670751203, + "narHash": "sha256-XdoH1v3shKDGlrwjgrNX/EN8s3c+kQV7xY6cLCE8vcI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a115bb9bd56831941be3776c8a94005867f316a7", + "rev": "64e0bf055f9d25928c31fb12924e59ff8ce71e60", "type": "github" }, "original": { From e2ca6e45f39045cbc6ce6ad619050e0324a93a23 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Thu, 13 Apr 2023 12:45:06 +0200 Subject: [PATCH 063/225] docs: add instructions for rfc6186-compliant setup --- docs/autodiscovery.rst | 17 +++++++++++++++++ docs/index.rst | 1 + 2 files changed, 18 insertions(+) create mode 100644 docs/autodiscovery.rst diff --git a/docs/autodiscovery.rst b/docs/autodiscovery.rst new file mode 100644 index 0000000..7f4d438 --- /dev/null +++ b/docs/autodiscovery.rst @@ -0,0 +1,17 @@ +Autodiscovery +============= + +`RFC6186 `_ allows supporting email clients to automatically discover SMTP / IMAP addresses +of the mailserver. For that, the following records are required: + +================ ==== ==== ======== ====== ==== ================= +Record TTL Type Priority Weight Port Value +================ ==== ==== ======== ====== ==== ================= +_submission._tcp 3600 SRV 5 0 587 mail.example.com. +_imap._tcp 3600 SRV 5 0 143 mail.example.com. +_imaps._tcp 3600 SRV 5 0 993 mail.example.com. +================ ==== ==== ======== ====== ==== ================= + +Please note that only a few MUAs currently implement this. For vendor-specific +discovery mechanisms `automx `_ can be used instead. + diff --git a/docs/index.rst b/docs/index.rst index b8700ff..717eed0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,6 +29,7 @@ Welcome to NixOS Mailserver's documentation! rspamd-tuning fts flakes + autodiscovery Indices and tables ================== From c04e4f22da48319d15593a2c942431744c12f27c Mon Sep 17 00:00:00 2001 From: Juergen Fitschen Date: Sat, 18 Mar 2023 12:26:27 +0000 Subject: [PATCH 064/225] opendkim: make public key world-readable --- mail-server/opendkim.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/mail-server/opendkim.nix b/mail-server/opendkim.nix index 27df835..cdb283c 100644 --- a/mail-server/opendkim.nix +++ b/mail-server/opendkim.nix @@ -37,6 +37,7 @@ let --directory="${cfg.dkimKeyDirectory}" mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}" mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}" + chmod 644 "${dkim_txt}" echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}" fi ''; From bd99079363d36d762d3862a4df6f026692560cba Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Sun, 16 Apr 2023 20:30:03 +0200 Subject: [PATCH 065/225] mail-server/dovecot: also learn spam/ham on APPEND The current configuration doesn't work when moving spam from the INBOX to Junk on a local maildir and then syncing the result to the IMAP server with `mbsync(1)`. This is because `mbsync(1)` doesn't support a mvoe-detection[1] (i.e. an IMAP MOVE which subsequently causes a Sieve COPY according to RFC6851 which then triggers report{h,sp}am.sieve), but instead sends `APPEND` (and removes the message in the src mailbox after that). Tested on my own mailserver that this fixes spam learning. This doesn't work the other way round though because `APPEND` doesn't have an origin. However, learning mails as spam happens more often than learning spam as ham, so this is IMHO still useful. [1] https://sourceforge.net/p/isync/mailman/isync-devel/thread/87y2p1tihz.fsf%40ericabrahamsen.net/#msg37030483 --- mail-server/dovecot.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 18a9262..b1c4e59 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -243,7 +243,7 @@ in # From elsewhere to Spam folder imapsieve_mailbox1_name = ${junkMailboxName} - imapsieve_mailbox1_causes = COPY + imapsieve_mailbox1_causes = COPY,APPEND imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve # From Spam folder to elsewhere From d8131ffc61553df6137b382eec380689596cae3d Mon Sep 17 00:00:00 2001 From: Lafiel Date: Thu, 16 Mar 2023 16:59:05 +0300 Subject: [PATCH 066/225] dovecot: split passdb and userdb --- mail-server/dovecot.nix | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index b1c4e59..01563e0 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -22,7 +22,8 @@ let cfg = config.mailserver; passwdDir = "/run/dovecot2"; - passwdFile = "${passwdDir}/passwd"; + passdbFile = "${passwdDir}/passdb"; + userdbFile = "${passwdDir}/userdb"; bool2int = x: if x then "1" else "0"; @@ -74,16 +75,23 @@ let fi done - cat < ${passwdFile} + cat < ${passdbFile} ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: - "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:" + "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" + ) cfg.loginAccounts)} + EOF + + cat < ${userdbFile} + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: + "${name}:::::::" + (if lib.isString value.quota then "userdb_quota_rule=*:storage=${value.quota}" else "") ) cfg.loginAccounts)} EOF - chmod 600 ${passwdFile} + chmod 600 ${passdbFile} + chmod 600 ${userdbFile} ''; junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes); @@ -212,12 +220,13 @@ in passdb { driver = passwd-file - args = ${passwdFile} + args = ${passdbFile} } userdb { driver = passwd-file - args = ${passwdFile} + args = ${userdbFile} + default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} } service auth { From fd605a419bcad2513844ccee0fc6f7760cdd657e Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 24 May 2023 22:02:55 +0200 Subject: [PATCH 067/225] Fix test names --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index f002909..f4da306 100644 --- a/flake.nix +++ b/flake.nix @@ -36,7 +36,7 @@ "multiple" ]; genTest = testName: release: { - "name"= "${testName}-${release.name}"; + "name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}"; "value"= import (./tests/. + "/${testName}.nix") { pkgs = release.pkgs; inherit blobs; From 42c55647910820eae41586c9ff08443a1cd46ac5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Wed, 15 Feb 2023 12:43:44 +0100 Subject: [PATCH 068/225] tests: use `services.dnsmasq.settings` Gets rid of the warning about `extraConfig` being deprecated. --- tests/multiple.nix | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/multiple.nix b/tests/multiple.nix index cef20e3..8a4c07b 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -30,10 +30,7 @@ let }; services.dnsmasq = { enable = true; - extraConfig = '' - mx-host=domain1.com,domain1,10 - mx-host=domain2.com,domain2,10 - ''; + settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ]; }; }; From a948c49ca7e484636f618cf3933f193092370906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Wed, 15 Feb 2023 13:15:09 +0100 Subject: [PATCH 069/225] Allow using existing ACME certificates Add a certificate scheme for using an existing ACME certificate without setting up Nginx. Also use names instead of magic numbers for certificate schemes. --- default.nix | 50 ++++++++++++++++++++++++------------- docs/setup-guide.rst | 2 +- mail-server/common.nix | 16 ++++++------ mail-server/environment.nix | 2 +- mail-server/networking.nix | 4 +-- mail-server/nginx.nix | 4 +-- mail-server/systemd.nix | 6 ++--- 7 files changed, 49 insertions(+), 35 deletions(-) diff --git a/default.nix b/default.nix index ef27f43..49103f3 100644 --- a/default.nix +++ b/default.nix @@ -48,7 +48,11 @@ in type = types.listOf types.str; example = [ "imap.example.com" "pop3.example.com" ]; default = []; - description = "Secondary domains and subdomains for which it is necessary to generate a certificate."; + description = '' + ({option}`mailserver.certificateScheme` == `acme-nginx`) + + Secondary domains and subdomains for which it is necessary to generate a certificate. + ''; }; messageSizeLimit = mkOption { @@ -448,19 +452,26 @@ in }; }; - certificateScheme = mkOption { - type = types.enum [ 1 2 3 ]; - default = 2; + certificateScheme = let + schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ]; + translate = i: warn "setting mailserver.certificateScheme by number is deprecated, please use names instead" + (builtins.elemAt schemes (i - 1)); + in mkOption { + type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes); + default = "selfsigned"; description = '' - Certificate Files. There are three options for these. + The scheme to use for managing TLS certificates: - 1) You specify locations and manually copy certificates there. - 2) You let the server create new (self signed) certificates on the fly. - 3) You let the server create a certificate via `Let's Encrypt`. Note that - this implies that a stripped down webserver has to be started. This also - implies that the FQDN must be set as an `A` record to point to the IP of - the server. In particular port 80 on the server will be opened. For details - on how to set up the domain records, see the guide in the readme. + 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. ''; }; @@ -468,8 +479,9 @@ in type = types.path; example = "/root/mail-server.crt"; description = '' - Scheme 1) - Location of the certificate + ({option}`mailserver.certificateScheme` == `manual`) + + Location of the certificate. ''; }; @@ -477,8 +489,9 @@ in type = types.path; example = "/root/mail-server.key"; description = '' - Scheme 1) - Location of the key file + ({option}`mailserver.certificateScheme` == `manual`) + + Location of the key file. ''; }; @@ -486,8 +499,9 @@ in type = types.path; default = "/var/certs"; description = '' - Scheme 2) - This is the folder where the certificate will be created. The name is + ({option}`mailserver.certificateScheme` == `selfsigned`) + + This is the folder where the self-signed certificate will be created. The name is hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the certificate is valid for 10 years. ''; diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 8127734..c74a53d 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -81,7 +81,7 @@ these should be the most common ones. # Use Let's Encrypt certificates. Note that this needs to set up a stripped # down nginx and opens port 80. - certificateScheme = 3; + certificateScheme = "acme-nginx"; }; } diff --git a/mail-server/common.nix b/mail-server/common.nix index 2a264a7..e8beb7a 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -21,22 +21,22 @@ let in { # cert :: PATH - certificatePath = if cfg.certificateScheme == 1 + certificatePath = if cfg.certificateScheme == "manual" then cfg.certificateFile - else if cfg.certificateScheme == 2 + else if cfg.certificateScheme == "selfsigned" then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" - else if cfg.certificateScheme == 3 + else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem" - else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; + else throw "unknown certificate scheme"; # key :: PATH - keyPath = if cfg.certificateScheme == 1 + keyPath = if cfg.certificateScheme == "manual" then cfg.keyFile - else if cfg.certificateScheme == 2 + else if cfg.certificateScheme == "selfsigned" then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" - else if cfg.certificateScheme == 3 + else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem" - else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; + else throw "unknown certificate scheme"; passwordFiles = let mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; diff --git a/mail-server/environment.nix b/mail-server/environment.nix index cc85202..e509ea6 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -23,6 +23,6 @@ in config = with cfg; lib.mkIf enable { environment.systemPackages = with pkgs; [ dovecot opendkim openssh postfix rspamd - ] ++ (if certificateScheme == 2 then [ openssl ] else []); + ] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []); }; } diff --git a/mail-server/networking.nix b/mail-server/networking.nix index e8a222e..6af186a 100644 --- a/mail-server/networking.nix +++ b/mail-server/networking.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ config, lib, ... }: let cfg = config.mailserver; @@ -31,7 +31,7 @@ in ++ lib.optional enablePop3 110 ++ lib.optional enablePop3Ssl 995 ++ lib.optional enableManageSieve 4190 - ++ lib.optional (certificateScheme == 3) 80; + ++ lib.optional (certificateScheme == "acme-nginx") 80; }; }; } diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index abc6421..e5fa597 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -24,8 +24,8 @@ let acmeRoot = "/var/lib/acme/acme-challenge"; in { - config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) { - services.nginx = { + 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; diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 36e48d6..0fdcf90 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -19,9 +19,9 @@ let cfg = config.mailserver; certificatesDeps = - if cfg.certificateScheme == 1 then + if cfg.certificateScheme == "manual" then [] - else if cfg.certificateScheme == 2 then + else if cfg.certificateScheme == "selfsigned" then [ "mailserver-selfsigned-certificate.service" ] else [ "acme-finished-${cfg.fqdn}.target" ]; @@ -29,7 +29,7 @@ in { config = with cfg; lib.mkIf enable { # Create self signed certificate - systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) { + systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") { after = [ "local-fs.target" ]; script = '' # Create certificates if they do not exist yet From 1bcfcf786bc289ca1bd2c9d29d6f02d9141b1da3 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 24 May 2023 23:37:17 +0200 Subject: [PATCH 070/225] Remove the NixOS 22.11 support Because the option `nodes.domain1.services.dnsmasq.settings' does not exist. --- flake.nix | 4 ---- 1 file changed, 4 deletions(-) diff --git a/flake.nix b/flake.nix index f4da306..45b84a7 100644 --- a/flake.nix +++ b/flake.nix @@ -24,10 +24,6 @@ name = "unstable"; pkgs = nixpkgs.legacyPackages.${system}; } - { - name = "22.11"; - pkgs = nixpkgs-22_11.legacyPackages.${system}; - } ]; testNames = [ "internal" From 7e09d8f537d2c95af57a3a9784d4daa83501a3a7 Mon Sep 17 00:00:00 2001 From: Mynacol <2505725-Mynacol@users.noreply.gitlab.com> Date: Mon, 29 May 2023 15:09:08 +0200 Subject: [PATCH 071/225] docs: add submissions DNS record for autodiscovery Add the submissions autodiscovery SRV DNS record for implicit TLS in SMTP (submission) connections according to [RFC 8314](https://www.rfc-editor.org/rfc/rfc8314#section-5.1). --- docs/autodiscovery.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/autodiscovery.rst b/docs/autodiscovery.rst index 7f4d438..a630850 100644 --- a/docs/autodiscovery.rst +++ b/docs/autodiscovery.rst @@ -4,13 +4,14 @@ Autodiscovery `RFC6186 `_ allows supporting email clients to automatically discover SMTP / IMAP addresses of the mailserver. For that, the following records are required: -================ ==== ==== ======== ====== ==== ================= -Record TTL Type Priority Weight Port Value -================ ==== ==== ======== ====== ==== ================= -_submission._tcp 3600 SRV 5 0 587 mail.example.com. -_imap._tcp 3600 SRV 5 0 143 mail.example.com. -_imaps._tcp 3600 SRV 5 0 993 mail.example.com. -================ ==== ==== ======== ====== ==== ================= +================= ==== ==== ======== ====== ==== ================= +Record TTL Type Priority Weight Port Value +================= ==== ==== ======== ====== ==== ================= +_submission._tcp 3600 SRV 5 0 587 mail.example.com. +_submissions._tcp 3600 SRV 5 0 465 mail.example.com. +_imap._tcp 3600 SRV 5 0 143 mail.example.com. +_imaps._tcp 3600 SRV 5 0 993 mail.example.com. +================= ==== ==== ======== ====== ==== ================= Please note that only a few MUAs currently implement this. For vendor-specific discovery mechanisms `automx `_ can be used instead. From 290d00f6db4e80467013728819ad73dd4a394d9a Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Tue, 6 Jun 2023 23:24:53 +0200 Subject: [PATCH 072/225] Improve the certificateScheme number deprecation warning message --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 49103f3..2dfbbfb 100644 --- a/default.nix +++ b/default.nix @@ -454,7 +454,7 @@ in certificateScheme = let schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ]; - translate = i: warn "setting mailserver.certificateScheme by number is deprecated, please use names instead" + 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); From 131c48de9bbf85da77434cde858da1d922cbc4ea Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Tue, 30 May 2023 23:36:53 +0200 Subject: [PATCH 073/225] Preserve the compatibility with nixos-22.11 --- tests/multiple.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/multiple.nix b/tests/multiple.nix index 8a4c07b..daf468a 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -30,7 +30,12 @@ let }; services.dnsmasq = { enable = true; - settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ]; + # Fixme: once nixos-22.11 has been removed, could be replaced by + # settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ]; + extraConfig = '' + mx-host=domain1.com,domain1,10 + mx-host=domain2.com,domain2,10 + ''; }; }; From c4ec122aacf58944f381bb4912f01588f99d0ba0 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Tue, 30 May 2023 23:27:38 +0200 Subject: [PATCH 074/225] readme: remove the announcement public key Current maintainer no longer has it. --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 9aee259..0d151e7 100644 --- a/README.md +++ b/README.md @@ -22,12 +22,7 @@ SNM branch corresponding to your NixOS version. [Subscribe to SNM Announcement List](https://www.freelists.org/list/snm) This is a very low volume list where new releases of SNM are announced, so you -can stay up to date with bug fixes and updates. All announcements are signed by -the gpg key with fingerprint - -``` -D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A -``` +can stay up to date with bug fixes and updates. ## Features From 24128c3052090311688b09a400aa408ba61c6ee5 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Tue, 23 May 2023 23:06:06 +0200 Subject: [PATCH 075/225] Release 23.05 --- .hydra/declarative-jobsets.nix | 2 +- README.md | 33 +++++---------------------------- docs/release-notes.rst | 9 ++++++++- flake.lock | 16 ++++++++++++++++ flake.nix | 7 ++++++- 5 files changed, 36 insertions(+), 31 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index eeb82d2..10ed381 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -32,8 +32,8 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; - "nixos-22.05" = mkFlakeJobset "nixos-22.05"; "nixos-22.11" = mkFlakeJobset "nixos-22.11"; + "nixos-23.05" = mkFlakeJobset "nixos-23.05"; }; log = { diff --git a/README.md b/README.md index 0d151e7..cf8bbc2 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. +* For NixOS 23.05 + - Use the [SNM branch `nixos-23.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.05) + - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/) + - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/release-notes.html#nixos-23-05) * For NixOS 22.11 - Use the [SNM branch `nixos-22.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.11) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/release-notes.html#nixos-22-11) -* For NixOS 22.05 - - Use the [SNM branch `nixos-22.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.05) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/) - - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/release-notes.html#nixos-22-05) * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) @@ -112,30 +112,9 @@ For a complete list of options, see `default.nix`. ## How to Set Up a 10/10 Mail Server Guide Check out the [Complete Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation. -## How to Backup - -Checkout the [Complete Backup Guide](https://nixos-mailserver.readthedocs.io/en/latest/backup-guide.html). Backups are easy with `SNM`. - ## Development -See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) wiki page. - -## Release notes - -### nixos-20.03 - -- Rspamd is upgraded to 2.0 which deprecates the SQLite Bayes - backend. We then moved to the Redis backend (the default since - Rspamd 2.0). If you don't want to relearn the Redis backend from the - scratch, we could manually run - - rspamadm statconvert --spam-db /var/lib/rspamd/bayes.spam.sqlite --ham-db /var/lib/rspamd/bayes.ham.sqlite -h 127.0.0.1:6379 --symbol-ham BAYES_HAM --symbol-spam BAYES_SPAM - - See the [Rspamd migration - notes](https://rspamd.com/doc/migration.html#migration-to-rspamd-20) - and [this SNM Merge - Request](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/164) - for details. +See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page. ## Contributors See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master) @@ -150,6 +129,4 @@ See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mails * Logo made with [Logomakr.com](https://logomakr.com) - - [logo]: docs/logo.png diff --git a/docs/release-notes.rst b/docs/release-notes.rst index fa1a87c..4e4687b 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,10 +1,17 @@ Release Notes ============= + +NixOS 23.05 +----------- + +- Existing ACME certificates can be reused without configuring NGINX +- Certificate scheme is no longer a number, but a meaningful string instead + NixOS 22.11 ----------- -- Allow Rspamd to send dmarc reporting +- Allow Rspamd to send DMARC reporting (`merge request `__) NixOS 22.05 diff --git a/flake.lock b/flake.lock index 4c8b160..29711c4 100644 --- a/flake.lock +++ b/flake.lock @@ -62,12 +62,28 @@ "type": "indirect" } }, + "nixpkgs-23_05": { + "locked": { + "lastModified": 1684782344, + "narHash": "sha256-SHN8hPYYSX0thDrMLMWPWYulK3YFgASOrCsIL3AJ78g=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8966c43feba2c701ed624302b6a935f97bcbdf88", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-23.05", + "type": "indirect" + } + }, "root": { "inputs": { "blobs": "blobs", "flake-compat": "flake-compat", "nixpkgs": "nixpkgs", "nixpkgs-22_11": "nixpkgs-22_11", + "nixpkgs-23_05": "nixpkgs-23_05", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index 45b84a7..0deb9c8 100644 --- a/flake.nix +++ b/flake.nix @@ -9,13 +9,14 @@ utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; nixpkgs-22_11.url = "flake:nixpkgs/nixos-22.11"; + nixpkgs-23_05.url = "flake:nixpkgs/nixos-23.05"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_11, ... }: let + outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_11, nixpkgs-23_05, ... }: let lib = nixpkgs.lib; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; @@ -24,6 +25,10 @@ name = "unstable"; pkgs = nixpkgs.legacyPackages.${system}; } + { + name = "23.05"; + pkgs = nixpkgs-23_05.legacyPackages.${system}; + } ]; testNames = [ "internal" From 0c1801b48995ec6909e040abedaa56a64f0db430 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Tue, 27 Jun 2023 10:03:43 +0200 Subject: [PATCH 076/225] dovecot: add dovecot_pigeonhole to system packages `sieve-test` can be used to test sieve scripts. It's annoying to nix-shell it in, because it reads the dovecot global config and might stumble over incompatible .so files (as has happened to me). Simply providing it in $PATH is easier. --- mail-server/dovecot.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 01563e0..6730b61 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -109,6 +109,13 @@ in } ]; + # 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 + ]; + services.dovecot2 = { enable = true; enableImap = enableImap || enableImapSsl; From d460e9ff62ea1238fb3348a87326b743ae177902 Mon Sep 17 00:00:00 2001 From: Nigel Bray Date: Sun, 1 Jan 2023 17:38:07 +0000 Subject: [PATCH 077/225] Fix and improve the setup guide --- README.md | 43 ++++--------------------------------------- docs/setup-guide.rst | 27 +++++++++++++++------------ 2 files changed, 19 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index cf8bbc2..5e8db21 100644 --- a/README.md +++ b/README.md @@ -71,46 +71,11 @@ can stay up to date with bug fixes and updates. - Subscribe to the [mailing list](https://www.freelists.org/archive/snm/) - Join the Libera Chat IRC channel `#nixos-mailserver` -### Quick Start - -```nix - { config, pkgs, ... }: - let release = "nixos-21.11"; - in { - imports = [ - (builtins.fetchTarball { - url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz"; - # This hash needs to be updated - sha256 = "0000000000000000000000000000000000000000000000000000"; - }) - ]; - - mailserver = { - enable = true; - fqdn = "mail.example.com"; - domains = [ "example.com" "example2.com" ]; - loginAccounts = { - "user1@example.com" = { - # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' > /hashed/password/file/location - hashedPasswordFile = "/hashed/password/file/location"; - - aliases = [ - "info@example.com" - "postmaster@example.com" - "postmaster@example2.com" - ]; - }; - }; - }; - } -``` - -For a complete list of options, see `default.nix`. - - - ## How to Set Up a 10/10 Mail Server Guide -Check out the [Complete Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation. + +Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation. + +For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html). ## Development diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index c74a53d..dafe60c 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -48,18 +48,19 @@ Setup the server ~~~~~~~~~~~~~~~~ The following describes a server setup that is fairly complete. Even -though there are more possible options (see the ``default.nix`` file), -these should be the most common ones. +though there are more possible options (see the `NixOS Mailserver +options documentation `_), these should be the most +common ones. .. code:: nix - { config, pkgs, ... }: - { + { config, pkgs, ... }: { imports = [ (builtins.fetchTarball { - # Pick a commit from the branch you are interested in - url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz"; - # And set its hash + # Pick a release version you are interested in and set its hash, e.g. + url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz"; + # To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command: + # release="nixos-23.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack sha256 = "0000000000000000000000000000000000000000000000000000"; }) ]; @@ -72,17 +73,19 @@ these should be the most common ones. # A list of all login accounts. To create the password hashes, use # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' loginAccounts = { - "user1@example.com" = { - hashedPasswordFile = "/a/file/containing/a/hashed/password"; - aliases = ["postmaster@example.com"]; - }; - "user2@example.com" = { ... }; + "user1@example.com" = { + hashedPasswordFile = "/a/file/containing/a/hashed/password"; + aliases = ["postmaster@example.com"]; + }; + "user2@example.com" = { ... }; }; # Use Let's Encrypt certificates. Note that this needs to set up a stripped # down nginx and opens port 80. certificateScheme = "acme-nginx"; }; + security.acme.acceptTerms = true; + security.acme.defaults.email = "security@example.com"; } After a ``nixos-rebuild switch`` your server should be running all From 08f077c5ca907c8b7f386eb9fdac251f31f711dc Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 19 Jul 2022 23:45:29 +0200 Subject: [PATCH 078/225] Add support for LDAP users Allow configuring lookups for users and their mail addresses from an LDAP directory. The LDAP username will be used as an accountname as opposed to the email address used as the `loginName` for declarative accounts. Mailbox for LDAP users will be stored below `/var/vmail/ldap/`. Configuring domains is out of scope, since domains require further configuration within the NixOS mailserver construct to set up all related services accordingly. Aliases can already be configured using `mailserver.forwards` but could be supported using LDAP at a later point. --- default.nix | 150 ++++++++++++++++++++++++++++++++++++++++ mail-server/dovecot.nix | 50 ++++++++++++++ mail-server/postfix.nix | 37 +++++++++- 3 files changed, 235 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index 2dfbbfb..4b684d1 100644 --- a/default.nix +++ b/default.nix @@ -198,6 +198,156 @@ in default = {}; }; + ldap = { + enable = mkEnableOption "LDAP support"; + + uris = mkOption { + type = types.listOf types.str; + example = literalExpression '' + [ + "ldaps://ldap1.example.com" + "ldaps://ldap2.example.com" + ] + ''; + description = '' + URIs where your LDAP server can be reached + ''; + }; + + startTls = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable StartTLS upon connection to the server. + ''; + }; + + tlsCAFile = mkOption { + type = types.path; + default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + description = '' + Certifificate trust anchors used to verify the LDAP server certificate. + ''; + }; + + bind = { + dn = mkOption { + type = types.str; + example = "cn=mail,ou=accounts,dc=example,dc=com"; + description = '' + Distinguished name used by the mail server to do lookups + against the LDAP servers. + ''; + }; + + password = mkOption { + type = types.str; + example = "not$4f3"; + description = '' + Password required to authenticate against the LDAP servers. + ''; + }; + }; + + searchBase = mkOption { + type = types.str; + example = "ou=people,ou=accounts,dc=example,dc=com"; + description = '' + Base DN at below which to search for users accounts. + ''; + }; + + searchScope = mkOption { + type = types.enum [ "sub" "base" "one" ]; + default = "sub"; + description = '' + Search scope below which users accounts are looked for. + ''; + }; + + dovecot = { + userAttrs = mkOption { + type = types.str; + default = ""; + description = '' + LDAP attributes to be retrieved during userdb lookups. + + See the users_attrs reference at + https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-attrs + in the Dovecot manual. + ''; + }; + + userFilter = mkOption { + type = types.str; + default = "cn=%u"; + example = "(&(objectClass=inetOrgPerson)(cn=%u))"; + description = '' + Filter for user lookups in Dovecot. + + See the user_filter reference at + https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-filter + in the Dovecot manual. + ''; + }; + + passAttrs = mkOption { + type = types.str; + default = "userPassword=password"; + description = '' + LDAP attributes to be retrieved during passdb lookups. + + See the pass_attrs reference at + https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-attrs + in the Dovecot manual. + ''; + }; + + passFilter = mkOption { + type = types.str; + default = "cn=%u"; + example = "(&(objectClass=inetOrgPerson)(cn=%u))"; + description = '' + Filter for password lookups in Dovecot. + + See the pass_filter reference for + https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-filter + in the Dovecot manual. + ''; + }; + }; + + postfix = { + filter = mkOption { + type = types.str; + default = "mail=%s"; + example = "(&(objectClass=inetOrgPerson)(mail=%s))"; + description = '' + LDAP filter used to search for an account by mail, where + %s is a substitute for the address in + question. + ''; + }; + + uidAttribute = mkOption { + type = types.str; + default = "cn"; + example = "uid"; + description = '' + The LDAP attribute referencing the account name for a user. + ''; + }; + + mailAttribute = mkOption { + type = types.str; + default = "mail"; + description = '' + The LDAP attribute holding mail addresses for a user. + ''; + }; + }; + }; + indexDir = mkOption { type = types.nullOr types.str; default = null; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 6730b61..33dc3c8 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -99,6 +99,12 @@ let # 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 + ); + in { config = with cfg; lib.mkIf enable { @@ -236,6 +242,19 @@ in default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} } + ${lib.optionalString cfg.ldap.enable '' + passdb { + driver = ldap + args = /etc/dovecot/dovecot-ldap.conf.ext + } + + userdb { + driver = ldap + args = /etc/dovecot/dovecot-ldap.conf.ext + default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} + } + ''} + service auth { unix_listener auth { mode = 0660 @@ -298,6 +317,37 @@ in ''; }; + environment.etc = lib.optionalAttrs (cfg.ldap.enable) { + "dovecot/dovecot-ldap.conf.ext" = { + mode = "0600"; + uid = config.ids.uids.dovecot2; + gid = config.ids.gids.dovecot2; + text = '' + ldap_version = 3 + uris = ${lib.concatStringsSep " " cfg.ldap.uris} + ${lib.optionalString cfg.ldap.startTls '' + tls = yes + ''} + tls_require_cert = hard + tls_ca_cert_file = ${cfg.ldap.tlsCAFile} + dn = ${cfg.ldap.bind.dn} + dnpass = ${cfg.ldap.bind.password} + sasl_bind = no + auth_bind = yes + base = ${cfg.ldap.searchBase} + scope = ${mkLdapSearchScope cfg.ldap.searchScope} + ${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") '' + user_attrs = ${cfg.ldap.dovecot.user_attrs} + ''} + user_filter = ${cfg.ldap.dovecot.userFilter} + ${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") '' + pass_attrs = ${cfg.ldap.dovecot.passAttrs} + ''} + pass_filter = ${cfg.ldap.dovecot.passFilter} + ''; + }; + }; + systemd.services.dovecot2 = { preStart = '' ${genPasswdScript} diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 340122b..9ccf7bb 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -133,11 +133,40 @@ let 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"; + smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMap}"}"; 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 = lib.optionalString (cfg.ldap.enable) '' + server_host = ${lib.concatStringsSep " " cfg.ldap.uris} + start_tls = ${if cfg.ldap.startTls then "yes" else "no"} + version = 3 + tls_ca_cert_file = ${cfg.ldap.tlsCAFile} + tls_require_cert = yes + + search_base = ${cfg.ldap.searchBase} + scope = ${cfg.ldap.searchScope} + + bind = yes + bind_dn = ${cfg.ldap.bind.dn} + bind_pw = ${cfg.ldap.bind.password} + ''; + + ldapSenderLoginMap = lib.optionalString (cfg.ldap.enable) + (pkgs.writeText "ldap-sender-login-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.uidAttribute} + ''); + + ldapVirtualMailboxMap = lib.optionalString (cfg.ldap.enable) + (pkgs.writeText "ldap-virtual-mailbox-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.uidAttribute} + ''); in { config = with cfg; lib.mkIf enable { @@ -170,7 +199,11 @@ in virtual_gid_maps = "static:5000"; virtual_mailbox_base = mailDirectory; virtual_mailbox_domains = vhosts_file; - virtual_mailbox_maps = mappedFile "valias"; + virtual_mailbox_maps = [ + (mappedFile "valias") + ] ++ lib.optionals (cfg.ldap.enable) [ + "ldap:${ldapVirtualMailboxMap}" + ]; 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"; From 42e245b06909d63acc2310c14ba04ec700346a84 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 12 Aug 2022 18:54:12 +0200 Subject: [PATCH 079/225] scripts/mail-check: allow passing the smtp username Will be prefered over the from address when specified. --- scripts/mail-check.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scripts/mail-check.py b/scripts/mail-check.py index f5a97a5..0a96ce1 100644 --- a/scripts/mail-check.py +++ b/scripts/mail-check.py @@ -9,7 +9,7 @@ import time RETRY = 100 -def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, starttls): +def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls): print("Sending mail with subject '{}'".format(subject)) message = "\n".join([ "From: {from_addr}", @@ -30,7 +30,7 @@ def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, star if starttls: smtp.starttls() if from_pwd is not None: - smtp.login(from_addr, from_pwd) + smtp.login(smtp_username or from_addr, from_pwd) smtp.sendmail(from_addr, [to_addr], message) return @@ -141,6 +141,7 @@ def send_and_read(args): _send_mail(smtp_host=args.smtp_host, smtp_port=args.smtp_port, + smtp_username=args.smtp_username, from_addr=args.from_addr, from_pwd=src_pwd, to_addr=args.to_addr, @@ -171,6 +172,7 @@ parser_send_and_read = subparsers.add_parser('send-and-read', description="Send parser_send_and_read.add_argument('--smtp-host', type=str) parser_send_and_read.add_argument('--smtp-port', type=str, default=25) parser_send_and_read.add_argument('--smtp-starttls', action='store_true') +parser_send_and_read.add_argument('--smtp-username', type=str, default='', help="username used for smtp login. If not specified, the from-addr value is used") parser_send_and_read.add_argument('--from-addr', type=str) parser_send_and_read.add_argument('--imap-host', required=True, type=str) parser_send_and_read.add_argument('--imap-port', type=str, default=993) From 8b03ae5701a672186c704be70bff8390b662c323 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 12 Aug 2022 18:54:49 +0200 Subject: [PATCH 080/225] Create LDAP test Sets up a declaratively configured OpenLDAP instance with users alice and bob. They each own one email address, First we test that postfix can communicate with LDAP and do the expected lookups using the defined maps. Then we use doveadm to make sure it can look up the two accounts. Next we check the binding between account and mail address, by logging in as alice and trying to send from bob@example.com, which alice is not allowed to do. We expect postfix to reject the sender address here. Finally we check mail delivery between alice and bob. Alice tries to send a mail from alice@example.com to bob@example.com and bob then checks whether it arrived in their mailbox. --- flake.nix | 1 + tests/ldap.nix | 172 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 tests/ldap.nix diff --git a/flake.nix b/flake.nix index 0deb9c8..54d747e 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,7 @@ "external" "clamav" "multiple" + "ldap" ]; genTest = testName: release: { "name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}"; diff --git a/tests/ldap.nix b/tests/ldap.nix new file mode 100644 index 0000000..6077543 --- /dev/null +++ b/tests/ldap.nix @@ -0,0 +1,172 @@ +{ pkgs ? import {} +, ... +}: + +let + bindPassword = "unsafegibberish"; + alicePassword = "testalice"; + bobPassword = "testbob"; +in +pkgs.nixosTest { + name = "ldap"; + nodes = { + machine = { config, pkgs, ... }: { + imports = [ + ./../default.nix + ./lib/config.nix + ]; + + virtualisation.memorySize = 1024; + + environment.systemPackages = [ + (pkgs.writeScriptBin "mail-check" '' + ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ + '')]; + + 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"; + 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} + ''; + }; + + mailserver = { + enable = true; + fqdn = "mail.example.com"; + domains = [ "example.com" ]; + localDnsResolver = false; + + ldap = { + enable = true; + uris = [ + "ldap://" + ]; + bind = { + dn = "cn=mail,dc=example"; + password = bindPassword; + }; + searchBase = "ou=users,dc=example"; + searchScope = "sub"; + }; + + vmailGroupName = "vmail"; + vmailUID = 5000; + + enableImap = false; + }; + }; + }; + testScript = '' + import sys + + from glob import glob + + machine.start() + machine.wait_for_unit("multi-user.target") + + def test_lookup(map, key, expected): + path = glob(f"/nix/store/*-{map}")[0] + value = machine.succeed(f"postmap -q alice@example.com ldap:{path}").rstrip() + try: + assert value == expected + except AssertionError: + print(f"Expected {map} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr) + raise + + + with subtest("Test postmap lookups"): + test_lookup("ldap-virtual-mailbox-map.cf", "alice@example.com", "alice") + test_lookup("ldap-sender-login-map.cf", "alice", "alice") + + test_lookup("ldap-virtual-mailbox-map.cf", "bob@example.com", "alice") + test_lookup("ldap-sender-login-map.cf", "bob", "alice") + + with subtest("Test doveadm lookups"): + out = machine.succeed("doveadm user -u alice") + machine.log(out) + + out = machine.succeed("doveadm user -u bob") + machine.log(out) + + with subtest("Test account/mail address binding"): + machine.fail(" ".join([ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--smtp-username alice", + "--imap-host localhost", + "--imap-username bob", + "--from-addr bob@example.com", + "--to-addr aliceb@example.com", + "--src-password-file <(echo '${alicePassword}')", + "--dst-password-file <(echo '${bobPassword}')", + "--ignore-dkim-spf" + ])) + machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice'") + + with subtest("Test mail delivery"): + machine.succeed(" ".join([ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--smtp-username alice", + "--imap-host localhost", + "--imap-username bob", + "--from-addr alice@example.com", + "--to-addr bob@example.com", + "--src-password-file <(echo '${alicePassword}')", + "--dst-password-file <(echo '${bobPassword}')", + "--ignore-dkim-spf" + ])) + ''; +} From 33554e57ce66c828954b70a442138f71cffbc48d Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Fri, 19 May 2023 10:08:50 +0200 Subject: [PATCH 081/225] Make the ldap test working - The smtp/imap user name is now user@domain.tld - Make the test_lookup function much more robust: it was now getting the correct file from the store. --- default.nix | 12 +++++------ mail-server/postfix.nix | 2 +- tests/ldap.nix | 47 +++++++++++++++++++++++------------------ 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/default.nix b/default.nix index 4b684d1..86b436d 100644 --- a/default.nix +++ b/default.nix @@ -280,8 +280,8 @@ in userFilter = mkOption { type = types.str; - default = "cn=%u"; - example = "(&(objectClass=inetOrgPerson)(cn=%u))"; + default = "mail=%u"; + example = "(&(objectClass=inetOrgPerson)(mail=%u))"; description = '' Filter for user lookups in Dovecot. @@ -304,9 +304,9 @@ in }; passFilter = mkOption { - type = types.str; - default = "cn=%u"; - example = "(&(objectClass=inetOrgPerson)(cn=%u))"; + type = types.nullOr types.str; + default = "mail=%u"; + example = "(&(objectClass=inetOrgPerson)(mail=%u))"; description = '' Filter for password lookups in Dovecot. @@ -331,7 +331,7 @@ in uidAttribute = mkOption { type = types.str; - default = "cn"; + default = "mail"; example = "uid"; description = '' The LDAP attribute referencing the account name for a user. diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 9ccf7bb..576b8f7 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -158,7 +158,7 @@ let (pkgs.writeText "ldap-sender-login-map.cf" '' ${commonLdapConfig} query_filter = ${cfg.ldap.postfix.filter} - result_attribute = ${cfg.ldap.postfix.uidAttribute} + result_attribute = ${cfg.ldap.postfix.mailAttribute} ''); ldapVirtualMailboxMap = lib.optionalString (cfg.ldap.enable) diff --git a/tests/ldap.nix b/tests/ldap.nix index 6077543..6c8308d 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -18,6 +18,11 @@ pkgs.nixosTest { virtualisation.memorySize = 1024; + services.openssh = { + enable = true; + permitRootLogin = "yes"; + }; + environment.systemPackages = [ (pkgs.writeScriptBin "mail-check" '' ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ @@ -106,35 +111,35 @@ pkgs.nixosTest { }; testScript = '' import sys - - from glob import glob + import re machine.start() machine.wait_for_unit("multi-user.target") - def test_lookup(map, key, expected): - path = glob(f"/nix/store/*-{map}")[0] - value = machine.succeed(f"postmap -q alice@example.com ldap:{path}").rstrip() + # This function retrieves the ldap table file from a postconf + # command. + # A key lookup is achived and the returned value is compared + # to the expected value. + def test_lookup(postconf_cmdline, key, expected): + conf = machine.succeed(postconf_cmdline).rstrip() + ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1) + value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip() try: assert value == expected except AssertionError: - print(f"Expected {map} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr) + print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr) raise - with subtest("Test postmap lookups"): - test_lookup("ldap-virtual-mailbox-map.cf", "alice@example.com", "alice") - test_lookup("ldap-sender-login-map.cf", "alice", "alice") + test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com") + test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com") - test_lookup("ldap-virtual-mailbox-map.cf", "bob@example.com", "alice") - test_lookup("ldap-sender-login-map.cf", "bob", "alice") + test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com") + test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com") with subtest("Test doveadm lookups"): - out = machine.succeed("doveadm user -u alice") - machine.log(out) - - out = machine.succeed("doveadm user -u bob") - machine.log(out) + machine.succeed("doveadm user -u alice@example.com") + machine.succeed("doveadm user -u bob@example.com") with subtest("Test account/mail address binding"): machine.fail(" ".join([ @@ -142,16 +147,16 @@ pkgs.nixosTest { "--smtp-port 587", "--smtp-starttls", "--smtp-host localhost", - "--smtp-username alice", + "--smtp-username alice@example.com", "--imap-host localhost", - "--imap-username bob", + "--imap-username bob@example.com", "--from-addr bob@example.com", "--to-addr aliceb@example.com", "--src-password-file <(echo '${alicePassword}')", "--dst-password-file <(echo '${bobPassword}')", "--ignore-dkim-spf" ])) - machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice'") + machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'") with subtest("Test mail delivery"): machine.succeed(" ".join([ @@ -159,9 +164,9 @@ pkgs.nixosTest { "--smtp-port 587", "--smtp-starttls", "--smtp-host localhost", - "--smtp-username alice", + "--smtp-username alice@example.com", "--imap-host localhost", - "--imap-username bob", + "--imap-username bob@example.com", "--from-addr alice@example.com", "--to-addr bob@example.com", "--src-password-file <(echo '${alicePassword}')", From fb3210b9326b81dd24c6092a3da1a1168dff0b0d Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sat, 20 May 2023 00:12:02 +0200 Subject: [PATCH 082/225] ldap: do not write password to the Nix store --- default.nix | 6 +-- mail-server/common.nix | 21 ++++++++++ mail-server/dovecot.nix | 85 ++++++++++++++++++++++------------------- mail-server/postfix.nix | 53 +++++++++++++++++-------- tests/ldap.nix | 10 ++++- 5 files changed, 114 insertions(+), 61 deletions(-) diff --git a/default.nix b/default.nix index 86b436d..f98b4ad 100644 --- a/default.nix +++ b/default.nix @@ -240,11 +240,11 @@ in ''; }; - password = mkOption { + passwordFile = mkOption { type = types.str; - example = "not$4f3"; + example = "/run/my-secret"; description = '' - Password required to authenticate against the LDAP servers. + A file containing the password required to authenticate against the LDAP servers. ''; }; }; diff --git a/mail-server/common.nix b/mail-server/common.nix index e8beb7a..236530b 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -45,4 +45,25 @@ in 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, 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 + + cat ${file} > ${destination} + echo -n "${prefix}" >> ${destination} + cat ${passwordFile} >> ${destination} + chmod 600 ${destination} + ''; + } diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 33dc3c8..92f587a 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -22,9 +22,10 @@ let cfg = config.mailserver; passwdDir = "/run/dovecot2"; - passdbFile = "${passwdDir}/passdb"; + passwdFile = "${passwdDir}/passwd"; userdbFile = "${passwdDir}/userdb"; - + # This file contains the ldap bind password + ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext"; bool2int = x: if x then "1" else "0"; maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; @@ -58,6 +59,41 @@ let ''; }; + + ldapConfig = pkgs.writeTextFile { + name = "dovecot-ldap.conf.ext.template"; + text = '' + ldap_version = 3 + uris = ${lib.concatStringsSep " " cfg.ldap.uris} + ${lib.optionalString cfg.ldap.startTls '' + tls = yes + ''} + tls_require_cert = hard + tls_ca_cert_file = ${cfg.ldap.tlsCAFile} + dn = ${cfg.ldap.bind.dn} + sasl_bind = no + auth_bind = yes + base = ${cfg.ldap.searchBase} + scope = ${mkLdapSearchScope cfg.ldap.searchScope} + ${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") '' + user_attrs = ${cfg.ldap.dovecot.user_attrs} + ''} + user_filter = ${cfg.ldap.dovecot.userFilter} + ${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") '' + pass_attrs = ${cfg.ldap.dovecot.passAttrs} + ''} + pass_filter = ${cfg.ldap.dovecot.passFilter} + ''; + }; + + setPwdInLdapConfFile = appendLdapBindPwd { + name = "ldap-conf-file"; + file = ldapConfig; + prefix = "dnpass = "; + passwordFile = cfg.ldap.bind.passwordFile; + destination = ldapConfFile; + }; + genPasswdScript = pkgs.writeScript "generate-password-file" '' #!${pkgs.stdenv.shell} @@ -75,7 +111,7 @@ let fi done - cat < ${passdbFile} + cat < ${passwdFile} ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" ) cfg.loginAccounts)} @@ -90,7 +126,7 @@ let ) cfg.loginAccounts)} EOF - chmod 600 ${passdbFile} + chmod 600 ${passwdFile} chmod 600 ${userdbFile} ''; @@ -233,7 +269,7 @@ in passdb { driver = passwd-file - args = ${passdbFile} + args = ${passwdFile} } userdb { @@ -245,12 +281,12 @@ in ${lib.optionalString cfg.ldap.enable '' passdb { driver = ldap - args = /etc/dovecot/dovecot-ldap.conf.ext + args = ${ldapConfFile} } userdb { driver = ldap - args = /etc/dovecot/dovecot-ldap.conf.ext + args = ${ldapConfFile} default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} } ''} @@ -317,37 +353,6 @@ in ''; }; - environment.etc = lib.optionalAttrs (cfg.ldap.enable) { - "dovecot/dovecot-ldap.conf.ext" = { - mode = "0600"; - uid = config.ids.uids.dovecot2; - gid = config.ids.gids.dovecot2; - text = '' - ldap_version = 3 - uris = ${lib.concatStringsSep " " cfg.ldap.uris} - ${lib.optionalString cfg.ldap.startTls '' - tls = yes - ''} - tls_require_cert = hard - tls_ca_cert_file = ${cfg.ldap.tlsCAFile} - dn = ${cfg.ldap.bind.dn} - dnpass = ${cfg.ldap.bind.password} - sasl_bind = no - auth_bind = yes - base = ${cfg.ldap.searchBase} - scope = ${mkLdapSearchScope cfg.ldap.searchScope} - ${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") '' - user_attrs = ${cfg.ldap.dovecot.user_attrs} - ''} - user_filter = ${cfg.ldap.dovecot.userFilter} - ${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") '' - pass_attrs = ${cfg.ldap.dovecot.passAttrs} - ''} - pass_filter = ${cfg.ldap.dovecot.passFilter} - ''; - }; - }; - systemd.services.dovecot2 = { preStart = '' ${genPasswdScript} @@ -358,10 +363,10 @@ in ${pkgs.dovecot_pigeonhole}/bin/sievec "$k" done chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve' - ''; + '' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); }; - systemd.services.postfix.restartTriggers = [ genPasswdScript ]; + systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]); systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) { description = "Optimize dovecot indices for fts_xapian"; diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 576b8f7..ad7ce35 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -133,13 +133,13 @@ let 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:${ldapSenderLoginMap}"}"; + smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}"; 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 = lib.optionalString (cfg.ldap.enable) '' + commonLdapConfig = '' server_host = ${lib.concatStringsSep " " cfg.ldap.uris} start_tls = ${if cfg.ldap.startTls then "yes" else "no"} version = 3 @@ -151,26 +151,47 @@ let bind = yes bind_dn = ${cfg.ldap.bind.dn} - bind_pw = ${cfg.ldap.bind.password} ''; - ldapSenderLoginMap = lib.optionalString (cfg.ldap.enable) - (pkgs.writeText "ldap-sender-login-map.cf" '' - ${commonLdapConfig} - query_filter = ${cfg.ldap.postfix.filter} - result_attribute = ${cfg.ldap.postfix.mailAttribute} - ''); + ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.mailAttribute} + ''; + ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf"; + appendPwdInSenderLoginMap = appendLdapBindPwd { + name = "ldap-sender-login-map"; + file = ldapSenderLoginMap; + prefix = "bind_pw = "; + passwordFile = cfg.ldap.bind.passwordFile; + destination = ldapSenderLoginMapFile; + }; - ldapVirtualMailboxMap = lib.optionalString (cfg.ldap.enable) - (pkgs.writeText "ldap-virtual-mailbox-map.cf" '' - ${commonLdapConfig} - query_filter = ${cfg.ldap.postfix.filter} - result_attribute = ${cfg.ldap.postfix.uidAttribute} - ''); + ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" '' + ${commonLdapConfig} + query_filter = ${cfg.ldap.postfix.filter} + result_attribute = ${cfg.ldap.postfix.uidAttribute} + ''; + ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf"; + appendPwdInVirtualMailboxMap = appendLdapBindPwd { + name = "ldap-virtual-mailbox-map"; + file = ldapVirtualMailboxMap; + prefix = "bind_pw = "; + passwordFile = cfg.ldap.bind.passwordFile; + destination = ldapVirtualMailboxMapFile; + }; in { 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}"; @@ -202,7 +223,7 @@ in virtual_mailbox_maps = [ (mappedFile "valias") ] ++ lib.optionals (cfg.ldap.enable) [ - "ldap:${ldapVirtualMailboxMap}" + "ldap:${ldapVirtualMailboxMapFile}" ]; virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients diff --git a/tests/ldap.nix b/tests/ldap.nix index 6c8308d..172a77d 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -28,6 +28,8 @@ pkgs.nixosTest { ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ '')]; + environment.etc.bind-password.text = bindPassword; + services.openldap = { enable = true; settings = { @@ -45,7 +47,7 @@ pkgs.nixosTest { "olcMdbConfig" ]; olcDatabase = "{1}mdb"; - olcDbDirectory = "/var/lib/openldap"; + olcDbDirectory = "/var/lib/openldap/example"; olcSuffix = "dc=example"; }; }; @@ -96,7 +98,7 @@ pkgs.nixosTest { ]; bind = { dn = "cn=mail,dc=example"; - password = bindPassword; + passwordFile = "/etc/bind-password"; }; searchBase = "ou=users,dc=example"; searchScope = "sub"; @@ -141,6 +143,10 @@ pkgs.nixosTest { machine.succeed("doveadm user -u alice@example.com") machine.succeed("doveadm user -u bob@example.com") + with subtest("Files containing secrets are only readable by root"): + machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'") + machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'") + with subtest("Test account/mail address binding"): machine.fail(" ".join([ "mail-check send-and-read", From 7695c856f1d5d8292de2c7b94bf61517132aba84 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sat, 20 May 2023 00:18:58 +0200 Subject: [PATCH 083/225] ldap: improve the documentation --- default.nix | 3 ++- scripts/generate-options.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index f98b4ad..2cb7d3d 100644 --- a/default.nix +++ b/default.nix @@ -225,6 +225,7 @@ in tlsCAFile = mkOption { type = types.path; default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)"; description = '' Certifificate trust anchors used to verify the LDAP server certificate. ''; @@ -324,7 +325,7 @@ in example = "(&(objectClass=inetOrgPerson)(mail=%s))"; description = '' LDAP filter used to search for an account by mail, where - %s is a substitute for the address in + `%s` is a substitute for the address in question. ''; }; diff --git a/scripts/generate-options.py b/scripts/generate-options.py index a4973b1..75a25ae 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -27,6 +27,7 @@ groups = ["mailserver.loginAccounts", "mailserver.dmarcReporting", "mailserver.fullTextSearch", "mailserver.redis", + "mailserver.ldap", "mailserver.monitoring", "mailserver.backup", "mailserver.borgbackup"] From 6775502be3abe32beefb8fc3ec445628e41ac13a Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sat, 20 May 2023 09:52:25 +0200 Subject: [PATCH 084/225] ldap: set assertions to forbid ldap and loginAccounts simultaneously --- default.nix | 1 + mail-server/assertions.nix | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 mail-server/assertions.nix diff --git a/default.nix b/default.nix index 2cb7d3d..66b863e 100644 --- a/default.nix +++ b/default.nix @@ -1252,6 +1252,7 @@ in }; imports = [ + ./mail-server/assertions.nix ./mail-server/borgbackup.nix ./mail-server/debug.nix ./mail-server/rsnapshot.nix diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix new file mode 100644 index 0000000..d2c44ea --- /dev/null +++ b/mail-server/assertions.nix @@ -0,0 +1,17 @@ +{ config, lib, pkgs, ... }: +{ + assertions = lib.optionals config.mailserver.ldap.enable [ + { + assertion = config.mailserver.loginAccounts == {}; + message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts"; + } + { + assertion = config.mailserver.extraVirtualAliases == {}; + message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases"; + } + { + assertion = config.mailserver.forwards == {}; + message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.forwards"; + } + ]; +} From 71b4c62d85660a13657ded9d5194d90f2bd8122e Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 28 Jun 2023 23:07:11 +0200 Subject: [PATCH 085/225] dovecot: fix a typo on userAttrs --- mail-server/dovecot.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 92f587a..c683a8a 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -76,7 +76,7 @@ let base = ${cfg.ldap.searchBase} scope = ${mkLdapSearchScope cfg.ldap.searchScope} ${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") '' - user_attrs = ${cfg.ldap.dovecot.user_attrs} + user_attrs = ${cfg.ldap.dovecot.userAttrs} ''} user_filter = ${cfg.ldap.dovecot.userFilter} ${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") '' From 69a4b7ad67d2732ba1f86666b3d4d2d83b15200e Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Mon, 3 Jul 2023 22:35:58 +0200 Subject: [PATCH 086/225] ldap: add an entry in the doc --- docs/index.rst | 1 + docs/ldap.rst | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/ldap.rst diff --git a/docs/index.rst b/docs/index.rst index 717eed0..2fd1e1a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -30,6 +30,7 @@ Welcome to NixOS Mailserver's documentation! fts flakes autodiscovery + ldap Indices and tables ================== diff --git a/docs/ldap.rst b/docs/ldap.rst new file mode 100644 index 0000000..efd975d --- /dev/null +++ b/docs/ldap.rst @@ -0,0 +1,14 @@ +LDAP Support +============ + +It is possible to manage mail user accounts with LDAP rather than with +the option `loginAccounts `_. + +All related LDAP options are described in the `LDAP options section +`_ and the `LDAP test +`_ +provides a getting started example. + +.. note:: + The LDAP support can not be enabled if some accounts are also defined with ``mailserver.loginAccounts``. + From a3b03d1b5af5112bc94b448879a2f401043b42ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Forsman?= Date: Wed, 28 Jun 2023 10:22:07 +0200 Subject: [PATCH 087/225] Use umask for race-free permission setting Without using umask there's a small time window where paths are world readable. That is a bad idea to do for secret files (e.g. the dovecot code path). --- mail-server/dovecot.nix | 6 +++--- mail-server/systemd.nix | 2 ++ mail-server/users.nix | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index c683a8a..771dedd 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -104,6 +104,9 @@ let chmod 755 "${passwdDir}" fi + # Prevent world-readable password files, even temporarily. + umask 077 + for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do if [ ! -f "$f" ]; then echo "Expected password hash file $f does not exist!" @@ -125,9 +128,6 @@ let else "") ) cfg.loginAccounts)} EOF - - chmod 600 ${passwdFile} - chmod 600 ${userdbFile} ''; junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes); diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 0fdcf90..2c7f8ee 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -64,6 +64,8 @@ in 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} diff --git a/mail-server/users.nix b/mail-server/users.nix index 916ec0c..17196fc 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -34,6 +34,9 @@ let set -euo pipefail + # Prevent world-readable paths, even temporarily. + umask 007 + # Create directory to store user sieve scripts if it doesn't exist if (! test -d "${sieveDirectory}"); then mkdir "${sieveDirectory}" From c63f6e7b053c18325194ff0e274dba44e8d2271e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Na=C3=AFm=20Favier?= Date: Fri, 21 Jul 2023 23:55:54 +0200 Subject: [PATCH 088/225] docs: fix link --- docs/setup-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index dafe60c..61b1559 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -49,7 +49,7 @@ Setup the server The following describes a server setup that is fairly complete. Even though there are more possible options (see the `NixOS Mailserver -options documentation `_), these should be the most +options documentation `_), these should be the most common ones. .. code:: nix From 93221e4b2544ec111014d30dcba3616623369295 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Giraudeau Date: Thu, 13 Jul 2023 18:49:31 +0200 Subject: [PATCH 089/225] Add support for regex (PCRE) aliases. --- default.nix | 9 +++++++++ mail-server/postfix.nix | 20 +++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 66b863e..6361c47 100644 --- a/default.nix +++ b/default.nix @@ -111,6 +111,15 @@ in ''; }; + 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"]; diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index ad7ce35..9362e2c 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -33,6 +33,11 @@ let 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 @@ -65,6 +70,10 @@ 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; + # denied_recipients_postfix :: [ String ] denied_recipients_postfix = (map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") @@ -94,6 +103,7 @@ 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); submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ('' # Removes sensitive headers from mails handed in via the submission port. @@ -123,6 +133,7 @@ let policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig; mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; + mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}"; submissionOptions = { @@ -133,7 +144,7 @@ let 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}"}"; + 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"; @@ -197,7 +208,9 @@ in 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; @@ -224,7 +237,12 @@ in (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"; From 84783b661ecf33927c534b6476beb74ea3308968 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Giraudeau Date: Thu, 28 Sep 2023 16:13:00 +0200 Subject: [PATCH 090/225] Add tests for regex (PCRE) aliases --- tests/internal.nix | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/internal.nix b/tests/internal.nix index 02609fd..564b71a 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -55,7 +55,7 @@ pkgs.nixosTest { mailserver = { enable = true; fqdn = "mail.example.com"; - domains = [ "example.com" ]; + domains = [ "example.com" "domain.com" ]; localDnsResolver = false; loginAccounts = { @@ -64,6 +64,7 @@ pkgs.nixosTest { }; "user2@example.com" = { hashedPasswordFile = hashedPasswordFile; + aliasesRegexp = [''/^user2.*@domain\.com$/'']; }; "send-only@example.com" = { hashedPasswordFile = hashPassword "send-only"; @@ -126,6 +127,46 @@ pkgs.nixosTest { ) ) + with subtest("regex email alias are received"): + # A mail sent to user2-regex-alias@domain.com is in the user2@example.com mailbox + machine.succeed( + " ".join( + [ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--imap-host localhost", + "--imap-username user2@example.com", + "--from-addr user1@example.com", + "--to-addr user2-regex-alias@domain.com", + "--src-password-file ${passwordFile}", + "--dst-password-file ${passwordFile}", + "--ignore-dkim-spf", + ] + ) + ) + + with subtest("user can send from regex email alias"): + # A mail sent from user2-regex-alias@domain.com, using user2@example.com credentials is received + machine.succeed( + " ".join( + [ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--imap-host localhost", + "--smtp-username user2@example.com", + "--from-addr user2-regex-alias@domain.com", + "--to-addr user1@example.com", + "--src-password-file ${passwordFile}", + "--dst-password-file ${passwordFile}", + "--ignore-dkim-spf", + ] + ) + ) + with subtest("vmail gid is set correctly"): machine.succeed("getent group vmail | grep 5000") From 008d78cc21959e33d0d31f375b88353a7d7121ae Mon Sep 17 00:00:00 2001 From: Lafiel Date: Sun, 12 Mar 2023 20:13:51 +0300 Subject: [PATCH 091/225] dovecot: add support store mailbox names on disk using UTF-8 --- default.nix | 8 ++++++++ mail-server/dovecot.nix | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 6361c47..fdfaee3 100644 --- a/default.nix +++ b/default.nix @@ -574,6 +574,14 @@ in ''; }; + useUTF8FolderNames = mkOption { + type = types.bool; + default = false; + description = '' + Store mailbox names on disk using UTF-8 instead of modified UTF-7 (mUTF-7). + ''; + }; + hierarchySeparator = mkOption { type = types.str; default = "."; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 771dedd..7d73ee2 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -29,10 +29,11 @@ let bool2int = x: if x then "1" else "0"; maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; + maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8"; # maildir in format "/${domain}/${user}" dovecotMaildir = - "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}" + "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}" + (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%d/%n" ); From 3f526c08e8d2dc53343f5c12d4e53aecf7ea2172 Mon Sep 17 00:00:00 2001 From: Alvar Penning Date: Sat, 23 Dec 2023 20:15:16 +0100 Subject: [PATCH 092/225] postfix: SMTP Smuggling Protection Enable Postfix SMTP Smuggling protection, introduced in Postfix 3.8.4, which is, currently, only available within the nixpkgs' master branch. - https://github.com/NixOS/nixpkgs/pull/276104 - https://github.com/NixOS/nixpkgs/pull/276264 For information about SMTP Smuggling: - https://www.postfix.org/smtp-smuggling.html - https://www.postfix.org/postconf.5.html#smtpd_forbid_bare_newline --- default.nix | 15 +++++++++++++++ mail-server/postfix.nix | 1 + 2 files changed, 16 insertions(+) diff --git a/default.nix b/default.nix index fdfaee3..3abdfbc 100644 --- a/default.nix +++ b/default.nix @@ -955,6 +955,21 @@ in ''; }; + smtpdForbidBareNewline = mkOption { + type = types.bool; + default = true; + description = '' + With "smtpd_forbid_bare_newline = yes", the Postfix SMTP server + disconnects a remote SMTP client that sends a line ending in a 'bare + newline'. + + This feature was added in Postfix 3.8.4 against SMTP Smuggling and will + default to "yes" in Postfix 3.9. + + https://www.postfix.org/smtp-smuggling.html + ''; + }; + sendingFqdn = mkOption { type = types.str; default = cfg.fqdn; diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 9362e2c..4967e2d 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -309,6 +309,7 @@ in milter_protocol = "6"; milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"; + smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline; }; submissionOptions = submissionOptions; From b5023b36a1f6628865cb42b4353bd2ddde0ea9f4 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 27 Dec 2023 09:46:26 +0100 Subject: [PATCH 093/225] postfix: exclude $mynetwork from smtpd_forbid_bare_newline --- mail-server/postfix.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 4967e2d..c050736 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -309,7 +309,9 @@ in milter_protocol = "6"; milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"; + # Fix for https://www.postfix.org/smtp-smuggling.html smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline; + smtpd_forbid_bare_newline_exclusions = "$mynetworks"; }; submissionOptions = submissionOptions; From e47f3719f1db3e0961a4358d4cb234a0acaa7baf Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Tue, 19 Dec 2023 23:07:52 +0100 Subject: [PATCH 094/225] Release 23.11 --- .hydra/declarative-jobsets.nix | 2 +- README.md | 8 +++---- docs/release-notes.rst | 5 ++++ flake.lock | 44 +++++++++++++++++----------------- flake.nix | 9 +++++-- tests/external.nix | 1 - 6 files changed, 39 insertions(+), 30 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 10ed381..f3da570 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -32,8 +32,8 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; - "nixos-22.11" = mkFlakeJobset "nixos-22.11"; "nixos-23.05" = mkFlakeJobset "nixos-23.05"; + "nixos-23.11" = mkFlakeJobset "nixos-23.11"; }; log = { diff --git a/README.md b/README.md index 5e8db21..a2104d3 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. +* For NixOS 23.11 + - Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11) + - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/) + - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/release-notes.html#nixos-23-11) * For NixOS 23.05 - Use the [SNM branch `nixos-23.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.05) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/release-notes.html#nixos-23-05) -* For NixOS 22.11 - - Use the [SNM branch `nixos-22.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.11) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/) - - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/release-notes.html#nixos-22-11) * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 4e4687b..0d6a22c 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,6 +1,11 @@ Release Notes ============= +NixOS 23.11 +----------- + +- Add basic support for LDAP users +- Add support for regex (PCRE) aliases NixOS 23.05 ----------- diff --git a/flake.lock b/flake.lock index 29711c4..f42bed1 100644 --- a/flake.lock +++ b/flake.lock @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1670751203, - "narHash": "sha256-XdoH1v3shKDGlrwjgrNX/EN8s3c+kQV7xY6cLCE8vcI=", + "lastModified": 1705856552, + "narHash": "sha256-JXfnuEf5Yd6bhMs/uvM67/joxYKoysyE3M2k6T3eWbg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "64e0bf055f9d25928c31fb12924e59ff8ce71e60", + "rev": "612f97239e2cc474c13c9dafa0df378058c5ad8d", "type": "github" }, "original": { @@ -47,28 +47,13 @@ "type": "indirect" } }, - "nixpkgs-22_11": { - "locked": { - "lastModified": 1669558522, - "narHash": "sha256-yqxn+wOiPqe6cxzOo4leeJOp1bXE/fjPEi/3F/bBHv8=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "ce5fe99df1f15a09a91a86be9738d68fadfbad82", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "ref": "nixos-22.11", - "type": "indirect" - } - }, "nixpkgs-23_05": { "locked": { - "lastModified": 1684782344, - "narHash": "sha256-SHN8hPYYSX0thDrMLMWPWYulK3YFgASOrCsIL3AJ78g=", + "lastModified": 1704290814, + "narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8966c43feba2c701ed624302b6a935f97bcbdf88", + "rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421", "type": "github" }, "original": { @@ -77,13 +62,28 @@ "type": "indirect" } }, + "nixpkgs-23_11": { + "locked": { + "lastModified": 1706098335, + "narHash": "sha256-r3dWjT8P9/Ah5m5ul4WqIWD8muj5F+/gbCdjiNVBKmU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a77ab169a83a4175169d78684ddd2e54486ac651", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-23.11", + "type": "indirect" + } + }, "root": { "inputs": { "blobs": "blobs", "flake-compat": "flake-compat", "nixpkgs": "nixpkgs", - "nixpkgs-22_11": "nixpkgs-22_11", "nixpkgs-23_05": "nixpkgs-23_05", + "nixpkgs-23_11": "nixpkgs-23_11", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index 54d747e..88ae3d9 100644 --- a/flake.nix +++ b/flake.nix @@ -8,15 +8,15 @@ }; utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; - nixpkgs-22_11.url = "flake:nixpkgs/nixos-22.11"; nixpkgs-23_05.url = "flake:nixpkgs/nixos-23.05"; + nixpkgs-23_11.url = "flake:nixpkgs/nixos-23.11"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_11, nixpkgs-23_05, ... }: let + outputs = { self, utils, blobs, nixpkgs, nixpkgs-23_05, nixpkgs-23_11, ... }: let lib = nixpkgs.lib; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; @@ -29,6 +29,10 @@ name = "23.05"; pkgs = nixpkgs-23_05.legacyPackages.${system}; } + { + name = "23.11"; + pkgs = nixpkgs-23_11.legacyPackages.${system}; + } ]; testNames = [ "internal" @@ -91,6 +95,7 @@ sphinx sphinx_rtd_theme myst-parser + linkify-it-py ]) )]; buildPhase = '' diff --git a/tests/external.nix b/tests/external.nix index 6c03144..725e46e 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -501,7 +501,6 @@ pkgs.nixosTest { with subtest("dmarc reporting"): server.systemctl("start rspamd-dmarc-reporter.service") - server.wait_until_succeeds("journalctl -eu rspamd-dmarc-reporter.service -o cat | grep -q 'No reports for '") with subtest("no warnings or errors"): server.fail("journalctl -u postfix | grep -i error >&2") From 9e36323ae3dde787f761420465c3ae560f3dbf29 Mon Sep 17 00:00:00 2001 From: Sleepful Date: Tue, 30 Jan 2024 00:43:21 -0600 Subject: [PATCH 095/225] Update roundcube example configuration: smtp_server is deprecated Related issue on GH: https://github.com/roundcube/roundcubemail/issues/8756 --- docs/add-roundcube.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/add-roundcube.rst b/docs/add-roundcube.rst index 4e6be83..6b10d5b 100644 --- a/docs/add-roundcube.rst +++ b/docs/add-roundcube.rst @@ -20,7 +20,7 @@ servers may require more work. extraConfig = '' # starttls needed for authentication, so the fqdn required to match # the certificate - $config['smtp_server'] = "tls://${config.mailserver.fqdn}"; + $config['smtp_host'] = "tls://${config.mailserver.fqdn}"; $config['smtp_user'] = "%u"; $config['smtp_pass'] = "%p"; ''; From 572c1b4d69deea1093ac231c37927cfa8ccad477 Mon Sep 17 00:00:00 2001 From: Christian Theune Date: Fri, 8 Mar 2024 14:52:52 +0100 Subject: [PATCH 096/225] rspamd: fix duplicate and syntactically wrong header settings Fixes #280 --- mail-server/rspamd.nix | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index a506904..ee1b8a5 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -30,7 +30,7 @@ in inherit debug; locals = { "milter_headers.conf" = { text = '' - extended_spam_headers = yes; + extended_spam_headers = true; ''; }; "redis.conf" = { text = '' servers = "${cfg.redis.address}:${toString cfg.redis.port}"; @@ -69,14 +69,6 @@ in ''; }; }; - overrides = { - "milter_headers.conf" = { - text = '' - extended_spam_headers = true; - ''; - }; - }; - workers.rspamd_proxy = { type = "rspamd_proxy"; bindSockets = [{ From fe6d325397f35eecb51bc77d04cc1a805c98e249 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Sun, 14 Jan 2024 20:55:20 +0100 Subject: [PATCH 097/225] dovecot: support new `sieve` API in nixpkgs Since https://github.com/NixOS/nixpkgs/pull/275031 things have became more structured when it comes to the sieve plugin. Relies on https://github.com/NixOS/nixpkgs/pull/281001 for full features. --- mail-server/dovecot.nix | 54 +++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 7d73ee2..45c9d41 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -175,8 +175,18 @@ in mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ]; protocols = lib.optional cfg.enableManageSieve "sieve"; - sieveScripts = { - after = builtins.toFile "spam.sieve" '' + pluginSettings = { + sieve = "file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve"; + sieve_default = "file:${cfg.sieveDirectory}/%u/default.sieve"; + sieve_default_name = "default"; + }; + + sieve = { + extensions = [ + "fileinto" + ]; + + scripts.after = builtins.toFile "spam.sieve" '' require "fileinto"; if header :is "X-Spam" "Yes" { @@ -184,8 +194,26 @@ in stop; } ''; + + pipeBins = [ + pipeBin + ]; }; + imapsieve.mailbox = [ + { + name = junkMailboxName; + causes = [ "COPY" "APPEND" ]; + before = "${stateDir}/imap_sieve/report-spam.sieve"; + } + { + name = "*"; + from = junkMailboxName; + causes = [ "COPY" ]; + before = "${stateDir}/imap_sieve/report-ham.sieve"; + } + ]; + mailboxes = cfg.mailboxes; extraConfig = '' @@ -307,28 +335,6 @@ in inbox = yes } - plugin { - sieve_plugins = sieve_imapsieve sieve_extprograms - sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve - sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve - sieve_default_name = default - - # From elsewhere to Spam folder - imapsieve_mailbox1_name = ${junkMailboxName} - imapsieve_mailbox1_causes = COPY,APPEND - imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve - - # From Spam folder to elsewhere - imapsieve_mailbox2_name = * - imapsieve_mailbox2_from = ${junkMailboxName} - imapsieve_mailbox2_causes = COPY - imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve - - sieve_pipe_bin_dir = ${pipeBin}/pipe/bin - - sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment - } - ${lib.optionalString cfg.fullTextSearch.enable '' plugin { plugin = fts fts_xapian From d507bd9c9571001054792dd34099fa500f4573c1 Mon Sep 17 00:00:00 2001 From: Gaetan Lepage Date: Mon, 4 Mar 2024 08:14:21 +0100 Subject: [PATCH 098/225] dovecot: no longer need to copy sieve scripts --- mail-server/dovecot.nix | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 45c9d41..6459846 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -195,8 +195,11 @@ in } ''; - pipeBins = [ - pipeBin + pipeBins = map lib.getExe [ + (pkgs.writeShellScriptBin "sa-learn-ham.sh" + "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham") + (pkgs.writeShellScriptBin "sa-learn-spam.sh" + "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam") ]; }; @@ -204,13 +207,13 @@ in { name = junkMailboxName; causes = [ "COPY" "APPEND" ]; - before = "${stateDir}/imap_sieve/report-spam.sieve"; + before = ./dovecot/imap_sieve/report-spam.sieve; } { name = "*"; from = junkMailboxName; causes = [ "COPY" ]; - before = "${stateDir}/imap_sieve/report-ham.sieve"; + before = ./dovecot/imap_sieve/report-ham.sieve; } ]; @@ -363,13 +366,6 @@ in systemd.services.dovecot2 = { preStart = '' ${genPasswdScript} - rm -rf '${stateDir}/imap_sieve' - mkdir '${stateDir}/imap_sieve' - cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/' - for k in "${stateDir}/imap_sieve"/*.sieve ; do - ${pkgs.dovecot_pigeonhole}/bin/sievec "$k" - done - chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve' '' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); }; From 799fe34c12ac4fee9f299c0840504991b20e0d45 Mon Sep 17 00:00:00 2001 From: Gaetan Lepage Date: Mon, 4 Mar 2024 08:20:47 +0100 Subject: [PATCH 099/225] Update nixpkgs --- flake.lock | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/flake.lock b/flake.lock index f42bed1..d4362e6 100644 --- a/flake.lock +++ b/flake.lock @@ -19,11 +19,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1668681692, - "narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=", + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", "owner": "edolstra", "repo": "flake-compat", - "rev": "009399224d5e398d03b22badca40a37ac85412a1", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", "type": "github" }, "original": { @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1705856552, - "narHash": "sha256-JXfnuEf5Yd6bhMs/uvM67/joxYKoysyE3M2k6T3eWbg=", + "lastModified": 1709703039, + "narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "612f97239e2cc474c13c9dafa0df378058c5ad8d", + "rev": "9df3e30ce24fd28c7b3e2de0d986769db5d6225d", "type": "github" }, "original": { @@ -64,11 +64,11 @@ }, "nixpkgs-23_11": { "locked": { - "lastModified": 1706098335, - "narHash": "sha256-r3dWjT8P9/Ah5m5ul4WqIWD8muj5F+/gbCdjiNVBKmU=", + "lastModified": 1709884566, + "narHash": "sha256-NSYJg2sfdO/XS3L8XN/59Zhzn0dqWm7XtVnKI2mHq3w=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a77ab169a83a4175169d78684ddd2e54486ac651", + "rev": "2be119add7b37dc535da2dd4cba68e2cf8d1517e", "type": "github" }, "original": { @@ -87,13 +87,31 @@ "utils": "utils" } }, - "utils": { + "systems": { "locked": { - "lastModified": 1605370193, - "narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=", + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1709126324, + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", "owner": "numtide", "repo": "flake-utils", - "rev": "5021eac20303a61fafe17224c087f5519baed54d", + "rev": "d465f4819400de7c8d874d50b982301f28a84605", "type": "github" }, "original": { From 79c8cfcd5873a85559da6201b116fb38b490d030 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Thu, 14 Mar 2024 21:14:35 +0100 Subject: [PATCH 100/225] Remove the support of 23.05 and 23.11 This is because SNM now supports the new sieve nixpkgs interface, which is not backward compatible with previous releases. --- flake.lock | 32 -------------------------------- flake.nix | 12 +----------- 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/flake.lock b/flake.lock index d4362e6..8b9ab6f 100644 --- a/flake.lock +++ b/flake.lock @@ -47,43 +47,11 @@ "type": "indirect" } }, - "nixpkgs-23_05": { - "locked": { - "lastModified": 1704290814, - "narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "ref": "nixos-23.05", - "type": "indirect" - } - }, - "nixpkgs-23_11": { - "locked": { - "lastModified": 1709884566, - "narHash": "sha256-NSYJg2sfdO/XS3L8XN/59Zhzn0dqWm7XtVnKI2mHq3w=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "2be119add7b37dc535da2dd4cba68e2cf8d1517e", - "type": "github" - }, - "original": { - "id": "nixpkgs", - "ref": "nixos-23.11", - "type": "indirect" - } - }, "root": { "inputs": { "blobs": "blobs", "flake-compat": "flake-compat", "nixpkgs": "nixpkgs", - "nixpkgs-23_05": "nixpkgs-23_05", - "nixpkgs-23_11": "nixpkgs-23_11", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index 88ae3d9..4a8b52d 100644 --- a/flake.nix +++ b/flake.nix @@ -8,15 +8,13 @@ }; utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; - nixpkgs-23_05.url = "flake:nixpkgs/nixos-23.05"; - nixpkgs-23_11.url = "flake:nixpkgs/nixos-23.11"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, utils, blobs, nixpkgs, nixpkgs-23_05, nixpkgs-23_11, ... }: let + outputs = { self, utils, blobs, nixpkgs, ... }: let lib = nixpkgs.lib; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; @@ -25,14 +23,6 @@ name = "unstable"; pkgs = nixpkgs.legacyPackages.${system}; } - { - name = "23.05"; - pkgs = nixpkgs-23_05.legacyPackages.${system}; - } - { - name = "23.11"; - pkgs = nixpkgs-23_11.legacyPackages.${system}; - } ]; testNames = [ "internal" From 9f6635a0351c190179dc6904545f950108a23dd8 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sat, 13 Apr 2024 12:42:45 +0000 Subject: [PATCH 101/225] Drop default acmeRoot --- mail-server/nginx.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index e5fa597..4f0cb1a 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -21,7 +21,6 @@ with (import ./common.nix { inherit config; }); let cfg = config.mailserver; - acmeRoot = "/var/lib/acme/acme-challenge"; in { config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) { @@ -32,7 +31,6 @@ in serverAliases = cfg.certificateDomains; forceSSL = true; enableACME = true; - acmeRoot = acmeRoot; }; }; From ef4756bcfc8a6791adbcae2b32f87e2f0a00525d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20J=C3=A4ckel?= Date: Sat, 13 Apr 2024 16:08:58 +0200 Subject: [PATCH 102/225] Quote ldap password Otherwise special characters like # do not work --- mail-server/common.nix | 5 +++-- mail-server/dovecot.nix | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mail-server/common.nix b/mail-server/common.nix index 236530b..edea7f0 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -49,7 +49,7 @@ in # Appends the LDAP bind password to files to avoid writing this # password into the Nix store. appendLdapBindPwd = { - name, file, prefix, passwordFile, destination + name, file, prefix, suffix ? "", passwordFile, destination }: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' #!${pkgs.stdenv.shell} set -euo pipefail @@ -61,8 +61,9 @@ in fi cat ${file} > ${destination} - echo -n "${prefix}" >> ${destination} + echo -n '${prefix}' >> ${destination} cat ${passwordFile} >> ${destination} + echo -n '${suffix}' >> ${destination} chmod 600 ${destination} ''; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 6459846..a6251fd 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -90,7 +90,8 @@ let setPwdInLdapConfFile = appendLdapBindPwd { name = "ldap-conf-file"; file = ldapConfig; - prefix = "dnpass = "; + prefix = ''dnpass = "''; + suffix = ''"''; passwordFile = cfg.ldap.bind.passwordFile; destination = ldapConfFile; }; From 41059fc548088e49e3ddb3a2b4faeb5de018e60f Mon Sep 17 00:00:00 2001 From: jopejoe1 Date: Fri, 3 May 2024 09:14:16 +0200 Subject: [PATCH 103/225] docs: use settings instead of config in radicale --- docs/add-radicale.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/add-radicale.rst b/docs/add-radicale.rst index 2393f6e..cf98333 100644 --- a/docs/add-radicale.rst +++ b/docs/add-radicale.rst @@ -24,12 +24,13 @@ have to be used. These can still be generated using `mkpasswd -m bcrypt`. in { services.radicale = { enable = true; - config = '' - [auth] - type = htpasswd - htpasswd_filename = ${htpasswd} - htpasswd_encryption = bcrypt - ''; + settings = { + auth = { + type = "htpasswd"; + htpasswd_filename = "${htpasswd}"; + htpasswd_encryption = "bcrypt"; + }; + }; }; services.nginx = { From 46a0829aa82c5a56e6b6c24aa8d8046c52a716a4 Mon Sep 17 00:00:00 2001 From: Matthew Leach Date: Wed, 28 Jun 2023 20:42:37 +0100 Subject: [PATCH 104/225] acme: Add new option acmeCertificateName Allow the user to specify the name of the ACME configuration that the mailserver should use. This allows users that request certificates that aren't the FQDN of the mailserver, for example a wildcard certificate. --- default.nix | 13 +++++++++++++ mail-server/assertions.nix | 5 +++++ mail-server/common.nix | 4 ++-- mail-server/nginx.nix | 4 ++-- 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/default.nix b/default.nix index 3abdfbc..6bd499c 100644 --- a/default.nix +++ b/default.nix @@ -675,6 +675,19 @@ in ''; }; + acmeCertificateName = mkOption { + type = types.str; + default = cfg.fqdn; + example = "example.com"; + description = '' + ({option}`mailserver.certificateScheme` == `acme`) + + When the `acme` `certificateScheme` is selected, you can use this option + to override the default certificate name. This is useful if you've + generated a wildcard certificate, for example. + ''; + }; + enableImap = mkOption { type = types.bool; default = true; diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index d2c44ea..2b4b262 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -13,5 +13,10 @@ assertion = config.mailserver.forwards == {}; message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.forwards"; } + ] ++ lib.optionals (config.mailserver.certificateScheme != "acme") [ + { + assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; + message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName"; + } ]; } diff --git a/mail-server/common.nix b/mail-server/common.nix index edea7f0..4e301c5 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -26,7 +26,7 @@ in 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.fqdn}.directory}/fullchain.pem" + then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem" else throw "unknown certificate scheme"; # key :: PATH @@ -35,7 +35,7 @@ in 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.fqdn}.directory}/key.pem" + then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem" else throw "unknown certificate scheme"; passwordFiles = let diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index 4f0cb1a..a037f56 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -17,7 +17,7 @@ { config, pkgs, lib, ... }: -with (import ./common.nix { inherit config; }); +with (import ./common.nix { inherit config lib pkgs; }); let cfg = config.mailserver; @@ -34,7 +34,7 @@ in }; }; - security.acme.certs."${cfg.fqdn}".reloadServices = [ + security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [ "postfix.service" "dovecot2.service" ]; From ed80b589d303496f2329e2e6019a95ee2500b580 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 3 Jun 2024 12:34:43 +0200 Subject: [PATCH 105/225] postfix: remove deprecated smtpd_tls_eecdh_grade Causes a warning that suggests to just leave it at its default. --- mail-server/postfix.nix | 3 --- 1 file changed, 3 deletions(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index c050736..351da81 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -274,9 +274,6 @@ in # Submission by mail clients is handled in submissionOptions smtpd_tls_security_level = "may"; - # strong might suffice and is computationally less expensive - smtpd_tls_eecdh_grade = "ultra"; - # Disable obselete protocols smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; From 0d51a32e4799d081f260eb4db37145f5f4ee7456 Mon Sep 17 00:00:00 2001 From: RoastedCheese Date: Tue, 4 Jun 2024 15:31:28 +0000 Subject: [PATCH 106/225] acme: test acmeCertificateName if module is enabled --- mail-server/assertions.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index 2b4b262..0e5b15b 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -13,7 +13,7 @@ assertion = config.mailserver.forwards == {}; message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.forwards"; } - ] ++ lib.optionals (config.mailserver.certificateScheme != "acme") [ + ] ++ lib.optionals (config.mailserver.enable && config.mailserver.certificateScheme != "acme") [ { assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName"; From 29916981e7b3b5782dc5085ad18490113f8ff63b Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Mon, 3 Jun 2024 11:57:22 +0200 Subject: [PATCH 107/225] Release 24.05 --- .hydra/declarative-jobsets.nix | 2 +- README.md | 8 ++++---- docs/release-notes.rst | 6 ++++++ flake.lock | 22 +++++++++++++++++++--- flake.nix | 7 ++++++- tests/external.nix | 2 +- tests/internal.nix | 2 +- 7 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index f3da570..86ddad2 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -32,8 +32,8 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; - "nixos-23.05" = mkFlakeJobset "nixos-23.05"; "nixos-23.11" = mkFlakeJobset "nixos-23.11"; + "nixos-24.05" = mkFlakeJobset "nixos-24.05"; }; log = { diff --git a/README.md b/README.md index a2104d3..f8c7318 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. +* For NixOS 24.05 + - Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05) + - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/) + - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05) * For NixOS 23.11 - Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/release-notes.html#nixos-23-11) -* For NixOS 23.05 - - Use the [SNM branch `nixos-23.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.05) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/) - - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/release-notes.html#nixos-23-05) * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 0d6a22c..5d6088c 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,6 +1,12 @@ Release Notes ============= +NixOS 24.05 +----------- + +- Add new option ``acmeCertificateName`` which can be used to support + wildcard certificates + NixOS 23.11 ----------- diff --git a/flake.lock b/flake.lock index 8b9ab6f..a21958d 100644 --- a/flake.lock +++ b/flake.lock @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1709703039, - "narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=", + "lastModified": 1717602782, + "narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9df3e30ce24fd28c7b3e2de0d986769db5d6225d", + "rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6", "type": "github" }, "original": { @@ -47,11 +47,27 @@ "type": "indirect" } }, + "nixpkgs-24_05": { + "locked": { + "lastModified": 1717144377, + "narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "805a384895c696f802a9bf5bf4720f37385df547", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-24.05", + "type": "indirect" + } + }, "root": { "inputs": { "blobs": "blobs", "flake-compat": "flake-compat", "nixpkgs": "nixpkgs", + "nixpkgs-24_05": "nixpkgs-24_05", "utils": "utils" } }, diff --git a/flake.nix b/flake.nix index 4a8b52d..a090f9a 100644 --- a/flake.nix +++ b/flake.nix @@ -8,13 +8,14 @@ }; utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; + nixpkgs-24_05.url = "flake:nixpkgs/nixos-24.05"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, utils, blobs, nixpkgs, ... }: let + outputs = { self, utils, blobs, nixpkgs, nixpkgs-24_05, ... }: let lib = nixpkgs.lib; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; @@ -23,6 +24,10 @@ name = "unstable"; pkgs = nixpkgs.legacyPackages.${system}; } + { + name = "24.05"; + pkgs = nixpkgs-24_05.legacyPackages.${system}; + } ]; testNames = [ "internal" diff --git a/tests/external.nix b/tests/external.nix index 725e46e..b56101a 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -508,7 +508,7 @@ pkgs.nixosTest { server.fail("journalctl -u dovecot2 | 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 -i warning >&2" + "journalctl -u dovecot2 |grep -v 'Expunged message reappeared, giving a new UID'| grep -v 'FTS Xapian: Box is empty' | grep -i warning >&2" ) ''; } diff --git a/tests/internal.nix b/tests/internal.nix index 564b71a..5835ce6 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -177,7 +177,7 @@ pkgs.nixosTest { "set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) machine.succeed( - "cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q 'This account cannot receive emails'" + "cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q '554 5.5.0 Error'" ) with subtest("rspamd controller serves web ui"): From 54cbacb6eb9938bf1eaab7a7840fb527050c2af1 Mon Sep 17 00:00:00 2001 From: isabel Date: Fri, 14 Jun 2024 21:51:43 +0100 Subject: [PATCH 108/225] chore: remove flake utils --- flake.lock | 36 +----------------------------------- flake.nix | 3 +-- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/flake.lock b/flake.lock index a21958d..191417d 100644 --- a/flake.lock +++ b/flake.lock @@ -67,41 +67,7 @@ "blobs": "blobs", "flake-compat": "flake-compat", "nixpkgs": "nixpkgs", - "nixpkgs-24_05": "nixpkgs-24_05", - "utils": "utils" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1709126324, - "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "d465f4819400de7c8d874d50b982301f28a84605", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" + "nixpkgs-24_05": "nixpkgs-24_05" } } }, diff --git a/flake.nix b/flake.nix index a090f9a..faa307a 100644 --- a/flake.nix +++ b/flake.nix @@ -6,7 +6,6 @@ url = "github:edolstra/flake-compat"; flake = false; }; - utils.url = "github:numtide/flake-utils"; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; nixpkgs-24_05.url = "flake:nixpkgs/nixos-24.05"; blobs = { @@ -15,7 +14,7 @@ }; }; - outputs = { self, utils, blobs, nixpkgs, nixpkgs-24_05, ... }: let + outputs = { self, blobs, nixpkgs, nixpkgs-24_05, ... }: let lib = nixpkgs.lib; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; From 290a995de5c3d3f08468fa548f0d55ab2efc7b6b Mon Sep 17 00:00:00 2001 From: Isabel Date: Fri, 14 Jun 2024 15:16:40 +0000 Subject: [PATCH 109/225] refactor: policyd-spf -> spf-engine --- mail-server/postfix.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 351da81..5a93dc2 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -325,7 +325,7 @@ in privileged = true; chroot = false; command = "spawn"; - args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"]; + args = [ "user=nobody" "argv=${pkgs.spf-engine}/bin/policyd-spf" "${policyd-spf}"]; }; "submission-header-cleanup" = { type = "unix"; From 059b50b2e729729ea00c6831124d3837c494f3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20J=C3=A4ckel?= Date: Sat, 13 Jul 2024 00:31:30 +0200 Subject: [PATCH 110/225] Allow setting userAttrs to empty string This allows overwriting the default values for user_attrs to be empty which is required when using virtual mailboxes with ldap accounts that have posixAccount attributes set. When user_attrs is empty string those are ignored then. --- default.nix | 2 +- mail-server/dovecot.nix | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index 6bd499c..45875a3 100644 --- a/default.nix +++ b/default.nix @@ -277,7 +277,7 @@ in dovecot = { userAttrs = mkOption { - type = types.str; + type = types.nullOr types.str; default = ""; description = '' LDAP attributes to be retrieved during userdb lookups. diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index a6251fd..11f2708 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -76,7 +76,7 @@ let auth_bind = yes base = ${cfg.ldap.searchBase} scope = ${mkLdapSearchScope cfg.ldap.searchScope} - ${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") '' + ${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) '' user_attrs = ${cfg.ldap.dovecot.userAttrs} ''} user_filter = ${cfg.ldap.dovecot.userFilter} From af7d3bf5daeba3fc28089b015c0dd43f06b176f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20J=C3=A4ckel?= Date: Mon, 5 Aug 2024 19:00:00 +0200 Subject: [PATCH 111/225] Wrap rspamc to avoid having to specific socket manually --- docs/rspamd-tuning.rst | 9 +++------ mail-server/rspamd.nix | 9 +++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/rspamd-tuning.rst b/docs/rspamd-tuning.rst index 049858d..3ba8133 100644 --- a/docs/rspamd-tuning.rst +++ b/docs/rspamd-tuning.rst @@ -24,17 +24,14 @@ You can run the training in a root shell as follows: .. code:: bash - # Path to the controller socket - export RSOCK="/var/run/rspamd/worker-controller.sock" - # Learn the Junk folder as spam - rspamc -h $RSOCK learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/ + rspamc learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/ # Learn the INBOX as ham - rspamc -h $RSOCK learn_ham /var/vmail/$DOMAIN/$USER/cur/ + rspamc learn_ham /var/vmail/$DOMAIN/$USER/cur/ # Check that training was successful - rspamc -h $RSOCK stat | grep learned + rspamc stat | grep learned Tune symbol weight ~~~~~~~~~~~~~~~~~~ diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index ee1b8a5..8fb9b00 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -25,6 +25,15 @@ let 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 /var/run/rspamd/worker-controller.sock" + '') + ]; + services.rspamd = { enable = true; inherit debug; From 3a082011dcf28b5af1deb8f957c9fb70f65c88bd Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Sat, 23 Nov 2024 12:00:00 +0000 Subject: [PATCH 112/225] recent nixos-unstable requires larger dh params --- tests/lib/config.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/config.nix b/tests/lib/config.nix index 2cdc9d2..68a1b2e 100644 --- a/tests/lib/config.nix +++ b/tests/lib/config.nix @@ -1,3 +1,3 @@ { - security.dhparams.defaultBitSize = 1024; # minimum size required by dovecot + security.dhparams.defaultBitSize = 2048; # minimum size required by dovecot } From e901c5684978bfed0481da48f222de3e80ed9d7c Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Sat, 23 Nov 2024 12:00:00 +0000 Subject: [PATCH 113/225] services.dnsmasq.extraConfig was removed on nixos-unstable --- tests/multiple.nix | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/multiple.nix b/tests/multiple.nix index daf468a..8a4c07b 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -30,12 +30,7 @@ let }; services.dnsmasq = { enable = true; - # Fixme: once nixos-22.11 has been removed, could be replaced by - # settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ]; - extraConfig = '' - mx-host=domain1.com,domain1,10 - mx-host=domain2.com,domain2,10 - ''; + settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ]; }; }; From 9919033068cce7f97022df80c8087b7725ddebba Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Sat, 23 Nov 2024 12:00:00 +0000 Subject: [PATCH 114/225] tests: make the emails sent by mail-check.py look less like spam rspamd complains that these emails miss these headers --- scripts/mail-check.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/mail-check.py b/scripts/mail-check.py index 0a96ce1..7d3935c 100644 --- a/scripts/mail-check.py +++ b/scripts/mail-check.py @@ -5,6 +5,7 @@ import uuid import imaplib from datetime import datetime, timedelta import email +import email.utils import time RETRY = 100 @@ -15,11 +16,16 @@ def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr "From: {from_addr}", "To: {to_addr}", "Subject: {subject}", + "Message-ID: {random}@mail-check.py", + "Date: {date}", "", "This validates our mail server can send to Gmail :/"]).format( from_addr=from_addr, to_addr=to_addr, - subject=subject) + subject=subject, + random=str(uuid.uuid4()), + date=email.utils.formatdate(), + ) retry = RETRY From 0a801316cdedda084942bce79dc0493f0e16c4bf Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Sat, 23 Nov 2024 12:00:00 +0000 Subject: [PATCH 115/225] tests: ignore debug message that looks like an error --- tests/external.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/external.nix b/tests/external.nix index b56101a..77f807d 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -505,7 +505,7 @@ pkgs.nixosTest { 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 -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 'FTS Xapian: Box is empty' | grep -i warning >&2" From 1cf6d019895b29f3f9dd3a9537223f9dc56dedff Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Sat, 23 Nov 2024 12:00:00 +0000 Subject: [PATCH 116/225] nix flake update --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 191417d..87d65ac 100644 --- a/flake.lock +++ b/flake.lock @@ -34,11 +34,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1717602782, - "narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=", + "lastModified": 1732014248, + "narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6", + "rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", "type": "github" }, "original": { @@ -49,11 +49,11 @@ }, "nixpkgs-24_05": { "locked": { - "lastModified": 1717144377, - "narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=", + "lastModified": 1731797254, + "narHash": "sha256-df3dJApLPhd11AlueuoN0Q4fHo/hagP75LlM5K1sz9g=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "805a384895c696f802a9bf5bf4720f37385df547", + "rev": "e8c38b73aeb218e27163376a2d617e61a2ad9b59", "type": "github" }, "original": { From e4aabd3de6917e8263252ff1e5be3ce8d725fe9a Mon Sep 17 00:00:00 2001 From: Jany Doe Date: Sat, 9 Nov 2024 22:39:31 +0000 Subject: [PATCH 117/225] remove new line character if use agenix --- mail-server/common.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/common.nix b/mail-server/common.nix index 4e301c5..813a5f4 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -62,7 +62,7 @@ in cat ${file} > ${destination} echo -n '${prefix}' >> ${destination} - cat ${passwordFile} >> ${destination} + cat ${passwordFile} | tr -d '\n' >> ${destination} echo -n '${suffix}' >> ${destination} chmod 600 ${destination} ''; From 6db6c0dc728d442fe4e639fbfeb45e74e6ee8a22 Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Mon, 16 Dec 2024 17:35:11 +0000 Subject: [PATCH 118/225] Add instructions about creating a `AAAA` record --- docs/setup-guide.rst | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 61b1559..52cbfb8 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -20,25 +20,30 @@ an up and running mail server. Once the server is deployed, we could then set all DNS entries required to send and receive mails on this server. -Setup DNS A record for server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Setup DNS A/AAAA records for server +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Add a DNS record to the domain ``example.com`` with the following +Add DNS records to the domain ``example.com`` with the following entries ==================== ===== ==== ============= Name (Subdomain) TTL Type Value ==================== ===== ==== ============= ``mail.example.com`` 10800 A ``1.2.3.4`` +``mail.example.com`` 10800 AAAA ``2001::1`` ==================== ===== ==== ============= +If your server does not have an IPv6 address, you must skip the `AAAA` record. + You can check this with :: - $ ping mail.example.com - 64 bytes from mail.example.com (1.2.3.4): icmp_seq=1 ttl=46 time=21.3 ms - ... + $ nix-shell -p bind --command "host -t A mail.example.com" + mail.example.com has address 1.2.3.4 + + $ nix-shell -p bind --command "host -t AAAA mail.example.com" + mail.example.com has address 2001::1 Note that it can take a while until a DNS entry is propagated. This DNS entry is required for the Let's Encrypt certificate generation @@ -98,8 +103,11 @@ Set rDNS (reverse DNS) entry for server ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Wherever you have rented your server, you should be able to set reverse -DNS entries for the IP’s you own. Add an entry resolving ``1.2.3.4`` -to ``mail.example.com``. +DNS entries for the IP’s you own: + +- Add an entry resolving IPv4 address ``1.2.3.4`` to ``mail.example.com``. +- Add an entry resolving IPv6 ``2001::1`` to ``mail.example.com``. Again, this + must be skipped if your server does not have an IPv6 address. .. warning:: @@ -115,6 +123,9 @@ You can check this with $ nix-shell -p bind --command "host 1.2.3.4" 4.3.2.1.in-addr.arpa domain name pointer mail.example.com. + $ nix-shell -p bind --command "host 2001::1" + 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.2.ip6.arpa domain name pointer mail.example.com. + Note that it can take a while until a DNS entry is propagated. Set a ``MX`` record From c43d8c4a3ce84a7bebd110b06e69365484db6208 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sat, 10 Aug 2024 21:44:47 +0000 Subject: [PATCH 119/225] Fix wrong userAttrs default --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 45875a3..5d189f8 100644 --- a/default.nix +++ b/default.nix @@ -278,7 +278,7 @@ in dovecot = { userAttrs = mkOption { type = types.nullOr types.str; - default = ""; + default = null; description = '' LDAP attributes to be retrieved during userdb lookups. From 26a56d0a8f21b567c1b258dae301bfff52ef8b45 Mon Sep 17 00:00:00 2001 From: lennart Date: Fri, 20 Dec 2024 00:15:57 +0100 Subject: [PATCH 120/225] Fix example for rejectSender A domain prepended with an at sign does not work to reject senders on domain level. Thus misleading documentation is fixed by removing it. --- default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/default.nix b/default.nix index 5d189f8..ac75bce 100644 --- a/default.nix +++ b/default.nix @@ -506,7 +506,7 @@ in 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. From 63209b1def2c9fc891ad271f474a3464a5833294 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Mon, 16 Dec 2024 18:45:45 +0100 Subject: [PATCH 121/225] Release 24.11 --- .hydra/declarative-jobsets.nix | 2 +- README.md | 8 ++++---- docs/release-notes.rst | 5 +++++ docs/setup-guide.rst | 4 ++-- flake.lock | 12 ++++++------ flake.nix | 8 ++++---- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 86ddad2..0a158cf 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -32,8 +32,8 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; - "nixos-23.11" = mkFlakeJobset "nixos-23.11"; "nixos-24.05" = mkFlakeJobset "nixos-24.05"; + "nixos-24.11" = mkFlakeJobset "nixos-24.11"; }; log = { diff --git a/README.md b/README.md index f8c7318..098f68a 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. +* For NixOS 24.11 + - Use the [SNM branch `nixos-24.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.11) + - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/) + - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/release-notes.html#nixos-24-11) * For NixOS 24.05 - Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05) -* For NixOS 23.11 - - Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/) - - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/release-notes.html#nixos-23-11) * For NixOS unstable - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 5d6088c..806de8e 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,6 +1,11 @@ Release Notes ============= +NixOS 24.11 +----------- + +- No new feature, only bug fixes and documentation improvements + NixOS 24.05 ----------- diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 52cbfb8..08de1b3 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -63,9 +63,9 @@ common ones. imports = [ (builtins.fetchTarball { # Pick a release version you are interested in and set its hash, e.g. - url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz"; + url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-24.11/nixos-mailserver-nixos-24.11.tar.gz"; # To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command: - # release="nixos-23.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack + # release="nixos-24.11"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack sha256 = "0000000000000000000000000000000000000000000000000000"; }) ]; diff --git a/flake.lock b/flake.lock index 87d65ac..c6ec247 100644 --- a/flake.lock +++ b/flake.lock @@ -47,18 +47,18 @@ "type": "indirect" } }, - "nixpkgs-24_05": { + "nixpkgs-24_11": { "locked": { - "lastModified": 1731797254, - "narHash": "sha256-df3dJApLPhd11AlueuoN0Q4fHo/hagP75LlM5K1sz9g=", + "lastModified": 1734083684, + "narHash": "sha256-5fNndbndxSx5d+C/D0p/VF32xDiJCJzyOqorOYW4JEo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e8c38b73aeb218e27163376a2d617e61a2ad9b59", + "rev": "314e12ba369ccdb9b352a4db26ff419f7c49fa84", "type": "github" }, "original": { "id": "nixpkgs", - "ref": "nixos-24.05", + "ref": "nixos-24.11", "type": "indirect" } }, @@ -67,7 +67,7 @@ "blobs": "blobs", "flake-compat": "flake-compat", "nixpkgs": "nixpkgs", - "nixpkgs-24_05": "nixpkgs-24_05" + "nixpkgs-24_11": "nixpkgs-24_11" } } }, diff --git a/flake.nix b/flake.nix index faa307a..6fb5637 100644 --- a/flake.nix +++ b/flake.nix @@ -7,14 +7,14 @@ flake = false; }; nixpkgs.url = "flake:nixpkgs/nixos-unstable"; - nixpkgs-24_05.url = "flake:nixpkgs/nixos-24.05"; + nixpkgs-24_11.url = "flake:nixpkgs/nixos-24.11"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, blobs, nixpkgs, nixpkgs-24_05, ... }: let + outputs = { self, blobs, nixpkgs, nixpkgs-24_11, ... }: let lib = nixpkgs.lib; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; @@ -24,8 +24,8 @@ pkgs = nixpkgs.legacyPackages.${system}; } { - name = "24.05"; - pkgs = nixpkgs-24_05.legacyPackages.${system}; + name = "24.11"; + pkgs = nixpkgs-24_11.legacyPackages.${system}; } ]; testNames = [ From 4a5eb4baea6aa7ed775cf3a842c62b060ab37adb Mon Sep 17 00:00:00 2001 From: Ryan Trinkle Date: Mon, 8 Apr 2024 13:33:50 +0000 Subject: [PATCH 122/225] Make LMTP memory limit configurable --- default.nix | 8 ++++++++ mail-server/dovecot.nix | 1 + 2 files changed, 9 insertions(+) diff --git a/default.nix b/default.nix index ac75bce..f8a1305 100644 --- a/default.nix +++ b/default.nix @@ -460,6 +460,14 @@ in ''; }; + lmtpMemoryLimit = mkOption { + type = types.int; + default = 256; + description = '' + The memory limit for the LMTP service, in megabytes. + ''; + }; + extraVirtualAliases = mkOption { type = let loginAccount = mkOptionType { diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 11f2708..05b5552 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -291,6 +291,7 @@ in mode = 0600 user = ${postfixCfg.user} } + vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB } recipient_delimiter = ${cfg.recipientDelimiter} From 87ffaad9a3c54560983d133923b2f4f4ebefdca2 Mon Sep 17 00:00:00 2001 From: Ryan Trinkle Date: Mon, 8 Apr 2024 14:03:19 +0000 Subject: [PATCH 123/225] Add quota-status memory limit --- default.nix | 8 ++++++++ mail-server/dovecot.nix | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/default.nix b/default.nix index f8a1305..dfcb36e 100644 --- a/default.nix +++ b/default.nix @@ -468,6 +468,14 @@ in ''; }; + quotaStatusMemoryLimit = mkOption { + type = types.int; + default = 256; + description = '' + The memory limit for the quota-status service, in megabytes. + ''; + }; + extraVirtualAliases = mkOption { type = let loginAccount = mkOptionType { diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 05b5552..e4829d1 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -294,6 +294,10 @@ in vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB } + service quota-status { + vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB + } + recipient_delimiter = ${cfg.recipientDelimiter} lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} From dc0569066e79ae96184541da6fa28f35a33fbf7b Mon Sep 17 00:00:00 2001 From: Ryan Trinkle Date: Mon, 8 Apr 2024 14:20:46 +0000 Subject: [PATCH 124/225] Make imap memory limit configurable --- default.nix | 8 ++++++++ mail-server/dovecot.nix | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/default.nix b/default.nix index dfcb36e..8eb9c5b 100644 --- a/default.nix +++ b/default.nix @@ -712,6 +712,14 @@ in ''; }; + imapMemoryLimit = mkOption { + type = types.int; + default = 256; + description = '' + The memory limit for the imap service, in megabytes. + ''; + }; + enableImapSsl = mkOption { type = types.bool; default = true; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index e4829d1..6e39923 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -276,6 +276,10 @@ in mail_plugins = $mail_plugins imap_sieve } + service imap { + vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB + } + protocol pop3 { mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} } From ade37b2765032f83d2d4bd50b6204a40a4c05eb4 Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Sat, 18 Jan 2025 12:00:00 +0000 Subject: [PATCH 125/225] fts xapian: adapt to newer versions fts xapian does not publish configuration changes in a changelog. As a result, some options that nixos mailserver was setting for it have been ignored for several years. New options (process_limit) are now recommended. This adapts the module to these changes. The default value of partial= is 2, but fts_xapian 1.8.3 now requires it to be at least 3, and fails loudly in case it is 2. As a result, this change is required to support fts_xapian 1.8.3 and later. --- default.nix | 23 +++++++++-------------- docs/fts.rst | 5 +---- mail-server/dovecot.nix | 7 ++++--- tests/external.nix | 4 ++-- 4 files changed, 16 insertions(+), 23 deletions(-) diff --git a/default.nix b/default.nix index 8eb9c5b..ad6a53a 100644 --- a/default.nix +++ b/default.nix @@ -395,12 +395,6 @@ in ''; }; - indexAttachments = mkOption { - type = types.bool; - default = false; - description = "Also index text-only attachements. Binary attachements are never indexed."; - }; - enforced = mkOption { type = types.enum [ "yes" "no" "body" ]; default = "no"; @@ -413,14 +407,9 @@ in }; minSize = mkOption { - type = types.int; - default = 2; - description = "Size of the smallest n-gram to index."; - }; - maxSize = mkOption { - type = types.int; - default = 20; - description = "Size of the largest n-gram to index."; + type = types.ints.between 3 1000; + default = 3; + description = "Minimum size of search terms"; }; memoryLimit = mkOption { type = types.nullOr types.int; @@ -1321,6 +1310,12 @@ in }; imports = [ + (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] '' + 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 + '') ./mail-server/assertions.nix ./mail-server/borgbackup.nix ./mail-server/debug.nix diff --git a/docs/fts.rst b/docs/fts.rst index 5d84eaf..780ae3e 100644 --- a/docs/fts.rst +++ b/docs/fts.rst @@ -20,8 +20,6 @@ To enable indexing for full text search here is an example configuration. enable = true; # index new email as they arrive autoIndex = true; - # this only applies to plain text attachments, binary attachments are never indexed - indexAttachments = true; enforced = "body"; }; }; @@ -61,8 +59,7 @@ Mitigating resources requirements You can: -* disable indexation of attachements ``mailserver.fullTextSearch.indexAttachments = false`` -* reduce the size of ngrams to be indexed ``mailserver.fullTextSearch.minSize`` and ``maxSize`` +* increase the minimum search term size ``mailserver.fullTextSearch.minSize`` * disable automatic indexation for some folders with ``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard. diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 6e39923..e0efaff 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -352,7 +352,7 @@ in plugin { plugin = fts fts_xapian fts = xapian - fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug} + fts_xapian = partial=${toString cfg.fullTextSearch.minSize} verbose=${bool2int cfg.debug} fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"} @@ -361,11 +361,12 @@ in fts_enforced = ${cfg.fullTextSearch.enforced} } - ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' service indexer-worker { + ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)} - } ''} + process_limit = 0 + } ''} lda_mailbox_autosubscribe = yes diff --git a/tests/external.nix b/tests/external.nix index 77f807d..497b12a 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -322,7 +322,7 @@ pkgs.nixosTest { Hello User1, this email contains the needle: - 576a4565b70f5a4c1a0925cabdb587a6 + 576a4565b70f5a4c1a0925cabdb587a6 ''; "root/email7".text = '' Message-ID: <1234578qwerty@host.local.network> @@ -508,7 +508,7 @@ pkgs.nixosTest { 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 'FTS Xapian: Box is empty' | grep -i warning >&2" + "journalctl -u dovecot2 |grep -v 'Expunged message reappeared, giving a new UID'| grep -v 'FTS Xapian: Box is empty' | grep -vE 'FTS Xapian:.*does not exist. Creating it' | grep -i warning >&2" ) ''; } From 6b425d13f5a9d73cb63973d3609acacef4d1e261 Mon Sep 17 00:00:00 2001 From: euxane Date: Fri, 24 Jan 2025 17:40:48 +0100 Subject: [PATCH 126/225] tests: fix renamed options warnings --- tests/clamav.nix | 4 ++-- tests/external.nix | 4 ++-- tests/ldap.nix | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/clamav.nix b/tests/clamav.nix index 7a9f43c..ae186df 100644 --- a/tests/clamav.nix +++ b/tests/clamav.nix @@ -84,8 +84,8 @@ pkgs.nixosTest { }; }; client = { nodes, config, pkgs, ... }: let - serverIP = nodes.server.config.networking.primaryIPAddress; - clientIP = nodes.client.config.networking.primaryIPAddress; + serverIP = nodes.server.networking.primaryIPAddress; + clientIP = nodes.client.networking.primaryIPAddress; grep-ip = pkgs.writeScriptBin "grep-ip" '' #!${pkgs.stdenv.shell} echo grep '${clientIP}' "$@" >&2 diff --git a/tests/external.nix b/tests/external.nix index 497b12a..7579b6d 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -87,8 +87,8 @@ pkgs.nixosTest { }; }; client = { nodes, config, pkgs, ... }: let - serverIP = nodes.server.config.networking.primaryIPAddress; - clientIP = nodes.client.config.networking.primaryIPAddress; + serverIP = nodes.server.networking.primaryIPAddress; + clientIP = nodes.client.networking.primaryIPAddress; grep-ip = pkgs.writeScriptBin "grep-ip" '' #!${pkgs.stdenv.shell} echo grep '${clientIP}' "$@" >&2 diff --git a/tests/ldap.nix b/tests/ldap.nix index 172a77d..02c5ac1 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -20,7 +20,7 @@ pkgs.nixosTest { services.openssh = { enable = true; - permitRootLogin = "yes"; + settings.PermitRootLogin = "yes"; }; environment.systemPackages = [ From 8c1c4640b878c692dd3d8055e8cdea0a2bbd8cf3 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sun, 9 Feb 2025 18:05:08 +0100 Subject: [PATCH 127/225] Increase the evaluation periodicity from 30s to 5m This has been asked by the Nix community for debugging and maintenance purposes. --- .hydra/declarative-jobsets.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 0a158cf..0e68a86 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -8,7 +8,7 @@ let { enabled = 1; hidden = false; description = "PR ${num}: ${info.title}"; - checkinterval = 30; + checkinterval = 300; schedulingshares = 20; enableemail = false; emailoverride = ""; @@ -19,7 +19,7 @@ let ) prs; mkFlakeJobset = branch: { description = "Build ${branch} branch of Simple NixOS MailServer"; - checkinterval = "60"; + checkinterval = 300; enabled = "1"; schedulingshares = 100; enableemail = false; From f23faf97d6668a4847be5d5c6399493e596b406d Mon Sep 17 00:00:00 2001 From: Michael Lohmann Date: Mon, 24 Feb 2025 16:11:59 +0100 Subject: [PATCH 128/225] rebootAfterKernelUpgrade: document that this can be done from nixos Since NixOS 19.09 autoUpgrade also has the ability to do automatic reboots. Its detection on whether a reboot is necessary is a bit more sophisticated. Having this option in the mail-server implied to me that it did something additionally, though it was just a feature which was not included in NixOS at the time it was introduced for the mail-server. Mentioning the fact in the documentation might help people not to get confused why they should turn the `system.autoUpgrade.allowReboot` off and instead use the mail-servers reboot flag. --- default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/default.nix b/default.nix index ad6a53a..76ad3b4 100644 --- a/default.nix +++ b/default.nix @@ -1236,6 +1236,7 @@ in description = '' Whether to enable automatic reboot after kernel upgrades. This is to be used in conjunction with `system.autoUpgrade.enable = true;` + This can also be achieved via `system.autoUpgrade.allowReboot = true;` ''; }; method = mkOption { From c8ec4d5e432f5df4838eacd39c11828d23ce66ec Mon Sep 17 00:00:00 2001 From: Michael Lohmann Date: Mon, 24 Feb 2025 22:29:18 +0100 Subject: [PATCH 129/225] remove rebootAfterKernelUpgrade option This is not a feature specific to the mailserver. Indeed, the feature was added to `system.autoUpgrade.allowReboot` with NixOS 19.09 and it has better detection if a reboot is necessary. For the system.autoUpgrade there is no kexec option, but the use was discouraged. --- default.nix | 30 +++++-------------- mail-server/post-upgrade-check.nix | 46 ------------------------------ 2 files changed, 7 insertions(+), 69 deletions(-) delete mode 100644 mail-server/post-upgrade-check.nix diff --git a/default.nix b/default.nix index 76ad3b4..edc9294 100644 --- a/default.nix +++ b/default.nix @@ -1228,28 +1228,6 @@ in }; - rebootAfterKernelUpgrade = { - enable = mkOption { - type = types.bool; - default = false; - example = true; - description = '' - Whether to enable automatic reboot after kernel upgrades. - This is to be used in conjunction with `system.autoUpgrade.enable = true;` - This can also be achieved via `system.autoUpgrade.allowReboot = true;` - ''; - }; - method = mkOption { - type = types.enum [ "reboot" "systemctl kexec" ]; - default = "reboot"; - description = '' - Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot. - It is recommended to use the default value because the quicker kexec reboot has a number of problems. - Also if your server is running in a virtual machine the regular reboot will already be very quick. - ''; - }; - }; - backup = { enable = mkEnableOption "backup via rsnapshot"; @@ -1317,6 +1295,13 @@ in (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] '' 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. + '') ./mail-server/assertions.nix ./mail-server/borgbackup.nix ./mail-server/debug.nix @@ -1333,6 +1318,5 @@ in ./mail-server/rspamd.nix ./mail-server/nginx.nix ./mail-server/kresd.nix - ./mail-server/post-upgrade-check.nix ]; } diff --git a/mail-server/post-upgrade-check.nix b/mail-server/post-upgrade-check.nix deleted file mode 100644 index 9b418b2..0000000 --- a/mail-server/post-upgrade-check.nix +++ /dev/null @@ -1,46 +0,0 @@ -# nixos-mailserver: a simple mail server -# Copyright (C) 2016-2018 Robin Raymond -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see - -{ config, pkgs, lib, ... }: - -with lib; - -let - cfg = config.mailserver; -in -{ - config = mkIf (cfg.enable && cfg.rebootAfterKernelUpgrade.enable) { - systemd.services.nixos-upgrade.serviceConfig.ExecStartPost = pkgs.writeScript "post-upgrade-check" '' - #!${pkgs.stdenv.shell} - - # Checks whether the "current" kernel is different from the booted kernel - # and then triggers a reboot so that the "current" kernel will be the booted one. - # This is just an educated guess. If the links do not differ the kernels might still be different, according to spacefrogg in #nixos. - - current=$(readlink -f /run/current-system/kernel) - booted=$(readlink -f /run/booted-system/kernel) - - if [ "$current" == "$booted" ]; then - echo "kernel version seems unchanged, skipping reboot" | systemd-cat --priority 4 --identifier "post-upgrade-check"; - else - echo "kernel path changed, possibly a new version" | systemd-cat --priority 2 --identifier "post-upgrade-check" - echo "$booted" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check" - echo "$current" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check" - ${cfg.rebootAfterKernelUpgrade.method} - fi - ''; - }; -} From 90539a1a993a7ec16563139e82fa66f1c439ba0f Mon Sep 17 00:00:00 2001 From: Michael Lohmann Date: Fri, 14 Feb 2025 20:32:18 +0100 Subject: [PATCH 130/225] Fix URLs for dovecot The old wiki was deleted and so the new one has to be used --- default.nix | 4 ++-- mail-server/systemd.nix | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/default.nix b/default.nix index edc9294..ec88f7e 100644 --- a/default.nix +++ b/default.nix @@ -575,7 +575,7 @@ in - /var/vmail/example.com/user/.folder.subfolder/ (default layout) - /var/vmail/example.com/user/folder/subfolder/ (FS layout) - See https://wiki2.dovecot.org/MailboxFormat/Maildir for details. + See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details. ''; }; @@ -596,7 +596,7 @@ in This affects how mailboxes appear to mail clients and sieve scripts. For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example". This does not determine the way your mails are stored on disk. - See https://wiki.dovecot.org/Namespaces for details. + See https://doc.dovecot.org/main/core/config/namespaces.html#namespaces for details. ''; }; diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 2c7f8ee..121abfe 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -63,7 +63,7 @@ in ); in '' # Create mail directory and set permissions. See - # . + # . # Prevent world-readable paths, even temporarily. umask 007 mkdir -p ${directories} From 9b5df9613271b9d083a014b6652d7c75d1dc081c Mon Sep 17 00:00:00 2001 From: Philipp Bartsch Date: Wed, 8 Jul 2020 23:48:53 +0200 Subject: [PATCH 131/225] postfix: enable smtp tls logging Log a summary message on TLS handshake completion. --- mail-server/postfix.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 5a93dc2..6ba6ec6 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -296,6 +296,7 @@ in # Allowing AUTH on a non encrypted connection poses a security risk smtpd_tls_auth_only = true; # Log only a summary message on TLS handshake completion + smtp_tls_loglevel = "1"; smtpd_tls_loglevel = "1"; # Configure a non blocking source of randomness From 0c40a0b2c60b097e557cd669610a8baf3540a4dd Mon Sep 17 00:00:00 2001 From: Michael Lohmann Date: Mon, 17 Mar 2025 13:52:14 +0100 Subject: [PATCH 132/225] dovecot: use expanded variable names Since Dovecot 2.4 does not accept short notations for variables any more https://doc.dovecot.org/2.4.0/installation/upgrade/2.3-to-2.4.html#variable-expansion the long form needs to be used: %u => %{user} %n => %{username} %d => %{domain} This is backwards compatible with dovecot 2.3 as well: https://doc.dovecot.org/2.3/configuration_manual/config_file/config_variables/#user-variables --- default.nix | 8 ++++---- mail-server/dovecot.nix | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/default.nix b/default.nix index ec88f7e..60dbceb 100644 --- a/default.nix +++ b/default.nix @@ -290,8 +290,8 @@ in userFilter = mkOption { type = types.str; - default = "mail=%u"; - example = "(&(objectClass=inetOrgPerson)(mail=%u))"; + default = "mail=%{user}"; + example = "(&(objectClass=inetOrgPerson)(mail=%{user}))"; description = '' Filter for user lookups in Dovecot. @@ -315,8 +315,8 @@ in passFilter = mkOption { type = types.nullOr types.str; - default = "mail=%u"; - example = "(&(objectClass=inetOrgPerson)(mail=%u))"; + default = "mail=%{user}"; + example = "(&(objectClass=inetOrgPerson)(mail=%{user}))"; description = '' Filter for password lookups in Dovecot. diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index e0efaff..0abfec4 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -33,9 +33,9 @@ let # maildir in format "/${domain}/${user}" dovecotMaildir = - "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}" + "maildir:${cfg.mailDirectory}/%{domain}/%{username}${maildirLayoutAppendix}${maildirUTF8FolderNames}" + (lib.optionalString (cfg.indexDir != null) - ":INDEX=${cfg.indexDir}/%d/%n" + ":INDEX=${cfg.indexDir}/%{domain}/%{username}" ); postfixCfg = config.services.postfix; @@ -177,8 +177,8 @@ in protocols = lib.optional cfg.enableManageSieve "sieve"; pluginSettings = { - sieve = "file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve"; - sieve_default = "file:${cfg.sieveDirectory}/%u/default.sieve"; + sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; + sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; sieve_default_name = "default"; }; @@ -329,7 +329,7 @@ in userdb { driver = ldap args = ${ldapConfFile} - default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} + default_fields = home=/var/vmail/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} } ''} From b4fbffe79c00f19be94b86b4144ff67541613659 Mon Sep 17 00:00:00 2001 From: Yureka Date: Wed, 12 Mar 2025 23:51:07 +0100 Subject: [PATCH 133/225] services.dovecot2.modules option has been removed --- mail-server/dovecot.nix | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 0abfec4..8e6d2b2 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ options, config, pkgs, lib, ... }: with (import ./common.nix { inherit config pkgs lib; }); @@ -143,6 +143,12 @@ let else scope ); + dovecotModules = [ + pkgs.dovecot_pigeonhole + ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian; + # Remove and assume `false` after NixOS 25.05 + haveDovecotModulesOption = options.services.dovecot2 ? "modules" && (options.services.dovecot2.modules.visible or true); + in { config = with cfg; lib.mkIf enable { @@ -158,9 +164,14 @@ in # which are usually not compatible. environment.systemPackages = [ pkgs.dovecot_pigeonhole - ]; + ] ++ lib.optionals (!haveDovecotModulesOption) dovecotModules; - services.dovecot2 = { + # For compatibility with python imaplib + environment.etc = lib.mkIf (!haveDovecotModulesOption) { + "dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; + }; + + services.dovecot2 = lib.mkMerge [{ enable = true; enableImap = enableImap || enableImapSsl; enablePop3 = enablePop3 || enablePop3Ssl; @@ -172,7 +183,6 @@ in sslServerCert = certificatePath; sslServerKey = keyPath; enableLmtp = true; - modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian ); mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ]; protocols = lib.optional cfg.enableManageSieve "sieve"; @@ -372,7 +382,11 @@ in lda_mailbox_autosubscribe = yes lda_mailbox_autocreate = yes ''; - }; + } + (lib.mkIf haveDovecotModulesOption { + modules = dovecotModules; + }) + ]; systemd.services.dovecot2 = { preStart = '' From efe77ce80634bba53856e7ddbd76f74186b0a014 Mon Sep 17 00:00:00 2001 From: Maximilian Bosch Date: Sat, 6 May 2023 11:20:12 +0200 Subject: [PATCH 134/225] mail-server: add `dmarcReporting.excludeDomains` The option `exclude_domains` for dmarc reporting in `rspamd`[1] allows to configure a list of domains and/or eSLDs (external effective second level domain) to be excluded from dmarc reports. Helpful because e.g. dmarc reports to hotmail.com always fail for me with the following undeliverable notification: The recipient's mailbox is full and can't accept messages now. [1] https://www.rspamd.com/doc/modules/dmarc.html --- default.nix | 8 ++++++++ mail-server/rspamd.nix | 3 +++ 2 files changed, 11 insertions(+) diff --git a/default.nix b/default.nix index 60dbceb..4008df3 100644 --- a/default.nix +++ b/default.nix @@ -894,6 +894,14 @@ in The sender name for DMARC reports. Defaults to the organization name. ''; }; + + excludeDomains = mkOption { + type = types.listOf types.str; + default = []; + description = '' + List of domains or eSLDs to be excluded from DMARC reports. + ''; + }; }; debug = mkOption { diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index 8fb9b00..fc6f4b9 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -74,6 +74,9 @@ in org_name = "${cfg.dmarcReporting.organizationName}"; from_name = "${cfg.dmarcReporting.fromName}"; msgid_from = "dmarc-rua"; + ${lib.optionalString (cfg.dmarcReporting.excludeDomains != []) '' + exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains}; + ''} }''} ''; }; }; From 1873ed090803c615a9c729aa8a6c98ec880226c0 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 13 Apr 2025 04:57:17 +0200 Subject: [PATCH 135/225] README: Update existing and future features As the ecosystems around us evolve so should the NixOS mailserver project. DKIM signing could be improved by allowing users to treat DKIM keys like a secret that they would commonly manage through agenix/sops/etc. Forwarding mail these days requires SRS and possibly ARC. The latter has already become a required feature for bulk message to iCloud[1] and Google Mail[3]. I propose that we stay ahead of the curve by adding support for these features. LDAP user management was added, but one pain point is that we currently prevent it from coexisting with declarative users. And finally Oauth (via RFC7628[3]) is the new kid on the block that everyone wants to try out, but most notably client support[4] for hosting this yourself is not quite there yet. [1] https://support.apple.com/en-us/102322 [2] https://support.google.com/a/answer/81126?hl=en#zippy=%2Crequirements-for-all-senders%2Crequirements-for-sending-or-more-messages-per-day [3] https://www.rfc-editor.org/rfc/rfc7628.html [4] https://bugzilla.mozilla.org/show_bug.cgi?id=1602166 --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 098f68a..337163b 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ can stay up to date with bug fixes and updates. * User Management - [x] declarative user management - [x] declarative password management + - [x] LDAP users * Sieves - [x] A simple standard script that moves spam - [x] Allow user defined sieve scripts @@ -64,7 +65,15 @@ can stay up to date with bug fixes and updates. ### In the future * DKIM Signing - - [ ] Allow a per domain selector + - [ ] Allow per domain selectors + - [ ] Allow passing DKIM signing keys + * Improve the Forwarding Experience + - [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html) + - [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd) + * User management + - [ ] Allow local and LDAP user to coexist + * OpenID Connect + - Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166) ### Get in touch From 7bdf5003c730650cd9f57a3f8beaa5a435c53b2a Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Thu, 13 Mar 2025 11:50:56 -0500 Subject: [PATCH 136/225] docs/dns: update DKIM TXT instructions I recently went through this, and the generated file looks a bit different than was previously documented. I opted to be explicit about `k=rsa` (even though [the default is "rsa"](https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1)). I also opted to be explicit about `s=email` ([the default is "*"](https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1)). Honestly not sure what the consequences of this are, I don't know if DKIM is used for anything besides email. --- docs/setup-guide.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 08de1b3..f359893 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -180,18 +180,19 @@ like :: - mail._domainkey IN TXT "v=DKIM1; k=rsa; s=email; p=" ; ----- DKIM mail for domain.tld + mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " + "p=" ) ; ----- DKIM key mail for nixos.org where ``really-long-key`` is your public key. Based on the content of this file, we can add a ``DKIM`` record to the domain ``example.com``. -=========================== ===== ==== ============================== +=========================== ===== ==== ================================================ Name (Subdomain) TTL Type Value -=========================== ===== ==== ============================== -mail._domainkey.example.com 10800 TXT ``v=DKIM1; p=`` -=========================== ===== ==== ============================== +=========================== ===== ==== ================================================ +mail._domainkey.example.com 10800 TXT ``v=DKIM1; k=rsa; s=email; p=`` +=========================== ===== ==== ================================================ You can check this with From 745c6ee86199123b12f3c028c44bf41813f8c102 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 13 Apr 2025 03:54:51 +0200 Subject: [PATCH 137/225] rspamd: Use redis over a unix socket by default Both rspamd and redis run on the same host by default, so a UNIX domain socket is the cheapest way to facilitate that communication. It also allows us to get rid of overly complicated IP adddress parsing logic, that we can shift onto the user if they need it. --- default.nix | 23 +++++++---------------- docs/release-notes.rst | 8 ++++++++ mail-server/rspamd.nix | 12 +++++++----- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/default.nix b/default.nix index 4008df3..17fd16d 100644 --- a/default.nix +++ b/default.nix @@ -944,28 +944,19 @@ in address = mkOption { type = types.str; # read the default from nixos' redis module - default = let - cf = config.services.redis.servers.rspamd.bind; - cfdefault = if cf == null then "127.0.0.1" else cf; - ips = lib.strings.splitString " " cfdefault; - ip = lib.lists.head (ips ++ [ "127.0.0.1" ]); - isIpv6 = ip: lib.lists.elem ":" (lib.stringToCharacters ip); - in - if (ip == "0.0.0.0" || ip == "::") - then "127.0.0.1" - else if isIpv6 ip then "[${ip}]" else ip; - defaultText = lib.literalMD "computed from `config.services.redis.servers.rspamd.bind`"; + default = config.services.redis.servers.rspamd.unixSocket; + defaultText = lib.literalExpression "config.services.redis.servers.rspamd.unixSocket"; description = '' - Address that rspamd should use to contact redis. + Path, IP address or hostname that Rspamd should use to contact Redis. ''; }; port = mkOption { - type = types.port; - default = config.services.redis.servers.rspamd.port; - defaultText = lib.literalExpression "config.services.redis.servers.rspamd.port"; + type = with types; nullOr port; + default = null; + example = lib.literalExpression "config.services.redis.servers.rspamd.port"; description = '' - Port that rspamd should use to contact redis. + Port that Rspamd should use to contact Redis. ''; }; diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 806de8e..0cade83 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -1,6 +1,14 @@ Release Notes ============= +NixOS 25.05 +----------- + +- Rspamd now connects to Redis over its Unix Domain Socket by default + (`merge request Date: Thu, 17 Apr 2025 02:54:47 +0200 Subject: [PATCH 138/225] Remove policy-spf Rspamd can do the same as policy-spf, only better, with more settings, is well integrated and better maintained. Other projects are going the same route [1]. [1]: https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/ --- default.nix | 17 ++++------------- mail-server/debug.nix | 4 ---- mail-server/postfix.nix | 12 +----------- 3 files changed, 5 insertions(+), 28 deletions(-) delete mode 100644 mail-server/debug.nix diff --git a/default.nix b/default.nix index 17fd16d..1828c5f 100644 --- a/default.nix +++ b/default.nix @@ -1022,18 +1022,6 @@ in ''; }; - policydSPFExtraConfig = mkOption { - type = types.lines; - default = ""; - example = '' - skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 - ''; - description = '' - Extra configuration options for policyd-spf. This can be use to among - other things skip spf checking for some IP addresses. - ''; - }; - monitoring = { enable = mkEnableOption "monitoring via monit"; @@ -1303,7 +1291,6 @@ in '') ./mail-server/assertions.nix ./mail-server/borgbackup.nix - ./mail-server/debug.nix ./mail-server/rsnapshot.nix ./mail-server/clamav.nix ./mail-server/monit.nix @@ -1317,5 +1304,9 @@ in ./mail-server/rspamd.nix ./mail-server/nginx.nix ./mail-server/kresd.nix + (lib.mkRemovedOptionModule [ "mailserver" "policydSPFExtraConfig" ] '' + SPF checking has been migrated to Rspamd, which makes this config redundant. Please look into the rspamd config to migrate your settings. + It may be that they are redundant and are already configured in rspamd like for skip_addresses. + '') ]; } diff --git a/mail-server/debug.nix b/mail-server/debug.nix deleted file mode 100644 index 8107515..0000000 --- a/mail-server/debug.nix +++ /dev/null @@ -1,4 +0,0 @@ -{ config, lib, ... }: -{ - mailserver.policydSPFExtraConfig = lib.mkIf config.mailserver.debug "debugLevel = 4"; -} diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 6ba6ec6..c0bd2fb 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -255,19 +255,16 @@ in "permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination" ]; - policy-spf_time_limit = "3600s"; - # reject selected senders smtpd_sender_restrictions = [ "check_sender_access ${mappedFile "reject_senders"}" ]; - # quota and spf checking + # quota checking smtpd_recipient_restrictions = [ "check_recipient_access ${mappedFile "denied_recipients"}" "check_recipient_access ${mappedFile "reject_recipients"}" "check_policy_service inet:localhost:12340" - "check_policy_service unix:private/policy-spf" ]; # TLS settings, inspired by https://github.com/jeaye/nix-files @@ -321,13 +318,6 @@ in # D => Delivered-To, O => X-Original-To, R => Return-Path args = [ "flags=O" ]; }; - "policy-spf" = { - type = "unix"; - privileged = true; - chroot = false; - command = "spawn"; - args = [ "user=nobody" "argv=${pkgs.spf-engine}/bin/policyd-spf" "${policyd-spf}"]; - }; "submission-header-cleanup" = { type = "unix"; private = false; From 42651ce2d337921c99ae0c293ed9af49f7a89c6a Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 20 Apr 2025 17:49:39 +0200 Subject: [PATCH 139/225] docs: update release notes --- docs/release-notes.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 0cade83..f1ab80d 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -5,9 +5,13 @@ NixOS 25.05 ----------- - Rspamd now connects to Redis over its Unix Domain Socket by default - (`merge request `__) + + - If you need to revert TCP connections, configure ``mailserver.redis.address`` to reference the value of ``config.services.redis.servers.rspamd.bind``. +- The integration with policyd-spf was removed and SPF handling is now fully based on Rspamd scoring. + (`merge request `__) +- Individual domains can now be excluded from DMARC Reporting through ``mailserver.dmarcReporting.excludedDomains``. + (`merge request `__) NixOS 24.11 ----------- From ab52efd622a9f7dca269a49edbbea6b6b7294f57 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 23 Apr 2025 16:02:07 +0200 Subject: [PATCH 140/225] ci: update to nixos-24.11 --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b72b9f9..35980ae 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,11 +3,11 @@ hydra-pr: - merge_requests image: nixos/nix script: - - nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}' + - nix-shell -I nixpkgs=channel:nixos-24.11 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}' hydra-master: only: - master image: nixos/nix script: - - nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master' + - nix-shell -I nixpkgs=channel:nixos-24.11 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master' From 46fe2c25c8c92f4d11d94b319072a7667aa24746 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 23 Apr 2025 15:54:03 +0200 Subject: [PATCH 141/225] dovecot: prefer client cipher list All ciphers in TLSv1.2/TLSv1.3 are considered secure, so we can allow the client to choose the most performant cipher according to their hardware and software configuration. This is in line with general recommendations, e.g. by Mozilla[1]. [1] https://wiki.mozilla.org/Security/Server_Side_TLS --- mail-server/dovecot.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 8e6d2b2..31855db 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -297,7 +297,7 @@ in mail_access_groups = ${vmailGroupName} ssl = required ssl_min_protocol = TLSv1.2 - ssl_prefer_server_ciphers = yes + ssl_prefer_server_ciphers = no service lmtp { unix_listener dovecot-lmtp { From b859c910ab67ff700a1ae1856e91e6d40adbe869 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 11 Aug 2024 17:20:45 +0200 Subject: [PATCH 142/225] dmarc-reports: report mail message id with domain --- mail-server/rspamd.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index 5e1f8c2..ec919c2 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -77,7 +77,7 @@ in domain = "${cfg.dmarcReporting.domain}"; org_name = "${cfg.dmarcReporting.organizationName}"; from_name = "${cfg.dmarcReporting.fromName}"; - msgid_from = "dmarc-rua"; + msgid_from = "${cfg.dmarcReporting.domain}"; ${lib.optionalString (cfg.dmarcReporting.excludeDomains != []) '' exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains}; ''} From 75b1908f24903227ce970c0169055d204cfe8453 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Mon, 5 May 2025 20:22:45 +0200 Subject: [PATCH 143/225] Fix the RTD build --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 2211dd5..c77dd1e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,3 +2,4 @@ sphinx ~= 5.3 sphinx_rtd_theme ~= 1.1 myst-parser ~= 0.18 linkify-it-py ~= 2.0 +standard-imghdr From ca69f91f6b14ee46ce61aaac651680d07d46b231 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 5 May 2025 21:21:58 +0200 Subject: [PATCH 144/225] update.sh: drop The section it updates was removed in d460e9ff62ea1238fb3348a87326b743ae177902. --- update.sh | 7 ------- 1 file changed, 7 deletions(-) delete mode 100755 update.sh diff --git a/update.sh b/update.sh deleted file mode 100755 index 39d6402..0000000 --- a/update.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -sed -i -e "s/v[0-9]\+\.[0-9]\+\.[0-9]\+/$1/g" README.md - -HASH=$(nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/v2.3.0/nixos-mailserver-$1.tar.gz" --unpack) - -sed -i -e "s/sha256 = \"[0-9a-z]\{52\}\"/sha256 = \"$HASH\"/g" README.md From a071813b974b6ba7c46ad0ce428022937e7c4b99 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 5 May 2025 21:51:59 +0200 Subject: [PATCH 145/225] README: reword feature list and remove the v2.0 release title. --- README.md | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 337163b..ac38e5a 100644 --- a/README.md +++ b/README.md @@ -26,37 +26,36 @@ can stay up to date with bug fixes and updates. ## Features -### v2.0 + * [x] Continous Integration Testing * [x] Multiple Domains - * Postfix MTA - - [x] smtp on port 25 - - [x] submission tls on port 465 - - [x] submission starttls on port 587 - - [x] lmtp with dovecot + * Postfix + - [x] SMTP on port 25 + - [x] Submission TLS on port 465 + - [x] Submission StartTLS on port 587 + - [x] LMTP with Dovecot * Dovecot - - [x] maildir folders - - [x] imap with tls on port 993 - - [x] pop3 with tls on port 995 - - [x] imap with starttls on port 143 - - [x] pop3 with starttls on port 110 + - [x] Maildir folders + - [x] IMAP with TLS on port 993 + - [x] POP3 with TLS on port 995 + - [x] IMAP with StartTLS on port 143 + - [x] POP3 with StartTLS on port 110 * Certificates - - [x] manual certificates - - [x] on the fly creation - - [x] Let's Encrypt + - [x] ACME + - [x] Custom certificates * Spam Filtering - - [x] via rspamd + - [x] Via Rspamd * Virus Scanning - - [x] via clamav + - [x] Via ClamAV * DKIM Signing - - [x] via opendkim + - [x] Via OpenDKIM * User Management - - [x] declarative user management - - [x] declarative password management + - [x] Declarative user management + - [x] Declarative password management - [x] LDAP users - * Sieves - - [x] A simple standard script that moves spam + * Sieve - [x] Allow user defined sieve scripts + - [x] Moving mails from/to junk trains the Bayes filter - [x] ManageSieve support * User Aliases - [x] Regular aliases From 84bf0c0c079963d04230c179bbf4985d1cbdab23 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 5 May 2025 21:52:42 +0200 Subject: [PATCH 146/225] README.md: remove mailing list information Has been unused since 2019, so it is not a good recommendation to subscribe there anymore. --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index ac38e5a..3a1ccde 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,6 @@ SNM branch corresponding to your NixOS version. - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) -[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm) -This is a very low volume list where new releases of SNM are announced, so you -can stay up to date with bug fixes and updates. - - ## Features * [x] Continous Integration Testing @@ -76,7 +71,6 @@ can stay up to date with bug fixes and updates. ### Get in touch -- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/) - Join the Libera Chat IRC channel `#nixos-mailserver` ## How to Set Up a 10/10 Mail Server Guide From 8800bccab84dc963a7dbc4267a915169230d3434 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 5 May 2025 22:30:39 +0200 Subject: [PATCH 147/225] dovecot: fix config indent --- mail-server/dovecot.nix | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 31855db..ff2b3e6 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -287,8 +287,8 @@ in } service imap { - vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB - } + vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB + } protocol pop3 { mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} @@ -305,12 +305,12 @@ in mode = 0600 user = ${postfixCfg.user} } - vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB + vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB } service quota-status { - vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB - } + vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB + } recipient_delimiter = ${cfg.recipientDelimiter} lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} From 630b5c4fddb3b46fd383e4d0c95956617629b584 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 11 Apr 2025 05:25:08 +0200 Subject: [PATCH 148/225] Use rspamd for DKIM signing, drop OpenDKIM OpenDKIM has not been updated in the last 7 years and failed to adopt RFC8463, which introduces Ed25519-SHA256 signatures. It has thereby held back the DKIM ecosystem, which relies on the DNS system to publish its public keys. The DNS system in turn does not handle large record sizes well (see RFC8301), which is why Ed25519 public keys would be preferable, but I'm not sure the ecosystem has caught up, so we stay on the conservative side with RSA for now. Fixes: #203 #210 #279 Obsoletes: !162 !338 Supersedes: !246 --- README.md | 2 +- default.nix | 40 ++++++++--------- docs/release-notes.rst | 2 + docs/setup-guide.rst | 2 +- mail-server/environment.nix | 2 +- mail-server/opendkim.nix | 89 ------------------------------------- mail-server/postfix.nix | 8 ++-- mail-server/rspamd.nix | 52 ++++++++++++++++++++-- mail-server/systemd.nix | 4 +- 9 files changed, 78 insertions(+), 123 deletions(-) delete mode 100644 mail-server/opendkim.nix diff --git a/README.md b/README.md index 3a1ccde..5c079c6 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ SNM branch corresponding to your NixOS version. * Virus Scanning - [x] Via ClamAV * DKIM Signing - - [x] Via OpenDKIM + - [x] Via Rspamd * User Management - [x] Declarative user management - [x] Declarative password management diff --git a/default.nix b/default.nix index 1828c5f..3f46610 100644 --- a/default.nix +++ b/default.nix @@ -802,6 +802,19 @@ in ''; }; + dkimKeyType = mkOption { + type = types.enum [ "rsa" "ed25519" ]; + default = "rsa"; + description = '' + The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018). + + If you have already deployed a key with a different type 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 type, you will either have to + change the selector or delete the old key file. + ''; + }; + dkimKeyBits = mkOption { type = types.int; default = 1024; @@ -815,26 +828,6 @@ in ''; }; - dkimHeaderCanonicalization = mkOption { - type = types.enum ["relaxed" "simple"]; - default = "relaxed"; - description = '' - DKIM canonicalization algorithm for message headers. - - See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. - ''; - }; - - dkimBodyCanonicalization = mkOption { - type = types.enum ["relaxed" "simple"]; - default = "relaxed"; - description = '' - DKIM canonicalization algorithm for message bodies. - - See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details. - ''; - }; - dmarcReporting = { enable = mkOption { type = types.bool; @@ -1299,7 +1292,6 @@ in ./mail-server/networking.nix ./mail-server/systemd.nix ./mail-server/dovecot.nix - ./mail-server/opendkim.nix ./mail-server/postfix.nix ./mail-server/rspamd.nix ./mail-server/nginx.nix @@ -1308,5 +1300,11 @@ in SPF checking has been migrated to Rspamd, which makes this config redundant. Please look into the rspamd config to migrate your settings. It may be that they are redundant and are already configured in rspamd like for skip_addresses. '') + (lib.mkRemovedOptionModule [ "mailserver" "dkimHeaderCanonicalization" ] '' + DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization. + '') + (lib.mkRemovedOptionModule [ "mailserver" "dkimBodyCanonicalization" ] '' + DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization. + '') ]; } diff --git a/docs/release-notes.rst b/docs/release-notes.rst index f1ab80d..556de5f 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -4,6 +4,8 @@ Release Notes NixOS 25.05 ----------- +- OpenDKIM has been removed and DKIM signing is now handled by Rspamd, which only supports ``relaxed`` canoncalizaliaton. + (`merge request `__) - Rspamd now connects to Redis over its Unix Domain Socket by default (`merge request `__) diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index f359893..5f6f903 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -173,7 +173,7 @@ Note that it can take a while until a DNS entry is propagated. Set ``DKIM`` signature ^^^^^^^^^^^^^^^^^^^^^^ -On your server, the ``opendkim`` systemd service generated a file +On your server, the ``rspamd`` systemd service generated a file containing your DKIM public key in the file ``/var/dkim/example.com.mail.txt``. The content of this file looks like diff --git a/mail-server/environment.nix b/mail-server/environment.nix index e509ea6..b4326a1 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -22,7 +22,7 @@ in { config = with cfg; lib.mkIf enable { environment.systemPackages = with pkgs; [ - dovecot opendkim openssh postfix rspamd + dovecot openssh postfix rspamd ] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []); }; } diff --git a/mail-server/opendkim.nix b/mail-server/opendkim.nix deleted file mode 100644 index cdb283c..0000000 --- a/mail-server/opendkim.nix +++ /dev/null @@ -1,89 +0,0 @@ -# nixos-mailserver: a simple mail server -# Copyright (C) 2017 Brian Olsen -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.mailserver; - - dkimUser = config.services.opendkim.user; - dkimGroup = config.services.opendkim.group; - - createDomainDkimCert = dom: - let - dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key"; - dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt"; - in - '' - if [ ! -f "${dkim_key}" ] - then - ${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \ - -d "${dom}" \ - --bits="${toString cfg.dkimKeyBits}" \ - --directory="${cfg.dkimKeyDirectory}" - mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}" - mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}" - chmod 644 "${dkim_txt}" - echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}" - fi - ''; - createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains); - - keyTable = pkgs.writeText "opendkim-KeyTable" - (lib.concatStringsSep "\n" (lib.flip map cfg.domains - (dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key"))); - signingTable = pkgs.writeText "opendkim-SigningTable" - (lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}"))); - - dkim = config.services.opendkim; - args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ]; -in -{ - config = mkIf (cfg.dkimSigning && cfg.enable) { - services.opendkim = { - enable = true; - selector = cfg.dkimSelector; - keyPath = cfg.dkimKeyDirectory; - domains = "csl:${builtins.concatStringsSep "," cfg.domains}"; - configFile = pkgs.writeText "opendkim.conf" ('' - Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization} - UMask 0002 - Socket ${dkim.socket} - KeyTable file:${keyTable} - SigningTable file:${signingTable} - '' + (lib.optionalString cfg.debug '' - Syslog yes - SyslogSuccess yes - LogWhy yes - '')); - }; - - users.users = optionalAttrs (config.services.postfix.user == "postfix") { - postfix.extraGroups = [ "${dkimGroup}" ]; - }; - systemd.services.opendkim = { - preStart = lib.mkForce createAllCerts; - serviceConfig = { - ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}"; - PermissionsStartOnly = lib.mkForce false; - }; - }; - systemd.tmpfiles.rules = [ - "d '${cfg.dkimKeyDirectory}' - ${dkimUser} ${dkimGroup} - -" - ]; - }; -} diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index c0bd2fb..da06111 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -126,9 +126,7 @@ let inetSocket = addr: port: "inet:[${toString port}@${addr}]"; unixSocket = sock: "unix:${sock}"; - smtpdMilters = - (lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock") - ++ [ "unix:/run/rspamd/rspamd-milter.sock" ]; + smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ]; policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig; @@ -300,9 +298,9 @@ in tls_random_source = "dev:/dev/urandom"; smtpd_milters = smtpdMilters; - non_smtpd_milters = lib.mkIf cfg.dkimSigning ["unix:/run/opendkim/opendkim.sock"]; + 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_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"; + milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}"; # Fix for https://www.postfix.org/smtp-smuggling.html smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline; diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index ec919c2..fd94c84 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -22,6 +22,26 @@ let postfixCfg = config.services.postfix; rspamdCfg = config.services.rspamd; rspamdSocket = "rspamd.service"; + + 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 + ''; in { config = with cfg; lib.mkIf enable { @@ -66,8 +86,11 @@ in } ''; }; "dkim_signing.conf" = { text = '' - # Disable outbound email signing, we use opendkim for this - enabled = false; + 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 '' @@ -119,10 +142,33 @@ in 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.SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; + 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) { diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 121abfe..c411441 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -76,10 +76,10 @@ in systemd.services.postfix = { wants = certificatesDeps; after = [ "dovecot2.service" ] - ++ lib.optional cfg.dkimSigning "opendkim.service" + ++ lib.optional cfg.dkimSigning "rspamd.service" ++ certificatesDeps; requires = [ "dovecot2.service" ] - ++ lib.optional cfg.dkimSigning "opendkim.service"; + ++ lib.optional cfg.dkimSigning "rspamd.service"; }; }; } From 2520e662f7b0df084e0417d7daddc26c68592828 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 15 Apr 2025 01:57:24 +0200 Subject: [PATCH 149/225] tests/external: make DKIM signing test more explicit --- tests/external.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/external.nix b/tests/external.nix index 7579b6d..c7e9a0d 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -402,7 +402,7 @@ pkgs.nixosTest { client.succeed("fetchmail --nosslcertck -v") client.succeed("cat ~/mail/* >&2") # make sure it is dkim signed - client.succeed("grep DKIM ~/mail/*") + client.succeed("grep DKIM-Signature: ~/mail/*") with subtest("aliases"): client.execute("rm ~/mail/*") From 4320259e34166ccd9cfaf30049ecde72bc167905 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 6 May 2025 03:27:58 +0200 Subject: [PATCH 150/225] README: add matrix room, reference libera connection information --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c079c6..f5aaa8d 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,8 @@ SNM branch corresponding to your NixOS version. ### Get in touch -- Join the Libera Chat IRC channel `#nixos-mailserver` +- Matrix: [#nixos-mailserver:nixos.org](https://matrix.to/#/#nixos-mailserver:nixos.org) +- IRC: `#nixos-mailserver` on [Libera Chat](https://libera.chat/guides/connect) ## How to Set Up a 10/10 Mail Server Guide From 2d0b3fdeb03e6e41c1e45776615fa0db4ed9bee4 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 6 May 2025 03:37:23 +0200 Subject: [PATCH 151/225] README: Add automatic client configuration support to the roadmap --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index f5aaa8d..d351c1a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,10 @@ SNM branch corresponding to your NixOS version. ### In the future + * Automatic client configuration + - [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) + - [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019) + - [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac) * DKIM Signing - [ ] Allow per domain selectors - [ ] Allow passing DKIM signing keys From 6f3ece918171c1dfc63895db7a20c853561ea0f1 Mon Sep 17 00:00:00 2001 From: Leon Schuermann Date: Tue, 6 May 2025 02:27:36 +0000 Subject: [PATCH 152/225] mail-server/dovecot: check if quota is non-null instead of string --- mail-server/dovecot.nix | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index ff2b3e6..c75aff2 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -125,9 +125,7 @@ let cat < ${userdbFile} ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name}:::::::" - + (if lib.isString value.quota - then "userdb_quota_rule=*:storage=${value.quota}" - else "") + + lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}" ) cfg.loginAccounts)} EOF ''; From b343c5e8fa17f97c24a878398502388b4248ccad Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 27 Dec 2023 20:27:11 +0200 Subject: [PATCH 153/225] assertions: Allow mailserver.forwards with LDAP set up --- mail-server/assertions.nix | 6 +----- tests/ldap.nix | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index 0e5b15b..91921c6 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... }: +{ config, lib, ... }: { assertions = lib.optionals config.mailserver.ldap.enable [ { @@ -9,10 +9,6 @@ assertion = config.mailserver.extraVirtualAliases == {}; message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases"; } - { - assertion = config.mailserver.forwards == {}; - message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.forwards"; - } ] ++ lib.optionals (config.mailserver.enable && config.mailserver.certificateScheme != "acme") [ { assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; diff --git a/tests/ldap.nix b/tests/ldap.nix index 02c5ac1..bf4411b 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -104,6 +104,10 @@ pkgs.nixosTest { searchScope = "sub"; }; + forwards = { + "bob_fw@example.com" = "bob@example.com"; + }; + vmailGroupName = "vmail"; vmailUID = 5000; @@ -179,5 +183,39 @@ pkgs.nixosTest { "--dst-password-file <(echo '${bobPassword}')", "--ignore-dkim-spf" ])) + + with subtest("Test mail forwarding works"): + machine.succeed(" ".join([ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--smtp-username alice@example.com", + "--imap-host localhost", + "--imap-username bob@example.com", + "--from-addr alice@example.com", + "--to-addr bob_fw@example.com", + "--src-password-file <(echo '${alicePassword}')", + "--dst-password-file <(echo '${bobPassword}')", + "--ignore-dkim-spf" + ])) + + with subtest("Test cannot send mail from forwarded address"): + machine.fail(" ".join([ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--smtp-username bob@example.com", + "--imap-host localhost", + "--imap-username alice@example.com", + "--from-addr bob_fw@example.com", + "--to-addr alice@example.com", + "--src-password-file <(echo '${bobPassword}')", + "--dst-password-file <(echo '${alicePassword}')", + "--ignore-dkim-spf" + ])) + machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'") + ''; } From f6a64f713ce82945446b41a79e97fee93af97c2b Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 6 May 2025 05:30:05 +0200 Subject: [PATCH 154/225] docs/release-notes: advertise mailserver.forwards with ldap --- docs/release-notes.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 556de5f..3cdd5da 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -14,6 +14,8 @@ NixOS 25.05 (`merge request `__) - Individual domains can now be excluded from DMARC Reporting through ``mailserver.dmarcReporting.excludedDomains``. (`merge request `__) +- Configuring ``mailserver.forwards`` is now possible when the setup relies on LDAP. + (`merge request `__) NixOS 24.11 ----------- From 71c5fe04f1f8eb6a2b455290ee5a6883ee68fd07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Thu, 24 Jun 2021 18:02:50 +0200 Subject: [PATCH 155/225] postfix: disable TLSv1.1 In accordance with https://ssl-config.mozilla.org/#server=postfix. --- docs/release-notes.rst | 2 ++ mail-server/postfix.nix | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 3cdd5da..f6511ee 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -16,6 +16,8 @@ NixOS 25.05 (`merge request `__) - Configuring ``mailserver.forwards`` is now possible when the setup relies on LDAP. (`merge request `__) +- Support for TLS 1.1 was disabled in accordance with `Mozilla's recommendations `_. + (`merge request `__) NixOS 24.11 ----------- diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index da06111..d14e6d3 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -270,10 +270,10 @@ in smtpd_tls_security_level = "may"; # Disable obselete protocols - smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; - smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; - smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; - smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; + smtpd_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; + smtp_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; + smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; + smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; smtp_tls_ciphers = "high"; smtpd_tls_ciphers = "high"; From fac7efe94617d812a422d60b5434aca80eb1a80c Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 7 May 2025 02:23:32 +0200 Subject: [PATCH 156/225] postfix: Support opportunistic DANE TLS This migrates the security level for outgoing SMTP connections to dane[1]. Either a server is configured for DANE or it now uses mandatory unauthenticated TLS. If DANE validation fails, the delivery will be tempfailed. If DANE is invalid or unusable the connection will fall back to unauthenticated mandatory TLS This has been the default in various mail distributions: - Mailcow since December 2016[2] - mailinabox since July 2014[3] [1] https://www.postfix.org/TLS_README.html#client_tls_dane [2] https://github.com/mailcow/mailcow-dockerized/commit/47a5166383a4ecae780ffd6ad2081dc3f070bd45 [3] https://github.com/mail-in-a-box/mailinabox/commit/e713af5f5aeca202c2bf88be324472b3ef898dc7 --- mail-server/postfix.nix | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index da06111..35462a0 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -245,6 +245,11 @@ in # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients lmtp_destination_recipient_limit = "1"; + # Opportunistic DANE support + # https://www.postfix.org/postconf.5.html#smtp_tls_security_level + smtp_dns_support_level = "dnssec"; + smtp_tls_security_level = "dane"; + # sasl with dovecot smtpd_sasl_type = "dovecot"; smtpd_sasl_path = "/run/dovecot2/auth"; From 2e254b4b5e37dc3cbbbe09d9dbc77f3c2a213de5 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 7 May 2025 02:52:28 +0200 Subject: [PATCH 157/225] postfix: adjust comments around smtpd_recipient_restrictions --- mail-server/postfix.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index d14e6d3..11a304d 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -258,10 +258,11 @@ in "check_sender_access ${mappedFile "reject_senders"}" ]; - # quota checking smtpd_recipient_restrictions = [ + # reject selected recipients "check_recipient_access ${mappedFile "denied_recipients"}" "check_recipient_access ${mappedFile "reject_recipients"}" + # quota checking "check_policy_service inet:localhost:12340" ]; From 86b48f368f665c3ddc5421ec4d99f9d0d2555fda Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 7 May 2025 03:55:17 +0200 Subject: [PATCH 158/225] tests: remove invalid escape sequences >>> "\@" :1: SyntaxWarning: invalid escape sequence '\@' '\\@' --- tests/clamav.nix | 2 +- tests/external.nix | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/clamav.nix b/tests/clamav.nix index ae186df..71061a2 100644 --- a/tests/clamav.nix +++ b/tests/clamav.nix @@ -222,7 +222,7 @@ pkgs.nixosTest { 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' + '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 diff --git a/tests/external.nix b/tests/external.nix index c7e9a0d..15ea3b2 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -272,7 +272,7 @@ pkgs.nixosTest { To: Chuck Cc: Bcc: - Subject: This is a test Email from postmaster\@example.com to chuck + Subject: This is a test Email from postmaster@example.com to chuck Reply-To: Hello Chuck, @@ -286,7 +286,7 @@ pkgs.nixosTest { To: User1 Cc: Bcc: - Subject: This is a test Email from single-alias\@example.com to user1 + Subject: This is a test Email from single-alias@example.com to user1 Reply-To: Hello User1, @@ -301,7 +301,7 @@ pkgs.nixosTest { To: Multi Alias Cc: Bcc: - Subject: This is a test Email from user2\@example.com to multi-alias + Subject: This is a test Email from user2@example.com to multi-alias Reply-To: Hello Multi Alias, @@ -367,7 +367,7 @@ pkgs.nixosTest { 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" + "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" ]') @@ -395,7 +395,7 @@ pkgs.nixosTest { 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" + "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 @@ -408,7 +408,7 @@ pkgs.nixosTest { 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" + "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 @@ -418,7 +418,7 @@ pkgs.nixosTest { 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" + "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 @@ -427,7 +427,7 @@ pkgs.nixosTest { 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" + "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 @@ -438,7 +438,7 @@ pkgs.nixosTest { 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" + "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 @@ -447,7 +447,7 @@ pkgs.nixosTest { 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" + "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 @@ -458,7 +458,7 @@ pkgs.nixosTest { client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc") client.succeed( - "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota\@example.com < /etc/root/email2 >&2" + "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 @@ -467,7 +467,7 @@ pkgs.nixosTest { 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" + "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" ]') @@ -480,10 +480,10 @@ pkgs.nixosTest { 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" + "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" + "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" ]') From a1ff289bf91754b5ccb836ba7220a1bd5d4c5105 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 7 May 2025 18:00:16 +0200 Subject: [PATCH 159/225] dovecot: migrate queue-status to UNIX domain socket --- mail-server/dovecot.nix | 6 ++++++ mail-server/postfix.nix | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index c75aff2..64a13b2 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -307,6 +307,12 @@ in } service quota-status { + inet_listener { + port = 0 + } + unix_listener quota-status { + user = postfix + } vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB } diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 63ade37..db3e581 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -268,7 +268,7 @@ in "check_recipient_access ${mappedFile "denied_recipients"}" "check_recipient_access ${mappedFile "reject_recipients"}" # quota checking - "check_policy_service inet:localhost:12340" + "check_policy_service unix:/run/dovecot2/quota-status" ]; # TLS settings, inspired by https://github.com/jeaye/nix-files From 8970ed08496e83b5b9a8bc1fc0bf94c7d58ab24d Mon Sep 17 00:00:00 2001 From: Jeremy Fleischman Date: Sun, 13 Apr 2025 10:56:07 -0700 Subject: [PATCH 160/225] Suggest that folks enable DMARC reporting SNM supports DMARC reporting, but it's disabled by default. For email greybeards, that's fine, but I think it would be useful to teach email newbies (as I was a few months ago) that this is something you should seriously consider enabling. I opted to put this in a new "Advanced Configurations" section that points experienced mailserver admins to our howto guides, and newbies to a couple of important things. refs: https://github.com/NixOS/infra/pull/635 --- docs/advanced-configurations.rst | 14 ++++++++++++++ docs/index.rst | 1 + docs/setup-guide.rst | 5 +++++ 3 files changed, 20 insertions(+) create mode 100644 docs/advanced-configurations.rst diff --git a/docs/advanced-configurations.rst b/docs/advanced-configurations.rst new file mode 100644 index 0000000..e2b7837 --- /dev/null +++ b/docs/advanced-configurations.rst @@ -0,0 +1,14 @@ +Advanced Configurations +======================= + +Congratulations on completing the `Setup Guide `_! + +If you're an experienced mailserver admin, then you probably know what you want +to do next. Our How-to guides (accessible in the navigation sidebar) +might help you accomplish your goals. If not, consider contributing a guide! + +If this is your first mailserver, consider the following: + +- Set up `backups `_. +- Enable `DMARC reporting `_ to be a + good citizen in the mail ecosystem. diff --git a/docs/index.rst b/docs/index.rst index 2fd1e1a..3dd919a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,7 @@ Welcome to NixOS Mailserver's documentation! :maxdepth: 2 setup-guide + advanced-configurations howto-develop faq release-notes diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 08de1b3..35b6889 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -236,3 +236,8 @@ Besides that, you can send an email to score, and let `mxtoolbox.com `__ take a look at your setup, but if you followed the steps closely then everything should be awesome! + +Next steps (optional) +~~~~~~~~~~~~~~~~~~~~~ + +Take a look through our `Advanced Configurations `_. From b92870c24046ee1576c79b286aad8a6b8e9768bc Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 23:22:29 +0200 Subject: [PATCH 161/225] treewide: drop nixops docs and examples This is not a deployment system we recommend using anymore in 2025. Closes: #320 --- docs/howto-develop.rst | 26 -------------------------- nixops/single-server.nix | 31 ------------------------------- nixops/vbox.nix | 9 --------- 3 files changed, 66 deletions(-) delete mode 100644 nixops/single-server.nix delete mode 100644 nixops/vbox.nix diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index ded90b9..acdc7bd 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -44,29 +44,3 @@ To build the documentation, you need to enable `Nix Flakes $ nix build .#documentation $ xdg-open result/index.html - -Nixops ------- - -You can test the setup via ``nixops``. After installation, do - -:: - - $ nixops create nixops/single-server.nix nixops/vbox.nix -d mail - $ nixops deploy -d mail - $ nixops info -d mail - -You can then test the server via e.g. \ ``telnet``. To log into it, use - -:: - - $ nixops ssh -d mail mailserver - -Imap ----- - -To test imap manually use - -:: - - $ openssl s_client -host mail.example.com -port 143 -starttls imap diff --git a/nixops/single-server.nix b/nixops/single-server.nix deleted file mode 100644 index c002da7..0000000 --- a/nixops/single-server.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ - network.description = "mail server"; - - mailserver = - { config, pkgs, ... }: - { - imports = [ - ../default.nix - ]; - - mailserver = { - enable = true; - fqdn = "mail.example.com"; - domains = [ "example.com" "example2.com" ]; - loginAccounts = { - "user1@example.com" = { - hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; - }; - }; - extraVirtualAliases = { - "info@example.com" = "user1@example.com"; - "postmaster@example.com" = "user1@example.com"; - "abuse@example.com" = "user1@example.com"; - "user1@example2.com" = "user1@example.com"; - "info@example2.com" = "user1@example.com"; - "postmaster@example2.com" = "user1@example.com"; - "abuse@example2.com" = "user1@example.com"; - }; - }; - }; -} diff --git a/nixops/vbox.nix b/nixops/vbox.nix deleted file mode 100644 index 2af7518..0000000 --- a/nixops/vbox.nix +++ /dev/null @@ -1,9 +0,0 @@ -{ - mailserver = - { config, pkgs, ... }: - { deployment.targetEnv = "virtualbox"; - deployment.virtualbox.memorySize = 1024; # megabytes - deployment.virtualbox.vcpu = 2; # number of cpus - deployment.virtualbox.headless = true; - }; -} From ef1e02e555e5e3c55ebfca4705b1f899dcc4ff87 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sat, 10 May 2025 02:36:21 +0200 Subject: [PATCH 162/225] flake.nix: run tests against pinned nixpkgs and migrate to the new runTest, which evaluates much faster. --- flake.nix | 27 +++++++++++++++++++-------- tests/clamav.nix | 22 ++++++++++++++-------- tests/external.nix | 16 ++++++++-------- tests/internal.nix | 25 +++++++++++++++++-------- tests/ldap.nix | 9 +++------ tests/minimal.nix | 20 ++++++++------------ tests/multiple.nix | 17 +++++++++++------ 7 files changed, 80 insertions(+), 56 deletions(-) diff --git a/flake.nix b/flake.nix index 6fb5637..1581ea3 100644 --- a/flake.nix +++ b/flake.nix @@ -21,27 +21,38 @@ releases = [ { name = "unstable"; + nixpkgs = nixpkgs; pkgs = nixpkgs.legacyPackages.${system}; } { name = "24.11"; + nixpkgs = nixpkgs-24_11; pkgs = nixpkgs-24_11.legacyPackages.${system}; } ]; testNames = [ - "internal" - "external" "clamav" - "multiple" + "external" + "internal" "ldap" + "multiple" ]; - genTest = testName: release: { - "name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}"; - "value"= import (./tests/. + "/${testName}.nix") { - pkgs = release.pkgs; - inherit blobs; + + 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 = ; diff --git a/tests/clamav.nix b/tests/clamav.nix index 71061a2..19b799f 100644 --- a/tests/clamav.nix +++ b/tests/clamav.nix @@ -14,12 +14,17 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ pkgs ? import {}, blobs}: +{ + lib, + blobs, + ... +}: -pkgs.nixosTest { +{ name = "clamav"; + nodes = { - server = { config, pkgs, lib, ... }: + server = { pkgs, ... }: { imports = [ ../default.nix @@ -28,6 +33,8 @@ pkgs.nixosTest { virtualisation.memorySize = 1500; + environment.systemPackages = with pkgs; [ netcat ]; + services.rsyslogd = { enable = true; defaultConfig = '' @@ -83,7 +90,7 @@ pkgs.nixosTest { "root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; }; }; - client = { nodes, config, pkgs, ... }: let + client = { nodes, pkgs, ... }: let serverIP = nodes.server.networking.primaryIPAddress; clientIP = nodes.client.networking.primaryIPAddress; grep-ip = pkgs.writeScriptBin "grep-ip" '' @@ -180,8 +187,7 @@ pkgs.nixosTest { }; }; - testScript = { nodes, ... }: - '' + testScript = '' start_all() server.wait_for_unit("multi-user.target") @@ -189,10 +195,10 @@ pkgs.nixosTest { # 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 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) server.wait_until_succeeds( - "set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" ) client.execute("cp -p /etc/root/.* ~/") diff --git a/tests/external.nix b/tests/external.nix index 15ea3b2..77fa156 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -14,18 +14,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ pkgs ? import {}, ...}: - -pkgs.nixosTest { +{ name = "external"; + nodes = { - server = { config, pkgs, ... }: + server = { pkgs, ... }: { imports = [ ../default.nix ./lib/config.nix ]; + environment.systemPackages = with pkgs; [ netcat ]; + virtualisation.memorySize = 1024; services.rsyslogd = { @@ -86,7 +87,7 @@ pkgs.nixosTest { }; }; }; - client = { nodes, config, pkgs, ... }: let + client = { nodes, pkgs, ... }: let serverIP = nodes.server.networking.primaryIPAddress; clientIP = nodes.client.networking.primaryIPAddress; grep-ip = pkgs.writeScriptBin "grep-ip" '' @@ -341,8 +342,7 @@ pkgs.nixosTest { }; }; - testScript = { nodes, ... }: - '' + testScript = '' start_all() server.wait_for_unit("multi-user.target") @@ -350,7 +350,7 @@ pkgs.nixosTest { # TODO put this blocking into the systemd units? server.wait_until_succeeds( - "set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) client.execute("cp -p /etc/root/.* ~/") diff --git a/tests/internal.nix b/tests/internal.nix index 5835ce6..8f47e70 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -14,7 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ pkgs ? import {}, ...}: +{ + pkgs, + ... +}: let sendMail = pkgs.writeTextFile { @@ -36,10 +39,11 @@ let hashedPasswordFile = hashPassword "my-password"; passwordFile = pkgs.writeText "password" "my-password"; in -pkgs.nixosTest { +{ name = "internal"; + nodes = { - machine = { config, pkgs, ... }: { + machine = { pkgs, ... }: { imports = [ ./../default.nix ./lib/config.nix @@ -50,7 +54,12 @@ pkgs.nixosTest { environment.systemPackages = [ (pkgs.writeScriptBin "mail-check" '' ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ - '')]; + '') + ] ++ (with pkgs; [ + curl + openssl + netcat + ]); mailserver = { enable = true; @@ -174,22 +183,22 @@ pkgs.nixosTest { machine.wait_for_open_port(25) # TODO put this blocking into the systemd units machine.wait_until_succeeds( - "set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) machine.succeed( - "cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q '554 5.5.0 Error'" + "cat ${sendMail} | nc localhost 25 | grep -q '554 5.5.0 Error'" ) with subtest("rspamd controller serves web ui"): machine.succeed( - "set +o pipefail; ${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q ''" + "set +o pipefail; curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q ''" ) with subtest("imap port 143 is closed and imaps is serving SSL"): machine.wait_for_closed_port(143) machine.wait_for_open_port(993) machine.succeed( - "echo | ${pkgs.openssl}/bin/openssl s_client -connect localhost:993 | grep 'New, TLS'" + "echo | openssl s_client -connect localhost:993 | grep 'New, TLS'" ) ''; } diff --git a/tests/ldap.nix b/tests/ldap.nix index bf4411b..8187d7d 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -1,16 +1,13 @@ -{ pkgs ? import {} -, ... -}: - let bindPassword = "unsafegibberish"; alicePassword = "testalice"; bobPassword = "testbob"; in -pkgs.nixosTest { +{ name = "ldap"; + nodes = { - machine = { config, pkgs, ... }: { + machine = { pkgs, ... }: { imports = [ ./../default.nix ./lib/config.nix diff --git a/tests/minimal.nix b/tests/minimal.nix index 88cb276..407f221 100644 --- a/tests/minimal.nix +++ b/tests/minimal.nix @@ -14,18 +14,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -import { +{ + name = "minimal"; - nodes.machine = - { config, pkgs, ... }: - { - imports = [ - ./../default.nix - ]; - }; + nodes.machine = { + imports = [ ./../default.nix ]; + }; - testScript = - '' - machine.wait_for_unit("multi-user.target"); - ''; + testScript = '' + machine.wait_for_unit("multi-user.target"); + ''; } diff --git a/tests/multiple.nix b/tests/multiple.nix index 8a4c07b..2427feb 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -1,6 +1,9 @@ # This tests is used to test features requiring several mail domains. -{ pkgs ? import {}, ...}: +{ + pkgs, + ... +}: let hashPassword = password: pkgs.runCommand @@ -12,8 +15,9 @@ let password = pkgs.writeText "password" "password"; - domainGenerator = domain: { config, pkgs, ... }: { + domainGenerator = domain: { pkgs, ... }: { imports = [../default.nix]; + environment.systemPackages = with pkgs; [ netcat ]; virtualisation.memorySize = 1024; mailserver = { enable = true; @@ -36,8 +40,9 @@ let in -pkgs.nixosTest { +{ name = "multiple"; + nodes = { domain1 = {...}: { imports = [ @@ -50,7 +55,7 @@ pkgs.nixosTest { }; }; domain2 = domainGenerator "domain2.com"; - client = { config, pkgs, ... }: { + client = { pkgs, ... }: { environment.systemPackages = [ (pkgs.writeScriptBin "mail-check" '' ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ @@ -65,10 +70,10 @@ pkgs.nixosTest { # TODO put this blocking into the systemd units? domain1.wait_until_succeeds( - "set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) domain2.wait_until_succeeds( - "set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" ) # user@domain1.com sends a mail to user@domain2.com From 1f82d59d6728de34db3262125bba96b934b5deee Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sat, 10 May 2025 20:58:46 +0200 Subject: [PATCH 163/225] ci: use hydra-cli from pinned nixpkgs --- .gitlab-ci.yml | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 35980ae..8901bb5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,13 +1,18 @@ +.hydra-cli: + image: docker.nix-community.org/nixpkgs/nix-flakes + script: + - nix run --inputs-from ./. nixpkgs#hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver "${jobset}" + hydra-pr: + extends: .hydra-cli only: - merge_requests - image: nixos/nix - script: - - nix-shell -I nixpkgs=channel:nixos-24.11 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}' + variables: + jobset: $CI_MERGE_REQUEST_IID hydra-master: + extends: .hydra-cli only: - master - image: nixos/nix - script: - - nix-shell -I nixpkgs=channel:nixos-24.11 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master' + variables: + jobset: master From 1ce644871b1088dd8d469aef809cfd47570ae619 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 15 May 2025 04:18:31 +0200 Subject: [PATCH 164/225] flake.nix: ignore the flake registry There is no real benefit using it anyway. --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 1581ea3..dac72f8 100644 --- a/flake.nix +++ b/flake.nix @@ -6,8 +6,8 @@ url = "github:edolstra/flake-compat"; flake = false; }; - nixpkgs.url = "flake:nixpkgs/nixos-unstable"; - nixpkgs-24_11.url = "flake:nixpkgs/nixos-24.11"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nixpkgs-24_11.url = "github:NixOS/nixpkgs/nixos-24.11"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; From edd828ca88ca494edfe666e7decd6608d2f16538 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 15 May 2025 16:15:54 +0200 Subject: [PATCH 165/225] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'flake-compat': 'github:edolstra/flake-compat/0f9255e01c2351cc7d116c072cb317785dd33b33' (2023-10-04) → 'github:edolstra/flake-compat/9100a0f413b0c601e0533d1d94ffd501ce2e7885' (2025-05-12) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/23e89b7da85c3640bbc2173fe04f4bd114342367' (2024-11-19) → 'github:NixOS/nixpkgs/adaa24fbf46737f3f1b5497bf64bae750f82942e' (2025-05-13) • Updated input 'nixpkgs-24_11': 'github:NixOS/nixpkgs/314e12ba369ccdb9b352a4db26ff419f7c49fa84' (2024-12-13) → 'github:NixOS/nixpkgs/5d736263df906c5da72ab0f372427814de2f52f8' (2025-05-14) --- flake.lock | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/flake.lock b/flake.lock index c6ec247..d79f675 100644 --- a/flake.lock +++ b/flake.lock @@ -19,11 +19,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", "owner": "edolstra", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "type": "github" }, "original": { @@ -34,32 +34,34 @@ }, "nixpkgs": { "locked": { - "lastModified": 1732014248, - "narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", + "lastModified": 1747179050, + "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", + "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e", "type": "github" }, "original": { - "id": "nixpkgs", + "owner": "NixOS", "ref": "nixos-unstable", - "type": "indirect" + "repo": "nixpkgs", + "type": "github" } }, "nixpkgs-24_11": { "locked": { - "lastModified": 1734083684, - "narHash": "sha256-5fNndbndxSx5d+C/D0p/VF32xDiJCJzyOqorOYW4JEo=", + "lastModified": 1747209494, + "narHash": "sha256-fLise+ys+bpyjuUUkbwqo5W/UyIELvRz9lPBPoB0fbM=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "314e12ba369ccdb9b352a4db26ff419f7c49fa84", + "rev": "5d736263df906c5da72ab0f372427814de2f52f8", "type": "github" }, "original": { - "id": "nixpkgs", + "owner": "NixOS", "ref": "nixos-24.11", - "type": "indirect" + "repo": "nixpkgs", + "type": "github" } }, "root": { From 235dba2d82aed18ba696d94da85ca6d7a7d7f4da Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 12 May 2025 01:03:46 +0200 Subject: [PATCH 166/225] tests/external: ignore new xapian warnings These looks harmless. Closes: #322 --- tests/external.nix | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/external.nix b/tests/external.nix index 77fa156..61ffb4f 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -508,7 +508,13 @@ 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 'FTS Xapian: Box is empty' | grep -vE 'FTS Xapian:.*does not exist. Creating it' | grep -i warning >&2" + "journalctl -u dovecot2 | \ + grep -v 'Expunged message reappeared, giving a new UID' | \ + grep -v 'FTS Xapian: Box is empty' | \ + grep -v 'FTS Xapian: New version of the plugin' | \ + grep -vE 'FTS Xapian:.*does not exist. Creating it' | \ + grep -vE 'FTS Xapian:.*indexes do not exist. Initializing DB' | \ + grep -i warning >&2" ) ''; } From dd83a2c7ad6b30d9a0109c82143e08a877b3f3fb Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 12 May 2025 03:13:14 +0200 Subject: [PATCH 167/225] dovecot: rename sieve bayes/ham learning script Updates the spamassasin reference to talk about rspamd. --- mail-server/dovecot.nix | 4 ++-- mail-server/dovecot/imap_sieve/report-ham.sieve | 2 +- mail-server/dovecot/imap_sieve/report-spam.sieve | 2 +- tests/external.nix | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 64a13b2..27af741 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -205,9 +205,9 @@ in ''; pipeBins = map lib.getExe [ - (pkgs.writeShellScriptBin "sa-learn-ham.sh" + (pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham") - (pkgs.writeShellScriptBin "sa-learn-spam.sh" + (pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam") ]; }; diff --git a/mail-server/dovecot/imap_sieve/report-ham.sieve b/mail-server/dovecot/imap_sieve/report-ham.sieve index a9d30cf..720be7a 100644 --- a/mail-server/dovecot/imap_sieve/report-ham.sieve +++ b/mail-server/dovecot/imap_sieve/report-ham.sieve @@ -12,4 +12,4 @@ if environment :matches "imap.user" "*" { set "username" "${1}"; } -pipe :copy "sa-learn-ham.sh" [ "${username}" ]; +pipe :copy "rspamd-learn-ham.sh" [ "${username}" ]; diff --git a/mail-server/dovecot/imap_sieve/report-spam.sieve b/mail-server/dovecot/imap_sieve/report-spam.sieve index 4024b7a..4681aac 100644 --- a/mail-server/dovecot/imap_sieve/report-spam.sieve +++ b/mail-server/dovecot/imap_sieve/report-spam.sieve @@ -4,4 +4,4 @@ if environment :matches "imap.user" "*" { set "username" "${1}"; } -pipe :copy "sa-learn-spam.sh" [ "${username}" ]; \ No newline at end of file +pipe :copy "rspamd-learn-spam.sh" [ "${username}" ]; diff --git a/tests/external.nix b/tests/external.nix index 61ffb4f..c32a9e1 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -473,9 +473,9 @@ 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 sa-learn-spam.sh >&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 sa-learn-ham.sh >&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 From 41e513da641891fa615c1ee4a2fdeaf9d2d3db81 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 23 Apr 2025 16:51:22 +0200 Subject: [PATCH 168/225] flake.nix: configure pre-commit --- .gitignore | 1 + flake.lock | 46 ++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index b2be92b..58399cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ result +.pre-commit-config.yaml diff --git a/flake.lock b/flake.lock index d79f675..a712d89 100644 --- a/flake.lock +++ b/flake.lock @@ -32,6 +32,51 @@ "type": "github" } }, + "git-hooks": { + "inputs": { + "flake-compat": [ + "flake-compat" + ], + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1742649964, + "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1747179050, @@ -68,6 +113,7 @@ "inputs": { "blobs": "blobs", "flake-compat": "flake-compat", + "git-hooks": "git-hooks", "nixpkgs": "nixpkgs", "nixpkgs-24_11": "nixpkgs-24_11" } diff --git a/flake.nix b/flake.nix index dac72f8..8a154da 100644 --- a/flake.nix +++ b/flake.nix @@ -6,6 +6,11 @@ url = "github:edolstra/flake-compat"; flake = false; }; + git-hooks = { + url = "github:cachix/git-hooks.nix"; + inputs.flake-compat.follows = "flake-compat"; + inputs.nixpkgs.follows = "nixpkgs"; + }; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs-24_11.url = "github:NixOS/nixpkgs/nixos-24.11"; blobs = { @@ -14,7 +19,7 @@ }; }; - outputs = { self, blobs, nixpkgs, nixpkgs-24_11, ... }: let + outputs = { self, blobs, git-hooks, nixpkgs, nixpkgs-24_11, ... }: let lib = nixpkgs.lib; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; @@ -123,7 +128,51 @@ hydraJobs.${system} = allTests // { inherit documentation; }; - checks.${system} = allTests; + 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; + entry = lib.getExe pkgs.rstcheckWithSphinx; + files = "\\.rst$"; + }; + + # nix + deadnix.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; + entry = lib.getExe pkgs.check-sieve; + files = "\\.sieve$"; + }; + }; + }; + }; packages.${system} = { inherit optionsDoc documentation; }; @@ -131,7 +180,8 @@ inputsFrom = [ documentation ]; packages = with pkgs; [ clamav - ]; + ] ++ self.checks.${system}.pre-commit.enabledPackages; + shellHook = self.checks.${system}.pre-commit.shellHook; }; devShell.${system} = self.devShells.${system}.default; # compatibility }; From dccca0506a7b1b65ed24da6ca8d2b4ea305b5f0d Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 23 Apr 2025 17:12:12 +0200 Subject: [PATCH 169/225] Provide direnv integration for flake devshell --- .envrc | 3 +++ .gitignore | 1 + 2 files changed, 4 insertions(+) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..069abc3 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +# shellcheck shell=bash + +use flake diff --git a/.gitignore b/.gitignore index 58399cb..0d3fe25 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ result +.direnv .pre-commit-config.yaml From d0ac5ce64c794d67a0c85a671a387f03e0cc9f50 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 23 Apr 2025 17:39:44 +0200 Subject: [PATCH 170/225] flake.nix: annotate flake-compat usage It is not used within flake.nix, so add a note that it is used elsewhere. --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 8a154da..a25d04a 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,7 @@ inputs = { flake-compat = { + # for shell.nix compat url = "github:edolstra/flake-compat"; flake = false; }; From ff9087adb492cad53a4f6e2ea9fc49282a12c8a4 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 24 Apr 2025 01:48:36 +0200 Subject: [PATCH 171/225] flake.nix: drop CC from devshell We absolutely do not need a C compiler in here. --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index a25d04a..3accf24 100644 --- a/flake.nix +++ b/flake.nix @@ -177,7 +177,7 @@ packages.${system} = { inherit optionsDoc documentation; }; - devShells.${system}.default = pkgs.mkShell { + devShells.${system}.default = pkgs.mkShellNoCC { inputsFrom = [ documentation ]; packages = with pkgs; [ clamav From 313f94ed8ff4c7b19feb9d14e4153235670206bd Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 7 May 2025 21:46:57 +0200 Subject: [PATCH 172/225] flake.nix: create pre-commit hydra job --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 3accf24..286802e 100644 --- a/flake.nix +++ b/flake.nix @@ -128,6 +128,7 @@ 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 { From 1615c935119406f0fcb5aedde614438d1bba945d Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 01:13:15 +0200 Subject: [PATCH 173/225] scripts/mail-check: fix typing issues Replaces the body payload parsing with proper handling for multipart messages. --- scripts/mail-check.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/mail-check.py b/scripts/mail-check.py index 7d3935c..5cdfdca 100644 --- a/scripts/mail-check.py +++ b/scripts/mail-check.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta import email import email.utils import time +from typing import cast RETRY = 100 @@ -84,7 +85,7 @@ def _read_mail( for _ in range(0, RETRY): print("Retrying") obj.select() - typ, data = obj.search(None, '(SINCE %s) (SUBJECT "%s")'%(dt, subject)) + _, data = obj.search(None, '(SINCE %s) (SUBJECT "%s")' % (dt, subject)) if data == [b'']: time.sleep(1) continue @@ -99,12 +100,21 @@ def _read_mail( if delete: obj.store(uid, '+FLAGS', '\\Deleted') obj.expunge() - message = email.message_from_bytes(raw[0][1]) + assert raw[0] and raw[0][1] + message = email.message_from_bytes(cast(bytes, raw[0][1])) print("Message with subject '%s' has been found" % message['subject']) if show_body: - for m in message.get_payload(): - if m.get_content_type() == 'text/plain': - print("Body:\n%s" % m.get_payload(decode=True).decode('utf-8')) + if message.is_multipart(): + for part in message.walk(): + ctype = part.get_content_type() + if ctype == "text/plain": + body = cast(bytes, part.get_payload(decode=True)).decode() + print(f"Body:\n{body}") + else: + print(f"Body with content type {ctype} not printed") + else: + body = cast(bytes, message.get_payload(decode=True)).decode() + print(f"Body:\n{body}") break if message is None: @@ -164,7 +174,7 @@ def send_and_read(args): def read(args): _read_mail(imap_host=args.imap_host, imap_port=args.imap_port, - to_addr=args.imap_username, + imap_username=args.imap_username, to_pwd=args.imap_password, subject=args.subject, ignore_dkim_spf=args.ignore_dkim_spf, From f9fcbe9430097d34b5756833ad1a7cb7112676a7 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 01:38:42 +0200 Subject: [PATCH 174/225] scripts/generate-options: fix typing issue --- scripts/generate-options.py | 44 +++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/scripts/generate-options.py b/scripts/generate-options.py index 75a25ae..652f89a 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -33,27 +33,29 @@ groups = ["mailserver.loginAccounts", "mailserver.borgbackup"] def render_option_value(opt, attr): - if attr in opt: - if isinstance(opt[attr], dict) and '_type' in opt[attr]: - if opt[attr]['_type'] == 'literalExpression': - if '\n' in opt[attr]['text']: - res = '\n```nix\n' + opt[attr]['text'].rstrip('\n') + '\n```' - else: - res = '```{}```'.format(opt[attr]['text']) - elif opt[attr]['_type'] == 'literalMD': - res = opt[attr]['text'] - else: - s = str(opt[attr]) - if s == "": - res = '`""`' - elif '\n' in s: - res = '\n```\n' + s.rstrip('\n') + '\n```' - else: - res = '```{}```'.format(s) - res = '- ' + attr + ': ' + res - else: - res = "" - return res + if attr not in opt: + return "" + + if isinstance(opt[attr], dict) and '_type' in opt[attr]: + if opt[attr]['_type'] == 'literalExpression': + if '\n' in opt[attr]['text']: + res = '\n```nix\n' + opt[attr]['text'].rstrip('\n') + '\n```' + else: + res = '```{}```'.format(opt[attr]['text']) + elif opt[attr]['_type'] == 'literalMD': + res = opt[attr]['text'] + else: + assert RuntimeError(f"Unhandled option type {opt[attr]["_type"]}") + else: + s = str(opt[attr]) + if s == "": + res = '`""`' + elif '\n' in s: + res = '\n```\n' + s.rstrip('\n') + '\n```' + else: + res = '```{}```'.format(s) + + return '- ' + attr + ': ' + res # type: ignore def print_option(opt): if isinstance(opt['description'], dict) and '_type' in opt['description']: # mdDoc From a7d580b9342a547f884b64efb3fefd84403413c3 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 01:40:37 +0200 Subject: [PATCH 175/225] treewide: reformat python code --- docs/conf.py | 22 ++-- scripts/generate-options.py | 73 ++++++------ scripts/mail-check.py | 219 ++++++++++++++++++++++-------------- 3 files changed, 185 insertions(+), 129 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1845917..7bc771b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -17,9 +17,9 @@ # -- Project information ----------------------------------------------------- -project = 'NixOS Mailserver' -copyright = '2022, NixOS Mailserver Contributors' -author = 'NixOS Mailserver Contributors' +project = "NixOS Mailserver" +copyright = "2022, NixOS Mailserver Contributors" +author = "NixOS Mailserver Contributors" # -- General configuration --------------------------------------------------- @@ -27,33 +27,31 @@ author = 'NixOS Mailserver Contributors' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [ - 'myst_parser' -] +extensions = ["myst_parser"] myst_enable_extensions = [ - 'colon_fence', - 'linkify', + "colon_fence", + "linkify", ] smartquotes = False # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -master_doc = 'index' +master_doc = "index" # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/scripts/generate-options.py b/scripts/generate-options.py index 652f89a..ab1227d 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -21,64 +21,71 @@ template = """ f = open(sys.argv[1]) options = json.load(f) -groups = ["mailserver.loginAccounts", - "mailserver.certificate", - "mailserver.dkim", - "mailserver.dmarcReporting", - "mailserver.fullTextSearch", - "mailserver.redis", - "mailserver.ldap", - "mailserver.monitoring", - "mailserver.backup", - "mailserver.borgbackup"] +groups = [ + "mailserver.loginAccounts", + "mailserver.certificate", + "mailserver.dkim", + "mailserver.dmarcReporting", + "mailserver.fullTextSearch", + "mailserver.redis", + "mailserver.ldap", + "mailserver.monitoring", + "mailserver.backup", + "mailserver.borgbackup", +] + def render_option_value(opt, attr): if attr not in opt: return "" - if isinstance(opt[attr], dict) and '_type' in opt[attr]: - if opt[attr]['_type'] == 'literalExpression': - if '\n' in opt[attr]['text']: - res = '\n```nix\n' + opt[attr]['text'].rstrip('\n') + '\n```' + if isinstance(opt[attr], dict) and "_type" in opt[attr]: + if opt[attr]["_type"] == "literalExpression": + if "\n" in opt[attr]["text"]: + res = "\n```nix\n" + opt[attr]["text"].rstrip("\n") + "\n```" else: - res = '```{}```'.format(opt[attr]['text']) - elif opt[attr]['_type'] == 'literalMD': - res = opt[attr]['text'] + res = "```{}```".format(opt[attr]["text"]) + elif opt[attr]["_type"] == "literalMD": + res = opt[attr]["text"] else: assert RuntimeError(f"Unhandled option type {opt[attr]["_type"]}") else: s = str(opt[attr]) if s == "": res = '`""`' - elif '\n' in s: - res = '\n```\n' + s.rstrip('\n') + '\n```' + elif "\n" in s: + res = "\n```\n" + s.rstrip("\n") + "\n```" else: - res = '```{}```'.format(s) + res = "```{}```".format(s) + + return "- " + attr + ": " + res # type: ignore - return '- ' + attr + ': ' + res # type: ignore def print_option(opt): - if isinstance(opt['description'], dict) and '_type' in opt['description']: # mdDoc - description = opt['description']['text'] + if isinstance(opt["description"], dict) and "_type" in opt["description"]: # mdDoc + description = opt["description"]["text"] else: - description = opt['description'] - print(template.format( - key=opt['name'], - description=description or "", - type="- type: ```{}```".format(opt['type']), - default=render_option_value(opt, 'default'), - example=render_option_value(opt, 'example'))) + description = opt["description"] + print( + template.format( + key=opt["name"], + description=description or "", + type="- type: ```{}```".format(opt["type"]), + default=render_option_value(opt, "default"), + example=render_option_value(opt, "example"), + ) + ) print(header) for opt in options: - if any([opt['name'].startswith(c) for c in groups]): + if any([opt["name"].startswith(c) for c in groups]): continue print_option(opt) for c in groups: - print('## `{}`'.format(c)) + print("## `{}`".format(c)) print() for opt in options: - if opt['name'].startswith(c): + if opt["name"].startswith(c): print_option(opt) diff --git a/scripts/mail-check.py b/scripts/mail-check.py index 5cdfdca..db36bc9 100644 --- a/scripts/mail-check.py +++ b/scripts/mail-check.py @@ -1,33 +1,37 @@ -import smtplib, sys import argparse -import os -import uuid -import imaplib -from datetime import datetime, timedelta import email import email.utils +import imaplib +import smtplib import time +import uuid +from datetime import datetime, timedelta from typing import cast RETRY = 100 -def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls): - print("Sending mail with subject '{}'".format(subject)) - message = "\n".join([ - "From: {from_addr}", - "To: {to_addr}", - "Subject: {subject}", - "Message-ID: {random}@mail-check.py", - "Date: {date}", - "", - "This validates our mail server can send to Gmail :/"]).format( - from_addr=from_addr, - to_addr=to_addr, - subject=subject, - random=str(uuid.uuid4()), - date=email.utils.formatdate(), - ) +def _send_mail( + smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls +): + print("Sending mail with subject '{}'".format(subject)) + message = "\n".join( + [ + "From: {from_addr}", + "To: {to_addr}", + "Subject: {subject}", + "Message-ID: {random}@mail-check.py", + "Date: {date}", + "", + "This validates our mail server can send to Gmail :/", + ] + ).format( + from_addr=from_addr, + to_addr=to_addr, + subject=subject, + random=str(uuid.uuid4()), + date=email.utils.formatdate(), + ) retry = RETRY while True: @@ -44,7 +48,9 @@ def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr except smtplib.SMTPResponseException as e: if e.smtp_code == 451: # service unavailable error print(e) - elif e.smtp_code == 454: # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later') + elif ( + e.smtp_code == 454 + ): # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later') print(e) else: raise @@ -62,15 +68,17 @@ def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr print("Retry attempts exhausted") exit(5) + def _read_mail( - imap_host, - imap_port, - imap_username, - to_pwd, - subject, - ignore_dkim_spf, - show_body=False, - delete=True): + imap_host, + imap_port, + imap_username, + to_pwd, + subject, + ignore_dkim_spf, + show_body=False, + delete=True, +): print("Reading mail from %s" % imap_username) message = None @@ -81,28 +89,31 @@ def _read_mail( today = datetime.today() cutoff = today - timedelta(days=1) - dt = cutoff.strftime('%d-%b-%Y') + dt = cutoff.strftime("%d-%b-%Y") for _ in range(0, RETRY): print("Retrying") obj.select() _, data = obj.search(None, '(SINCE %s) (SUBJECT "%s")' % (dt, subject)) - if data == [b'']: + if data == [b""]: time.sleep(1) continue uids = data[0].decode("utf-8").split(" ") if len(uids) != 1: - print("Warning: %d messages have been found with subject containing %s " % (len(uids), subject)) + print( + "Warning: %d messages have been found with subject containing %s " + % (len(uids), subject) + ) # FIXME: we only consider the first matching message... uid = uids[0] - _, raw = obj.fetch(uid, '(RFC822)') + _, raw = obj.fetch(uid, "(RFC822)") if delete: - obj.store(uid, '+FLAGS', '\\Deleted') + obj.store(uid, "+FLAGS", "\\Deleted") obj.expunge() assert raw[0] and raw[0][1] message = email.message_from_bytes(cast(bytes, raw[0][1])) - print("Message with subject '%s' has been found" % message['subject']) + print("Message with subject '%s' has been found" % message["subject"]) if show_body: if message.is_multipart(): for part in message.walk(): @@ -118,21 +129,24 @@ def _read_mail( break if message is None: - print("Error: no message with subject '%s' has been found in INBOX of %s" % (subject, imap_username)) + print( + "Error: no message with subject '%s' has been found in INBOX of %s" + % (subject, imap_username) + ) exit(1) if ignore_dkim_spf: return # gmail set this standardized header - if 'ARC-Authentication-Results' in message: - if "dkim=pass" in message['ARC-Authentication-Results']: + if "ARC-Authentication-Results" in message: + if "dkim=pass" in message["ARC-Authentication-Results"]: print("DKIM ok") else: print("Error: no DKIM validation found in message:") print(message.as_string()) exit(2) - if "spf=pass" in message['ARC-Authentication-Results']: + if "spf=pass" in message["ARC-Authentication-Results"]: print("SPF ok") else: print("Error: no SPF validation found in message:") @@ -142,71 +156,108 @@ def _read_mail( print("DKIM and SPF verification failed") exit(4) + def send_and_read(args): src_pwd = None if args.src_password_file is not None: src_pwd = args.src_password_file.readline().rstrip() dst_pwd = args.dst_password_file.readline().rstrip() - if args.imap_username != '': + if args.imap_username != "": imap_username = args.imap_username else: imap_username = args.to_addr subject = "{}".format(uuid.uuid4()) - _send_mail(smtp_host=args.smtp_host, - smtp_port=args.smtp_port, - smtp_username=args.smtp_username, - from_addr=args.from_addr, - from_pwd=src_pwd, - to_addr=args.to_addr, - subject=subject, - starttls=args.smtp_starttls) + _send_mail( + smtp_host=args.smtp_host, + smtp_port=args.smtp_port, + smtp_username=args.smtp_username, + from_addr=args.from_addr, + from_pwd=src_pwd, + to_addr=args.to_addr, + subject=subject, + starttls=args.smtp_starttls, + ) + + _read_mail( + imap_host=args.imap_host, + imap_port=args.imap_port, + imap_username=imap_username, + to_pwd=dst_pwd, + subject=subject, + ignore_dkim_spf=args.ignore_dkim_spf, + ) - _read_mail(imap_host=args.imap_host, - imap_port=args.imap_port, - imap_username=imap_username, - to_pwd=dst_pwd, - subject=subject, - ignore_dkim_spf=args.ignore_dkim_spf) def read(args): - _read_mail(imap_host=args.imap_host, - imap_port=args.imap_port, - imap_username=args.imap_username, - to_pwd=args.imap_password, - subject=args.subject, - ignore_dkim_spf=args.ignore_dkim_spf, - show_body=args.show_body, - delete=False) + _read_mail( + imap_host=args.imap_host, + imap_port=args.imap_port, + imap_username=args.imap_username, + to_pwd=args.imap_password, + subject=args.subject, + ignore_dkim_spf=args.ignore_dkim_spf, + show_body=args.show_body, + delete=False, + ) + parser = argparse.ArgumentParser() subparsers = parser.add_subparsers() -parser_send_and_read = subparsers.add_parser('send-and-read', description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.") -parser_send_and_read.add_argument('--smtp-host', type=str) -parser_send_and_read.add_argument('--smtp-port', type=str, default=25) -parser_send_and_read.add_argument('--smtp-starttls', action='store_true') -parser_send_and_read.add_argument('--smtp-username', type=str, default='', help="username used for smtp login. If not specified, the from-addr value is used") -parser_send_and_read.add_argument('--from-addr', type=str) -parser_send_and_read.add_argument('--imap-host', required=True, type=str) -parser_send_and_read.add_argument('--imap-port', type=str, default=993) -parser_send_and_read.add_argument('--to-addr', type=str, required=True) -parser_send_and_read.add_argument('--imap-username', type=str, default='', help="username used for imap login. If not specified, the to-addr value is used") -parser_send_and_read.add_argument('--src-password-file', type=argparse.FileType('r')) -parser_send_and_read.add_argument('--dst-password-file', required=True, type=argparse.FileType('r')) -parser_send_and_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail") +parser_send_and_read = subparsers.add_parser( + "send-and-read", + description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.", +) +parser_send_and_read.add_argument("--smtp-host", type=str) +parser_send_and_read.add_argument("--smtp-port", type=str, default=25) +parser_send_and_read.add_argument("--smtp-starttls", action="store_true") +parser_send_and_read.add_argument( + "--smtp-username", + type=str, + default="", + help="username used for smtp login. If not specified, the from-addr value is used", +) +parser_send_and_read.add_argument("--from-addr", type=str) +parser_send_and_read.add_argument("--imap-host", required=True, type=str) +parser_send_and_read.add_argument("--imap-port", type=str, default=993) +parser_send_and_read.add_argument("--to-addr", type=str, required=True) +parser_send_and_read.add_argument( + "--imap-username", + type=str, + default="", + help="username used for imap login. If not specified, the to-addr value is used", +) +parser_send_and_read.add_argument("--src-password-file", type=argparse.FileType("r")) +parser_send_and_read.add_argument( + "--dst-password-file", required=True, type=argparse.FileType("r") +) +parser_send_and_read.add_argument( + "--ignore-dkim-spf", + action="store_true", + help="to ignore the dkim and spf verification on the read mail", +) parser_send_and_read.set_defaults(func=send_and_read) -parser_read = subparsers.add_parser('read', description="Search for an email with a subject containing 'subject' in the INBOX.") -parser_read.add_argument('--imap-host', type=str, default="localhost") -parser_read.add_argument('--imap-port', type=str, default=993) -parser_read.add_argument('--imap-username', required=True, type=str) -parser_read.add_argument('--imap-password', required=True, type=str) -parser_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail") -parser_read.add_argument('--show-body', action='store_true', help="print mail text/plain payload") -parser_read.add_argument('subject', type=str) +parser_read = subparsers.add_parser( + "read", + description="Search for an email with a subject containing 'subject' in the INBOX.", +) +parser_read.add_argument("--imap-host", type=str, default="localhost") +parser_read.add_argument("--imap-port", type=str, default=993) +parser_read.add_argument("--imap-username", required=True, type=str) +parser_read.add_argument("--imap-password", required=True, type=str) +parser_read.add_argument( + "--ignore-dkim-spf", + action="store_true", + help="to ignore the dkim and spf verification on the read mail", +) +parser_read.add_argument( + "--show-body", action="store_true", help="print mail text/plain payload" +) +parser_read.add_argument("subject", type=str) parser_read.set_defaults(func=read) args = parser.parse_args() From a6eb2a8f9affab4be821956b3cb838f1d1ec871b Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 02:17:10 +0200 Subject: [PATCH 176/225] README.md: reformat with markdownlint --- README.md | 126 +++++++++++++++++++++++++++--------------------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index d351c1a..11ccf3c 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,82 @@ # ![Simple Nixos MailServer][logo] + ![license](https://img.shields.io/badge/license-GPL3-brightgreen.svg) [![pipeline status](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/badges/master/pipeline.svg)](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/commits/master) - ## Release branches For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. * For NixOS 24.11 - - Use the [SNM branch `nixos-24.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.11) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/) - - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/release-notes.html#nixos-24-11) + * Use the [SNM branch `nixos-24.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.11) + * [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/) + * [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/release-notes.html#nixos-24-11) * For NixOS 24.05 - - Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/) - - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05) + * Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05) + * [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/) + * [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05) * For NixOS unstable - - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) + * Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) + * [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) ## Features - * [x] Continous Integration Testing - * [x] Multiple Domains - * Postfix - - [x] SMTP on port 25 - - [x] Submission TLS on port 465 - - [x] Submission StartTLS on port 587 - - [x] LMTP with Dovecot - * Dovecot - - [x] Maildir folders - - [x] IMAP with TLS on port 993 - - [x] POP3 with TLS on port 995 - - [x] IMAP with StartTLS on port 143 - - [x] POP3 with StartTLS on port 110 - * Certificates - - [x] ACME - - [x] Custom certificates - * Spam Filtering - - [x] Via Rspamd - * Virus Scanning - - [x] Via ClamAV - * DKIM Signing - - [x] Via Rspamd - * User Management - - [x] Declarative user management - - [x] Declarative password management - - [x] LDAP users - * Sieve - - [x] Allow user defined sieve scripts - - [x] Moving mails from/to junk trains the Bayes filter - - [x] ManageSieve support - * User Aliases - - [x] Regular aliases - - [x] Catch all aliases +* [x] Continous Integration Testing +* [x] Multiple Domains +* Postfix + * [x] SMTP on port 25 + * [x] Submission TLS on port 465 + * [x] Submission StartTLS on port 587 + * [x] LMTP with Dovecot +* Dovecot + * [x] Maildir folders + * [x] IMAP with TLS on port 993 + * [x] POP3 with TLS on port 995 + * [x] IMAP with StartTLS on port 143 + * [x] POP3 with StartTLS on port 110 +* Certificates + * [x] ACME + * [x] Custom certificates +* Spam Filtering + * [x] Via Rspamd +* Virus Scanning + * [x] Via ClamAV +* DKIM Signing + * [x] Via Rspamd +* User Management + * [x] Declarative user management + * [x] Declarative password management + * [x] LDAP users +* Sieve + * [x] Allow user defined sieve scripts + * [x] Moving mails from/to junk trains the Bayes filter + * [x] ManageSieve support +* User Aliases + * [x] Regular aliases + * [x] Catch all aliases ### In the future - * Automatic client configuration - - [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) - - [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019) - - [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac) - * DKIM Signing - - [ ] Allow per domain selectors - - [ ] Allow passing DKIM signing keys - * Improve the Forwarding Experience - - [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html) - - [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd) - * User management - - [ ] Allow local and LDAP user to coexist - * OpenID Connect - - Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166) +* Automatic client configuration + * [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) + * [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019) + * [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac) +* DKIM Signing + * [ ] Allow per domain selectors + * [ ] Allow passing DKIM signing keys +* Improve the Forwarding Experience + * [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html) + * [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd) +* User management + * [ ] Allow local and LDAP user to coexist +* OpenID Connect + * Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166) ### Get in touch -- Matrix: [#nixos-mailserver:nixos.org](https://matrix.to/#/#nixos-mailserver:nixos.org) -- IRC: `#nixos-mailserver` on [Libera Chat](https://libera.chat/guides/connect) +* Matrix: [#nixos-mailserver:nixos.org](https://matrix.to/#/#nixos-mailserver:nixos.org) +* IRC: `#nixos-mailserver` on [Libera Chat](https://libera.chat/guides/connect) ## How to Set Up a 10/10 Mail Server Guide @@ -89,16 +89,18 @@ For a complete list of options, [see in readthedocs](https://nixos-mailserver.re See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page. ## Contributors + See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master) ### Alternative Implementations - * [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices) + +* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices) ### Credits - * send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao) + +* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao) from [TheNounProject](https://thenounproject.com/) is licensed under [CC BY 3.0](http://creativecommons.org/~/3.0/) - * Logo made with [Logomakr.com](https://logomakr.com) - +* Logo made with [Logomakr.com](https://logomakr.com) [logo]: docs/logo.png From ddc6ce61db4eb05b417daf468864f67e94cbc898 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 03:43:43 +0200 Subject: [PATCH 177/225] docs: fix linting issues https://github.com/sphinx-doc/sphinx/issues/3921 --- docs/howto-develop.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index acdc7bd..108740a 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -10,7 +10,7 @@ Run NixOS tests --------------- To run the test suite, you need to enable `Nix Flakes -`_. +`__. You can then run the testsuite via @@ -37,7 +37,7 @@ For the syntax, see the `RST/Sphinx primer `_. To build the documentation, you need to enable `Nix Flakes -`_. +`__. :: From 4839fa6614de277dbde21c417ae6ff4db9aa2240 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 06:25:55 +0200 Subject: [PATCH 178/225] scripts: migrate format strings to f-strings --- scripts/generate-options.py | 14 +++++++------- scripts/mail-check.py | 32 ++++++++++++-------------------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/scripts/generate-options.py b/scripts/generate-options.py index ab1227d..3cfc0b0 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -42,9 +42,10 @@ def render_option_value(opt, attr): if isinstance(opt[attr], dict) and "_type" in opt[attr]: if opt[attr]["_type"] == "literalExpression": if "\n" in opt[attr]["text"]: - res = "\n```nix\n" + opt[attr]["text"].rstrip("\n") + "\n```" + text = opt[attr]["text"].rstrip("\n") + res = f"\n```nix\n{text}\n```" else: - res = "```{}```".format(opt[attr]["text"]) + res = f"```{opt[attr]["text"]}```" elif opt[attr]["_type"] == "literalMD": res = opt[attr]["text"] else: @@ -54,9 +55,9 @@ def render_option_value(opt, attr): if s == "": res = '`""`' elif "\n" in s: - res = "\n```\n" + s.rstrip("\n") + "\n```" + res = f"\n```\n{s.rstrip("\n")}\n```" else: - res = "```{}```".format(s) + res = f"```{s}```" return "- " + attr + ": " + res # type: ignore @@ -70,7 +71,7 @@ def print_option(opt): template.format( key=opt["name"], description=description or "", - type="- type: ```{}```".format(opt["type"]), + type=f"- type: ```{opt["type"]}```", default=render_option_value(opt, "default"), example=render_option_value(opt, "example"), ) @@ -84,8 +85,7 @@ for opt in options: print_option(opt) for c in groups: - print("## `{}`".format(c)) - print() + print(f"## `{c}`\n") for opt in options: if opt["name"].startswith(c): print_option(opt) diff --git a/scripts/mail-check.py b/scripts/mail-check.py index db36bc9..39b2688 100644 --- a/scripts/mail-check.py +++ b/scripts/mail-check.py @@ -14,23 +14,17 @@ RETRY = 100 def _send_mail( smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls ): - print("Sending mail with subject '{}'".format(subject)) + print(f"Sending mail with subject '{subject}'") message = "\n".join( [ - "From: {from_addr}", - "To: {to_addr}", - "Subject: {subject}", - "Message-ID: {random}@mail-check.py", - "Date: {date}", + f"From: {from_addr}", + f"To: {to_addr}", + f"Subject: {subject}", + f"Message-ID: {uuid.uuid4()}@mail-check.py", + f"Date: {email.utils.formatdate()}", "", "This validates our mail server can send to Gmail :/", ] - ).format( - from_addr=from_addr, - to_addr=to_addr, - subject=subject, - random=str(uuid.uuid4()), - date=email.utils.formatdate(), ) retry = RETRY @@ -79,7 +73,7 @@ def _read_mail( show_body=False, delete=True, ): - print("Reading mail from %s" % imap_username) + print("Reading mail from {imap_username}") message = None @@ -93,7 +87,7 @@ def _read_mail( for _ in range(0, RETRY): print("Retrying") obj.select() - _, data = obj.search(None, '(SINCE %s) (SUBJECT "%s")' % (dt, subject)) + _, data = obj.search(None, f'(SINCE {dt}) (SUBJECT "{subject}")') if data == [b""]: time.sleep(1) continue @@ -101,8 +95,7 @@ def _read_mail( uids = data[0].decode("utf-8").split(" ") if len(uids) != 1: print( - "Warning: %d messages have been found with subject containing %s " - % (len(uids), subject) + f"Warning: {len(uids)} messages have been found with subject containing {subject}" ) # FIXME: we only consider the first matching message... @@ -113,7 +106,7 @@ def _read_mail( obj.expunge() assert raw[0] and raw[0][1] message = email.message_from_bytes(cast(bytes, raw[0][1])) - print("Message with subject '%s' has been found" % message["subject"]) + print(f"Message with subject '{message['subject']}' has been found") if show_body: if message.is_multipart(): for part in message.walk(): @@ -130,8 +123,7 @@ def _read_mail( if message is None: print( - "Error: no message with subject '%s' has been found in INBOX of %s" - % (subject, imap_username) + f"Error: no message with subject '{subject}' has been found in INBOX of {imap_username}" ) exit(1) @@ -168,7 +160,7 @@ def send_and_read(args): else: imap_username = args.to_addr - subject = "{}".format(uuid.uuid4()) + subject = f"{uuid.uuid4()}" _send_mail( smtp_host=args.smtp_host, From 3268d8b0d8a487c39ce687a2bca2dbb695fe6e7f Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 07:30:09 +0200 Subject: [PATCH 179/225] scripts/generate-options: refactor - Extract the md syntax part into reusable functions - Rename variables so their purpose becomes clearer --- scripts/generate-options.py | 70 +++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/scripts/generate-options.py b/scripts/generate-options.py index 3cfc0b0..e78e262 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -1,5 +1,7 @@ import json import sys +from textwrap import indent +from typing import Any, Mapping header = """ # Mailserver options @@ -35,45 +37,61 @@ groups = [ ] -def render_option_value(opt, attr): - if attr not in opt: +def md_literal(value: str) -> str: + return f"`{value}`" + + +def md_codefence(value: str, language: str = "nix") -> str: + return indent( + f"\n```{language}\n{value}\n```", + prefix=2 * " ", + ) + + +def render_option_value(option: Mapping[str, Any], key: str) -> str: + if key not in option: return "" - if isinstance(opt[attr], dict) and "_type" in opt[attr]: - if opt[attr]["_type"] == "literalExpression": - if "\n" in opt[attr]["text"]: - text = opt[attr]["text"].rstrip("\n") - res = f"\n```nix\n{text}\n```" + if isinstance(option[key], dict) and "_type" in option[key]: + if option[key]["_type"] == "literalExpression": + # multi-line codeblock + if "\n" in option[key]["text"]: + text = option[key]["text"].rstrip("\n") + value = md_codefence(text) + # inline codeblock else: - res = f"```{opt[attr]["text"]}```" - elif opt[attr]["_type"] == "literalMD": - res = opt[attr]["text"] + value = md_literal(option[key]["text"]) + # literal markdown + elif option[key]["_type"] == "literalMD": + value = option[key]["text"] else: - assert RuntimeError(f"Unhandled option type {opt[attr]["_type"]}") + assert RuntimeError(f"Unhandled option type {option[key]['_type']}") else: - s = str(opt[attr]) - if s == "": - res = '`""`' - elif "\n" in s: - res = f"\n```\n{s.rstrip("\n")}\n```" + text = str(option[key]) + if text == "": + value = md_literal('""') + elif "\n" in text: + value = md_codefence(text.rstrip("\n")) else: - res = f"```{s}```" + value = md_literal(text) - return "- " + attr + ": " + res # type: ignore + return f"- {key}: {value}" # type: ignore -def print_option(opt): - if isinstance(opt["description"], dict) and "_type" in opt["description"]: # mdDoc - description = opt["description"]["text"] +def print_option(option): + if ( + isinstance(option["description"], dict) and "_type" in option["description"] + ): # mdDoc + description = option["description"]["text"] else: - description = opt["description"] + description = option["description"] print( template.format( - key=opt["name"], + key=option["name"], description=description or "", - type=f"- type: ```{opt["type"]}```", - default=render_option_value(opt, "default"), - example=render_option_value(opt, "example"), + type=f"- type: {md_literal(option['type'])}", + default=render_option_value(option, "default"), + example=render_option_value(option, "example"), ) ) From 4c252785078055b36ac5465812173ae383a849d7 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 07:31:25 +0200 Subject: [PATCH 180/225] flake.nix: print options.md outpath during build Helpful for debugging the resulting options file. --- flake.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/flake.nix b/flake.nix index 286802e..b2ae304 100644 --- a/flake.nix +++ b/flake.nix @@ -96,6 +96,7 @@ 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 { From fbfd948535bcbe98f9ec189d12164ec0ced86749 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 07:52:26 +0200 Subject: [PATCH 181/225] flake.nix: remove clamav from devshell, add glab With glab we provide the GitLab CLI utility to interact programatically with the platform. Useful for checking our Merge request branches for example. --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index b2ae304..96ffb21 100644 --- a/flake.nix +++ b/flake.nix @@ -182,7 +182,7 @@ devShells.${system}.default = pkgs.mkShellNoCC { inputsFrom = [ documentation ]; packages = with pkgs; [ - clamav + glab ] ++ self.checks.${system}.pre-commit.enabledPackages; shellHook = self.checks.${system}.pre-commit.shellHook; }; From a73982f5b4bc37f8e3a7adb8bd4492f16984277b Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 18:32:21 +0200 Subject: [PATCH 182/225] docs: migrate wiki references to wiki.nixos.org This has been the official wiki platform for a while now. --- docs/flakes.rst | 2 +- docs/howto-develop.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/flakes.rst b/docs/flakes.rst index 254a02a..f56ec96 100644 --- a/docs/flakes.rst +++ b/docs/flakes.rst @@ -1,7 +1,7 @@ Nix Flakes ========== -If you're using `flakes `__, you can use +If you're using `flakes `__, you can use the following minimal ``flake.nix`` as an example: .. code:: nix diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index 108740a..4826782 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -10,7 +10,7 @@ Run NixOS tests --------------- To run the test suite, you need to enable `Nix Flakes -`__. +`__. You can then run the testsuite via @@ -37,7 +37,7 @@ For the syntax, see the `RST/Sphinx primer `_. To build the documentation, you need to enable `Nix Flakes -`__. +`__. :: From 040f07ff459d187ebaa1d818bec35b78ce92eebe Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 18:53:01 +0200 Subject: [PATCH 183/225] docs/howto-develop: update chat room references --- docs/howto-develop.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index 4826782..10e53e5 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -4,7 +4,10 @@ Contribute or troubleshoot To report an issue, please go to ``_. -You can also chat with us on the Libera IRC channel ``#nixos-mailserver``. +If you have questions, feel free to reach out: + +* Matrix: `#nixos-mailserver:nixos.org `__ +* IRC: `#nixos-mailserver `__ on `Libera Chat `__ Run NixOS tests --------------- From fce540024a7e87101f6dfc3b217b1f664bf0dc84 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 8 May 2025 18:53:25 +0200 Subject: [PATCH 184/225] docs/howto-develop: document the devshell --- docs/howto-develop.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index 10e53e5..40527f9 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -9,6 +9,23 @@ If you have questions, feel free to reach out: * Matrix: `#nixos-mailserver:nixos.org `__ * IRC: `#nixos-mailserver `__ on `Libera Chat `__ +All our workflows rely on Nix being configured with `Flakes `__. + +Development Shell +----------------- + +We provide a `flake.nix` devshell that automatically sets up pre-commit hooks, +which allows for fast feedback cycles when making changes to the repository. + + +:: + + $ nix develop + + +We recommend setting up `direnv `__ to automatically +attach to the development environment when entering the project directories. + Run NixOS tests --------------- From 1e51a503b192e2e8149ac33527808c99278748ea Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 9 May 2025 22:50:10 +0200 Subject: [PATCH 185/225] dovecot: drop unused pipe scripts Leftovers from d507bd9c9571001054792dd34099fa500f4573c1 --- mail-server/dovecot/pipe_bin/sa-learn-ham.sh | 3 --- mail-server/dovecot/pipe_bin/sa-learn-spam.sh | 3 --- 2 files changed, 6 deletions(-) delete mode 100755 mail-server/dovecot/pipe_bin/sa-learn-ham.sh delete mode 100755 mail-server/dovecot/pipe_bin/sa-learn-spam.sh diff --git a/mail-server/dovecot/pipe_bin/sa-learn-ham.sh b/mail-server/dovecot/pipe_bin/sa-learn-ham.sh deleted file mode 100755 index 76fc4ed..0000000 --- a/mail-server/dovecot/pipe_bin/sa-learn-ham.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -set -o errexit -exec rspamc -h /run/rspamd/worker-controller.sock learn_ham \ No newline at end of file diff --git a/mail-server/dovecot/pipe_bin/sa-learn-spam.sh b/mail-server/dovecot/pipe_bin/sa-learn-spam.sh deleted file mode 100755 index 2a2f766..0000000 --- a/mail-server/dovecot/pipe_bin/sa-learn-spam.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -set -o errexit -exec rspamc -h /run/rspamd/worker-controller.sock learn_spam \ No newline at end of file From 9a6190ceea09a52108654f66d8650a0e4a16a542 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sat, 10 May 2025 01:02:44 +0200 Subject: [PATCH 186/225] rspamd: remove indirection in path to runtime directory --- mail-server/rspamd.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index fd94c84..0e37c20 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -50,7 +50,7 @@ in nativeBuildInputs = with pkgs; [ makeWrapper ]; }'' makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \ - --add-flags "-h /var/run/rspamd/worker-controller.sock" + --add-flags "-h /run/rspamd/worker-controller.sock" '') ]; From aa8366d234f159575b09308095bd582e96f8f950 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 15 May 2025 16:41:30 +0200 Subject: [PATCH 187/225] treewide: remove dead nix references --- mail-server/clamav.nix | 2 +- mail-server/dovecot.nix | 27 +++------------------------ mail-server/kresd.nix | 2 +- mail-server/monit.nix | 2 +- mail-server/postfix.nix | 7 +------ 5 files changed, 7 insertions(+), 33 deletions(-) diff --git a/mail-server/clamav.nix b/mail-server/clamav.nix index 25418f0..0dafd4f 100644 --- a/mail-server/clamav.nix +++ b/mail-server/clamav.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, options, ... }: +{ config, lib, ... }: let cfg = config.mailserver; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 27af741..6704426 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -39,27 +39,6 @@ let ); postfixCfg = config.services.postfix; - dovecot2Cfg = config.services.dovecot2; - - stateDir = "/var/lib/dovecot"; - - pipeBin = pkgs.stdenv.mkDerivation { - name = "pipe_bin"; - src = ./dovecot/pipe_bin; - buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ]; - buildCommand = '' - mkdir -p $out/pipe/bin - cp $src/* $out/pipe/bin/ - chmod a+x $out/pipe/bin/* - patchShebangs $out/pipe/bin - - for file in $out/pipe/bin/*; do - wrapProgram $file \ - --set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin" - done - ''; - }; - ldapConfig = pkgs.writeTextFile { name = "dovecot-ldap.conf.ext.template"; @@ -109,7 +88,7 @@ let # Prevent world-readable password files, even temporarily. umask 077 - for f in ${builtins.toString (lib.mapAttrsToList (name: value: 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 @@ -117,7 +96,7 @@ let done cat < ${passwdFile} - ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" ) cfg.loginAccounts)} EOF @@ -130,7 +109,7 @@ let EOF ''; - junkMailboxes = builtins.attrNames (lib.filterAttrs (n: 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 ""; diff --git a/mail-server/kresd.nix b/mail-server/kresd.nix index e3baa07..230bdea 100644 --- a/mail-server/kresd.nix +++ b/mail-server/kresd.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ config, lib, ... }: let cfg = config.mailserver; diff --git a/mail-server/monit.nix b/mail-server/monit.nix index c69b19e..c3f8760 100644 --- a/mail-server/monit.nix +++ b/mail-server/monit.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, pkgs, lib, ... }: +{ config, lib, ... }: let cfg = config.mailserver; diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index db3e581..d1c59b2 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -25,7 +25,7 @@ let # Merge several lookup tables. A lookup table is a attribute set where # - the key is an address (user@example.com) or a domain (@example.com) # - the value is a list of addresses - mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables; + mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables; # valiases_postfix :: Map String [String] valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList @@ -123,13 +123,8 @@ let /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}> ''); - inetSocket = addr: port: "inet:[${toString port}@${addr}]"; - unixSocket = sock: "unix:${sock}"; - smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ]; - policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig; - mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}"; From 2ed7a9478284fa122b48632024dc4e7be428ca9f Mon Sep 17 00:00:00 2001 From: euxane Date: Fri, 24 Jan 2025 17:36:40 +0100 Subject: [PATCH 188/225] dovecot/fts: switch to fts-flatcurve This switches the full-text search plugin from fts-xapian to fts-flatcurve, the now preferred indexer still powered by Xapian, which will be integrated into Dovecot core 2.4. This sets a sane minimal configuration for the plugin with international language support. The plugin options marked as "advanced" in Dovecot's documentation aren't re-exposed for simplicity. They can nevertheless be overridden by module consumers by directly setting keys with `services.dovecot2.pluginSettings.fts_*`. The `fullTextSearch.maintenance` option is removed as the index is now incrementally optimised in the background. GitLab: closes https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/239 --- default.nix | 98 +++++++++++++++++++++++++++++------------ docs/fts.rst | 5 ++- mail-server/dovecot.nix | 67 ++++++++++------------------ tests/external.nix | 18 ++------ 4 files changed, 101 insertions(+), 87 deletions(-) diff --git a/default.nix b/default.nix index 3f46610..aaa0987 100644 --- a/default.nix +++ b/default.nix @@ -380,7 +380,21 @@ in }; fullTextSearch = { - enable = mkEnableOption "Full text search indexing with xapian. This has significant performance and disk space cost."; + enable = mkEnableOption '' + Full text search indexing with Xapian through the fts_flatcurve plugin. + This has significant performance and disk space cost. + ''; + memoryLimit = mkOption { + type = types.nullOr types.int; + default = null; + example = 2000; + description = '' + Memory limit for the indexer process, in MiB. + If null, leaves the default (which is rather low), + and if 0, no limit. + ''; + }; + autoIndex = mkOption { type = types.bool; default = true; @@ -406,36 +420,54 @@ in ''; }; - minSize = mkOption { - type = types.ints.between 3 1000; - default = 3; - description = "Minimum size of search terms"; - }; - memoryLimit = mkOption { - type = types.nullOr types.int; - default = null; - example = 2000; - description = "Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit."; + languages = mkOption { + type = types.nonEmptyListOf types.str; + default = [ "en" ]; + example = [ "en" "de" ]; + description = '' + A list of languages that the full text search should detect. + At least one language must be specified. + The language listed first is the default and is used when language recognition fails. + See . + ''; }; - maintenance = { - enable = mkOption { - type = types.bool; - default = true; - description = "Regularly optmize indices, as recommended by upstream."; - }; + substringSearch = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, allows substring searches. + See . - onCalendar = mkOption { - type = types.str; - default = "daily"; - description = "When to run the maintenance job. See systemd.time(7) for more information about the format."; - }; + Enabling this requires significant additional storage space. + ''; + }; - randomizedDelaySec = mkOption { - type = types.int; - default = 1000; - description = "Run the maintenance job not exactly at the time specified with `onCalendar`, but plus or minus this many seconds."; - }; + headerExcludes = mkOption { + type = types.listOf types.str; + default = [ + "Received" + "DKIM-*" + "X-*" + "Comments" + ]; + description = '' + The list of headers to exclude. + See . + ''; + }; + + filters = mkOption { + type = types.listOf types.str; + default = [ + "normalizer-icu" + "snowball" + "stopwords" + ]; + description = '' + The list of filters to apply. + . + ''; }; }; @@ -1269,6 +1301,18 @@ in }; imports = [ + (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] '' + This option is not needed for fts-flatcurve + '') + (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] '' + This option is not needed for fts-flatcurve + '') + (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] '' + This option is not needed for fts-flatcurve + '') + (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] '' + This option is not supported by fts-flatcurve + '') (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] '' This option is not needed since fts-xapian 1.8.3 '') diff --git a/docs/fts.rst b/docs/fts.rst index 780ae3e..bb2fe88 100644 --- a/docs/fts.rst +++ b/docs/fts.rst @@ -4,7 +4,7 @@ Full text search By default, when your IMAP client searches for an email containing some text in its *body*, dovecot will read all your email sequentially. This is very slow and IO intensive. To speed body searches up, it is possible to -*index* emails with a plugin to dovecot, ``fts_xapian``. +*index* emails with a plugin to dovecot, ``fts_flatcurve``. Enabling full text search ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -59,7 +59,8 @@ Mitigating resources requirements You can: -* increase the minimum search term size ``mailserver.fullTextSearch.minSize`` +* exclude some headers from indexation with ``mailserver.fullTextSearch.headerExcludes`` +* disable expensive token normalisation in ``mailserver.fullTextSearch.filters`` * disable automatic indexation for some folders with ``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard. diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 6704426..ee8db25 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -26,7 +26,12 @@ let userdbFile = "${passwdDir}/userdb"; # This file contains the ldap bind password ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext"; - bool2int = x: if x then "1" else "0"; + 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); maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8"; @@ -122,10 +127,22 @@ let dovecotModules = [ pkgs.dovecot_pigeonhole - ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian; + ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve; # Remove and assume `false` after NixOS 25.05 haveDovecotModulesOption = options.services.dovecot2 ? "modules" && (options.services.dovecot2.modules.visible or true); + ftsPluginSettings = { + fts = "flatcurve"; + fts_languages = listToLine cfg.fullTextSearch.languages; + 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; + fts_header_excludes = listToLine cfg.fullTextSearch.headerExcludes; + fts_autoindex = boolToYesNo cfg.fullTextSearch.autoIndex; + fts_enforced = cfg.fullTextSearch.enforced; + } // (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude); + in { config = with cfg; lib.mkIf enable { @@ -160,14 +177,17 @@ in sslServerCert = certificatePath; sslServerKey = keyPath; enableLmtp = true; - mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ]; + mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ + "fts" + "fts_flatcurve" + ]; 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); sieve = { extensions = [ @@ -341,26 +361,11 @@ in inbox = yes } - ${lib.optionalString cfg.fullTextSearch.enable '' - plugin { - plugin = fts fts_xapian - fts = xapian - fts_xapian = partial=${toString cfg.fullTextSearch.minSize} verbose=${bool2int cfg.debug} - - fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"} - - ${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude} - - fts_enforced = ${cfg.fullTextSearch.enforced} - } - service indexer-worker { ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)} ''} - process_limit = 0 } - ''} lda_mailbox_autosubscribe = yes lda_mailbox_autocreate = yes @@ -378,29 +383,5 @@ in }; systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]); - - systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) { - description = "Optimize dovecot indices for fts_xapian"; - requisite = [ "dovecot2.service" ]; - after = [ "dovecot2.service" ]; - startAt = cfg.fullTextSearch.maintenance.onCalendar; - serviceConfig = { - Type = "oneshot"; - ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A"; - PrivateDevices = true; - PrivateNetwork = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectSystem = true; - PrivateTmp = true; - }; - }; - systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) { - timerConfig = { - RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec; - }; - }; }; } diff --git a/tests/external.nix b/tests/external.nix index c32a9e1..0f51d46 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -82,8 +82,6 @@ # 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"; - # fts-xapian warns when memory is low, which makes the test fail - memoryLimit = 100000; }; }; }; @@ -493,11 +491,9 @@ # 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 -E 'indexer-worker.* Done indexing .INBOX.' >&2" - ) + server.succeed("journalctl -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2") # check that Junk is not indexed - server.fail("journalctl -u dovecot2 | grep 'indexer-worker' | grep -i 'JUNK' >&2") + server.fail("journalctl -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2") with subtest("dmarc reporting"): server.systemctl("start rspamd-dmarc-reporter.service") @@ -507,14 +503,6 @@ 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 'FTS Xapian: Box is empty' | \ - grep -v 'FTS Xapian: New version of the plugin' | \ - grep -vE 'FTS Xapian:.*does not exist. Creating it' | \ - grep -vE 'FTS Xapian:.*indexes do not exist. Initializing DB' | \ - grep -i warning >&2" - ) + server.fail("journalctl -u dovecot2 | grep -v 'Expunged message reappeared, giving a new UID' | grep -i warning >&2") ''; } From e287d83ab1818ac34b510a58b15dd44012f59c3c Mon Sep 17 00:00:00 2001 From: euxane Date: Thu, 30 Jan 2025 21:06:23 +0100 Subject: [PATCH 189/225] release-notes: mention switch to fts-flatcurve for FTS --- docs/release-notes.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index f6511ee..8cee0bd 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -12,6 +12,17 @@ NixOS 25.05 - If you need to revert TCP connections, configure ``mailserver.redis.address`` to reference the value of ``config.services.redis.servers.rspamd.bind``. - The integration with policyd-spf was removed and SPF handling is now fully based on Rspamd scoring. (`merge request `__) +- Switch to the more efficient `fts-flatcurve` indexer for full text search + (`merge request `__). + This makes use of a new index, which will be automatically re-generated the + next time a folder is searched. + The operation is now quick enough to be performed "just-in-time". + Alternatively, all indices can be immediately re-generated for all users and + folders by running + `doveadm fts rescan -u '*' && doveadm index -u '*' -q '*'`. + The previous index (which is not automatically discarded to allow rollbacks) + can be cleaned up by removing all the `xapian-indexes` directories within + `mailserver.indexDir`. - Individual domains can now be excluded from DMARC Reporting through ``mailserver.dmarcReporting.excludedDomains``. (`merge request `__) - Configuring ``mailserver.forwards`` is now possible when the setup relies on LDAP. From 0cbdf465e49e4bc6a4625d0610fa73f9b469a39d Mon Sep 17 00:00:00 2001 From: euxane Date: Mon, 19 May 2025 16:36:50 +0200 Subject: [PATCH 190/225] dovecot/fts: warn on stopwords filter with multiple languages --- mail-server/dovecot.nix | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index ee8db25..56cebf2 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -153,6 +153,20 @@ in } ]; + 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. From 826a3b2fcf68173cd93e1f7664dee68e01d6a175 Mon Sep 17 00:00:00 2001 From: euxane Date: Mon, 19 May 2025 17:13:11 +0200 Subject: [PATCH 191/225] tests/external: ignore time adjustments warnings Seems to be happening randomly during tests: dovecot: master: Warning: Time moved forwards by 0.101534 seconds - adjusting timeouts. --- tests/external.nix | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/external.nix b/tests/external.nix index 0f51d46..a65f0ce 100644 --- a/tests/external.nix +++ b/tests/external.nix @@ -503,6 +503,11 @@ 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 -i warning >&2") + server.fail( + "journalctl -u dovecot2 | \ + grep -v 'Expunged message reappeared, giving a new UID' | \ + grep -v 'Time moved forwards' | \ + grep -i warning >&2" + ) ''; } From f7a221bc69ef6e67a56e370cbaf031ca2b3c7aa7 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 21 May 2025 00:56:01 +0200 Subject: [PATCH 192/225] flake.nix: expose packages for custom pre-commit hooks in devshell --- flake.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flake.nix b/flake.nix index 96ffb21..5c79705 100644 --- a/flake.nix +++ b/flake.nix @@ -146,6 +146,7 @@ }; rstcheck = { enable = true; + package = pkgs.rstcheckWithSphinx; entry = lib.getExe pkgs.rstcheckWithSphinx; files = "\\.rst$"; }; @@ -170,6 +171,7 @@ # sieve check-sieve = { enable = true; + package = pkgs.check-sieve; entry = lib.getExe pkgs.check-sieve; files = "\\.sieve$"; }; From b4ae17d224add5d200833221a49c5ac92ecfddb7 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Wed, 21 May 2025 00:53:28 +0200 Subject: [PATCH 193/225] Reformat release notes --- docs/release-notes.rst | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 8cee0bd..7e1429f 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -14,15 +14,20 @@ NixOS 25.05 (`merge request `__) - Switch to the more efficient `fts-flatcurve` indexer for full text search (`merge request `__). + This makes use of a new index, which will be automatically re-generated the next time a folder is searched. The operation is now quick enough to be performed "just-in-time". Alternatively, all indices can be immediately re-generated for all users and folders by running - `doveadm fts rescan -u '*' && doveadm index -u '*' -q '*'`. + + .. code-block:: bash + + doveadm fts rescan -u '*' && doveadm index -u '*' -q '*' + The previous index (which is not automatically discarded to allow rollbacks) can be cleaned up by removing all the `xapian-indexes` directories within - `mailserver.indexDir`. + ``mailserver.indexDir``. - Individual domains can now be excluded from DMARC Reporting through ``mailserver.dmarcReporting.excludedDomains``. (`merge request `__) - Configuring ``mailserver.forwards`` is now possible when the setup relies on LDAP. @@ -79,7 +84,6 @@ NixOS 21.11 - New option ``certificateDomains`` to generate certificate for additional domains (such as ``imap.example.com``) - NixOS 21.05 ----------- From 51d48f1492f237bf111c454a12ea482c3e098d85 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 20 May 2025 01:00:54 +0200 Subject: [PATCH 194/225] Release 25.11 --- .hydra/declarative-jobsets.nix | 2 +- README.md | 8 ++++---- docs/setup-guide.rst | 4 ++-- flake.lock | 12 ++++++------ flake.nix | 10 +++++----- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix index 0e68a86..7b99844 100644 --- a/.hydra/declarative-jobsets.nix +++ b/.hydra/declarative-jobsets.nix @@ -32,8 +32,8 @@ let desc = prJobsets // { "master" = mkFlakeJobset "master"; - "nixos-24.05" = mkFlakeJobset "nixos-24.05"; "nixos-24.11" = mkFlakeJobset "nixos-24.11"; + "nixos-25.05" = mkFlakeJobset "nixos-25.05"; }; log = { diff --git a/README.md b/README.md index 11ccf3c..ef3042a 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ For each NixOS release, we publish a branch. You then have to use the SNM branch corresponding to your NixOS version. +* For NixOS 25.05 + * Use the [SNM branch `nixos-25.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-25.05) + * [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/) + * [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/release-notes.html#nixos-25-05) * For NixOS 24.11 * Use the [SNM branch `nixos-24.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.11) * [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/) * [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/release-notes.html#nixos-24-11) -* For NixOS 24.05 - * Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05) - * [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/) - * [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05) * For NixOS unstable * Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) * [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 5f6f903..de04cd4 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -63,9 +63,9 @@ common ones. imports = [ (builtins.fetchTarball { # Pick a release version you are interested in and set its hash, e.g. - url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-24.11/nixos-mailserver-nixos-24.11.tar.gz"; + url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-25.05/nixos-mailserver-nixos-25.05.tar.gz"; # To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command: - # release="nixos-24.11"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack + # release="nixos-25.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack sha256 = "0000000000000000000000000000000000000000000000000000"; }) ]; diff --git a/flake.lock b/flake.lock index a712d89..c077208 100644 --- a/flake.lock +++ b/flake.lock @@ -93,18 +93,18 @@ "type": "github" } }, - "nixpkgs-24_11": { + "nixpkgs-25_05": { "locked": { - "lastModified": 1747209494, - "narHash": "sha256-fLise+ys+bpyjuUUkbwqo5W/UyIELvRz9lPBPoB0fbM=", + "lastModified": 1747610100, + "narHash": "sha256-rpR5ZPMkWzcnCcYYo3lScqfuzEw5Uyfh+R0EKZfroAc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5d736263df906c5da72ab0f372427814de2f52f8", + "rev": "ca49c4304acf0973078db0a9d200fd2bae75676d", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-24.11", + "ref": "nixos-25.05", "repo": "nixpkgs", "type": "github" } @@ -115,7 +115,7 @@ "flake-compat": "flake-compat", "git-hooks": "git-hooks", "nixpkgs": "nixpkgs", - "nixpkgs-24_11": "nixpkgs-24_11" + "nixpkgs-25_05": "nixpkgs-25_05" } } }, diff --git a/flake.nix b/flake.nix index 5c79705..e93f8c2 100644 --- a/flake.nix +++ b/flake.nix @@ -13,14 +13,14 @@ inputs.nixpkgs.follows = "nixpkgs"; }; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - nixpkgs-24_11.url = "github:NixOS/nixpkgs/nixos-24.11"; + nixpkgs-25_05.url = "github:NixOS/nixpkgs/nixos-25.05"; blobs = { url = "gitlab:simple-nixos-mailserver/blobs"; flake = false; }; }; - outputs = { self, blobs, git-hooks, nixpkgs, nixpkgs-24_11, ... }: let + outputs = { self, blobs, git-hooks, nixpkgs, nixpkgs-25_05, ... }: let lib = nixpkgs.lib; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; @@ -31,9 +31,9 @@ pkgs = nixpkgs.legacyPackages.${system}; } { - name = "24.11"; - nixpkgs = nixpkgs-24_11; - pkgs = nixpkgs-24_11.legacyPackages.${system}; + name = "25.05"; + nixpkgs = nixpkgs-25_05; + pkgs = nixpkgs-25_05.legacyPackages.${system}; } ]; testNames = [ From 792225e2562cfcbc149db1a23e95292a1ca2bc02 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 22 May 2025 02:45:55 +0200 Subject: [PATCH 195/225] Introduce stateVersion concept With upcoming changes to the dovecot home and maildirectories we need to introduce a way to nudge users to inform themselves about manual migration steps they might need to carry out. The idea here is to allow us to safely make breaking changes and notify the user of required migration steps at eval time, so they can make the necessary changes in time. --- default.nix | 16 +++++++++++++++ docs/howto-develop.rst | 41 ++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + docs/migrations.rst | 22 ++++++++++++++++++++ docs/setup-guide.rst | 1 + mail-server/assertions.nix | 7 ++++++- tests/lib/config.nix | 6 +++++- tests/minimal.nix | 5 ++++- tests/multiple.nix | 5 ++++- 9 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 docs/migrations.rst diff --git a/default.nix b/default.nix index aaa0987..71effa0 100644 --- a/default.nix +++ b/default.nix @@ -25,6 +25,22 @@ in options.mailserver = { enable = mkEnableOption "nixos-mailserver"; + stateVersion = mkOption { + type = types.nullOr types.ints.positive; + default = null; + description = '' + Tracking stateful version changes as an incrementing number. + + When a new release comes out we may require manual migration steps to + be completed, before the new version can be put into production. + + If your `stateVersion` is too low one or multiple assertions may + trigger to give you instructions on what migrations steps are required + to continue. Increase the `stateVersion` as instructed by the assertion + message. + ''; + }; + openFirewall = mkOption { type = types.bool; default = true; diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index 40527f9..4261418 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -64,3 +64,44 @@ To build the documentation, you need to enable `Nix Flakes $ nix build .#documentation $ xdg-open result/index.html + + +Manual migrations +----------------- + +We need to take great care around providing a migration story around breaking +changes. If manual intervention becomes necessary we provide the `stateVersion` +option to notify the user that they need to complete a migration before +they can deploy an update. + +If that is the case for your change, find the highest `stateVersion` that is +being asserted on in `mail-server/assertions.nix`. Then pick the next number +and add a new assertion, write a good summary describing the issue and what +remediation steps are necessary. Finally reference the URL to the specific +section on the migration page in the documentation. + +.. code-block:: nix + + { + assertions = [ + { + assertion = config.mailserver.stateVersion < 1; + message = '' + Problem: The home directory for the foobar service is snafu. + Remediation: + - Stop the `foobar.service` + - Rename `/var/lib/foobaz` to `/var/lib/foobar` + - Increase the `mailserver.stateVersion` to 1. + + Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#specific-anchor-here for further details. + ''; + } + ]; + } + +The setup guide should always reference the latest `stateVersion`, since we +don't require any migration steps for new setups. + +The migration documentation should paint a more complete picture about the steps +that need to be carried out and why this has become necessary. Make sure to +reference the correct anchor in the URL you put into the assertion message. diff --git a/docs/index.rst b/docs/index.rst index 2fd1e1a..d31ed27 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,6 +18,7 @@ Welcome to NixOS Mailserver's documentation! faq release-notes options + migrations .. toctree:: :maxdepth: 1 diff --git a/docs/migrations.rst b/docs/migrations.rst new file mode 100644 index 0000000..bd52196 --- /dev/null +++ b/docs/migrations.rst @@ -0,0 +1,22 @@ +Migrations +========== + +With mail server configuration best practices changing over time we might need +to make changes that require you to complete manual migration steps before you +can deploy a new version of NixOS mailserver. + +The initial `mailserver.stateVersion` value should be copied from the setup +guide that you used to initially set up your mail server. If in doubt you can +always initialize it at `1` and walk through all assertions, that might apply +to your setup. + +NixOS 25.11 +----------- + +This option was introduced in the NixOS 25.11 release cycle, in which case you +can safely initialize its value at `1`. + +:: code-block: nix + + mailserver.stateVersion = 1; + diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 5f6f903..f92ef20 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -72,6 +72,7 @@ common ones. mailserver = { enable = true; + stateVersion = 1; fqdn = "mail.example.com"; domains = [ "example.com" ]; diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index 91921c6..b30ccaa 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -1,6 +1,11 @@ { config, lib, ... }: { - assertions = lib.optionals config.mailserver.ldap.enable [ + assertions = lib.optionals config.mailserver.enable [ + { + assertion = config.mailserver.stateVersion != null; + message = "The `mailserver.stateVersion` option is not set. Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html to determine the proper value to initialize it at."; + } + ] ++ lib.optionals config.mailserver.ldap.enable [ { assertion = config.mailserver.loginAccounts == {}; message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts"; diff --git a/tests/lib/config.nix b/tests/lib/config.nix index 68a1b2e..09ae517 100644 --- a/tests/lib/config.nix +++ b/tests/lib/config.nix @@ -1,3 +1,7 @@ { - security.dhparams.defaultBitSize = 2048; # minimum size required by dovecot + # Testing eval failures that result from stateVersion assertion is out of scope + mailserver.stateVersion = 999; + + # minimum size required by dovecot + security.dhparams.defaultBitSize = 2048; } diff --git a/tests/minimal.nix b/tests/minimal.nix index 407f221..e78814e 100644 --- a/tests/minimal.nix +++ b/tests/minimal.nix @@ -18,7 +18,10 @@ name = "minimal"; nodes.machine = { - imports = [ ./../default.nix ]; + imports = [ + ../default.nix + ./lib/config.nix + ]; }; testScript = '' diff --git a/tests/multiple.nix b/tests/multiple.nix index 2427feb..3e71cd6 100644 --- a/tests/multiple.nix +++ b/tests/multiple.nix @@ -16,7 +16,10 @@ let password = pkgs.writeText "password" "password"; domainGenerator = domain: { pkgs, ... }: { - imports = [../default.nix]; + imports = [ + ../default.nix + ./lib/config.nix + ]; environment.systemPackages = with pkgs; [ netcat ]; virtualisation.memorySize = 1024; mailserver = { From 10cccc77062965e5a961995dcab95e14123936aa Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 29 May 2025 08:48:56 +0200 Subject: [PATCH 196/225] docs: fix code block syntax in migration init --- docs/migrations.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/migrations.rst b/docs/migrations.rst index bd52196..101c7d5 100644 --- a/docs/migrations.rst +++ b/docs/migrations.rst @@ -16,7 +16,7 @@ NixOS 25.11 This option was introduced in the NixOS 25.11 release cycle, in which case you can safely initialize its value at `1`. -:: code-block: nix +.. code-block:: nix mailserver.stateVersion = 1; From 11bfdbf136df4a08eba20ee1ffc8176d865844d3 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 29 May 2025 08:49:37 +0200 Subject: [PATCH 197/225] tests: drop dhparam default length configuration This has been the default value since the option was introduced back in 2018[0]. [0] https://github.com/NixOS/nixpkgs/commit/81fc2c35097f81ecb29a576148486cc1ce5a5bcc --- tests/lib/config.nix | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/lib/config.nix b/tests/lib/config.nix index 09ae517..fe66875 100644 --- a/tests/lib/config.nix +++ b/tests/lib/config.nix @@ -1,7 +1,4 @@ { # Testing eval failures that result from stateVersion assertion is out of scope mailserver.stateVersion = 999; - - # minimum size required by dovecot - security.dhparams.defaultBitSize = 2048; } From 233c5e1a70be1b6cd01f88a2755459d32b66ab38 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 29 May 2025 14:06:34 +0200 Subject: [PATCH 198/225] dovecot: remove workaround for services.dovecot2.modules removal --- mail-server/dovecot.nix | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 56cebf2..894b02e 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ options, config, pkgs, lib, ... }: +{ config, pkgs, lib, ... }: with (import ./common.nix { inherit config pkgs lib; }); @@ -125,12 +125,6 @@ let else scope ); - dovecotModules = [ - pkgs.dovecot_pigeonhole - ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve; - # Remove and assume `false` after NixOS 25.05 - haveDovecotModulesOption = options.services.dovecot2 ? "modules" && (options.services.dovecot2.modules.visible or true); - ftsPluginSettings = { fts = "flatcurve"; fts_languages = listToLine cfg.fullTextSearch.languages; @@ -172,14 +166,12 @@ in # which are usually not compatible. environment.systemPackages = [ pkgs.dovecot_pigeonhole - ] ++ lib.optionals (!haveDovecotModulesOption) dovecotModules; + ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve; # For compatibility with python imaplib - environment.etc = lib.mkIf (!haveDovecotModulesOption) { - "dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; - }; + environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; - services.dovecot2 = lib.mkMerge [{ + services.dovecot2 = { enable = true; enableImap = enableImap || enableImapSsl; enablePop3 = enablePop3 || enablePop3Ssl; @@ -384,11 +376,7 @@ in lda_mailbox_autosubscribe = yes lda_mailbox_autocreate = yes ''; - } - (lib.mkIf haveDovecotModulesOption { - modules = dovecotModules; - }) - ]; + }; systemd.services.dovecot2 = { preStart = '' From 7cb61e6e3a4085e12ce0a9a05e15da1bd66a086d Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 22 May 2025 01:52:17 +0200 Subject: [PATCH 199/225] dovecot: respect the mailDirectory base for LDAP home directories This change is safe, if you have not altered the default value of the `mailserver.mailDirectory` setting. --- docs/migrations.rst | 23 +++++++++++++++++++++++ mail-server/assertions.nix | 17 ++++++++++++++++- mail-server/dovecot.nix | 2 +- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/migrations.rst b/docs/migrations.rst index bd52196..e1972e1 100644 --- a/docs/migrations.rst +++ b/docs/migrations.rst @@ -13,6 +13,29 @@ to your setup. NixOS 25.11 ----------- +#2 LDAP home directory migration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Dovecot configuration for LDAP home directories previously did not respect +the ``mailserver.mailDirectory`` setting. + +This means that home directories were unconditionally located at +``/var/vmail/ldap/%{user}``. + +This migration is required if you both: + +* enabled the LDAP integration (``mailserver.ldap.enable``) +* and customized the default mail directory (``mailserver.mailDirectory != "/var/vmail"``) + +For remediating this issue the following steps are required: + +1. Stop ``dovecot2.service``. +2. Move ``/var/vmail/ldap`` below your ``m̀ailserver.mailDirectory``. +3. Update the ``mailserver.stateVersion`` to ``2``. + +#1 Initialization +^^^^^^^^^^^^^^^^^ + This option was introduced in the NixOS 25.11 release cycle, in which case you can safely initialize its value at `1`. diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index b30ccaa..deabe03 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -1,6 +1,21 @@ { config, lib, ... }: { - assertions = lib.optionals config.mailserver.enable [ + assertions = [ + { + assertion = config.mailserver.stateVersion < 2 + && config.mailserver.ldap.enable + && config.mailserver.mailDirectory != "/var/vmail"; + message = '' + Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`. + Remediation: + - Stop the `dovecot2.service` + - Move `/var/vmail/ldap` below your `mailserver.mailDirectory` + - Increase the `stateVersion` to 2. + + Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#ldap-home-directory-migration for more information. + ''; + } + ] ++ lib.optionals config.mailserver.enable [ { assertion = config.mailserver.stateVersion != null; message = "The `mailserver.stateVersion` option is not set. Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html to determine the proper value to initialize it at."; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 56cebf2..5cdd67b 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -356,7 +356,7 @@ in userdb { driver = ldap args = ${ldapConfFile} - default_fields = home=/var/vmail/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} + default_fields = home=${cfg.mailDirectory}/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} } ''} From 519a85a801c84341f0d007af86734ba2b5780a24 Mon Sep 17 00:00:00 2001 From: Charlotte Van Petegem Date: Fri, 30 May 2025 12:49:02 +0000 Subject: [PATCH 200/225] Fix assertion for ldap mail directory --- mail-server/assertions.nix | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index deabe03..a2749a0 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -1,20 +1,7 @@ { config, lib, ... }: { assertions = [ - { - assertion = config.mailserver.stateVersion < 2 - && config.mailserver.ldap.enable - && config.mailserver.mailDirectory != "/var/vmail"; - message = '' - Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`. - Remediation: - - Stop the `dovecot2.service` - - Move `/var/vmail/ldap` below your `mailserver.mailDirectory` - - Increase the `stateVersion` to 2. - - Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#ldap-home-directory-migration for more information. - ''; - } + ] ++ lib.optionals config.mailserver.enable [ { assertion = config.mailserver.stateVersion != null; @@ -29,6 +16,19 @@ assertion = config.mailserver.extraVirtualAliases == {}; message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases"; } + ] ++ lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail") [ + { + assertion = config.mailserver.stateVersion >= 2; + message = '' + Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`. + Remediation: + - Stop the `dovecot2.service` + - Move `/var/vmail/ldap` below your `mailserver.mailDirectory` + - Increase the `stateVersion` to 2. + + Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#ldap-home-directory-migration for more information. + ''; + } ] ++ lib.optionals (config.mailserver.enable && config.mailserver.certificateScheme != "acme") [ { assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; From ea1b0f8e2bf94b3e46112b0b719146874c588fa5 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 30 May 2025 18:28:16 +0200 Subject: [PATCH 201/225] assertions: guard by enable flag and reformat None of these should trigger when you've not enabled mailserver. --- mail-server/assertions.nix | 80 +++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 35 deletions(-) diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index a2749a0..4a7b3b0 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -1,38 +1,48 @@ -{ config, lib, ... }: { - assertions = [ - - ] ++ lib.optionals config.mailserver.enable [ - { - assertion = config.mailserver.stateVersion != null; - message = "The `mailserver.stateVersion` option is not set. Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html to determine the proper value to initialize it at."; - } - ] ++ lib.optionals config.mailserver.ldap.enable [ - { - assertion = config.mailserver.loginAccounts == {}; - message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts"; - } - { - assertion = config.mailserver.extraVirtualAliases == {}; - message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases"; - } - ] ++ lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail") [ - { - assertion = config.mailserver.stateVersion >= 2; - message = '' - Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`. - Remediation: - - Stop the `dovecot2.service` - - Move `/var/vmail/ldap` below your `mailserver.mailDirectory` - - Increase the `stateVersion` to 2. + config, + lib, + ... +}: +{ + # We guard all assertions by requiring mailserver to be actually enabled + assertions = lib.optionals config.mailserver.enable ( + [ + { + assertion = config.mailserver.stateVersion != null; + message = "The `mailserver.stateVersion` option is not set. Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html to determine the proper value to initialize it at."; + } + ] + ++ lib.optionals config.mailserver.ldap.enable [ + { + assertion = config.mailserver.loginAccounts == { }; + message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts"; + } + { + assertion = config.mailserver.extraVirtualAliases == { }; + message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases"; + } + ] + ++ + lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail") + [ + { + assertion = config.mailserver.stateVersion >= 2; + message = '' + Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`. + Remediation: + - Stop the `dovecot2.service` + - Move `/var/vmail/ldap` below your `mailserver.mailDirectory` + - Increase the `stateVersion` to 2. - Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#ldap-home-directory-migration for more information. - ''; - } - ] ++ lib.optionals (config.mailserver.enable && config.mailserver.certificateScheme != "acme") [ - { - assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; - message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName"; - } - ]; + Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#ldap-home-directory-migration for more information. + ''; + } + ] + ++ lib.optionals (config.mailserver.certificateScheme != "acme") [ + { + assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; + message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName"; + } + ] + ); } From c9f61e02aee97dc8c7d4f3739b012a992183508c Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sat, 31 May 2025 13:06:29 +0200 Subject: [PATCH 202/225] docs/howto-develop: fix stateVersion assertion example --- docs/howto-develop.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst index 4261418..dbf3024 100644 --- a/docs/howto-develop.rst +++ b/docs/howto-develop.rst @@ -85,7 +85,7 @@ section on the migration page in the documentation. { assertions = [ { - assertion = config.mailserver.stateVersion < 1; + assertion = config.mailserver.stateVersion >= 1; message = '' Problem: The home directory for the foobar service is snafu. Remediation: From 8c835feaa77494ca8755dfc56601065aab43b77a Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 2 Jun 2025 04:28:53 +0200 Subject: [PATCH 203/225] docs/migrations: Improve title scoping for LDAP home dir migration --- docs/migrations.rst | 4 ++-- mail-server/assertions.nix | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/migrations.rst b/docs/migrations.rst index 49d7690..daef17e 100644 --- a/docs/migrations.rst +++ b/docs/migrations.rst @@ -13,8 +13,8 @@ to your setup. NixOS 25.11 ----------- -#2 LDAP home directory migration -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +#2 Dovecot LDAP home directory migration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The Dovecot configuration for LDAP home directories previously did not respect the ``mailserver.mailDirectory`` setting. diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index 4a7b3b0..8e8ce05 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -34,7 +34,7 @@ - Move `/var/vmail/ldap` below your `mailserver.mailDirectory` - Increase the `stateVersion` to 2. - Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#ldap-home-directory-migration for more information. + Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-ldap-home-directory-migration for more information. ''; } ] From c4628a4c04a9cad31b9ed8ed4ea8a5be8775bc18 Mon Sep 17 00:00:00 2001 From: Tom Herbers Date: Fri, 30 May 2025 12:47:24 +0200 Subject: [PATCH 204/225] docs/backup-guide: add recommendation for sieveDirectory Co-authored-by: Martin Weinelt --- docs/backup-guide.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/backup-guide.rst b/docs/backup-guide.rst index ef7a848..a3c15af 100644 --- a/docs/backup-guide.rst +++ b/docs/backup-guide.rst @@ -14,6 +14,11 @@ forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them back (or whatever you specified as ``vmailUserName``, and ``vmailGoupName``). +If you enabled ``enableManageSieve`` then you also may want to backup +``/var/sieve`` or whatever you have specified as ``sieveDirectory``. +The same considerations regarding file ownership apply as for the +Maildir. + Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever you specified as ``dkimKeyDirectory``). If you should lose those don’t worry, new ones will be created on the fly. But you will need to repeat From f9b15192b8bbb777822785e27d4e0ad02377e186 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 3 Jun 2025 00:45:12 +0200 Subject: [PATCH 205/225] postfix: allow client to select the preferred cipher As long as all cipher we support are considered safe we can allow clients to select one that suits them best. --- mail-server/postfix.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index d1c59b2..5d7f9a2 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -287,10 +287,12 @@ in smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; - tls_preempt_cipherlist = true; + # As long as all cipher suites are considered safe, let the client use its preferred cipher + tls_preempt_cipherlist = false; # Allowing AUTH on a non encrypted connection poses a security risk smtpd_tls_auth_only = true; + # Log only a summary message on TLS handshake completion smtp_tls_loglevel = "1"; smtpd_tls_loglevel = "1"; From 49980abd25921a4fda5488b9e199e072a74f0a88 Mon Sep 17 00:00:00 2001 From: Guillaume Girol Date: Fri, 6 Jun 2025 12:00:00 +0000 Subject: [PATCH 206/225] mention spam and ham training data in backup guide --- docs/backup-guide.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/backup-guide.rst b/docs/backup-guide.rst index a3c15af..67d08d0 100644 --- a/docs/backup-guide.rst +++ b/docs/backup-guide.rst @@ -19,6 +19,8 @@ If you enabled ``enableManageSieve`` then you also may want to backup The same considerations regarding file ownership apply as for the Maildir. +To backup spam and ham training data, backup ``/var/lib/redis-rspamd``. + Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever you specified as ``dkimKeyDirectory``). If you should lose those don’t worry, new ones will be created on the fly. But you will need to repeat From e540dc864cc9bf0618b31e47a5eb59b7ad1152cb Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Thu, 12 Jun 2025 01:01:38 +0200 Subject: [PATCH 207/225] postfix: configure cert/key using smtpd_tls_chain_files The sslCert and sslKey options are going away, because they do too much, e.g. provision the keypair for client certificate authentication, which is not at all what we want or need. --- mail-server/postfix.nix | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index d1c59b2..2106ea0 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -207,13 +207,16 @@ in mapFiles."denied_recipients" = denied_recipients_file; mapFiles."reject_senders" = reject_senders_file; mapFiles."reject_recipients" = reject_recipients_file; - sslCert = certificatePath; - sslKey = keyPath; enableSubmission = cfg.enableSubmission; enableSubmissions = cfg.enableSubmissionSsl; virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]); config = { + smtpd_tls_chain_files = [ + "${keyPath}" + "${certificatePath}" + ]; + # Extra Config mydestination = ""; recipient_delimiter = cfg.recipientDelimiter; From f1bd4b821510eec7d38f39f9ddce5106e679afd1 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 13 Jun 2025 00:18:50 +0200 Subject: [PATCH 208/225] postfix: remove option to toggle SMTP smuggling workarounnd It has been default enabled since Postfix 3.9 and can still be configured from the NixOS option mentioned in the removal warning. Removing the option makes our interface leaner. Information is based on https://www.postfix.org/smtp-smuggling.html#long. --- default.nix | 26 +++++++++++--------------- mail-server/postfix.nix | 4 ---- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/default.nix b/default.nix index 71effa0..afe77b8 100644 --- a/default.nix +++ b/default.nix @@ -982,6 +982,14 @@ in }; redis = { + configureLocally = mkOption { + type = types.bool; + default = true; + description = '' + Whether to provision a local Redis instance. + ''; + }; + address = mkOption { type = types.str; # read the default from nixos' redis module @@ -1021,21 +1029,6 @@ in ''; }; - smtpdForbidBareNewline = mkOption { - type = types.bool; - default = true; - description = '' - With "smtpd_forbid_bare_newline = yes", the Postfix SMTP server - disconnects a remote SMTP client that sends a line ending in a 'bare - newline'. - - This feature was added in Postfix 3.8.4 against SMTP Smuggling and will - default to "yes" in Postfix 3.9. - - https://www.postfix.org/smtp-smuggling.html - ''; - }; - sendingFqdn = mkOption { type = types.str; default = cfg.fqdn; @@ -1366,5 +1359,8 @@ in (lib.mkRemovedOptionModule [ "mailserver" "dkimBodyCanonicalization" ] '' DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization. '') + (lib.mkRemovedOptionModule [ "mailserver" "smtpdForbidBareNewline" ] '' + The workaround for the SMTP Smuggling attack is default enabled in Postfix >3.9. Use `services.postfix.config.smtpd_forbid_bare_newline` if you need to deviate from its default. + '') ]; } diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index d1c59b2..2546dd5 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -302,10 +302,6 @@ in 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}"; - - # Fix for https://www.postfix.org/smtp-smuggling.html - smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline; - smtpd_forbid_bare_newline_exclusions = "$mynetworks"; }; submissionOptions = submissionOptions; From 3b7cda8cc5e5c37b5f2234c1667ed678aed213cb Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 13 Jun 2025 04:00:52 +0200 Subject: [PATCH 209/225] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'git-hooks': 'github:cachix/git-hooks.nix/dcf5072734cb576d2b0c59b2ac44f5050b5eac82' (2025-03-22) → 'github:cachix/git-hooks.nix/623c56286de5a3193aa38891a6991b28f9bab056' (2025-06-11) • Updated input 'nixpkgs': 'github:NixOS/nixpkgs/adaa24fbf46737f3f1b5497bf64bae750f82942e' (2025-05-13) → 'github:NixOS/nixpkgs/3e3afe5174c561dee0df6f2c2b2236990146329f' (2025-06-07) • Updated input 'nixpkgs-25_05': 'github:NixOS/nixpkgs/ca49c4304acf0973078db0a9d200fd2bae75676d' (2025-05-18) → 'github:NixOS/nixpkgs/fd487183437963a59ba763c0cc4f27e3447dd6dd' (2025-06-12) --- flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index c077208..c7979e2 100644 --- a/flake.lock +++ b/flake.lock @@ -43,11 +43,11 @@ ] }, "locked": { - "lastModified": 1742649964, - "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=", + "lastModified": 1749636823, + "narHash": "sha256-WUaIlOlPLyPgz9be7fqWJA5iG6rHcGRtLERSCfUDne4=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82", + "rev": "623c56286de5a3193aa38891a6991b28f9bab056", "type": "github" }, "original": { @@ -79,11 +79,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1747179050, - "narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=", + "lastModified": 1749285348, + "narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e", + "rev": "3e3afe5174c561dee0df6f2c2b2236990146329f", "type": "github" }, "original": { @@ -95,11 +95,11 @@ }, "nixpkgs-25_05": { "locked": { - "lastModified": 1747610100, - "narHash": "sha256-rpR5ZPMkWzcnCcYYo3lScqfuzEw5Uyfh+R0EKZfroAc=", + "lastModified": 1749727998, + "narHash": "sha256-mHv/yeUbmL91/TvV95p+mBVahm9mdQMJoqaTVTALaFw=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ca49c4304acf0973078db0a9d200fd2bae75676d", + "rev": "fd487183437963a59ba763c0cc4f27e3447dd6dd", "type": "github" }, "original": { From e0ab4eeb673391ac148f7c6951fe1181a61f1fdb Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sat, 14 Jun 2025 01:20:27 +0200 Subject: [PATCH 210/225] docs/setup-guide: bump example stateVersion to 2 If you do a fresh install now you should be able to skip the first migration step. --- docs/setup-guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index e9f4a87..e45525a 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -72,7 +72,7 @@ common ones. mailserver = { enable = true; - stateVersion = 1; + stateVersion = 2; fqdn = "mail.example.com"; domains = [ "example.com" ]; From e27326d3176974a98b2a557fd35ce97394991aa7 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 13 Jun 2025 01:42:48 +0200 Subject: [PATCH 211/225] postfix: refactor and prune TLS settings - Groups settings between server and client - Uses a range comparator for supported TLS versions - Prune excluded primitives to what affects the supported TLS versions --- mail-server/postfix.nix | 50 +++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 76f65a9..9f25971 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -240,11 +240,6 @@ in # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients lmtp_destination_recipient_limit = "1"; - # Opportunistic DANE support - # https://www.postfix.org/postconf.5.html#smtp_tls_security_level - smtp_dns_support_level = "dnssec"; - smtp_tls_security_level = "dane"; - # sasl with dovecot smtpd_sasl_type = "dovecot"; smtpd_sasl_path = "/run/dovecot2/auth"; @@ -266,33 +261,44 @@ in "check_policy_service unix:/run/dovecot2/quota-status" ]; - # TLS settings, inspired by https://github.com/jeaye/nix-files - # Submission by mail clients is handled in submissionOptions + # TLS for incoming mail is optional smtpd_tls_security_level = "may"; - # Disable obselete protocols - smtpd_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; - smtp_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; - smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; - smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; + # But required for authentication attempts + smtpd_tls_auth_only = true; - smtp_tls_ciphers = "high"; + # TLS versions supported for the SMTP server + smtpd_tls_protocols = ">=TLSv1.2"; + smtpd_tls_mandatory_protocols = ">=TLSv1.2"; + + # Require ciphersuites that OpenSSL classifies as "High" smtpd_tls_ciphers = "high"; - smtp_tls_mandatory_ciphers = "high"; smtpd_tls_mandatory_ciphers = "high"; - # Disable deprecated ciphers - smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; - smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; - smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; - smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; + # Exclude cipher suites with undesirable properties + smtpd_tls_exclude_ciphers = "eNULL, aNULL"; + smtpd_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"; + + # 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" + smtp_tls_ciphers = "high"; + smtp_tls_mandatory_ciphers = "high"; + + # Exclude ciphersuites with undesirable properties + smtp_tls_exclude_ciphers = "eNULL, aNULL"; + smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; # As long as all cipher suites are considered safe, let the client use its preferred cipher tls_preempt_cipherlist = false; - # Allowing AUTH on a non encrypted connection poses a security risk - smtpd_tls_auth_only = true; - # Log only a summary message on TLS handshake completion smtp_tls_loglevel = "1"; smtpd_tls_loglevel = "1"; From 3828b00deac1713117e8bbd0bf31b3ffbfe7e2a5 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 13 Jun 2025 03:02:26 +0200 Subject: [PATCH 212/225] postfix: configure preferred curves and disable FFDHE This aligns with the intermediate configuration recommended by Mozilla. --- mail-server/postfix.nix | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 9f25971..0c52d7c 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -296,6 +296,20 @@ in smtp_tls_exclude_ciphers = "eNULL, aNULL"; smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; + # 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" + ]; + + # 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 = [ ]; + # As long as all cipher suites are considered safe, let the client use its preferred cipher tls_preempt_cipherlist = false; From 4fd9508d41145c6e9a4018f4f85811d0a3cbeb4a Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 13 Jun 2025 03:04:49 +0200 Subject: [PATCH 213/225] postfix: drop tls_random_source config The setting already defaults to /dev/urandom. --- mail-server/postfix.nix | 3 --- 1 file changed, 3 deletions(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 0c52d7c..1a5d1f9 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -317,9 +317,6 @@ in smtp_tls_loglevel = "1"; smtpd_tls_loglevel = "1"; - # Configure a non blocking source of randomness - tls_random_source = "dev:/dev/urandom"; - smtpd_milters = smtpdMilters; non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ]; milter_protocol = "6"; From efebf59b137b269ee5716aa82b6d377c22580fb5 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 13 Jun 2025 03:13:27 +0200 Subject: [PATCH 214/225] dovecot: configure preferred elliptic curves --- mail-server/dovecot.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index edb244c..375bfe8 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -298,9 +298,12 @@ in } 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 { From 21ce4b4ff86ba0771e41551c6144396a930773a9 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 13 Jun 2025 03:20:14 +0200 Subject: [PATCH 215/225] dovecot: disable Diffie-Hellman support Recommended in the modern recommendation by Mozilla. Support for elliptic curves is widespread and they are much faster. --- mail-server/dovecot.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 375bfe8..c06b478 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -182,6 +182,7 @@ in mailLocation = dovecotMaildir; sslServerCert = certificatePath; sslServerKey = keyPath; + enableDHE = lib.mkDefault false; enableLmtp = true; mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" From c7497cd5f6d3cd0d750bd4ba1884a5f89b53d851 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 15 Jun 2025 03:28:48 +0200 Subject: [PATCH 216/225] treewide: remove redundant parenthesis in nix code --- default.nix | 4 ++-- mail-server/dovecot.nix | 4 ++-- mail-server/postfix.nix | 18 +++++++++--------- mail-server/rspamd.nix | 4 ++-- mail-server/users.nix | 10 +++++----- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/default.nix b/default.nix index afe77b8..fa471bf 100644 --- a/default.nix +++ b/default.nix @@ -517,7 +517,7 @@ in type = let loginAccount = mkOptionType { name = "Login Account"; - check = (account: builtins.elem account (builtins.attrNames cfg.loginAccounts)); + check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts); }; in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount)); example = { @@ -901,7 +901,7 @@ in }; domain = mkOption { - type = types.enum (cfg.domains); + type = types.enum cfg.domains; example = "example.com"; description = '' The domain from which outgoing DMARC reports are served. diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index c06b478..c6e5587 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -148,7 +148,7 @@ in ]; warnings = - (lib.optional ( + lib.optional ( (builtins.length cfg.fullTextSearch.languages > 1) && (builtins.elem "stopwords" cfg.fullTextSearch.filters) ) '' @@ -158,7 +158,7 @@ in 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 diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index e45a725..9fb316f 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -75,23 +75,23 @@ let in builtins.toFile "regex_valias" content; # denied_recipients_postfix :: [ String ] - denied_recipients_postfix = (map + denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") - (lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts))); + (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 + reject_senders_postfix = map (sender: "${sender} REJECT") - (cfg.rejectSender)); - reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ; + cfg.rejectSender; + reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" reject_senders_postfix) ; - reject_recipients_postfix = (map + reject_recipients_postfix = map (recipient: "${recipient} REJECT") - (cfg.rejectRecipients)); + 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); @@ -231,7 +231,7 @@ in virtual_mailbox_domains = vhosts_file; virtual_mailbox_maps = [ (mappedFile "valias") - ] ++ lib.optionals (cfg.ldap.enable) [ + ] ++ lib.optionals cfg.ldap.enable [ "ldap:${ldapVirtualMailboxMapFile}" ] ++ lib.optionals (regex_valiases_postfix != {}) [ (mappedRegexFile "regex_valias") diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index 0e37c20..ab5113a 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -171,7 +171,7 @@ in ]; }; - systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) { + 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 @@ -216,7 +216,7 @@ in }; }; - systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) { + systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { description = "Daily delivery of aggregated DMARC reports"; wantedBy = [ "timers.target" diff --git a/mail-server/users.nix b/mail-server/users.nix index 17196fc..bf654c3 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -70,17 +70,17 @@ let in { config = lib.mkIf enable { # assert that all accounts provide a password - assertions = (map (acct: { - assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null); + assertions = map (acct: { + assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null; message = "${acct.name} must provide either a hashed password or a password hash file"; - }) (lib.attrValues loginAccounts)); + }) (lib.attrValues loginAccounts); # warn for accounts that specify both password and file - warnings = (map + 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))); + (lib.attrValues loginAccounts)); # set the vmail gid to a specific value users.groups = { From 03433d472fdd7f36311841fc1afe7f5e18dccb59 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 15 Jun 2025 03:34:20 +0200 Subject: [PATCH 217/225] flake.nix: enable nixfmt-rfc-style hook and formatter --- flake.nix | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flake.nix b/flake.nix index e93f8c2..641a858 100644 --- a/flake.nix +++ b/flake.nix @@ -153,6 +153,7 @@ # nix deadnix.enable = true; + nixfmt-rfc-style.enable = true; # python pyright.enable = true; @@ -189,5 +190,7 @@ shellHook = self.checks.${system}.pre-commit.shellHook; }; devShell.${system} = self.devShells.${system}.default; # compatibility + + formatter.${system} = pkgs.nixfmt-tree; }; } From 1a7f3d718c5a6406b7d5b54f10f5c9c69ed90ef9 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 15 Jun 2025 03:39:44 +0200 Subject: [PATCH 218/225] 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() From fb56bcf747d126be73be426efc809077af2058c9 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 15 Jun 2025 05:08:47 +0200 Subject: [PATCH 219/225] treewide: remove global `with lib` Instead inherit required functions from lib. --- default.nix | 53 +++++++++++++++++++++++---------------- mail-server/rsnapshot.nix | 7 ++++-- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/default.nix b/default.nix index 94b3186..60d9cec 100644 --- a/default.nix +++ b/default.nix @@ -21,9 +21,20 @@ ... }: -with lib; - let + inherit (lib) + literalExpression + literalMD + mkDefault + mkEnableOption + mkOption + mkOptionType + mkRemovedOptionModule + mkRenamedOptionModule + types + warn + ; + cfg = config.mailserver; in { @@ -269,7 +280,7 @@ in tlsCAFile = mkOption { type = types.path; default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; - defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)"; + defaultText = literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)"; description = '' Certifificate trust anchors used to verify the LDAP server certificate. ''; @@ -1064,7 +1075,7 @@ in type = types.str; # read the default from nixos' redis module default = config.services.redis.servers.rspamd.unixSocket; - defaultText = lib.literalExpression "config.services.redis.servers.rspamd.unixSocket"; + defaultText = literalExpression "config.services.redis.servers.rspamd.unixSocket"; description = '' Path, IP address or hostname that Rspamd should use to contact Redis. ''; @@ -1073,7 +1084,7 @@ in port = mkOption { type = with types; nullOr port; default = null; - example = lib.literalExpression "config.services.redis.servers.rspamd.port"; + example = literalExpression "config.services.redis.servers.rspamd.port"; description = '' Port that Rspamd should use to contact Redis. ''; @@ -1082,7 +1093,7 @@ in password = mkOption { type = types.nullOr types.str; default = config.services.redis.servers.rspamd.requirePass; - defaultText = lib.literalExpression "config.services.redis.servers.rspamd.requirePass"; + defaultText = literalExpression "config.services.redis.servers.rspamd.requirePass"; description = '' Password that rspamd should use to contact redis, or null if not required. ''; @@ -1102,7 +1113,7 @@ in sendingFqdn = mkOption { type = types.str; default = cfg.fqdn; - defaultText = lib.literalMD "{option}`mailserver.fqdn`"; + defaultText = literalMD "{option}`mailserver.fqdn`"; example = "myserver.example.com"; description = '' The fully qualified domain name of the mail server used to @@ -1178,7 +1189,7 @@ in start program = "${pkgs.systemd}/bin/systemctl start rspamd" stop program = "${pkgs.systemd}/bin/systemctl stop rspamd" ''; - defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)"; + defaultText = literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)"; description = '' The configuration used for monitoring via monit. Use a mail address that you actively check and set it via 'set alert ...'. @@ -1287,7 +1298,7 @@ in locations = mkOption { type = types.listOf types.path; default = [ cfg.mailDirectory ]; - defaultText = lib.literalExpression "[ config.mailserver.mailDirectory ]"; + defaultText = literalExpression "[ config.mailserver.mailDirectory ]"; description = "The locations that are to be backed up by borg."; }; @@ -1388,29 +1399,29 @@ in }; imports = [ - (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] '' + (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] '' This option is not needed for fts-flatcurve '') - (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] '' + (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] '' This option is not needed for fts-flatcurve '') - (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] '' + (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] '' This option is not needed for fts-flatcurve '') - (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] '' + (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] '' This option is not supported by fts-flatcurve '') - (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] '' + (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] '' This option is not needed since fts-xapian 1.8.3 '') - (lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] '' + (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] '' Text attachments are always indexed since fts-xapian 1.4.8 '') - (lib.mkRenamedOptionModule + (mkRenamedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "enable" ] [ "system" "autoUpgrade" "allowReboot" ] ) - (lib.mkRemovedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "method" ] '' + (mkRemovedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "method" ] '' Use `system.autoUpgrade` instead. '') ./mail-server/assertions.nix @@ -1427,17 +1438,17 @@ in ./mail-server/rspamd.nix ./mail-server/nginx.nix ./mail-server/kresd.nix - (lib.mkRemovedOptionModule [ "mailserver" "policydSPFExtraConfig" ] '' + (mkRemovedOptionModule [ "mailserver" "policydSPFExtraConfig" ] '' SPF checking has been migrated to Rspamd, which makes this config redundant. Please look into the rspamd config to migrate your settings. It may be that they are redundant and are already configured in rspamd like for skip_addresses. '') - (lib.mkRemovedOptionModule [ "mailserver" "dkimHeaderCanonicalization" ] '' + (mkRemovedOptionModule [ "mailserver" "dkimHeaderCanonicalization" ] '' DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization. '') - (lib.mkRemovedOptionModule [ "mailserver" "dkimBodyCanonicalization" ] '' + (mkRemovedOptionModule [ "mailserver" "dkimBodyCanonicalization" ] '' DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization. '') - (lib.mkRemovedOptionModule [ "mailserver" "smtpdForbidBareNewline" ] '' + (mkRemovedOptionModule [ "mailserver" "smtpdForbidBareNewline" ] '' The workaround for the SMTP Smuggling attack is default enabled in Postfix >3.9. Use `services.postfix.config.smtpd_forbid_bare_newline` if you need to deviate from its default. '') ]; diff --git a/mail-server/rsnapshot.nix b/mail-server/rsnapshot.nix index de4f13e..f01ff8d 100644 --- a/mail-server/rsnapshot.nix +++ b/mail-server/rsnapshot.nix @@ -21,9 +21,12 @@ ... }: -with lib; - let + inherit (lib) + optionalString + mkIf + ; + cfg = config.mailserver; preexecDefined = cfg.backup.cmdPreexec != null; From a2152f98073bce7d59cb64180b134d499547f716 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Sun, 15 Jun 2025 05:39:20 +0200 Subject: [PATCH 220/225] treewide: remove overly broad `with cfg` Makes it really hard to follow references and we were being explicit in most places already anyway. --- mail-server/dovecot.nix | 496 ++++++++++++++++++------------------ mail-server/environment.nix | 24 +- mail-server/networking.nix | 28 +- mail-server/postfix.nix | 338 ++++++++++++------------ mail-server/rspamd.nix | 394 ++++++++++++++-------------- mail-server/systemd.nix | 104 ++++---- 6 files changed, 686 insertions(+), 698 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 148befc..d2da51b 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -163,283 +163,281 @@ let in { - config = - with cfg; - lib.mkIf enable { - assertions = [ + config = lib.mkIf cfg.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 = cfg.enableImap || cfg.enableImapSsl; + enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl; + enablePAM = false; + enableQuota = true; + mailGroup = cfg.vmailGroupName; + mailUser = cfg.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"; + + 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); + + 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 = [ { - 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)"; + name = junkMailboxName; + causes = [ + "COPY" + "APPEND" + ]; + before = ./dovecot/imap_sieve/report-spam.sieve; + } + { + name = "*"; + from = junkMailboxName; + causes = [ "COPY" ]; + before = ./dovecot/imap_sieve/report-ham.sieve; } ]; - 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. + mailboxes = cfg.mailboxes; - The recommended solution is to NOT use the stopword filter when - multiple languages are present in the configuration. - ''; + extraConfig = '' + #Extra Config + ${lib.optionalString cfg.debug '' + mail_debug = yes + auth_debug = yes + verbose_ssl = yes + ''} - # 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"; - - 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); - - 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; - } - ]; - - 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.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 + '' } } - ''} - ${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 - '' - } + inet_listener imaps { + ${ + if cfg.enableImapSsl then + '' + port = 993 + 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} + ''} + ${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 + '' + } } - vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB - } - - service quota-status { - inet_listener { - 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 + '' + } } - 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 imap { + mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} + mail_plugins = $mail_plugins imap_sieve + } - protocol lmtp { - mail_plugins = $mail_plugins sieve + service imap { + vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB + } + + protocol pop3 { + mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} + } + + mail_access_groups = ${cfg.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 = passwd-file - args = ${passwdFile} + driver = ldap + args = ${ldapConfFile} } userdb { - driver = passwd-file - args = ${userdbFile} - default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} + driver = ldap + args = ${ldapConfFile} + default_fields = home=${cfg.mailDirectory}/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} } + ''} - ${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} - } + service auth { + unix_listener auth { + mode = 0660 + user = ${postfixCfg.user} + group = ${postfixCfg.group} } + } - auth_mechanisms = plain login + auth_mechanisms = plain login - namespace inbox { - separator = ${cfg.hierarchySeparator} - inbox = yes - } + namespace inbox { + separator = ${cfg.hierarchySeparator} + inbox = yes + } - service indexer-worker { - ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' - vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)} - ''} - } + 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.dovecot2 = { - preStart = - '' - ${genPasswdScript} - '' - + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); - }; - - systemd.services.postfix.restartTriggers = [ - genPasswdScript - ] ++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]); + lda_mailbox_autosubscribe = yes + lda_mailbox_autocreate = yes + ''; }; + + 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 b853211..462cb05 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -25,17 +25,15 @@ 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 = lib.mkIf cfg.enable { + environment.systemPackages = + with pkgs; + [ + dovecot + openssh + postfix + rspamd + ] + ++ (if cfg.certificateScheme == "selfsigned" then [ openssl ] else [ ]); + }; } diff --git a/mail-server/networking.nix b/mail-server/networking.nix index 587a8ae..f560ec0 100644 --- a/mail-server/networking.nix +++ b/mail-server/networking.nix @@ -20,21 +20,19 @@ let cfg = config.mailserver; in { - config = - with cfg; - lib.mkIf (enable && openFirewall) { + config = lib.mkIf (cfg.enable && cfg.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 cfg.enableSubmission 587 + ++ lib.optional cfg.enableSubmissionSsl 465 + ++ lib.optional cfg.enableImap 143 + ++ lib.optional cfg.enableImapSsl 993 + ++ lib.optional cfg.enablePop3 110 + ++ lib.optional cfg.enablePop3Ssl 995 + ++ lib.optional cfg.enableManageSieve 4190 + ++ lib.optional (cfg.certificateScheme == "acme-nginx") 80; }; + }; } diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 4237efc..680077d 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -233,183 +233,181 @@ let }; in { - config = - with cfg; - lib.mkIf enable { + config = lib.mkIf cfg.enable { - systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { - preStart = '' - ${appendPwdInVirtualMailboxMap} - ${appendPwdInSenderLoginMap} - ''; - restartTriggers = [ - appendPwdInVirtualMailboxMap - appendPwdInSenderLoginMap + systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { + preStart = '' + ${appendPwdInVirtualMailboxMap} + ${appendPwdInSenderLoginMap} + ''; + restartTriggers = [ + appendPwdInVirtualMailboxMap + appendPwdInSenderLoginMap + ]; + }; + + services.postfix = { + enable = true; + hostname = "${cfg.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}" ]; + + # Extra Config + mydestination = ""; + recipient_delimiter = cfg.recipientDelimiter; + smtpd_banner = "${cfg.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 = cfg.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"; + + # 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" + ]; + + # reject selected senders + smtpd_sender_restrictions = [ + "check_sender_access ${mappedFile "reject_senders"}" + ]; + + 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" + ]; + + # TLS for incoming mail is optional + smtpd_tls_security_level = "may"; + + # But required for authentication attempts + smtpd_tls_auth_only = true; + + # TLS versions supported for the SMTP server + smtpd_tls_protocols = ">=TLSv1.2"; + smtpd_tls_mandatory_protocols = ">=TLSv1.2"; + + # Require ciphersuites that OpenSSL classifies as "High" + smtpd_tls_ciphers = "high"; + smtpd_tls_mandatory_ciphers = "high"; + + # Exclude cipher suites with undesirable properties + smtpd_tls_exclude_ciphers = "eNULL, aNULL"; + smtpd_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"; + + # 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" + smtp_tls_ciphers = "high"; + smtp_tls_mandatory_ciphers = "high"; + + # Exclude ciphersuites with undesirable properties + smtp_tls_exclude_ciphers = "eNULL, aNULL"; + smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; + + # 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" + ]; + + # 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 = [ ]; + + # As long as all cipher suites are considered safe, let the client use its preferred cipher + tls_preempt_cipherlist = false; + + # 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}"; }; - 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 - ]); + submissionOptions = submissionOptions; + submissionsOptions = submissionOptions; - config = { - smtpd_tls_chain_files = [ - "${keyPath}" - "${certificatePath}" - ]; - - # 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") - ] - ); - 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" - ]; - - # reject selected senders - smtpd_sender_restrictions = [ - "check_sender_access ${mappedFile "reject_senders"}" - ]; - - 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" - ]; - - # TLS for incoming mail is optional - smtpd_tls_security_level = "may"; - - # But required for authentication attempts - smtpd_tls_auth_only = true; - - # TLS versions supported for the SMTP server - smtpd_tls_protocols = ">=TLSv1.2"; - smtpd_tls_mandatory_protocols = ">=TLSv1.2"; - - # Require ciphersuites that OpenSSL classifies as "High" - smtpd_tls_ciphers = "high"; - smtpd_tls_mandatory_ciphers = "high"; - - # Exclude cipher suites with undesirable properties - smtpd_tls_exclude_ciphers = "eNULL, aNULL"; - smtpd_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"; - - # 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" - smtp_tls_ciphers = "high"; - smtp_tls_mandatory_ciphers = "high"; - - # Exclude ciphersuites with undesirable properties - smtp_tls_exclude_ciphers = "eNULL, aNULL"; - smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL"; - - # 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" - ]; - - # 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 = [ ]; - - # As long as all cipher suites are considered safe, let the client use its preferred cipher - tls_preempt_cipherlist = false; - - # 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}"; + 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" ]; }; - - 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}" - ]; - }; + "submission-header-cleanup" = { + type = "unix"; + private = false; + chroot = false; + maxproc = 0; + command = "cleanup"; + args = [ + "-o" + "header_checks=pcre:${submissionHeaderCleanupRules}" + ]; }; }; }; + }; } diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index a4fcdce..7ed2a0e 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -52,223 +52,221 @@ let ''; 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 = lib.mkIf cfg.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 = '' - 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 = '' - cache { - backend = "redis"; - } - ''; - }; - "antivirus.conf" = lib.mkIf cfg.virusScanning { - text = '' - clamav { - action = "reject"; - symbol = "CLAM_VIRUS"; - type = "clamav"; - log_clean = true; - 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 = '' - ${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}; - ''} - }''} - ''; - }; + services.rspamd = { + enable = true; + inherit (cfg) debug; + locals = { + "milter_headers.conf" = { + text = '' + extended_spam_headers = true; + ''; }; - - 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 + "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"; } ''; }; - workers.controller = { - type = "controller"; - count = 1; - bindSockets = [ - { - socket = "/run/rspamd/worker-controller.sock"; - mode = "0666"; + "antivirus.conf" = lib.mkIf cfg.virusScanning { + text = '' + clamav { + action = "reject"; + symbol = "CLAM_VIRUS"; + type = "clamav"; + log_clean = true; + 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 } - ]; - 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; - }; + "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}; + ''} + }''} + ''; }; }; - 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 [ + workers.rspamd_proxy = { + type = "rspamd_proxy"; + bindSockets = [ { - SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; + socket = "/run/rspamd/rspamd-milter.sock"; + mode = "0664"; } - (lib.optionalAttrs cfg.dkimSigning { - ExecStartPre = map createDkimKeypair cfg.domains; - ReadWritePaths = [ cfg.dkimKeyDirectory ]; - }) ]; - }; + count = 1; # Do not spawn too many processes of this type + extraConfig = '' + milter = yes; # Enable milter mode + timeout = 120s; # Needed for Milter usually - 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") + upstream "local" { + default = yes; # Self-scan upstreams are always default + self_scan = yes; # Enable self-scan + } ''; - 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.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { - description = "Daily delivery of aggregated DMARC reports"; - wantedBy = [ - "timers.target" + workers.controller = { + type = "controller"; + count = 1; + bindSockets = [ + { + socket = "/run/rspamd/worker-controller.sock"; + mode = "0666"; + } ]; - timerConfig = { - OnCalendar = "daily"; - Persistent = true; - RandomizedDelaySec = 86400; - FixedRandomDelay = true; + 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.postfix = { - after = [ rspamdSocket ]; - requires = [ rspamdSocket ]; - }; - - users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ]; }; + + 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" + ]; + 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.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 dd4bd63..8fb0da7 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -32,61 +32,59 @@ let [ "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 = lib.mkIf cfg.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; - }; - }; - - # 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} + 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; + }; + }; - # 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 ( + [ cfg.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 "${cfg.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"; + }; + }; } From c8f809fa768bde90d99df1051d86a9d9b94a0b94 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 16 Jun 2025 06:17:39 +0200 Subject: [PATCH 221/225] postfix: migrate more options to services.postfix.config I'm working on deprecating the top-level options, that configure main.cf upstream in nixpkgs. With this change we stay ahead of the curve. The `networks_style` option already defaults to `host` since Postfix 3.0, so I dropped the setting. ``` $ postconf -d | grep networks_style mynetworks_style = ${{$compatibility_level} Date: Mon, 16 Jun 2025 06:20:15 +0200 Subject: [PATCH 222/225] postfix: rearrange smtpd_tls_chain_files option --- mail-server/postfix.nix | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 8124a6a..e29983a 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -264,11 +264,6 @@ in ]); config = { - smtpd_tls_chain_files = [ - "${keyPath}" - "${certificatePath}" - ]; - myhostname = cfg.sendingFqdn; mydestination = ""; # disable local mail delivery recipient_delimiter = cfg.recipientDelimiter; @@ -297,6 +292,7 @@ in ] ); 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"; @@ -323,6 +319,12 @@ in "check_policy_service unix:/run/dovecot2/quota-status" ]; + # The X509 private key followed by the corresponding certificate + smtpd_tls_chain_files = [ + "${keyPath}" + "${certificatePath}" + ]; + # TLS for incoming mail is optional smtpd_tls_security_level = "may"; From f76919c938d4849fc57b392040f81d5584d8d81d Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Mon, 9 Oct 2023 15:19:08 +0000 Subject: [PATCH 223/225] test: Checking if virtual aliases are functional. Relates to https://gitlab.skynet.ie/compsoc1/skynet/nixos/-/issues/22 test: Remove the account type limiatation # Conflicts: # default.nix # mail-server/assertions.nix --- default.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/default.nix b/default.nix index 60d9cec..3f31420 100644 --- a/default.nix +++ b/default.nix @@ -566,7 +566,6 @@ in let loginAccount = mkOptionType { name = "Login Account"; - check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts); }; in with types; From 192a7d426fae0ebb01a86d1530b48cc1d877886c Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Fri, 9 Aug 2024 20:55:15 +0100 Subject: [PATCH 224/225] ci: deploy upstream on changes --- .forgejo/workflows/build.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .forgejo/workflows/build.yaml diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml new file mode 100644 index 0000000..7a76f6c --- /dev/null +++ b/.forgejo/workflows/build.yaml @@ -0,0 +1,17 @@ +name: Build + +on: + push: + branches: + - 'master' + +jobs: + # deploy it upstream + deploy: + runs-on: docker + steps: + - name: "Deploy to Skynet" + uses: https://forgejo.skynet.ie/Skynet/actions-deploy-to-skynet@v2 + with: + input: 'simple-nixos-mailserver' + token: ${{ secrets.API_TOKEN_FORGEJO }} From c097bd662c9e1aea8c1fca10d57188e81c5574a0 Mon Sep 17 00:00:00 2001 From: Brendan Golden Date: Tue, 17 Jun 2025 19:10:46 +0100 Subject: [PATCH 225/225] fix: allow for extraVirtualAliases and ldap --- mail-server/assertions.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index 8e8ce05..1c7d3f9 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -17,10 +17,10 @@ assertion = config.mailserver.loginAccounts == { }; message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts"; } - { - assertion = config.mailserver.extraVirtualAliases == { }; - message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases"; - } +# { +# assertion = config.mailserver.extraVirtualAliases == { }; +# message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases"; +# } ] ++ lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail")