#!perl -w =head1 NAME uvscan =head1 DESCRIPTION A qpsmtpd plugin for the McAfee commandline virus scanner, uvscan. =head1 INSTALL AND CONFIG Place this plugin in the plugin/virus directory beneath the standard qpsmtpd installation. If you installed uvscan with the default path, you can use this plugin with default options (nothing specified): =over 4 =item B Full path to the uvscan binary and all signature files; defaults to /usr/local/bin/uvscan. =item B Whether the scanner will automatically delete messages which have viruses. Takes either 'yes' or 'no' (defaults to 'yes'). =back =head1 AUTHOR John Peacock =head1 COPYRIGHT AND LICENSE Copyright (c) 2004 John Peacock Based heavily on the clamav plugin This plugin is licensed under the same terms as the qpsmtpd package itself. Please see the LICENSE file included with qpsmtpd for details. =cut sub register { my ($self, $qp, @args) = @_; while (@args) { $self->{"_uvscan"}->{pop @args} = pop @args; } $self->{"_uvscan"}->{"uvscan_location"} ||= "/usr/local/bin/uvscan"; } sub hook_data_post { my ($self, $transaction) = @_; return DECLINED if $transaction->data_size > 250_000; # Ignore non-multipart emails my $content_type = $transaction->header->get('Content-Type'); $content_type =~ s/\s/ /g if defined $content_type; unless ( $content_type && $content_type =~ m!\bmultipart/.*\bboundary="?([^"]+)!i) { $self->log(LOGWARN, "non-multipart mail - skipping"); return DECLINED; } my $filename = $transaction->body_filename; return DECLINED if !$filename; # Now do the actual scanning! my @cmd = ( $self->{"_uvscan"}->{"uvscan_location"}, '--mime', '--unzip', '--secure', '--noboot', $filename, '2>&1 |' ); $self->log(LOGINFO, "Running: ", join(' ', @cmd)); open(FILE, join(' ', @cmd)); #perl 5.6 doesn't properly support the pipe # mode list form of open, but this is basically the same thing. This form # of exec is safe(ish). my $output; while () { $output .= $_; } close FILE; my $result = ($? >> 8); my $signal = ($? & 127); my $virus; if ($output && $output =~ m/.*\W+Found (.*)\n/m) { $virus = $1; } if ($output && $output =~ m/password-protected/m) { return DENY, 'We do not accept password-protected zip files!'; } if ($signal) { $self->log(LOGWARN, "uvscan exited with signal: $signal"); return DECLINED; } if ($result == 2) { $self->log(LOGERROR, "Integrity check for a DAT file failed."); return DECLINED; } elsif ($result == 6) { $self->log(LOGERROR, "A general problem has occurred."); return DECLINED; } elsif ($result == 8) { $self->log(LOGERROR, "The program could not find a DAT file."); return DECLINED; } elsif ($result == 15) { $self->log(LOGERROR, "The program self-check failed"); return DECLINED; } elsif ($result) { # all of the possible virus returns if ($result == 12) { $self->log(LOGERROR, "The program tried to clean a file but failed."); } elsif ($result == 13) { $self->log(LOGERROR, "One or more virus(es) found"); } elsif ($result == 19) { $self->log(LOGERROR, "Successfully cleaned the file"); } if (lc($self->{"_uvscan"}->{"deny_viruses"}) eq "yes") { return DENY, "Virus Found: $virus"; } $transaction->header->add('X-Virus-Found', 'Yes'); $transaction->header->add('X-Virus-Details', $virus); return DECLINED; } $transaction->header->add('X-Virus-Checked', "Checked by McAfee uvscan on " . $self->qp->config("me")); return DECLINED; }