Merge branch 'dovecot-2.4.3' into 'main'

dovecot: migrate to dovecot 2.4

See merge request simple-nixos-mailserver/nixos-mailserver!512
This commit is contained in:
Martin Weinelt
2026-04-20 23:23:08 +00:00
6 changed files with 436 additions and 386 deletions
+47 -36
View File
@@ -587,47 +587,41 @@ 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;
example = 2000; example = 1024;
description = '' description = ''
Memory limit for the indexer process, in MiB. Memory limit for the indexer process, in MiB.
If null, leaves the default (which is rather low),
and if 0, no limit. When `null` the [`default_vsz_limit`](https://doc.dovecot.org/main/core/plugins/fts.html#fts_search_read_fallback)
applies while with `0` no limit is applied.
''; '';
}; };
autoIndex = mkOption { autoIndex = mkOption {
type = types.bool; type = types.bool;
default = true; 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 = '' description = ''
Mailboxes to exclude from automatic indexing. Enable automatic indexing of messages as they are received or modified.
:::{tip}
Can be overridden per mailbox by setting `fts_autoindex` for
{option}`mailserver.mailboxes`. By default the Junk and Trash folders
are already excluded.
:::
''; '';
}; };
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. See <https://doc.dovecot.org/main/core/plugins/fts.html#fts_search_read_fallback>.
''; '';
}; };
@@ -640,9 +634,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>.
''; '';
}; };
@@ -650,10 +646,13 @@ in
type = types.bool; type = types.bool;
default = false; default = false;
description = '' description = ''
If enabled, allows substring searches. Whether to allows substring searches. By default only prefix searches are supported.
See <https://doc.dovecot.org/main/core/plugins/fts_flatcurve.html#fts_flatcurve_substring_search>.
Enabling this requires significant additional storage space. :::{warning}
Enabling this significantly increases storage requirements.
:::
See <https://doc.dovecot.org/main/core/plugins/fts_flatcurve.html#fts_flatcurve_substring_search>.
''; '';
}; };
@@ -666,7 +665,8 @@ in
"Comments" "Comments"
]; ];
description = '' description = ''
The list of headers to exclude. The list of headers to exclude while indexing.
See <https://doc.dovecot.org/main/core/plugins/fts.html#fts_header_excludes>. See <https://doc.dovecot.org/main/core/plugins/fts.html#fts_header_excludes>.
''; '';
}; };
@@ -679,8 +679,9 @@ 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>.
[language filters]: https://doc.dovecot.org/main/core/plugins/fts.html#filter-configuration
''; '';
}; };
}; };
@@ -825,9 +826,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 +925,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 +1787,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.
'')
]; ];
} }
+25 -23
View File
@@ -4,7 +4,7 @@ Full text search
By default, when your IMAP client searches for an email containing some By default, when your IMAP client searches for an email containing some
text in its *body*, dovecot will read all your email sequentially. This text in its *body*, dovecot will read all your email sequentially. This
is very slow and IO intensive. To speed body searches up, it is possible to is very slow and IO intensive. To speed body searches up, it is possible to
*index* emails with a plugin to dovecot, ``fts_flatcurve``. *index* emails with the ``fts_flatcurve`` dovecot plugin.
Enabling full text search Enabling full text search
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -20,48 +20,50 @@ 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"; # only query index
fallback = false;
}; };
}; };
} }
The ``enforced`` parameter tells dovecot to fail any body search query that cannot Disabling the :option:`mailserver.fullTextSearch.fallback` option tells dovecot
use an index. This prevents dovecot to fall back to the IO-intensive brute to fail any body search query that cannot use an index. This prevents Dovecot to
force search. fall back to the IO-intensive brute force search.
If you set ``autoIndex`` to ``false``, indices will be created when the IMAP client If you set :option:`mailserver.fullTextSearch.autoIndex` to ``false``, indices
issues a search query, so latency will be high. will be created when the IMAP client issues a search query, so latency will
be high.
Resource requirements Resource requirements
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
Indices created by the full text search feature can take more disk Indices created by the full text search feature can take more disk space than
space than the emails themselves. By default, they are kept in the the emails themselves. By default, they are kept within the maildir. When
emails location. When enabling the full text search feature, it is enabling the full text search feature, it is recommended to move indices in a
recommended to move indices in a different location, such as different location, such as (``/var/lib/dovecot/indices``) by configuring
(``/var/lib/dovecot/indices``) by using the option :option:`mailserver.indexDir`.
``mailserver.indexDir``.
.. warning:: .. warning::
When the value of the ``indexDir`` option is changed, all dovecot When the value of the :option:`mailserver.indexDir` option is changed, all
indices needs to be recreated: clients would need to resynchronize. dovecot indices needs to be recreated: clients would need to resynchronize.
Indexation itself is rather resource intensive, in CPU, and for emails with Indexation itself is rather resource intensive, in CPU, and for emails with
large headers, in memory as well. Initial indexation of existing emails can take large headers, in memory as well. Initial indexation of existing emails can take
hours. If the indexer worker is killed or segfaults during indexation, it can hours. If the indexer worker is killed or segfaults during indexation, it can be
be that it tried to allocate more memory than allowed. You can increase the memory that it tried to allocate more memory than allowed. You can increase the default
limit by eg ``mailserver.fullTextSearch.memoryLimit = 2000`` (in MiB). memory limit through :option:`mailserver.fullTextSearch.memoryLimit`.
Mitigating resources requirements Mitigating resources requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can: You can:
* exclude some headers from indexation with ``mailserver.fullTextSearch.headerExcludes`` * exclude some headers from indexation with :option:`mailserver.fullTextSearch.headerExcludes`
* disable expensive token normalisation in ``mailserver.fullTextSearch.filters`` * disable expensive token normalisation in :option:`mailserver.fullTextSearch.filters`
* disable automatic indexation for some folders with * disable automatic indexation for individual mailboxes by overriding
``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by `fts_autoindex`_ on the mailbox level. This is exposed via
name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard. :option:`mailserver.mailboxes`, where all default mailboxes are defined.
.. _fts_autoindex: https://doc.dovecot.org/main/core/plugins/fts.html#fts_autoindex
+318 -309
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,310 +167,365 @@ 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 = [ (pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
"fileinto" (pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
];
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;
}
]; ];
settings = { # https://doc.dovecot.org/2.4.3/core/settings/syntax.html
# vmail user # https://doc.dovecot.org/2.4.3/core/settings/types.html#boolean-list
mail_uid = cfg.storage.owner; settings = mkMerge [
mail_gid = cfg.storage.group; ({
mail_access_groups = cfg.storage.group; # 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 # server identity
auth_mechanisms = [ hostname = cfg.fqdn;
"plain"
"login"
];
disable_plaintext_auth = true;
# hostname # vmail user
hostname = cfg.fqdn; mail_uid = cfg.storage.owner;
mail_gid = cfg.storage.group;
mail_access_groups = cfg.storage.group;
# backend services # authentication
"service auth" = { auth_mechanisms = [
"unix_listener auth" = { "plain"
mode = "0660"; "login"
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"
]; ];
"unix_listener quota-status" = {
user = "postfix";
};
client_limit = 1;
vsz_limit = "${toString cfg.quotaStatusMemoryLimit} MB";
};
# frontend services # backend services
"service imap-login" = mkIf (cfg.enableImap || cfg.enableImapSsl) { "service anvil" = {
"inet_listener imap" = { "unix_listener anvil" = {
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html mode = "0660";
port = if cfg.enableImap then 143 else 0; group = cfg.storage.group;
};
}; };
"inet_listener imaps" = { "service auth" = {
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html "unix_listener auth" = {
port = if cfg.enableImapSsl then 993 else 0; user = config.services.postfix.user;
ssl = true; group = config.services.postfix.group;
mode = "0660";
};
}; };
}; "service lmtp" = {
"service pop3-login" = mkIf (cfg.enablePop3 || cfg.enablePop3Ssl) { "unix_listener dovecot-lmtp" = {
"inet_listener pop3" = { user = config.services.postfix.user;
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html group = config.services.postfix.group;
port = if cfg.enablePop3 then 110 else 0; 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 # frontend services
protocols = [ "service imap-login" = mkIf (cfg.enableImap || cfg.enableImapSsl) {
"lmtp" "inet_listener imap" = {
] # https://dovecot.org/pipermail/dovecot/2010-March/047479.html
++ lib.optionals (cfg.enableImap || cfg.enableImapSsl) [ "imap" ] port = if cfg.enableImap then 143 else 0;
++ lib.optionals (cfg.enablePop3 || cfg.enablePop3Ssl) [ "pop3" ] };
++ lib.optionals cfg.enableManageSieve [ "sieve" ]; "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" = { # protocols
mail_plugins = [ protocols = {
"$mail_plugins" lmtp = true;
"sieve" 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"
]; ];
}; ssl_curve_list = lib.concatStringsSep ":" [
"protocol imap" = { "X25519MLKEM768"
mail_max_userip_connections = cfg.maxConnectionsPerUser; "X25519"
mail_plugins = [ "prime256v1"
"$mail_plugins" "secp384r1"
"imap_sieve"
]
++ lib.optionals cfg.quota.enable [
"imap_quota"
]; ];
};
"protocol pop3" = {
mail_max_userip_connections = cfg.maxConnectionsPerUser;
};
# globally enabled plugins # default user mailboxes
mail_plugins = [ "namespace inbox" = {
"$mail_plugins" inbox = true;
] separator = cfg.hierarchySeparator;
++ lib.optionals cfg.quota.enable [ }
"quota" // mapAttrs' (name: value: nameValuePair ''mailbox "${name}"'' value) cfg.mailboxes;
] lda_mailbox_autosubscribe = true;
++ lib.optionals cfg.fullTextSearch.enable [ lda_mailbox_autocreate = true;
"fts"
"fts_flatcurve"
];
# tls settings # subaddressing
ssl_cert = "<${x509CertificateFile}"; recipient_delimiter = cfg.recipientDelimiter;
ssl_key = "<${x509PrivateKeyFile}"; lmtp_save_to_detail_mailbox = cfg.lmtpSaveToDetailMailbox;
# 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"
];
# default mail storage # sieve filtering
# https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory "sieve_script spamfilter" = {
mail_location = # junk filter
"maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}" path = pkgs.writeText "after.sieve" ''
+ (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}"); 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"; driver = "passwd-file";
args = "${passwdFile}"; passwd_file_path = userdbFile;
} fields = {
] home = "${cfg.storage.path}/%{user | domain}/%{user | username}";
++ lib.optionals cfg.ldap.enable [ inherit (cfg.storage) uid gid;
{ mail_index_path = "${
driver = "ldap"; if cfg.indexDir != null then cfg.indexDir else cfg.storage.path
args = "${ldapConfFile}"; }/%{user | domain }/%{user | username}";
} };
]; };
userdb = [ "passdb declarative" = {
{
driver = "passwd-file"; driver = "passwd-file";
args = "${userdbFile}"; passwd_file_path = passwdFile;
default_fields = [ };
"home=${cfg.storage.path}/%{domain}/%{username}"
"uid=${builtins.toString cfg.storage.uid}" })
"gid=${builtins.toString cfg.storage.gid}" (mkIf cfg.ldap.enable {
]; # ldap users
} ssl_client_ca_file = cfg.ldap.caFile;
] ssl_client_require_valid_cert = true;
++ lib.optionals cfg.ldap.enable [
{ 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"; driver = "ldap";
args = "${ldapConfFile}"; filter = cfg.ldap.dovecot.userFilter;
override_fields = [ fields = {
"uid=${toString cfg.storage.uid}" home = "${cfg.storage.path}/ldap/%{ldap:${cfg.ldap.attributes.uuid}}";
"gid=${toString cfg.storage.gid}" 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_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";
// lib.optionalAttrs cfg.debug.dovecot { storage_size = mkIf (cfg.quota.defaults.perUser != null) cfg.quota.defaults.perUser;
mail_debug = true; };
auth_debug = true; })
verbose_ssl = true; (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 = { 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([