postfix-log-parser 0.2.0

高性能模块化Postfix日志解析器,经3.2GB生产数据验证,SMTPD事件100%准确率
Documentation
//! # Pickup 邮件拾取组件解析器
//!
//! Pickup 是 Postfix 的邮件拾取组件,负责:
//! - 从邮件队列目录拾取新邮件
//! - 扫描并处理待发送的邮件文件
//! - 记录邮件的发送者信息和用户 ID
//! - 监控配置文件变更和覆盖警告
//!
//! ## 核心功能
//!
//! - **邮件拾取**: 从 maildrop 目录拾取新邮件
//! - **发送者验证**: 记录邮件发送者和用户身份
//! - **配置监控**: 检测配置文件参数覆盖
//! - **队列管理**: 分配队列 ID 并转发到下一阶段
//!
//! ## 支持的事件类型
//!
//! - **邮件拾取**: 新邮件被拾取并分配队列 ID
//! - **配置覆盖警告**: 配置文件中的参数重复定义警告
//!
//! ## 示例日志格式
//!
//! ```text
//! # 邮件拾取事件
//! 226751E20F00: uid=0 from=<root>
//!
//! # 配置覆盖警告
//! /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
//! ```

use crate::components::ComponentParser;
use crate::events::pickup::PickupEvent;
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: parameter=value
    // 注意:主解析器已经移除了"warning:"前缀
    static ref CONFIG_OVERRIDE_WARNING_REGEX: Regex = Regex::new(
        r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$"
    ).unwrap();

    // 邮件拾取事件:226751E20F00: uid=0 from=<root>
    static ref MAIL_PICKUP_REGEX: Regex = Regex::new(
        r"^([A-F0-9]+): uid=(\d+) from=(.+)$"
    ).unwrap();
}

pub struct PickupParser;

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

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

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

    /// 解析邮件拾取事件
    fn parse_mail_pickup(&self, message: &str) -> Option<PickupEvent> {
        if let Some(captures) = MAIL_PICKUP_REGEX.captures(message) {
            let queue_id = captures.get(1)?.as_str().to_string();
            let uid = captures.get(2)?.as_str().parse::<u32>().ok()?;
            let sender = captures.get(3)?.as_str().to_string();

            Some(PickupEvent::mail_pickup(
                Utc::now(),
                None,
                queue_id,
                uid,
                sender,
            ))
        } else {
            None
        }
    }
}

impl ComponentParser for PickupParser {
    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::Pickup(event));
        }

        // 然后尝试解析邮件拾取事件
        if let Some(event) = self.parse_mail_pickup(clean_message) {
            return Ok(ComponentEvent::Pickup(event));
        }

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

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

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

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

    fn create_parser() -> PickupParser {
        PickupParser::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 {
            PickupEventType::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_mail_pickup() {
        let parser = create_parser();

        let message = "226751E20F00: uid=0 from=<root>";
        let result = parser.parse_mail_pickup(message);

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

        match &event.event_type {
            PickupEventType::MailPickup {
                queue_id,
                uid,
                sender,
            } => {
                assert_eq!(queue_id, "226751E20F00");
                assert_eq!(*uid, 0);
                assert_eq!(sender, "<root>");
            }
            _ => panic!("Expected MailPickup 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::Pickup(event) => {
                assert_eq!(event.severity(), "warning");
                assert_eq!(
                    event.parameter_name(),
                    Some("smtpd_client_message_rate_limit")
                );
                assert_eq!(event.queue_id(), None);
                assert_eq!(event.sender(), None);
            }
            _ => panic!("Expected Pickup ComponentEvent"),
        }
    }

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

        let message = "226751E20F00: uid=0 from=<root>";
        let result = parser.parse(message);

        assert!(result.is_ok());
        match result.unwrap() {
            ComponentEvent::Pickup(event) => {
                assert_eq!(event.severity(), "info");
                assert_eq!(event.parameter_name(), None);
                assert_eq!(event.queue_id(), Some("226751E20F00"));
                assert_eq!(event.sender(), Some("<root>"));
                assert_eq!(event.uid(), Some(0));
            }
            _ => panic!("Expected Pickup 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, "pickup");
            }
            _ => panic!("Expected ComponentParseError"),
        }
    }

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

    #[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 = "226751E20F00: uid=0 from=<root>";
        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::Pickup(event) => {
                assert_eq!(event.parameter_name(), Some("smtpd_discard_ehlo_keywords"));
            }
            _ => panic!("Expected Pickup ComponentEvent"),
        }
    }
}