dovecot: migrate to dovecot 2.4

This commit is contained in:
Martin Weinelt
2026-04-18 16:15:29 +02:00
parent 7dce7fbd5a
commit f9d1435378
6 changed files with 391 additions and 359 deletions
+25 -30
View File
@@ -587,6 +587,7 @@ in
Full text search indexing with Xapian through the fts_flatcurve plugin.
This has significant performance and disk space cost.
'';
memoryLimit = mkOption {
type = types.nullOr types.int;
default = null;
@@ -603,31 +604,13 @@ in
default = true;
description = "Enable automatic indexing of messages as they are received or modified.";
};
autoIndexExclude = mkOption {
type = types.listOf types.str;
default = [ ];
example = [
"\\Trash"
"SomeFolder"
"Other/*"
];
description = ''
Mailboxes to exclude from automatic indexing.
'';
};
enforced = mkOption {
type = types.enum [
"yes"
"no"
"body"
];
default = "no";
fallback = mkOption {
type = types.bool;
default = true;
description = ''
Fail searches when no index is available. If set to
`body`, then only body searches (as opposed to
header) are affected. If set to `no`, searches may
fall back to a very slow brute force search.
Whether to fallback to slow non-indexed search, if FTS lookup and
indexing have failed.
'';
};
@@ -640,9 +623,11 @@ in
];
description = ''
A list of languages that the full text search should detect.
At least one language must be specified.
The language listed first is the default and is used when language recognition fails.
See <https://doc.dovecot.org/main/core/plugins/fts.html#fts_languages>.
At least one language must be specified. The language listed first is
the default and is used when language recognition fails.
See <https://doc.dovecot.org/main/core/plugins/fts.html#languages>.
'';
};
@@ -679,8 +664,8 @@ in
"stopwords"
];
description = ''
The list of filters to apply.
<https://doc.dovecot.org/main/core/plugins/fts.html#filter-configuration>.
The list of language filters to apply.
See <https://doc.dovecot.org/main/core/plugins/fts.html#filter-configuration>.
'';
};
};
@@ -825,9 +810,9 @@ in
directoryLayout = mkOption {
type = types.enum [
"fs"
"maildir++"
"Maildir++"
];
default = "maildir++";
default = "Maildir++";
description = ''
Sets whether dovecot should organize mail in subdirectories:
@@ -924,10 +909,12 @@ in
Trash = {
auto = "no";
special_use = "\\Trash";
fts_autoindex = false;
};
Junk = {
auto = "subscribe";
special_use = "\\Junk";
fts_autoindex = false;
};
Drafts = {
auto = "subscribe";
@@ -1784,5 +1771,13 @@ in
(mkChangedOptionModule [ "mailserver" "useFSLayout" ] [ "mailserver" "storage" "directoryLayout" ] (
config: if config.mailserver.useFSLayout then "fs" else "maildir++"
))
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "enforced" ] ''
Whether to fallback to non-indexed search is now controlled via
`mailserver.fullTextSearch.fallback`. Missing mails are now always indexed,
since flatcurve is very fast.
'')
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "autoIndexExclude" ] ''
Configure `fts_autoindex` on mail directories in `mailserver.mailboxes` instead.
'')
];
}
+2 -2
View File
@@ -20,13 +20,13 @@ To enable indexing for full text search here is an example configuration.
enable = true;
# index new email as they arrive
autoIndex = true;
enforced = "body";
fallback = false;
};
};
}
The ``enforced`` parameter tells dovecot to fail any body search query that cannot
The ``fallback`` parameter tells dovecot to fail any body search query that cannot
use an index. This prevents dovecot to fall back to the IO-intensive brute
force search.
+318 -309
View File
@@ -39,6 +39,7 @@ let
mapAttrs'
mkForce
mkIf
mkMerge
nameValuePair
;
@@ -47,56 +48,6 @@ let
passwdDir = "/run/dovecot2";
passwdFile = "${passwdDir}/passwd";
userdbFile = "${passwdDir}/userdb";
# This file contains the ldap bind password
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
listToMultiAttrs =
keyPrefix: attrs:
lib.listToAttrs (
lib.imap1 (n: x: {
name = "${keyPrefix}${if n == 1 then "" else toString n}";
value = x;
}) attrs
);
maildirLayoutAppendix = lib.optionalString (cfg.storage.directoryLayout == "fs") ":LAYOUT=fs";
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
ldapConfig = pkgs.writeTextFile {
name = "dovecot-ldap.conf.ext.template";
text = ''
ldap_version = 3
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
${lib.optionalString cfg.ldap.startTls ''
tls = yes
''}
tls_require_cert = hard
tls_ca_cert_file = ${cfg.ldap.caFile}
dn = ${cfg.ldap.bind.dn}
sasl_bind = no
auth_bind = yes
base = ${cfg.ldap.base}
scope = ${mkLdapSearchScope cfg.ldap.scope}
user_attrs = \
=home=${cfg.storage.path}/ldap/%{ldap:${cfg.ldap.attributes.uuid}}, \
=mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${
lib.optionalString (
cfg.indexDir != null
) ":INDEX=${cfg.indexDir}/ldap/%{ldap:${cfg.ldap.attributes.uuid}}"
}
user_filter = ${cfg.ldap.dovecot.userFilter}
pass_attrs = ${cfg.ldap.attributes.password}=password
pass_filter = ${cfg.ldap.dovecot.passFilter}
'';
};
setPwdInLdapConfFile = appendLdapBindPwd {
name = "ldap-conf-file";
file = ldapConfig;
prefix = ''dnpass = "'';
suffix = ''"'';
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapConfFile;
};
genPasswdScript =
pkgs.writeScript "generate-password-file"
@@ -130,7 +81,7 @@ let
if lib.elem name accountsWithPlaintextPasswordFiles then
"${name}:${"$(sed -n '1{p;p;q}' ${passwordFiles."${name}"} | ${lib.getExe' config.services.dovecot2.package "doveadm"} pw)"}::::::"
else
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
"${name}:{CRYPT}${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.accounts
)}
EOF
@@ -140,8 +91,11 @@ let
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: value:
# https://doc.dovecot.org/2.4.3/core/config/auth/databases/passwd_file.html
# https://doc.dovecot.org/2.4.3/core/plugins/quota.html#per-user-quota
# https://dovecot.org/mailman3/archives/list/dovecot@dovecot.org/thread/67DBLLW4L5QBTEYRKGA26POFZ52ZR7ZO/#67DBLLW4L5QBTEYRKGA26POFZ52ZR7ZO
"${name}:::::::"
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
+ lib.optionalString (value.quota != null) "userdb_quota/user/storage_size=${value.quota}"
) cfg.accounts
)}
EOF
@@ -213,310 +167,365 @@ in
};
};
# for sieve-test. Shelling it in on demand usually doesn't work, as it reads
# the global config and tries to open shared libraries configured in there,
# which are usually not compatible.
# Dovecot modules
environment.systemPackages = [
pkgs.dovecot_pigeonhole_0_5
]
++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
pkgs.dovecot_pigeonhole
];
# For compatibility with python imaplib
environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
services.dovecot2 = {
package = pkgs.dovecot_2_3;
enable = true;
package = pkgs.dovecot; # pin over stateVersion logic in nixox 26.05
enablePAM = mkForce false;
sieve = {
extensions = [
"fileinto"
];
scripts.after = builtins.toFile "spam.sieve" ''
require "fileinto";
if header :is "X-Spam" "Yes" {
fileinto "${junkMailboxName}";
stop;
}
'';
pipeBins = map lib.getExe [
(pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
(pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
];
};
imapsieve.mailbox = [
{
name = junkMailboxName;
causes = [
"COPY"
"APPEND"
];
before = ./dovecot/imap_sieve/report-spam.sieve;
}
{
name = "*";
from = junkMailboxName;
causes = [ "COPY" ];
before = ./dovecot/imap_sieve/report-ham.sieve;
}
sieve.pipeBins = map lib.getExe [
(pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
(pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
];
settings = {
# vmail user
mail_uid = cfg.storage.owner;
mail_gid = cfg.storage.group;
mail_access_groups = cfg.storage.group;
# https://doc.dovecot.org/2.4.3/core/settings/syntax.html
# https://doc.dovecot.org/2.4.3/core/settings/types.html#boolean-list
settings = mkMerge [
({
# https://doc.dovecot.org/main/core/summaries/settings.html#dovecot_config_version
dovecot_config_version = "2.4.3";
# https://doc.dovecot.org/main/core/summaries/settings.html#dovecot_storage_version
dovecot_storage_version = "2.3.21.1";
# authentication
auth_mechanisms = [
"plain"
"login"
];
disable_plaintext_auth = true;
# server identity
hostname = cfg.fqdn;
# hostname
hostname = cfg.fqdn;
# vmail user
mail_uid = cfg.storage.owner;
mail_gid = cfg.storage.group;
mail_access_groups = cfg.storage.group;
# 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;
};
user = cfg.storage.owner;
vsz_limit = "${toString cfg.lmtpMemoryLimit} MB";
};
"service quota-status" = mkIf cfg.quota.enable {
executable = toString [
"${config.services.dovecot2.package}/libexec/dovecot/quota-status"
"-p"
"postfix"
# authentication
auth_mechanisms = [
"plain"
"login"
];
"unix_listener quota-status" = {
user = "postfix";
};
client_limit = 1;
vsz_limit = "${toString cfg.quotaStatusMemoryLimit} MB";
};
# 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;
# backend services
"service anvil" = {
"unix_listener anvil" = {
mode = "0660";
group = cfg.storage.group;
};
};
"inet_listener imaps" = {
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = if cfg.enableImapSsl then 993 else 0;
ssl = true;
"service auth" = {
"unix_listener auth" = {
user = config.services.postfix.user;
group = config.services.postfix.group;
mode = "0660";
};
};
};
"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;
"service lmtp" = {
"unix_listener dovecot-lmtp" = {
user = config.services.postfix.user;
group = config.services.postfix.group;
mode = "0600";
};
user = cfg.storage.owner;
vsz_limit = "${toString cfg.lmtpMemoryLimit} MB";
};
"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";
};
# protocols
protocols = [
"lmtp"
]
++ lib.optionals (cfg.enableImap || cfg.enableImapSsl) [ "imap" ]
++ lib.optionals (cfg.enablePop3 || cfg.enablePop3Ssl) [ "pop3" ]
++ lib.optionals cfg.enableManageSieve [ "sieve" ];
# frontend services
"service imap-login" = mkIf (cfg.enableImap || cfg.enableImapSsl) {
"inet_listener imap" = {
# https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = if cfg.enableImap then 143 else 0;
};
"inet_listener imaps" = mkIf cfg.enableImapSsl {
port = 993;
ssl = true;
};
};
"service pop3-login" = mkIf (cfg.enablePop3 || cfg.enablePop3Ssl) {
"inet_listener pop3" = {
# https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = if cfg.enablePop3 then 110 else 0;
};
"inet_listener pop3s" = mkIf cfg.enablePop3Ssl {
port = 995;
ssl = true;
};
};
"service imap" = {
vsz_limit = "${toString cfg.imapMemoryLimit} MB";
};
"protocol lmtp" = {
mail_plugins = [
"$mail_plugins"
"sieve"
# protocols
protocols = {
lmtp = true;
imap = cfg.enableImap || cfg.enableImapSsl;
pop3 = cfg.enablePop3 || cfg.enablePop3Ssl;
sieve = cfg.enableManageSieve;
};
"protocol lmtp" = {
mail_plugins = {
sieve = true;
};
};
"protocol imap" = {
mail_max_userip_connections = cfg.maxConnectionsPerUser;
mail_plugins = {
imap_sieve = true;
};
};
"protocol pop3" = {
mail_max_userip_connections = cfg.maxConnectionsPerUser;
};
# tls settings
ssl_server_cert_file = x509CertificateFile;
ssl_server_key_file = 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_server_prefer_ciphers = "client";
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"
];
};
"protocol imap" = {
mail_max_userip_connections = cfg.maxConnectionsPerUser;
mail_plugins = [
"$mail_plugins"
"imap_sieve"
]
++ lib.optionals cfg.quota.enable [
"imap_quota"
ssl_curve_list = lib.concatStringsSep ":" [
"X25519MLKEM768"
"X25519"
"prime256v1"
"secp384r1"
];
};
"protocol pop3" = {
mail_max_userip_connections = cfg.maxConnectionsPerUser;
};
# globally enabled plugins
mail_plugins = [
"$mail_plugins"
]
++ lib.optionals cfg.quota.enable [
"quota"
]
++ lib.optionals cfg.fullTextSearch.enable [
"fts"
"fts_flatcurve"
];
# default user mailboxes
"namespace inbox" = {
inbox = true;
separator = cfg.hierarchySeparator;
}
// mapAttrs' (name: value: nameValuePair ''mailbox "${name}"'' value) cfg.mailboxes;
lda_mailbox_autosubscribe = true;
lda_mailbox_autocreate = true;
# 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 = 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"
];
# subaddressing
recipient_delimiter = cfg.recipientDelimiter;
lmtp_save_to_detail_mailbox = cfg.lmtpSaveToDetailMailbox;
# 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}");
# sieve filtering
"sieve_script spamfilter" = {
# junk filter
path = pkgs.writeText "after.sieve" ''
require "fileinto";
passdb = [
{
if header :is "X-Spam" "Yes" {
fileinto "${junkMailboxName}";
stop;
}
'';
type = "after";
};
"sieve_script default" = {
# declarative
type = "default";
path = "${cfg.sieveDirectory}/%{user}/default.sieve";
};
"sieve_script personal" = {
# managesieve
type = "personal";
active_path = "${cfg.sieveDirectory}/%{user}/active.sieve";
path = "${cfg.sieveDirectory}/%{user}/scripts";
};
sieve_extensions = {
fileinto = true;
};
sieve_global_extensions = {
"vnd.dovecot.pipe" = true;
};
sieve_plugins = {
sieve_imapsieve = true;
sieve_extprograms = true;
};
# imapsieve (spam/ham learning)
"mailbox ${junkMailboxName}" = {
"sieve_script spam" = {
cause = [
"APPEND"
"COPY"
];
path = ./dovecot/imap_sieve/report-spam.sieve;
type = "before";
};
};
"imapsieve_from ${junkMailboxName}" = {
"sieve_script ham" = {
cause = "copy";
path = ./dovecot/imap_sieve/report-ham.sieve;
type = "before";
};
};
mailbox_list_layout = cfg.storage.directoryLayout;
mailbox_list_utf8 = cfg.useUTF8FolderNames;
mail_driver = "maildir";
mail_path = "~/mail";
# declarative users
"userdb declarative" = {
driver = "passwd-file";
args = "${passwdFile}";
}
]
++ lib.optionals cfg.ldap.enable [
{
driver = "ldap";
args = "${ldapConfFile}";
}
];
userdb = [
{
passwd_file_path = userdbFile;
fields = {
home = "${cfg.storage.path}/%{user | domain}/%{user | username}";
inherit (cfg.storage) uid gid;
mail_index_path = "${
if cfg.indexDir != null then cfg.indexDir else cfg.storage.path
}/%{user | domain }/%{user | username}";
};
};
"passdb declarative" = {
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}"
];
}
]
++ lib.optionals cfg.ldap.enable [
{
passwd_file_path = passwdFile;
};
})
(mkIf cfg.ldap.enable {
# ldap users
ssl_client_ca_file = cfg.ldap.caFile;
ssl_client_require_valid_cert = true;
ldap_version = 3;
ldap_uris = cfg.ldap.uris;
ldap_starttls = cfg.ldap.startTls;
ldap_auth_dn = cfg.ldap.bind.dn;
ldap_auth_dn_password = "</run/credentials/dovecot.service/ldap-bind-pw";
ldap_base = cfg.ldap.base;
ldap_scope = mkLdapSearchScope cfg.ldap.scope;
"userdb ldap" = {
driver = "ldap";
args = "${ldapConfFile}";
override_fields = [
"uid=${toString cfg.storage.uid}"
"gid=${toString cfg.storage.gid}"
filter = cfg.ldap.dovecot.userFilter;
fields = {
home = "${cfg.storage.path}/ldap/%{ldap:${cfg.ldap.attributes.uuid}}";
inherit (cfg.storage) uid gid;
mail_index_path = "${
if cfg.indexDir != null then cfg.indexDir else cfg.storage.path
}/ldap/%{ldap:${cfg.ldap.attributes.uuid}}";
};
ldap_connection_group = "ldap-userdb-conn";
};
"passdb ldap" = {
driver = "ldap";
filter = cfg.ldap.dovecot.passFilter;
fields = {
password = "%{ldap:userPassword}";
};
ldap_connection_group = "ldap-passdb-conn";
};
})
(mkIf cfg.quota.enable {
mail_plugins.quota = true;
"protocol imap".mail_plugins.imap_quota = true;
"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";
};
# default user mailboxes
"namespace inbox" = {
inbox = true;
separator = cfg.hierarchySeparator;
}
// 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 = {
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;
}
// (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude)
)
// lib.optionalAttrs cfg.quota.enable {
quota_rule = mkIf (cfg.quota.defaults.perUser != null) "*:storage=${cfg.quota.defaults.perUser}";
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;
};
}
// lib.optionalAttrs cfg.debug.dovecot {
mail_debug = true;
auth_debug = true;
verbose_ssl = true;
};
# quota_storage_grace = "10M";
"quota user" = {
driver = "count";
storage_size = mkIf (cfg.quota.defaults.perUser != null) cfg.quota.defaults.perUser;
};
})
(mkIf cfg.fullTextSearch.enable (
{
mail_plugins = {
fts = true;
fts_flatcurve = true;
};
"service indexer-worker" = mkIf (cfg.fullTextSearch.memoryLimit != null) {
vsz_limit = "${toString cfg.fullTextSearch.memoryLimit} MB";
};
fts_autoindex = cfg.fullTextSearch.autoIndex;
fts_driver = "flatcurve";
fts_search_add_missing = "yes";
fts_search_read_fallback = cfg.fullTextSearch.fallback;
fts_header_excludes = lib.genAttrs cfg.fullTextSearch.headerExcludes (_: true);
"fts flatcurve" = {
flatcurve_substring_search = cfg.fullTextSearch.substringSearch;
};
# languages
language_filters = lib.genAttrs cfg.fullTextSearch.filters (_: true);
language_tokenizer_address_token_maxlen = 100; # default 250 too large for Xapian
}
# build languages from list, the first one becomes the default language
// lib.listToAttrs (
lib.imap0 (i: lang: {
name = "language ${lang}";
value = (if i == 0 then { default = true; } else { }) // {
language_tokenizers = [
"generic"
"email-address"
];
};
}) cfg.fullTextSearch.languages
)
))
(mkIf cfg.debug.dovecot {
mail_debug = true;
# https://doc.dovecot.org/2.4.3/core/config/events/filter.html#common-unified-filter-language
log_debug = "category=ssl OR category=auth";
})
];
};
systemd.services.dovecot = {
preStart = ''
${genPasswdScript}
''
+ (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
'';
reloadTriggers = lib.mkIf (!withACME) [
x509CertificateFile
x509PrivateKeyFile
];
serviceConfig = lib.optionalAttrs cfg.ldap.enable {
LoadCredential = [
"ldap-bind-pw:${cfg.ldap.bind.passwordFile}"
];
};
};
systemd.services.postfix.restartTriggers = [
genPasswdScript
]
++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]);
];
};
}
+23 -7
View File
@@ -81,7 +81,7 @@
};
"lowquota@example.com" = {
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
quota = "1B";
quota = "1K";
};
};
@@ -98,13 +98,13 @@
fullTextSearch = {
enable = true;
autoIndex = true;
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201
autoIndexExclude = [
(if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk")
];
enforced = "yes";
fallback = false;
};
};
# by default quota can be exceeded once with this amount (default: 10M)
# this is required to make the quota subtest hard fail on the first attempt.
services.dovecot2.settings.quota_storage_grace = "0";
};
client =
{ nodes, pkgs, ... }:
@@ -306,7 +306,21 @@
Hello User1,
how are you doing today?
how are you doing today? I have this exciting text for you, that helps fill
your quota.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod
tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At
vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren,
no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit
amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut
labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam
et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata
sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur
sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore
magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo
dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est
Lorem ipsum dolor sit amet.
XOXO User1
'';
@@ -514,6 +528,8 @@
client.execute("rm ~/mail/*")
client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
server.log(server.succeed("doveadm quota get -u lowquota@example.com"))
client.succeed(
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota@example.com < /etc/root/email2 >&2"
)
+5 -2
View File
@@ -78,6 +78,8 @@ in
};
};
systemd.services.dovecot.serviceConfig.CacheDirectory = "dovecot";
mailserver = {
enable = true;
fqdn = "mail.example.com";
@@ -114,7 +116,7 @@ in
group = "vmail";
};
indexDir = "/var/lib/dovecot/indices";
indexDir = "/var/cache/dovecot/fts";
enableImap = false;
};
@@ -219,7 +221,8 @@ in
with subtest("Check dovecot maildir and index locations"):
# If these paths change we need a migration
machine.succeed("doveadm user -f home user1@example.com | grep ${nodes.machine.mailserver.storage.path}/example.com/user1")
machine.succeed("doveadm user -f mail user1@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/example.com/user1'")
machine.succeed("doveadm user -f mail_path user1@example.com | grep ${nodes.machine.mailserver.storage.path}/example.com/user1/mail")
machine.succeed("doveadm user -f mail_index_path user1@example.com | grep ${nodes.machine.mailserver.indexDir}/example.com/user1")
with subtest("mail to send only accounts is rejected"):
machine.wait_for_open_port(25)
+18 -9
View File
@@ -18,7 +18,6 @@ let
alicePassword = "testalice";
bobPassword = "testbob";
carolPassword = "testcarol";
frankPassword = "testfrank";
malloryPassword = "testmallory";
in
{
@@ -76,7 +75,8 @@ in
objectClass: simpleSecurityObject
objectClass: top
cn: mail
userPassword: ${bindPassword}
# unsafegibberish
userPassword: {SSHA}JNr6l3s/RHo1LKRXqFsJg8sXznyRid8L
dn: ou=users,dc=example
objectClass: organizationalUnit
@@ -88,7 +88,8 @@ in
uid: alice
sn: Foo
mail: alice@example.com
userPassword: ${alicePassword}
# testalice
userPassword: {SSHA}gkJq4Dm4jfIKjxviR0WD63wMt0Ti6zMB
dn: cn=bob,ou=users,dc=example
entryUUID: f3b4e8ea-087f-42cc-95f0-cbfd99386092
@@ -100,7 +101,8 @@ in
sn: Bar
mail: bob@example.com
homeDirectory: /home/bob
userPassword: ${bobPassword}
# testbob
userPassword: {SSHA}qqUveZGZrDrjYFnREXLDZc//y89RppVN
dn: cn=carol,ou=users,dc=example
entryUUID: 41240499-27e2-4fa2-be4f-4113a77661b1
@@ -108,7 +110,8 @@ in
uid: carol
sn: Baz
mail: carol@example.com
userPassword: ${carolPassword}
# testcarol
userPassword: {SSHA}69HOuP+OPWE+3+tDucFZxzXDC7p4e3ML
dn: cn=frank,ou=users,dc=example
entryUUID: ca16f594-f6b2-418f-87d3-0d02d746461f
@@ -116,17 +119,23 @@ in
uid: frank
sn: Moo
mail: frank@example.com
userPassword: ${frankPassword}
# testfrank
userPassword: {SSHA}xqtMl8/uJ6HEFWDzLYpAE+Wq7FvKrtkm
'';
};
systemd.services.dovecot.serviceConfig = {
CacheDirectory = "dovecot";
StateDirectory = "dovecot";
};
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" ];
localDnsResolver = false;
storage.path = "/var/lib/dovecot/vmail";
indexDir = "/var/lib/dovecot/indices";
indexDir = "/var/cache/dovecot/indices";
aliases = {
# Steal frank@example.com from LDAP user frank
@@ -215,11 +224,11 @@ in
machine.succeed("doveadm user -f gid bob@example.com | grep ${toString nodes.machine.mailserver.storage.uid}")
machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.storage.path}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092")
machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092'")
machine.succeed("doveadm user -f mail_path bob@example.com | grep ${nodes.machine.mailserver.storage.path}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092")
machine.succeed("doveadm user -f mail_index_path bob@example.com | grep ${nodes.machine.mailserver.indexDir}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092")
with subtest("Files containing secrets are only readable by root"):
machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'")
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'")
with subtest("Test account/mail address binding via explicit TLS"):
machine.fail(" ".join([