#!perl -w
# Kasperski-AV plugin.

=head1 NAME

kavscanner - plugin for qpsmtpd which calls the Kasperski anti virus scanner

=head1 DESCRIPTION

Check a mail with the B<kavscanner> and deny if it matches a configured virus
list.

=head1 VERSION

this is B<kavscanner> version 1.0

=head1 CONFIGURATION

Add (perl-)regexps to the F<kav_deny> configuration file, one per line for the
virii you want to block, e.g.:

  I-Worm\.Sober\..*
  I-Worm\.NetSky\..*

NOTE: untested and disabled currently, need volunteers :-)

If this list does not match the virus found in the mail, you may set 
I<bcc_virusadmin viradm@your.company.com> in the plugin config to send a 
B<Bcc:> to the given mail address, i.e. the line 

  kavscanner bcc_virusadmin viradm@your.company.com 

in the F<config/plugin> file instead of just

  kavscanner

Set the location of the binary with 

  kavscanner kavscanner_bin /path/to/kavscanner

(default: F</opt/AVP/kavscanner>), NOTE: this may be broken, you want to
set B<kavscanner_bin> explicitly ;-)

=head1 NOTES

This is a merge of the clam_av plugin for qpsmtpd and qmail-scanner-queue.pl
L<http://qmail-scanner.sourceforge.net/> with my own improvements ;-)
Only tested with kavscanner 4.0.x, and bcc_virusadmin untested, as we have no
use for it currently. I wait for an official change in Qpsmtpd::Transaction
(reset/set the RCPT TO list) to activate and test the currently disabled 
B<to_virusadmin> option.

=cut

use File::Temp qw(tempfile);
use Mail::Address;

sub register {
    my ($self, $qp, @args) = @_;

    if (@args % 2) {
        $self->log(LOGWARN, "kavscanner: Wrong number of arguments");
        $self->{_kavscanner_bin} = "/opt/AVP/kavscanner";
    }
    else {
        my %args = @args;
        foreach my $key (keys %args) {
            my $arg = $key;
            $key =~ s/^/_/;
            $self->{$key} = $args{$arg};
        }

        # Untaint scanner location
        if (exists $self->{_kavscanner_bin}
            && $self->{_kavscanner_bin} =~ /^(\/[\/\-\_\.a-z0-9A-Z]*)$/)
        {
            $self->{_kavscanner_bin} = $1;
        }
        else {
            $self->log(LOGALERT,
                   "FATAL ERROR: Unexpected characters in kavscanner argument");
            exit 3;
        }
    }
}

sub hook_data_post {
    my ($self, $transaction) = @_;

    my ($temp_fh, $filename) = tempfile();
    print $temp_fh $transaction->header->as_string;
    print $temp_fh "\n";
    $transaction->body_resetpos;
    while (my $line = $transaction->body_getline) {
        print $temp_fh $line;
    }
    seek($temp_fh, 0, 0);

    # Now do the actual scanning!
    my $cmd = $self->{_kavscanner_bin} . " -Y -P -B -MP -MD -* $filename 2>&1";
    $self->log(LOGNOTICE, "Running: $cmd");
    my @output = `$cmd`;
    chomp(@output);

    my $result = ($? >> 8);
    my $signal = ($? & 127);

    unlink($filename);
    close $temp_fh;

    if ($signal) {
        $self->log(LOGWARN, "kavscanner exited with signal: $signal");
        return (DECLINED);
    }

    my $description = 'clean';
    my @infected    = ();
    my @suspicious  = ();
    if ($result > 0) {
        if ($result =~ /^(2|3|4|8)$/) {
            foreach (@output) {
                if (/^.* infected: (.*)$/) {

                    # This covers the specific
                    push @infected, $1;
                }
                elsif (/^\s*.* suspicion: (.*)$/) {

                    # This covers the potential viruses
                    push @suspicious, $1;
                }
            }
            $description =
                "infected by: "
              . join(", ", @infected) . "; "
              . "suspicions: "
              . join(", ", @suspicious);

            # else we may get a veeeery long X-Virus-Details: line or log entry
            $description = substr($description, 0, 60);
            $self->log(LOGWARN, "There be a virus! ($description)");
            ### Untested by now, need volunteers ;-)
            #if ($self->qp->config("kav_deny")) {
            #  foreach my $d (keys %{$self->qp->config("kav_deny", "map")}) {
            #    foreach my $v (@infected) {
            #      return(DENY, "Virus found: $description")
            #        if ($v =~ /^$d$/i);
            #    }
            #    foreach my $s (@suspicious) {
            #      return(DENY, "Virus found: $description")
            #        if ($s =~ /^$d$/i);
            #    }
            #  }
            #}
            $transaction->header->add('X-Virus-Found',   'Yes');
            $transaction->header->add('X-Virus-Details', $description);
            ### maybe the spamassassin plugin can skip this mail if a virus
            ### was found (and $transaction->notes('virus_flag') exists :))
            ### ...ok, works with our spamassassin plugin version
            ###   -- hah
            $transaction->notes('virus',      $description);
            $transaction->notes('virus_flag', 'Yes');

            #### requires modification of Qpsmtpd/Transaction.pm:
# if ($self->{_to_virusadmin}) {
#   my @addrs = ();
#   foreach (@{$transaction->recipients}) {
#     push @addr, $_->address;
#   }
#   $transaction->header->add('X-Virus-Orig-RcptTo', join(", ", @addrs));
#   $transaction->set_recipients(@{ Mail::Address->parse($self->{_to_virusadmin}) });
# } elsif ($self->{_bcc_virusadmin}) {
            if ($self->{_bcc_virusadmin}) {
                foreach (@{Mail::Address->parse($self->{_bcc_virusadmin})}) {
                    $transaction->add_recipient($_);
                }
            }
        }
        else {
            $self->log(LOGEMERG,
"corrupt or unknown Kaspersky scanner/resource problems - exit status $result"
            );
        }
    }

    $self->log(LOGINFO, "kavscanner results: $description");

    $transaction->header->add('X-Virus-Checked',
                              'Checked by ' . $self->qp->config("me"));
    return (DECLINED);
}