From fd61d013a22728cea6a58d129914cdb213c2d0a5 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Thu, 21 May 2026 17:57:43 +0800 Subject: [PATCH] fix: Imported emails and UTF-8 folders missing #182 --- crates/cli/src/mbox/gmail.rs | 72 ++++++++++++++++++++++++++++++++++++ crates/cli/src/mbox/mod.rs | 22 +++++------ 2 files changed, 83 insertions(+), 11 deletions(-) diff --git a/crates/cli/src/mbox/gmail.rs b/crates/cli/src/mbox/gmail.rs index 0952552..afa6f83 100644 --- a/crates/cli/src/mbox/gmail.rs +++ b/crates/cli/src/mbox/gmail.rs @@ -60,3 +60,75 @@ pub fn determine_folder(labels_raw: &str) -> String { } } } + +#[cfg(test)] +mod tests { + use mail_parser::{HeaderValue, MessageParser}; + + use super::*; + + fn parse_x_gmail_labels(raw_message: &[u8]) -> Option { + // MessageParser::new() has an empty header_map so the hardcoded match at + // parsers/header.rs:76 treats ALL unknown headers as raw (no RFC 2047 + // decoding). We need three things to get decoding: + // 1. A non-empty header_map (so the else branch runs) + // 2. default_header_text() so the fallback fn is parse_unstructured + // 3. OR register X-Gmail-Labels explicitly via header_text() + let message = MessageParser::new() + .with_minimal_headers() + .default_header_text() + .parse(raw_message)?; + let value: &HeaderValue<'_> = message.header("X-Gmail-Labels")?; + value.as_text().map(|s| s.to_string()) + } + + /// Construct a raw MIME message with RFC 2047 encoded X-Gmail-Labels, + /// parse it, and verify the header is correctly decoded. + fn build_email(x_gmail_labels: &str) -> Vec { + format!( + "From: sender@example.com\r\n\ + To: recipient@example.com\r\n\ + Subject: Test\r\n\ + X-Gmail-Labels: {}\r\n\ + \r\n\ + Body text here.\r\n", + x_gmail_labels + ) + .into_bytes() + } + + #[test] + fn rfc2047_encoded_labels_are_decoded() { + // Exactly the format the user reported: French Gmail labels + let raw = build_email("=?UTF-8?Q?Corbeille?=, =?UTF-8?Q?Messages_archiv=C3=A9s?="); + let labels = parse_x_gmail_labels(&raw).expect("failed to parse X-Gmail-Labels"); + + // mail-parser decodes RFC 2047 header values during initial parsing. + // The decoded text should NOT contain raw =?UTF-8?Q?... sequences. + assert!(!labels.contains("=?UTF-8"), "labels still encoded: {labels:?}"); + assert!(labels.contains("Corbeille"), "missing 'Corbeille': {labels:?}"); + assert!( + labels.contains("archivés"), + "missing decoded 'archivés': {labels:?}", + ); + + // Full pipeline: decoded labels → determine_folder + let folder = determine_folder(&labels); + assert_eq!(folder, "Corbeille"); + } + + #[test] + fn plain_ascii_labels_passthrough() { + let raw = build_email("Inbox, Important"); + let labels = parse_x_gmail_labels(&raw).expect("failed to parse X-Gmail-Labels"); + assert_eq!(labels, "Inbox, Important"); + assert_eq!(determine_folder(&labels), "Important"); + } + + #[test] + fn missing_x_gmail_labels_header() { + let raw = b"From: sender@example.com\r\nTo: r@example.com\r\n\r\nBody.\r\n"; + let message = MessageParser::new().parse(raw.as_slice()).unwrap(); + assert!(message.header("X-Gmail-Labels").is_none()); + } +} diff --git a/crates/cli/src/mbox/mod.rs b/crates/cli/src/mbox/mod.rs index f3065a2..4ed48c0 100644 --- a/crates/cli/src/mbox/mod.rs +++ b/crates/cli/src/mbox/mod.rs @@ -28,7 +28,6 @@ use bichon_core::envelope::meta::{parse_bichon_metadata, BichonMetadata}; use console::style; use dialoguer::{theme::ColorfulTheme, Input}; use dialoguer::{Confirm, Select}; -use mail_parser::parsers::MessageStream; use mail_parser::MessageParser; use reqwest::Client; @@ -161,7 +160,11 @@ pub async fn run_import( continue; } - let message = match MessageParser::new().parse(body) { + let message = match MessageParser::new() + .with_minimal_headers() + .default_header_text() + .parse(body) + { Some(msg) => msg, None => { eprintln!( @@ -181,15 +184,12 @@ pub async fn run_import( } let get_default_folder = || { - let gmail_labels = message.header_raw("X-Gmail-Labels").unwrap_or("INBOX"); - let text_cow = MessageStream::new(gmail_labels.as_bytes()) - .parse_unstructured() - .into_text(); - let data: &str = match &text_cow { - Some(c) => c.as_ref(), - None => "INBOX", - }; - determine_folder(data) + let labels = message + .header("X-Gmail-Labels") + .and_then(|h| h.as_text()) + .map(|s| s.to_string()) + .unwrap_or_else(|| "INBOX".to_string()); + determine_folder(&labels) }; let folder_name = if let Some(ref folder) = target_folder {