#!/usr/bin/perl -w

# policy-test: Postfix Policy Daemon Testing Tool
# Copyright (c) 2007 Open Systems AG, Switzerland
# released under the GNU General Public License

# see the documentation with 'perldoc policy-test'

use strict;
use Getopt::Long 2.25 qw(:config posix_default no_ignore_case);
use Pod::Usage;
use Parse::Syslog;
use IO::Socket::INET;

my $VERSION = '0.1';
my $requests; # total number of requests done
my $policy_service;
my %opt = ();

sub log_to_policy($)
{
    my ($file) = @_;
    my $parser = Parse::Syslog->new($file);

    my %state;

    while(my $sl = $parser->next) {
        $sl->{program} =~ /^postfix\/(.*)/ or next;
        my $pprogram = $1;
        if($sl->{text} =~ /^NOQUEUE/) {
            $sl->{text} =~ m{
                ^NOQUEUE:\ reject:   # NOQUEUE: reject: 
                \ RCPT\ from         # RCPT from
                \ ([^[]+)            # mail.onezot.com
                \[([^]]*)\]          # [64.38.44.154]
                :.*?;                # : 450 4.1.8 <apache@...;
                \ (.*)               # from=<apache@host.onezot.com>
            }x or do {
                warn "can't parse: $sl->{text}\n";
                next;
            };
            my ($name, $addr, $other) = ($1, $2, $3);
            my ($sender, $recipient);
            $other =~ /\bfrom=<([^>]*)>/ and $sender=$1;
            $other =~ /\bto=<([^>]*)>/   and $recipient=$1;
            my %attrs = (
                time => $sl->{timestamp},
                client_name => $name,
                client_address => $addr,
                sender => $sender,
                recipient => $recipient
            );
            feed_policy(\%attrs);
        }
        elsif($sl->{text} =~ /^([0-9A-F]+):(.*)/) {
            my ($queue_id, $data) = ($1, $2);
            if($data =~ / client=([^[]+)\[([^]]+)\]/) {
                $state{$queue_id}{time}=$sl->{timestamp};
                $state{$queue_id}{client_name}=$1;
                $state{$queue_id}{client_address}=$2;
            }
            defined $state{$queue_id}{time} or next;
            if($data =~ / from=<([^>]*)>/) {
                $state{$queue_id}{sender}=$1;
            }
            if($data =~ /to=<([^>]*)>/) {
                $state{$queue_id}{recipient}=$1;
            }
            if(defined $state{$queue_id}{sender}
                and defined $state{$queue_id}{recipient})
            {
                feed_policy($state{$queue_id});
                delete($state{$queue_id});
            }
        }
    }
}

my $socket;
sub feed_policy($)
{
    my ($data) = @_;
    $requests++;
    if(not defined $socket) {
        if($policy_service =~ /^inet:(.*):(\d+)$/) {
            $socket = new IO::Socket::INET(
                PeerAddr => $1,
                PeerPort => $2,
                Proto    => 'tcp',
            ) or die "Can't connect to $1:$2: $!\n";
        }
        elsif($policy_service =~ /^unix:(.*)$/) {
            $socket = new IO::Socket::UNIX($1) or
                die "Can't open $1: $1:$2: $!\n";
        }
    }
    
    my $request = "request=smtpd_access_policy\n";
    $request   .= "protocol_state=RCPT\n";
    $request   .= "protocol_name=SMTP\n";

    for my $k (keys %$data) {
        my $val = $data->{$k};
        $k = 'policy_test_time' if $k eq 'time';
        $request .= "$k=$val\n";
    }
    $request .= "\n";

    print $socket $request;
    print $request if $opt{verbose};

    my $line;
    do {
        $line = <$socket>;
        print $line if $opt{verbose} and $line ne "\n";
    } while($line ne "\n");
    print "\n" if $opt{verbose};
}

sub main()
{
    # parse options
    GetOptions(\%opt, 'help|h', 'man', 'version', 'verbose|v') or exit(1);

    if($opt{help})     { pod2usage(1) }
    if($opt{man})      { pod2usage(-exitstatus => 0, -verbose => 2) }
    if($opt{version})  { print "postgrey $VERSION\n"; exit(0) }

    $policy_service = shift @ARGV;
    defined $policy_service and
        $policy_service =~ /^(unix:\S+|inet:\S+:\d+)$/ or
        pod2usage(1);

    my $start_time = time;
    log_to_policy('-');
    my $elapsed = time-$start_time;
    my $rate = $elapsed != 0 ? "(". $requests / $elapsed . " r/s)" : '';
    print "$requests requests in ${elapsed}s $rate\n";
}

main;

__END__

=head1 NAME

policy-test - Postfix Policy Daemon Testing Tool

=head1 SYNOPSIS

B<policy-test> [I<options>...] unix:/file/path
B<policy-test> [I<options>...] inet:hostname:port

 -h, --help              display this help and exit
     --version           output version information and exit
 -v, --verbose           increase verbosity level

=head1 DESCRIPTION

policy-test is a script that converts Postfix log entries on the standard input
into requests to the Postfix policy daemon given as the first argument. It
can be used for:

=over 4

=item Testing the performance of a policy daemon

=item Pre-seeding a Greylist policy daemon with data from the logs

=back

Note that this program is still in its very early stages of development and
does only support a small subset of the normal attributes used in the Postfix
policy delegation protocol. It basically supports the bare minimum to make it
work with Postgrey.

Also note that a non-standard attribute is being generated: policy_test_time
contains the Unix timestamp of when the client connected.

=head1 COPYRIGHT

Copyright (c) 2007 by Open Systems AG. All rights reserved.

=head1 LICENSE

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

=head1 AUTHOR

S<David Schweikert E<lt>david@schweikert.chE<gt>>

=cut

# vim: sw=4 et


syntax highlighted by Code2HTML, v. 0.9.1