use crate::policy::Alignment;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AlignmentOutcome {
Aligned,
NotAligned,
InvalidDomain,
}
impl AlignmentOutcome {
pub fn is_aligned(self) -> bool {
matches!(self, AlignmentOutcome::Aligned)
}
}
pub fn check(auth_domain: &str, from_domain: &str, mode: Alignment) -> AlignmentOutcome {
if auth_domain.is_empty() || from_domain.is_empty() {
return AlignmentOutcome::InvalidDomain;
}
let a = auth_domain.trim().trim_end_matches('.').to_ascii_lowercase();
let f = from_domain.trim().trim_end_matches('.').to_ascii_lowercase();
match mode {
Alignment::Strict => {
if a == f {
AlignmentOutcome::Aligned
} else {
AlignmentOutcome::NotAligned
}
}
Alignment::Relaxed => {
let Some(a_org) = organizational_domain(&a) else {
return AlignmentOutcome::InvalidDomain;
};
let Some(f_org) = organizational_domain(&f) else {
return AlignmentOutcome::InvalidDomain;
};
if a_org == f_org {
AlignmentOutcome::Aligned
} else {
AlignmentOutcome::NotAligned
}
}
}
}
pub fn organizational_domain(domain: &str) -> Option<String> {
let domain = domain.trim().trim_end_matches('.');
if domain.is_empty() {
return None;
}
let parsed = psl::domain(domain.as_bytes())?;
let bytes = parsed.as_bytes();
if bytes.is_empty() {
return None;
}
Some(String::from_utf8_lossy(bytes).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strict_alignment_exact_match() {
let r = check("example.com", "example.com", Alignment::Strict);
assert_eq!(r, AlignmentOutcome::Aligned);
}
#[test]
fn strict_alignment_subdomain_fails() {
let r = check("mail.example.com", "example.com", Alignment::Strict);
assert_eq!(r, AlignmentOutcome::NotAligned);
}
#[test]
fn strict_alignment_case_insensitive() {
let r = check("Example.COM", "example.com", Alignment::Strict);
assert_eq!(r, AlignmentOutcome::Aligned);
}
#[test]
fn strict_alignment_trailing_dot_ignored() {
let r = check("example.com.", "example.com", Alignment::Strict);
assert_eq!(r, AlignmentOutcome::Aligned);
}
#[test]
fn relaxed_alignment_subdomain_passes() {
let r = check("mail.example.com", "example.com", Alignment::Relaxed);
assert_eq!(r, AlignmentOutcome::Aligned);
}
#[test]
fn relaxed_alignment_different_orgs_fails() {
let r = check("mail.attacker.com", "example.com", Alignment::Relaxed);
assert_eq!(r, AlignmentOutcome::NotAligned);
}
#[test]
fn relaxed_alignment_handles_psl_double_suffix() {
let r = check("mail.example.co.uk", "example.co.uk", Alignment::Relaxed);
assert_eq!(r, AlignmentOutcome::Aligned);
}
#[test]
fn relaxed_alignment_subdomain_under_double_suffix() {
let r = check(
"smtp.mail.example.co.uk",
"www.example.co.uk",
Alignment::Relaxed,
);
assert_eq!(r, AlignmentOutcome::Aligned);
}
#[test]
fn empty_auth_domain_is_invalid() {
let r = check("", "example.com", Alignment::Relaxed);
assert_eq!(r, AlignmentOutcome::InvalidDomain);
}
#[test]
fn empty_from_domain_is_invalid() {
let r = check("example.com", "", Alignment::Relaxed);
assert_eq!(r, AlignmentOutcome::InvalidDomain);
}
#[test]
fn organizational_domain_basic() {
assert_eq!(
organizational_domain("mail.example.com").as_deref(),
Some("example.com")
);
}
#[test]
fn organizational_domain_co_uk() {
assert_eq!(
organizational_domain("news.example.co.uk").as_deref(),
Some("example.co.uk")
);
}
#[test]
fn organizational_domain_already_orgdomain() {
assert_eq!(
organizational_domain("example.com").as_deref(),
Some("example.com")
);
}
#[test]
fn organizational_domain_empty_returns_none() {
assert_eq!(organizational_domain("").as_deref(), None);
}
#[test]
fn is_aligned_helper() {
assert!(AlignmentOutcome::Aligned.is_aligned());
assert!(!AlignmentOutcome::NotAligned.is_aligned());
assert!(!AlignmentOutcome::InvalidDomain.is_aligned());
}
}