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.

zig-smtp / src / Tls.zig
4.6 kB 127 lines
1// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us> 2// SPDX-License-Identifier: MIT 3 4//! Client-side TLS transport layered over an existing stream, using 5//! ianic/tls.zig (the same library that backs server-side STARTTLS). Works 6//! for both implicit TLS (connect, then `init` before any SMTP traffic) and 7//! STARTTLS (after `Client.starttls` succeeds, `init` over the same stream, 8//! then `Client.setTransport` and a fresh `hello`). 9//! 10//! The standard library's TLS client is not used because it requires the 11//! optional TLS 1.3 middlebox-compatibility ChangeCipherSpec record from the 12//! server; servers that disable it (e.g. Exim) break its handshake. 13//! 14//! The underlying stream reader and writer must each have a buffer of at 15//! least `min_buffer_len` bytes. `init` must be called on the `Tls` at its 16//! final resting address (the connection holds interior pointers), e.g.: 17//! 18//! ``` 19//! var tls_transport: Tls = undefined; 20//! try tls_transport.init(gpa, io, &stream_reader.interface, 21//! &stream_writer.interface, .{ .host = "smtp.example.com" }); 22//! ``` 23 24const Tls = @This(); 25 26const std = @import("std"); 27const Io = std.Io; 28const tls = @import("tls"); 29 30connection: tls.Connection, 31reader_state: tls.Connection.Reader, 32writer_state: tls.Connection.Writer, 33read_buffer: []u8, 34write_buffer: []u8, 35 36/// Minimum buffer size for the underlying stream reader and writer. 37pub const min_buffer_len = tls.input_buffer_len; 38 39pub const Options = struct { 40 /// Host name the server's certificate must be valid for (also sent via 41 /// SNI). Ignored with `ca = .insecure`. 42 host: []const u8, 43 ca: Ca = .system, 44 45 pub const Ca = union(enum) { 46 /// Verify the server certificate against the system trust store, 47 /// loaded fresh for this connection. 48 system, 49 /// Verify against a caller-provided bundle (reusable across 50 /// connections; see `tls.config.cert.fromSystem`). Not deinitialized 51 /// by this transport. 52 bundle: std.crypto.Certificate.Bundle, 53 /// No certificate verification at all. This provides encryption but 54 /// no authentication — fine for tests, unsafe on real networks. 55 insecure, 56 }; 57}; 58 59/// Performs the TLS handshake over `input`/`output` (the stream's 60/// reader/writer, each with a buffer of at least `min_buffer_len` bytes). 61/// Allocates plaintext buffers from `gpa`; free them with `deinit`. 62pub fn init( 63 t: *Tls, 64 gpa: std.mem.Allocator, 65 io: Io, 66 input: *Io.Reader, 67 output: *Io.Writer, 68 options: Options, 69) !void { 70 const read_buffer = try gpa.alloc(u8, 4096); 71 errdefer gpa.free(read_buffer); 72 const write_buffer = try gpa.alloc(u8, 4096); 73 errdefer gpa.free(write_buffer); 74 75 var rng_source: std.Random.IoSource = .{ .io = io }; 76 77 var system_ca: ?std.crypto.Certificate.Bundle = null; 78 defer if (system_ca) |*bundle| bundle.deinit(gpa); 79 if (options.ca == .system) system_ca = try tls.config.cert.fromSystem(gpa, io); 80 81 t.connection = try tls.client(input, output, .{ 82 .rng = rng_source.interface(), 83 .now = Io.Clock.real.now(io), 84 .host = if (options.ca == .insecure) "" else options.host, 85 .root_ca = switch (options.ca) { 86 .system => system_ca.?, 87 .bundle => |bundle| bundle, 88 .insecure => .empty, 89 }, 90 .insecure_skip_verify = options.ca == .insecure, 91 }); 92 t.read_buffer = read_buffer; 93 t.write_buffer = write_buffer; 94 t.reader_state = t.connection.reader(read_buffer); 95 t.writer_state = t.connection.writer(write_buffer); 96} 97 98/// The decrypted stream from the server. 99pub fn reader(t: *Tls) *Io.Reader { 100 return &t.reader_state.interface; 101} 102 103/// The plaintext stream to the server; each flush encrypts and pushes the 104/// records through to the underlying stream. 105pub fn writer(t: *Tls) *Io.Writer { 106 return &t.writer_state.interface; 107} 108 109/// Flushes pending data and sends a TLS close_notify alert, letting the 110/// server distinguish a clean shutdown from a truncation attack. Call before 111/// closing the underlying stream. 112pub fn end(t: *Tls) error{WriteFailed}!void { 113 try t.writer_state.interface.flush(); 114 t.connection.close() catch return error.WriteFailed; 115} 116 117pub fn deinit(t: *Tls, gpa: std.mem.Allocator) void { 118 gpa.free(t.read_buffer); 119 gpa.free(t.write_buffer); 120 t.* = undefined; 121} 122 123test { 124 // The handshake needs a live peer, so tests only force full semantic 125 // analysis here; end-to-end coverage comes from the NixOS interop test. 126 std.testing.refAllDecls(Tls); 127}