From b5aeef311689873efc7b4cfedb3ae4847cdb9e20 Mon Sep 17 00:00:00 2001 From: Katalin Rebhan Date: Mon, 30 Mar 2026 18:19:05 +0200 Subject: [PATCH 1/8] dovecot{,_pigeonhole}: make 2.4 the default version --- doc/release-notes/rl-2605.section.md | 2 ++ pkgs/by-name/do/dovecot/2.3.nix | 18 ++++++++++++++++++ pkgs/by-name/do/dovecot/2.4.nix | 8 -------- pkgs/by-name/do/dovecot/generic.nix | 3 +-- pkgs/by-name/do/dovecot/package.nix | 16 +++------------- pkgs/by-name/do/dovecot_pigeonhole/0.5.nix | 10 ++++++++++ pkgs/by-name/do/dovecot_pigeonhole/2.4.nix | 17 ----------------- pkgs/by-name/do/dovecot_pigeonhole/generic.nix | 3 +-- pkgs/by-name/do/dovecot_pigeonhole/package.nix | 13 ++++++++++--- pkgs/top-level/all-packages.nix | 12 ++++++------ 10 files changed, 51 insertions(+), 51 deletions(-) create mode 100644 pkgs/by-name/do/dovecot/2.3.nix delete mode 100644 pkgs/by-name/do/dovecot/2.4.nix create mode 100644 pkgs/by-name/do/dovecot_pigeonhole/0.5.nix delete mode 100644 pkgs/by-name/do/dovecot_pigeonhole/2.4.nix diff --git a/doc/release-notes/rl-2605.section.md b/doc/release-notes/rl-2605.section.md index ed1f18a3c3bb..b1023b71f107 100644 --- a/doc/release-notes/rl-2605.section.md +++ b/doc/release-notes/rl-2605.section.md @@ -228,6 +228,8 @@ - All Xfce packages have been moved to top level (e.g. if you previously added `pkgs.xfce.xfce4-whiskermenu-plugin` to `environment.systemPackages`, you will need to change it to `pkgs.xfce4-whiskermenu-plugin`). The `xfce` scope will be removed in NixOS 26.11. +- The Dovecot IMAP server has been updated to version 2.4, with the `dovecot` attribute now referring to this backwards-incompatible version. The attribute `dovecot_2_3` refers to the previous version. The Pigeonhole plugin has been similarly updated to 2.4, with the version compatible with Dovecot 2.3 being at `dovecot_pigeonhole_0_5`. See for more information on how to upgrade. + - `spacefm` was removed because it appeared to be unmaintained upstream. - `vimPlugins.nvim-treesitter` has been updated to `main` branch, which is a full and incompatible rewrite. If you can't or don't want to update, you should use `vimPlugins.nvim-treesitter-legacy`. diff --git a/pkgs/by-name/do/dovecot/2.3.nix b/pkgs/by-name/do/dovecot/2.3.nix new file mode 100644 index 000000000000..3b8ad371034e --- /dev/null +++ b/pkgs/by-name/do/dovecot/2.3.nix @@ -0,0 +1,18 @@ +import ./generic.nix { + version = "2.3.21.1"; + hash = "sha256-1THBB401r6AnZbnHGhIq+o+nR8v94O0mIX68Pv0miiE="; + patches = fetchpatch: [ + # Fix loading extended modules. + ./load-extended-modules.patch + # fix openssl 3.0 compatibility + (fetchpatch { + url = "https://salsa.debian.org/debian/dovecot/-/raw/debian/1%252.3.19.1+dfsg1-2/debian/patches/Support-openssl-3.0.patch"; + hash = "sha256-PbBB1jIY3jIC8Js1NY93zkV0gISGUq7Nc67Ul5tN7sw="; + }) + (fetchpatch { + name = "dovecot-test-data-stack-drop-bogus-assertion.patch"; + url = "https://github.com/dovecot/core/commit/9f642dd868db6e7401f24e4fb4031b5bdca8aae7.patch"; + hash = "sha256-dAX80dRqOba9Fkzl11ChYJ6vqcgfkaw/o+TOQKCnnns="; + }) + ]; +} diff --git a/pkgs/by-name/do/dovecot/2.4.nix b/pkgs/by-name/do/dovecot/2.4.nix deleted file mode 100644 index 7421368e2a3e..000000000000 --- a/pkgs/by-name/do/dovecot/2.4.nix +++ /dev/null @@ -1,8 +0,0 @@ -import ./generic.nix { - version = "2.4.3"; - hash = "sha256-NTtQMHK/IzAYHKb1lxClUUJkyJpeLo7mKRCAR1GaUTo="; - patches = _: [ - # Fix loading extended modules. - ./load-extended-modules.patch - ]; -} diff --git a/pkgs/by-name/do/dovecot/generic.nix b/pkgs/by-name/do/dovecot/generic.nix index 49d809e6fae5..fbdfcef9557a 100644 --- a/pkgs/by-name/do/dovecot/generic.nix +++ b/pkgs/by-name/do/dovecot/generic.nix @@ -33,8 +33,7 @@ fetchpatch, rpcsvc-proto, libtirpc, - dovecot_pigeonhole_0_5, - dovecot_pigeonhole ? dovecot_pigeonhole_0_5, + dovecot_pigeonhole, withApparmor ? false, libapparmor, withLDAP ? true, diff --git a/pkgs/by-name/do/dovecot/package.nix b/pkgs/by-name/do/dovecot/package.nix index 3b8ad371034e..7421368e2a3e 100644 --- a/pkgs/by-name/do/dovecot/package.nix +++ b/pkgs/by-name/do/dovecot/package.nix @@ -1,18 +1,8 @@ import ./generic.nix { - version = "2.3.21.1"; - hash = "sha256-1THBB401r6AnZbnHGhIq+o+nR8v94O0mIX68Pv0miiE="; - patches = fetchpatch: [ + version = "2.4.3"; + hash = "sha256-NTtQMHK/IzAYHKb1lxClUUJkyJpeLo7mKRCAR1GaUTo="; + patches = _: [ # Fix loading extended modules. ./load-extended-modules.patch - # fix openssl 3.0 compatibility - (fetchpatch { - url = "https://salsa.debian.org/debian/dovecot/-/raw/debian/1%252.3.19.1+dfsg1-2/debian/patches/Support-openssl-3.0.patch"; - hash = "sha256-PbBB1jIY3jIC8Js1NY93zkV0gISGUq7Nc67Ul5tN7sw="; - }) - (fetchpatch { - name = "dovecot-test-data-stack-drop-bogus-assertion.patch"; - url = "https://github.com/dovecot/core/commit/9f642dd868db6e7401f24e4fb4031b5bdca8aae7.patch"; - hash = "sha256-dAX80dRqOba9Fkzl11ChYJ6vqcgfkaw/o+TOQKCnnns="; - }) ]; } diff --git a/pkgs/by-name/do/dovecot_pigeonhole/0.5.nix b/pkgs/by-name/do/dovecot_pigeonhole/0.5.nix new file mode 100644 index 000000000000..4de2e7419269 --- /dev/null +++ b/pkgs/by-name/do/dovecot_pigeonhole/0.5.nix @@ -0,0 +1,10 @@ +import ./generic.nix { + version = "0.5.21.1"; + url = + { + version, + dovecotMajorMinor, + }: + "https://pigeonhole.dovecot.org/releases/${dovecotMajorMinor}/dovecot-${dovecotMajorMinor}-pigeonhole-${version}.tar.gz"; + hash = "sha256-andOWZmgWYIPATEk0V+mHmReL+quG81azwPkBMoo9OE="; +} diff --git a/pkgs/by-name/do/dovecot_pigeonhole/2.4.nix b/pkgs/by-name/do/dovecot_pigeonhole/2.4.nix deleted file mode 100644 index ca18eaa2d271..000000000000 --- a/pkgs/by-name/do/dovecot_pigeonhole/2.4.nix +++ /dev/null @@ -1,17 +0,0 @@ -import ./generic.nix { - version = "2.4.3"; - url = - { - version, - dovecotMajorMinor, - }: - "https://pigeonhole.dovecot.org/releases/${dovecotMajorMinor}/dovecot-pigeonhole-${version}.tar.gz"; - hash = "sha256-LQNhqYnBVICabluj8F07UOZR5frt6bd9JSyuHJDS6hc="; - patches = fetchpatch: [ - # https://github.com/NixOS/nixpkgs/pull/388463#issuecomment-3066016707 - (fetchpatch { - url = "https://github.com/dovecot/pigeonhole/commit/517d74aa1d98b853b72608ce722bc58009c0f4a9.patch"; - hash = "sha256-BLBz9ZhOGEIIitnXG0uM6bZBRNnQBy4K2IJlh1+Un50="; - }) - ]; -} diff --git a/pkgs/by-name/do/dovecot_pigeonhole/generic.nix b/pkgs/by-name/do/dovecot_pigeonhole/generic.nix index 5f11b695efa1..fca1f17172ec 100644 --- a/pkgs/by-name/do/dovecot_pigeonhole/generic.nix +++ b/pkgs/by-name/do/dovecot_pigeonhole/generic.nix @@ -9,8 +9,7 @@ stdenv, fetchpatch, fetchzip, - dovecot_2_3, - dovecot ? dovecot_2_3, + dovecot, openssl, libstemmer, perl, diff --git a/pkgs/by-name/do/dovecot_pigeonhole/package.nix b/pkgs/by-name/do/dovecot_pigeonhole/package.nix index 4de2e7419269..ca18eaa2d271 100644 --- a/pkgs/by-name/do/dovecot_pigeonhole/package.nix +++ b/pkgs/by-name/do/dovecot_pigeonhole/package.nix @@ -1,10 +1,17 @@ import ./generic.nix { - version = "0.5.21.1"; + version = "2.4.3"; url = { version, dovecotMajorMinor, }: - "https://pigeonhole.dovecot.org/releases/${dovecotMajorMinor}/dovecot-${dovecotMajorMinor}-pigeonhole-${version}.tar.gz"; - hash = "sha256-andOWZmgWYIPATEk0V+mHmReL+quG81azwPkBMoo9OE="; + "https://pigeonhole.dovecot.org/releases/${dovecotMajorMinor}/dovecot-pigeonhole-${version}.tar.gz"; + hash = "sha256-LQNhqYnBVICabluj8F07UOZR5frt6bd9JSyuHJDS6hc="; + patches = fetchpatch: [ + # https://github.com/NixOS/nixpkgs/pull/388463#issuecomment-3066016707 + (fetchpatch { + url = "https://github.com/dovecot/pigeonhole/commit/517d74aa1d98b853b72608ce722bc58009c0f4a9.patch"; + hash = "sha256-BLBz9ZhOGEIIitnXG0uM6bZBRNnQBy4K2IJlh1+Un50="; + }) + ]; } diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 305ece210f28..3ffabf922308 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -8070,15 +8070,15 @@ with pkgs; dodgy = with python3Packages; toPythonApplication dodgy; - dovecot_2_3 = dovecot; - dovecot_2_4 = callPackage ../by-name/do/dovecot/2.4.nix { - dovecot_pigeonhole = dovecot_pigeonhole_2_4; + dovecot_2_3 = callPackage ../by-name/do/dovecot/2.3.nix { + dovecot_pigeonhole = dovecot_pigeonhole_0_5; }; + dovecot_2_4 = dovecot; - dovecot_pigeonhole_0_5 = dovecot_pigeonhole; - dovecot_pigeonhole_2_4 = callPackage ../by-name/do/dovecot_pigeonhole/2.4.nix { - dovecot = dovecot_2_4; + dovecot_pigeonhole_0_5 = callPackage ../by-name/do/dovecot_pigeonhole/0.5.nix { + dovecot = dovecot_2_3; }; + dovecot_pigeonhole_2_4 = dovecot_pigeonhole; inherit (callPackages ../servers/firebird { }) firebird_4 From ad56a1d988b448518aaad2bc5577b3059c8dfd12 Mon Sep 17 00:00:00 2001 From: Jappie3 Date: Sat, 20 Dec 2025 16:16:52 +0100 Subject: [PATCH 2/8] nixos/dovecot: add settings option (RFC42) Co-authored-by: Katalin Rebhan --- nixos/modules/services/mail/dovecot.nix | 695 +++++++++++------------- nixos/tests/dovecot.nix | 14 +- 2 files changed, 336 insertions(+), 373 deletions(-) diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix index af91440c6a3f..c4e71daff438 100644 --- a/nixos/modules/services/mail/dovecot.nix +++ b/nixos/modules/services/mail/dovecot.nix @@ -7,33 +7,45 @@ let inherit (lib) - attrValues + all + attrsToList concatMapStringsSep - concatStrings concatStringsSep + elemAt + filter flatten imap1 + isAttrs + isBool + isDerivation + isInt + isList + isPath + isString + listToAttrs literalExpression + mapAttrs' mapAttrsToList + mkDefault mkEnableOption mkIf + mkMerge mkOption + mkPackageOption mkRemovedOptionModule + mkRenamedOptionModule + nameValuePair optional optionalAttrs optionalString + optionals singleton + splitString + traceSeq types - mkRenamedOptionModule - nameValuePair - mapAttrs' - listToAttrs - filter ; - inherit (lib.strings) match; cfg = config.services.dovecot2; - dovecotPkg = pkgs.dovecot; baseDir = "/run/dovecot2"; stateDir = "/var/lib/dovecot"; @@ -69,35 +81,6 @@ let ) ); - 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 "[[:blank:]]*${term}[[:blank:]]*=.*" lines) != null; - - warnAboutExtraConfigCollisions = map mkExtraConfigCollisionWarning ( - filter (configContainsSetting cfg.extraConfig) automaticallySetPluginSettings - ); - sievePipeBinScriptDirectory = pkgs.linkFarm "sieve-pipe-bins" ( map (el: { name = builtins.unsafeDiscardStringContext (baseNameOf el); @@ -105,200 +88,263 @@ let }) cfg.sieve.pipeBins ); - dovecotConf = concatStrings [ - '' - base_dir = ${baseDir} - protocols = ${concatStringsSep " " cfg.protocols} - sendmail_path = /run/wrappers/bin/sendmail - mail_plugin_dir = /run/current-system/sw/lib/dovecot/modules - # defining mail_plugins must be done before the first protocol {} filter because of https://doc.dovecot.org/configuration_manual/config_file/config_file_syntax/#variable-expansion - mail_plugins = $mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable} - '' - - (concatStringsSep "\n" ( - mapAttrsToList (protocol: plugins: '' - protocol ${protocol} { - mail_plugins = $mail_plugins ${concatStringsSep " " plugins.enable} - } - '') cfg.mailPlugins.perProtocol - )) - - ( - if cfg.sslServerCert == null then - '' - ssl = no - disable_plaintext_auth = no - '' - else - '' - ssl_cert = <${cfg.sslServerCert} - ssl_key = <${cfg.sslServerKey} - ${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)} - ${optionalString cfg.enableDHE "ssl_dh = <${config.security.dhparams.params.dovecot2.path}"} - disable_plaintext_auth = yes - '' - ) - - '' - default_internal_user = ${cfg.user} - default_internal_group = ${cfg.group} - ${optionalString (cfg.mailUser != null) "mail_uid = ${cfg.mailUser}"} - ${optionalString (cfg.mailGroup != null) "mail_gid = ${cfg.mailGroup}"} - - mail_location = ${cfg.mailLocation} - - maildir_copy_with_hardlinks = yes - pop3_uidl_format = %08Xv%08Xu - - auth_mechanisms = plain login - - service auth { - user = root - } - '' - - (optionalString cfg.enablePAM '' - userdb { - driver = passwd - } - - passdb { - driver = pam - args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2 - } - '') - - (optionalString (cfg.mailboxes != { }) '' - namespace inbox { - inbox=yes - ${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))} - } - '') - - (optionalString cfg.enableQuota '' - service quota-status { - executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix - inet_listener { - port = ${cfg.quotaPort} - } - client_limit = 1 - } - - plugin { - quota_rule = *:storage=${cfg.quotaGlobalPerUser} - quota = count:User quota # per virtual mail user quota - quota_status_success = DUNNO - quota_status_nouser = DUNNO - quota_status_overquota = "552 5.2.2 Mailbox is full" - quota_grace = 10%% - quota_vsizes = yes - } - '') - - # General plugin settings: - # - sieve is mostly generated here, refer to `pluginSettings` to follow - # the control flow. - '' - plugin { - ${concatStringsSep "\n" (mapAttrsToList (key: value: " ${key} = ${value}") cfg.pluginSettings)} - } - '' - - cfg.extraConfig + allSieveSettings = mkMerge [ + imapSieveMailboxSettings + sieveScriptSettings + { + sieve_plugins = + mkIf (cfg.sieve.plugins != [ ]) # . + (concatStringsSep " " cfg.sieve.plugins); + sieve_extensions = + mkIf (cfg.sieve.extensions != [ ]) # . + (concatMapStringsSep " " (el: "+${el}") cfg.sieve.extensions); + sieve_global_extensions = + mkIf (cfg.sieve.globalExtensions != [ ]) # . + (concatMapStringsSep " " (el: "+${el}") cfg.sieve.globalExtensions); + sieve_pipe_bin_dir = + mkIf (cfg.sieve.pipeBins != [ ]) # . + sievePipeBinScriptDirectory; + } ]; - mailboxConfig = - mailbox: - '' - mailbox "${mailbox.name}" { - auto = ${toString mailbox.auto} - '' - + optionalString (mailbox.autoexpunge != null) '' - autoexpunge = ${mailbox.autoexpunge} - '' - + optionalString (mailbox.specialUse != null) '' - special_use = \${toString mailbox.specialUse} - '' - + "}"; + yesOrNo = v: if v then "yes" else "no"; - mailboxes = - { name, ... }: - { - options = { - name = mkOption { - type = types.strMatching ''[^"]+''; - example = "Spam"; - default = name; - readOnly = true; - description = "The name of the mailbox."; - }; - auto = mkOption { - type = types.enum [ - "no" - "create" - "subscribe" - ]; - default = "no"; - example = "subscribe"; - description = "Whether to automatically create or create and subscribe to the mailbox or not."; - }; - specialUse = mkOption { - type = types.nullOr ( - types.enum [ - "All" - "Archive" - "Drafts" - "Flagged" - "Junk" - "Sent" - "Trash" - ] - ); - default = null; - example = "Junk"; - description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid."; - }; - autoexpunge = mkOption { - type = types.nullOr types.str; - default = null; - example = "60d"; - description = '' - To automatically remove all email from the mailbox which is older than the - specified time. - ''; - }; - }; - }; + toOption = + i: n: v: + "${i}${toString n} = ${v}"; + + formatKeyValue = + indent: n: v: + if (v == null) then + "" + else if isInt v then + toOption indent n (toString v) + else if isBool v then + toOption indent n (yesOrNo v) + else if isString v then + toOption indent n v + else if isList v then + if all isString v then + toOption indent n (concatStringsSep " " v) + else + map (formatKeyValue indent n) v + else if isPath v || isDerivation v then + # paths -> copy to store + # derivations -> just use output path instead of looping over the attrs + toOption indent n "${v}" + else if isAttrs v then + let + sectionType = v._section.type; + sectionName = v._section.name; + sectionTitle = concatStringsSep " " ( + filter (s: s != null) [ + sectionType + sectionName + ] + ); + in + concatStringsSep "\n" ( + [ + "${indent}${sectionTitle} {" + ] + ++ (mapAttrsToList (formatKeyValue "${indent} ") (removeAttrs v [ "_section" ])) + ++ [ "${indent}}" ] + ) + else + throw (traceSeq v "services.dovecot2.settings: unexpected type"); + + doveConf = concatStringsSep "\n" ( + optionals (cfg.includeFiles != [ ]) (map (f: "!include ${f}") cfg.includeFiles) + ++ flatten (mapAttrsToList (formatKeyValue "") cfg.settings) + ); in { imports = [ - (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "") (mkRemovedOptionModule [ "services" "dovecot2" "modules" ] "Now need to use `environment.systemPackages` to load additional Dovecot modules") + (mkRemovedOptionModule [ + "services" + "dovecot2" + "enablePop3" + ] "Set 'services.dovecot2.settings.protocols.pop3 = true/false;' instead.") + (mkRemovedOptionModule [ + "services" + "dovecot2" + "enableImap" + ] "Set 'services.dovecot2.settings.protocols.imap = true/false;' instead.") + (mkRemovedOptionModule [ + "services" + "dovecot2" + "enableLmtp" + ] "Set 'services.dovecot2.settings.protocols.lmtp = true/false;' instead.") (mkRenamedOptionModule [ "services" "dovecot2" "sieveScripts" ] [ "services" "dovecot2" "sieve" "scripts" ] ) - ]; + (mkRenamedOptionModule + [ "services" "dovecot2" "mailUser" ] + [ "services" "dovecot2" "settings" "mail_uid" ] + ) + (mkRenamedOptionModule + [ "services" "dovecot2" "mailGroup" ] + [ "services" "dovecot2" "settings" "mail_gid" ] + ) + (mkRenamedOptionModule + [ "services" "dovecot2" "protocols" ] + [ "services" "dovecot2" "settings" "protocols" ] + ) + ] + ++ ( + let + basePath = [ + "services" + "dovecot2" + ]; + mkRemovedOptions = + list: + map ( + name: mkRemovedOptionModule (basePath ++ name) "Please use services.dovecot2.settings instead." + ) list; + in + mkRemovedOptions [ + [ "extraConfig" ] + [ "mailboxes" ] + [ "pluginSettings" ] + [ "enableQuota" ] + [ "quotaPort" ] + [ "quotaGlobalPerUser" ] + ] + ); options.services.dovecot2 = { enable = mkEnableOption "the dovecot 2.x POP3/IMAP server"; - enablePop3 = mkEnableOption "starting the POP3 listener (when Dovecot is enabled)"; - - enableImap = mkEnableOption "starting the IMAP listener (when Dovecot is enabled)" // { - default = true; + package = mkPackageOption pkgs "dovecot" { } // { }; - enableLmtp = mkEnableOption "starting the LMTP listener (when Dovecot is enabled)"; + settings = mkOption { + default = { }; + type = + let + inherit (lib.types) + attrsOf + path + bool + int + nonEmptyListOf + nonEmptyStr + nullOr + oneOf + str + submodule + ; + inherit (lib.lists) last dropEnd; - protocols = mkOption { - type = types.listOf types.str; - default = [ ]; - description = "Additional listeners to start when Dovecot is enabled."; + section = submodule ( + { name, options, ... }: + let + # if the current name is a list (matches '[definition .*]') -> get + # name' from _module.args.loc & use it for {type,name}Default + name' = + if (builtins.match "[[]definition .*].*" name) == null then + name + else + last (dropEnd 3 options._module.args.loc); + + # split name' on the first space + splits = builtins.match "([^ ]+) (.+)" name'; + typeDefault = if splits == null then name' else builtins.elemAt splits 0; + nameDefault = if splits == null then null else builtins.elemAt splits 1; + in + { + options = { + _section = { + type = mkOption { + description = "Section type, mandatory for every section."; + type = nonEmptyStr; + default = typeDefault; + }; + name = mkOption { + description = "Section name, comes after section type & is optional in some cases."; + type = nullOr nonEmptyStr; + default = nameDefault; + }; + }; + }; + + freeformType = attrsOf valueType; + } + ); + + primitiveType = oneOf [ + int + str + bool + # path must order before section, otherwise the latter will + # interpret path literals as a module path to load + path + section + ]; + + valueType = + nullOr (oneOf [ + primitiveType + (nonEmptyListOf primitiveType) + ]) + // { + description = "Dovecot config value"; + }; + in + attrsOf valueType; + description = '' + Dovecot configuration, see + for all available options. + + For information on the configuration structure, see . + + ::: {.warning} + Explicit settings in [{option}`services.dovecot2.settings`](#opt-services.dovecot2.settings) can silently override values set by other `services.dovecot2.*` options. + ::: + ''; + example = { + protocols = { + imap = true; + submission = true; + lmtp = true; + }; + mail_driver = "maildir"; + "namespace inbox" = { + inbox = true; + separator = "/"; + }; + service = [ + { + _section.name = "imap"; + process_min_avail = 1; + client_limit = 100; + "inet_listener imap".port = 31143; + "inet_listener imaps".port = 31993; + } + { + _section.name = "lmtp"; + user = "dovemail"; + "unix_listener lmtp" = { + mode = "0660"; + user = "postfix"; + }; + } + ]; + "protocol imap".mail_plugins = { + imap_sieve = true; + imap_filter_sieve = true; + }; + mail_attribute."dict file" = { + path = "%{home}/dovecot-attributes"; + }; + }; }; user = mkOption { @@ -313,13 +359,6 @@ in description = "Dovecot group name."; }; - extraConfig = mkOption { - type = types.lines; - default = ""; - example = "mail_debug = yes"; - description = "Additional entries to put verbatim into Dovecot's config file."; - }; - mailPlugins = let plugins = @@ -374,129 +413,31 @@ in type = types.nullOr types.path; default = null; description = "Config file used for the whole dovecot configuration."; - apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf; + apply = v: if v != null then v else pkgs.writeText "dovecot.conf" doveConf; }; - mailLocation = mkOption { - type = types.str; - default = "maildir:/var/spool/mail/%u"; # Same as inbox, as postfix - example = "maildir:~/mail:INBOX=/var/spool/mail/%u"; - description = '' - Location that dovecot will use for mail folders. Dovecot mail_location option. - ''; - }; - - mailUser = mkOption { - type = types.nullOr types.str; - default = null; - description = "Default user to store mail for virtual users."; - }; - - mailGroup = mkOption { - type = types.nullOr types.str; - default = null; - description = "Default group to store mail for virtual users."; + includeFiles = mkOption { + type = types.listOf types.path; + default = [ ]; + description = "Files to include in the Dovecot config file using !include directives."; + example = [ "/foo/bar/extraDovecotConfig.conf" ]; }; createMailUser = mkEnableOption '' automatically creating the user - given in {option}`services.dovecot.user` and the group - given in {option}`services.dovecot.group`'' + given in {option}`services.dovecot2.settings.mail_uid` and the group + given in {option}`services.dovecot2.settings.mail_gid`'' // { default = true; }; - sslCACert = mkOption { - type = types.nullOr types.str; - default = null; - description = "Path to the server's CA certificate key."; - }; - - sslServerCert = mkOption { - type = types.nullOr types.str; - default = null; - description = "Path to the server's public key."; - }; - - sslServerKey = mkOption { - type = types.nullOr types.str; - default = null; - description = "Path to the server's private key."; - }; - enablePAM = mkEnableOption "creating a own Dovecot PAM service and configure PAM user logins" // { default = true; }; - enableDHE = mkEnableOption "ssl_dh and generation of primes for the key exchange" // { - default = true; - }; - showPAMFailure = mkEnableOption "showing the PAM failure message on authentication error (useful for OTPW)"; - mailboxes = mkOption { - type = - with types; - coercedTo (listOf unspecified) ( - list: - listToAttrs ( - map (entry: { - name = entry.name; - value = removeAttrs entry [ "name" ]; - }) list - ) - ) (attrsOf (submodule mailboxes)); - default = { }; - example = literalExpression '' - { - Spam = { specialUse = "Junk"; auto = "create"; }; - } - ''; - description = "Configure mailboxes and auto create or subscribe them."; - }; - - enableQuota = mkEnableOption "the dovecot quota service"; - - quotaPort = mkOption { - type = types.str; - default = "12340"; - description = '' - The Port the dovecot quota service binds to. - If using postfix, add check_policy_service inet:localhost:12340 to your smtpd_recipient_restrictions in your postfix config. - ''; - }; - quotaGlobalPerUser = mkOption { - type = types.str; - default = "100G"; - example = "10G"; - description = "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"; @@ -624,36 +565,56 @@ in config = mkIf cfg.enable { security.pam.services.dovecot2 = mkIf cfg.enablePAM { }; - security.dhparams = mkIf (cfg.sslServerCert != null && cfg.enableDHE) { - enable = true; - params.dovecot2 = { }; - }; - services.dovecot2 = { - protocols = - optional cfg.enableImap "imap" ++ optional cfg.enablePop3 "pop3" ++ optional cfg.enableLmtp "lmtp"; - - mailPlugins = mkIf cfg.enableQuota { - globally.enable = [ "quota" ]; - perProtocol.imap.enable = [ "imap_quota" ]; - }; - sieve.plugins = optional (cfg.imapsieve.mailbox != [ ]) "sieve_imapsieve" ++ optional (cfg.sieve.pipeBins != [ ]) "sieve_extprograms"; sieve.globalExtensions = optional (cfg.sieve.pipeBins != [ ]) "vnd.dovecot.pipe"; - pluginSettings = lib.mapAttrs (n: lib.mkDefault) ( + settings = mkMerge [ { - 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; + base_dir = mkDefault baseDir; + sendmail_path = mkDefault "/run/wrappers/bin/sendmail"; + mail_plugin_dir = mkDefault "/run/current-system/sw/lib/dovecot/modules"; + + default_internal_user = mkDefault cfg.user; + default_internal_group = mkDefault cfg.group; + + maildir_copy_with_hardlinks = mkDefault true; + pop3_uidl_format = mkDefault "%08Xv%08Xu"; + + auth_mechanisms = mkDefault "plain login"; + "service auth" = { + user = "root"; + }; + + mail_location = mkDefault "maildir:/var/spool/mail/%u"; + + passdb = mkIf cfg.enablePAM (mkDefault { + driver = "pam"; + args = "${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2"; + }); + + userdb = mkIf cfg.enablePAM (mkDefault { + driver = "passwd"; + }); + + mail_plugins = mkDefault "$mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}"; + + plugin = allSieveSettings; } - // sieveScriptSettings - // imapSieveMailboxSettings - ); + (mkIf (cfg.mailPlugins.perProtocol != { } || cfg.mailPlugins.globally.enable != [ ]) ( + listToAttrs ( + map (m: { + name = "protocol ${elemAt (splitString "." m.name) 0}"; + value.mail_plugins = "$mail_plugins ${ + concatStringsSep " " (m.value.enable ++ cfg.mailPlugins.globally.enable) + }"; + }) (attrsToList cfg.mailPlugins.perProtocol) + ) + )) + ]; }; users.users = { @@ -670,12 +631,14 @@ in group = cfg.group; }; } - // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) { - ${cfg.mailUser} = { + // optionalAttrs (cfg.settings.mail_uid or null != null && cfg.createMailUser) { + ${cfg.settings.mail_uid} = { description = "Virtual Mail User"; isSystemUser = true; } - // optionalAttrs (cfg.mailGroup != null) { group = cfg.mailGroup; }; + // optionalAttrs (cfg.settings.mail_gid or null != null) { + group = cfg.settings.mail_gid; + }; }; users.groups = { @@ -684,8 +647,8 @@ in // optionalAttrs (cfg.group == "dovecot2") { dovecot2.gid = config.ids.gids.dovecot2; } - // optionalAttrs (cfg.createMailUser && cfg.mailGroup != null) { - ${cfg.mailGroup} = { }; + // optionalAttrs (cfg.settings.mail_gid or null != null && cfg.createMailUser) { + ${cfg.settings.mail_gid} = { }; }; environment.etc."dovecot/dovecot.conf".source = cfg.configFile; @@ -704,8 +667,8 @@ in startLimitIntervalSec = 60; # 1 min serviceConfig = { Type = "notify"; - ExecStart = "${dovecotPkg}/sbin/dovecot -F"; - ExecReload = "${dovecotPkg}/sbin/doveadm reload"; + ExecStart = "${lib.getExe cfg.package} -F"; + ExecReload = "${lib.getExe' cfg.package "doveadm"} reload"; CapabilityBoundingSet = [ "CAP_CHOWN" @@ -773,7 +736,9 @@ in ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}' '') cfg.sieve.scripts )} - chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve' + ${optionalString ( + cfg.settings ? mail_uid && cfg.settings.mail_uid != null && cfg.settings.mail_gid != null + ) "chown -R '${cfg.settings.mail_uid}:${cfg.settings.mail_gid}' '${stateDir}/sieve'"} '' + optionalString (cfg.imapsieve.mailbox != [ ]) '' mkdir -p ${stateDir}/imapsieve/{before,after} @@ -791,29 +756,22 @@ in ) cfg.imapsieve.mailbox} ${optionalString ( - cfg.mailUser != null && cfg.mailGroup != null - ) "chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/imapsieve'"} + cfg.settings ? mail_uid && cfg.settings.mail_uid != null && cfg.settings.mail_gid != null + ) "chown -R '${cfg.settings.mail_uid}:${cfg.settings.mail_gid}' '${stateDir}/imapsieve'"} ''; }; - environment.systemPackages = [ dovecotPkg ]; - - warnings = warnAboutExtraConfigCollisions; + environment.systemPackages = [ cfg.package ]; assertions = [ - { - assertion = - (cfg.sslServerCert == null) == (cfg.sslServerKey == null) - && (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null)); - message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto"; - } { assertion = cfg.showPAMFailure -> cfg.enablePAM; message = "dovecot is configured with showPAMFailure while enablePAM is disabled"; } { - assertion = cfg.sieve.scripts != { } -> (cfg.mailUser != null && cfg.mailGroup != null); - message = "dovecot requires mailUser and mailGroup to be set when `sieve.scripts` is set"; + assertion = + cfg.sieve.scripts != { } -> (cfg.settings.mail_uid != null && cfg.settings.mail_gid != null); + message = "dovecot requires settings.mail_uid and settings.mail_gid to be set when `sieve.scripts` is set"; } { assertion = config.systemd.services ? dovecot2 == false; @@ -822,8 +780,11 @@ in ''; } ]; - }; - meta.maintainers = [ lib.maintainers.dblsaiko ]; + meta.maintainers = with lib.maintainers; [ + dblsaiko + jappie3 + prince213 + ]; } diff --git a/nixos/tests/dovecot.nix b/nixos/tests/dovecot.nix index 83b3781c773d..b7291114ccee 100644 --- a/nixos/tests/dovecot.nix +++ b/nixos/tests/dovecot.nix @@ -8,12 +8,14 @@ services.postfix.enable = true; services.dovecot2 = { enable = true; - protocols = [ - "imap" - "pop3" - ]; - mailUser = "vmail"; - mailGroup = "vmail"; + settings = { + protocols = [ + "imap" + "pop3" + ]; + mail_uid = "vmail"; + mail_gid = "vmail"; + }; }; environment.systemPackages = let From d49db5bd9653c04dab56e3b2255e518c381f3b1d Mon Sep 17 00:00:00 2001 From: Jappie3 Date: Sat, 20 Dec 2025 16:16:52 +0100 Subject: [PATCH 3/8] nixos/dovecot: add compatibility for Dovecot 2.4 Co-authored-by: Katalin Rebhan --- nixos/modules/services/mail/dovecot.nix | 190 +++++++++++++++++++++--- 1 file changed, 170 insertions(+), 20 deletions(-) diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix index c4e71daff438..db183d26382d 100644 --- a/nixos/modules/services/mail/dovecot.nix +++ b/nixos/modules/services/mail/dovecot.nix @@ -43,6 +43,8 @@ let splitString traceSeq types + versionAtLeast + versionOlder ; cfg = config.services.dovecot2; @@ -50,6 +52,8 @@ let baseDir = "/run/dovecot2"; stateDir = "/var/lib/dovecot"; + sievec = lib.getExe' cfg.package.passthru.dovecot_pigeonhole "sievec"; + sieveScriptSettings = mapAttrs' ( to: _: nameValuePair "sieve_${to}" "${stateDir}/sieve/${to}" ) cfg.sieve.scripts; @@ -89,9 +93,14 @@ let ); allSieveSettings = mkMerge [ - imapSieveMailboxSettings sieveScriptSettings { + sieve_pipe_bin_dir = + mkIf (cfg.sieve.pipeBins != [ ]) # . + sievePipeBinScriptDirectory; + } + (mkIf isPre24 imapSieveMailboxSettings) + (mkIf isPre24 { sieve_plugins = mkIf (cfg.sieve.plugins != [ ]) # . (concatStringsSep " " cfg.sieve.plugins); @@ -101,10 +110,7 @@ let sieve_global_extensions = mkIf (cfg.sieve.globalExtensions != [ ]) # . (concatMapStringsSep " " (el: "+${el}") cfg.sieve.globalExtensions); - sieve_pipe_bin_dir = - mkIf (cfg.sieve.pipeBins != [ ]) # . - sievePipeBinScriptDirectory; - } + }) ]; yesOrNo = v: if v then "yes" else "no"; @@ -153,10 +159,23 @@ let else throw (traceSeq v "services.dovecot2.settings: unexpected type"); - doveConf = concatStringsSep "\n" ( - optionals (cfg.includeFiles != [ ]) (map (f: "!include ${f}") cfg.includeFiles) - ++ flatten (mapAttrsToList (formatKeyValue "") cfg.settings) - ); + doveConf = + let + configVersion = cfg.settings.dovecot_config_version or null; + storageVersion = cfg.settings.dovecot_storage_version or null; + remainingSettings = builtins.removeAttrs cfg.settings [ + "dovecot_config_version" + "dovecot_storage_version" + ]; + in + concatStringsSep "\n" ( + optional (configVersion != null) (formatKeyValue "" "dovecot_config_version" configVersion) + ++ optional (storageVersion != null) (formatKeyValue "" "dovecot_storage_version" storageVersion) + ++ optionals (cfg.includeFiles != [ ]) (map (f: "!include ${f}") cfg.includeFiles) + ++ flatten (mapAttrsToList (formatKeyValue "") remainingSettings) + ); + + isPre24 = versionOlder cfg.package.version "2.4"; in { imports = [ @@ -180,6 +199,31 @@ in "dovecot2" "enableLmtp" ] "Set 'services.dovecot2.settings.protocols.lmtp = true/false;' instead.") + (mkRemovedOptionModule [ + "services" + "dovecot2" + "sslServerCert" + ] "Use `settings.ssl_cert` for Dovecot 2.3, `settings.ssl_server_cert_file` for 2.4.") + (mkRemovedOptionModule [ + "services" + "dovecot2" + "sslServerKey" + ] "Use `settings.ssl_key` for Dovecot 2.3, `settings.ssl_server_key_file` for 2.4.") + (mkRemovedOptionModule [ + "services" + "dovecot2" + "sslCACert" + ] "Use `settings.ssl_ca` for Dovecot 2.3, `settings.ssl_server_ca_file` for 2.4.") + (mkRemovedOptionModule [ + "services" + "dovecot2" + "mailLocation" + ] "Use `settings.mail_location` for Dovecot 2.3, `settings.mail_path` for 2.4.") + (mkRemovedOptionModule [ + "services" + "dovecot2" + "enableDHE" + ] "Use ECDHE instead, or use recommended parameters from RFC7919.") (mkRenamedOptionModule [ "services" "dovecot2" "sieveScripts" ] [ "services" "dovecot2" "sieve" "scripts" ] @@ -223,6 +267,9 @@ in enable = mkEnableOption "the dovecot 2.x POP3/IMAP server"; package = mkPackageOption pkgs "dovecot" { } // { + default = + if versionAtLeast config.system.stateVersion "26.05" then pkgs.dovecot else pkgs.dovecot_2_3; + defaultText = lib.literalExpression ''if versionAtLeast config.system.stateVersion "26.05" then pkgs.dovecot else pkgs.dovecot_2_3''; }; settings = mkOption { @@ -440,7 +487,13 @@ in imapsieve.mailbox = mkOption { default = [ ]; - description = "Configure Sieve filtering rules on IMAP actions"; + description = '' + Configure Sieve filtering rules on IMAP actions + + ::: {.note} + This option is no longer used starting from Dovecot version 2.4. + ::: + ''; type = types.listOf ( types.submodule ( { config, ... }: @@ -566,13 +619,15 @@ in security.pam.services.dovecot2 = mkIf cfg.enablePAM { }; services.dovecot2 = { - sieve.plugins = + sieve.plugins = mkIf isPre24 ( optional (cfg.imapsieve.mailbox != [ ]) "sieve_imapsieve" - ++ optional (cfg.sieve.pipeBins != [ ]) "sieve_extprograms"; + ++ optional (cfg.sieve.pipeBins != [ ]) "sieve_extprograms" + ); - sieve.globalExtensions = optional (cfg.sieve.pipeBins != [ ]) "vnd.dovecot.pipe"; + sieve.globalExtensions = mkIf isPre24 (optional (cfg.sieve.pipeBins != [ ]) "vnd.dovecot.pipe"); settings = mkMerge [ + # options shared between 2.3 and 2.4 { base_dir = mkDefault baseDir; sendmail_path = mkDefault "/run/wrappers/bin/sendmail"; @@ -582,13 +637,16 @@ in default_internal_group = mkDefault cfg.group; maildir_copy_with_hardlinks = mkDefault true; - pop3_uidl_format = mkDefault "%08Xv%08Xu"; + pop3_uidl_format = if !isPre24 then mkDefault "%{uidvalidity}%{uid}" else mkDefault "%08Xv%08Xu"; auth_mechanisms = mkDefault "plain login"; "service auth" = { user = "root"; }; - + } + # these options differ quite a bit between 2.3 and 2.4, which is why they were split up here + # for pre-2.4: + (mkIf isPre24 { mail_location = mkDefault "maildir:/var/spool/mail/%u"; passdb = mkIf cfg.enablePAM (mkDefault { @@ -603,8 +661,8 @@ in mail_plugins = mkDefault "$mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}"; plugin = allSieveSettings; - } - (mkIf (cfg.mailPlugins.perProtocol != { } || cfg.mailPlugins.globally.enable != [ ]) ( + }) + (mkIf (isPre24 && cfg.mailPlugins.perProtocol != { } || cfg.mailPlugins.globally.enable != [ ]) ( listToAttrs ( map (m: { name = "protocol ${elemAt (splitString "." m.name) 0}"; @@ -614,6 +672,32 @@ in }) (attrsToList cfg.mailPlugins.perProtocol) ) )) + # for 2.4: + (mkIf (!isPre24) { + mail_driver = mkDefault "maildir"; + mail_path = mkDefault "/var/spool/mail/%{user}"; + + sieve_script_bin_path = "/tmp/dovecot-%{user|username|lower}"; + + "passdb pam" = mkIf cfg.enablePAM (mkDefault { + driver = "pam"; + service_name = "dovecot2"; + failure_show_msg = mkIf cfg.showPAMFailure true; + }); + + "userdb passwd" = mkIf cfg.enablePAM (mkDefault { + driver = "passwd"; + }); + + sieve_plugins = mkIf (cfg.sieve.pipeBins != [ ]) { + "sieve_extprograms" = true; + }; + + sieve_global_extensions = mkIf (cfg.sieve.pipeBins != [ ]) { + "vnd.dovecot.pipe" = true; + }; + }) + (mkIf (!isPre24) allSieveSettings) ]; }; @@ -733,7 +817,7 @@ in else cp -p '${from}' '${stateDir}/sieve/${to}' fi - ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}' + ${sievec} '${stateDir}/sieve/${to}' '') cfg.sieve.scripts )} ${optionalString ( @@ -747,11 +831,11 @@ in el: optionalString (el.before != null) '' cp -p ${el.before} ${stateDir}/imapsieve/before/${baseNameOf el.before} - ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/imapsieve/before/${baseNameOf el.before}' + ${sievec} '${stateDir}/imapsieve/before/${baseNameOf el.before}' '' + optionalString (el.after != null) '' cp -p ${el.after} ${stateDir}/imapsieve/after/${baseNameOf el.after} - ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/imapsieve/after/${baseNameOf el.after}' + ${sievec} '${stateDir}/imapsieve/after/${baseNameOf el.after}' '' ) cfg.imapsieve.mailbox} @@ -763,7 +847,73 @@ in environment.systemPackages = [ cfg.package ]; + warnings = optional isPre24 '' + While Dovecot 2.3 is not yet deprecated or EOL, + there is a newer version available in Nixpkgs (Dovecot 2.4). + Check https://doc.dovecot.org/latest/installation/upgrade/2.3-to-2.4.html + before upgrading. + ''; + assertions = [ + { + assertion = isPre24 || cfg.settings.dovecot_config_version != null; + message = '' + services.dovecot2: Since Dovecot 2.4, the option 'services.dovecot2.settings.dovecot_config_version' must be explicitly set. + To retain compatibility with future updates, set the following and manually update as needed. + + services.dovecot2.settings.dovecot_config_version = "${cfg.package.version}"; + + Alternatively, you can automatically update to newer versions of the configuration format, which might break compatibility with future updates. + + services.dovecot2.settings.dovecot_config_version = config.services.dovecot2.package.version; + + See . + ''; + } + { + assertion = isPre24 || cfg.settings.dovecot_storage_version != null; + message = '' + services.dovecot2: Since Dovecot 2.4, the option 'services.dovecot2.settings.dovecot_storage_version' must be explicitly set. + Set it to the oldest version the storage should stay compatible with, for example the following for the currently selected version. + + services.dovecot2.settings.dovecot_storage_version = "${cfg.package.version}"; + + See . + ''; + } + { + assertion = isPre24 || cfg.imapsieve.mailbox == [ ]; + message = "since Dovecot 2.4, the `imapsieve.mailbox` option is no longer used in the NixOS module, please use the `settings` option instead."; + } + { + assertion = isPre24 || cfg.sieve.extensions == [ ]; + message = '' + services.dovecot2: Since Dovecot 2.4, the option 'services.dovecot2.sieve.extensions' is no longer valid, but it is set. + Set 'services.dovecot2.settings.sieve_extensions' instead. + See . + ''; + } + { + assertion = isPre24 || cfg.sieve.globalExtensions == [ ]; + message = '' + services.dovecot2: Since Dovecot 2.4, the option 'services.dovecot2.sieve.globalExtensions' is no longer valid, but it is set. + Set 'services.dovecot2.settings.sieve_global_extensions' instead. + See . + ''; + } + { + assertion = + isPre24 + || + # this is the default value for cfg.mailPlugins + cfg.mailPlugins == { + globally = { + enable = [ ]; + }; + perProtocol = { }; + }; + message = "since Dovecot 2.4, the `mailPlugins` option is no longer used in the NixOS module, please use the `settings` option instead."; + } { assertion = cfg.showPAMFailure -> cfg.enablePAM; message = "dovecot is configured with showPAMFailure while enablePAM is disabled"; From 8df2a9cbab64ed655a04dacceaeacbac022984f1 Mon Sep 17 00:00:00 2001 From: Katalin Rebhan Date: Wed, 25 Mar 2026 11:34:42 +0100 Subject: [PATCH 4/8] nixos/dovecot: define module defaults as options --- nixos/modules/services/mail/dovecot.nix | 501 +++++++++++++++++++----- 1 file changed, 406 insertions(+), 95 deletions(-) diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix index db183d26382d..9de696b3045b 100644 --- a/nixos/modules/services/mail/dovecot.nix +++ b/nixos/modules/services/mail/dovecot.nix @@ -92,27 +92,6 @@ let }) cfg.sieve.pipeBins ); - allSieveSettings = mkMerge [ - sieveScriptSettings - { - sieve_pipe_bin_dir = - mkIf (cfg.sieve.pipeBins != [ ]) # . - sievePipeBinScriptDirectory; - } - (mkIf isPre24 imapSieveMailboxSettings) - (mkIf isPre24 { - sieve_plugins = - mkIf (cfg.sieve.plugins != [ ]) # . - (concatStringsSep " " cfg.sieve.plugins); - sieve_extensions = - mkIf (cfg.sieve.extensions != [ ]) # . - (concatMapStringsSep " " (el: "+${el}") cfg.sieve.extensions); - sieve_global_extensions = - mkIf (cfg.sieve.globalExtensions != [ ]) # . - (concatMapStringsSep " " (el: "+${el}") cfg.sieve.globalExtensions); - }) - ]; - yesOrNo = v: if v then "yes" else "no"; toOption = @@ -138,7 +117,7 @@ let # paths -> copy to store # derivations -> just use output path instead of looping over the attrs toOption indent n "${v}" - else if isAttrs v then + else if isAttrs v && v ? _section then let sectionType = v._section.type; sectionName = v._section.name; @@ -156,6 +135,14 @@ let ++ (mapAttrsToList (formatKeyValue "${indent} ") (removeAttrs v [ "_section" ])) ++ [ "${indent}}" ] ) + else if isAttrs v then + concatStringsSep "\n" ( + [ + "${indent}${n} {" + ] + ++ (mapAttrsToList (formatKeyValue "${indent} ") v) + ++ [ "${indent}}" ] + ) else throw (traceSeq v "services.dovecot2.settings: unexpected type"); @@ -228,6 +215,12 @@ in [ "services" "dovecot2" "sieveScripts" ] [ "services" "dovecot2" "sieve" "scripts" ] ) + (mkRemovedOptionModule [ + "services" + "dovecot2" + "sieve" + "plugins" + ] "Set 'services.dovecot2.settings.plugin.sieve_plugins' instead.") (mkRenamedOptionModule [ "services" "dovecot2" "mailUser" ] [ "services" "dovecot2" "settings" "mail_uid" ] @@ -281,6 +274,7 @@ in path bool int + listOf nonEmptyListOf nonEmptyStr nullOr @@ -290,7 +284,8 @@ in ; inherit (lib.lists) last dropEnd; - section = submodule ( + sectionBase = + fixed: { name, options, ... }: let # if the current name is a list (matches '[definition .*]') -> get @@ -313,18 +308,30 @@ in description = "Section type, mandatory for every section."; type = nonEmptyStr; default = typeDefault; + + readOnly = fixed; + internal = fixed; }; name = mkOption { description = "Section name, comes after section type & is optional in some cases."; type = nullOr nonEmptyStr; default = nameDefault; + + readOnly = fixed; + internal = fixed; }; }; }; freeformType = attrsOf valueType; - } - ); + }; + section = submodule (sectionBase false); + fixedSectionWith = + extraModule: + submodule [ + (sectionBase true) + extraModule + ]; primitiveType = oneOf [ int @@ -344,8 +351,342 @@ in // { description = "Dovecot config value"; }; + + booleanList = oneOf [ + (attrsOf bool) + (listOf str) + ]; + + toplevel = submodule { + options = { + base_dir = mkOption { + default = baseDir; + description = '' + The base directory in which Dovecot should store runtime data. + + See . + ''; + type = path; + }; + + sendmail_path = mkOption { + default = "/run/wrappers/bin/sendmail"; + description = '' + The binary to use for sending email. + + See . + ''; + type = path; + }; + + mail_plugin_dir = mkOption { + default = "/run/current-system/sw/lib/dovecot/modules"; + description = '' + The directory in which to search for Dovecot mail plugins. + + See . + ''; + type = path; + }; + + default_internal_user = mkOption { + default = "dovecot2"; + description = '' + Define the default internal user. + + See . + ''; + type = str; + }; + + default_internal_group = mkOption { + default = "dovecot2"; + description = '' + Define the default internal group. + + See . + ''; + type = str; + }; + + maildir_copy_with_hardlinks = mkOption { + default = true; + description = '' + If enabled, copying of a message is done with hard links whenever possible. + + See . + ''; + type = bool; + }; + + auth_mechanisms = mkOption { + default = [ + "plain" + "login" + ]; + description = '' + Here you can supply a space-separated list of the authentication mechanisms you wish to use. + + See . + ''; + type = booleanList; + }; + + "passdb pam" = mkOption { + default = null; + description = '' + Configuration for the PAM password database. + + See . + ''; + type = nullOr (fixedSectionWith { + options = { + driver = mkOption { + default = if isPre24 then "pam" else null; + defaultText = literalExpression ''if isPre24 then "pam" else null''; + description = '' + The driver used for this password database. + + See . + ''; + type = nullOr str; + }; + + args = mkOption { + # set below in config + default = null; + defaultText = ''if isPre24 then [ "dovecot2" ] else null''; + description = '' + Arguments for the passdb backend. + + This option is exclusive to Dovecot 2.3. + + See . + ''; + type = nullOr (listOf str); + }; + + service_name = mkOption { + default = if isPre24 then null else "dovecot2"; + defaultText = literalExpression ''if isPre24 then null else "dovecot2"''; + description = '' + The PAM service name to be used with the pam passdb. + + This option is exclusive to Dovecot 2.4. + + See . + ''; + type = nullOr str; + }; + + failure_show_msg = mkOption { + default = if isPre24 then null else cfg.showPAMFailure; + defaultText = literalExpression "if isPre24 then null else config.services.dovecot2.showPAMFailure"; + description = '' + Replace the default "Authentication failed" reply with PAM's failure. + + This option is exclusive to Dovecot 2.4. + + See . + ''; + type = nullOr bool; + }; + }; + }); + }; + + "userdb passwd" = mkOption { + default = null; + description = '' + Configuration for the Passwd user database. + + See . + ''; + type = nullOr (fixedSectionWith { + options = { + driver = mkOption { + default = if isPre24 then "passwd" else null; + defaultText = literalExpression ''if isPre24 then "passwd" else null''; + description = '' + The driver used for this user database. + + See . + ''; + type = nullOr str; + }; + }; + }); + }; + + # 2.3-only options + + mail_location = mkOption { + default = if isPre24 then "maildir:/var/spool/mail/%u" else null; + defaultText = literalExpression '' + if isPre24 + then "maildir:/var/spool/mail/%u" + else null + ''; + description = '' + This setting indicates the location for users’ mailboxes. + + This option is exclusive to Dovecot 2.3. + + See . + ''; + type = nullOr str; + }; + + plugin = mkOption { + default = null; + description = "Plugin settings. This option is exclusive to Dovecot 2.3."; + type = nullOr (fixedSectionWith { + options = { + sieve_pipe_bin_dir = mkOption { + default = null; + description = '' + Points to a directory where the plugin looks for programs (shell scripts) to execute directly and pipe messages to for the *vnd.dovecot.pipe* extension. + + This option is exclusive to Dovecot 2.3. + + See . + ''; + type = nullOr path; + }; + + sieve_plugins = mkOption { + default = null; + description = '' + List of Sieve plugins to load. + + This option is exclusive to Dovecot 2.3. + + See . + ''; + type = nullOr (listOf str); + }; + + sieve_extensions = mkOption { + default = null; + description = '' + The Sieve language extensions available to users. + + This option is exclusive to Dovecot 2.3. + + See . + ''; + type = nullOr (listOf str); + }; + + sieve_global_extensions = mkOption { + default = null; + description = '' + Which Sieve language extensions are **only** available in global scripts. + + This option is exclusive to Dovecot 2.3. + + See . + ''; + type = nullOr (listOf str); + }; + }; + }); + }; + + # 2.4-only options + + mail_driver = mkOption { + default = if isPre24 then null else "maildir"; + defaultText = literalExpression '' + if isPre24 + then null + else "maildir" + ''; + description = '' + One of the mailbox formats described at [Mailbox Formats](https://doc.dovecot.org/latest/core/config/mailbox_formats/overview.html#mailbox-formats). + + This option is exclusive to Dovecot 2.4. + + See . + ''; + type = nullOr str; + }; + + mail_path = mkOption { + default = if isPre24 then null else "/var/spool/mail/%{user}"; + defaultText = literalExpression '' + if isPre24 + then null + else "/var/spool/mail/%{user}" + ''; + description = '' + Path to a directory where the mail is stored. + + This option is exclusive to Dovecot 2.4. + + See . + ''; + type = nullOr str; + }; + + sieve_script_bin_path = mkOption { + default = if isPre24 then null else "/tmp/dovecot-%{user|username|lower}"; + defaultText = literalExpression '' + if isPre24 + then null + else "/tmp/dovecot-%{user|username|lower}" + ''; + description = '' + Points to the directory where the compiled binaries for this script location are stored. This directory is created automatically if possible. + + This option is exclusive to Dovecot 2.4. + + See . + ''; + type = nullOr str; + }; + + sieve_pipe_bin_dir = mkOption { + default = null; + description = '' + Points to a directory where the plugin looks for programs (shell scripts) to execute directly and pipe messages to for the *vnd.dovecot.pipe* extension. + + This option is exclusive to Dovecot 2.4. + + See . + ''; + type = nullOr path; + }; + + sieve_plugins = mkOption { + default = null; + description = '' + List of Sieve plugins to load. + + This option is exclusive to Dovecot 2.4. + + See . + ''; + type = nullOr booleanList; + }; + + sieve_global_extensions = mkOption { + default = null; + description = '' + Which Sieve language extensions are **only** available in global scripts. + + This option is exclusive to Dovecot 2.4. + + See . + ''; + type = nullOr booleanList; + }; + }; + + freeformType = attrsOf valueType; + }; in - attrsOf valueType; + toplevel; description = '' Dovecot configuration, see for all available options. @@ -394,18 +735,6 @@ in }; }; - user = mkOption { - type = types.str; - default = "dovecot2"; - description = "Dovecot user name."; - }; - - group = mkOption { - type = types.str; - default = "dovecot2"; - description = "Dovecot group name."; - }; - mailPlugins = let plugins = @@ -570,13 +899,6 @@ in }; sieve = { - plugins = mkOption { - default = [ ]; - example = [ "sieve_extprograms" ]; - description = "Sieve plugins to load"; - type = types.listOf types.str; - }; - extensions = mkOption { default = [ ]; description = "Sieve extensions for use in user scripts"; @@ -619,48 +941,41 @@ in security.pam.services.dovecot2 = mkIf cfg.enablePAM { }; services.dovecot2 = { - sieve.plugins = mkIf isPre24 ( - optional (cfg.imapsieve.mailbox != [ ]) "sieve_imapsieve" - ++ optional (cfg.sieve.pipeBins != [ ]) "sieve_extprograms" - ); - sieve.globalExtensions = mkIf isPre24 (optional (cfg.sieve.pipeBins != [ ]) "vnd.dovecot.pipe"); settings = mkMerge [ - # options shared between 2.3 and 2.4 - { - base_dir = mkDefault baseDir; - sendmail_path = mkDefault "/run/wrappers/bin/sendmail"; - mail_plugin_dir = mkDefault "/run/current-system/sw/lib/dovecot/modules"; - - default_internal_user = mkDefault cfg.user; - default_internal_group = mkDefault cfg.group; - - maildir_copy_with_hardlinks = mkDefault true; - pop3_uidl_format = if !isPre24 then mkDefault "%{uidvalidity}%{uid}" else mkDefault "%08Xv%08Xu"; - - auth_mechanisms = mkDefault "plain login"; - "service auth" = { - user = "root"; - }; - } # these options differ quite a bit between 2.3 and 2.4, which is why they were split up here # for pre-2.4: (mkIf isPre24 { - mail_location = mkDefault "maildir:/var/spool/mail/%u"; + "passdb pam" = mkIf cfg.enablePAM { + args = mkMerge ( + optional cfg.showPAMFailure "failure_show_msg=yes" ++ singleton (lib.mkAfter [ "dovecot2" ]) + ); + }; - passdb = mkIf cfg.enablePAM (mkDefault { - driver = "pam"; - args = "${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2"; - }); - - userdb = mkIf cfg.enablePAM (mkDefault { - driver = "passwd"; - }); + "userdb passwd" = mkIf cfg.enablePAM { }; mail_plugins = mkDefault "$mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}"; - plugin = allSieveSettings; + plugin = mkMerge [ + sieveScriptSettings + imapSieveMailboxSettings + { + sieve_plugins = mkMerge [ + (mkIf (cfg.imapsieve.mailbox != [ ]) [ "sieve_imapsieve" ]) + (mkIf (cfg.sieve.pipeBins != [ ]) [ "sieve_extprograms" ]) + ]; + sieve_pipe_bin_dir = + mkIf (cfg.sieve.pipeBins != [ ]) # . + sievePipeBinScriptDirectory; + sieve_extensions = + mkIf (cfg.sieve.extensions != [ ]) # . + (map (el: "+${el}") cfg.sieve.extensions); + sieve_global_extensions = + mkIf (cfg.sieve.globalExtensions != [ ]) # . + (map (el: "+${el}") cfg.sieve.globalExtensions); + } + ]; }) (mkIf (isPre24 && cfg.mailPlugins.perProtocol != { } || cfg.mailPlugins.globally.enable != [ ]) ( listToAttrs ( @@ -674,20 +989,8 @@ in )) # for 2.4: (mkIf (!isPre24) { - mail_driver = mkDefault "maildir"; - mail_path = mkDefault "/var/spool/mail/%{user}"; - - sieve_script_bin_path = "/tmp/dovecot-%{user|username|lower}"; - - "passdb pam" = mkIf cfg.enablePAM (mkDefault { - driver = "pam"; - service_name = "dovecot2"; - failure_show_msg = mkIf cfg.showPAMFailure true; - }); - - "userdb passwd" = mkIf cfg.enablePAM (mkDefault { - driver = "passwd"; - }); + "passdb pam" = mkIf cfg.enablePAM { }; + "userdb passwd" = mkIf cfg.enablePAM { }; sieve_plugins = mkIf (cfg.sieve.pipeBins != [ ]) { "sieve_extprograms" = true; @@ -696,8 +999,9 @@ in sieve_global_extensions = mkIf (cfg.sieve.pipeBins != [ ]) { "vnd.dovecot.pipe" = true; }; + + sieve_pipe_bin_dir = mkIf (cfg.sieve.pipeBins != [ ]) sievePipeBinScriptDirectory; }) - (mkIf (!isPre24) allSieveSettings) ]; }; @@ -708,11 +1012,11 @@ in group = "dovenull"; }; } - // optionalAttrs (cfg.user == "dovecot2") { + // optionalAttrs (cfg.settings.default_internal_user == "dovecot2") { dovecot2 = { uid = config.ids.uids.dovecot2; description = "Dovecot user"; - group = cfg.group; + group = cfg.settings.default_internal_group; }; } // optionalAttrs (cfg.settings.mail_uid or null != null && cfg.createMailUser) { @@ -728,7 +1032,7 @@ in users.groups = { dovenull.gid = config.ids.gids.dovenull2; } - // optionalAttrs (cfg.group == "dovecot2") { + // optionalAttrs (cfg.settings.default_internal_group == "dovecot2") { dovecot2.gid = config.ids.gids.dovecot2; } // optionalAttrs (cfg.settings.mail_gid or null != null && cfg.createMailUser) { @@ -885,6 +1189,13 @@ in assertion = isPre24 || cfg.imapsieve.mailbox == [ ]; message = "since Dovecot 2.4, the `imapsieve.mailbox` option is no longer used in the NixOS module, please use the `settings` option instead."; } + { + assertion = isPre24 || cfg.sieve.scripts == { }; + message = '' + services.dovecot2: Since Dovecot 2.4, the option 'services.dovecot2.sieve.scripts' is no longer valid, but it is set. + See . + ''; + } { assertion = isPre24 || cfg.sieve.extensions == [ ]; message = '' From 0ad86118b7b977437d136d104fe3b5e6f1a906e4 Mon Sep 17 00:00:00 2001 From: Sizhe Zhao Date: Mon, 9 Feb 2026 18:26:18 +0800 Subject: [PATCH 5/8] nixosTests.dovecot: use matching dovecot_pigeonhole --- nixos/tests/dovecot.nix | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nixos/tests/dovecot.nix b/nixos/tests/dovecot.nix index b7291114ccee..2d1e481a7f38 100644 --- a/nixos/tests/dovecot.nix +++ b/nixos/tests/dovecot.nix @@ -2,7 +2,10 @@ name = "dovecot"; nodes.machine = - { pkgs, ... }: + { config, pkgs, ... }: + let + dovecot = config.services.dovecot2.package; + in { imports = [ common/user-account.nix ]; services.postfix.enable = true; @@ -33,7 +36,7 @@ sendTestMailViaDeliveryAgent = pkgs.writeScriptBin "send-lda" '' #!${pkgs.runtimeShell} - exec ${pkgs.dovecot}/libexec/dovecot/deliver -d bob < Date: Fri, 27 Mar 2026 10:12:25 +0100 Subject: [PATCH 6/8] nixosTests.dovecot: set dovecot_{config,storage}_version --- nixos/tests/dovecot.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/tests/dovecot.nix b/nixos/tests/dovecot.nix index 2d1e481a7f38..e85512602f20 100644 --- a/nixos/tests/dovecot.nix +++ b/nixos/tests/dovecot.nix @@ -12,6 +12,8 @@ services.dovecot2 = { enable = true; settings = { + dovecot_config_version = dovecot.version; + dovecot_storage_version = dovecot.version; protocols = [ "imap" "pop3" From 400aecc29b905b717b4a608bd37406451b74de99 Mon Sep 17 00:00:00 2001 From: Katalin Rebhan Date: Tue, 7 Apr 2026 15:12:17 +0200 Subject: [PATCH 7/8] nixos/dovecot: remove defaults for PAM and mail driver/path --- nixos/modules/services/mail/dovecot.nix | 59 +++---------------------- nixos/tests/dovecot.nix | 3 ++ 2 files changed, 8 insertions(+), 54 deletions(-) diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix index 9de696b3045b..a9dd9531fadf 100644 --- a/nixos/modules/services/mail/dovecot.nix +++ b/nixos/modules/services/mail/dovecot.nix @@ -520,23 +520,6 @@ in # 2.3-only options - mail_location = mkOption { - default = if isPre24 then "maildir:/var/spool/mail/%u" else null; - defaultText = literalExpression '' - if isPre24 - then "maildir:/var/spool/mail/%u" - else null - ''; - description = '' - This setting indicates the location for users’ mailboxes. - - This option is exclusive to Dovecot 2.3. - - See . - ''; - type = nullOr str; - }; - plugin = mkOption { default = null; description = "Plugin settings. This option is exclusive to Dovecot 2.3."; @@ -595,40 +578,6 @@ in # 2.4-only options - mail_driver = mkOption { - default = if isPre24 then null else "maildir"; - defaultText = literalExpression '' - if isPre24 - then null - else "maildir" - ''; - description = '' - One of the mailbox formats described at [Mailbox Formats](https://doc.dovecot.org/latest/core/config/mailbox_formats/overview.html#mailbox-formats). - - This option is exclusive to Dovecot 2.4. - - See . - ''; - type = nullOr str; - }; - - mail_path = mkOption { - default = if isPre24 then null else "/var/spool/mail/%{user}"; - defaultText = literalExpression '' - if isPre24 - then null - else "/var/spool/mail/%{user}" - ''; - description = '' - Path to a directory where the mail is stored. - - This option is exclusive to Dovecot 2.4. - - See . - ''; - type = nullOr str; - }; - sieve_script_bin_path = mkOption { default = if isPre24 then null else "/tmp/dovecot-%{user|username|lower}"; defaultText = literalExpression '' @@ -703,7 +652,11 @@ in submission = true; lmtp = true; }; + mail_driver = "maildir"; + mail_home = "/var/vmail/%{user | domain}/%{user | username}"; + mail_path = "~/mail"; + "namespace inbox" = { inbox = true; separator = "/"; @@ -808,9 +761,7 @@ in default = true; }; - enablePAM = mkEnableOption "creating a own Dovecot PAM service and configure PAM user logins" // { - default = true; - }; + enablePAM = mkEnableOption "creating a own Dovecot PAM service and configure PAM user logins"; showPAMFailure = mkEnableOption "showing the PAM failure message on authentication error (useful for OTPW)"; diff --git a/nixos/tests/dovecot.nix b/nixos/tests/dovecot.nix index e85512602f20..ddc8b6632acc 100644 --- a/nixos/tests/dovecot.nix +++ b/nixos/tests/dovecot.nix @@ -11,9 +11,12 @@ services.postfix.enable = true; services.dovecot2 = { enable = true; + enablePAM = true; settings = { dovecot_config_version = dovecot.version; dovecot_storage_version = dovecot.version; + mail_driver = "maildir"; + mail_path = "${config.services.postfix.settings.main.mail_spool_directory}/%{user}"; protocols = [ "imap" "pop3" From 3fe307bd52b9b8b8f6f4e61b41fe7c66f795fb78 Mon Sep 17 00:00:00 2001 From: Katalin Rebhan Date: Tue, 17 Mar 2026 17:18:14 +0100 Subject: [PATCH 8/8] nixos/dovecot: add release notes --- nixos/doc/manual/release-notes/rl-2605.section.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index 5220d49e71e2..54ba0bc43817 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -276,6 +276,10 @@ See . - For shortlived certificates with a total validty below 10 days renewal will happen after half of the total lifetime has passed +- The module for the Dovecot IMAP server, *services.dovecot*, now uses RFC-42-style settings, exposing a structured interface to write the configuration file. + + Also see the list of available settings for [Dovecot 2.3](https://doc.dovecot.org/2.3/settings/core/) or [2.4](https://doc.dovecot.org/2.4.2/core/summaries/settings.html). + - Cinnamon has been updated to 6.6, please check the [upstream announcement](https://www.linuxmint.com/rel_zena_whatsnew.php) for more details. - Budgie has been updated to 10.10, please check the [upstream announcement](https://buddiesofbudgie.org/blog/budgie-10-10-released) for more details.