use hashbrown::HashMap;
use mail_parser::{MessageParser, MimeHeaders};
use crate::content;
use crate::types::{AttachmentInfo, MessageInfo};
pub fn parse_rfc822(
raw: &[u8],
uid: u32,
flags: Vec<String>,
size: Option<u32>,
mailbox: &str,
account: &str,
include_content: bool,
include_headers: bool,
) -> crate::Result<MessageInfo> {
let parsed = MessageParser::default().parse(raw).ok_or_else(|| {
crate::AgentmailError::Parse(format!("Failed to parse RFC822 message UID {}", uid))
})?;
let subject = parsed.subject().unwrap_or("").to_string();
let sender = format_address(parsed.from());
let reply_to = format_address(parsed.reply_to());
let to = format_address_list(parsed.to());
let cc = format_address_list(parsed.cc());
let bcc = format_address_list(parsed.bcc());
let date = parsed
.date()
.and_then(|d| chrono::DateTime::from_timestamp(d.to_timestamp(), 0));
let list_unsubscribe = parsed
.header("List-Unsubscribe")
.and_then(header_to_string)
.filter(|s| !s.is_empty());
let list_unsubscribe_post = parsed
.header("List-Unsubscribe-Post")
.and_then(header_to_string)
.filter(|s| !s.is_empty());
let list_id = parsed
.header("List-Id")
.and_then(header_to_string)
.filter(|s| !s.is_empty());
let list_help = parsed
.header("List-Help")
.and_then(header_to_string)
.filter(|s| !s.is_empty());
let message_id = parsed.message_id().map(|s| s.to_string());
let in_reply_to = extract_header_value_text(parsed.in_reply_to());
let references = extract_header_value_text_list(parsed.references());
let mime_type = extract_mime_type(&parsed);
let attachments = if include_content {
extract_attachments(&parsed)
} else {
Vec::new()
};
let headers = if include_headers {
build_headers_map(&parsed)
} else {
HashMap::new()
};
let (body_content, content_format, content_truncated) = if include_content {
extract_content(&parsed)
} else {
(None, None, None)
};
Ok(MessageInfo {
uid,
subject,
sender,
reply_to,
to,
cc,
mailbox: mailbox.to_string(),
account: account.to_string(),
date,
flags,
size,
content: body_content,
content_format,
content_truncated,
list_unsubscribe,
list_unsubscribe_post,
list_id,
list_help,
message_id,
in_reply_to,
references,
bcc,
mime_type,
attachments,
headers,
})
}
pub fn parse_sender_date(
raw: &[u8],
) -> crate::Result<(String, String, Option<chrono::DateTime<chrono::Utc>>)> {
let parsed = MessageParser::default().parse(raw).ok_or_else(|| {
crate::AgentmailError::Parse("Failed to parse partial headers".to_string())
})?;
let (email, name) = extract_from_parts(parsed.from());
let date = parsed
.date()
.and_then(|d| chrono::DateTime::from_timestamp(d.to_timestamp(), 0));
Ok((email, name, date))
}
fn extract_from_parts(addr: Option<&mail_parser::Address<'_>>) -> (String, String) {
match addr {
Some(mail_parser::Address::List(list)) if !list.is_empty() => {
let a = &list[0];
let email = a.address.as_deref().unwrap_or("").to_lowercase();
let name = a.name.as_deref().unwrap_or("").to_string();
(email, name)
}
Some(mail_parser::Address::Group(groups)) if !groups.is_empty() => {
if let Some(a) = groups[0].addresses.first() {
let email = a.address.as_deref().unwrap_or("").to_lowercase();
let name = a.name.as_deref().unwrap_or("").to_string();
(email, name)
} else {
(String::new(), String::new())
}
}
_ => (String::new(), String::new()),
}
}
fn extract_content(
msg: &mail_parser::Message<'_>,
) -> (Option<String>, Option<String>, Option<bool>) {
if let Some(html) = msg.body_html(0) {
let md = content::html_to_markdown(&html);
let (truncated, was_truncated) =
content::truncate_for_context(&md, content::DEFAULT_CONTENT_MAX_CHARS);
(
Some(truncated),
Some("markdown".to_string()),
Some(was_truncated),
)
} else if let Some(text) = msg.body_text(0) {
let clean = content::plain_to_markdown(&text);
let (truncated, was_truncated) =
content::truncate_for_context(&clean, content::DEFAULT_CONTENT_MAX_CHARS);
(
Some(truncated),
Some("plain".to_string()),
Some(was_truncated),
)
} else {
(None, None, None)
}
}
fn build_headers_map(parsed: &mail_parser::Message<'_>) -> HashMap<String, Vec<String>> {
let mut map: HashMap<String, Vec<String>> = HashMap::new();
for (name, value) in parsed.headers_raw() {
let trimmed = value.trim().to_string();
if !trimmed.is_empty() {
map.entry(name.to_string()).or_default().push(trimmed);
}
}
map
}
fn extract_header_value_text(hv: &mail_parser::HeaderValue<'_>) -> Option<String> {
match hv {
mail_parser::HeaderValue::Text(s) => Some(s.to_string()),
mail_parser::HeaderValue::TextList(list) => list.first().map(|s| s.to_string()),
_ => None,
}
}
fn extract_header_value_text_list(hv: &mail_parser::HeaderValue<'_>) -> Vec<String> {
match hv {
mail_parser::HeaderValue::Text(s) => vec![s.to_string()],
mail_parser::HeaderValue::TextList(list) => list.iter().map(|s| s.to_string()).collect(),
_ => Vec::new(),
}
}
fn header_to_string(hv: &mail_parser::HeaderValue<'_>) -> Option<String> {
match hv {
mail_parser::HeaderValue::Text(s) => Some(s.to_string()),
mail_parser::HeaderValue::TextList(list) => Some(
list.iter()
.map(|s| s.as_ref())
.collect::<Vec<_>>()
.join(", "),
),
_ => None,
}
}
fn extract_mime_type(parsed: &mail_parser::Message<'_>) -> Option<String> {
parsed.content_type().map(|ct| {
let mut s = ct.c_type.to_string();
if let Some(ref sub) = ct.c_subtype {
s.push('/');
s.push_str(sub);
}
s
})
}
fn extract_attachments(parsed: &mail_parser::Message<'_>) -> Vec<AttachmentInfo> {
parsed
.attachments()
.map(|part| {
let content_type = part
.content_type()
.map(|ct| {
let mut s = ct.c_type.to_string();
if let Some(ref sub) = ct.c_subtype {
s.push('/');
s.push_str(sub);
}
s
})
.unwrap_or_else(|| "application/octet-stream".to_string());
AttachmentInfo {
name: part.attachment_name().map(|s| s.to_string()),
content_type,
size: part.contents().len(),
content_id: part.content_id().map(|s| s.to_string()),
}
})
.collect()
}
pub fn extract_attachment_data(
raw: &[u8],
uid: u32,
) -> crate::Result<Vec<(String, String, Vec<u8>)>> {
let parsed = MessageParser::default().parse(raw).ok_or_else(|| {
crate::AgentmailError::Parse(format!("Failed to parse RFC822 message UID {}", uid))
})?;
let mut results = Vec::new();
for part in parsed.attachments() {
let name = part
.attachment_name()
.map(|s| s.to_string())
.unwrap_or_else(|| "unnamed".to_string());
let content_type = part
.content_type()
.map(|ct| {
let mut s = ct.c_type.to_string();
if let Some(ref sub) = ct.c_subtype {
s.push('/');
s.push_str(sub);
}
s
})
.unwrap_or_else(|| "application/octet-stream".to_string());
let bytes = part.contents().to_vec();
results.push((name, content_type, bytes));
}
Ok(results)
}
fn format_address(addr: Option<&mail_parser::Address<'_>>) -> String {
match addr {
Some(mail_parser::Address::List(list)) if !list.is_empty() => format_single_addr(&list[0]),
Some(mail_parser::Address::Group(groups)) if !groups.is_empty() => {
if let Some(a) = groups[0].addresses.first() {
format_single_addr(a)
} else {
String::new()
}
}
_ => String::new(),
}
}
fn format_address_list(addr: Option<&mail_parser::Address<'_>>) -> Vec<String> {
match addr {
Some(mail_parser::Address::List(list)) => list.iter().map(format_single_addr).collect(),
Some(mail_parser::Address::Group(groups)) => groups
.iter()
.flat_map(|g| g.addresses.iter())
.map(format_single_addr)
.collect(),
_ => Vec::new(),
}
}
fn format_single_addr(a: &mail_parser::Addr<'_>) -> String {
let name = a.name.as_deref().unwrap_or("");
let email = a.address.as_deref().unwrap_or("");
if name.is_empty() {
email.to_string()
} else {
format!("{} <{}>", name, email)
}
}