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.

Rename the project to zig-smtp

It sits beside zig-pop3, zig-ftp, zig-scram and zig-sasl, and "zsmtp" was
the odd one out.

Three names come out of it rather than one, following what those siblings
do. The repository and the package are `zig-smtp`; the Zig module is
`smtp`, so callers write `@import("smtp")` the way zig-pop3's callers
write `@import("pop3")`; and the demo CLI is `zig-smtp`, matching zig-ftp
rather than pop3's bare `pop3`, because `smtp` is too generic a name to
put on somebody's PATH.

The manifest fingerprint had to change with the package name -- Zig
derives it from that name and refuses the old one -- so this is a new
package as far as the package manager is concerned, not a renamed one.

The Radicle identity was renamed in place, so the RID is unchanged and
every `rad clone rad:z3ZKHgoDKEue8FT7sV6fHZdtjxRx1` in the wild still
works. The Tangled mirror could not be renamed and was recreated.

The published documentation moves with it, from jeff.jcollie.page/zsmtp/
to jeff.jcollie.page/zig-smtp/.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SDrB41sGu5k1ubD1ufbxqC

+186 -186
+40 -40
README.md
··· 3 3 SPDX-License-Identifier: MIT 4 4 --> 5 5 6 - # zsmtp 6 + # zig-smtp 7 7 8 8 An SMTP client and server library for Zig (RFC 5321). 9 9 ··· 15 15 16 16 The canonical repository is on Forgejo, with mirrors on Tangled and Radicle: 17 17 18 - - <https://git.jcollie.dev/jeff/zsmtp> — issues and pull requests 19 - - <https://tangled.org/jcollie.dev/zsmtp> 18 + - <https://git.jcollie.dev/jeff/zig-smtp> — issues and pull requests 19 + - <https://tangled.org/jcollie.dev/zig-smtp> 20 20 21 21 ```sh 22 - git clone https://git.jcollie.dev/jeff/zsmtp.git 22 + git clone https://git.jcollie.dev/jeff/zig-smtp.git 23 23 ``` 24 24 25 25 On [Radicle](https://radicle.xyz/), the peer-to-peer forge, the repository is ··· 35 35 network. 36 36 37 37 The API documentation is generated from the doc comments and published at 38 - <https://jeff.jcollie.page/zsmtp/>; `zig build docs` builds it locally and 38 + <https://jeff.jcollie.page/zig-smtp/>; `zig build docs` builds it locally and 39 39 `zig build docs-serve` serves it for reading. 40 40 41 41 ## Client 42 42 43 43 ```zig 44 - const zsmtp = @import("zsmtp"); 44 + const zig-smtp = @import("smtp"); 45 45 46 46 var reply_buf: [1024]u8 = undefined; 47 - var client: zsmtp.Client = .init(&stream_reader.interface, &stream_writer.interface, &reply_buf); 47 + var client: zig-smtp.Client = .init(&stream_reader.interface, &stream_writer.interface, &reply_buf); 48 48 49 49 _ = try client.greet(); // read the 220 greeting 50 50 _ = try client.hello("my-host.example.com"); // EHLO (HELO fallback), returns extensions ··· 161 161 162 162 The mechanisms themselves live in 163 163 [zig-sasl](https://git.jcollie.dev/jeff/zig-sasl), re-exported here as 164 - `zsmtp.sasl`, because nothing about PLAIN or CRAM-MD5 or XOAUTH2 is specific 164 + `zig-smtp.sasl`, because nothing about PLAIN or CRAM-MD5 or XOAUTH2 is specific 165 165 to SMTP — POP3 and IMAP want the same ones, and one implementation of each is 166 166 better than three. What is specific to SMTP is `authenticate`: the `AUTH` 167 167 command, the 334 challenges, the `*` that cancels, and the 235 that ends it. ··· 170 170 exactly as it sent them, for `sasl.Client.selectFromList`: 171 171 172 172 ```zig 173 - var plain: zsmtp.sasl.Plain = .init("user", "password"); 174 - var cram: zsmtp.sasl.CramMd5 = .init("user", "password"); 173 + var plain: zig-smtp.sasl.Plain = .init("user", "password"); 174 + var cram: zig-smtp.sasl.CramMd5 = .init("user", "password"); 175 175 176 176 const extensions = try client.hello("my-host.example.com"); 177 - const mechanism = zsmtp.sasl.Client.selectFromList( 177 + const mechanism = zig-smtp.sasl.Client.selectFromList( 178 178 &.{ plain.client(), cram.client() }, // in order of preference 179 179 extensions.auth, 180 180 client.security == .encrypted, ··· 212 212 213 213 ### TLS 214 214 215 - `zsmtp.Tls` wraps [ianic/tls.zig](https://github.com/ianic/tls.zig) and 215 + `zig-smtp.Tls` wraps [ianic/tls.zig](https://github.com/ianic/tls.zig) and 216 216 verifies against the system trust store by default (a caller-managed CA 217 217 bundle and an insecure mode are also available). The stream reader/writer 218 - handed to it need buffers of at least `zsmtp.Tls.min_buffer_len` bytes, and 218 + handed to it need buffers of at least `zig-smtp.Tls.min_buffer_len` bytes, and 219 219 `init` must run at the value's final address (the connection holds interior 220 220 pointers). The standard library's TLS client is deliberately not used: it 221 221 requires the optional TLS 1.3 middlebox-compatibility ChangeCipherSpec ··· 224 224 Implicit TLS (port 465) — handshake first, then speak SMTP: 225 225 226 226 ```zig 227 - var tls: zsmtp.Tls = undefined; 227 + var tls: zig-smtp.Tls = undefined; 228 228 try tls.init(io, gpa, &stream_reader.interface, &stream_writer.interface, .{ 229 229 .host = "smtp.example.com", 230 230 }); 231 231 defer tls.deinit(gpa); 232 - var client: zsmtp.Client = .init(tls.reader(), tls.writer(), &reply_buf); 232 + var client: zig-smtp.Client = .init(tls.reader(), tls.writer(), &reply_buf); 233 233 client.security = .encrypted; // the transport is TLS; `init` cannot tell 234 234 // ... greet, hello, sendMail ... 235 235 try client.quit(); ··· 242 242 _ = try client.greet(); 243 243 _ = try client.hello("my-host.example.com"); // check .starttls in the result 244 244 try client.starttls(); 245 - var tls: zsmtp.Tls = undefined; 245 + var tls: zig-smtp.Tls = undefined; 246 246 try tls.init(io, gpa, &stream_reader.interface, &stream_writer.interface, .{ 247 247 .host = "smtp.example.com", 248 248 }); ··· 253 253 ## Server 254 254 255 255 ```zig 256 - var session: zsmtp.Server = .init(&stream_reader.interface, &stream_writer.interface, .{ 256 + var session: zig-smtp.Server = .init(&stream_reader.interface, &stream_writer.interface, .{ 257 257 .context = &my_state, 258 258 .vtable = &.{ 259 259 .authenticate = onAuth, // optional; enables AUTH PLAIN and LOGIN ··· 297 297 callback supplies each verdict: 298 298 299 299 ```zig 300 - fn onRecipientResult(ctx: ?*anyopaque, envelope: zsmtp.Server.Envelope, index: usize) zsmtp.Server.Decision { 300 + fn onRecipientResult(ctx: ?*anyopaque, envelope: zig-smtp.Server.Envelope, index: usize) zig-smtp.Server.Decision { 301 301 return if (mailboxIsFull(envelope.recipients[index].address)) 302 302 .{ .reject = .{ .code = 452, .text = "4.2.2 Mailbox full" } } 303 303 else ··· 328 328 329 329 To advertise and accept STARTTLS (TLS 1.3, via 330 330 [ianic/tls.zig](https://github.com/ianic/tls.zig)), pass a certificate key 331 - pair; the stream buffers must then be at least `zsmtp.tls.input_buffer_len` / 332 - `zsmtp.tls.output_buffer_len` bytes, since the handshake runs over them: 331 + pair; the stream buffers must then be at least `zig-smtp.tls.input_buffer_len` / 332 + `zig-smtp.tls.output_buffer_len` bytes, since the handshake runs over them: 333 333 334 334 ```zig 335 - var auth: zsmtp.tls.config.CertKeyPair = 335 + var auth: zig-smtp.tls.config.CertKeyPair = 336 336 try .fromFilePath(gpa, io, .cwd(), "cert.pem", "key.pem"); 337 337 defer auth.deinit(gpa); 338 338 339 - var session: zsmtp.Server = .init(&stream_reader.interface, &stream_writer.interface, handler, .{ 339 + var session: zig-smtp.Server = .init(&stream_reader.interface, &stream_writer.interface, handler, .{ 340 340 .hostname = "mx.example.com", 341 341 .tls = .{ .io = io, .auth = &auth }, 342 342 }); ··· 349 349 before the greeting (SMTPS, port 465 style): 350 350 351 351 ```zig 352 - var session: zsmtp.Server = .init(&stream_reader.interface, &stream_writer.interface, handler, .{ 352 + var session: zig-smtp.Server = .init(&stream_reader.interface, &stream_writer.interface, handler, .{ 353 353 .hostname = "mx.example.com", 354 354 .tls = .{ .io = io, .auth = &auth, .mode = .implicit }, 355 355 }); ··· 362 362 363 363 # Debug server that prints received messages to stdout 364 364 # (with a cert/key pair it advertises and accepts STARTTLS): 365 - ./zig-out/bin/zsmtp serve 2525 366 - ./zig-out/bin/zsmtp serve --tls-cert cert.pem --tls-key key.pem 2525 367 - ./zig-out/bin/zsmtp serve --tls-cert cert.pem --tls-key key.pem --implicit-tls 2465 365 + ./zig-out/bin/zig-smtp serve 2525 366 + ./zig-out/bin/zig-smtp serve --tls-cert cert.pem --tls-key key.pem 2525 367 + ./zig-out/bin/zig-smtp serve --tls-cert cert.pem --tls-key key.pem --implicit-tls 2465 368 368 369 369 # Send a message read from stdin: 370 370 printf 'Subject: hi\r\n\r\nhello\r\n' | \ 371 - ./zig-out/bin/zsmtp send 127.0.0.1 2525 me@example.com you@example.net 371 + ./zig-out/bin/zig-smtp send 127.0.0.1 2525 me@example.com you@example.net 372 372 373 373 # Same, over implicit TLS or STARTTLS (--insecure skips cert verification): 374 - zsmtp send --tls smtp.example.com 465 me@example.com you@example.net 375 - zsmtp send --starttls smtp.example.com 587 me@example.com you@example.net 374 + zig-smtp send --tls smtp.example.com 465 me@example.com you@example.net 375 + zig-smtp send --starttls smtp.example.com 587 me@example.com you@example.net 376 376 377 377 # Send arbitrary binary content (RFC 3030), framed by BDAT rather than DATA: 378 - ./zig-out/bin/zsmtp send --binarymime 127.0.0.1 2525 me@example.com you@example.net \ 378 + ./zig-out/bin/zig-smtp send --binarymime 127.0.0.1 2525 me@example.com you@example.net \ 379 379 < some-binary-file 380 380 381 381 # Speak LMTP (RFC 2033) instead of SMTP. The server reports one verdict per 382 382 # recipient, and --fail-delivery makes one of them fail to show it: 383 - ./zig-out/bin/zsmtp serve --lmtp --fail-delivery bad@example.net 2529 383 + ./zig-out/bin/zig-smtp serve --lmtp --fail-delivery bad@example.net 2529 384 384 printf 'Subject: hi\r\n\r\nhello\r\n' | \ 385 - ./zig-out/bin/zsmtp send --lmtp 127.0.0.1 2529 me@example.com \ 385 + ./zig-out/bin/zig-smtp send --lmtp 127.0.0.1 2529 me@example.com \ 386 386 good@example.net bad@example.net 387 387 388 388 # Request a delivery status notification (RFC 3461): 389 - zsmtp send --ret hdrs --envid 'batch 7' --notify success,failure \ 389 + zig-smtp send --ret hdrs --envid 'batch 7' --notify success,failure \ 390 390 --orcpt team@example.net 127.0.0.1 2525 me@example.com you@example.net 391 391 392 392 # Authenticate. Over a plaintext connection this refuses PLAIN and LOGIN 393 393 # rather than put the password on the wire; --allow-cleartext-auth overrides 394 394 # that for a connection protected by other means: 395 - zsmtp send --starttls --user me --password secret smtp.example.com 587 \ 395 + zig-smtp send --starttls --user me --password secret smtp.example.com 587 \ 396 396 me@example.com you@example.net 397 397 ``` 398 398 ··· 400 400 401 401 TLS is supported on both sides via 402 402 [ianic/tls.zig](https://github.com/ianic/tls.zig): the client does implicit 403 - TLS and STARTTLS via `zsmtp.Tls`, and the server accepts both STARTTLS and 403 + TLS and STARTTLS via `zig-smtp.Tls`, and the server accepts both STARTTLS and 404 404 implicit TLS (TLS 1.3 only). AUTH covers PLAIN, LOGIN, and CRAM-MD5 on the 405 405 client and PLAIN and LOGIN on the server. Message bodies can be streamed on 406 406 both sides, and the server validates MAIL and RCPT parameters (SIZE=, BODY=, ··· 419 419 ### Out of scope, not missing 420 420 421 421 - **Message composition.** No MIME builder, headers, attachments, transfer 422 - encodings, `Message-ID` or `Date` generation. zsmtp carries a message that 422 + encodings, `Message-ID` or `Date` generation. zig-smtp carries a message that 423 423 already exists; building one is RFC 5322's job and belongs in a library of 424 424 its own. 425 425 - **DSN report generation** ··· 647 647 648 648 ```sh 649 649 zig build -Dexim-client # fetches exim, installs zig-out/bin/exim-client 650 - ./zig-out/bin/zsmtp serve 2525 & 650 + ./zig-out/bin/zig-smtp serve 2525 & 651 651 ./zig-out/bin/exim-client 127.0.0.1 2525 < test/protocol-torture.script 652 652 ``` 653 653 ··· 684 684 `zig build test` suite. 685 685 686 686 Interoperability against third-party implementations is covered by a NixOS 687 - VM test (`nix/interop-test.nix`): the zsmtp client delivers mail to Postfix 687 + VM test (`nix/interop-test.nix`): the zig-smtp client delivers mail to Postfix 688 688 and Exim over plaintext, STARTTLS, and implicit TLS against each, and swaks 689 - delivers to the zsmtp server over plaintext and STARTTLS. 689 + delivers to the zig-smtp server over plaintext and STARTTLS. 690 690 691 691 ```sh 692 - nix build .#zsmtp # build the package 692 + nix build .#zig-smtp # build the package 693 693 nix build .#checks.x86_64-linux.interop # run the VM interop test 694 694 ```
+7 -7
build.zig
··· 44 44 .optimize = optimize, 45 45 }); 46 46 47 - const mod = b.addModule("zsmtp", .{ 47 + const mod = b.addModule("smtp", .{ 48 48 // The root source file is the "entry point" of this module. Users of 49 49 // this module will only be able to access public declarations contained 50 50 // in this file, which means that if you have declarations that you ··· 78 78 // If neither case applies to you, feel free to delete the declaration you 79 79 // don't need and to put everything under a single module. 80 80 const exe = b.addExecutable(.{ 81 - .name = "zsmtp", 81 + .name = "zig-smtp", 82 82 .root_module = b.createModule(.{ 83 83 // b.createModule defines a new module just like b.addModule but, 84 84 // unlike b.addModule, it does not expose the module to consumers of ··· 93 93 // List of modules available for import in source files part of the 94 94 // root module. 95 95 .imports = &.{ 96 - // Here "zsmtp" is the name you will use in your source code to 97 - // import this module (e.g. `@import("zsmtp")`). The name is 96 + // Here "smtp" is the name you will use in your source code to 97 + // import this module (e.g. `@import("smtp")`). The name is 98 98 // repeated because you are allowed to rename your imports, which 99 99 // can be extremely useful in case of collisions (which can happen 100 100 // importing modules from different packages). 101 - .{ .name = "zsmtp", .module = mod }, 101 + .{ .name = "smtp", .module = mod }, 102 102 }, 103 103 }), 104 104 }); ··· 136 136 } 137 137 138 138 const lib = b.addLibrary(.{ 139 - .name = "zsmtp", 139 + .name = "zig-smtp", 140 140 .root_module = mod, 141 141 }); 142 142 ··· 169 169 test_options.addOption(bool, "isemail_corpus", isemail_available); 170 170 mod.addOptions("build_options", test_options); 171 171 172 - // Exim's scriptable SMTP test client, handy for driving the zsmtp 172 + // Exim's scriptable SMTP test client, handy for driving the zig-smtp 173 173 // server through raw protocol dialogues with reply expectations (see 174 174 // test/protocol-torture.script). Guarded by an option so the lazy exim 175 175 // dependency is only fetched on demand:
+2 -2
build.zig.zon
··· 9 9 // 10 10 // It is redundant to include "zig" in this name because it is already 11 11 // within the Zig package namespace. 12 - .name = .zsmtp, 12 + .name = .zig_smtp, 13 13 // This is a [Semantic Version](https://semver.org/). 14 14 // In a future version of Zig it will be used for package deduplication. 15 15 .version = "0.0.0", ··· 25 25 // original project's identity. Thus it is recommended to leave the comment 26 26 // on the following line intact, so that it shows up in code reviews that 27 27 // modify the field. 28 - .fingerprint = 0x579395bcd3b65a42, // Changing this has security and trust implications. 28 + .fingerprint = 0xcbfda630785727a0, // Changing this has security and trust implications. 29 29 // Tracks the earliest Zig version that the package considers to be a 30 30 // supported use case. 31 31 .minimum_zig_version = "0.16.0",
+4 -4
flake.nix
··· 34 34 pkgs = makePackages system; 35 35 in 36 36 rec { 37 - zsmtp = pkgs.callPackage ./package.nix { }; 38 - default = zsmtp; 37 + zig-smtp = pkgs.callPackage ./package.nix { }; 38 + default = zig-smtp; 39 39 zig-deps = pkgs.callPackage ./build.zig.zon.nix { }; 40 40 } 41 41 ); ··· 46 46 pkgs = makePackages system; 47 47 in 48 48 { 49 - zsmtp = pkgs.callPackage ./package.nix { }; 49 + zig-smtp = pkgs.callPackage ./package.nix { }; 50 50 interop = pkgs.callPackage ./nix/interop-test.nix { }; 51 51 } 52 52 ); ··· 58 58 in 59 59 { 60 60 default = pkgs.mkShell { 61 - name = "zsmtp"; 61 + name = "zig-smtp"; 62 62 nativeBuildInputs = [ 63 63 pkgs.git-pages-cli 64 64 pkgs.kcov
+4 -4
package.nix
··· 13 13 zigDeps = callPackage ./build.zig.zon.nix { }; 14 14 in 15 15 stdenv.mkDerivation (finalAttrs: { 16 - pname = "zsmtp"; 16 + pname = "zig-smtp"; 17 17 # Keep in step with `.version` in build.zig.zon. 18 18 version = "0.0.0"; 19 19 ··· 51 51 meta = { 52 52 description = "SMTP client and server library for Zig"; 53 53 longDescription = '' 54 - zsmtp is an SMTP client and server library (RFC 5321) for Zig. Both 54 + zig-smtp is an SMTP client and server library (RFC 5321) for Zig. Both 55 55 sides run over plain std.Io.Reader/std.Io.Writer pairs, making them 56 56 transport-agnostic, and support STARTTLS and implicit TLS, AUTH, 57 57 streamed message bodies, and the SIZE, 8BITMIME, CHUNKING and 58 58 SMTPUTF8 extensions. A small demo CLI for sending and receiving is 59 59 included. 60 60 ''; 61 - homepage = "https://git.jcollie.dev/jeff/zsmtp"; 61 + homepage = "https://git.jcollie.dev/jeff/zig-smtp"; 62 62 license = lib.licenses.mit; 63 - mainProgram = "zsmtp"; 63 + mainProgram = "zig-smtp"; 64 64 platforms = lib.platforms.unix; 65 65 }; 66 66 })
+91 -91
nix/interop-test.nix
··· 1 1 # SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us> 2 2 # SPDX-License-Identifier: MIT 3 3 4 - # NixOS VM test exercising zsmtp against third-party implementations: 5 - # - zsmtp client -> Postfix (25/465) and Exim (2625/2626): plaintext, 4 + # NixOS VM test exercising zig-smtp against third-party implementations: 5 + # - zig-smtp client -> Postfix (25/465) and Exim (2625/2626): plaintext, 6 6 # STARTTLS, and implicit TLS against each, verified by checking local 7 7 # delivery to alice's mailbox 8 - # - swaks -> zsmtp server: plaintext and STARTTLS, verified by checking 8 + # - swaks -> zig-smtp server: plaintext and STARTTLS, verified by checking 9 9 # the received message in the server's journal 10 - # - exim -> zsmtp LMTP server (2529): a two-recipient delivery where the 10 + # - exim -> zig-smtp LMTP server (2529): a two-recipient delivery where the 11 11 # server accepts one mailbox and refuses the other, which is the thing 12 12 # LMTP exists to express and which exim has to read correctly 13 - # - zsmtp LMTP client -> dovecot LMTP (2024) 13 + # - zig-smtp LMTP client -> dovecot LMTP (2024) 14 14 15 15 { 16 16 testers, ··· 19 19 openssl, 20 20 }: 21 21 let 22 - zsmtp = callPackage ../package.nix { }; 22 + zig-smtp = callPackage ../package.nix { }; 23 23 24 24 snakeoil = 25 - runCommand "zsmtp-test-cert" 25 + runCommand "zig-smtp-test-cert" 26 26 { 27 27 nativeBuildInputs = [ openssl ]; 28 28 } ··· 35 35 ''; 36 36 in 37 37 testers.runNixOSTest { 38 - name = "zsmtp-interop"; 38 + name = "zig-smtp-interop"; 39 39 40 40 nodes.machine = 41 41 { pkgs, ... }: 42 42 { 43 43 environment.systemPackages = [ 44 - zsmtp 44 + zig-smtp 45 45 pkgs.swaks 46 46 pkgs.netcat 47 47 pkgs.python3 ··· 49 49 50 50 users.users.alice.isNormalUser = true; 51 51 52 - # A real LMTP server for the zsmtp LMTP client to deliver to. 52 + # A real LMTP server for the zig-smtp LMTP client to deliver to. 53 53 services.dovecot2 = { 54 54 enable = true; 55 55 # alice is a normal user declared above, not one for dovecot to make. ··· 128 128 129 129 begin routers 130 130 131 - # Everything for lmtp.test goes to the zsmtp LMTP server, which 131 + # Everything for lmtp.test goes to the zig-smtp LMTP server, which 132 132 # accepts one of the two mailboxes below and refuses the other. 133 133 # Listed first because the first matching router wins. 134 134 lmtp_route: ··· 189 189 "d /var/spool/dovecot-mail 0755 alice users -" 190 190 ]; 191 191 192 - systemd.services.zsmtp-server = { 193 - description = "zsmtp debug server (plaintext)"; 192 + systemd.services.zig-smtp-server = { 193 + description = "zig-smtp debug server (plaintext)"; 194 194 wantedBy = [ "multi-user.target" ]; 195 195 serviceConfig = { 196 - ExecStart = "${zsmtp}/bin/zsmtp serve 2525"; 196 + ExecStart = "${zig-smtp}/bin/zig-smtp serve 2525"; 197 197 DynamicUser = true; 198 198 }; 199 199 }; 200 200 201 - systemd.services.zsmtp-server-tls = { 202 - description = "zsmtp debug server (STARTTLS)"; 201 + systemd.services.zig-smtp-server-tls = { 202 + description = "zig-smtp debug server (STARTTLS)"; 203 203 wantedBy = [ "multi-user.target" ]; 204 204 serviceConfig = { 205 - ExecStart = "${zsmtp}/bin/zsmtp serve --tls-cert ${snakeoil}/cert.pem --tls-key ${snakeoil}/key.pem 2526"; 205 + ExecStart = "${zig-smtp}/bin/zig-smtp serve --tls-cert ${snakeoil}/cert.pem --tls-key ${snakeoil}/key.pem 2526"; 206 206 DynamicUser = true; 207 207 }; 208 208 }; 209 209 210 - systemd.services.zsmtp-server-tlsc = { 211 - description = "zsmtp debug server (implicit TLS)"; 210 + systemd.services.zig-smtp-server-tlsc = { 211 + description = "zig-smtp debug server (implicit TLS)"; 212 212 wantedBy = [ "multi-user.target" ]; 213 213 serviceConfig = { 214 - ExecStart = "${zsmtp}/bin/zsmtp serve --tls-cert ${snakeoil}/cert.pem --tls-key ${snakeoil}/key.pem --implicit-tls 2528"; 214 + ExecStart = "${zig-smtp}/bin/zig-smtp serve --tls-cert ${snakeoil}/cert.pem --tls-key ${snakeoil}/key.pem --implicit-tls 2528"; 215 215 DynamicUser = true; 216 216 }; 217 217 }; 218 218 219 - systemd.services.zsmtp-server-lmtp = { 220 - description = "zsmtp debug server (LMTP)"; 219 + systemd.services.zig-smtp-server-lmtp = { 220 + description = "zig-smtp debug server (LMTP)"; 221 221 wantedBy = [ "multi-user.target" ]; 222 222 serviceConfig = { 223 - ExecStart = "${zsmtp}/bin/zsmtp serve --lmtp --fail-delivery bad@lmtp.test 2529"; 223 + ExecStart = "${zig-smtp}/bin/zig-smtp serve --lmtp --fail-delivery bad@lmtp.test 2529"; 224 224 DynamicUser = true; 225 225 }; 226 226 }; 227 227 228 - systemd.services.zsmtp-server-auth = { 229 - description = "zsmtp debug server (authentication required)"; 228 + systemd.services.zig-smtp-server-auth = { 229 + description = "zig-smtp debug server (authentication required)"; 230 230 wantedBy = [ "multi-user.target" ]; 231 231 serviceConfig = { 232 - ExecStart = "${zsmtp}/bin/zsmtp serve --auth alice:secret 2527"; 232 + ExecStart = "${zig-smtp}/bin/zig-smtp serve --auth alice:secret 2527"; 233 233 DynamicUser = true; 234 234 }; 235 235 }; ··· 242 242 machine.wait_for_unit("exim.service") 243 243 machine.wait_for_open_port(2625) 244 244 machine.wait_for_open_port(2626) 245 - machine.wait_for_unit("zsmtp-server.service") 246 - machine.wait_for_unit("zsmtp-server-tls.service") 247 - machine.wait_for_unit("zsmtp-server-tlsc.service") 248 - machine.wait_for_unit("zsmtp-server-auth.service") 249 - machine.wait_for_unit("zsmtp-server-lmtp.service") 245 + machine.wait_for_unit("zig-smtp-server.service") 246 + machine.wait_for_unit("zig-smtp-server-tls.service") 247 + machine.wait_for_unit("zig-smtp-server-tlsc.service") 248 + machine.wait_for_unit("zig-smtp-server-auth.service") 249 + machine.wait_for_unit("zig-smtp-server-lmtp.service") 250 250 machine.wait_for_unit("dovecot.service") 251 251 machine.wait_for_open_port(2529) 252 252 machine.wait_for_open_port(2024) ··· 259 259 def deliver(flags, port, needle, mailbox): 260 260 machine.succeed( 261 261 f"printf 'Subject: interop\\r\\n\\r\\n{needle}\\r\\n'" 262 - f" | zsmtp send {flags} 127.0.0.1 {port}" 262 + f" | zig-smtp send {flags} 127.0.0.1 {port}" 263 263 " bob@example.com alice@localhost" 264 264 ) 265 265 machine.wait_until_succeeds(f"grep -r '{needle}' {mailbox}", timeout=60) ··· 271 271 } 272 272 273 273 for name, (port, tls_port, mailbox) in servers.items(): 274 - with subtest(f"zsmtp client to {name}, plaintext"): 275 - deliver("", port, f"zsmtp to {name} plain", mailbox) 274 + with subtest(f"zig-smtp client to {name}, plaintext"): 275 + deliver("", port, f"zig-smtp to {name} plain", mailbox) 276 276 277 - with subtest(f"zsmtp client to {name}, STARTTLS"): 277 + with subtest(f"zig-smtp client to {name}, STARTTLS"): 278 278 deliver( 279 - "--starttls --insecure", port, f"zsmtp to {name} starttls", mailbox 279 + "--starttls --insecure", port, f"zig-smtp to {name} starttls", mailbox 280 280 ) 281 281 282 - with subtest(f"zsmtp client to {name}, implicit TLS"): 283 - deliver("--tls --insecure", tls_port, f"zsmtp to {name} smtps", mailbox) 282 + with subtest(f"zig-smtp client to {name}, implicit TLS"): 283 + deliver("--tls --insecure", tls_port, f"zig-smtp to {name} smtps", mailbox) 284 284 285 285 for name, (port, tls_port, mailbox) in servers.items(): 286 - with subtest(f"zsmtp client to {name}, CHUNKING"): 287 - deliver("--chunking", port, f"zsmtp to {name} chunked", mailbox) 286 + with subtest(f"zig-smtp client to {name}, CHUNKING"): 287 + deliver("--chunking", port, f"zig-smtp to {name} chunked", mailbox) 288 288 289 289 # RFC 3461. Postfix advertises DSN out of the box; exim is told to above. 290 290 for name, (port, tls_port, mailbox) in servers.items(): 291 - with subtest(f"zsmtp client to {name}, DSN parameters"): 291 + with subtest(f"zig-smtp client to {name}, DSN parameters"): 292 292 deliver( 293 293 "--ret hdrs --envid batch7 --notify success,failure" 294 294 " --orcpt team@example.net", 295 295 port, 296 - f"zsmtp to {name} dsn", 296 + f"zig-smtp to {name} dsn", 297 297 mailbox, 298 298 ) 299 299 300 - with subtest("zsmtp client to zsmtp server, DSN parameters round trip"): 300 + with subtest("zig-smtp client to zig-smtp server, DSN parameters round trip"): 301 301 machine.succeed( 302 - "printf 'Subject: interop\\r\\n\\r\\nzsmtp dsn round trip\\r\\n'" 303 - " | zsmtp send --ret full --envid 'batch 7'" 302 + "printf 'Subject: interop\\r\\n\\r\\nzig-smtp dsn round trip\\r\\n'" 303 + " | zig-smtp send --ret full --envid 'batch 7'" 304 304 " --notify success,delay --orcpt 'team+list@example.net'" 305 305 " 127.0.0.1 2525 bob@example.com alice@example.net" 306 306 ) 307 307 # The server prints what it parsed: the ENVID comes back with its 308 308 # space, and the ORCPT with the '+' that had to be xtext-encoded. 309 309 machine.wait_until_succeeds( 310 - "journalctl -u zsmtp-server | grep -F" 310 + "journalctl -u zig-smtp-server | grep -F" 311 311 " 'NOTIFY=SUCCESS,DELAY ORCPT=rfc822;team+2Blist@example.net" 312 312 " RET=FULL ENVID=batch 7'", 313 313 timeout=60, ··· 315 315 316 316 # RFC 2033. The point of LMTP is a separate verdict per mailbox, so the 317 317 # cases that matter are the ones where those verdicts differ. 318 - with subtest("exim to zsmtp LMTP server, one mailbox accepted and one refused"): 318 + with subtest("exim to zig-smtp LMTP server, one mailbox accepted and one refused"): 319 319 machine.succeed( 320 320 "swaks --server 127.0.0.1:2625 --from bob@example.com" 321 321 # One --to with both, since a second --to replaces the first. 322 322 " --to good@lmtp.test,bad@lmtp.test" 323 - " --header 'Subject: lmtp' --body 'exim to zsmtp lmtp'" 323 + " --header 'Subject: lmtp' --body 'exim to zig-smtp lmtp'" 324 324 ) 325 325 # Both recipients in one transaction, which is what makes the two 326 326 # differing verdicts possible. 327 327 machine.wait_until_succeeds( 328 - "journalctl -u zsmtp-server-lmtp | grep -F" 328 + "journalctl -u zig-smtp-server-lmtp | grep -F" 329 329 " '<good@lmtp.test> <bad@lmtp.test>'", 330 330 timeout=60, 331 331 ) ··· 340 340 ) 341 341 machine.succeed("journalctl -u exim | grep -F 'Mailbox disabled'") 342 342 343 - with subtest("zsmtp LMTP client to dovecot"): 343 + with subtest("zig-smtp LMTP client to dovecot"): 344 344 machine.succeed( 345 - "printf 'Subject: interop\\r\\n\\r\\nzsmtp to dovecot lmtp\\r\\n'" 346 - " | zsmtp send --lmtp 127.0.0.1 2024 bob@example.com alice@localhost" 345 + "printf 'Subject: interop\\r\\n\\r\\nzig-smtp to dovecot lmtp\\r\\n'" 346 + " | zig-smtp send --lmtp 127.0.0.1 2024 bob@example.com alice@localhost" 347 347 ) 348 348 machine.wait_until_succeeds( 349 - "grep -r 'zsmtp to dovecot lmtp' /var/spool/dovecot-mail/alice/", timeout=60 349 + "grep -r 'zig-smtp to dovecot lmtp' /var/spool/dovecot-mail/alice/", timeout=60 350 350 ) 351 351 352 - with subtest("zsmtp LMTP client reports dovecot's refusal of one recipient"): 352 + with subtest("zig-smtp LMTP client reports dovecot's refusal of one recipient"): 353 353 # Two recipients, one of whom does not exist: dovecot accepts the 354 354 # RCPT for alice and refuses nosuchuser outright, so this fails at 355 355 # RCPT rather than at the end of data -- still per-recipient, and 356 356 # still the client's job to report which. 357 357 status, output = machine.execute( 358 358 "printf 'Subject: interop\\r\\n\\r\\nnope\\r\\n'" 359 - " | zsmtp send --lmtp 127.0.0.1 2024 bob@example.com nosuchuser@localhost 2>&1" 359 + " | zig-smtp send --lmtp 127.0.0.1 2024 bob@example.com nosuchuser@localhost 2>&1" 360 360 ) 361 361 assert status != 0, f"expected a failure, got: {output}" 362 362 ··· 365 365 "printf 'EHLO x\\r\\nQUIT\\r\\n' | timeout 5 nc 127.0.0.1 2024" 366 366 ) 367 367 assert "250-" not in output, f"dovecot answered EHLO positively: {output}" 368 - # And zsmtp's own LMTP server says the same thing. 368 + # And zig-smtp's own LMTP server says the same thing. 369 369 status, output = machine.execute( 370 370 "printf 'EHLO x\\r\\nQUIT\\r\\n' | timeout 5 nc 127.0.0.1 2529" 371 371 ) ··· 374 374 # RFC 3030 BINARYMIME. Neither postfix nor exim offers it, which is 375 375 # what makes them the test of the refusal: RFC 3030 is absolute that 376 376 # binary must not be sent to a server that did not advertise it. 377 - with subtest("zsmtp refuses to send binary to a server that lacks BINARYMIME"): 377 + with subtest("zig-smtp refuses to send binary to a server that lacks BINARYMIME"): 378 378 for name, (port, _tls, _mailbox) in servers.items(): 379 379 status, output = machine.execute( 380 - f"printf 'x' | zsmtp send --binarymime 127.0.0.1 {port}" 380 + f"printf 'x' | zig-smtp send --binarymime 127.0.0.1 {port}" 381 381 " bob@example.com alice@localhost 2>&1" 382 382 ) 383 383 assert status != 0, f"{name} was sent binary anyway: {output}" 384 384 assert "BINARYMIME" in output, f"{name}: unexpected failure: {output}" 385 385 386 - with subtest("zsmtp to zsmtp, every octet survives BINARYMIME"): 386 + with subtest("zig-smtp to zig-smtp, every octet survives BINARYMIME"): 387 387 # All 256 byte values plus a lone dot line and a bare CR: content 388 388 # that DATA could not carry at all, and that must arrive unchanged. 389 389 machine.succeed( ··· 392 392 " > /tmp/binary.in" 393 393 ) 394 394 machine.succeed( 395 - "zsmtp send --binarymime 127.0.0.1 2525 bob@example.com alice@example.net" 395 + "zig-smtp send --binarymime 127.0.0.1 2525 bob@example.com alice@example.net" 396 396 " < /tmp/binary.in" 397 397 ) 398 398 machine.wait_until_succeeds( 399 - "journalctl -u zsmtp-server | grep -F 'to <alice@example.net> (266 bytes)'", 399 + "journalctl -u zig-smtp-server | grep -F 'to <alice@example.net> (266 bytes)'", 400 400 timeout=60, 401 401 ) 402 402 403 - with subtest("zsmtp client to postfix, SMTPUTF8"): 403 + with subtest("zig-smtp client to postfix, SMTPUTF8"): 404 404 machine.succeed( 405 - "printf 'Subject: interop\\r\\n\\r\\nzsmtp to postfix utf8\\r\\n'" 406 - " | zsmtp send --smtputf8 127.0.0.1 25" 405 + "printf 'Subject: interop\\r\\n\\r\\nzig-smtp to postfix utf8\\r\\n'" 406 + " | zig-smtp send --smtputf8 127.0.0.1 25" 407 407 " 'böb@example.com' alice@localhost" 408 408 ) 409 409 machine.wait_until_succeeds( 410 - "grep -r 'zsmtp to postfix utf8' /var/spool/mail/alice/", timeout=60 410 + "grep -r 'zig-smtp to postfix utf8' /var/spool/mail/alice/", timeout=60 411 411 ) 412 412 413 413 # Exim advertises only PLAIN and LOGIN, both of which put the password 414 414 # on the wire, so the client refuses them over this plaintext loopback 415 415 # connection unless it is told to allow it. 416 - with subtest("zsmtp client to exim, AUTH PLAIN"): 416 + with subtest("zig-smtp client to exim, AUTH PLAIN"): 417 417 deliver( 418 418 "--allow-cleartext-auth --user alice --password secret --auth-method plain", 419 419 2625, 420 - "zsmtp to exim auth plain", 420 + "zig-smtp to exim auth plain", 421 421 "/var/spool/exim-mail/alice", 422 422 ) 423 423 424 - with subtest("zsmtp client to exim, AUTH LOGIN"): 424 + with subtest("zig-smtp client to exim, AUTH LOGIN"): 425 425 deliver( 426 426 "--allow-cleartext-auth --user alice --password secret --auth-method login", 427 427 2625, 428 - "zsmtp to exim auth login", 428 + "zig-smtp to exim auth login", 429 429 "/var/spool/exim-mail/alice", 430 430 ) 431 431 432 - with subtest("zsmtp client to exim, wrong password is rejected"): 432 + with subtest("zig-smtp client to exim, wrong password is rejected"): 433 433 machine.fail( 434 434 "printf 'Subject: interop\\r\\n\\r\\nnope\\r\\n'" 435 - " | zsmtp send --allow-cleartext-auth --user alice --password wrong" 435 + " | zig-smtp send --allow-cleartext-auth --user alice --password wrong" 436 436 " 127.0.0.1 2625 bob@example.com alice@localhost" 437 437 ) 438 438 439 439 # The refusal itself, which is what stops a password reaching the network 440 440 # by accident: the same delivery without the opt-in must not go through. 441 - with subtest("zsmtp client refuses cleartext AUTH without the opt-in"): 441 + with subtest("zig-smtp client refuses cleartext AUTH without the opt-in"): 442 442 machine.fail( 443 443 "printf 'Subject: interop\\r\\n\\r\\nnope\\r\\n'" 444 - " | zsmtp send --user alice --password secret 127.0.0.1 2625" 444 + " | zig-smtp send --user alice --password secret 127.0.0.1 2625" 445 445 " bob@example.com alice@localhost" 446 446 ) 447 447 # ...and over STARTTLS it goes through with no opt-in at all. 448 448 deliver( 449 449 "--starttls --insecure --user alice --password secret", 450 450 2625, 451 - "zsmtp to exim auth over starttls", 451 + "zig-smtp to exim auth over starttls", 452 452 "/var/spool/exim-mail/alice", 453 453 ) 454 454 455 - with subtest("swaks to zsmtp server, AUTH PLAIN"): 455 + with subtest("swaks to zig-smtp server, AUTH PLAIN"): 456 456 machine.succeed( 457 457 "swaks --server 127.0.0.1:2527 --auth PLAIN --auth-user alice" 458 458 " --auth-password secret --from bob@example.com" 459 - " --to alice@example.net --body 'swaks to zsmtp auth plain'" 459 + " --to alice@example.net --body 'swaks to zig-smtp auth plain'" 460 460 ) 461 461 machine.wait_until_succeeds( 462 - "journalctl -u zsmtp-server-auth | grep 'swaks to zsmtp auth plain'", 462 + "journalctl -u zig-smtp-server-auth | grep 'swaks to zig-smtp auth plain'", 463 463 timeout=60, 464 464 ) 465 465 466 - with subtest("swaks to zsmtp server, AUTH LOGIN"): 466 + with subtest("swaks to zig-smtp server, AUTH LOGIN"): 467 467 machine.succeed( 468 468 "swaks --server 127.0.0.1:2527 --auth LOGIN --auth-user alice" 469 469 " --auth-password secret --from bob@example.com" 470 - " --to alice@example.net --body 'swaks to zsmtp auth login'" 470 + " --to alice@example.net --body 'swaks to zig-smtp auth login'" 471 471 ) 472 472 machine.wait_until_succeeds( 473 - "journalctl -u zsmtp-server-auth | grep 'swaks to zsmtp auth login'", 473 + "journalctl -u zig-smtp-server-auth | grep 'swaks to zig-smtp auth login'", 474 474 timeout=60, 475 475 ) 476 476 477 - with subtest("swaks to zsmtp server, wrong password is rejected"): 477 + with subtest("swaks to zig-smtp server, wrong password is rejected"): 478 478 machine.fail( 479 479 "swaks --server 127.0.0.1:2527 --auth PLAIN --auth-user alice" 480 480 " --auth-password wrong --from bob@example.com" ··· 484 484 with subtest("unauthenticated mail to auth-required server is rejected"): 485 485 machine.fail( 486 486 "printf 'Subject: interop\\r\\n\\r\\nnope\\r\\n'" 487 - " | zsmtp send 127.0.0.1 2527 bob@example.com alice@example.net" 487 + " | zig-smtp send 127.0.0.1 2527 bob@example.com alice@example.net" 488 488 ) 489 489 490 - with subtest("swaks to zsmtp server, plaintext"): 490 + with subtest("swaks to zig-smtp server, plaintext"): 491 491 machine.succeed( 492 492 "swaks --server 127.0.0.1:2525 --from bob@example.com" 493 493 " --to alice@example.net --header 'Subject: swaks plain'" 494 - " --body 'swaks to zsmtp plain'" 494 + " --body 'swaks to zig-smtp plain'" 495 495 ) 496 496 machine.wait_until_succeeds( 497 - "journalctl -u zsmtp-server | grep 'swaks to zsmtp plain'", timeout=60 497 + "journalctl -u zig-smtp-server | grep 'swaks to zig-smtp plain'", timeout=60 498 498 ) 499 499 500 - with subtest("swaks to zsmtp server, implicit TLS"): 500 + with subtest("swaks to zig-smtp server, implicit TLS"): 501 501 machine.succeed( 502 502 "swaks --tlsc --server 127.0.0.1:2528 --from bob@example.com" 503 503 " --to alice@example.net --header 'Subject: swaks tlsc'" 504 - " --body 'swaks to zsmtp implicit tls'" 504 + " --body 'swaks to zig-smtp implicit tls'" 505 505 ) 506 506 machine.wait_until_succeeds( 507 - "journalctl -u zsmtp-server-tlsc | grep 'swaks to zsmtp implicit tls'", 507 + "journalctl -u zig-smtp-server-tlsc | grep 'swaks to zig-smtp implicit tls'", 508 508 timeout=60, 509 509 ) 510 510 511 - with subtest("swaks to zsmtp server, STARTTLS"): 511 + with subtest("swaks to zig-smtp server, STARTTLS"): 512 512 machine.succeed( 513 513 "swaks --tls --server 127.0.0.1:2526 --from bob@example.com" 514 514 " --to alice@example.net --header 'Subject: swaks starttls'" 515 - " --body 'swaks to zsmtp starttls'" 515 + " --body 'swaks to zig-smtp starttls'" 516 516 ) 517 517 machine.wait_until_succeeds( 518 - "journalctl -u zsmtp-server-tls | grep 'swaks to zsmtp starttls'", timeout=60 518 + "journalctl -u zig-smtp-server-tls | grep 'swaks to zig-smtp starttls'", timeout=60 519 519 ) 520 520 ''; 521 521 }
+34 -34
src/main.zig
··· 1 1 // SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us> 2 2 // SPDX-License-Identifier: MIT 3 3 4 - //! Demo CLI for the zsmtp library. 4 + //! Demo CLI for the zig-smtp library. 5 5 //! 6 - //! zsmtp send [--tls|--starttls] [--insecure] [--allow-cleartext-auth] 6 + //! zig-smtp send [--tls|--starttls] [--insecure] [--allow-cleartext-auth] 7 7 //! [--chunking] [--smtputf8] [--user <u> --password <p>] 8 8 //! [--auth-method plain|login|cram-md5] 9 9 //! [--ret full|hdrs] [--envid <id>] ··· 18 18 //! options (RFC 3461) are --ret and --envid on the message and 19 19 //! --notify and --orcpt on every recipient; --binarymime sends the 20 20 //! input as BODY=BINARYMIME over BDAT, byte for byte 21 - //! zsmtp serve [--tls-cert <pem> --tls-key <pem> [--implicit-tls]] 21 + //! zig-smtp serve [--tls-cert <pem> --tls-key <pem> [--implicit-tls]] 22 22 //! [--auth <user>:<pass>] [--lmtp [--fail-delivery <address>]] 23 23 //! <port> 24 24 //! run a debug server on 127.0.0.1 that prints received messages; ··· 30 30 31 31 const std = @import("std"); 32 32 const Io = std.Io; 33 - const zsmtp = @import("zsmtp"); 33 + const smtp = @import("smtp"); 34 34 35 35 pub fn main(init: std.process.Init) !void { 36 36 const arena = init.arena.allocator(); ··· 61 61 config.body = .binary_mime; 62 62 config.chunking = true; 63 63 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--ret")) { 64 - config.ret = zsmtp.protocol.Ret.parse(rest[1]) catch return usage(); 64 + config.ret = smtp.protocol.Ret.parse(rest[1]) catch return usage(); 65 65 rest = rest[1..]; 66 66 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--envid")) { 67 67 config.envid = rest[1]; 68 68 rest = rest[1..]; 69 69 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--notify")) { 70 - config.notify = zsmtp.protocol.Notify.parse(rest[1]) catch return usage(); 70 + config.notify = smtp.protocol.Notify.parse(rest[1]) catch return usage(); 71 71 rest = rest[1..]; 72 72 } else if (rest.len >= 2 and std.mem.eql(u8, rest[0], "--orcpt")) { 73 73 // Applied to every recipient, which is all a one-shot ··· 135 135 cert_path: ?[]const u8 = null, 136 136 key_path: ?[]const u8 = null, 137 137 implicit_tls: bool = false, 138 - protocol: zsmtp.Server.Protocol = .smtp, 138 + protocol: smtp.Server.Protocol = .smtp, 139 139 /// Accepted at RCPT time and then failed at the end of the message, 140 140 /// which only LMTP can say. 141 141 fail_delivery: ?[]const u8 = null, ··· 149 149 allow_cleartext_auth: bool = false, 150 150 chunking: bool = false, 151 151 smtputf8: bool = false, 152 - protocol: zsmtp.Client.Protocol = .smtp, 153 - body: ?zsmtp.protocol.Body = null, 154 - ret: ?zsmtp.protocol.Ret = null, 152 + protocol: smtp.Client.Protocol = .smtp, 153 + body: ?smtp.protocol.Body = null, 154 + ret: ?smtp.protocol.Ret = null, 155 155 envid: ?[]const u8 = null, 156 - notify: ?zsmtp.protocol.Notify = null, 156 + notify: ?smtp.protocol.Notify = null, 157 157 orcpt: ?[]const u8 = null, 158 158 username: ?[]const u8 = null, 159 159 password: ?[]const u8 = null, ··· 163 163 fn usage() noreturn { 164 164 std.log.err( 165 165 \\usage: 166 - \\ zsmtp send [--tls|--starttls] [--insecure] [--allow-cleartext-auth] 166 + \\ zig-smtp send [--tls|--starttls] [--insecure] [--allow-cleartext-auth] 167 167 \\ [--user <u> --password <p>] 168 168 \\ [--auth-method plain|login|cram-md5] 169 169 \\ [--ret full|hdrs] [--envid <id>] 170 170 \\ [--notify never|success,failure,delay] [--orcpt <address>] 171 171 \\ [--lmtp] [--binarymime] <host> <port> <from> <to>... 172 172 \\ (message is read from stdin) 173 - \\ zsmtp serve [--tls-cert <pem> --tls-key <pem> [--implicit-tls]] 173 + \\ zig-smtp serve [--tls-cert <pem> --tls-key <pem> [--implicit-tls]] 174 174 \\ [--auth <user>:<pass>] [--lmtp [--fail-delivery <address>]] 175 175 \\ <port> 176 176 , .{}); ··· 195 195 const stream = try host.connect(io, port, .{ .mode = .stream }); 196 196 defer stream.close(io); 197 197 // The TLS layer requires stream buffers of at least min_buffer_len. 198 - const read_buf = try arena.alloc(u8, zsmtp.Tls.min_buffer_len); 199 - const write_buf = try arena.alloc(u8, zsmtp.Tls.min_buffer_len); 198 + const read_buf = try arena.alloc(u8, smtp.Tls.min_buffer_len); 199 + const write_buf = try arena.alloc(u8, smtp.Tls.min_buffer_len); 200 200 var stream_reader = stream.reader(io, read_buf); 201 201 var stream_writer = stream.writer(io, write_buf); 202 202 203 - const tls_options: zsmtp.Tls.Options = .{ 203 + const tls_options: smtp.Tls.Options = .{ 204 204 .host = host_arg, 205 205 .ca = if (config.insecure) .insecure else .system, 206 206 }; 207 - var tls: zsmtp.Tls = undefined; 207 + var tls: smtp.Tls = undefined; 208 208 var tls_active = false; 209 209 defer if (tls_active) { 210 210 tls.end() catch {}; ··· 212 212 }; 213 213 214 214 var reply_buf: [1024]u8 = undefined; 215 - var client: zsmtp.Client = .init(&stream_reader.interface, &stream_writer.interface, &reply_buf); 215 + var client: smtp.Client = .init(&stream_reader.interface, &stream_writer.interface, &reply_buf); 216 216 client.allow_cleartext_auth = config.allow_cleartext_auth; 217 217 client.mode = config.protocol; 218 218 ··· 237 237 const password = config.password.?; 238 238 // The mechanisms come from zig-sasl; what is chosen from them is the 239 239 // caller's business, and this one lets --auth-method force it. 240 - var plain: zsmtp.sasl.Plain = .init(username, password); 241 - var login: zsmtp.sasl.Login = .init(username, password); 242 - var cram_md5: zsmtp.sasl.CramMd5 = .init(username, password); 243 - const offered: []const zsmtp.sasl.Client = switch (config.auth_method) { 240 + var plain: smtp.sasl.Plain = .init(username, password); 241 + var login: smtp.sasl.Login = .init(username, password); 242 + var cram_md5: smtp.sasl.CramMd5 = .init(username, password); 243 + const offered: []const smtp.sasl.Client = switch (config.auth_method) { 244 244 // In order of preference, which `selectFromList` reads as such: 245 245 // PLAIN because every server implements it correctly, CRAM-MD5 246 246 // last because it is the oldest. On a carrier with no encryption ··· 250 250 .login => &.{login.client()}, 251 251 .cram_md5 => &.{cram_md5.client()}, 252 252 }; 253 - const mechanism = zsmtp.sasl.Client.selectFromList( 253 + const mechanism = smtp.sasl.Client.selectFromList( 254 254 offered, 255 255 extensions.auth, 256 256 client.security == .encrypted or client.allow_cleartext_auth, ··· 328 328 /// Runs the mail transaction, streaming the message from `message` so 329 329 /// arbitrarily large input never has to fit in memory. 330 330 fn transact( 331 - client: *zsmtp.Client, 331 + client: *smtp.Client, 332 332 config: SendConfig, 333 333 from: []const u8, 334 334 recipients: []const []const u8, 335 335 message: *Io.Reader, 336 - ) (zsmtp.Client.Error || zsmtp.Client.ArgumentError)!void { 336 + ) (smtp.Client.Error || smtp.Client.ArgumentError)!void { 337 337 try client.mail(from, .{ 338 338 .smtputf8 = config.smtputf8, 339 339 .body = config.body, ··· 398 398 var listener = try address.listen(io, .{}); 399 399 defer listener.deinit(io); 400 400 401 - var auth: ?zsmtp.tls.config.CertKeyPair = if (config.cert_path) |cert_path| 401 + var auth: ?smtp.tls.config.CertKeyPair = if (config.cert_path) |cert_path| 402 402 try .fromFilePath(gpa, io, .cwd(), cert_path, config.key_path.?) 403 403 else 404 404 null; 405 - const tls_options: ?zsmtp.Server.TlsOptions = if (auth) |*a| .{ 405 + const tls_options: ?smtp.Server.TlsOptions = if (auth) |*a| .{ 406 406 .io = io, 407 407 .auth = a, 408 408 .mode = if (config.implicit_tls) .implicit else .starttls, ··· 428 428 const stream = try listener.accept(io); 429 429 defer stream.close(io); 430 430 // Sized for the TLS handshake, which runs over the raw stream. 431 - const read_buf = try gpa.alloc(u8, zsmtp.tls.input_buffer_len); 431 + const read_buf = try gpa.alloc(u8, smtp.tls.input_buffer_len); 432 432 defer gpa.free(read_buf); 433 - const write_buf = try gpa.alloc(u8, zsmtp.tls.output_buffer_len); 433 + const write_buf = try gpa.alloc(u8, smtp.tls.output_buffer_len); 434 434 defer gpa.free(write_buf); 435 435 var stream_reader = stream.reader(io, read_buf); 436 436 var stream_writer = stream.writer(io, write_buf); 437 - var session: zsmtp.Server = .init( 437 + var session: smtp.Server = .init( 438 438 &stream_reader.interface, 439 439 &stream_writer.interface, 440 440 .{ .context = &printer, .vtable = if (config.username != null) &.{ ··· 470 470 std.mem.eql(u8, password, printer.password.?); 471 471 } 472 472 473 - fn onMessage(context: ?*anyopaque, envelope: zsmtp.Server.Envelope, data: []const u8) zsmtp.Server.Decision { 473 + fn onMessage(context: ?*anyopaque, envelope: smtp.Server.Envelope, data: []const u8) smtp.Server.Decision { 474 474 const printer: *MessagePrinter = @ptrCast(@alignCast(context.?)); 475 475 printer.print(envelope, data) catch 476 476 return .{ .reject = .{ .code = 451, .text = "4.3.0 Local error" } }; ··· 483 483 /// express for one recipient out of several. 484 484 fn onRecipientResult( 485 485 context: ?*anyopaque, 486 - envelope: zsmtp.Server.Envelope, 486 + envelope: smtp.Server.Envelope, 487 487 index: usize, 488 - ) zsmtp.Server.Decision { 488 + ) smtp.Server.Decision { 489 489 const printer: *MessagePrinter = @ptrCast(@alignCast(context.?)); 490 490 const failing = printer.fail_delivery orelse return .accept; 491 491 if (std.mem.eql(u8, envelope.recipients[index].address, failing)) ··· 493 493 return .accept; 494 494 } 495 495 496 - fn print(printer: *MessagePrinter, envelope: zsmtp.Server.Envelope, data: []const u8) !void { 496 + fn print(printer: *MessagePrinter, envelope: smtp.Server.Envelope, data: []const u8) !void { 497 497 try printer.out.print("--- message from <{s}> to", .{envelope.from}); 498 498 for (envelope.recipients) |recipient| { 499 499 try printer.out.print(" <{s}>", .{recipient.address});
+3 -3
src/root.zig
··· 1 1 // SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us> 2 2 // SPDX-License-Identifier: MIT 3 3 4 - //! zsmtp — an SMTP client and server library for Zig 4 + //! zig-smtp — an SMTP client and server library for Zig 5 5 //! ([RFC 5321](https://datatracker.ietf.org/doc/html/rfc5321)). 6 6 //! 7 7 //! Both `Client` and `Server` run over plain `std.Io.Reader`/`std.Io.Writer` ··· 18 18 pub const Server = @import("Server.zig"); 19 19 pub const Tls = @import("Tls.zig"); 20 20 /// Re-export of the ianic/tls.zig library used for server-side STARTTLS, 21 - /// e.g. `zsmtp.tls.config.CertKeyPair` for loading the server certificate. 21 + /// e.g. `smtp.tls.config.CertKeyPair` for loading the server certificate. 22 22 pub const tls = @import("tls"); 23 23 /// Re-export of [zig-sasl](https://git.jcollie.dev/jeff/zig-sasl), which is 24 - /// where the AUTH mechanisms live: `zsmtp.sasl.Plain`, `sasl.CramMd5`, 24 + /// where the AUTH mechanisms live: `smtp.sasl.Plain`, `sasl.CramMd5`, 25 25 /// `sasl.XOAuth2` and the rest, for handing to `Client.authenticate`. They 26 26 /// are not here because nothing about them is specific to SMTP — POP3 and 27 27 /// IMAP want the same ones.
+1 -1
.forgejo/workflows/publish.yaml
··· 21 21 run: nix develop -c zig build docs --system "$(nix build --print-out-paths .#zig-deps)" 22 22 23 23 - name: Publish docs 24 - run: nix develop -c git-pages-cli https://jeff.jcollie.page/zsmtp/ --server jcollie.page --token ${{ forge.token }} --upload-dir zig-out/docs 24 + run: nix develop -c git-pages-cli https://jeff.jcollie.page/zig-smtp/ --server jcollie.page --token ${{ forge.token }} --upload-dir zig-out/docs