postfix_log_parser/components/
relay.rs1use 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
51pub struct RelayParser;
56
57lazy_static! {
58 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 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 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 static ref TLS_ERROR_PATTERN: Regex = Regex::new(
78 r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,\[\]]+).*?(?:TLS|SSL|certificate)"
79 ).unwrap();
80
81 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 pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<RelayEvent> {
94 if let Some(caps) = DELIVERY_STATUS_PATTERN.captures(line) {
98 return self.parse_delivery_status(caps, base_event);
99 }
100
101 if let Some(caps) = NO_RELAY_PATTERN.captures(line) {
103 return self.parse_no_relay_status(caps, base_event);
104 }
105
106 if let Some(caps) = CONNECTION_ISSUE_PATTERN.captures(line) {
108 return self.parse_connection_issue(caps, base_event, line);
109 }
110
111 if let Some(caps) = TLS_ERROR_PATTERN.captures(line) {
113 return self.parse_tls_issue(caps, base_event, line);
114 }
115
116 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 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 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 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 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 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 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 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 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}