diff --git a/Changes b/Changes index c208580..c928f09 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,13 @@ 0.29 + Store entire incoming message in spool file (so that scanners can read + the complete message) and ignore old headers before adding lines and + queuing for delivery. + + New anti-virus scanners: hbedv (Hanno Hecker), bitdefender, and clamdscan + (John Peacock). Update clamav plugin to directly scan the spool file. + New temp_file() and temp_dir() methods; when used by plugins, they create a filename or directory which will last only as long as the current transaction. Also created a spool_dir() method which checks/creates the diff --git a/MANIFEST b/MANIFEST index efe3d40..6dfa5cf 100644 --- a/MANIFEST +++ b/MANIFEST @@ -40,6 +40,7 @@ plugins/content_log plugins/count_unrecognized_commands plugins/dnsbl plugins/dns_whitelist_soft +plugins/greylisting plugins/http_config plugins/ident/geoip plugins/ident/p0f @@ -55,8 +56,11 @@ plugins/rhsbl plugins/sender_permitted_from plugins/spamassassin plugins/virus/aveclient +plugins/virus/bitdefender plugins/virus/check_for_hi_virus plugins/virus/clamav +plugins/virus/clamdscan +plugins/virus/hbedv plugins/virus/kavscanner plugins/virus/klez_filter plugins/virus/uvscan @@ -75,4 +79,5 @@ t/plugin_tests.t t/plugin_tests/check_badrcptto t/plugin_tests/dnsbl t/Test/Qpsmtpd/Plugin.pm +t/tempstuff.t META.yml Module meta-data (added by MakeMaker) diff --git a/plugins/virus/clamdscan b/plugins/virus/clamdscan new file mode 100644 index 0000000..03a40e3 --- /dev/null +++ b/plugins/virus/clamdscan @@ -0,0 +1,174 @@ +#!/usr/bin/perl -w + +=head1 NAME + +clamdscan + +=head1 DESCRIPTION + +A qpsmtpd plugin for virus scanning using the ClamAV scan daemon, clamd. + +=head1 RESTRICTIONS + +The ClamAV scan daemon, clamd, must have at least read access to the +qpsmtpd spool directory in order to sucessfully scan the messages. You can +ensure this by running clamd as the same user as qpsmtpd does (by far the +easiest method) or by doing the following: + +=over 4 + +=item * Change the group ownership of the spool directory to be a group +of which clamav is a member or add clamav to the same group as the qpsmtpd +user. + +=item * Enable the "AllowSupplementaryGroups" option in clamd.conf. + +=item * Change the permissions of the qpsmtpd spool directory to 0750 (this +will emit a warning when the qpsmtpd service starts up, but can be safely +ignored). + +=item * Make sure that all directories above the spool directory (to the +root) are g+x so that the group has directory traversal rights; it is not +necessary for the group to have any read rights except to the spool +directory itself. + +=back + +It may be helpful to temporary grant the clamav user a shell and test to +make sure you can cd into the spool directory and read files located there. +Remember to remove the shell from the clamav user when you are done +testing. + +=head1 INSTALL AND CONFIG + +Place this plugin in the plugin/virus directory beneath the standard +qpsmtpd installation. If you installed clamd with the default path, you +can use this plugin with default options (nothing specified): + +=over 4 + +=item B + +Full path to the clamd socket (the recommended mode); defaults to +/tmp/clamd and is the default method. + +=item B + +If present, must be the TCP port where the clamd service is running, +typically 3310; default disabled. + +=item B + +Whether the scanner will automatically delete messages which have viruses. +Takes either 'yes' or 'no' (defaults to 'yes'). If set to 'no' it will add +a header to the message with the virus results. + +=item B + +The maximum size, in kilobytes, of messages to scan; defaults to 128k. + +=back + +=head1 REQUIREMENTS + +This module requires the Clamd module, found on CPAN here: + +L + +=head1 AUTHOR + +John Peacock + +=head1 COPYRIGHT AND LICENSE + +Copyright (c) 2005 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 + +use Clamd; + +sub register { + my ( $self, $qp, @args ) = @_; + $self->register_hook( "data_post", "clamdscan" ); + + %{ $self->{"_clamd"} } = @args; + + # Set some sensible defaults + $self->{"_clamd"}->{"clamd_socket"} ||= "/tmp/clamd"; + $self->{"_clamd"}->{"deny_viruses"} ||= "yes"; + $self->{"_clamd"}->{"max_size"} ||= 128; +} + +sub clamdscan { + my ( $self, $transaction ) = @_; + $DB::single = 1; + + if ( $transaction->body_size > $self->{"_clamd"}->{"max_size"} * 1024 ) { + $self->log( LOGNOTICE, "Declining due to body_size" ); + return (DECLINED); + } + + # 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( LOGERROR, "non-multipart mail - skipping" ); + return DECLINED; + } + + my $filename = $transaction->body_filename; + unless ($filename) { + $self->log( LOGWARN, "Cannot process due to lack of filename" ); + return (DECLINED); # unless $filename; + } + + my $mode = ( stat( $self->spool_dir() ) )[2]; + if ( $mode & 07077 ) { # must be sharing spool directory with external app + $self->log( LOGWARN, + "Changing permissions on file to permit scanner access" ); + chmod $mode, $filename; + } + + my $clamd; + + if ( $self->{"_clamd"}->{"clamd_port"} + and $self->{"_clamd"}->{"clamd_port"} =~ /(\d+)/ ) + { + my $port = $1; + $clamd = Clamd->new( port => $port ); + } + else { + $clamd = Clamd->new(); # default unix domain socket + } + + return (DECLINED) unless $clamd->ping(); + + if ( my %found = $clamd->scan($filename) ) { + my $viruses = join( ",", values(%found) ); + $self->log( LOGERROR, "One or more virus(es) found: $viruses" ); + + if ( lc( $self->{"_clamd"}->{"deny_viruses"} ) eq "yes" ) { + return ( DENY, + "Virus" + . ( $viruses =~ /,/ ? "es " : " " ) + . "Found: $viruses" ); + } + else { + $transaction->header->add( 'X-Virus-Found', 'Yes' ); + $transaction->header->add( 'X-Virus-Details', $viruses ); + return (DECLINED); + } + } + + $transaction->header->add( 'X-Virus-Checked', + "Checked by ClamAV on " . $self->qp->config("me") ); + + return (DECLINED); +}