# SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie # SPDX-License-Identifier: MIT # NixOS VM test exercising zsmtp against third-party implementations: # - zsmtp client -> Postfix (25/465) and Exim (2625/2626): plaintext, # STARTTLS, and implicit TLS against each, verified by checking local # delivery to alice's mailbox # - swaks -> zsmtp server: plaintext and STARTTLS, verified by checking # the received message in the server's journal { testers, callPackage, runCommand, openssl, }: let zsmtp = callPackage ./package.nix { }; snakeoil = runCommand "zsmtp-test-cert" { nativeBuildInputs = [ openssl ]; } '' mkdir -p $out openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \ -keyout $out/key.pem -out $out/cert.pem -days 36500 -nodes \ -subj "/CN=localhost" \ -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" ''; in testers.runNixOSTest { name = "zsmtp-interop"; nodes.machine = { pkgs, ... }: { environment.systemPackages = [ zsmtp pkgs.swaks ]; users.users.alice.isNormalUser = true; services.postfix = { enable = true; # Implicit-TLS smtpd on port 465; allow loopback without SASL. enableSubmissions = true; submissionsOptions = { smtpd_client_restrictions = "permit_mynetworks,reject"; }; settings.main = { mydestination = [ "localhost" "$myhostname" ]; smtpd_tls_security_level = "may"; smtpd_tls_chain_files = [ "${snakeoil}/key.pem" "${snakeoil}/cert.pem" ]; }; }; # Exim: plaintext + STARTTLS on 2625, implicit TLS on 2626, delivering # to /var/spool/exim-mail/. services.exim = { enable = true; config = '' primary_hostname = machine.test qualify_domain = localhost local_interfaces = 127.0.0.1 daemon_smtp_ports = 2625 : 2626 tls_on_connect_ports = 2626 tls_advertise_hosts = * tls_certificate = ${snakeoil}/cert.pem tls_privatekey = ${snakeoil}/key.pem acl_smtp_rcpt = acl_rcpt begin acl acl_rcpt: accept begin routers local_users: driver = accept local_parts = alice : bob transport = local_delivery begin transports local_delivery: driver = appendfile file = /var/spool/exim-mail/$local_part_data user = exim delivery_date_add envelope_to_add return_path_add begin authenticators plain_server: driver = plaintext public_name = PLAIN server_prompts = : server_condition = ''${if and{{eq{$auth2}{alice}}{eq{$auth3}{secret}}}} server_set_id = $auth2 login_server: driver = plaintext public_name = LOGIN server_prompts = Username:: : Password:: server_condition = ''${if and{{eq{$auth1}{alice}}{eq{$auth2}{secret}}}} server_set_id = $auth1 ''; }; systemd.tmpfiles.rules = [ "d /var/spool/exim-mail 0755 exim exim -" ]; systemd.services.zsmtp-server = { description = "zsmtp debug server (plaintext)"; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${zsmtp}/bin/zsmtp serve 2525"; DynamicUser = true; }; }; systemd.services.zsmtp-server-tls = { description = "zsmtp debug server (STARTTLS)"; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${zsmtp}/bin/zsmtp serve --tls-cert ${snakeoil}/cert.pem --tls-key ${snakeoil}/key.pem 2526"; DynamicUser = true; }; }; systemd.services.zsmtp-server-auth = { description = "zsmtp debug server (authentication required)"; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = "${zsmtp}/bin/zsmtp serve --auth alice:secret 2527"; DynamicUser = true; }; }; }; testScript = '' machine.wait_for_unit("postfix.service") machine.wait_for_open_port(25) machine.wait_for_open_port(465) machine.wait_for_unit("exim.service") machine.wait_for_open_port(2625) machine.wait_for_open_port(2626) machine.wait_for_unit("zsmtp-server.service") machine.wait_for_unit("zsmtp-server-tls.service") machine.wait_for_unit("zsmtp-server-auth.service") machine.wait_for_open_port(2525) machine.wait_for_open_port(2526) machine.wait_for_open_port(2527) def deliver(flags, port, needle, mailbox): machine.succeed( f"printf 'Subject: interop\\r\\n\\r\\n{needle}\\r\\n'" f" | zsmtp send {flags} 127.0.0.1 {port}" " bob@example.com alice@localhost" ) machine.wait_until_succeeds(f"grep -r '{needle}' {mailbox}", timeout=60) servers = { "postfix": (25, 465, "/var/spool/mail/alice/"), "exim": (2625, 2626, "/var/spool/exim-mail/alice"), } for name, (port, tls_port, mailbox) in servers.items(): with subtest(f"zsmtp client to {name}, plaintext"): deliver("", port, f"zsmtp to {name} plain", mailbox) with subtest(f"zsmtp client to {name}, STARTTLS"): deliver( "--starttls --insecure", port, f"zsmtp to {name} starttls", mailbox ) with subtest(f"zsmtp client to {name}, implicit TLS"): deliver("--tls --insecure", tls_port, f"zsmtp to {name} smtps", mailbox) with subtest("zsmtp client to exim, AUTH PLAIN"): deliver( "--user alice --password secret --auth-method plain", 2625, "zsmtp to exim auth plain", "/var/spool/exim-mail/alice", ) with subtest("zsmtp client to exim, AUTH LOGIN"): deliver( "--user alice --password secret --auth-method login", 2625, "zsmtp to exim auth login", "/var/spool/exim-mail/alice", ) with subtest("zsmtp client to exim, wrong password is rejected"): machine.fail( "printf 'Subject: interop\\r\\n\\r\\nnope\\r\\n'" " | zsmtp send --user alice --password wrong 127.0.0.1 2625" " bob@example.com alice@localhost" ) with subtest("swaks to zsmtp server, AUTH PLAIN"): machine.succeed( "swaks --server 127.0.0.1:2527 --auth PLAIN --auth-user alice" " --auth-password secret --from bob@example.com" " --to alice@example.net --body 'swaks to zsmtp auth plain'" ) machine.wait_until_succeeds( "journalctl -u zsmtp-server-auth | grep 'swaks to zsmtp auth plain'", timeout=60, ) with subtest("swaks to zsmtp server, AUTH LOGIN"): machine.succeed( "swaks --server 127.0.0.1:2527 --auth LOGIN --auth-user alice" " --auth-password secret --from bob@example.com" " --to alice@example.net --body 'swaks to zsmtp auth login'" ) machine.wait_until_succeeds( "journalctl -u zsmtp-server-auth | grep 'swaks to zsmtp auth login'", timeout=60, ) with subtest("swaks to zsmtp server, wrong password is rejected"): machine.fail( "swaks --server 127.0.0.1:2527 --auth PLAIN --auth-user alice" " --auth-password wrong --from bob@example.com" " --to alice@example.net --body nope" ) with subtest("unauthenticated mail to auth-required server is rejected"): machine.fail( "printf 'Subject: interop\\r\\n\\r\\nnope\\r\\n'" " | zsmtp send 127.0.0.1 2527 bob@example.com alice@example.net" ) with subtest("swaks to zsmtp server, plaintext"): machine.succeed( "swaks --server 127.0.0.1:2525 --from bob@example.com" " --to alice@example.net --header 'Subject: swaks plain'" " --body 'swaks to zsmtp plain'" ) machine.wait_until_succeeds( "journalctl -u zsmtp-server | grep 'swaks to zsmtp plain'", timeout=60 ) with subtest("swaks to zsmtp server, STARTTLS"): machine.succeed( "swaks --tls --server 127.0.0.1:2526 --from bob@example.com" " --to alice@example.net --header 'Subject: swaks starttls'" " --body 'swaks to zsmtp starttls'" ) machine.wait_until_succeeds( "journalctl -u zsmtp-server-tls | grep 'swaks to zsmtp starttls'", timeout=60 ) ''; }