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 canCreateMailUserGroup = cfg.mailUser != null && cfg.mailGroup != null;
13
14 dovecotConf = concatStrings [
15 ''
16 base_dir = ${baseDir}
17 protocols = ${concatStringsSep " " cfg.protocols}
18 sendmail_path = /run/wrappers/bin/sendmail
19 ''
20
21 (if isNull cfg.sslServerCert then ''
22 ssl = no
23 disable_plaintext_auth = no
24 '' else ''
25 ssl_cert = <${cfg.sslServerCert}
26 ssl_key = <${cfg.sslServerKey}
27 ${optionalString (!(isNull cfg.sslCACert)) ("ssl_ca = <" + cfg.sslCACert)}
28 disable_plaintext_auth = yes
29 '')
30
31 ''
32 default_internal_user = ${cfg.user}
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 = { lib, pkgs, ... }: {
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
300 security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
301
302 services.dovecot2.protocols =
303 optional cfg.enableImap "imap"
304 ++ optional cfg.enablePop3 "pop3"
305 ++ optional cfg.enableLmtp "lmtp";
306
307 users.extraUsers = [
308 { name = "dovenull";
309 uid = config.ids.uids.dovenull2;
310 description = "Dovecot user for untrusted logins";
311 group = cfg.group;
312 }
313 ] ++ optional (cfg.user == "dovecot2")
314 { name = "dovecot2";
315 uid = config.ids.uids.dovecot2;
316 description = "Dovecot user";
317 group = cfg.group;
318 }
319 ++ optional (cfg.createMailUser && cfg.mailUser != null)
320 ({ name = cfg.mailUser;
321 description = "Virtual Mail User";
322 } // optionalAttrs (cfg.mailGroup != null) {
323 group = cfg.mailGroup;
324 });
325
326 users.extraGroups = optional (cfg.group == "dovecot2")
327 { name = "dovecot2";
328 gid = config.ids.gids.dovecot2;
329 }
330 ++ optional (cfg.createMailUser && cfg.mailGroup != null)
331 { name = cfg.mailGroup;
332 };
333
334 environment.etc."dovecot/modules".source = modulesDir;
335 environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
336
337 systemd.services.dovecot2 = {
338 description = "Dovecot IMAP/POP3 server";
339
340 after = [ "keys.target" "network.target" ];
341 wants = [ "keys.target" ];
342 wantedBy = [ "multi-user.target" ];
343 restartTriggers = [ cfg.configFile ];
344
345 serviceConfig = {
346 ExecStart = "${dovecotPkg}/sbin/dovecot -F";
347 ExecReload = "${dovecotPkg}/sbin/doveadm reload";
348 Restart = "on-failure";
349 RestartSec = "1s";
350 StartLimitInterval = "1min";
351 RuntimeDirectory = [ "dovecot2" ];
352 };
353
354 # When copying sieve scripts preserve the original time stamp
355 # (should be 0) so that the compiled sieve script is newer than
356 # the source file and Dovecot won't try to compile it.
357 preStart = ''
358 rm -rf ${stateDir}/sieve
359 '' + optionalString (cfg.sieveScripts != {}) ''
360 mkdir -p ${stateDir}/sieve
361 ${concatStringsSep "\n" (mapAttrsToList (to: from: ''
362 if [ -d '${from}' ]; then
363 mkdir '${stateDir}/sieve/${to}'
364 cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
365 else
366 cp -p '${from}' '${stateDir}/sieve/${to}'
367 fi
368 ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
369 '') cfg.sieveScripts)}
370 chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
371 '';
372 };
373
374 environment.systemPackages = [ dovecotPkg ];
375
376 assertions = [
377 { assertion = intersectLists cfg.protocols [ "pop3" "imap" ] != [];
378 message = "dovecot needs at least one of the IMAP or POP3 listeners enabled";
379 }
380 { assertion = isNull cfg.sslServerCert == isNull cfg.sslServerKey
381 && (!(isNull cfg.sslCACert) -> !(isNull cfg.sslServerCert || isNull cfg.sslServerKey));
382 message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
383 }
384 { assertion = cfg.showPAMFailure -> cfg.enablePAM;
385 message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
386 }
387 { assertion = (cfg.sieveScripts != {}) -> ((cfg.mailUser != null) && (cfg.mailGroup != null));
388 message = "dovecot requires mailUser and mailGroup to be set when sieveScripts is set";
389 }
390 ];
391
392 };
393
394}