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