postfix_log_parser/events/
base_log.rs

1//! Postfix日志基础结构体
2//!
3//! 统一的日志基础结构,包含所有Postfix组件的公共字段。
4//! 实现"一次解析,多处使用"的设计模式,避免重复的字段解析逻辑。
5
6use crate::utils::common_fields::{
7    ClientInfo, CommonFieldsParser, DelayInfo, EmailAddress, RelayInfo, StatusInfo,
8};
9use serde::{Deserialize, Serialize};
10
11/// Postfix日志基础结构体
12/// 包含所有组件通用的字段,避免重复解析
13#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
14pub struct BaseLogEntry {
15    // === 队列和邮件标识 ===
16    /// 队列ID(如果有)
17    pub queue_id: Option<String>,
18    /// Message-ID(如果有)
19    pub message_id: Option<String>,
20
21    // === 邮件地址信息 ===
22    /// 发件人地址
23    pub from_email: Option<EmailAddress>,
24    /// 收件人地址
25    pub to_email: Option<EmailAddress>,
26    /// 原始收件人地址(alias/forward前)
27    pub orig_to_email: Option<EmailAddress>,
28
29    // === 连接和中继信息 ===
30    /// 客户端连接信息
31    pub client_info: Option<ClientInfo>,
32    /// 中继主机信息
33    pub relay_info: Option<RelayInfo>,
34
35    // === 性能和状态信息 ===
36    /// 延迟信息
37    pub delay_info: Option<DelayInfo>,
38    /// 状态信息
39    pub status_info: Option<StatusInfo>,
40
41    // === 邮件属性 ===
42    /// 邮件大小(字节)
43    pub size: Option<u64>,
44    /// 收件人数量
45    pub nrcpt: Option<u32>,
46
47    // === 协议和认证信息 ===
48    /// 协议类型(SMTP/ESMTP)
49    pub protocol: Option<String>,
50    /// HELO/EHLO信息
51    pub helo: Option<String>,
52    /// SASL认证方法
53    pub sasl_method: Option<String>,
54    /// SASL用户名
55    pub sasl_username: Option<String>,
56
57    // === 原始消息(用于未解析的字段) ===
58    /// 原始日志消息(去除时间戳和组件名)
59    pub raw_message: String,
60}
61
62impl BaseLogEntry {
63    /// 从原始日志消息创建基础日志条目
64    ///
65    /// # 参数
66    /// * `raw_message` - 原始日志消息(去除时间戳和组件名)
67    /// * `queue_id` - 预先提取的队列ID(如果有)
68    ///
69    /// # 返回
70    /// 填充了公共字段的基础日志条目
71    pub fn from_message(raw_message: &str, queue_id: Option<String>) -> Self {
72        let mut entry = BaseLogEntry {
73            queue_id,
74            message_id: CommonFieldsParser::extract_message_id(raw_message),
75            from_email: CommonFieldsParser::extract_from_email(raw_message),
76            to_email: CommonFieldsParser::extract_to_email(raw_message),
77            orig_to_email: CommonFieldsParser::extract_orig_to_email(raw_message),
78            client_info: CommonFieldsParser::extract_client_info(raw_message),
79            relay_info: CommonFieldsParser::extract_relay_info(raw_message),
80            delay_info: CommonFieldsParser::extract_delay_info(raw_message),
81            status_info: CommonFieldsParser::extract_status_info(raw_message),
82            size: CommonFieldsParser::extract_size(raw_message),
83            nrcpt: CommonFieldsParser::extract_nrcpt(raw_message),
84            protocol: CommonFieldsParser::extract_protocol(raw_message),
85            helo: CommonFieldsParser::extract_helo(raw_message),
86            sasl_method: CommonFieldsParser::extract_sasl_method(raw_message),
87            sasl_username: CommonFieldsParser::extract_sasl_username(raw_message),
88            raw_message: raw_message.to_string(),
89        };
90
91        // 优化:如果没有queue_id但消息中有,尝试提取
92        if entry.queue_id.is_none() {
93            entry.queue_id = crate::utils::queue_id::extract_queue_id(raw_message);
94        }
95
96        entry
97    }
98
99    /// 创建空白的基础日志条目
100    pub fn empty(raw_message: &str) -> Self {
101        BaseLogEntry {
102            queue_id: None,
103            message_id: None,
104            from_email: None,
105            to_email: None,
106            orig_to_email: None,
107            client_info: None,
108            relay_info: None,
109            delay_info: None,
110            status_info: None,
111            size: None,
112            nrcpt: None,
113            protocol: None,
114            helo: None,
115            sasl_method: None,
116            sasl_username: None,
117            raw_message: raw_message.to_string(),
118        }
119    }
120
121    /// 检查是否包含邮件传输相关的字段
122    pub fn has_delivery_info(&self) -> bool {
123        self.to_email.is_some()
124            || self.relay_info.is_some()
125            || self.status_info.is_some()
126            || self.delay_info.is_some()
127    }
128
129    /// 检查是否包含客户端连接信息
130    pub fn has_client_info(&self) -> bool {
131        self.client_info.is_some() || self.helo.is_some()
132    }
133
134    /// 检查是否包含认证信息
135    pub fn has_auth_info(&self) -> bool {
136        self.sasl_method.is_some() || self.sasl_username.is_some()
137    }
138
139    /// 获取格式化的发件人地址字符串
140    pub fn formatted_from(&self) -> String {
141        self.from_email
142            .as_ref()
143            .map(|email| {
144                if email.is_empty {
145                    "<>".to_string()
146                } else {
147                    format!("<{}>", email.address)
148                }
149            })
150            .unwrap_or_else(|| "N/A".to_string())
151    }
152
153    /// 获取格式化的收件人地址字符串
154    pub fn formatted_to(&self) -> String {
155        self.to_email
156            .as_ref()
157            .map(|email| format!("<{}>", email.address))
158            .unwrap_or_else(|| "N/A".to_string())
159    }
160
161    /// 获取格式化的客户端信息字符串
162    pub fn formatted_client(&self) -> String {
163        self.client_info
164            .as_ref()
165            .map(|client| {
166                if let Some(port) = client.port {
167                    format!("{}[{}]:{}", client.hostname, client.ip, port)
168                } else {
169                    format!("{}[{}]", client.hostname, client.ip)
170                }
171            })
172            .unwrap_or_else(|| "N/A".to_string())
173    }
174
175    /// 获取格式化的中继信息字符串
176    pub fn formatted_relay(&self) -> String {
177        self.relay_info
178            .as_ref()
179            .map(|relay| {
180                if relay.is_none {
181                    "none".to_string()
182                } else if let Some(ip) = &relay.ip {
183                    if let Some(port) = relay.port {
184                        format!("{}[{}]:{}", relay.hostname, ip, port)
185                    } else {
186                        format!("{}[{}]", relay.hostname, ip)
187                    }
188                } else {
189                    relay.hostname.clone()
190                }
191            })
192            .unwrap_or_else(|| "N/A".to_string())
193    }
194
195    /// 获取格式化的状态信息字符串
196    pub fn formatted_status(&self) -> String {
197        self.status_info
198            .as_ref()
199            .map(|status| {
200                let mut result = status.status.clone();
201                if let Some(desc) = &status.description {
202                    result.push_str(&format!(" ({})", desc));
203                }
204                result
205            })
206            .unwrap_or_else(|| "N/A".to_string())
207    }
208}
209
210/// 扩展的日志条目trait
211/// 允许各组件基于BaseLogEntry添加特定字段
212pub trait ExtendedLogEntry {
213    /// 获取基础日志条目
214    fn base_entry(&self) -> &BaseLogEntry;
215
216    /// 获取组件类型名称
217    fn component_type(&self) -> &'static str;
218
219    /// 获取事件类型名称
220    fn event_type(&self) -> &'static str;
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_base_log_entry_creation() {
229        let message =
230            "4bG4VR5z: from=<sender@example.com>, to=<recipient@example.com>, size=1234, delay=5.5";
231        let entry = BaseLogEntry::from_message(message, Some("4bG4VR5z".to_string()));
232
233        assert_eq!(entry.queue_id, Some("4bG4VR5z".to_string()));
234        assert!(entry.from_email.is_some());
235        assert!(entry.to_email.is_some());
236        assert_eq!(entry.size, Some(1234));
237        assert!(entry.delay_info.is_some());
238        assert_eq!(entry.delay_info.as_ref().unwrap().total, 5.5);
239    }
240
241    #[test]
242    fn test_formatted_methods() {
243        let message = "4bG4VR5z: from=<sender@example.com>, to=<recipient@example.com>, client=mail.example.com[192.168.1.100]:25";
244        let entry = BaseLogEntry::from_message(message, None);
245
246        assert_eq!(entry.formatted_from(), "<sender@example.com>");
247        assert_eq!(entry.formatted_to(), "<recipient@example.com>");
248        assert_eq!(
249            entry.formatted_client(),
250            "mail.example.com[192.168.1.100]:25"
251        );
252    }
253
254    #[test]
255    fn test_empty_from_address() {
256        let message = "4bG4VR5z: from=<>, to=<recipient@example.com>";
257        let entry = BaseLogEntry::from_message(message, None);
258
259        assert_eq!(entry.formatted_from(), "<>");
260        assert!(entry.from_email.as_ref().unwrap().is_empty);
261    }
262
263    #[test]
264    fn test_capability_checks() {
265        let delivery_message = "4bG4VR5z: to=<user@example.com>, relay=mx.example.com, status=sent";
266        let delivery_entry = BaseLogEntry::from_message(delivery_message, None);
267        assert!(delivery_entry.has_delivery_info());
268
269        let client_message =
270            "4bG4VR5z: client=mail.example.com[192.168.1.100], helo=<mail.example.com>";
271        let client_entry = BaseLogEntry::from_message(client_message, None);
272        assert!(client_entry.has_client_info());
273
274        let auth_message = "4bG4VR5z: sasl_method=PLAIN, sasl_username=testuser";
275        let auth_entry = BaseLogEntry::from_message(auth_message, None);
276        assert!(auth_entry.has_auth_info());
277    }
278}