{pkgs, lib, config, system, ...}: let inherit (builtins) toString toFile attrNames; inherit (lib) types; inherit (config.services) dovecot2 postfix x509; unlines = lib.concatStringsSep "\n"; when = x: y: if x == null then "" else y; extSep = postfix.recipientDelimiter; dirSep = extSep; libDir = "/var/lib/dovecot"; mailDir = "${libDir}/mail"; sieveDir = "${libDir}/sieve"; authDir = "${libDir}/auth"; authUser = dovecot2.mailUser; # TODO: php_roundcube authGroup = dovecot2.mailGroup; # TODO: php_roundcube escapeGroup = lib.stringAsChars (c: if "a"<=c && c<="z" || "0"<=c && c<="9" || c=="-" then c else "_"); in { options.services.dovecot2 = { domains = lib.mkOption { default = {}; type = types.attrsOf (types.submodule ({domain, ...}: { #config.domain = lib.mkDefault domain; options = { accounts = lib.mkOption { type = types.attrsOf (types.submodule ({account, ...}: { options = { password = lib.mkOption { type = types.str; example = "{SSHA512}uyjL1KYx4z7HpfNvnKzuVxpMLD2KVueGGBvOcj7AF1EZCTVhT++IIKUVOC4xpZtWdqVD0OVmZqgYr2qpn/3t3Aj4oU0="; description = ''Password. Use: `doveadm pw -s SSHA512 -p "$password"` ''; }; aliases = lib.mkOption { type = with types; listOf types.str; example = [ "abuse@${config.networking.domain}" ]; default = []; description = ''Aliases of this account.''; }; quota = lib.mkOption { type = with types; nullOr types.str; default = null; example = "2G"; description = '' Per user quota rules. Accepted sizes are `xx k/M/G/T` with the obvious meaning. Leave blank for the standard quota `100G`. ''; }; sieves = lib.mkOption { type = with types; attrsOf str; default = { main = '' require ["include"]; #include :personal "roundcube"; include :global "spam"; include :global "list"; include :global "extension"; ''; }; }; groups = lib.mkOption { type = with types; listOf str; default = []; }; }; })); }; }; })); }; debug = lib.mkOption { type = types.bool; default = false; description = '' Whether to enable verbose logging or not in mail related services. ''; }; }; # config = lib.mkIf dovecot2.enable { # environment.etc."nginx/site.d/autoconfig.conf".source = # let servers = lib.concatMapStringsSep " " # (dom: "autoconfig.${dom}") # (attrNames dovecot2.domains); # autoconfigSite = pkgs.writeTextFile { # name = "autoconfig"; # destination = "/mail/config-v1.1.xml"; # text = '' # # # # # # %EMAILDOMAIN% # # imap.%EMAILDOMAIN% # 993 # SSL # %EMAILADDRESS% # password-cleartext # # # pop.%EMAILDOMAIN% # 995 # SSL # %EMAILADDRESS% # password-cleartext # # false # true # # # # smtp.%EMAILDOMAIN% # 465 # SSL # %EMAILADDRESS% # password-cleartext # # true # false # # # # # ''; # }; # in # pkgs.writeText "autoconfig.conf" '' # server { # listen 80; # server_name ${servers}; # root ${autoconfigSite}; # access_log off; # log_not_found off; # } # server { # listen 443 ssl http2; # ssl on; # server_name ${servers}; # root ${autoconfigSite}; # access_log off; # log_not_found off; # } # ''; # #services.postfix.mapFiles."transport-dovecot" = # # toFile "transport-dovecot" # # (unlines # # (lib.mapAttrsToList # # (dom: {...}: "${transportSubDomain}.${dom} lmtp:unix:private/dovecot-lmtp") # # dovecot2.domains)); # systemd.services.dovecot2.after = [ "postfix.service" ]; # #users.extraUsers = [ # # { name = "dovecot"; # # uid = config.ids.uids.dovecot2; # # description = "Dovecot user"; # # group = dovecot2.group; # # } # #]; # users.extraGroups = lib.mapAttrs # (domain: {...}: # { name = escapeGroup "${dovecot2.mailGroup}-${domain}"; # }) # dovecot2.domains; # systemd.services.dovecot2.preStart = # let sieveList = # pkgs.writeText "list.sieve" '' # require # [ "date" # , "fileinto" # , "mailbox" # , "variables" # ]; # # if currentdate :matches "year" "*" { set "year" "''${1}"; } # if currentdate :matches "month" "*" { set "month" "''${1}"; } # # if exists "List-ID" { # if header :matches "List-ID" "*<*.*.*.*>*" { # set "list" "''${2}"; # set "domain" "''${4}"; # } # elsif header :matches "List-ID" "*<*.*.*>*" { # set "list" "''${2}"; # set "domain" "''${3}"; # } # fileinto :create "Listes+''${domain}+''${list}+''${year}+''${month}"; # stop; # } # ''; # sieveSpam = # pkgs.writeText "spam.sieve" '' # require # [ "imap4flags" # ]; # # if header :contains "X-Spam-Level" "***" { # addflag "Junk"; # } # ''; # sieveExtension = # pkgs.writeText "extension.sieve" '' # require # [ "envelope" # , "fileinto" # , "mailbox" # , "subaddress" # , "variables" # ]; # if envelope :matches :detail "TO" "*" { # set "extension" "''${1}"; # } # if not string :is "''${extension}" "" { # fileinto :create "Plus+''${extension}"; # stop; # } # ''; # dovecot-virtual = # pkgs.writeText "dovecot-virtual" '' # all # all+* # all # ''; # in '' # # SEE: http://wiki2.dovecot.org/SharedMailboxes/Permissions # # The sticky bit is to allow the acl.db{.lock,} done by dovecot # install -D -d -m 2771 \ # -o ${dovecot2.mailUser} \ # -g ${dovecot2.mailGroup} \ # ${mailDir} # # # Install global sieves # install -D -d -m 0755 \ # -o root \ # -g root \ # ${sieveDir} \ # ${sieveDir}/after.d \ # ${sieveDir}/before.d \ # ${sieveDir}/global.d # ln -fns ${sieveList} ${sieveDir}/global.d/list.sieve # ln -fns ${sieveExtension} ${sieveDir}/global.d/extension.sieve # ln -fns ${sieveSpam} ${sieveDir}/global.d/spam.sieve # for f in ${sieveDir}/*/*.sieve # do ${pkgs.dovecot_pigeonhole}/bin/sievec $f # done # # # Install pop3 Inbox # install -D -m 0644 \ # -o root \ # -g root \ # ${dovecot-virtual} \ # ${libDir}/pop3/INBOX/dovecot-virtual # '' # + '' # # Install domains # new_uid=5000 # '' # + unlines (lib.mapAttrsToList (domain: {accounts, ...}: # let domainGroup = escapeGroup "${dovecot2.mailGroup}-${domain}"; in # '' # install -D -d -m 1770 \ # -o ${dovecot2.mailUser} \ # -g ${domainGroup} \ # ${mailDir}/${domain} \ # ${libDir}/control/${domain} \ # ${libDir}/index/${domain} # install -D -d -m 1770 \ # -o ${dovecot2.mailUser} \ # -g ${authGroup} \ # ${libDir}/auth \ # ${libDir}/auth/${domain} # dir_passwd=${libDir}/auth/${domain} # old_passwd=$dir_passwd/passwd # new_passwd=$(TMPDIR= mktemp --tmpdir=$dir_passwd -t passwd.XXXXXXXX.tmp) # # # Install users # '' # + unlines (lib.mapAttrsToList (user: acct: '' # home=${mailDir}/${domain}/${user} # gecos= # shell=/run/current-system/sw/bin/nologin # if test -e $home # then # uid=$(stat -c %u $home) # gid=$(stat -c %g $home) # fi # [ "''${uid:+set}" ] || { # while test exists = "$(find $(dirname $home) -mindepth 1 -maxdepth 1 -uid $new_uid -printf exists -quit)" # do new_uid=$((new_uid + 1)) # done # uid=$new_uid # gid=$new_uid # } # install -D -d -o $uid -g $gid -m 2770 $home $home/Maildir # install -d -o $uid -g $gid -m 0700 $home/sieve # '' # + unlines (lib.mapAttrsToList # (n: v: '' # install -D -m 640 -o $uid -g $gid \ # ${pkgs.writeText "${n}.sieve" v} \ # $home/sieve/${n}.sieve # ${pkgs.dovecot_pigeonhole}/bin/sievec \ # $home/sieve/${n}.sieve # '') # acct.sieves) # + '' # mail_access_groups=${lib.concatStringsSep "," ([domainGroup] ++ acct.groups)} # quota=${if lib.isString acct.quota # then ''"userdb_quota_rule=*:storage=${acct.quota}"'' # else ""} # extra_fields="userdb_uid=$uid userdb_gid=$gid userdb_mail_access_groups=$mail_access_groups $quota" # #test ! -e $old_passwd || { # # # Preserve password changed by another mechanism, eg. roundcube. # # # But this also does not overwrite any old password set by this config. # # pass="$(sed -ne "s/^${user}:\([^:]*\):.*/\1/p" $old_passwd)" # #} # [ "''${pass:+set}" ] || { # pass=${lib.escapeShellArg acct.password} # } # printf '%s\n' >>$new_passwd \ # "${user}:$pass:$uid:$gid:$gecos:$home:$shell:$extra_fields" # '') accounts) # + '' # install -o ${authUser} -g ${authGroup} -m 0640 $new_passwd $old_passwd # rm $new_passwd # '' # ) dovecot2.domains); # services.dovecot2 = { # enable = true; # mailUser = "dovemail"; # mailGroup = "dovemail"; # modules = [ # #pkgs.dovecot_antispam # pkgs.dovecot_pigeonhole # ]; # # ${lib.concatMapStringsSep "\n" # # (dom: '' # # local_name imap.${dom} { # # #ssl_ca = <''${caPath} # # ssl_cert = <${x509.cert dom} # # ssl_key = <${x509.key dom} # # } # # local_name pop.${dom} { # # #ssl_ca = <''${caPath} # # ssl_cert = <${x509.cert dom} # # ssl_key = <${x509.key dom} # # } # # '') # # dovecot2.domains # # } # # configFile = toString (pkgs.writeText "dovecot.conf" '' # passdb { # driver = passwd-file # args = scheme=crypt username_format=%n ${authDir}/%d/passwd # } # userdb { # driver = prefetch # } # userdb { # # NOTE: this userdb is only used by lda. # driver = passwd-file # args = username_format=%n ${authDir}/%d/passwd # #default_fields = home=${mailDir}/%d/%n # } # mail_home = ${mailDir}/%d/%n # auth_mechanisms = plain login # # postfix does not supply a client cert. # auth_ssl_require_client_cert = no # auth_ssl_username_from_cert = yes # auth_verbose = yes # ${lib.optionalString dovecot2.debug '' # auth_debug = yes # mail_debug = yes # verbose_ssl = yes # ''} # default_internal_user = ${dovecot2.user} # default_internal_group = ${dovecot2.group} # disable_plaintext_auth = yes # first_valid_uid = 1000 # lda_mailbox_autocreate = yes # lda_mailbox_autosubscribe = yes # listen = * # log_timestamp = "%Y-%m-%d %H:%M:%S " # # NOTE: INDEX and CONTROL are on a partition without quota, as explain in the doc. # # SEE: http://wiki2.dovecot.org/Quota/FS # mail_location = maildir:${mailDir}/%d/%n/Maildir:LAYOUT=fs:INDEX=${libDir}/index/%d/%n:CONTROL=${libDir}/control/%d/%n # namespace inbox { # # NOTE: here because protocol sieve {namespace inbox{}} does not seem to work. # inbox = yes # location = # list = yes # prefix = # separator = ${dirSep} # } # namespace { # #list = children # list = yes # location = maildir:${mailDir}/%%d/%%n/Maildir:LAYOUT=fs:INDEX=${libDir}/index/%d/%n/Shared/%%n:CONTROL=${libDir}/control/%d/%n/Shared/%%n # prefix = Partages+%%n+ # separator = ${dirSep} # subscriptions = yes # type = shared # } # mail_plugins = $mail_plugins acl quota virtual # #mail_uid = ${dovecot2.mailUser} # #mail_gid = ${dovecot2.mailGroup} # #mail_privileged_group = mail # #mail_access_groups = # plugin { # acl = vfile:/etc/dovecot/acl/global.d # acl_anyone = allow # acl_shared_dict = file:${mailDir}/%d/acl.db # ##antispam_allow_append_to_spam = yes # # # NOTE: pour offlineimap # #antispam_backend = pipe # ##antispam_crm_args = -u;${mailDir}/%d/.crm114;/usr/share/crm114/mailfilter.crm # #antispam_crm_args = -u;${mailDir}/crm114;/usr/share/crm114/mailfilter.crm # #antispam_crm_binary = /usr/bin/crm # #antispam_debug_target = syslog # ##antispam_crm_env = HOME=%h;USER=%u # #antispam_ham_keywords = NonJunk # #antispam_pipe_program = /usr/bin/crm # #antispam_pipe_program_args = -u;${mailDir}/crm114;/usr/share/crm114/mailfilter.crm;--stats_only;--force # #antispam_pipe_program_notspam_arg = --learnnonspam # #antispam_pipe_program_spam_arg = --learnspam # #antispam_pipe_program_unlearn_spam_args = --unlearn;--learnspam # #antispam_pipe_program_unlearn_notspam_args = --unlearn;--learnnonspam # #antispam_pipe_tmpdir = ${mailDir}/crm114/tmp # #antispam_signature = X-CRM114-CacheID # #antispam_signature_missing = move # #antispam_spam = Junk # #antispam_spam_keywords = Junk # #antispam_trash = Trash # #antispam_unsure = Unsure # #antispam_verbose_debug = 0 # quota = maildir:User quota # quota_rule = *:storage=256M # quota_rule2 = Trash:storage=+64M # recipient_delimiter = ${extSep} # sieve = file:${mailDir}/%d/%n/sieve;active=${mailDir}/%d/%n/sieve/main.sieve # #sieve_default = file:${mailDir}/%u/default.sieve # #sieve_default_name = default # sieve_after = ${sieveDir}/after.d/ # sieve_before = ${sieveDir}/before.d/ # sieve_dir = ${mailDir}/%d/%n/sieve/ # #sieve_extensions = +spamtest +spamtestplus # sieve_global_dir = ${sieveDir}/global.d/ # sieve_max_script_size = 1M # sieve_quota_max_scripts = 0 # sieve_quota_max_storage = 10M # sieve_spamtest_max_value = 10 # sieve_spamtest_status_header = X-Spam-Score # sieve_spamtest_status_type = strlen # sieve_user_log = /var/log/dovecot/%d/sieve.%n.log # } # protocol imap { # #mail_max_userip_connections = 10 # mail_plugins = $mail_plugins imap_acl imap_quota # antispam # namespace inbox { # inbox = yes # location = # list = yes # mailbox Drafts { # special_use = \Drafts # } # mailbox Junk { # special_use = \Junk # } # mailbox Sent { # special_use = \Sent # } # mailbox "Sent Messages" { # special_use = \Sent # } # mailbox Trash { # special_use = \Trash # } # prefix = # separator = ${dirSep} # } # } # protocol lda { # auth_socket_path = /var/run/dovecot/auth-userdb # hostname = ${machine.fqdn} # info_log_path = # log_path = # mail_plugins = $mail_plugins sieve # namespace inbox { # inbox = yes # location = # list = yes # prefix = # separator = ${dirSep} # } # postmaster_address = postmaster${extSep}dovecot${extSep}lda@${machine.fqdn} # syslog_facility = mail # } # protocol lmtp { # #info_log_path = /tmp/dovecot-lmtp.log # mail_plugins = $mail_plugins sieve # namespace inbox { # inbox = yes # location = # list = yes # prefix = # separator = ${dirSep} # } # postmaster_address = postmaster${extSep}dovecot${extSep}lmtp@${machine.fqdn} # } # protocol pop3 { # #mail_max_userip_connections = 10 # # Used by ${libDir}/pop3/INBOX/dovecot-virtual # namespace all { # hidden = yes # list = no # location = # prefix = all+ # separator = ${dirSep} # } # # Virtual namespace for the virtual INBOX. # # Use a global directory for dovecot-virtual files. # namespace inbox { # inbox = yes # hidden = yes # list = no # location = virtual:${libDir}/pop3:INDEX=${libDir}/index/%d/%n/POP3:LAYOUT=fs # prefix = pop3+ # separator = ${dirSep} # } # pop3_client_workarounds = # pop3_fast_size_lookups = yes # pop3_lock_session = yes # pop3_no_flag_updates = yes # # Use GUIDs to avoid accidental POP3 UIDL changes instead of IMAP UIDs. # pop3_uidl_format = %g # } # protocol sieve { # #mail_max_userip_connections = 10 # #managesieve_implementation_string = Dovecot Pigeonhole # managesieve_max_compile_errors = 5 # #managesieve_max_line_length = 65536 # #managesieve_notify_capability = mailto # #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 # } # protocols = imap lmtp pop3 sieve # service lmtp { # #executable = lmtp -L # process_min_avail = 2 # unix_listener /var/lib/postfix/queue/private/dovecot-lmtp { # user = ${postfix.user} # group = ${postfix.group} # mode = 0600 # } # #user = mail # } # service auth { # user = root # unix_listener auth-userdb { # user = ${dovecot2.user} # group = ${dovecot2.group} # mode = 0660 # } # unix_listener /var/lib/postfix/queue/private/auth { # user = ${postfix.user} # group = ${postfix.group} # mode = 0660 # } # } # service imap { # # Most of the memory goes to mmap()ing files. # # You may need to increase this limit if you have huge mailboxes. # #vsz_limit = # process_limit = 1024 # } # service imap-login { # #inet_listener imap { # # address = 127.0.0.1 # # port = 143 # # ssl = no # # } # inet_listener imaps { # port = 993 # ssl = yes # } # } # service pop3 { # process_limit = 1024 # } # service pop3-login { # inet_listener pop3s { # port = 995 # ssl = yes # } # } # ssl = required # #ssl_ca = <''${caPath} # ssl_cert = <${x509.cert} # # Only with dovecot >= 2.3 # #ssl_dh = <${x509.dir}/dh.pem # ssl_cipher_list = ALL:!LOW:!SSLv2:!EXP:!aNULL # ssl_key = <${x509.key} # #ssl_verify_client_cert = yes # ''); # }; # }; } #loginAccounts = lib.mkOption { # type = types.loaOf (types.submodule ({name, ...}: { # config.name = lib.mkDefault name; # options = { # name = lib.mkOption { # type = types.str; # example = "user1@example.coop"; # description = "Username"; # }; # password = lib.mkOption { # type = types.str; # example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; # description = '' # Hashed password. Use `mkpasswd` as follows # # ``` # mkpasswd -m sha-512 "super secret password" # ``` # ''; # }; # aliases = lib.mkOption { # type = with types; listOf types.str; # example = ["abuse@example.coop" "postmaster@example.coop"]; # default = []; # description = '' # A list of aliases of this login account. # ''; # }; # catchAll = lib.mkOption { # type = with types; listOf (enum dovecot2.domains); # example = ["example.coop" "example2.coop"]; # default = []; # description = '' # For which domains should this account act as a catch all? # ''; # }; # sieveScript = lib.mkOption { # type = with types; nullOr lines; # default = null; # example = '' # require ["fileinto", "mailbox"]; # # if address :is "from" "notifications@github.coop" { # fileinto :create "GitHub"; # stop; # } # # # This must be the last rule, it will check if list-id is set, and # # file the message into the Lists folder for further investigation # elsif header :matches "list-id" "" { # fileinto :create "Lists"; # stop; # } # ''; # description = '' # Per-user sieve script. # ''; # }; # }; # })); # example = { # user1 = { # password = "$6$vy7SOr8Cg$l1QwFSkK6YR72ASUBmMmAqg51Fqu96mPZrKzADh5aI7bEOtTzDger9JSVnUhQ/DiqhxO1N55BUikE01mWvBee1"; # }; # user2 = { # password = "$6$gmebVgh5iJ9IyAJ5$i2aEvWZqS3iUq7mxSAhs5F./uUvQ4zmqFAdH3fsGiwabekdP.On8HCzpDCRS2nzzYNQ8ZisqyIwXf9R2rkC531"; # }; # }; # description = '' # The login account of the domain. Every account is mapped to a unix user, # e.g. `user1@example.coop`. To generate the passwords use `mkpasswd` as # follows # # ``` # mkpasswd -m sha-512 "super secret password" # ``` # ''; # default = {}; #}; #extraVirtualAliases = lib.mkOption { # type = with types; attrsOf (enum (builtins.attrNames machine.mail.loginAccounts)); # example = { # "info@example.coop" = "user1@example.coop"; # "postmaster@example.coop" = "user1@example.coop"; # "abuse@example.coop" = "user1@example.coop"; # }; # default = {}; # description = '' # Virtual Aliases. A virtual alias `"info@example2.coop" = "user1@example.coop"` # means that all mail to `info@example2.coop` is forwarded to `user1@example.coop`. # Note that it is expected that `postmaster@example.coop` and `abuse@example.coop` is # forwarded to some valid email address. (Alternatively you can create login # accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows # the user `user1@example.coop` to send emails as `info@example2.coop`. # ''; #};