{ pkgs, lib, config, system, ... }: let inherit (builtins) toString toFile; inherit (builtins.extraBuiltins) pass; inherit (lib) types; inherit (pkgs.lib) loadFile unlines unlinesAttrs unlinesValues unwords; inherit (config) networking; inherit (config.services) dovecot2 postfix openldap; when = x: y: if x == null then "" else y; extSep = postfix.recipientDelimiter; dirSep = extSep; # NOTE: nixpkgs' dovecot2.stateDir is currently not exported stateDir = "/var/lib/dovecot"; mailDir = "${stateDir}/mail"; sieveDir = "${stateDir}/sieve"; authDir = "${stateDir}/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 "_"); domainGroup = escapeGroup "${networking.domainBase}"; etc_dovecot = [ { target = "dovecot/${networking.domain}/dovecot-ldap.conf"; source = pkgs.writeText "dovecot-ldap.conf" '' debug_level = 0 # LDAP database uris = ldapi:// base = ou=posix,${openldap.domainSuffix} scope = subtree deref = never # NOTE: sufficient for small systems and uses less resources. blocking = no # 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 # DOC: http://wiki2.dovecot.org/PasswordDatabase/ExtraFields pass_filter = (&(objectClass=posixAccount)(uid=%n)(mailEnabled=TRUE)) # TODO: userdb_quota_rule=*:storage= pass_attrs = userPassword=password,\ uidNumber=userdb_uid,\ gidNumber=userdb_gid,\ mailGroupMember=userdb_mail_access_groups=${domainGroup},\ quotaBytes=userdb_quota_rule=*:bytes=%{ldap:quotaBytes},\ =user=%n@%d #homeDirectory=userdb_home default_pass_scheme = CRYPT # dovecot userdb query # DOC: http://wiki2.dovecot.org/UserDatabase/ExtraFields user_filter = (&(objectClass=posixAccount)(uid=%n)(mailEnabled=TRUE)) #user_filter = (&(objectClass=inetOrgPerson)(uid=%n)) #user_attrs = homeDirectory=home,uidNumber=uid,gidNumber=gid #user_attrs = mailHomeDirectory=home,\ # mailStorageDirectory=mail,\ # mailUidNumber=uid,\ # mailGidNumber=gid,\ # mailQuota=quota_rule=*:bytes=%$ # doveadm user query iterate_attrs = =user=%{ldap:uid}@${networking.domain} iterate_filter = (&(objectClass=posixAccount)(mailEnabled=TRUE)) ''; } ]; dovecot-virtual-pop3 = pkgs.writeTextFile { name = "dovecot-virtual-pop3"; destination = "/pop3/INBOX/dovecot-virtual"; text = '' All All+* all ''; }; learn-spam = pkgs.writeShellScriptBin "learn-spam.sh" '' exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/learner.sock learn_spam ''; learn-ham = pkgs.writeShellScriptBin "learn-ham.sh" '' exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/learner.sock learn_ham ''; sieve_pipe_bin_dir = pkgs.buildEnv { name = "sieve_pipe_bin_dir"; pathsToLink = [ "/bin" ]; paths = [ learn-spam learn-ham ]; }; dovecot-virtual-all = pkgs.writeTextFile { name = "dovecot-virtual-all"; destination = "/All/dovecot-virtual"; text = '' * all ''; }; dovecot-virtual-recents = pkgs.writeTextFile { name = "dovecot-virtual-recents"; destination = "/Recents/dovecot-virtual"; text = '' * all younger 172800 ''; }; dovecot-virtual = pkgs.buildEnv { name = "dovecot-virtual"; pathsToLink = [ "/" ]; paths = [ dovecot-virtual-all dovecot-virtual-recents ]; }; in { imports = [ dovecot/autoconfig.nix ]; #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" "openldap.service" "dovecot.${networking.domainBase}.key.pem-key.service" ]; restartTriggers = map (f: f.source) etc_dovecot; }; deployment.keys = { "dovecot.${networking.domainBase}.key.pem" = { text = pass "x509/${networking.domainBase}/key.pem"; user = dovecot2.user; group = "root"; destDir = "/run/keys/"; permissions = "0400"; # WARNING: not enforced when deployment.storeKeysOnMachine = true }; }; environment.etc = etc_dovecot; users.users."${dovecot2.mailUser}".isSystemUser = true; # Fix nixpkgs services.dovecot2 = { enable = true; mailUser = "dovemail"; mailGroup = "dovemail"; modules = [ pkgs.dovecot_pigeonhole pkgs.dovecot_fts_xapian ]; sieves = { global = { list = '' 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; } ''; spam = '' require [ "imap4flags" ]; if header :contains "X-Spam-Level" "***" { addflag "Junk"; } ''; extension = '' require [ "envelope" , "fileinto" , "mailbox" , "subaddress" , "variables" ]; if envelope :matches :detail "TO" "*" { set "extension" "''${1}"; } if not string :is "''${extension}" "" { fileinto :create "INBOX+''${extension}"; stop; } ''; spam-or-ham = '' require ["vnd.dovecot.pipe", "copy", "imapsieve", "variables", "imap4flags", "environment"]; if environment :is "imap.changedflags" "Junk" { if hasflag :is "Junk" { pipe :copy :try "learn-spam.sh"; } elsif not hasflag :is "Junk" { pipe :copy :try "learn-ham.sh"; } } ''; report-spam = '' require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; if environment :matches "imap.user" "*" { set "username" "''${1}"; } pipe :copy :try "learn-spam.sh" [ "''${username}" ]; ''; report-ham = '' require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; if environment :matches "imap.mailbox" "*" { set "mailbox" "''${1}"; } if string "''${mailbox}" "Trash" { stop; } if environment :matches "imap.user" "*" { set "username" "''${1}"; } pipe :copy :try "learn-ham.sh" [ "''${username}" ]; ''; }; }; configFile = toString ( pkgs.writeText "dovecot.conf" '' auth_debug = yes mail_debug = yes verbose_ssl = yes passdb { driver = ldap args = /etc/dovecot/${networking.domain}/dovecot-ldap.conf default_fields = userdb_mail_access_groups=${domainGroup} override_fields = } userdb { driver = prefetch } userdb { # NOTE: this userdb is only used by lda. driver = ldap args = /etc/dovecot/${networking.domain}/dovecot-ldap.conf default_fields = mail_access_groups=${domainGroup} override_fields = skip = found } auth_cache_verify_password_with_worker = yes #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 #} # If needed, may be overrided by userdb_mail mail_home = ${mailDir}/%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 # NOTE: postfix does not supply a client cert. auth_ssl_require_client_cert = no #auth_ssl_username_from_cert = yes auth_verbose = yes # NOTE: lowercase the username, help with LDAP? auth_username_format = %Lu default_internal_user = ${dovecot2.user} default_internal_group = ${dovecot2.group} disable_plaintext_auth = yes # NOTE: sync with LDAP's data. first_valid_uid = 1000 lda_mailbox_autocreate = yes lda_mailbox_autosubscribe = yes listen = * log_timestamp = "%Y-%m-%d %H:%M:%S " 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 # No dirty syncs while I'm using neomutt directly on the Maildirs #maildir_very_dirty_syncs = yes #maildir_copy_with_hardlinks = yes namespace Inbox { type = private inbox = yes hidden = no list = yes prefix = separator = ${dirSep} } namespace Shared { type = shared #list = children # NOTE: always listed in the LIST command. list = yes # NOTE: how to access the other users' mailboxes. # NOTE: %var expands to the logged in user's variable, while # %%var expands to the other users' variables. # NOTE: INDEX and CONTROL are shared, INDEXPVT is not. 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 prefix = Partages+%%n+ separator = ${dirSep} subscriptions = yes } namespace Virtual { prefix = Virtual+ separator = ${dirSep} hidden = no list = yes subscriptions = no location = virtual:${dovecot-virtual}:UTF-8:INDEX=${stateDir}/index/%d/%n/virtual } # Default VSZ (virtual memory size) limit for service processes. This is mainly # intended to catch and kill processes that leak memory before they eat up everything. # Increased for fts_xapian. default_vsz_limit = 1G mail_plugins = $mail_plugins acl quota virtual fts fts_xapian #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 { plugin = fts fts_xapian acl = vfile:/etc/dovecot/acl/global.d acl_anyone = allow # NOTE: to let users LIST mailboxes shared by other users, # Dovecot needs a shared mailbox dictionary. # FIXME: %d not working with userdb ldap acl_shared_dict = file:${stateDir}/acl/%d/acl.db fts = xapian fts_autoindex = yes fts_autoindex_exclude = \Junk fts_autoindex_exclude2 = \Trash fts_enforced = yes # 2 and 20 are the NGram values for header fields, which means the # keywords created for fields (To, Cc, ...) are between is 2 and 20 chars long. # Full words are also added by default. fts_xapian = partial=2 full=20 verbose=0 quota = maildir:User quota quota_rule = *:storage=256M quota_rule2 = Trash:storage=+64M quota_max_mail_size = 20M #quota_exceeded_message = service_count = 1 # Number of processes to always keep waiting for more connections. process_min_avail = 0 # If you set service_count=0, you probably need to grow this. #vsz_limit = 64M } ssl = required ssl_dh = <${../../../sec/openssl/dh.pem} ssl_cipher_list = HIGH:!LOW:!SSLv2:!EXP:!aNULL ssl_cert = <${loadFile (../../../sec + "/openssl/${networking.domainBase}/cert.self-signed.pem")} ssl_key =