]> git.proxmox.com Git - pmg-log-tracker.git/blobdiff - src/main.rs
bump version to 2.5.0
[pmg-log-tracker.git] / src / main.rs
index fb064637fb7dc5e513a3410a129211139459268c..0a4f1922d442f38409aab2d56b12dec386876ad7 100644 (file)
@@ -9,113 +9,59 @@ use std::io::BufReader;
 use std::io::BufWriter;
 use std::io::Write;
 
-use anyhow::{bail, Error};
+use anyhow::{bail, Context, Error};
 use flate2::read;
 use libc::time_t;
 
-use clap::{App, Arg};
+mod time;
+use time::{Tm, CAL_MTOD};
+
+fn print_usage() {
+    let pkg_version = env!("CARGO_PKG_VERSION");
+    println!(
+        "\
+pmg-log-tracker {pkg_version}
+Proxmox Mailgateway Log Tracker. Tool to scan mail logs.
+
+USAGE:
+    pmg-log-tracker [OPTIONS]
+
+OPTIONS:
+    -e, --endtime <TIME>            End time (YYYY-MM-DD HH:MM:SS) or seconds since epoch
+    -f, --from <SENDER>             Mails from SENDER
+    -g, --exclude-greylist          Exclude greylist entries
+    -h, --host <HOST>               Hostname or Server IP
+        --help                      Print help information
+    -i, --inputfile <INPUTFILE>     Input file to use instead of /var/log/syslog, or '-' for stdin
+    -l, --limit <MAX>               Print MAX entries [default: 0]
+    -m, --message-id <MSGID>        Message ID (exact match)
+    -n, --exclude-ndr               Exclude NDR entries
+    -q, --queue-id <QID>            Queue ID (exact match), can be specified multiple times
+    -s, --starttime <TIME>          Start time (YYYY-MM-DD HH:MM:SS) or seconds since epoch
+    -t, --to <RECIPIENT>            Mails to RECIPIENT
+    -v, --verbose                   Verbose output, can be specified multiple times
+    -V, --version                   Print version information
+    -x, --search-string <STRING>    Search for string",
+    );
+}
 
 fn main() -> Result<(), Error> {
-    let matches = App::new(clap::crate_name!())
-        .version(clap::crate_version!())
-        .about(clap::crate_description!())
-        .arg(
-            Arg::with_name("verbose")
-                .short("v")
-                .long("verbose")
-                .help("Verbose output, can be specified multiple times")
-                .multiple(true)
-                .takes_value(false),
-        )
-        .arg(
-            Arg::with_name("inputfile")
-                .short("i")
-                .long("inputfile")
-                .help("Input file to use instead of /var/log/syslog, or '-' for stdin")
-                .value_name("INPUTFILE"),
-        )
-        .arg(
-            Arg::with_name("host")
-                .short("h")
-                .long("host")
-                .help("Hostname or Server IP")
-                .value_name("HOST"),
-        )
-        .arg(
-            Arg::with_name("from")
-                .short("f")
-                .long("from")
-                .help("Mails from SENDER")
-                .value_name("SENDER"),
-        )
-        .arg(
-            Arg::with_name("to")
-                .short("t")
-                .long("to")
-                .help("Mails to RECIPIENT")
-                .value_name("RECIPIENT"),
-        )
-        .arg(
-            Arg::with_name("start")
-                .short("s")
-                .long("starttime")
-                .help("Start time (YYYY-MM-DD HH:MM:SS) or seconds since epoch")
-                .value_name("TIME"),
-        )
-        .arg(
-            Arg::with_name("end")
-                .short("e")
-                .long("endtime")
-                .help("End time (YYYY-MM-DD HH:MM:SS) or seconds since epoch")
-                .value_name("TIME"),
-        )
-        .arg(
-            Arg::with_name("msgid")
-                .short("m")
-                .long("message-id")
-                .help("Message ID (exact match)")
-                .value_name("MSGID"),
-        )
-        .arg(
-            Arg::with_name("qids")
-                .short("q")
-                .long("queue-id")
-                .help("Queue ID (exact match), can be specified multiple times")
-                .value_name("QID")
-                .multiple(true)
-                .number_of_values(1),
-        )
-        .arg(
-            Arg::with_name("search")
-                .short("x")
-                .long("search-string")
-                .help("Search for string")
-                .value_name("STRING"),
-        )
-        .arg(
-            Arg::with_name("limit")
-                .short("l")
-                .long("limit")
-                .help("Print MAX entries")
-                .value_name("MAX")
-                .default_value("0"),
-        )
-        .arg(
-            Arg::with_name("exclude_greylist")
-                .short("g")
-                .long("exclude-greylist")
-                .help("Exclude greylist entries"),
-        )
-        .arg(
-            Arg::with_name("exclude_ndr")
-                .short("n")
-                .long("exclude-ndr")
-                .help("Exclude NDR entries"),
-        )
-        .get_matches();
+    let mut args = pico_args::Arguments::from_env();
+    if args.contains("--help") {
+        print_usage();
+        return Ok(());
+    }
 
-    let mut parser = Parser::new();
-    parser.handle_args(matches)?;
+    let mut parser = Parser::new()?;
+    parser.handle_args(&mut args)?;
+
+    let remaining_options = args.finish();
+    if !remaining_options.is_empty() {
+        bail!(
+            "Found invalid arguments: {:?}",
+            remaining_options.join(", ".as_ref())
+        )
+    }
 
     println!("# LogReader: {}", std::process::id());
     println!("# Query options");
@@ -144,12 +90,12 @@ fn main() -> Result<(), Error> {
 
     println!(
         "# Start: {} ({})",
-        time::strftime("%F %T", &parser.start_tm)?,
+        proxmox_time::strftime_local("%F %T", parser.options.start)?,
         parser.options.start
     );
     println!(
         "# End: {} ({})",
-        time::strftime("%F %T", &parser.end_tm)?,
+        proxmox_time::strftime_local("%F %T", parser.options.end)?,
         parser.options.end
     );
 
@@ -251,7 +197,6 @@ fn handle_pmg_smtp_filter_message(msg: &[u8], parser: &mut Parser, complete_line
         let time = &data[..time_count];
 
         fe.borrow_mut().set_processing_time(time);
-        return;
     }
 }
 
@@ -803,8 +748,9 @@ impl Default for ToEntry {
     }
 }
 
-#[derive(Debug, PartialEq, Copy, Clone)]
+#[derive(Debug, PartialEq, Copy, Clone, Default)]
 enum DStatus {
+    #[default]
     Invalid,
     Accept,
     Quarantine,
@@ -817,12 +763,6 @@ enum DStatus {
     Dsn(u32),
 }
 
-impl Default for DStatus {
-    fn default() -> Self {
-        DStatus::Invalid
-    }
-}
-
 impl std::fmt::Display for DStatus {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let c = match self {
@@ -845,7 +785,6 @@ impl std::fmt::Display for DStatus {
 struct SEntry {
     log: Vec<(Box<[u8]>, u64)>,
     connect: Box<[u8]>,
-    cursor: Box<[u8]>,
     pid: u64,
     // references to QEntries, Weak so they are not kept alive longer than
     // necessary, RefCell for mutability (Rc<> is immutable)
@@ -992,18 +931,15 @@ impl SEntry {
             return;
         }
 
-        // don't print if there's a string match specified, but none of the
-        // log entries matches. in the before-queue case we also have to check
-        // the attached filter for a match
+        // don't print if there's a string match specified, but none of the log entries matches.
+        // in the before-queue case we also have to check the attached filter for a match
         if !parser.options.string_match.is_empty() {
             if let Some(fe) = &self.filter() {
                 if !self.string_match && !fe.borrow().string_match {
                     return;
                 }
-            } else {
-                if !self.string_match {
-                    return;
-                }
+            } else if !self.string_match {
+                return;
             }
         }
 
@@ -1373,6 +1309,7 @@ impl QEntry {
         true
     }
 
+    #[allow(clippy::wrong_self_convention)]
     fn from_to_matches(&mut self, parser: &Parser) -> bool {
         if !parser.options.from.is_empty() {
             if self.from.is_empty() {
@@ -1743,10 +1680,10 @@ struct Parser {
 }
 
 impl Parser {
-    fn new() -> Self {
-        let ltime = time::now();
+    fn new() -> Result<Self, Error> {
+        let ltime = Tm::now_local()?;
 
-        Self {
+        Ok(Self {
             sentries: HashMap::new(),
             fentries: HashMap::new(),
             qentries: HashMap::new(),
@@ -1760,12 +1697,12 @@ impl Parser {
             count: 0,
             buffered_stdout: BufWriter::with_capacity(4 * 1024 * 1024, std::io::stdout()),
             options: Options::default(),
-            start_tm: time::empty_tm(),
-            end_tm: time::empty_tm(),
+            start_tm: Tm::zero(),
+            end_tm: Tm::zero(),
             ctime: 0,
             string_match: false,
             lines: 0,
-        }
+        })
     }
 
     fn free_sentry(&mut self, sentry_pid: u64) {
@@ -1802,7 +1739,6 @@ impl Parser {
         } else {
             let filecount = self.count_files_in_time_range();
             for i in (0..filecount).rev() {
-                self.current_month = 0;
                 if let Ok(file) = File::open(LOGFILES[i]) {
                     self.current_file_index = i;
                     if i > 1 {
@@ -1844,6 +1780,11 @@ impl Parser {
                 line,
                 self.current_year,
                 self.current_month,
+                // use start time for timezone offset in parse_time_no_year rather than the
+                // timezone offset of the current time
+                // this is required for cases where current time is in standard time, while start
+                // time is in summer time or the other way around
+                self.start_tm.tm_gmtoff,
             ) {
                 Some(t) => t,
                 None => continue,
@@ -1916,8 +1857,6 @@ impl Parser {
         let mut buffer = Vec::new();
 
         for (i, item) in LOGFILES.iter().enumerate() {
-            self.current_month = 0;
-
             count = i;
             if let Ok(file) = File::open(item) {
                 self.current_file_index = i;
@@ -1934,6 +1873,7 @@ impl Parser {
                             &buffer[0..size],
                             self.current_year,
                             self.current_month,
+                            self.start_tm.tm_gmtoff,
                         ) {
                             // found the earliest file in the time frame
                             if time < self.options.start {
@@ -1953,6 +1893,7 @@ impl Parser {
                             &buffer[0..size],
                             self.current_year,
                             self.current_month,
+                            self.start_tm.tm_gmtoff,
                         ) {
                             if time < self.options.start {
                                 break;
@@ -1970,99 +1911,97 @@ impl Parser {
         count + 1
     }
 
-    fn handle_args(&mut self, args: clap::ArgMatches) -> Result<(), Error> {
-        if let Some(inputfile) = args.value_of("inputfile") {
-            self.options.inputfile = inputfile.to_string();
+    fn handle_args(&mut self, args: &mut pico_args::Arguments) -> Result<(), Error> {
+        if let Some(inputfile) = args.opt_value_from_str(["-i", "--inputfile"])? {
+            self.options.inputfile = inputfile;
         }
 
-        if let Some(start) = args.value_of("start") {
-            if let Ok(res) = time::strptime(start, "%F %T") {
-                self.options.start = mkgmtime(&res);
-                self.start_tm = res;
-            } else if let Ok(res) = time::strptime(start, "%s") {
-                let res = res.to_local();
-                self.options.start = mkgmtime(&res);
-                self.start_tm = res;
+        if let Some(start) = args.opt_value_from_str::<_, String>(["-s", "--starttime"])? {
+            if let Ok(epoch) = proxmox_time::parse_rfc3339(&start).or_else(|_| {
+                time::date_to_rfc3339(&start).and_then(|s| proxmox_time::parse_rfc3339(&s))
+            }) {
+                self.options.start = epoch;
+                self.start_tm = time::Tm::at_local(epoch).context("failed to parse start time")?;
+            } else if let Ok(epoch) = start.parse::<time_t>() {
+                self.options.start = epoch;
+                self.start_tm = time::Tm::at_local(epoch).context("failed to parse start time")?;
             } else {
                 bail!("failed to parse start time");
             }
         } else {
-            let mut ltime = time::now();
+            let mut ltime = Tm::now_local()?;
             ltime.tm_sec = 0;
             ltime.tm_min = 0;
             ltime.tm_hour = 0;
-            self.options.start = mkgmtime(&ltime);
+            self.options.start = ltime.as_utc_to_epoch();
             self.start_tm = ltime;
         }
 
-        if let Some(end) = args.value_of("end") {
-            if let Ok(res) = time::strptime(end, "%F %T") {
-                self.options.end = mkgmtime(&res);
-                self.end_tm = res;
-            } else if let Ok(res) = time::strptime(end, "%s") {
-                let res = res.to_local();
-                self.options.end = mkgmtime(&res);
-                self.end_tm = res;
+        if let Some(end) = args.opt_value_from_str::<_, String>(["-e", "--endtime"])? {
+            if let Ok(epoch) = proxmox_time::parse_rfc3339(&end).or_else(|_| {
+                time::date_to_rfc3339(&end).and_then(|s| proxmox_time::parse_rfc3339(&s))
+            }) {
+                self.options.end = epoch;
+                self.end_tm = time::Tm::at_local(epoch).context("failed to parse end time")?;
+            } else if let Ok(epoch) = end.parse::<time_t>() {
+                self.options.end = epoch;
+                self.end_tm = time::Tm::at_local(epoch).context("failed to parse end time")?;
             } else {
                 bail!("failed to parse end time");
             }
         } else {
-            let ltime = time::now();
-            self.options.end = mkgmtime(&ltime);
-            self.end_tm = ltime;
+            self.options.end = unsafe { libc::time(std::ptr::null_mut()) };
+            self.end_tm = Tm::at_local(self.options.end)?;
         }
 
         if self.options.end < self.options.start {
             bail!("end time before start time");
         }
 
-        self.options.limit = match args.value_of("limit") {
+        self.options.limit = match args.opt_value_from_str::<_, String>(["-l", "--limit"])? {
             Some(l) => l.parse().unwrap(),
             None => 0,
         };
 
-        if let Some(qids) = args.values_of("qids") {
-            for q in qids {
-                let ltime: time_t = 0;
-                let rel_line_nr: libc::c_ulong = 0;
-                let input = CString::new(q)?;
-                let bytes = concat!("T%08lXL%08lX", "\0");
-                let format =
-                    unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) };
-                if unsafe {
-                    libc::sscanf(input.as_ptr(), format.as_ptr(), &ltime, &rel_line_nr) == 2
-                } {
-                    self.options
-                        .match_list
-                        .push(Match::RelLineNr(ltime, rel_line_nr));
-                } else {
-                    self.options
-                        .match_list
-                        .push(Match::Qid(q.as_bytes().into()));
-                }
+        while let Some(q) = args.opt_value_from_str::<_, String>(["-q", "--queue-id"])? {
+            let ltime: time_t = 0;
+            let rel_line_nr: libc::c_ulong = 0;
+            let input = CString::new(q.as_str())?;
+            let bytes = concat!("T%08lXL%08lX", "\0");
+            let format = unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) };
+            if unsafe { libc::sscanf(input.as_ptr(), format.as_ptr(), &ltime, &rel_line_nr) == 2 } {
+                self.options
+                    .match_list
+                    .push(Match::RelLineNr(ltime, rel_line_nr));
+            } else {
+                self.options
+                    .match_list
+                    .push(Match::Qid(q.as_bytes().into()));
             }
         }
 
-        if let Some(from) = args.value_of("from") {
-            self.options.from = from.to_string();
+        if let Some(from) = args.opt_value_from_str(["-f", "--from"])? {
+            self.options.from = from;
         }
-        if let Some(to) = args.value_of("to") {
-            self.options.to = to.to_string();
+        if let Some(to) = args.opt_value_from_str(["-t", "--to"])? {
+            self.options.to = to;
         }
-        if let Some(host) = args.value_of("host") {
-            self.options.host = host.to_string();
+        if let Some(host) = args.opt_value_from_str(["-h", "--host"])? {
+            self.options.host = host;
         }
-        if let Some(msgid) = args.value_of("msgid") {
-            self.options.msgid = msgid.to_string();
+        if let Some(msgid) = args.opt_value_from_str(["-m", "--msgid"])? {
+            self.options.msgid = msgid;
         }
 
-        self.options.exclude_greylist = args.is_present("exclude_greylist");
-        self.options.exclude_ndr = args.is_present("exclude_ndr");
+        self.options.exclude_greylist = args.contains(["-g", "--exclude-greylist"]);
+        self.options.exclude_ndr = args.contains(["-n", "--exclude-ndr"]);
 
-        self.options.verbose = args.occurrences_of("verbose") as _;
+        while args.contains(["-v", "--verbose"]) {
+            self.options.verbose += 1;
+        }
 
-        if let Some(string_match) = args.value_of("search") {
-            self.options.string_match = string_match.to_string();
+        if let Some(string_match) = args.opt_value_from_str(["-x", "--search-string"])? {
+            self.options.string_match = string_match;
         }
 
         Ok(())
@@ -2171,30 +2110,6 @@ fn get_or_create_fentry(
     }
 }
 
-fn mkgmtime(tm: &time::Tm) -> time_t {
-    let mut res: time_t;
-
-    let mut year = tm.tm_year as i64 + 1900;
-    let mon = tm.tm_mon;
-
-    res = (year - 1970) * 365 + CAL_MTOD[mon as usize];
-
-    if mon <= 1 {
-        year -= 1;
-    }
-
-    res += (year - 1968) / 4;
-    res -= (year - 1900) / 100;
-    res += (year - 1600) / 400;
-
-    res += (tm.tm_mday - 1) as i64;
-    res = res * 24 + tm.tm_hour as i64;
-    res = res * 60 + tm.tm_min as i64;
-    res = res * 60 + tm.tm_sec as i64;
-
-    res
-}
-
 const LOGFILES: [&str; 32] = [
     "/var/log/syslog",
     "/var/log/syslog.1",
@@ -2276,11 +2191,54 @@ fn parse_number(data: &[u8], max_digits: usize) -> Option<(usize, &[u8])> {
 }
 
 /// Parse time. Returns a tuple of (parsed_time, remaining_text) or None.
-fn parse_time<'a>(
-    data: &'a [u8],
+fn parse_time(
+    data: &'_ [u8],
     cur_year: i64,
     cur_month: i64,
-) -> Option<(time_t, &'a [u8])> {
+    timezone_offset: time_t,
+) -> Option<(time_t, &'_ [u8])> {
+    parse_time_with_year(data)
+        .or_else(|| parse_time_no_year(data, cur_year, cur_month, timezone_offset))
+}
+
+fn parse_time_with_year(data: &'_ [u8]) -> Option<(time_t, &'_ [u8])> {
+    let mut timestamp_buffer = [0u8; 25];
+
+    let count = data.iter().take_while(|b| **b != b' ').count();
+    if count != 27 && count != 32 {
+        return None;
+    }
+    let (timestamp, data) = data.split_at(count);
+    // remove whitespace
+    let data = &data[1..];
+
+    // microseconds: .123456 -> 7 bytes
+    let microseconds_idx = timestamp.iter().take_while(|b| **b != b'.').count();
+
+    // YYYY-MM-DDTHH:MM:SS
+    let year_time = &timestamp[0..microseconds_idx];
+    let year_time_len = year_time.len();
+    // Z | +HH:MM | -HH:MM
+    let timezone = &timestamp[microseconds_idx + 7..];
+    let timezone_len = timezone.len();
+    let timestamp_len = year_time_len + timezone_len;
+    timestamp_buffer[0..year_time_len].copy_from_slice(year_time);
+    timestamp_buffer[year_time_len..timestamp_len].copy_from_slice(timezone);
+
+    match proxmox_time::parse_rfc3339(unsafe {
+        std::str::from_utf8_unchecked(&timestamp_buffer[0..timestamp_len])
+    }) {
+        Ok(ltime) => Some((ltime, data)),
+        Err(_err) => None,
+    }
+}
+
+fn parse_time_no_year(
+    data: &'_ [u8],
+    cur_year: i64,
+    cur_month: i64,
+    timezone_offset: time_t,
+) -> Option<(time_t, &'_ [u8])> {
     if data.len() < 15 {
         return None;
     }
@@ -2302,15 +2260,14 @@ fn parse_time<'a>(
     };
     let data = &data[3..];
 
-    let mut ltime: time_t;
-    let mut year = cur_year;
-
     // assume smaller month now than in log line means yearwrap
-    if cur_month < mon {
-        year -= 1;
-    }
+    let mut year = if cur_month < mon {
+        cur_year - 1
+    } else {
+        cur_year
+    };
 
-    ltime = (year - 1970) * 365 + CAL_MTOD[mon as usize];
+    let mut ltime: time_t = (year - 1970) * 365 + CAL_MTOD[mon as usize];
 
     // leap year considerations
     if mon <= 1 {
@@ -2325,9 +2282,7 @@ fn parse_time<'a>(
 
     let (mday, data) = match parse_number(data, 2) {
         Some(t) => t,
-        None => {
-            return None;
-        }
+        None => return None,
     };
     if mday == 0 {
         return None;
@@ -2343,9 +2298,7 @@ fn parse_time<'a>(
 
     let (hour, data) = match parse_number(data, 2) {
         Some(t) => t,
-        None => {
-            return None;
-        }
+        None => return None,
     };
 
     ltime *= 24;
@@ -2362,9 +2315,7 @@ fn parse_time<'a>(
 
     let (min, data) = match parse_number(data, 2) {
         Some(t) => t,
-        None => {
-            return None;
-        }
+        None => return None,
     };
 
     ltime *= 60;
@@ -2381,9 +2332,7 @@ fn parse_time<'a>(
 
     let (sec, data) = match parse_number(data, 2) {
         Some(t) => t,
-        None => {
-            return None;
-        }
+        None => return None,
     };
 
     ltime *= 60;
@@ -2394,11 +2343,9 @@ fn parse_time<'a>(
         _ => &data[1..],
     };
 
-    Some((ltime, data))
+    Some((ltime - timezone_offset, data))
 }
 
-const CAL_MTOD: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
-
 type ByteSlice<'a> = &'a [u8];
 /// Parse Host, Service and PID at the beginning of data. Returns a tuple of (host, service, pid, remaining_text).
 fn parse_host_service_pid(data: &[u8]) -> Option<(ByteSlice, ByteSlice, u64, ByteSlice)> {
@@ -2417,7 +2364,7 @@ fn parse_host_service_pid(data: &[u8]) -> Option<(ByteSlice, ByteSlice, u64, Byt
         .count();
     let service = &data[0..service_count];
     let data = &data[service_count..];
-    if data.get(0) != Some(&b'[') {
+    if data.first() != Some(&b'[') {
         return None;
     }
     let data = &data[1..];