#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Envelope {
pub date: Option<String>,
pub subject: Option<String>,
pub from: Vec<EnvelopeAddress>,
pub sender: Vec<EnvelopeAddress>,
pub reply_to: Vec<EnvelopeAddress>,
pub to: Vec<EnvelopeAddress>,
pub cc: Vec<EnvelopeAddress>,
pub bcc: Vec<EnvelopeAddress>,
pub in_reply_to: Option<String>,
pub message_id: Option<String>,
}
impl Envelope {
pub fn first_in_reply_to(&self) -> Option<&str> {
let raw = self.in_reply_to.as_deref()?;
if let Some(id) = first_structural_angle_token(raw) {
return Some(id);
}
if find_structural_angle(raw, 0, b'<').is_some() {
return None;
}
let trimmed = raw.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
pub fn bare_message_id(&self) -> Option<&str> {
let raw = self.message_id.as_deref()?;
if let Some(id) = first_structural_angle_token(raw) {
return Some(id);
}
if find_structural_angle(raw, 0, b'<').is_some() {
return None;
}
let trimmed = raw.trim();
(!trimmed.is_empty()).then_some(trimmed)
}
}
fn first_structural_angle_token(raw: &str) -> Option<&str> {
let mut offset = 0usize;
while let Some(start) = find_structural_angle(raw, offset, b'<') {
let Some(end) = find_structural_angle(raw, start + 1, b'>') else {
break;
};
let inner = raw[start + 1..end].trim();
if !inner.is_empty() {
return Some(inner);
}
offset = end + 1;
}
None
}
fn find_structural_angle(raw: &str, start: usize, target: u8) -> Option<usize> {
let bytes = raw.as_bytes();
let mut idx = start;
let mut comment_depth = 0u32;
let mut in_quotes = false;
let mut escaped = false;
while idx < bytes.len() {
let byte = bytes[idx];
if escaped {
escaped = false;
idx += 1;
continue;
}
if in_quotes {
match byte {
b'\\' => escaped = true,
b'"' => in_quotes = false,
_ => {}
}
idx += 1;
continue;
}
if comment_depth > 0 {
match byte {
b'\\' => escaped = true,
b'(' => comment_depth = comment_depth.saturating_add(1),
b')' => comment_depth = comment_depth.saturating_sub(1),
_ => {}
}
idx += 1;
continue;
}
match byte {
b'"' => in_quotes = true,
b'(' => comment_depth = 1,
_ if byte == target => return Some(idx),
_ => {}
}
idx += 1;
}
None
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EnvelopeAddress {
pub name: Option<String>,
pub adl: Option<String>,
pub mailbox: Option<String>,
pub host: Option<String>,
}
impl EnvelopeAddress {
pub fn email(&self) -> Option<String> {
match (&self.mailbox, &self.host) {
(Some(m), Some(h)) if !m.is_empty() && !h.is_empty() => Some(format!("{m}@{h}")),
_ => None,
}
}
pub fn is_group_start(&self) -> bool {
self.host.is_none() && self.mailbox.is_some()
}
pub fn is_group_end(&self) -> bool {
self.host.is_none() && self.mailbox.is_none()
}
pub fn is_address(&self) -> bool {
self.host.is_some()
}
pub fn to_message_address(&self) -> Option<daaki_message::Address> {
let email = self.email()?;
Some(daaki_message::Address::new_unchecked(
self.name.clone(),
email,
))
}
}
impl From<&EnvelopeAddress> for daaki_message::Address {
fn from(addr: &EnvelopeAddress) -> Self {
Self::new_unchecked(addr.name.clone(), addr.email().unwrap_or_default())
}
}
impl From<EnvelopeAddress> for daaki_message::Address {
fn from(addr: EnvelopeAddress) -> Self {
let email = addr.email().unwrap_or_default();
Self::new_unchecked(addr.name, email)
}
}
#[cfg(test)]
#[path = "envelope_tests.rs"]
mod tests;