grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / mail / dovecot.nix
blob1d17dd66e071680275dc85d81518231bfbf4f453
1 { config, lib, pkgs, ... }:
3 let
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:
19     singleton {
20       name = "imapsieve_mailbox${toString idx}_name";
21       value = el.name;
22     } ++ optional (el.from != null) {
23       name = "imapsieve_mailbox${toString idx}_from";
24       value = el.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}";
34     }
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.
44   '';
46   # Those settings are automatically set based on other parts
47   # of this module.
48   automaticallySetPluginSettings = [
49     "sieve_plugins"
50     "sieve_extensions"
51     "sieve_global_extensions"
52     "sieve_pipe_bin_dir"
53   ]
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);
66       path = el;
67   }) cfg.sieve.pipeBins);
69   dovecotConf = concatStrings [
70     ''
71       base_dir = ${baseDir}
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}
76     ''
78     (
79       concatStringsSep "\n" (
80         mapAttrsToList (
81           protocol: plugins: ''
82             protocol ${protocol} {
83               mail_plugins = $mail_plugins ${concatStringsSep " " plugins.enable}
84             }
85           ''
86         ) cfg.mailPlugins.perProtocol
87       )
88     )
90     (
91       if cfg.sslServerCert == null then ''
92         ssl = no
93         disable_plaintext_auth = no
94       '' else ''
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
100       ''
101     )
103     ''
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
116       service auth {
117         user = root
118       }
119     ''
121     (
122       optionalString cfg.enablePAM ''
123         userdb {
124           driver = passwd
125         }
127         passdb {
128           driver = pam
129           args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
130         }
131       ''
132     )
134     (
135       optionalString (cfg.mailboxes != {}) ''
136         namespace inbox {
137           inbox=yes
138           ${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
139         }
140       ''
141     )
143     (
144       optionalString cfg.enableQuota ''
145         service quota-status {
146           executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
147           inet_listener {
148             port = ${cfg.quotaPort}
149           }
150           client_limit = 1
151         }
153         plugin {
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"
159           quota_grace = 10%%
160           quota_vsizes = yes
161         }
162       ''
163     )
165     # General plugin settings:
166     # - sieve is mostly generated here, refer to `pluginSettings` to follow
167     # the control flow.
168     ''
169       plugin {
170         ${concatStringsSep "\n" (mapAttrsToList (key: value: "  ${key} = ${value}") cfg.pluginSettings)}
171       }
172     ''
174     cfg.extraConfig
175   ];
177   modulesDir = pkgs.symlinkJoin {
178     name = "dovecot-modules";
179     paths = map (pkg: "${pkg}/lib/dovecot") ([ dovecotPkg ] ++ map (module: module.override { dovecot = dovecotPkg; }) cfg.modules);
180   };
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}
189   '' + "}";
191   mailboxes = { name, ... }: {
192     options = {
193       name = mkOption {
194         type = types.strMatching ''[^"]+'';
195         example = "Spam";
196         default = name;
197         readOnly = true;
198         description = "The name of the mailbox.";
199       };
200       auto = mkOption {
201         type = types.enum [ "no" "create" "subscribe" ];
202         default = "no";
203         example = "subscribe";
204         description = "Whether to automatically create or create and subscribe to the mailbox or not.";
205       };
206       specialUse = mkOption {
207         type = types.nullOr (types.enum [ "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash" ]);
208         default = null;
209         example = "Junk";
210         description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
211       };
212       autoexpunge = mkOption {
213         type = types.nullOr types.str;
214         default = null;
215         example = "60d";
216         description = ''
217           To automatically remove all email from the mailbox which is older than the
218           specified time.
219         '';
220       };
221     };
222   };
225   imports = [
226     (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
227     (mkRenamedOptionModule [ "services" "dovecot2" "sieveScripts" ] [ "services" "dovecot2" "sieve" "scripts" ])
228   ];
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;
241       default = [];
242       description = "Additional listeners to start when Dovecot is enabled.";
243     };
245     user = mkOption {
246       type = types.str;
247       default = "dovecot2";
248       description = "Dovecot user name.";
249     };
251     group = mkOption {
252       type = types.str;
253       default = "dovecot2";
254       description = "Dovecot group name.";
255     };
257     extraConfig = mkOption {
258       type = types.lines;
259       default = "";
260       example = "mail_debug = yes";
261       description = "Additional entries to put verbatim into Dovecot's config file.";
262     };
264     mailPlugins =
265       let
266         plugins = hint: types.submodule {
267           options = {
268             enable = mkOption {
269               type = types.listOf types.str;
270               default = [];
271               description = "mail plugins to enable as a list of strings to append to the ${hint} `$mail_plugins` configuration variable";
272             };
273           };
274         };
275       in
276         mkOption {
277           type = with types; submodule {
278             options = {
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 = []; };
284               };
285               perProtocol = mkOption {
286                 description = "Additional entries to add to the mail_plugins variable, per protocol";
287                 type = attrsOf (plugins "corresponding per-protocol");
288                 default = {};
289                 example = { imap = [ "imap_acl" ]; };
290               };
291             };
292           };
293           description = "Additional entries to add to the mail_plugins variable, globally and per protocol";
294           example = {
295             globally.enable = [ "acl" ];
296             perProtocol.imap.enable = [ "imap_acl" ];
297           };
298           default = { globally.enable = []; perProtocol = {}; };
299         };
301     configFile = mkOption {
302       type = types.nullOr types.path;
303       default = null;
304       description = "Config file used for the whole dovecot configuration.";
305       apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf;
306     };
308     mailLocation = mkOption {
309       type = types.str;
310       default = "maildir:/var/spool/mail/%u"; /* Same as inbox, as postfix */
311       example = "maildir:~/mail:INBOX=/var/spool/mail/%u";
312       description = ''
313         Location that dovecot will use for mail folders. Dovecot mail_location option.
314       '';
315     };
317     mailUser = mkOption {
318       type = types.nullOr types.str;
319       default = null;
320       description = "Default user to store mail for virtual users.";
321     };
323     mailGroup = mkOption {
324       type = types.nullOr types.str;
325       default = null;
326       description = "Default group to store mail for virtual users.";
327     };
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; };
333     modules = mkOption {
334       type = types.listOf types.package;
335       default = [];
336       example = literalExpression "[ pkgs.dovecot_pigeonhole ]";
337       description = ''
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.
341       '';
342     };
344     sslCACert = mkOption {
345       type = types.nullOr types.str;
346       default = null;
347       description = "Path to the server's CA certificate key.";
348     };
350     sslServerCert = mkOption {
351       type = types.nullOr types.str;
352       default = null;
353       description = "Path to the server's public key.";
354     };
356     sslServerKey = mkOption {
357       type = types.nullOr types.str;
358       default = null;
359       description = "Path to the server's private key.";
360     };
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
370         (listOf unspecified)
371         (list: listToAttrs (map (entry: { name = entry.name; value = removeAttrs entry ["name"]; }) list))
372         (attrsOf (submodule mailboxes));
373       default = {};
374       example = literalExpression ''
375         {
376           Spam = { specialUse = "Junk"; auto = "create"; };
377         }
378       '';
379       description = "Configure mailboxes and auto create or subscribe them.";
380     };
382     enableQuota = mkEnableOption "the dovecot quota service";
384     quotaPort = mkOption {
385       type = types.str;
386       default = "12340";
387       description = ''
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.
390       '';
391     };
392     quotaGlobalPerUser = mkOption {
393       type = types.str;
394       default = "100G";
395       example = "10G";
396       description = "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
397     };
400     pluginSettings = mkOption {
401       # types.str does not coerce from packages, like `sievePipeBinScriptDirectory`.
402       type = types.attrsOf (types.oneOf [ types.str types.package ]);
403       default = {};
404       example = literalExpression ''
405         {
406           sieve = "file:~/sieve;active=~/.dovecot.sieve";
407         }
408       '';
409       description = ''
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.
416       '';
417     };
419     imapsieve.mailbox = mkOption {
420       default = [];
421       description = "Configure Sieve filtering rules on IMAP actions";
422       type = types.listOf (types.submodule ({ config, ... }: {
423         options = {
424           name = mkOption {
425             description = ''
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.
431             '';
432             example = "Junk";
433             type = types.str;
434           };
436           from = mkOption {
437             default = null;
438             description = ''
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.
442             '';
443             example = "*";
444             type = types.nullOr types.str;
445           };
447           causes = mkOption {
448             default = [ ];
449             description = ''
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.
453             '';
454             example = [ "COPY" "APPEND" ];
455             type = types.listOf (types.enum [ "APPEND" "COPY" "FLAG" ]);
456           };
458           before = mkOption {
459             default = null;
460             description = ''
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.
464             '';
465             example = literalExpression "./report-spam.sieve";
466             type = types.nullOr types.path;
467           };
469           after = mkOption {
470             default = null;
471             description = ''
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.
475             '';
476             example = literalExpression "./report-spam.sieve";
477             type = types.nullOr types.path;
478           };
479         };
480       }));
481     };
483     sieve = {
484       plugins = mkOption {
485         default = [];
486         example = [ "sieve_extprograms" ];
487         description = "Sieve plugins to load";
488         type = types.listOf types.str;
489       };
491       extensions = mkOption {
492         default = [];
493         description = "Sieve extensions for use in user scripts";
494         example = [ "notify" "imapflags" "vnd.dovecot.filter" ];
495         type = types.listOf types.str;
496       };
498       globalExtensions = mkOption {
499         default = [];
500         example = [ "vnd.dovecot.environment" ];
501         description = "Sieve extensions for use in global scripts";
502         type = types.listOf types.str;
503       };
505       scripts = mkOption {
506         type = types.attrsOf types.path;
507         default = {};
508         description = "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
509       };
511       pipeBins = mkOption {
512         default = [];
513         example = literalExpression ''
514           map lib.getExe [
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")
517           ]
518         '';
519         description = "Programs available for use by the vnd.dovecot.pipe extension";
520         type = types.listOf types.path;
521       };
522     };
523   };
525   config = mkIf cfg.enable {
526     security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
528     security.dhparams = mkIf (cfg.sslServerCert != null && cfg.enableDHE) {
529       enable = true;
530       params.dovecot2 = {};
531     };
533     services.dovecot2 = {
534       protocols =
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" ];
542       };
544       sieve.plugins =
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);
556     };
558     users.users = {
559       dovenull =
560         {
561           uid = config.ids.uids.dovenull2;
562           description = "Dovecot user for untrusted logins";
563           group = "dovenull";
564         };
565     } // optionalAttrs (cfg.user == "dovecot2") {
566       dovecot2 =
567         {
568           uid = config.ids.uids.dovecot2;
569           description = "Dovecot user";
570           group = cfg.group;
571         };
572     } // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
573       ${cfg.mailUser} =
574         { description = "Virtual Mail User"; isSystemUser = true; } // optionalAttrs (cfg.mailGroup != null)
575           { group = cfg.mailGroup; };
576     };
578     users.groups = {
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} = {};
584     };
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
597       serviceConfig = {
598         Type = "notify";
599         ExecStart = "${dovecotPkg}/sbin/dovecot -F";
600         ExecReload = "${dovecotPkg}/sbin/doveadm reload";
601         Restart = "on-failure";
602         RestartSec = "1s";
603         RuntimeDirectory = [ "dovecot2" ];
604       };
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.
609       preStart = ''
610         rm -rf ${stateDir}/sieve ${stateDir}/imapsieve
611       '' + optionalString (cfg.sieve.scripts != {}) ''
612         mkdir -p ${stateDir}/sieve
613         ${concatStringsSep "\n" (
614         mapAttrsToList (
615           to: from: ''
616             if [ -d '${from}' ]; then
617               mkdir '${stateDir}/sieve/${to}'
618               cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
619             else
620               cp -p '${from}' '${stateDir}/sieve/${to}'
621             fi
622             ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
623           ''
624         ) cfg.sieve.scripts
625       )}
626         chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
627       ''
628       + optionalString (cfg.imapsieve.mailbox != []) ''
629         mkdir -p ${stateDir}/imapsieve/{before,after}
631         ${
632           concatMapStringsSep "\n"
633             (el:
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}'
637               ''
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}'
641               ''
642             )
643             cfg.imapsieve.mailbox
644         }
646         ${
647           optionalString (cfg.mailUser != null && cfg.mailGroup != null)
648             "chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/imapsieve'"
649         }
650       '';
651     };
653     environment.systemPackages = [ dovecotPkg ];
655     warnings = warnAboutExtraConfigCollisions;
657     assertions = [
658       {
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";
662       }
663       {
664         assertion = cfg.showPAMFailure -> cfg.enablePAM;
665         message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
666       }
667       {
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";
670       }
671     ];
673   };
675   meta.maintainers = [ lib.maintainers.dblsaiko ];