//! Compound email security verification. //! //! Runs DKIM, SPF, and DMARC verification in a single call, avoiding multiple //! IPC round-trips and handling the internal `mail-auth` types that DMARC needs. use mail_auth::spf::verify::SpfParameters; use mail_auth::{AuthenticatedMessage, MessageAuthenticator}; use serde::{Deserialize, Serialize}; use std::net::IpAddr; use crate::dkim::DkimVerificationResult; use crate::dmarc::{check_dmarc, DmarcResult}; use crate::error::{Result, SecurityError}; use crate::spf::SpfResult; /// Combined result of DKIM + SPF + DMARC verification. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailSecurityResult { pub dkim: Vec, pub spf: Option, pub dmarc: Option, } /// Run all email security checks (DKIM, SPF, DMARC) in one call. /// /// This is the preferred entry point for inbound email verification because: /// 1. DMARC requires raw `mail-auth` DKIM/SPF outputs (not our serialized types). /// 2. A single call avoids 3 sequential IPC round-trips. /// /// # Arguments /// * `raw_message` - The raw RFC 5322 message bytes /// * `ip` - The connecting client's IP address /// * `helo_domain` - The domain from the SMTP EHLO/HELO command /// * `host_domain` - Your receiving server's hostname /// * `mail_from` - The full MAIL FROM address (e.g. "sender@example.com") /// * `authenticator` - The `MessageAuthenticator` for DNS lookups pub async fn verify_email_security( raw_message: &[u8], ip: IpAddr, helo_domain: &str, host_domain: &str, mail_from: &str, authenticator: &MessageAuthenticator, ) -> Result { // Parse the message once for all checks let message = AuthenticatedMessage::parse(raw_message) .ok_or_else(|| SecurityError::Parse("Failed to parse email message".into()))?; // --- DKIM verification --- let dkim_outputs = authenticator.verify_dkim(&message).await; let dkim_results = crate::dkim::dkim_outputs_to_results(&dkim_outputs); // --- SPF verification --- let spf_output = authenticator .verify_spf(SpfParameters::verify_mail_from( ip, helo_domain, host_domain, mail_from, )) .await; let spf_result = SpfResult::from_output(&spf_output, ip); // --- DMARC verification (needs raw dkim_outputs + spf_output) --- let mail_from_domain = mail_from .rsplit_once('@') .map(|(_, d)| d) .unwrap_or(helo_domain); let dmarc_result = check_dmarc( raw_message, &dkim_outputs, &spf_output, mail_from_domain, authenticator, ) .await .ok(); // DMARC failure is non-fatal; we still return DKIM + SPF results Ok(EmailSecurityResult { dkim: dkim_results, spf: Some(spf_result), dmarc: dmarc_result, }) } #[cfg(test)] mod tests { use super::*; #[test] fn test_email_security_result_serialization() { let result = EmailSecurityResult { dkim: vec![DkimVerificationResult { is_valid: false, domain: None, selector: None, status: "none".to_string(), details: Some("No DKIM signatures".to_string()), }], spf: Some(SpfResult { result: "none".to_string(), domain: "example.com".to_string(), ip: "1.2.3.4".to_string(), explanation: None, }), dmarc: None, }; let json = serde_json::to_string(&result).unwrap(); assert!(json.contains("\"dkim\"")); assert!(json.contains("\"spf\"")); assert!(json.contains("\"dmarc\":null")); } }