From 58aba9648c741f274928fe3cf66d6aa8f146daff Mon Sep 17 00:00:00 2001 From: Steven Wilcox Date: Sun, 3 May 2026 07:17:09 -0500 Subject: [PATCH] net.smtp: strip display name from MAIL FROM / RCPT TO envelope addresses (#27072) * net.smtp: strip display name from MAIL FROM / RCPT TO envelope addresses The SMTP envelope commands `MAIL FROM:` and `RCPT TO:` (RFC 5321) accept a bare mailbox only, while the `From:` / `To:` message headers (RFC 5322) may include a display name. `Mail.from` and `Mail.to` are reused for both, so passing `'Ivan Petrov '` previously emitted `MAIL FROM: >`, which servers reject with `Sending mailfrom failed`. Extract the angle-bracketed address (when present) for the envelope commands; the message-data path is unchanged and still preserves the display name in the `From:` / `To:` headers. Discussion: https://github.com/vlang/v/discussions/26862 * net.smtp: only strip angle-addr wrapper, preserve quoted local-parts Previous parser unconditionally split on the first '<', which corrupted otherwise-valid bare mailboxes whose quoted local-part contains '<' (e.g. `"a' (i.e. it really is an angle-addr form). Bare mailboxes pass through unchanged. - When walking for the opening '<', skip over quoted strings (with backslash escapes) so a quoted local-part isn't mistaken for the wrapper opener. Tests cover quoted local-part with '<', quoted display name with escaped quotes, and malformed unterminated input. --- vlib/net/smtp/smtp.v | 34 ++++++++++++++++++++++++++++-- vlib/net/smtp/smtp_internal_test.v | 30 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/vlib/net/smtp/smtp.v b/vlib/net/smtp/smtp.v index 90f2ca519..34b655c3b 100644 --- a/vlib/net/smtp/smtp.v +++ b/vlib/net/smtp/smtp.v @@ -242,14 +242,44 @@ fn (mut c Client) send_auth() ! { c.expect_reply(.auth_ok)! } +// envelope_addr extracts the bare mailbox from an address that may include a +// display name. The SMTP envelope (`MAIL FROM:` / `RCPT TO:`) only accepts a +// bare mailbox (RFC 5321), while `Mail.from`/`Mail.to` may also be written in +// the RFC 5322 `Display Name ` form for the message header. +// +// Only strip an angle-addr wrapper when the input actually ends in `>`; bare +// mailboxes are returned unchanged. When walking for the opening `<`, quoted +// strings are skipped so a quoted local-part like `"a') { + return trimmed + } + mut in_quote := false + mut i := 0 + for i < trimmed.len - 1 { + c := trimmed[i] + if c == `"` { + in_quote = !in_quote + } else if in_quote && c == `\\` && i + 1 < trimmed.len { + i++ + } else if c == `<` && !in_quote { + return trimmed[i + 1..trimmed.len - 1] + } + i++ + } + return trimmed +} + fn (mut c Client) send_mailfrom(from string) ! { - c.send_str('MAIL FROM: <${from}>\r\n')! + c.send_str('MAIL FROM:<${envelope_addr(from)}>\r\n')! c.expect_reply(.action_ok)! } fn (mut c Client) send_mailto(to string) ! { for rcpt in to.split(';') { - c.send_str('RCPT TO: <${rcpt}>\r\n')! + c.send_str('RCPT TO:<${envelope_addr(rcpt)}>\r\n')! c.expect_reply(.action_ok)! } } diff --git a/vlib/net/smtp/smtp_internal_test.v b/vlib/net/smtp/smtp_internal_test.v index 70ec4d61f..0ba0d3442 100644 --- a/vlib/net/smtp/smtp_internal_test.v +++ b/vlib/net/smtp/smtp_internal_test.v @@ -85,3 +85,33 @@ fn test_fold_base64_wraps_long_lines() { assert lines[0].len == 76 assert lines[1].len == 32 } + +fn test_envelope_addr_strips_display_name() { + assert envelope_addr('ivan@example.com') == 'ivan@example.com' + assert envelope_addr(' ivan@example.com ') == 'ivan@example.com' + assert envelope_addr('') == 'ivan@example.com' + assert envelope_addr('Ivan Petrov ') == 'ivan@example.com' + assert envelope_addr('"Petrov, Ivan" ') == 'ivan@example.com' + // Quoted local-parts may legitimately contain '<'. Without a trailing '>', + // the input is not an angle-addr wrapper and must pass through unchanged. + assert envelope_addr('"a'. + assert envelope_addr('"a') == '"a') == 'ivan@example.com' + // Malformed input (no closing '>') passes through; the server can reject. + assert envelope_addr('Ivan ' + to: 'recipient@example.com' + subject: 'Test' + body: 'hi' + } + + message := mail.message_data() + + assert message.contains('From: Ivan Petrov \r\n') +} -- 2.39.5