// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Making a name no other delivery will produce. //! //! This is the part of a maildir that has to be right, because it is the only //! thing standing between two programs delivering at the same moment and one //! of them writing over the other's message. There is no lock anywhere in the //! design: a maildir is safe over NFS, between processes that cannot see each //! other, precisely because every delivery invents a name nobody else will. //! //! The original specification asks for three parts joined by dots — the time, //! something unique within the process, and the host — and lists the //! identifiers that may make up the middle: `M` microseconds, `P` a process //! id, `Q` a delivery counter, `R` random bytes, `V` and `I` a device and //! inode. What this generator writes is //! //! ```text //! 1757700000.M492817R3f0a1c2b4d5e6f70Q3.mail.example.com //! ``` //! //! — the second, the microsecond within it, sixty-four random bits, and a //! counter that advances once per delivery from this generator. //! //! There is deliberately **no `P` field**, which is the one thing here that //! departs from what everyone else writes. A process id is a portable idea //! and not a portable call: Zig 0.16 has `getpid` on Linux and through libc, //! and nowhere else, so a library that wanted one would either drag in libc //! or stop working on a platform it has no reason to stop working on. What a //! process id buys is that two processes that start in the same microsecond //! do not collide, and sixty-four random bits buy that far more convincingly //! — a birthday collision needs about five billion deliveries in the same //! microsecond. The counter then covers the case the random number generator //! cannot: two deliveries from *this* generator, which must differ even if //! the clock does not move and the entropy source is repeating itself. //! //! None of that is trusted on its own. The file in `tmp` is created //! exclusively, so a name that has somehow been used before is refused by the //! kernel rather than silently overwritten, and `Maildir.deliver` tries //! again with a fresh one. const std = @import("std"); const Io = std.Io; const testing = std.testing; /// The longest host name this will keep. Longer than `HOST_NAME_MAX` on every /// system that defines it, and stored inline so that a generator needs no /// allocator and no lifetime beyond its own. pub const max_hostname = 255; /// Writes a host name with the two characters a maildir name cannot contain /// replaced by their octal escapes: `/`, which would make the name a path, /// and `:`, which would look like the start of the flags. /// /// This is the escaping the specification defines, and it is not reversed /// anywhere here — the host name is part of an opaque unique string, and /// nothing reads it back. A host name containing a separator other than `:` /// is not escaped, on the grounds that no real one contains anything but /// letters, digits, hyphens and dots. pub fn writeEscapedHostname(w: *Io.Writer, host: []const u8) Io.Writer.Error!void { for (host) |c| switch (c) { '/' => try w.writeAll("\\057"), ':' => try w.writeAll("\\072"), else => try w.writeByte(c), }; } /// The system's host name, into a buffer the caller owns. Falls back to /// `localhost` rather than failing, because a delivery that cannot name the /// host is still a delivery that must not be lost, and the host name is only /// one of four things making the name unique. pub fn systemHostname(buffer: *[max_hostname]u8) []const u8 { var raw: [std.posix.HOST_NAME_MAX]u8 = undefined; const host = std.posix.gethostname(&raw) catch return fallback(buffer); if (host.len == 0 or host.len > buffer.len) return fallback(buffer); @memcpy(buffer[0..host.len], host); return buffer[0..host.len]; } fn fallback(buffer: *[max_hostname]u8) []const u8 { const name = "localhost"; @memcpy(buffer[0..name.len], name); return buffer[0..name.len]; } /// Produces the unique part of a message name. Holds its host name inline, so /// it needs no allocator, and its counter is atomic, so one generator can be /// shared by every thread delivering into a maildir. pub const Generator = struct { hostname_buffer: [max_hostname]u8, hostname_len: usize, counter: std.atomic.Value(u32), /// Takes a copy of `host`, truncated to `max_hostname`. Pass what /// `systemHostname` returned unless there is a reason not to — the reason /// usually being a machine whose mail is delivered under a name other /// than the one `uname` gives. pub fn init(host: []const u8) Generator { var self: Generator = .{ .hostname_buffer = undefined, .hostname_len = @min(host.len, max_hostname), .counter = .init(0), }; @memcpy(self.hostname_buffer[0..self.hostname_len], host[0..self.hostname_len]); return self; } /// A generator naming this machine. pub fn initSystem() Generator { var buffer: [max_hostname]u8 = undefined; return .init(systemHostname(&buffer)); } pub fn hostname(self: *const Generator) []const u8 { return self.hostname_buffer[0..self.hostname_len]; } /// Writes one unique name. Advances the counter, so two calls never /// produce the same thing even with a stopped clock. pub fn next(self: *Generator, io: Io, w: *Io.Writer) Io.Writer.Error!void { const nanoseconds = Io.Timestamp.now(io, .real).toNanoseconds(); const seconds = @divFloor(nanoseconds, std.time.ns_per_s); const microseconds = @divFloor( nanoseconds - seconds * std.time.ns_per_s, std.time.ns_per_us, ); var entropy: [8]u8 = undefined; io.random(&entropy); try w.print("{d}.M{d}R{x:0>16}Q{d}.", .{ seconds, microseconds, std.mem.readInt(u64, &entropy, .little), self.counter.fetchAdd(1, .monotonic), }); try writeEscapedHostname(w, self.hostname()); } /// `next`, into a buffer. The name is short — about sixty characters plus /// the host — so `Io.Dir.max_name_bytes` is always enough room. pub fn bufNext(self: *Generator, io: Io, buffer: []u8) error{NoSpaceLeft}![]u8 { var w: Io.Writer = .fixed(buffer); self.next(io, &w) catch return error.NoSpaceLeft; return w.buffered(); } }; test "a name has the four parts, and the host on the end" { var generator: Generator = .init("mail.example.com"); var buffer: [std.Io.Dir.max_name_bytes]u8 = undefined; // `Io.failing`'s clock reads zero rather than panicking, and its `random` // is a real one, which is exactly enough to exercise this without a // thread pool. const name = try generator.bufNext(std.Io.failing, &buffer); try std.testing.expect(std.mem.startsWith(u8, name, "0.M0R")); try std.testing.expect(std.mem.endsWith(u8, name, "Q0.mail.example.com")); } test "the counter advances even when the clock does not" { var generator: Generator = .init("host"); var a: [std.Io.Dir.max_name_bytes]u8 = undefined; var b: [std.Io.Dir.max_name_bytes]u8 = undefined; const first = try generator.bufNext(std.Io.failing, &a); const second = try generator.bufNext(std.Io.failing, &b); try testing.expect(!std.mem.eql(u8, first, second)); try testing.expect(std.mem.endsWith(u8, first, "Q0.host")); try testing.expect(std.mem.endsWith(u8, second, "Q1.host")); } test "the two characters that would break a name are escaped" { var buffer: [64]u8 = undefined; var w: Io.Writer = .fixed(&buffer); try writeEscapedHostname(&w, "a/b:c"); try testing.expectEqualStrings("a\\057b\\072c", w.buffered()); } test "a host name longer than the buffer is truncated rather than refused" { const long = "x" ** (max_hostname + 10); var generator: Generator = .init(long); try testing.expectEqual(@as(usize, max_hostname), generator.hostname().len); } test "the system host name is never empty" { var buffer: [max_hostname]u8 = undefined; try testing.expect(systemHostname(&buffer).len > 0); }