From 76e2860b43d73e6fbd9f9a06b6bf5d457471a950 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:32:26 +0200 Subject: [PATCH] Google takeout: Decode MIME-encoded values in `X-Gmail-Labels` (fixes #7) --- CHANGELOG.md | 1 + src/sync/import_takeout/labels.rs | 38 ++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 936843b..c1313c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. This projec ### Fixed - Mailbox roles must be unique per archive (#8). +- Google takeout: Decode MIME-encoded values in `X-Gmail-Labels`. # Change Log diff --git a/src/sync/import_takeout/labels.rs b/src/sync/import_takeout/labels.rs index 1a51446..90a6c57 100644 --- a/src/sync/import_takeout/labels.rs +++ b/src/sync/import_takeout/labels.rs @@ -4,8 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 OR MIT */ +use std::borrow::Cow; use std::collections::BTreeSet; +use mail_parser::HeaderValue; +use mail_parser::parsers::MessageStream; + #[derive(Debug, Clone, PartialEq, Eq)] pub struct LabelClassification { pub mailboxes: Vec, @@ -43,7 +47,8 @@ pub struct MailboxAssignment { } pub fn parse_header(value: &str) -> Vec { - value + let decoded = decode_encoded_words(value); + decoded .split(',') .map(str::trim) .filter(|t| !t.is_empty()) @@ -51,6 +56,19 @@ pub fn parse_header(value: &str) -> Vec { .collect() } +fn decode_encoded_words(value: &str) -> Cow<'_, str> { + if !value.contains("=?") { + return Cow::Borrowed(value); + } + let mut buf = Vec::with_capacity(value.len() + 1); + buf.extend_from_slice(value.as_bytes()); + buf.push(b'\n'); + match MessageStream::new(&buf).parse_unstructured() { + HeaderValue::Text(text) => Cow::Owned(text.into_owned()), + _ => Cow::Borrowed(value), + } +} + pub fn classify(tokens: &[String]) -> LabelClassification { let mut mailboxes: Vec = Vec::new(); let mut keywords: BTreeSet = BTreeSet::new(); @@ -158,6 +176,24 @@ mod tests { assert_eq!(v, vec!["Inbox", "Opened", "Starred", "Important"]); } + #[test] + fn parse_header_decodes_rfc2047_encoded_words() { + let v = parse_header("=?UTF-8?Q?=C3=85pnet,Kategori:_Reklame?="); + assert_eq!(v, vec!["Åpnet", "Kategori: Reklame"]); + } + + #[test] + fn parse_header_decodes_mixed_plain_and_encoded_tokens() { + let v = parse_header("Inbox,=?UTF-8?Q?=C3=85pnet?=,Github"); + assert_eq!(v, vec!["Inbox", "Åpnet", "Github"]); + } + + #[test] + fn parse_header_leaves_plain_ascii_untouched() { + let v = parse_header("Inbox,Opened,Label_001/Label_002"); + assert_eq!(v, vec!["Inbox", "Opened", "Label_001/Label_002"]); + } + #[test] fn parse_header_empty_yields_empty() { assert!(parse_header("").is_empty());