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 {