postfix_log_parser/components/
discard.rs

1//! # Discard 邮件丢弃组件解析器
2//!
3//! Discard 是 Postfix 的邮件丢弃代理,负责:
4//! - 静默丢弃不需要的邮件(如垃圾邮件)
5//! - 假装投递但实际丢弃邮件内容
6//! - 提供策略性邮件丢弃功能
7//! - 记录丢弃操作的详细统计信息
8//!
9//! ## 特点
10//!
11//! - 总是报告 `relay=none` 或 `relay=nonediscard`
12//! - 状态始终为 `sent`,但实际邮件被丢弃
13//! - DSN 通常为 `2.0.0` 表示成功处理
14//! - 延迟时间通常很短,因为无实际网络投递
15//!
16//! ## 支持的事件类型
17//!
18//! - **邮件丢弃**: 邮件被丢弃的详细记录,包含延迟分析
19//! - **配置事件**: 服务启动、传输映射、丢弃规则配置
20//!
21//! ## 示例日志格式
22//!
23//! ```text
24//! # 邮件丢弃事件
25//! queue_id: to=<user@example.com>, relay=none, delay=0.1, delays=0.1/0/0/0, dsn=2.0.0, status=sent (discarded)
26//!
27//! # 配置事件
28//! starting discard service
29//! transport mapping updated
30//! ```
31
32use regex::Regex;
33
34use crate::error::ParseError;
35use crate::events::discard::{DelayBreakdown, DiscardConfigType, DiscardEvent};
36use crate::events::{base::BaseEvent, ComponentEvent};
37use crate::utils::common_fields::CommonFieldsParser;
38
39use super::ComponentParser;
40
41/// DISCARD组件解析器
42/// 
43/// 基于Postfix DISCARD邮件丢弃代理的真实日志格式开发
44/// DISCARD代理的特点:
45/// - 假装投递但实际丢弃邮件
46/// - 总是报告relay=nonediscard
47/// - 状态总是为sent,但DSN为2.0.0表示成功丢弃
48/// - 不进行实际网络投递,延迟时间通常很短
49pub struct DiscardParser {
50    /// 邮件丢弃事件解析 - 主要模式(95%+频率)
51    /// 格式: "queue_id: to=&lt;recipient&gt;, relay=none, delay=X, delays=a/b/c/d, dsn=X.X.X, status=sent (reason)"
52    message_discard_regex: Regex,
53    
54    /// 服务启动/配置事件解析
55    /// 格式: 各种配置相关消息
56    config_regex: Regex,
57}
58
59impl DiscardParser {
60    pub fn new() -> Self {
61        Self {
62            // 主要的邮件丢弃事件模式
63            message_discard_regex: Regex::new(
64                r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,]+),\s+delay=([0-9.]+),\s+delays=([0-9./]+),\s+dsn=([0-9.]+),\s+status=(\w+)\s+\(([^)]+)\)$"
65            ).expect("DISCARD消息丢弃正则表达式编译失败"),
66            
67            // 配置和启动事件模式
68            config_regex: Regex::new(
69                r"^(starting|stopping|warning|configuration|transport).*$"
70            ).expect("DISCARD配置正则表达式编译失败"),
71        }
72    }
73
74    /// 解析DISCARD日志行
75    pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<DiscardEvent> {
76        // 尝试解析邮件丢弃事件(主要模式)
77        if let Some(captures) = self.message_discard_regex.captures(line) {
78            return self.parse_message_discard(captures, base_event);
79        }
80        
81        // 尝试解析配置事件
82        if let Some(captures) = self.config_regex.captures(line) {
83            return self.parse_config_event(captures, base_event);
84        }
85        
86        None
87    }
88
89    /// 解析邮件丢弃事件(使用公共字段解析器)
90    fn parse_message_discard(
91        &self,
92        captures: regex::Captures,
93        base_event: BaseEvent,
94    ) -> Option<DiscardEvent> {
95        let full_message = base_event.raw_message.as_str();
96        let queue_id = captures.get(1)?.as_str().to_string();
97        
98        // 使用公共字段解析器提取字段
99        let recipient = CommonFieldsParser::extract_to_email(full_message)
100            .map(|email| email.address)
101            .unwrap_or_else(|| captures.get(2).map_or_else(String::new, |m| m.as_str().to_string()));
102        
103        let relay_info = CommonFieldsParser::extract_relay_info(full_message);
104        let relay = relay_info.as_ref()
105            .map(|r| r.hostname.clone())
106            .unwrap_or_else(|| captures.get(3).map_or_else(String::new, |m| m.as_str().to_string()));
107        
108        let delay_info = CommonFieldsParser::extract_delay_info(full_message);
109        let delay = delay_info.as_ref()
110            .map(|d| d.total)
111            .unwrap_or_else(|| {
112                captures.get(4)
113                    .and_then(|m| m.as_str().parse().ok())
114                    .unwrap_or(0.0)
115            });
116        
117        // 使用公共解析器的延迟分解,回退到自定义解析
118        let delays = delay_info.as_ref()
119            .and_then(|d| d.breakdown.as_ref())
120            .and_then(|breakdown| {
121                DelayBreakdown::from_delays_string(&format!("{}/{}/{}/{}", 
122                    breakdown[0], breakdown[1], breakdown[2], breakdown[3]))
123            })
124            .or_else(|| {
125                captures.get(5)
126                    .and_then(|m| DelayBreakdown::from_delays_string(m.as_str()))
127            })?;
128        
129        let status_info = CommonFieldsParser::extract_status_info(full_message);
130        let dsn = status_info.as_ref()
131            .and_then(|s| s.dsn.clone())
132            .unwrap_or_else(|| captures.get(6).map_or_else(String::new, |m| m.as_str().to_string()));
133        
134        let status = status_info.as_ref()
135            .map(|s| s.status.clone())
136            .unwrap_or_else(|| captures.get(7).map_or_else(String::new, |m| m.as_str().to_string()));
137        
138        let discard_reason = status_info.as_ref()
139            .and_then(|s| s.description.clone())
140            .unwrap_or_else(|| captures.get(8).map_or_else(String::new, |m| m.as_str().to_string()));
141        
142        Some(DiscardEvent::MessageDiscard {
143            base: base_event,
144            queue_id,
145            recipient,
146            relay,
147            delay,
148            delays,
149            dsn,
150            status,
151            discard_reason,
152        })
153    }
154    
155    /// 解析配置事件
156    /// 处理DISCARD服务的配置、启动、停止等事件
157    fn parse_config_event(
158        &self,
159        captures: regex::Captures,
160        base_event: BaseEvent,
161    ) -> Option<DiscardEvent> {
162        let message = captures.get(0)?.as_str();
163        
164        // 根据消息内容确定配置类型
165        // 注意:优先级很重要!更具体的匹配应该放在前面
166        let config_type = if message.contains("starting") || message.contains("stopping") {
167            DiscardConfigType::ServiceStartup
168        } else if message.contains("transport") {
169            DiscardConfigType::TransportMapping
170        } else if message.contains("discard") || message.contains("rule") {
171            DiscardConfigType::DiscardRules
172        } else {
173            DiscardConfigType::Other
174        };
175        
176        Some(DiscardEvent::Configuration {
177            base: base_event,
178            config_type,
179            details: message.to_string(),
180        })
181    }
182}
183
184impl ComponentParser for DiscardParser {
185    fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
186        // 创建一个临时的BaseEvent,用于解析
187        // 在实际使用中,这些字段会被MasterParser正确填充
188        let base_event = BaseEvent {
189            timestamp: chrono::Utc::now(),
190            hostname: "temp".to_string(),
191            component: "discard".to_string(),
192            process_id: 0,
193            log_level: crate::events::base::PostfixLogLevel::Info,
194            raw_message: message.to_string(),
195        };
196
197        if let Some(discard_event) = self.parse_line(message, base_event) {
198            Ok(ComponentEvent::Discard(discard_event))
199        } else {
200            Err(ParseError::ComponentParseError {
201                component: "discard".to_string(),
202                reason: "无法识别的discard日志格式".to_string(),
203            })
204        }
205    }
206    
207    fn component_name(&self) -> &'static str {
208        "discard"
209    }
210
211    fn can_parse(&self, message: &str) -> bool {
212        // 检查是否包含discard特征
213        self.message_discard_regex.is_match(message) || self.config_regex.is_match(message)
214    }
215}
216
217impl Default for DiscardParser {
218    fn default() -> Self {
219        Self::new()
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::events::base::BaseEvent;
227    use chrono::{DateTime, Utc};
228    
229    fn create_test_base_event() -> BaseEvent {
230        BaseEvent {
231            timestamp: DateTime::parse_from_rfc3339("2024-04-07T10:51:05+00:00")
232                .unwrap()
233                .with_timezone(&Utc),
234            hostname: "m01".to_string(),
235            component: "discard".to_string(),
236            process_id: 85,
237            log_level: crate::events::base::PostfixLogLevel::Info,
238            raw_message: "test message".to_string(),
239        }
240    }
241    
242    #[test]
243    fn test_parse_message_discard() {
244        let parser = DiscardParser::new();
245        let base_event = create_test_base_event();
246        
247        let message = "5A4DF1C801B0: to=<six@nextcloud.games>, relay=none, delay=0.05, delays=0.04/0/0/0, dsn=2.0.0, status=sent (nextcloud.games)";
248        
249        let result = parser.parse_line(message, base_event);
250        assert!(result.is_some());
251        
252        if let Some(DiscardEvent::MessageDiscard { 
253            queue_id, 
254            recipient, 
255            relay,
256            delay,
257            dsn,
258            status,
259            discard_reason,
260            .. 
261        }) = result {
262            assert_eq!(queue_id, "5A4DF1C801B0");
263            assert_eq!(recipient, "six@nextcloud.games");
264            assert_eq!(relay, "none");
265            assert_eq!(delay, 0.05);
266            assert_eq!(dsn, "2.0.0");
267            assert_eq!(status, "sent");
268            assert_eq!(discard_reason, "nextcloud.games");
269        } else {
270            panic!("解析结果类型不正确");
271        }
272    }
273    
274    #[test]
275    fn test_parse_various_delays() {
276        let parser = DiscardParser::new();
277        let base_event = create_test_base_event();
278        
279        // 测试不同的延迟格式
280        let test_cases = vec![
281            ("delays=0.04/0/0/0", 0.04),
282            ("delays=0/0/0/0", 0.0),
283            ("delays=0.01/0.02/0/0", 0.03),
284        ];
285        
286        for (delays_part, expected_total) in test_cases {
287            let message = format!("5A4DF1C801B0: to=<test@example.com>, relay=none, delay=0.05, {}, dsn=2.0.0, status=sent (example.com)", delays_part);
288            
289            let result = parser.parse_line(&message, base_event.clone());
290            assert!(result.is_some());
291            
292            if let Some(DiscardEvent::MessageDiscard { delays, .. }) = result {
293                assert!((delays.total_delay() - expected_total).abs() < 0.001);
294            }
295        }
296    }
297    
298    #[test]
299    fn test_parse_config_event() {
300        let parser = DiscardParser::new();
301        let base_event = create_test_base_event();
302        
303        let message = "starting mail discard service";
304        
305        let result = parser.parse_line(message, base_event);
306        assert!(result.is_some());
307        
308        if let Some(DiscardEvent::Configuration { config_type, details, .. }) = result {
309            assert!(matches!(config_type, DiscardConfigType::ServiceStartup));
310            assert_eq!(details, "starting mail discard service");
311        } else {
312            panic!("解析结果类型不正确");
313        }
314    }
315    
316    #[test]
317    fn test_delay_breakdown_parsing() {
318        // 测试DelayBreakdown的解析功能
319        let delay_breakdown = DelayBreakdown::from_delays_string("0.04/0/0/0").unwrap();
320        assert_eq!(delay_breakdown.queue_wait, 0.04);
321        assert_eq!(delay_breakdown.connection_setup, 0.0);
322        assert_eq!(delay_breakdown.connection_time, 0.0);
323        assert_eq!(delay_breakdown.transmission_time, 0.0);
324        assert_eq!(delay_breakdown.total_delay(), 0.04);
325        assert!(delay_breakdown.is_fast_discard());
326        
327        let delay_breakdown = DelayBreakdown::from_delays_string("0.01/0.02/0.03/0.04").unwrap();
328        assert_eq!(delay_breakdown.total_delay(), 0.10);
329        assert!(!delay_breakdown.is_fast_discard());
330    }
331}