File Coverage

blib/lib/Mail/SpamAssassin/Plugin/URIDNSBL.pm
Criterion Covered Total %
statement 234 442 52.9
branch 79 188 42.0
condition 24 72 33.3
subroutine 20 39 51.2
pod 2 17 11.7
total 359 758 47.3


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             =head1 NAME
19              
20             URIDNSBL - look up URLs against DNS blocklists
21              
22             =head1 SYNOPSIS
23              
24             loadplugin Mail::SpamAssassin::Plugin::URIDNSBL
25             uridnsbl URIBL_SBLXBL sbl-xbl.spamhaus.org. TXT
26              
27             =head1 DESCRIPTION
28              
29             This works by analysing message text and HTML for URLs, extracting host
30             names from those, then querying various DNS blocklists for either:
31             IP addresses of these hosts (uridnsbl,a) or their nameservers (uridnsbl,ns),
32             or domain names of these hosts (urirhsbl), or domain names of their
33             nameservers (urinsrhsbl, urifullnsrhsbl).
34              
35             =head1 USER SETTINGS
36              
37             =over 4
38              
39             =item skip_uribl_checks ( 0 | 1 ) (default: 0)
40              
41             Turning on the skip_uribl_checks setting will disable the URIDNSBL plugin.
42              
43             By default, SpamAssassin will run URI DNSBL checks. Individual URI blocklists
44             may be disabled selectively by setting a score of a corresponding rule to 0
45             or through the uridnsbl_skip_domain parameter.
46              
47             See also a related configuration parameter skip_rbl_checks,
48             which controls the DNSEval plugin (documented in the Conf man page).
49              
50             =back
51              
52             =over 4
53              
54             =item uridnsbl_skip_domain domain1 domain2 ...
55              
56             Specify a domain, or a number of domains, which should be skipped for the
57             URIBL checks. This is very useful to specify very common domains which are
58             not going to be listed in URIBLs.
59              
60             =back
61              
62             =over 4
63              
64             =item clear_uridnsbl_skip_domain [domain1 domain2 ...]
65              
66             If no argument is given, then clears the entire list of domains declared
67             by I<uridnsbl_skip_domain> configuration directives so far. Any subsequent
68             I<uridnsbl_skip_domain> directives will start creating a new list of skip
69             domains.
70              
71             When given a list of domains as arguments, only the specified domains
72             are removed from the list of skipped domains.
73              
74             =back
75              
76             =head1 RULE DEFINITIONS AND PRIVILEGED SETTINGS
77              
78             =over 4
79              
80             =item uridnsbl NAME_OF_RULE dnsbl_zone lookuptype
81              
82             Specify a lookup. C<NAME_OF_RULE> is the name of the rule to be
83             used, C<dnsbl_zone> is the zone to look up IPs in, and C<lookuptype>
84             is the type of lookup (B<TXT> or B<A>). Note that you must also
85             define a body-eval rule calling C<check_uridnsbl()> to use this.
86              
87             This works by collecting domain names from URLs and querying DNS
88             blocklists with an IP address of host names found in URLs or with
89             IP addresses of their name servers, according to tflags as follows.
90              
91             If the corresponding body rule has a tflag 'a', the DNS blocklist will
92             be queried with an IP address of a host found in URLs.
93              
94             If the corresponding body rule has a tflag 'ns', DNS will be queried
95             for name servers (NS records) of a domain name found in URLs, then
96             these name server names will be resolved to their IP addresses, which
97             in turn will be sent to DNS blocklist.
98              
99             Tflags directive may specify either 'a' or 'ns' or both flags. In absence
100             of any of these two flags, a default is a 'ns', which is compatible with
101             pre-3.4 versions of SpamAssassin.
102              
103             The choice of tflags must correspond to the policy and expected use of
104             each DNS blocklist and is normally not a local decision. As an example,
105             a blocklist expecting queries resulting from an 'a' tflag is a
106             "black_a.txt" ( http://www.uribl.com/datasets.shtml ).
107              
108             Example:
109              
110             uridnsbl URIBL_SBLXBL sbl-xbl.spamhaus.org. TXT
111             body URIBL_SBLXBL eval:check_uridnsbl('URIBL_SBLXBL')
112             describe URIBL_SBLXBL Contains a URL listed in the SBL/XBL blocklist
113             tflags URIBL_SBLXBL net ns
114              
115             =item uridnssub NAME_OF_RULE dnsbl_zone lookuptype subtest
116              
117             Specify a DNSBL-style domain lookup with a sub-test. C<NAME_OF_RULE> is the
118             name of the rule to be used, C<dnsbl_zone> is the zone to look up IPs in,
119             and C<lookuptype> is the type of lookup (B<TXT> or B<A>).
120              
121             Tflags 'ns' and 'a' on a corresponding body rule are recognized and have
122             the same meaning as in the uridnsbl directive.
123              
124             C<subtest> is a sub-test to run against the returned data. The sub-test may
125             be in one of the following forms: m, n1-n2, or n/m, where n,n1,n2,m can be
126             any of: decimal digits, 0x followed by up to 8 hexadecimal digits, or an IPv4
127             address in quad-dot form. The 'A' records (IPv4 dotted address) as returned
128             by DNSBLs lookups are converted into a numerical form (r) and checked against
129             the specified sub-test as follows:
130             for a range n1-n2 the following must be true: (r >= n1 && r <= n2);
131             for a n/m form the following must be true: (r & m) == (n & m);
132             for a single value in quad-dot form the following must be true: r == n;
133             for a single decimal or hex form the following must be true:
134             ((r & n) != 0) && ((r & 0xff000000) == 0x7f000000), i.e. within 127.0.0.0/8
135              
136             Some typical examples of a sub-test are: 127.0.1.2, 127.0.1.20-127.0.1.39,
137             127.0.1.0/255.255.255.0, 0.0.0.16/0.0.0.16, 0x10/0x10, 16, 0x10 .
138              
139             Note that, as with C<uridnsbl>, you must also define a body-eval rule calling
140             C<check_uridnsbl()> to use this.
141              
142             Example:
143              
144             uridnssub URIBL_DNSBL_4 dnsbl.example.org. A 127.0.0.4
145             uridnssub URIBL_DNSBL_8 dnsbl.example.org. A 8
146              
147             =item urirhsbl NAME_OF_RULE rhsbl_zone lookuptype
148              
149             Specify a RHSBL-style domain lookup. C<NAME_OF_RULE> is the name of the rule
150             to be used, C<rhsbl_zone> is the zone to look up domain names in, and
151             C<lookuptype> is the type of lookup (B<TXT> or B<A>). Note that you must also
152             define a body-eval rule calling C<check_uridnsbl()> to use this.
153              
154             An RHSBL zone is one where the domain name is looked up, as a string; e.g. a
155             URI using the domain C<foo.com> will cause a lookup of
156             C<foo.com.uriblzone.net>. Note that hostnames are stripped from the domain
157             used in the URIBL lookup, so the domain C<foo.bar.com> will look up
158             C<bar.com.uriblzone.net>, and C<foo.bar.co.uk> will look up
159             C<bar.co.uk.uriblzone.net>.
160              
161             If an URI consists of an IP address instead of a hostname, the IP address is
162             looked up (using the standard reversed quads method) in each C<rhsbl_zone>.
163              
164             Example:
165              
166             urirhsbl URIBL_RHSBL rhsbl.example.org. TXT
167              
168             =item urirhssub NAME_OF_RULE rhsbl_zone lookuptype subtest
169              
170             Specify a RHSBL-style domain lookup with a sub-test. C<NAME_OF_RULE> is the
171             name of the rule to be used, C<rhsbl_zone> is the zone to look up domain names
172             in, and C<lookuptype> is the type of lookup (B<TXT> or B<A>).
173              
174             C<subtest> is a sub-test to run against the returned data. The sub-test may
175             be in one of the following forms: m, n1-n2, or n/m, where n,n1,n2,m can be
176             any of: decimal digits, 0x followed by up to 8 hexadecimal digits, or an IPv4
177             address in quad-dot form. The 'A' records (IPv4 dotted address) as returned
178             by DNSBLs lookups are converted into a numerical form (r) and checked against
179             the specified sub-test as follows:
180             for a range n1-n2 the following must be true: (r >= n1 && r <= n2);
181             for a n/m form the following must be true: (r & m) == (n & m);
182             for a single value in quad-dot form the following must be true: r == n;
183             for a single decimal or hex form the following must be true:
184             ((r & n) != 0) && ((r & 0xff000000) == 0x7f000000), i.e. within 127.0.0.0/8
185              
186             Some typical examples of a sub-test are: 127.0.1.2, 127.0.1.20-127.0.1.39,
187             127.2.3.0/255.255.255.0, 0.0.0.16/0.0.0.16, 0x10/0x10, 16, 0x10 .
188              
189             Note that, as with C<urirhsbl>, you must also define a body-eval rule calling
190             C<check_uridnsbl()> to use this.
191              
192             Example:
193              
194             urirhssub URIBL_RHSBL_4 rhsbl.example.org. A 127.0.0.4
195             urirhssub URIBL_RHSBL_8 rhsbl.example.org. A 8
196              
197             =item urinsrhsbl NAME_OF_RULE rhsbl_zone lookuptype
198              
199             Perform a RHSBL-style domain lookup against the contents of the NS records
200             for each URI. In other words, a URI using the domain C<foo.com> will cause
201             an NS lookup to take place; assuming that domain has an NS of C<ns0.bar.com>,
202             that will cause a lookup of C<bar.com.uriblzone.net>. Note that hostnames
203             are stripped from both the domain used in the URI, and the domain in the
204             lookup.
205              
206             C<NAME_OF_RULE> is the name of the rule to be used, C<rhsbl_zone> is the zone
207             to look up domain names in, and C<lookuptype> is the type of lookup (B<TXT> or
208             B<A>).
209              
210             Note that, as with C<urirhsbl>, you must also define a body-eval rule calling
211             C<check_uridnsbl()> to use this.
212              
213             =item urinsrhssub NAME_OF_RULE rhsbl_zone lookuptype subtest
214              
215             Specify a RHSBL-style domain-NS lookup, as above, with a sub-test.
216             C<NAME_OF_RULE> is the name of the rule to be used, C<rhsbl_zone> is the zone
217             to look up domain names in, and C<lookuptype> is the type of lookup (B<TXT> or
218             B<A>). C<subtest> is the sub-test to run against the returned data; see
219             <urirhssub>.
220              
221             Note that, as with C<urirhsbl>, you must also define a body-eval rule calling
222             C<check_uridnsbl()> to use this.
223              
224             =item urifullnsrhsbl NAME_OF_RULE rhsbl_zone lookuptype
225              
226             Perform a RHSBL-style domain lookup against the contents of the NS records for
227             each URI. In other words, a URI using the domain C<foo.com> will cause an NS
228             lookup to take place; assuming that domain has an NS of C<ns0.bar.com>, that
229             will cause a lookup of C<ns0.bar.com.uriblzone.net>. Note that hostnames are
230             stripped from the domain used in the URI.
231              
232             C<NAME_OF_RULE> is the name of the rule to be used, C<rhsbl_zone> is the zone
233             to look up domain names in, and C<lookuptype> is the type of lookup (B<TXT> or
234             B<A>).
235              
236             Note that, as with C<urirhsbl>, you must also define a body-eval rule calling
237             C<check_uridnsbl()> to use this.
238              
239             =item urifullnsrhssub NAME_OF_RULE rhsbl_zone lookuptype subtest
240              
241             Specify a RHSBL-style domain-NS lookup, as above, with a sub-test.
242             C<NAME_OF_RULE> is the name of the rule to be used, C<rhsbl_zone> is the zone
243             to look up domain names in, and C<lookuptype> is the type of lookup (B<TXT> or
244             B<A>). C<subtest> is the sub-test to run against the returned data; see
245             <urirhssub>.
246              
247             Note that, as with C<urirhsbl>, you must also define a body-eval rule calling
248             C<check_uridnsbl()> to use this.
249              
250             =item tflags NAME_OF_RULE ips_only
251              
252             Only URIs containing IP addresses as the "host" component will be matched
253             against the named "urirhsbl"/"urirhssub" rule.
254              
255             =item tflags NAME_OF_RULE domains_only
256              
257             Only URIs containing a non-IP-address "host" component will be matched against
258             the named "urirhsbl"/"urirhssub" rule.
259              
260             =item tflags NAME_OF_RULE ns
261              
262             The 'ns' flag may be applied to rules corresponding to uridnsbl and uridnssub
263             directives. Host names from URLs will be mapped to their name server IP
264             addresses (a NS lookup followed by an A lookup), which in turn will be sent
265             to blocklists. This is a default when neither 'a' nor 'ns' flags are specified.
266              
267             =item tflags NAME_OF_RULE a
268              
269             The 'a' flag may be applied to rules corresponding to uridnsbl and uridnssub
270             directives. Host names from URLs will be mapped to their IP addresses, which
271             will be sent to blocklists. When both 'ns' and 'a' flags are specified,
272             both queries will be performed.
273              
274             =back
275              
276             =head1 ADMINISTRATOR SETTINGS
277              
278             =over 4
279              
280             =item uridnsbl_max_domains N (default: 20)
281              
282             The maximum number of domains to look up.
283              
284             =back
285              
286             =head1 NOTES
287              
288             The C<uridnsbl_timeout> option has been obsoleted by the C<rbl_timeout>
289             option. See the C<Mail::SpamAssassin::Conf> POD for details on C<rbl_timeout>.
290              
291             =cut
292              
293             package Mail::SpamAssassin::Plugin::URIDNSBL;
294              
295 22     22   151 use Mail::SpamAssassin::Plugin;
  22         52  
  22         1010  
296 22     22   154 use Mail::SpamAssassin::Constants qw(:ip);
  22         44  
  22         3480  
297 22     22   167 use Mail::SpamAssassin::Util;
  22         60  
  22         855  
298 22     22   160 use Mail::SpamAssassin::Logger;
  22         44  
  22         1309  
299 22     22   142 use strict;
  22         55  
  22         590  
300 22     22   117 use warnings;
  22         47  
  22         815  
301             # use bytes;
302 22     22   152 use re 'taint';
  22         43  
  22         1411  
303              
304             our @ISA = qw(Mail::SpamAssassin::Plugin);
305              
306 22     22   157 use constant LOG_COMPLETION_TIMES => 0;
  22         51  
  22         119853  
307              
308             # constructor
309             sub new {
310 63     63 1 234 my $class = shift;
311 63         150 my $samain = shift;
312              
313             # some boilerplate...
314 63   33     541 $class = ref($class) || $class;
315 63         655 my $self = $class->SUPER::new($samain);
316 63         139 bless ($self, $class);
317              
318             # this can be effectively global, at least in each process, safely
319              
320 63         305 $self->{finished} = { };
321              
322 63         434 $self->register_eval_rule ("check_uridnsbl");
323 63         339 $self->set_config($samain->{conf});
324              
325 63         733 return $self;
326             }
327              
328             # this is just a placeholder; in fact the results are dealt with later
329             sub check_uridnsbl {
330 88     88 0 1187 return 0;
331             }
332              
333             # ---------------------------------------------------------------------------
334              
335             # once the metadata is parsed, we can access the URI list. So start off
336             # the lookups here!
337             sub parsed_metadata {
338 81     81 1 218 my ($self, $opts) = @_;
339 81         236 my $pms = $opts->{permsgstatus};
340 81         183 my $conf = $pms->{conf};
341              
342 81 50       395 return 0 if $conf->{skip_uribl_checks};
343              
344 81 100       307 if (!$pms->is_dns_available()) {
345 77         262 $self->{dns_not_available} = 1;
346 77         251 return 0;
347             } else {
348             # due to re-testing dns may become available after being unavailable
349             # DOS: I don't think dns_not_available is even used anymore
350 4         10 $self->{dns_not_available} = 0;
351             }
352              
353 4         11 $pms->{'uridnsbl_activerules'} = { };
354 4         11 $pms->{'uridnsbl_hits'} = { };
355 4         9 $pms->{'uridnsbl_seen_lookups'} = { };
356              
357             # only hit DNSBLs for active rules (defined and score != 0)
358 4         16 $pms->{'uridnsbl_active_rules_rhsbl'} = { };
359 4         20 $pms->{'uridnsbl_active_rules_rhsbl_ipsonly'} = { };
360 4         14 $pms->{'uridnsbl_active_rules_rhsbl_domsonly'} = { };
361 4         10 $pms->{'uridnsbl_active_rules_nsrhsbl'} = { };
362 4         9 $pms->{'uridnsbl_active_rules_fullnsrhsbl'} = { };
363 4         10 $pms->{'uridnsbl_active_rules_nsrevipbl'} = { };
364 4         9 $pms->{'uridnsbl_active_rules_arevipbl'} = { };
365              
366 4         7 foreach my $rulename (keys %{$conf->{uridnsbls}}) {
  4         44  
367 88 50       186 next unless ($conf->is_rule_active('body_evals',$rulename));
368              
369 88         157 my $rulecf = $conf->{uridnsbls}->{$rulename};
370 88         158 my $tflags = $conf->{tflags}->{$rulename};
371 88 100       131 $tflags = '' if !defined $tflags;
372 88         156 my %tfl = map { ($_,1) } split(' ',$tflags);
  76         187  
373              
374 88         157 my $is_rhsbl = $rulecf->{is_rhsbl};
375 88 50 33     340 if ( $is_rhsbl && $tfl{'ips_only'}) {
    100 33        
    50          
    0          
    0          
376 0         0 $pms->{uridnsbl_active_rules_rhsbl_ipsonly}->{$rulename} = 1;
377             } elsif ($is_rhsbl && $tfl{'domains_only'}) {
378 76         186 $pms->{uridnsbl_active_rules_rhsbl_domsonly}->{$rulename} = 1;
379             } elsif ($is_rhsbl) {
380 12         28 $pms->{uridnsbl_active_rules_rhsbl}->{$rulename} = 1;
381             } elsif ($rulecf->{is_fullnsrhsbl}) {
382 0         0 $pms->{uridnsbl_active_rules_fullnsrhsbl}->{$rulename} = 1;
383             } elsif ($rulecf->{is_nsrhsbl}) {
384 0         0 $pms->{uridnsbl_active_rules_nsrhsbl}->{$rulename} = 1;
385             } else { # just a plain dnsbl rule (IP based), not a RHS rule (name-based)
386 0 0       0 if ($tfl{'a'}) { # tflag 'a' explicitly
387 0         0 $pms->{uridnsbl_active_rules_arevipbl}->{$rulename} = 1;
388             }
389 0 0 0     0 if ($tfl{'ns'} || !$tfl{'a'}) { # tflag 'ns' explicitly, or default
390 0         0 $pms->{uridnsbl_active_rules_nsrevipbl}->{$rulename} = 1;
391             }
392             }
393             }
394              
395             # get all domains in message
396              
397             # don't keep dereferencing this
398 4         12 my $skip_domains = $conf->{uridnsbl_skip_domains};
399 4 50       13 $skip_domains = {} if !$skip_domains;
400              
401             # list of hashes to use in order
402 4         7 my @uri_ordered;
403              
404             # Generate the full list of html-parsed domains.
405 4         23 my $uris = $pms->get_uri_detail_list();
406              
407             # go from uri => info to uri_ordered
408             # 0: a
409             # 1: form
410             # 2: img
411             # 3: !a_empty
412             # 4: parsed
413             # 5: a_empty
414 4         12 while (my($uri, $info) = each %{$uris}) {
  11         35  
415             # we want to skip mailto: uris
416 7 50       20 next if ($uri =~ /^mailto:/i);
417              
418             # no hosts/domains were found via this uri, so skip
419 7 50       16 next unless ($info->{hosts});
420              
421 7         10 my $entry = 3;
422              
423 7 50 33     37 if ($info->{types}->{a}) {
    50          
    50          
    50          
424 0         0 $entry = 5;
425              
426             # determine a vs a_empty
427 0         0 foreach my $at (@{$info->{anchor_text}}) {
  0         0  
428 0 0       0 if (length $at) {
429 0         0 $entry = 0;
430 0         0 last;
431             }
432             }
433             }
434             elsif ($info->{types}->{form}) {
435 0         0 $entry = 1;
436             }
437             elsif ($info->{types}->{img}) {
438 0         0 $entry = 2;
439             }
440 7         29 elsif ($info->{types}->{parsed} && (keys %{$info->{types}} == 1)) {
441 7         13 $entry = 4;
442             }
443              
444             # take the usable domains and add them to the ordered list
445 7         10 while (my($host,$domain) = each( %{$info->{hosts}} )) {
  14         46  
446 7 50       23 if ($skip_domains->{$domain}) {
447 0         0 dbg("uridnsbl: domain $domain in skip list, host $host");
448             } else {
449             # use hostname as a key, and drag along the stripped domain name part
450 7         21 $uri_ordered[$entry]->{$host} = $domain;
451             }
452             }
453             }
454              
455             # at this point, @uri_ordered is an ordered array of hostname hashes
456              
457 4         8 my %hostlist; # keys are host names, values are their domain parts
458              
459 4         10 my $umd = $conf->{uridnsbl_max_domains};
460 4   66     29 while (keys %hostlist < $umd && @uri_ordered) {
461 15         24 my $array = shift @uri_ordered;
462 15 100       46 next unless $array;
463              
464             # run through and find the new domains in this grouping
465 3         8 my @hosts = grep(!$hostlist{$_}, keys %{$array});
  3         19  
466 3 50       9 next unless @hosts;
467              
468             # the new hosts are all useful, just add them in
469 3 50       14 if (keys(%hostlist) + @hosts <= $umd) {
470 3         9 foreach my $host (@hosts) {
471 7         27 $hostlist{$host} = $array->{$host};
472             }
473             }
474             else {
475 0         0 dbg("uridnsbl: more than $umd URIs, picking a subset");
476             # trim down to a limited number - pick randomly
477 0   0     0 while (@hosts && keys %hostlist < $umd) {
478 0         0 my $r = int rand(scalar @hosts);
479 0         0 my $picked_host = splice(@hosts, $r, 1);
480 0         0 $hostlist{$picked_host} = $array->{$picked_host};
481             }
482             }
483             }
484              
485 4         16 my @hnames = keys %hostlist;
486 4 100       28 $pms->set_tag('URIHOSTS',
    100          
487             @hnames == 1 ? $hnames[0] : \@hnames) if @hnames;
488 4         12 my @dnames = values %hostlist;
489 4 100       23 $pms->set_tag('URIDOMAINS',
    100          
490             @dnames == 1 ? $dnames[0] : \@dnames) if @dnames;
491              
492             # and query
493 4         29 $self->query_hosts_or_domains($pms, \%hostlist);
494              
495 4         32 return 1;
496             }
497              
498             # Accepts argument in one of the following forms: m, n1-n2, or n/m,
499             # where n,n1,n2,m can be any of: decimal digits, 0x followed by up to 8
500             # hexadecimal digits, or an IPv4 address in quad-dot form. The argument
501             # is checked for syntax (undef is returned on syntax errors), hex numbers
502             # are converted to decimal, and quad-dot is converted to decimal, then
503             # reassembled into original string delimited by '-' or '/'. As a special
504             # backward compatibility measure, a single quad-dot (with no second number)
505             # is converted into n-n, to distinguish it from a traditional mask-only form.
506             #
507             # In practice, arguments like the following are anticipated:
508             # 127.0.1.2 (same as 127.0.1.2-127.0.1.2 or 127.0.1.2/255.255.255.255)
509             # 127.0.1.20-127.0.1.39 (= 0x7f000114-0x7f000127 or 2130706708-2130706727)
510             # 0.0.0.16/0.0.0.16 (same as 0x10/0x10 or 16/0x10 or 16/16)
511             # 16 (traditional style mask-only, same as 0x10)
512             #
513             sub parse_and_canonicalize_subtest {
514 22     22 0 40 my($subtest) = @_;
515 22         24 my $digested_subtest;
516              
517 22         63 local($1,$2,$3);
518 22 50       85 if ($subtest =~ m{^ ([^/-]+) (?: ([/-]) (.+) )? \z}xs) {
519 22         63 my($n1,$delim,$n2) = ($1,$2,$3);
520 22         28 my $any_quad_dot;
521 22         43 for ($n1,$n2) {
522 44 100       184 if (!defined $_) {
    100          
    100          
    50          
523             # ok, $n2 may not exist
524             } elsif (/^\d{1,10}\z/) {
525             # ok, already a decimal number
526             } elsif (/^0x[0-9a-zA-Z]{1,8}\z/) {
527 5         17 $_ = hex($_); # hex -> number
528             } elsif (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/) {
529 26         61 $_ = Mail::SpamAssassin::Util::my_inet_aton($_); # quad-dot -> number
530 26         52 $any_quad_dot = 1;
531             } else {
532 0         0 return;
533             }
534             }
535 22 100       78 $digested_subtest = defined $n2 ? $n1.$delim.$n2
    100          
536             : $any_quad_dot ? $n1.'-'.$n1 : "$n1";
537             }
538 22         61 return $digested_subtest;
539             }
540              
541             sub set_config {
542 63     63 0 188 my($self, $conf) = @_;
543 63         156 my @cmds;
544              
545 63         422 push(@cmds, {
546             setting => 'skip_uribl_checks',
547             default => 0,
548             type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
549             });
550              
551 63         363 push(@cmds, {
552             setting => 'uridnsbl_max_domains',
553             is_admin => 1,
554             default => 20,
555             type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
556             });
557              
558             push (@cmds, {
559             setting => 'uridnsbl',
560             is_priv => 1,
561             code => sub {
562 0     0   0 my ($self, $key, $value, $line) = @_;
563 0         0 local($1,$2,$3);
564 0 0       0 if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)$/) {
    0          
565 0         0 my $rulename = $1;
566 0         0 my $zone = $2;
567 0         0 my $type = $3;
568 0         0 $zone =~ s/\.\z//; # strip a redundant trailing dot
569 0         0 $self->{uridnsbls}->{$rulename} = {
570             zone => $zone, type => $type,
571             is_rhsbl => 0
572             };
573             }
574             elsif ($value =~ /^$/) {
575 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
576             }
577             else {
578 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
579             }
580             }
581 63         635 });
582              
583             push (@cmds, {
584             setting => 'uridnssub',
585             is_priv => 1,
586             code => sub {
587 0     0   0 my ($self, $key, $value, $line) = @_;
588 0         0 local($1,$2,$3,$4);
589 0 0       0 if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
    0          
590 0         0 my $rulename = $1;
591 0         0 my $zone = $2;
592 0         0 my $type = $3;
593 0         0 my $subrule = $4;
594 0         0 $zone =~ s/\.\z//; # strip a redundant trailing dot
595 0         0 $subrule = parse_and_canonicalize_subtest($subrule);
596 0 0       0 defined $subrule or return $Mail::SpamAssassin::Conf::INVALID_VALUE;
597 0         0 $self->{uridnsbls}->{$rulename} = {
598             zone => $zone, type => $type,
599             is_rhsbl => 0, subtest => $subrule,
600             };
601             }
602             elsif ($value =~ /^$/) {
603 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
604             }
605             else {
606 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
607             }
608             }
609 63         560 });
610              
611             push (@cmds, {
612             setting => 'urirhsbl',
613             is_priv => 1,
614             code => sub {
615 0     0   0 my ($self, $key, $value, $line) = @_;
616 0         0 local($1,$2,$3);
617 0 0       0 if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)$/) {
    0          
618 0         0 my $rulename = $1;
619 0         0 my $zone = $2;
620 0         0 my $type = $3;
621 0         0 $zone =~ s/\.\z//; # strip a redundant trailing dot
622 0         0 $self->{uridnsbls}->{$rulename} = {
623             zone => $zone, type => $type,
624             is_rhsbl => 1
625             };
626             }
627             elsif ($value =~ /^$/) {
628 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
629             }
630             else {
631 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
632             }
633             }
634 63         554 });
635              
636             push (@cmds, {
637             setting => 'urirhssub',
638             is_priv => 1,
639             code => sub {
640 22     22   43 my ($self, $key, $value, $line) = @_;
641 22         118 local($1,$2,$3,$4);
642 22 50       132 if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
    0          
643 22         47 my $rulename = $1;
644 22         35 my $zone = $2;
645 22         32 my $type = $3;
646 22         34 my $subrule = $4;
647 22         36 $zone =~ s/\.\z//; # strip a redundant trailing dot
648 22         48 $subrule = parse_and_canonicalize_subtest($subrule);
649 22 50       45 defined $subrule or return $Mail::SpamAssassin::Conf::INVALID_VALUE;
650 22         152 $self->{uridnsbls}->{$rulename} = {
651             zone => $zone, type => $type,
652             is_rhsbl => 1, subtest => $subrule,
653             };
654             }
655             elsif ($value =~ /^$/) {
656 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
657             }
658             else {
659 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
660             }
661             }
662 63         494 });
663              
664             push (@cmds, {
665             setting => 'urinsrhsbl',
666             is_priv => 1,
667             code => sub {
668 0     0   0 my ($self, $key, $value, $line) = @_;
669 0         0 local($1,$2,$3);
670 0 0       0 if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)$/) {
    0          
671 0         0 my $rulename = $1;
672 0         0 my $zone = $2;
673 0         0 my $type = $3;
674 0         0 $zone =~ s/\.\z//; # strip a redundant trailing dot
675 0         0 $self->{uridnsbls}->{$rulename} = {
676             zone => $zone, type => $type,
677             is_nsrhsbl => 1
678             };
679             }
680             elsif ($value =~ /^$/) {
681 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
682             }
683             else {
684 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
685             }
686             }
687 63         543 });
688              
689             push (@cmds, {
690             setting => 'urinsrhssub',
691             is_priv => 1,
692             code => sub {
693 0     0   0 my ($self, $key, $value, $line) = @_;
694 0         0 local($1,$2,$3,$4);
695 0 0       0 if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
    0          
696 0         0 my $rulename = $1;
697 0         0 my $zone = $2;
698 0         0 my $type = $3;
699 0         0 my $subrule = $4;
700 0         0 $zone =~ s/\.\z//; # strip a redundant trailing dot
701 0         0 $subrule = parse_and_canonicalize_subtest($subrule);
702 0 0       0 defined $subrule or return $Mail::SpamAssassin::Conf::INVALID_VALUE;
703 0         0 $self->{uridnsbls}->{$rulename} = {
704             zone => $zone, type => $type,
705             is_nsrhsbl => 1, subtest => $subrule,
706             };
707             }
708             elsif ($value =~ /^$/) {
709 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
710             }
711             else {
712 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
713             }
714             }
715 63         501 });
716              
717             push (@cmds, {
718             setting => 'urifullnsrhsbl',
719             is_priv => 1,
720             code => sub {
721 0     0   0 my ($self, $key, $value, $line) = @_;
722 0         0 local($1,$2,$3);
723 0 0       0 if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)$/) {
    0          
724 0         0 my $rulename = $1;
725 0         0 my $zone = $2;
726 0         0 my $type = $3;
727 0         0 $zone =~ s/\.\z//; # strip a redundant trailing dot
728 0         0 $self->{uridnsbls}->{$rulename} = {
729             zone => $zone, type => $type,
730             is_fullnsrhsbl => 1
731             };
732             }
733             elsif ($value =~ /^$/) {
734 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
735             }
736             else {
737 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
738             }
739             }
740 63         624 });
741              
742             push (@cmds, {
743             setting => 'urifullnsrhssub',
744             is_priv => 1,
745             code => sub {
746 0     0   0 my ($self, $key, $value, $line) = @_;
747 0         0 local($1,$2,$3,$4);
748 0 0       0 if ($value =~ /^(\S+)\s+(\S+)\s+(\S+)\s+(.*?)\s*$/) {
    0          
749 0         0 my $rulename = $1;
750 0         0 my $zone = $2;
751 0         0 my $type = $3;
752 0         0 my $subrule = $4;
753 0         0 $zone =~ s/\.\z//; # strip a redundant trailing dot
754 0         0 $subrule = parse_and_canonicalize_subtest($subrule);
755 0 0       0 defined $subrule or return $Mail::SpamAssassin::Conf::INVALID_VALUE;
756 0         0 $self->{uridnsbls}->{$rulename} = {
757             zone => $zone, type => $type,
758             is_fullnsrhsbl => 1, subtest => $subrule,
759             };
760             }
761             elsif ($value =~ /^$/) {
762 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
763             }
764             else {
765 0         0 return $Mail::SpamAssassin::Conf::INVALID_VALUE;
766             }
767             }
768 63         505 });
769              
770             push (@cmds, {
771             setting => 'uridnsbl_skip_domain',
772             default => {},
773             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
774             code => sub {
775 0     0   0 my ($self, $key, $value, $line) = @_;
776 0 0       0 if ($value =~ /^$/) {
777 0         0 return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
778             }
779 0         0 foreach my $domain (split(/\s+/, $value)) {
780 0         0 $self->{uridnsbl_skip_domains}->{lc $domain} = 1;
781             }
782             }
783 63         561 });
784              
785             push (@cmds, {
786             setting => 'clear_uridnsbl_skip_domain',
787             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
788             code => sub {
789 0     0   0 my ($self, $key, $value, $line) = @_;
790 0 0 0     0 if (!defined $value || $value eq '') {
791             # clear the entire list
792 0         0 $self->{uridnsbl_skip_domains} = {};
793             } else {
794 0         0 foreach my $domain (split(/\s+/, $value)) {
795 0         0 delete $self->{uridnsbl_skip_domains}->{lc $domain};
796             }
797             }
798             }
799 63         479 });
800              
801             # obsolete
802             push(@cmds, {
803             setting => 'uridnsbl_timeout',
804             code => sub {
805             # not a lint_warn(), since it's pretty harmless and we don't want
806             # to break stuff like sa-update
807 0     0   0 warn("config: 'uridnsbl_timeout' is obsolete, use 'rbl_timeout' instead");
808 0         0 return 0;
809             }
810 63         443 });
811              
812 63         387 $conf->{parser}->register_commands(\@cmds);
813             }
814              
815             # ---------------------------------------------------------------------------
816              
817             sub query_hosts_or_domains {
818 4     4 0 13 my ($self, $pms, $hosthash_ref) = @_;
819 4         9 my $conf = $pms->{conf};
820 4         9 my $seen_lookups = $pms->{'uridnsbl_seen_lookups'};
821              
822 4         9 my $rhsblrules = $pms->{uridnsbl_active_rules_rhsbl};
823 4         10 my $rhsbliprules = $pms->{uridnsbl_active_rules_rhsbl_ipsonly};
824 4         8 my $rhsbldomrules = $pms->{uridnsbl_active_rules_rhsbl_domsonly};
825 4         10 my $nsrhsblrules = $pms->{uridnsbl_active_rules_nsrhsbl};
826 4         12 my $fullnsrhsblrules = $pms->{uridnsbl_active_rules_fullnsrhsbl};
827 4         9 my $nsreviprules = $pms->{uridnsbl_active_rules_nsrevipbl};
828 4         10 my $areviprules = $pms->{uridnsbl_active_rules_arevipbl};
829              
830 4         20 while (my($host,$domain) = each(%$hosthash_ref)) {
831 7         22 $domain = lc $domain; # just in case
832 7         13 $host = lc $host;
833 7         29 dbg("uridnsbl: considering host=$host, domain=$domain");
834 7         20 my $obj = { dom => $domain };
835              
836 7         14 my ($is_ip, $single_dnsbl);
837 7 100       24 if ($host =~ /^\d+\.\d+\.\d+\.\d+$/) {
838 1         3 my $IPV4_ADDRESS = IPV4_ADDRESS;
839 1         3 my $IP_PRIVATE = IP_PRIVATE;
840             # only look up the IP if it is public and valid
841 1 50 33     347 if ($host =~ /^$IPV4_ADDRESS$/o && $host !~ /^$IP_PRIVATE$/o) {
842 1         5 my $obj = { dom => $host };
843 1         8 $self->lookup_dnsbl_for_ip($pms, $obj, $host);
844             # and check the IP in RHSBLs too
845 1         4 local($1,$2,$3,$4);
846 1 50       8 if ($host =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/) {
847 1         5 $domain = "$4.$3.$2.$1";
848 1         3 $single_dnsbl = 1;
849 1         8 $is_ip = 1;
850             }
851             }
852             }
853             else {
854 6         13 $single_dnsbl = 1;
855             }
856              
857 7 50       22 if ($single_dnsbl) {
858             # rule names which look up a domain in the basic RHSBL subset
859 7         12 my @rhsblrules = keys %{$rhsblrules};
  7         24  
860              
861             # and add the "domains_only" and "ips_only" subsets as appropriate
862 7 100       22 if ($is_ip) {
863 1         3 push @rhsblrules, keys %{$rhsbliprules};
  1         3  
864             } else {
865 6         10 push @rhsblrules, keys %{$rhsbldomrules};
  6         47  
866             }
867              
868 7         19 foreach my $rulename (@rhsblrules) {
869 135         239 my $rulecf = $conf->{uridnsbls}->{$rulename};
870             $self->lookup_single_dnsbl($pms, $obj, $rulename,
871 135         352 $domain, $rulecf->{zone}, $rulecf->{type});
872              
873             # note that these rules are now underway. important: unless the
874             # rule hits, in the current design, these will not be considered
875             # "finished" until harvest_dnsbl_queries() completes
876 135         295 $pms->register_async_rule_start($rulename);
877             }
878              
879             # perform NS+A or A queries to look up the domain in the non-RHSBL subset,
880             # but only if there are active reverse-IP-URIBL rules
881 7 100       31 if ($host !~ /^\d+\.\d+\.\d+\.\d+$/) {
882 6 50 33     75 if ( !$seen_lookups->{'NS:'.$domain} &&
      33        
883             (%$nsreviprules || %$nsrhsblrules || %$fullnsrhsblrules) ) {
884 0         0 $seen_lookups->{'NS:'.$domain} = 1;
885 0         0 $self->lookup_domain_ns($pms, $obj, $domain);
886             }
887 6 50 33     50 if (%$areviprules && !$seen_lookups->{'A:'.$host}) {
888 0         0 $seen_lookups->{'A:'.$host} = 1;
889 0         0 my $obj = { dom => $host };
890 0         0 $self->lookup_a_record($pms, $obj, $host);
891 0         0 $pms->register_async_rule_start($_) for keys %$areviprules;
892             }
893             }
894             }
895             }
896             }
897              
898             # ---------------------------------------------------------------------------
899              
900             sub lookup_domain_ns {
901 0     0 0 0 my ($self, $pms, $obj, $dom, $rulename) = @_;
902              
903 0         0 my $key = "NS:" . $dom;
904 0         0 my $ent = {
905             key => $key, zone => $dom, obj => $obj, type => "URI-NS",
906             rulename => $rulename,
907             };
908             # dig $dom ns
909             $ent = $pms->{async}->bgsend_and_start_lookup(
910             $dom, 'NS', undef, $ent,
911 0     0   0 sub { my ($ent2,$pkt) = @_;
912 0         0 $self->complete_ns_lookup($pms, $ent2, $pkt, $dom) },
913 0         0 master_deadline => $pms->{master_deadline} );
914              
915 0         0 return $ent;
916             }
917              
918             sub complete_ns_lookup {
919 0     0 0 0 my ($self, $pms, $ent, $pkt, $dom) = @_;
920              
921 0 0       0 if (!$pkt) {
922             # $pkt will be undef if the DNS query was aborted (e.g. timed out)
923 0         0 dbg("uridnsbl: complete_ns_lookup aborted %s", $ent->{key});
924 0         0 return;
925             }
926              
927 0         0 dbg("uridnsbl: complete_ns_lookup %s", $ent->{key});
928 0         0 my $conf = $pms->{conf};
929 0         0 my @answer = $pkt->answer;
930              
931 0         0 my $IPV4_ADDRESS = IPV4_ADDRESS;
932 0         0 my $IP_PRIVATE = IP_PRIVATE;
933 0         0 my $nsrhsblrules = $pms->{uridnsbl_active_rules_nsrhsbl};
934 0         0 my $fullnsrhsblrules = $pms->{uridnsbl_active_rules_fullnsrhsbl};
935 0         0 my $seen_lookups = $pms->{'uridnsbl_seen_lookups'};
936              
937 0         0 my $j = 0;
938 0         0 foreach my $rr (@answer) {
939 0         0 $j++;
940 0         0 my $str = $rr->string;
941 0 0 0     0 next unless (defined($str) && defined($dom));
942 0         0 dbg("uridnsbl: got($j) NS for $dom: $str");
943              
944 0 0       0 if ($rr->type eq 'NS') {
945 0         0 my $nsmatch = lc $rr->nsdname; # available since at least Net::DNS 0.14
946 0         0 my $nsrhblstr = $nsmatch;
947 0         0 my $fullnsrhblstr = $nsmatch;
948              
949 0 0       0 if ($nsmatch =~ /^\d+\.\d+\.\d+\.\d+$/) {
950             # only look up the IP if it is public and valid
951 0 0 0     0 if ($nsmatch =~ /^$IPV4_ADDRESS$/o && $nsmatch !~ /^$IP_PRIVATE$/o) {
952 0         0 $self->lookup_dnsbl_for_ip($pms, $ent->{obj}, $nsmatch);
953             }
954 0         0 $nsrhblstr = $nsmatch;
955             }
956             else {
957 0 0       0 if (!$seen_lookups->{'A:'.$nsmatch}) {
958 0         0 $seen_lookups->{'A:'.$nsmatch} = 1;
959 0         0 $self->lookup_a_record($pms, $ent->{obj}, $nsmatch);
960             }
961 0         0 $nsrhblstr = $self->{main}->{registryboundaries}->trim_domain($nsmatch);
962             }
963              
964 0         0 foreach my $rulename (keys %{$nsrhsblrules}) {
  0         0  
965 0         0 my $rulecf = $conf->{uridnsbls}->{$rulename};
966             $self->lookup_single_dnsbl($pms, $ent->{obj}, $rulename,
967 0         0 $nsrhblstr, $rulecf->{zone}, $rulecf->{type});
968              
969 0         0 $pms->register_async_rule_start($rulename);
970             }
971              
972 0         0 foreach my $rulename (keys %{$fullnsrhsblrules}) {
  0         0  
973 0         0 my $rulecf = $conf->{uridnsbls}->{$rulename};
974             $self->lookup_single_dnsbl($pms, $ent->{obj}, $rulename,
975 0         0 $fullnsrhblstr, $rulecf->{zone}, $rulecf->{type});
976              
977 0         0 $pms->register_async_rule_start($rulename);
978             }
979             }
980             }
981             }
982              
983             # ---------------------------------------------------------------------------
984              
985             sub lookup_a_record {
986 0     0 0 0 my ($self, $pms, $obj, $hname, $rulename) = @_;
987              
988 0         0 my $key = "A:" . $hname;
989 0         0 my $ent = {
990             key => $key, zone => $hname, obj => $obj, type => "URI-A",
991             rulename => $rulename,
992             };
993             # dig $hname a
994             $ent = $pms->{async}->bgsend_and_start_lookup(
995             $hname, 'A', undef, $ent,
996 0     0   0 sub { my ($ent2,$pkt) = @_;
997 0         0 $self->complete_a_lookup($pms, $ent2, $pkt, $hname) },
998 0         0 master_deadline => $pms->{master_deadline} );
999              
1000 0         0 return $ent;
1001             }
1002              
1003             sub complete_a_lookup {
1004 0     0 0 0 my ($self, $pms, $ent, $pkt, $hname) = @_;
1005              
1006 0 0       0 if (!$pkt) {
1007             # $pkt will be undef if the DNS query was aborted (e.g. timed out)
1008 0         0 dbg("uridnsbl: complete_a_lookup aborted %s", $ent->{key});
1009 0         0 return;
1010             }
1011 0         0 dbg("uridnsbl: complete_a_lookup %s", $ent->{key});
1012 0         0 my $j = 0;
1013 0         0 my @answer = $pkt->answer;
1014 0         0 foreach my $rr (@answer) {
1015 0         0 $j++;
1016 0         0 my $str = $rr->string;
1017 0 0       0 if (!defined $hname) {
    0          
1018 0         0 warn "complete_a_lookup-1: $j, (hname is undef), $str";
1019             } elsif (!defined $str) {
1020 0         0 warn "complete_a_lookup-2: $j, $hname, (str is undef)";
1021 0         0 next;
1022             }
1023 0         0 dbg("uridnsbl: complete_a_lookup got(%d) A for %s: %s", $j,$hname,$str);
1024              
1025 0 0       0 if ($rr->type eq 'A') {
1026 0         0 my $ip_address = $rr->rdatastr;
1027 0         0 $self->lookup_dnsbl_for_ip($pms, $ent->{obj}, $ip_address);
1028             }
1029             }
1030             }
1031              
1032             # ---------------------------------------------------------------------------
1033              
1034             sub lookup_dnsbl_for_ip {
1035 1     1 0 7 my ($self, $pms, $obj, $ip) = @_;
1036              
1037 1         6 local($1,$2,$3,$4);
1038 1         7 $ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/;
1039 1         7 my $revip = "$4.$3.$2.$1";
1040              
1041 1         3 my $conf = $pms->{conf};
1042 1         3 my $tflags = $conf->{tflags};
1043 1         3 my $cfns = $pms->{uridnsbl_active_rules_nsrevipbl};
1044 1         3 my $cfa = $pms->{uridnsbl_active_rules_arevipbl};
1045 1         8 foreach my $rulename (keys %$cfa, keys %$cfns) {
1046 0         0 my $rulecf = $conf->{uridnsbls}->{$rulename};
1047              
1048             # ips_only/domains_only lookups should not act on this kind of BL
1049             next if defined $tflags->{$rulename} &&
1050 0 0 0     0 $tflags->{$rulename} =~ /\b(?:ips_only|domains_only)\b/;
1051              
1052             $self->lookup_single_dnsbl($pms, $obj, $rulename,
1053 0         0 $revip, $rulecf->{zone}, $rulecf->{type});
1054             }
1055             }
1056              
1057             sub lookup_single_dnsbl {
1058 135     135 0 286 my ($self, $pms, $obj, $rulename, $lookupstr, $dnsbl, $qtype) = @_;
1059              
1060 135         312 my $key = "DNSBL:" . $lookupstr . ':' . $dnsbl;
1061 135         432 my $ent = {
1062             key => $key, zone => $dnsbl, obj => $obj, type => 'URI-DNSBL',
1063             rulename => $rulename,
1064             };
1065             $ent = $pms->{async}->bgsend_and_start_lookup(
1066             $lookupstr.".".$dnsbl, $qtype, undef, $ent,
1067 135     135   233 sub { my ($ent2,$pkt) = @_;
1068 135         304 $self->complete_dnsbl_lookup($pms, $ent2, $pkt) },
1069 135         970 master_deadline => $pms->{master_deadline} );
1070              
1071 135         236 return $ent;
1072             }
1073              
1074             sub complete_dnsbl_lookup {
1075 135     135 0 210 my ($self, $pms, $ent, $pkt) = @_;
1076              
1077 135 50       221 if (!$pkt) {
1078             # $pkt will be undef if the DNS query was aborted (e.g. timed out)
1079             dbg("uridnsbl: complete_dnsbl_lookup aborted %s %s",
1080 0         0 $ent->{rulename}, $ent->{key});
1081 0         0 return;
1082             }
1083              
1084 135         331 dbg("uridnsbl: complete_dnsbl_lookup %s %s", $ent->{rulename}, $ent->{key});
1085 135         200 my $conf = $pms->{conf};
1086              
1087 135         217 my $zone = $ent->{zone};
1088 135         251 my $dom = $ent->{obj}->{dom};
1089 135         167 my $rulename = $ent->{rulename};
1090 135         253 my $rulecf = $conf->{uridnsbls}->{$rulename};
1091              
1092 135         153 my @subtests;
1093 135         305 my @answer = $pkt->answer;
1094 135         684 foreach my $rr (@answer)
1095             {
1096 126         164 my($rdatastr,$rdatanum);
1097 126         317 my $rr_type = $rr->type;
1098              
1099 126 50       1226 if ($rr_type eq 'A') {
    0          
1100             # Net::DNS::RR::A::address() is available since Net::DNS 0.69
1101 126 50       550 $rdatastr = $rr->UNIVERSAL::can('address') ? $rr->address
1102             : $rr->rdatastr;
1103 126 50       1564 if ($rdatastr =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) {
1104 126         276 $rdatanum = Mail::SpamAssassin::Util::my_inet_aton($rdatastr);
1105             }
1106             } elsif ($rr_type eq 'TXT') {
1107             # txtdata returns a non- zone-file-format encoded result, unlike rdatastr;
1108             # avoid space-separated RDATA <character-string> fields if possible;
1109             # txtdata provides a list of strings in list context since Net::DNS 0.69
1110 0         0 $rdatastr = join('',$rr->txtdata);
1111             } else {
1112 0         0 next;
1113             }
1114              
1115 126         276 my $subtest = $rulecf->{subtest};
1116              
1117 126 50       437 dbg("uridnsbl: %s . %s -> %s, %s%s",
1118             $dom, $zone, $rdatastr, $rulename,
1119             !defined $subtest ? '' : ', subtest:'.$subtest);
1120              
1121 126         179 my $match;
1122 126 50       609 if (!defined $subtest) {
    50          
    50          
1123             # this zone is a simple rule, not a set of subrules
1124             # skip any A record that isn't on 127/8
1125 0 0 0     0 if ($rr_type eq 'A' && $rdatastr !~ /^127\./) {
1126 0         0 warn("uridnsbl: bogus rr for domain=$dom, rule=$rulename, id=" .
1127             $pkt->header->id." rr=".$rr->string);
1128 0         0 next;
1129             }
1130 0         0 $match = 1;
1131             } elsif ($subtest eq $rdatastr) {
1132 0         0 $match = 1;
1133             } elsif ($subtest =~ m{^ (\d+) (?: ([/-]) (\d+) )? \z}x) {
1134 126         420 my($n1,$delim,$n2) = ($1,$2,$3);
1135 126 50 100     536 $match =
    100 100        
    100          
1136             !defined $n2 ? ($rdatanum & $n1) && # mask only
1137             (($rdatanum & 0xff000000) == 0x7f000000) # 127/8
1138             : $delim eq '-' ? $rdatanum >= $n1 && $rdatanum <= $n2 # range
1139             : $delim eq '/' ? ($rdatanum & $n2) == ($n1 & $n2) # value/mask
1140             : 0;
1141              
1142 126 100       614 dbg("uridnsbl: %s . %s -> %s, %s, %08x %s %s",
    100          
    100          
1143             $dom, $zone, $rdatastr, $rulename, $rdatanum,
1144             !defined $n2 ? sprintf('& %08x', $n1)
1145             : $n1 == $n2 ? sprintf('== %08x', $n1)
1146             : sprintf('%08x%s%08x', $n1,$delim,$n2),
1147             $match ? 'match' : 'no');
1148             }
1149 126 100       454 $self->got_dnsbl_hit($pms, $ent, $rdatastr, $dom, $rulename) if $match;
1150             }
1151             }
1152              
1153             sub got_dnsbl_hit {
1154 42     42 0 92 my ($self, $pms, $ent, $str, $dom, $rulename) = @_;
1155              
1156 42         85 $str =~ s/\s+/ /gs; # long whitespace => short
1157 42         138 dbg("uridnsbl: domain \"$dom\" listed ($rulename): $str");
1158              
1159 42 100       104 if (!defined $pms->{uridnsbl_hits}->{$rulename}) {
1160 34         83 $pms->{uridnsbl_hits}->{$rulename} = { };
1161             };
1162 42         97 $pms->{uridnsbl_hits}->{$rulename}->{$dom} = 1;
1163              
1164 42 50 33     457 if ( $pms->{uridnsbl_active_rules_nsrevipbl}->{$rulename}
      33        
      33        
      66        
      66        
      33        
1165             || $pms->{uridnsbl_active_rules_arevipbl}->{$rulename}
1166             || $pms->{uridnsbl_active_rules_nsrhsbl}->{$rulename}
1167             || $pms->{uridnsbl_active_rules_fullnsrhsbl}->{$rulename}
1168             || $pms->{uridnsbl_active_rules_rhsbl}->{$rulename}
1169             || $pms->{uridnsbl_active_rules_rhsbl_ipsonly}->{$rulename}
1170             || $pms->{uridnsbl_active_rules_rhsbl_domsonly}->{$rulename})
1171             {
1172             # TODO: this needs to handle multiple domain hits per rule
1173 42         144 $pms->clear_test_state();
1174 42         74 my $uris = join (' ', keys %{$pms->{uridnsbl_hits}->{$rulename}});
  42         148  
1175 42         170 $pms->test_log ("URIs: $uris");
1176 42         132 $pms->got_hit ($rulename, "");
1177              
1178             # note that this rule has completed (since it got at least 1 hit)
1179 42         144 $pms->register_async_rule_finish($rulename);
1180             }
1181             }
1182              
1183             # ---------------------------------------------------------------------------
1184              
1185             # capability checks for "if can()":
1186             #
1187 0     0 0   sub has_tflags_domains_only { 1 }
1188 0     0 0   sub has_subtest_for_ranges { 1 }
1189 0     0 0   sub has_uridnsbl_for_a { 1 } # uridnsbl rules recognize tflags 'a' and 'ns'
1190              
1191             1;