postfix-log-parser 0.2.0

高性能模块化Postfix日志解析器,经3.2GB生产数据验证,SMTPD事件100%准确率
Documentation
//! # Sendmail 兼容接口组件解析器
//!
//! Sendmail 是 Postfix 的 Sendmail 兼容接口组件,负责:
//! - 提供与传统 Sendmail 兼容的命令行接口
//! - 处理来自应用程序的邮件提交请求
//! - 验证命令行参数和使用方法
//! - 桥接应用程序与 Postfix 核心系统
//!
//! ## 核心功能
//!
//! - **兼容接口**: 提供 sendmail、mailq、newaliases 等经典命令
//! - **参数验证**: 检查命令行参数的有效性和完整性
//! - **邮件提交**: 接收应用程序提交的邮件并转发给 Postfix
//! - **错误处理**: 提供详细的使用帮助和错误诊断
//!
//! ## 支持的事件类型
//!
//! - **致命使用错误**: 命令行参数错误或使用方法不当
//!
//! ## 示例日志格式
//!
//! ```text
//! # 使用错误
//! fatal: usage: mailq [options]
//! fatal: usage: sendmail [options] [recipient ...]
//! ```

use regex::Regex;

use crate::events::sendmail::{SendmailEvent, SendmailEventType};

/// SENDMAIL parser for Sendmail compatibility interface
#[derive(Debug)]
pub struct SendmailParser {
    fatal_usage_regex: Regex, // 用于完整日志行: "fatal: usage: ..."
    usage_only_regex: Regex,  // 用于组件消息: "usage: ..."
}

impl SendmailParser {
    /// Creates a new SendmailParser
    pub fn new() -> Self {
        Self {
            fatal_usage_regex: Regex::new(r"^fatal: (.+)$")
                .expect("Failed to compile fatal usage regex"),
            usage_only_regex: Regex::new(r"^usage: (.+)$")
                .expect("Failed to compile usage only regex"),
        }
    }

    /// Parse a complete log line into a SendmailEvent
    pub fn parse_log_line(&self, line: &str) -> Result<SendmailEvent, String> {
        // Basic line format: Month Day Time Host postfix/sendmail[PID]: message
        let basic_regex = Regex::new(
            r"^((?:\d{4}\s+)?\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+\S+\s+postfix/sendmail\[(\d+)\]:\s+(.+)$",
        )
        .map_err(|e| format!("Regex compilation error: {}", e))?;

        let captures = basic_regex
            .captures(line)
            .ok_or_else(|| "Line does not match sendmail log format".to_string())?;

        let timestamp = captures.get(1).unwrap().as_str();
        let process_id = captures.get(2).unwrap().as_str();
        let message = captures.get(3).unwrap().as_str();

        // Try to parse as fatal usage error
        if let Some(event) = self.parse_fatal_usage_error(timestamp, process_id, message) {
            return Ok(event);
        }

        Err(format!("Unknown sendmail message type: {}", message))
    }

    /// Get the number of supported event types
    pub fn supported_event_types(&self) -> usize {
        1
    }

    /// Check if this parser can handle the given line
    pub fn matches_component(&self, line: &str) -> bool {
        line.contains("postfix/sendmail[")
    }

    /// Parse fatal usage error
    fn parse_fatal_usage_error(
        &self,
        timestamp: &str,
        process_id: &str,
        message: &str,
    ) -> Option<SendmailEvent> {
        if let Some(captures) = self.fatal_usage_regex.captures(message) {
            let error_message = captures.get(1).unwrap().as_str();

            Some(SendmailEvent {
                timestamp: timestamp.to_string(),
                process_id: process_id.to_string(),
                event_type: SendmailEventType::FatalUsageError {
                    message: error_message.to_string(),
                },
            })
        } else {
            None
        }
    }
}

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

impl crate::components::ComponentParser for SendmailParser {
    fn parse(
        &self,
        message: &str,
    ) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
        // Try to parse as fatal usage error (message format: "usage: mailq [options]")
        if let Some(captures) = self.usage_only_regex.captures(message) {
            let error_message = captures.get(1).unwrap().as_str();

            let event = SendmailEvent {
                timestamp: "0".to_string(),  // Temporary timestamp
                process_id: "0".to_string(), // Temporary process ID
                event_type: SendmailEventType::FatalUsageError {
                    message: format!("usage: {}", error_message),
                },
            };

            return Ok(crate::events::base::ComponentEvent::Sendmail(event));
        }

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

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

    fn can_parse(&self, message: &str) -> bool {
        self.usage_only_regex.is_match(message)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::components::ComponentParser;

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

    #[test]
    fn test_fatal_usage_error_parsing() {
        let parser = create_parser();
        let log_line = "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]";

        let result = parser.parse_log_line(log_line);
        assert!(result.is_ok());

        let event = result.unwrap();
        assert_eq!(event.timestamp, "Apr 24 17:20:55");
        assert_eq!(event.process_id, "180");

        let SendmailEventType::FatalUsageError { message } = event.event_type;
        assert_eq!(message, "usage: mailq [options]");
    }

    #[test]
    fn test_different_process_ids() {
        let parser = create_parser();
        let test_cases = vec!["180", "187", "208", "216", "223", "230"];

        for pid in test_cases {
            let log_line = format!(
                "Apr 24 17:20:55 m01 postfix/sendmail[{}]: fatal: usage: mailq [options]",
                pid
            );

            let result = parser.parse_log_line(&log_line);
            assert!(result.is_ok());

            let event = result.unwrap();
            assert_eq!(event.process_id, pid);
        }
    }

    #[test]
    fn test_different_timestamps() {
        let parser = create_parser();
        let test_cases = vec![
            "Apr 24 17:20:55",
            "Apr 24 17:20:56",
            "Apr 24 17:23:16",
            "Apr 24 17:23:19",
        ];

        for timestamp in test_cases {
            let log_line = format!(
                "{} m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
                timestamp
            );

            let result = parser.parse_log_line(&log_line);
            assert!(result.is_ok());

            let event = result.unwrap();
            assert_eq!(event.timestamp, timestamp);
        }
    }

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

        // 应该匹配的行
        let matching_lines = vec![
            "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
            "Apr 24 17:20:56 m01 postfix/sendmail[187]: fatal: some other error",
        ];

        for line in matching_lines {
            assert!(parser.matches_component(line), "Should match: {}", line);
        }

        // 不应该匹配的行
        let non_matching_lines = vec![
            "Apr 24 17:20:55 m01 postfix/qmgr[78]: info: statistics",
            "Apr 24 17:20:55 m01 postfix/smtpd[78]: connect from localhost",
            "Apr 24 17:20:55 m01 postfix/cleanup[78]: message-id=<test@example.com>",
        ];

        for line in non_matching_lines {
            assert!(
                !parser.matches_component(line),
                "Should not match: {}",
                line
            );
        }
    }

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

        let invalid_lines = vec![
            "Invalid log line",
            "Apr 24 17:20:55 m01 postfix/qmgr[78]: info: statistics",
            "Apr 24 17:20:55 m01 postfix/sendmail[180]: info: some info message",
            "incomplete line",
        ];

        for line in invalid_lines {
            let result = parser.parse_log_line(line);
            assert!(result.is_err(), "Should fail to parse: {}", line);
        }
    }

    #[test]
    fn test_supported_event_types() {
        let parser = create_parser();
        assert_eq!(parser.supported_event_types(), 1);
    }

    #[test]
    fn test_parser_default() {
        let parser = SendmailParser::default();
        assert_eq!(parser.supported_event_types(), 1);
    }

    #[test]
    fn test_component_parser_parse() {
        let parser = SendmailParser::new();

        // 测试fatal usage错误 (主解析器传递的消息格式: "usage: mailq [options]")
        let message = "usage: mailq [options]";
        let result = parser.parse(message);

        assert!(result.is_ok());
        match result.unwrap() {
            crate::events::base::ComponentEvent::Sendmail(event) => {
                assert_eq!(event.process_id, "0"); // 临时进程ID
                let SendmailEventType::FatalUsageError { message } = event.event_type;
                assert_eq!(message, "usage: mailq [options]");
            }
            _ => panic!("Expected Sendmail ComponentEvent"),
        }
    }

    #[test]
    fn test_component_parser_invalid() {
        let parser = SendmailParser::new();

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

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

    #[test]
    fn test_component_name() {
        let parser = SendmailParser::new();
        assert_eq!(parser.component_name(), "sendmail");
    }

    #[test]
    fn test_can_parse() {
        let parser = SendmailParser::new();

        // 应该能解析的消息 (主解析器传递的格式,已去掉fatal前缀)
        assert!(parser.can_parse("usage: mailq [options]"));
        assert!(parser.can_parse("usage: some other command"));

        // 不应该解析的消息
        assert!(!parser.can_parse("some random message"));
        assert!(!parser.can_parse("info: some info message"));
        assert!(!parser.can_parse("warning: some warning"));
        assert!(!parser.can_parse("fatal: some other error")); // fatal前缀已被主解析器处理
    }

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

        // 真实日志样本
        let real_logs = vec![
            "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
            "Apr 24 17:20:56 m01 postfix/sendmail[187]: fatal: usage: mailq [options]",
            "Apr 24 17:23:16 m01 postfix/sendmail[208]: fatal: usage: mailq [options]",
            "Apr 24 17:23:19 m01 postfix/sendmail[216]: fatal: usage: mailq [options]",
            "Apr 24 17:23:22 m01 postfix/sendmail[223]: fatal: usage: mailq [options]",
            "Apr 24 17:23:24 m01 postfix/sendmail[230]: fatal: usage: mailq [options]",
        ];

        for (i, log_line) in real_logs.iter().enumerate() {
            let result = parser.parse_log_line(log_line);
            assert!(
                result.is_ok(),
                "Failed to parse real log sample {}: {}",
                i,
                log_line
            );

            let event = result.unwrap();
            let SendmailEventType::FatalUsageError { message } = event.event_type;
            assert_eq!(message, "usage: mailq [options]");
        }
    }
}