use mail_auth::dmarc::verify::DmarcParameters; use mail_auth::dmarc::Policy; use mail_auth::{ AuthenticatedMessage, DkimOutput, DmarcResult as MailAuthDmarcResult, MessageAuthenticator, SpfOutput, }; use serde::{Deserialize, Serialize}; use crate::error::{Result, SecurityError}; /// DMARC policy. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum DmarcPolicy { None, Quarantine, Reject, } impl From for DmarcPolicy { fn from(p: Policy) -> Self { match p { Policy::None | Policy::Unspecified => DmarcPolicy::None, Policy::Quarantine => DmarcPolicy::Quarantine, Policy::Reject => DmarcPolicy::Reject, } } } /// DMARC verification result. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DmarcResult { /// Whether DMARC verification passed overall. pub passed: bool, /// The evaluated policy. pub policy: DmarcPolicy, /// The domain that was checked. pub domain: String, /// DKIM alignment result: "pass", "fail", etc. pub dkim_result: String, /// SPF alignment result: "pass", "fail", etc. pub spf_result: String, /// Recommended action: "pass", "quarantine", "reject". pub action: String, /// Human-readable details. pub details: Option, } /// Check DMARC for an email, given prior DKIM and SPF results. /// /// * `raw_message` - The raw RFC 5322 message bytes /// * `dkim_output` - DKIM verification results from `verify_dkim` /// * `spf_output` - SPF verification output from `check_spf` /// * `mail_from_domain` - The MAIL FROM domain (RFC 5321) /// * `authenticator` - The MessageAuthenticator for DNS lookups pub async fn check_dmarc<'x>( raw_message: &'x [u8], dkim_output: &'x [DkimOutput<'x>], spf_output: &'x SpfOutput, mail_from_domain: &'x str, authenticator: &MessageAuthenticator, ) -> Result { let message = AuthenticatedMessage::parse(raw_message) .ok_or_else(|| SecurityError::Parse("Failed to parse email for DMARC check".into()))?; let dmarc_output = authenticator .verify_dmarc( DmarcParameters::new(&message, dkim_output, mail_from_domain, spf_output) .with_domain_suffix_fn(|domain| psl::domain_str(domain).unwrap_or(domain)), ) .await; let policy = DmarcPolicy::from(dmarc_output.policy()); let domain = dmarc_output.domain().to_string(); let dkim_result_str = dmarc_result_to_string(dmarc_output.dkim_result()); let spf_result_str = dmarc_result_to_string(dmarc_output.spf_result()); let dkim_passed = matches!(dmarc_output.dkim_result(), MailAuthDmarcResult::Pass); let spf_passed = matches!(dmarc_output.spf_result(), MailAuthDmarcResult::Pass); let passed = dkim_passed || spf_passed; let action = if passed { "pass".to_string() } else { match policy { DmarcPolicy::None => "pass".to_string(), // p=none means monitor only DmarcPolicy::Quarantine => "quarantine".to_string(), DmarcPolicy::Reject => "reject".to_string(), } }; Ok(DmarcResult { passed, policy, domain, dkim_result: dkim_result_str, spf_result: spf_result_str, action, details: None, }) } fn dmarc_result_to_string(result: &MailAuthDmarcResult) -> String { match result { MailAuthDmarcResult::Pass => "pass".to_string(), MailAuthDmarcResult::Fail(err) => format!("fail: {}", err), MailAuthDmarcResult::TempError(err) => format!("temperror: {}", err), MailAuthDmarcResult::PermError(err) => format!("permerror: {}", err), MailAuthDmarcResult::None => "none".to_string(), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_dmarc_policy_from() { assert_eq!(DmarcPolicy::from(Policy::None), DmarcPolicy::None); assert_eq!( DmarcPolicy::from(Policy::Quarantine), DmarcPolicy::Quarantine ); assert_eq!(DmarcPolicy::from(Policy::Reject), DmarcPolicy::Reject); } }