use serde::{Deserialize, Serialize};
use crate::error::Result;
use crate::api::{check_dns, check_email_security, DnsCheckRequest, EmailSecurityRequest};
use std::time::SystemTime;
pub async fn check_bimi(req: &BimiCheckRequest) -> Result<BimiCheckResult> {
let mut bimi_present = false;
let mut version = None;
let mut vmc_url = None;
let bimi_domain = format!("default._bimi.{}", req.domain);
let dns_req = DnsCheckRequest {
domain: bimi_domain,
record_types: vec!["TXT".to_string()],
timeout_secs: req.timeout_secs,
..Default::default()
};
if let Ok(dns_results) = check_dns(&dns_req).await {
for result in dns_results {
for record in &result.answers {
use crate::resolver::RecordData;
if let RecordData::Txt(texts) = &record.data {
for text in texts {
if text.starts_with("v=BIMI1") {
bimi_present = true;
version = Some("BIMI1".to_string());
for part in text.split(';') {
let part = part.trim();
if let Some(url) = part.strip_prefix("l=") {
vmc_url = Some(url.to_string());
}
}
}
}
}
}
}
}
let email_req = EmailSecurityRequest {
domain: req.domain.clone(),
timeout_secs: req.timeout_secs,
dkim_selectors: vec![],
};
let dmarc_aligned = if let Ok(email_result) = check_email_security(&email_req).await {
Some(matches!(
email_result.dmarc.policy,
Some(crate::api::email::DmarcPolicy::Reject) | Some(crate::api::email::DmarcPolicy::Quarantine)
))
} else {
None
};
let vmc_valid = if let Some(ref url) = vmc_url {
validate_vmc_certificate(url).await.ok()
} else {
None
};
let error = if bimi_present && dmarc_aligned == Some(false) {
Some("BIMI present but DMARC policy is not reject/quarantine".to_string())
} else {
None
};
Ok(BimiCheckResult {
domain: req.domain.clone(),
bimi_present,
version,
vmc_url,
dmarc_aligned,
vmc_valid,
error,
})
}
async fn validate_vmc_certificate(url: &str) -> Result<bool> {
let client = reqwest::Client::new();
let response = client.get(url)
.timeout(std::time::Duration::from_secs(10))
.send()
.await
.map_err(|e| crate::error::ShoheError::Transport(format!("VMC fetch failed: {}", e)))?;
if !response.status().is_success() {
return Err(crate::error::ShoheError::Transport(format!("VMC fetch returned {}", response.status())));
}
let cert_bytes = response.bytes()
.await
.map_err(|e| crate::error::ShoheError::Transport(format!("VMC read failed: {}", e)))?;
let cert_der = if cert_bytes.starts_with(b"-----BEGIN") {
let pem_str = String::from_utf8(cert_bytes.to_vec())
.map_err(|_| crate::error::ShoheError::Parse("Invalid UTF-8 in PEM".to_string()))?;
parse_pem_cert(&pem_str)?
} else {
cert_bytes.to_vec()
};
let (_, cert) = x509_parser::prelude::parse_x509_certificate(&cert_der)
.map_err(|e| crate::error::ShoheError::Parse(format!("Certificate parse failed: {}", e)))?;
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let not_before = cert.validity.not_before.timestamp() as u64;
let not_after = cert.validity.not_after.timestamp() as u64;
Ok(now >= not_before && now <= not_after)
}
fn parse_pem_cert(pem_str: &str) -> Result<Vec<u8>> {
let lines: Vec<&str> = pem_str.lines().collect();
let mut cert_data = String::new();
let mut in_cert = false;
for line in lines {
if line.contains("-----BEGIN") {
in_cert = true;
continue;
}
if line.contains("-----END") {
break;
}
if in_cert {
cert_data.push_str(line);
}
}
base64_decode(&cert_data)
}
fn base64_decode(data: &str) -> Result<Vec<u8>> {
const TABLE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let data = data.trim();
let mut result = Vec::new();
let bytes: Vec<u8> = data.as_bytes().to_vec();
let mut i = 0;
while i < bytes.len() {
let mut chunk = 0u32;
let mut bits = 0;
for _ in 0..4 {
if i >= bytes.len() {
break;
}
let c = bytes[i];
i += 1;
if c == b'=' {
break;
}
let val = if let Some(pos) = TABLE.find(c as char) {
pos as u32
} else {
continue;
};
chunk = (chunk << 6) | val;
bits += 6;
}
if bits >= 8 {
chunk <<= 24 - bits;
for _ in 0..bits / 8 {
result.push((chunk >> 16) as u8);
chunk <<= 8;
}
}
}
Ok(result)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BimiCheckRequest {
pub domain: String,
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
fn default_timeout() -> u64 { 5 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BimiCheckResult {
pub domain: String,
pub bimi_present: bool,
pub version: Option<String>,
pub vmc_url: Option<String>,
#[serde(default)]
pub dmarc_aligned: Option<bool>,
#[serde(default)]
pub vmc_valid: Option<bool>,
#[serde(default)]
pub error: Option<String>,
}