diff --git a/default.nix b/default.nix index 3e70624..cb1f4d5 100644 --- a/default.nix +++ b/default.nix @@ -654,11 +654,8 @@ in }; lmtpSaveToDetailMailbox = mkOption { - type = types.enum [ - "yes" - "no" - ]; - default = "yes"; + type = types.bool; + default = true; description = '' If an email address is delimited by a "+", should it be filed into a mailbox matching the string after the "+"? For example, @@ -882,25 +879,31 @@ in mailboxes = mkOption { description = '' - The mailboxes for dovecot. + The default mailboxes for Dovecot maildirs. + + The [`special_use`] option must refer to an [RFC6154 2] attribute and lead with an escaped backslash. + Depending on the mail client used it might be necessary to change some mailbox's name. + + [special_use]: https://doc.dovecot.org/2.3/configuration_manual/namespace/#core_setting-namespace/mailbox/special_use + [RFC6154 2]: https://datatracker.ietf.org/doc/html/rfc6154.html#section-2 ''; default = { Trash = { auto = "no"; - specialUse = "Trash"; + special_use = "\\Trash"; }; Junk = { auto = "subscribe"; - specialUse = "Junk"; + special_use = "\\Junk"; }; Drafts = { auto = "subscribe"; - specialUse = "Drafts"; + special_use = "\\Drafts"; }; Sent = { auto = "subscribe"; - specialUse = "Sent"; + special_use = "\\Sent"; }; }; }; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index b022996..cc4f564 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -32,6 +32,13 @@ with (import ./common.nix { }); let + inherit (lib) + mapAttrs' + mkForce + mkIf + nameValuePair + ; + cfg = config.mailserver; passwdDir = "/run/dovecot2"; @@ -39,8 +46,6 @@ let userdbFile = "${passwdDir}/userdb"; # This file contains the ldap bind password ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext"; - boolToYesNo = x: if x then "yes" else "no"; - listToLine = lib.concatStringsSep " "; listToMultiAttrs = keyPrefix: attrs: lib.listToAttrs ( @@ -53,14 +58,6 @@ let maildirLayoutAppendix = lib.optionalString (cfg.storage.directoryLayout == "fs") ":LAYOUT=fs"; maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8"; - # https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory - # Mail directory below the home directory - dovecotMaildir = - "maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}" - + (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}"); - - postfixCfg = config.services.postfix; - ldapConfig = pkgs.writeTextFile { name = "dovecot-ldap.conf.ext.template"; text = '' @@ -128,12 +125,13 @@ let lib.mapAttrsToList ( name: _: if lib.elem name accountsWithPlaintextPasswordFiles then - "${name}:${"$(sed -n '1{p;p;q}' ${passwordFiles."${name}"} | ${lib.getExe' pkgs.dovecot "doveadm"} pw)"}::::::" + "${name}:${"$(sed -n '1{p;p;q}' ${passwordFiles."${name}"} | ${lib.getExe' config.services.dovecot2.package "doveadm"} pw)"}::::::" else "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" ) cfg.accounts )} EOF + chown dovecot2:dovecot2 ${passwdFile} cat < ${userdbFile} ${lib.concatStringsSep "\n" ( @@ -144,10 +142,11 @@ let ) cfg.accounts )} EOF + chown dovecot2:dovecot2 ${userdbFile} ''; junkMailboxes = builtins.attrNames ( - lib.filterAttrs (_: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes + lib.filterAttrs (_: v: v ? "special_use" && v.special_use == "\\Junk") cfg.mailboxes ); junkMailboxNumber = builtins.length junkMailboxes; # The assertion guarantees there is exactly one Junk mailbox. @@ -163,30 +162,13 @@ let else scope ); - - ftsPluginSettings = { - fts = "flatcurve"; - fts_languages = listToLine cfg.fullTextSearch.languages; - fts_tokenizers = listToLine [ - "generic" - "email-address" - ]; - fts_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian - fts_flatcurve_substring_search = boolToYesNo cfg.fullTextSearch.substringSearch; - fts_filters = listToLine cfg.fullTextSearch.filters; - fts_header_excludes = listToLine cfg.fullTextSearch.headerExcludes; - fts_autoindex = boolToYesNo cfg.fullTextSearch.autoIndex; - fts_enforced = cfg.fullTextSearch.enforced; - } - // (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude); - in { config = lib.mkIf cfg.enable { assertions = [ { assertion = junkMailboxNumber == 1; - message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)"; + message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special_use' flag set to '\\Junk' (${builtins.toString junkMailboxNumber} have been found)"; } ]; @@ -215,7 +197,7 @@ in # the global config and tries to open shared libraries configured in there, # which are usually not compatible. environment.systemPackages = [ - pkgs.dovecot_pigeonhole + pkgs.dovecot_pigeonhole_0_5 ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve; @@ -223,30 +205,9 @@ in environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; services.dovecot2 = { + package = pkgs.dovecot_2_3; enable = true; - enableImap = cfg.enableImap || cfg.enableImapSsl; - enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl; - enablePAM = false; - enableQuota = true; - mailGroup = cfg.storage.group; - mailUser = cfg.storage.owner; - mailLocation = dovecotMaildir; - sslServerCert = x509CertificateFile; - sslServerKey = x509PrivateKeyFile; - enableDHE = lib.mkDefault false; - enableLmtp = true; - mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ - "fts" - "fts_flatcurve" - ]; - protocols = lib.optional cfg.enableManageSieve "sieve"; - - pluginSettings = { - sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; - sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; - sieve_default_name = "default"; - } - // (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings); + enablePAM = mkForce false; sieve = { extensions = [ @@ -285,196 +246,232 @@ in } ]; - mailboxes = cfg.mailboxes; + settings = { + # vmail user + mail_uid = cfg.storage.owner; + mail_gid = cfg.storage.group; + mail_access_groups = cfg.storage.group; - extraConfig = '' - #Extra Config - ${lib.optionalString cfg.debug.dovecot '' - mail_debug = yes - auth_debug = yes - verbose_ssl = yes - ''} + # authentication + auth_mechanisms = [ + "plain" + "login" + ]; + disable_plaintext_auth = true; - ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) '' - service imap-login { - inet_listener imap { - ${ - if cfg.enableImap then - '' - port = 143 - '' - else - '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - '' - } - } - inet_listener imaps { - ${ - if cfg.enableImapSsl then - '' - port = 993 - ssl = yes - '' - else - '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - '' - } - } - } - ''} - ${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) '' - service pop3-login { - inet_listener pop3 { - ${ - if cfg.enablePop3 then - '' - port = 110 - '' - else - '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - '' - } - } - inet_listener pop3s { - ${ - if cfg.enablePop3Ssl then - '' - port = 995 - ssl = yes - '' - else - '' - # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html - port = 0 - '' - } - } - } - ''} + # backend services + "service auth" = { + "unix_listener auth" = { + mode = "0660"; + user = config.services.postfix.user; + group = config.services.postfix.group; + }; + }; + "service indexer-worker" = mkIf (cfg.fullTextSearch.memoryLimit != null) { + vsz_limit = "${toString cfg.fullTextSearch.memoryLimit} MB"; + }; + "service lmtp" = { + "unix_listener dovecot-lmtp" = { + group = config.services.postfix.group; + mode = "0600"; + user = config.services.postfix.user; + }; + vsz_limit = "${toString cfg.lmtpMemoryLimit} MB"; + }; + "service quota-status" = { + executable = toString [ + "${config.services.dovecot2.package}/libexec/dovecot/quota-status" + "-p" + "postfix" + ]; + "unix_listener quota-status" = { + user = "postfix"; + }; + client_limit = 1; + vsz_limit = "${toString cfg.quotaStatusMemoryLimit} MB"; + }; - protocol imap { - mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} - mail_plugins = $mail_plugins imap_sieve - } + # frontend services + "service imap-login" = mkIf (cfg.enableImap || cfg.enableImapSsl) { + "inet_listener imap" = { + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = if cfg.enableImap then 143 else 0; + }; + "inet_listener imaps" = { + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = if cfg.enableImapSsl then 993 else 0; + ssl = true; + }; + }; + "service pop3-login" = mkIf (cfg.enablePop3 || cfg.enablePop3Ssl) { + "inet_listener pop3" = { + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = if cfg.enablePop3 then 110 else 0; + }; + "inet_listener pop3s" = { + # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html + port = if cfg.enablePop3Ssl then 995 else 0; + ssl = true; + }; + }; + "service imap" = { + vsz_limit = "${toString cfg.imapMemoryLimit} MB"; + }; - service imap { - vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB - } + # protocols + protocols = [ + "lmtp" + ] + ++ lib.optionals (cfg.enableImap || cfg.enableImapSsl) [ "imap" ] + ++ lib.optionals (cfg.enablePop3 || cfg.enablePop3Ssl) [ "pop3" ] + ++ lib.optionals cfg.enableManageSieve [ "sieve" ]; - protocol pop3 { - mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} - } + "protocol lmtp" = { + mail_plugins = [ + "$mail_plugins" + "sieve" + ]; + }; + "protocol imap" = { + mail_max_userip_connections = cfg.maxConnectionsPerUser; + mail_plugins = [ + "$mail_plugins" + "imap_quota" + "imap_sieve" + ]; + }; + "protocol pop3" = { + mail_max_userip_connections = cfg.maxConnectionsPerUser; + }; - mail_access_groups = ${cfg.storage.group} + # globally enabled plugins + mail_plugins = [ + "$mail_plugins" + "quota" + ] + ++ lib.optionals cfg.fullTextSearch.enable [ + "fts" + "fts_flatcurve" + ]; + # tls settings + ssl_cert = "<${x509CertificateFile}"; + ssl_key = "<${x509PrivateKeyFile}"; # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7 - ssl = required - ssl_min_protocol = TLSv1.2 - ssl_prefer_server_ciphers = no - ssl_cipher_list = ${ - lib.concatStringsSep ":" [ - # TLS1.3 - "TLS_AES_128_GCM_SHA256" - "TLS_CHACHA20_POLY1305_SHA256" - "TLS_AES_256_GCM_SHA384" - # TLS1.2 - # EC key material - "ECDHE-ECDSA-AES128-GCM-SHA256" - "ECDHE-ECDSA-CHACHA20-POLY1305" - "ECDHE-ECDSA-AES256-GCM-SHA384" - # RSA key material - "ECDHE-RSA-AES128-GCM-SHA256" - "ECDHE-RSA-CHACHA20-POLY1305" - "ECDHE-RSA-AES256-GCM-SHA384" - ] - } - ssl_curve_list = X25519MLKEM768:X25519:prime256v1:secp384r1 + ssl = "required"; + ssl_min_protocol = "TLSv1.2"; + ssl_prefer_server_ciphers = false; + ssl_cipher_list = lib.concatStringsSep ":" [ + # TLS1.3 + "TLS_AES_128_GCM_SHA256" + "TLS_CHACHA20_POLY1305_SHA256" + "TLS_AES_256_GCM_SHA384" + # TLS1.2 + # EC key material + "ECDHE-ECDSA-AES128-GCM-SHA256" + "ECDHE-ECDSA-CHACHA20-POLY1305" + "ECDHE-ECDSA-AES256-GCM-SHA384" + # RSA key material + "ECDHE-RSA-AES128-GCM-SHA256" + "ECDHE-RSA-CHACHA20-POLY1305" + "ECDHE-RSA-AES256-GCM-SHA384" + ]; + ssl_curve_list = lib.concatStringsSep ":" [ + "X25519MLKEM768" + "X25519" + "prime256v1" + "secp384r1" + ]; - service lmtp { - unix_listener dovecot-lmtp { - group = ${postfixCfg.group} - mode = 0600 - user = ${postfixCfg.user} + # default mail storage + # https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory + mail_location = + "maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}" + + (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}"); + + passdb = [ + { + driver = "passwd-file"; + args = "${passwdFile}"; } - vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB - } - - service quota-status { - inet_listener { - port = 0 + ] + ++ lib.optionals cfg.ldap.enable [ + { + driver = "ldap"; + args = "${ldapConfFile}"; } - unix_listener quota-status { - user = postfix + ]; + userdb = [ + { + driver = "passwd-file"; + args = "${userdbFile}"; + default_fields = [ + "home=${cfg.storage.path}/%{domain}/%{username}" + "uid=${builtins.toString cfg.storage.uid}" + "gid=${builtins.toString cfg.storage.gid}" + ]; } - vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB - } - - recipient_delimiter = ${cfg.recipientDelimiter} - lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} - - protocol lmtp { - mail_plugins = $mail_plugins sieve - } - - passdb { - driver = passwd-file - args = ${passwdFile} - } - - userdb { - driver = passwd-file - args = ${userdbFile} - default_fields = \ - home=${cfg.storage.path}/%{domain}/%{username} \ - uid=${builtins.toString cfg.storage.uid} \ - gid=${builtins.toString cfg.storage.uid} - } - - ${lib.optionalString cfg.ldap.enable '' - passdb { - driver = ldap - args = ${ldapConfFile} + ] + ++ lib.optionals cfg.ldap.enable [ + { + driver = "ldap"; + args = "${ldapConfFile}"; + override_fields = [ + "uid=${toString cfg.storage.uid}" + "gid=${toString cfg.storage.gid}" + ]; } + ]; - userdb { - driver = ldap - args = ${ldapConfFile} - override_fields = \ - uid=${toString cfg.storage.uid} \ - gid=${toString cfg.storage.uid} + # default user mailboxes + "namespace inbox" = { + inbox = true; + separator = "."; + } + // mapAttrs' (name: value: nameValuePair ''mailbox "${name}"'' value) cfg.mailboxes; + lda_mailbox_autosubscribe = true; + lda_mailbox_autocreate = true; + + # subaddressing + recipient_delimiter = cfg.recipientDelimiter; + lmtp_save_to_detail_mailbox = cfg.lmtpSaveToDetailMailbox; + + plugin = { + quota_rule = "*:storage=100G"; # TODO: quota option + quota = "count:User quota"; # per virtual mail user quota + quota_status_success = "DUNNO"; + quota_status_nouser = "DUNNO"; + quota_status_overquota = "552 5.2.2 Mailbox is full"; + quota_grace = "10%%"; + quota_vsizes = true; + + sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; + sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; + sieve_default_name = "default"; + } + // lib.optionalAttrs cfg.fullTextSearch.enable ( + { + fts = "flatcurve"; + fts_languages = cfg.fullTextSearch.languages; + fts_tokenizers = [ + "generic" + "email-address" + ]; + fts_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian + fts_flatcurve_substring_search = cfg.fullTextSearch.substringSearch; + fts_filters = cfg.fullTextSearch.filters; + fts_header_excludes = cfg.fullTextSearch.headerExcludes; + fts_autoindex = cfg.fullTextSearch.autoIndex; + fts_enforced = cfg.fullTextSearch.enforced; } - ''} - - service auth { - unix_listener auth { - mode = 0660 - user = ${postfixCfg.user} - group = ${postfixCfg.group} - } - } - - auth_mechanisms = plain login - - namespace inbox { - separator = ${cfg.hierarchySeparator} - inbox = yes - } - - service indexer-worker { - ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' - vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)} - ''} - } - - lda_mailbox_autosubscribe = yes - lda_mailbox_autocreate = yes - ''; + // (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude) + ); + } + // lib.optionalAttrs cfg.debug.dovecot { + mail_debug = true; + auth_debug = true; + verbose_ssl = true; + }; }; systemd.services.dovecot = { diff --git a/mail-server/environment.nix b/mail-server/environment.nix index 86a81df..020930a 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -27,7 +27,7 @@ in { config = lib.mkIf cfg.enable { environment.systemPackages = with pkgs; [ - dovecot + config.services.dovecot2.package openssh postfix rspamd