View | Details | Raw Unified | Return to bug 6698
Collapse All | Expand All

(-)lib/Mail/SpamAssassin/Plugin/DCC.pm (-567 / +617 lines)
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

Return to bug 6698