use lazy_static::lazy_static;
use regex::Regex;
use std::net::IpAddr;
use std::str::FromStr;
use super::ComponentParser;
use crate::error::ParseError;
use crate::events::base::BaseEvent;
use crate::events::relay::{
ConnectionIssueType, DelayBreakdown, DeliveryStatus, RelayConfigType, RelayEvent,
};
use crate::events::ComponentEvent;
pub struct RelayParser;
lazy_static! {
static ref DELIVERY_STATUS_PATTERN: Regex = Regex::new(
r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,\[\]]+)(?:\[([^\]]+)\])?(?::(\d+))?,\s+delay=([0-9.]+),\s+delays=([0-9./]+),\s+dsn=([0-9.]+),\s+status=(\w+)\s*\(([^)]*)\)"
).unwrap();
static ref NO_RELAY_PATTERN: Regex = Regex::new(
r"^([A-F0-9]+):\s+to=<([^>]+)>(?:,\s+orig_to=<[^>]+>)?,\s+relay=none,\s+delay=([0-9.]+),\s+delays=([0-9./]+),\s+dsn=([0-9.]+),\s+status=(\w+)\s*\(([^)]*)\)"
).unwrap();
static ref CONNECTION_ISSUE_PATTERN: Regex = Regex::new(
r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,\[\]]+)(?:\[([^\]]+)\])?(?::(\d+))?,.*?(?:connection\s+(lost|refused|timeout|failed)|host\s+unreachable|network\s+unreachable)"
).unwrap();
static ref TLS_ERROR_PATTERN: Regex = Regex::new(
r"^([A-F0-9]+):\s+to=<([^>]+)>,\s+relay=([^,\[\]]+).*?(?:TLS|SSL|certificate)"
).unwrap();
static ref TRANSPORT_MAP_PATTERN: Regex = Regex::new(
r"transport\s+maps?\s+(?:lookup|configuration|error)"
).unwrap();
}
impl RelayParser {
pub fn new() -> Self {
Self
}
pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<RelayEvent> {
if let Some(caps) = DELIVERY_STATUS_PATTERN.captures(line) {
return self.parse_delivery_status(caps, base_event);
}
if let Some(caps) = NO_RELAY_PATTERN.captures(line) {
return self.parse_no_relay_status(caps, base_event);
}
if let Some(caps) = CONNECTION_ISSUE_PATTERN.captures(line) {
return self.parse_connection_issue(caps, base_event, line);
}
if let Some(caps) = TLS_ERROR_PATTERN.captures(line) {
return self.parse_tls_issue(caps, base_event, line);
}
if TRANSPORT_MAP_PATTERN.is_match(line) {
return Some(RelayEvent::RelayConfiguration {
base: base_event,
config_type: RelayConfigType::TransportMapping,
details: line.to_string(),
});
}
None
}
fn parse_delivery_status(
&self,
caps: regex::Captures,
base_event: BaseEvent,
) -> Option<RelayEvent> {
let queue_id = caps.get(1)?.as_str().to_string();
let recipient = caps.get(2)?.as_str().to_string();
let relay_host = caps.get(3)?.as_str().to_string();
let relay_ip = caps.get(4).and_then(|m| IpAddr::from_str(m.as_str()).ok());
let relay_port = caps.get(5).and_then(|m| m.as_str().parse().ok());
let delay = caps.get(6)?.as_str().parse().ok()?;
let delays_str = caps.get(7)?.as_str();
let dsn = caps.get(8)?.as_str().to_string();
let status_str = caps.get(9)?.as_str();
let status_description = caps.get(10)?.as_str().to_string();
let delays = DelayBreakdown::from_delays_string(delays_str)?;
let status = self.parse_delivery_status_type(status_str)?;
Some(RelayEvent::DeliveryStatus {
base: base_event,
queue_id,
recipient,
relay_host,
relay_ip,
relay_port,
delay,
delays,
dsn,
status,
status_description,
})
}
fn parse_no_relay_status(
&self,
caps: regex::Captures,
base_event: BaseEvent,
) -> Option<RelayEvent> {
let queue_id = caps.get(1)?.as_str().to_string();
let recipient = caps.get(2)?.as_str().to_string();
let delay = caps.get(3)?.as_str().parse().ok()?;
let delays_str = caps.get(4)?.as_str();
let dsn = caps.get(5)?.as_str().to_string();
let status_str = caps.get(6)?.as_str();
let status_description = caps.get(7)?.as_str().to_string();
let delays = DelayBreakdown::from_delays_string(delays_str)?;
let status = self.parse_delivery_status_type(status_str)?;
Some(RelayEvent::DeliveryStatus {
base: base_event,
queue_id,
recipient,
relay_host: "none".to_string(),
relay_ip: None,
relay_port: None,
delay,
delays,
dsn,
status,
status_description,
})
}
fn parse_connection_issue(
&self,
caps: regex::Captures,
base_event: BaseEvent,
full_line: &str,
) -> Option<RelayEvent> {
let queue_id = caps.get(1)?.as_str().to_string();
let recipient = caps.get(2)?.as_str().to_string();
let relay_host = caps.get(3)?.as_str().to_string();
let relay_ip = caps.get(4).and_then(|m| IpAddr::from_str(m.as_str()).ok());
let issue_type = if full_line.contains("lost connection") {
ConnectionIssueType::LostConnection
} else if full_line.contains("connection refused") {
ConnectionIssueType::ConnectionRefused
} else if full_line.contains("timeout") {
ConnectionIssueType::ConnectionTimeout
} else if full_line.contains("host unreachable")
|| full_line.contains("network unreachable")
{
ConnectionIssueType::DnsResolutionFailed
} else {
ConnectionIssueType::Other
};
Some(RelayEvent::ConnectionIssue {
base: base_event,
queue_id,
recipient,
relay_host,
relay_ip,
issue_type,
error_message: full_line.to_string(),
})
}
fn parse_tls_issue(
&self,
caps: regex::Captures,
base_event: BaseEvent,
full_line: &str,
) -> Option<RelayEvent> {
let queue_id = caps.get(1)?.as_str().to_string();
let recipient = caps.get(2)?.as_str().to_string();
let relay_host = caps.get(3)?.as_str().to_string();
Some(RelayEvent::ConnectionIssue {
base: base_event,
queue_id,
recipient,
relay_host,
relay_ip: None,
issue_type: ConnectionIssueType::TlsHandshakeFailed,
error_message: full_line.to_string(),
})
}
fn parse_delivery_status_type(&self, status_str: &str) -> Option<DeliveryStatus> {
match status_str.to_lowercase().as_str() {
"sent" => Some(DeliveryStatus::Sent),
"deferred" => Some(DeliveryStatus::Deferred),
"bounced" => Some(DeliveryStatus::Bounced),
"failed" => Some(DeliveryStatus::Failed),
"rejected" => Some(DeliveryStatus::Rejected),
_ => None,
}
}
pub fn info(&self) -> &'static str {
"RELAY组件解析器 - 处理Postfix relay/smtp传输代理日志,支持投递状态、连接问题和配置事件"
}
}
impl ComponentParser for RelayParser {
fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
let base_event = BaseEvent {
timestamp: chrono::Utc::now(),
hostname: "temp".to_string(),
component: "relay".to_string(),
process_id: 0,
log_level: crate::events::base::PostfixLogLevel::Info,
raw_message: message.to_string(),
};
if let Some(relay_event) = self.parse_line(message, base_event) {
Ok(ComponentEvent::Relay(relay_event))
} else {
Err(ParseError::ComponentParseError {
component: "relay".to_string(),
reason: "无法识别的relay日志格式".to_string(),
})
}
}
fn component_name(&self) -> &'static str {
"relay"
}
fn can_parse(&self, message: &str) -> bool {
DELIVERY_STATUS_PATTERN.is_match(message)
|| NO_RELAY_PATTERN.is_match(message)
|| CONNECTION_ISSUE_PATTERN.is_match(message)
|| TLS_ERROR_PATTERN.is_match(message)
|| TRANSPORT_MAP_PATTERN.is_match(message)
}
}
impl Default for RelayParser {
fn default() -> Self {
Self::new()
}
}