use crate::policy::{Policy, PolicyMode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Decision {
Allow,
Deny,
NoPolicy,
}
pub fn policy_url(domain: &str) -> String {
let domain = domain.trim().trim_end_matches('.').to_ascii_lowercase();
format!("https://mta-sts.{domain}/.well-known/mta-sts.txt")
}
pub fn mx_matches(mx_host: &str, pattern: &str) -> bool {
let mx = mx_host.trim().trim_end_matches('.').to_ascii_lowercase();
let pat = pattern.trim().trim_end_matches('.').to_ascii_lowercase();
if let Some(suffix) = pat.strip_prefix("*.") {
let Some(rest) = mx.strip_suffix(&format!(".{suffix}")) else {
return false;
};
!rest.is_empty() && !rest.contains('.')
} else {
mx == pat
}
}
pub fn enforce(policy: &Policy, mx_host: &str) -> Decision {
let matches = policy.mx.iter().any(|p| mx_matches(mx_host, p));
if matches {
return Decision::Allow;
}
match policy.mode {
PolicyMode::Enforce => Decision::Deny,
PolicyMode::Testing | PolicyMode::None => Decision::NoPolicy,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn policy(mode: PolicyMode, mx: &[&str]) -> Policy {
Policy {
mode,
mx: mx
.iter()
.map(|s| compact_str::CompactString::new(s))
.collect(),
max_age: 86400,
}
}
#[test]
fn policy_url_basic() {
assert_eq!(
policy_url("example.com"),
"https://mta-sts.example.com/.well-known/mta-sts.txt"
);
}
#[test]
fn policy_url_lowercases_and_strips_trailing_dot() {
assert_eq!(
policy_url("Example.COM."),
"https://mta-sts.example.com/.well-known/mta-sts.txt"
);
}
#[test]
fn mx_literal_match() {
assert!(mx_matches("mail.example.com", "mail.example.com"));
}
#[test]
fn mx_literal_no_match() {
assert!(!mx_matches("attacker.com", "mail.example.com"));
}
#[test]
fn mx_case_insensitive() {
assert!(mx_matches("Mail.Example.COM", "mail.example.com"));
assert!(mx_matches("mail.example.com", "MAIL.EXAMPLE.COM"));
}
#[test]
fn mx_trailing_dot_ignored() {
assert!(mx_matches("mail.example.com.", "mail.example.com"));
assert!(mx_matches("mail.example.com", "mail.example.com."));
}
#[test]
fn mx_wildcard_matches_one_label() {
assert!(mx_matches("a.example.com", "*.example.com"));
assert!(mx_matches("mail.example.com", "*.example.com"));
}
#[test]
fn mx_wildcard_does_not_match_two_labels() {
assert!(!mx_matches("a.b.example.com", "*.example.com"));
}
#[test]
fn mx_wildcard_does_not_match_bare_suffix() {
assert!(!mx_matches("example.com", "*.example.com"));
}
#[test]
fn mx_wildcard_does_not_match_different_domain() {
assert!(!mx_matches("a.attacker.com", "*.example.com"));
}
#[test]
fn enforce_allows_matching_mx() {
let p = policy(PolicyMode::Enforce, &["mail.example.com"]);
assert_eq!(enforce(&p, "mail.example.com"), Decision::Allow);
}
#[test]
fn enforce_denies_unmatched_mx_in_enforce_mode() {
let p = policy(PolicyMode::Enforce, &["mail.example.com"]);
assert_eq!(enforce(&p, "rogue.example.com"), Decision::Deny);
}
#[test]
fn enforce_returns_nopolicy_in_testing_mode_on_unmatched() {
let p = policy(PolicyMode::Testing, &["mail.example.com"]);
assert_eq!(enforce(&p, "rogue.example.com"), Decision::NoPolicy);
}
#[test]
fn enforce_returns_nopolicy_when_mode_none() {
let p = policy(PolicyMode::None, &["mail.example.com"]);
assert_eq!(enforce(&p, "rogue.example.com"), Decision::NoPolicy);
assert_eq!(enforce(&p, "mail.example.com"), Decision::Allow);
}
#[test]
fn enforce_wildcard_match_in_enforce_mode() {
let p = policy(PolicyMode::Enforce, &["*.example.com"]);
assert_eq!(enforce(&p, "mx1.example.com"), Decision::Allow);
assert_eq!(enforce(&p, "a.b.example.com"), Decision::Deny);
}
#[test]
fn enforce_first_matching_pattern_wins() {
let p = policy(PolicyMode::Enforce, &["backup.example", "*.example.com"]);
assert_eq!(enforce(&p, "backup.example"), Decision::Allow);
assert_eq!(enforce(&p, "any.example.com"), Decision::Allow);
assert_eq!(enforce(&p, "evil.com"), Decision::Deny);
}
}