1#!perl -w
2
3=head1 NAME
4
5uvscan
6
7=head1 DESCRIPTION
8
9A qpsmtpd plugin for the McAfee commandline virus scanner, uvscan.
10
11=head1 INSTALL AND CONFIG
12
13Place this plugin in the plugin/virus directory beneath the standard
14qpsmtpd installation.  If you installed uvscan with the default path, you
15can use this plugin with default options (nothing specified):
16
17=over 4
18
19=item B<uvscan_location>
20
21Full path to the uvscan binary and all signature files; defaults to
22/usr/local/bin/uvscan.
23
24=item B<deny_viruses>
25
26Whether the scanner will automatically delete messages which have viruses.
27Takes either 'yes' or 'no' (defaults to 'yes').
28
29=back
30
31=head1 AUTHOR
32
33John Peacock <jpeacock@cpan.org>
34
35=head1 COPYRIGHT AND LICENSE
36
37Copyright (c) 2004 John Peacock
38
39Based heavily on the clamav plugin
40
41This plugin is licensed under the same terms as the qpsmtpd package itself.
42Please see the LICENSE file included with qpsmtpd for details.
43
44=cut
45
46sub register {
47    my ($self, $qp, @args) = @_;
48
49    while (@args) {
50        $self->{"_uvscan"}->{pop @args} = pop @args;
51    }
52    $self->{"_uvscan"}->{"uvscan_location"} ||= "/usr/local/bin/uvscan";
53}
54
55sub hook_data_post {
56    my ($self, $transaction) = @_;
57
58    return (DECLINED)
59      if $transaction->data_size > 250_000;
60
61    # Ignore non-multipart emails
62    my $content_type = $transaction->header->get('Content-Type');
63    $content_type =~ s/\s/ /g if defined $content_type;
64    unless (   $content_type
65            && $content_type =~ m!\bmultipart/.*\bboundary="?([^"]+)!i)
66    {
67        $self->log(LOGWARN, "non-multipart mail - skipping");
68        return DECLINED;
69    }
70
71    my $filename = $transaction->body_filename;
72    return (DECLINED) unless $filename;
73
74    # Now do the actual scanning!
75    my @cmd = (
76               $self->{"_uvscan"}->{"uvscan_location"},
77               '--mime', '--unzip', '--secure', '--noboot', $filename, '2>&1 |'
78              );
79    $self->log(LOGINFO, "Running: ", join(' ', @cmd));
80    open(FILE, join(' ', @cmd));    #perl 5.6 doesn't properly support the pipe
81       # mode list form of open, but this is basically the same thing. This form
82       # of exec is safe(ish).
83    my $output;
84    while (<FILE>) { $output .= $_; }
85    close FILE;
86
87    my $result = ($? >> 8);
88    my $signal = ($? & 127);
89
90    my $virus;
91    if ($output && $output =~ m/.*\W+Found (.*)\n/m) {
92        $virus = $1;
93    }
94    if ($output && $output =~ m/password-protected/m) {
95        return (DENY, 'We do not accept password-protected zip files!');
96    }
97
98    if ($signal) {
99        $self->log(LOGWARN, "uvscan exited with signal: $signal");
100        return (DECLINED);
101    }
102    if ($result == 2) {
103        $self->log(LOGERROR, "Integrity check for a DAT file failed.");
104        return (DECLINED);
105    }
106    elsif ($result == 6) {
107        $self->log(LOGERROR, "A general problem has occurred.");
108        return (DECLINED);
109    }
110    elsif ($result == 8) {
111        $self->log(LOGERROR, "The program could not find a DAT file.");
112        return (DECLINED);
113    }
114    elsif ($result == 15) {
115        $self->log(LOGERROR, "The program self-check failed");
116        return (DECLINED);
117    }
118    elsif ($result) {    # all of the possible virus returns
119        if ($result == 12) {
120            $self->log(LOGERROR,
121                       "The program tried to clean a file but failed.");
122        }
123        elsif ($result == 13) {
124            $self->log(LOGERROR, "One or more virus(es) found");
125        }
126        elsif ($result == 19) {
127            $self->log(LOGERROR, "Successfully cleaned the file");
128        }
129
130        if (lc($self->{"_uvscan"}->{"deny_viruses"}) eq "yes") {
131            return (DENY, "Virus Found: $virus");
132        }
133        $transaction->header->add('X-Virus-Found',   'Yes');
134        $transaction->header->add('X-Virus-Details', $virus);
135        return (DECLINED);
136    }
137
138    $transaction->header->add('X-Virus-Checked',
139                      "Checked by McAfee uvscan on " . $self->qp->config("me"));
140
141    return (DECLINED);
142}
143