qpsmtpd/plugins/virus/clamav
John Peacock c049917d8e * plugins/virus/clamav
Provide more documentation on using clamdscan
    Provide back_compat option to eliminate warnings in log with old ClamAV
    Use new $self->spool_dir() function instead of homebrew


git-svn-id: https://svn.perl.org/qpsmtpd/trunk@371 958fd67b-6ff1-0310-b445-bb7760255be9
2005-02-24 16:54:02 +00:00

192 lines
5.5 KiB
Perl

#!/usr/bin/perl -Tw
=head1 NAME
clamav -- ClamAV antivirus plugin for qpsmtpd
$Id$
=head1 DESCRIPTION
This plugin scans incoming mail with the clamav A/V scanner, and can at your
option reject or flag infected messages.
=head1 CONFIGURATION
Arguments to clamav should be specified in the form of name=value pairs,
separated by whitespace. For sake of backwards compatibility, a single
leading argument containing only alphanumerics, -, _, . and slashes will
be tolerated, and interpreted as the path to clamscan/clamdscan. All
new installations should use the name=value form as follows:
=over 4
=item clamscan_path=I<path> (e.g. I<clamscan_path=/usr/bin/clamdscan>)
Path to the clamav commandline scanner. Using clamdscan is recommended
for sake of performance. However, in this case, the user executing clamd
requires access to the qpsmtpd spool directory, which usually means either
running clamd as the same user, or changing the group ownership of the
spool directory to be the clamd group and changing the permissions to 0750
(this will emit warning when the qpsmtpd service starts up).
Mail will be passed to the clamav scanner in Berkeley mbox format (that is,
with a "From " line).
=item action=E<lt>I<add-header> | I<reject>E<gt> (e.g. I<action=reject>)
Selects an action to take when an inbound message is found to be infected.
Valid arguments are 'add-header' and 'reject'. All rejections are hard
5xx-code rejects; the SMTP error will contain an explanation of the virus
found in the mail (for example, '552 Virus Found: Worm.SomeFool.P').
The default action is 'add-header'.
=item max_size=I<bytes> (e.g. I<max_size=1048576>)
Specifies the maximum size, in bytes, for mail to be scanned. Any mail
exceeding this size will be left alone. This is recommended, as large mail
can take an exceedingly long time to scan. The default is 524288, or 512k.
=item tmp_dir=I<path> (e.g. I<max_size=/tmp>)
Specify an alternate temporary directory. If not specified, the qpsmtpd
I<spool_dir> will be used. If neither is available, I<~/tmp/> will be tried,
and if that that fails the plugin will gracefully fail.
=item back_compat
If you are using a version of ClamAV prior to 0.80, you need to set this
variable to include a couple of now deprecated options.
=back
=head2 CLAMAV CONFIGURATION
At the least, you should have 'ScanMail' supplied in your clamav.conf file.
It is recommended that you also have sane limits on ArchiveMaxRecursion and
StreamMaxLength also.
=head1 LICENSE
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 File::Temp qw(tempfile);
use strict;
use warnings;
sub register {
my ($self, $qp, @args) = @_;
my %args;
if ($args[0] && $args[0] =~ /^(\/[\/\-\_\.a-z0-9A-Z]*)$/ && -x $1) {
$self->{_clamscan_loc} = $1;
shift @args;
}
for (@args) {
if (/^max_size=(\d+)$/) {
$self->{_max_size} = $1;
}
elsif (/^clamscan_path=(\/[\/\-\_\.a-z0-9A-Z]*)$/) {
$self->{_clamscan_loc} = $1;
}
elsif (/^tmp_dir=(\/[\/\-\_\.a-z0-9A-Z]*)$/) {
$self->{_spool_dir} = $1;
}
elsif (/^action=(add-header|reject)$/) {
$self->{_action} = $1;
}
elsif (/back_compat/) {
$self->{_back_compat} = '-i --max-recursion=50';
}
else {
$self->log(LOGERROR, "Unrecognized argument '$_' to clamav plugin");
return undef;
}
}
$self->{_max_size} ||= 512 * 1024;
$self->{_spool_dir} ||= $self->spool_dir();
$self->{_back_compat} ||= ''; # make sure something is set
unless ($self->{_spool_dir}) {
$self->log(LOGERROR, "No spool dir configuration found");
return undef;
}
unless (-d $self->{_spool_dir}) {
$self->log(LOGERROR, "Spool dir $self->{_spool_dir} does not exist");
return undef;
}
$self->register_hook("data_post", "clam_scan");
1;
}
sub clam_scan {
my ($self, $transaction) = @_;
if ($transaction->body_size > $self->{_max_size}) {
$self->log(LOGWARN, 'Mail too large to scan ('.
$transaction->body_size . " vs $self->{_max_size})" );
return (DECLINED);
}
my ($temp_fh, $filename) = tempfile("qpsmtpd.clamav.$$.XXXXXX",
DIR => $self->{_spool_dir});
unless ($temp_fh) {
$self->logerror("Couldn't open tempfile in $self->{_spool_dir}: $!");
return DECLINED;
}
print $temp_fh "From ",
$transaction->sender->format, " " , scalar gmtime, "\n";
print $temp_fh $transaction->header->as_string, "\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->{_clamscan_loc}." --stdout "
.$self->{_back_compat}
." --disable-summary $filename 2>&1";
$self->log(LOGDEBUG, "Running: $cmd");
my $output = `$cmd`;
my $result = ($? >> 8);
my $signal = ($? & 127);
unlink($filename);
chomp($output);
$output =~ s/^.* (.*) FOUND$/$1 /mg;
$self->log(LOGINFO, "clamscan results: $output");
if ($signal) {
$self->log(LOGINFO, "clamscan exited with signal: $signal");
return (DECLINED);
}
if ($result == 1) {
$self->log(LOGINFO, "Virus(es) found: $output");
if ($self->{_action} eq 'add-header') {
$transaction->header->add('X-Virus-Found', 'Yes');
$transaction->header->add('X-Virus-Details', $output);
} else {
return (DENY, "Virus Found: $output");
}
}
elsif ($result) {
$self->log(LOGERROR, "ClamAV error: $cmd: $result\n");
}
return (DECLINED);
}
1;