postfix-log-parser 0.2.0

高性能模块化Postfix日志解析器,经3.2GB生产数据验证,SMTPD事件100%准确率
Documentation
//! Postfix日志基础结构体
//!
//! 统一的日志基础结构,包含所有Postfix组件的公共字段。
//! 实现"一次解析,多处使用"的设计模式,避免重复的字段解析逻辑。

use crate::utils::common_fields::{
    ClientInfo, CommonFieldsParser, DelayInfo, EmailAddress, RelayInfo, StatusInfo,
};
use serde::{Deserialize, Serialize};

/// Postfix日志基础结构体
/// 包含所有组件通用的字段,避免重复解析
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BaseLogEntry {
    // === 队列和邮件标识 ===
    /// 队列ID(如果有)
    pub queue_id: Option<String>,
    /// Message-ID(如果有)
    pub message_id: Option<String>,

    // === 邮件地址信息 ===
    /// 发件人地址
    pub from_email: Option<EmailAddress>,
    /// 收件人地址
    pub to_email: Option<EmailAddress>,
    /// 原始收件人地址(alias/forward前)
    pub orig_to_email: Option<EmailAddress>,

    // === 连接和中继信息 ===
    /// 客户端连接信息
    pub client_info: Option<ClientInfo>,
    /// 中继主机信息
    pub relay_info: Option<RelayInfo>,

    // === 性能和状态信息 ===
    /// 延迟信息
    pub delay_info: Option<DelayInfo>,
    /// 状态信息
    pub status_info: Option<StatusInfo>,

    // === 邮件属性 ===
    /// 邮件大小(字节)
    pub size: Option<u64>,
    /// 收件人数量
    pub nrcpt: Option<u32>,

    // === 协议和认证信息 ===
    /// 协议类型(SMTP/ESMTP)
    pub protocol: Option<String>,
    /// HELO/EHLO信息
    pub helo: Option<String>,
    /// SASL认证方法
    pub sasl_method: Option<String>,
    /// SASL用户名
    pub sasl_username: Option<String>,

    // === 原始消息(用于未解析的字段) ===
    /// 原始日志消息(去除时间戳和组件名)
    pub raw_message: String,
}

impl BaseLogEntry {
    /// 从原始日志消息创建基础日志条目
    ///
    /// # 参数
    /// * `raw_message` - 原始日志消息(去除时间戳和组件名)
    /// * `queue_id` - 预先提取的队列ID(如果有)
    ///
    /// # 返回
    /// 填充了公共字段的基础日志条目
    pub fn from_message(raw_message: &str, queue_id: Option<String>) -> Self {
        let mut entry = BaseLogEntry {
            queue_id,
            message_id: CommonFieldsParser::extract_message_id(raw_message),
            from_email: CommonFieldsParser::extract_from_email(raw_message),
            to_email: CommonFieldsParser::extract_to_email(raw_message),
            orig_to_email: CommonFieldsParser::extract_orig_to_email(raw_message),
            client_info: CommonFieldsParser::extract_client_info(raw_message),
            relay_info: CommonFieldsParser::extract_relay_info(raw_message),
            delay_info: CommonFieldsParser::extract_delay_info(raw_message),
            status_info: CommonFieldsParser::extract_status_info(raw_message),
            size: CommonFieldsParser::extract_size(raw_message),
            nrcpt: CommonFieldsParser::extract_nrcpt(raw_message),
            protocol: CommonFieldsParser::extract_protocol(raw_message),
            helo: CommonFieldsParser::extract_helo(raw_message),
            sasl_method: CommonFieldsParser::extract_sasl_method(raw_message),
            sasl_username: CommonFieldsParser::extract_sasl_username(raw_message),
            raw_message: raw_message.to_string(),
        };

        // 优化:如果没有queue_id但消息中有,尝试提取
        if entry.queue_id.is_none() {
            entry.queue_id = crate::utils::queue_id::extract_queue_id(raw_message);
        }

        entry
    }

    /// 创建空白的基础日志条目
    pub fn empty(raw_message: &str) -> Self {
        BaseLogEntry {
            queue_id: None,
            message_id: None,
            from_email: None,
            to_email: None,
            orig_to_email: None,
            client_info: None,
            relay_info: None,
            delay_info: None,
            status_info: None,
            size: None,
            nrcpt: None,
            protocol: None,
            helo: None,
            sasl_method: None,
            sasl_username: None,
            raw_message: raw_message.to_string(),
        }
    }

    /// 检查是否包含邮件传输相关的字段
    pub fn has_delivery_info(&self) -> bool {
        self.to_email.is_some()
            || self.relay_info.is_some()
            || self.status_info.is_some()
            || self.delay_info.is_some()
    }

    /// 检查是否包含客户端连接信息
    pub fn has_client_info(&self) -> bool {
        self.client_info.is_some() || self.helo.is_some()
    }

    /// 检查是否包含认证信息
    pub fn has_auth_info(&self) -> bool {
        self.sasl_method.is_some() || self.sasl_username.is_some()
    }

    /// 获取格式化的发件人地址字符串
    pub fn formatted_from(&self) -> String {
        self.from_email
            .as_ref()
            .map(|email| {
                if email.is_empty {
                    "<>".to_string()
                } else {
                    format!("<{}>", email.address)
                }
            })
            .unwrap_or_else(|| "N/A".to_string())
    }

    /// 获取格式化的收件人地址字符串
    pub fn formatted_to(&self) -> String {
        self.to_email
            .as_ref()
            .map(|email| format!("<{}>", email.address))
            .unwrap_or_else(|| "N/A".to_string())
    }

    /// 获取格式化的客户端信息字符串
    pub fn formatted_client(&self) -> String {
        self.client_info
            .as_ref()
            .map(|client| {
                if let Some(port) = client.port {
                    format!("{}[{}]:{}", client.hostname, client.ip, port)
                } else {
                    format!("{}[{}]", client.hostname, client.ip)
                }
            })
            .unwrap_or_else(|| "N/A".to_string())
    }

    /// 获取格式化的中继信息字符串
    pub fn formatted_relay(&self) -> String {
        self.relay_info
            .as_ref()
            .map(|relay| {
                if relay.is_none {
                    "none".to_string()
                } else if let Some(ip) = &relay.ip {
                    if let Some(port) = relay.port {
                        format!("{}[{}]:{}", relay.hostname, ip, port)
                    } else {
                        format!("{}[{}]", relay.hostname, ip)
                    }
                } else {
                    relay.hostname.clone()
                }
            })
            .unwrap_or_else(|| "N/A".to_string())
    }

    /// 获取格式化的状态信息字符串
    pub fn formatted_status(&self) -> String {
        self.status_info
            .as_ref()
            .map(|status| {
                let mut result = status.status.clone();
                if let Some(desc) = &status.description {
                    result.push_str(&format!(" ({})", desc));
                }
                result
            })
            .unwrap_or_else(|| "N/A".to_string())
    }
}

/// 扩展的日志条目trait
/// 允许各组件基于BaseLogEntry添加特定字段
pub trait ExtendedLogEntry {
    /// 获取基础日志条目
    fn base_entry(&self) -> &BaseLogEntry;

    /// 获取组件类型名称
    fn component_type(&self) -> &'static str;

    /// 获取事件类型名称
    fn event_type(&self) -> &'static str;
}

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

    #[test]
    fn test_base_log_entry_creation() {
        let message =
            "4bG4VR5z: from=<sender@example.com>, to=<recipient@example.com>, size=1234, delay=5.5";
        let entry = BaseLogEntry::from_message(message, Some("4bG4VR5z".to_string()));

        assert_eq!(entry.queue_id, Some("4bG4VR5z".to_string()));
        assert!(entry.from_email.is_some());
        assert!(entry.to_email.is_some());
        assert_eq!(entry.size, Some(1234));
        assert!(entry.delay_info.is_some());
        assert_eq!(entry.delay_info.as_ref().unwrap().total, 5.5);
    }

    #[test]
    fn test_formatted_methods() {
        let message = "4bG4VR5z: from=<sender@example.com>, to=<recipient@example.com>, client=mail.example.com[192.168.1.100]:25";
        let entry = BaseLogEntry::from_message(message, None);

        assert_eq!(entry.formatted_from(), "<sender@example.com>");
        assert_eq!(entry.formatted_to(), "<recipient@example.com>");
        assert_eq!(
            entry.formatted_client(),
            "mail.example.com[192.168.1.100]:25"
        );
    }

    #[test]
    fn test_empty_from_address() {
        let message = "4bG4VR5z: from=<>, to=<recipient@example.com>";
        let entry = BaseLogEntry::from_message(message, None);

        assert_eq!(entry.formatted_from(), "<>");
        assert!(entry.from_email.as_ref().unwrap().is_empty);
    }

    #[test]
    fn test_capability_checks() {
        let delivery_message = "4bG4VR5z: to=<user@example.com>, relay=mx.example.com, status=sent";
        let delivery_entry = BaseLogEntry::from_message(delivery_message, None);
        assert!(delivery_entry.has_delivery_info());

        let client_message =
            "4bG4VR5z: client=mail.example.com[192.168.1.100], helo=<mail.example.com>";
        let client_entry = BaseLogEntry::from_message(client_message, None);
        assert!(client_entry.has_client_info());

        let auth_message = "4bG4VR5z: sasl_method=PLAIN, sasl_username=testuser";
        let auth_entry = BaseLogEntry::from_message(auth_message, None);
        assert!(auth_entry.has_auth_info());
    }
}