From 2aa19b9aa469ccdc9c2db8b97a9044adca8be4fe Mon Sep 17 00:00:00 2001 From: Rich Anderson Date: Mon, 16 Mar 2026 04:29:07 -0400 Subject: [PATCH 1/2] fix: quote display names with RFC 2822 special characters in +reply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When replying to emails from corporate senders with display names like "Anderson, Rich (CORP)" , the +reply command fails with "Invalid To header" (400) from the Gmail API. The root cause: encode_address_header() strips quotes from the display name via extract_display_name(), then reconstructs the address without re-quoting. When the display name contains RFC 2822 special characters (commas, parentheses), the unquoted form is ambiguous — commas split it into multiple malformed mailboxes and parentheses are interpreted as RFC 2822 comments. Fix: re-quote the display name when it contains any RFC 2822 special characters, using a single-pass character iterator that preserves already-escaped sequences and escapes bare quotes/backslashes. Fixes #512 --- .changeset/fix-reply-display-name-quoting.md | 5 ++ src/helpers/gmail/mod.rs | 62 +++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-reply-display-name-quoting.md diff --git a/.changeset/fix-reply-display-name-quoting.md b/.changeset/fix-reply-display-name-quoting.md new file mode 100644 index 00000000..d73b6c76 --- /dev/null +++ b/.changeset/fix-reply-display-name-quoting.md @@ -0,0 +1,5 @@ +--- +"@googleworkspace/cli": patch +--- + +Fix `+reply` failing with "Invalid To header" when the sender's display name contains commas or parentheses (e.g. `"Anderson, Rich (CORP)"`). Display names with RFC 2822 special characters are now re-quoted in `encode_address_header()`. diff --git a/src/helpers/gmail/mod.rs b/src/helpers/gmail/mod.rs index 999d65ce..201726cd 100644 --- a/src/helpers/gmail/mod.rs +++ b/src/helpers/gmail/mod.rs @@ -491,7 +491,32 @@ pub(super) fn encode_address_header(value: &str) -> String { // ASCII display name — reconstruct from parsed components // to strip any potential residual injection data. - format!("{} <{}>", display, email) + // Re-quote if the display name contains RFC 2822 special characters + // (commas, parens, etc.) to prevent header parsing issues. + if display.contains(|c: char| ",;()<>@\\\".[]".contains(c)) { + // Build a properly escaped quoted-string in one pass. + // extract_display_name strips outer quotes but leaves inner + // escapes (e.g. \"), so we skip already-escaped chars and + // escape any bare quotes or backslashes. + let mut escaped = String::with_capacity(display.len()); + let mut chars = display.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '\\' && chars.peek().map_or(false, |&c| c == '"' || c == '\\') { + // Already escaped — pass through as-is + escaped.push(ch); + escaped.push(chars.next().unwrap()); + } else if ch == '"' || ch == '\\' { + // Bare special char — escape it + escaped.push('\\'); + escaped.push(ch); + } else { + escaped.push(ch); + } + } + format!("\"{}\" <{}>", escaped, email) + } else { + format!("{} <{}>", display, email) + } }) .collect(); @@ -1489,6 +1514,41 @@ mod tests { assert_eq!(encode_address_header(""), ""); } + #[test] + fn test_encode_address_header_display_name_with_comma() { + // Corporate email: "Last, First (DEPT)" + let input = "\"Anderson, Rich (CORP)\" "; + let result = encode_address_header(input); + assert_eq!( + result, + "\"Anderson, Rich (CORP)\" ", + "Display name with comma and parens must be quoted: {result}" + ); + } + + #[test] + fn test_encode_address_header_display_name_with_parens() { + let input = "Rich (CORP) "; + let result = encode_address_header(input); + assert_eq!( + result, + "\"Rich (CORP)\" ", + "Display name with parentheses must be quoted: {result}" + ); + } + + #[test] + fn test_encode_address_header_display_name_with_escaped_quotes() { + // Display name with already-escaped quotes must not double-escape + let input = "\"Rich \\\"The Man\\\" Anderson\" "; + let result = encode_address_header(input); + assert_eq!( + result, + "\"Rich \\\"The Man\\\" Anderson\" ", + "Already-escaped quotes must not be double-escaped: {result}" + ); + } + #[test] fn test_message_builder_basic() { let raw = MessageBuilder { From df6a0975ff64748334fd49429b06773f85547999 Mon Sep 17 00:00:00 2001 From: Rich Anderson Date: Mon, 16 Mar 2026 04:34:36 -0400 Subject: [PATCH 2/2] retrigger CLA check