1 { config, lib, pkgs, ... }:
4 inherit (lib) attrValues concatMapStringsSep concatStrings
5 concatStringsSep flatten imap1 literalExpression mapAttrsToList
6 mkEnableOption mkIf mkOption mkRemovedOptionModule optional optionalAttrs
7 optionalString singleton types mkRenamedOptionModule nameValuePair
8 mapAttrs' listToAttrs filter;
9 inherit (lib.strings) match;
11 cfg = config.services.dovecot2;
12 dovecotPkg = pkgs.dovecot;
14 baseDir = "/run/dovecot2";
15 stateDir = "/var/lib/dovecot";
17 sieveScriptSettings = mapAttrs' (to: _: nameValuePair "sieve_${to}" "${stateDir}/sieve/${to}") cfg.sieve.scripts;
18 imapSieveMailboxSettings = listToAttrs (flatten (imap1 (idx: el:
20 name = "imapsieve_mailbox${toString idx}_name";
22 } ++ optional (el.from != null) {
23 name = "imapsieve_mailbox${toString idx}_from";
25 } ++ optional (el.causes != []) {
26 name = "imapsieve_mailbox${toString idx}_causes";
27 value = concatStringsSep "," el.causes;
28 } ++ optional (el.before != null) {
29 name = "imapsieve_mailbox${toString idx}_before";
30 value = "file:${stateDir}/imapsieve/before/${baseNameOf el.before}";
31 } ++ optional (el.after != null) {
32 name = "imapsieve_mailbox${toString idx}_after";
33 value = "file:${stateDir}/imapsieve/after/${baseNameOf el.after}";
35 ) cfg.imapsieve.mailbox));
37 mkExtraConfigCollisionWarning = term: ''
38 You referred to ${term} in `services.dovecot2.extraConfig`.
40 Due to gradual transition to structured configuration for plugin configuration, it is possible
41 this will cause your plugin configuration to be ignored.
43 Consider setting `services.dovecot2.pluginSettings.${term}` instead.
46 # Those settings are automatically set based on other parts
48 automaticallySetPluginSettings = [
51 "sieve_global_extensions"
54 ++ (builtins.attrNames sieveScriptSettings)
55 ++ (builtins.attrNames imapSieveMailboxSettings);
57 # The idea is to match everything that looks like `$term =`
58 # but not `# $term something something`
59 # or `# $term = some value` because those are comments.
60 configContainsSetting = lines: term: (match "^[^#]*\b${term}\b.*=" lines) != null;
62 warnAboutExtraConfigCollisions = map mkExtraConfigCollisionWarning (filter (configContainsSetting cfg.extraConfig) automaticallySetPluginSettings);
64 sievePipeBinScriptDirectory = pkgs.linkFarm "sieve-pipe-bins" (map (el: {
65 name = builtins.unsafeDiscardStringContext (baseNameOf el);
67 }) cfg.sieve.pipeBins);
69 dovecotConf = concatStrings [
72 protocols = ${concatStringsSep " " cfg.protocols}
73 sendmail_path = /run/wrappers/bin/sendmail
74 # 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
75 mail_plugins = $mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}
79 concatStringsSep "\n" (
82 protocol ${protocol} {
83 mail_plugins = $mail_plugins ${concatStringsSep " " plugins.enable}
86 ) cfg.mailPlugins.perProtocol
91 if cfg.sslServerCert == null then ''
93 disable_plaintext_auth = no
95 ssl_cert = <${cfg.sslServerCert}
96 ssl_key = <${cfg.sslServerKey}
97 ${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)}
98 ${optionalString cfg.enableDHE ''ssl_dh = <${config.security.dhparams.params.dovecot2.path}''}
99 disable_plaintext_auth = yes
104 default_internal_user = ${cfg.user}
105 default_internal_group = ${cfg.group}
106 ${optionalString (cfg.mailUser != null) "mail_uid = ${cfg.mailUser}"}
107 ${optionalString (cfg.mailGroup != null) "mail_gid = ${cfg.mailGroup}"}
109 mail_location = ${cfg.mailLocation}
111 maildir_copy_with_hardlinks = yes
112 pop3_uidl_format = %08Xv%08Xu
114 auth_mechanisms = plain login
122 optionalString cfg.enablePAM ''
129 args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
135 optionalString (cfg.mailboxes != {}) ''
138 ${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
144 optionalString cfg.enableQuota ''
145 service quota-status {
146 executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
148 port = ${cfg.quotaPort}
154 quota_rule = *:storage=${cfg.quotaGlobalPerUser}
155 quota = count:User quota # per virtual mail user quota
156 quota_status_success = DUNNO
157 quota_status_nouser = DUNNO
158 quota_status_overquota = "552 5.2.2 Mailbox is full"
165 # General plugin settings:
166 # - sieve is mostly generated here, refer to `pluginSettings` to follow
170 ${concatStringsSep "\n" (mapAttrsToList (key: value: " ${key} = ${value}") cfg.pluginSettings)}
177 modulesDir = pkgs.symlinkJoin {
178 name = "dovecot-modules";
179 paths = map (pkg: "${pkg}/lib/dovecot") ([ dovecotPkg ] ++ map (module: module.override { dovecot = dovecotPkg; }) cfg.modules);
182 mailboxConfig = mailbox: ''
183 mailbox "${mailbox.name}" {
184 auto = ${toString mailbox.auto}
185 '' + optionalString (mailbox.autoexpunge != null) ''
186 autoexpunge = ${mailbox.autoexpunge}
187 '' + optionalString (mailbox.specialUse != null) ''
188 special_use = \${toString mailbox.specialUse}
191 mailboxes = { name, ... }: {
194 type = types.strMatching ''[^"]+'';
198 description = "The name of the mailbox.";
201 type = types.enum [ "no" "create" "subscribe" ];
203 example = "subscribe";
204 description = "Whether to automatically create or create and subscribe to the mailbox or not.";
206 specialUse = mkOption {
207 type = types.nullOr (types.enum [ "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash" ]);
210 description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
212 autoexpunge = mkOption {
213 type = types.nullOr types.str;
217 To automatically remove all email from the mailbox which is older than the
226 (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
227 (mkRenamedOptionModule [ "services" "dovecot2" "sieveScripts" ] [ "services" "dovecot2" "sieve" "scripts" ])
230 options.services.dovecot2 = {
231 enable = mkEnableOption "the dovecot 2.x POP3/IMAP server";
233 enablePop3 = mkEnableOption "starting the POP3 listener (when Dovecot is enabled)";
235 enableImap = mkEnableOption "starting the IMAP listener (when Dovecot is enabled)" // { default = true; };
237 enableLmtp = mkEnableOption "starting the LMTP listener (when Dovecot is enabled)";
239 protocols = mkOption {
240 type = types.listOf types.str;
242 description = "Additional listeners to start when Dovecot is enabled.";
247 default = "dovecot2";
248 description = "Dovecot user name.";
253 default = "dovecot2";
254 description = "Dovecot group name.";
257 extraConfig = mkOption {
260 example = "mail_debug = yes";
261 description = "Additional entries to put verbatim into Dovecot's config file.";
266 plugins = hint: types.submodule {
269 type = types.listOf types.str;
271 description = "mail plugins to enable as a list of strings to append to the ${hint} `$mail_plugins` configuration variable";
277 type = with types; submodule {
279 globally = mkOption {
280 description = "Additional entries to add to the mail_plugins variable for all protocols";
281 type = plugins "top-level";
282 example = { enable = [ "virtual" ]; };
283 default = { enable = []; };
285 perProtocol = mkOption {
286 description = "Additional entries to add to the mail_plugins variable, per protocol";
287 type = attrsOf (plugins "corresponding per-protocol");
289 example = { imap = [ "imap_acl" ]; };
293 description = "Additional entries to add to the mail_plugins variable, globally and per protocol";
295 globally.enable = [ "acl" ];
296 perProtocol.imap.enable = [ "imap_acl" ];
298 default = { globally.enable = []; perProtocol = {}; };
301 configFile = mkOption {
302 type = types.nullOr types.path;
304 description = "Config file used for the whole dovecot configuration.";
305 apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf;
308 mailLocation = mkOption {
310 default = "maildir:/var/spool/mail/%u"; /* Same as inbox, as postfix */
311 example = "maildir:~/mail:INBOX=/var/spool/mail/%u";
313 Location that dovecot will use for mail folders. Dovecot mail_location option.
317 mailUser = mkOption {
318 type = types.nullOr types.str;
320 description = "Default user to store mail for virtual users.";
323 mailGroup = mkOption {
324 type = types.nullOr types.str;
326 description = "Default group to store mail for virtual users.";
329 createMailUser = mkEnableOption ''automatically creating the user
330 given in {option}`services.dovecot.user` and the group
331 given in {option}`services.dovecot.group`'' // { default = true; };
334 type = types.listOf types.package;
336 example = literalExpression "[ pkgs.dovecot_pigeonhole ]";
338 Symlinks the contents of lib/dovecot of every given package into
339 /etc/dovecot/modules. This will make the given modules available
340 if a dovecot package with the module_dir patch applied is being used.
344 sslCACert = mkOption {
345 type = types.nullOr types.str;
347 description = "Path to the server's CA certificate key.";
350 sslServerCert = mkOption {
351 type = types.nullOr types.str;
353 description = "Path to the server's public key.";
356 sslServerKey = mkOption {
357 type = types.nullOr types.str;
359 description = "Path to the server's private key.";
362 enablePAM = mkEnableOption "creating a own Dovecot PAM service and configure PAM user logins" // { default = true; };
364 enableDHE = mkEnableOption "ssl_dh and generation of primes for the key exchange" // { default = true; };
366 showPAMFailure = mkEnableOption "showing the PAM failure message on authentication error (useful for OTPW)";
368 mailboxes = mkOption {
369 type = with types; coercedTo
371 (list: listToAttrs (map (entry: { name = entry.name; value = removeAttrs entry ["name"]; }) list))
372 (attrsOf (submodule mailboxes));
374 example = literalExpression ''
376 Spam = { specialUse = "Junk"; auto = "create"; };
379 description = "Configure mailboxes and auto create or subscribe them.";
382 enableQuota = mkEnableOption "the dovecot quota service";
384 quotaPort = mkOption {
388 The Port the dovecot quota service binds to.
389 If using postfix, add check_policy_service inet:localhost:12340 to your smtpd_recipient_restrictions in your postfix config.
392 quotaGlobalPerUser = mkOption {
396 description = "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
400 pluginSettings = mkOption {
401 # types.str does not coerce from packages, like `sievePipeBinScriptDirectory`.
402 type = types.attrsOf (types.oneOf [ types.str types.package ]);
404 example = literalExpression ''
406 sieve = "file:~/sieve;active=~/.dovecot.sieve";
410 Plugin settings for dovecot in general, e.g. `sieve`, `sieve_default`, etc.
412 Some of the other knobs of this module will influence by default the plugin settings, but you
413 can still override any plugin settings.
415 If you override a plugin setting, its value is cleared and you have to copy over the defaults.
419 imapsieve.mailbox = mkOption {
421 description = "Configure Sieve filtering rules on IMAP actions";
422 type = types.listOf (types.submodule ({ config, ... }: {
426 This setting configures the name of a mailbox for which administrator scripts are configured.
428 The settings defined hereafter with matching sequence numbers apply to the mailbox named by this setting.
430 This setting supports wildcards with a syntax compatible with the IMAP LIST command, meaning that this setting can apply to multiple or even all ("*") mailboxes.
439 Only execute the administrator Sieve scripts for the mailbox configured with services.dovecot2.imapsieve.mailbox.<name>.name when the message originates from the indicated mailbox.
441 This setting supports wildcards with a syntax compatible with the IMAP LIST command, meaning that this setting can apply to multiple or even all ("*") mailboxes.
444 type = types.nullOr types.str;
450 Only execute the administrator Sieve scripts for the mailbox configured with services.dovecot2.imapsieve.mailbox.<name>.name when one of the listed IMAPSIEVE causes apply.
452 This has no effect on the user script, which is always executed no matter the cause.
454 example = [ "COPY" "APPEND" ];
455 type = types.listOf (types.enum [ "APPEND" "COPY" "FLAG" ]);
461 When an IMAP event of interest occurs, this sieve script is executed before any user script respectively.
463 This setting each specify the location of a single sieve script. The semantics of this setting is similar to sieve_before: the specified scripts form a sequence together with the user script in which the next script is only executed when an (implicit) keep action is executed.
465 example = literalExpression "./report-spam.sieve";
466 type = types.nullOr types.path;
472 When an IMAP event of interest occurs, this sieve script is executed after any user script respectively.
474 This setting each specify the location of a single sieve script. The semantics of this setting is similar to sieve_after: the specified scripts form a sequence together with the user script in which the next script is only executed when an (implicit) keep action is executed.
476 example = literalExpression "./report-spam.sieve";
477 type = types.nullOr types.path;
486 example = [ "sieve_extprograms" ];
487 description = "Sieve plugins to load";
488 type = types.listOf types.str;
491 extensions = mkOption {
493 description = "Sieve extensions for use in user scripts";
494 example = [ "notify" "imapflags" "vnd.dovecot.filter" ];
495 type = types.listOf types.str;
498 globalExtensions = mkOption {
500 example = [ "vnd.dovecot.environment" ];
501 description = "Sieve extensions for use in global scripts";
502 type = types.listOf types.str;
506 type = types.attrsOf types.path;
508 description = "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
511 pipeBins = mkOption {
513 example = literalExpression ''
515 (pkgs.writeShellScriptBin "learn-ham.sh" "exec ''${pkgs.rspamd}/bin/rspamc learn_ham")
516 (pkgs.writeShellScriptBin "learn-spam.sh" "exec ''${pkgs.rspamd}/bin/rspamc learn_spam")
519 description = "Programs available for use by the vnd.dovecot.pipe extension";
520 type = types.listOf types.path;
525 config = mkIf cfg.enable {
526 security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
528 security.dhparams = mkIf (cfg.sslServerCert != null && cfg.enableDHE) {
530 params.dovecot2 = {};
533 services.dovecot2 = {
535 optional cfg.enableImap "imap"
536 ++ optional cfg.enablePop3 "pop3"
537 ++ optional cfg.enableLmtp "lmtp";
539 mailPlugins = mkIf cfg.enableQuota {
540 globally.enable = [ "quota" ];
541 perProtocol.imap.enable = [ "imap_quota" ];
545 optional (cfg.imapsieve.mailbox != []) "sieve_imapsieve"
546 ++ optional (cfg.sieve.pipeBins != []) "sieve_extprograms";
548 sieve.globalExtensions = optional (cfg.sieve.pipeBins != []) "vnd.dovecot.pipe";
550 pluginSettings = lib.mapAttrs (n: lib.mkDefault) ({
551 sieve_plugins = concatStringsSep " " cfg.sieve.plugins;
552 sieve_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.extensions);
553 sieve_global_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.globalExtensions);
554 sieve_pipe_bin_dir = sievePipeBinScriptDirectory;
555 } // sieveScriptSettings // imapSieveMailboxSettings);
561 uid = config.ids.uids.dovenull2;
562 description = "Dovecot user for untrusted logins";
565 } // optionalAttrs (cfg.user == "dovecot2") {
568 uid = config.ids.uids.dovecot2;
569 description = "Dovecot user";
572 } // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
574 { description = "Virtual Mail User"; isSystemUser = true; } // optionalAttrs (cfg.mailGroup != null)
575 { group = cfg.mailGroup; };
579 dovenull.gid = config.ids.gids.dovenull2;
580 } // optionalAttrs (cfg.group == "dovecot2") {
581 dovecot2.gid = config.ids.gids.dovecot2;
582 } // optionalAttrs (cfg.createMailUser && cfg.mailGroup != null) {
583 ${cfg.mailGroup} = {};
586 environment.etc."dovecot/modules".source = modulesDir;
587 environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
589 systemd.services.dovecot2 = {
590 description = "Dovecot IMAP/POP3 server";
592 after = [ "network.target" ];
593 wantedBy = [ "multi-user.target" ];
594 restartTriggers = [ cfg.configFile modulesDir ];
596 startLimitIntervalSec = 60; # 1 min
599 ExecStart = "${dovecotPkg}/sbin/dovecot -F";
600 ExecReload = "${dovecotPkg}/sbin/doveadm reload";
601 Restart = "on-failure";
603 RuntimeDirectory = [ "dovecot2" ];
606 # When copying sieve scripts preserve the original time stamp
607 # (should be 0) so that the compiled sieve script is newer than
608 # the source file and Dovecot won't try to compile it.
610 rm -rf ${stateDir}/sieve ${stateDir}/imapsieve
611 '' + optionalString (cfg.sieve.scripts != {}) ''
612 mkdir -p ${stateDir}/sieve
613 ${concatStringsSep "\n" (
616 if [ -d '${from}' ]; then
617 mkdir '${stateDir}/sieve/${to}'
618 cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
620 cp -p '${from}' '${stateDir}/sieve/${to}'
622 ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
626 chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
628 + optionalString (cfg.imapsieve.mailbox != []) ''
629 mkdir -p ${stateDir}/imapsieve/{before,after}
632 concatMapStringsSep "\n"
634 optionalString (el.before != null) ''
635 cp -p ${el.before} ${stateDir}/imapsieve/before/${baseNameOf el.before}
636 ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/imapsieve/before/${baseNameOf el.before}'
638 + optionalString (el.after != null) ''
639 cp -p ${el.after} ${stateDir}/imapsieve/after/${baseNameOf el.after}
640 ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/imapsieve/after/${baseNameOf el.after}'
643 cfg.imapsieve.mailbox
647 optionalString (cfg.mailUser != null && cfg.mailGroup != null)
648 "chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/imapsieve'"
653 environment.systemPackages = [ dovecotPkg ];
655 warnings = warnAboutExtraConfigCollisions;
659 assertion = (cfg.sslServerCert == null) == (cfg.sslServerKey == null)
660 && (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
661 message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
664 assertion = cfg.showPAMFailure -> cfg.enablePAM;
665 message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
668 assertion = cfg.sieve.scripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
669 message = "dovecot requires mailUser and mailGroup to be set when `sieve.scripts` is set";
675 meta.maintainers = [ lib.maintainers.dblsaiko ];