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.

Parse quoted local-parts in MAIL/RCPT paths

Reviewing postfix's address corpora (src/global/mail_addr_crunch.in)
surfaced a parser bug: parsePathArgs located the closing angle bracket
with a plain scan, truncating legal RFC 5321 addresses whose quoted
local-part contains '>' (e.g. <"a>b"@example.com>). The scan is now
quote-aware with backslash-escape handling, and an unterminated quote
is a syntax error.

Postfix has no exim-style protocol dialogue tests to adopt: its .in/
.ref corpora exercise the policy engine with pre-tokenized inputs, and
the smtpstone tools are load generators that are neither shipped by
nixpkgs nor standalone-buildable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012HBHFhoTYa8TU9GLwobfbx

+38 -1
+38 -1
src/protocol.zig
··· 251 251 .params = std.mem.trimStart(u8, after[end..], " \t"), 252 252 }; 253 253 } 254 - const close = std.mem.indexOfScalar(u8, after, '>') orelse return error.Syntax; 254 + // The closing bracket must be found outside any quoted local-part: 255 + // <"a>b"@example.com> is legal (RFC 5321 quoted-string, with 256 + // backslash escapes). 257 + const close = close: { 258 + var in_quotes = false; 259 + var i: usize = 1; 260 + while (i < after.len) : (i += 1) { 261 + const byte = after[i]; 262 + if (in_quotes) { 263 + if (byte == '\\') { 264 + i += 1; 265 + } else if (byte == '"') { 266 + in_quotes = false; 267 + } 268 + } else if (byte == '"') { 269 + in_quotes = true; 270 + } else if (byte == '>') { 271 + break :close i; 272 + } 273 + } 274 + return error.Syntax; 275 + }; 255 276 var path = after[1..close]; 256 277 // Strip an obsolete source route: <@relay1,@relay2:user@host>. 257 278 if (path.len > 0 and path[0] == '@') { ··· 408 429 const cmd = try Command.parse("RCPT TO:<@relay.example:bob@example.net>"); 409 430 try std.testing.expectEqualStrings("bob@example.net", cmd.rcpt.path); 410 431 } 432 + { 433 + // Quoted local-parts (from postfix's address corpora) may contain 434 + // spaces and even '>' or escaped quotes. 435 + const cmd = try Command.parse("MAIL FROM:<\"foo bar\"@example.com> SIZE=9"); 436 + try std.testing.expectEqualStrings("\"foo bar\"@example.com", cmd.mail.path); 437 + try std.testing.expectEqualStrings("SIZE=9", cmd.mail.params); 438 + } 439 + { 440 + const cmd = try Command.parse("RCPT TO:<\"a>b\"@example.com>"); 441 + try std.testing.expectEqualStrings("\"a>b\"@example.com", cmd.rcpt.path); 442 + } 443 + { 444 + const cmd = try Command.parse("RCPT TO:<\"a\\\">b\"@example.com>"); 445 + try std.testing.expectEqualStrings("\"a\\\">b\"@example.com", cmd.rcpt.path); 446 + } 447 + try std.testing.expectError(error.Syntax, Command.parse("MAIL FROM:<\"unterminated@example.com>")); 411 448 { 412 449 const cmd = try Command.parse("QUIT"); 413 450 try std.testing.expectEqual(Command.quit, cmd);