#!/usr/bin/perl
#
# parse_qmail_send_log.pl - Qmail log parser for qmail-send
#  Version: 2.1
#
# By Phil2k@gmail.com
#

use POSIX;
use Time::TAI64 qw/tai2unix/;
#use Time::HiRes;

$one_rcpt_per_sender=0;
$show_date=0;
$show_text=0;

#2008-03-03 12:22:08.279709500 new msg 46138112
#2008-03-03 12:22:08.345234500 info msg 46138112: bytes 3423 from <alaala@mumu.com> qp 16854 uid 89
#2008-03-03 12:22:08.359002310 starting delivery 3245266: msg 46138112 to remote gigi@kaka.ro
#2008-03-03 12:22:08.359002310 starting delivery 3245267: msg 46138112 to local sugus@mumu.ro
#2008-03-03 12:22:08.370301000 delivery 3245266: success: (gigi@kaka.ro)_1.2.3.4_accepted_message./Remote_host_said:_250_ok_1102089533_qp_28485/
#2008-03-03 12:22:08.370301000 delivery 3245267: success: success: did_0+0+2/
#2008-03-03 12:22:08.405230100 end msg 46138112

# Filters: $f_*
$f_interval_start=0;
$f_interval_end=0;
$f_from="";
$f_to="";

select STDERR; $| = 1;      # make unbuffered
select STDOUT; $| = 1;      # make unbuffered
                   

# Parsing options from arguments
$opts=1;
$opt="";
while(($param=shift()) ne "") {
  AGAIN:
  if ($opts) {
    if ($opt eq "") {
      if ($param=~/^-(.*)$/) {
        $opt=$1;
        if ($opt eq "-") { $opts=0; }
        elsif (($opt eq "h") || ($opt eq "help")) {
          print "$0 <options>\n";
          print "Where options are:\n";
          print " -h = this help\n";
          print " -i <starting>[ <ending>]= interval ( <ending> it's optional )\n";
          print " -f <from> = from address ( @domain = all addresses from that domain )\n";
          print " -t <to> = to address ( @domain = all addresses to that domain )\n";
          print " -1 = show one rpct per sender\n";
          print " -d = show date & time ( of message, or of delivery if -1 option )\n";
          print " -r = show returned text (error or succesfull) returned ( of message, or of delivery if -1 option )\n";
          exit(1);
          }
        elsif ($opt=~/^([ift1dr])(.*)?$/) {
          $opt=$1;
          $param=$2;
          $param=~s/^\s+//g;
          next if (($param eq "") && ($opt ne "1") && ($opt ne "d") && ($opt ne "r"));
          }
        else {
          print STDERR "Invalid option: -$opt ! Try: -h for help.\n";
          exit(1);
          }
        }
      }
    if ($opt ne "") {
      if ($param=~/^-(.*)$/) {
        if ((($opt eq "i") && ($f_interval_start || $f_interval_end)) || ($opt eq "1") || ($opt eq "d") || ($opt eq "r")) { $opt=""; goto AGAIN; }
          else { print STDERR "Missing parameter(s) for option $opt !\n"; exit(1); }
        } else {
        if ($opt eq "i") {
          if (!$f_interval_start) {
            $f_interval_start=&parse_date($param, 1);
            if (!defined($f_interval_start)) {
              print STDERR "Invalid date and/or time format in starting interval ($param) ! Valid formats are: \"YYYY-MM-DD\" or \"YYYY-MM-DD HH[:MM[:SS]]\" !\n";
              exit(1);
              }
            } else {
            $f_interval_end=&parse_date($param, 2);
            if (!defined($f_interval_end)) {
              print STDERR "Invalid date and/or time format in ending interval ($param) ! Valid formats are: \"YYYY-MM-DD\" or \"YYYY-MM-DD HH[:MM[:SS]]\" !\n";
              exit(1);
              }
            $opt="";
            }
          }
        elsif ($opt eq "f") {
          $f_from=$param;
          $opt="";
          }
        elsif ($opt eq "t") {
          $f_to=$param;
          $opt="";
          }
        elsif ($opt eq "1") {
          if ($param ne "") {
            print STDERR "Option \"-$opt\" doesn't need a parameter ($param) !\n";
            exit(1);
            }
          $one_rcpt_per_sender=1;
          $opt="";
          }
        elsif ($opt eq "d") {
          if ($param ne "") {
            print STDERR "Option \"-$opt\" doesn't need a parameter ($param) !\n";
            exit(1);
            }
          $show_date=1;
          $opt="";
          }
        elsif ($opt eq "r") {
          if ($param ne "") {
            print STDERR "Option \"-$opt\" doesn't need a parameter ($param) !\n";
            exit(1);
            }
          $show_text=1;
          $opt="";
          }
        else {
          print STDERR "Invalid option: -$opt ! Try: -h for help.\n";
          exit(1);
          }
        }
      }
    }
  }

#print "$one_rcpt_per_sender $show_date\n"; exit(0);

@msg=();
%inmsg=();
%indelivery=();
$ln=0;

#print "interval = $f_interval_start - $f_interval_end\n";
#print "from=$f_from to=$f_to\n";
while($line=<STDIN>) {
  $ln++;
  if ($line=~/^(\@\S+) (.*)/) {
    $taidate=$1;
    if ($f_interval_start || $f_interval_end || $show_date) { # parse time only when interval filters exists, for speedup
      $utime=Time::TAI64::tai2unix($taidate);
      }
    $rest=$2;
    } else {
    ($date, $rest)=$line=~/^(\d+-\d+-\d+ \d+:\d+:\d+\.\d+) (.*)$/;
    if ($f_interval_start || $f_interval_end || $show_date) { # parse time only when interval filters exists, for speedup
      ($year, $month, $day, $hour, $min, $sec)=$date=~/^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/;
      $utime=POSIX::mktime($sec, $min, $hour, $day, $month-1, $year-1900);
      }
    }
  # applying time filters:
  $valid=1;
  #print "utime=$utime interval_start=$f_interval_start interval_end=$f_interval_end\n";
  if ($f_interval_start && ($utime<$f_interval_start)) { $valid=0; }
  if ($valid && $f_interval_end && ($utime>$f_interval_end)) { $valid=0; }
  next if (!$valid);
  if ($rest=~/^new msg (\d+)/) {
    $id_msg=$1;
    if (exists($inmsg{$id_msg})) { # if the ID already exists !
      if ($inmsg{$id_msg}{"status"}<2) { # not ended
        print "[$ln] $id_msg already exists : $line";
        }
      delete($inmsg{$id_msg});
      }
    $inmsg{$id_msg}{"status"}=0;
    }
  elsif ($rest=~/^info msg (\d+): bytes (\d+) from <([^>]+)> qp \d+ uid \d+/) {
    $id_msg=$1;
    $bytes=$2;
    $from=$3;
    # applying FROM filter
    if (($f_from ne "") && (($from eq "") || (index($from, $f_from)<0))) {
      #print "Not matched: $from with $_from ... deleted.\n";
      delete($inmsg{$id_msg});
      next;
      }
    #print "!!! $id_msg $bytes $from\n";
    #if ($from eq "") {
    #  print "from it's null in $id_msg: $rest !\n";
    #  exit;
    #  }
    if (exists($inmsg{$id_msg}) && ($inmsg{$id_msg}{"status"}==0)) {
      $inmsg{$id_msg}{"status"}=1;
      $inmsg{$id_msg}{"from"}=$from;
      $inmsg{$id_msg}{"bytes"}=$bytes;
      if ($show_date && (!$one_rcpt_per_sender)) {
        $inmsg{$id_msg}{"time"}=$utime;
        }
      #} else {
      #print STDERR "Invalid mess $id_msg (info) !\n";
      }
    }
  elsif ($rest=~/^starting delivery (\d+): msg (\d+) to (local|remote) (\S+)/) {
    $id_delivery=$1;
    $id_msg=$2;
    $lr=$3;
    $rcpt=$4;
    if ($lr eq "local" && $rcpt=~/^([^-@]+)-([^@]+\@.*)$/) {
      $rcpt=$2;
      }
    # applying TO filter
    if (($f_to ne "") && (($to eq "") || (index($rcpt, $f_to)<0))) {
      delete($inmsg{$id_msg});
      next;
      }
    #print "starting delivery to mess $id_msg, delivery $id_delivery\n";
    #print $rcpt."\n";
    if (exists($inmsg{$id_msg})) {
      #print " ok $id_msg\n";
      if (exists($inmsg{$id_msg}{"dels"}{$id_delivery})) {
        print STDERR "Invalid delivery $id_delivery to $rcpt (starting delivery of mess $id_msg) !\n";
        } else {
        foreach $idd (keys %{$inmsg{$id_msg}{"dels"}}) {
          if (($inmsg{$id_msg}{"dels"}{$idd}{"status"} eq "deferral") && ($inmsg{$id_msg}{"dels"}{$idd}{"rcpt"} eq $rcpt)) { # seems to be the same email, delayed before
            delete($inmsg{$id_msg}{"dels"}{$idd});
            }
          }
        $inmsg{$id_msg}{"dels"}{$id_delivery}{"rcpt"}=$rcpt;
        $inmsg{$id_msg}{"dels"}{$id_delivery}{"lr"}=$lr;
        $inmsg{$id_msg}{"dels"}{$id_delivery}{"status"}=undef;
        if ($show_date && $one_rcpt_per_sender) {
          $inmsg{$id_msg}{"dels"}{$id_delivery}{"time"}=$utime;
          }
        #$msg{$id_msg}{"rcpts"}{$id_delivery} = [ $rcpt, 0 ];
        $indelivery{$id_delivery}=$id_msg;
        }
      }
    }
  elsif ($rest=~/^delivery (\d+): ([^:]+): (.*)/) {
    $id_delivery=$1;
    $status=$2;
    $text=$3; 
    if (exists($indelivery{$id_delivery})) {
      $id_msg=$indelivery{$id_delivery};
      $inmsg{$id_msg}{"dels"}{$id_delivery}{"status"}=$status;
      if ($show_text && $one_rcpt_per_sender) {
        $inmsg{$id_msg}{"dels"}{$id_delivery}{"text"}=$text;
        }
      if ($status eq "success") { # success delivery
        #$msg{$id_msg}{rcpts{$id_delivery}[1]=1;
        delete($indelivery{$id_delivery});
        }
      elsif ($status eq "deferral") { # deferral delivery ( resend later )
        #
        }
      elsif ($status eq "failure") { # failure delivery ( stop sending that delivery: mess to rcpt )
        delete($indelivery{$id_delivery});
        }
      }
    }
  elsif ($rest=~/^end msg (\d+)/) {
    $id_msg=$1;
    if (exists($inmsg{$id_msg})) {
      if ($inmsg{$id_msg}{"status"}==0) {
        delete($inmsg{$id_msg});
        next;
        }
      $inmsg{$id_msg}{"status"}=3;
      foreach $id_delivery (keys %{$inmsg{$id_msg}{"dels"}}) {
        #print "rcpt: ".$inmsg{$id_msg}{"dels"}{$id_delivery}{"rcpt"}."\n";
        if (exists($indelivery{$id_delivery})) {
          delete($indelivery{$id_delivery});
          }
        }
      $inmsg{$id_msg}{"id"}=$id_msg;
      
      #push @msg, \%{$inmsg{$id_msg}};
      $x=$#msg;
      $x++;
      $msg[$x]{"id"}=$id_msg;
      $msg[$x]{"from"}=$inmsg{$id_msg}{"from"};
      if ($show_date && (!$one_rcpt_per_sender)) {
        $msg[$x]{"time"}=$inmsg{$id_msg}{"time"};
        }
      #if ($msg[$x]{"from"} eq "") {
      #if ($inmsg{$id_msg}{"from"} eq "") {
      #  print "from it's null in $id_msg !!!\n";
      #  exit;
      #  }
      $msg[$x]{"status"}=$inmsg{$id_msg}{"status"};
      $msg[$x]{"bytes"}=$inmsg{$id_msg}{"bytes"};
      foreach $del (keys %{$inmsg{$id_msg}{"dels"}}) {
        foreach $key (keys %{$inmsg{$id_msg}{"dels"}{$del}}) {
          $msg[$x]{"dels"}{$del}{$key} = $inmsg{$id_msg}{"dels"}{$del}{$key};
          }
        }
      
      #print scalar(keys %{$msg[$#msg]})."\n";
      #print "OK(".$msg[$#msg]{"from"}.")\n" if (exists($msg[$#msg]{"dels"}));
      #print scalar(keys %{$msg[$#msg]{"dels"}})."\n";
      delete($inmsg{$id_msg});
      }
    }
  }

#print $#msg."\n";
for($i=0;$i<=$#msg;$i++) {
  #foreach $key (keys %{$msg[$i]}) {
  #  print "key: $key\n";
  #  }
  #exit;
  $id_msg=$msg[$i]{"id"};
  $from=$msg[$i]{"from"};
  if ($show_date && (!$one_rcpt_per_sender)) {
    print strftime("%F %T", localtime($msg[$i]{"time"}))."  ";
    }
  #if ($from eq "") {
  #  print "from is null !!!\n";
  #  exit;
  #  }
  #print "id_msg = $id_msg\n";
  if ($one_rcpt_per_sender) {
    #print "$id_msg $from\n";
    foreach $id_delivery (keys %{$msg[$i]{"dels"}}) {
      #print "$id_delivery ".$msg[$i]{"dels"}{$id_delivery}{"rcpt"}."\n";
      if ($show_date) {
        print strftime("%F %T", localtime($msg[$i]{"dels"}{$id_delivery}{"time"}))."  ";
        }
      print $from." -> ".$msg[$i]{"dels"}{$id_delivery}{"rcpt"};
      if ($msg[$i]{"dels"}{$id_delivery}{"status"} eq "success") {
        if ($show_text) { print " ( success : ".$msg[$i]{"dels"}{$id_delivery}{"text"}." )\n"; }
        } else {
        print " ( ".$msg[$i]{"dels"}{$id_delivery}{"status"}.($show_text?" : ".$msg[$i]{"dels"}{$id_delivery}{"text"}:"")." )\n";
        }
      }
    } else {
    print $from." -> ";
    $cnt=0;
    #print "(".scalar(keys %{$msg[$i]{"dels"}}).")";
    foreach $id_delivery (keys %{$msg[$i]{"dels"}}) {
      print ", " if ($cnt++);
      print $msg[$i]{"dels"}{$id_delivery}{"rcpt"};
      if ($msg[$i]{"dels"}{$id_delivery}{"status"} ne "success") {
        print " ( ".$msg[$i]{"dels"}{$id_delivery}{"status"}." )";
        }
      }
    print "\n";
    }
  }  

sub parse_date() {
  my ($str, $mode) = @_;
  my $year=0;
  my $month=0;
  my $day=0;
  my $hour;
  my $min;
  my $sec;
  if ($mode==1) {
    $hour=0;
    $min=0;
    $sec=0;
    } else {
    $hour=23;
    $min=59;
    $sec=59;
    }
  
  if ($str=~/^(\d+)-(\d+)-(\d+)(\s+.*)?$/) {
    $year=$1;
    $month=$2;
    $day=$3;
    $rest=$4;
    $rest=~s/^\s+//g;
    ($hour, $min, $sec)=split(/:/, $rest);
    if ($hour eq "") {
      if ($mode==1) { $hour=0; }
        else { $hour=23; }
      }
    if ($min eq "") {
      if  ($mode==1) { $min=0; }
        else { $min=59; }
      }
    if ($sec eq "") {
      if ($mode==1) { $sec=0; }
        else { $sec=59; }
      }
    }
  #print "$str ($mode) => year=$year month=$month day=$day hour=$hour min=$min sec=$sec\n";
  if (($year>=1900) && ($month>=1) && ($month<=12) && ($day>=1) && ($day<=31) && ($hour>=0) && ($hour<=23) && ($min>=0) && ($min<=59) && ($sec>=0) && ($sec<=59)) {
    # mktime(sec, min, hour, mday, mon, year, wday = 0, yday = 0, isdst = 0)
    return POSIX::mktime($sec, $min, $hour, $day, $month-1, $year-1900);
    } else {
    return undef;
    }
}