use mail_auth::spf::verify::SpfParameters; use mail_auth::{MessageAuthenticator, SpfResult as MailAuthSpfResult}; use serde::{Deserialize, Serialize}; use std::net::IpAddr; use crate::error::Result; /// SPF verification result. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SpfResult { /// The SPF result: "pass", "fail", "softfail", "neutral", "temperror", "permerror", "none". pub result: String, /// The domain that was checked. pub domain: String, /// The IP address that was checked. pub ip: String, /// Optional explanation string from the SPF record. pub explanation: Option, } impl SpfResult { /// Whether the SPF check passed. pub fn passed(&self) -> bool { self.result == "pass" } } /// Check SPF for a given sender IP, HELO domain, and MAIL FROM address. /// /// * `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") pub async fn check_spf( ip: IpAddr, helo_domain: &str, host_domain: &str, mail_from: &str, authenticator: &MessageAuthenticator, ) -> Result { let output = authenticator .verify_spf(SpfParameters::verify_mail_from( ip, helo_domain, host_domain, mail_from, )) .await; let result_str = match output.result() { MailAuthSpfResult::Pass => "pass", MailAuthSpfResult::Fail => "fail", MailAuthSpfResult::SoftFail => "softfail", MailAuthSpfResult::Neutral => "neutral", MailAuthSpfResult::TempError => "temperror", MailAuthSpfResult::PermError => "permerror", MailAuthSpfResult::None => "none", }; Ok(SpfResult { result: result_str.to_string(), domain: output.domain().to_string(), ip: ip.to_string(), explanation: output.explanation().map(|s| s.to_string()), }) } /// Check SPF for the EHLO identity (before MAIL FROM). pub async fn check_spf_ehlo( ip: IpAddr, helo_domain: &str, host_domain: &str, authenticator: &MessageAuthenticator, ) -> Result { let output = authenticator .verify_spf(SpfParameters::verify_ehlo(ip, helo_domain, host_domain)) .await; let result_str = match output.result() { MailAuthSpfResult::Pass => "pass", MailAuthSpfResult::Fail => "fail", MailAuthSpfResult::SoftFail => "softfail", MailAuthSpfResult::Neutral => "neutral", MailAuthSpfResult::TempError => "temperror", MailAuthSpfResult::PermError => "permerror", MailAuthSpfResult::None => "none", }; Ok(SpfResult { result: result_str.to_string(), domain: helo_domain.to_string(), ip: ip.to_string(), explanation: output.explanation().map(|s| s.to_string()), }) } /// Build a Received-SPF header value. pub fn received_spf_header(result: &SpfResult) -> String { format!( "{} (domain of {} designates {} as permitted sender) receiver={}; client-ip={};", result.result, result.domain, result.ip, result.domain, result.ip, ) } #[cfg(test)] mod tests { use super::*; #[test] fn test_spf_result_passed() { let result = SpfResult { result: "pass".to_string(), domain: "example.com".to_string(), ip: "1.2.3.4".to_string(), explanation: None, }; assert!(result.passed()); let result = SpfResult { result: "fail".to_string(), domain: "example.com".to_string(), ip: "1.2.3.4".to_string(), explanation: None, }; assert!(!result.passed()); } #[test] fn test_received_spf_header() { let result = SpfResult { result: "pass".to_string(), domain: "example.com".to_string(), ip: "1.2.3.4".to_string(), explanation: None, }; let header = received_spf_header(&result); assert!(header.contains("pass")); assert!(header.contains("example.com")); assert!(header.contains("1.2.3.4")); } }