HexName-NixOS/pkgs/stalwart.nix

235 lines
6.5 KiB
Nix

{ config, inputs, lib, pkgs, ... }:
let
domain = "hexname.com";
stalwartDomain = "mx.${domain}";
roundcubeDomain = "email.${domain}";
dataDir = "/var/lib/stalwart-mail";
credPath = "/run/credentials/stalwart-mail.service";
in
{
services.stalwart-mail = {
enable = true;
package = pkgs.stalwart-mail;
openFirewall = true;
settings = {
server = {
hostname = stalwartDomain;
tls = {
enable = true;
implicit = true;
};
listener = {
smtp = {
bind = [ "[::]:25" ];
protocol = "smtp";
};
submission = {
bind = [ "[::]:587" ];
protocol = "smtp";
};
submissions = {
bind = [ "[::]:465" ];
protocol = "smtp";
tls.implicit = true;
};
imap = {
bind = [ "[::]:143" ];
protocol = "imap";
};
imaps = {
bind = [ "[::]:993" ];
protocol = "imap";
tls.implicit = true;
};
http = {
bind = [ "[::]:51020" ];
protocol = "http";
url = "https://${stalwartDomain}";
};
};
};
directory."in-memory" = {
type = "memory";
# Generate hashes with:
# $ openssl passwd -6
principals = [
{
name = "contact-us@${domain}";
email = [ "contact-us@${domain}" "privacy@${domain}" ];
secret = "$6$iyUwAnKuGTz31jeu$QPfoaUQPccVDWjCWs4PY43dBI6oG4eNb7buNlGBlnNJrvQOePYKyF8RXN8FI5H6y2x191kOa4U8aDD4K/ssKn/";
class = "individual";
}
{
name = "no-reply@${domain}";
email = [ "no-reply@${domain}" ];
secret = "$6$FpTIF6mjoBRXyZAO$9lqf/u3NyJNHYNutFY0WmPkbfkq8J.SIkhzya3izl7AbCRE72TlyKeGx/OOyPuI1QTMV10NgOEGzL8jboOWhZ1";
class = "individual";
}
];
};
authentication.fallback-admin = {
user = "unguessable-username";
secret = "$6$1sRTqTbiXuGNE3zt$oLcXi.kPsy72W5SDMwWSitpJyKlZSKSzhr1QO3DBn6Q9LSE.YpWUbT2Thu5Kbs0bmTMvqAPFI7x/qa1wm9Bj91";
};
email.folders = let
mkFolder = name: {
inherit name;
create = true;
subscribe = true;
};
in {
inbox = mkFolder "Inbox";
sent = mkFolder "Sent";
drafts = mkFolder "Drafts";
archive = mkFolder "Archive";
junk = mkFolder "Spam";
trash = mkFolder "Trash";
};
# Change the DNS records manually to these addresses to
# keep postmaster free for non-automated emails
# https://github.com/stalwartlabs/mail-server/discussions/877
report.analysis = {
addresses = [
"dmarc-reports@*"
"tls-reports@*"
"spf-reports@*"
];
forward = false;
};
# Stop warnings about what's managed where
config.local-keys = [
"authentication.fallback-admin.*"
"certificate.*"
"cluster.node-id"
"directory.*"
"email.folders.*"
"lookup.default.domain"
"lookup.default.hostname"
"report.analysis.*"
"resolver.*"
"server.*"
"!server.blocked-ip.*"
"session.mta-sts.*"
"session.rcpt.catch-all"
"session.rcpt.script"
"sieve.trusted.scripts.*"
"spam-filter.resource"
"storage.blob"
"storage.data"
"storage.directory"
"storage.fts"
"storage.lookup"
"store.*"
"tracer.*"
"webadmin.*"
];
# Store blobs in the file system for easier backups.
# Since the database is backed up to /tmp, it would not fit in RAM
# with all the blobs.
store.fs = {
type = "fs";
path = "${dataDir}/blobs";
};
storage.blob = "fs";
# We have DANE and don't want a certificate for each domain
session.mta-sts.mode = "none";
certificate.default = {
cert = "%{file:${credPath}/cert.pem}%";
private-key = "%{file:${credPath}/key.pem}%";
default = true;
};
lookup.default = {
inherit domain;
hostname = stalwartDomain;
};
tracer.stdout.level = "info";
};
};
networking.firewall.allowedTCPPorts = [ 25 143 465 587 993 ];
systemd.services.stalwart-mail = {
wants = [ "acme-${stalwartDomain}.service" ];
after = [ "acme-${stalwartDomain}.service" ];
preStart = ''
mkdir -p ${dataDir}/db
'';
serviceConfig = {
LogsDirectory = "stalwart-mail";
LoadCredential = [
"cert.pem:${config.security.acme.certs.${stalwartDomain}.directory}/cert.pem"
"key.pem:${config.security.acme.certs.${stalwartDomain}.directory}/key.pem"
];
};
};
services.roundcube = {
enable = true;
package = pkgs.roundcube;
dicts = with pkgs.aspellDicts; [ en de ];
hostName = roundcubeDomain;
plugins = [
"archive"
"zipdownload"
"acl"
];
extraConfig = ''
$config['imap_host'] = 'ssl://${stalwartDomain}:993';
$config['smtp_host'] = 'ssl://%h:465';
$config['mail_domain'] = '%z';
'';
};
services.nginx.virtualHosts = let
proxy = "http://localhost:51020";
in {
${stalwartDomain} = {
enableACME = true;
forceSSL = true;
locations."/".proxyPass = proxy;
};
${roundcubeDomain}.locations."/".extraConfig = ''
add_header Cache-Control "public, max-age=604800, must-revalidate" always;
add_header Referrer-Policy "origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
'';
"mta-sts.${domain}" = {
enableACME = true;
forceSSL = true;
locations."/".root = pkgs.writeTextFile {
name = "mta-sts.txt";
text = ''
version: STSv1
mode: enforce
mx: ${stalwartDomain}
max_age: 86400
'';
destination = "/.well-known/mta-sts.txt";
};
};
};
security.acme.certs.${stalwartDomain} = {
# Keep a stable private key for TLSA records (DANE)
# https://community.letsencrypt.org/t/please-avoid-3-0-1-and-3-0-2-dane-tlsa-records-with-le-certificates/7022/14
extraLegoRenewFlags = [ "--reuse-key" ];
# Restart Stalwart to apply new certificates
reloadServices = [ "stalwart-mail.service" ];
};
}