mailsystem: Add minimal dovecot configuration

This commit is contained in:
Thomas Preisner 2024-11-30 19:35:15 +01:00
parent 56feea5754
commit c738037669
4 changed files with 309 additions and 0 deletions

255
mailsystem/dovecot.nix Normal file
View file

@ -0,0 +1,255 @@
{
config,
lib,
pkgs,
...
}:
with (import ./common.nix {inherit config;}); let
cfg = config.mailsystem;
postfixCfg = config.services.postfix;
dovecot2Cfg = config.services.dovecot2;
runtimeStateDir = "/run/dovecot2";
staticPasswdFile = "${runtimeStateDir}/passwd";
initialPasswdFile = "${runtimeStateDir}/initial-passwd";
userdbFile = "${runtimeStateDir}/userdb";
# dovecotDynamicStateDir and dovecotDynamicPasswdFile are defined in common.nix
systemUsers = lib.filterAttrs (user: value: value.isSystemUser) cfg.accounts;
normalUsers = lib.filterAttrs (user: value: !value.isSystemUser) cfg.accounts;
genUserdbEntry = user: value: "${user}:::::::";
genPasswdEntry = user: value: "${user}:${"$(head -n 1 ${value.hashedPasswordFile})"}::::::";
genAuthDbsScript = pkgs.writeScript "generate-dovecot-auth-dbs" ''
#!${pkgs.stdenv.shell}
set -euo pipefail
if (! test -d "${runtimeStateDir}"); then
mkdir "${runtimeStateDir}"
chmod 755 "${runtimeStateDir}"
fi
if (! test -d "${dovecotDynamicStateDir}"); then
mkdir "${dovecotDynamicStateDir}"
chmod 755 "${dovecotDynamicStateDir}"
fi
# Ensure passwd files are not world-readable at any time
umask 077
# Ensure we have a file for every user's (initial) password hash.
for f in ${builtins.toString (lib.mapAttrsToList (user: value: value.hashedPasswordFile) cfg.accounts)}; do
if [ ! -f "$f" ]; then
echo "Expected password hash file $f does not exist!"
exit 1
fi
done
# Prepare static passwd-file for system users
cat <<EOF > "${staticPasswdFile}"
${lib.concatStringsSep "\n" (lib.mapAttrsToList genPasswdEntry systemUsers)}
EOF
# Prepare initial passwd-file for dynamic users
# (used for lookup during actual passwd-file generation)
cat <<EOF > "${initialPasswdFile}"
${lib.concatStringsSep "\n" (lib.mapAttrsToList genPasswdEntry normalUsers)}
EOF
# Check for existence of dynamic passwd-file
touch "${dovecotDynamicPasswdFile}"
if (! test -f "${dovecotDynamicPasswdFile}"); then
echo "${dovecotDynamicPasswdFile} exists and is no regular file"
exit 1
fi
# Ensure that only configured users are actually present and remove any others
truncate -s 0 "${dovecotDynamicPasswdFile}-filtered"
for u in ${builtins.toString (lib.mapAttrsToList (user: value: value.name) normalUsers)}; do
if grep -q "^$u:" "${dovecotDynamicPasswdFile}"; then
# User already has some password set -> Keep currently set password
grep "^$u:" "${dovecotDynamicPasswdFile}" >> "${dovecotDynamicPasswdFile}-filtered"
else
# User has no password set, yet -> Take password from initialPasswdFile
grep "^$u:" "${initialPasswdFile}" >> "${dovecotDynamicPasswdFile}-filtered"
fi
done
mv "${dovecotDynamicPasswdFile}-filtered" "${dovecotDynamicPasswdFile}"
# Prepare userdb-file
cat <<EOF > "${userdbFile}"
${lib.concatStringsSep "\n" (lib.mapAttrsToList genUserdbEntry cfg.accounts)}
EOF
'';
genMaildir = pkgs.writeScript "generate-maildir" ''
#!${pkgs.stdenv.shell}
# Create mail directory and set permissions accordingly.
umask 007
mkdir -p ${cfg.mailDirectory}
chgrp "${cfg.vmailGroupName}" ${cfg.mailDirectory}
chmod 02770 ${cfg.mailDirectory}
'';
in {
options.mailsystem.dovecot.dhparamSize = lib.mkOption {
type = lib.types.int;
default = 2048;
description = "The bit size for the prime that is used during a Diffie-Hellman key exchange by dovecot.";
};
config = lib.mkIf cfg.enable {
assertions =
lib.mapAttrsToList (user: value: [
{
assertion = value.hashedPasswordFile != null;
message = "A file containing the hashed password for user ${user} needs to be set.";
}
])
cfg.accounts;
services.dovecot2 = {
enable = true;
enableImap = true;
enablePAM = false;
# TODO: enable quota and setup quota warnings
#enableQuota = true;
mailUser = cfg.vmailUserName;
mailGroup = cfg.vmailGroupName;
mailLocation = "maildir:~/Maildir";
sslServerCert = sslCertPath;
sslServerKey = sslKeyPath;
enableLmtp = true;
modules = [
# sieves + managesieve
pkgs.dovecot_pigeonhole
];
# enable managesieve
protocols = ["sieve"];
pluginSettings = {
sieve = "file:~/sieve;active=~/.dovecot.sieve";
};
# TODO: move configuration to default.nix?
mailboxes = {
Drafts = {
auto = "subscribe";
specialUse = "Drafts";
};
Junk = {
auto = "subscribe";
specialUse = "Junk";
};
Trash = {
auto = "subscribe";
specialUse = "Trash";
};
Sent = {
auto = "subscribe";
specialUse = "Sent";
};
};
extraConfig = ''
service imap-login {
inet_listener imap {
# Using starttls for encryption shifts the responsibility for
# encrypted communication over to the client in the face of MITM.
# However, it has shown that clients are often not enforcing it.
port = 0 # clients should use starttls instaed
}
inet_listener imaps {
port = 993
ssl = yes
}
}
protocol imap {
mail_max_userip_connections = 100
mail_plugins = $mail_plugins imap_sieve
}
mail_access_groups = ${cfg.vmailGroupName}
ssl = required
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
service lmtp {
unix_listener dovecot-lmtp {
user = ${postfixCfg.user}
group = ${postfixCfg.group}
mode = 0600
}
}
recipient_delimiter = +
lmtp_save_to_detail_mailbox = no
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
# Passwords stored among two passwd-files: One for users allowed to
# change their password and one for any other user (mostly system
# users) with immutable passwords.
passdb {
driver = passwd-file
args = ${dovecotDynamicPasswdFile}
}
passdb {
driver = passwd-file
args = ${staticPasswdFile}
}
userdb {
driver = passwd-file
args = ${userdbFile}
default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}/%d/%n
}
service auth {
unix_listener auth {
user = ${postfixCfg.user}
group = ${postfixCfg.group}
mode = 0660
}
}
auth_mechanisms = plain login
namespace inbox {
separator = /
inbox = yes
}
lda_mailbox_autosubscribe = yes
lda_mailbox_autocreate = yes
'';
};
security.dhparams.params.dovecot2.bits = cfg.dovecot.dhparamSize;
systemd.services.dovecot2 = {
preStart = ''
${genAuthDbsScript}
${genMaildir}
'';
wants = sslCertService;
after = sslCertService;
};
systemd.services.postfix.restartTriggers = [genAuthDbsScript];
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [
993 # imaps
4190 #managesieve
];
};
};
}