about summary refs log tree commit diff stats
path: root/tests/by-name/em/email-dns
diff options
context:
space:
mode:
Diffstat (limited to 'tests/by-name/em/email-dns')
-rwxr-xr-xtests/by-name/em/email-dns/nodes/acme/certs/generate66
-rwxr-xr-xtests/by-name/em/email-dns/nodes/acme/certs/generate.ca38
-rwxr-xr-xtests/by-name/em/email-dns/nodes/acme/certs/generate.client44
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem11
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem25
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template5
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem10
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem25
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template5
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix13
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/client.nix21
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/default.nix114
-rw-r--r--tests/by-name/em/email-dns/nodes/mail_server.nix57
-rw-r--r--tests/by-name/em/email-dns/nodes/name_server.nix320
-rw-r--r--tests/by-name/em/email-dns/nodes/user.nix74
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age11
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/alice.com/public1
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age13
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/bob.com/public1
-rwxr-xr-xtests/by-name/em/email-dns/secrets/dkim/gen_key.sh33
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age10
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public1
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age13
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public1
-rw-r--r--tests/by-name/em/email-dns/secrets/hostKey7
-rw-r--r--tests/by-name/em/email-dns/test.nix203
26 files changed, 1122 insertions, 0 deletions
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate b/tests/by-name/em/email-dns/nodes/acme/certs/generate
new file mode 100755
index 0000000..0d6258e
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/generate
@@ -0,0 +1,66 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -p gnutls -p dash -i dash --impure
+# shellcheck shell=dash
+
+# For development and testing.
+# Create a CA key and cert, and use that to generate a server key and cert.
+# Creates:
+#   ca.key.pem
+#   ca.cert.pem
+#   server.key.pem
+#   server.cert.pem
+
+export SEC_PARAM=ultra
+export EXPIRATION_DAYS=123456
+export ORGANIZATION="Vhack.eu Test Keys"
+export COUNTRY=EU
+export SAN="acme.test"
+export KEY_TYPE="ed25519"
+
+BASEDIR="$(dirname "$0")"
+GENERATION_LOCATION="$BASEDIR/output"
+cd "$BASEDIR" || {
+    echo "(BUG?) No basedir ('$BASEDIR')" 1>&2
+    exit 1
+}
+
+ca=false
+clients=false
+
+usage() {
+    echo "Usage: $0 --ca|--clients"
+    exit 2
+}
+
+if [ "$#" -eq 0 ]; then
+    usage
+fi
+
+for arg in "$@"; do
+    case "$arg" in
+    "--ca")
+        ca=true
+        ;;
+    "--clients")
+        clients=true
+        ;;
+    *)
+        usage
+        ;;
+    esac
+done
+
+[ -d "$GENERATION_LOCATION" ] || mkdir --parents "$GENERATION_LOCATION"
+cd "$GENERATION_LOCATION" || echo "(BUG?) No generation location fould!" 1>&2
+
+[ "$ca" = true ] && ../generate.ca
+
+# Creates:
+#   <client_name>.key.pem
+#   <client_name>.cert.pem
+#
+[ "$clients" = true ] && ../generate.client "acme.test"
+
+echo "(INFO) Look for the keys at: $GENERATION_LOCATION"
+
+# vim: ft=sh
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca b/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca
new file mode 100755
index 0000000..92832c5
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca
@@ -0,0 +1,38 @@
+#! /usr/bin/env sh
+
+# Take the correct binary to create the certificates
+CERTTOOL=$(command -v gnutls-certtool 2>/dev/null || command -v certtool 2>/dev/null)
+if [ -z "$CERTTOOL" ]; then
+    echo "ERROR: No certtool found" >&2
+    exit 1
+fi
+
+# Create a CA key.
+$CERTTOOL \
+    --generate-privkey \
+    --sec-param "$SEC_PARAM" \
+    --key-type "$KEY_TYPE" \
+    --outfile ca.key.pem
+
+chmod 600 ca.key.pem
+
+# Sign a CA cert.
+cat <<EOF >ca.template
+country = $COUNTRY
+dns_name = "$SAN"
+expiration_days = $EXPIRATION_DAYS
+organization = $ORGANIZATION
+ca
+EOF
+#state = $STATE
+#locality = $LOCALITY
+
+$CERTTOOL \
+    --generate-self-signed \
+    --load-privkey ca.key.pem \
+    --template ca.template \
+    --outfile ca.cert.pem
+
+chmod 600 ca.cert.pem
+
+# vim: ft=sh
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate.client b/tests/by-name/em/email-dns/nodes/acme/certs/generate.client
new file mode 100755
index 0000000..5930298
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/generate.client
@@ -0,0 +1,44 @@
+#! /usr/bin/env sh
+
+# Take the correct binary to create the certificates
+CERTTOOL=$(command -v gnutls-certtool 2>/dev/null || command -v certtool 2>/dev/null)
+if [ -z "$CERTTOOL" ]; then
+    echo "ERROR: No certtool found" >&2
+    exit 1
+fi
+
+NAME=client
+if [ $# -gt 0 ]; then
+    NAME="$1"
+fi
+
+# Create a client key.
+$CERTTOOL \
+    --generate-privkey \
+    --sec-param "$SEC_PARAM" \
+    --key-type "$KEY_TYPE" \
+    --outfile "$NAME".key.pem
+
+chmod 600 "$NAME".key.pem
+
+# Sign a client cert with the key.
+cat <<EOF >"$NAME".template
+dns_name = "$NAME"
+dns_name = "$SAN"
+expiration_days = $EXPIRATION_DAYS
+organization = $ORGANIZATION
+encryption_key
+signing_key
+EOF
+
+$CERTTOOL \
+    --generate-certificate \
+    --load-privkey "$NAME".key.pem \
+    --load-ca-certificate ca.cert.pem \
+    --load-ca-privkey ca.key.pem \
+    --template "$NAME".template \
+    --outfile "$NAME".cert.pem
+
+chmod 600 "$NAME".cert.pem
+
+# vim: ft=sh
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem
new file mode 100644
index 0000000..687101d
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBjTCCAT+gAwIBAgIUfiDKld3eiPKuFhsaiHpPNmbMJU8wBQYDK2VwMCoxCzAJ
+BgNVBAYTAkVVMRswGQYDVQQKExJWaGFjay5ldSBUZXN0IEtleXMwIBcNMjUwMzAx
+MTEyNjU2WhgPMjM2MzAzMDYxMTI2NTZaMB0xGzAZBgNVBAoTElZoYWNrLmV1IFRl
+c3QgS2V5czAqMAUGAytlcAMhAHYq2cjrfrlslWxvcKjs2cD7THbpmtq+jf/dlrKW
+UEo8o4GBMH8wDAYDVR0TAQH/BAIwADAfBgNVHREEGDAWgglhY21lLnRlc3SCCWFj
+bWUudGVzdDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFN/1UyS0jnC3LoryMIL2
+/6cdsYBBMB8GA1UdIwQYMBaAFLUZcL/zguHlulHg5GYyYhXmVt/6MAUGAytlcANB
+ALz3u7lBreHeVZ0YXrwK3SDwlhWIH/SeUQwbxQlarzR47qu3cwQQ93Y1xjtOdu+h
+hOM/ig3nLGVOT6qL8IsZrQk=
+-----END CERTIFICATE-----
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem
new file mode 100644
index 0000000..06195b8
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem
@@ -0,0 +1,25 @@
+Public Key Info:
+	Public Key Algorithm: EdDSA (Ed25519)
+	Key Security Level: High (256 bits)
+
+curve:	Ed25519
+private key:
+	9d:25:38:89:f2:37:d7:65:41:f5:24:ba:4c:19:fb:0f
+	86:c8:a3:cf:f7:08:57:69:cc:64:cf:55:2d:8e:99:3e
+	
+
+x:
+	76:2a:d9:c8:eb:7e:b9:6c:95:6c:6f:70:a8:ec:d9:c0
+	fb:4c:76:e9:9a:da:be:8d:ff:dd:96:b2:96:50:4a:3c
+	
+
+
+Public Key PIN:
+	pin-sha256:NPwZitkDv4isUmdiicSsM1t1OtYoxqhdvBUnqSc4bFQ=
+Public Key ID:
+	sha256:34fc198ad903bf88ac52676289c4ac335b753ad628c6a85dbc1527a927386c54
+	sha1:dff55324b48e70b72e8af23082f6ffa71db18041
+
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIJ0lOInyN9dlQfUkukwZ+w+GyKPP9whXacxkz1Utjpk+
+-----END PRIVATE KEY-----
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template
new file mode 100644
index 0000000..320a170
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template
@@ -0,0 +1,5 @@
+dns_name = "acme.test"
+dns_name = "acme.test"
+expiration_days = 123456
+organization = Vhack.eu Test Keys
+encryption_key
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem
new file mode 100644
index 0000000..0fa9d14
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----
+MIIBYDCCARKgAwIBAgIUdhVVcf+NgElqGuutU55FUDBtFVMwBQYDK2VwMCoxCzAJ
+BgNVBAYTAkVVMRswGQYDVQQKExJWaGFjay5ldSBUZXN0IEtleXMwIBcNMjUwMzAx
+MTEyNjU2WhgPMjM2MzAzMDYxMTI2NTZaMCoxCzAJBgNVBAYTAkVVMRswGQYDVQQK
+ExJWaGFjay5ldSBUZXN0IEtleXMwKjAFBgMrZXADIQCkO1LhHINvJjt41JD6UEc4
+ZKKUubB8lKPxSOyTkFBOgqNIMEYwDwYDVR0TAQH/BAUwAwEB/zAUBgNVHREEDTAL
+gglhY21lLnRlc3QwHQYDVR0OBBYEFLUZcL/zguHlulHg5GYyYhXmVt/6MAUGAytl
+cANBAFMFFy5tjuQtp5GVEN6qM50L4lteQuxfhlQqmOOfl06HV6153wJnrlKaTOYO
+t0dKlSqKROMYUYeU39xDp07MLAc=
+-----END CERTIFICATE-----
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem
new file mode 100644
index 0000000..64263bc
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem
@@ -0,0 +1,25 @@
+Public Key Info:
+	Public Key Algorithm: EdDSA (Ed25519)
+	Key Security Level: High (256 bits)
+
+curve:	Ed25519
+private key:
+	82:0d:fc:f0:d6:82:89:63:e5:bc:23:78:ba:98:38:83
+	09:2d:e0:78:4c:53:92:e3:db:5b:2f:e4:39:ce:96:3d
+	
+
+x:
+	a4:3b:52:e1:1c:83:6f:26:3b:78:d4:90:fa:50:47:38
+	64:a2:94:b9:b0:7c:94:a3:f1:48:ec:93:90:50:4e:82
+	
+
+
+Public Key PIN:
+	pin-sha256:jpzYZMOHDPCeSXxfL+YUXgSPcbO9MAs8foGMP5CJiD8=
+Public Key ID:
+	sha256:8e9cd864c3870cf09e497c5f2fe6145e048f71b3bd300b3c7e818c3f9089883f
+	sha1:b51970bff382e1e5ba51e0e466326215e656dffa
+
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIIIN/PDWgolj5bwjeLqYOIMJLeB4TFOS49tbL+Q5zpY9
+-----END PRIVATE KEY-----
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template
new file mode 100644
index 0000000..a2295d8
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template
@@ -0,0 +1,5 @@
+country = EU
+dns_name = "acme.test"
+expiration_days = 123456
+organization = Vhack.eu Test Keys
+ca
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix b/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix
new file mode 100644
index 0000000..aeb6dfc
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix
@@ -0,0 +1,13 @@
+let
+  domain = "acme.test";
+in {
+  inherit domain;
+  ca = {
+    cert = ./output/ca.cert.pem;
+    key = ./output/ca.key.pem;
+  };
+  "${domain}" = {
+    cert = ./output/. + "/${domain}.cert.pem";
+    key = ./output/. + "/${domain}.key.pem";
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/acme/client.nix b/tests/by-name/em/email-dns/nodes/acme/client.nix
new file mode 100644
index 0000000..2b870e8
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/client.nix
@@ -0,0 +1,21 @@
+{
+  nodes,
+  lib,
+  ...
+}: let
+  inherit (nodes.acme.test-support.acme) caCert;
+  inherit (nodes.acme.test-support.acme) caDomain;
+in {
+  security = {
+    acme = {
+      acceptTerms = true;
+      defaults = {
+        server = "https://${caDomain}/dir";
+      };
+    };
+
+    pki = {
+      certificateFiles = lib.mkForce [caCert];
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/acme/default.nix b/tests/by-name/em/email-dns/nodes/acme/default.nix
new file mode 100644
index 0000000..236ba6a
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/default.nix
@@ -0,0 +1,114 @@
+# The certificate for the ACME service is exported as:
+#
+#   config.test-support.acme.caCert
+#
+# This value can be used inside the configuration of other test nodes to inject
+# the test certificate into security.pki.certificateFiles or into package
+# overlays.
+#
+# {
+#   acme = { nodes, lib, ... }: {
+#     imports = [ ./common/acme/server ];
+#     networking.nameservers = lib.mkForce [
+#       nodes.mydnsresolver.networking.primaryIPAddress
+#     ];
+#   };
+#
+#   dnsmyresolver = ...;
+# }
+#
+# Keep in mind, that currently only _one_ resolver is supported, if you have
+# more than one resolver in networking.nameservers only the first one will be
+# used.
+#
+# Also make sure that whenever you use a resolver from a different test node
+# that it has to be started _before_ the ACME service.
+{
+  config,
+  pkgs,
+  lib,
+  ...
+}: let
+  testCerts = import ./certs/snakeoil-certs.nix;
+  inherit (testCerts) domain;
+
+  pebbleConf.pebble = {
+    listenAddress = "0.0.0.0:443";
+    managementListenAddress = "0.0.0.0:15000";
+
+    # The cert and key are used only for the Web Front End (WFE)
+    certificate = testCerts.${domain}.cert;
+    privateKey = testCerts.${domain}.key;
+
+    httpPort = 80;
+    tlsPort = 443;
+    ocspResponderURL = "http://${domain}:4002";
+    strict = true;
+  };
+
+  pebbleConfFile = pkgs.writeText "pebble.conf" (builtins.toJSON pebbleConf);
+in {
+  options.test-support.acme = {
+    caDomain = lib.mkOption {
+      type = lib.types.str;
+      default = domain;
+      readOnly = true;
+      description = ''
+        A domain name to use with the `nodes` attribute to
+        identify the CA server in the `client` config.
+      '';
+    };
+    caCert = lib.mkOption {
+      type = lib.types.path;
+      readOnly = true;
+      default = testCerts.ca.cert;
+      description = ''
+        A certificate file to use with the `nodes` attribute to
+        inject the test CA certificate used in the ACME server into
+        {option}`security.pki.certificateFiles`.
+      '';
+    };
+  };
+
+  config = {
+    networking = {
+      # This has priority 140, because modules/testing/test-instrumentation.nix
+      # already overrides this with priority 150.
+      nameservers = lib.mkOverride 140 ["127.0.0.1"];
+      firewall.allowedTCPPorts = [
+        80
+        443
+        15000
+        4002
+      ];
+
+      extraHosts = ''
+        127.0.0.1 ${domain}
+        ${config.networking.primaryIPAddress} ${domain}
+      '';
+    };
+
+    systemd.services = {
+      pebble = {
+        enable = true;
+        description = "Pebble ACME server";
+        wantedBy = ["network.target"];
+        environment = {
+          # We're not testing lego, we're just testing our configuration.
+          # No need to sleep.
+          PEBBLE_VA_NOSLEEP = "1";
+        };
+
+        serviceConfig = {
+          RuntimeDirectory = "pebble";
+          WorkingDirectory = "/run/pebble";
+
+          # Required to bind on privileged ports.
+          AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
+
+          ExecStart = "${pkgs.pebble}/bin/pebble -config ${pebbleConfFile}";
+        };
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/mail_server.nix b/tests/by-name/em/email-dns/nodes/mail_server.nix
new file mode 100644
index 0000000..ba554ac
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/mail_server.nix
@@ -0,0 +1,57 @@
+{
+  extraModules,
+  pkgs,
+  vhackPackages,
+}: {
+  mkMailServer = serverName: principal: {
+    config,
+    lib,
+    nodes,
+    ...
+  }: {
+    imports =
+      extraModules
+      ++ [
+        ../../../../../modules
+        ./acme/client.nix
+      ];
+
+    environment.systemPackages = [
+      pkgs.bind
+      pkgs.openssl
+    ];
+
+    networking.nameservers = lib.mkForce [
+      nodes.name_server.networking.primaryIPAddress
+      nodes.name_server.networking.primaryIPv6Address
+    ];
+
+    age.identityPaths = ["${../secrets/hostKey}"];
+
+    vhack = {
+      stalwart-mail = {
+        enable = true;
+        fqdn = "${serverName}.server.com";
+        admin = "admin@${serverName}.server.com";
+        security = {
+          dkimKeys = let
+            loadKey = name: {
+              dkimPublicKey = builtins.readFile (../secrets/dkim + "/${name}/public");
+              dkimPrivateKeyPath = ../secrets/dkim + "/${name}/private.age";
+              keyAlgorithm = "ed25519-sha256";
+            };
+          in {
+            "mail1.server.com" = loadKey "mail1.server.com";
+            "mail2.server.com" = loadKey "mail2.server.com";
+            "alice.com" = loadKey "alice.com";
+            "bob.com" = loadKey "bob.com";
+          };
+          verificationMode = "strict";
+          allowInsecureSmtp = false;
+        };
+        openFirewall = true;
+        principals = [principal];
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/name_server.nix b/tests/by-name/em/email-dns/nodes/name_server.nix
new file mode 100644
index 0000000..ef657f4
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/name_server.nix
@@ -0,0 +1,320 @@
+{extraModules}: {
+  config,
+  lib,
+  nodes,
+  pkgs,
+  ...
+}: let
+  keyAlgoToKeyType = keyAlgo:
+    if keyAlgo == "ed25519-sha256"
+    then "ed25519"
+    else if keyAlgo == "rsa-sha-256" || keyAlgo == "rsa-sha-1"
+    then "rsa"
+    else builtins.throw "Impossible";
+
+  mkZone = user: nodes: lib: cfg: {
+    SOA = {
+      nameServer = "ns.server.com";
+      adminEmail = "${user}@${user}.com";
+      serial = 2024012301;
+    };
+
+    MX = [
+      {
+        preference = 10;
+        exchange = "${cfg.fqdn}.";
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc8461.html#section-3.1
+    # Also see the policy in the hmtl part.
+    MTA-STS = [
+      {
+        id = "20250228Z";
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc7208.html
+    # https://en.wikipedia.org/wiki/Sender_Policy_Framework
+    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.
+        ])
+    ];
+
+    # 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."${user}.com".keyAlgorithm;
+        p = cfg.security.dkimKeys."${user}.com".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 = cfg.admin;
+        ruf = [cfg.admin];
+      }
+    ];
+
+    A = [
+      nodes.${user}.networking.primaryIPAddress
+    ];
+    AAAA = [
+      nodes.${user}.networking.primaryIPv6Address
+    ];
+  };
+  mkServerZone = serverName: nodes: lib: let
+    cfg = nodes."${serverName}_server".vhack.stalwart-mail;
+  in {
+    SOA = {
+      nameServer = "ns.server.com";
+      adminEmail = "admin@server.com";
+      serial = 2024012301;
+    };
+    MX = [
+      {
+        preference = 10;
+        exchange = "${serverName}.server.com.";
+      }
+    ];
+
+    # 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}.server.com".keyAlgorithm;
+        p = cfg.security.dkimKeys."${serverName}.server.com".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 = cfg.admin;
+        ruf = [cfg.admin];
+      }
+    ];
+
+    # 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.
+        ])
+    ];
+
+    A = [
+      nodes."${serverName}_server".networking.primaryIPAddress
+    ];
+    AAAA = [
+      nodes."${serverName}_server".networking.primaryIPv6Address
+    ];
+  };
+in {
+  imports =
+    extraModules
+    ++ [
+      ../../../../../modules
+      ./acme/client.nix
+    ];
+
+  networking.nameservers = lib.mkForce [
+    nodes.name_server.networking.primaryIPAddress
+    nodes.name_server.networking.primaryIPv6Address
+  ];
+
+  services.nginx = {
+    logError = "stderr debug";
+    virtualHosts = let
+      mkStsHost = mx: {
+        forceSSL = true;
+        enableACME = true;
+        root = pkgs.runCommandLocal "mkPolicy" {} ''
+          mkdir --parents $out/.well-known/
+
+          # https://www.rfc-editor.org/rfc/rfc8461.html#section-3.2
+          cat << EOF > $out/.well-known/mta-sts.txt
+          version: STSv1
+          mode: enforce
+          mx: ${mx}
+          max_age: 604800
+          EOF
+        '';
+      };
+    in {
+      "mta-sts.alice.com" = mkStsHost "mail2.server.com";
+      "mta-sts.bob.com" = mkStsHost "mail1.server.com";
+    };
+  };
+
+  vhack = {
+    nginx = {
+      enable = true;
+    };
+    dns = {
+      enable = true;
+      openFirewall = true;
+      interfaces = [
+        nodes.name_server.networking.primaryIPAddress
+        nodes.name_server.networking.primaryIPv6Address
+      ];
+
+      zones = let
+        stsZone = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+
+          useOrigin = false;
+
+          A = [
+            nodes.name_server.networking.primaryIPAddress
+          ];
+          AAAA = [
+            nodes.name_server.networking.primaryIPv6Address
+          ];
+        };
+      in {
+        "arpa" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+          useOrigin = false;
+
+          PTR = [
+            {
+              name = "acme.test";
+              ip.v4 = nodes.acme.networking.primaryIPAddress;
+            }
+            {
+              name = "acme.test";
+              ip.v6 = nodes.acme.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "alice.com";
+              ip.v4 = nodes.alice.networking.primaryIPAddress;
+            }
+            {
+              name = "alice.com";
+              ip.v6 = nodes.alice.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "bob";
+              ip.v4 = nodes.bob.networking.primaryIPAddress;
+            }
+            {
+              name = "bob";
+              ip.v6 = nodes.bob.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "mail1.server.com";
+              ip.v4 = nodes.mail1_server.networking.primaryIPAddress;
+            }
+            {
+              name = "mail1.server.com";
+              ip.v6 = nodes.mail1_server.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "mail2.server.com";
+              ip.v4 = nodes.mail2_server.networking.primaryIPAddress;
+            }
+            {
+              name = "mail2.server.com";
+              ip.v6 = nodes.mail2_server.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "ns.server.com";
+              ip.v4 = nodes.name_server.networking.primaryIPAddress;
+            }
+            {
+              name = "ns.server.com";
+              ip.v6 = nodes.name_server.networking.primaryIPv6Address;
+            }
+          ];
+        };
+
+        "alice.com" = mkZone "alice" nodes lib nodes.mail2_server.vhack.stalwart-mail;
+        "mta-sts.alice.com" = stsZone;
+        "bob.com" = mkZone "bob" nodes lib nodes.mail1_server.vhack.stalwart-mail;
+        "mta-sts.bob.com" = stsZone;
+        "mail1.server.com" = mkServerZone "mail1" nodes lib;
+        "mail2.server.com" = mkServerZone "mail2" nodes lib;
+        "ns.server.com" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+          useOrigin = false;
+
+          A = [
+            nodes.name_server.networking.primaryIPAddress
+          ];
+          AAAA = [
+            nodes.name_server.networking.primaryIPv6Address
+          ];
+        };
+        "acme.test" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+          useOrigin = false;
+
+          A = [
+            nodes.acme.networking.primaryIPAddress
+          ];
+          AAAA = [
+            nodes.acme.networking.primaryIPv6Address
+          ];
+        };
+        "server.com" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+
+          useOrigin = false;
+          NS = [
+            "ns.server.com."
+          ];
+        };
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/user.nix b/tests/by-name/em/email-dns/nodes/user.nix
new file mode 100644
index 0000000..e4db347
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/user.nix
@@ -0,0 +1,74 @@
+{
+  pkgs,
+  vhackPackages,
+}: {
+  mkUser = user: serverName: {
+    nodes,
+    lib,
+    ...
+  }: {
+    imports = [
+      ./acme/client.nix
+    ];
+
+    environment.systemPackages = [
+      vhackPackages.fetchmail-common-name
+      pkgs.msmtp
+      pkgs.procmail
+
+      pkgs.bind
+      pkgs.openssl
+    ];
+
+    networking.nameservers = lib.mkForce [
+      nodes.name_server.networking.primaryIPAddress
+      nodes.name_server.networking.primaryIPv6Address
+    ];
+
+    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 "${serverName}.server.com" protocol IMAP
+            username "${user}"
+            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           ${serverName}.server.com
+          domain         ${user}.com
+          port           465
+          from           ${user}@${user}.com
+          user           ${user}
+          password       ${user}-password
+          auth           on
+          tls            on
+          tls_starttls   off
+        '';
+        mode = "0600";
+        inherit user;
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age
new file mode 100644
index 0000000..97b9be7
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age
@@ -0,0 +1,11 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSAxanpG
+VWxCYjF3aWFUUEM0b2Q2bGJUNEJZMGFlVkJpTFg3eUc1UGdyblF3CnlJVXY2Ti9z
+SmltMmIzM25jajl3WE5kMkVsY05NUkpDdzlJVHdJYXByZjQKLT4gU0t5SSxMLWdy
+ZWFzZSB6Tgova0Juc0x3RlFrR1NSVzBIMllYRmZiRXlzN2hSdHZQaFVDS3FPUFNr
+NzBiM0Q5dExOREgydFpKWm1MaGQ5QkxBCgotLS0gNC81ZHQ5eFBrUGJxWXF6dWF4
+Mlg0WHBXS2RqeW1uY1hGUVJXbHpUaDhlWQpih0QTGjejnwIQ2lvDFS1wbNiiOJ+c
+awJ2tX8chzWm+wOECaIZAqwW2NwVZj5Sj+Vzv6LQ1BVaQAiEN41GRvjyP/u3X+d+
+LKI3bPa8DWxQNd7/zAhFjSB1KEIBrqGb2GtW/Yv8Mu07V8IV/MaGUwpDOXgvFQVH
+UQ1qpM0R1r190IuV2Y7M558J42crH9/5mIvMH5rW++Ru
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/by-name/em/email-dns/secrets/dkim/alice.com/public b/tests/by-name/em/email-dns/secrets/dkim/alice.com/public
new file mode 100644
index 0000000..0f3c3b2
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/alice.com/public
@@ -0,0 +1 @@
+cLWzd3zg51ITME1Fnu16/h07lXIUxfhdLivktUMoVQs=
\ No newline at end of file
diff --git a/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age
new file mode 100644
index 0000000..6bd9e28
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age
@@ -0,0 +1,13 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBtQ1lC
+bmtXZWlqeE1SWmR2d1JTcEZMZFRKZW1jVnRGSHpUMEM1cnpvTkdFCkRVbmlNV0ZS
+SDdobTFFQUcyU3dMVXlTditvZXN0L0pPN1ZHaWtxOFZSL1EKLT4gfSM6JGpjOlot
+Z3JlYXNlICdRYiBLKlV6CmZmczdSa2U5cWl6OG5QL0VaUGsyNUlCVFJ1UjJxVnpV
+ZE1sN2lSRTgzVjI1S3pJVzdqN05WUVZmaTRYMXptb0kKczJEOG9EM2xtMFRHd3Vt
+TUpiK2RzZkRwZTZqb3lEOGpKNy80Vk9BVDlSNjhYSkROYlVGQ1ZESGhIV3ZJWVEK
+LS0tIEk0MXVEci9ITERYRzZFbTJJQWxSQzhFV3NqV2o3M0NvVlNhLzhhVkJYcTQK
+GJtIH4AxSSwZhnLn5IUhOihz9Ai2lLnf00uhvF6+i29TtyEgxgWhisBJtzShB/Aq
+Bct5em093jryJPNQBNDJpImEViP9WS/kTqQG0bnu2i/Nr5+vZyRcK8qv75guMxki
+p7sUirbzCNtA+5JGGJb30PqOAWpflBPL0fkC5L7JyAjhNRCOgIL+QQS3mosU1AYJ
+izFOdod2DA==
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/by-name/em/email-dns/secrets/dkim/bob.com/public b/tests/by-name/em/email-dns/secrets/dkim/bob.com/public
new file mode 100644
index 0000000..ddea670
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/bob.com/public
@@ -0,0 +1 @@
+3yrKD52yd5hBA6ue5uQVl7FXGK8UOlUE9Y+yCdBRfVQ=
\ No newline at end of file
diff --git a/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh b/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh
new file mode 100755
index 0000000..1e090f4
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh
@@ -0,0 +1,33 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -p rage -p openssl -p dash -i dash --impure
+
+cd "$(dirname "$0")" || {
+    echo "No basedir?!"
+    exit 1
+}
+
+key_name="$1"
+[ -z "$key_name" ] && {
+    echo "Usage: $0 KEY_NAME"
+    exit 2
+}
+
+[ -d "$key_name" ] || mkdir "$key_name"
+cd "$key_name" || {
+    echo "Just created."
+    exit 1
+}
+
+openssl genpkey -algorithm ed25519 -out "private"
+openssl pkey -in "private" -pubout -out "public.tmp"
+
+openssl asn1parse -in "public.tmp" -offset 12 -noout -out /dev/stdout | base64 --wrap 0 >"public"
+rm "public.tmp"
+
+rage --encrypt \
+    --armor \
+    --recipient "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILxdvBk/PC9fC7B5vqe9TvygZKY6LgDQ2mXRdVrthBM/" \
+    "private" >"private.age"
+rm "private"
+
+# vim: ft=sh
diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age
new file mode 100644
index 0000000..03bb0b1
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age
@@ -0,0 +1,10 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBiZmcz
+OVlacDhjS3pCWlZCYlpyVVFoQWxFN1Z0eTJIY2ovNVg2MjQ1SXpVClRZUnhkblFI
+c0VyelMxYzBsZ2NMUEVzWmtTSzJuVTdoVHFiZEc5cEd3aHMKLT4gJyctZ3JlYXNl
+IG1tIFt2YSAwCkQvY2VnMlBTSHhPbXJ2NE55ck0KLS0tIGkxWHdQb0NIVmZyaTdW
+bmorU2NLMjByakpTMlo1NUtFQ0NFd1YvOC9EaFEKtDNLHVtnsFiyhsREJOPq1xlk
+74MURNlYnlF1IMrUaA3oUQSR5M34Crg7rHtjF54OsRhm79Y1dGHWeeC3evVNVpY3
+1dn/q/12aWIzT/TgGcSi3bK5fPkv+nMs/WPKTREHJ1HcWLGDeH6e8uTV7lAwiSuP
+PjYhDbnNUCMMyaBsgbtCnMe8HuHdTwXQWuh0dApS5iL3z8qoSQ==
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public
new file mode 100644
index 0000000..4941b85
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public
@@ -0,0 +1 @@
+quDd9+ogqiIUWybfegosFFkG7jAsblij2VrkuUXEzzY=
\ No newline at end of file
diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age
new file mode 100644
index 0000000..6768973
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age
@@ -0,0 +1,13 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBBZ3Ex
+ZWNUU01JK0ZGOEhEWUpEM3JLdXpjc3doMVA2RUI5TVVpblQxS0JJCnlHRmdVVk05
+MW56cEt4M00raE1nU3JZaTMvTXdUQXdzbTYvVElqVjdvNWcKLT4gRnJFLWdyZWFz
+ZSA5bEhvWn4Kb01McHBBekVTalcwM0tob3VUd3NuVFlvZUpnSFQxbXVBaEJNMVlQ
+K1BiSjRCL1YrZDZoSnFBNU9aQkQyNjRoSwpqcnBnd2NJQlMxaHdoa0pPWGR0SEZO
+SU5DNjFxb3JQTTZITVZNRGF1VUR4Zm9laWhYd3lHZityRTNJVVF1bXdwCnhGYzMK
+LS0tIFFsN0Q3V1pxWUduSU9xd21uVEF2R0tJcURYa1FOTS9kMDh6RGkwNS9SMUEK
+Ni+1WbmAiavBCwLg8r1nvVipXQJ2/cItN1MgWlYe0+UrgLxRU5VLhoWi9BEulGEV
+KHkNWyMCK4Tl/NJt1PAQVJ6QBVHYYxIYQWY1QkNCqXe1YdaJ5jDcWGSZdhbCrzMN
+3tx3EPhigU2DiQZB6l4OOaHLjAw2a+POVwwsCavnRp7vEhs/5O2t5Lo2vCoDGCot
+6o+Sdr86mw==
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public
new file mode 100644
index 0000000..5c4406d
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public
@@ -0,0 +1 @@
+th9exwaYvoAjxW1tAj3k/VNLl5jKzSC/dxKrxM2mTZE=
\ No newline at end of file
diff --git a/tests/by-name/em/email-dns/secrets/hostKey b/tests/by-name/em/email-dns/secrets/hostKey
new file mode 100644
index 0000000..79c9d6c
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/hostKey
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACC8XbwZPzwvXwuweb6nvU78oGSmOi4A0Npl0XVa7YQTPwAAAJjFZPqHxWT6
+hwAAAAtzc2gtZWQyNTUxOQAAACC8XbwZPzwvXwuweb6nvU78oGSmOi4A0Npl0XVa7YQTPw
+AAAEA9D5AP+Uqhrg8rPx2DjgucjfnJknkk7lkeKHMV04ZZv7xdvBk/PC9fC7B5vqe9Tvyg
+ZKY6LgDQ2mXRdVrthBM/AAAAFSAnUHVibGljIHRlc3Rpbmcga2V5Jw==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/by-name/em/email-dns/test.nix b/tests/by-name/em/email-dns/test.nix
new file mode 100644
index 0000000..32447ae
--- /dev/null
+++ b/tests/by-name/em/email-dns/test.nix
@@ -0,0 +1,203 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}: let
+  mail_server = import ./nodes/mail_server.nix {inherit extraModules pkgs vhackPackages;};
+  inherit (mail_server) mkMailServer;
+  user = import ./nodes/user.nix {inherit pkgs vhackPackages;};
+  inherit (user) mkUser;
+in
+  nixos-lib.runTest {
+    hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+    name = "email-dns";
+
+    node = {
+      specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+      # Use the nixpkgs as constructed by the `nixpkgs.*` options
+      pkgs = null;
+    };
+
+    nodes = {
+      acme = {
+        nodes,
+        lib,
+        ...
+      }: {
+        imports = [./nodes/acme];
+        networking.nameservers = lib.mkForce [
+          nodes.name_server.networking.primaryIPAddress
+        ];
+      };
+
+      name_server = import ./nodes/name_server.nix {inherit extraModules;};
+
+      mail1_server =
+        mkMailServer "mail1"
+        {
+          class = "individual";
+          name = "bob";
+          secret = "bob-password";
+          email = ["bob@bob.com"];
+        };
+
+      mail2_server =
+        mkMailServer "mail2"
+        {
+          class = "individual";
+          name = "alice";
+          secret = "alice-password";
+          email = ["alice@alice.com"];
+        };
+
+      bob = mkUser "bob" "mail1";
+      alice = mkUser "alice" "mail2";
+    };
+
+    testScript = {...}: let
+      checkEmailEmpty = 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
+        }
+      '';
+      checkEmailNotEmpty = 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
+        }
+      '';
+      checkSpamEmailNotEmpty = 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 mail" >&2
+          exit 1
+        }
+      '';
+      inherit (pkgs) lib;
+    in
+      /*
+      python
+      */
+      ''
+        from time import sleep
+
+        # Start dependencies for the other services
+        acme.start()
+        acme.wait_for_unit("pebble.service")
+        name_server.start()
+        name_server.wait_for_unit("nsd.service")
+
+        # Start the actual testing machines
+        start_all()
+
+        mail1_server.wait_for_unit("stalwart-mail.service")
+        mail1_server.wait_for_open_port(993) # imap
+        mail1_server.wait_for_open_port(465) # smtp
+        mail2_server.wait_for_unit("stalwart-mail.service")
+        mail2_server.wait_for_open_port(993) # imap
+        mail2_server.wait_for_open_port(465) # smtp
+
+        alice.wait_for_unit("multi-user.target")
+        bob.wait_for_unit("multi-user.target")
+
+        name_server.wait_until_succeeds("stat /var/lib/acme/mta-sts.alice.com/cert.pem")
+        name_server.wait_until_succeeds("stat /var/lib/acme/mta-sts.bob.com/cert.pem")
+
+        with subtest("Add pebble ca key to all services"):
+          for node in [name_server, mail1_server, mail2_server, alice, bob]:
+            node.succeed("${pkgs.writeShellScript "fetch-and-set-ca" ''
+          set -xe
+
+          # Fetch the randomly generated ca certificate
+          curl https://acme.test:15000/roots/0 > /tmp/ca.crt
+          curl https://acme.test:15000/intermediates/0 >> /tmp/ca.crt
+
+          # Append it to the various system stores
+          # The file paths are from <nixpgks>/modules/security/ca.nix
+          for cert_path in "ssl/certs/ca-certificates.crt" "ssl/certs/ca-bundle.crt" "pki/tls/certs/ca-bundle.crt"; do
+            cert_path="/etc/$cert_path"
+
+            mv "$cert_path" "$cert_path.old"
+            cat "$cert_path.old" > "$cert_path"
+            cat /tmp/ca.crt >> "$cert_path"
+          done
+
+          export NIX_SSL_CERT_FILE=/tmp/ca.crt
+          export SSL_CERT_FILE=/tmp/ca.crt
+
+          # TODO
+          # # P11-Kit trust source.
+          # environment.etc."ssl/trust-source".source = "$${cacertPackage.p11kit}/etc/ssl/trust-source";
+        ''}")
+
+        with subtest("Both mailserver successfully started all services"):
+          import json
+          def all_services_running(host):
+            (status, output) = host.systemctl("list-units --state=failed --plain --no-pager --output=json")
+            host_failed = json.loads(output)
+            assert len(host_failed) == 0, f"Expected zero failing services, but found: {json.dumps(host_failed, indent=4)}"
+          all_services_running(mail1_server)
+          all_services_running(mail2_server)
+
+        with subtest("Both start without mail"):
+          alice.succeed("sudo -u alice ${checkEmailEmpty}")
+          bob.succeed("sudo -u bob ${checkEmailEmpty}")
+
+        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@bob.com >&2
+        ''}")
+
+          # Give `mail2_server` some time to send the email.
+          sleep(160)
+
+          bob.succeed("sudo -u bob ${checkSpamEmailNotEmpty}")
+
+        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@bob.com >&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 `mail2_server` some time to send the email.
+          sleep(120)
+
+          bob.succeed("sudo -u bob ${checkEmailNotEmpty}")
+
+        mail1_server.copy_from_vm("/var/lib/", "server1")
+        mail2_server.copy_from_vm("/var/lib/", "server2")
+        bob.copy_from_vm("/home/bob/mail", "bob")
+      '';
+  }