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.

Add exim as a lazy dependency to build its SMTP test client

The exim source (pinned commit) is declared with .lazy = true and only
fetched when requested: `zig build -Dexim-client` compiles exim's
scriptable SMTP test client (test/src/client.c) to
zig-out/bin/exim-client. The option guard keeps plain builds, tests,
and the network-less Nix sandbox build from ever fetching it.

test/protocol-torture.script (with a REUSE.toml annotation) carries
the 28-reply dialogue distilled from exim's test suite; running
exim-client with it against zsmtp serve passes all expectations, and
the same dialogue is asserted byte-for-byte by the protocol gauntlet
unit test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HBHFhoTYa8TU9GLwobfbx

+112
+17
README.md
··· 234 234 fuzzer-chosen chunk boundaries, and the collecting and streaming server 235 235 DATA paths must yield identical message content. 236 236 237 + ### Protocol torture testing with exim's test client 238 + 239 + Exim's scriptable SMTP test client (`test/src/client.c` in the exim 240 + source) sends raw protocol lines and asserts reply prefixes. The exim 241 + source is declared as a *lazy* Zig dependency, fetched only on demand: 242 + 243 + ```sh 244 + zig build -Dexim-client # fetches exim, installs zig-out/bin/exim-client 245 + ./zig-out/bin/zsmtp serve 2525 & 246 + ./zig-out/bin/exim-client 127.0.0.1 2525 < test/protocol-torture.script 247 + ``` 248 + 249 + `test/protocol-torture.script` is a 28-reply dialogue distilled from 250 + exim's own test suite (syntax errors, sequencing violations, parameter 251 + validation, dot-stuffing); the same dialogue is asserted byte-for-byte 252 + as a unit test in `Server.zig`. 253 + 237 254 Note: Zig 0.16.0's fuzz *driver* is broken out of the box (its bundled 238 255 test runner fails to compile in fuzz mode, and the coverage server panics 239 256 on a test binary with no fuzz tests); both are fixed on Zig master. Until
+5
REUSE.toml
··· 7 7 path = "flake.lock" 8 8 SPDX-FileCopyrightText = "© 2026 Jeffrey C. Ollie <jeff@ocjtech.us>" 9 9 SPDX-License-Identifier = "MIT" 10 + 11 + [[annotations]] 12 + path = "test/protocol-torture.script" 13 + SPDX-FileCopyrightText = "© 2026 Jeffrey C. Ollie <jeff@ocjtech.us>" 14 + SPDX-License-Identifier = "MIT"
+23
build.zig
··· 126 126 run_cmd.addArgs(args); 127 127 } 128 128 129 + // Exim's scriptable SMTP test client, handy for driving the zsmtp 130 + // server through raw protocol dialogues with reply expectations (see 131 + // test/protocol-torture.script). Guarded by an option so the lazy exim 132 + // dependency is only fetched on demand: 133 + // zig build -Dexim-client && ./zig-out/bin/exim-client <host> <port> 134 + if (b.option(bool, "exim-client", "Build exim's scriptable SMTP test client (fetches the exim source)") orelse false) { 135 + if (b.lazyDependency("exim", .{})) |exim_dep| { 136 + const exim_client = b.addExecutable(.{ 137 + .name = "exim-client", 138 + .root_module = b.createModule(.{ 139 + .target = target, 140 + .optimize = optimize, 141 + .link_libc = true, 142 + }), 143 + }); 144 + exim_client.root_module.addCSourceFile(.{ 145 + .file = exim_dep.path("test/src/client.c"), 146 + .flags = &.{"-w"}, 147 + }); 148 + b.installArtifact(exim_client); 149 + } 150 + } 151 + 129 152 // Creates an executable that will run `test` blocks from the provided module. 130 153 // Here `mod` needs to define a target, which is why earlier we made sure to 131 154 // set the releative field.
+7
build.zig.zon
··· 39 39 .url = "https://github.com/ianic/tls.zig/archive/e04ae448ce7ee70c136d4d48b059314543203809.tar.gz", 40 40 .hash = "tls-0.1.0-ER2e0jGpBgCkVC-Yp12NgSdHNUtZr52MleJ8roHlUa54", 41 41 }, 42 + // Exim's source, used only for its scriptable SMTP test client 43 + // (test/src/client.c). Lazy: fetched only by `zig build -Dexim-client`. 44 + .exim = .{ 45 + .url = "https://github.com/Exim/exim/archive/13835a3c1e057efad7da269c0f93bf2eac850205.tar.gz", 46 + .hash = "N-V-__8AAFtMiwHRrbE2kejFyc1FTFciBTZJlJOPGt-eIVGJ", 47 + .lazy = true, 48 + }, 42 49 }, 43 50 .paths = .{ 44 51 "build.zig",
+60
test/protocol-torture.script
··· 1 + ??? 220 2 + NOOP 3 + ??? 250 4 + rhubarb 5 + ??? 500 6 + mail from:<x@y> 7 + ??? 503 8 + rcpt to:<a@b> 9 + ??? 503 10 + ehlo test.client 11 + ??? 250- 12 + ??? 250-PIPELINING 13 + ??? 250-8BITMIME 14 + ??? 250-ENHANCEDSTATUSCODES 15 + ??? 250 SIZE 16 + mail 17 + ??? 501 18 + mail from: 19 + ??? 501 20 + mail from:<> 21 + ??? 250 22 + mail from:<x@y> 23 + ??? 503 24 + rcpt to: 25 + ??? 501 26 + data 27 + ??? 503 28 + rset 29 + ??? 250 30 + etrn abc 31 + ??? 500 32 + vrfy userx 33 + ??? 252 34 + help 35 + ??? 214 36 + mail from:<ok@test1> SIZE=100 BODY=8BITMIME 37 + ??? 250 38 + rcpt to:<userx@test.ex> 39 + ??? 250 40 + rcpt to:<@relay.example:route@test.ex> 41 + ??? 250 42 + data 43 + ??? 354 44 + ..that line started with a dot 45 + .. and one starting with two dots 46 + Message body 47 + . 48 + ??? 250 49 + mail from:<a@b> SIZE=99999999 50 + ??? 552 51 + mail from:<a@b> BODY=BINARYMIME 52 + ??? 555 53 + mail from:<a@b> FOO=bar 54 + ??? 555 55 + mail from:<a@b> SIZE=nan 56 + ??? 501 57 + starttls 58 + ??? 502 59 + quit 60 + ??? 221