diff --git a/src/main.rs b/src/main.rs index 3d9dcbe..f55f285 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,18 +38,33 @@ fn run_app( fn main() -> Result<()> { // Parse command line arguments let args: Vec = env::args().collect(); - if args.len() != 2 { - eprintln!("Usage: {} ", args[0]); + if args.len() < 2 { + eprintln!("Usage: {} [additional-files...]", args[0]); std::process::exit(1); } - // Read the log file - let log_content = fs::read_to_string(&args[1]) - .with_context(|| format!("Failed to read file: {}", args[1]))?; + // Process all log files + let mut all_sessions = Vec::new(); + for filename in &args[1..] { + // Read and parse the log file with its path for better timestamp handling + let log_content = fs::read_to_string(filename) + .with_context(|| format!("Failed to read file: {}", filename))?; - // Parse the log file - let sessions = parser::parse_log_file(&log_content) - .context("Failed to parse log file")?; + // Parse the log file with the file path for better timestamp handling + let mut sessions = parser::parse_log_file_with_path(&log_content, Some(filename)) + .with_context(|| format!("Failed to parse log file: {}", filename))?; + + all_sessions.append(&mut sessions); + } + + // Sort all sessions by timestamp (newest first) + all_sessions.sort_by(|a, b| { + let a_time = a.start_time.unwrap_or_else(|| chrono::DateTime::::MIN_UTC.naive_local()); + let b_time = b.start_time.unwrap_or_else(|| chrono::DateTime::::MIN_UTC.naive_local()); + b_time.cmp(&a_time) + }); + + let sessions = all_sessions; if sessions.is_empty() { println!("No SMTP sessions found in the log file."); diff --git a/src/parser.rs b/src/parser.rs index 51f0d33..d04ebdc 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,7 +1,8 @@ use crate::models::{LogEntry, SMTPSession}; use chrono::{Datelike, NaiveDateTime}; use regex::Regex; -use std::collections::HashMap; +use std::{collections::HashMap, path::Path, time::SystemTime}; +use anyhow::Context; lazy_static::lazy_static! { static ref LOG_LINE_RE: Regex = Regex::new( @@ -9,8 +10,22 @@ lazy_static::lazy_static! { ).unwrap(); } +fn get_log_year(file_path: Option<&str>) -> Option { + file_path.and_then(|path| { + let metadata = std::fs::metadata(path).ok()?; + let modified = metadata.modified().ok()?; + let datetime: chrono::DateTime = modified.into(); + Some(datetime.year()) + }) +} + pub fn parse_log_file(contents: &str) -> anyhow::Result> { + parse_log_file_with_path(contents, None) +} + +pub fn parse_log_file_with_path(contents: &str, file_path: Option<&str>) -> anyhow::Result> { let mut sessions: HashMap = HashMap::new(); + let log_year = get_log_year(file_path).unwrap_or_else(|| chrono::Local::now().year()); for line in contents.lines() { if let Some(captures) = LOG_LINE_RE.captures(line) { @@ -22,13 +37,19 @@ pub fn parse_log_file(contents: &str) -> anyhow::Result> { let message = captures["message"].to_string(); // Create a timestamp string in the format that chrono can parse - let year = chrono::Local::now().year(); - let timestamp_str = format!("{} {} {} {}", year, month, day, time); - let timestamp = match NaiveDateTime::parse_from_str(×tamp_str, "%Y %b %d %H:%M:%S") { + let timestamp_str = format!("{} {} {} {}", log_year, month, day, time); + let mut timestamp = match NaiveDateTime::parse_from_str(×tamp_str, "%Y %b %d %H:%M:%S") { Ok(ts) => ts, Err(_) => continue, // Skip lines with invalid timestamps }; + // Handle year rollover (if log entry is from next year but file is from previous year) + if timestamp.month() == 1 && log_year != chrono::Local::now().year() { + if let Some(prev_year_timestamp) = timestamp.with_year(log_year + 1) { + timestamp = prev_year_timestamp; + } + } + let entry = LogEntry { timestamp, hostname,