116 lines
3.7 KiB
Rust
116 lines
3.7 KiB
Rust
//! 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<DkimVerificationResult>,
|
|
pub spf: Option<SpfResult>,
|
|
pub dmarc: Option<DmarcResult>,
|
|
}
|
|
|
|
/// 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<EmailSecurityResult> {
|
|
// 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"));
|
|
}
|
|
}
|