#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AddressPart {
All,
LocalPart,
Domain,
User,
Detail,
}
pub(crate) fn address_part_from_tags(tags: &[String]) -> AddressPart {
for t in tags {
match t.as_str() {
"all" => return AddressPart::All,
"localpart" => return AddressPart::LocalPart,
"domain" => return AddressPart::Domain,
"user" => return AddressPart::User,
"detail" => return AddressPart::Detail,
_ => {}
}
}
AddressPart::All
}
pub(crate) fn scope_to_part(addr: &str, part: AddressPart) -> Option<String> {
match part {
AddressPart::All => Some(addr.to_string()),
AddressPart::LocalPart => Some(local_part(addr).to_string()),
AddressPart::Domain => Some(
addr.split_once('@')
.map(|(_, d)| d.to_string())
.unwrap_or_default(),
),
AddressPart::User => Some(match local_part(addr).split_once('+') {
Some((user, _)) => user.to_string(),
None => local_part(addr).to_string(),
}),
AddressPart::Detail => local_part(addr)
.rsplit_once('+')
.map(|(_, detail)| detail.to_string()),
}
}
fn local_part(addr: &str) -> &str {
addr.split_once('@').map(|(l, _)| l).unwrap_or(addr)
}
pub(crate) fn extract_addresses(value: &str) -> Vec<String> {
let mut out = Vec::new();
for piece in value.split(',') {
let trim = piece.trim();
if let Some(open) = trim.rfind('<')
&& trim.ends_with('>')
{
out.push(trim[open + 1..trim.len() - 1].trim().to_string());
} else if trim.contains('@') {
out.push(trim.to_string());
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn part_default_all() {
assert_eq!(address_part_from_tags(&[]), AddressPart::All);
}
#[test]
fn part_localpart() {
assert_eq!(
address_part_from_tags(&["localpart".into()]),
AddressPart::LocalPart,
);
}
#[test]
fn part_user_distinct_from_localpart() {
assert_eq!(
address_part_from_tags(&["user".into()]),
AddressPart::User,
);
}
#[test]
fn part_detail() {
assert_eq!(
address_part_from_tags(&["detail".into()]),
AddressPart::Detail,
);
}
#[test]
fn scope_local_and_domain() {
assert_eq!(
scope_to_part("a@b.c", AddressPart::LocalPart),
Some("a".into()),
);
assert_eq!(
scope_to_part("a@b.c", AddressPart::Domain),
Some("b.c".into()),
);
assert_eq!(
scope_to_part("a@b.c", AddressPart::All),
Some("a@b.c".into()),
);
}
#[test]
fn scope_user_with_plus_strips_detail() {
assert_eq!(
scope_to_part("alice+work@example.com", AddressPart::User),
Some("alice".into()),
);
}
#[test]
fn scope_user_without_plus_returns_full_localpart() {
assert_eq!(
scope_to_part("alice@example.com", AddressPart::User),
Some("alice".into()),
);
}
#[test]
fn scope_detail_with_plus_returns_after_plus() {
assert_eq!(
scope_to_part("alice+work@example.com", AddressPart::Detail),
Some("work".into()),
);
}
#[test]
fn scope_detail_without_plus_is_undefined() {
assert_eq!(
scope_to_part("alice@example.com", AddressPart::Detail),
None,
);
}
#[test]
fn scope_detail_multi_plus_takes_last_segment() {
assert_eq!(
scope_to_part("alice+work+sub@example.com", AddressPart::Detail),
Some("sub".into()),
);
assert_eq!(
scope_to_part("alice+work+sub@example.com", AddressPart::User),
Some("alice".into()),
);
}
#[test]
fn extract_bare_addr() {
assert_eq!(extract_addresses("alice@example.com"), vec!["alice@example.com"]);
}
#[test]
fn extract_angle_addr() {
assert_eq!(
extract_addresses("Alice <alice@example.com>"),
vec!["alice@example.com"]
);
}
#[test]
fn extract_list_of_addrs() {
assert_eq!(
extract_addresses("bob@x.com, carol@y.com"),
vec!["bob@x.com", "carol@y.com"]
);
}
#[test]
fn extract_quoted_display_name_with_comma() {
assert_eq!(
extract_addresses("\"Alice, Sr.\" <alice@example.com>"),
vec!["alice@example.com"]
);
}
}