146 lines
4.2 KiB
Rust
146 lines
4.2 KiB
Rust
|
|
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<String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
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<SpfResult> {
|
||
|
|
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<SpfResult> {
|
||
|
|
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"));
|
||
|
|
}
|
||
|
|
}
|