1#!perl -w
2use IO::Socket;
3
4sub register {
5    my ($self, $qp, @args) = @_;
6
7    %{$self->{"_sophie"}} = @args;
8
9    # Set some sensible defaults
10    $self->{"_sophie"}->{"sophie_socket"} ||= "/var/run/sophie";
11    $self->{"_sophie"}->{"deny_viruses"}  ||= "yes";
12    $self->{"_sophie"}->{"max_size"}      ||= 128;
13}
14
15sub hook_data_post {
16    my ($self, $transaction) = @_;
17    $DB::single = 1;
18
19    if ($transaction->data_size > $self->{"_sophie"}->{"max_size"} * 1024) {
20        $self->log(LOGNOTICE, "Declining due to data_size");
21        return (DECLINED);
22    }
23
24    # Ignore non-multipart emails
25    my $content_type = $transaction->header->get('Content-Type');
26    $content_type =~ s/\s/ /g if defined $content_type;
27    unless (   $content_type
28            && $content_type =~ m!\bmultipart/.*\bboundary="?([^"]+)!i)
29    {
30        $self->log(LOGWARN, "non-multipart mail - skipping");
31        return DECLINED;
32    }
33
34    my $filename = $transaction->body_filename;
35    unless ($filename) {
36        $self->log(LOGWARN, "Cannot process due to lack of filename");
37        return (DECLINED);    # unless $filename;
38    }
39
40    my $mode = (stat($self->spool_dir()))[2];
41    if ($mode & 07077) {    # must be sharing spool directory with external app
42        $self->log(LOGWARN,
43                   "Changing permissions on file to permit scanner access");
44        chmod $mode, $filename;
45    }
46
47    my ($SOPHIE, $response);
48    socket(\*SOPHIE, AF_UNIX, SOCK_STREAM, 0)
49      || die "Couldn't create socket ($!)\n";
50
51    connect(\*SOPHIE, pack_sockaddr_un $self->{"_sophie"}->{"sophie_socket"})
52      || die "Couldn't connect() to the socket ($!)\n";
53
54    syswrite(\*SOPHIE, $filename . "\n", length($filename) + 1);
55    sysread(\*SOPHIE, $response, 256);
56    close(\*SOPHIE);
57
58    my $virus;
59
60    if (($virus) = ($response =~ m/^1:?(.*)?$/)) {
61        $self->log(LOGERROR, "One or more virus(es) found: $virus");
62
63        if (lc($self->{"_sophie"}->{"deny_viruses"}) eq "yes") {
64            return (DENY,
65                    "Virus" . ($virus =~ /,/ ? "es " : " ") . "Found: $virus");
66        }
67        else {
68            $transaction->header->add('X-Virus-Found',   'Yes');
69            $transaction->header->add('X-Virus-Details', $virus);
70            return (DECLINED);
71        }
72    }
73
74    $transaction->header->add('X-Virus-Checked',
75                             "Checked by SOPHIE on " . $self->qp->config("me"));
76
77    return (DECLINED);
78}
79
80=head1 NAME
81
82sophie scanner
83
84=head1 DESCRIPTION
85
86A qpsmtpd plugin for virus scanning using the SOPHOS scan daemon, Sophie.
87
88=head1 RESTRICTIONS
89
90The Sophie scan daemon must have at least read access to the qpsmtpd spool
91directory in order to sucessfully scan the messages.  You can ensure this
92by running Sophie as the same user as qpsmtpd does (by far the easiest
93method) or by doing the following:
94
95=over 4
96
97=item * Change the group ownership of the spool directory to be a group
98of which the Sophie user is a member or add the Sophie user to the same group
99as the qpsmtpd user.
100
101=item * Change the permissions of the qpsmtpd spool directory to 0750 (this
102will emit a warning when the qpsmtpd service starts up, but can be safely
103ignored).
104
105=item * Make sure that all directories above the spool directory (to the
106root) are g+x so that the group has directory traversal rights; it is not
107necessary for the group to have any read rights except to the spool
108directory itself.
109
110=back
111
112It may be helpful to temporary grant the Sophie user a shell and test to
113make sure you can cd into the spool directory and read files located there.
114Remember to remove the shell from the Sophieav user when you are done
115testing.
116
117Note also that the contents of config/spool_dir must be the full path to the
118spool directory (not a relative path) in order for the scanner to locate the
119file.
120
121=head1 INSTALL AND CONFIG
122
123Place this plugin in the plugin/virus directory beneath the standard
124qpsmtpd installation.  If you installed Sophie with the default path, you
125can use this plugin with default options (nothing specified):
126
127=over 4
128
129=item B<Sophie_socket>
130
131Full path to the Sophie socket defaults to /var/run/Sophie.
132
133=item B<deny_viruses>
134
135Whether the scanner will automatically delete messages which have viruses.
136Takes either 'yes' or 'no' (defaults to 'yes').  If set to 'no' it will add
137a header to the message with the virus results.
138
139=item B<max_size>
140
141The maximum size, in kilobytes, of messages to scan; defaults to 128k.
142
143=back
144
145=head1 REQUIREMENTS
146
147This module requires the Sophie daemon, available here:
148
149L<http://www.clanfield.info/sophie/>
150
151which in turn requires the libsavi.so library (available with the Sophos
152Anti-Virus for Linux or Unix).
153
154The following changes to F</etc/sophie.cfg> B<should> be made:
155
156=over 4
157
158=item user: qmaild
159
160Change the "user" parameter to match the qpsmtpd user.
161
162=item group: nofiles
163
164Change the "group" parameter to match the qpsmtpd group.
165
166=item umask: 0001
167
168If you don't change the umask, only the above user/group will be able to scan.
169
170=back
171
172The following changes to F</etc/sophie.savi> B<must> be made:
173
174=over 4
175
176=item Mime: 1
177
178This option will permit the SAVI engine to directly scan e-mail messages.
179
180=back
181
182=head1 AUTHOR
183
184John Peacock <jpeacock@cpan.org>
185
186=head1 COPYRIGHT AND LICENSE
187
188Copyright (c) 2005 John Peacock
189
190Based heavily on the clamav plugin
191
192This plugin is licensed under the same terms as the qpsmtpd package itself.
193Please see the LICENSE file included with qpsmtpd for details.
194
195=cut
196
197