mailguard_rs/
detector.rs

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/// Email detection status
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct EmailStatus {
16    /// Email address
17    pub email: String,
18    /// Domain
19    pub domain: String,
20    /// Whether it's a temporary email or malicious domain
21    pub is_threat: bool,
22    /// Threat type (if exists)
23    pub threat_type: Option<ThreatType>,
24    /// Whether from cache
25    pub from_cache: bool,
26}
27
28/// Domain detection status
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub struct DomainStatus {
31    /// Domain
32    pub domain: String,
33    /// Whether it's a malicious domain
34    pub is_threat: bool,
35    /// Threat type (if exists)
36    pub threat_type: Option<ThreatType>,
37    /// Whether from cache
38    pub from_cache: bool,
39}
40
41/// Email detector configuration
42#[derive(Debug, Clone)]
43pub struct MailGuardConfig {
44    /// DNS query timeout
45    pub dns_timeout: Duration,
46    /// 是否启用缓存
47    pub enable_cache: bool,
48    /// 缓存 TTL
49    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), // 5分钟
58        }
59    }
60}
61
62/// 主要的邮箱检测器
63pub 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    /// 创建新的检测器实例
73    pub fn new() -> Self {
74        Self::with_config(MailGuardConfig::default())
75    }
76
77    /// 使用自定义配置创建检测器
78    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        // 邮箱格式验证正则表达式
87        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    /// 检查单个邮箱地址
100    pub async fn check_email(&self, email: &str) -> Result<EmailStatus> {
101        // 验证邮箱格式
102        if !self.email_regex.is_match(email) {
103            return Err(MailGuardError::InvalidEmail(email.to_string()));
104        }
105
106        // 提取域名
107        let domain = self.extract_domain(email)?;
108
109        // 检查域名
110        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    /// 检查域名
122    pub async fn check_domain(&self, domain: &str) -> Result<DomainStatus> {
123        // 验证域名格式
124        self.dns_client.validate_domain(domain)?;
125
126        let domain = domain.to_lowercase();
127
128        // 检查缓存
129        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        // 执行 DNS 查询
141        let threat_type = self.dns_client.query_surbl(&domain).await?;
142
143        // 更新缓存
144        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    /// 批量检查邮箱
157    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    /// 批量检查域名
169    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    /// 从邮箱地址提取域名
181    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    /// 清理缓存中的过期条目
194    pub fn cleanup_cache(&self) {
195        if let Some(cache) = &self.cache {
196            cache.cleanup_expired();
197        }
198    }
199
200    /// 获取缓存统计信息
201    pub fn cache_stats(&self) -> Option<usize> {
202        self.cache.as_ref().map(|cache| cache.size())
203    }
204
205    /// 清空缓存
206    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}