aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-10-28 13:48:44 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-10-28 13:48:44 +0100
commitb849839d66c353c53548615d8e55c2397027aacb (patch)
tree1db8562e3d27d3164daa6bc8d133e295ac9dcf8e
parenttests/common/email/dkim: Remove old dkim keys (diff)
downloadnixos-server-b849839d66c353c53548615d8e55c2397027aacb.zip
tests/simple-nixos-mailserver: Init
These two tests kinda cover our email infrastructure. Importantly, all new mail gets classified as spam, which is why the tests do not really cover `rspamd`. As such things like dkim signing, correct DNS records and so on are not actually covered.
Diffstat (limited to '')
-rw-r--r--tests/by-name/sn/snm-email-dual-server/test.nix134
-rw-r--r--tests/by-name/sn/snm-email-single-server/test.nix83
-rw-r--r--tests/common/email/dkim/mail.server1/private.age13
-rw-r--r--tests/common/email/dkim/mail.server1/public1
-rw-r--r--tests/common/email/dkim/mail.server2/private.age11
-rw-r--r--tests/common/email/dkim/mail.server2/public1
-rw-r--r--tests/common/email/functions.nix224
7 files changed, 467 insertions, 0 deletions
diff --git a/tests/by-name/sn/snm-email-dual-server/test.nix b/tests/by-name/sn/snm-email-dual-server/test.nix
new file mode 100644
index 0000000..6f7bebb
--- /dev/null
+++ b/tests/by-name/sn/snm-email-dual-server/test.nix
@@ -0,0 +1,134 @@
+{
+ lib,
+ vhack,
+ pkgs,
+ vhackPackages,
+ ...
+}: let
+ server1Domain = "mail.server1";
+ server2Domain = "mail.server2";
+
+ common = import ../../../common/email/functions.nix {inherit pkgs vhackPackages;};
+
+ # That's a horrible hack. We should instead wait, until the delivery of the email is
+ # confirmed.
+ sleepTime = "40";
+in
+ vhack.runTest {
+ name = "email";
+
+ serverDomains = [
+ {server1 = server1Domain;}
+ {server2 = server2Domain;}
+ ];
+
+ nodes = {
+ server1 = common.mkMailServer server1Domain "alice";
+ server2 = common.mkMailServer server2Domain "bob";
+
+ alice = common.mkUser "alice" server1Domain;
+ bob = common.mkUser "bob" server2Domain;
+ };
+
+ dns = {
+ server1 = common.mkDnsZone server1Domain;
+ server2 = common.mkDnsZone server2Domain;
+ };
+
+ services = [
+ {server1 = "postfix.service";}
+ {server1 = "dovecot.service";}
+ {server2 = "postfix.service";}
+ {server2 = "dovecot.service";}
+
+ {alice = "multi-user.target";}
+ {bob = "multi-user.target";}
+ ];
+
+ ports = [
+ {server1 = 993;} # imap
+ {server1 = 465;} # smtp
+
+ {server2 = 993;} # imap
+ {server2 = 465;} # smtp
+ ];
+
+ testScript = {...}:
+ # Python
+ ''
+ from time import sleep
+
+ with subtest("Dkim keys work"):
+ server1.succeed("sudo -u rspamd ${pkgs.writeShellScript "dkim-keys-check" ''
+ set -xe
+
+ stat /run/agenix/mailDkim
+
+ ${lib.getExe pkgs.openssl} pkey -text -noout -in /run/agenix/mailDkim | grep Private-Key
+ ${lib.getExe pkgs.openssl} pkey -text -noout -in /run/agenix/mailDkim
+ ''}")
+
+ with subtest("Both start without mail"):
+ alice.succeed("sudo -u alice ${common.scripts.expectNoNewMail}")
+ bob.succeed("sudo -u bob ${common.scripts.expectNoNewMail}")
+
+
+ with subtest("Alice can send an empty email to bob"):
+ alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+ set -xe
+
+ echo "" | msmtp --debug --account alice bob@${server2Domain} >&2
+ ''}")
+
+ # Give the mail server some time to send the email.
+ sleep(${sleepTime})
+
+ bob.succeed("sudo -u bob ${common.scripts.expectNewSpamMail}")
+
+ with subtest("Alice can send an non-empty email to bob"):
+ alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+ set -xe
+
+ cat << EOF | msmtp --debug --account alice bob@${server2Domain} >&2
+ Subject: Hi bob, I'm Alice!
+
+ Good day, Bob!
+
+ This is an email.
+ It contains a subject and a body.
+ I also assert utf8 support by including my last name in this very message.
+
+ XOXO
+ Alice van Dåligen.
+
+ .
+ EOF
+ ''}")
+
+ # Give the mail server some time to send the email.
+ sleep(${sleepTime})
+
+ bob.succeed("sudo -u bob ${common.scripts.expectNewSpamMail}")
+
+ with subtest("Bob can send an email to alice"):
+ bob.succeed("sudo -u bob ${pkgs.writeShellScript "bob-send" ''
+ set -xe
+
+ cat << EOF | msmtp --debug --account bob alice@${server1Domain} >&2
+ Hi Alice!
+
+ Thank you for your emails! (Although they seemed kinda weird tbh :?)
+
+ XXX
+ Bob Älšter
+
+ .
+ EOF
+ ''}")
+
+ # Give the mail server some time to send the email.
+ sleep(${sleepTime})
+
+ alice.succeed("sudo -u alice ${common.scripts.expectNewSpamMail}")
+ '';
+ }
diff --git a/tests/by-name/sn/snm-email-single-server/test.nix b/tests/by-name/sn/snm-email-single-server/test.nix
new file mode 100644
index 0000000..68c6022
--- /dev/null
+++ b/tests/by-name/sn/snm-email-single-server/test.nix
@@ -0,0 +1,83 @@
+{
+ vhack,
+ pkgs,
+ vhackPackages,
+ ...
+}: let
+ domain = "mail.server.test";
+
+ common = import ../../../common/email/functions.nix {inherit pkgs vhackPackages;};
+in
+ vhack.runTest {
+ name = "email";
+
+ serverDomains = [
+ {server = domain;}
+ ];
+
+ nodes = {
+ server = {config, ...}: {
+ systemd.tmpfiles.rules = [
+ #- path mode user group - content
+ "f /passwords/alice 777 root root - alice-password"
+ "f /passwords/bob 777 root root - bob-password"
+ ];
+
+ vhack = {
+ mail = {
+ enable = true;
+ fqdn = domain;
+ domains = [
+ domain
+ ];
+ accounts = {
+ "alice@${domain}" = {
+ passwordFile = "/passwords/alice";
+ };
+ "bob@${domain}" = {
+ passwordFile = "/passwords/bob";
+ };
+ };
+ };
+ };
+ };
+
+ alice = common.mkUser "alice" domain;
+ bob = common.mkUser "bob" domain;
+ };
+
+ services = [
+ {server = "postfix.service";}
+ ];
+
+ ports = [
+ {server = 993;} # imap
+ {server = 465;} # smtp
+ ];
+
+ testScript = {...}:
+ # Python
+ ''
+ with subtest("Both start without mail"):
+ alice.succeed("sudo -u alice ${common.scripts.expectNoNewMail}")
+ bob.succeed("sudo -u bob ${common.scripts.expectNoNewMail}")
+
+ with subtest("Alice can send an email to bob"):
+ alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+ set -xe
+
+ cat << EOF | msmtp --debug --account alice bob@${domain} >&2
+ Hi Bob!
+
+ This is an email.
+ It contains a subject and a body.
+
+ ALICE
+ EOF
+ ''}")
+ bob.succeed("sudo -u bob ${common.scripts.expectNewMail}")
+
+ server.copy_from_machine("/var/lib/", "server")
+ bob.copy_from_machine("/home/bob/mail", "bob")
+ '';
+ }
diff --git a/tests/common/email/dkim/mail.server1/private.age b/tests/common/email/dkim/mail.server1/private.age
new file mode 100644
index 0000000..a97c70b
--- /dev/null
+++ b/tests/common/email/dkim/mail.server1/private.age
@@ -0,0 +1,13 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBndElV
+WFFTcEZpTGwrTUg5WHk1MElLSU5ZZWp1NU5ocit4VGtnM09KZFNBCjNvaWVPR3Zt
+RG14N2E3b0RwK21IUzBZZmg1UzFwakMwTVVOdDgrTTd4U1UKLT4geD9OYm0tZ3Jl
+YXNlIDwgcjkKQUJtaDlVbzk3Slk3S1ozNS9STGwrdzhvWTlodWgxNVd1WVRadTV5
+VmFVUmRmSTV3ckIzYmpQYUFsMUxkdGNzUgo1UXozYzBCK3UxU1BPSnlvVVVxQjI2
+QVdWY0tBbUg1R3p5NmNaZ2RiVGNlL3pWN3FHZU5QOEN2VVdraDBzM1R2CgotLS0g
+TElGejJDYUk4WlhxajdXekI4S0tUMVlrNmdxelpjUnBUa1VCU2krSFloawpUfMj8
+iqVP45yV8fON6fDUur6q/UYqTaaHhCW07LTsBZQpReG8LA+PiOhHw1TmPSJlc/JH
+yEkwLachwvLGCGEO2yoFUFhYwoNhl3vrBsuDLXpqfNuoPrg3wcFXBdrg9zBjZ5Pc
+C/rdzpysjW/52gKRCdGgFj2WyTW2wO2yVmmUk1RWhMabmdd+XLkKhynaMtWsBF0j
+NQ3F
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/common/email/dkim/mail.server1/public b/tests/common/email/dkim/mail.server1/public
new file mode 100644
index 0000000..6c9e653
--- /dev/null
+++ b/tests/common/email/dkim/mail.server1/public
@@ -0,0 +1 @@
+3/bsVrnWag3eMCuj78VpsHDQSONi97fIzY+WPKvAYWM= \ No newline at end of file
diff --git a/tests/common/email/dkim/mail.server2/private.age b/tests/common/email/dkim/mail.server2/private.age
new file mode 100644
index 0000000..fbd88fc
--- /dev/null
+++ b/tests/common/email/dkim/mail.server2/private.age
@@ -0,0 +1,11 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBTcktS
+L0ovODhxd2tyWFdINWRtbXNNYksyNzl6RUxRcTU2T251eVNhQlg4CjVzTVpnc0FC
+eHBNaUtEQnJVRXVpTjg2c3gxcC90VXJ3cUJVaHZJY1pnb00KLT4gKDEkKS1ncmVh
+c2UgYnYqQSBAVSU+CnVRbkRhdUhRS0Z6NGIybll1WVdmSmxzS0lxYlAwSXp0TnA4
+anRDSmQKLS0tIGxxSXRaM0lKeXRoeHFxWW9qN3Y1OE55b1FBVGJTZzB2RmFmZVk5
+eW94aW8KleHhm6fKalehWR0IfHYiC+0bsm0SeYjnJeA60hePdwikmr0QQv5D2xCX
+Nf2QRuai/BpsiJpnvKUiP1uwkm5x6a2Zgwa7t8BwJ7SHqD4oJ+VgCsWMffLs55uC
+A1pKLRmAI26cNt7oibVxNSCCuxZRWeCn+xAsQ393jVCUuDW2FkgyDsKFYHvKRpnC
+BNfwDWWHJ+pifPPrqg==
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/common/email/dkim/mail.server2/public b/tests/common/email/dkim/mail.server2/public
new file mode 100644
index 0000000..40b6ff7
--- /dev/null
+++ b/tests/common/email/dkim/mail.server2/public
@@ -0,0 +1 @@
+QmbJbp/b671OCOQa0Qx1XGl7c1jzFuASuzBr1f5ATpE= \ No newline at end of file
diff --git a/tests/common/email/functions.nix b/tests/common/email/functions.nix
new file mode 100644
index 0000000..7be1bfa
--- /dev/null
+++ b/tests/common/email/functions.nix
@@ -0,0 +1,224 @@
+{
+ pkgs,
+ vhackPackages,
+}: {
+ scripts = {
+ expectNoNewMail = pkgs.writeShellScript "assert-empty-emails" ''
+ set -xe
+
+ # fetchmail returns EXIT_CODE 1 when no new mail
+ fetchmail --verbose >&2 || [ "$?" -eq 1 ] || {
+ echo "Mail was not empty" >&2
+ exit 1
+ }
+ '';
+ expectNewMail = pkgs.writeShellScript "assert-empty-emails" ''
+ set -xe
+
+ # fetchmail returns EXIT_CODE 1 when no new mail
+ fetchmail --verbose >&2 || [ "$?" -ne 1 ] || {
+ echo "No new mail" >&2
+ exit 1
+ }
+ '';
+ expectNewSpamMail = pkgs.writeShellScript "assert-empty-emails" ''
+ set -xe
+
+ # fetchmail returns EXIT_CODE 1 when no new mail
+ fetchmail --folder JUNK --verbose >&2 || [ "$?" -ne 1 ] || {
+ echo "No new spam mail" >&2
+ exit 1
+ }
+ '';
+ };
+
+ mkMailServer = serverName: userName: {
+ config,
+ lib,
+ nodes,
+ ...
+ }: {
+ age.identityPaths = ["${../../common/email/hostKey}"];
+
+ systemd.tmpfiles.rules = [
+ #- path mode user group - content
+ "f /passwords/${userName} 777 root root - ${userName}-password"
+ ];
+
+ age.secrets = {
+ "mailDkim" = {
+ file = ../../common/email/dkim + "/${serverName}/private.age";
+ mode = "600";
+ owner = "rspamd";
+ group = "rspamd";
+ };
+ };
+
+ # Disable rspamd. rspamd can't do the dns lookups in the sandbox for
+ # determining if something is spam or not. Therefore it just rejects every mail with
+ # 'Try again later.'.
+ # services.rspamd.enable = lib.mkForce false;
+ # users.users.rspamd.group = "rspamd";
+ # users.groups.rspamd = {};
+ #
+ # services.postfix.settings.main = {
+ # smtpd_milters = lib.mkForce [];
+ # non_smtpd_milters = lib.mkForce [];
+ # };
+ # mailserver.dkim.enable = false;
+ services = {
+ rspamd = {
+ locals = {
+ "asn.conf".text = lib.mkForce ''
+ enabled = false;
+ '';
+ "rbl.conf".text = lib.mkForce ''
+ enabled = false;
+ '';
+ "greylist.conf".text = lib.mkForce ''
+ enabled = false;
+ '';
+ };
+ };
+ };
+
+ # This DNS resolver is only there for rspamd (and apparently not even needed). But it
+ # also removes our own name server changes, so the test node could no longer find
+ # e.g. `acme.test` and so on.
+ # See: https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/work_items/100
+ mailserver.localDnsResolver = false;
+
+ vhack = {
+ mail = {
+ enable = true;
+ fqdn = serverName;
+ domains = [
+ serverName
+ ];
+
+ security.dkimKeys."${serverName}" = {
+ dkimPrivateKeyPath = config.age.secrets."mailDkim".path;
+ dkimPublicKey =
+ builtins.readFile (../../common/email/dkim + "/${serverName}/public");
+ keyAlgorithm = "rsa-sha-256";
+ };
+
+ accounts = {
+ "${userName}@${serverName}" = {
+ passwordFile = "/passwords/${userName}";
+ };
+ };
+ };
+ };
+ };
+
+ mkDnsZone = serverName: {config, ...}: let
+ cfg = config.vhack.mail;
+
+ keyAlgoToKeyType = keyAlgo:
+ if keyAlgo == "ed25519-sha256"
+ then "ed25519"
+ else if keyAlgo == "rsa-sha-256" || keyAlgo == "rsa-sha-1"
+ then "rsa"
+ else builtins.throw "Impossible";
+ in {
+ "${serverName}" = {
+ MX = [
+ {
+ preference = 10;
+ exchange = "${serverName}.";
+ }
+ ];
+
+ # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.6.1
+ # https://www.rfc-editor.org/rfc/rfc6376.html#section-7.5
+ DKIM = [
+ {
+ selector = "mail";
+ k = keyAlgoToKeyType cfg.security.dkimKeys."${serverName}".keyAlgorithm;
+ p = cfg.security.dkimKeys."${serverName}".dkimPublicKey;
+ s = ["email"];
+ t = ["s"];
+ }
+ ];
+
+ # https://www.rfc-editor.org/rfc/rfc7489.html#section-6.3
+ DMARC = [
+ {
+ adkim = "strict";
+ aspf = "strict";
+ fo = ["0" "1" "d" "s"];
+ p = "quarantine";
+ rua = "admin@vhack.eu";
+ ruf = ["admin@vhack.eu"];
+ }
+ ];
+
+ # https://www.rfc-editor.org/rfc/rfc7208.html
+ # NOTE(@bpeetz): This server might not be directly sending mail, but it is still required for
+ # the SMTP EHLO check. <2025-02-25>
+ TXT = [
+ (builtins.concatStringsSep " "
+ [
+ "v=spf1" # The version.
+ "+mx" # Allow mail from this domain MX record.
+ "-all" # Reject all other emails if the previous mechanism did not match.
+ ])
+ ];
+ };
+ };
+
+ mkUser = user: mailServer: {nodes, ...}: {
+ environment.systemPackages = [
+ vhackPackages.fetchmail-common-name
+ pkgs.msmtp
+ pkgs.procmail
+ ];
+
+ users.users."${user}" = {isNormalUser = true;};
+
+ systemd.tmpfiles.rules = [
+ "d /home/${user}/mail 0700 ${user} users - -"
+ "L /home/${user}/.fetchmailrc - - - - /etc/homeSetup/.fetchmailrc"
+ "L /home/${user}/.procmailrc - - - - /etc/homeSetup/.procmailrc"
+ "L /home/${user}/.msmtprc - - - - /etc/homeSetup/.msmtprc"
+ ];
+
+ environment.etc = {
+ "homeSetup/.fetchmailrc" = {
+ text = ''
+ poll "${mailServer}" protocol IMAP
+ username "${user}@${mailServer}"
+ password "${user}-password"
+ ssl
+ mda procmail;
+ '';
+ mode = "0600";
+ inherit user;
+ };
+ "homeSetup/.procmailrc" = {
+ text = ''
+ DEFAULT=$HOME/mail
+ '';
+ mode = "0600";
+ inherit user;
+ };
+ "homeSetup/.msmtprc" = {
+ text = ''
+ account ${user}
+ host ${mailServer}
+ domain ${mailServer}
+ port 465
+ from ${user}@${mailServer}
+ user ${user}@${mailServer}
+ password ${user}-password
+ auth on
+ tls on
+ tls_starttls off
+ '';
+ mode = "0600";
+ inherit user;
+ };
+ };
+ };
+}