Lines 5-13
Link Here
|
5 |
# The ASF licenses this file to you under the Apache License, Version 2.0 |
5 |
# The ASF licenses this file to you under the Apache License, Version 2.0 |
6 |
# (the "License"); you may not use this file except in compliance with |
6 |
# (the "License"); you may not use this file except in compliance with |
7 |
# the License. You may obtain a copy of the License at: |
7 |
# the License. You may obtain a copy of the License at: |
8 |
# |
8 |
# |
9 |
# http://www.apache.org/licenses/LICENSE-2.0 |
9 |
# http://www.apache.org/licenses/LICENSE-2.0 |
10 |
# |
10 |
# |
11 |
# Unless required by applicable law or agreed to in writing, software |
11 |
# Unless required by applicable law or agreed to in writing, software |
12 |
# distributed under the License is distributed on an "AS IS" BASIS, |
12 |
# distributed under the License is distributed on an "AS IS" BASIS, |
13 |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
Lines 15-60
Link Here
|
15 |
# limitations under the License. |
15 |
# limitations under the License. |
16 |
# </@LICENSE> |
16 |
# </@LICENSE> |
17 |
|
17 |
|
|
|
18 |
# Changes since SpamAssassin 3.3.2: |
19 |
# support for DCC learning. See dcc_learn_score. |
20 |
# deal with orphan dccifd sockets |
21 |
# use `cdcc -q` to not stall waiting to find a DCC server when deciding |
22 |
# whether DCC checks are enabled |
23 |
# use dccproc -Q or dccifd query if a pre-existing X-DCC header shows |
24 |
# the message has already been reported |
25 |
# dccproc now uses -w /var/dcc/whiteclnt so it acts more like dccifd |
26 |
# warn about the use of ancient versions of dccproc and dccifd |
27 |
# turn off dccifd greylisting |
28 |
# query instead of reporting mail messages that contain X-DCC headers and |
29 |
# and so has probably already been reported |
30 |
# try harder to find dccproc and cdcc when not explicitly configured |
31 |
# Rhyolite Software DCC 2.3.140-1.4 $Revision$ |
32 |
|
18 |
=head1 NAME |
33 |
=head1 NAME |
19 |
|
34 |
|
20 |
Mail::SpamAssassin::Plugin::DCC - perform DCC check of messages |
35 |
Mail::SpamAssassin::Plugin::DCC - perform DCC check of messages |
21 |
|
36 |
|
22 |
=head1 SYNOPSIS |
37 |
=head1 SYNOPSIS |
23 |
|
38 |
|
24 |
loadplugin Mail::SpamAssassin::Plugin::DCC |
39 |
loadplugin Mail::SpamAssassin::Plugin::DCC |
25 |
|
40 |
|
26 |
full DCC_CHECK eval:check_dcc() |
41 |
full DCC_CHECK eval:check_dcc() |
27 |
full DCC_CHECK_50_79 eval:check_dcc_reputation_range('50','79') |
42 |
full DCC_CHECK_50_79 eval:check_dcc_reputation_range('50','79') |
28 |
|
43 |
|
29 |
=head1 DESCRIPTION |
44 |
=head1 DESCRIPTION |
30 |
|
45 |
|
31 |
The DCC or Distributed Checksum Clearinghouse is a system of servers |
46 |
The DCC or Distributed Checksum Clearinghouse is a system of servers |
32 |
collecting and counting checksums of millions of mail messages. |
47 |
collecting and counting checksums of millions of mail messages. |
33 |
TheSpamAssassin.pm counts can be used by SpamAssassin to detect and |
48 |
The counts can be used by SpamAssassin to detect and filter spam. |
34 |
reject or filter spam. |
|
|
35 |
|
49 |
|
36 |
Because simplistic checksums of spam can be easily defeated, the main |
50 |
See http://www.dcc-servers.net/dcc/ for more information about DCC. |
37 |
DCC checksums are fuzzy and ignore aspects of messages. The fuzzy |
|
|
38 |
checksums are changed as spam evolves. |
39 |
|
51 |
|
40 |
Note that DCC is disabled by default in C<init.pre> because it is not |
52 |
Note that DCC is disabled by default in C<v310.pre> because its use requires |
41 |
open source. See the DCC license for more details. |
53 |
software that is not distributed with SpamAssassin and that has license |
|
|
54 |
restrictions for certain commercial uses. |
55 |
See the DCC license at http://www.dcc-servers.net/dcc/LICENSE for details. |
42 |
|
56 |
|
43 |
See http://www.rhyolite.com/anti-spam/dcc/ for more information about |
57 |
Enable it by uncommenting the "loadplugin Mail::SpamAssassin::Plugin::DCC" |
44 |
DCC. |
58 |
confdir/v310.pre or by adding this line to your local.pre. It might also |
|
|
59 |
be necessary to install a DCC package, port, rpm, or equivalent from your |
60 |
operating system distributor or a tarball from the primary DCC source |
61 |
at http://www.dcc-servers.net/dcc/#download |
62 |
See also http://www.dcc-servers.net/dcc/INSTALL.html |
45 |
|
63 |
|
46 |
=head1 TAGS |
64 |
=head1 TAGS |
47 |
|
65 |
|
48 |
The following tags are added to the set, available for use in reports, |
66 |
The following tags are added to the set, available for use in reports, |
49 |
header fields, other plugins, etc.: |
67 |
header fields, other plugins, etc.: |
50 |
|
68 |
|
51 |
_DCCB_ DCC server ID in a response |
69 |
_DCCB_ DCC server ID in X-DCC-*-Metrics header field name |
52 |
_DCCR_ response from DCC - header field body in X-DCC-*-Metrics |
70 |
_DCCR_ X-DCC-*-Metrics header field body |
53 |
_DCCREP_ response from DCC - DCC reputation in percents (0..100) |
71 |
_DCCREP_ DCC Reputation or percent bulk mail (0..100) from |
|
|
72 |
commercial DCC software |
54 |
|
73 |
|
55 |
Tag _DCCREP_ provides a nonempty value only with commercial DCC systems. |
|
|
56 |
This is the percentage of spam vs. ham sent from the first untrusted relay. |
57 |
|
58 |
=cut |
74 |
=cut |
59 |
|
75 |
|
60 |
package Mail::SpamAssassin::Plugin::DCC; |
76 |
package Mail::SpamAssassin::Plugin::DCC; |
Lines 75-82
Link Here
|
75 |
use vars qw(@ISA); |
91 |
use vars qw(@ISA); |
76 |
@ISA = qw(Mail::SpamAssassin::Plugin); |
92 |
@ISA = qw(Mail::SpamAssassin::Plugin); |
77 |
|
93 |
|
78 |
use vars qw($have_inet6); |
|
|
79 |
|
80 |
sub new { |
94 |
sub new { |
81 |
my $class = shift; |
95 |
my $class = shift; |
82 |
my $mailsaobject = shift; |
96 |
my $mailsaobject = shift; |
Lines 87-93
Link Here
|
87 |
|
101 |
|
88 |
# are network tests enabled? |
102 |
# are network tests enabled? |
89 |
if ($mailsaobject->{local_tests_only}) { |
103 |
if ($mailsaobject->{local_tests_only}) { |
90 |
$self->{dcc_disabled} = 1; |
104 |
$self->{use_dcc} = 0; |
91 |
dbg("dcc: local tests only, disabling DCC"); |
105 |
dbg("dcc: local tests only, disabling DCC"); |
92 |
} |
106 |
} |
93 |
else { |
107 |
else { |
Lines 128-147
Link Here
|
128 |
|
142 |
|
129 |
=item dcc_fuz2_max NUMBER |
143 |
=item dcc_fuz2_max NUMBER |
130 |
|
144 |
|
131 |
This option sets how often a message's body/fuz1/fuz2 checksum must have been |
145 |
Sets how often a message's body/fuz1/fuz2 checksum must have been reported |
132 |
reported to the DCC server before SpamAssassin will consider the DCC check as |
146 |
to the DCC server before SpamAssassin will consider the DCC check hit. |
133 |
matched. |
147 |
C<999999> is DCC's MANY count. |
134 |
|
148 |
|
135 |
As nearly all DCC clients are auto-reporting these checksums, you should set |
|
|
136 |
this to a relatively high value, e.g. C<999999> (this is DCC's MANY count). |
137 |
|
138 |
The default is C<999999> for all these options. |
149 |
The default is C<999999> for all these options. |
139 |
|
150 |
|
140 |
=item dcc_rep_percent NUMBER |
151 |
=item dcc_rep_percent NUMBER |
141 |
|
152 |
|
142 |
Only commercial DCC systems provide DCC reputation information. This is the |
153 |
Only the commercial DCC software provides DCC Reputations. A DCC Reputation |
143 |
percentage of spam vs. ham sent from the first untrusted relay. It will hit |
154 |
is the percentage of bulk mail received from the last untrusted relay in the |
144 |
on new spam from spam sources. Default is C<90>. |
155 |
path taken by a mail message as measured by all commercial DCC installations. |
|
|
156 |
See http://www.rhyolite.com/dcc/reputations.html |
157 |
You C<must> whitelist your trusted relays or MX servers with MX or |
158 |
MXDCC lines in /var/dcc/whiteclnt as described in the main DCC man page |
159 |
to avoid seeing your own MX servers as sources of bulk mail. |
160 |
See http://www.dcc-servers.net/dcc/dcc-tree/dcc.html#White-and-Blacklists |
161 |
The default is C<90>. |
145 |
|
162 |
|
146 |
=cut |
163 |
=cut |
147 |
|
164 |
|
Lines 189-201
Link Here
|
189 |
=item dcc_home STRING |
206 |
=item dcc_home STRING |
190 |
|
207 |
|
191 |
This option tells SpamAssassin where to find the dcc homedir. |
208 |
This option tells SpamAssassin where to find the dcc homedir. |
192 |
If not given, it will try to get dcc to specify one, and if that fails it |
209 |
If not specified, try to use the locally configured directory |
193 |
will try dcc's own default homedir of '/var/dcc'. |
210 |
from the C<cdcc homedir> command. |
194 |
If C<dcc_path> is not specified, it will default to looking in |
211 |
Try /var/dcc if that command fails. |
195 |
C<dcc_home/bin> for dcc client instead of relying on SpamAssassin to find it |
|
|
196 |
in the current PATH. If it isn't found there, it will look in the current |
197 |
PATH. If a C<dccifd> socket is found in C<dcc_home> or specified explicitly, |
198 |
it will use that interface instead of C<dccproc>. |
199 |
|
212 |
|
200 |
=cut |
213 |
=cut |
201 |
|
214 |
|
Lines 211-218
Link Here
|
211 |
$value = untaint_file_path($value); |
224 |
$value = untaint_file_path($value); |
212 |
my $stat_errn = stat($value) ? 0 : 0+$!; |
225 |
my $stat_errn = stat($value) ? 0 : 0+$!; |
213 |
if ($stat_errn != 0 || !-d _) { |
226 |
if ($stat_errn != 0 || !-d _) { |
214 |
my $msg = $stat_errn == ENOENT ? "does not exist" |
227 |
my $msg = $stat_errn == ENOENT ? "does not exist" |
215 |
: !-d _ ? "is not a directory" : "not accessible: $!"; |
228 |
: !-d _ ? "is not a directory" : "not accessible: $!"; |
216 |
info("config: dcc_home \"$value\" $msg"); |
229 |
info("config: dcc_home \"$value\" $msg"); |
217 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
230 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
218 |
} |
231 |
} |
Lines 223-237
Link Here
|
223 |
|
236 |
|
224 |
=item dcc_dccifd_path STRING |
237 |
=item dcc_dccifd_path STRING |
225 |
|
238 |
|
226 |
This option tells SpamAssassin where to find the dccifd socket. If |
239 |
This option tells SpamAssassin where to find the dccifd socket instead |
227 |
C<dcc_dccifd_path> is not specified, it will default to looking for a socket |
240 |
of a local Unix socket named C<dccifd> in the C<dcc_home> directory. |
228 |
named C<dccifd> in a directory C<dcc_home>. The C<dcc_dccifd_path> can be |
241 |
If a socket is specified or found, use it instead of C<dccproc>. |
229 |
a Unix socket name (absolute path), or an INET socket specification in a form |
|
|
230 |
C<[host]:port> or C<host:port>, where a host can be an IPv4 or IPv6 address |
231 |
or a host name, and port is a TCP port number. In case of an IPv6 address the |
232 |
brackets are required syntax. If a C<dccifd> socket is found, the plugin will |
233 |
use it instead of C<dccproc>. |
234 |
|
242 |
|
|
|
243 |
If specifed, C<dcc_dccifd_path> is the absolute path of local Unix socket |
244 |
or an INET socket specified as C<[Host]:Port> or C<Host:Port>. |
245 |
Host can be an IPv4 or IPv6 address or a host name |
246 |
Port is a TCP port number. The brackets are required for an IPv6 address. |
247 |
|
248 |
The default is C<undef>. |
249 |
|
235 |
=cut |
250 |
=cut |
236 |
|
251 |
|
237 |
push (@cmds, { |
252 |
push (@cmds, { |
Lines 240-285
Link Here
|
240 |
type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING, |
255 |
type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING, |
241 |
code => sub { |
256 |
code => sub { |
242 |
my ($self, $key, $value, $line) = @_; |
257 |
my ($self, $key, $value, $line) = @_; |
243 |
$value = '' if !defined $value; |
258 |
|
244 |
$self->{dcc_dccifd_path_raw} = $value; # for logging purposes |
259 |
if (!defined $value || $value eq '') { |
245 |
undef $self->{dcc_dccifd_host}; |
|
|
246 |
undef $self->{dcc_dccifd_port}; |
247 |
undef $self->{dcc_dccifd_socket}; |
248 |
local($1,$2,$3); |
249 |
if ($value eq '') { |
250 |
return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; |
260 |
return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; |
251 |
} elsif ($value =~ m{^ (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) \z}sx) { |
|
|
252 |
# "[host]:port" or "host:port", where a host can be an IPv4 or IPv6 |
253 |
# address or a host name, and port is a TCP port number or service name |
254 |
my $host = defined $1 ? $1 : $2; |
255 |
my $port = $3; |
256 |
$self->{dcc_dccifd_host} = untaint_var($host); |
257 |
$self->{dcc_dccifd_port} = untaint_var($port); |
258 |
dbg("config: dcc_dccifd_path set to [%s]:%s", $host,$port); |
259 |
} else { # assume a unix socket |
260 |
if ($value !~ m{^/}) { |
261 |
info("config: dcc_dccifd_path should be an absolute socket path"); |
262 |
# return $Mail::SpamAssassin::Conf::INVALID_VALUE; # abort or accept? |
263 |
} |
264 |
$value = untaint_file_path($value); |
265 |
# test disabled, dccifd may not yet be running at spamd startup time |
266 |
# if (!-S $value) { |
267 |
# info("config: dcc_dccifd_path '$value' isn't a local socket"); |
268 |
# return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
269 |
# } |
270 |
$self->{dcc_dccifd_socket} = $value; |
271 |
dbg("config: dcc_dccifd_path set to local socket %s", $value); |
272 |
} |
261 |
} |
|
|
262 |
|
263 |
local($1,$2,$3); |
264 |
if ($value =~ m{^ (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) \z}sx) { |
265 |
my $host = untaint_var(defined $1 ? $1 : $2); |
266 |
my $port = untaint_var($3); |
267 |
if (!$host) { |
268 |
info("config: missing or bad host name in dcc_dccifd_path '$value'"); |
269 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
270 |
} |
271 |
if (!$port || $port !~ /^\d+\z/ || $port < 1 || $port > 65535) { |
272 |
info("config: bad TCP port number in dcc_dccifd_path '$value'"); |
273 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
274 |
} |
275 |
|
276 |
$self->{dcc_dccifd_host} = $host; |
277 |
$self->{dcc_dccifd_port} = $port; |
278 |
if ($host !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/) { |
279 |
# remember to try IPv6 if we can with a host name or non-IPv4 address |
280 |
$self->{dcc_dccifd_IPv6} = eval { require IO::Socket::INET6 }; |
281 |
} |
282 |
dbg("config: dcc_dccifd_path set to [%s]:%s", $host, $port); |
283 |
|
284 |
} else { |
285 |
# assume a unix socket |
286 |
if ($value !~ m{^/}) { |
287 |
info("config: dcc_dccifd_path '$value' is not an absolute path"); |
288 |
# return $Mail::SpamAssassin::Conf::INVALID_VALUE; # abort or accept? |
289 |
} |
290 |
$value = untaint_file_path($value); |
291 |
|
292 |
$self->{dcc_dccifd_socket} = $value; |
293 |
dbg("config: dcc_dccifd_path set to local socket %s", $value); |
294 |
dbg("dcc: dcc_dccifd_path set to local socket %s", $value); |
295 |
} |
296 |
|
297 |
$self->{dcc_dccifd_path_raw} = $value; |
273 |
} |
298 |
} |
274 |
}); |
299 |
}); |
275 |
|
300 |
|
276 |
=item dcc_path STRING |
301 |
=item dcc_path STRING |
277 |
|
302 |
|
278 |
This option tells SpamAssassin specifically where to find the C<dccproc> |
303 |
Where to find the C<dccproc> client program instead of relying on SpamAssassin |
279 |
client instead of relying on SpamAssassin to find it in the current PATH. |
304 |
to find it in the current PATH or C<dcc_home/bin>. This must often be set, |
280 |
Note that if I<taint mode> is enabled in the Perl interpreter, you should |
305 |
because the current PATH is cleared by I<taint mode> in the Perl interpreter, |
281 |
use this, as the current PATH will have been cleared. |
|
|
282 |
|
306 |
|
|
|
307 |
If a C<dccifd> socket is found in C<dcc_home> or specified explicitly |
308 |
with C<dcc_dccifd_path>, use the C<dccifd(8)> interface instead of C<dccproc>. |
309 |
|
310 |
The default is C<undef>. |
311 |
|
312 |
|
283 |
=cut |
313 |
=cut |
284 |
|
314 |
|
285 |
push (@cmds, { |
315 |
push (@cmds, { |
Lines 294-300
Link Here
|
294 |
} |
324 |
} |
295 |
$value = untaint_file_path($value); |
325 |
$value = untaint_file_path($value); |
296 |
if (!-x $value) { |
326 |
if (!-x $value) { |
297 |
info("config: dcc_path '$value' isn't an executable"); |
327 |
info("config: dcc_path '$value' is not executable"); |
298 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
328 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
299 |
} |
329 |
} |
300 |
|
330 |
|
Lines 304-310
Link Here
|
304 |
|
334 |
|
305 |
=item dcc_options options |
335 |
=item dcc_options options |
306 |
|
336 |
|
307 |
Specify additional options to the dccproc(8) command. Please note that only |
337 |
Specify additional options to the dccproc(8) command. Only |
308 |
characters in the range [0-9A-Za-z ,._/-] are allowed for security reasons. |
338 |
characters in the range [0-9A-Za-z ,._/-] are allowed for security reasons. |
309 |
|
339 |
|
310 |
The default is C<undef>. |
340 |
The default is C<undef>. |
Lines 319-324
Link Here
|
319 |
code => sub { |
349 |
code => sub { |
320 |
my ($self, $key, $value, $line) = @_; |
350 |
my ($self, $key, $value, $line) = @_; |
321 |
if ($value !~ m{^([0-9A-Za-z ,._/-]+)$}) { |
351 |
if ($value !~ m{^([0-9A-Za-z ,._/-]+)$}) { |
|
|
352 |
info("config: dcc_options '$value' contains impermissible characters"); |
322 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
353 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
323 |
} |
354 |
} |
324 |
$self->{dcc_options} = $1; |
355 |
$self->{dcc_options} = $1; |
Lines 327-334
Link Here
|
327 |
|
358 |
|
328 |
=item dccifd_options options |
359 |
=item dccifd_options options |
329 |
|
360 |
|
330 |
Specify additional options to send to the dccifd(8) daemon. Please note that only |
361 |
Specify additional options to send to the dccifd daemon with |
331 |
characters in the range [0-9A-Za-z ,._/-] are allowed for security reasons. |
362 |
the ASCII protocol described on the dccifd(8) man page. |
|
|
363 |
Only characters in the range [0-9A-Za-z ,._/-] are allowed for security reasons. |
332 |
|
364 |
|
333 |
The default is C<undef>. |
365 |
The default is C<undef>. |
334 |
|
366 |
|
Lines 342-606
Link Here
|
342 |
code => sub { |
374 |
code => sub { |
343 |
my ($self, $key, $value, $line) = @_; |
375 |
my ($self, $key, $value, $line) = @_; |
344 |
if ($value !~ m{^([0-9A-Za-z ,._/-]+)$}) { |
376 |
if ($value !~ m{^([0-9A-Za-z ,._/-]+)$}) { |
|
|
377 |
info("config: dccifd_options '$value' contains impermissible characters"); |
345 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
378 |
return $Mail::SpamAssassin::Conf::INVALID_VALUE; |
346 |
} |
379 |
} |
347 |
$self->{dccifd_options} = $1; |
380 |
$self->{dccifd_options} = $1; |
348 |
} |
381 |
} |
349 |
}); |
382 |
}); |
350 |
|
383 |
|
|
|
384 |
=item dcc_learn_score n (default: undef) |
385 |
|
386 |
Report messages with total scores this much larger than the |
387 |
SpamAssassin spam threshold to DCC as spam. |
388 |
|
389 |
=cut |
390 |
|
391 |
push (@cmds, { |
392 |
setting => 'dcc_learn_score', |
393 |
is_admin => 1, |
394 |
default => undef, |
395 |
type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC, |
396 |
}); |
397 |
|
351 |
$conf->{parser}->register_commands(\@cmds); |
398 |
$conf->{parser}->register_commands(\@cmds); |
352 |
} |
399 |
} |
353 |
|
400 |
|
|
|
401 |
|
402 |
|
403 |
|
404 |
sub ck_dir { |
405 |
my ($self, $dir, $tgt, $src) = @_; |
406 |
|
407 |
$dir = untaint_file_path($dir); |
408 |
if (!stat($dir)) { |
409 |
my $dir_errno = 0+$!; |
410 |
if ($dir_errno == ENOENT) { |
411 |
dbg("dcc: $tgt $dir from $src does not exist"); |
412 |
} else { |
413 |
dbg("dcc: $tgt $dir from $src is not accessible: $!"); |
414 |
} |
415 |
return; |
416 |
} |
417 |
if (!-d _) { |
418 |
dbg("dcc: $tgt $dir from $src is not a directory"); |
419 |
return; |
420 |
} |
421 |
|
422 |
$self->{main}->{conf}->{$tgt} = $dir; |
423 |
dbg("dcc: use '$tgt $dir' from $src"); |
424 |
} |
425 |
|
354 |
sub find_dcc_home { |
426 |
sub find_dcc_home { |
355 |
my ($self) = @_; |
427 |
my ($self) = @_; |
|
|
428 |
my $dcc_libexec; |
356 |
|
429 |
|
|
|
430 |
# just once |
431 |
return if defined $self->{dcc_version}; |
432 |
$self->{dcc_version} = '?'; |
433 |
|
357 |
my $conf = $self->{main}->{conf}; |
434 |
my $conf = $self->{main}->{conf}; |
358 |
return if !$conf->{use_dcc}; |
|
|
359 |
|
435 |
|
360 |
my $dcchome = $conf->{dcc_home} || ''; |
|
|
361 |
|
436 |
|
362 |
# If we're not given the DCC homedir, try getting DCC to tell us it. |
437 |
# Get the DCC software version for talking to dccifd and formating the |
363 |
# If that fails, try the DCC default homedir of '/var/dcc'. |
438 |
# dccifd options and the built-in DCC homedir. Use -q to prevent delays. |
364 |
if ($dcchome eq '') { |
439 |
my $cdcc_home; |
|
|
440 |
my $cdcc = $self->dcc_pgm_path('cdcc'); |
441 |
my $cmd = '-qV homedir libexecdir'; |
442 |
if ($cdcc && open(CDCC, "$cdcc $cmd 2>&1 |")) { |
443 |
my $cdcc_output = do { local $/ = undef; <CDCC> }; |
444 |
close CDCC; |
365 |
|
445 |
|
366 |
my $cdcc = Mail::SpamAssassin::Util::find_executable_in_env_path('cdcc'); |
446 |
$cdcc_output =~ s/\n/ /g; # everything in 1 line for debugging |
|
|
447 |
dbg("dcc: `%s %s` reports '%s'", $cdcc, $cmd, $cdcc_output); |
448 |
$self->{dcc_version} = ($cdcc_output =~ /^(\d+\.\d+\.\d+)/) ? $1 : ''; |
449 |
$cdcc_home = ($cdcc_output =~ /\s+homedir=(\S+)/) ? $1 : ''; |
450 |
if ($cdcc_output =~ /\s+libexecdir=(\S+)/) { |
451 |
$self->ck_dir($1, 'dcc_libexec', 'cdcc'); |
452 |
} |
453 |
} |
367 |
|
454 |
|
368 |
my $cdcc_home = ''; |
455 |
# without a home, try the homedir from cdcc |
369 |
if ($cdcc && -x $cdcc && open(CDCC, "$cdcc homedir 2>&1|")) { |
456 |
if (!$conf->{dcc_home} && $cdcc_home) { |
370 |
dbg("dcc: dcc_home not set, querying cdcc utility"); |
457 |
$self->ck_dir($cdcc_home, 'dcc_home', 'cdcc'); |
371 |
$cdcc_home = <CDCC> || ''; |
458 |
} |
372 |
close CDCC; |
459 |
# finally fall back to /var/dcc |
|
|
460 |
if (!$conf->{dcc_home}) { |
461 |
$self->ck_dir($conf->{dcc_home} = '/var/dcc', 'dcc_home', 'default') |
462 |
} |
373 |
|
463 |
|
374 |
chomp $cdcc_home; |
464 |
# fall back to $conf->{dcc_home}/libexec or /var/dcc/libexec for dccsight |
375 |
$cdcc_home =~ s/\s+homedir=//; |
465 |
if (!$conf->{dcc_libexec}) { |
376 |
dbg("dcc: cdcc reports homedir as '%s'", $cdcc_home); |
466 |
$self->ck_dir($conf->{dcc_home} . '/libexec', 'dcc_libexec', 'dcc_home'); |
377 |
} |
467 |
} |
|
|
468 |
if (!$conf->{dcc_libexec}) { |
469 |
$self->ck_dir('/var/dcc/libexec', 'dcc_libexec', 'dcc_home'); |
470 |
} |
378 |
|
471 |
|
379 |
# try first with whatever the cdcc utility reported |
472 |
# format options for dccifd |
380 |
my $cdcc_home_errno = 0; |
473 |
my $opts = ($conf->{dccifd_options} || '') . "\n"; |
381 |
if ($cdcc_home eq '') { |
474 |
if ($self->{dcc_version} =~ /\d+\.(\d+)\.(\d+)$/ && |
382 |
$cdcc_home_errno = ENOENT; |
475 |
($1 < 3 || ($1 == 3 && $2 < 123))) { |
383 |
} elsif (!stat($cdcc_home)) { |
476 |
if ($1 < 3 || ($1 == 3 && $2 < 50)) { |
384 |
$cdcc_home_errno = 0+$!; |
477 |
info("dcc: DCC version $self->{dcc_version} is years old, ". |
|
|
478 |
"obsolete, and likely to cause problems. ". |
479 |
"See http://www.dcc-servers.net/dcc/old-versions.html"); |
385 |
} |
480 |
} |
386 |
if ($cdcc_home_errno == ENOENT) { |
481 |
$self->{dccifd_lookup_options} = "header " . $opts; |
387 |
# no such file |
482 |
$self->{dccifd_report_options} = "header spam " . $opts; |
388 |
} elsif ($cdcc_home_errno != 0) { |
483 |
} else { |
389 |
dbg("dcc: cdcc reported homedir $cdcc_home is not accessible: $!"); |
484 |
# dccifd after version 1.2.123 understands "cksums" and "no-grey" |
390 |
} elsif (!-d _) { |
485 |
$self->{dccifd_lookup_options} = "cksums grey-off " . $opts; |
391 |
dbg("dcc: cdcc reported homedir $cdcc_home is not a directory"); |
486 |
$self->{dccifd_report_options} = "header spam grey-off " . $opts; |
392 |
} else { # ok |
487 |
} |
393 |
dbg("dcc: cdcc reported homedir $cdcc_home exists, using it"); |
488 |
} |
394 |
$dcchome = untaint_var($cdcc_home); |
|
|
395 |
} |
396 |
|
489 |
|
397 |
# try falling back to /var/dcc |
490 |
sub dcc_pgm_path { |
398 |
if ($dcchome eq '') { |
491 |
my ($self, $pgm) = @_; |
399 |
my $var_dcc_errno = stat('/var/dcc') ? 0 : 0+$!; |
492 |
my $pgmpath; |
400 |
if ($var_dcc_errno == ENOENT) { |
493 |
my $conf = $self->{main}->{conf}; |
401 |
# no such file |
|
|
402 |
} elsif ($var_dcc_errno != 0) { |
403 |
dbg("dcc: dcc_home not set and dcc default homedir /var/dcc ". |
404 |
"is not accessible: $!"); |
405 |
} elsif (!-d _) { |
406 |
dbg("dcc: dcc_home not set and dcc default homedir /var/dcc ". |
407 |
"is not a directory"); |
408 |
} else { # ok |
409 |
dbg("dcc: dcc_home not set but dcc default homedir /var/dcc exists, ". |
410 |
"using it"); |
411 |
$dcchome = '/var/dcc'; |
412 |
} |
413 |
} |
414 |
|
494 |
|
415 |
if ($dcchome eq '') { |
495 |
if ($pgmpath = $conf->{dcc_path}) { |
416 |
dbg("dcc: unable to get homedir from cdcc ". |
496 |
# accept explicit setting for dccproc |
417 |
"and the dcc default homedir was not found"); |
497 |
return $pgmpath if $pgm eq 'dccproc'; |
|
|
498 |
# try adapting it for cdcc and everything else |
499 |
if ($pgmpath =~ s/[^\/]+$/$pgm/) { |
500 |
$pgmpath = untaint_file_path($pgmpath); |
501 |
return $pgmpath if (-x $pgmpath); |
418 |
} |
502 |
} |
|
|
503 |
} |
419 |
|
504 |
|
420 |
# Remember found homedir path |
505 |
$pgmpath = Mail::SpamAssassin::Util::find_executable_in_env_path($pgm); |
421 |
dbg("dcc: using '%s' as DCC homedir", $dcchome); |
506 |
return $pgmpath if $pgmpath; |
422 |
$conf->{dcc_home} = $dcchome; |
507 |
|
|
|
508 |
# we might be looking for cdcc for $self->find_dcc_home() |
509 |
my $home = $conf->{dcc_home} || '/var/dcc'; |
510 |
|
511 |
$pgmpath = untaint_file_path($conf->{dcc_home} . "/bin/" . $pgm); |
512 |
return ($pgmpath) if (-x $pgmpath); |
513 |
|
514 |
if ($conf->{dcc_libexec}) { |
515 |
$pgmpath = $conf->{dcc_libexec} . "/" . $pgm; |
516 |
return ($pgmpath) if (-x $pgmpath); |
423 |
} |
517 |
} |
|
|
518 |
|
519 |
# desperate last attempts |
520 |
$pgmpath = '/usr/local/bin/' . $pgm; |
521 |
return $pgmpath if (-x $pgmpath); |
522 |
$pgmpath = '/var/dcc/' . $pgm; |
523 |
return $pgmpath if (-x $pgmpath); |
524 |
|
525 |
return undef; |
424 |
} |
526 |
} |
425 |
|
527 |
|
426 |
sub is_dccifd_available { |
528 |
sub is_dccifd_available { |
427 |
my ($self) = @_; |
529 |
my ($self) = @_; |
428 |
|
|
|
429 |
my $conf = $self->{main}->{conf}; |
530 |
my $conf = $self->{main}->{conf}; |
430 |
$self->{dccifd_available} = 0; |
|
|
431 |
|
531 |
|
432 |
if (!$conf->{use_dcc}) { |
532 |
# dccifd remains available until it breaks |
433 |
dbg("dcc: dccifd is not available: use_dcc is false"); |
533 |
return $self->{dccifd_available} if $self->{dccifd_available}; |
434 |
} elsif (defined $conf->{dcc_dccifd_host}) { |
534 |
|
435 |
dbg("dcc: dccifd inet socket chosen: [%s]:%s", |
535 |
# deal with configured INET socket |
436 |
$conf->{dcc_dccifd_host}, $conf->{dcc_dccifd_port}); |
536 |
if (defined $conf->{dcc_dccifd_host}) { |
437 |
$self->{dccifd_available} = 1; |
537 |
dbg("dcc: dccifd is available via INET socket [%s]:%s", |
438 |
} else { |
538 |
$conf->{dcc_dccifd_host}, $conf->{dcc_dccifd_port}); |
439 |
my $sockpath = $conf->{dcc_dccifd_socket}; |
539 |
return ($self->{dccifd_available} = 1); |
440 |
my $dcchome = $conf->{dcc_home}; |
540 |
} |
441 |
if (defined $sockpath) { |
541 |
|
442 |
dbg("dcc: dccifd local socket chosen: %s", $sockpath); |
542 |
# the first time here, compute a default local socket based on DCC home |
443 |
} elsif (defined $conf->{dcc_dccifd_path_raw}) { |
543 |
# from self->find_dcc_home() called elsewhere |
444 |
# avoid falling back to defaults if explicitly provided but wrong |
544 |
my $sockpath = $conf->{dcc_dccifd_socket}; |
445 |
} elsif (defined $dcchome && $dcchome ne '' && -S "$dcchome/dccifd") { |
545 |
if (!$sockpath) { |
446 |
$sockpath = "$dcchome/dccifd"; |
546 |
if ($conf->{dcc_dccifd_path_raw}) { |
|
|
547 |
$sockpath = $conf->{dcc_dccifd_path_raw}; |
548 |
} else { |
549 |
$sockpath = "$conf->{dcc_home}/dccifd"; |
550 |
} |
447 |
$conf->{dcc_dccifd_socket} = $sockpath; |
551 |
$conf->{dcc_dccifd_socket} = $sockpath; |
448 |
dbg("dcc: dccifd default local socket chosen: %s", $sockpath); |
|
|
449 |
} |
450 |
if (defined $sockpath && -S $sockpath && -w _ && -r _) { |
451 |
$self->{dccifd_available} = 1; |
452 |
} elsif (!defined $conf->{dcc_dccifd_path_raw}) { |
453 |
dbg("dcc: dccifd is not available: no r/w dccifd socket found"); |
454 |
} else { |
455 |
dbg("dcc: dccifd is not available: no r/w dccifd socket found: %s", |
456 |
$conf->{dcc_dccifd_path_raw}); |
457 |
} |
458 |
} |
552 |
} |
459 |
|
553 |
|
460 |
return $self->{dccifd_available}; |
554 |
# check the socket every time because it can appear and disappear |
|
|
555 |
return ($self->{dccifd_available} = 1) if (-S $sockpath && -w _ && -r _); |
556 |
|
557 |
dbg("dcc: dccifd is not available; no r/w socket at %s", $sockpath); |
558 |
return ($self->{dccifd_available} = 0); |
461 |
} |
559 |
} |
462 |
|
560 |
|
463 |
sub is_dccproc_available { |
561 |
sub is_dccproc_available { |
464 |
my ($self) = @_; |
562 |
my ($self) = @_; |
465 |
my $conf = $self->{main}->{conf}; |
563 |
my $conf = $self->{main}->{conf}; |
466 |
|
564 |
|
467 |
$self->{dccproc_available} = 0; |
565 |
# dccproc remains (un)available so check only once |
|
|
566 |
return $self->{dccproc_available} if defined $self->{dccproc_available}; |
468 |
|
567 |
|
469 |
if (!$conf->{use_dcc}) { |
568 |
my $dccproc = $conf->{dcc_path}; |
470 |
dbg("dcc: dccproc is not available: use_dcc is false"); |
569 |
if (!$dccproc) { |
471 |
return 0; |
570 |
$dccproc = $self->dcc_pgm_path('dccproc'); |
|
|
571 |
$conf->{dcc_path} = $dccproc; |
572 |
if (!$dccproc || ! -x $dccproc) { |
573 |
dbg("dcc: dccproc is not available: no dccproc executable found"); |
574 |
return ($self->{dccproc_available} = 0); |
575 |
} |
472 |
} |
576 |
} |
473 |
my $dcchome = $conf->{dcc_home} || ''; |
|
|
474 |
my $dccproc = $conf->{dcc_path} || ''; |
475 |
|
577 |
|
476 |
if ($dccproc eq '' && ($dcchome ne '' && -x "$dcchome/bin/dccproc")) { |
578 |
dbg("dcc: %s is available", $conf->{dcc_path}); |
477 |
$dccproc = "$dcchome/bin/dccproc"; |
579 |
return ($self->{dccproc_available} = 1); |
478 |
} |
|
|
479 |
if ($dccproc eq '') { |
480 |
$dccproc = Mail::SpamAssassin::Util::find_executable_in_env_path('dccproc'); |
481 |
} |
482 |
|
483 |
unless (defined $dccproc && $dccproc ne '' && -x $dccproc) { |
484 |
dbg("dcc: dccproc is not available: no dccproc executable found"); |
485 |
return 0; |
486 |
} |
487 |
|
488 |
# remember any found dccproc |
489 |
$conf->{dcc_path} = $dccproc; |
490 |
|
491 |
dbg("dcc: dccproc is available: %s", $conf->{dcc_path}); |
492 |
$self->{dccproc_available} = 1; |
493 |
return 1; |
494 |
} |
580 |
} |
495 |
|
581 |
|
496 |
sub dccifd_connect { |
582 |
sub dccifd_connect { |
497 |
my($self) = @_; |
583 |
my($self, $tag) = @_; |
498 |
my $conf = $self->{main}->{conf}; |
584 |
my $conf = $self->{main}->{conf}; |
499 |
my $sockpath = $conf->{dcc_dccifd_socket}; |
585 |
my $sockpath = $conf->{dcc_dccifd_socket}; |
500 |
my $host = $conf->{dcc_dccifd_host}; |
|
|
501 |
my $port = $conf->{dcc_dccifd_port}; |
502 |
my $sock; |
586 |
my $sock; |
|
|
587 |
|
503 |
if (defined $sockpath) { |
588 |
if (defined $sockpath) { |
504 |
dbg("dcc: connecting to a local socket %s", $sockpath); |
589 |
$sock = IO::Socket::UNIX->new(Type => SOCK_STREAM, Peer => $sockpath); |
505 |
$sock = IO::Socket::UNIX->new( |
590 |
if ($sock) { |
506 |
Type => SOCK_STREAM, Peer => $sockpath); |
591 |
dbg("$tag connected to local socket %s", $sockpath); |
507 |
$sock or die "dcc: failed to connect to a socket $sockpath: $!\n"; |
592 |
return $sock; |
508 |
} elsif (defined $host) { |
|
|
509 |
my $specified_path = $conf->{dcc_dccifd_path_raw}; |
510 |
if ($host eq '') { |
511 |
die "dcc: empty host specification: $specified_path\n"; |
512 |
} |
593 |
} |
513 |
if (!defined $port || $port !~ /^\d+\z/ || $port < 1 || $port > 65535) { |
594 |
$self->{dccifd_available} = 0; |
514 |
die "dcc: bad TCP port number: $specified_path\n"; |
595 |
info("$tag failed to connect to local socket $sockpath"); |
515 |
} |
596 |
return $sock |
516 |
my $is_inet4 = $host =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/; |
|
|
517 |
if ($is_inet4) { # inet4 socket (IPv4 address) |
518 |
dbg("dcc: connecting to inet4 socket [%s]:%s", $host,$port); |
519 |
$sock = IO::Socket::INET->new( |
520 |
Proto => 'tcp', PeerAddr => $host, PeerPort => $port); |
521 |
} else { |
522 |
if (!defined $have_inet6) { |
523 |
$have_inet6 = eval { require IO::Socket::INET6 }; |
524 |
$have_inet6 = 0 if !defined $have_inet6; |
525 |
} |
526 |
if (!$have_inet6) { # fallback to an inet4 socket (IPv4) |
527 |
dbg("dcc: connecting(2) to inet4 socket [%s]:%s", $host,$port); |
528 |
$sock = IO::Socket::INET->new( |
529 |
Proto => 'tcp', PeerAddr => $host, PeerPort => $port); |
530 |
} else { # inet6 socket (IPv6) or a host name |
531 |
dbg("dcc: connecting to inet6 socket [%s]:%s", $host,$port); |
532 |
$sock = IO::Socket::INET6->new( |
533 |
Proto => 'tcp', PeerAddr => $host, PeerPort => $port); |
534 |
} |
535 |
} |
536 |
$sock or die "dcc: failed to connect to [$host]:$port : $!\n"; |
537 |
} else { |
538 |
die "dcc: dccifd socket not provided: $conf->{dcc_dccifd_path_raw}\n"; |
539 |
} |
597 |
} |
|
|
598 |
|
599 |
# must be TCP/IP |
600 |
my $host = $conf->{dcc_dccifd_host}; |
601 |
my $port = $conf->{dcc_dccifd_port}; |
602 |
|
603 |
if ($conf->{dcc_dccifd_IPv6}) { |
604 |
# try IPv6 if we can with a host name or non-IPv4 address |
605 |
dbg("$tag connecting to inet6 socket [%s]:%s", $host,$port); |
606 |
$sock = IO::Socket::INET6->new( |
607 |
Proto => 'tcp', PeerAddr => $host, PeerPort => $port); |
608 |
# fall back to IPv4 if that failed |
609 |
} |
610 |
if (!$sock) { |
611 |
dbg("$tag connecting to inet4 socket [%s]:%s", $host, $port); |
612 |
$sock = IO::Socket::INET->new( |
613 |
Proto => 'tcp', PeerAddr => $host, PeerPort => $port); |
614 |
} |
615 |
|
616 |
info("failed to connect to [$host]:$port : $!") if !$sock; |
540 |
return $sock; |
617 |
return $sock; |
541 |
} |
618 |
} |
542 |
|
619 |
|
|
|
620 |
# check for dccifd every time in case enough uses of dccproc starts dccifd |
543 |
sub get_dcc_interface { |
621 |
sub get_dcc_interface { |
544 |
my ($self) = @_; |
622 |
my ($self) = @_; |
|
|
623 |
my $conf = $self->{main}->{conf}; |
545 |
|
624 |
|
546 |
if ($self->is_dccifd_available()) { |
625 |
if (!$conf->{use_dcc}) { |
547 |
$self->{dcc_interface} = "dccifd"; |
626 |
$self->{dcc_disabled} = 1; |
548 |
$self->{dcc_disabled} = 0; |
627 |
return; |
549 |
} |
628 |
} |
550 |
elsif ($self->is_dccproc_available()) { |
629 |
|
551 |
$self->{dcc_interface} = "dccproc"; |
630 |
$self->find_dcc_home(); |
552 |
$self->{dcc_disabled} = 0; |
631 |
if (!$self->is_dccifd_available() && !$self->is_dccproc_available()) { |
553 |
} |
632 |
dbg("dcc: dccifd and dccproc are not available"); |
554 |
else { |
|
|
555 |
dbg("dcc: dccifd and dccproc are not available, disabling DCC"); |
556 |
$self->{dcc_interface} = "none"; |
557 |
$self->{dcc_disabled} = 1; |
633 |
$self->{dcc_disabled} = 1; |
558 |
} |
634 |
} |
|
|
635 |
|
636 |
$self->{dcc_disabled} = 0; |
559 |
} |
637 |
} |
560 |
|
638 |
|
561 |
sub dcc_query { |
639 |
sub dcc_query { |
562 |
my ($self, $permsgstatus, $full) = @_; |
640 |
my ($self, $permsgstatus, $fulltext) = @_; |
563 |
|
641 |
|
564 |
$permsgstatus->{dcc_checked} = 1; |
642 |
$permsgstatus->{dcc_checked} = 1; |
565 |
|
643 |
|
|
|
644 |
if (!$self->{main}->{conf}->{use_dcc}) { |
645 |
dbg("dcc: DCC is not available: use_dcc is 0"); |
646 |
return; |
647 |
} |
648 |
|
566 |
# initialize valid tags |
649 |
# initialize valid tags |
567 |
$permsgstatus->{tag_data}->{DCCB} = ""; |
650 |
$permsgstatus->{tag_data}->{DCCB} = ""; |
568 |
$permsgstatus->{tag_data}->{DCCR} = ""; |
651 |
$permsgstatus->{tag_data}->{DCCR} = ""; |
569 |
$permsgstatus->{tag_data}->{DCCREP} = ""; |
652 |
$permsgstatus->{tag_data}->{DCCREP} = ""; |
570 |
|
653 |
|
571 |
# short-circuit if there's already a X-DCC header with value of |
654 |
if ($$fulltext eq '') { |
572 |
# "bulk" from an upstream DCC check |
655 |
dbg("dcc: empty message; skipping dcc check"); |
573 |
if ($permsgstatus->get('ALL') =~ |
|
|
574 |
/^(X-DCC-([^:]{1,80})?-?Metrics:.*bulk.*)$/m) { |
575 |
$permsgstatus->{dcc_response} = $1; |
576 |
return; |
656 |
return; |
577 |
} |
657 |
} |
578 |
|
658 |
|
|
|
659 |
if ($permsgstatus->get('ALL') =~ /^(X-DCC-.*-Metrics:.*)$/m) { |
660 |
$permsgstatus->{dcc_raw_x_dcc} = $1; |
661 |
# short-circuit if there is already a X-DCC header with value of |
662 |
# "bulk" from an upstream DCC check |
663 |
# require "bulk" because then at least one body checksum will be "many" |
664 |
# and so we know the X-DCC header is not forged by spammers |
665 |
return if $permsgstatus->{dcc_raw_x_dcc} =~ / bulk /; |
666 |
} |
667 |
|
579 |
my $timer = $self->{main}->time_method("check_dcc"); |
668 |
my $timer = $self->{main}->time_method("check_dcc"); |
580 |
|
669 |
|
581 |
$self->find_dcc_home(); |
670 |
$self->get_dcc_interface(); |
|
|
671 |
return if $self->{dcc_disabled}; |
582 |
|
672 |
|
583 |
$self->get_dcc_interface(); |
673 |
my $envelope = $permsgstatus->{relays_external}->[0]; |
584 |
my $result; |
674 |
($permsgstatus->{dcc_raw_x_dcc}, |
585 |
if ($self->{dcc_disabled}) { |
675 |
$permsgstatus->{dcc_cksums}) = $self->ask_dcc("dcc:", $permsgstatus, |
586 |
$result = 0; |
676 |
$fulltext, $envelope); |
587 |
} elsif ($$full eq '') { |
|
|
588 |
dbg("dcc: empty message, skipping dcc check"); |
589 |
$result = 0; |
590 |
} elsif ($self->{dccifd_available}) { |
591 |
my $client = $permsgstatus->{relays_external}->[0]->{ip}; |
592 |
my $clientname = $permsgstatus->{relays_external}->[0]->{rdns}; |
593 |
my $helo = $permsgstatus->{relays_external}->[0]->{helo} || ""; |
594 |
if ($client) { |
595 |
$client = $client . "\r" . $clientname if $clientname; |
596 |
} else { |
597 |
$client = "0.0.0.0"; |
598 |
} |
599 |
$self->dccifd_lookup($permsgstatus, $full, $client, $clientname, $helo); |
600 |
} else { |
601 |
my $client = $permsgstatus->{relays_external}->[0]->{ip}; |
602 |
$self->dccproc_lookup($permsgstatus, $full, $client); |
603 |
} |
604 |
} |
677 |
} |
605 |
|
678 |
|
606 |
sub check_dcc { |
679 |
sub check_dcc { |
Lines 609-636
Link Here
|
609 |
|
682 |
|
610 |
$self->dcc_query($permsgstatus, $full) if !$permsgstatus->{dcc_checked}; |
683 |
$self->dcc_query($permsgstatus, $full) if !$permsgstatus->{dcc_checked}; |
611 |
|
684 |
|
612 |
my $response = $permsgstatus->{dcc_response}; |
685 |
my $x_dcc = $permsgstatus->{dcc_raw_x_dcc}; |
613 |
return 0 if !defined $response || $response eq ''; |
686 |
return 0 if !defined $x_dcc || $x_dcc eq ''; |
614 |
|
687 |
|
615 |
local($1,$2); |
688 |
if ($x_dcc =~ /^X-DCC-(.*)-Metrics: (.*)$/) { |
616 |
if ($response =~ /^X-DCC-(.*)-Metrics: (.*)$/) { |
|
|
617 |
$permsgstatus->set_tag('DCCB', $1); |
689 |
$permsgstatus->set_tag('DCCB', $1); |
618 |
$permsgstatus->set_tag('DCCR', $2); |
690 |
$permsgstatus->set_tag('DCCR', $2); |
619 |
} |
691 |
} |
620 |
$response =~ s/many/999999/ig; |
692 |
$x_dcc =~ s/many/999999/ig; |
621 |
$response =~ s/ok\d?/0/ig; |
693 |
$x_dcc =~ s/ok\d?/0/ig; |
622 |
|
694 |
|
623 |
my %count = (body => 0, fuz1 => 0, fuz2 => 0, rep => 0); |
695 |
my %count = (body => 0, fuz1 => 0, fuz2 => 0, rep => 0); |
624 |
if ($response =~ /\bBody=(\d+)/) { |
696 |
if ($x_dcc =~ /\bBody=(\d+)/) { |
625 |
$count{body} = $1+0; |
697 |
$count{body} = $1+0; |
626 |
} |
698 |
} |
627 |
if ($response =~ /\bFuz1=(\d+)/) { |
699 |
if ($x_dcc =~ /\bFuz1=(\d+)/) { |
628 |
$count{fuz1} = $1+0; |
700 |
$count{fuz1} = $1+0; |
629 |
} |
701 |
} |
630 |
if ($response =~ /\bFuz2=(\d+)/) { |
702 |
if ($x_dcc =~ /\bFuz2=(\d+)/) { |
631 |
$count{fuz2} = $1+0; |
703 |
$count{fuz2} = $1+0; |
632 |
} |
704 |
} |
633 |
if ($response =~ /\brep=(\d+)/) { |
705 |
if ($x_dcc =~ /\brep=(\d+)/) { |
634 |
$count{rep} = $1+0; |
706 |
$count{rep} = $1+0; |
635 |
} |
707 |
} |
636 |
if ($count{body} >= $conf->{dcc_body_max} || |
708 |
if ($count{body} >= $conf->{dcc_body_max} || |
Lines 639-864
Link Here
|
639 |
$count{rep} >= $conf->{dcc_rep_percent}) |
711 |
$count{rep} >= $conf->{dcc_rep_percent}) |
640 |
{ |
712 |
{ |
641 |
dbg(sprintf("dcc: listed: BODY=%s/%s FUZ1=%s/%s FUZ2=%s/%s REP=%s/%s", |
713 |
dbg(sprintf("dcc: listed: BODY=%s/%s FUZ1=%s/%s FUZ2=%s/%s REP=%s/%s", |
642 |
map { defined $_ ? $_ : 'undef' } ( |
714 |
map { defined $_ ? $_ : 'undef' } ( |
643 |
$count{body}, $conf->{dcc_body_max}, |
715 |
$count{body}, $conf->{dcc_body_max}, |
644 |
$count{fuz1}, $conf->{dcc_fuz1_max}, |
716 |
$count{fuz1}, $conf->{dcc_fuz1_max}, |
645 |
$count{fuz2}, $conf->{dcc_fuz2_max}, |
717 |
$count{fuz2}, $conf->{dcc_fuz2_max}, |
646 |
$count{rep}, $conf->{dcc_rep_percent}) |
718 |
$count{rep}, $conf->{dcc_rep_percent}) |
647 |
)); |
719 |
)); |
648 |
return 1; |
720 |
return 1; |
649 |
} |
721 |
} |
650 |
return 0; |
722 |
return 0; |
651 |
} |
723 |
} |
652 |
|
724 |
|
653 |
sub check_dcc_reputation_range { |
725 |
sub check_dcc_reputation_range { |
654 |
my ($self, $permsgstatus, $full, $min, $max) = @_; |
726 |
my ($self, $permsgstatus, $fulltext, $min, $max) = @_; |
655 |
$self->dcc_query($permsgstatus, $full) if !$permsgstatus->{dcc_checked}; |
|
|
656 |
|
727 |
|
657 |
my $response = $permsgstatus->{dcc_response}; |
728 |
# this is called several times per message, so parse the X-DCC header once |
658 |
return 0 if !defined $response || $response eq ''; |
729 |
my $dcc_rep = $permsgstatus->{dcc_rep}; |
|
|
730 |
if (!defined $dcc_rep) { |
731 |
$self->dcc_query($permsgstatus, $fulltext) if !$permsgstatus->{dcc_checked}; |
732 |
my $x_dcc = $permsgstatus->{dcc_raw_x_dcc}; |
733 |
if (defined $x_dcc && $x_dcc =~ /\brep=(\d+)/) { |
734 |
$dcc_rep = $1+0; |
735 |
$permsgstatus->set_tag('DCCREP', $dcc_rep); |
736 |
} else { |
737 |
$dcc_rep = -1; |
738 |
} |
739 |
$permsgstatus->{dcc_rep} = $dcc_rep; |
740 |
} |
659 |
|
741 |
|
|
|
742 |
# no X-DCC header or no reputation in the X-DCC header, perhaps for lack |
743 |
# of data in the DCC Reputation server |
744 |
return 0 if $dcc_rep < 0; |
745 |
|
746 |
# cover the entire range of reputations if not told otherwise |
660 |
$min = 0 if !defined $min; |
747 |
$min = 0 if !defined $min; |
661 |
$max = 999 if !defined $max; |
748 |
$max = 100 if !defined $max; |
662 |
|
749 |
|
663 |
local $1; |
750 |
my $result = $dcc_rep >= $min && $dcc_rep <= $max ? 1 : 0; |
664 |
my $dcc_rep; |
751 |
dbg("dcc: dcc_rep %s, min %s, max %s => result=%s", |
665 |
$dcc_rep = $1+0 if defined $response && $response =~ /\brep=(\d+)/; |
752 |
$dcc_rep, $min, $max, $result?'YES':'no'); |
666 |
if (defined $dcc_rep) { |
753 |
return $result; |
667 |
$dcc_rep = int($dcc_rep); # just in case, rule ranges are integer percents |
754 |
} |
668 |
my $result = $dcc_rep >= $min && $dcc_rep <= $max ? 1 : 0; |
755 |
|
669 |
dbg("dcc: dcc_rep %s, min %s, max %s => result=%s", |
756 |
# get the X-DCC header line and save the checksums from dccifd or dccproc |
670 |
$dcc_rep, $min, $max, $result?'YES':'no'); |
757 |
sub parse_dcc_response { |
671 |
$permsgstatus->set_tag('DCCREP', $dcc_rep); |
758 |
my ($self, $resp) = @_; |
672 |
return $dcc_rep >= $min && $dcc_rep <= $max ? 1 : 0; |
759 |
my ($raw_x_dcc, $cksums); |
|
|
760 |
|
761 |
# The first line is the header we want. It uses SMTP folded whitespace |
762 |
# if it is long. The folded whitespace is always a single \t. |
763 |
chomp($raw_x_dcc = shift @$resp); |
764 |
my $v; |
765 |
while (($v = shift @$resp) && $v =~ s/^\t(.+)\s*\n/ $1/) { |
766 |
$raw_x_dcc .= $v; |
673 |
} |
767 |
} |
674 |
return 0; |
768 |
|
|
|
769 |
# skip the "reported:" line between the X-DCC header and any checksums |
770 |
# remove ':' to avoid a bug in versions 1.3.115 - 1.3.122 in dccsight |
771 |
# with the length of "Message-ID:" |
772 |
$cksums = ''; |
773 |
while (($v = shift @$resp) && $v =~ s/^([^:]*):/$1/) { |
774 |
$cksums .= $v; |
775 |
} |
776 |
|
777 |
return ($raw_x_dcc, $cksums); |
675 |
} |
778 |
} |
676 |
|
779 |
|
677 |
sub dccifd_lookup { |
780 |
sub ask_dcc { |
678 |
my ($self, $permsgstatus, $fulltext, $client, $clientname, $helo) = @_; |
781 |
my ($self, $tag, $permsgstatus, $fulltext, $envelope) = @_; |
679 |
my $conf = $self->{main}->{conf}; |
782 |
my $conf = $self->{main}->{conf}; |
680 |
my $response; |
783 |
my ($pgm, $err, $sock, $pid, @resp); |
681 |
my $left; |
784 |
my ($client, $clientname, $helo, $opts); |
682 |
my $right; |
|
|
683 |
my $timeout = $conf->{dcc_timeout}; |
684 |
my $opts = $conf->{dccifd_options}; |
685 |
my @opts = !defined $opts ? () : split(' ',$opts); |
686 |
|
785 |
|
687 |
$permsgstatus->enter_helper_run_mode(); |
786 |
$permsgstatus->enter_helper_run_mode(); |
688 |
|
787 |
|
|
|
788 |
my $timeout = $conf->{dcc_timeout}; |
689 |
my $timer = Mail::SpamAssassin::Timeout->new( |
789 |
my $timer = Mail::SpamAssassin::Timeout->new( |
690 |
{ secs => $timeout, deadline => $permsgstatus->{master_deadline} }); |
790 |
{ secs => $timeout, deadline => $permsgstatus->{master_deadline} }); |
691 |
my $err = $timer->run_and_catch(sub { |
|
|
692 |
|
791 |
|
|
|
792 |
$err = $timer->run_and_catch(sub { |
693 |
local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; |
793 |
local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; |
694 |
|
794 |
|
695 |
my $sock = $self->dccifd_connect(); |
795 |
# prefer dccifd to dccproc |
696 |
$sock or die "dcc: failed to connect to a dccifd socket"; |
796 |
if ($self->{dccifd_available}) { |
|
|
797 |
$pgm = 'dccifd'; |
697 |
|
798 |
|
698 |
# send the options and other parameters to the daemon |
799 |
$sock = $self->dccifd_connect($tag); |
699 |
$sock->print("header " . join(" ",@opts) . "\n") |
800 |
if (!$sock) { |
700 |
or die "dcc: failed write"; # options |
801 |
$self->{dccifd_available} = 0; |
701 |
$sock->print($client . "\n") or die "dcc: failed write"; # client |
802 |
die("dccproc not available") if (!$self->is_dccproc_available()); |
702 |
$sock->print($helo . "\n") or die "dcc: failed write"; # HELO value |
|
|
703 |
$sock->print("\n") or die "dcc: failed write"; # sender |
704 |
$sock->print("unknown\r\n") or die "dcc: failed write"; # recipients |
705 |
$sock->print("\n") or die "dcc: failed write"; # recipients |
706 |
|
803 |
|
707 |
$sock->print($$fulltext) or die "dcc: failed write"; |
804 |
# fall back on dccproc if the socket is an orphan from |
|
|
805 |
# a killed dccifd daemon or some other obvious (no timeout) problem |
806 |
dbg("$tag fall back on dccproc"); |
807 |
} |
808 |
} |
708 |
|
809 |
|
709 |
$sock->shutdown(1) or die "dcc: failed socket shutdown: $!"; |
810 |
if ($self->{dccifd_available}) { |
710 |
|
811 |
|
711 |
$sock->getline() or die "dcc: failed read status"; |
812 |
# send the options and other parameters to the daemon |
712 |
$sock->getline() or die "dcc: failed read multistatus"; |
813 |
$client = $envelope->{ip}; |
|
|
814 |
$clientname = $envelope->{rdns}; |
815 |
if (!defined $client) { |
816 |
$client = ''; |
817 |
} else { |
818 |
$client .= ("\r" . $clientname) if defined $clientname; |
819 |
} |
820 |
$helo = $envelope->{helo} || ''; |
821 |
if ($tag ne "dcc:") { |
822 |
$opts = $self->{dccifd_report_options} |
823 |
} else { |
824 |
$opts = $self->{dccifd_lookup_options}; |
825 |
# only query if there is an X-DCC header |
826 |
$opts =~ s/grey-off/& query/ if defined $permsgstatus->{dcc_raw_x_dcc}; |
827 |
} |
828 |
$sock->print($opts) or die "failed write options\n"; |
829 |
$sock->print($client . "\n") or die "failed write SMTP client\n"; |
830 |
$sock->print($helo . "\n") or die "failed write HELO value\n"; |
831 |
$sock->print("\n") or die "failed write sender\n"; |
832 |
$sock->print("unknown\n\n") or die "failed write 1 recipient\n"; |
833 |
$sock->print($$fulltext) or die "failed write mail message\n"; |
834 |
$sock->shutdown(1) or die "failed socket shutdown: $!"; |
713 |
|
835 |
|
714 |
my @null = $sock->getlines(); |
836 |
$sock->getline() or die "failed read status\n"; |
715 |
if (!@null) { |
837 |
$sock->getline() or die "failed read multistatus\n"; |
716 |
# no facility prefix on this |
838 |
|
717 |
die "dcc: failed to read header\n"; |
839 |
@resp = $sock->getlines(); |
|
|
840 |
die "failed to read dccifd response\n" if !@resp; |
841 |
|
842 |
} else { |
843 |
$pgm = 'dccproc'; |
844 |
# use a temp file -- open2() is unreliable, buffering-wise, under spamd |
845 |
# first ensure that we do not hit a stray file from some other filter. |
846 |
$permsgstatus->delete_fulltext_tmpfile(); |
847 |
my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext); |
848 |
|
849 |
my $path = $conf->{dcc_path}; |
850 |
$opts = $conf->{dcc_options}; |
851 |
my @opts = !defined $opts ? () : split(' ',$opts); |
852 |
untaint_var(\@opts); |
853 |
unshift(@opts, '-w', 'whiteclnt'); |
854 |
$client = $envelope->{ip}; |
855 |
if ($client) { |
856 |
unshift(@opts, '-a', untaint_var($client)); |
857 |
} else { |
858 |
# get external relay IP address from Received: header if not available |
859 |
unshift(@opts, '-R'); |
860 |
} |
861 |
if ($tag eq "dcc:") { |
862 |
# query instead of report if there is an X-DCC header from upstream |
863 |
unshift(@opts, '-Q', 'many') if defined $permsgstatus->{dcc_raw_x_dcc}; |
864 |
} else { |
865 |
# learn or report spam |
866 |
unshift(@opts, '-t', 'many'); |
867 |
} |
868 |
|
869 |
dbg("$tag opening pipe to %s", |
870 |
join(' ', $path, "-C", "-x", "0", @opts, "<$tmpf")); |
871 |
|
872 |
$pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC, |
873 |
$tmpf, 1, $path, "-C", "-x", "0", @opts); |
874 |
$pid or die "$!\n"; |
875 |
|
876 |
# read+split avoids a Perl I/O bug (Bug 5985) |
877 |
my($inbuf,$nread,$resp); $resp = ''; |
878 |
while ( $nread=read(DCC,$inbuf,8192) ) { $resp .= $inbuf } |
879 |
defined $nread or die "error reading from pipe: $!"; |
880 |
@resp = split(/^/m, $resp, -1); undef $resp; |
881 |
|
882 |
my $errno = 0; close DCC or $errno = $!; |
883 |
proc_status_ok($?,$errno) |
884 |
or info("$tag [%s] finished: %s", $pid, exit_status_str($?,$errno)); |
885 |
|
886 |
die "failed to read X-DCC header from dccproc\n" if !@resp; |
718 |
} |
887 |
} |
|
|
888 |
}); |
719 |
|
889 |
|
720 |
# the first line will be the header we want to look at |
890 |
if ($pgm eq 'dccproc') { |
721 |
chomp($response = shift @null); |
891 |
if (defined(fileno(*DCC))) { # still open |
722 |
# but newer versions of DCC fold the header if it's too long... |
892 |
if ($pid) { |
723 |
while (my $v = shift @null) { |
893 |
if (kill('TERM',$pid)) { |
724 |
last unless ($v =~ s/^\s+/ /); # if this line wasn't folded, stop |
894 |
dbg("$tag killed stale dccproc process [$pid]") |
725 |
chomp $v; |
895 |
} else { |
726 |
$response .= $v; |
896 |
dbg("$tag killing dccproc process [$pid] failed: $!") |
|
|
897 |
} |
898 |
} |
899 |
my $errno = 0; close(DCC) or $errno = $!; |
900 |
proc_status_ok($?,$errno) or info("$tag [%s] dccproc terminated: %s", |
901 |
$pid, exit_status_str($?,$errno)); |
727 |
} |
902 |
} |
|
|
903 |
} |
728 |
|
904 |
|
729 |
dbg("dcc: dccifd got response: %s", $response); |
|
|
730 |
|
731 |
}); |
732 |
|
733 |
$permsgstatus->leave_helper_run_mode(); |
905 |
$permsgstatus->leave_helper_run_mode(); |
734 |
|
906 |
|
735 |
if ($timer->timed_out()) { |
907 |
if ($timer->timed_out()) { |
736 |
dbg("dcc: dccifd check timed out after $timeout secs."); |
908 |
dbg("$tag $pgm timed out after $timeout seconds"); |
737 |
return; |
909 |
return (undef, undef); |
738 |
} |
910 |
} |
739 |
|
911 |
|
740 |
if ($err) { |
912 |
if ($err) { |
741 |
chomp $err; |
913 |
chomp $err; |
742 |
warn("dcc: dccifd -> check skipped: $err\n"); |
914 |
info("$tag $pgm failed: $err\n"); |
|
|
915 |
return (undef, undef); |
916 |
} |
917 |
|
918 |
my ($raw_x_dcc, $cksums) = $self->parse_dcc_response(\@resp); |
919 |
if (!defined $raw_x_dcc || $raw_x_dcc !~ /^X-DCC/) { |
920 |
info("$tag instead of X-DCC header, $pgm returned '%s'", $raw_x_dcc); |
921 |
return (undef, undef); |
922 |
} |
923 |
dbg("$tag %s responded with '%s'", $pgm, $raw_x_dcc); |
924 |
return ($raw_x_dcc, $cksums); |
925 |
} |
926 |
|
927 |
# tell DCC server that the message is spam according to SpamAssassin |
928 |
sub check_post_learn { |
929 |
my ($self, $options) = @_; |
930 |
|
931 |
# learn only if allowed |
932 |
return if $self->{learn_disabled}; |
933 |
my $conf = $self->{main}->{conf}; |
934 |
if (!$conf->{use_dcc}) { |
935 |
$self->{learn_disabled} = 1; |
743 |
return; |
936 |
return; |
744 |
} |
937 |
} |
|
|
938 |
my $learn_score = $conf->{dcc_learn_score}; |
939 |
if (!defined $learn_score || $learn_score eq '') { |
940 |
dbg("dcc: DCC learning not enabled by dcc_learn_score"); |
941 |
$self->{learn_disabled} = 1; |
942 |
return; |
943 |
} |
745 |
|
944 |
|
746 |
if (!defined $response || $response !~ /^X-DCC/) { |
945 |
# and if SpamAssassin concluded that the message is spam |
747 |
dbg("dcc: dccifd check failed - no X-DCC returned: %s", $response); |
946 |
# worse than our threshold |
|
|
947 |
my $permsgstatus = $options->{permsgstatus}; |
948 |
if ($permsgstatus->is_spam()) { |
949 |
my $score = $permsgstatus->get_score(); |
950 |
my $required_score = $permsgstatus->get_required_score(); |
951 |
if ($score < $required_score + $learn_score) { |
952 |
dbg("dcc: score=%d required_score=%d dcc_learn_score=%d", |
953 |
$score, $required_score, $learn_score); |
954 |
return; |
955 |
} |
956 |
} |
957 |
|
958 |
# and if we checked the message |
959 |
return if (!defined $permsgstatus->{dcc_raw_x_dcc}); |
960 |
|
961 |
# and if the DCC server thinks it was not spam |
962 |
if ($permsgstatus->{dcc_raw_x_dcc} !~ /\b(Body|Fuz1|Fuz2)=\d/) { |
963 |
dbg("dcc: already known as spam; no need to learn"); |
748 |
return; |
964 |
return; |
749 |
} |
965 |
} |
750 |
|
966 |
|
751 |
$response =~ s/[ \t]\z//; # strip trailing whitespace |
967 |
# dccsight is faster than dccifd or dccproc if we have checksums, |
752 |
$permsgstatus->{dcc_response} = $response; |
968 |
# which we do not have with dccifd before 1.3.123 |
|
|
969 |
my $old_cksums = $permsgstatus->{dcc_cksums}; |
970 |
return if ($old_cksums && $self->dccsight_learn($permsgstatus, $old_cksums)); |
971 |
|
972 |
# Fall back on dccifd or dccproc without saved checksums or dccsight. |
973 |
# get_dcc_interface() was called when the message was checked |
974 |
|
975 |
# is getting the full text this way kosher? Is get_pristine() public? |
976 |
my $fulltext = $permsgstatus->{msg}->get_pristine(); |
977 |
my $envelope = $permsgstatus->{relays_external}->[0]; |
978 |
my ($raw_x_dcc, $cksums) = $self->ask_dcc("dcc: learn:", $permsgstatus, |
979 |
\$fulltext, $envelope); |
980 |
dbg("dcc: learned as spam") if defined $raw_x_dcc; |
753 |
} |
981 |
} |
754 |
|
982 |
|
755 |
sub dccproc_lookup { |
983 |
sub dccsight_learn { |
756 |
my ($self, $permsgstatus, $fulltext, $client) = @_; |
984 |
my ($self, $permsgstatus, $old_cksums) = @_; |
757 |
my $conf = $self->{main}->{conf}; |
985 |
my ($raw_x_dcc, $new_cksums); |
758 |
my $response; |
|
|
759 |
my %count = (body => 0, fuz1 => 0, fuz2 => 0, rep => 0); |
760 |
my $timeout = $conf->{dcc_timeout}; |
761 |
|
986 |
|
|
|
987 |
return 0 if !$old_cksums; |
988 |
|
989 |
my $dccsight = $self->dcc_pgm_path('dccsight'); |
990 |
if (!$dccsight) { |
991 |
info("dcc: cannot find dccsight") if $dccsight eq ''; |
992 |
return 0; |
993 |
} |
994 |
|
762 |
$permsgstatus->enter_helper_run_mode(); |
995 |
$permsgstatus->enter_helper_run_mode(); |
763 |
|
996 |
|
764 |
# use a temp file here -- open2() is unreliable, buffering-wise, under spamd |
997 |
# use a temp file here -- open2() is unreliable, buffering-wise, under spamd |
765 |
my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext); |
998 |
# ensure that we do not hit a stray file from some other filter. |
|
|
999 |
$permsgstatus->delete_fulltext_tmpfile(); |
1000 |
my $tmpf = $permsgstatus->create_fulltext_tmpfile(\$old_cksums); |
766 |
my $pid; |
1001 |
my $pid; |
767 |
|
1002 |
|
|
|
1003 |
my $timeout = $self->{main}->{conf}->{dcc_timeout}; |
768 |
my $timer = Mail::SpamAssassin::Timeout->new( |
1004 |
my $timer = Mail::SpamAssassin::Timeout->new( |
769 |
{ secs => $timeout, deadline => $permsgstatus->{master_deadline} }); |
1005 |
{ secs => $timeout, deadline => $permsgstatus->{master_deadline} }); |
770 |
my $err = $timer->run_and_catch(sub { |
1006 |
my $err = $timer->run_and_catch(sub { |
771 |
|
|
|
772 |
local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; |
1007 |
local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; |
773 |
|
1008 |
|
774 |
# note: not really tainted, this came from system configuration file |
1009 |
dbg("dcc: opening pipe to %s", |
775 |
my $path = untaint_file_path($conf->{dcc_path}); |
1010 |
join(' ', $dccsight, "-t", "many", "<$tmpf")); |
776 |
|
1011 |
|
777 |
my $opts = $conf->{dcc_options}; |
|
|
778 |
my @opts = !defined $opts ? () : split(' ',$opts); |
779 |
untaint_var(\@opts); |
780 |
|
781 |
unshift(@opts, "-a", |
782 |
untaint_var($client)) if defined $client && $client ne ''; |
783 |
|
784 |
dbg("dcc: opening pipe: %s", |
785 |
join(' ', $path, "-H", "-x", "0", @opts, "< $tmpf")); |
786 |
|
787 |
$pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC, |
1012 |
$pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC, |
788 |
$tmpf, 1, $path, "-H", "-x", "0", @opts); |
1013 |
$tmpf, 1, $dccsight, "-t", "many"); |
789 |
$pid or die "$!\n"; |
1014 |
$pid or die "$!\n"; |
790 |
|
1015 |
|
791 |
# read+split avoids a Perl I/O bug (Bug 5985) |
1016 |
# read+split avoids a Perl I/O bug (Bug 5985) |
792 |
my($inbuf,$nread,$resp); $resp = ''; |
1017 |
my($inbuf,$nread,$resp); $resp = ''; |
793 |
while ( $nread=read(DCC,$inbuf,8192) ) { $resp .= $inbuf } |
1018 |
while ( $nread=read(DCC,$inbuf,8192) ) { $resp .= $inbuf } |
794 |
defined $nread or die "error reading from pipe: $!"; |
1019 |
defined $nread or die "error reading from pipe: $!"; |
795 |
my @null = split(/^/m, $resp, -1); undef $resp; |
1020 |
my @resp = split(/^/m, $resp, -1); undef $resp; |
796 |
|
1021 |
|
797 |
my $errno = 0; close DCC or $errno = $!; |
1022 |
my $errno = 0; close DCC or $errno = $!; |
798 |
proc_status_ok($?,$errno) |
1023 |
proc_status_ok($?,$errno) |
799 |
or info("dcc: [%s] finished: %s", $pid, exit_status_str($?,$errno)); |
1024 |
or info("dcc: [%s] finished: %s", $pid, exit_status_str($?,$errno)); |
800 |
|
1025 |
|
801 |
if (!@null) { |
1026 |
die "dcc: failed to read learning response\n" if !@resp; |
802 |
# no facility prefix on this |
|
|
803 |
die "failed to read header\n"; |
804 |
} |
805 |
|
1027 |
|
806 |
# the first line will be the header we want to look at |
1028 |
($raw_x_dcc, $new_cksums) = $self->parse_dcc_response(\@resp); |
807 |
chomp($response = shift @null); |
|
|
808 |
# but newer versions of DCC fold the header if it's too long... |
809 |
while (my $v = shift @null) { |
810 |
last unless ($v =~ s/^\s+/ /); # if this line wasn't folded, stop |
811 |
chomp $v; |
812 |
$response .= $v; |
813 |
} |
814 |
|
815 |
unless (defined($response)) { |
816 |
# no facility prefix on this |
817 |
die "no response\n"; # yes, this is possible |
818 |
} |
819 |
|
820 |
dbg("dcc: got response: %s", $response); |
821 |
|
822 |
}); |
1029 |
}); |
823 |
|
1030 |
|
824 |
if (defined(fileno(*DCC))) { # still open |
1031 |
if (defined(fileno(*DCC))) { # still open |
825 |
if ($pid) { |
1032 |
if ($pid) { |
826 |
if (kill('TERM',$pid)) { dbg("dcc: killed stale helper [$pid]") } |
1033 |
if (kill('TERM',$pid)) { |
827 |
else { dbg("dcc: killing helper application [$pid] failed: $!") } |
1034 |
dbg("dcc: killed stale dccsight process [$pid]") |
|
|
1035 |
} else { |
1036 |
dbg("dcc: killing stale dccsight process [$pid] failed: $!") } |
828 |
} |
1037 |
} |
829 |
my $errno = 0; close(DCC) or $errno = $!; |
1038 |
my $errno = 0; close(DCC) or $errno = $!; |
830 |
proc_status_ok($?,$errno) |
1039 |
proc_status_ok($?,$errno) or info("dcc: dccsight [%s] terminated: %s", |
831 |
or info("dcc: [%s] terminated: %s", $pid, exit_status_str($?,$errno)); |
1040 |
$pid, exit_status_str($?,$errno)); |
832 |
} |
1041 |
} |
|
|
1042 |
$permsgstatus->delete_fulltext_tmpfile(); |
833 |
$permsgstatus->leave_helper_run_mode(); |
1043 |
$permsgstatus->leave_helper_run_mode(); |
834 |
|
1044 |
|
835 |
if ($timer->timed_out()) { |
1045 |
if ($timer->timed_out()) { |
836 |
dbg("dcc: check timed out after $timeout seconds"); |
1046 |
dbg("dcc: dccsight timed out after $timeout seconds"); |
837 |
return; |
1047 |
return 0; |
838 |
} |
1048 |
} |
839 |
|
1049 |
|
840 |
if ($err) { |
1050 |
if ($err) { |
841 |
chomp $err; |
1051 |
chomp $err; |
842 |
if ($err eq "__brokenpipe__ignore__") { |
1052 |
info("dcc: dccsight failed: $err\n"); |
843 |
dbg("dcc: check failed: broken pipe"); |
1053 |
return 0; |
844 |
} elsif ($err eq "no response") { |
|
|
845 |
dbg("dcc: check failed: no response"); |
846 |
} else { |
847 |
warn("dcc: check failed: $err\n"); |
848 |
} |
849 |
return; |
850 |
} |
1054 |
} |
851 |
|
1055 |
|
852 |
if (!defined($response) || $response !~ /^X-DCC/) { |
1056 |
if ($raw_x_dcc) { |
853 |
$response ||= ''; |
1057 |
dbg("dcc: learned response: %s", $raw_x_dcc); |
854 |
dbg("dcc: check failed: no X-DCC returned (did you create a map file?): %s", $response); |
1058 |
return 1; |
855 |
return; |
|
|
856 |
} |
1059 |
} |
857 |
|
1060 |
|
858 |
$permsgstatus->{dcc_response} = $response; |
1061 |
return 0; |
859 |
} |
1062 |
} |
860 |
|
1063 |
|
861 |
# only supports dccproc right now |
|
|
862 |
sub plugin_report { |
1064 |
sub plugin_report { |
863 |
my ($self, $options) = @_; |
1065 |
my ($self, $options) = @_; |
864 |
|
1066 |
|
Lines 866-1036
Link Here
|
866 |
$self->get_dcc_interface(); |
1068 |
$self->get_dcc_interface(); |
867 |
return if $self->{dcc_disabled}; |
1069 |
return if $self->{dcc_disabled}; |
868 |
|
1070 |
|
869 |
# get the metadata from the message so we can pass the external relay information |
1071 |
# get the metadata from the message so we can report the external relay |
870 |
$options->{msg}->extract_message_metadata($options->{report}->{main}); |
1072 |
$options->{msg}->extract_message_metadata($options->{report}->{main}); |
871 |
my $client = $options->{msg}->{metadata}->{relays_external}->[0]->{ip}; |
1073 |
my $envelope = $options->{msg}->{metadata}->{relays_external}->[0]; |
872 |
if ($self->{dccifd_available}) { |
1074 |
my ($raw_x_dcc, $cksums) = $self->ask_dcc("reporter:", $options->{report}, |
873 |
my $clientname = $options->{msg}->{metadata}->{relays_external}->[0]->{rdns}; |
1075 |
$options->{text}, $envelope); |
874 |
my $helo = $options->{msg}->{metadata}->{relays_external}->[0]->{helo} || ""; |
1076 |
|
875 |
if ($client) { |
1077 |
if (defined $raw_x_dcc) { |
876 |
if ($clientname) { |
1078 |
$options->{report}->{report_available} = 1; |
877 |
$client = $client . "\r" . $clientname; |
1079 |
info("reporter: spam reported to DCC"); |
878 |
} |
1080 |
$options->{report}->{report_return} = 1; |
879 |
} else { |
|
|
880 |
$client = "0.0.0.0"; |
881 |
} |
882 |
if ($self->dccifd_report($options, $options->{text}, $client, $helo)) { |
883 |
$options->{report}->{report_available} = 1; |
884 |
info("reporter: spam reported to DCC"); |
885 |
$options->{report}->{report_return} = 1; |
886 |
} |
887 |
else { |
888 |
info("reporter: could not report spam to DCC via dccifd"); |
889 |
} |
890 |
} else { |
1081 |
} else { |
891 |
# use temporary file: open2() is unreliable due to buffering under spamd |
1082 |
info("reporter: could not report spam to DCC"); |
892 |
my $tmpf = $options->{report}->create_fulltext_tmpfile($options->{text}); |
|
|
893 |
|
894 |
if ($self->dcc_report($options, $tmpf, $client)) { |
895 |
$options->{report}->{report_available} = 1; |
896 |
info("reporter: spam reported to DCC"); |
897 |
$options->{report}->{report_return} = 1; |
898 |
} |
899 |
else { |
900 |
info("reporter: could not report spam to DCC via dccproc"); |
901 |
} |
902 |
$options->{report}->delete_fulltext_tmpfile(); |
903 |
} |
1083 |
} |
904 |
} |
1084 |
} |
905 |
|
1085 |
|
906 |
sub dccifd_report { |
|
|
907 |
my ($self, $options, $fulltext, $client, $helo) = @_; |
908 |
my $conf = $self->{main}->{conf}; |
909 |
my $timeout = $conf->{dcc_timeout}; |
910 |
# instead of header use whatever the report option is |
911 |
my $opts = $conf->{dccifd_options}; |
912 |
my @opts = !defined $opts ? () : split(' ',$opts); |
913 |
|
914 |
$options->{report}->enter_helper_run_mode(); |
915 |
my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout }); |
916 |
|
917 |
my $err = $timer->run_and_catch(sub { |
918 |
|
919 |
local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; |
920 |
|
921 |
my $sock = $self->dccifd_connect(); |
922 |
$sock or die "report: failed to connect to a dccifd socket"; |
923 |
|
924 |
# send the options and other parameters to the daemon |
925 |
$sock->print("spam " . join(" ",@opts) . "\n") |
926 |
or die "report: dccifd failed write"; # options |
927 |
$sock->print($client . "\n") |
928 |
or die "report: dccifd failed write"; # client |
929 |
$sock->print($helo . "\n") |
930 |
or die "report: dccifd failed write"; # HELO value |
931 |
$sock->print("\n") |
932 |
or die "report: dccifd failed write"; # sender |
933 |
$sock->print("unknown\r\n") |
934 |
or die "report: dccifd failed write"; # recipients |
935 |
$sock->print("\n") |
936 |
or die "report: dccifd failed write"; # recipients |
937 |
|
938 |
$sock->print($$fulltext) or die "report: dccifd failed write"; |
939 |
|
940 |
$sock->shutdown(1) or die "report: dccifd failed socket shutdown: $!"; |
941 |
|
942 |
$sock->getline() or die "report: dccifd failed read status"; |
943 |
$sock->getline() or die "report: dccifd failed read multistatus"; |
944 |
|
945 |
my @ignored = $sock->getlines(); |
946 |
}); |
947 |
|
948 |
$options->{report}->leave_helper_run_mode(); |
949 |
|
950 |
if ($timer->timed_out()) { |
951 |
dbg("reporter: DCC report via dccifd timed out after $timeout secs."); |
952 |
return 0; |
953 |
} |
954 |
|
955 |
if ($err) { |
956 |
chomp $err; |
957 |
if ($err eq "__brokenpipe__ignore__") { |
958 |
dbg("reporter: DCC report via dccifd failed: broken pipe"); |
959 |
} else { |
960 |
warn("reporter: DCC report via dccifd failed: $err\n"); |
961 |
} |
962 |
return 0; |
963 |
} |
964 |
|
965 |
return 1; |
966 |
} |
967 |
|
968 |
sub dcc_report { |
969 |
my ($self, $options, $tmpf, $client) = @_; |
970 |
my $conf = $self->{main}->{conf}; |
971 |
my $timeout = $options->{report}->{conf}->{dcc_timeout}; |
972 |
|
973 |
# note: not really tainted, this came from system configuration file |
974 |
my $path = untaint_file_path($options->{report}->{conf}->{dcc_path}); |
975 |
my $opts = $conf->{dcc_options}; |
976 |
my @opts = !defined $opts ? () : split(' ',$opts); |
977 |
untaint_var(\@opts); |
978 |
|
979 |
# get the metadata from the message so we can pass the external relay info |
980 |
|
981 |
unshift(@opts, "-a", |
982 |
untaint_var($client)) if defined $client && $client ne ''; |
983 |
|
984 |
my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout }); |
985 |
|
986 |
$options->{report}->enter_helper_run_mode(); |
987 |
my $err = $timer->run_and_catch(sub { |
988 |
|
989 |
local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; |
990 |
|
991 |
dbg("report: opening pipe: %s", |
992 |
join(' ', $path, "-H", "-t", "many", "-x", "0", @opts, "< $tmpf")); |
993 |
|
994 |
my $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC, |
995 |
$tmpf, 1, $path, "-H", "-t", "many", "-x", "0", @opts); |
996 |
$pid or die "$!\n"; |
997 |
|
998 |
my($inbuf,$nread,$nread_all); $nread_all = 0; |
999 |
# response is ignored, just check its existence |
1000 |
while ( $nread=read(DCC,$inbuf,8192) ) { $nread_all += $nread } |
1001 |
defined $nread or die "error reading from pipe: $!"; |
1002 |
|
1003 |
dbg("dcc: empty response") if $nread_all < 1; |
1004 |
|
1005 |
my $errno = 0; close DCC or $errno = $!; |
1006 |
# closing a pipe also waits for the process executing on the pipe to |
1007 |
# complete, no need to explicitly call waitpid |
1008 |
# my $child_stat = waitpid($pid,0) > 0 ? $? : undef; |
1009 |
proc_status_ok($?,$errno) |
1010 |
or die "dcc: reporter error: ".exit_status_str($?,$errno)."\n"; |
1011 |
}); |
1012 |
$options->{report}->leave_helper_run_mode(); |
1013 |
|
1014 |
if ($timer->timed_out()) { |
1015 |
dbg("reporter: DCC report via dccproc timed out after $timeout seconds"); |
1016 |
return 0; |
1017 |
} |
1018 |
|
1019 |
if ($err) { |
1020 |
chomp $err; |
1021 |
if ($err eq "__brokenpipe__ignore__") { |
1022 |
dbg("reporter: DCC report via dccproc failed: broken pipe"); |
1023 |
} else { |
1024 |
warn("reporter: DCC report via dccproc failed: $err\n"); |
1025 |
} |
1026 |
return 0; |
1027 |
} |
1028 |
|
1029 |
return 1; |
1030 |
} |
1031 |
|
1032 |
1; |
1086 |
1; |
1033 |
|
|
|
1034 |
=back |
1035 |
|
1036 |
=cut |