mailnix/mailsystem/dovecot.nix
2024-12-28 01:42:00 +01:00

313 lines
9.1 KiB
Nix

{
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}:::::::"
+ (
if lib.isString value.quota
then "userdb_quota_rule=*:storage=${value.quota}"
else ""
);
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}
'';
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") dovecot2Cfg.mailboxes);
junkMailboxNumber = builtins.length junkMailboxes;
# The assertion guarantees that there is exactly one Junk mailbox.
junkMailboxName =
if junkMailboxNumber == 1
then builtins.elemAt junkMailboxes 0
else "";
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 =
[
{
assertion = junkMailboxNumber == 1;
message = "mailnix requires exactly one dovecot mailbox with the 'special use' flag to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
}
]
++ 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";
};
sieve = {
extensions = [
"fileinto"
"mailbox"
];
scripts.after = builtins.toFile "spam.sieve" ''
require "fileinto";
require "mailbox";
if header :is "X-Spam" "Yes" {
fileinto :create "${junkMailboxName}";
stop;
}
'';
pipeBins = map lib.getExe [
(pkgs.writeShellScriptBin "learn-ham.sh"
"exec ${pkgs.rspamd}/bin/rspamc -h ${rspamdControllerSocket} learn_ham")
(pkgs.writeShellScriptBin "learn-spam.sh"
"exec ${pkgs.rspamd}/bin/rspamc -h ${rspamdControllerSocket} learn_spam")
];
};
imapsieve.mailbox = [
{
name = junkMailboxName;
causes = ["COPY" "APPEND"];
before = ./dovecot/report-spam.sieve;
}
{
name = "*";
from = junkMailboxName;
causes = ["COPY"];
before = ./dovecot/report-ham.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
];
};
};
}