Skip to content

Instantly share code, notes, and snippets.

@hauleth
Last active May 17, 2024 07:11
Show Gist options
  • Save hauleth/5efebe0ab198d4927ab691c71c8b2425 to your computer and use it in GitHub Desktop.
Save hauleth/5efebe0ab198d4927ab691c71c8b2425 to your computer and use it in GitHub Desktop.
{config, pkgs, ...}: let
domain = "hauleth.dev";
mail-domain = "mail.${domain}";
sieve-data = let
stalwart-mail = config.services.stalwart-mail.package;
in pkgs.stdenvNoCC.mkDerivation {
pname = "stalwart-mail-sieve-data";
inherit (stalwart-mail) version src;
dontBuild = true;
installPhase = ''
mkdir -p $out/etc/stalwart
cp -R resources/config/spamfilter $out/etc/stalwart/
'';
};
in {
age.secrets."dkim-hauleth.dev".file = ../secrets/dkim-hauleth.dev-priv.key.age;
networking.firewall.allowedTCPPorts = [25 465 587 993];
services.nginx.virtualHosts.${mail-domain} = {
forceSSL = true;
enableACME = true;
};
security.acme.certs.${mail-domain}.reloadServices = [
"stalwart-mail"
];
systemd.services.stalwart-mail = {
enable = config.services.stalwart-mail.enable;
requires = ["acme-finished-${mail-domain}.target"];
serviceConfig.LoadCredential = let
certDir = config.security.acme.certs.${mail-domain}.directory;
in [
"cert.pem:${certDir}/cert.pem"
"key.pem:${certDir}/key.pem"
"dkim.cert:${config.age.secrets."dkim-hauleth.dev".path}"
];
};
services.stalwart-mail = let
secrets = "/run/credentials/stalwart-mail.service";
case = field: check: value: data: {
"if" = field;
${check} = value;
"then" = data;
};
otherwise = value: {"else" = value;};
is-smtp = case "listener" "eq" "smtp";
is-authenticated = case "authenticated-as" "ne" "";
in {
enable = true;
settings = {
# global.tracing.level = "debug";
server.hostname = mail-domain;
server.certificate = "default";
server.max-connections = 1024;
server.socket = {
reuse-addr = true;
nodelay = true;
backlog = 1024;
};
server.listener = {
smtp = {
bind = ["[::]:25"];
protocol = "smtp";
};
submission = {
bind = ["[::]:587"];
protocol = "smtp";
};
submissions = {
bind = ["[::]:465"];
protocol = "smtp";
tls.implicit = true;
};
imap = {
bind = ["[::]:993"];
protocol = "imap";
tls.implicit = true;
};
};
server.tls = {
enable = true;
implicit = false;
timeout = "1m";
certificate = "default";
ignore-client-order = true;
};
queue.outbound = {
hostname = mail-domain;
next-hop = [
(case "rcpt-domain" "in-list" "default/domains" "local")
(otherwise false)
];
ip-strategy = "ipv4-then-ipv6";
};
queue.schedule = {
retry = ["2m" "5m" "10m" "15m" "30m" "1h" "2h"];
notify = ["1d" "3d"];
expire = "5d";
};
certificate.default = {
cert = "file://${secrets}/cert.pem";
private-key = "file://${secrets}/key.pem";
};
jmap.directory = "default";
imap = {
request.max-size = 52428800;
auth = {
max-failures = 3;
allow-plain-text = false;
};
timeout = {
authentication = "30m";
anonymous = "1m";
idle = "30m";
};
rate-limit = {
requests = "2000/1m";
concurrent = 4;
};
};
session.extensions = {
pipelining = true;
chunking = true;
requiretls = true;
no-soliciting = "";
dsn = [
(is-authenticated false)
(otherwise true)
];
expn = [
(is-authenticated false)
(otherwise true)
];
vrfy = [
(is-authenticated false)
(otherwise true)
];
future-release = [
(is-authenticated false)
(otherwise "7d")
];
deliver-by = [
(is-authenticated false)
(otherwise "15d")
];
mt-priority = [
(is-authenticated false)
(otherwise "mixer")
];
};
session.ehlo = {
require = true;
reject-non-fqdn = [
(is-smtp true)
(otherwise false)
];
};
session.rcpt = {
relay = [
(is-authenticated true)
(otherwise false)
];
max-recipients = 25;
directory = "default";
};
session.auth = {
mechanisms = [
(is-smtp [])
(otherwise ["plain" "login"])
];
directory = [
(is-smtp false)
(otherwise "default")
];
require = [
(is-smtp false)
(otherwise true)
];
allow-plain-text = false;
errors = {
allow = 3;
wait = "5s";
};
};
session.data.add-headers = {
received = [
(is-smtp true)
(otherwise false)
];
received-spf = [
(is-smtp true)
(otherwise false)
];
auth-results = [
(is-smtp true)
(otherwise false)
];
message-id = [
(is-smtp false)
(otherwise true)
];
date = [
(is-smtp false)
(otherwise true)
];
return-path = false;
};
signature."rsa" = {
inherit domain;
private-key = "file://${secrets}/dkim.cert";
selector = "default";
headers = ["From" "To" "Date" "Subject" "Message-ID"];
algorithm = "rsa-sha256";
canonicalization = "relaxed/relaxed";
set-body-length = false;
report = true;
};
auth.iprev = {
verify = [
(is-smtp "relaxed")
(otherwise "disable")
];
};
auth.dkim = {
verify = "relaxed";
sign = [
(is-smtp [])
(otherwise ["rsa"])
];
};
auth.spf.verify = {
ehlo = [
(is-smtp "relaxed")
(otherwise "disable")
];
};
auth.arc = {
verify = "relaxed";
seal = ["rsa"];
};
auth.dmarc = {
verify = [
(is-smtp "relaxed")
(otherwise "disable")
];
};
resolver = {
type = "system";
concurrency = 2;
timeout = "5s";
attempts = 2;
try-tcp-on-error = true;
public-suffix = [
"file://${pkgs.publicsuffix-list}/share/publicsuffix/public_suffix_list.dat"
];
};
directory.default = {
type = "memory";
options = {
catch-all = true;
subaddressing = true;
superuser-group = "superusers";
};
users = let
emails = map (value: "${value}@${domain}");
in [
{
name = "hauleth";
description = "Main email account";
secret = "secret";
email = emails ["~" "#" "$" "postmaster" "hauleth"];
groups = ["superusers"];
}
];
lookup.domains = [domain];
};
directory.spamdb = {
type = "sql";
address = "sqlite:///var/lib/stalwart-mail/data/spamfilter.sqlite3?mode=rwc";
pool = {
max-connections = 10;
min-connections = 0;
idle-timeout = "5m";
};
};
directory.spam = {
type = "memory";
};
};
};
# Autoconfiguration
services.go-autoconfig = {
enable = true;
settings = {
service_addr = ":1234";
domain = "autoconfig.${domain}";
imap = {
server = mail-domain;
port = 993;
starttls = true;
};
smtp = {
server = mail-domain;
port = 465;
starttls = true;
};
};
};
services.nginx.virtualHosts."autoconfig.${domain}" = {
forceSSL = true;
enableACME = true;
locations."/".proxyPass = "http://127.0.0.1:1234";
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment