File Coverage

blib/lib/Mail/SpamAssassin/Plugin/DCC.pm
Criterion Covered Total %
statement 185 487 37.9
branch 53 286 18.5
condition 15 104 14.4
subroutine 23 34 67.6
pod 3 17 17.6
total 279 928 30.0


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