1use std::time::Duration;
2
3use regex::Regex;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 cache::Cache,
8 dns::DnsClient,
9 error::{MailGuardError, Result},
10 threat::ThreatType,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct EmailStatus {
16 pub email: String,
18 pub domain: String,
20 pub is_threat: bool,
22 pub threat_type: Option<ThreatType>,
24 pub from_cache: bool,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct DomainStatus {
31 pub domain: String,
33 pub is_threat: bool,
35 pub threat_type: Option<ThreatType>,
37 pub from_cache: bool,
39}
40
41#[derive(Debug, Clone)]
43pub struct MailGuardConfig {
44 pub dns_timeout: Duration,
46 pub enable_cache: bool,
48 pub cache_ttl: Duration,
50}
51
52impl Default for MailGuardConfig {
53 fn default() -> Self {
54 Self {
55 dns_timeout: Duration::from_secs(5),
56 enable_cache: true,
57 cache_ttl: Duration::from_secs(300), }
59 }
60}
61
62pub struct MailGuard {
64 dns_client: DnsClient,
65 cache: Option<Cache>,
66 email_regex: Regex,
67 #[allow(dead_code)]
68 config: MailGuardConfig,
69}
70
71impl MailGuard {
72 pub fn new() -> Self {
74 Self::with_config(MailGuardConfig::default())
75 }
76
77 pub fn with_config(config: MailGuardConfig) -> Self {
79 let dns_client = DnsClient::with_timeout(config.dns_timeout);
80 let cache = if config.enable_cache {
81 Some(Cache::with_ttl(config.cache_ttl))
82 } else {
83 None
84 };
85
86 let email_regex = Regex::new(
88 r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"
89 ).expect("Invalid email regex");
90
91 Self {
92 dns_client,
93 cache,
94 email_regex,
95 config,
96 }
97 }
98
99 pub async fn check_email(&self, email: &str) -> Result<EmailStatus> {
101 if !self.email_regex.is_match(email) {
103 return Err(MailGuardError::InvalidEmail(email.to_string()));
104 }
105
106 let domain = self.extract_domain(email)?;
108
109 let domain_status = self.check_domain(&domain).await?;
111
112 Ok(EmailStatus {
113 email: email.to_string(),
114 domain: domain_status.domain,
115 is_threat: domain_status.is_threat,
116 threat_type: domain_status.threat_type,
117 from_cache: domain_status.from_cache,
118 })
119 }
120
121 pub async fn check_domain(&self, domain: &str) -> Result<DomainStatus> {
123 self.dns_client.validate_domain(domain)?;
125
126 let domain = domain.to_lowercase();
127
128 if let Some(cache) = &self.cache
130 && let Some(cached_threat) = cache.get(&domain)
131 {
132 return Ok(DomainStatus {
133 domain: domain.clone(),
134 is_threat: cached_threat.is_some(),
135 threat_type: cached_threat,
136 from_cache: true,
137 });
138 }
139
140 let threat_type = self.dns_client.query_surbl(&domain).await?;
142
143 if let Some(cache) = &self.cache {
145 cache.set(domain.clone(), threat_type.clone());
146 }
147
148 Ok(DomainStatus {
149 domain,
150 is_threat: threat_type.is_some(),
151 threat_type,
152 from_cache: false,
153 })
154 }
155
156 pub async fn check_emails_batch(&self, emails: &[&str]) -> Vec<Result<EmailStatus>> {
158 let mut results = Vec::with_capacity(emails.len());
159
160 for email in emails {
161 let result = self.check_email(email).await;
162 results.push(result);
163 }
164
165 results
166 }
167
168 pub async fn check_domains_batch(&self, domains: &[&str]) -> Vec<Result<DomainStatus>> {
170 let mut results = Vec::with_capacity(domains.len());
171
172 for domain in domains {
173 let result = self.check_domain(domain).await;
174 results.push(result);
175 }
176
177 results
178 }
179
180 fn extract_domain(&self, email: &str) -> Result<String> {
182 if let Some(at_pos) = email.rfind('@') {
183 let domain = &email[at_pos + 1..];
184 if domain.is_empty() {
185 return Err(MailGuardError::InvalidEmail("邮箱域名为空".to_string()));
186 }
187 Ok(domain.to_string())
188 } else {
189 Err(MailGuardError::InvalidEmail("邮箱格式无效".to_string()))
190 }
191 }
192
193 pub fn cleanup_cache(&self) {
195 if let Some(cache) = &self.cache {
196 cache.cleanup_expired();
197 }
198 }
199
200 pub fn cache_stats(&self) -> Option<usize> {
202 self.cache.as_ref().map(|cache| cache.size())
203 }
204
205 pub fn clear_cache(&self) {
207 if let Some(cache) = &self.cache {
208 cache.clear();
209 }
210 }
211}
212
213impl Default for MailGuard {
214 fn default() -> Self {
215 Self::new()
216 }
217}