File Coverage

blib/lib/Mail/SpamAssassin/Dns.pm
Criterion Covered Total %
statement 136 343 39.6
branch 27 150 18.0
condition 7 50 14.0
subroutine 27 42 64.2
pod 0 26 0.0
total 197 611 32.2


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 41     41   236 use strict; # make Test::Perl::Critic happy
  41         65  
  41         1724  
19              
20              
21             use strict;
22             use warnings;
23 41     41   230 # use bytes;
  41         68  
  41         837  
24 41     41   181 use re 'taint';
  41         70  
  41         1262  
25              
26 41     41   226 use Mail::SpamAssassin::Conf;
  41         72  
  41         1274  
27             use Mail::SpamAssassin::PerMsgStatus;
28 41     41   220 use Mail::SpamAssassin::AsyncLoop;
  41         93  
  41         949  
29 41     41   209 use Mail::SpamAssassin::Constants qw(:ip);
  41         68  
  41         807  
30 41     41   196 use Mail::SpamAssassin::Util qw(untaint_var am_running_on_windows);
  41         65  
  41         995  
31 41     41   191  
  41         72  
  41         4515  
32 41     41   275 use File::Spec;
  41         75  
  41         1825  
33             use IO::Socket;
34 41     41   242 use POSIX ":sys_wait_h";
  41         73  
  41         991  
35 41     41   2741  
  41         47180  
  41         459  
36 41     41   22773  
  41         94  
  41         352  
37             our $KNOWN_BAD_DIALUP_RANGES; # Nothing uses this var???
38             our $LAST_DNS_CHECK;
39              
40             # use very well-connected domains (fast DNS response, many DNS servers,
41             # geographical distribution is a plus, TTL of at least 3600s)
42             our @EXISTING_DOMAINS = qw{
43             adelphia.net
44             akamai.com
45             apache.org
46             cingular.com
47             colorado.edu
48             comcast.net
49             doubleclick.com
50             ebay.com
51             gmx.net
52             google.com
53             intel.com
54             kernel.org
55             linux.org
56             mit.edu
57             motorola.com
58             msn.com
59             sourceforge.net
60             sun.com
61             w3.org
62             yahoo.com
63             };
64              
65             our $IS_DNS_AVAILABLE = undef;
66              
67             #Removed $VERSION per BUG 6422
68             #$VERSION = 'bogus'; # avoid CPAN.pm picking up razor ver
69              
70             ###########################################################################
71              
72             BEGIN {
73             # some trickery. Load these modules right here, if possible; that way, if
74             # the module exists, we'll get it loaded now. Very useful to avoid attempted
75             # loads later (which will happen). If we do a fork(), we could wind up
76             # attempting to load these modules in *every* subprocess.
77             #
78             # # We turn off strict and warnings, because Net::DNS and Razor both contain
79             # # crud that -w complains about (perl 5.6.0). Not that this seems to work,
80             # # mind ;)
81             # no strict;
82             # local ($^W) = 0;
83              
84             no warnings;
85             eval {
86 41     41   8632 require Net::DNS;
  41         68  
  41         3089  
87 41     41   113 require Net::DNS::Resolver;
88 41         17173 };
89 41         2000159 eval {
90             require MIME::Base64;
91 41         354 };
92 41         383 eval {
93             require IO::Socket::UNIX;
94 41         317 };
95 41         176702 };
96              
97             ###########################################################################
98              
99             my ($self, $rule, $set, $type, $host, $subtest) = @_;
100              
101             $host =~ s/\.\z//s; # strip a redundant trailing dot
102 0     0 0 0 my $key = "dns:$type:$host";
103             my $existing_ent = $self->{async}->get_lookup($key);
104 0         0  
105 0         0 # only make a specific query once
106 0         0 if (!$existing_ent) {
107             my $ent = {
108             key => $key,
109 0 0       0 zone => $host, # serves to fetch other per-zone settings
110 0         0 type => "DNSBL-".$type,
111             sets => [ ], # filled in below
112             rules => [ ], # filled in below
113             # id is filled in after we send the query below
114             };
115             $existing_ent = $self->{async}->bgsend_and_start_lookup(
116             $host, $type, undef, $ent,
117             sub { my($ent, $pkt) = @_; $self->process_dnsbl_result($ent, $pkt) },
118             master_deadline => $self->{master_deadline} );
119             }
120 0     0   0  
  0         0  
121 0         0 if ($existing_ent) {
122             # always add set
123             push @{$existing_ent->{sets}}, $set;
124 0 0       0  
125             # sometimes match or always match
126 0         0 if (defined $subtest) {
  0         0  
127             $self->{dnspost}->{$set}->{$subtest} = $rule;
128             } else {
129 0 0       0 push @{$existing_ent->{rules}}, $rule;
130 0         0 }
131              
132 0         0 $self->{rule_to_rblkey}->{$rule} = $key;
  0         0  
133             }
134             }
135 0         0  
136             # TODO: these are constant so they should only be added once at startup
137             my ($self, $rule, $set, $subtest) = @_;
138              
139             if ($subtest =~ /^sb:/) {
140             warn("dns: ignored $rule, SenderBase rules are deprecated\n");
141 0     0 0 0 return 0;
142             }
143 0 0       0  
144 0         0 $self->{dnspost}->{$set}->{$subtest} = $rule;
145 0         0 }
146              
147             my ($self, $rule, $type, $host) = @_;
148 0         0  
149             $host =~ s/\.\z//s; # strip a redundant trailing dot
150             my $key = "dns:$type:$host";
151              
152 0     0 0 0 my $ent = {
153             key => $key,
154 0         0 zone => $host, # serves to fetch other per-zone settings
155 0         0 type => "DNSBL-".$type,
156             rules => [ $rule ],
157 0         0 # id is filled in after we send the query below
158             };
159             $ent = $self->{async}->bgsend_and_start_lookup(
160             $host, $type, undef, $ent,
161             sub { my($ent, $pkt) = @_; $self->process_dnsbl_result($ent, $pkt) },
162             master_deadline => $self->{master_deadline} );
163             $ent;
164             }
165              
166 0     0   0 ###########################################################################
  0         0  
167 0         0  
168 0         0 my ($self, $rule, $question, $answer) = @_;
169              
170             my $log = "";
171             if (substr($rule, 0, 2) eq "__") {
172             # don't bother with meta rules
173             } elsif ($answer->type eq 'TXT') {
174 0     0 0 0 # txtdata returns a non- zone-file-format encoded result, unlike rdstring;
175             # avoid space-separated RDATA <character-string> fields if possible,
176 0         0 # txtdata provides a list of strings in a list context since Net::DNS 0.69
177 0 0       0 $log = join('',$answer->txtdata);
    0          
178             local $1;
179             $log =~ s{ (?<! [<(\[] ) (https? : // \S+)}{<$1>}xgi;
180             } else { # assuming $answer->type eq 'A'
181             local($1,$2,$3,$4,$5);
182             if ($question->string =~ m/^((?:[0-9a-fA-F]\.){32})(\S+\w)/) {
183 0         0 $log = ' listed in ' . lc($2);
184 0         0 my $ipv6addr = join('', reverse split(/\./, lc $1));
185 0         0 $ipv6addr =~ s/\G(....)/$1:/g; chop $ipv6addr;
186             $ipv6addr =~ s/:0{1,3}/:/g;
187 0         0 $log = $ipv6addr . $log;
188 0 0       0 } elsif ($question->string =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)\.(\S+\w)/) {
    0          
189 0         0 $log = "$4.$3.$2.$1 listed in " . lc($5);
190 0         0 } else {
191 0         0 $log = 'listed in ' . $question->string;
  0         0  
192 0         0 }
193 0         0 }
194              
195 0         0 # TODO: this may result in some log messages appearing under the
196             # wrong rules, since we could see this sequence: { test one hits,
197 0         0 # test one's message is logged, test two hits, test one fires again
198             # on another IP, test one's message is logged for that other IP --
199             # but under test two's heading }. Right now though it's better
200             # than just not logging at all.
201              
202             $self->{already_logged} ||= { };
203             if ($log && !$self->{already_logged}->{$log}) {
204             $self->test_log($log);
205             $self->{already_logged}->{$log} = 1;
206             }
207              
208 0   0     0 if (!$self->{tests_already_hit}->{$rule}) {
209 0 0 0     0 $self->got_hit($rule, "RBL: ", ruletype => "dnsbl");
210 0         0 }
211 0         0 }
212              
213             my ($self, $question, $answer) = @_;
214 0 0       0  
215 0         0 my $qname = $question->qname;
216              
217             # txtdata returns a non- zone-file-format encoded result, unlike rdstring;
218             # avoid space-separated RDATA <character-string> fields if possible,
219             # txtdata provides a list of strings in a list context since Net::DNS 0.69
220 0     0 0 0 #
221             # rdatastr() is historical/undocumented, use rdstring() since Net::DNS 0.69
222 0         0 my $rdatastr = $answer->UNIVERSAL::can('txtdata') ? join('',$answer->txtdata)
223             : $answer->UNIVERSAL::can('rdstring') ? $answer->rdstring
224             : $answer->rdatastr;
225             if (defined $qname && defined $rdatastr) {
226             my $qclass = $question->qclass;
227             my $qtype = $question->qtype;
228             my @vals;
229 0 0       0 push(@vals, "class=$qclass") if $qclass ne "IN";
    0          
230             push(@vals, "type=$qtype") if $qtype ne "A";
231             my $uri = "dns:$qname" . (@vals ? "?" . join(";", @vals) : "");
232 0 0 0     0 push @{ $self->{dnsuri}->{$uri} }, $rdatastr;
233 0         0  
234 0         0 dbg("dns: hit <$uri> $rdatastr");
235 0         0 }
236 0 0       0 }
237 0 0       0  
238 0 0       0 # called as a completion routine to bgsend by DnsResolver::poll_responses;
239 0         0 # returns 1 on successful packet processing
  0         0  
240             my ($self, $ent, $pkt) = @_;
241 0         0  
242             return if !$pkt;
243             my $question = ($pkt->question)[0];
244             return if !$question;
245              
246             my $sets = $ent->{sets} || [];
247             my $rules = $ent->{rules};
248 0     0 0 0  
249             # NO_DNS_FOR_FROM
250 0 0       0 if ($self->{sender_host} &&
251 0         0 # fishy, qname should have been "RFC 1035 zone format" -decoded first
252 0 0       0 lc($question->qname) eq lc($self->{sender_host}) &&
253             $question->qtype =~ /^(?:A|MX)$/ &&
254 0   0     0 $pkt->header->rcode =~ /^(?:NXDOMAIN|SERVFAIL)$/ &&
255 0         0 ++$self->{sender_host_fail} == 2)
256             {
257             for my $rule (@{$rules}) {
258 0 0 0     0 $self->got_hit($rule, "DNS: ", ruletype => "dns");
      0        
      0        
      0        
259             }
260             }
261              
262             # DNSBL tests are here
263             foreach my $answer ($pkt->answer) {
264             next if !$answer;
265 0         0 # track all responses
  0         0  
266 0         0 $self->dnsbl_uri($question, $answer);
267             my $answ_type = $answer->type;
268             # TODO: there are some CNAME returns that might be useful
269             next if ($answ_type ne 'A' && $answ_type ne 'TXT');
270             if ($answ_type eq 'A') {
271 0         0 # Net::DNS::RR::A::address() is available since Net::DNS 0.69
272 0 0       0 my $ip_address = $answer->UNIVERSAL::can('address') ? $answer->address
273             : $answer->rdatastr;
274 0         0 # skip any A record that isn't on 127.0.0.0/8
275 0         0 next if $ip_address !~ /^127\./;
276             }
277 0 0 0     0 for my $rule (@{$rules}) {
278 0 0       0 $self->dnsbl_hit($rule, $question, $answer);
279             }
280 0 0       0 for my $set (@{$sets}) {
281             if ($self->{dnspost}->{$set}) {
282             $self->process_dnsbl_set($set, $question, $answer);
283 0 0       0 }
284             }
285 0         0 }
  0         0  
286 0         0 return 1;
287             }
288 0         0  
  0         0  
289 0 0       0 my ($self, $set, $question, $answer) = @_;
290 0         0  
291             # txtdata returns a non- zone-file-format encoded result, unlike rdstring;
292             # avoid space-separated RDATA <character-string> fields if possible,
293             # txtdata provides a list of strings in a list context since Net::DNS 0.69
294 0         0 #
295             # rdatastr() is historical/undocumented, use rdstring() since Net::DNS 0.69
296             my $rdatastr = $answer->UNIVERSAL::can('txtdata') ? join('',$answer->txtdata)
297             : $answer->UNIVERSAL::can('rdstring') ? $answer->rdstring
298 0     0 0 0 : $answer->rdatastr;
299              
300             while (my ($subtest, $rule) = each %{ $self->{dnspost}->{$set} }) {
301             next if $self->{tests_already_hit}->{$rule};
302              
303             if ($subtest =~ /^\d+\.\d+\.\d+\.\d+$/) {
304             # test for exact equality, not a regexp (an IPv4 address)
305 0 0       0 $self->dnsbl_hit($rule, $question, $answer) if $subtest eq $rdatastr;
    0          
306             }
307             # bitmask
308             elsif ($subtest =~ /^\d+$/) {
309 0         0 # Bug 6803: response should be within 127.0.0.0/8, ignore otherwise
  0         0  
310 0 0       0 if ($rdatastr =~ m/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ &&
311             Mail::SpamAssassin::Util::my_inet_aton($rdatastr) & $subtest)
312 0 0       0 {
    0          
313             $self->dnsbl_hit($rule, $question, $answer);
314 0 0       0 }
315             }
316             # regular expression
317             else {
318             my $test = qr/$subtest/;
319 0 0 0     0 if ($rdatastr =~ /$test/) {
320             $self->dnsbl_hit($rule, $question, $answer);
321             }
322 0         0 }
323             }
324             }
325              
326             my ($self, $rule) = @_;
327 0         0  
328 0 0       0 dbg("dns: harvest_until_rule_completes");
329 0         0 my $result = 0;
330              
331             for (my $first=1; ; $first=0) {
332             # complete_lookups() may call completed_callback(), which may
333             # call start_lookup() again (like in Plugin::URIDNSBL)
334             my ($alldone,$anydone) =
335             $self->{async}->complete_lookups($first ? 0 : 1.0, 1);
336 0     0 0 0  
337             $result = 1 if $self->is_rule_complete($rule);
338 0         0 last if $result || $alldone;
339 0         0  
340             dbg("dns: harvest_until_rule_completes - check_tick");
341 0         0 $self->{main}->call_plugins ("check_tick", { permsgstatus => $self });
342             }
343              
344             return $result;
345 0 0       0 }
346              
347 0 0       0 my ($self) = @_;
348 0 0 0     0  
349             dbg("dns: harvest_dnsbl_queries");
350 0         0  
351 0         0 for (my $first=1; ; $first=0) {
352             # complete_lookups() may call completed_callback(), which may
353             # call start_lookup() again (like in Plugin::URIDNSBL)
354 0         0  
355             # the first time around we specify a 0 timeout, which gives
356             # complete_lookups a chance to ripe any available results and
357             # abort overdue requests, without needlessly waiting for more
358 96     96 0 218  
359             my ($alldone,$anydone) =
360 96         266 $self->{async}->complete_lookups($first ? 0 : 1.0, 1);
361              
362 96         189 last if $alldone;
363              
364             dbg("dns: harvest_dnsbl_queries - check_tick");
365             $self->{main}->call_plugins ("check_tick", { permsgstatus => $self });
366             }
367              
368             # explicitly abort anything left
369             $self->{async}->abort_remaining_lookups();
370             $self->{async}->log_lookups_timing();
371 96 50       414 $self->mark_all_async_rules_complete();
372             1;
373 96 50       346 }
374              
375 0         0 # collect and process whatever DNS responses have already arrived,
376 0         0 # don't waste time waiting for more, don't poll too often.
377             # don't abort any queries even if overdue,
378             my ($self) = @_;
379              
380 96         471 # don't bother collecting responses too often
381 96         388 my $last_poll_time = $self->{async}->last_poll_responses_time();
382 96         427 return if defined $last_poll_time && time - $last_poll_time < 0.1;
383 96         230  
384             my ($alldone,$anydone) = $self->{async}->complete_lookups(0, 0);
385             if ($anydone) {
386             dbg("dns: harvested completed queries");
387             # $self->{main}->call_plugins ("check_tick", { permsgstatus => $self });
388             }
389             }
390 3096     3096 0 4323  
391             my ($self) = @_;
392              
393 3096         8058 # DNS URIs
394 3096 100 66     5826 my $rbl_tag = $self->{tag_data}->{RBL}; # just in case, should be empty
395             $rbl_tag = '' if !defined $rbl_tag;
396 3063         6292 while (my ($dnsuri, $answers) = each %{ $self->{dnsuri} }) {
397 3063 100       7228 # when parsing, look for elements of \".*?\" or \S+ with ", " as separator
398 3         8 $rbl_tag .= "<$dnsuri>" . " [" . join(", ", @{ $answers }) . "]\n";
399             }
400             if (defined $rbl_tag && $rbl_tag ne '') {
401             chomp $rbl_tag;
402             $self->set_tag('RBL', $rbl_tag);
403             }
404 96     96 0 185 }
405              
406             ###########################################################################
407 96         238  
408 96 50       252 my ($self) = @_;
409 96         186  
  96         572  
410             $self->set_rbl_tag_data();
411 0         0  
  0         0  
412             delete $self->{dnspost};
413 96 50 33     553 delete $self->{dnsuri};
414 0         0 }
415 0         0  
416             ###########################################################################
417              
418             my ($self) = @_;
419             $self->{resolver} = $self->{main}->{resolver};
420             return $self->{resolver}->load_resolver();
421             }
422 96     96 0 192  
423             my ($self) = @_;
424 96         303 dbg("dns: clear_resolver");
425             $self->{main}->{resolver}->{res} = undef;
426 96         182 return 0;
427 96         252 }
428              
429             my ($self, $dom) = @_;
430              
431             return unless $self->load_resolver();
432             return if ($self->server_failed_to_respond_for_domain ($dom));
433 1     1 0 2  
434 1         2 my $nsrecords;
435 1         12 dbg("dns: looking up NS for '$dom'");
436              
437             eval {
438             my $query = $self->{resolver}->send($dom, 'NS');
439 1     1 0 2 my @nses;
440 1         4 if ($query) {
441 1         13 foreach my $rr ($query->answer) {
442 1         2 if ($rr->type eq "NS") { push (@nses, $rr->nsdname); }
443             }
444             }
445             $nsrecords = [ @nses ];
446 0     0 0 0 1;
447             } or do {
448 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
449 0 0       0 dbg("dns: NS lookup failed horribly, perhaps bad resolv.conf setting? (%s)", $eval_stat);
450             return;
451 0         0 };
452 0         0  
453             $nsrecords;
454             }
455 0         0  
456 0         0 my ($self) = @_;
457 0 0       0 my $dnsopt = $self->{conf}->{dns_available};
458 0         0 my $dnsint = $self->{conf}->{dns_test_interval} || 600;
459 0 0       0 my @domains;
  0         0  
460              
461             $LAST_DNS_CHECK ||= 0;
462 0         0 my $diff = time() - $LAST_DNS_CHECK;
463 0         0  
464 0 0       0 # undef $IS_DNS_AVAILABLE if we should be testing for
465 0 0       0 # working DNS and our check interval time has passed
  0         0  
466 0         0 if ($dnsopt eq "test" && $diff > $dnsint) {
467 0         0 $IS_DNS_AVAILABLE = undef;
468             dbg("dns: is_dns_available() last checked %.1f seconds ago; re-checking",
469             $diff);
470 0         0 }
471              
472             return $IS_DNS_AVAILABLE if (defined $IS_DNS_AVAILABLE);
473             $LAST_DNS_CHECK = time();
474 336     336 0 574  
475 336         608 $IS_DNS_AVAILABLE = 0;
476 336   50     783 if ($dnsopt eq "no") {
477 336         410 dbg("dns: dns_available set to no in config file, skipping test");
478             return $IS_DNS_AVAILABLE;
479 336   100     683 }
480 336         787  
481             # Even if "dns_available" is explicitly set to "yes", we want to ignore
482             # DNS if we're only supposed to be looking at local tests.
483             goto done if ($self->{main}->{local_tests_only});
484 336 50 33     715  
485 0         0 # Check version numbers - runtime check only
486 0         0 if (defined $Net::DNS::VERSION) {
487             if (am_running_on_windows()) {
488             if ($Net::DNS::VERSION < 0.46) {
489             warn("dns: Net::DNS version is $Net::DNS::VERSION, but need 0.46 for Win32");
490 336 100       1594 return $IS_DNS_AVAILABLE;
491 8         25 }
492             }
493 8         16 else {
494 8 50       26 if ($Net::DNS::VERSION < 0.34) {
495 0         0 warn("dns: Net::DNS version is $Net::DNS::VERSION, but need 0.34");
496 0         0 return $IS_DNS_AVAILABLE;
497             }
498             }
499             }
500              
501 8 100       52 $self->clear_resolver();
502             goto done unless $self->load_resolver();
503              
504 1 50       3 if ($dnsopt eq "yes") {
505 1 50       5 # optionally shuffle the list of nameservers to distribute the load
506 0 0       0 if ($self->{conf}->{dns_options}->{rotate}) {
507 0         0 my @nameservers = $self->{resolver}->available_nameservers();
508 0         0 Mail::SpamAssassin::Util::fisher_yates_shuffle(\@nameservers);
509             dbg("dns: shuffled NS list: " . join(", ", @nameservers));
510             $self->{resolver}->available_nameservers(@nameservers);
511             }
512 1 50       4 $IS_DNS_AVAILABLE = 1;
513 0         0 dbg("dns: dns_available set to yes in config file, skipping test");
514 0         0 return $IS_DNS_AVAILABLE;
515             }
516              
517             if ($dnsopt =~ /^test:\s*(\S.*)$/) {
518             @domains = split (/\s+/, $1);
519 1         4 dbg("dns: looking up NS records for user specified domains: %s",
520 1 50       3 join(", ", @domains));
521             } else {
522 1 50       4 @domains = @EXISTING_DOMAINS;
523             dbg("dns: looking up NS records for built-in domains");
524 1 50       3 }
525 0         0  
526 0         0 # do the test with a full set of configured nameservers
527 0         0 my @nameservers = $self->{resolver}->configured_nameservers();
528 0         0  
529             # optionally shuffle the list of nameservers to distribute the load
530 1         3 if ($self->{conf}->{dns_options}->{rotate}) {
531 1         3 Mail::SpamAssassin::Util::fisher_yates_shuffle(\@nameservers);
532 1         13 dbg("dns: shuffled NS list, testing: " . join(", ", @nameservers));
533             } else {
534             dbg("dns: testing resolver nameservers: " . join(", ", @nameservers));
535 0 0       0 }
536 0         0  
537 0         0 # Try the different nameservers here and collect a list of working servers
538             my @good_nameservers;
539             foreach my $ns (@nameservers) {
540 0         0 $self->{resolver}->available_nameservers($ns); # try just this one
541 0         0 for (my $retry = 3; $retry > 0 && @domains; $retry--) {
542             my $domain = splice(@domains, rand(@domains), 1);
543             dbg("dns: trying ($retry) $domain, server $ns ...");
544             my $result = $self->lookup_ns($domain);
545 0         0 $self->{resolver}->finish_socket();
546             if (!$result) {
547             dbg("dns: NS lookup of $domain using $ns failed horribly, ".
548 0 0       0 "may not be a valid nameserver");
549 0         0 last;
550 0         0 } elsif (!@$result) {
551             dbg("dns: NS lookup of $domain using $ns failed, no results found");
552 0         0 } else {
553             dbg("dns: NS lookup of $domain using $ns succeeded => DNS available".
554             " (set dns_available to override)");
555             push(@good_nameservers, $ns);
556 0         0 last;
557 0         0 }
558 0         0 }
559 0   0     0 }
560 0         0  
561 0         0 if (!@good_nameservers) {
562 0         0 dbg("dns: all NS queries failed => DNS unavailable ".
563 0         0 "(set dns_available to override)");
564 0 0       0 } else {
    0          
565 0         0 $IS_DNS_AVAILABLE = 1;
566             dbg("dns: NS list: ".join(", ", @good_nameservers));
567 0         0 $self->{resolver}->available_nameservers(@good_nameservers);
568             }
569 0         0  
570             done:
571 0         0 # jm: leaving this in!
572             dbg("dns: is DNS available? " . $IS_DNS_AVAILABLE);
573 0         0 return $IS_DNS_AVAILABLE;
574 0         0 }
575              
576             ###########################################################################
577              
578             my ($self, $dom) = @_;
579 0 0       0 if ($self->{dns_server_too_slow}->{$dom}) {
580 0         0 dbg("dns: server for '$dom' failed to reply previously, not asking again");
581             return 1;
582             }
583 0         0 return 0;
584 0         0 }
585 0         0  
586             my ($self, $dom) = @_;
587             dbg("dns: server for '$dom' failed to reply, marking as bad");
588 7         38 $self->{dns_server_too_slow}->{$dom} = 1;
589             }
590              
591 7         43 ###########################################################################
592              
593             my ($self) = @_;
594              
595             dbg("dns: entering helper-app run mode");
596             $self->{old_slash} = $/; # Razor pollutes this
597 0     0 0 0 %{$self->{old_env}} = ();
598 0 0       0 if ( %ENV ) {
599 0         0 # undefined values in %ENV can result due to autovivification elsewhere,
600 0         0 # this prevents later possible warnings when we restore %ENV
601             while (my ($key, $value) = each %ENV) {
602 0         0 $self->{old_env}->{$key} = $value if defined $value;
603             }
604             }
605              
606 0     0 0 0 Mail::SpamAssassin::Util::clean_path_in_taint_mode();
607 0         0  
608 0         0 my $newhome;
609             if ($self->{main}->{home_dir_for_helpers}) {
610             $newhome = $self->{main}->{home_dir_for_helpers};
611             } else {
612             # use spamd -u user's home dir
613             $newhome = (Mail::SpamAssassin::Util::portable_getpwuid ($>))[7];
614 4     4 0 7 }
615              
616 4         9 if ($newhome) {
617 4         16 $ENV{'HOME'} = Mail::SpamAssassin::Util::untaint_file_path ($newhome);
618 4         5 }
  4         11  
619 4 50       12  
620             # enforce SIGCHLD as DEFAULT; IGNORE causes spurious kernel warnings
621             # on Red Hat NPTL kernels (bug 1536), and some users of the
622 4         23 # Mail::SpamAssassin modules set SIGCHLD to be a fatal signal
623 136 50       554 # for some reason! (bug 3507)
624             $self->{old_sigchld_handler} = $SIG{CHLD};
625             $SIG{CHLD} = 'DEFAULT';
626             }
627 4         15  
628             my ($self) = @_;
629 4         5  
630 4 50       11 dbg("dns: leaving helper-app run mode");
631 0         0 $/ = $self->{old_slash};
632             %ENV = %{$self->{old_env}};
633              
634 4         14 if (defined $self->{old_sigchld_handler}) {
635             $SIG{CHLD} = $self->{old_sigchld_handler};
636             } else {
637 4 50       24 # if SIGCHLD has never been explicitly set, it's returned as undef.
638 4         13 # however, when *setting* SIGCHLD, using undef(%) or assigning to an
639             # undef value produces annoying 'Use of uninitialized value in scalar
640             # assignment' warnings. That's silly. workaround:
641             $SIG{CHLD} = 'DEFAULT';
642             }
643             }
644              
645 4         20 # note: this must be called before leave_helper_run_mode() is called,
646 4         60 # as the SIGCHLD signal must be set to DEFAULT for it to work.
647             my ($self, $pid) = @_;
648            
649             if ($SIG{CHLD} && $SIG{CHLD} ne 'IGNORE') { # running from spamd
650 4     4 0 9 waitpid ($pid, 0);
651             }
652 4         13 }
653 4         13  
654 4         6 ###########################################################################
  4         330  
655              
656 4 100       24 my ($self, $rule) = @_;
657 3         40 dbg("dns: $rule lookup start");
658             $self->{rule_to_rblkey}->{$rule} = '*ASYNC_START';
659             }
660              
661             my ($self, $rule) = @_;
662             dbg("dns: $rule lookup finished");
663 1         13 delete $self->{rule_to_rblkey}->{$rule};
664             }
665              
666             my ($self) = @_;
667             $self->{rule_to_rblkey} = { };
668             }
669              
670 0     0 0 0 my ($self, $rule) = @_;
671              
672 0 0 0     0 my $key = $self->{rule_to_rblkey}->{$rule};
673 0         0 if (!defined $key) {
674             # dbg("dns: $rule lookup complete, not in list");
675             return 1;
676             }
677              
678             if ($key eq '*ASYNC_START') {
679             dbg("dns: $rule lookup not yet complete");
680 135     135 0 202 return 0; # not yet complete
681 135         334 }
682 135         363  
683             my $ent = $self->{async}->get_lookup($key);
684             if (!defined $ent) {
685             dbg("dns: $rule lookup complete, $key no longer pending");
686 42     42 0 64 return 1;
687 42         121 }
688 42         260  
689             dbg("dns: $rule lookup not yet complete");
690             return 0; # not yet complete
691             }
692 96     96 0 203  
693 96         320 ###########################################################################
694              
695             # interface called by SPF plugin
696             my ($self, $pms) = @_;
697 0     0 0 0 if (defined $pms->{sender_host_fail}) {
698             return ($pms->{sender_host_fail} == 2); # both MX and A need to fail
699 0         0 }
700 0 0       0 }
701              
702 0         0 1;