37
37
use Mail::SpamAssassin::Plugin;
38
38
use Mail::SpamAssassin::Logger;
39
39
use Mail::SpamAssassin::Timeout;
40
use Mail::SpamAssassin::Util qw(untaint_var untaint_file_path
41
proc_status_ok exit_status_str);
45
48
@ISA = qw(Mail::SpamAssassin::Plugin);
153
156
setting => 'pyzor_options',
159
type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
157
161
my ($self, $key, $value, $line) = @_;
158
162
if ($value !~ m{^([0-9A-Za-z ,._/-]+)$}) {
175
179
setting => 'pyzor_path',
177
181
default => undef,
182
type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING,
179
184
my ($self, $key, $value, $line) = @_;
180
185
if (!defined $value || !length $value) {
181
186
return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
183
$value = Mail::SpamAssassin::Util::untaint_file_path($value);
188
$value = untaint_file_path($value);
184
189
if (!-x $value) {
185
190
info("config: pyzor_path \"$value\" isn't an executable");
186
191
return $Mail::SpamAssassin::Conf::INVALID_VALUE;
257
264
my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext);
259
266
# note: not really tainted, this came from system configuration file
260
my $path = Mail::SpamAssassin::Util::untaint_file_path($self->{main}->{conf}->{pyzor_path});
262
my $opts = $self->{main}->{conf}->{pyzor_options} || '';
267
my $path = untaint_file_path($self->{main}->{conf}->{pyzor_path});
268
my $opts = untaint_var($self->{main}->{conf}->{pyzor_options}) || '';
264
270
$permsgstatus->enter_helper_run_mode();
266
my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
272
my $timer = Mail::SpamAssassin::Timeout->new(
273
{ secs => $timeout, deadline => $permsgstatus->{master_deadline} });
267
274
my $err = $timer->run_and_catch(sub {
269
276
local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
274
281
$tmpf, 1, $path, split(' ', $opts), "check");
275
282
$pid or die "$!\n";
279
or dbg(sprintf("pyzor: [%s] finished: %s exit=0x%04x",$pid,$!,$?));
284
# read+split avoids a Perl I/O bug (Bug 5985)
285
my($inbuf,$nread,$resp); $resp = '';
286
while ( $nread=read(PYZOR,$inbuf,8192) ) { $resp .= $inbuf }
287
defined $nread or die "error reading from pipe: $!";
288
@response = split(/^/m, $resp, -1); undef $resp;
290
my $errno = 0; close PYZOR or $errno = $!;
291
if (proc_status_ok($?,$errno)) {
292
dbg("pyzor: [%s] finished successfully", $pid);
293
} elsif (proc_status_ok($?,$errno, 0,1)) { # sometimes it exits with 1
294
dbg("pyzor: [%s] finished: %s", $pid, exit_status_str($?,$errno));
296
info("pyzor: [%s] error: %s", $pid, exit_status_str($?,$errno));
281
299
if (!@response) {
282
300
# this exact string is needed below
283
301
die("no response\n"); # yes, this is possible
285
map { chomp } @response;
286
304
dbg("pyzor: got response: " . join("\\n", @response));
288
306
if ($response[0] =~ /^Traceback/) {
289
# this exact string is needed below
290
die("internal error\n");
307
die("internal error, python traceback seen in response\n");
297
314
if (kill('TERM',$pid)) { dbg("pyzor: killed stale helper [$pid]") }
298
315
else { dbg("pyzor: killing helper application [$pid] failed: $!") }
301
or dbg(sprintf("pyzor: [%s] terminated: %s exit=0x%04x",$pid,$!,$?));
317
my $errno = 0; close PYZOR or $errno = $!;
318
proc_status_ok($?,$errno)
319
or info("pyzor: [%s] error: %s", $pid, exit_status_str($?,$errno));
303
321
$permsgstatus->leave_helper_run_mode();
374
392
my ($self, $options, $tmpf) = @_;
376
394
# note: not really tainted, this came from system configuration file
377
my $path = Mail::SpamAssassin::Util::untaint_file_path($options->{report}->{conf}->{pyzor_path});
395
my $path = untaint_file_path($options->{report}->{conf}->{pyzor_path});
396
my $opts = untaint_var($options->{report}->{conf}->{pyzor_options}) || '';
379
my $opts = $options->{report}->{conf}->{pyzor_options} || '';
380
398
my $timeout = $self->{main}->{conf}->{pyzor_timeout};
382
400
$options->{report}->enter_helper_run_mode();
392
410
$tmpf, 1, $path, split(' ', $opts), "report");
393
411
$pid or die "$!\n";
395
my @ignored = <PYZOR>;
396
$options->{report}->close_pipe_fh(\*PYZOR);
413
my($inbuf,$nread,$nread_all); $nread_all = 0;
414
# response is ignored, just check its existence
415
while ( $nread=read(PYZOR,$inbuf,8192) ) { $nread_all += $nread }
416
defined $nread or die "error reading from pipe: $!";
418
dbg("pyzor: empty response") if $nread_all < 1;
420
my $errno = 0; close PYZOR or $errno = $!;
421
# closing a pipe also waits for the process executing on the pipe to
422
# complete, no need to explicitly call waitpid
423
# my $child_stat = waitpid($pid,0) > 0 ? $? : undef;
424
if (proc_status_ok($?,$errno, 0)) {
425
dbg("pyzor: [%s] reporter finished successfully", $pid);
427
info("pyzor: [%s] reporter error: %s", $pid, exit_status_str($?,$errno));
401
432
$options->{report}->leave_helper_run_mode();