a23d4b3da9
Exclude some tests with dependencies. Remove -T from perl line in plugins This makes it harder to test with PERL5LIB/perlbrew etc
235 lines
7.4 KiB
Perl
235 lines
7.4 KiB
Perl
#!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 execute 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, 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 * Add group-execute permissions to the qpsmtpd spool directory.
|
|
|
|
=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.
|
|
|
|
=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):
|
|
|
|
You must have the ClamAV::Client module installed to use the plugin.
|
|
|
|
=over 4
|
|
|
|
=item B<clamd_socket>
|
|
|
|
Full path to the clamd socket (the recommended mode), if different from the
|
|
ClamAV::Client defaults.
|
|
|
|
=item B<clamd_port>
|
|
|
|
If present, must be the TCP port where the clamd service is running,
|
|
typically 3310; default disabled. If present, overrides the clamd_socket.
|
|
|
|
=item B<deny_viruses>
|
|
|
|
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<defer_on_error>
|
|
|
|
Whether to defer the mail (with a soft-failure error, which will incur a retry)
|
|
if an unrecoverable error occurs during the scan. The default is to accept
|
|
the mail under these conditions. This can permit viruses to be accepted when
|
|
the clamd daemon is malfunctioning or unreadable, but will not allow mail to
|
|
backlog or be lost if the condition persists.
|
|
|
|
=item B<max_size>
|
|
|
|
The maximum size, in kilobytes, of messages to scan; defaults to 128k.
|
|
|
|
=item B<scan_all>
|
|
|
|
Scan all messages, even if there are no attachments
|
|
|
|
=back
|
|
|
|
=head1 REQUIREMENTS
|
|
|
|
This module requires the ClamAV::Client module, found on CPAN here:
|
|
|
|
L<http://search.cpan.org/dist/ClamAV-Client/>
|
|
|
|
=head1 AUTHOR
|
|
|
|
Originally written for the Clamd module by John Peacock <jpeacock@cpan.org>;
|
|
adjusted for ClamAV::Client by Devin Carraway <qpsmtpd/@/devin.com>.
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
Copyright (c) 2005 John Peacock,
|
|
Copyright (c) 2007 Devin Carraway
|
|
|
|
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 strict;
|
|
use warnings;
|
|
|
|
use ClamAV::Client;
|
|
use Qpsmtpd::Constants;
|
|
|
|
sub register {
|
|
my ( $self, $qp, @args ) = @_;
|
|
|
|
$self->log(LOGERROR, "Bad parameters for the clamdscan plugin") if @_ % 2;
|
|
%{ $self->{"_clamd"} } = @args;
|
|
|
|
# Set some sensible defaults
|
|
$self->{"_clamd"}->{"deny_viruses"} ||= "yes";
|
|
$self->{"_clamd"}->{"max_size"} ||= 128;
|
|
$self->{"_clamd"}->{"scan_all"} ||= 0;
|
|
for my $setting ('deny_viruses', 'defer_on_error') {
|
|
next unless $self->{"_clamd"}->{$setting};
|
|
$self->{"_clamd"}->{$setting} = 0
|
|
if lc $self->{"_clamd"}->{$setting} eq 'no';
|
|
}
|
|
}
|
|
|
|
sub hook_data_post {
|
|
my ( $self, $transaction ) = @_;
|
|
$DB::single = 1;
|
|
|
|
if ( $transaction->data_size > $self->{"_clamd"}->{"max_size"} * 1024 ) {
|
|
$self->log( LOGNOTICE, "Declining due to data_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 ( $self->{"_clamd"}->{"scan_all"}
|
|
|| $content_type
|
|
&& $content_type =~ m!\bmultipart/.*\bboundary="?([^"]+)!i )
|
|
{
|
|
$self->log( LOGNOTICE, "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;
|
|
}
|
|
|
|
# the spool directory must be readable and executable by the scanner;
|
|
# this generally means either group or world exec; if
|
|
# neither of these is set, issue a warning but try to proceed anyway
|
|
my $mode = ( stat( $self->spool_dir() ) )[2];
|
|
if ( $mode & 0010 || $mode & 0001 ) {
|
|
# match the spool file mode with the mode of the directory -- add
|
|
# the read bit for group, world, or both, depending on what the
|
|
# spool dir had, and strip all other bits, especially the sticky bit
|
|
my $fmode = ($mode & 0044) |
|
|
($mode & 0010 ? 0040 : 0) |
|
|
($mode & 0001 ? 0004 : 0);
|
|
unless ( chmod $fmode, $filename ) {
|
|
$self->log( LOGERROR, "chmod: $filename: $!" );
|
|
return DECLINED;
|
|
}
|
|
} else {
|
|
$self->log( LOGWARN,
|
|
"Permission on spool directory do not permit scanner access" );
|
|
}
|
|
|
|
my $clamd;
|
|
|
|
if ( ($self->{"_clamd"}->{"clamd_port"} || '') =~ /^(\d+)/ ) {
|
|
$clamd = new ClamAV::Client( socket_host =>
|
|
$self->{_clamd}->{clamd_host},
|
|
socket_port => $1 );
|
|
}
|
|
elsif ( ($self->{"_clamd"}->{"clamd_socket"} || '') =~ /([\w\/.]+)/ ) {
|
|
$clamd = new ClamAV::Client( socket_name => $1 );
|
|
}
|
|
else {
|
|
$clamd = new ClamAV::Client;
|
|
}
|
|
|
|
unless ( $clamd ) {
|
|
$self->log( LOGERROR, "Cannot instantiate ClamAV::Client" );
|
|
return (DENYSOFT, "Unable to scan for viruses")
|
|
if $self->{"_clamd"}->{"defer_on_error"};
|
|
return DECLINED;
|
|
}
|
|
|
|
unless ( eval { $clamd->ping() } ) {
|
|
$self->log( LOGERROR, "Cannot ping clamd server: $@" );
|
|
return (DENYSOFT, "Unable to scan for viruses")
|
|
if $self->{"_clamd"}->{"defer_on_error"};
|
|
return DECLINED;
|
|
}
|
|
|
|
my @clamd_version = split(/\//, $clamd->version);
|
|
$self->{"_clamd"}->{'version'} = $clamd_version[0] || 'ClamAV';
|
|
|
|
my ( $path, $found ) = eval { $clamd->scan_path( $filename ) };
|
|
if ($@) {
|
|
$self->log( LOGERROR, "Error scanning mail: $@" );
|
|
return (DENYSOFT, "Unable to scan for viruses")
|
|
if $self->{"_clamd"}->{"defer_on_error"};
|
|
return DECLINED;
|
|
}
|
|
elsif ( $found ) {
|
|
$self->log( LOGERROR, "Virus found: $found" );
|
|
|
|
if ( $self->{"_clamd"}->{"deny_viruses"} ) {
|
|
return ( DENY, "Virus found: $found" );
|
|
}
|
|
else {
|
|
$transaction->header->add( 'X-Virus-Found', 'Yes' );
|
|
$transaction->header->add( 'X-Virus-Details', $found );
|
|
return (DECLINED);
|
|
}
|
|
}
|
|
else {
|
|
$transaction->header->add( 'X-Virus-Found', 'No' );
|
|
$self->log( LOGINFO, "ClamAV scan reports clean");
|
|
}
|
|
|
|
$transaction->header->add( 'X-Virus-Checked',
|
|
"Checked by $self->{'_clamd'}->{'version'} on " . $self->qp->config("me") );
|
|
|
|
return (DECLINED);
|
|
}
|
|
|