Initial checkin
This commit is contained in:
commit
49897ba6fe
|
@ -0,0 +1,58 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let cfg = config.fudo.mail.clamav;
|
||||||
|
|
||||||
|
in {
|
||||||
|
options.fudo.mail.clamav = with types; {
|
||||||
|
enable = mkEnableOption "Enable virus scanning with ClamAV.";
|
||||||
|
|
||||||
|
state-directory = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Path at which to store ClamAV database.";
|
||||||
|
default = "/var/lib/clamav";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port on which to listen for incoming requests.";
|
||||||
|
default = 15407;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
users = {
|
||||||
|
users.clamav = {
|
||||||
|
isSystemUser = true;
|
||||||
|
uid = config.ids.uids.clamav;
|
||||||
|
home = mkForce cfg.state-directory;
|
||||||
|
description = "ClamAV daemon user";
|
||||||
|
group = "clamav";
|
||||||
|
};
|
||||||
|
groups.clamav = {
|
||||||
|
members = [ "clamav" ];
|
||||||
|
gid = config.ids.gids.clamav;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules =
|
||||||
|
[ "d ${cfg.state-directory} 0750 clamav clamav - -" ];
|
||||||
|
|
||||||
|
services.clamav = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
PhishingScanURLs = "no";
|
||||||
|
DatabaseDirectory = mkForce cfg.state-directory;
|
||||||
|
User = "clavmav";
|
||||||
|
TCPSocket = cfg.port;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
updater = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
DatabaseDirectory = mkForce cfg.state-directory;
|
||||||
|
DatabaseOwner = "clamav";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
cfg = config.fudo.mail.dkim;
|
||||||
|
|
||||||
|
ensureDomainDkimCert = keyDir: domain:
|
||||||
|
let
|
||||||
|
dkimKey = "${keyDir}/${domain}.mail.key";
|
||||||
|
dkimTxt = "${keyDir}/${domain}.mail.txt";
|
||||||
|
in ''
|
||||||
|
if [ ! -f "${dkimKey}" ] || [ ! -f ${dkimTxt} ]; then
|
||||||
|
opendkim-genkey \
|
||||||
|
-s mail \
|
||||||
|
-d ${domain} \
|
||||||
|
--bits="${toString cfg.key-bits}" \
|
||||||
|
--directory=$TMPDIR
|
||||||
|
mv $TMPDIR/mail.private ${dkimKey}
|
||||||
|
mv $TMPDIR/mail.txt ${dkimTxt}
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
ensureAllDkimCerts = keyDir:
|
||||||
|
domains concatStringsSep "\n" (map (ensureDomainDkimCert keyDir) domains);
|
||||||
|
|
||||||
|
makeKeyTable = keyDir: domains:
|
||||||
|
pkgs.writeText "opendkim-key-table" (concatStringsSep "\n"
|
||||||
|
(map (dom: "${dom}:mail:${keyDir}/${dom}.mail.key")));
|
||||||
|
|
||||||
|
makeSigningTable = domains:
|
||||||
|
pkgs.writeText "opendkim-signing-table"
|
||||||
|
(concatStringsSep "\n" (map (dom: "${dom} ${dom}") domains));
|
||||||
|
|
||||||
|
in {
|
||||||
|
options.fudo.mail.dkim = with types; {
|
||||||
|
enable = mkEnableOption "Enable DKIM signature verification.";
|
||||||
|
|
||||||
|
debug = mkEnableOption "Enable debug logs.";
|
||||||
|
|
||||||
|
domains = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description =
|
||||||
|
"List of domains to be considered local, and signed instead of verified.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port at which to listen for incoming signing requests.";
|
||||||
|
default = 5324;
|
||||||
|
};
|
||||||
|
|
||||||
|
state-directory = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Directory at which to store DKIM state (i.e. keys).";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
services.opendkim = {
|
||||||
|
enable = true;
|
||||||
|
selector = cfg.selector;
|
||||||
|
domains = let domainString = concatStringsSep "," cfg.domains;
|
||||||
|
in "csl:${domainsString}";
|
||||||
|
configFile = let
|
||||||
|
debugString = ''
|
||||||
|
Syslog yes
|
||||||
|
SyslogSuccess yes
|
||||||
|
LogWhy yes
|
||||||
|
'';
|
||||||
|
in pkgs.writeText "opendkim.conf" ''
|
||||||
|
Canonicalization relaxed/simple
|
||||||
|
Socket inet:${toString cfg.port}
|
||||||
|
KeyTable file: ${keyTable}
|
||||||
|
SigningTable file:${signingTable}
|
||||||
|
${optionalString cfg.debug debugString}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
tmpfiles.rules = let
|
||||||
|
user = config.services.opendkim.user;
|
||||||
|
group = config.services.opendkim.group;
|
||||||
|
in [ "d ${cfg.state-directory} 0700 ${user} ${group} - -" ];
|
||||||
|
services.opendkim = {
|
||||||
|
serviceConfig.ReadWritePaths = [ cfg.state-directory ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,440 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
sievePath = let
|
||||||
|
isRegularFile = _: type: type == "regular";
|
||||||
|
sieves = filter isRegularFile (builtins.readDir ./sieves);
|
||||||
|
headOrNull = lst: if lst == [ ] then null else head lst;
|
||||||
|
stripExt = ext: filename: headOrNull (match "(.+)[.]${ext}$" filename);
|
||||||
|
compileFile = filename:
|
||||||
|
let
|
||||||
|
filePath = ./sieves + "/${filename}";
|
||||||
|
fileBaseName = stripExt "sieve" filename;
|
||||||
|
in "sievec ${filePath} $out/${fileBaseName}.svbin";
|
||||||
|
in pkgs.stdenv.mkDerivation {
|
||||||
|
name = "dovecot-sieves";
|
||||||
|
buildInputs = with pkgs; [ dovecot_pidgeonhole ];
|
||||||
|
phases = [ "installPhase" ];
|
||||||
|
buildPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
${concatStringsSep "\n" (map compileFile sieves)}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
in {
|
||||||
|
options.fudo.mail.dovecot = with types; {
|
||||||
|
enable = mkEnableOption "Enable Dovecot2 IMAP server.";
|
||||||
|
|
||||||
|
debug = mkEnableOption "Enable debug logs.";
|
||||||
|
|
||||||
|
state-directory = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Directory at which to store server state.";
|
||||||
|
};
|
||||||
|
|
||||||
|
ports = {
|
||||||
|
lmtp = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port on which to listen for LMTP connections.";
|
||||||
|
default = 24;
|
||||||
|
};
|
||||||
|
auth = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port on which to listen for auth requests.";
|
||||||
|
default = 5447;
|
||||||
|
};
|
||||||
|
userdb = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port on which to listen for userdb requests.";
|
||||||
|
default = 5448;
|
||||||
|
};
|
||||||
|
metrics = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port on which to serve metrics data.";
|
||||||
|
default = 5034;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
mail-user = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "User as which to run store & access mail.";
|
||||||
|
default = "fudo-mail";
|
||||||
|
};
|
||||||
|
|
||||||
|
mail-group = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Group as which to store & access mail.";
|
||||||
|
default = "fudo-mail";
|
||||||
|
};
|
||||||
|
|
||||||
|
ssl = {
|
||||||
|
certificate = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Location of the Dovecot SSL certificate.";
|
||||||
|
};
|
||||||
|
|
||||||
|
private-key = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Location of the Dovecot SSL private key.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
metrics = {
|
||||||
|
user = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "User as which to fetch metrics.";
|
||||||
|
default = "dovecot-metrics";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Group as which to fetch metrics.";
|
||||||
|
default = "dovecot-metrics";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
mailboxes = let
|
||||||
|
mailboxOpts = { name, ... }: {
|
||||||
|
options = {
|
||||||
|
name = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Mailbox Name.";
|
||||||
|
default = name;
|
||||||
|
};
|
||||||
|
auto = mkOption {
|
||||||
|
type = enum [ "no" "create" "subscribe" ];
|
||||||
|
description = "Whether to auto-create/subscribe.";
|
||||||
|
default = "no";
|
||||||
|
};
|
||||||
|
specialUse = mkOption {
|
||||||
|
type = nullOr (enum [
|
||||||
|
"All"
|
||||||
|
"Archive"
|
||||||
|
"Drafts"
|
||||||
|
"Flagged"
|
||||||
|
"Junk"
|
||||||
|
"Sent"
|
||||||
|
"Trash"
|
||||||
|
]);
|
||||||
|
description = "Mailbox special use.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
autoexpunge = mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
description =
|
||||||
|
"How long to wait before clearing mail from this mailbox. Null is never.";
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in mkOption {
|
||||||
|
type = attrsOf (submodule mailboxOpts);
|
||||||
|
description = "Mailboxes to be created for dovecot.";
|
||||||
|
default = {
|
||||||
|
Trash = {
|
||||||
|
auto = "create";
|
||||||
|
specialUse = "Trash";
|
||||||
|
autoexpunge = "30d";
|
||||||
|
};
|
||||||
|
Junk = {
|
||||||
|
auto = "create";
|
||||||
|
specialUse = "Junk";
|
||||||
|
autoexpunge = "60d";
|
||||||
|
};
|
||||||
|
Drafts = {
|
||||||
|
auto = "create";
|
||||||
|
specialUse = "Drafts";
|
||||||
|
autoexpunge = "60d";
|
||||||
|
};
|
||||||
|
Sent = {
|
||||||
|
auto = "create";
|
||||||
|
specialUse = "Sent";
|
||||||
|
};
|
||||||
|
Archive = {
|
||||||
|
atuo = "no";
|
||||||
|
specialUse = "Archive";
|
||||||
|
};
|
||||||
|
Flagged = {
|
||||||
|
auto = "subscribe";
|
||||||
|
specialUse = "Flagged";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
rspamd = {
|
||||||
|
host = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Host to which spam/ham will be forwarded.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Port to which spam/ham will be forwarded.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
max-user-connections = mkOption {
|
||||||
|
type = int;
|
||||||
|
description = "Maximum allowed simultaneous connections by one user.";
|
||||||
|
default = 5;
|
||||||
|
};
|
||||||
|
|
||||||
|
ldap = let
|
||||||
|
ldapOpts = {
|
||||||
|
options = {
|
||||||
|
host = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "LDAP hostname.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Port on which LDAP is listening.";
|
||||||
|
};
|
||||||
|
|
||||||
|
base = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Base of the LDAP server database.";
|
||||||
|
example = "dc=mydomain,dc=org";
|
||||||
|
};
|
||||||
|
|
||||||
|
bind-dn = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = ''
|
||||||
|
DN used for fetching user information.
|
||||||
|
|
||||||
|
Needs access to homeDirectory, uidNumber, gidNumber, and uid, but not
|
||||||
|
password attributes.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
bind-password-file = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Path to file containing bind password for bind-dn.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in mkOption {
|
||||||
|
type = nullOr ldapOpts;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
services = {
|
||||||
|
prometheus.exporters.dovecot = {
|
||||||
|
enable = true;
|
||||||
|
scopes = [ "user" "global" ];
|
||||||
|
user = cfg.metrics.user;
|
||||||
|
listenAddresses = "127.0.0.1";
|
||||||
|
port = cfg.metrics.port;
|
||||||
|
socketPath = "/var/run/dovecot2/old-stats";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users = {
|
||||||
|
users = {
|
||||||
|
"${cfg.mail-user}" = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.mail-group;
|
||||||
|
};
|
||||||
|
"${cfg.metrics.user}" = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.metrics.group;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
groups = {
|
||||||
|
"${cfg.mail-group}".members = [ cfg.mail-user ];
|
||||||
|
"${cfg.metrics.group}".members = [ cfg.metrics.user ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd = {
|
||||||
|
tmpfiles.rules = [
|
||||||
|
"d ${cfg.state-directory} 0750 ${cfg.mail-user} ${cfg.mail-group} - -"
|
||||||
|
"d ${cfg.state-directory}/mail 0750 ${cfg.mail-user} ${cfg.mail-group} - -"
|
||||||
|
"d ${cfg.state-directory}/sieves 0750 ${cfg.mail-user} ${cfg.mail-group} - -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
dovecot2 = {
|
||||||
|
enable = true;
|
||||||
|
enableImap = true;
|
||||||
|
enableLmtp = true;
|
||||||
|
enablePAM = cfg.ldap == null;
|
||||||
|
|
||||||
|
mailUser = cfg.mail-user;
|
||||||
|
mailGroup = cfg.mail-group;
|
||||||
|
mailLocation = "maildir:${cfg.state-directory}/mail//%u/";
|
||||||
|
createMailUser = false;
|
||||||
|
|
||||||
|
sslServerCert = cfg.ssl.certificate;
|
||||||
|
sslServerKey = cfg.ssl.private-key;
|
||||||
|
|
||||||
|
mailboxes = cfg.mailboxes;
|
||||||
|
|
||||||
|
modules = with pkgs; [ dovecot_pidgeonhole ];
|
||||||
|
protocols = [ "sieve" ];
|
||||||
|
|
||||||
|
mailPlugins.globally.enable = [ "old_stats" ];
|
||||||
|
|
||||||
|
sieveScripts = {
|
||||||
|
after = builtins.toFile "spam.sieve" ''
|
||||||
|
require "fileinto";
|
||||||
|
|
||||||
|
if header :is "X-Spam" "Yes" {
|
||||||
|
fileinto "Junk";
|
||||||
|
stop;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
extraConfig = let
|
||||||
|
# Add learn_ham & learn_spam to dovecot2 path for use by sieves
|
||||||
|
pipeBin = let
|
||||||
|
teachRspamd = msg:
|
||||||
|
pkgs.writeShellApplication {
|
||||||
|
name = "rspamd_${msg}";
|
||||||
|
runtimeInputs = with pkgs; [ rspamd ];
|
||||||
|
text =
|
||||||
|
"exec rspamc -h ${cfg.rspamd.host}:${cfg.rspam.port} ${msg}";
|
||||||
|
};
|
||||||
|
learnHam = teachRspamd "learn_ham";
|
||||||
|
learnSpam = teachRspamd "learn_spam";
|
||||||
|
in pkgs.buildEnv {
|
||||||
|
name = "rspam_pipe_bin";
|
||||||
|
paths = [ learnHam learnSpam ];
|
||||||
|
};
|
||||||
|
in ''
|
||||||
|
## Extra Config
|
||||||
|
|
||||||
|
mail_plugins = $mail_plugins
|
||||||
|
|
||||||
|
${lib.optionalString cfg.debug ''
|
||||||
|
mail_debug = yes
|
||||||
|
auth_debug = yes
|
||||||
|
verbose_ssl = yes
|
||||||
|
''}
|
||||||
|
|
||||||
|
protocol imap {
|
||||||
|
mail_max_userip_connections = ${toString cfg.max-user-connections}
|
||||||
|
mail_plugins = $mail_plugins imap_sieve
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol lmtp {
|
||||||
|
mail_plugins = $mail_plugins sieve
|
||||||
|
}
|
||||||
|
|
||||||
|
mail_access_groups = ${cfg.mail-group}
|
||||||
|
|
||||||
|
# When looking up usernames, just use the name, not the full address
|
||||||
|
auth_username_format = %n
|
||||||
|
|
||||||
|
service lmtp {
|
||||||
|
# Enable logging in debug mode
|
||||||
|
${optionalString cfg.debug "executable = lmtp -L"}
|
||||||
|
|
||||||
|
inet_listener dovecot-lmtp {
|
||||||
|
address = 0.0.0.0
|
||||||
|
port = ${toString cfg.port.lmtp}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Drop privs, since all mail is owned by one user
|
||||||
|
user = ${cfg.mail-user}
|
||||||
|
# group = ${cfg.mail-group}
|
||||||
|
# user = root
|
||||||
|
}
|
||||||
|
|
||||||
|
auth_mechanisms = login plain
|
||||||
|
|
||||||
|
${optionalString (cfg.dovecot.ldap != null) ''
|
||||||
|
passdb {
|
||||||
|
driver = ldap
|
||||||
|
args = ${cfg.dovecot.ldap.generated-ldap-config}
|
||||||
|
}
|
||||||
|
''}
|
||||||
|
userdb {
|
||||||
|
driver = static
|
||||||
|
args = uid=${
|
||||||
|
toString cfg.mail-user-id
|
||||||
|
} home=${cfg.state-directory}/mail/%u
|
||||||
|
}
|
||||||
|
|
||||||
|
# Used by postfix to authorize users
|
||||||
|
service auth {
|
||||||
|
inet_listener auth {
|
||||||
|
address = 0.0.0.0
|
||||||
|
port = ${toString cfg.ports.auth}
|
||||||
|
}
|
||||||
|
inet_listener auth-userdb {
|
||||||
|
address = 0.0.0.0
|
||||||
|
port = ${toString cfg.ports.userdb}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
service auth-worker {
|
||||||
|
user = ${config.services.dovecot2.user}
|
||||||
|
idle_kill = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
service imap {
|
||||||
|
vsz_limit = 1024M
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace inbox {
|
||||||
|
separator = "/"
|
||||||
|
inbox = yes
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin {
|
||||||
|
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||||
|
sieve = file:${cfg.state-directory}/sieves/%u/scripts;active=${cfg.state-directory}/sieves/%u/active.sieve
|
||||||
|
sieve_default = file:${cfg.sieve-directory}/%u/default.sieve
|
||||||
|
sieve_default_name = default
|
||||||
|
# From elsewhere to Spam folder
|
||||||
|
imapsieve_mailbox1_name = Junk
|
||||||
|
imapsieve_mailbox1_causes = COPY
|
||||||
|
imapsieve_mailbox1_before = file:${sievePath}/report-spam.sieve
|
||||||
|
# From Spam folder to elsewhere
|
||||||
|
imapsieve_mailbox2_name = *
|
||||||
|
imapsieve_mailbox2_from = Junk
|
||||||
|
imapsieve_mailbox2_causes = COPY
|
||||||
|
imapsieve_mailbox2_before = file:${sievePath}/report-ham.sieve
|
||||||
|
|
||||||
|
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
|
||||||
|
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
|
||||||
|
}
|
||||||
|
|
||||||
|
recipient_delimiter = +
|
||||||
|
|
||||||
|
lmtp_save_to_detail_mailbox = yes
|
||||||
|
|
||||||
|
lda_mailbox_autosubscribe = yes
|
||||||
|
lda_mailbox_autocreate = yes
|
||||||
|
|
||||||
|
service old-stats {
|
||||||
|
unix_listener old-stats {
|
||||||
|
user = ${cfg.metrics.user}
|
||||||
|
group = ${cfg.metrics.group}
|
||||||
|
}
|
||||||
|
fifo_listener old-stats-mail {
|
||||||
|
mode = 0660
|
||||||
|
user = ${config.services.dovecot2.user}
|
||||||
|
group = ${config.services.dovecot2.group}
|
||||||
|
}
|
||||||
|
fifo_listener old-stats-user {
|
||||||
|
mode = 0660
|
||||||
|
user = ${config.services.dovecot2.user}
|
||||||
|
group = ${config.services.dovecot2.group}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin {
|
||||||
|
old_stats_refresh = 30 secs
|
||||||
|
old_stats_track_cmds = yes
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
description = "Mail server running in containers.";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "nixpkgs/nixos-23.05";
|
||||||
|
arion.url = "github:hercules-ci/arion";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, arion, ... }: {
|
||||||
|
nixosModules = rec {
|
||||||
|
default = mailServerContainer;
|
||||||
|
mailServerContainer = { ... }: {
|
||||||
|
imports = [ arion.nixosModules.arion ./mail-server.nix ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,291 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib; {
|
||||||
|
options.fudo.mail = with types; {
|
||||||
|
enable = mkEnableOption "Enable mail server.";
|
||||||
|
|
||||||
|
state-directory = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Directory at which to store server state.";
|
||||||
|
};
|
||||||
|
|
||||||
|
primary-domain = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Primary domain name served by this server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
extra-domains = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description = "List of additional domains served by this server.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
smtp = {
|
||||||
|
hostname = mkOption {
|
||||||
|
type = str;
|
||||||
|
description =
|
||||||
|
"Hostname too use for the SMTP server. Must resolve to this host.";
|
||||||
|
default = "smtp.${config.fudo.mail.primary-domain}";
|
||||||
|
};
|
||||||
|
|
||||||
|
ssl = {
|
||||||
|
certificate = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "SSL certificate for the SMTP host.";
|
||||||
|
};
|
||||||
|
private-key = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "SSL private key for the SMTP host.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
imap = {
|
||||||
|
hostname = mkOption {
|
||||||
|
type = str;
|
||||||
|
description =
|
||||||
|
"Hostname too use for the IMAP server. Must resolve to this host.";
|
||||||
|
default = "imap.${config.fudo.mail.primary-domain}";
|
||||||
|
};
|
||||||
|
|
||||||
|
ssl = {
|
||||||
|
certificate = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "SSL certificate for the IMAP host.";
|
||||||
|
};
|
||||||
|
private-key = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "SSL private key for the IMAP host.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
services.nginx = {
|
||||||
|
virtualHosts = {
|
||||||
|
"${cfg.smtp.hostname}".locations."/metrics" = {
|
||||||
|
proxyPass = "http://localhost:${metricsPort}/metrics";
|
||||||
|
};
|
||||||
|
"${cfg.imap.hostname}".locations."/metrics" = {
|
||||||
|
proxyPass = "http://localhost:${metricsPort}/metrics";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
virtualisation.arion.projects.mail-server.settings = let
|
||||||
|
image = { pkgs, ... }: {
|
||||||
|
project.name = "fudo-mailserver";
|
||||||
|
networks = {
|
||||||
|
external_network.internal = false;
|
||||||
|
internal_network.internal = true;
|
||||||
|
};
|
||||||
|
serices = let
|
||||||
|
antivirusPort = 15407;
|
||||||
|
antispamPort = 11335;
|
||||||
|
lmtpPort = 24;
|
||||||
|
authPort = 5447;
|
||||||
|
userdbPort = 5448;
|
||||||
|
metricsPort = 5034;
|
||||||
|
in {
|
||||||
|
smtp = {
|
||||||
|
networks = [
|
||||||
|
"internal_network"
|
||||||
|
# Needs access to internet to forward emails
|
||||||
|
"external_network"
|
||||||
|
];
|
||||||
|
ports = [ "25:25" "587:587" "465:465" "2525:2525" ];
|
||||||
|
nixos = {
|
||||||
|
useSystemd = true;
|
||||||
|
configuration = [
|
||||||
|
(import ./postfix.nix)
|
||||||
|
{
|
||||||
|
boot.tmpOnTmpfs = true;
|
||||||
|
system.nssModules = lib.mkForce [ ];
|
||||||
|
fudo.mail.postfix = {
|
||||||
|
enable = true;
|
||||||
|
debug = cfg.debug;
|
||||||
|
domain = cfg.primary-domain;
|
||||||
|
local-domains = cfg.extra-domains;
|
||||||
|
hostname = cfg.smtp.hostname;
|
||||||
|
trusted-networks = cfg.trusted-networks;
|
||||||
|
blacklist = {
|
||||||
|
senders = cfg.blacklist.senders;
|
||||||
|
recipients = cfg.blacklist.recipients;
|
||||||
|
dns = cfg.blacklist.dns;
|
||||||
|
};
|
||||||
|
aliases = {
|
||||||
|
user-aliases = cfg.user-aliases;
|
||||||
|
alias-users = cfg.alias-users;
|
||||||
|
};
|
||||||
|
ssl = {
|
||||||
|
certificate = cfg.smtp.ssl.certificate;
|
||||||
|
private-key = cfg.smtp.ssl.private-key;
|
||||||
|
};
|
||||||
|
sasl-domain = cfg.sasl-domain;
|
||||||
|
message-size-limit = cfg.message-size-limit;
|
||||||
|
ports = { metrics = metricsPort; };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
imap = {
|
||||||
|
networks = [ "internal_network" ];
|
||||||
|
ports = [ "143:143" "993:993" ];
|
||||||
|
nixos = {
|
||||||
|
useSystemd = true;
|
||||||
|
configuration = [
|
||||||
|
(import ./dovecot.nix)
|
||||||
|
{
|
||||||
|
boot.tmpOnTmpfs = true;
|
||||||
|
system.nssModules = lib.mkForce [ ];
|
||||||
|
fudo.mail.dovecot = {
|
||||||
|
enable = true;
|
||||||
|
debug = cfg.debug;
|
||||||
|
state-directory = "${cfg.state-directory}/dovecot";
|
||||||
|
ports = {
|
||||||
|
lmtp = lmtpPort;
|
||||||
|
auth = authPort;
|
||||||
|
userdb = userdbPort;
|
||||||
|
metrics = metricsPort;
|
||||||
|
};
|
||||||
|
mail-user = cfg.mail-user;
|
||||||
|
mail-group = cfg.mail-group;
|
||||||
|
ssl = {
|
||||||
|
certificate = cfg.imap.ssl.certificate;
|
||||||
|
private-key = cfg.imap.ssl.private-key;
|
||||||
|
};
|
||||||
|
rspamd = {
|
||||||
|
host = "antispam";
|
||||||
|
port = antispamPort;
|
||||||
|
};
|
||||||
|
ldap = mkIf cfg.ldap-proxy {
|
||||||
|
host = "ldap-proxy";
|
||||||
|
port = 3389;
|
||||||
|
base = cfg.ldap.base;
|
||||||
|
bind-dn = cfg.ldap.bind-dn;
|
||||||
|
bind-password-file = cfg.ldap.bind-password-file;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
ldap-proxy.service = mkIf (cfg.ldap-proxy != null) {
|
||||||
|
image = cfg.images.ldap-proxy;
|
||||||
|
restart = "always";
|
||||||
|
networks = [
|
||||||
|
"internal_network"
|
||||||
|
# Needs access to external network for user lookups
|
||||||
|
"external_network"
|
||||||
|
];
|
||||||
|
envFile = hostSecrets.mailLdapProxyEnv.target-file;
|
||||||
|
};
|
||||||
|
antispam = {
|
||||||
|
networks = [
|
||||||
|
"internal_network"
|
||||||
|
# Needs external access for blacklist checks
|
||||||
|
"external_network"
|
||||||
|
];
|
||||||
|
nixos = {
|
||||||
|
useSystemd = true;
|
||||||
|
configuration = [
|
||||||
|
(import ./rspamd.nix)
|
||||||
|
{
|
||||||
|
boot.tmpOnTmpfs = true;
|
||||||
|
system.nssModules = lib.mkForce [ ];
|
||||||
|
fudo.mail.rspamd = {
|
||||||
|
enable = true;
|
||||||
|
ports = {
|
||||||
|
milter = antispamPort;
|
||||||
|
controller = antispamControllerPort;
|
||||||
|
metrics = metricsPort;
|
||||||
|
};
|
||||||
|
antivirus = {
|
||||||
|
host = "antivirus";
|
||||||
|
port = antivirusPort;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
antivirus = {
|
||||||
|
networks = [
|
||||||
|
"internal_network"
|
||||||
|
# Needs external access for database updates
|
||||||
|
"external_network"
|
||||||
|
];
|
||||||
|
nixos = {
|
||||||
|
useSystemd = true;
|
||||||
|
configuration = [
|
||||||
|
(import ./clamav.nix)
|
||||||
|
{
|
||||||
|
boot.tmpOnTmpfs = true;
|
||||||
|
system.nssModules = lib.mkForce [ ];
|
||||||
|
fudo.mail.clamav = {
|
||||||
|
enable = true;
|
||||||
|
state-directory = "${cfg.state-directory}/rspamd";
|
||||||
|
port = antispamPort;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
dkim = {
|
||||||
|
networks = [ "internal_network" ];
|
||||||
|
nixos = {
|
||||||
|
useSystemd = true;
|
||||||
|
configuration = [
|
||||||
|
(import ./dkim.nix)
|
||||||
|
{
|
||||||
|
boot.tmpOnTmpfs = true;
|
||||||
|
system.nssModules = lib.mkForce [ ];
|
||||||
|
fudo.mail.dkim = {
|
||||||
|
enable = true;
|
||||||
|
debug = cfg.debug;
|
||||||
|
domains = [ cfg.primary-domain ] ++ cfg.extra-domains;
|
||||||
|
};
|
||||||
|
port = dkimPort;
|
||||||
|
state-directory = "${cfg.state-directory}/dkim";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
metrics-proxy = {
|
||||||
|
networks = [ "internal_network" ];
|
||||||
|
ports = [ "${cfg.metricsPort}:80" ];
|
||||||
|
nixos = {
|
||||||
|
useSystemd = true;
|
||||||
|
configuration = {
|
||||||
|
boot.tmpOnTmpfs = true;
|
||||||
|
system.nssModules = lib.mkForce [ ];
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
recommendedGzipSettings = true;
|
||||||
|
recommendedOptimisation = true;
|
||||||
|
virtualHosts.localhost = {
|
||||||
|
default = true;
|
||||||
|
locations = {
|
||||||
|
"/postfix" = {
|
||||||
|
proxyPass = "http://smtp:${metricsPort}/";
|
||||||
|
};
|
||||||
|
"/dovecot" = {
|
||||||
|
proxyPass = "http://imap:${metricsPort}/";
|
||||||
|
};
|
||||||
|
"rspamd" = {
|
||||||
|
proxyPass = "http://antispam:${metricsPort}/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in { imports = [ image ]; };
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,381 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let allDomains = [ cfg.domain ] ++ cfg.local-domains;
|
||||||
|
|
||||||
|
in {
|
||||||
|
options.fudo.mail.postfix = with types; {
|
||||||
|
enable = mkEnableOption "Enable Postfix SMTP server.";
|
||||||
|
|
||||||
|
domain = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Primary domain served by this mail server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
local-domains = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description =
|
||||||
|
"List of domains to be considered local to this server. Don't include the primary domain.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
hostname = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Fully-qualified hostname of this mail server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
trusted-networks = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description = "List of trusted network ranges.";
|
||||||
|
};
|
||||||
|
|
||||||
|
blacklist = {
|
||||||
|
senders = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description =
|
||||||
|
"List of email addresses for which we will never send email.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
recipients = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description =
|
||||||
|
"List of email addresses for which we will not accept email.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
|
||||||
|
dns = mkOption {
|
||||||
|
type = listOf str;
|
||||||
|
description = "List of DNS spam blacklists to use.";
|
||||||
|
default = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
aliases = {
|
||||||
|
user-aliases = mkOption {
|
||||||
|
type = attrsOf (listOf str);
|
||||||
|
description =
|
||||||
|
"Map of username to list of emails belonging to that user.";
|
||||||
|
default = { };
|
||||||
|
example = { some_user = [ "foo@bar.com" "baz@bar.com" ]; };
|
||||||
|
};
|
||||||
|
|
||||||
|
alias-users = mkOption {
|
||||||
|
type = attrsOf (listOf str);
|
||||||
|
description =
|
||||||
|
"Map of aliases to list of accounts which should receive incoming email.";
|
||||||
|
default = { };
|
||||||
|
example = { hostmaster = [ "admin0" "admin1" ]; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
ssl = {
|
||||||
|
certificate = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Location of host SSL certificate.";
|
||||||
|
};
|
||||||
|
|
||||||
|
private-key = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Location of host SSL private key.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sasl-domain = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "SASL domain to use for authentication.";
|
||||||
|
};
|
||||||
|
|
||||||
|
policy-spf.extraConfig = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "";
|
||||||
|
example = "skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1";
|
||||||
|
description = "Extra configuration options for policyd-spf.";
|
||||||
|
};
|
||||||
|
|
||||||
|
user = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "User as which to run Postfix server.";
|
||||||
|
default = "postfix";
|
||||||
|
};
|
||||||
|
|
||||||
|
group = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Group as which to run Postfix server.";
|
||||||
|
default = "postfix";
|
||||||
|
};
|
||||||
|
|
||||||
|
message-size-limit = mkOption {
|
||||||
|
type = int;
|
||||||
|
description = "Max size of email messages, in MB.";
|
||||||
|
default = 200;
|
||||||
|
};
|
||||||
|
|
||||||
|
ports = {
|
||||||
|
metrics = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port on which to listen for metrics requests.";
|
||||||
|
default = 1725;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
users = {
|
||||||
|
users."${cfg.user}" = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
};
|
||||||
|
|
||||||
|
groups."${cfg.group}".members = [ cfg.user ];
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
prometheus.exporters.postfix = {
|
||||||
|
enable = true;
|
||||||
|
systemd.enable = true;
|
||||||
|
showqPath = "/var/lib/postfix/queue/public/showq";
|
||||||
|
group = config.services.postfix.group;
|
||||||
|
listenAddress = "127.0.0.1";
|
||||||
|
port = cfg.metrics-port;
|
||||||
|
};
|
||||||
|
|
||||||
|
postfix = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
user = cfg.user;
|
||||||
|
group = cfg.group;
|
||||||
|
|
||||||
|
domain = cfg.domain;
|
||||||
|
origin = cfg.domain;
|
||||||
|
hostname = cfg.hostname;
|
||||||
|
destination = [ "localhost" "localhost.localdomain" ];
|
||||||
|
|
||||||
|
enableHeaderChecks = true;
|
||||||
|
enableSmtp = true;
|
||||||
|
enableSubmission = true;
|
||||||
|
useSrs = true;
|
||||||
|
|
||||||
|
dnsBlacklists = cfg.dns-blacklists;
|
||||||
|
|
||||||
|
concatMapAttrsToList = f: as: concatLists (mapAttrsToList f as);
|
||||||
|
|
||||||
|
mapFiles = let
|
||||||
|
writeEntries = filename: entries:
|
||||||
|
pkgs.writeText filename (concatStringsSep "\n" entries);
|
||||||
|
mkRejectList = entries: map (entry: "${entry} REJECT") entries;
|
||||||
|
escapeDot = replaceString [ "." ] [ "\\." ];
|
||||||
|
in {
|
||||||
|
reject_senders = writeEntries "sender_blacklist"
|
||||||
|
(mkRejectList cfg.blacklist.senders);
|
||||||
|
reject_recipients = writeEntries "recipient_blacklist"
|
||||||
|
(mkRejectList cfg.blacklist.recipients);
|
||||||
|
virtual_mailbox_map = writeEntries "virtual_mailbox_map"
|
||||||
|
(map (domain: "@${domain} OK") allDomains);
|
||||||
|
sender_login_map = writeEntries "sender_login_maps"
|
||||||
|
(map (domain: "/^(.*)@${escapeDot domain}$/ \${1}") allDomains);
|
||||||
|
};
|
||||||
|
|
||||||
|
networks = cfg.trusted-networks;
|
||||||
|
|
||||||
|
virtual = let
|
||||||
|
mkEmail = domain: user: "${user}@${domain}";
|
||||||
|
mkUserAliases = concatMapAttrsToList (user: aliases:
|
||||||
|
map (alias: "${alias} ${mkEmail cfg.domain user}"));
|
||||||
|
mkAliasUsers = domains:
|
||||||
|
let
|
||||||
|
userList = users:
|
||||||
|
concatStringsSep "," (map (mkEmail cfg.domain) users);
|
||||||
|
in concatMapAttrsToList (alias: users:
|
||||||
|
concatMap
|
||||||
|
(domain: "${mkEmail domain alias} ${mkAliasUsers users}"));
|
||||||
|
in concatStringsSep "\n" ((mkUserAliases cfg.aliases.user-aliases)
|
||||||
|
++ (mkAliasUsers allDomains cfg.aliases.alias-users));
|
||||||
|
|
||||||
|
sslCert = cfg.ssl.certificate;
|
||||||
|
sslKey = cfg.ssl.private-key;
|
||||||
|
|
||||||
|
config = let
|
||||||
|
pcreFile = name: "pcre:/var/lib/postfix/conf/${name}";
|
||||||
|
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
||||||
|
|
||||||
|
in {
|
||||||
|
virtual_mailbox_domains = allDomains;
|
||||||
|
virtual_mailbox_maps = mappedFile "virtual_mailbox_map";
|
||||||
|
|
||||||
|
## I don't think these are needed...
|
||||||
|
# virtual_uid_maps = let uid = config.users.users."${cfg.user}".uid;
|
||||||
|
# in "static:${toString uid}";
|
||||||
|
# virtual_gid_maps = let gid = config.users.groups."${cfg.group}".gid;
|
||||||
|
# in "static: ${toString gid}";
|
||||||
|
|
||||||
|
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
||||||
|
|
||||||
|
message_size_limit = toString (cfg.message-size-limit * 1024 * 1024);
|
||||||
|
|
||||||
|
stmpd_banner = "${cfg.hostname} ESMTP NO UCE";
|
||||||
|
|
||||||
|
tls_eecdh_strong_curve = "prime256v1";
|
||||||
|
tls_eecdh_ultra_curve = "secp384r1";
|
||||||
|
|
||||||
|
policy-spf_time_limit = "3600s";
|
||||||
|
|
||||||
|
smtp_host_lookup = "dns, native";
|
||||||
|
|
||||||
|
smtpd_sasl_type = "dovecot";
|
||||||
|
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||||
|
smtpd_sasl_auth_enable = "yes";
|
||||||
|
smtpd_sasl_local_domain = cfg.sasl-domain;
|
||||||
|
|
||||||
|
smtpd_sasl_security_options = "noanonymous";
|
||||||
|
smtpd_sasl_tls_security_options = "noanonymous";
|
||||||
|
|
||||||
|
smtpd_sender_login_maps = (pcreFile "sender_login_map");
|
||||||
|
|
||||||
|
disable_vrfy_command = "yes";
|
||||||
|
|
||||||
|
recipient_delimiter = "+";
|
||||||
|
|
||||||
|
milter_protocol = "6";
|
||||||
|
milter_mail_macros =
|
||||||
|
"i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
||||||
|
|
||||||
|
smtpd_milters = [
|
||||||
|
"unix:/run/rspamd/rspamd-milter.sock"
|
||||||
|
"unix:/var/run/opendkim/opendkim.sock"
|
||||||
|
];
|
||||||
|
|
||||||
|
non_smtpd_milters = [
|
||||||
|
"unix:/run/rspamd/rspamd-milter.sock"
|
||||||
|
"unix:/var/run/opendkim/opendkim.sock"
|
||||||
|
];
|
||||||
|
|
||||||
|
smtpd_relay_restrictions = [
|
||||||
|
"permit_mynetworks"
|
||||||
|
"permit_sasl_authenticated"
|
||||||
|
"reject_unauth_destination"
|
||||||
|
"reject_unauth_pipelining"
|
||||||
|
"reject_unauth_destination"
|
||||||
|
"reject_unknown_sender_domain"
|
||||||
|
];
|
||||||
|
|
||||||
|
smtpd_sender_restrictions = [
|
||||||
|
"check_sender_access ${mapped-file "reject_senders"}"
|
||||||
|
"permit_mynetworks"
|
||||||
|
"permit_sasl_authenticated"
|
||||||
|
"reject_unknown_sender_domain"
|
||||||
|
];
|
||||||
|
|
||||||
|
smtpd_recipient_restrictions = [
|
||||||
|
"check_sender_access ${mapped-file "reject_recipients"}"
|
||||||
|
"permit_mynetworks"
|
||||||
|
"permit_sasl_authenticated"
|
||||||
|
"check_policy_service unix:private/policy-spf"
|
||||||
|
"reject_unknown_recipient_domain"
|
||||||
|
"reject_unauth_pipelining"
|
||||||
|
"reject_unauth_destination"
|
||||||
|
"reject_invalid_hostname"
|
||||||
|
"reject_non_fqdn_hostname"
|
||||||
|
"reject_non_fqdn_sender"
|
||||||
|
"reject_non_fqdn_recipient"
|
||||||
|
];
|
||||||
|
|
||||||
|
smtpd_helo_restrictions =
|
||||||
|
[ "permit_mynetworks" "reject_invalid_hostname" "permit" ];
|
||||||
|
|
||||||
|
# Handled by submission
|
||||||
|
smtpd_tls_security_level = "may";
|
||||||
|
|
||||||
|
smtpd_tls_eecdh_grade = "ultra";
|
||||||
|
|
||||||
|
# Disable obselete protocols
|
||||||
|
smtpd_tls_protocols =
|
||||||
|
[ "TLSv1.2" "TLSv1.1" "!TLSv1" "!SSLv2" "!SSLv3" ];
|
||||||
|
smtp_tls_protocols =
|
||||||
|
[ "TLSv1.2" "TLSv1.1" "!TLSv1" "!SSLv2" "!SSLv3" ];
|
||||||
|
smtpd_tls_mandatory_protocols =
|
||||||
|
[ "TLSv1.2" "TLSv1.1" "!TLSv1" "!SSLv2" "!SSLv3" ];
|
||||||
|
smtp_tls_mandatory_protocols =
|
||||||
|
[ "TLSv1.2" "TLSv1.1" "!TLSv1" "!SSLv2" "!SSLv3" ];
|
||||||
|
|
||||||
|
smtp_tls_ciphers = "high";
|
||||||
|
smtpd_tls_ciphers = "high";
|
||||||
|
smtp_tls_mandatory_ciphers = "high";
|
||||||
|
smtpd_tls_mandatory_ciphers = "high";
|
||||||
|
|
||||||
|
smtpd_tls_mandatory_exclude_ciphers =
|
||||||
|
[ "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL" ];
|
||||||
|
smtpd_tls_exclude_ciphers =
|
||||||
|
[ "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL" ];
|
||||||
|
smtp_tls_mandatory_exclude_ciphers =
|
||||||
|
[ "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL" ];
|
||||||
|
smtp_tls_exclude_ciphers =
|
||||||
|
[ "MD5" "DES" "ADH" "RC4" "PSD" "SRP" "3DES" "eNULL" "aNULL" ];
|
||||||
|
|
||||||
|
tls_preempt_cipherlist = "yes";
|
||||||
|
|
||||||
|
smtpd_tls_auth_only = "yes";
|
||||||
|
|
||||||
|
smtpd_tls_loglevel = "1";
|
||||||
|
|
||||||
|
tls_random_source = "dev:/dev/urandom";
|
||||||
|
};
|
||||||
|
|
||||||
|
submissionOptions = {
|
||||||
|
smtpd_tls_security_level = "encrypt";
|
||||||
|
smtpd_sasl_auth_enable = "yes";
|
||||||
|
smtpd_sasl_type = "dovecot";
|
||||||
|
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||||
|
smtpd_sasl_security_options = "noanonymous";
|
||||||
|
smtpd_sasl_local_domain = cfg.domain;
|
||||||
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||||
|
smtpd_sender_restrictions =
|
||||||
|
"reject_sender_login_mismatch,reject_unknown_sender_domain";
|
||||||
|
smtpd_recipient_restrictions =
|
||||||
|
"reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||||
|
cleanup_service_name = "submission-header-cleanup";
|
||||||
|
};
|
||||||
|
|
||||||
|
masterConfig = {
|
||||||
|
"policy-spf" = let
|
||||||
|
policySpfFile = pkgs.writeText "policyd-spf.conf"
|
||||||
|
(cfg.postfix.policy-spf.extraConfig
|
||||||
|
+ (lib.optionalString cfg.debug "debugLevel = 4"));
|
||||||
|
in {
|
||||||
|
type = "unix";
|
||||||
|
privileged = true;
|
||||||
|
chroot = false;
|
||||||
|
command = "spawn";
|
||||||
|
args = [
|
||||||
|
"user=nobody"
|
||||||
|
"argv=${pkgs.pypolicyd-spf}/bin/policyd-spf"
|
||||||
|
"${policydSpf}"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
"submission-header-cleanup" = let
|
||||||
|
submissionHeaderCleanupRules =
|
||||||
|
pkgs.writeText "submission_header_cleanup_rules" ''
|
||||||
|
# Removes sensitive headers from mails handed in via the submission port.
|
||||||
|
# See https://thomas-leister.de/mailserver-debian-stretch/
|
||||||
|
# Uses "pcre" style regex.
|
||||||
|
|
||||||
|
/^Received:/ IGNORE
|
||||||
|
/^X-Originating-IP:/ IGNORE
|
||||||
|
/^X-Mailer:/ IGNORE
|
||||||
|
/^User-Agent:/ IGNORE
|
||||||
|
/^X-Enigmail:/ IGNORE
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
type = "unix";
|
||||||
|
private = false;
|
||||||
|
chroot = false;
|
||||||
|
maxproc = 0;
|
||||||
|
command = "cleanup";
|
||||||
|
args =
|
||||||
|
[ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
# TODO: use blacklists
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
cfg = config.fudo.mail.rspamd;
|
||||||
|
mailCfg = config.fudo.mail;
|
||||||
|
|
||||||
|
in {
|
||||||
|
options.fudo.mail.rspamd = with types; {
|
||||||
|
enable = mkEnableOption "Enable rspamd spam test server.";
|
||||||
|
|
||||||
|
ports = {
|
||||||
|
metrics = mkOption {
|
||||||
|
type = port;
|
||||||
|
default = 7573;
|
||||||
|
};
|
||||||
|
controller = mkOption {
|
||||||
|
type = port;
|
||||||
|
default = 11334;
|
||||||
|
};
|
||||||
|
milter = mkOption {
|
||||||
|
type = port;
|
||||||
|
default = 11335;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
antivirus = {
|
||||||
|
host = mkOption {
|
||||||
|
type = str;
|
||||||
|
description = "Host of the ClamAV server.";
|
||||||
|
};
|
||||||
|
|
||||||
|
port = mkOption {
|
||||||
|
type = port;
|
||||||
|
description = "Port at which to reach ClamAV";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
services.prometheus.exporters.rspamd = {
|
||||||
|
enable = true;
|
||||||
|
listenAddress = "127.0.0.1";
|
||||||
|
port = cfg.metrics-port;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.rspamd = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
locals = {
|
||||||
|
"milter_headers.conf".text = "extended_spam_headers = yes;";
|
||||||
|
|
||||||
|
"antivirus.conf".text = ''
|
||||||
|
clamav {
|
||||||
|
action = "reject";
|
||||||
|
symbol = "CLAM_VIRUS";
|
||||||
|
type = "clamav";
|
||||||
|
log_clean = true;
|
||||||
|
servers = "${cfg.antivirus.host}:${cfg.antivirus.port}";
|
||||||
|
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
overrides."milter_headers.conf".text = "extended_spam_headers = true;";
|
||||||
|
|
||||||
|
workers = {
|
||||||
|
rspamd_proxy = {
|
||||||
|
type = "rspamd_proxy";
|
||||||
|
bindSockets = [ "localhost:${toString cfg.port}" ];
|
||||||
|
count = 4;
|
||||||
|
extraConfig = ''
|
||||||
|
milter = yes;
|
||||||
|
timeout = 120s;
|
||||||
|
|
||||||
|
upstream "local" {
|
||||||
|
default = yes;
|
||||||
|
self_scan = yes;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
controller = {
|
||||||
|
type = "controller";
|
||||||
|
count = 4;
|
||||||
|
bindSockets = [ "localhost:${toString cfg.controller-port}" ];
|
||||||
|
includes = [ ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
|
||||||
|
|
||||||
|
if environment :matches "imap.mailbox" "*" {
|
||||||
|
set "mailbox" "${1}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if string "${mailbox}" "Trash" {
|
||||||
|
stop;
|
||||||
|
}
|
||||||
|
|
||||||
|
if string "${mailbox}" "Junk" {
|
||||||
|
stop
|
||||||
|
}
|
||||||
|
|
||||||
|
if environment :matches "imap.user" "*" {
|
||||||
|
set "username" "${1}";
|
||||||
|
}
|
||||||
|
|
||||||
|
pipe :copy "rspamd_learn_ham" [ "${username}" ];
|
|
@ -0,0 +1,7 @@
|
||||||
|
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
|
||||||
|
|
||||||
|
if environment :matches "imap.user" "*" {
|
||||||
|
set "username" "${1}";
|
||||||
|
}
|
||||||
|
|
||||||
|
pipe :copy "rspamd_learn_spam" [ "${username}" ];
|
Loading…
Reference in New Issue