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.

zsmtp#

An SMTP client and server library for Zig (RFC 5321).

Both the client and the server run over plain std.Io.Reader/std.Io.Writer pairs, so they are transport-agnostic: wrap a TCP stream for real use, or fixed in-memory buffers in tests. Requires Zig 0.16.

Client#

const zsmtp = @import("zsmtp");

var reply_buf: [1024]u8 = undefined;
var client: zsmtp.Client = .init(&stream_reader.interface, &stream_writer.interface, &reply_buf);

_ = try client.greet();                        // read the 220 greeting
_ = try client.hello("my-host.example.com");   // EHLO (HELO fallback), returns extensions
try client.sendMail("me@example.com", &.{"you@example.net"}, message);
try client.quit();

Line endings in the message are normalized to CRLF and leading dots are stuffed automatically. On error.UnexpectedReply, client.last_reply holds the server's actual code and text. mailFrom/rcptTo/sendMessage are also available individually, as is authPlain.

Server#

var session: zsmtp.Server = .init(&stream_reader.interface, &stream_writer.interface, .{
    .context = &my_state,
    .vtable = &.{
        .rcptTo = onRcptTo,     // optional; accept/reject each recipient
        .message = onMessage,   // required; receives envelope + message data
    },
}, .{ .hostname = "mx.example.com" });
try session.run(gpa);

run serves one connection until QUIT or disconnect, enforcing command sequencing, recipient and message-size limits, and un-stuffing message data. Listening, accepting, and concurrency are up to the caller.

Demo CLI#

zig build

# Debug server that prints received messages to stdout:
./zig-out/bin/zsmtp serve 2525

# Send a message read from stdin:
printf 'Subject: hi\r\n\r\nhello\r\n' | \
    ./zig-out/bin/zsmtp send 127.0.0.1 2525 me@example.com you@example.net

Status#

Plaintext SMTP only for now — STARTTLS/implicit TLS is the next planned step (the transport-agnostic design is meant to make that a drop-in layer). Not yet implemented: TLS, streaming (non-slice) message bodies, AUTH beyond PLAIN, and ESMTP parameter handling (SIZE=, BODY=) on the server side.

Tests#

zig build test