Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-reply-display-name-quoting.md
Original file line number Diff line number Diff line change
@@ -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()`.
62 changes: 61 additions & 1 deletion src/helpers/gmail/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The list of special characters to check for is missing the colon (:). According to RFC 5322, the colon is one of the specials that requires the display name to be quoted. Without this, a display name like Group: Work would not be quoted, potentially leading to it being misinterpreted as a group address syntax.

Suggested change
if display.contains(|c: char| ",;()<>@\\\".[]".contains(c)) {
if display.contains(|c: char| ":,;()<>@\\\".[]".contains(c)) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The list of RFC 2822 special characters used to determine if a display name needs re-quoting is incomplete. According to RFC 5322 (section 3.2.3), the characters : (colon) and . (period) are also considered 'specials' and require the display name to be quoted if they appear outside of a quoted-string context. Since extract_display_name strips surrounding quotes, a display name like My:Name or First.Last would not be re-quoted by the current logic, leading to an invalid header.

This can result in malformed email headers and potential delivery issues or incorrect parsing by email clients.

Suggested change
if display.contains(|c: char| ",;()<>@\\\".[]".contains(c)) {
if display.contains(|c: char| ",;()<>@\".[]:.".contains(c)) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The list of special characters used to determine if a display name needs quoting appears to be missing the colon (:) character. According to RFC 2822 (section 3.2.1), the colon is considered a "special" character and should not appear in an unquoted atom. An unquoted display name containing a colon could lead to header parsing issues, similar to the ones this PR is fixing for commas and parentheses. It would be more robust to include it in the check.

Suggested change
if display.contains(|c: char| ",;()<>@\\\".[]".contains(c)) {
if display.contains(|c: char| ",;()<>@\\\":.[]:".contains(c)) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

According to RFC 2822, the list of specials characters that require a display name to be quoted also includes the colon (:). This character is currently missing from your check.

While less common in display names than commas or parentheses, omitting it could lead to incorrectly formatted headers if a name like "Location: Main Office" <main@example.com> is processed.

Please add the colon to the list of special characters to ensure full compliance and prevent potential parsing issues.

Suggested change
if display.contains(|c: char| ",;()<>@\\\".[]".contains(c)) {
if display.contains(|c: char| ",;()<>@:\\\"..[]".contains(c)) {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The list of special characters used to determine if a display name needs quoting is missing the colon (:). According to RFC 2822 and RFC 5322, the colon is a special character that requires the display name to be quoted if present. Without this, a display name containing a colon could result in a malformed header.

Suggested change
if display.contains(|c: char| ",;()<>@\\\".[]".contains(c)) {
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();

Expand Down Expand Up @@ -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)" <email@corp.com>
let input = "\"Anderson, Rich (CORP)\" <richard.anderson@adp.com>";
let result = encode_address_header(input);
assert_eq!(
result,
"\"Anderson, Rich (CORP)\" <richard.anderson@adp.com>",
"Display name with comma and parens must be quoted: {result}"
);
}

#[test]
fn test_encode_address_header_display_name_with_parens() {
let input = "Rich (CORP) <rich@example.com>";
let result = encode_address_header(input);
assert_eq!(
result,
"\"Rich (CORP)\" <rich@example.com>",
"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\" <rich@example.com>";
let result = encode_address_header(input);
assert_eq!(
result,
"\"Rich \\\"The Man\\\" Anderson\" <rich@example.com>",
"Already-escaped quotes must not be double-escaped: {result}"
);
}

#[test]
fn test_message_builder_basic() {
let raw = MessageBuilder {
Expand Down
Loading