postfix-log-parser 0.2.0

高性能模块化Postfix日志解析器,经3.2GB生产数据验证,SMTPD事件100%准确率
Documentation
//! # Trivial Rewrite 地址重写组件解析器
//!
//! Trivial Rewrite 是 Postfix 的地址重写和路由决策组件,负责:
//! - 解析和重写邮件地址格式
//! - 确定邮件的路由和传输方式
//! - 处理虚拟域和别名映射
//! - 验证域配置的一致性
//!
//! ## 核心功能
//!
//! - **地址重写**: 标准化和重写邮件地址格式
//! - **路由决策**: 确定邮件的投递路径和方法
//! - **域解析**: 处理本地域、虚拟域、中继域
//! - **配置验证**: 检查域配置的冲突和重复
//!
//! ## 支持的事件类型
//!
//! - **配置覆盖警告**: 配置文件中重复参数定义的警告
//! - **域配置警告**: 域在多个配置项中重复定义的警告
//!
//! ## 示例日志格式
//!
//! ```text
//! # 配置覆盖警告
//! warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
//!
//! # 域配置警告
//! warning: do not list domain qq.com in BOTH virtual_alias_domains and relay_domains
//! ```

use crate::components::ComponentParser;
use crate::events::trivial_rewrite::TrivialRewriteEvent;
use crate::events::ComponentEvent;
use chrono::Utc;
use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
    // 配置覆盖警告:/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access...
    // 注意:主解析器已经移除了"warning:"前缀
    static ref CONFIG_OVERRIDE_WARNING_REGEX: Regex = Regex::new(
        r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$"
    ).unwrap();

    // 域配置警告:do not list domain qq.com in BOTH virtual_alias_domains and relay_domains
    // 注意:主解析器已经移除了"warning:"前缀
    static ref DOMAIN_CONFIG_WARNING_REGEX: Regex = Regex::new(
        r"^do not list domain (.+?) in BOTH (.+?) and (.+?)$"
    ).unwrap();
}

pub struct TrivialRewriteParser;

impl TrivialRewriteParser {
    pub fn new() -> Self {
        Self
    }

    /// 解析配置覆盖警告
    /// 注意:主解析器已经移除了"warning:"前缀
    fn parse_config_override_warning(&self, message: &str) -> Option<TrivialRewriteEvent> {
        if let Some(captures) = CONFIG_OVERRIDE_WARNING_REGEX.captures(message) {
            let file_path = captures.get(1)?.as_str().to_string();
            let line_number: u32 = captures.get(2)?.as_str().parse().ok()?;
            let parameter_name = captures.get(3)?.as_str().to_string();
            let parameter_value = captures.get(4)?.as_str().to_string();

            Some(TrivialRewriteEvent::config_override_warning(
                Utc::now(),
                None,
                file_path,
                line_number,
                parameter_name,
                parameter_value,
            ))
        } else {
            None
        }
    }

    /// 解析域配置警告
    /// 注意:主解析器已经移除了"warning:"前缀
    fn parse_domain_config_warning(&self, message: &str) -> Option<TrivialRewriteEvent> {
        if let Some(captures) = DOMAIN_CONFIG_WARNING_REGEX.captures(message) {
            let domain = captures.get(1)?.as_str().to_string();
            let domain_list1 = captures.get(2)?.as_str().to_string();
            let domain_list2 = captures.get(3)?.as_str().to_string();
            let full_message = message.to_string(); // 不再需要移除"warning:"前缀

            Some(TrivialRewriteEvent::domain_config_warning(
                Utc::now(),
                None,
                domain,
                domain_list1,
                domain_list2,
                full_message,
            ))
        } else {
            None
        }
    }
}

impl ComponentParser for TrivialRewriteParser {
    fn parse(&self, message: &str) -> Result<ComponentEvent, crate::error::ParseError> {
        let clean_message = message.trim();

        // 首先尝试解析配置覆盖警告
        if let Some(event) = self.parse_config_override_warning(clean_message) {
            return Ok(ComponentEvent::TrivialRewrite(event));
        }

        // 然后尝试解析域配置警告
        if let Some(event) = self.parse_domain_config_warning(clean_message) {
            return Ok(ComponentEvent::TrivialRewrite(event));
        }

        Err(crate::error::ParseError::ComponentParseError {
            component: "trivial-rewrite".to_string(),
            reason: format!("Unable to parse message: {}", message),
        })
    }

    fn component_name(&self) -> &'static str {
        "trivial-rewrite"
    }
}

impl Default for TrivialRewriteParser {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::events::trivial_rewrite::TrivialRewriteEventType;

    fn create_parser() -> TrivialRewriteParser {
        TrivialRewriteParser::new()
    }

    #[test]
    fn test_parse_config_override_warning() {
        let parser = create_parser();

        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";
        let result = parser.parse_config_override_warning(message);

        assert!(result.is_some());
        let event = result.unwrap();

        match &event.event_type {
            TrivialRewriteEventType::ConfigOverrideWarning {
                file_path,
                line_number,
                parameter_name,
                parameter_value,
            } => {
                assert_eq!(file_path, "/etc/postfix/main.cf");
                assert_eq!(*line_number, 820);
                assert_eq!(parameter_name, "smtpd_recipient_restrictions");
                assert!(parameter_value.contains("check_client_access"));
            }
            _ => panic!("Expected ConfigOverrideWarning event type"),
        }
    }

    #[test]
    fn test_parse_domain_config_warning() {
        let parser = create_parser();

        let message = "do not list domain qq.com in BOTH virtual_alias_domains and relay_domains";
        let result = parser.parse_domain_config_warning(message);

        assert!(result.is_some());
        let event = result.unwrap();

        match &event.event_type {
            TrivialRewriteEventType::DomainConfigWarning {
                domain,
                domain_list1,
                domain_list2,
                message: full_message,
            } => {
                assert_eq!(domain, "qq.com");
                assert_eq!(domain_list1, "virtual_alias_domains");
                assert_eq!(domain_list2, "relay_domains");
                assert_eq!(
                    full_message,
                    "do not list domain qq.com in BOTH virtual_alias_domains and relay_domains"
                );
            }
            _ => panic!("Expected DomainConfigWarning event type"),
        }
    }

    #[test]
    fn test_component_parser_parse_config_override() {
        let parser = create_parser();

        let message = "/etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0";
        let result = parser.parse(message);

        assert!(result.is_ok());
        match result.unwrap() {
            ComponentEvent::TrivialRewrite(event) => {
                assert_eq!(event.severity(), "warning");
                assert_eq!(
                    event.parameter_name(),
                    Some("smtpd_client_message_rate_limit")
                );
            }
            _ => panic!("Expected TrivialRewrite ComponentEvent"),
        }
    }

    #[test]
    fn test_component_parser_parse_domain_config() {
        let parser = create_parser();

        let message = "do not list domain qq.com in BOTH virtual_alias_domains and relay_domains";
        let result = parser.parse(message);

        assert!(result.is_ok());
        match result.unwrap() {
            ComponentEvent::TrivialRewrite(event) => {
                assert_eq!(event.severity(), "warning");
                assert_eq!(event.domain(), Some("qq.com"));
            }
            _ => panic!("Expected TrivialRewrite ComponentEvent"),
        }
    }

    #[test]
    fn test_component_parser_parse_invalid() {
        let parser = create_parser();

        let message = "some unknown message format";
        let result = parser.parse(message);

        assert!(result.is_err());
        match result.unwrap_err() {
            crate::error::ParseError::ComponentParseError { component, .. } => {
                assert_eq!(component, "trivial-rewrite");
            }
            _ => panic!("Expected ComponentParseError"),
        }
    }

    #[test]
    fn test_component_name() {
        let parser = create_parser();
        assert_eq!(parser.component_name(), "trivial-rewrite");
    }

    #[test]
    fn test_parse_real_log_samples() {
        let parser = create_parser();

        // 真实日志样例1:配置覆盖
        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";
        let result1 = parser.parse(message1);
        assert!(result1.is_ok());

        // 真实日志样例2:域配置警告
        let message2 = "do not list domain qq.com in BOTH virtual_alias_domains and relay_domains";
        let result2 = parser.parse(message2);
        assert!(result2.is_ok());

        // 真实日志样例3:另一个参数覆盖
        let message3 = "/etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn";
        let result3 = parser.parse(message3);
        assert!(result3.is_ok());

        match result3.unwrap() {
            ComponentEvent::TrivialRewrite(event) => {
                assert_eq!(event.parameter_name(), Some("smtpd_discard_ehlo_keywords"));
            }
            _ => panic!("Expected TrivialRewrite ComponentEvent"),
        }
    }
}