From 78e0ae1c961d2b5f0ae2224cb9f6da0971ff5d8a Mon Sep 17 00:00:00 2001 From: AIKAWA Shigechika Date: Thu, 18 Jun 2026 14:22:10 +0900 Subject: [PATCH] fix(gmail): match message header names case-insensitively parse_message_headers used exact-case string matching, so headers whose field names use non-canonical casing -- e.g. "CC" (common from Exchange/Outlook) or a lowercase "from" from some MTAs -- fell through and were silently dropped. This dropped CC recipients from +reply-all. Per RFC 5322 1.2.2 header field names are case-insensitive, and the sibling get_part_header already uses eq_ignore_ascii_case. Normalize the name to lowercase before matching, and add a regression test covering mixed-case FROM/to/CC/Reply-TO/subject/MESSAGE-ID/References. Fixes #642. Claude-Session: https://claude.ai/code/session_01EuRYoXhx7ozQWx19mAFLUS --- .../fix-gmail-header-case-insensitive.md | 12 +++++ .../src/helpers/gmail/mod.rs | 50 +++++++++++++++---- 2 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 .changeset/fix-gmail-header-case-insensitive.md diff --git a/.changeset/fix-gmail-header-case-insensitive.md b/.changeset/fix-gmail-header-case-insensitive.md new file mode 100644 index 00000000..6bf3ee62 --- /dev/null +++ b/.changeset/fix-gmail-header-case-insensitive.md @@ -0,0 +1,12 @@ +--- +"@googleworkspace/cli": patch +--- + +fix(gmail): match message header names case-insensitively. + +`parse_message_headers` used exact-case string matching, so headers whose field +names use non-canonical casing — e.g. `"CC"` (common from Exchange/Outlook) or a +lowercase `"from"` from some MTAs — fell through and were silently dropped. This +dropped CC recipients from `+reply-all`. Per RFC 5322 §1.2.2 header field names +are case-insensitive (the sibling `get_part_header` already uses +`eq_ignore_ascii_case`). Normalize the name to lowercase before matching. diff --git a/crates/google-workspace-cli/src/helpers/gmail/mod.rs b/crates/google-workspace-cli/src/helpers/gmail/mod.rs index caeb8b6b..cac2392f 100644 --- a/crates/google-workspace-cli/src/helpers/gmail/mod.rs +++ b/crates/google-workspace-cli/src/helpers/gmail/mod.rs @@ -258,15 +258,20 @@ fn parse_message_headers(headers: &[Value]) -> ParsedMessageHeaders { let name = header.get("name").and_then(|v| v.as_str()).unwrap_or(""); let value = header.get("value").and_then(|v| v.as_str()).unwrap_or(""); - match name { - "From" => parsed.from = value.to_string(), - "Reply-To" => append_address_list_header_value(&mut parsed.reply_to, value), - "To" => append_address_list_header_value(&mut parsed.to, value), - "Cc" => append_address_list_header_value(&mut parsed.cc, value), - "Subject" => parsed.subject = value.to_string(), - "Date" => parsed.date = value.to_string(), - "Message-ID" | "Message-Id" => parsed.message_id = value.to_string(), - "References" => append_header_value(&mut parsed.references, value), + // RFC 5322 §1.2.2: header field names are case-insensitive. Gmail + // preserves the sender's original casing, so e.g. `"CC"` from + // Exchange/Outlook (or a lowercase `"from"` from some MTAs) would fall + // through a case-sensitive match and be silently dropped — dropping CC + // recipients in +reply-all (#642). + match name.to_ascii_lowercase().as_str() { + "from" => parsed.from = value.to_string(), + "reply-to" => append_address_list_header_value(&mut parsed.reply_to, value), + "to" => append_address_list_header_value(&mut parsed.to, value), + "cc" => append_address_list_header_value(&mut parsed.cc, value), + "subject" => parsed.subject = value.to_string(), + "date" => parsed.date = value.to_string(), + "message-id" => parsed.message_id = value.to_string(), + "references" => append_header_value(&mut parsed.references, value), _ => {} } } @@ -3758,6 +3763,33 @@ mod tests { ); } + #[test] + fn test_parse_message_headers_case_insensitive() { + // Regression for #642: Exchange/Outlook emit "CC" in uppercase and some + // MTAs emit lowercase field names. Per RFC 5322 §1.2.2 header field + // names are case-insensitive, so every variant must be recognized + // (previously a case-sensitive match silently dropped them — e.g. CC + // recipients vanished from +reply-all). + let headers = [ + json!({ "name": "FROM", "value": "alice@example.com" }), + json!({ "name": "to", "value": "bob@example.com" }), + json!({ "name": "CC", "value": "carol@example.com" }), + json!({ "name": "Reply-TO", "value": "team@example.com" }), + json!({ "name": "subject", "value": "Re: test" }), + json!({ "name": "MESSAGE-ID", "value": "" }), + json!({ "name": "References", "value": "" }), + ]; + + let parsed = parse_message_headers(&headers); + assert_eq!(parsed.from, "alice@example.com"); + assert_eq!(parsed.to, "bob@example.com"); + assert_eq!(parsed.cc, "carol@example.com"); + assert_eq!(parsed.reply_to, "team@example.com"); + assert_eq!(parsed.subject, "Re: test"); + assert_eq!(parsed.message_id, ""); + assert_eq!(parsed.references, ""); + } + #[test] fn test_filename_control_char_sanitization() { let payload = json!({