1{ options, config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.dovecot2;
7 dovecotPkg = pkgs.dovecot;
8
9 baseDir = "/run/dovecot2";
10 stateDir = "/var/lib/dovecot";
11
12 dovecotConf = concatStrings [
13 ''
14 base_dir = ${baseDir}
15 protocols = ${concatStringsSep " " cfg.protocols}
16 sendmail_path = /run/wrappers/bin/sendmail
17 # 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
18 mail_plugins = $mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}
19 ''
20
21 (
22 concatStringsSep "\n" (
23 mapAttrsToList (
24 protocol: plugins: ''
25 protocol ${protocol} {
26 mail_plugins = $mail_plugins ${concatStringsSep " " plugins.enable}
27 }
28 ''
29 ) cfg.mailPlugins.perProtocol
30 )
31 )
32
33 (
34 if cfg.sslServerCert == null then ''
35 ssl = no
36 disable_plaintext_auth = no
37 '' else ''
38 ssl_cert = <${cfg.sslServerCert}
39 ssl_key = <${cfg.sslServerKey}
40 ${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)}
41 ${optionalString cfg.enableDHE ''ssl_dh = <${config.security.dhparams.params.dovecot2.path}''}
42 disable_plaintext_auth = yes
43 ''
44 )
45
46 ''
47 default_internal_user = ${cfg.user}
48 default_internal_group = ${cfg.group}
49 ${optionalString (cfg.mailUser != null) "mail_uid = ${cfg.mailUser}"}
50 ${optionalString (cfg.mailGroup != null) "mail_gid = ${cfg.mailGroup}"}
51
52 mail_location = ${cfg.mailLocation}
53
54 maildir_copy_with_hardlinks = yes
55 pop3_uidl_format = %08Xv%08Xu
56
57 auth_mechanisms = plain login
58
59 service auth {
60 user = root
61 }
62 ''
63
64 (
65 optionalString cfg.enablePAM ''
66 userdb {
67 driver = passwd
68 }
69
70 passdb {
71 driver = pam
72 args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
73 }
74 ''
75 )
76
77 (
78 optionalString (cfg.sieveScripts != {}) ''
79 plugin {
80 ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieveScripts)}
81 }
82 ''
83 )
84
85 (
86 optionalString (cfg.mailboxes != {}) ''
87 namespace inbox {
88 inbox=yes
89 ${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
90 }
91 ''
92 )
93
94 (
95 optionalString cfg.enableQuota ''
96 service quota-status {
97 executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
98 inet_listener {
99 port = ${cfg.quotaPort}
100 }
101 client_limit = 1
102 }
103
104 plugin {
105 quota_rule = *:storage=${cfg.quotaGlobalPerUser}
106 quota = count:User quota # per virtual mail user quota
107 quota_status_success = DUNNO
108 quota_status_nouser = DUNNO
109 quota_status_overquota = "552 5.2.2 Mailbox is full"
110 quota_grace = 10%%
111 quota_vsizes = yes
112 }
113 ''
114 )
115
116 cfg.extraConfig
117 ];
118
119 modulesDir = pkgs.symlinkJoin {
120 name = "dovecot-modules";
121 paths = map (pkg: "${pkg}/lib/dovecot") ([ dovecotPkg ] ++ map (module: module.override { dovecot = dovecotPkg; }) cfg.modules);
122 };
123
124 mailboxConfig = mailbox: ''
125 mailbox "${mailbox.name}" {
126 auto = ${toString mailbox.auto}
127 '' + optionalString (mailbox.autoexpunge != null) ''
128 autoexpunge = ${mailbox.autoexpunge}
129 '' + optionalString (mailbox.specialUse != null) ''
130 special_use = \${toString mailbox.specialUse}
131 '' + "}";
132
133 mailboxes = { name, ... }: {
134 options = {
135 name = mkOption {
136 type = types.strMatching ''[^"]+'';
137 example = "Spam";
138 default = name;
139 readOnly = true;
140 description = lib.mdDoc "The name of the mailbox.";
141 };
142 auto = mkOption {
143 type = types.enum [ "no" "create" "subscribe" ];
144 default = "no";
145 example = "subscribe";
146 description = lib.mdDoc "Whether to automatically create or create and subscribe to the mailbox or not.";
147 };
148 specialUse = mkOption {
149 type = types.nullOr (types.enum [ "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash" ]);
150 default = null;
151 example = "Junk";
152 description = lib.mdDoc "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
153 };
154 autoexpunge = mkOption {
155 type = types.nullOr types.str;
156 default = null;
157 example = "60d";
158 description = lib.mdDoc ''
159 To automatically remove all email from the mailbox which is older than the
160 specified time.
161 '';
162 };
163 };
164 };
165in
166{
167 imports = [
168 (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
169 ];
170
171 options.services.dovecot2 = {
172 enable = mkEnableOption (lib.mdDoc "the dovecot 2.x POP3/IMAP server");
173
174 enablePop3 = mkEnableOption (lib.mdDoc "starting the POP3 listener (when Dovecot is enabled)");
175
176 enableImap = mkEnableOption (lib.mdDoc "starting the IMAP listener (when Dovecot is enabled)") // { default = true; };
177
178 enableLmtp = mkEnableOption (lib.mdDoc "starting the LMTP listener (when Dovecot is enabled)");
179
180 protocols = mkOption {
181 type = types.listOf types.str;
182 default = [];
183 description = lib.mdDoc "Additional listeners to start when Dovecot is enabled.";
184 };
185
186 user = mkOption {
187 type = types.str;
188 default = "dovecot2";
189 description = lib.mdDoc "Dovecot user name.";
190 };
191
192 group = mkOption {
193 type = types.str;
194 default = "dovecot2";
195 description = lib.mdDoc "Dovecot group name.";
196 };
197
198 extraConfig = mkOption {
199 type = types.lines;
200 default = "";
201 example = "mail_debug = yes";
202 description = lib.mdDoc "Additional entries to put verbatim into Dovecot's config file.";
203 };
204
205 mailPlugins =
206 let
207 plugins = hint: types.submodule {
208 options = {
209 enable = mkOption {
210 type = types.listOf types.str;
211 default = [];
212 description = lib.mdDoc "mail plugins to enable as a list of strings to append to the ${hint} `$mail_plugins` configuration variable";
213 };
214 };
215 };
216 in
217 mkOption {
218 type = with types; submodule {
219 options = {
220 globally = mkOption {
221 description = lib.mdDoc "Additional entries to add to the mail_plugins variable for all protocols";
222 type = plugins "top-level";
223 example = { enable = [ "virtual" ]; };
224 default = { enable = []; };
225 };
226 perProtocol = mkOption {
227 description = lib.mdDoc "Additional entries to add to the mail_plugins variable, per protocol";
228 type = attrsOf (plugins "corresponding per-protocol");
229 default = {};
230 example = { imap = [ "imap_acl" ]; };
231 };
232 };
233 };
234 description = lib.mdDoc "Additional entries to add to the mail_plugins variable, globally and per protocol";
235 example = {
236 globally.enable = [ "acl" ];
237 perProtocol.imap.enable = [ "imap_acl" ];
238 };
239 default = { globally.enable = []; perProtocol = {}; };
240 };
241
242 configFile = mkOption {
243 type = types.nullOr types.path;
244 default = null;
245 description = lib.mdDoc "Config file used for the whole dovecot configuration.";
246 apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf;
247 };
248
249 mailLocation = mkOption {
250 type = types.str;
251 default = "maildir:/var/spool/mail/%u"; /* Same as inbox, as postfix */
252 example = "maildir:~/mail:INBOX=/var/spool/mail/%u";
253 description = lib.mdDoc ''
254 Location that dovecot will use for mail folders. Dovecot mail_location option.
255 '';
256 };
257
258 mailUser = mkOption {
259 type = types.nullOr types.str;
260 default = null;
261 description = lib.mdDoc "Default user to store mail for virtual users.";
262 };
263
264 mailGroup = mkOption {
265 type = types.nullOr types.str;
266 default = null;
267 description = lib.mdDoc "Default group to store mail for virtual users.";
268 };
269
270 createMailUser = mkEnableOption (lib.mdDoc ''automatically creating the user
271 given in {option}`services.dovecot.user` and the group
272 given in {option}`services.dovecot.group`.'') // { default = true; };
273
274 modules = mkOption {
275 type = types.listOf types.package;
276 default = [];
277 example = literalExpression "[ pkgs.dovecot_pigeonhole ]";
278 description = lib.mdDoc ''
279 Symlinks the contents of lib/dovecot of every given package into
280 /etc/dovecot/modules. This will make the given modules available
281 if a dovecot package with the module_dir patch applied is being used.
282 '';
283 };
284
285 sslCACert = mkOption {
286 type = types.nullOr types.str;
287 default = null;
288 description = lib.mdDoc "Path to the server's CA certificate key.";
289 };
290
291 sslServerCert = mkOption {
292 type = types.nullOr types.str;
293 default = null;
294 description = lib.mdDoc "Path to the server's public key.";
295 };
296
297 sslServerKey = mkOption {
298 type = types.nullOr types.str;
299 default = null;
300 description = lib.mdDoc "Path to the server's private key.";
301 };
302
303 enablePAM = mkEnableOption (lib.mdDoc "creating a own Dovecot PAM service and configure PAM user logins") // { default = true; };
304
305 enableDHE = mkEnableOption (lib.mdDoc "enable ssl_dh and generation of primes for the key exchange") // { default = true; };
306
307 sieveScripts = mkOption {
308 type = types.attrsOf types.path;
309 default = {};
310 description = lib.mdDoc "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
311 };
312
313 showPAMFailure = mkEnableOption (lib.mdDoc "showing the PAM failure message on authentication error (useful for OTPW)");
314
315 mailboxes = mkOption {
316 type = with types; coercedTo
317 (listOf unspecified)
318 (list: listToAttrs (map (entry: { name = entry.name; value = removeAttrs entry ["name"]; }) list))
319 (attrsOf (submodule mailboxes));
320 default = {};
321 example = literalExpression ''
322 {
323 Spam = { specialUse = "Junk"; auto = "create"; };
324 }
325 '';
326 description = lib.mdDoc "Configure mailboxes and auto create or subscribe them.";
327 };
328
329 enableQuota = mkEnableOption (lib.mdDoc "the dovecot quota service");
330
331 quotaPort = mkOption {
332 type = types.str;
333 default = "12340";
334 description = lib.mdDoc ''
335 The Port the dovecot quota service binds to.
336 If using postfix, add check_policy_service inet:localhost:12340 to your smtpd_recipient_restrictions in your postfix config.
337 '';
338 };
339 quotaGlobalPerUser = mkOption {
340 type = types.str;
341 default = "100G";
342 example = "10G";
343 description = lib.mdDoc "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
344 };
345
346 };
347
348
349 config = mkIf cfg.enable {
350 security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
351
352 security.dhparams = mkIf (cfg.sslServerCert != null && cfg.enableDHE) {
353 enable = true;
354 params.dovecot2 = {};
355 };
356 services.dovecot2.protocols =
357 optional cfg.enableImap "imap"
358 ++ optional cfg.enablePop3 "pop3"
359 ++ optional cfg.enableLmtp "lmtp";
360
361 services.dovecot2.mailPlugins = mkIf cfg.enableQuota {
362 globally.enable = [ "quota" ];
363 perProtocol.imap.enable = [ "imap_quota" ];
364 };
365
366 users.users = {
367 dovenull =
368 {
369 uid = config.ids.uids.dovenull2;
370 description = "Dovecot user for untrusted logins";
371 group = "dovenull";
372 };
373 } // optionalAttrs (cfg.user == "dovecot2") {
374 dovecot2 =
375 {
376 uid = config.ids.uids.dovecot2;
377 description = "Dovecot user";
378 group = cfg.group;
379 };
380 } // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
381 ${cfg.mailUser} =
382 { description = "Virtual Mail User"; isSystemUser = true; } // optionalAttrs (cfg.mailGroup != null)
383 { group = cfg.mailGroup; };
384 };
385
386 users.groups = {
387 dovenull.gid = config.ids.gids.dovenull2;
388 } // optionalAttrs (cfg.group == "dovecot2") {
389 dovecot2.gid = config.ids.gids.dovecot2;
390 } // optionalAttrs (cfg.createMailUser && cfg.mailGroup != null) {
391 ${cfg.mailGroup} = {};
392 };
393
394 environment.etc."dovecot/modules".source = modulesDir;
395 environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
396
397 systemd.services.dovecot2 = {
398 description = "Dovecot IMAP/POP3 server";
399
400 after = [ "network.target" ];
401 wantedBy = [ "multi-user.target" ];
402 restartTriggers = [ cfg.configFile modulesDir ];
403
404 startLimitIntervalSec = 60; # 1 min
405 serviceConfig = {
406 Type = "notify";
407 ExecStart = "${dovecotPkg}/sbin/dovecot -F";
408 ExecReload = "${dovecotPkg}/sbin/doveadm reload";
409 Restart = "on-failure";
410 RestartSec = "1s";
411 RuntimeDirectory = [ "dovecot2" ];
412 };
413
414 # When copying sieve scripts preserve the original time stamp
415 # (should be 0) so that the compiled sieve script is newer than
416 # the source file and Dovecot won't try to compile it.
417 preStart = ''
418 rm -rf ${stateDir}/sieve
419 '' + optionalString (cfg.sieveScripts != {}) ''
420 mkdir -p ${stateDir}/sieve
421 ${concatStringsSep "\n" (
422 mapAttrsToList (
423 to: from: ''
424 if [ -d '${from}' ]; then
425 mkdir '${stateDir}/sieve/${to}'
426 cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
427 else
428 cp -p '${from}' '${stateDir}/sieve/${to}'
429 fi
430 ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
431 ''
432 ) cfg.sieveScripts
433 )}
434 chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
435 '';
436 };
437
438 environment.systemPackages = [ dovecotPkg ];
439
440 warnings = mkIf (any isList options.services.dovecot2.mailboxes.definitions) [
441 "Declaring `services.dovecot2.mailboxes' as a list is deprecated and will break eval in 21.05! See the release notes for more info for migration."
442 ];
443
444 assertions = [
445 {
446 assertion = (cfg.sslServerCert == null) == (cfg.sslServerKey == null)
447 && (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
448 message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
449 }
450 {
451 assertion = cfg.showPAMFailure -> cfg.enablePAM;
452 message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
453 }
454 {
455 assertion = cfg.sieveScripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
456 message = "dovecot requires mailUser and mailGroup to be set when sieveScripts is set";
457 }
458 ];
459
460 };
461
462}