about summary refs log tree commit diff stats
path: root/tests/by-name/em/email-dns/test.nix
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-03-05 19:06:53 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-03-09 13:44:40 +0100
commitef0b3f491e1868c7b3899aff3f53be0325313c2d (patch)
tree913ddeb99ca5ce3e10f49dfe858d37780aea3c12 /tests/by-name/em/email-dns/test.nix
parentpkgs/fetchmail-common-name: Patch fetchmail to accept certificates without co... (diff)
downloadnixos-server-ef0b3f491e1868c7b3899aff3f53be0325313c2d.zip
tests/email-dns: Init
This test is somewhat involved, but tries to exercise our full mail
handling capabilities.
It effectively only tests that alice can send a message to bob, but it
checks nearly all security mechanisms (DNSSEC is currently still missing).
Diffstat (limited to 'tests/by-name/em/email-dns/test.nix')
-rw-r--r--tests/by-name/em/email-dns/test.nix203
1 files changed, 203 insertions, 0 deletions
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")
+      '';
+  }