From caf9e51e0f836a14f34acafef3cde2e608f15f33 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Sun, 14 Jan 2024 21:07:42 +0100 Subject: [PATCH 1/3] nixos/mail/dovecot2: re-introduce extra settings and rename `sieveScripts` https://github.com/NixOS/nixpkgs/pull/275031 introduced structured configuration for the dovecot2 sieve plugin, by doing so, it broke SNM configuration doing Sieve configurations. This attempts to fix up the public API to make it possible for SNM to pick up the pieces. --- nixos/modules/services/mail/dovecot.nix | 89 +++++++++++++++++++------ 1 file changed, 69 insertions(+), 20 deletions(-) diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix index 25c7017a1d25..13f3e337cec2 100644 --- a/nixos/modules/services/mail/dovecot.nix +++ b/nixos/modules/services/mail/dovecot.nix @@ -4,7 +4,8 @@ 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; cfg = config.services.dovecot2; dovecotPkg = pkgs.dovecot; @@ -12,6 +13,30 @@ 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)); + 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 +102,6 @@ let '' ) - ( - optionalString (cfg.sieveScripts != {}) '' - plugin { - ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieveScripts)} - } - '' - ) - ( optionalString (cfg.mailboxes != {}) '' namespace inbox { @@ -116,11 +133,16 @@ let '' ) + # General plugin settings: + # - sieve is mostly generated here, refer to `pluginSettings` to follow + # the control flow. '' plugin { + ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieve.scripts)} 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)} + ${concatStringsSep "\n" (mapAttrsToList (key: value: "${key} = ${value}") cfg.sieve.extraPluginSettings)} '' (optionalString (cfg.imapsieve.mailbox != []) '' ${ @@ -199,6 +221,7 @@ in { imports = [ (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "") + (mkRenamedOptionModule [ "services" "dovecot2" "sieveScripts" ] [ "services" "dovecot2" "sieve" "scripts" ]) ]; options.services.dovecot2 = { @@ -337,12 +360,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 +393,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"; @@ -462,6 +499,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 +519,6 @@ in }; }; - config = mkIf cfg.enable { security.pam.services.dovecot2 = mkIf cfg.enablePAM {}; @@ -501,6 +543,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 +605,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 +618,7 @@ in fi ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}' '' - ) cfg.sieveScripts + ) cfg.sieve.scripts )} chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve' '' @@ -615,8 +664,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"; } ]; From 72e23635e61a3d1f8992f6d3116cf46cf185ca68 Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Sun, 14 Jan 2024 21:42:40 +0100 Subject: [PATCH 2/3] nixos/mail/dovecot2: `imapsieve.mailbox.*.causes` is a list Otherwise, it's not possible to pass `COPY,APPEND` properly. --- nixos/modules/services/mail/dovecot.nix | 34 +++---------------------- 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix index 13f3e337cec2..84f39f26c185 100644 --- a/nixos/modules/services/mail/dovecot.nix +++ b/nixos/modules/services/mail/dovecot.nix @@ -138,33 +138,7 @@ let # the control flow. '' plugin { - ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieve.scripts)} - 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)} - ${concatStringsSep "\n" (mapAttrsToList (key: value: "${key} = ${value}") cfg.sieve.extraPluginSettings)} - '' - (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)} } '' @@ -442,14 +416,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 { From 3cb78237388c3fa9559458a2b00af7beb57c446b Mon Sep 17 00:00:00 2001 From: Raito Bezarius Date: Tue, 23 Jan 2024 13:49:02 +0100 Subject: [PATCH 3/3] nixos/mail/dovecot2: warn about potential collision due to structured configuration Plugin configuration is pesky in dovecot2, let's warn about potential conflicts in the module system by using a fancy regex. This is only band-aid, this should be removed ASAP. We clean up also a 21.05-era warning. --- nixos/modules/services/mail/dovecot.nix | 35 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix index 84f39f26c185..8d298de6945b 100644 --- a/nixos/modules/services/mail/dovecot.nix +++ b/nixos/modules/services/mail/dovecot.nix @@ -5,7 +5,8 @@ let concatStringsSep flatten imap1 isList literalExpression mapAttrsToList mkEnableOption mkIf mkOption mkRemovedOptionModule optional optionalAttrs optionalString singleton types mkRenamedOptionModule nameValuePair - mapAttrs' listToAttrs; + mapAttrs' listToAttrs filter; + inherit (lib.strings) match; cfg = config.services.dovecot2; dovecotPkg = pkgs.dovecot; @@ -32,6 +33,34 @@ let 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; @@ -623,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 = [ {