]> 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 419257b32f7c6cab06421093acde0fd10742cccd..0a4f1922d442f38409aab2d56b12dec386876ad7 100644 (file)
@@ -1,9 +1,3 @@
-#[macro_use]
-extern crate clap;
-extern crate failure;
-extern crate flate2;
-extern crate libc;
-
 use std::cell::RefCell;
 use std::collections::HashMap;
 use std::ffi::CString;
@@ -15,112 +9,59 @@ use std::io::BufReader;
 use std::io::BufWriter;
 use std::io::Write;
 
-use failure::Error;
+use anyhow::{bail, Context, Error};
 use flate2::read;
+use libc::time_t;
+
+mod time;
+use time::{Tm, CAL_MTOD};
 
-use clap::{App, Arg};
+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(crate_name!())
-        .version(crate_version!())
-        .about(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");
@@ -139,7 +80,7 @@ fn main() -> Result<(), Error> {
     for m in parser.options.match_list.iter() {
         match m {
             Match::Qid(b) => println!("# QID: {}", std::str::from_utf8(b)?),
-            Match::RelLineNr(t, l) => println!("# QID: T{:8X}L{:08X}", *t as u32, *l as u32),
+            Match::RelLineNr(t, l) => println!("# QID: T{:8X}L{:08X}", *t, *l as u32),
         }
     }
 
@@ -149,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
     );
 
@@ -256,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;
     }
 }
 
@@ -786,7 +726,7 @@ struct NoqueueEntry {
     from: Box<[u8]>,
     to: Box<[u8]>,
     dstatus: DStatus,
-    timestamp: u64,
+    timestamp: time_t,
 }
 
 #[derive(Debug)]
@@ -794,7 +734,7 @@ struct ToEntry {
     to: Box<[u8]>,
     relay: Box<[u8]>,
     dstatus: DStatus,
-    timestamp: u64,
+    timestamp: time_t,
 }
 
 impl Default for ToEntry {
@@ -808,8 +748,9 @@ impl Default for ToEntry {
     }
 }
 
-#[derive(Debug, PartialEq, Copy, Clone)]
+#[derive(Debug, PartialEq, Copy, Clone, Default)]
 enum DStatus {
+    #[default]
     Invalid,
     Accept,
     Quarantine,
@@ -822,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 {
@@ -850,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)
@@ -861,7 +795,7 @@ struct SEntry {
     // used as a fallback in case no QEntry is referenced
     filter: Option<Weak<RefCell<FEntry>>>,
     string_match: bool,
-    timestamp: u64,
+    timestamp: time_t,
     rel_line_nr: u64,
     // before queue filtering with the mail accepted for at least one receiver
     is_bq_accepted: bool,
@@ -872,7 +806,7 @@ struct SEntry {
 }
 
 impl SEntry {
-    fn add_noqueue_entry(&mut self, from: &[u8], to: &[u8], dstatus: DStatus, timestamp: u64) {
+    fn add_noqueue_entry(&mut self, from: &[u8], to: &[u8], dstatus: DStatus, timestamp: time_t) {
         let ne = NoqueueEntry {
             to: to.into(),
             from: from.into(),
@@ -981,7 +915,7 @@ impl SEntry {
                 match m {
                     Match::Qid(_) => return,
                     Match::RelLineNr(t, l) => {
-                        if (*t as u64) == self.timestamp && *l == self.rel_line_nr {
+                        if *t == self.timestamp && *l == self.rel_line_nr {
                             found = true;
                             break;
                         }
@@ -997,25 +931,22 @@ 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;
             }
         }
 
         if parser.options.verbose > 0 {
             parser.write_all_ok(format!(
                 "SMTPD: T{:8X}L{:08X}\n",
-                self.timestamp as u32, self.rel_line_nr as u32
+                self.timestamp, self.rel_line_nr as u32
             ));
             parser.write_all_ok(format!("CTIME: {:8X}\n", parser.ctime).as_bytes());
 
@@ -1034,7 +965,7 @@ impl SEntry {
             if nq.dstatus != DStatus::Invalid {
                 parser.write_all_ok(format!(
                     "TO:{:X}:T{:08X}L{:08X}:{}: from <",
-                    nq.timestamp as i32, self.timestamp as i32, self.rel_line_nr, nq.dstatus,
+                    nq.timestamp, self.timestamp, self.rel_line_nr, nq.dstatus,
                 ));
                 parser.write_all_ok(&nq.from);
                 parser.write_all_ok(b"> to <");
@@ -1049,7 +980,7 @@ impl SEntry {
                 for to in fe.borrow().to_entries.iter().rev() {
                     parser.write_all_ok(format!(
                         "TO:{:X}:T{:08X}L{:08X}:{}: from <",
-                        to.timestamp as i32, se.timestamp as i32, se.rel_line_nr, to.dstatus,
+                        to.timestamp, se.timestamp, se.rel_line_nr, to.dstatus,
                     ));
                     parser.write_all_ok(&se.bq_from);
                     parser.write_all_ok(b"> to <");
@@ -1237,7 +1168,7 @@ struct QEntry {
 }
 
 impl QEntry {
-    fn add_to_entry(&mut self, to: &[u8], relay: &[u8], dstatus: DStatus, timestamp: u64) {
+    fn add_to_entry(&mut self, to: &[u8], relay: &[u8], dstatus: DStatus, timestamp: time_t) {
         let te = ToEntry {
             to: to.into(),
             relay: relay.into(),
@@ -1340,7 +1271,7 @@ impl QEntry {
                     }
                     Match::RelLineNr(t, l) => {
                         if let Some(s) = se {
-                            if s.timestamp == (*t as u64) && s.rel_line_nr == *l {
+                            if s.timestamp == *t && s.rel_line_nr == *l {
                                 found = true;
                                 break;
                             }
@@ -1378,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() {
@@ -1549,7 +1481,7 @@ impl QEntry {
                     }
                 }
 
-                parser.write_all_ok(format!("TO:{:X}:", to.timestamp as i32,));
+                parser.write_all_ok(format!("TO:{:X}:", to.timestamp));
                 parser.write_all_ok(&self.qid);
                 parser.write_all_ok(format!(":{}: from <", final_to.dstatus));
                 parser.write_all_ok(&self.from);
@@ -1582,7 +1514,7 @@ impl QEntry {
                     });
 
                     for to in fe.borrow().to_entries.iter().rev() {
-                        parser.write_all_ok(format!("TO:{:X}:", to.timestamp as i32,));
+                        parser.write_all_ok(format!("TO:{:X}:", to.timestamp));
                         parser.write_all_ok(&self.qid);
                         parser.write_all_ok(format!(":{}: from <", to.dstatus));
                         parser.write_all_ok(&self.from);
@@ -1675,7 +1607,7 @@ struct FEntry {
 }
 
 impl FEntry {
-    fn add_accept(&mut self, to: &[u8], qid: &[u8], timestamp: u64) {
+    fn add_accept(&mut self, to: &[u8], qid: &[u8], timestamp: time_t) {
         let te = ToEntry {
             to: to.into(),
             relay: qid.into(),
@@ -1686,7 +1618,7 @@ impl FEntry {
         self.is_accepted = true;
     }
 
-    fn add_quarantine(&mut self, to: &[u8], qid: &[u8], timestamp: u64) {
+    fn add_quarantine(&mut self, to: &[u8], qid: &[u8], timestamp: time_t) {
         let te = ToEntry {
             to: to.into(),
             relay: qid.into(),
@@ -1696,7 +1628,7 @@ impl FEntry {
         self.to_entries.push(te);
     }
 
-    fn add_block(&mut self, to: &[u8], timestamp: u64) {
+    fn add_block(&mut self, to: &[u8], timestamp: time_t) {
         let te = ToEntry {
             to: to.into(),
             relay: (&b"none"[..]).into(),
@@ -1728,7 +1660,7 @@ struct Parser {
     current_record_state: RecordState,
     rel_line_nr: u64,
 
-    current_year: [i64; 32],
+    current_year: i64,
     current_month: i64,
     current_file_index: usize,
 
@@ -1741,24 +1673,17 @@ struct Parser {
     start_tm: time::Tm,
     end_tm: time::Tm,
 
-    ctime: libc::time_t,
+    ctime: time_t,
     string_match: bool,
 
     lines: u64,
 }
 
 impl Parser {
-    fn new() -> Self {
-        let mut years: [i64; 32] = [0; 32];
+    fn new() -> Result<Self, Error> {
+        let ltime = Tm::now_local()?;
 
-        for (i, year) in years.iter_mut().enumerate() {
-            let mut ts = time::get_time();
-            ts.sec -= (3600 * 24 * i) as i64;
-            let ltime = time::at(ts);
-            *year = (ltime.tm_year + 1900) as i64;
-        }
-
-        Self {
+        Ok(Self {
             sentries: HashMap::new(),
             fentries: HashMap::new(),
             qentries: HashMap::new(),
@@ -1766,18 +1691,18 @@ impl Parser {
             smtp_tls_log_by_pid: HashMap::new(),
             current_record_state: Default::default(),
             rel_line_nr: 0,
-            current_year: years,
-            current_month: 0,
+            current_year: (ltime.tm_year + 1900) as i64,
+            current_month: ltime.tm_mon as i64,
             current_file_index: 0,
             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) {
@@ -1814,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 {
@@ -1854,8 +1778,13 @@ impl Parser {
 
             let (time, line) = match parse_time(
                 line,
-                self.current_year[self.current_file_index],
-                &mut self.current_month,
+                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,
@@ -1890,7 +1819,7 @@ impl Parser {
             self.current_record_state.host = host.into();
             self.current_record_state.service = service.into();
             self.current_record_state.pid = pid;
-            self.current_record_state.timestamp = time as u64;
+            self.current_record_state.timestamp = time;
 
             self.string_match = false;
             if !self.options.string_match.is_empty()
@@ -1928,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;
@@ -1944,8 +1871,9 @@ impl Parser {
                         }
                         if let Some((time, _)) = parse_time(
                             &buffer[0..size],
-                            self.current_year[i],
-                            &mut self.current_month,
+                            self.current_year,
+                            self.current_month,
+                            self.start_tm.tm_gmtoff,
                         ) {
                             // found the earliest file in the time frame
                             if time < self.options.start {
@@ -1963,8 +1891,9 @@ impl Parser {
                         }
                         if let Some((time, _)) = parse_time(
                             &buffer[0..size],
-                            self.current_year[i],
-                            &mut self.current_month,
+                            self.current_year,
+                            self.current_month,
+                            self.start_tm.tm_gmtoff,
                         ) {
                             if time < self.options.start {
                                 break;
@@ -1982,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 {
-                failure::bail!(failure::err_msg("failed to parse start time"));
+                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 {
-                failure::bail!(failure::err_msg("failed to parse end time"));
+                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 {
-            failure::bail!(failure::err_msg("end time before start time"));
+            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: libc::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(())
@@ -2116,8 +2043,8 @@ struct Options {
     msgid: String,
     from: String,
     to: String,
-    start: libc::time_t,
-    end: libc::time_t,
+    start: time_t,
+    end: time_t,
     limit: u64,
     verbose: u32,
     exclude_greylist: bool,
@@ -2127,7 +2054,7 @@ struct Options {
 #[derive(Debug)]
 enum Match {
     Qid(Box<[u8]>),
-    RelLineNr(libc::time_t, u64),
+    RelLineNr(time_t, u64),
 }
 
 #[derive(Debug, Default)]
@@ -2135,7 +2062,7 @@ struct RecordState {
     host: Box<[u8]>,
     service: Box<[u8]>,
     pid: u64,
-    timestamp: u64,
+    timestamp: time_t,
 }
 
 fn get_or_create_qentry(
@@ -2156,7 +2083,7 @@ fn get_or_create_sentry(
     sentries: &mut HashMap<u64, Rc<RefCell<SEntry>>>,
     pid: u64,
     rel_line_nr: u64,
-    timestamp: u64,
+    timestamp: time_t,
 ) -> Rc<RefCell<SEntry>> {
     if let Some(se) = sentries.get(&pid) {
         Rc::clone(se)
@@ -2183,30 +2110,6 @@ fn get_or_create_fentry(
     }
 }
 
-fn mkgmtime(tm: &time::Tm) -> libc::time_t {
-    let mut res: libc::time_t;
-
-    let mut year = (tm.tm_year + 1900) as i64;
-    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",
@@ -2288,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,
+    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: &mut i64,
-) -> Option<(libc::time_t, &'a [u8])> {
+    cur_month: i64,
+    timezone_offset: time_t,
+) -> Option<(time_t, &'_ [u8])> {
     if data.len() < 15 {
         return None;
     }
@@ -2314,22 +2260,19 @@ fn parse_time<'a>(
     };
     let data = &data[3..];
 
-    let mut ltime: libc::time_t;
-    let mut year = cur_year;
-
-    if *cur_month == 11 && mon == 0 {
-        year += 1;
-    }
-    if mon > *cur_month {
-        *cur_month = mon;
-    }
+    // assume smaller month now than in log line means yearwrap
+    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 {
         year -= 1;
     }
-
     ltime += (year - 1968) / 4;
     ltime -= (year - 1900) / 100;
     ltime += (year - 1600) / 400;
@@ -2339,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;
@@ -2357,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;
@@ -2376,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;
@@ -2395,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;
@@ -2408,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)> {
@@ -2431,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..];