Stalwart
Stalwart is an open-source, all-in-one mail server solution that supports JMAP, IMAP4, and SMTP protocols. It's designed to be secure, fast, robust, and scalable, with features like built-in DMARC, DKIM, SPF, and ARC support for message authentication. It also provides strong transport security through DANE, MTA-STS, and SMTP TLS reporting. Stalwart is written in Rust, ensuring high performance and memory safety.
Setup
The following example enables the Stalwart mail server for the domain example.org, listening on mail delivery SMTP/Submission (25, 465), IMAPS (993) and JMAP ports (8080/443) for mail clients to connect to. Mailboxes for the accounts postmaster@example.org and user1@example.org get created if they don't exist yet.
environment.etc = {
"stalwart/mail-pw1".text = "foobar";
"stalwart/mail-pw2".text = "foobar";
"stalwart/admin-pw".text = "foobar";
"stalwart/acme-secret".text = "secret123";
};
services.stalwart-mail = {
enable = true;
openFirewall = true;
settings = {
server = {
hostname = "mx1.example.org";
tls = {
enable = true;
implicit = true;
};
listener = {
smtp = {
protocol = "smtp";
bind = "[::]:25";
};
submissions = {
bind = "[::]:465";
protocol = "smtp";
tls.implicit = true
};
imaps = {
bind = "[::]:993";
protocol = "imap";
tls.implicit = true
};
jmap = {
bind = "[::]:8080";
url = "https://mail.example.org";
protocol = "http";
};
management = {
bind = [ "127.0.0.1:8080" ];
protocol = "http";
};
};
};
lookup.default = {
hostname = "mx1.example.org";
domain = "example.org";
};
acme."letsencrypt" = {
directory = "https://acme-v02.api.letsencrypt.org/directory";
challenge = "dns-01";
contact = "user1@example.org";
domains = [ "example.org" "mx1.example.org" ];
provider = "cloudflare";
secret = "%{file:/etc/stalwart/acme-secret}%";
};
session.auth = {
mechanisms = "[plain]";
directory = "'in-memory'";
};
storage.directory = "in-memory";
session.rcpt.directory = "'in-memory'";
directory."imap".lookup.domains = [ "example.org" ];
directory."in-memory" = {
type = "memory";
principals = [
{
class = "individual";
name = "User 1";
secret = "%{file:/etc/stalwart/mail-pw1}%";
email = [ "user1@example.org" ];
}
{
class = "individual";
name = "postmaster";
secret = "%{file:/etc/stalwart/mail-pw1}%";
email = [ "postmaster@example.org" ];
}
];
};
authentication.fallback-admin = {
user = "admin";
secret = "%{file:/etc/stalwart/admin-pw}%";
};
};
};
services.caddy = {
enable = true;
virtualHosts = {
"webadmin.example.org" = {
extraConfig = ''
reverse_proxy http://127.0.0.1:8080
'';
serverAliases = [
"mta-sts.example.org"
"autoconfig.example.org"
"autodiscover.example.org"
"mail.example.org"
];
};
};
};
TLS key generation is done using DNS-01 challenge through Cloudflare domain provider, see dns-update library for further providers or configure manual certificates.
DNS records
Before adding required records to the example domain example.org, we need to register the domain on the Stalwart server.
stalwart-cli --url https://webadmin.example.org domain create example.org
Authenticate using the fallback-admin password.
Review the list of which DNS records are required including their values for the mail server to work at https://webadmin.example.org/manage/directory/domains/tuxtux.com.co/view. Especially following records are essential:
| Record Type | Name | Value / Target | Notes |
|---|---|---|---|
| A | example.org | IPv4 address of the mail server | Required |
| AAAA | example.org | IPv6 address of the mail server | Required |
| CNAME | autoconfig | example.org | Mail client autoconfiguration |
| CNAME | autodiscover | example.org | Outlook / Exchange compatibility |
| CNAME | example.org | Mail host | |
| CNAME | mta-sts | example.org | MTA-STS |
| CNAME | webadmin | example.org | Stalwart web administration interface |
| MX | example.org | mx1.example.org | Mail delivery |
| SRV | _imaps._tcp | See Web Admin for exact values | IMAPS service |
| SRV | _submissions._tcp | See Web Admin for exact values | SMTP Submission service |
| TLSA | _25._tcp.example.org. | 3 1 1 … | Only the record starting with 3 1 1 is required
|
| TLSA | _25._tcp.mx1.example.org. | 3 1 1 … | Only the record starting with 3 1 1 is required
|
| TXT | 202409e._domainkey | DKIM public key | DKIM |
| TXT | 202409r._domainkey | DKIM public key | DKIM |
| TXT | _dmarc | DMARC policy | DMARC |
| TXT | mx1 | SPF or server information | Depends on configuration |
| TXT | _smtp._tls | MTA-STS policy | SMTP TLS reporting |
| TXT | example.org | SPF record | SPF |
DNSSEC
Ensure that DNSSEC is enabled for your primary and mail server domain. It can be enabled by your domain provider.
For example, check if DNSSEC is working correctly for your new TLSA record
# nix shell nixpkgs#dnsutils --command delv _25._tcp.mx1.example.org TLSA @1.1.1.1 ; fully validated _25._tcp.mx1.example.org. 10800 IN TLSA 3 1 1 7f59d873a70e224b184c95a4eb54caa9621e47d48b4a25d312d83d96 e3498238 _25._tcp.mx1.example.org. 10800 IN RRSIG TLSA 13 5 10800 20230601000000 20230511000000 39688 example.org. He9VYZ35xTC3fNo8GJa6swPrZodSnjjIWPG6Th2YbsOEKTV1E8eGtJ2A +eyBd9jgG+B3cA/jw8EJHmpvy/buCw==
Configuration
Mail aliases
Considering the configuration above, we could add a mail alias for user1@example.org by simply adding further addresses to the email-array such as user1real@example.org
services.stalwart-mail = {
settings = {
[...]
directory."in-memory" = {
type = "memory";
principals = [
{
class = "individual";
name = "User 1";
secret = "%{file:/etc/stalwart/mail-pw1}%";
email = [ "user1@example.org" "user1real@example.org ];
}
];
};
};
};
Tips and tricks
Auto update TLSA records
Stalwart does not yet automatically update the TLSA record if your ACME certificate changes.
Following script is a possible workaounrd. It extracts the ACME cert every five minute, calculates the TLSA hash and compares it with the upstream record. If it doesn't match, it uses gotlsaflare to update the TLSA record on Cloudflare.
systemd.services.tlsa-cloudflare-update = {
description = "Check and update TLSA/DANE record for mx1 from Stalwart ACME Cert";
after = [
"network-online.target"
"stalwart-mail.service"
];
wants = [
"network-online.target"
"stalwart-mail.service"
];
serviceConfig = {
Type = "oneshot";
User = "stalwart-mail";
Group = "stalwart-mail";
EnvironmentFile = config.age.secrets.gotlsaflare-cloudflare-token.path;
RuntimeDirectory = "stalwart-tlsa";
};
environment = {
DOMAIN = "example.org";
SUBDOMAIN = "mail";
PORT = "25";
ACME_PROVIDER_ID = "cloudflare";
};
path = with pkgs; [
bash
coreutils
openssl
dnsutils
gotlsaflare
rocksdb.tools
gawk
];
script = ''
set -eu
TLSA_RECORD="_$PORT._tcp.$SUBDOMAIN.$DOMAIN"
DB_PATH="/var/lib/stalwart-mail/db"
TEMP_RAW="/run/stalwart-tlsa/cert.bundle"
TEMP_CRT="/run/stalwart-tlsa/cert.crt"
echo "Starting TLSA update process for $DOMAIN"
ldb --db="$DB_PATH" --column_family=s get "acme.$ACME_PROVIDER_ID.cert" | base64 -d > "$TEMP_RAW"
if [ ! -s "$TEMP_RAW" ]; then
echo "ERROR: ACME certificate extraction failed"
exit 1
fi
openssl x509 -in "$TEMP_RAW" -out "$TEMP_CRT"
LOCAL_HASH=$(openssl x509 -in "$TEMP_CRT" -pubkey -noout | openssl pkey -pubin -outform DER | openssl sha256 | awk '{print tolower($2)}')
echo "Local hash: $LOCAL_HASH"
UPSTREAM_HASH=$(dig +nosplit +short TLSA "$TLSA_RECORD" | awk '{print tolower($4)}' | head -n1)
echo "Upstream hash: $UPSTREAM_HASH"
if [ "$LOCAL_HASH" = "$UPSTREAM_HASH" ]; then
echo "Hashes match. DNS is up to date."
exit 0
fi
echo "Hashes differ! Updating Cloudflare..."
gotlsaflare update \
--url "$DOMAIN" \
--subdomain "$SUBDOMAIN" \
--tcp"$PORT" \
--cert "$TEMP_CRT"
echo "TLSA update completed successfully."
'';
};
systemd.timers.tlsa-cloudflare-update = {
description = "Run TLSA check and update every 5 minutes";
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "2m";
OnUnitActiveSec = "5m";
Unit = "tlsa-cloudflare-update.service";
};
};
Adapt the variables DOMAIN, SUBDOMAIN, and PORT according to your needs. The variable ACME_PROVIDER_ID corresponds to the ACME profile name you've setup in the Stalwart webadmin interface. EnvironmentFile points to a file containing the secret Cloudflare api token in the format: TOKEN=12345678[...].
Sending from subaddresses
Receiving mails to subaddresses like john+secondary@example.org is enabled by default. Sending from subaddresses will fail with "You are not allowed to send from this address" as long as they are not an configured alias address. You can disable this check but it will allow any authenticated user to send from any other address.
services.stalwart-mail = {
settings = {
[...]
session.auth.must-match-sender = false;
};
};
A configuration option to customize the pattern of authorized sender addresses is a planned feature.
Test mail server
You can use several online tools to test your mail server configuration:
- en.internet.nl/test-mail: Test your mail server configuration for validity and security.
- mail-tester.com: Send a mail to this service and get a rating about the "spaminess" of your mail server.
- Send a mail to the echo server
echo@univie.ac.at. You should receive a response containing your message in several seconds.
Unsecure setup for testing environments
The following minimal configuration example is unsecure and for testing purpose only. It will run the Stalwart mail server on localhost, listening on port 143 (IMAP) and 587 (Submission). Users alice and bob are configured with the password foobar.
services.stalwart-mail = {
enable = true;
settings = {
server = {
hostname = "localhost";
tls.enable = false;
listener = {
"smtp-submission" = {
bind = [ "[::]:587" ];
protocol = "smtp";
};
"imap" = {
bind = [ "[::]:143" ];
protocol = "imap";
};
};
};
imap.auth.allow-plain-text = true;
session.auth = {
mechanisms = "[plain, auth]";
directory = "'in-memory'";
};
storage.directory = "in-memory";
session.rcpt.directory = "'in-memory'";
queue.outbound.next-hop = "'local'";
directory."in-memory" = {
type = "memory";
principals = [
{
class = "individual";
name = "alice";
secret = "foobar";
email = [ "alice@localhost" ];
}
{
class = "individual";
name = "bob";
secret = "foobar";
email = [ "bob@$localhost" ];
}
];
};
};
};
See also
- Maddy, a composable, modern mail server written in Go.
- Simple NixOS Mailserver