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 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 = maildir:User quota # per virtual mail user quota # BUG/FIXME broken, we couldn't get this working
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 }
112 ''
113 )
114
115 cfg.extraConfig
116 ];
117
118 modulesDir = pkgs.symlinkJoin {
119 name = "dovecot-modules";
120 paths = map (pkg: "${pkg}/lib/dovecot") ([ dovecotPkg ] ++ map (module: module.override { dovecot = dovecotPkg; }) cfg.modules);
121 };
122
123 mailboxConfig = mailbox: ''
124 mailbox "${mailbox.name}" {
125 auto = ${toString mailbox.auto}
126 '' + optionalString (mailbox.autoexpunge != null) ''
127 autoexpunge = ${mailbox.autoexpunge}
128 '' + optionalString (mailbox.specialUse != null) ''
129 special_use = \${toString mailbox.specialUse}
130 '' + "}";
131
132 mailboxes = { name, ... }: {
133 options = {
134 name = mkOption {
135 type = types.strMatching ''[^"]+'';
136 example = "Spam";
137 default = name;
138 readOnly = true;
139 description = "The name of the mailbox.";
140 };
141 auto = mkOption {
142 type = types.enum [ "no" "create" "subscribe" ];
143 default = "no";
144 example = "subscribe";
145 description = "Whether to automatically create or create and subscribe to the mailbox or not.";
146 };
147 specialUse = mkOption {
148 type = types.nullOr (types.enum [ "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash" ]);
149 default = null;
150 example = "Junk";
151 description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
152 };
153 autoexpunge = mkOption {
154 type = types.nullOr types.str;
155 default = null;
156 example = "60d";
157 description = ''
158 To automatically remove all email from the mailbox which is older than the
159 specified time.
160 '';
161 };
162 };
163 };
164in
165{
166 imports = [
167 (mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
168 ];
169
170 options.services.dovecot2 = {
171 enable = mkEnableOption "Dovecot 2.x POP3/IMAP server";
172
173 enablePop3 = mkOption {
174 type = types.bool;
175 default = false;
176 description = "Start the POP3 listener (when Dovecot is enabled).";
177 };
178
179 enableImap = mkOption {
180 type = types.bool;
181 default = true;
182 description = "Start the IMAP listener (when Dovecot is enabled).";
183 };
184
185 enableLmtp = mkOption {
186 type = types.bool;
187 default = false;
188 description = "Start the LMTP listener (when Dovecot is enabled).";
189 };
190
191 protocols = mkOption {
192 type = types.listOf types.str;
193 default = [];
194 description = "Additional listeners to start when Dovecot is enabled.";
195 };
196
197 user = mkOption {
198 type = types.str;
199 default = "dovecot2";
200 description = "Dovecot user name.";
201 };
202
203 group = mkOption {
204 type = types.str;
205 default = "dovecot2";
206 description = "Dovecot group name.";
207 };
208
209 extraConfig = mkOption {
210 type = types.lines;
211 default = "";
212 example = "mail_debug = yes";
213 description = "Additional entries to put verbatim into Dovecot's config file.";
214 };
215
216 mailPlugins =
217 let
218 plugins = hint: types.submodule {
219 options = {
220 enable = mkOption {
221 type = types.listOf types.str;
222 default = [];
223 description = "mail plugins to enable as a list of strings to append to the ${hint} <literal>$mail_plugins</literal> configuration variable";
224 };
225 };
226 };
227 in
228 mkOption {
229 type = with types; submodule {
230 options = {
231 globally = mkOption {
232 description = "Additional entries to add to the mail_plugins variable for all protocols";
233 type = plugins "top-level";
234 example = { enable = [ "virtual" ]; };
235 default = { enable = []; };
236 };
237 perProtocol = mkOption {
238 description = "Additional entries to add to the mail_plugins variable, per protocol";
239 type = attrsOf (plugins "corresponding per-protocol");
240 default = {};
241 example = { imap = [ "imap_acl" ]; };
242 };
243 };
244 };
245 description = "Additional entries to add to the mail_plugins variable, globally and per protocol";
246 example = {
247 globally.enable = [ "acl" ];
248 perProtocol.imap.enable = [ "imap_acl" ];
249 };
250 default = { globally.enable = []; perProtocol = {}; };
251 };
252
253 configFile = mkOption {
254 type = types.nullOr types.path;
255 default = null;
256 description = "Config file used for the whole dovecot configuration.";
257 apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf;
258 };
259
260 mailLocation = mkOption {
261 type = types.str;
262 default = "maildir:/var/spool/mail/%u"; /* Same as inbox, as postfix */
263 example = "maildir:~/mail:INBOX=/var/spool/mail/%u";
264 description = ''
265 Location that dovecot will use for mail folders. Dovecot mail_location option.
266 '';
267 };
268
269 mailUser = mkOption {
270 type = types.nullOr types.str;
271 default = null;
272 description = "Default user to store mail for virtual users.";
273 };
274
275 mailGroup = mkOption {
276 type = types.nullOr types.str;
277 default = null;
278 description = "Default group to store mail for virtual users.";
279 };
280
281 createMailUser = mkOption {
282 type = types.bool;
283 default = true;
284 description = ''Whether to automatically create the user
285 given in <option>services.dovecot.user</option> and the group
286 given in <option>services.dovecot.group</option>.'';
287 };
288
289 modules = mkOption {
290 type = types.listOf types.package;
291 default = [];
292 example = literalExample "[ pkgs.dovecot_pigeonhole ]";
293 description = ''
294 Symlinks the contents of lib/dovecot of every given package into
295 /etc/dovecot/modules. This will make the given modules available
296 if a dovecot package with the module_dir patch applied is being used.
297 '';
298 };
299
300 sslCACert = mkOption {
301 type = types.nullOr types.str;
302 default = null;
303 description = "Path to the server's CA certificate key.";
304 };
305
306 sslServerCert = mkOption {
307 type = types.nullOr types.str;
308 default = null;
309 description = "Path to the server's public key.";
310 };
311
312 sslServerKey = mkOption {
313 type = types.nullOr types.str;
314 default = null;
315 description = "Path to the server's private key.";
316 };
317
318 enablePAM = mkOption {
319 type = types.bool;
320 default = true;
321 description = "Whether to create a own Dovecot PAM service and configure PAM user logins.";
322 };
323
324 sieveScripts = mkOption {
325 type = types.attrsOf types.path;
326 default = {};
327 description = "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
328 };
329
330 showPAMFailure = mkOption {
331 type = types.bool;
332 default = false;
333 description = "Show the PAM failure message on authentication error (useful for OTPW).";
334 };
335
336 mailboxes = mkOption {
337 type = with types; coercedTo
338 (listOf unspecified)
339 (list: listToAttrs (map (entry: { name = entry.name; value = removeAttrs entry ["name"]; }) list))
340 (attrsOf (submodule mailboxes));
341 default = {};
342 example = literalExample ''
343 {
344 Spam = { specialUse = "Junk"; auto = "create"; };
345 }
346 '';
347 description = "Configure mailboxes and auto create or subscribe them.";
348 };
349
350 enableQuota = mkOption {
351 type = types.bool;
352 default = false;
353 example = true;
354 description = "Whether to enable the dovecot quota service.";
355 };
356
357 quotaPort = mkOption {
358 type = types.str;
359 default = "12340";
360 description = ''
361 The Port the dovecot quota service binds to.
362 If using postfix, add check_policy_service inet:localhost:12340 to your smtpd_recipient_restrictions in your postfix config.
363 '';
364 };
365 quotaGlobalPerUser = mkOption {
366 type = types.str;
367 default = "100G";
368 example = "10G";
369 description = "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
370 };
371
372 };
373
374
375 config = mkIf cfg.enable {
376 security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
377
378 security.dhparams = mkIf (cfg.sslServerCert != null) {
379 enable = true;
380 params.dovecot2 = {};
381 };
382 services.dovecot2.protocols =
383 optional cfg.enableImap "imap"
384 ++ optional cfg.enablePop3 "pop3"
385 ++ optional cfg.enableLmtp "lmtp";
386
387 services.dovecot2.mailPlugins = mkIf cfg.enableQuota {
388 globally.enable = [ "quota" ];
389 perProtocol.imap.enable = [ "imap_quota" ];
390 };
391
392 users.users = {
393 dovenull =
394 {
395 uid = config.ids.uids.dovenull2;
396 description = "Dovecot user for untrusted logins";
397 group = "dovenull";
398 };
399 } // optionalAttrs (cfg.user == "dovecot2") {
400 dovecot2 =
401 {
402 uid = config.ids.uids.dovecot2;
403 description = "Dovecot user";
404 group = cfg.group;
405 };
406 } // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
407 ${cfg.mailUser} =
408 { description = "Virtual Mail User"; isSystemUser = true; } // optionalAttrs (cfg.mailGroup != null)
409 { group = cfg.mailGroup; };
410 };
411
412 users.groups = {
413 dovenull.gid = config.ids.gids.dovenull2;
414 } // optionalAttrs (cfg.group == "dovecot2") {
415 dovecot2.gid = config.ids.gids.dovecot2;
416 } // optionalAttrs (cfg.createMailUser && cfg.mailGroup != null) {
417 ${cfg.mailGroup} = {};
418 };
419
420 environment.etc."dovecot/modules".source = modulesDir;
421 environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
422
423 systemd.services.dovecot2 = {
424 description = "Dovecot IMAP/POP3 server";
425
426 after = [ "network.target" ];
427 wantedBy = [ "multi-user.target" ];
428 restartTriggers = [ cfg.configFile modulesDir ];
429
430 startLimitIntervalSec = 60; # 1 min
431 serviceConfig = {
432 ExecStart = "${dovecotPkg}/sbin/dovecot -F";
433 ExecReload = "${dovecotPkg}/sbin/doveadm reload";
434 Restart = "on-failure";
435 RestartSec = "1s";
436 RuntimeDirectory = [ "dovecot2" ];
437 };
438
439 # When copying sieve scripts preserve the original time stamp
440 # (should be 0) so that the compiled sieve script is newer than
441 # the source file and Dovecot won't try to compile it.
442 preStart = ''
443 rm -rf ${stateDir}/sieve
444 '' + optionalString (cfg.sieveScripts != {}) ''
445 mkdir -p ${stateDir}/sieve
446 ${concatStringsSep "\n" (
447 mapAttrsToList (
448 to: from: ''
449 if [ -d '${from}' ]; then
450 mkdir '${stateDir}/sieve/${to}'
451 cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
452 else
453 cp -p '${from}' '${stateDir}/sieve/${to}'
454 fi
455 ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
456 ''
457 ) cfg.sieveScripts
458 )}
459 chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
460 '';
461 };
462
463 environment.systemPackages = [ dovecotPkg ];
464
465 warnings = mkIf (any isList options.services.dovecot2.mailboxes.definitions) [
466 "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."
467 ];
468
469 assertions = [
470 {
471 assertion = intersectLists cfg.protocols [ "pop3" "imap" ] != [];
472 message = "dovecot needs at least one of the IMAP or POP3 listeners enabled";
473 }
474 {
475 assertion = (cfg.sslServerCert == null) == (cfg.sslServerKey == null)
476 && (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
477 message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
478 }
479 {
480 assertion = cfg.showPAMFailure -> cfg.enablePAM;
481 message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
482 }
483 {
484 assertion = cfg.sieveScripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
485 message = "dovecot requires mailUser and mailGroup to be set when sieveScripts is set";
486 }
487 ];
488
489 };
490
491}