From 131c48de9bbf85da77434cde858da1d922cbc4ea Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Tue, 30 May 2023 23:36:53 +0200 Subject: [PATCH 001/161] 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 002/161] 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 003/161] 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 93a6542ff76196a5c64e0110cfc6098043b51987 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 19 Jul 2022 23:45:29 +0200 Subject: [PATCH 004/161] 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 01563e0..3ffa232 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 { @@ -229,6 +235,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 @@ -291,6 +310,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 975b6fca35511a37735bf189a1bf254dff36c6db Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 12 Aug 2022 18:54:12 +0200 Subject: [PATCH 005/161] 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 c35e3c96a3acb23539802d4c70fd06d694b4ab84 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 12 Aug 2022 18:54:49 +0200 Subject: [PATCH 006/161] 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 45b84a7..3cbc8ed 100644 --- a/flake.nix +++ b/flake.nix @@ -30,6 +30,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 f7a800ff8ce558380a4fc0cf6170890ff97cea92 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Fri, 19 May 2023 10:08:50 +0200 Subject: [PATCH 007/161] 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 870b1d11879324fea77bafdaa5a848a857b21bb2 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sat, 20 May 2023 00:12:02 +0200 Subject: [PATCH 008/161] 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 3ffa232..18b4a50 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} ''; @@ -226,7 +262,7 @@ in passdb { driver = passwd-file - args = ${passdbFile} + args = ${passwdFile} } userdb { @@ -238,12 +274,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} } ''} @@ -310,37 +346,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} @@ -351,10 +356,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 014166ef0a19c8e6a3b8f541bde3d16c90f1005a Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sat, 20 May 2023 00:18:58 +0200 Subject: [PATCH 009/161] 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 55a6e97fa441de6e0a965c3dfdcc6ed33d056c63 Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Sat, 20 May 2023 09:52:25 +0200 Subject: [PATCH 010/161] 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 caa6ecb54ddbd75a33b72ba242e1762c60cf4cab Mon Sep 17 00:00:00 2001 From: Antoine Eiche Date: Wed, 28 Jun 2023 23:07:11 +0200 Subject: [PATCH 011/161] 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 18b4a50..c9f4ca7 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 0c1801b48995ec6909e040abedaa56a64f0db430 Mon Sep 17 00:00:00 2001 From: Florian Klink Date: Tue, 27 Jun 2023 10:03:43 +0200 Subject: [PATCH 012/161] 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 013/161] 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 014/161] 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 015/161] 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 016/161] 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 017/161] 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 018/161] 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 019/161] 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 020/161] 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 021/161] 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 022/161] 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 023/161] 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 024/161] 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 025/161] 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 026/161] 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 027/161] 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 028/161] 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 029/161] 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 030/161] 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 031/161] 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 032/161] 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 033/161] 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 034/161] 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 035/161] 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 036/161] 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 037/161] 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 038/161] 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 039/161] 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 040/161] 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 041/161] 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 042/161] 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 043/161] 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 044/161] 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 045/161] 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 046/161] 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 047/161] 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 048/161] 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 049/161] 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 050/161] 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 051/161] 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 052/161] 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 053/161] 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 054/161] 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 055/161] 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 056/161] 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 057/161] 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 058/161] 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 059/161] 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 060/161] 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 061/161] 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 062/161] 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 063/161] 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 064/161] 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 065/161] 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 066/161] 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 067/161] 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 068/161] 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 069/161] 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 070/161] 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 071/161] 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 072/161] 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 073/161] 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 074/161] 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 075/161] 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 076/161] 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 077/161] 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 078/161] 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 079/161] 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 080/161] 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 081/161] 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 082/161] 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 083/161] 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 084/161] 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 085/161] 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 086/161] 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 087/161] 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 088/161] 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 089/161] 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 090/161] 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 091/161] 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 092/161] 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 093/161] 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 094/161] 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 095/161] 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 096/161] 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 097/161] 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 098/161] 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 099/161] 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 100/161] 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 101/161] 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 102/161] 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 103/161] 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 104/161] 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 105/161] 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 106/161] 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 107/161] 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 108/161] 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 109/161] 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 110/161] 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 111/161] 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 112/161] 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 113/161] 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 114/161] 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 115/161] 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 116/161] 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 117/161] 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 118/161] 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 119/161] 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 120/161] 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 121/161] 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 122/161] 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 123/161] 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 124/161] 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 125/161] 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 126/161] 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 127/161] 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 128/161] 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 129/161] 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 130/161] 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 131/161] 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 132/161] 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 133/161] 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 134/161] 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 135/161] 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 136/161] 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 137/161] 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 138/161] 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 139/161] 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 140/161] 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 141/161] 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 142/161] 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 143/161] 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 144/161] 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 145/161] 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 146/161] 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 147/161] 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 148/161] 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 149/161] 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 150/161] 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 151/161] 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 152/161] 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 153/161] 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 154/161] 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 155/161] 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 156/161] 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 157/161] 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 158/161] 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 159/161] 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 160/161] 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 161/161] 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")