postfix_log_parser/components/
relay.rs

1//! # Relay 邮件中继组件解析器
2//!
3//! Relay 是 Postfix 的邮件中继传输组件,负责:
4//! - 通过 SMTP 协议向远程服务器投递邮件
5//! - 处理邮件路由和中继决策
6//! - 管理投递重试和失败处理
7//! - 记录详细的投递状态和性能指标
8//!
9//! ## 核心功能
10//!
11//! - **邮件投递**: 通过 SMTP 向目标服务器投递邮件
12//! - **连接管理**: 建立和维护与远程服务器的连接
13//! - **投递状态**: 跟踪投递成功、失败、延迟状态
14//! - **性能监控**: 记录延迟分解和连接质量
15//! - **错误处理**: 详细的连接错误和 TLS 问题诊断
16//!
17//! ## 支持的事件类型
18//!
19//! - **投递状态**: 成功、退回、延迟投递的详细记录
20//! - **连接问题**: 连接失败、超时、拒绝等网络问题
21//! - **TLS 事件**: SSL/TLS 连接建立和证书验证
22//! - **中继配置**: 传输映射和路由配置事件
23//!
24//! ## 示例日志格式
25//!
26//! ```text
27//! # 投递成功
28//! 4D2952A00AD6: to=<user@example.com>, relay=mail.example.com[1.2.3.4]:25, delay=88, delays=0/0.01/88/0, dsn=2.0.0, status=sent (250 Message accepted)
29//!
30//! # 投递失败
31//! 4D2952A00AD6: to=<user@example.com>, relay=none, delay=0, delays=0/0/0/0, dsn=5.4.6, status=bounced (mail for domain.com loops back to myself)
32//!
33//! # 连接问题
34//! 4D2952A00AD6: connect to mail.example.com[1.2.3.4]:25: Connection timed out
35//! 4D2952A00AD6: lost connection with mail.example.com[1.2.3.4] while sending message body
36//! ```
37
38use lazy_static::lazy_static;
39use regex::Regex;
40use std::net::IpAddr;
41use std::str::FromStr;
42
43use super::ComponentParser;
44use crate::error::ParseError;
45use crate::events::base::BaseEvent;
46use crate::events::relay::{
47    ConnectionIssueType, DelayBreakdown, DeliveryStatus, RelayConfigType, RelayEvent,
48};
49use crate::events::ComponentEvent;
50
51/// RELAY组件解析器
52///
53/// 处理Postfix relay/smtp传输代理的日志
54/// relay组件负责通过SMTP将邮件发送到远程或本地目标
55pub struct RelayParser;
56
57lazy_static! {
58    /// 主要投递状态模式 - 匹配95%+的relay日志
59    /// 例如: "4D2952A00AD6: to=<m01@zcloud.center>, relay=mail.zcloud.center[223.223.197.126]:25, delay=88, delays=0/0.01/88/0, dsn=4.4.2, status=deferred (lost connection)"
60    static ref DELIVERY_STATUS_PATTERN: Regex = Regex::new(
61        r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,\[\]]+)(?:\[([^\]]+)\])?(?::(\d+))?,\s+delay=([0-9.]+),\s+delays=([0-9./]+),\s+dsn=([0-9.]+),\s+status=(\w+)\s*\(([^)]*)\)"
62    ).unwrap();
63
64    /// 无法投递模式 - 当没有可用的relay时
65    /// 例如: "4D2952A00AD6: to=<user@domain.com>, orig_to=<user>, relay=none, delay=0, delays=0/0/0/0, dsn=5.4.6, status=bounced (mail for domain.com loops back to myself)"
66    static ref NO_RELAY_PATTERN: Regex = Regex::new(
67        r"^([A-F0-9]+):\s+to=<([^>]+)>(?:,\s+orig_to=<[^>]+>)?,\s+relay=none,\s+delay=([0-9.]+),\s+delays=([0-9./]+),\s+dsn=([0-9.]+),\s+status=(\w+)\s*\(([^)]*)\)"
68    ).unwrap();
69
70    /// 连接问题模式 - 匹配连接失败、超时等问题
71    /// 例如: "warning: smtp_connect_timeout: stream not ready"
72    static ref CONNECTION_ISSUE_PATTERN: Regex = Regex::new(
73        r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,\[\]]+)(?:\[([^\]]+)\])?(?::(\d+))?,.*?(?:connection\s+(lost|refused|timeout|failed)|host\s+unreachable|network\s+unreachable)"
74    ).unwrap();
75
76    /// TLS相关错误模式
77    static ref TLS_ERROR_PATTERN: Regex = Regex::new(
78        r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,\[\]]+).*?(?:TLS|SSL|certificate)"
79    ).unwrap();
80
81    /// 传输映射配置模式
82    static ref TRANSPORT_MAP_PATTERN: Regex = Regex::new(
83        r"transport\s+maps?\s+(?:lookup|configuration|error)"
84    ).unwrap();
85}
86
87impl RelayParser {
88    pub fn new() -> Self {
89        Self
90    }
91
92    /// 解析单行relay日志
93    pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<RelayEvent> {
94        // 按频率优先级处理,最常见的模式优先
95
96        // 1. 投递状态模式(最常见,95%+)
97        if let Some(caps) = DELIVERY_STATUS_PATTERN.captures(line) {
98            return self.parse_delivery_status(caps, base_event);
99        }
100
101        // 2. 无relay可用模式
102        if let Some(caps) = NO_RELAY_PATTERN.captures(line) {
103            return self.parse_no_relay_status(caps, base_event);
104        }
105
106        // 3. 连接问题模式
107        if let Some(caps) = CONNECTION_ISSUE_PATTERN.captures(line) {
108            return self.parse_connection_issue(caps, base_event, line);
109        }
110
111        // 4. TLS相关错误
112        if let Some(caps) = TLS_ERROR_PATTERN.captures(line) {
113            return self.parse_tls_issue(caps, base_event, line);
114        }
115
116        // 5. 传输配置相关
117        if TRANSPORT_MAP_PATTERN.is_match(line) {
118            return Some(RelayEvent::RelayConfiguration {
119                base: base_event,
120                config_type: RelayConfigType::TransportMapping,
121                details: line.to_string(),
122            });
123        }
124
125        None
126    }
127
128    /// 解析投递状态信息
129    fn parse_delivery_status(
130        &self,
131        caps: regex::Captures,
132        base_event: BaseEvent,
133    ) -> Option<RelayEvent> {
134        let queue_id = caps.get(1)?.as_str().to_string();
135        let recipient = caps.get(2)?.as_str().to_string();
136        let relay_host = caps.get(3)?.as_str().to_string();
137        let relay_ip = caps.get(4).and_then(|m| IpAddr::from_str(m.as_str()).ok());
138        let relay_port = caps.get(5).and_then(|m| m.as_str().parse().ok());
139        let delay = caps.get(6)?.as_str().parse().ok()?;
140        let delays_str = caps.get(7)?.as_str();
141        let dsn = caps.get(8)?.as_str().to_string();
142        let status_str = caps.get(9)?.as_str();
143        let status_description = caps.get(10)?.as_str().to_string();
144
145        let delays = DelayBreakdown::from_delays_string(delays_str)?;
146        let status = self.parse_delivery_status_type(status_str)?;
147
148        Some(RelayEvent::DeliveryStatus {
149            base: base_event,
150            queue_id,
151            recipient,
152            relay_host,
153            relay_ip,
154            relay_port,
155            delay,
156            delays,
157            dsn,
158            status,
159            status_description,
160        })
161    }
162
163    /// 解析无relay可用的投递状态
164    fn parse_no_relay_status(
165        &self,
166        caps: regex::Captures,
167        base_event: BaseEvent,
168    ) -> Option<RelayEvent> {
169        let queue_id = caps.get(1)?.as_str().to_string();
170        let recipient = caps.get(2)?.as_str().to_string();
171        let delay = caps.get(3)?.as_str().parse().ok()?;
172        let delays_str = caps.get(4)?.as_str();
173        let dsn = caps.get(5)?.as_str().to_string();
174        let status_str = caps.get(6)?.as_str();
175        let status_description = caps.get(7)?.as_str().to_string();
176
177        let delays = DelayBreakdown::from_delays_string(delays_str)?;
178        let status = self.parse_delivery_status_type(status_str)?;
179
180        Some(RelayEvent::DeliveryStatus {
181            base: base_event,
182            queue_id,
183            recipient,
184            relay_host: "none".to_string(),
185            relay_ip: None,
186            relay_port: None,
187            delay,
188            delays,
189            dsn,
190            status,
191            status_description,
192        })
193    }
194
195    /// 解析连接问题
196    fn parse_connection_issue(
197        &self,
198        caps: regex::Captures,
199        base_event: BaseEvent,
200        full_line: &str,
201    ) -> Option<RelayEvent> {
202        let queue_id = caps.get(1)?.as_str().to_string();
203        let recipient = caps.get(2)?.as_str().to_string();
204        let relay_host = caps.get(3)?.as_str().to_string();
205        let relay_ip = caps.get(4).and_then(|m| IpAddr::from_str(m.as_str()).ok());
206
207        let issue_type = if full_line.contains("lost connection") {
208            ConnectionIssueType::LostConnection
209        } else if full_line.contains("connection refused") {
210            ConnectionIssueType::ConnectionRefused
211        } else if full_line.contains("timeout") {
212            ConnectionIssueType::ConnectionTimeout
213        } else if full_line.contains("host unreachable")
214            || full_line.contains("network unreachable")
215        {
216            ConnectionIssueType::DnsResolutionFailed
217        } else {
218            ConnectionIssueType::Other
219        };
220
221        Some(RelayEvent::ConnectionIssue {
222            base: base_event,
223            queue_id,
224            recipient,
225            relay_host,
226            relay_ip,
227            issue_type,
228            error_message: full_line.to_string(),
229        })
230    }
231
232    /// 解析TLS相关问题
233    fn parse_tls_issue(
234        &self,
235        caps: regex::Captures,
236        base_event: BaseEvent,
237        full_line: &str,
238    ) -> Option<RelayEvent> {
239        let queue_id = caps.get(1)?.as_str().to_string();
240        let recipient = caps.get(2)?.as_str().to_string();
241        let relay_host = caps.get(3)?.as_str().to_string();
242
243        Some(RelayEvent::ConnectionIssue {
244            base: base_event,
245            queue_id,
246            recipient,
247            relay_host,
248            relay_ip: None,
249            issue_type: ConnectionIssueType::TlsHandshakeFailed,
250            error_message: full_line.to_string(),
251        })
252    }
253
254    /// 解析投递状态类型
255    fn parse_delivery_status_type(&self, status_str: &str) -> Option<DeliveryStatus> {
256        match status_str.to_lowercase().as_str() {
257            "sent" => Some(DeliveryStatus::Sent),
258            "deferred" => Some(DeliveryStatus::Deferred),
259            "bounced" => Some(DeliveryStatus::Bounced),
260            "failed" => Some(DeliveryStatus::Failed),
261            "rejected" => Some(DeliveryStatus::Rejected),
262            _ => None,
263        }
264    }
265
266    /// 获取解析器信息
267    pub fn info(&self) -> &'static str {
268        "RELAY组件解析器 - 处理Postfix relay/smtp传输代理日志,支持投递状态、连接问题和配置事件"
269    }
270}
271
272impl ComponentParser for RelayParser {
273    fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
274        // 创建一个临时的BaseEvent,用于解析
275        // 在实际使用中,这些字段会被MasterParser正确填充
276        let base_event = BaseEvent {
277            timestamp: chrono::Utc::now(),
278            hostname: "temp".to_string(),
279            component: "relay".to_string(),
280            process_id: 0,
281            log_level: crate::events::base::PostfixLogLevel::Info,
282            raw_message: message.to_string(),
283        };
284
285        if let Some(relay_event) = self.parse_line(message, base_event) {
286            Ok(ComponentEvent::Relay(relay_event))
287        } else {
288            Err(ParseError::ComponentParseError {
289                component: "relay".to_string(),
290                reason: "无法识别的relay日志格式".to_string(),
291            })
292        }
293    }
294
295    fn component_name(&self) -> &'static str {
296        "relay"
297    }
298
299    fn can_parse(&self, message: &str) -> bool {
300        // 检查是否包含relay特征
301        DELIVERY_STATUS_PATTERN.is_match(message)
302            || NO_RELAY_PATTERN.is_match(message)
303            || CONNECTION_ISSUE_PATTERN.is_match(message)
304            || TLS_ERROR_PATTERN.is_match(message)
305            || TRANSPORT_MAP_PATTERN.is_match(message)
306    }
307}
308
309impl Default for RelayParser {
310    fn default() -> Self {
311        Self::new()
312    }
313}