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. Full text search indexing with Xapian through the fts_flatcurve plugin.
This has significant performance and disk space cost. This has significant performance and disk space cost.
''; '';
memoryLimit = mkOption { memoryLimit = mkOption {
type = types.nullOr types.int; type = types.nullOr types.int;
default = null; default = null;
@@ -603,31 +604,13 @@ in
default = true; default = true;
description = "Enable automatic indexing of messages as they are received or modified."; 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 { fallback = mkOption {
type = types.enum [ type = types.bool;
"yes" default = true;
"no"
"body"
];
default = "no";
description = '' description = ''
Fail searches when no index is available. If set to Whether to fallback to slow non-indexed search, if FTS lookup and
`body`, then only body searches (as opposed to indexing have failed.
header) are affected. If set to `no`, searches may
fall back to a very slow brute force search.
''; '';
}; };
@@ -640,9 +623,11 @@ in
]; ];
description = '' description = ''
A list of languages that the full text search should detect. 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. At least one language must be specified. The language listed first is
See <https://doc.dovecot.org/main/core/plugins/fts.html#fts_languages>. 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" "stopwords"
]; ];
description = '' description = ''
The list of filters to apply. The list of language filters to apply.
<https://doc.dovecot.org/main/core/plugins/fts.html#filter-configuration>. See <https://doc.dovecot.org/main/core/plugins/fts.html#filter-configuration>.
''; '';
}; };
}; };
@@ -825,9 +810,9 @@ in
directoryLayout = mkOption { directoryLayout = mkOption {
type = types.enum [ type = types.enum [
"fs" "fs"
"maildir++" "Maildir++"
]; ];
default = "maildir++"; default = "Maildir++";
description = '' description = ''
Sets whether dovecot should organize mail in subdirectories: Sets whether dovecot should organize mail in subdirectories:
@@ -924,10 +909,12 @@ in
Trash = { Trash = {
auto = "no"; auto = "no";
special_use = "\\Trash"; special_use = "\\Trash";
fts_autoindex = false;
}; };
Junk = { Junk = {
auto = "subscribe"; auto = "subscribe";
special_use = "\\Junk"; special_use = "\\Junk";
fts_autoindex = false;
}; };
Drafts = { Drafts = {
auto = "subscribe"; auto = "subscribe";
@@ -1784,5 +1771,13 @@ in
(mkChangedOptionModule [ "mailserver" "useFSLayout" ] [ "mailserver" "storage" "directoryLayout" ] ( (mkChangedOptionModule [ "mailserver" "useFSLayout" ] [ "mailserver" "storage" "directoryLayout" ] (
config: if config.mailserver.useFSLayout then "fs" else "maildir++" 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; enable = true;
# index new email as they arrive # index new email as they arrive
autoIndex = true; 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 use an index. This prevents dovecot to fall back to the IO-intensive brute
force search. force search.
+235 -226
View File
@@ -39,6 +39,7 @@ let
mapAttrs' mapAttrs'
mkForce mkForce
mkIf mkIf
mkMerge
nameValuePair nameValuePair
; ;
@@ -47,56 +48,6 @@ let
passwdDir = "/run/dovecot2"; passwdDir = "/run/dovecot2";
passwdFile = "${passwdDir}/passwd"; passwdFile = "${passwdDir}/passwd";
userdbFile = "${passwdDir}/userdb"; 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 = genPasswdScript =
pkgs.writeScript "generate-password-file" pkgs.writeScript "generate-password-file"
@@ -130,7 +81,7 @@ let
if lib.elem name accountsWithPlaintextPasswordFiles then if lib.elem name accountsWithPlaintextPasswordFiles then
"${name}:${"$(sed -n '1{p;p;q}' ${passwordFiles."${name}"} | ${lib.getExe' config.services.dovecot2.package "doveadm"} pw)"}::::::" "${name}:${"$(sed -n '1{p;p;q}' ${passwordFiles."${name}"} | ${lib.getExe' config.services.dovecot2.package "doveadm"} pw)"}::::::"
else else
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" "${name}:{CRYPT}${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.accounts ) cfg.accounts
)} )}
EOF EOF
@@ -140,8 +91,11 @@ let
${lib.concatStringsSep "\n" ( ${lib.concatStringsSep "\n" (
lib.mapAttrsToList ( lib.mapAttrsToList (
name: value: 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}:::::::" "${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 ) cfg.accounts
)} )}
EOF EOF
@@ -213,60 +167,36 @@ in
}; };
}; };
# for sieve-test. Shelling it in on demand usually doesn't work, as it reads # Dovecot modules
# the global config and tries to open shared libraries configured in there,
# which are usually not compatible.
environment.systemPackages = [ environment.systemPackages = [
pkgs.dovecot_pigeonhole_0_5 pkgs.dovecot_pigeonhole
] ];
++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
# For compatibility with python imaplib # For compatibility with python imaplib
environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
services.dovecot2 = { services.dovecot2 = {
package = pkgs.dovecot_2_3;
enable = true; enable = true;
package = pkgs.dovecot; # pin over stateVersion logic in nixox 26.05
enablePAM = mkForce false; enablePAM = mkForce false;
sieve = { sieve.pipeBins = map lib.getExe [
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-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") (pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
]; ];
};
imapsieve.mailbox = [ # https://doc.dovecot.org/2.4.3/core/settings/syntax.html
{ # https://doc.dovecot.org/2.4.3/core/settings/types.html#boolean-list
name = junkMailboxName; settings = mkMerge [
causes = [ ({
"COPY" # https://doc.dovecot.org/main/core/summaries/settings.html#dovecot_config_version
"APPEND" dovecot_config_version = "2.4.3";
]; # https://doc.dovecot.org/main/core/summaries/settings.html#dovecot_storage_version
before = ./dovecot/imap_sieve/report-spam.sieve; dovecot_storage_version = "2.3.21.1";
}
{ # server identity
name = "*"; hostname = cfg.fqdn;
from = junkMailboxName;
causes = [ "COPY" ];
before = ./dovecot/imap_sieve/report-ham.sieve;
}
];
settings = {
# vmail user # vmail user
mail_uid = cfg.storage.owner; mail_uid = cfg.storage.owner;
mail_gid = cfg.storage.group; mail_gid = cfg.storage.group;
@@ -277,64 +207,49 @@ in
"plain" "plain"
"login" "login"
]; ];
disable_plaintext_auth = true;
# hostname
hostname = cfg.fqdn;
# backend services # backend services
"service anvil" = {
"unix_listener anvil" = {
mode = "0660";
group = cfg.storage.group;
};
};
"service auth" = { "service auth" = {
"unix_listener auth" = { "unix_listener auth" = {
mode = "0660";
user = config.services.postfix.user; user = config.services.postfix.user;
group = config.services.postfix.group; group = config.services.postfix.group;
mode = "0660";
}; };
}; };
"service indexer-worker" = mkIf (cfg.fullTextSearch.memoryLimit != null) {
vsz_limit = "${toString cfg.fullTextSearch.memoryLimit} MB";
};
"service lmtp" = { "service lmtp" = {
"unix_listener dovecot-lmtp" = { "unix_listener dovecot-lmtp" = {
user = config.services.postfix.user;
group = config.services.postfix.group; group = config.services.postfix.group;
mode = "0600"; mode = "0600";
user = config.services.postfix.user;
}; };
user = cfg.storage.owner; user = cfg.storage.owner;
vsz_limit = "${toString cfg.lmtpMemoryLimit} MB"; 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"
];
"unix_listener quota-status" = {
user = "postfix";
};
client_limit = 1;
vsz_limit = "${toString cfg.quotaStatusMemoryLimit} MB";
};
# frontend services # frontend services
"service imap-login" = mkIf (cfg.enableImap || cfg.enableImapSsl) { "service imap-login" = mkIf (cfg.enableImap || cfg.enableImapSsl) {
"inet_listener imap" = { "inet_listener imap" = {
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html # https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = if cfg.enableImap then 143 else 0; port = if cfg.enableImap then 143 else 0;
}; };
"inet_listener imaps" = { "inet_listener imaps" = mkIf cfg.enableImapSsl {
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html port = 993;
port = if cfg.enableImapSsl then 993 else 0;
ssl = true; ssl = true;
}; };
}; };
"service pop3-login" = mkIf (cfg.enablePop3 || cfg.enablePop3Ssl) { "service pop3-login" = mkIf (cfg.enablePop3 || cfg.enablePop3Ssl) {
"inet_listener pop3" = { "inet_listener pop3" = {
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html # https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = if cfg.enablePop3 then 110 else 0; port = if cfg.enablePop3 then 110 else 0;
}; };
"inet_listener pop3s" = { "inet_listener pop3s" = mkIf cfg.enablePop3Ssl {
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html port = 995;
port = if cfg.enablePop3Ssl then 995 else 0;
ssl = true; ssl = true;
}; };
}; };
@@ -343,52 +258,35 @@ in
}; };
# protocols # protocols
protocols = [ protocols = {
"lmtp" lmtp = true;
] imap = cfg.enableImap || cfg.enableImapSsl;
++ lib.optionals (cfg.enableImap || cfg.enableImapSsl) [ "imap" ] pop3 = cfg.enablePop3 || cfg.enablePop3Ssl;
++ lib.optionals (cfg.enablePop3 || cfg.enablePop3Ssl) [ "pop3" ] sieve = cfg.enableManageSieve;
++ lib.optionals cfg.enableManageSieve [ "sieve" ]; };
"protocol lmtp" = { "protocol lmtp" = {
mail_plugins = [ mail_plugins = {
"$mail_plugins" sieve = true;
"sieve" };
];
}; };
"protocol imap" = { "protocol imap" = {
mail_max_userip_connections = cfg.maxConnectionsPerUser; mail_max_userip_connections = cfg.maxConnectionsPerUser;
mail_plugins = [ mail_plugins = {
"$mail_plugins" imap_sieve = true;
"imap_sieve" };
]
++ lib.optionals cfg.quota.enable [
"imap_quota"
];
}; };
"protocol pop3" = { "protocol pop3" = {
mail_max_userip_connections = cfg.maxConnectionsPerUser; 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"
];
# tls settings # tls settings
ssl_cert = "<${x509CertificateFile}"; ssl_server_cert_file = x509CertificateFile;
ssl_key = "<${x509PrivateKeyFile}"; 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 # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7
ssl = "required"; ssl = "required";
ssl_min_protocol = "TLSv1.2"; ssl_min_protocol = "TLSv1.2";
ssl_prefer_server_ciphers = false; ssl_server_prefer_ciphers = "client";
ssl_cipher_list = lib.concatStringsSep ":" [ ssl_cipher_list = lib.concatStringsSep ":" [
# TLS1.3 # TLS1.3
"TLS_AES_128_GCM_SHA256" "TLS_AES_128_GCM_SHA256"
@@ -411,46 +309,6 @@ in
"secp384r1" "secp384r1"
]; ];
# 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}";
}
]
++ lib.optionals cfg.ldap.enable [
{
driver = "ldap";
args = "${ldapConfFile}";
}
];
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}"
];
}
]
++ lib.optionals cfg.ldap.enable [
{
driver = "ldap";
args = "${ldapConfFile}";
override_fields = [
"uid=${toString cfg.storage.uid}"
"gid=${toString cfg.storage.gid}"
];
}
];
# default user mailboxes # default user mailboxes
"namespace inbox" = { "namespace inbox" = {
inbox = true; inbox = true;
@@ -464,59 +322,210 @@ in
recipient_delimiter = cfg.recipientDelimiter; recipient_delimiter = cfg.recipientDelimiter;
lmtp_save_to_detail_mailbox = cfg.lmtpSaveToDetailMailbox; lmtp_save_to_detail_mailbox = cfg.lmtpSaveToDetailMailbox;
plugin = { # sieve filtering
sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; "sieve_script spamfilter" = {
sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; # junk filter
sieve_default_name = "default"; path = pkgs.writeText "after.sieve" ''
require "fileinto";
if header :is "X-Spam" "Yes" {
fileinto "${junkMailboxName}";
stop;
} }
// lib.optionalAttrs cfg.fullTextSearch.enable ( '';
{ type = "after";
fts = "flatcurve"; };
fts_languages = cfg.fullTextSearch.languages; "sieve_script default" = {
fts_tokenizers = [ # declarative
"generic" type = "default";
"email-address" 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"
]; ];
fts_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian path = ./dovecot/imap_sieve/report-spam.sieve;
fts_flatcurve_substring_search = cfg.fullTextSearch.substringSearch; type = "before";
fts_filters = cfg.fullTextSearch.filters; };
fts_header_excludes = cfg.fullTextSearch.headerExcludes; };
fts_autoindex = cfg.fullTextSearch.autoIndex; "imapsieve_from ${junkMailboxName}" = {
fts_enforced = cfg.fullTextSearch.enforced; "sieve_script ham" = {
} cause = "copy";
// (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude) path = ./dovecot/imap_sieve/report-ham.sieve;
) type = "before";
// 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
mailbox_list_layout = cfg.storage.directoryLayout;
mailbox_list_utf8 = cfg.useUTF8FolderNames;
mail_driver = "maildir";
mail_path = "~/mail";
# declarative users
"userdb declarative" = {
driver = "passwd-file";
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";
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";
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";
};
quota_status_success = "DUNNO"; quota_status_success = "DUNNO";
quota_status_nouser = "DUNNO"; quota_status_nouser = "DUNNO";
quota_status_overquota = "552 5.2.2 Mailbox is full"; quota_status_overquota = "552 5.2.2 Mailbox is full";
quota_grace = "10%%"; # quota_storage_grace = "10M";
quota_vsizes = true;
"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
} }
// lib.optionalAttrs cfg.debug.dovecot { # build languages from list, the first one becomes the default language
mail_debug = true; // lib.listToAttrs (
auth_debug = true; lib.imap0 (i: lang: {
verbose_ssl = true; 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 = { systemd.services.dovecot = {
preStart = '' preStart = ''
${genPasswdScript} ${genPasswdScript}
'' '';
+ (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
reloadTriggers = lib.mkIf (!withACME) [ reloadTriggers = lib.mkIf (!withACME) [
x509CertificateFile x509CertificateFile
x509PrivateKeyFile x509PrivateKeyFile
]; ];
serviceConfig = lib.optionalAttrs cfg.ldap.enable {
LoadCredential = [
"ldap-bind-pw:${cfg.ldap.bind.passwordFile}"
];
};
}; };
systemd.services.postfix.restartTriggers = [ systemd.services.postfix.restartTriggers = [
genPasswdScript genPasswdScript
] ];
++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]);
}; };
} }
+23 -7
View File
@@ -81,7 +81,7 @@
}; };
"lowquota@example.com" = { "lowquota@example.com" = {
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
quota = "1B"; quota = "1K";
}; };
}; };
@@ -98,13 +98,13 @@
fullTextSearch = { fullTextSearch = {
enable = true; enable = true;
autoIndex = true; autoIndex = true;
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201 fallback = false;
autoIndexExclude = [
(if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk")
];
enforced = "yes";
}; };
}; };
# 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 = client =
{ nodes, pkgs, ... }: { nodes, pkgs, ... }:
@@ -306,7 +306,21 @@
Hello User1, 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 XOXO User1
''; '';
@@ -514,6 +528,8 @@
client.execute("rm ~/mail/*") client.execute("rm ~/mail/*")
client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc") client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
server.log(server.succeed("doveadm quota get -u lowquota@example.com"))
client.succeed( client.succeed(
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota@example.com < /etc/root/email2 >&2" "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 = { mailserver = {
enable = true; enable = true;
fqdn = "mail.example.com"; fqdn = "mail.example.com";
@@ -114,7 +116,7 @@ in
group = "vmail"; group = "vmail";
}; };
indexDir = "/var/lib/dovecot/indices"; indexDir = "/var/cache/dovecot/fts";
enableImap = false; enableImap = false;
}; };
@@ -219,7 +221,8 @@ in
with subtest("Check dovecot maildir and index locations"): with subtest("Check dovecot maildir and index locations"):
# If these paths change we need a migration # 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 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"): with subtest("mail to send only accounts is rejected"):
machine.wait_for_open_port(25) machine.wait_for_open_port(25)
+18 -9
View File
@@ -18,7 +18,6 @@ let
alicePassword = "testalice"; alicePassword = "testalice";
bobPassword = "testbob"; bobPassword = "testbob";
carolPassword = "testcarol"; carolPassword = "testcarol";
frankPassword = "testfrank";
malloryPassword = "testmallory"; malloryPassword = "testmallory";
in in
{ {
@@ -76,7 +75,8 @@ in
objectClass: simpleSecurityObject objectClass: simpleSecurityObject
objectClass: top objectClass: top
cn: mail cn: mail
userPassword: ${bindPassword} # unsafegibberish
userPassword: {SSHA}JNr6l3s/RHo1LKRXqFsJg8sXznyRid8L
dn: ou=users,dc=example dn: ou=users,dc=example
objectClass: organizationalUnit objectClass: organizationalUnit
@@ -88,7 +88,8 @@ in
uid: alice uid: alice
sn: Foo sn: Foo
mail: alice@example.com mail: alice@example.com
userPassword: ${alicePassword} # testalice
userPassword: {SSHA}gkJq4Dm4jfIKjxviR0WD63wMt0Ti6zMB
dn: cn=bob,ou=users,dc=example dn: cn=bob,ou=users,dc=example
entryUUID: f3b4e8ea-087f-42cc-95f0-cbfd99386092 entryUUID: f3b4e8ea-087f-42cc-95f0-cbfd99386092
@@ -100,7 +101,8 @@ in
sn: Bar sn: Bar
mail: bob@example.com mail: bob@example.com
homeDirectory: /home/bob homeDirectory: /home/bob
userPassword: ${bobPassword} # testbob
userPassword: {SSHA}qqUveZGZrDrjYFnREXLDZc//y89RppVN
dn: cn=carol,ou=users,dc=example dn: cn=carol,ou=users,dc=example
entryUUID: 41240499-27e2-4fa2-be4f-4113a77661b1 entryUUID: 41240499-27e2-4fa2-be4f-4113a77661b1
@@ -108,7 +110,8 @@ in
uid: carol uid: carol
sn: Baz sn: Baz
mail: carol@example.com mail: carol@example.com
userPassword: ${carolPassword} # testcarol
userPassword: {SSHA}69HOuP+OPWE+3+tDucFZxzXDC7p4e3ML
dn: cn=frank,ou=users,dc=example dn: cn=frank,ou=users,dc=example
entryUUID: ca16f594-f6b2-418f-87d3-0d02d746461f entryUUID: ca16f594-f6b2-418f-87d3-0d02d746461f
@@ -116,17 +119,23 @@ in
uid: frank uid: frank
sn: Moo sn: Moo
mail: frank@example.com mail: frank@example.com
userPassword: ${frankPassword} # testfrank
userPassword: {SSHA}xqtMl8/uJ6HEFWDzLYpAE+Wq7FvKrtkm
''; '';
}; };
systemd.services.dovecot.serviceConfig = {
CacheDirectory = "dovecot";
StateDirectory = "dovecot";
};
mailserver = { mailserver = {
enable = true; enable = true;
fqdn = "mail.example.com"; fqdn = "mail.example.com";
domains = [ "example.com" ]; domains = [ "example.com" ];
localDnsResolver = false; localDnsResolver = false;
storage.path = "/var/lib/dovecot/vmail"; storage.path = "/var/lib/dovecot/vmail";
indexDir = "/var/lib/dovecot/indices"; indexDir = "/var/cache/dovecot/indices";
aliases = { aliases = {
# Steal frank@example.com from LDAP user frank # 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 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 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"): 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/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"): with subtest("Test account/mail address binding via explicit TLS"):
machine.fail(" ".join([ machine.fail(" ".join([