postfix_log_parser/components/
relay.rs1use lazy_static::lazy_static;
2use regex::Regex;
3use std::net::IpAddr;
4use std::str::FromStr;
5
6use super::ComponentParser;
7use crate::error::ParseError;
8use crate::events::base::BaseEvent;
9use crate::events::relay::{
10 ConnectionIssueType, DelayBreakdown, DeliveryStatus, RelayConfigType, RelayEvent,
11};
12use crate::events::ComponentEvent;
13
14pub struct RelayParser;
19
20lazy_static! {
21 static ref DELIVERY_STATUS_PATTERN: Regex = Regex::new(
24 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*\(([^)]*)\)"
25 ).unwrap();
26
27 static ref NO_RELAY_PATTERN: Regex = Regex::new(
30 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*\(([^)]*)\)"
31 ).unwrap();
32
33 static ref CONNECTION_ISSUE_PATTERN: Regex = Regex::new(
36 r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,\[\]]+)(?:\[([^\]]+)\])?(?::(\d+))?,.*?(?:connection\s+(lost|refused|timeout|failed)|host\s+unreachable|network\s+unreachable)"
37 ).unwrap();
38
39 static ref TLS_ERROR_PATTERN: Regex = Regex::new(
41 r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,\[\]]+).*?(?:TLS|SSL|certificate)"
42 ).unwrap();
43
44 static ref TRANSPORT_MAP_PATTERN: Regex = Regex::new(
46 r"transport\s+maps?\s+(?:lookup|configuration|error)"
47 ).unwrap();
48}
49
50impl RelayParser {
51 pub fn new() -> Self {
52 Self
53 }
54
55 pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<RelayEvent> {
57 if let Some(caps) = DELIVERY_STATUS_PATTERN.captures(line) {
61 return self.parse_delivery_status(caps, base_event);
62 }
63
64 if let Some(caps) = NO_RELAY_PATTERN.captures(line) {
66 return self.parse_no_relay_status(caps, base_event);
67 }
68
69 if let Some(caps) = CONNECTION_ISSUE_PATTERN.captures(line) {
71 return self.parse_connection_issue(caps, base_event, line);
72 }
73
74 if let Some(caps) = TLS_ERROR_PATTERN.captures(line) {
76 return self.parse_tls_issue(caps, base_event, line);
77 }
78
79 if TRANSPORT_MAP_PATTERN.is_match(line) {
81 return Some(RelayEvent::RelayConfiguration {
82 base: base_event,
83 config_type: RelayConfigType::TransportMapping,
84 details: line.to_string(),
85 });
86 }
87
88 None
89 }
90
91 fn parse_delivery_status(
93 &self,
94 caps: regex::Captures,
95 base_event: BaseEvent,
96 ) -> Option<RelayEvent> {
97 let queue_id = caps.get(1)?.as_str().to_string();
98 let recipient = caps.get(2)?.as_str().to_string();
99 let relay_host = caps.get(3)?.as_str().to_string();
100 let relay_ip = caps.get(4).and_then(|m| IpAddr::from_str(m.as_str()).ok());
101 let relay_port = caps.get(5).and_then(|m| m.as_str().parse().ok());
102 let delay = caps.get(6)?.as_str().parse().ok()?;
103 let delays_str = caps.get(7)?.as_str();
104 let dsn = caps.get(8)?.as_str().to_string();
105 let status_str = caps.get(9)?.as_str();
106 let status_description = caps.get(10)?.as_str().to_string();
107
108 let delays = DelayBreakdown::from_delays_string(delays_str)?;
109 let status = self.parse_delivery_status_type(status_str)?;
110
111 Some(RelayEvent::DeliveryStatus {
112 base: base_event,
113 queue_id,
114 recipient,
115 relay_host,
116 relay_ip,
117 relay_port,
118 delay,
119 delays,
120 dsn,
121 status,
122 status_description,
123 })
124 }
125
126 fn parse_no_relay_status(
128 &self,
129 caps: regex::Captures,
130 base_event: BaseEvent,
131 ) -> Option<RelayEvent> {
132 let queue_id = caps.get(1)?.as_str().to_string();
133 let recipient = caps.get(2)?.as_str().to_string();
134 let delay = caps.get(3)?.as_str().parse().ok()?;
135 let delays_str = caps.get(4)?.as_str();
136 let dsn = caps.get(5)?.as_str().to_string();
137 let status_str = caps.get(6)?.as_str();
138 let status_description = caps.get(7)?.as_str().to_string();
139
140 let delays = DelayBreakdown::from_delays_string(delays_str)?;
141 let status = self.parse_delivery_status_type(status_str)?;
142
143 Some(RelayEvent::DeliveryStatus {
144 base: base_event,
145 queue_id,
146 recipient,
147 relay_host: "none".to_string(),
148 relay_ip: None,
149 relay_port: None,
150 delay,
151 delays,
152 dsn,
153 status,
154 status_description,
155 })
156 }
157
158 fn parse_connection_issue(
160 &self,
161 caps: regex::Captures,
162 base_event: BaseEvent,
163 full_line: &str,
164 ) -> Option<RelayEvent> {
165 let queue_id = caps.get(1)?.as_str().to_string();
166 let recipient = caps.get(2)?.as_str().to_string();
167 let relay_host = caps.get(3)?.as_str().to_string();
168 let relay_ip = caps.get(4).and_then(|m| IpAddr::from_str(m.as_str()).ok());
169
170 let issue_type = if full_line.contains("lost connection") {
171 ConnectionIssueType::LostConnection
172 } else if full_line.contains("connection refused") {
173 ConnectionIssueType::ConnectionRefused
174 } else if full_line.contains("timeout") {
175 ConnectionIssueType::ConnectionTimeout
176 } else if full_line.contains("host unreachable")
177 || full_line.contains("network unreachable")
178 {
179 ConnectionIssueType::DnsResolutionFailed
180 } else {
181 ConnectionIssueType::Other
182 };
183
184 Some(RelayEvent::ConnectionIssue {
185 base: base_event,
186 queue_id,
187 recipient,
188 relay_host,
189 relay_ip,
190 issue_type,
191 error_message: full_line.to_string(),
192 })
193 }
194
195 fn parse_tls_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
206 Some(RelayEvent::ConnectionIssue {
207 base: base_event,
208 queue_id,
209 recipient,
210 relay_host,
211 relay_ip: None,
212 issue_type: ConnectionIssueType::TlsHandshakeFailed,
213 error_message: full_line.to_string(),
214 })
215 }
216
217 fn parse_delivery_status_type(&self, status_str: &str) -> Option<DeliveryStatus> {
219 match status_str.to_lowercase().as_str() {
220 "sent" => Some(DeliveryStatus::Sent),
221 "deferred" => Some(DeliveryStatus::Deferred),
222 "bounced" => Some(DeliveryStatus::Bounced),
223 "failed" => Some(DeliveryStatus::Failed),
224 "rejected" => Some(DeliveryStatus::Rejected),
225 _ => None,
226 }
227 }
228
229 pub fn info(&self) -> &'static str {
231 "RELAY组件解析器 - 处理Postfix relay/smtp传输代理日志,支持投递状态、连接问题和配置事件"
232 }
233}
234
235impl ComponentParser for RelayParser {
236 fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
237 let base_event = BaseEvent {
240 timestamp: chrono::Utc::now(),
241 hostname: "temp".to_string(),
242 component: "relay".to_string(),
243 process_id: 0,
244 log_level: crate::events::base::PostfixLogLevel::Info,
245 raw_message: message.to_string(),
246 };
247
248 if let Some(relay_event) = self.parse_line(message, base_event) {
249 Ok(ComponentEvent::Relay(relay_event))
250 } else {
251 Err(ParseError::ComponentParseError {
252 component: "relay".to_string(),
253 reason: "无法识别的relay日志格式".to_string(),
254 })
255 }
256 }
257
258 fn component_name(&self) -> &'static str {
259 "relay"
260 }
261
262 fn can_parse(&self, message: &str) -> bool {
263 DELIVERY_STATUS_PATTERN.is_match(message)
265 || NO_RELAY_PATTERN.is_match(message)
266 || CONNECTION_ISSUE_PATTERN.is_match(message)
267 || TLS_ERROR_PATTERN.is_match(message)
268 || TRANSPORT_MAP_PATTERN.is_match(message)
269 }
270}