use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::api::{check_dns, DnsCheckRequest};
use crate::resolver::RecordData;
pub async fn check_email_security(req: &EmailSecurityRequest) -> Result<EmailSecurityResult> {
let dns_req = DnsCheckRequest {
domain: req.domain.clone(),
record_types: vec!["MX".to_string(), "TXT".to_string()],
timeout_secs: req.timeout_secs,
..Default::default()
};
let dns_results = check_dns(&dns_req).await?;
let mut mx_records = Vec::new();
let mut spf_raw = None;
let mut dmarc_raw = None;
for result in &dns_results {
if result.query.record_type == "MX" {
for record in &result.answers {
if let RecordData::Mx { priority, exchange } = &record.data {
mx_records.push(MxEntry { priority: *priority, exchange: exchange.clone() });
}
}
} else if result.query.record_type == "TXT" {
for record in &result.answers {
if let RecordData::Txt(texts) = &record.data {
for text in texts {
if text.starts_with("v=spf1") {
spf_raw = Some(text.clone());
} else if text.starts_with("v=DMARC1") {
dmarc_raw = Some(text.clone());
}
}
}
}
}
}
let dmarc_req = DnsCheckRequest {
domain: format!("_dmarc.{}", req.domain),
record_types: vec!["TXT".to_string()],
timeout_secs: req.timeout_secs,
..Default::default()
};
if let Ok(dmarc_results) = check_dns(&dmarc_req).await {
if let Some(result) = dmarc_results.first() {
for record in &result.answers {
if let RecordData::Txt(texts) = &record.data {
for text in texts {
if text.starts_with("v=DMARC1") {
dmarc_raw = Some(text.clone());
}
}
}
}
}
}
let mx_valid = !mx_records.is_empty();
let spf_valid = spf_raw.is_some();
let dmarc_policy = parse_dmarc_policy(&dmarc_raw);
let dmarc_valid = dmarc_policy.is_some();
let mut score: u8 = 0;
if mx_valid { score += 25; }
if spf_valid { score += 25; }
if dmarc_policy != Some(DmarcPolicy::None) && dmarc_policy.is_some() { score += 25; }
let mut dkim_results = Vec::new();
for selector in &req.dkim_selectors {
let dkim_req = DnsCheckRequest {
domain: format!("{}._domainkey.{}", selector, req.domain),
record_types: vec!["TXT".to_string()],
timeout_secs: req.timeout_secs,
..Default::default()
};
let present = check_dns(&dkim_req).await
.ok()
.map(|r| !r.is_empty() && !r[0].answers.is_empty())
.unwrap_or(false);
if present { score += 6; } dkim_results.push(DkimCheckResult {
selector: selector.clone(),
present,
raw: None,
});
}
score = score.min(100);
Ok(EmailSecurityResult {
domain: req.domain.clone(),
mx: MxCheckResult { records: mx_records, valid: mx_valid },
spf: SpfCheckResult { raw: spf_raw, valid: spf_valid, issues: vec![] },
dmarc: DmarcCheckResult { raw: dmarc_raw, policy: dmarc_policy, valid: dmarc_valid, issues: vec![] },
dkim: dkim_results,
score,
})
}
fn parse_dmarc_policy(raw: &Option<String>) -> Option<DmarcPolicy> {
raw.as_ref().and_then(|s| {
if s.contains("p=reject") {
Some(DmarcPolicy::Reject)
} else if s.contains("p=quarantine") {
Some(DmarcPolicy::Quarantine)
} else if s.contains("p=none") {
Some(DmarcPolicy::None)
} else {
None
}
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailSecurityRequest {
pub domain: String,
pub timeout_secs: u64,
#[serde(default = "default_dkim_selectors")]
pub dkim_selectors: Vec<String>,
}
fn default_dkim_selectors() -> Vec<String> {
vec!["default".to_string(), "google".to_string(), "selector1".to_string(), "selector2".to_string()]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailSecurityResult {
pub domain: String,
pub mx: MxCheckResult,
pub spf: SpfCheckResult,
pub dmarc: DmarcCheckResult,
pub dkim: Vec<DkimCheckResult>,
pub score: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MxCheckResult {
pub records: Vec<MxEntry>,
pub valid: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MxEntry {
pub priority: u16,
pub exchange: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpfCheckResult {
pub raw: Option<String>,
pub valid: bool,
pub issues: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DmarcCheckResult {
pub raw: Option<String>,
pub policy: Option<DmarcPolicy>,
pub valid: bool,
pub issues: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DmarcPolicy {
None,
Quarantine,
Reject,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DkimCheckResult {
pub selector: String,
pub present: bool,
pub raw: Option<String>,
}