128 lines
4.1 KiB
Rust
128 lines
4.1 KiB
Rust
|
|
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<Policy> 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<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 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<DmarcResult> {
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|