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//! Demo CLI for the zsmtp library.
5//!
6//! zsmtp send <host> <port> <from> <to>... send a message read from stdin
7//! zsmtp serve <port> run a debug server on 127.0.0.1
8//! that prints received messages
9
10const std = @import("std");
11const Io = std.Io;
12const zsmtp = @import("zsmtp");
13
14pub fn main(init: std.process.Init) !void {
15 const arena = init.arena.allocator();
16 const io = init.io;
17 const args = try init.minimal.args.toSlice(arena);
18
19 if (args.len >= 6 and std.mem.eql(u8, args[1], "send")) {
20 return send(io, arena, args[2], args[3], args[4], args[5..]);
21 }
22 if (args.len == 3 and std.mem.eql(u8, args[1], "serve")) {
23 return serve(io, arena, args[2]);
24 }
25 std.log.err(
26 \\usage:
27 \\ zsmtp send <host> <port> <from> <to>... (message is read from stdin)
28 \\ zsmtp serve <port>
29 , .{});
30 std.process.exit(1);
31}
32
33fn send(
34 io: Io,
35 arena: std.mem.Allocator,
36 host_arg: []const u8,
37 port_arg: []const u8,
38 from: []const u8,
39 recipients: []const []const u8,
40) !void {
41 const host = try Io.net.HostName.init(host_arg);
42 const port = try std.fmt.parseInt(u16, port_arg, 10);
43
44 var stdin_buf: [4096]u8 = undefined;
45 var stdin: Io.File.Reader = .init(.stdin(), io, &stdin_buf);
46 const message = try stdin.interface.allocRemaining(arena, .unlimited);
47
48 const stream = try host.connect(io, port, .{ .mode = .stream });
49 defer stream.close(io);
50 var read_buf: [4096]u8 = undefined;
51 var write_buf: [4096]u8 = undefined;
52 var stream_reader = stream.reader(io, &read_buf);
53 var stream_writer = stream.writer(io, &write_buf);
54
55 var reply_buf: [1024]u8 = undefined;
56 var client: zsmtp.Client = .init(&stream_reader.interface, &stream_writer.interface, &reply_buf);
57
58 _ = try client.greet();
59 _ = try client.hello("localhost");
60 client.sendMail(from, recipients, message) catch |err| {
61 if (err == error.UnexpectedReply) {
62 const reply = client.last_reply.?;
63 std.log.err("server rejected: {d} {s}", .{ reply.code, reply.text });
64 }
65 return err;
66 };
67 try client.quit();
68 std.log.info("message sent to {d} recipient(s)", .{recipients.len});
69}
70
71fn serve(io: Io, gpa: std.mem.Allocator, port_arg: []const u8) !void {
72 const port = try std.fmt.parseInt(u16, port_arg, 10);
73 const address: Io.net.IpAddress = .{ .ip4 = .loopback(port) };
74 var listener = try address.listen(io, .{});
75 defer listener.deinit(io);
76 std.log.info("listening on 127.0.0.1:{d}", .{port});
77
78 var stdout_buf: [4096]u8 = undefined;
79 var stdout: Io.File.Writer = .init(.stdout(), io, &stdout_buf);
80
81 var printer: MessagePrinter = .{ .out = &stdout.interface };
82 while (true) {
83 const stream = try listener.accept(io);
84 defer stream.close(io);
85 var read_buf: [4096]u8 = undefined;
86 var write_buf: [4096]u8 = undefined;
87 var stream_reader = stream.reader(io, &read_buf);
88 var stream_writer = stream.writer(io, &write_buf);
89 var session: zsmtp.Server = .init(
90 &stream_reader.interface,
91 &stream_writer.interface,
92 .{ .context = &printer, .vtable = &.{ .message = MessagePrinter.onMessage } },
93 .{ .hostname = "localhost" },
94 );
95 session.run(gpa) catch |err| {
96 std.log.warn("session ended with error: {t}", .{err});
97 };
98 }
99}
100
101const MessagePrinter = struct {
102 out: *Io.Writer,
103
104 fn onMessage(context: ?*anyopaque, envelope: zsmtp.Server.Envelope, data: []const u8) zsmtp.Server.Decision {
105 const printer: *MessagePrinter = @ptrCast(@alignCast(context.?));
106 printer.print(envelope, data) catch
107 return .{ .reject = .{ .code = 451, .text = "4.3.0 Local error" } };
108 return .accept;
109 }
110
111 fn print(printer: *MessagePrinter, envelope: zsmtp.Server.Envelope, data: []const u8) !void {
112 try printer.out.print("--- message from <{s}> to", .{envelope.from});
113 for (envelope.recipients) |recipient| {
114 try printer.out.print(" <{s}>", .{recipient});
115 }
116 try printer.out.print(" ({d} bytes)\n{s}---\n", .{ data.len, data });
117 try printer.out.flush();
118 }
119};