postfix_log_parser/components/
local.rs1use crate::error::ParseError;
4use crate::events::local::{LocalEvent, ConfigurationWarning, LocalDelivery, ExternalDelivery, WarningType, DeliveryStatus, DeliveryMethod};
5use crate::events::base::BaseEvent;
6use crate::events::ComponentEvent;
7use super::ComponentParser;
8
9use regex::Regex;
10use std::collections::HashMap;
11
12pub struct LocalParser {
21 nis_domain_regex: Regex,
23 alias_not_found_regex: Regex,
24
25 local_delivery_regex: Regex,
27
28 external_delivery_regex: Regex,
30}
31
32impl LocalParser {
33 pub fn new() -> Self {
34 Self {
35 nis_domain_regex: Regex::new(
37 r"^dict_nis_init: NIS domain name not set - NIS lookups disabled$"
38 ).expect("LOCAL NIS域名警告正则表达式编译失败"),
39
40 alias_not_found_regex: Regex::new(
41 r"^required alias not found: (.+)$"
42 ).expect("LOCAL别名未找到警告正则表达式编译失败"),
43
44 local_delivery_regex: Regex::new(
46 r"^([0-9A-F]+): to=<([^>]+)>(?:, orig_to=<([^>]+)>)?, relay=([^,]+), delay=([0-9.]+), delays=([0-9.]+/[0-9.]+/[0-9.]+/[0-9.]+), dsn=([^,]+), status=(\w+)(?: \(([^)]+)\))?$"
47 ).expect("LOCAL本地投递正则表达式编译失败"),
48
49 external_delivery_regex: Regex::new(
51 r"([0-9A-F]+): to=<([^>]+)>, relay=([^,]+), delay=([0-9.]+), (?:cmd|file)=(.+), status=(\w+)"
52 ).expect("LOCAL外部投递正则表达式编译失败"),
53 }
54 }
55
56 pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<LocalEvent> {
58 if let Some(event) = self.parse_configuration_warning(line, base_event.clone()) {
60 return Some(event);
61 }
62
63 if let Some(event) = self.parse_local_delivery(line, base_event.clone()) {
65 return Some(event);
66 }
67
68 if let Some(event) = self.parse_external_delivery(line, base_event) {
70 return Some(event);
71 }
72
73 None
74 }
75
76 fn parse_configuration_warning(&self, line: &str, base_event: BaseEvent) -> Option<LocalEvent> {
77 if self.nis_domain_regex.is_match(line) {
79 let warning = ConfigurationWarning {
80 timestamp: base_event.timestamp,
81 warning_type: WarningType::NisDomainNotSet,
82 message: "NIS domain name not set - NIS lookups disabled".to_string(),
83 details: HashMap::new(),
84 };
85 return Some(LocalEvent::ConfigurationWarning(warning));
86 }
87
88 if let Some(caps) = self.alias_not_found_regex.captures(line) {
90 let alias_name = caps.get(1).unwrap().as_str();
91 let mut details = HashMap::new();
92 details.insert("alias_name".to_string(), alias_name.to_string());
93
94 let warning = ConfigurationWarning {
95 timestamp: base_event.timestamp,
96 warning_type: WarningType::RequiredAliasNotFound,
97 message: format!("required alias not found: {}", alias_name),
98 details,
99 };
100 return Some(LocalEvent::ConfigurationWarning(warning));
101 }
102
103 None
104 }
105
106 fn parse_local_delivery(&self, line: &str, base_event: BaseEvent) -> Option<LocalEvent> {
107 if let Some(caps) = self.local_delivery_regex.captures(line) {
108 let queue_id = caps.get(1).unwrap().as_str().to_string();
109 let recipient = caps.get(2).unwrap().as_str().to_string();
110 let original_recipient = caps.get(3).map(|m| m.as_str().to_string());
111 let relay = caps.get(4).unwrap().as_str().to_string();
112 let delay = caps.get(5).unwrap().as_str().parse::<f64>().unwrap_or(0.0);
113 let delays_str = caps.get(6).unwrap().as_str();
114 let dsn = caps.get(7).unwrap().as_str().to_string();
115 let status_str = caps.get(8).unwrap().as_str();
116 let extra_info = caps.get(9).map(|m| m.as_str());
117
118 let delays: Vec<f64> = delays_str
120 .split('/')
121 .map(|s| s.parse::<f64>().unwrap_or(0.0))
122 .collect();
123
124 let status = match status_str.to_lowercase().as_str() {
126 "sent" => DeliveryStatus::Sent,
127 "bounced" => DeliveryStatus::Bounced,
128 "deferred" => DeliveryStatus::Deferred,
129 _ => DeliveryStatus::Sent,
130 };
131
132 let delivery_method = if let Some(info) = extra_info {
134 if info.contains("discarded") {
135 DeliveryMethod::Discarded
136 } else if info.contains("forwarded") {
137 DeliveryMethod::Forwarded
138 } else if info.contains("piped") {
139 DeliveryMethod::Piped
140 } else if info.contains("file") {
141 DeliveryMethod::File
142 } else {
143 DeliveryMethod::Mailbox
144 }
145 } else {
146 DeliveryMethod::Mailbox
147 };
148
149 let delivery = LocalDelivery {
150 timestamp: base_event.timestamp,
151 queue_id,
152 recipient,
153 original_recipient,
154 relay,
155 delay,
156 delays,
157 dsn,
158 status,
159 delivery_method,
160 size: None,
161 nrcpt: None,
162 };
163
164 return Some(LocalEvent::LocalDelivery(delivery));
165 }
166
167 None
168 }
169
170 fn parse_external_delivery(&self, line: &str, base_event: BaseEvent) -> Option<LocalEvent> {
171 if let Some(caps) = self.external_delivery_regex.captures(line) {
172 let queue_id = caps.get(1).unwrap().as_str().to_string();
173 let recipient = caps.get(2).unwrap().as_str().to_string();
174 let relay = caps.get(3).unwrap().as_str().to_string();
175 let delay = caps.get(4).unwrap().as_str().parse::<f64>().unwrap_or(0.0);
176 let external_target = caps.get(5).unwrap().as_str().to_string();
177 let status_str = caps.get(6).unwrap().as_str();
178
179 let status = match status_str.to_lowercase().as_str() {
180 "sent" => DeliveryStatus::Sent,
181 "bounced" => DeliveryStatus::Bounced,
182 "deferred" => DeliveryStatus::Deferred,
183 _ => DeliveryStatus::Sent,
184 };
185
186 let mut details = HashMap::new();
187 details.insert("relay".to_string(), relay);
188
189 let (command, file_path) = if external_target.starts_with('/') {
190 (None, Some(external_target))
192 } else {
193 (Some(external_target), None)
195 };
196
197 let delivery = ExternalDelivery {
198 timestamp: base_event.timestamp,
199 queue_id,
200 recipient,
201 original_recipient: None,
202 command,
203 file_path,
204 status,
205 delay,
206 details,
207 };
208
209 return Some(LocalEvent::ExternalDelivery(delivery));
210 }
211
212 None
213 }
214}
215
216impl ComponentParser for LocalParser {
217 fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
218 let base_event = BaseEvent {
221 timestamp: chrono::Utc::now(),
222 hostname: "temp".to_string(),
223 component: "local".to_string(),
224 process_id: 0,
225 log_level: crate::events::base::PostfixLogLevel::Info,
226 raw_message: message.to_string(),
227 };
228
229 if let Some(local_event) = self.parse_line(message, base_event) {
230 Ok(ComponentEvent::Local(local_event))
231 } else {
232 Err(ParseError::ComponentParseError {
233 component: "local".to_string(),
234 reason: "无法识别的local日志格式".to_string(),
235 })
236 }
237 }
238
239 fn component_name(&self) -> &'static str {
240 "local"
241 }
242}
243
244impl Default for LocalParser {
245 fn default() -> Self {
246 Self::new()
247 }
248}