postfix_log_parser/components/
trivial_rewrite.rs

1//! # Trivial Rewrite 地址重写组件解析器
2//!
3//! Trivial Rewrite 是 Postfix 的地址重写和路由决策组件,负责:
4//! - 解析和重写邮件地址格式
5//! - 确定邮件的路由和传输方式
6//! - 处理虚拟域和别名映射
7//! - 验证域配置的一致性
8//!
9//! ## 核心功能
10//!
11//! - **地址重写**: 标准化和重写邮件地址格式
12//! - **路由决策**: 确定邮件的投递路径和方法
13//! - **域解析**: 处理本地域、虚拟域、中继域
14//! - **配置验证**: 检查域配置的冲突和重复
15//!
16//! ## 支持的事件类型
17//!
18//! - **配置覆盖警告**: 配置文件中重复参数定义的警告
19//! - **域配置警告**: 域在多个配置项中重复定义的警告
20//!
21//! ## 示例日志格式
22//!
23//! ```text
24//! # 配置覆盖警告
25//! warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
26//!
27//! # 域配置警告
28//! warning: do not list domain qq.com in BOTH virtual_alias_domains and relay_domains
29//! ```
30
31use crate::components::ComponentParser;
32use crate::events::trivial_rewrite::TrivialRewriteEvent;
33use crate::events::ComponentEvent;
34use chrono::Utc;
35use lazy_static::lazy_static;
36use regex::Regex;
37
38lazy_static! {
39    // 配置覆盖警告:/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access...
40    // 注意:主解析器已经移除了"warning:"前缀
41    static ref CONFIG_OVERRIDE_WARNING_REGEX: Regex = Regex::new(
42        r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$"
43    ).unwrap();
44
45    // 域配置警告:do not list domain qq.com in BOTH virtual_alias_domains and relay_domains
46    // 注意:主解析器已经移除了"warning:"前缀
47    static ref DOMAIN_CONFIG_WARNING_REGEX: Regex = Regex::new(
48        r"^do not list domain (.+?) in BOTH (.+?) and (.+?)$"
49    ).unwrap();
50}
51
52pub struct TrivialRewriteParser;
53
54impl TrivialRewriteParser {
55    pub fn new() -> Self {
56        Self
57    }
58
59    /// 解析配置覆盖警告
60    /// 注意:主解析器已经移除了"warning:"前缀
61    fn parse_config_override_warning(&self, message: &str) -> Option<TrivialRewriteEvent> {
62        if let Some(captures) = CONFIG_OVERRIDE_WARNING_REGEX.captures(message) {
63            let file_path = captures.get(1)?.as_str().to_string();
64            let line_number: u32 = captures.get(2)?.as_str().parse().ok()?;
65            let parameter_name = captures.get(3)?.as_str().to_string();
66            let parameter_value = captures.get(4)?.as_str().to_string();
67
68            Some(TrivialRewriteEvent::config_override_warning(
69                Utc::now(),
70                None,
71                file_path,
72                line_number,
73                parameter_name,
74                parameter_value,
75            ))
76        } else {
77            None
78        }
79    }
80
81    /// 解析域配置警告
82    /// 注意:主解析器已经移除了"warning:"前缀
83    fn parse_domain_config_warning(&self, message: &str) -> Option<TrivialRewriteEvent> {
84        if let Some(captures) = DOMAIN_CONFIG_WARNING_REGEX.captures(message) {
85            let domain = captures.get(1)?.as_str().to_string();
86            let domain_list1 = captures.get(2)?.as_str().to_string();
87            let domain_list2 = captures.get(3)?.as_str().to_string();
88            let full_message = message.to_string(); // 不再需要移除"warning:"前缀
89
90            Some(TrivialRewriteEvent::domain_config_warning(
91                Utc::now(),
92                None,
93                domain,
94                domain_list1,
95                domain_list2,
96                full_message,
97            ))
98        } else {
99            None
100        }
101    }
102}
103
104impl ComponentParser for TrivialRewriteParser {
105    fn parse(&self, message: &str) -> Result<ComponentEvent, crate::error::ParseError> {
106        let clean_message = message.trim();
107
108        // 首先尝试解析配置覆盖警告
109        if let Some(event) = self.parse_config_override_warning(clean_message) {
110            return Ok(ComponentEvent::TrivialRewrite(event));
111        }
112
113        // 然后尝试解析域配置警告
114        if let Some(event) = self.parse_domain_config_warning(clean_message) {
115            return Ok(ComponentEvent::TrivialRewrite(event));
116        }
117
118        Err(crate::error::ParseError::ComponentParseError {
119            component: "trivial-rewrite".to_string(),
120            reason: format!("Unable to parse message: {}", message),
121        })
122    }
123
124    fn component_name(&self) -> &'static str {
125        "trivial-rewrite"
126    }
127}
128
129impl Default for TrivialRewriteParser {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::events::trivial_rewrite::TrivialRewriteEventType;
139
140    fn create_parser() -> TrivialRewriteParser {
141        TrivialRewriteParser::new()
142    }
143
144    #[test]
145    fn test_parse_config_override_warning() {
146        let parser = create_parser();
147
148        let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted,permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination,pcre:/etc/postfix/filter_default";
149        let result = parser.parse_config_override_warning(message);
150
151        assert!(result.is_some());
152        let event = result.unwrap();
153
154        match &event.event_type {
155            TrivialRewriteEventType::ConfigOverrideWarning {
156                file_path,
157                line_number,
158                parameter_name,
159                parameter_value,
160            } => {
161                assert_eq!(file_path, "/etc/postfix/main.cf");
162                assert_eq!(*line_number, 820);
163                assert_eq!(parameter_name, "smtpd_recipient_restrictions");
164                assert!(parameter_value.contains("check_client_access"));
165            }
166            _ => panic!("Expected ConfigOverrideWarning event type"),
167        }
168    }
169
170    #[test]
171    fn test_parse_domain_config_warning() {
172        let parser = create_parser();
173
174        let message = "do not list domain qq.com in BOTH virtual_alias_domains and relay_domains";
175        let result = parser.parse_domain_config_warning(message);
176
177        assert!(result.is_some());
178        let event = result.unwrap();
179
180        match &event.event_type {
181            TrivialRewriteEventType::DomainConfigWarning {
182                domain,
183                domain_list1,
184                domain_list2,
185                message: full_message,
186            } => {
187                assert_eq!(domain, "qq.com");
188                assert_eq!(domain_list1, "virtual_alias_domains");
189                assert_eq!(domain_list2, "relay_domains");
190                assert_eq!(
191                    full_message,
192                    "do not list domain qq.com in BOTH virtual_alias_domains and relay_domains"
193                );
194            }
195            _ => panic!("Expected DomainConfigWarning event type"),
196        }
197    }
198
199    #[test]
200    fn test_component_parser_parse_config_override() {
201        let parser = create_parser();
202
203        let message = "/etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0";
204        let result = parser.parse(message);
205
206        assert!(result.is_ok());
207        match result.unwrap() {
208            ComponentEvent::TrivialRewrite(event) => {
209                assert_eq!(event.severity(), "warning");
210                assert_eq!(
211                    event.parameter_name(),
212                    Some("smtpd_client_message_rate_limit")
213                );
214            }
215            _ => panic!("Expected TrivialRewrite ComponentEvent"),
216        }
217    }
218
219    #[test]
220    fn test_component_parser_parse_domain_config() {
221        let parser = create_parser();
222
223        let message = "do not list domain qq.com in BOTH virtual_alias_domains and relay_domains";
224        let result = parser.parse(message);
225
226        assert!(result.is_ok());
227        match result.unwrap() {
228            ComponentEvent::TrivialRewrite(event) => {
229                assert_eq!(event.severity(), "warning");
230                assert_eq!(event.domain(), Some("qq.com"));
231            }
232            _ => panic!("Expected TrivialRewrite ComponentEvent"),
233        }
234    }
235
236    #[test]
237    fn test_component_parser_parse_invalid() {
238        let parser = create_parser();
239
240        let message = "some unknown message format";
241        let result = parser.parse(message);
242
243        assert!(result.is_err());
244        match result.unwrap_err() {
245            crate::error::ParseError::ComponentParseError { component, .. } => {
246                assert_eq!(component, "trivial-rewrite");
247            }
248            _ => panic!("Expected ComponentParseError"),
249        }
250    }
251
252    #[test]
253    fn test_component_name() {
254        let parser = create_parser();
255        assert_eq!(parser.component_name(), "trivial-rewrite");
256    }
257
258    #[test]
259    fn test_parse_real_log_samples() {
260        let parser = create_parser();
261
262        // 真实日志样例1:配置覆盖
263        let message1 = "/etc/postfix/main.cf, line 819: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted,permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination,pcre:/etc/postfix/filter_default";
264        let result1 = parser.parse(message1);
265        assert!(result1.is_ok());
266
267        // 真实日志样例2:域配置警告
268        let message2 = "do not list domain qq.com in BOTH virtual_alias_domains and relay_domains";
269        let result2 = parser.parse(message2);
270        assert!(result2.is_ok());
271
272        // 真实日志样例3:另一个参数覆盖
273        let message3 = "/etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn";
274        let result3 = parser.parse(message3);
275        assert!(result3.is_ok());
276
277        match result3.unwrap() {
278            ComponentEvent::TrivialRewrite(event) => {
279                assert_eq!(event.parameter_name(), Some("smtpd_discard_ehlo_keywords"));
280            }
281            _ => panic!("Expected TrivialRewrite ComponentEvent"),
282        }
283    }
284}