From e62966d0678b22ab3c3b1962fb08ef5a9176a418 Mon Sep 17 00:00:00 2001 From: gura105 Date: Mon, 30 Mar 2026 19:11:52 +0900 Subject: [PATCH 1/3] fix(gmail): use case-insensitive matching for email headers in parse_message_headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Gmail API preserves original header casing from the sending MTA. For example, Microsoft Exchange emits "CC" instead of "Cc". Per RFC 5322 ยง1.2.2, header field names are case-insensitive. parse_message_headers used exact case-sensitive string matching, so headers like "CC", "FROM", or "message-id" silently fell through to the catch-all, dropping recipients and metadata. This change normalizes header names via to_ascii_lowercase() before matching, consistent with the existing get_part_header function in the same file which already uses eq_ignore_ascii_case. Fixes #642 --- .changeset/fix-header-case-insensitive.md | 7 ++ .../src/helpers/gmail/mod.rs | 73 ++++++++++++++++--- 2 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 .changeset/fix-header-case-insensitive.md diff --git a/.changeset/fix-header-case-insensitive.md b/.changeset/fix-header-case-insensitive.md new file mode 100644 index 00000000..b9980164 --- /dev/null +++ b/.changeset/fix-header-case-insensitive.md @@ -0,0 +1,7 @@ +--- +"@googleworkspace/cli": patch +--- + +fix(gmail): use case-insensitive matching for email headers in parse_message_headers + +The Gmail API preserves original header casing from the sending MTA (e.g., Microsoft Exchange emits "CC" instead of "Cc"). Per RFC 5322, header field names are case-insensitive. This change normalizes header names to lowercase before matching, consistent with the existing `get_part_header` function in the same file. diff --git a/crates/google-workspace-cli/src/helpers/gmail/mod.rs b/crates/google-workspace-cli/src/helpers/gmail/mod.rs index caeb8b6b..a84629ee 100644 --- a/crates/google-workspace-cli/src/helpers/gmail/mod.rs +++ b/crates/google-workspace-cli/src/helpers/gmail/mod.rs @@ -258,15 +258,15 @@ 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), + 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), _ => {} } } @@ -2140,6 +2140,61 @@ mod tests { assert!(original.body_html.is_none()); } + #[test] + fn test_parse_message_headers_all_uppercase() { + let headers = vec![ + json!({ "name": "FROM", "value": "alice@example.com" }), + json!({ "name": "TO", "value": "bob@example.com" }), + json!({ "name": "CC", "value": "carol@example.com, dave@example.com" }), + json!({ "name": "SUBJECT", "value": "Uppercase" }), + json!({ "name": "DATE", "value": "Mon, 1 Jan 2026 00:00:00 +0000" }), + json!({ "name": "MESSAGE-ID", "value": "" }), + json!({ "name": "REFERENCES", "value": "" }), + json!({ "name": "REPLY-TO", "value": "reply@example.com" }), + ]; + 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, dave@example.com"); + assert_eq!(parsed.subject, "Uppercase"); + assert_eq!(parsed.date, "Mon, 1 Jan 2026 00:00:00 +0000"); + assert_eq!(parsed.message_id, ""); + assert_eq!(parsed.references, ""); + assert_eq!(parsed.reply_to, "reply@example.com"); + } + + #[test] + fn test_parse_message_headers_all_lowercase() { + let headers = vec![ + json!({ "name": "from", "value": "alice@example.com" }), + json!({ "name": "to", "value": "bob@example.com" }), + json!({ "name": "cc", "value": "carol@example.com" }), + json!({ "name": "subject", "value": "Lowercase" }), + json!({ "name": "message-id", "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.subject, "Lowercase"); + assert_eq!(parsed.message_id, ""); + } + + #[test] + fn test_parse_message_headers_mixed_case() { + let headers = vec![ + json!({ "name": "From", "value": "alice@example.com" }), + json!({ "name": "CC", "value": "carol@example.com" }), + json!({ "name": "message-id", "value": "" }), + json!({ "name": "REPLY-TO", "value": "reply@example.com" }), + ]; + let parsed = parse_message_headers(&headers); + assert_eq!(parsed.from, "alice@example.com"); + assert_eq!(parsed.cc, "carol@example.com"); + assert_eq!(parsed.message_id, ""); + assert_eq!(parsed.reply_to, "reply@example.com"); + } + #[test] fn test_parse_original_message_bare_message_id() { let msg = json!({ From 6f89e36dea7ac4d7c9a08a14a3e53c9fa2d41682 Mon Sep 17 00:00:00 2001 From: gura105 Date: Mon, 30 Mar 2026 19:19:56 +0900 Subject: [PATCH 2/3] chore: retrigger CLA check From 16567394d63e1695cf47daa16ff6637bdac55020 Mon Sep 17 00:00:00 2001 From: gura105 Date: Mon, 30 Mar 2026 19:22:45 +0900 Subject: [PATCH 3/3] refactor: use eq_ignore_ascii_case instead of to_ascii_lowercase Avoids per-header String allocation in the loop. Consistent with get_part_header in the same file. Addresses gemini-code-assist review feedback. --- .../src/helpers/gmail/mod.rs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/crates/google-workspace-cli/src/helpers/gmail/mod.rs b/crates/google-workspace-cli/src/helpers/gmail/mod.rs index a84629ee..74d8c9b1 100644 --- a/crates/google-workspace-cli/src/helpers/gmail/mod.rs +++ b/crates/google-workspace-cli/src/helpers/gmail/mod.rs @@ -258,15 +258,23 @@ 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.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), + match name { + s if s.eq_ignore_ascii_case("from") => parsed.from = value.to_string(), + s if s.eq_ignore_ascii_case("reply-to") => { + append_address_list_header_value(&mut parsed.reply_to, value) + } + s if s.eq_ignore_ascii_case("to") => { + append_address_list_header_value(&mut parsed.to, value) + } + s if s.eq_ignore_ascii_case("cc") => { + append_address_list_header_value(&mut parsed.cc, value) + } + s if s.eq_ignore_ascii_case("subject") => parsed.subject = value.to_string(), + s if s.eq_ignore_ascii_case("date") => parsed.date = value.to_string(), + s if s.eq_ignore_ascii_case("message-id") => parsed.message_id = value.to_string(), + s if s.eq_ignore_ascii_case("references") => { + append_header_value(&mut parsed.references, value) + } _ => {} } }