1 {pkgs, lib, config, system, ...}:
2 let inherit (builtins) toString toFile attrNames;
4 inherit (config.services) dovecot2 postfix x509;
5 unlines = lib.concatStringsSep "\n";
6 when = x: y: if x == null then "" else y;
7 extSep = postfix.recipientDelimiter;
9 libDir = "/var/lib/dovecot";
10 mailDir = "${libDir}/mail";
11 sieveDir = "${libDir}/sieve";
12 authDir = "${libDir}/auth";
13 authUser = dovecot2.mailUser; # TODO: php_roundcube
14 authGroup = dovecot2.mailGroup; # TODO: php_roundcube
15 escapeGroup = lib.stringAsChars (c: if "a"<=c && c<="z"
22 options.services.dovecot2 = {
23 domains = lib.mkOption {
25 type = types.attrsOf (types.submodule ({domain, ...}: {
26 #config.domain = lib.mkDefault domain;
28 accounts = lib.mkOption {
29 type = types.attrsOf (types.submodule ({account, ...}: {
31 password = lib.mkOption {
33 example = "{SSHA512}uyjL1KYx4z7HpfNvnKzuVxpMLD2KVueGGBvOcj7AF1EZCTVhT++IIKUVOC4xpZtWdqVD0OVmZqgYr2qpn/3t3Aj4oU0=";
34 description = ''Password.
35 Use: `doveadm pw -s SSHA512 -p "$password"`
38 aliases = lib.mkOption {
39 type = with types; listOf types.str;
40 example = [ "abuse@${config.networking.domain}" ];
42 description = ''Aliases of this account.'';
44 quota = lib.mkOption {
45 type = with types; nullOr types.str;
49 Per user quota rules. Accepted sizes are `xx k/M/G/T` with the
50 obvious meaning. Leave blank for the standard quota `100G`.
53 sieves = lib.mkOption {
54 type = with types; attrsOf str;
58 #include :personal "roundcube";
59 include :global "spam";
60 include :global "list";
61 include :global "extension";
65 groups = lib.mkOption {
66 type = with types; listOf str;
75 debug = lib.mkOption {
79 Whether to enable verbose logging or not in mail related services.
84 # config = lib.mkIf dovecot2.enable {
85 # environment.etc."nginx/site.d/autoconfig.conf".source =
86 # let servers = lib.concatMapStringsSep " "
87 # (dom: "autoconfig.${dom}")
88 # (attrNames dovecot2.domains);
89 # autoconfigSite = pkgs.writeTextFile {
90 # name = "autoconfig";
91 # destination = "/mail/config-v1.1.xml";
93 # <?xml version="1.0"?>
94 # <clientConfig version="1.1">
95 # <emailProvider id="%EMAILDOMAIN%">
96 # <!-- <displayName></displayName> -->
97 # <!-- <displayShortName></displayShortName> -->
98 # <domain>%EMAILDOMAIN%</domain>
99 # <incomingServer type="imap">
100 # <hostname>imap.%EMAILDOMAIN%</hostname>
102 # <socketType>SSL</socketType>
103 # <username>%EMAILADDRESS%</username>
104 # <authentication>password-cleartext</authentication>
106 # <incomingServer type="pop3">
107 # <hostname>pop.%EMAILDOMAIN%</hostname>
109 # <socketType>SSL</socketType>
110 # <username>%EMAILADDRESS%</username>
111 # <authentication>password-cleartext</authentication>
113 # <leaveMessagesOnServer>false</leaveMessagesOnServer>
114 # <downloadOnBiff>true</downloadOnBiff>
117 # <outgoingServer type="smtp">
118 # <hostname>smtp.%EMAILDOMAIN%</hostname>
120 # <socketType>SSL</socketType> <!-- see above -->
121 # <username>%EMAILADDRESS%</username> <!-- if smtp-auth -->
122 # <authentication>password-cleartext</authentication>
123 # <!-- <restriction>client-IP-address</restriction> -->
124 # <addThisServer>true</addThisServer>
125 # <useGlobalPreferredServer>false</useGlobalPreferredServer>
128 # <!-- <clientConfigUpdate url="https://www.example.com/config/mozilla.xml" /> -->
133 # pkgs.writeText "autoconfig.conf" ''
136 # server_name ${servers};
137 # root ${autoconfigSite};
142 # listen 443 ssl http2;
144 # server_name ${servers};
145 # root ${autoconfigSite};
150 # #services.postfix.mapFiles."transport-dovecot" =
151 # # toFile "transport-dovecot"
153 # # (lib.mapAttrsToList
154 # # (dom: {...}: "${transportSubDomain}.${dom} lmtp:unix:private/dovecot-lmtp")
155 # # dovecot2.domains));
156 # systemd.services.dovecot2.after = [ "postfix.service" ];
157 # #users.extraUsers = [
158 # # { name = "dovecot";
159 # # uid = config.ids.uids.dovecot2;
160 # # description = "Dovecot user";
161 # # group = dovecot2.group;
164 # users.extraGroups = lib.mapAttrs
166 # { name = escapeGroup "${dovecot2.mailGroup}-${domain}";
169 # systemd.services.dovecot2.preStart =
171 # pkgs.writeText "list.sieve" ''
179 # if currentdate :matches "year" "*" { set "year" "''${1}"; }
180 # if currentdate :matches "month" "*" { set "month" "''${1}"; }
182 # if exists "List-ID" {
183 # if header :matches "List-ID" "*<*.*.*.*>*" {
184 # set "list" "''${2}";
185 # set "domain" "''${4}";
187 # elsif header :matches "List-ID" "*<*.*.*>*" {
188 # set "list" "''${2}";
189 # set "domain" "''${3}";
191 # fileinto :create "Listes+''${domain}+''${list}+''${year}+''${month}";
196 # pkgs.writeText "spam.sieve" ''
201 # if header :contains "X-Spam-Level" "***" {
206 # pkgs.writeText "extension.sieve" ''
214 # if envelope :matches :detail "TO" "*" {
215 # set "extension" "''${1}";
217 # if not string :is "''${extension}" "" {
218 # fileinto :create "Plus+''${extension}";
223 # pkgs.writeText "dovecot-virtual" ''
229 # # SEE: http://wiki2.dovecot.org/SharedMailboxes/Permissions
230 # # The sticky bit is to allow the acl.db{.lock,} done by dovecot
231 # install -D -d -m 2771 \
232 # -o ${dovecot2.mailUser} \
233 # -g ${dovecot2.mailGroup} \
236 # # Install global sieves
237 # install -D -d -m 0755 \
241 # ${sieveDir}/after.d \
242 # ${sieveDir}/before.d \
243 # ${sieveDir}/global.d
244 # ln -fns ${sieveList} ${sieveDir}/global.d/list.sieve
245 # ln -fns ${sieveExtension} ${sieveDir}/global.d/extension.sieve
246 # ln -fns ${sieveSpam} ${sieveDir}/global.d/spam.sieve
247 # for f in ${sieveDir}/*/*.sieve
248 # do ${pkgs.dovecot_pigeonhole}/bin/sievec $f
251 # # Install pop3 Inbox
252 # install -D -m 0644 \
255 # ${dovecot-virtual} \
256 # ${libDir}/pop3/INBOX/dovecot-virtual
262 # + unlines (lib.mapAttrsToList (domain: {accounts, ...}:
263 # let domainGroup = escapeGroup "${dovecot2.mailGroup}-${domain}"; in
265 # install -D -d -m 1770 \
266 # -o ${dovecot2.mailUser} \
267 # -g ${domainGroup} \
268 # ${mailDir}/${domain} \
269 # ${libDir}/control/${domain} \
270 # ${libDir}/index/${domain}
271 # install -D -d -m 1770 \
272 # -o ${dovecot2.mailUser} \
275 # ${libDir}/auth/${domain}
276 # dir_passwd=${libDir}/auth/${domain}
277 # old_passwd=$dir_passwd/passwd
278 # new_passwd=$(TMPDIR= mktemp --tmpdir=$dir_passwd -t passwd.XXXXXXXX.tmp)
282 # + unlines (lib.mapAttrsToList (user: acct: ''
283 # home=${mailDir}/${domain}/${user}
285 # shell=/run/current-system/sw/bin/nologin
288 # uid=$(stat -c %u $home)
289 # gid=$(stat -c %g $home)
291 # [ "''${uid:+set}" ] || {
292 # while test exists = "$(find $(dirname $home) -mindepth 1 -maxdepth 1 -uid $new_uid -printf exists -quit)"
293 # do new_uid=$((new_uid + 1))
298 # install -D -d -o $uid -g $gid -m 2770 $home $home/Maildir
299 # install -d -o $uid -g $gid -m 0700 $home/sieve
301 # + unlines (lib.mapAttrsToList
303 # install -D -m 640 -o $uid -g $gid \
304 # ${pkgs.writeText "${n}.sieve" v} \
305 # $home/sieve/${n}.sieve
306 # ${pkgs.dovecot_pigeonhole}/bin/sievec \
307 # $home/sieve/${n}.sieve
311 # mail_access_groups=${lib.concatStringsSep "," ([domainGroup] ++ acct.groups)}
312 # quota=${if lib.isString acct.quota
313 # then ''"userdb_quota_rule=*:storage=${acct.quota}"''
315 # extra_fields="userdb_uid=$uid userdb_gid=$gid userdb_mail_access_groups=$mail_access_groups $quota"
316 # #test ! -e $old_passwd || {
317 # # # Preserve password changed by another mechanism, eg. roundcube.
318 # # # But this also does not overwrite any old password set by this config.
319 # # pass="$(sed -ne "s/^${user}:\([^:]*\):.*/\1/p" $old_passwd)"
321 # [ "''${pass:+set}" ] || {
322 # pass=${lib.escapeShellArg acct.password}
324 # printf '%s\n' >>$new_passwd \
325 # "${user}:$pass:$uid:$gid:$gecos:$home:$shell:$extra_fields"
328 # install -o ${authUser} -g ${authGroup} -m 0640 $new_passwd $old_passwd
331 # ) dovecot2.domains);
332 # services.dovecot2 = {
334 # mailUser = "dovemail";
335 # mailGroup = "dovemail";
337 # #pkgs.dovecot_antispam
338 # pkgs.dovecot_pigeonhole
340 # # ${lib.concatMapStringsSep "\n"
342 # # local_name imap.${dom} {
343 # # #ssl_ca = <''${caPath}
344 # # ssl_cert = <${x509.cert dom}
345 # # ssl_key = <${x509.key dom}
347 # # local_name pop.${dom} {
348 # # #ssl_ca = <''${caPath}
349 # # ssl_cert = <${x509.cert dom}
350 # # ssl_key = <${x509.key dom}
356 # configFile = toString (pkgs.writeText "dovecot.conf" ''
358 # driver = passwd-file
359 # args = scheme=crypt username_format=%n ${authDir}/%d/passwd
365 # # NOTE: this userdb is only used by lda.
366 # driver = passwd-file
367 # args = username_format=%n ${authDir}/%d/passwd
368 # #default_fields = home=${mailDir}/%d/%n
370 # mail_home = ${mailDir}/%d/%n
371 # auth_mechanisms = plain login
372 # # postfix does not supply a client cert.
373 # auth_ssl_require_client_cert = no
374 # auth_ssl_username_from_cert = yes
376 # ${lib.optionalString dovecot2.debug ''
381 # default_internal_user = ${dovecot2.user}
382 # default_internal_group = ${dovecot2.group}
383 # disable_plaintext_auth = yes
384 # first_valid_uid = 1000
385 # lda_mailbox_autocreate = yes
386 # lda_mailbox_autosubscribe = yes
388 # log_timestamp = "%Y-%m-%d %H:%M:%S "
389 # # NOTE: INDEX and CONTROL are on a partition without quota, as explain in the doc.
390 # # SEE: http://wiki2.dovecot.org/Quota/FS
391 # mail_location = maildir:${mailDir}/%d/%n/Maildir:LAYOUT=fs:INDEX=${libDir}/index/%d/%n:CONTROL=${libDir}/control/%d/%n
393 # # NOTE: here because protocol sieve {namespace inbox{}} does not seem to work.
398 # separator = ${dirSep}
403 # location = maildir:${mailDir}/%%d/%%n/Maildir:LAYOUT=fs:INDEX=${libDir}/index/%d/%n/Shared/%%n:CONTROL=${libDir}/control/%d/%n/Shared/%%n
404 # prefix = Partages+%%n+
405 # separator = ${dirSep}
406 # subscriptions = yes
409 # mail_plugins = $mail_plugins acl quota virtual
410 # #mail_uid = ${dovecot2.mailUser}
411 # #mail_gid = ${dovecot2.mailGroup}
412 # #mail_privileged_group = mail
413 # #mail_access_groups =
415 # acl = vfile:/etc/dovecot/acl/global.d
417 # acl_shared_dict = file:${mailDir}/%d/acl.db
418 # ##antispam_allow_append_to_spam = yes
419 # # # NOTE: pour offlineimap
420 # #antispam_backend = pipe
421 # ##antispam_crm_args = -u;${mailDir}/%d/.crm114;/usr/share/crm114/mailfilter.crm
422 # #antispam_crm_args = -u;${mailDir}/crm114;/usr/share/crm114/mailfilter.crm
423 # #antispam_crm_binary = /usr/bin/crm
424 # #antispam_debug_target = syslog
425 # ##antispam_crm_env = HOME=%h;USER=%u
426 # #antispam_ham_keywords = NonJunk
427 # #antispam_pipe_program = /usr/bin/crm
428 # #antispam_pipe_program_args = -u;${mailDir}/crm114;/usr/share/crm114/mailfilter.crm;--stats_only;--force
429 # #antispam_pipe_program_notspam_arg = --learnnonspam
430 # #antispam_pipe_program_spam_arg = --learnspam
431 # #antispam_pipe_program_unlearn_spam_args = --unlearn;--learnspam
432 # #antispam_pipe_program_unlearn_notspam_args = --unlearn;--learnnonspam
433 # #antispam_pipe_tmpdir = ${mailDir}/crm114/tmp
434 # #antispam_signature = X-CRM114-CacheID
435 # #antispam_signature_missing = move
436 # #antispam_spam = Junk
437 # #antispam_spam_keywords = Junk
438 # #antispam_trash = Trash
439 # #antispam_unsure = Unsure
440 # #antispam_verbose_debug = 0
441 # quota = maildir:User quota
442 # quota_rule = *:storage=256M
443 # quota_rule2 = Trash:storage=+64M
444 # recipient_delimiter = ${extSep}
445 # sieve = file:${mailDir}/%d/%n/sieve;active=${mailDir}/%d/%n/sieve/main.sieve
446 # #sieve_default = file:${mailDir}/%u/default.sieve
447 # #sieve_default_name = default
448 # sieve_after = ${sieveDir}/after.d/
449 # sieve_before = ${sieveDir}/before.d/
450 # sieve_dir = ${mailDir}/%d/%n/sieve/
451 # #sieve_extensions = +spamtest +spamtestplus
452 # sieve_global_dir = ${sieveDir}/global.d/
453 # sieve_max_script_size = 1M
454 # sieve_quota_max_scripts = 0
455 # sieve_quota_max_storage = 10M
456 # sieve_spamtest_max_value = 10
457 # sieve_spamtest_status_header = X-Spam-Score
458 # sieve_spamtest_status_type = strlen
459 # sieve_user_log = /var/log/dovecot/%d/sieve.%n.log
462 # #mail_max_userip_connections = 10
463 # mail_plugins = $mail_plugins imap_acl imap_quota # antispam
469 # special_use = \Drafts
472 # special_use = \Junk
475 # special_use = \Sent
477 # mailbox "Sent Messages" {
478 # special_use = \Sent
481 # special_use = \Trash
484 # separator = ${dirSep}
488 # auth_socket_path = /var/run/dovecot/auth-userdb
489 # hostname = ${machine.fqdn}
492 # mail_plugins = $mail_plugins sieve
498 # separator = ${dirSep}
500 # postmaster_address = postmaster${extSep}dovecot${extSep}lda@${machine.fqdn}
501 # syslog_facility = mail
504 # #info_log_path = /tmp/dovecot-lmtp.log
505 # mail_plugins = $mail_plugins sieve
511 # separator = ${dirSep}
513 # postmaster_address = postmaster${extSep}dovecot${extSep}lmtp@${machine.fqdn}
516 # #mail_max_userip_connections = 10
517 # # Used by ${libDir}/pop3/INBOX/dovecot-virtual
523 # separator = ${dirSep}
525 # # Virtual namespace for the virtual INBOX.
526 # # Use a global directory for dovecot-virtual files.
531 # location = virtual:${libDir}/pop3:INDEX=${libDir}/index/%d/%n/POP3:LAYOUT=fs
533 # separator = ${dirSep}
535 # pop3_client_workarounds =
536 # pop3_fast_size_lookups = yes
537 # pop3_lock_session = yes
538 # pop3_no_flag_updates = yes
539 # # Use GUIDs to avoid accidental POP3 UIDL changes instead of IMAP UIDs.
540 # pop3_uidl_format = %g
543 # #mail_max_userip_connections = 10
544 # #managesieve_implementation_string = Dovecot Pigeonhole
545 # managesieve_max_compile_errors = 5
546 # #managesieve_max_line_length = 65536
547 # #managesieve_notify_capability = mailto
548 # #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
550 # protocols = imap lmtp pop3 sieve
552 # #executable = lmtp -L
553 # process_min_avail = 2
554 # unix_listener /var/lib/postfix/queue/private/dovecot-lmtp {
555 # user = ${postfix.user}
556 # group = ${postfix.group}
563 # unix_listener auth-userdb {
564 # user = ${dovecot2.user}
565 # group = ${dovecot2.group}
568 # unix_listener /var/lib/postfix/queue/private/auth {
569 # user = ${postfix.user}
570 # group = ${postfix.group}
575 # # Most of the memory goes to mmap()ing files.
576 # # You may need to increase this limit if you have huge mailboxes.
578 # process_limit = 1024
580 # service imap-login {
581 # #inet_listener imap {
582 # # address = 127.0.0.1
586 # inet_listener imaps {
592 # process_limit = 1024
594 # service pop3-login {
595 # inet_listener pop3s {
601 # #ssl_ca = <''${caPath}
602 # ssl_cert = <${x509.cert}
603 # # Only with dovecot >= 2.3
604 # #ssl_dh = <${x509.dir}/dh.pem
605 # ssl_cipher_list = ALL:!LOW:!SSLv2:!EXP:!aNULL
606 # ssl_key = <${x509.key}
607 # #ssl_verify_client_cert = yes
614 #loginAccounts = lib.mkOption {
615 # type = types.loaOf (types.submodule ({name, ...}: {
616 # config.name = lib.mkDefault name;
618 # name = lib.mkOption {
620 # example = "user1@example.coop";
621 # description = "Username";
623 # password = lib.mkOption {
625 # example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
627 # Hashed password. Use `mkpasswd` as follows
630 # mkpasswd -m sha-512 "super secret password"
634 # aliases = lib.mkOption {
635 # type = with types; listOf types.str;
636 # example = ["abuse@example.coop" "postmaster@example.coop"];
639 # A list of aliases of this login account.
642 # catchAll = lib.mkOption {
643 # type = with types; listOf (enum dovecot2.domains);
644 # example = ["example.coop" "example2.coop"];
647 # For which domains should this account act as a catch all?
650 # sieveScript = lib.mkOption {
651 # type = with types; nullOr lines;
654 # require ["fileinto", "mailbox"];
656 # if address :is "from" "notifications@github.coop" {
657 # fileinto :create "GitHub";
661 # # This must be the last rule, it will check if list-id is set, and
662 # # file the message into the Lists folder for further investigation
663 # elsif header :matches "list-id" "<?*>" {
664 # fileinto :create "Lists";
669 # Per-user sieve script.
676 # password = "$6$vy7SOr8Cg$l1QwFSkK6YR72ASUBmMmAqg51Fqu96mPZrKzADh5aI7bEOtTzDger9JSVnUhQ/DiqhxO1N55BUikE01mWvBee1";
679 # password = "$6$gmebVgh5iJ9IyAJ5$i2aEvWZqS3iUq7mxSAhs5F./uUvQ4zmqFAdH3fsGiwabekdP.On8HCzpDCRS2nzzYNQ8ZisqyIwXf9R2rkC531";
683 # The login account of the domain. Every account is mapped to a unix user,
684 # e.g. `user1@example.coop`. To generate the passwords use `mkpasswd` as
688 # mkpasswd -m sha-512 "super secret password"
693 #extraVirtualAliases = lib.mkOption {
694 # type = with types; attrsOf (enum (builtins.attrNames machine.mail.loginAccounts));
696 # "info@example.coop" = "user1@example.coop";
697 # "postmaster@example.coop" = "user1@example.coop";
698 # "abuse@example.coop" = "user1@example.coop";
702 # Virtual Aliases. A virtual alias `"info@example2.coop" = "user1@example.coop"`
703 # means that all mail to `info@example2.coop` is forwarded to `user1@example.coop`.
704 # Note that it is expected that `postmaster@example.coop` and `abuse@example.coop` is
705 # forwarded to some valid email address. (Alternatively you can create login
706 # accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows
707 # the user `user1@example.coop` to send emails as `info@example2.coop`.