use crate::align::check as align_check;
use crate::policy::{DmarcPolicy, PolicyAction};
#[derive(Debug, Clone)]
pub struct DkimSignatureResult {
pub d_domain: String,
pub pass: bool,
}
#[derive(Debug, Clone)]
pub struct SpfResult {
pub domain: String,
pub pass: bool,
}
#[derive(Debug, Clone)]
pub struct DmarcInput {
pub from_domain: String,
pub policy_domain: String,
pub spf: Option<SpfResult>,
pub dkim: Vec<DkimSignatureResult>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DmarcOutcome {
pub aligned_spf_pass: bool,
pub aligned_dkim_pass: bool,
pub dmarc_pass: bool,
pub disposition: PolicyAction,
pub reason: String,
pub pct: u8,
}
fn pick_disposition(input: &DmarcInput, policy: &DmarcPolicy) -> PolicyAction {
let from = input.from_domain.trim().trim_end_matches('.').to_ascii_lowercase();
let pol = input.policy_domain.trim().trim_end_matches('.').to_ascii_lowercase();
if from == pol {
policy.policy
} else {
policy.subdomain_policy
}
}
pub fn evaluate(policy: &DmarcPolicy, input: &DmarcInput) -> DmarcOutcome {
let aligned_spf_pass = match input.spf.as_ref() {
Some(spf) if spf.pass => align_check(&spf.domain, &input.from_domain, policy.aspf).is_aligned(),
_ => false,
};
let aligned_dkim_pass = input.dkim.iter().any(|sig| {
sig.pass && align_check(&sig.d_domain, &input.from_domain, policy.adkim).is_aligned()
});
let dmarc_pass = aligned_spf_pass || aligned_dkim_pass;
let disposition = if dmarc_pass {
PolicyAction::None
} else {
pick_disposition(input, policy)
};
let reason = format_reason(policy, input, aligned_spf_pass, aligned_dkim_pass);
DmarcOutcome {
aligned_spf_pass,
aligned_dkim_pass,
dmarc_pass,
disposition,
reason,
pct: policy.pct,
}
}
fn format_reason(
policy: &DmarcPolicy,
input: &DmarcInput,
spf_pass: bool,
dkim_pass: bool,
) -> String {
let mut s = String::with_capacity(64);
if spf_pass {
s.push_str("aligned-spf=pass");
} else if let Some(spf) = input.spf.as_ref() {
s.push_str(if spf.pass {
"aligned-spf=misaligned"
} else {
"aligned-spf=fail"
});
} else {
s.push_str("aligned-spf=absent");
}
s.push_str("; ");
if dkim_pass {
s.push_str("aligned-dkim=pass");
} else if input.dkim.iter().any(|d| d.pass) {
s.push_str("aligned-dkim=misaligned");
} else if input.dkim.is_empty() {
s.push_str("aligned-dkim=absent");
} else {
s.push_str("aligned-dkim=fail");
}
s.push_str(&format!(
"; p={}, sp={}, adkim={}, aspf={}, pct={}",
policy.policy, policy.subdomain_policy, policy.adkim, policy.aspf, policy.pct
));
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::Alignment;
fn policy_with(p: PolicyAction) -> DmarcPolicy {
DmarcPolicy {
policy: p,
subdomain_policy: p,
..DmarcPolicy::default()
}
}
fn input_from(from: &str, policy_domain: &str) -> DmarcInput {
DmarcInput {
from_domain: from.into(),
policy_domain: policy_domain.into(),
spf: None,
dkim: vec![],
}
}
#[test]
fn pass_via_aligned_spf_only() {
let mut input = input_from("example.com", "example.com");
input.spf = Some(SpfResult {
domain: "example.com".into(),
pass: true,
});
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(out.aligned_spf_pass);
assert!(!out.aligned_dkim_pass);
assert!(out.dmarc_pass);
assert_eq!(out.disposition, PolicyAction::None);
}
#[test]
fn pass_via_aligned_dkim_only() {
let mut input = input_from("example.com", "example.com");
input.dkim = vec![DkimSignatureResult {
d_domain: "example.com".into(),
pass: true,
}];
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(!out.aligned_spf_pass);
assert!(out.aligned_dkim_pass);
assert!(out.dmarc_pass);
}
#[test]
fn fail_when_spf_misaligned() {
let mut input = input_from("example.com", "example.com");
input.spf = Some(SpfResult {
domain: "different.com".into(),
pass: true,
});
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(!out.aligned_spf_pass);
assert!(!out.dmarc_pass);
assert_eq!(out.disposition, PolicyAction::Reject);
}
#[test]
fn fail_when_dkim_misaligned() {
let mut input = input_from("example.com", "example.com");
input.dkim = vec![DkimSignatureResult {
d_domain: "attacker.com".into(),
pass: true,
}];
let out = evaluate(&policy_with(PolicyAction::Quarantine), &input);
assert!(!out.aligned_dkim_pass);
assert!(!out.dmarc_pass);
assert_eq!(out.disposition, PolicyAction::Quarantine);
}
#[test]
fn fail_when_spf_fail_but_aligned() {
let mut input = input_from("example.com", "example.com");
input.spf = Some(SpfResult {
domain: "example.com".into(),
pass: false,
});
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(!out.aligned_spf_pass);
assert!(!out.dmarc_pass);
}
#[test]
fn relaxed_alignment_subdomain_passes() {
let mut input = input_from("example.com", "example.com");
input.spf = Some(SpfResult {
domain: "mail.example.com".into(),
pass: true,
});
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(out.aligned_spf_pass);
}
#[test]
fn strict_alignment_subdomain_fails() {
let p = DmarcPolicy {
policy: PolicyAction::Reject,
subdomain_policy: PolicyAction::Reject,
aspf: Alignment::Strict,
adkim: Alignment::Strict,
..DmarcPolicy::default()
};
let mut input = input_from("example.com", "example.com");
input.spf = Some(SpfResult {
domain: "mail.example.com".into(),
pass: true,
});
let out = evaluate(&p, &input);
assert!(!out.aligned_spf_pass);
assert_eq!(out.disposition, PolicyAction::Reject);
}
#[test]
fn subdomain_uses_sp_policy() {
let p = DmarcPolicy {
policy: PolicyAction::Reject,
subdomain_policy: PolicyAction::Quarantine,
..DmarcPolicy::default()
};
let input = input_from("sub.example.com", "example.com");
let out = evaluate(&p, &input);
assert!(!out.dmarc_pass);
assert_eq!(out.disposition, PolicyAction::Quarantine);
}
#[test]
fn dkim_pass_wins_even_when_spf_fails() {
let mut input = input_from("example.com", "example.com");
input.spf = Some(SpfResult {
domain: "wrong.com".into(),
pass: true,
});
input.dkim = vec![DkimSignatureResult {
d_domain: "mail.example.com".into(),
pass: true,
}];
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(!out.aligned_spf_pass);
assert!(out.aligned_dkim_pass);
assert!(out.dmarc_pass);
}
#[test]
fn first_passing_aligned_dkim_signature_wins() {
let mut input = input_from("example.com", "example.com");
input.dkim = vec![
DkimSignatureResult {
d_domain: "attacker.com".into(),
pass: true,
},
DkimSignatureResult {
d_domain: "example.com".into(),
pass: true,
},
];
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(out.aligned_dkim_pass);
}
#[test]
fn dkim_signatures_that_dont_pass_dont_count() {
let mut input = input_from("example.com", "example.com");
input.dkim = vec![DkimSignatureResult {
d_domain: "example.com".into(),
pass: false,
}];
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(!out.aligned_dkim_pass);
}
#[test]
fn no_auth_data_fails_dmarc() {
let input = input_from("example.com", "example.com");
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(!out.dmarc_pass);
assert_eq!(out.disposition, PolicyAction::Reject);
}
#[test]
fn reason_string_captures_state() {
let mut input = input_from("example.com", "example.com");
input.spf = Some(SpfResult {
domain: "example.com".into(),
pass: true,
});
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(out.reason.contains("aligned-spf=pass"));
assert!(out.reason.contains("aligned-dkim=absent"));
assert!(out.reason.contains("p=reject"));
}
#[test]
fn pct_passes_through() {
let p = DmarcPolicy {
policy: PolicyAction::Reject,
pct: 25,
..DmarcPolicy::default()
};
let input = input_from("example.com", "example.com");
let out = evaluate(&p, &input);
assert_eq!(out.pct, 25);
}
#[test]
fn relaxed_default_co_uk_subdomain_aligns() {
let mut input = input_from("example.co.uk", "example.co.uk");
input.spf = Some(SpfResult {
domain: "mail.example.co.uk".into(),
pass: true,
});
let out = evaluate(&policy_with(PolicyAction::Reject), &input);
assert!(out.aligned_spf_pass);
}
}