use mail_parser::{parsers::MessageStream, Address, HeaderValue};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmailAddress {
pub name: Option<String>,
pub address: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AddressGroup {
pub name: Option<String>,
pub addresses: Vec<EmailAddress>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct HeaderDateTime {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub tz_before_gmt: bool,
pub tz_hour: u8,
pub tz_minute: u8,
}
impl HeaderDateTime {
#[must_use]
pub fn to_rfc3339(&self) -> String {
self.to_mail_parser().to_rfc3339()
}
#[must_use]
pub fn to_timestamp(&self) -> i64 {
self.to_mail_parser().to_timestamp()
}
fn to_mail_parser(self) -> mail_parser::DateTime {
mail_parser::DateTime {
year: self.year,
month: self.month,
day: self.day,
hour: self.hour,
minute: self.minute,
second: self.second,
tz_before_gmt: self.tz_before_gmt,
tz_hour: self.tz_hour,
tz_minute: self.tz_minute,
}
}
fn from_mail_parser(dt: &mail_parser::DateTime) -> Self {
Self {
year: dt.year,
month: dt.month,
day: dt.day,
hour: dt.hour,
minute: dt.minute,
second: dt.second,
tz_before_gmt: dt.tz_before_gmt,
tz_hour: dt.tz_hour,
tz_minute: dt.tz_minute,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HeaderForm {
Raw,
Addresses,
GroupedAddresses,
MessageIds,
Date,
URLs,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HeaderValueTyped {
Raw(String),
Addresses(Vec<EmailAddress>),
GroupedAddresses(Vec<AddressGroup>),
MessageIds(Vec<String>),
DateTime(Option<HeaderDateTime>),
URLs(Vec<String>),
}
#[must_use]
pub fn parse_header_typed(form: HeaderForm, raw_value: &[u8]) -> HeaderValueTyped {
if matches!(form, HeaderForm::Raw) {
let s = std::str::from_utf8(raw_value).unwrap_or("").trim();
return HeaderValueTyped::Raw(s.to_owned());
}
let owned: Vec<u8>;
let buf: &[u8] = if raw_value.ends_with(b"\r\n") {
raw_value
} else if raw_value.ends_with(b"\n") {
owned = raw_value
.split_last()
.map(|(_, head)| {
let mut v = Vec::with_capacity(head.len() + 2);
v.extend_from_slice(head);
v.extend_from_slice(b"\r\n");
v
})
.unwrap_or_else(|| b"\r\n".to_vec());
&owned
} else {
owned = {
let mut v = Vec::with_capacity(raw_value.len() + 2);
v.extend_from_slice(raw_value);
v.extend_from_slice(b"\r\n");
v
};
&owned
};
match form {
HeaderForm::Raw => unreachable!("handled above"),
HeaderForm::Addresses => {
let hv = MessageStream::new(buf).parse_address();
HeaderValueTyped::Addresses(flatten_addresses(&hv))
}
HeaderForm::GroupedAddresses => {
let hv = MessageStream::new(buf).parse_address();
HeaderValueTyped::GroupedAddresses(group_addresses(&hv))
}
HeaderForm::MessageIds => {
let hv = MessageStream::new(buf).parse_id();
HeaderValueTyped::MessageIds(text_list(&hv))
}
HeaderForm::Date => {
let hv = MessageStream::new(buf).parse_date();
let dt = match hv {
HeaderValue::DateTime(dt) if dt.year != 0 && dt.month != 0 && dt.day != 0 => {
Some(HeaderDateTime::from_mail_parser(&dt))
}
_ => None,
};
HeaderValueTyped::DateTime(dt)
}
HeaderForm::URLs => {
let hv = MessageStream::new(buf).parse_address();
HeaderValueTyped::URLs(extract_urls(&hv))
}
}
}
fn convert_addr(addr: &mail_parser::Addr<'_>) -> EmailAddress {
let name = addr.name.as_ref().and_then(|s| {
let trimmed = s.as_ref().trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_owned())
}
});
EmailAddress {
name,
address: addr.address.as_ref().map(|s| s.as_ref().to_owned()),
}
}
fn flatten_addresses(hv: &HeaderValue<'_>) -> Vec<EmailAddress> {
match hv {
HeaderValue::Address(Address::List(list)) => list.iter().map(convert_addr).collect(),
HeaderValue::Address(Address::Group(groups)) => groups
.iter()
.flat_map(|g| g.addresses.iter().map(convert_addr))
.collect(),
_ => Vec::new(),
}
}
fn group_addresses(hv: &HeaderValue<'_>) -> Vec<AddressGroup> {
match hv {
HeaderValue::Address(Address::List(list)) if !list.is_empty() => {
vec![AddressGroup {
name: None,
addresses: list.iter().map(convert_addr).collect(),
}]
}
HeaderValue::Address(Address::Group(groups)) => groups
.iter()
.map(|g| AddressGroup {
name: g.name.as_ref().and_then(|s| {
let trimmed = s.as_ref().trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_owned())
}
}),
addresses: g.addresses.iter().map(convert_addr).collect(),
})
.collect(),
_ => Vec::new(),
}
}
fn text_list(hv: &HeaderValue<'_>) -> Vec<String> {
match hv {
HeaderValue::Text(s) => vec![s.as_ref().to_owned()],
HeaderValue::TextList(list) => list.iter().map(|s| s.as_ref().to_owned()).collect(),
_ => Vec::new(),
}
}
fn extract_urls(hv: &HeaderValue<'_>) -> Vec<String> {
match hv {
HeaderValue::Address(Address::List(list)) => list
.iter()
.filter_map(|a| a.address.as_ref().map(|s| s.as_ref().to_owned()))
.collect(),
HeaderValue::Address(Address::Group(groups)) => groups
.iter()
.flat_map(|g| {
g.addresses
.iter()
.filter_map(|a| a.address.as_ref().map(|s| s.as_ref().to_owned()))
})
.collect(),
_ => Vec::new(),
}
}