{pkgs, lib, config, system, ...}: let inherit (builtins) toString toFile; inherit (lib) types; inherit (pkgs.lib) unlines; inherit (config.services) dovecot2 postfix x509 openldap; 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 "_"); etc_dovecot = [ { target = "dovecot/${config.networking.domain}/dovecot-ldap.conf"; source = pkgs.writeText "dovecot-ldap.conf" '' ${lib.optionalString dovecot2.debug '' debug_level = 1 ''} # LDAP database uris = ldapi:// base = ou=posix,${openldap.domainSuffix} scope = subtree deref = never blocking = no # NOTE: sufficient for small systems and uses less resources. # LDAP auth sasl_bind = yes sasl_mech = EXTERNAL #dn = cn=admin,${openldap.domainSuffix} #dnpass = useless with sasl_mech=EXTERNAL auth_bind = no #auth_bind_userdn = cn=%n,ou=accounts,ou=posix,dc=${openldap.domainSuffix} # dovecot passdb query pass_filter = (&(objectClass=posixAccount)(uid=%n)) pass_attrs = userPassword=password,\ uidNumber=userdb_uid,\ gidNumber=userdb_gid,\ mailGroupMember=mail_access_groups,\ =user=%n@%d #homeDirectory=userdb_home #quotaBytes=userdb_quota_rule=*:bytes=%{ldap:quotaBytes} # TODO: userdb_quota_rule=*:storage= # TODO: userdb_mail_access_groups # DOC: http://wiki2.dovecot.org/PasswordDatabase/ExtraFields default_pass_scheme = CRYPT # dovecot userdb query user_filter = (&(objectClass=posixAccount)(uid=%n)) #user_filter = (&(objectClass=inetOrgPerson)(uid=%n)(mailEnabled=TRUE)) # # DOC: http://wiki2.dovecot.org/Variables #user_attrs = homeDirectory=home,uidNumber=uid,gidNumber=gid #user_attrs = mailHomeDirectory=home,\ # mailStorageDirectory=mail,\ # mailUidNumber=uid,\ # mailGidNumber=gid,\ # mailQuota=quota_rule=*:bytes=%$ # # DOC: http://wiki2.dovecot.org/UserDatabase/ExtraFields # doveadm user query iterate_attrs = =user=%{ldap:uid}@${config.networking.domain} iterate_filter = (objectClass=posixAccount) ''; } ]; in { imports = [ dovecot/autoconfig.nix ]; config = { systemd.services.dovecot2.after = [ "postfix.service" ]; systemd.services.dovecot2.restartTriggers = map (f: f.source) etc_dovecot; environment.etc = etc_dovecot; #services.postfix.mapFiles."transport-dovecot" = # toFile "transport-dovecot" # (unlines # (lib.mapAttrsToList # (dom: {...}: "${transportSubDomain}.${dom} lmtp:unix:private/dovecot-lmtp") # dovecot2.domains)); #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 ''; installSieve = '' # SEE: http://wiki2.dovecot.org/SharedMailboxes/Permissions # NOTE: 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 ''; installPOP3 = '' # Install pop3 Inbox install -D -m 0644 \ -o root \ -g root \ ${dovecot-virtual} \ ${libDir}/pop3/INBOX/dovecot-virtual ''; installDomains = '' # 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=${authDir}/${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); in unlines [ installSieve installPOP3 installDomains ]; services.dovecot2 = { enable = true; debug = true; mailUser = "dovemail"; mailGroup = "dovemail"; modules = [ #pkgs.dovecot_antispam pkgs.dovecot_pigeonhole ]; configFile = toString (pkgs.writeText "dovecot.conf" '' passdb { driver = ldap args = /etc/dovecot/${config.networking.domain}/dovecot-ldap.conf default_fields = override_fields = } userdb { driver = prefetch } userdb { # NOTE: this userdb is only used by lda. driver = ldap args = /etc/dovecot/${config.networking.domain}/dovecot-ldap.conf default_fields = override_fields = skip = found } #passdb { # driver = passwd-file # args = scheme=crypt username_format=%n ${authDir}/%d/passwd #} #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 # skip = found #} mail_home = ${mailDir}/%d/%n # NOTE: if needed, may be overrided by userdb_mail mail_location = maildir:${mailDir}/%d/%n/Maildir:LAYOUT=fs:INDEX=${libDir}/index/%d/%n:CONTROL=${libDir}/control/%d/%n # NOTE: if needed, may be overrided by userdb_mail # NOTE: INDEX and CONTROL are on a partition without quota, as explain in the doc. # SEE: http://wiki2.dovecot.org/Quota/FS auth_mechanisms = plain login auth_ssl_require_client_cert = no # NOTE: postfix does not supply a client cert. auth_ssl_username_from_cert = yes auth_verbose = yes auth_username_format = %Lu # NOTE: lowercase the username, help with LDAP? ${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 " #maildir_copy_with_hardlinks = yes 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 # FIXME: %d not working prefix = Partages+%%n+ separator = ${dirSep} subscriptions = yes type = shared } mail_plugins = $mail_plugins acl quota virtual #mail_uid = ${dovecot2.mailUser} #mail_gid = ${dovecot2.mailGroup} # NOTE: each user has a dedicated (uid,gid) pair #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 # NOTE: to let users LIST mailboxes shared by other users, # Dovecot needs a shared mailbox dictionary. # FIXME: %d not working with userdb ldap ##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 quota_max_mail_size = 20M #quota_exceeded_message =