use super::types::EmailAddress; pub(crate) fn name_attribute_to_string(attr: &async_imap::types::NameAttribute<'_>) -> String { use async_imap::types::NameAttribute; match attr { NameAttribute::NoInferiors => "\\Noinferiors".to_string(), NameAttribute::NoSelect => "\\Noselect".to_string(), NameAttribute::Marked => "\\Marked".to_string(), NameAttribute::Unmarked => "\\Unmarked".to_string(), NameAttribute::All => "\\All".to_string(), NameAttribute::Archive => "\\Archive".to_string(), NameAttribute::Drafts => "\\Drafts".to_string(), NameAttribute::Flagged => "\\Flagged".to_string(), NameAttribute::Junk => "\\Junk".to_string(), NameAttribute::Sent => "\\Sent".to_string(), NameAttribute::Trash => "\\Trash".to_string(), NameAttribute::Extension(s) => s.to_string(), _ => format!("{attr:?}"), } } pub(crate) fn flag_to_string(flag: &async_imap::types::Flag<'_>) -> String { use async_imap::types::Flag; match flag { Flag::Seen => "\\Seen".to_string(), Flag::Answered => "\\Answered".to_string(), Flag::Flagged => "\\Flagged".to_string(), Flag::Deleted => "\\Deleted".to_string(), Flag::Draft => "\\Draft".to_string(), Flag::Recent => "\\Recent".to_string(), Flag::MayCreate => "\\*".to_string(), Flag::Custom(s) => s.to_string(), } } pub(crate) fn decode_rfc2047(input: &str) -> String { if !input.contains("=?") { return input.to_string(); } let mut result = String::with_capacity(input.len()); let mut remaining = input; while let Some(start) = remaining.find("=?") { result.push_str(&remaining[..start]); let after_start = &remaining[start + 2..]; if let Some(q1) = after_start.find('?') { let charset = &after_start[..q1]; let after_charset = &after_start[q1 + 1..]; if let Some(q2) = after_charset.find('?') { let encoding = &after_charset[..q2]; let after_encoding = &after_charset[q2 + 1..]; if let Some(end) = after_encoding.find("?=") { let encoded_text = &after_encoding[..end]; let decoded_bytes = match encoding.to_ascii_uppercase().as_str() { "B" => base64::Engine::decode( &base64::engine::general_purpose::STANDARD, encoded_text, ) .ok(), "Q" => decode_quoted_printable_header(encoded_text), _ => None, }; if let Some(bytes) = decoded_bytes { let text = decode_charset(charset, &bytes); result.push_str(&text); } else { let full_len = 2 + q1 + 1 + q2 + 1 + end + 2; result.push_str(&remaining[start..start + full_len]); } let skip = q1 + 1 + q2 + 1 + end + 2; remaining = &after_start[skip..]; if remaining.starts_with("=?") || remaining.trim_start().starts_with("=?") { remaining = remaining.trim_start(); } continue; } } } result.push_str("=?"); remaining = after_start; } result.push_str(remaining); result } pub(crate) fn decode_quoted_printable_header(input: &str) -> Option> { let mut bytes = Vec::with_capacity(input.len()); let mut chars = input.bytes(); while let Some(b) = chars.next() { match b { b'_' => bytes.push(b' '), b'=' => { let hi = chars.next()?; let lo = chars.next()?; let hex = [hi, lo]; let hex_str = std::str::from_utf8(&hex).ok()?; let byte = u8::from_str_radix(hex_str, 16).ok()?; bytes.push(byte); } _ => bytes.push(b), } } Some(bytes) } pub(crate) fn decode_charset(charset: &str, bytes: &[u8]) -> String { match charset.to_ascii_lowercase().as_str() { "utf-8" | "utf8" => String::from_utf8_lossy(bytes).into_owned(), "iso-8859-1" | "latin1" | "latin-1" => bytes.iter().map(|&b| b as char).collect(), _ => String::from_utf8(bytes.to_vec()) .unwrap_or_else(|_| bytes.iter().map(|&b| b as char).collect()), } } pub(crate) fn imap_address_to_email( addr: &async_imap::imap_proto::types::Address<'_>, ) -> EmailAddress { let name = addr .name .as_ref() .and_then(|b| std::str::from_utf8(b).ok()) .map(decode_rfc2047); let mailbox = addr .mailbox .as_ref() .and_then(|b| std::str::from_utf8(b).ok()) .unwrap_or(""); let host = addr .host .as_ref() .and_then(|b| std::str::from_utf8(b).ok()) .unwrap_or(""); let address = if host.is_empty() { mailbox.to_string() } else { format!("{mailbox}@{host}") }; EmailAddress { name, address } } pub(crate) fn has_attachments(bs: &async_imap::imap_proto::types::BodyStructure<'_>) -> bool { use async_imap::imap_proto::types::BodyStructure; match bs { BodyStructure::Basic { common, .. } => { if let Some(ref disp) = common.disposition && disp.ty.eq_ignore_ascii_case("attachment") { return true; } let ty = common.ty.ty.to_ascii_lowercase(); matches!(ty.as_str(), "application" | "image" | "audio" | "video") } BodyStructure::Text { common, .. } => { if let Some(ref disp) = common.disposition { return disp.ty.eq_ignore_ascii_case("attachment"); } false } BodyStructure::Message { body, .. } => has_attachments(body), BodyStructure::Multipart { bodies, .. } => bodies.iter().any(has_attachments), } }