Co-authored-by: flowey <floweydev@gmail.com> Co-authored-by: gitea-actions[bot] <gitea-actions@noreply.local> Reviewed-on: #11
270 lines
7.4 KiB
Nix
270 lines
7.4 KiB
Nix
{
|
|
name,
|
|
pkgs,
|
|
lib,
|
|
hostInfo,
|
|
dsl,
|
|
...
|
|
}:
|
|
let
|
|
inherit (dsl) mkContainer mkService;
|
|
inherit (hostInfo.paths) data secrets;
|
|
|
|
domain = "mail.floweytf.com";
|
|
accountName = "flowey";
|
|
stateDir = "/var/lib/stalwart";
|
|
acmeDir = "/etc/mail-acme";
|
|
|
|
taggingScript = ''
|
|
require ["envelope", "variables", "imap4flags", "fileinto"];
|
|
|
|
if envelope :localpart :matches "to" "*" {
|
|
addflag "''${1}";
|
|
}
|
|
|
|
fileinto "INBOX";
|
|
'';
|
|
|
|
cli = lib.getExe' pkgs.stalwart-cli "stalwart-cli";
|
|
jq = lib.getExe pkgs.jq;
|
|
curl = lib.getExe pkgs.curl;
|
|
grep = lib.getExe' pkgs.gnugrep "grep";
|
|
paste = lib.getExe' pkgs.coreutils "paste";
|
|
|
|
# use sqlite b/c rocksdb segfaults
|
|
configJsonTemplate = pkgs.writeText "stalwart-config.json" (
|
|
builtins.toJSON {
|
|
"@type" = "Sqlite";
|
|
path = "${stateDir}/data.sqlite3";
|
|
}
|
|
);
|
|
|
|
staticPlan = pkgs.writeText "mail-static-plan.json" (
|
|
builtins.concatStringsSep "\n" (
|
|
map builtins.toJSON [
|
|
{
|
|
"@type" = "upsert";
|
|
object = "Domain";
|
|
matchOn = [ "name" ];
|
|
value."domain-mail" = {
|
|
name = domain;
|
|
catchAllAddress = "${accountName}@${domain}";
|
|
isEnabled = true;
|
|
};
|
|
}
|
|
{
|
|
"@type" = "upsert";
|
|
object = "SieveSystemScript";
|
|
matchOn = [ "name" ];
|
|
value."sieve-tag" = {
|
|
name = "tag-by-recipient";
|
|
isActive = true;
|
|
contents = taggingScript;
|
|
};
|
|
}
|
|
]
|
|
)
|
|
+ "\n"
|
|
);
|
|
|
|
stalwartAuthEnv = ''
|
|
export STALWART_URL="http://127.0.0.1:8080"
|
|
export STALWART_USER="admin"
|
|
export STALWART_PASSWORD="$(cat "$CREDENTIALS_DIRECTORY/admin_password")"
|
|
'';
|
|
|
|
readyGate = pkgs.writeShellScript "mail-ready-gate" ''
|
|
set -uo pipefail
|
|
until ${curl} -sS -o /dev/null "http://127.0.0.1:8080/api/account"; do
|
|
if ! systemctl -q is-active stalwart.service; then
|
|
echo "mail: stalwart is not running" >&2
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
'';
|
|
|
|
bootstrapScript = pkgs.writeShellScript "mail-bootstrap" ''
|
|
set -uo pipefail
|
|
${stalwartAuthEnv}
|
|
|
|
out="$(${cli} update Bootstrap singleton \
|
|
--field defaultDomain=${domain} \
|
|
--field serverHostname=${domain} \
|
|
--field generateDkimKeys=true \
|
|
--field requestTlsCertificate=false \
|
|
--field 'directory={"@type":"Internal"}' 2>&1)"
|
|
status=$?
|
|
|
|
if [ "$status" -eq 0 ]; then
|
|
echo "mail-bootstrap: first-time setup complete, restarting stalwart into normal mode" >&2
|
|
systemctl restart stalwart.service
|
|
exit 0
|
|
fi
|
|
|
|
if ${grep} -qi 'bootstrap mode' <<<"$out"; then
|
|
exit 0
|
|
fi
|
|
|
|
echo "mail-bootstrap: $out" >&2
|
|
exit 1
|
|
'';
|
|
|
|
certCredCert = "/run/credentials/stalwart.service/mail_tls_cert";
|
|
certCredKey = "/run/credentials/stalwart.service/mail_tls_key";
|
|
|
|
retryOnFailure = {
|
|
Restart = "on-failure";
|
|
RestartSec = 5;
|
|
};
|
|
|
|
retryLimit = {
|
|
StartLimitBurst = 5;
|
|
StartLimitIntervalSec = 120;
|
|
};
|
|
|
|
provisionScript = pkgs.writeShellScript "mail-provision" ''
|
|
set -euo pipefail
|
|
${stalwartAuthEnv}
|
|
export account_pass="$(cat "$CREDENTIALS_DIRECTORY/account_password")"
|
|
|
|
account_entry="$(${jq} -nc --arg name "${accountName}" \
|
|
'{"@type":"upsert","object":"Account","matchOn":["name"],"value":{("account-"+$name):{"@type":"User","name":$name,"domainId":"#domain-mail","credentials":{"0":{"@type":"Password","secret":env.account_pass}}}}}')"
|
|
|
|
{ cat ${staticPlan}; echo "$account_entry"; } | ${cli} apply --stdin
|
|
|
|
existing_cert_ids="$(${cli} query Certificate --json --fields id | ${jq} -r .id | ${paste} -sd, -)"
|
|
if [ -n "$existing_cert_ids" ]; then
|
|
${cli} delete Certificate --ids "$existing_cert_ids"
|
|
fi
|
|
${cli} create Certificate \
|
|
--field 'certificate={"@type":"File","filePath":"${certCredCert}"}' \
|
|
--field 'privateKey={"@type":"File","filePath":"${certCredKey}"}'
|
|
|
|
systemctl restart stalwart.service
|
|
'';
|
|
in
|
|
mkContainer {
|
|
inherit name;
|
|
includedInTests = false; # TODO: fixme, clearly mail is flakey
|
|
|
|
net.access = [ { to = "@internet"; } ];
|
|
svc.autoStart = true;
|
|
miscProperties.nginx = {
|
|
domain = "mail.floweytf.com";
|
|
};
|
|
net.forward.tcp = [
|
|
25
|
|
993
|
|
];
|
|
resources.fs = {
|
|
"${stateDir}" = {
|
|
hostPath = "${data}/core/mail";
|
|
isReadOnly = false;
|
|
};
|
|
"${acmeDir}" = {
|
|
hostPath = "${data}/core/nginx/acme/${domain}";
|
|
isReadOnly = true;
|
|
};
|
|
"/run/secrets/mail-admin-password" = {
|
|
hostPath = "${secrets}/mail-admin-password";
|
|
isReadOnly = true;
|
|
};
|
|
"/run/secrets/mail-account-password" = {
|
|
hostPath = "${secrets}/mail-account-password";
|
|
isReadOnly = true;
|
|
};
|
|
};
|
|
users = [
|
|
{
|
|
name = "stalwart";
|
|
uid = 1000;
|
|
home = stateDir;
|
|
}
|
|
];
|
|
containerConfig = _: {
|
|
systemd.tmpfiles.rules = [
|
|
"Z ${stateDir} 0750 stalwart stalwart -"
|
|
"C ${stateDir}/config.json 0640 stalwart stalwart - ${configJsonTemplate}"
|
|
];
|
|
|
|
systemd.services.stalwart = mkService {
|
|
description = "Stalwart Mail Server";
|
|
execStart = pkgs.writeShellScript "stalwart-run" ''
|
|
set -euo pipefail
|
|
export STALWART_RECOVERY_ADMIN="admin:$(cat "$CREDENTIALS_DIRECTORY/admin_password")"
|
|
exec ${lib.getExe' pkgs.stalwart_0_16 "stalwart"} -c ${stateDir}/config.json
|
|
'';
|
|
serviceConfig = {
|
|
User = "stalwart";
|
|
Group = "stalwart";
|
|
AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
|
|
ReadWritePaths = [ stateDir ];
|
|
LoadCredential = [
|
|
"admin_password:/run/secrets/mail-admin-password"
|
|
"mail_tls_cert:${acmeDir}/fullchain.pem"
|
|
"mail_tls_key:${acmeDir}/key.pem"
|
|
];
|
|
Restart = "on-failure";
|
|
RestartSec = 5;
|
|
};
|
|
};
|
|
|
|
systemd.paths.mail-cert-renew = {
|
|
wantedBy = [ "multi-user.target" ];
|
|
pathConfig = {
|
|
PathChanged = "${acmeDir}/key.pem";
|
|
Unit = "mail-provision.service";
|
|
};
|
|
};
|
|
|
|
systemd.services.mail-ready = mkService {
|
|
description = "Wait for the Stalwart management API";
|
|
execStart = readyGate;
|
|
after = [ "stalwart.service" ];
|
|
wants = [ "stalwart.service" ];
|
|
wantedBy = [ ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
TimeoutStartSec = 60;
|
|
};
|
|
};
|
|
|
|
systemd.services.mail-bootstrap = mkService {
|
|
description = "One-time Stalwart bootstrap (server identity + admin account)";
|
|
execStart = bootstrapScript;
|
|
after = [ "mail-ready.service" ];
|
|
wants = [ "mail-ready.service" ];
|
|
unitConfig = retryLimit;
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
LoadCredential = [ "admin_password:/run/secrets/mail-admin-password" ];
|
|
}
|
|
// retryOnFailure;
|
|
};
|
|
|
|
systemd.services.mail-provision = mkService {
|
|
description = "Provision mail.floweytf.com domain + catch-all account in Stalwart";
|
|
execStart = provisionScript;
|
|
after = [
|
|
"mail-ready.service"
|
|
"mail-bootstrap.service"
|
|
];
|
|
wants = [
|
|
"mail-ready.service"
|
|
"mail-bootstrap.service"
|
|
];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
LoadCredential = [
|
|
"admin_password:/run/secrets/mail-admin-password"
|
|
"account_password:/run/secrets/mail-account-password"
|
|
];
|
|
}
|
|
// retryOnFailure;
|
|
unitConfig = retryLimit;
|
|
};
|
|
};
|
|
}
|