// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Demo CLI for the zsmtp library. //! //! zsmtp send ... send a message read from stdin //! zsmtp serve run a debug server on 127.0.0.1 //! that prints received messages const std = @import("std"); const Io = std.Io; const zsmtp = @import("zsmtp"); pub fn main(init: std.process.Init) !void { const arena = init.arena.allocator(); const io = init.io; const args = try init.minimal.args.toSlice(arena); if (args.len >= 6 and std.mem.eql(u8, args[1], "send")) { return send(io, arena, args[2], args[3], args[4], args[5..]); } if (args.len == 3 and std.mem.eql(u8, args[1], "serve")) { return serve(io, arena, args[2]); } std.log.err( \\usage: \\ zsmtp send ... (message is read from stdin) \\ zsmtp serve , .{}); std.process.exit(1); } fn send( io: Io, arena: std.mem.Allocator, host_arg: []const u8, port_arg: []const u8, from: []const u8, recipients: []const []const u8, ) !void { const host = try Io.net.HostName.init(host_arg); const port = try std.fmt.parseInt(u16, port_arg, 10); var stdin_buf: [4096]u8 = undefined; var stdin: Io.File.Reader = .init(.stdin(), io, &stdin_buf); const message = try stdin.interface.allocRemaining(arena, .unlimited); const stream = try host.connect(io, port, .{ .mode = .stream }); defer stream.close(io); var read_buf: [4096]u8 = undefined; var write_buf: [4096]u8 = undefined; var stream_reader = stream.reader(io, &read_buf); var stream_writer = stream.writer(io, &write_buf); var reply_buf: [1024]u8 = undefined; var client: zsmtp.Client = .init(&stream_reader.interface, &stream_writer.interface, &reply_buf); _ = try client.greet(); _ = try client.hello("localhost"); client.sendMail(from, recipients, message) catch |err| { if (err == error.UnexpectedReply) { const reply = client.last_reply.?; std.log.err("server rejected: {d} {s}", .{ reply.code, reply.text }); } return err; }; try client.quit(); std.log.info("message sent to {d} recipient(s)", .{recipients.len}); } fn serve(io: Io, gpa: std.mem.Allocator, port_arg: []const u8) !void { const port = try std.fmt.parseInt(u16, port_arg, 10); const address: Io.net.IpAddress = .{ .ip4 = .loopback(port) }; var listener = try address.listen(io, .{}); defer listener.deinit(io); std.log.info("listening on 127.0.0.1:{d}", .{port}); var stdout_buf: [4096]u8 = undefined; var stdout: Io.File.Writer = .init(.stdout(), io, &stdout_buf); var printer: MessagePrinter = .{ .out = &stdout.interface }; while (true) { const stream = try listener.accept(io); defer stream.close(io); var read_buf: [4096]u8 = undefined; var write_buf: [4096]u8 = undefined; var stream_reader = stream.reader(io, &read_buf); var stream_writer = stream.writer(io, &write_buf); var session: zsmtp.Server = .init( &stream_reader.interface, &stream_writer.interface, .{ .context = &printer, .vtable = &.{ .message = MessagePrinter.onMessage } }, .{ .hostname = "localhost" }, ); session.run(gpa) catch |err| { std.log.warn("session ended with error: {t}", .{err}); }; } } const MessagePrinter = struct { out: *Io.Writer, fn onMessage(context: ?*anyopaque, envelope: zsmtp.Server.Envelope, data: []const u8) zsmtp.Server.Decision { const printer: *MessagePrinter = @ptrCast(@alignCast(context.?)); printer.print(envelope, data) catch return .{ .reject = .{ .code = 451, .text = "4.3.0 Local error" } }; return .accept; } fn print(printer: *MessagePrinter, envelope: zsmtp.Server.Envelope, data: []const u8) !void { try printer.out.print("--- message from <{s}> to", .{envelope.from}); for (envelope.recipients) |recipient| { try printer.out.print(" <{s}>", .{recipient}); } try printer.out.print(" ({d} bytes)\n{s}---\n", .{ data.len, data }); try printer.out.flush(); } };