File Coverage

blib/lib/Mail/SpamAssassin/Dns.pm
Criterion Covered Total %
statement 112 353 31.7
branch 22 156 14.1
condition 7 53 13.2
subroutine 24 42 57.1
pod 0 26 0.0
total 165 630 26.1


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