An SMTP client and server library for Zig implementing RFC 5321.
1# SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>
2# SPDX-License-Identifier: MIT
3
4# NixOS VM test exercising zsmtp against third-party implementations:
5# - zsmtp client -> Postfix: plaintext, STARTTLS (port 25), implicit TLS
6# (port 465), verified by checking local delivery to alice's spool
7# - swaks -> zsmtp server: plaintext and STARTTLS, verified by checking
8# the received message in the server's journal
9
10{
11 testers,
12 callPackage,
13 runCommand,
14 openssl,
15}:
16let
17 zsmtp = callPackage ./package.nix { };
18
19 snakeoil =
20 runCommand "zsmtp-test-cert"
21 {
22 nativeBuildInputs = [ openssl ];
23 }
24 ''
25 mkdir -p $out
26 openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \
27 -keyout $out/key.pem -out $out/cert.pem -days 36500 -nodes \
28 -subj "/CN=localhost" \
29 -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
30 '';
31in
32testers.runNixOSTest {
33 name = "zsmtp-interop";
34
35 nodes.machine =
36 { pkgs, ... }:
37 {
38 environment.systemPackages = [
39 zsmtp
40 pkgs.swaks
41 ];
42
43 users.users.alice.isNormalUser = true;
44
45 services.postfix = {
46 enable = true;
47 # Implicit-TLS smtpd on port 465; allow loopback without SASL.
48 enableSubmissions = true;
49 submissionsOptions = {
50 smtpd_client_restrictions = "permit_mynetworks,reject";
51 };
52 settings.main = {
53 mydestination = [
54 "localhost"
55 "$myhostname"
56 ];
57 smtpd_tls_security_level = "may";
58 smtpd_tls_chain_files = [
59 "${snakeoil}/key.pem"
60 "${snakeoil}/cert.pem"
61 ];
62 };
63 };
64
65 systemd.services.zsmtp-server = {
66 description = "zsmtp debug server (plaintext)";
67 wantedBy = [ "multi-user.target" ];
68 serviceConfig = {
69 ExecStart = "${zsmtp}/bin/zsmtp serve 2525";
70 DynamicUser = true;
71 };
72 };
73
74 systemd.services.zsmtp-server-tls = {
75 description = "zsmtp debug server (STARTTLS)";
76 wantedBy = [ "multi-user.target" ];
77 serviceConfig = {
78 ExecStart = "${zsmtp}/bin/zsmtp serve --tls-cert ${snakeoil}/cert.pem --tls-key ${snakeoil}/key.pem 2526";
79 DynamicUser = true;
80 };
81 };
82 };
83
84 testScript = ''
85 machine.wait_for_unit("postfix.service")
86 machine.wait_for_open_port(25)
87 machine.wait_for_open_port(465)
88 machine.wait_for_unit("zsmtp-server.service")
89 machine.wait_for_unit("zsmtp-server-tls.service")
90 machine.wait_for_open_port(2525)
91 machine.wait_for_open_port(2526)
92
93 with subtest("zsmtp client to postfix, plaintext"):
94 machine.succeed(
95 "printf 'Subject: interop\\r\\n\\r\\nzsmtp to postfix plain\\r\\n'"
96 " | zsmtp send 127.0.0.1 25 bob@example.com alice@localhost"
97 )
98 machine.wait_until_succeeds(
99 "grep -r 'zsmtp to postfix plain' /var/spool/mail/alice/", timeout=60
100 )
101
102 with subtest("zsmtp client to postfix, STARTTLS"):
103 machine.succeed(
104 "printf 'Subject: interop\\r\\n\\r\\nzsmtp to postfix starttls\\r\\n'"
105 " | zsmtp send --starttls --insecure 127.0.0.1 25"
106 " bob@example.com alice@localhost"
107 )
108 machine.wait_until_succeeds(
109 "grep -r 'zsmtp to postfix starttls' /var/spool/mail/alice/", timeout=60
110 )
111
112 with subtest("zsmtp client to postfix, implicit TLS"):
113 machine.succeed(
114 "printf 'Subject: interop\\r\\n\\r\\nzsmtp to postfix smtps\\r\\n'"
115 " | zsmtp send --tls --insecure 127.0.0.1 465"
116 " bob@example.com alice@localhost"
117 )
118 machine.wait_until_succeeds(
119 "grep -r 'zsmtp to postfix smtps' /var/spool/mail/alice/", timeout=60
120 )
121
122 with subtest("swaks to zsmtp server, plaintext"):
123 machine.succeed(
124 "swaks --server 127.0.0.1:2525 --from bob@example.com"
125 " --to alice@example.net --header 'Subject: swaks plain'"
126 " --body 'swaks to zsmtp plain'"
127 )
128 machine.wait_until_succeeds(
129 "journalctl -u zsmtp-server | grep 'swaks to zsmtp plain'", timeout=60
130 )
131
132 with subtest("swaks to zsmtp server, STARTTLS"):
133 machine.succeed(
134 "swaks --tls --server 127.0.0.1:2526 --from bob@example.com"
135 " --to alice@example.net --header 'Subject: swaks starttls'"
136 " --body 'swaks to zsmtp starttls'"
137 )
138 machine.wait_until_succeeds(
139 "journalctl -u zsmtp-server-tls | grep 'swaks to zsmtp starttls'", timeout=60
140 )
141 '';
142}