#[derive(Debug, Clone, PartialEq, Eq, Default)]
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(start) = raw.find('<') {
let rest = &raw[start + 1..];
rest.find('>')
.map(|end| &rest[..end])
.map(str::trim)
.filter(|s| !s.is_empty())
} else {
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()?;
let trimmed = raw.trim();
trimmed
.strip_prefix('<')
.and_then(|s| s.strip_suffix('>'))
.map(str::trim)
.or(Some(trimmed))
.filter(|s| !s.is_empty())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
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 {
name: self.name.clone(),
email,
})
}
}
impl From<&EnvelopeAddress> for daaki_message::Address {
fn from(addr: &EnvelopeAddress) -> Self {
Self {
name: addr.name.clone(),
email: addr.email().unwrap_or_default(),
}
}
}
impl From<EnvelopeAddress> for daaki_message::Address {
fn from(addr: EnvelopeAddress) -> Self {
Self {
email: addr.email().unwrap_or_default(),
name: addr.name,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn address_email_both_parts() {
let addr = EnvelopeAddress {
name: Some("Alice".into()),
adl: None,
mailbox: Some("alice".into()),
host: Some("example.com".into()),
};
assert_eq!(addr.email(), Some("alice@example.com".into()));
assert!(addr.is_address());
assert!(!addr.is_group_start());
assert!(!addr.is_group_end());
}
#[test]
fn address_email_missing_host() {
let addr = EnvelopeAddress {
name: None,
adl: None,
mailbox: Some("alice".into()),
host: None,
};
assert_eq!(addr.email(), None);
}
#[test]
fn address_email_missing_mailbox() {
let addr = EnvelopeAddress {
name: None,
adl: None,
mailbox: None,
host: Some("example.com".into()),
};
assert_eq!(addr.email(), None);
}
#[test]
fn default_envelope_is_empty() {
let env = Envelope::default();
assert!(env.date.is_none());
assert!(env.subject.is_none());
assert!(env.from.is_empty());
assert!(env.message_id.is_none());
}
#[test]
fn group_start_marker() {
let addr = EnvelopeAddress {
name: None,
adl: None,
mailbox: Some("Friends".into()),
host: None,
};
assert!(addr.is_group_start());
assert!(!addr.is_group_end());
assert!(!addr.is_address());
assert_eq!(addr.email(), None);
}
#[test]
fn group_end_marker() {
let addr = EnvelopeAddress {
name: None,
adl: None,
mailbox: None,
host: None,
};
assert!(addr.is_group_end());
assert!(!addr.is_group_start());
assert!(!addr.is_address());
}
#[test]
fn bare_message_id_strips_brackets() {
let env = Envelope {
message_id: Some("<abc@example.com>".into()),
..Default::default()
};
assert_eq!(env.bare_message_id(), Some("abc@example.com"));
}
#[test]
fn bare_message_id_no_brackets() {
let env = Envelope {
message_id: Some("abc@example.com".into()),
..Default::default()
};
assert_eq!(env.bare_message_id(), Some("abc@example.com"));
}
#[test]
fn bare_message_id_nil() {
let env = Envelope::default();
assert_eq!(env.bare_message_id(), None);
}
#[test]
fn first_in_reply_to_single() {
let env = Envelope {
in_reply_to: Some("<parent@example.com>".into()),
..Default::default()
};
assert_eq!(env.first_in_reply_to(), Some("parent@example.com"));
}
#[test]
fn first_in_reply_to_multiple() {
let env = Envelope {
in_reply_to: Some("<first@a.com> <second@b.com>".into()),
..Default::default()
};
assert_eq!(env.first_in_reply_to(), Some("first@a.com"));
}
#[test]
fn first_in_reply_to_no_brackets() {
let env = Envelope {
in_reply_to: Some("bare-id@example.com".into()),
..Default::default()
};
assert_eq!(env.first_in_reply_to(), Some("bare-id@example.com"));
}
#[test]
fn first_in_reply_to_nil() {
let env = Envelope::default();
assert_eq!(env.first_in_reply_to(), None);
}
#[test]
fn first_in_reply_to_empty_angle_brackets() {
let env = Envelope {
in_reply_to: Some("<>".into()),
..Default::default()
};
assert_eq!(
env.first_in_reply_to(),
None,
"first_in_reply_to() must return None for empty angle brackets <> \
(RFC 5322 Section 3.6.4)"
);
}
#[test]
fn first_in_reply_to_whitespace_only_in_brackets() {
let env = Envelope {
in_reply_to: Some("< >".into()),
..Default::default()
};
assert_eq!(
env.first_in_reply_to(),
None,
"first_in_reply_to() must return None for whitespace-only angle brackets < > \
(RFC 5322 Section 3.6.4)"
);
}
#[test]
fn first_in_reply_to_single_space_in_brackets() {
let env = Envelope {
in_reply_to: Some("< >".into()),
..Default::default()
};
assert_eq!(
env.first_in_reply_to(),
None,
"first_in_reply_to() must return None for single-space angle brackets < > \
(RFC 5322 Section 3.6.4)"
);
}
#[test]
fn first_in_reply_to_trims_whitespace() {
let env = Envelope {
in_reply_to: Some("< real@example.com >".into()),
..Default::default()
};
assert_eq!(
env.first_in_reply_to(),
Some("real@example.com"),
"first_in_reply_to() must trim whitespace inside angle brackets \
(RFC 5322 Section 3.6.4)"
);
}
#[test]
fn first_in_reply_to_normal_bracketed() {
let env = Envelope {
in_reply_to: Some("<real@example.com>".into()),
..Default::default()
};
assert_eq!(env.first_in_reply_to(), Some("real@example.com"));
}
#[test]
fn bare_message_id_empty_angle_brackets() {
let env = Envelope {
message_id: Some("<>".into()),
..Default::default()
};
assert_eq!(
env.bare_message_id(),
None,
"bare_message_id() must return None for empty angle brackets <>"
);
}
#[test]
fn bare_message_id_whitespace_only_in_brackets() {
let env = Envelope {
message_id: Some("< >".into()),
..Default::default()
};
assert_eq!(
env.bare_message_id(),
None,
"bare_message_id() must return None for whitespace-only angle brackets < > \
(RFC 5322 Section 3.6.4)"
);
}
#[test]
fn bare_message_id_multi_space_in_brackets() {
let env = Envelope {
message_id: Some("< >".into()),
..Default::default()
};
assert_eq!(
env.bare_message_id(),
None,
"bare_message_id() must return None for multi-space angle brackets < > \
(RFC 5322 Section 3.6.4)"
);
}
#[test]
fn address_email_rejects_empty_strings() {
let both_empty = EnvelopeAddress {
name: None,
adl: None,
mailbox: Some(String::new()),
host: Some(String::new()),
};
assert_eq!(
both_empty.email(),
None,
"email() must return None when both mailbox and host are empty strings \
(RFC 5322 Section 3.4.1: addr-spec = local-part '@' domain)"
);
let empty_mailbox = EnvelopeAddress {
name: None,
adl: None,
mailbox: Some(String::new()),
host: Some("example.com".into()),
};
assert_eq!(
empty_mailbox.email(),
None,
"email() must return None when mailbox is empty \
(RFC 5322 Section 3.4.1)"
);
let empty_host = EnvelopeAddress {
name: None,
adl: None,
mailbox: Some("alice".into()),
host: Some(String::new()),
};
assert_eq!(
empty_host.email(),
None,
"email() must return None when host is empty \
(RFC 5322 Section 3.4.1)"
);
}
#[test]
fn bare_message_id_trims_whitespace() {
let env = Envelope {
message_id: Some("< real@example.com >".into()),
..Default::default()
};
assert_eq!(
env.bare_message_id(),
Some("real@example.com"),
"bare_message_id() must trim whitespace inside angle brackets \
(RFC 5322 Section 3.6.4)"
);
}
#[test]
fn envelope_address_is_distinct_from_message_address() {
let env_addr = EnvelopeAddress {
name: Some("Alice".into()),
adl: None,
mailbox: Some("alice".into()),
host: Some("example.com".into()),
};
assert_eq!(env_addr.name.as_deref(), Some("Alice"));
assert!(env_addr.adl.is_none());
assert_eq!(env_addr.mailbox.as_deref(), Some("alice"));
assert_eq!(env_addr.host.as_deref(), Some("example.com"));
let msg_addr = daaki_message::Address {
name: Some("Alice".into()),
email: "alice@example.com".into(),
};
assert_eq!(msg_addr.name.as_deref(), Some("Alice"));
assert_eq!(msg_addr.email, "alice@example.com");
}
#[test]
fn to_message_address_normal() {
let addr = EnvelopeAddress {
name: Some("Alice".into()),
adl: None,
mailbox: Some("alice".into()),
host: Some("example.com".into()),
};
let msg_addr = addr.to_message_address().unwrap();
assert_eq!(msg_addr.name.as_deref(), Some("Alice"));
assert_eq!(msg_addr.email, "alice@example.com");
}
#[test]
fn to_message_address_group_marker_returns_none() {
let group_start = EnvelopeAddress {
name: None,
adl: None,
mailbox: Some("undisclosed".into()),
host: None,
};
assert!(
group_start.to_message_address().is_none(),
"group start marker must return None"
);
}
#[test]
fn from_ref_conversion() {
let addr = EnvelopeAddress {
name: Some("Bob".into()),
adl: None,
mailbox: Some("bob".into()),
host: Some("test.com".into()),
};
let msg_addr: daaki_message::Address = (&addr).into();
assert_eq!(msg_addr.name.as_deref(), Some("Bob"));
assert_eq!(msg_addr.email, "bob@test.com");
}
#[test]
fn from_owned_conversion() {
let addr = EnvelopeAddress {
name: None,
adl: None,
mailbox: Some("user".into()),
host: Some("domain.org".into()),
};
let msg_addr: daaki_message::Address = addr.into();
assert!(msg_addr.name.is_none());
assert_eq!(msg_addr.email, "user@domain.org");
}
#[test]
fn from_conversion_group_marker_empty_email() {
let addr = EnvelopeAddress {
name: None,
adl: None,
mailbox: None,
host: None,
};
let msg_addr: daaki_message::Address = (&addr).into();
assert!(msg_addr.email.is_empty());
}
#[test]
fn from_ref_and_owned_match_for_empty_parts() {
let cases = [
EnvelopeAddress {
name: Some("X".into()),
adl: None,
mailbox: Some(String::new()),
host: Some("h.com".into()),
},
EnvelopeAddress {
name: None,
adl: None,
mailbox: Some("u".into()),
host: Some(String::new()),
},
EnvelopeAddress {
name: None,
adl: None,
mailbox: Some(String::new()),
host: Some(String::new()),
},
EnvelopeAddress {
name: None,
adl: None,
mailbox: None,
host: Some("h.com".into()),
},
];
for addr in &cases {
let from_ref: daaki_message::Address = addr.into();
let from_owned: daaki_message::Address = addr.clone().into();
assert_eq!(
from_ref.email, from_owned.email,
"From<&EnvelopeAddress> and From<EnvelopeAddress> must produce the same email for {addr:?}"
);
assert_eq!(
from_ref.name, from_owned.name,
"From<&EnvelopeAddress> and From<EnvelopeAddress> must produce the same name for {addr:?}"
);
}
}
}