1 { pkgs, lib, config, system, ... }:
2 let inherit (builtins) toString toFile;
3 inherit (builtins.extraBuiltins) pass;
5 inherit (pkgs.lib) loadFile unlines unlinesAttrs unlinesValues unwords;
6 inherit (config) networking;
7 inherit (config.services) dovecot2 postfix openldap;
8 when = x: y: if x == null then "" else y;
9 extSep = postfix.recipientDelimiter;
11 # NOTE: nixpkgs' dovecot2.stateDir is currently not exported
12 stateDir = "/var/lib/dovecot";
13 mailDir = "${stateDir}/mail";
14 sieveDir = "${stateDir}/sieve";
15 authDir = "${stateDir}/auth";
16 authUser = dovecot2.mailUser; # TODO: php_roundcube
17 authGroup = dovecot2.mailGroup; # TODO: php_roundcube
18 escapeGroup = lib.stringAsChars (c: if "a"<=c && c<="z"
22 domainGroup = escapeGroup "${networking.domainBase}";
24 { target = "dovecot/${networking.domain}/dovecot-ldap.conf";
25 source = pkgs.writeText "dovecot-ldap.conf" ''
30 base = ou=posix,${openldap.domainSuffix}
33 # NOTE: sufficient for small systems and uses less resources.
39 #dn = cn=admin,${openldap.domainSuffix}
40 #dnpass = useless with sasl_mech=EXTERNAL
42 #auth_bind_userdn = cn=%n,ou=accounts,ou=posix,dc=${openldap.domainSuffix}
44 # dovecot passdb query
45 # DOC: http://wiki2.dovecot.org/PasswordDatabase/ExtraFields
46 pass_filter = (&(objectClass=posixAccount)(uid=%n)(mailEnabled=TRUE))
47 # TODO: userdb_quota_rule=*:storage=
48 pass_attrs = userPassword=password,\
49 uidNumber=userdb_uid,\
50 gidNumber=userdb_gid,\
51 mailGroupMember=userdb_mail_access_groups=${domainGroup},\
52 quotaBytes=userdb_quota_rule=*:bytes=%{ldap:quotaBytes},\
54 #homeDirectory=userdb_home
55 default_pass_scheme = CRYPT
57 # dovecot userdb query
58 # DOC: http://wiki2.dovecot.org/UserDatabase/ExtraFields
59 user_filter = (&(objectClass=posixAccount)(uid=%n)(mailEnabled=TRUE))
60 #user_filter = (&(objectClass=inetOrgPerson)(uid=%n))
61 #user_attrs = homeDirectory=home,uidNumber=uid,gidNumber=gid
62 #user_attrs = mailHomeDirectory=home,\
63 # mailStorageDirectory=mail,\
66 # mailQuota=quota_rule=*:bytes=%$
69 iterate_attrs = =user=%{ldap:uid}@${networking.domain}
70 iterate_filter = (&(objectClass=posixAccount)(mailEnabled=TRUE))
74 dovecot-virtual-pop3 = pkgs.writeTextFile {
75 name = "dovecot-virtual-pop3";
76 destination = "/pop3/INBOX/dovecot-virtual";
84 learn-spam = pkgs.writeShellScriptBin "learn-spam.sh" ''
85 exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/learner.sock learn_spam
87 learn-ham = pkgs.writeShellScriptBin "learn-ham.sh" ''
88 exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/learner.sock learn_ham
90 sieve_pipe_bin_dir = pkgs.buildEnv {
91 name = "sieve_pipe_bin_dir";
92 pathsToLink = [ "/bin" ];
98 dovecot-virtual-all = pkgs.writeTextFile {
99 name = "dovecot-virtual-all";
100 destination = "/All/dovecot-virtual";
106 dovecot-virtual-recents = pkgs.writeTextFile {
107 name = "dovecot-virtual-recents";
108 destination = "/Recents/dovecot-virtual";
114 dovecot-virtual = pkgs.buildEnv {
115 name = "dovecot-virtual";
116 pathsToLink = [ "/" ];
119 dovecot-virtual-recents
125 dovecot/autoconfig.nix
127 #services.postfix.mapFiles."transport-dovecot" =
128 # toFile "transport-dovecot"
130 # (lib.mapAttrsToList
131 # (dom: {...}: "${transportSubDomain}.${dom} lmtp:unix:private/dovecot-lmtp")
132 # dovecot2.domains));
133 systemd.services.dovecot2 = {
137 "dovecot.${networking.domainBase}.key.pem-key.service"
139 restartTriggers = map (f: f.source) etc_dovecot;
142 "dovecot.${networking.domainBase}.key.pem" = {
143 text = pass "x509/${networking.domainBase}/key.pem";
144 user = dovecot2.user;
146 destDir = "/run/keys/";
147 permissions = "0400"; # WARNING: not enforced when deployment.storeKeysOnMachine = true
150 environment.etc = etc_dovecot;
151 users.users."${dovecot2.mailUser}".isSystemUser = true; # Fix nixpkgs
152 services.dovecot2 = {
154 mailUser = "dovemail";
155 mailGroup = "dovemail";
157 pkgs.dovecot_pigeonhole
158 pkgs.dovecot_fts_xapian
163 require [ "date", "fileinto", "mailbox", "variables" ];
165 if currentdate :matches "year" "*" { set "year" "''${1}"; }
166 if currentdate :matches "month" "*" { set "month" "''${1}"; }
168 if exists "List-ID" {
169 if header :matches "List-ID" "*<*.*.*.*>*" {
171 set "domain" "''${4}";
173 elsif header :matches "List-ID" "*<*.*.*>*" {
175 set "domain" "''${3}";
177 fileinto :create "Listes+''${domain}+''${list}+''${year}+''${month}";
182 require [ "imap4flags" ];
184 if header :contains "X-Spam-Level" "***" {
196 if envelope :matches :detail "TO" "*" {
197 set "extension" "''${1}";
199 if not string :is "''${extension}" "" {
200 fileinto :create "INBOX+''${extension}";
205 require ["vnd.dovecot.pipe", "copy", "imapsieve", "variables", "imap4flags", "environment"];
207 if environment :is "imap.changedflags" "Junk" {
208 if hasflag :is "Junk" {
209 pipe :copy :try "learn-spam.sh";
210 } elsif not hasflag :is "Junk" {
211 pipe :copy :try "learn-ham.sh";
216 require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
218 if environment :matches "imap.user" "*" {
219 set "username" "''${1}";
222 pipe :copy :try "learn-spam.sh" [ "''${username}" ];
225 require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
227 if environment :matches "imap.mailbox" "*" {
228 set "mailbox" "''${1}";
231 if string "''${mailbox}" "Trash" {
235 if environment :matches "imap.user" "*" {
236 set "username" "''${1}";
239 pipe :copy :try "learn-ham.sh" [ "''${username}" ];
243 configFile = toString ( pkgs.writeText "dovecot.conf" ''
250 args = /etc/dovecot/${networking.domain}/dovecot-ldap.conf
251 default_fields = userdb_mail_access_groups=${domainGroup}
258 # NOTE: this userdb is only used by lda.
260 args = /etc/dovecot/${networking.domain}/dovecot-ldap.conf
261 default_fields = mail_access_groups=${domainGroup}
265 auth_cache_verify_password_with_worker = yes
267 # driver = passwd-file
268 # args = scheme=crypt username_format=%n ${authDir}/%d/passwd
271 # # NOTE: this userdb is only used by lda.
272 # driver = passwd-file
273 # args = username_format=%n ${authDir}/%d/passwd
274 # #default_fields = home=${mailDir}/%d/%n
277 # If needed, may be overrided by userdb_mail
278 mail_home = ${mailDir}/%d/%n
279 # NOTE: if needed, may be overrided by userdb_mail
280 # NOTE: INDEX and CONTROL are on a partition without quota, as explain in the doc.
281 # SEE: http://wiki2.dovecot.org/Quota/FS
282 auth_mechanisms = plain login
283 # NOTE: postfix does not supply a client cert.
284 auth_ssl_require_client_cert = no
285 #auth_ssl_username_from_cert = yes
287 # NOTE: lowercase the username, help with LDAP?
288 auth_username_format = %Lu
289 default_internal_user = ${dovecot2.user}
290 default_internal_group = ${dovecot2.group}
291 disable_plaintext_auth = yes
292 # NOTE: sync with LDAP's data.
293 first_valid_uid = 1000
294 lda_mailbox_autocreate = yes
295 lda_mailbox_autosubscribe = yes
297 log_timestamp = "%Y-%m-%d %H:%M:%S "
298 mail_location = sdbox:/var/lib/dovecot/mail/%d/%n/mail.d:UTF-8:CONTROL=/var/lib/dovecot/control/%d/%n:INDEX=/var/lib/dovecot/index/%d/%n
299 # No dirty syncs while I'm using neomutt directly on the Maildirs
300 #maildir_very_dirty_syncs = yes
301 #maildir_copy_with_hardlinks = yes
308 separator = ${dirSep}
313 # NOTE: always listed in the LIST command.
315 # NOTE: how to access the other users' mailboxes.
316 # NOTE: %var expands to the logged in user's variable, while
317 # %%var expands to the other users' variables.
318 # NOTE: INDEX and CONTROL are shared, INDEXPVT is not.
319 location = sdbox:${mailDir}/%%d/%%n/mail.d:UTF-8:CONTROL=${stateDir}/control/%%d/%%n/Shared:INDEX=${stateDir}/index/%%d/%%n/Shared:INDEXPVT=${stateDir}/index/%d/%n/Shared/%%n
320 prefix = Partages+%%n+
321 separator = ${dirSep}
326 separator = ${dirSep}
330 location = virtual:${dovecot-virtual}:UTF-8:INDEX=${stateDir}/index/%d/%n/virtual
332 # Default VSZ (virtual memory size) limit for service processes. This is mainly
333 # intended to catch and kill processes that leak memory before they eat up everything.
334 # Increased for fts_xapian.
335 default_vsz_limit = 1G
336 mail_plugins = $mail_plugins acl quota virtual fts fts_xapian
337 #mail_uid = ${dovecot2.mailUser}
338 #mail_gid = ${dovecot2.mailGroup}
339 # NOTE: each user has a dedicated (uid,gid) pair
340 #mail_privileged_group = mail
341 #mail_access_groups =
343 plugin = fts fts_xapian
345 acl = vfile:/etc/dovecot/acl/global.d
347 # NOTE: to let users LIST mailboxes shared by other users,
348 # Dovecot needs a shared mailbox dictionary.
349 # FIXME: %d not working with userdb ldap
350 acl_shared_dict = file:${stateDir}/acl/%d/acl.db
354 fts_autoindex_exclude = \Junk
355 fts_autoindex_exclude2 = \Trash
357 # 2 and 20 are the NGram values for header fields, which means the
358 # keywords created for fields (To, Cc, ...) are between is 2 and 20 chars long.
359 # Full words are also added by default.
360 fts_xapian = partial=2 full=20 verbose=0
362 quota = maildir:User quota
363 quota_rule = *:storage=256M
364 quota_rule2 = Trash:storage=+64M
365 quota_max_mail_size = 20M
366 #quota_exceeded_message = </path/to/quota_exceeded_message.txt
367 quota_warning = storage=95%% quota-warning 95 %u
368 quota_warning2 = storage=80%% quota-warning 80 %u
369 quota_warning3 = -storage=100%% quota-warning below %u
371 # Let extension.sieve do the handling of the detail
372 #lmtp_save_to_detail_mailbox = yes
373 recipient_delimiter = ${extSep}
375 #sieve_default = file:${mailDir}/%u/default.sieve
376 #sieve_default_name = default
377 sieve_plugins = sieve_imapsieve sieve_extprograms
378 sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
379 #sieve_extensions = +editheader
380 sieve = file:~/sieve.d;active=~/sieve
381 sieve_after = ${sieveDir}/after.d/
382 sieve_before = ${sieveDir}/before.d/
383 sieve_pipe_bin_dir = ${sieve_pipe_bin_dir}/bin
384 sieve_global = ${sieveDir}/global.d/
385 sieve_max_script_size = 1M
386 sieve_quota_max_scripts = 0
387 sieve_quota_max_storage = 10M
388 #sieve_spamtest_max_value = 10
389 #sieve_spamtest_status_header = X-Spam-Score
390 #sieve_spamtest_status_type = strlen
391 sieve_user_log = /var/log/dovecot/%d/sieve.%n.log
392 # Enables support for user Sieve scripts in IMAP
393 #imapsieve_url = sieve://mail.${networking.domain}:4190
395 # When a flag changes, spam or ham according to the \Junk or \NonJunk flags
396 imapsieve_mailbox1_name = *
397 imapsieve_mailbox1_causes = FLAG
398 imapsieve_mailbox1_before = file:${sieveDir}/global.d/spam-or-ham.sieve
400 # From elsewhere to Junk folder
401 imapsieve_mailbox2_name = Pourriel
402 imapsieve_mailbox2_causes = COPY APPEND
403 imapsieve_mailbox2_before = file:${sieveDir}/global.d/report-spam.sieve
405 # From Junk folder to elsewhere
406 imapsieve_mailbox3_name = *
407 imapsieve_mailbox3_from = Pourriel
408 imapsieve_mailbox3_causes = COPY
409 imapsieve_mailbox3_before = file:${sieveDir}/global.d/report-ham.sieve
411 # If you have Dovecot v2.2.8+ you may get a significant performance improvement with fetch-headers:
412 imapc_features = $imapc_features fetch-headers
413 # Read multiple mails in parallel, improves performance
414 mail_prefetch_count = 20
415 service quota-warning {
416 executable = script ${
417 pkgs.writeScript "quota-warning" ''
421 cat << EOF | ${pkgs.dovecot}/libexec/dovecot/dovecot-lda -d $USER -o
422 "plugin/quota=maildir:User quota:noenforcing"
423 From: postmaster@${networking.domain}
424 Subject: [WARNING] your mailbox is now $PERCENT% full.
426 Please remove some mails to make room for new ones.
430 # use some unprivileged user for executing the quota warnings
431 user = ${dovecot2.user}
432 unix_listener quota-warning {
435 # Store METADATA information within user's HOME directory
436 mail_attribute_dict = file:%h/dovecot-attributes
438 #mail_max_userip_connections = 10
439 mail_plugins = $mail_plugins imap_acl imap_quota imap_sieve virtual
442 # DOC: https://wiki.dovecot.org/MailboxSettings
443 # Due to a bug in Dovecot v2.2.30+ if special-use flags are used,
444 # SPECIAL-USE needs to be added to post-login CAPABILITY response as RFC 6154 mandates.
445 imap_capability = +SPECIAL-USE
450 special_use = \Archive
454 special_use = \Drafts
456 mailbox "Brouillons" {
458 special_use = \Drafts
473 mailbox "Pourriels" {
485 mailbox "Sent Items" {
489 mailbox "Sent Messages" {
502 mailbox "Corbeille" {
510 comment = All messages from all mailboxes
515 comment = All messages from all mailboxes arrived in the past 48h
520 auth_socket_path = /var/run/dovecot/auth-userdb
521 hostname = ${networking.domain}
524 mail_plugins = $mail_plugins sieve
525 postmaster_address = postmaster${extSep}dovecot${extSep}lda@${networking.domain}
526 syslog_facility = mail
529 #info_log_path = /tmp/dovecot-lmtp.log
530 mail_plugins = $mail_plugins sieve
531 postmaster_address = postmaster${extSep}dovecot${extSep}lmtp@${networking.domain}
534 mail_plugins = $mail_plugins virtual
535 #mail_max_userip_connections = 10
536 # Virtual namespace for the virtual INBOX.
537 # Use a global directory for dovecot-virtual files.
541 # location = virtual:''${dovecot-virtual-pop3}/pop3:INDEX=${stateDir}/index/%d/%n/virtual/pop3:LAYOUT=fs
544 pop3_client_workarounds =
545 pop3_fast_size_lookups = yes
546 pop3_lock_session = yes
547 pop3_no_flag_updates = yes
548 # Use GUIDs to avoid accidental POP3 UIDL changes instead of IMAP UIDs.
549 pop3_uidl_format = %g
551 # DOC: https://wiki2.dovecot.org/Pigeonhole/ManageSieve/Configuration
553 # Maximum number of ManageSieve connections allowed for a user from each IP address.
554 # NOTE: The username is compared case-sensitively.
555 mail_max_userip_connections = 10
557 # To fool ManageSieve clients that are focused on CMU's timesieved you can specify
558 # the IMPLEMENTATION capability that the dovecot reports to clients.
559 # For example: 'Cyrus timsieved v2.2.13'
560 managesieve_implementation_string = Dovecot Pigeonhole
562 # The maximum number of compile errors that are returned to the client upon script
563 # upload or script verification.
564 managesieve_max_compile_errors = 5
566 #mail_max_userip_connections = 10
567 #managesieve_implementation_string = Dovecot Pigeonhole
568 managesieve_max_compile_errors = 5
569 #managesieve_max_line_length = 65536
570 #managesieve_notify_capability = mailto
571 #managesieve_sieve_capability = fileinto reject envelope encoded-character vacation subaddress comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment mailbox date ihave
573 protocols = imap lmtp pop3 sieve
575 #executable = lmtp -L
576 process_min_avail = ${toString config.nix.maxJobs}
577 unix_listener /var/lib/postfix/queue/private/dovecot-lmtp {
578 user = ${postfix.user}
579 group = ${postfix.group}
585 # FIXME: may be user=dovecot-auth with LDAP?
587 unix_listener auth-userdb {
588 user = ${dovecot2.user}
589 group = ${dovecot2.group}
592 unix_listener /var/lib/postfix/queue/private/auth {
593 user = ${postfix.user}
594 group = ${postfix.group}
599 # Most of the memory goes to mmap()ing files.
600 # You may need to increase this limit if you have huge mailboxes.
605 #inet_listener imap {
606 # address = 127.0.0.1
610 inet_listener imaps {
619 inet_listener pop3s {
624 service managesieve-login {
625 inet_listener sieve {
630 # Number of connections to handle before starting a new process. Typically
631 # the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0
632 # is faster. <doc/wiki/LoginProcess.txt>
635 # Number of processes to always keep waiting for more connections.
636 process_min_avail = 0
638 # If you set service_count=0, you probably need to grow this.
642 ssl_dh = <${../../../sec/openssl/dh.pem}
643 ssl_cipher_list = HIGH:!LOW:!SSLv2:!EXP:!aNULL
644 ssl_cert = <${loadFile (../../../sec + "/openssl/${networking.domainBase}/cert.self-signed.pem")}
645 ssl_key = </run/keys/dovecot.${networking.domainBase}.key.pem
646 #ssl_ca = <''${caPath}
647 #ssl_verify_client_cert = yes
649 #${lib.concatMapStringsSep "\n"
651 # local_name mail.${dom} {
652 # #ssl_ca = <''${caPath}
653 # ssl_cert = <${x509.cert dom}
654 # ssl_key = <${x509.key dom}