An SMTP client and server library for Zig implementing RFC 5321.
0

Configure Feed

Select the types of activity you want to include in your feed.

zig-smtp / nix / interop-test.nix
13 kB 356 lines
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 (25/465) and Exim (2625/2626): plaintext, 6# STARTTLS, and implicit TLS against each, verified by checking local 7# delivery to alice's mailbox 8# - swaks -> zsmtp server: plaintext and STARTTLS, verified by checking 9# the received message in the server's journal 10 11{ 12 testers, 13 callPackage, 14 runCommand, 15 openssl, 16}: 17let 18 zsmtp = callPackage ../package.nix { }; 19 20 snakeoil = 21 runCommand "zsmtp-test-cert" 22 { 23 nativeBuildInputs = [ openssl ]; 24 } 25 '' 26 mkdir -p $out 27 openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 \ 28 -keyout $out/key.pem -out $out/cert.pem -days 36500 -nodes \ 29 -subj "/CN=localhost" \ 30 -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 31 ''; 32in 33testers.runNixOSTest { 34 name = "zsmtp-interop"; 35 36 nodes.machine = 37 { pkgs, ... }: 38 { 39 environment.systemPackages = [ 40 zsmtp 41 pkgs.swaks 42 ]; 43 44 users.users.alice.isNormalUser = true; 45 46 services.postfix = { 47 enable = true; 48 # Implicit-TLS smtpd on port 465; allow loopback without SASL. 49 enableSubmissions = true; 50 submissionsOptions = { 51 smtpd_client_restrictions = "permit_mynetworks,reject"; 52 }; 53 settings.main = { 54 mydestination = [ 55 "localhost" 56 "$myhostname" 57 ]; 58 smtpd_tls_security_level = "may"; 59 smtpd_tls_chain_files = [ 60 "${snakeoil}/key.pem" 61 "${snakeoil}/cert.pem" 62 ]; 63 }; 64 }; 65 66 # Exim: plaintext + STARTTLS on 2625, implicit TLS on 2626, delivering 67 # to /var/spool/exim-mail/<user>. 68 services.exim = { 69 enable = true; 70 config = '' 71 primary_hostname = machine.test 72 qualify_domain = localhost 73 local_interfaces = 127.0.0.1 74 daemon_smtp_ports = 2625 : 2626 75 tls_on_connect_ports = 2626 76 tls_advertise_hosts = * 77 # Off by default in exim, and the point of the DSN subtest below. 78 dsn_advertise_hosts = * 79 tls_certificate = ${snakeoil}/cert.pem 80 tls_privatekey = ${snakeoil}/key.pem 81 acl_smtp_rcpt = acl_rcpt 82 83 begin acl 84 85 acl_rcpt: 86 accept 87 88 begin routers 89 90 local_users: 91 driver = accept 92 local_parts = alice : bob 93 transport = local_delivery 94 95 begin transports 96 97 local_delivery: 98 driver = appendfile 99 file = /var/spool/exim-mail/$local_part_data 100 user = exim 101 delivery_date_add 102 envelope_to_add 103 return_path_add 104 105 begin authenticators 106 107 plain_server: 108 driver = plaintext 109 public_name = PLAIN 110 server_prompts = : 111 server_condition = ''${if and{{eq{$auth2}{alice}}{eq{$auth3}{secret}}}} 112 server_set_id = $auth2 113 114 login_server: 115 driver = plaintext 116 public_name = LOGIN 117 server_prompts = Username:: : Password:: 118 server_condition = ''${if and{{eq{$auth1}{alice}}{eq{$auth2}{secret}}}} 119 server_set_id = $auth1 120 ''; 121 }; 122 123 systemd.tmpfiles.rules = [ 124 "d /var/spool/exim-mail 0755 exim exim -" 125 ]; 126 127 systemd.services.zsmtp-server = { 128 description = "zsmtp debug server (plaintext)"; 129 wantedBy = [ "multi-user.target" ]; 130 serviceConfig = { 131 ExecStart = "${zsmtp}/bin/zsmtp serve 2525"; 132 DynamicUser = true; 133 }; 134 }; 135 136 systemd.services.zsmtp-server-tls = { 137 description = "zsmtp debug server (STARTTLS)"; 138 wantedBy = [ "multi-user.target" ]; 139 serviceConfig = { 140 ExecStart = "${zsmtp}/bin/zsmtp serve --tls-cert ${snakeoil}/cert.pem --tls-key ${snakeoil}/key.pem 2526"; 141 DynamicUser = true; 142 }; 143 }; 144 145 systemd.services.zsmtp-server-tlsc = { 146 description = "zsmtp debug server (implicit TLS)"; 147 wantedBy = [ "multi-user.target" ]; 148 serviceConfig = { 149 ExecStart = "${zsmtp}/bin/zsmtp serve --tls-cert ${snakeoil}/cert.pem --tls-key ${snakeoil}/key.pem --implicit-tls 2528"; 150 DynamicUser = true; 151 }; 152 }; 153 154 systemd.services.zsmtp-server-auth = { 155 description = "zsmtp debug server (authentication required)"; 156 wantedBy = [ "multi-user.target" ]; 157 serviceConfig = { 158 ExecStart = "${zsmtp}/bin/zsmtp serve --auth alice:secret 2527"; 159 DynamicUser = true; 160 }; 161 }; 162 }; 163 164 testScript = '' 165 machine.wait_for_unit("postfix.service") 166 machine.wait_for_open_port(25) 167 machine.wait_for_open_port(465) 168 machine.wait_for_unit("exim.service") 169 machine.wait_for_open_port(2625) 170 machine.wait_for_open_port(2626) 171 machine.wait_for_unit("zsmtp-server.service") 172 machine.wait_for_unit("zsmtp-server-tls.service") 173 machine.wait_for_unit("zsmtp-server-tlsc.service") 174 machine.wait_for_unit("zsmtp-server-auth.service") 175 machine.wait_for_open_port(2525) 176 machine.wait_for_open_port(2526) 177 machine.wait_for_open_port(2527) 178 machine.wait_for_open_port(2528) 179 180 181 def deliver(flags, port, needle, mailbox): 182 machine.succeed( 183 f"printf 'Subject: interop\\r\\n\\r\\n{needle}\\r\\n'" 184 f" | zsmtp send {flags} 127.0.0.1 {port}" 185 " bob@example.com alice@localhost" 186 ) 187 machine.wait_until_succeeds(f"grep -r '{needle}' {mailbox}", timeout=60) 188 189 190 servers = { 191 "postfix": (25, 465, "/var/spool/mail/alice/"), 192 "exim": (2625, 2626, "/var/spool/exim-mail/alice"), 193 } 194 195 for name, (port, tls_port, mailbox) in servers.items(): 196 with subtest(f"zsmtp client to {name}, plaintext"): 197 deliver("", port, f"zsmtp to {name} plain", mailbox) 198 199 with subtest(f"zsmtp client to {name}, STARTTLS"): 200 deliver( 201 "--starttls --insecure", port, f"zsmtp to {name} starttls", mailbox 202 ) 203 204 with subtest(f"zsmtp client to {name}, implicit TLS"): 205 deliver("--tls --insecure", tls_port, f"zsmtp to {name} smtps", mailbox) 206 207 for name, (port, tls_port, mailbox) in servers.items(): 208 with subtest(f"zsmtp client to {name}, CHUNKING"): 209 deliver("--chunking", port, f"zsmtp to {name} chunked", mailbox) 210 211 # RFC 3461. Postfix advertises DSN out of the box; exim is told to above. 212 for name, (port, tls_port, mailbox) in servers.items(): 213 with subtest(f"zsmtp client to {name}, DSN parameters"): 214 deliver( 215 "--ret hdrs --envid batch7 --notify success,failure" 216 " --orcpt team@example.net", 217 port, 218 f"zsmtp to {name} dsn", 219 mailbox, 220 ) 221 222 with subtest("zsmtp client to zsmtp server, DSN parameters round trip"): 223 machine.succeed( 224 "printf 'Subject: interop\\r\\n\\r\\nzsmtp dsn round trip\\r\\n'" 225 " | zsmtp send --ret full --envid 'batch 7'" 226 " --notify success,delay --orcpt 'team+list@example.net'" 227 " 127.0.0.1 2525 bob@example.com alice@example.net" 228 ) 229 # The server prints what it parsed: the ENVID comes back with its 230 # space, and the ORCPT with the '+' that had to be xtext-encoded. 231 machine.wait_until_succeeds( 232 "journalctl -u zsmtp-server | grep -F" 233 " 'NOTIFY=SUCCESS,DELAY ORCPT=rfc822;team+2Blist@example.net" 234 " RET=FULL ENVID=batch 7'", 235 timeout=60, 236 ) 237 238 with subtest("zsmtp client to postfix, SMTPUTF8"): 239 machine.succeed( 240 "printf 'Subject: interop\\r\\n\\r\\nzsmtp to postfix utf8\\r\\n'" 241 " | zsmtp send --smtputf8 127.0.0.1 25" 242 " 'böb@example.com' alice@localhost" 243 ) 244 machine.wait_until_succeeds( 245 "grep -r 'zsmtp to postfix utf8' /var/spool/mail/alice/", timeout=60 246 ) 247 248 # Exim advertises only PLAIN and LOGIN, both of which put the password 249 # on the wire, so the client refuses them over this plaintext loopback 250 # connection unless it is told to allow it. 251 with subtest("zsmtp client to exim, AUTH PLAIN"): 252 deliver( 253 "--allow-cleartext-auth --user alice --password secret --auth-method plain", 254 2625, 255 "zsmtp to exim auth plain", 256 "/var/spool/exim-mail/alice", 257 ) 258 259 with subtest("zsmtp client to exim, AUTH LOGIN"): 260 deliver( 261 "--allow-cleartext-auth --user alice --password secret --auth-method login", 262 2625, 263 "zsmtp to exim auth login", 264 "/var/spool/exim-mail/alice", 265 ) 266 267 with subtest("zsmtp client to exim, wrong password is rejected"): 268 machine.fail( 269 "printf 'Subject: interop\\r\\n\\r\\nnope\\r\\n'" 270 " | zsmtp send --allow-cleartext-auth --user alice --password wrong" 271 " 127.0.0.1 2625 bob@example.com alice@localhost" 272 ) 273 274 # The refusal itself, which is what stops a password reaching the network 275 # by accident: the same delivery without the opt-in must not go through. 276 with subtest("zsmtp client refuses cleartext AUTH without the opt-in"): 277 machine.fail( 278 "printf 'Subject: interop\\r\\n\\r\\nnope\\r\\n'" 279 " | zsmtp send --user alice --password secret 127.0.0.1 2625" 280 " bob@example.com alice@localhost" 281 ) 282 # ...and over STARTTLS it goes through with no opt-in at all. 283 deliver( 284 "--starttls --insecure --user alice --password secret", 285 2625, 286 "zsmtp to exim auth over starttls", 287 "/var/spool/exim-mail/alice", 288 ) 289 290 with subtest("swaks to zsmtp server, AUTH PLAIN"): 291 machine.succeed( 292 "swaks --server 127.0.0.1:2527 --auth PLAIN --auth-user alice" 293 " --auth-password secret --from bob@example.com" 294 " --to alice@example.net --body 'swaks to zsmtp auth plain'" 295 ) 296 machine.wait_until_succeeds( 297 "journalctl -u zsmtp-server-auth | grep 'swaks to zsmtp auth plain'", 298 timeout=60, 299 ) 300 301 with subtest("swaks to zsmtp server, AUTH LOGIN"): 302 machine.succeed( 303 "swaks --server 127.0.0.1:2527 --auth LOGIN --auth-user alice" 304 " --auth-password secret --from bob@example.com" 305 " --to alice@example.net --body 'swaks to zsmtp auth login'" 306 ) 307 machine.wait_until_succeeds( 308 "journalctl -u zsmtp-server-auth | grep 'swaks to zsmtp auth login'", 309 timeout=60, 310 ) 311 312 with subtest("swaks to zsmtp server, wrong password is rejected"): 313 machine.fail( 314 "swaks --server 127.0.0.1:2527 --auth PLAIN --auth-user alice" 315 " --auth-password wrong --from bob@example.com" 316 " --to alice@example.net --body nope" 317 ) 318 319 with subtest("unauthenticated mail to auth-required server is rejected"): 320 machine.fail( 321 "printf 'Subject: interop\\r\\n\\r\\nnope\\r\\n'" 322 " | zsmtp send 127.0.0.1 2527 bob@example.com alice@example.net" 323 ) 324 325 with subtest("swaks to zsmtp server, plaintext"): 326 machine.succeed( 327 "swaks --server 127.0.0.1:2525 --from bob@example.com" 328 " --to alice@example.net --header 'Subject: swaks plain'" 329 " --body 'swaks to zsmtp plain'" 330 ) 331 machine.wait_until_succeeds( 332 "journalctl -u zsmtp-server | grep 'swaks to zsmtp plain'", timeout=60 333 ) 334 335 with subtest("swaks to zsmtp server, implicit TLS"): 336 machine.succeed( 337 "swaks --tlsc --server 127.0.0.1:2528 --from bob@example.com" 338 " --to alice@example.net --header 'Subject: swaks tlsc'" 339 " --body 'swaks to zsmtp implicit tls'" 340 ) 341 machine.wait_until_succeeds( 342 "journalctl -u zsmtp-server-tlsc | grep 'swaks to zsmtp implicit tls'", 343 timeout=60, 344 ) 345 346 with subtest("swaks to zsmtp server, STARTTLS"): 347 machine.succeed( 348 "swaks --tls --server 127.0.0.1:2526 --from bob@example.com" 349 " --to alice@example.net --header 'Subject: swaks starttls'" 350 " --body 'swaks to zsmtp starttls'" 351 ) 352 machine.wait_until_succeeds( 353 "journalctl -u zsmtp-server-tls | grep 'swaks to zsmtp starttls'", timeout=60 354 ) 355 ''; 356}