From b0c95f0d40fe002847bb2de1fd7f7b1bf0f41d6c Mon Sep 17 00:00:00 2001 From: Charlotte Van Petegem Date: Mon, 29 Jan 2024 10:17:20 +0100 Subject: [PATCH] mail: Use structured sieve configuration --- flake.lock | 12 +- flake.nix | 2 +- modules/services/mail/default.nix | 38 +++-- patches/281001.patch | 268 ++++++++++++++++++++++++++++++ 4 files changed, 295 insertions(+), 25 deletions(-) create mode 100644 patches/281001.patch diff --git a/flake.lock b/flake.lock index cc2726f7..8edf02c6 100644 --- a/flake.lock +++ b/flake.lock @@ -460,16 +460,16 @@ ] }, "locked": { - "lastModified": 1701529057, - "narHash": "sha256-/R+ADRDHR3M1n2GSOjC7LjwBzijA1JGUt1kl15pUnNU=", - "owner": "robbevp", + "lastModified": 1705361146, + "narHash": "sha256-oN6fJdswGueekl3VYLyP8GyZR76In8Au338JdNYX88Q=", + "owner": "dotlambda", "repo": "nixos-mailserver", - "rev": "60132b990f4086f2ef004daa430941ed2b729e14", + "rev": "c0c812117d67e346a94c3b546d04ab4820ed38d3", "type": "gitlab" }, "original": { - "owner": "robbevp", - "ref": "customize-sieve-extentions", + "owner": "dotlambda", + "ref": "sieve-fix", "repo": "nixos-mailserver", "type": "gitlab" } diff --git a/flake.nix b/flake.nix index a497a62a..2994c510 100644 --- a/flake.nix +++ b/flake.nix @@ -63,7 +63,7 @@ }; }; nixos-mailserver = { - url = "gitlab:robbevp/nixos-mailserver/customize-sieve-extentions"; + url = "gitlab:dotlambda/nixos-mailserver/sieve-fix"; inputs = { nixpkgs.follows = "nixpkgs"; utils.follows = "flake-utils"; diff --git a/modules/services/mail/default.nix b/modules/services/mail/default.nix index 862caa38..9e666cbf 100644 --- a/modules/services/mail/default.nix +++ b/modules/services/mail/default.nix @@ -107,7 +107,6 @@ in "sarina@vanpetegem.be" ]; mailDirectory = "${config.chvp.dataPrefix}/var/vmail"; - sieveExtensions = [ "+editheader" ]; useFsLayout = false; certificateScheme = "manual"; certificateFile = certFile; @@ -115,24 +114,27 @@ in dkimKeyDirectory = "${config.chvp.dataPrefix}/var/dkim"; }; - services.dovecot2.sieveScripts = { - after2 = pkgs.writeText "custom-spam.sieve" '' - require ["fileinto", "regex"]; + services.dovecot2 = { + sieve = { + extensions = [ "+editheader" ]; + scripts.after2 = pkgs.writeText "custom-spam.sieve" '' + require ["fileinto", "regex"]; - if anyof(address :is "From" ["junjunggaming07@gmail.com", - "censysnetbackup@gmail.com", - "vitor.carvalheiro@escola.pr.gov.br", - "spam@vuztc.ru", - # itsme spam - "noreply.mailing.itsme@online.be", - "mailing.itsme@online.be"], - # Freshdesk is often used to sent spam from emails like `support@info5813.freshdesk.com` - address :regex "From" "[a-z\d]+@[a-z\d]+\.freshdesk\.com", - header :contains "From" ["jakubbielec", "Jakub Bielec"]) { - fileinto "Junk"; - stop; - } - ''; + if anyof(address :is "From" ["junjunggaming07@gmail.com", + "censysnetbackup@gmail.com", + "vitor.carvalheiro@escola.pr.gov.br", + "spam@vuztc.ru", + # itsme spam + "noreply.mailing.itsme@online.be", + "mailing.itsme@online.be"], + # Freshdesk is often used to sent spam from emails like `support@info5813.freshdesk.com` + address :regex "From" "[a-z\d]+@[a-z\d]+\.freshdesk\.com", + header :contains "From" ["jakubbielec", "Jakub Bielec"]) { + fileinto "Junk"; + stop; + } + ''; + }; }; services.rspamd.extraConfig = '' diff --git a/patches/281001.patch b/patches/281001.patch new file mode 100644 index 00000000..4f781167 --- /dev/null +++ b/patches/281001.patch @@ -0,0 +1,268 @@ +diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix +index 25c7017a1d2589..8d298de6945bbc 100644 +--- a/nixos/modules/services/mail/dovecot.nix ++++ b/nixos/modules/services/mail/dovecot.nix +@@ -4,7 +4,9 @@ let + inherit (lib) any attrValues concatMapStringsSep concatStrings + concatStringsSep flatten imap1 isList literalExpression mapAttrsToList + mkEnableOption mkIf mkOption mkRemovedOptionModule optional optionalAttrs +- optionalString singleton types; ++ optionalString singleton types mkRenamedOptionModule nameValuePair ++ mapAttrs' listToAttrs filter; ++ inherit (lib.strings) match; + + cfg = config.services.dovecot2; + dovecotPkg = pkgs.dovecot; +@@ -12,6 +14,58 @@ let + baseDir = "/run/dovecot2"; + stateDir = "/var/lib/dovecot"; + ++ sieveScriptSettings = mapAttrs' (to: from: nameValuePair "sieve_${to}" "${stateDir}/sieve/${from}") cfg.sieve.scripts; ++ imapSieveMailboxSettings = listToAttrs (flatten (imap1 (idx: el: ++ singleton { ++ name = "imapsieve_mailbox${toString idx}_name"; ++ value = el.name; ++ } ++ optional (el.from != null) { ++ name = "imapsieve_mailbox${toString idx}_from"; ++ value = el.from; ++ } ++ optional (el.causes != []) { ++ name = "imapsieve_mailbox${toString idx}_causes"; ++ value = concatStringsSep "," el.causes; ++ } ++ optional (el.before != null) { ++ name = "imapsieve_mailbox${toString idx}_before"; ++ value = "file:${stateDir}/imapsieve/before/${baseNameOf el.before}"; ++ } ++ optional (el.after != null) { ++ name = "imapsieve_mailbox${toString idx}_after"; ++ value = "file:${stateDir}/imapsieve/after/${baseNameOf el.after}"; ++ } ++ ) cfg.imapsieve.mailbox)); ++ ++ mkExtraConfigCollisionWarning = term: '' ++ You referred to ${term} in `services.dovecot2.extraConfig`. ++ ++ Due to gradual transition to structured configuration for plugin configuration, it is possible ++ this will cause your plugin configuration to be ignored. ++ ++ Consider setting `services.dovecot2.pluginSettings.${term}` instead. ++ ''; ++ ++ # Those settings are automatically set based on other parts ++ # of this module. ++ automaticallySetPluginSettings = [ ++ "sieve_plugins" ++ "sieve_extensions" ++ "sieve_global_extensions" ++ "sieve_pipe_bin_dir" ++ ] ++ ++ (builtins.attrNames sieveScriptSettings) ++ ++ (builtins.attrNames imapSieveMailboxSettings); ++ ++ # The idea is to match everything that looks like `$term =` ++ # but not `# $term something something` ++ # or `# $term = some value` because those are comments. ++ configContainsSetting = lines: term: (match "^[^#]*\b${term}\b.*=" lines) != null; ++ ++ warnAboutExtraConfigCollisions = map mkExtraConfigCollisionWarning (filter (configContainsSetting cfg.extraConfig) automaticallySetPluginSettings); ++ ++ sievePipeBinScriptDirectory = pkgs.linkFarm "sieve-pipe-bins" (map (el: { ++ name = builtins.unsafeDiscardStringContext (baseNameOf el); ++ path = el; ++ }) cfg.sieve.pipeBins); ++ + dovecotConf = concatStrings [ + '' + base_dir = ${baseDir} +@@ -77,14 +131,6 @@ let + '' + ) + +- ( +- optionalString (cfg.sieveScripts != {}) '' +- plugin { +- ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieveScripts)} +- } +- '' +- ) +- + ( + optionalString (cfg.mailboxes != {}) '' + namespace inbox { +@@ -116,33 +162,12 @@ let + '' + ) + ++ # General plugin settings: ++ # - sieve is mostly generated here, refer to `pluginSettings` to follow ++ # the control flow. + '' + plugin { +- sieve_plugins = ${concatStringsSep " " cfg.sieve.plugins} +- sieve_extensions = ${concatStringsSep " " (map (el: "+${el}") cfg.sieve.extensions)} +- sieve_global_extensions = ${concatStringsSep " " (map (el: "+${el}") cfg.sieve.globalExtensions)} +- '' +- (optionalString (cfg.imapsieve.mailbox != []) '' +- ${ +- concatStringsSep "\n" (flatten (imap1 ( +- idx: el: +- singleton "imapsieve_mailbox${toString idx}_name = ${el.name}" +- ++ optional (el.from != null) "imapsieve_mailbox${toString idx}_from = ${el.from}" +- ++ optional (el.causes != null) "imapsieve_mailbox${toString idx}_causes = ${el.causes}" +- ++ optional (el.before != null) "imapsieve_mailbox${toString idx}_before = file:${stateDir}/imapsieve/before/${baseNameOf el.before}" +- ++ optional (el.after != null) "imapsieve_mailbox${toString idx}_after = file:${stateDir}/imapsieve/after/${baseNameOf el.after}" +- ) +- cfg.imapsieve.mailbox)) +- } +- '') +- (optionalString (cfg.sieve.pipeBins != []) '' +- sieve_pipe_bin_dir = ${pkgs.linkFarm "sieve-pipe-bins" (map (el: { +- name = builtins.unsafeDiscardStringContext (baseNameOf el); +- path = el; +- }) +- cfg.sieve.pipeBins)} +- '') +- '' ++ ${concatStringsSep "\n" (mapAttrsToList (key: value: " ${key} = ${value}") cfg.pluginSettings)} + } + '' + +@@ -199,6 +224,7 @@ in + { + imports = [ + (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "") ++ (mkRenamedOptionModule [ "services" "dovecot2" "sieveScripts" ] [ "services" "dovecot2" "sieve" "scripts" ]) + ]; + + options.services.dovecot2 = { +@@ -337,12 +363,6 @@ in + + enableDHE = mkEnableOption (lib.mdDoc "ssl_dh and generation of primes for the key exchange") // { default = true; }; + +- sieveScripts = mkOption { +- type = types.attrsOf types.path; +- default = {}; +- description = lib.mdDoc "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc."; +- }; +- + showPAMFailure = mkEnableOption (lib.mdDoc "showing the PAM failure message on authentication error (useful for OTPW)"); + + mailboxes = mkOption { +@@ -376,6 +396,26 @@ in + description = lib.mdDoc "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %."; + }; + ++ ++ pluginSettings = mkOption { ++ # types.str does not coerce from packages, like `sievePipeBinScriptDirectory`. ++ type = types.attrsOf (types.oneOf [ types.str types.package ]); ++ default = {}; ++ example = literalExpression '' ++ { ++ sieve = "file:~/sieve;active=~/.dovecot.sieve"; ++ } ++ ''; ++ description = '' ++ Plugin settings for dovecot in general, e.g. `sieve`, `sieve_default`, etc. ++ ++ Some of the other knobs of this module will influence by default the plugin settings, but you ++ can still override any plugin settings. ++ ++ If you override a plugin setting, its value is cleared and you have to copy over the defaults. ++ ''; ++ }; ++ + imapsieve.mailbox = mkOption { + default = []; + description = "Configure Sieve filtering rules on IMAP actions"; +@@ -405,14 +445,14 @@ in + }; + + causes = mkOption { +- default = null; ++ default = [ ]; + description = '' + Only execute the administrator Sieve scripts for the mailbox configured with services.dovecot2.imapsieve.mailbox..name when one of the listed IMAPSIEVE causes apply. + + This has no effect on the user script, which is always executed no matter the cause. + ''; +- example = "COPY"; +- type = types.nullOr (types.enum [ "APPEND" "COPY" "FLAG" ]); ++ example = [ "COPY" "APPEND" ]; ++ type = types.listOf (types.enum [ "APPEND" "COPY" "FLAG" ]); + }; + + before = mkOption { +@@ -462,6 +502,12 @@ in + type = types.listOf types.str; + }; + ++ scripts = mkOption { ++ type = types.attrsOf types.path; ++ default = {}; ++ description = lib.mdDoc "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc."; ++ }; ++ + pipeBins = mkOption { + default = []; + example = literalExpression '' +@@ -476,7 +522,6 @@ in + }; + }; + +- + config = mkIf cfg.enable { + security.pam.services.dovecot2 = mkIf cfg.enablePAM {}; + +@@ -501,6 +546,13 @@ in + ++ optional (cfg.sieve.pipeBins != []) "sieve_extprograms"; + + sieve.globalExtensions = optional (cfg.sieve.pipeBins != []) "vnd.dovecot.pipe"; ++ ++ pluginSettings = lib.mapAttrs (n: lib.mkDefault) ({ ++ sieve_plugins = concatStringsSep " " cfg.sieve.plugins; ++ sieve_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.extensions); ++ sieve_global_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.globalExtensions); ++ sieve_pipe_bin_dir = sievePipeBinScriptDirectory; ++ } // sieveScriptSettings // imapSieveMailboxSettings); + }; + + users.users = { +@@ -556,7 +608,7 @@ in + # the source file and Dovecot won't try to compile it. + preStart = '' + rm -rf ${stateDir}/sieve ${stateDir}/imapsieve +- '' + optionalString (cfg.sieveScripts != {}) '' ++ '' + optionalString (cfg.sieve.scripts != {}) '' + mkdir -p ${stateDir}/sieve + ${concatStringsSep "\n" ( + mapAttrsToList ( +@@ -569,7 +621,7 @@ in + fi + ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}' + '' +- ) cfg.sieveScripts ++ ) cfg.sieve.scripts + )} + chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve' + '' +@@ -600,9 +652,7 @@ in + + environment.systemPackages = [ dovecotPkg ]; + +- warnings = mkIf (any isList options.services.dovecot2.mailboxes.definitions) [ +- "Declaring `services.dovecot2.mailboxes' as a list is deprecated and will break eval in 21.05! See the release notes for more info for migration." +- ]; ++ warnings = warnAboutExtraConfigCollisions; + + assertions = [ + { +@@ -615,8 +665,8 @@ in + message = "dovecot is configured with showPAMFailure while enablePAM is disabled"; + } + { +- assertion = cfg.sieveScripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null); +- message = "dovecot requires mailUser and mailGroup to be set when sieveScripts is set"; ++ assertion = cfg.sieve.scripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null); ++ message = "dovecot requires mailUser and mailGroup to be set when `sieve.scripts` is set"; + } + ]; +