#!/usr/bin/perl

=head1 NAME 

qgrep - grep through qpsmtpd logs

=head1 SYNOPSIS

qgrep ( -g code | -m code | -l code | -f string ) ... files ...

=head1 DESCRIPTION

qpsmtpd writes very detailed log files. Each connection creates many
lines of log output. This script simplifies searching through qpsmtpd
logs by treating all the lines for a single connection (identified by
the second field of each line) as a single string which is then filtered
through perl grep (option -g) and map (option -m) operations.

The -l (line grep) option is a shortcut for 

    -m 'join "\n", grep { $op }, split (/\n/, $_), ""' \

i.e. it splits each connection log into lines and returns only the matching
lines. This saves a lot of typing as qpsmtpd log files often contain lots of 
uninteresting lines.

The -f (fixed string grep) option is a simpler variant of the -g option.
It selects all connections containing the string given as an argument. 
This is especially useful if you search for message-ids, because they often 
contain characters (like @ and $) which are special to Perl and need to be
escaped.


=head2 EXAMPLES

    qgrep logfile

Dump all log entries to stdout ordered by connection.
All lines for a connection will be dumped before the next connection
starts. Connections will be in the same order as their first lines in
the logfile. This is already quite a bit more readable if you have many
parallel connections.

    qgrep -g '/Queued/' logfile

Dump only lines from connections where at least one mail was queued.

    qgrep -g '/Queued/' \
    -m '"."'  logfile | wc -c

As above, but replace all lines of of a connection with a single dot and
count the dots: This gives you the number of connections which deliverd
mail.

    qgrep -g '/Queued/' -g '/RCPT TO: /i' \
    -m 'join "\n", grep /  (dispatching|\d{3}) /, split /\n/, $_' \
    -m '"$_\n\n"' \
    logfile

or, using the -l option:

    qgrep -g '/Queued/' -g '/RCPT TO: /i' \
    -l '/  (dispatching|\d{3}) /' \
    -m '"$_\n"' \
    logfile

Select all connections which match both /Queued/ and /RCPT TO: /i, then
select only the SMTP dialogue from them (actually, all lines which
contain either the word "dispatching" or a three-digit number) and add
an empty line to each connection.

=head1 TODO

Identify complex but generally useful patterns (like the one to extract
the SMTP dialogue in the example above) and turn them into options.

=head1 COPYRIGHT AND LICENSE

Copyright (c) 2006,2007 Peter J. Holzer <hjp@hjp.at>.

This plugin is licensed under the same terms as the qpsmtpd package
itself.
Please see the LICENSE file included with qpsmtpd for details.

=cut 

use warnings;
use strict;
use Getopt::Long;

my @ops;

sub make_op {
    my $sub = eval "sub { $_[1] }";
    die "cannot eval sub { $_[1] }: $@\n" unless $sub;
    push @ops, [ $_[0], $sub ]
}

sub make_fixed_string_grep {
    my ($option, $needle) = @_;
    push (@ops, [ 'g', sub { index($_, $needle) != -1 } ]);
}


GetOptions("g=s" => \&make_op,
           "m=s" => \&make_op,
           "l=s" => \&make_op,
           "f=s" => \&make_fixed_string_grep,
          );

for my $f (@ARGV) {
    my $fh;
    if ($f =~ /\.gz$/) {
	if ($] >= 5.8) {
	    open ($fh, "-|", "zcat", $f) or die "cannot open $f: $!";
	} else {
	    open ($fh, "-|", "zcat $f") or die "cannot open $f: $!";
	}
    } else {
	open ($fh, "<", $f) or die "cannot open $f: $!";
    }
    my @conns;
    my %conns;
    while (<$fh>) {
	if (/^\S+ ([0-9a-f.]+) /) {
	    my $cid = $1;
	    if ($conns{$cid}) {
		${ $conns{$cid} } .= $_;
	    } else {
		push @conns, $_;
		$conns{$cid} = \$conns[-1];
	    }
	}
    }
    close($fh);
    %conns = ();
    for my $op (@ops) {
	if ($op->[0] eq 'g') {
	    @conns = grep { $op->[1]($_) } @conns;
	} elsif ($op->[0] eq 'm') {
	    @conns = map { $op->[1]($_) } @conns;
	} elsif ($op->[0] eq 'l') {
	    @conns = map {
			join("\n", (grep { $op->[1]($_) } split(/\n/)), '');
		     } @conns;
	} else {
	    die "op $op->[0] not implemented\n";
	}
    }
    print @conns; 
}

