// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Demo CLI for the zsmtp library. //! //! zsmtp send [--tls|--starttls] [--insecure] ... //! send a message read from stdin; --tls speaks TLS from the first //! byte (port 465 style), --starttls upgrades after EHLO (port 587 //! style), --insecure skips certificate verification //! zsmtp serve [--tls-cert --tls-key ] //! run a debug server on 127.0.0.1 that prints received messages; //! with a certificate and key it advertises and accepts STARTTLS 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 >= 2 and std.mem.eql(u8, args[1], "send")) { var config: SendConfig = .{}; var rest = args[2..]; while (rest.len > 0 and std.mem.startsWith(u8, rest[0], "--")) { if (std.mem.eql(u8, rest[0], "--tls")) { config.mode = .tls; } else if (std.mem.eql(u8, rest[0], "--starttls")) { config.mode = .starttls; } else if (std.mem.eql(u8, rest[0], "--insecure")) { config.insecure = true; } else { return usage(); } rest = rest[1..]; } if (rest.len < 4) return usage(); return send(io, arena, config, rest[0], rest[1], rest[2], rest[3..]); } if (args.len >= 2 and std.mem.eql(u8, args[1], "serve")) { var config: ServeConfig = .{}; var rest = args[2..]; while (rest.len >= 2 and std.mem.startsWith(u8, rest[0], "--")) { if (std.mem.eql(u8, rest[0], "--tls-cert")) { config.cert_path = rest[1]; } else if (std.mem.eql(u8, rest[0], "--tls-key")) { config.key_path = rest[1]; } else { return usage(); } rest = rest[2..]; } if (rest.len != 1) return usage(); if ((config.cert_path == null) != (config.key_path == null)) return usage(); return serve(io, arena, config, rest[0]); } return usage(); } const ServeConfig = struct { cert_path: ?[]const u8 = null, key_path: ?[]const u8 = null, }; const SendConfig = struct { mode: enum { plain, tls, starttls } = .plain, insecure: bool = false, }; fn usage() noreturn { std.log.err( \\usage: \\ zsmtp send [--tls|--starttls] [--insecure] ... \\ (message is read from stdin) \\ zsmtp serve [--tls-cert --tls-key ] , .{}); std.process.exit(1); } fn send( io: Io, arena: std.mem.Allocator, config: SendConfig, 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); // The TLS layer requires stream buffers of at least min_buffer_len. const read_buf = try arena.alloc(u8, zsmtp.Tls.min_buffer_len); const write_buf = try arena.alloc(u8, zsmtp.Tls.min_buffer_len); var stream_reader = stream.reader(io, read_buf); var stream_writer = stream.writer(io, write_buf); const tls_options: zsmtp.Tls.Options = .{ .host = host_arg, .ca = if (config.insecure) .insecure else .system, }; var tls: zsmtp.Tls = undefined; var tls_active = false; defer if (tls_active) { tls.end() catch {}; tls.deinit(arena); }; var reply_buf: [1024]u8 = undefined; var client: zsmtp.Client = .init(&stream_reader.interface, &stream_writer.interface, &reply_buf); if (config.mode == .tls) { try tls.init(arena, io, &stream_reader.interface, &stream_writer.interface, tls_options); tls_active = true; client.setTransport(tls.reader(), tls.writer()); } _ = try client.greet(); _ = try client.hello("localhost"); if (config.mode == .starttls) { try client.starttls(); try tls.init(arena, io, &stream_reader.interface, &stream_writer.interface, tls_options); tls_active = true; client.setTransport(tls.reader(), tls.writer()); _ = 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, config: ServeConfig, 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); var auth: ?zsmtp.tls.config.CertKeyPair = if (config.cert_path) |cert_path| try .fromFilePath(gpa, io, .cwd(), cert_path, config.key_path.?) else null; const starttls: ?zsmtp.Server.StartTls = if (auth) |*a| .{ .io = io, .auth = a } else null; std.log.info("listening on 127.0.0.1:{d}{s}", .{ port, if (starttls != null) " with STARTTLS" else "", }); 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); // Sized for the TLS handshake, which runs over the raw stream. const read_buf = try gpa.alloc(u8, zsmtp.tls.input_buffer_len); defer gpa.free(read_buf); const write_buf = try gpa.alloc(u8, zsmtp.tls.output_buffer_len); defer gpa.free(write_buf); 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", .starttls = starttls }, ); 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(); } };