File Coverage

blib/lib/Mail/SpamAssassin/Plugin/HashBL.pm
Criterion Covered Total %
statement 45 306 14.7
branch 5 186 2.6
condition 1 27 3.7
subroutine 11 27 40.7
pod 7 12 58.3
total 69 558 12.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             HashBL - query hashed (and unhashed) DNS blocklists
21              
22             =head1 SYNOPSIS
23              
24             loadplugin Mail::SpamAssassin::Plugin::HashBL
25              
26             # NON-WORKING usage examples below, replace xxx.example.invalid with real list
27             # See documentation below for detailed usage
28              
29             header HASHBL_EMAIL eval:check_hashbl_emails('ebl.example.invalid')
30             describe HASHBL_EMAIL Message contains email address found on EBL
31             priority HASHBL_EMAIL -100 # required priority to launch async lookups early
32             tflags HASHBL_EMAIL net
33              
34             hashbl_acl_freemail gmail.com
35             header HASHBL_OSENDR eval:check_hashbl_emails('rbl.example.invalid/A', 'md5/max=10/shuffle', 'X-Original-Sender', '^127\.', 'freemail')
36             describe HASHBL_OSENDR Message contains email address found on HASHBL
37             priority HASHBL_OSENDR -100 # required priority to launch async lookups early
38             tflags HASHBL_OSENDR net
39              
40             body HASHBL_BTC eval:check_hashbl_bodyre('btcbl.example.invalid', 'sha1/max=10/shuffle', '\b([13][a-km-zA-HJ-NP-Z1-9]{25,34})\b')
41             describe HASHBL_BTC Message contains BTC address found on BTCBL
42             priority HASHBL_BTC -100 # required priority to launch async lookups early
43             tflags HASHBL_BTC net
44              
45             header HASHBL_URI eval:check_hashbl_uris('rbl.example.invalid', 'sha1', '127.0.0.32')
46             describe HASHBL_URI Message contains uri found on rbl
47             priority HASHBL_URI -100 # required priority to launch async lookups early
48             tflags HASHBL_URI net
49              
50             =head1 DESCRIPTION
51              
52             This plugin support multiple types of hashed or unhashed DNS blocklists.
53              
54             OPTS refers to multiple generic options:
55              
56             raw do not hash data, query as is
57             md5 hash query with MD5
58             sha1 hash query with SHA1
59             case keep case before hashing, default is to lowercase
60             max=x maximum number of queries
61             shuffle if max exceeded, random shuffle queries before truncating to limit
62              
63             Multiple options can be separated with slash or other non-word character.
64             If OPTS is empty ('') or missing, default is used.
65              
66             HEADERS refers to slash separated list of Headers to process:
67              
68             ALL all headers
69             ALLFROM all From headers as returned by $pms->all_from_addrs()
70             EnvelopeFrom message envelope from (Return-Path etc)
71             HeaderName any header as used with $pms->get()
72              
73             if HEADERS is empty ('') or missing, default is used.
74              
75             =over 4
76              
77             =item header RULE check_hashbl_emails('bl.example.invalid/A', 'OPTS', 'HEADERS/body', '^127\.')
78              
79             Check email addresses from DNS list, "body" can be specified along with
80             headers to search body for emails. Optional subtest regexp to match DNS
81             answer. Note that eval rule type must always be "header".
82              
83             DNS query type can be appended to list with /A (default) or /TXT.
84              
85             Additional supported OPTS:
86              
87             nodot strip username dots from email
88             notag strip username tags from email
89             nouri ignore emails inside uris
90             noquote ignore emails inside < > or possible quotings
91              
92             Default OPTS: sha1/notag/noquote/max=10/shuffle
93              
94             Default HEADERS: ALLFROM/Reply-To/body
95              
96             For existing public email blacklist, see: http://msbl.org/ebl.html
97              
98             # Working example, see http://msbl.org/ebl.html before usage
99             header HASHBL_EMAIL eval:check_hashbl_emails('ebl.msbl.org')
100             describe HASHBL_EMAIL Message contains email address found on EBL
101             priority HASHBL_EMAIL -100 # required priority to launch async lookups early
102             tflags HASHBL_EMAIL net
103              
104             =over 4
105              
106             =item header RULE check_hashbl_uris('bl.example.invalid/A', 'OPTS', '^127\.')
107              
108             Check uris from DNS list, optional subtest regexp to match DNS
109             answer.
110              
111             DNS query type can be appended to list with /A (default) or /TXT.
112              
113             Default OPTS: sha1/max=10/shuffle
114              
115             =back
116              
117             =item body RULE check_hashbl_bodyre('bl.example.invalid/A', 'OPTS', '\b(match)\b', '^127\.')
118              
119             Search body for matching regexp and query the string captured. Regexp must
120             have a single capture ( ) for the string ($1). Optional subtest regexp to
121             match DNS answer. Note that eval rule type must be "body" or "rawbody".
122              
123             =back
124              
125             =cut
126              
127             use strict;
128 20     20   140 use warnings;
  20         40  
  20         569  
129 20     20   113  
  20         43  
  20         909  
130             my $VERSION = 0.101;
131              
132             use Digest::MD5 qw(md5_hex);
133 20     20   126 use Digest::SHA qw(sha1_hex);
  20         46  
  20         1074  
134 20     20   133  
  20         48  
  20         836  
135             use Mail::SpamAssassin::Plugin;
136 20     20   121 use Mail::SpamAssassin::Util qw(compile_regexp);
  20         49  
  20         586  
137 20     20   128  
  20         52  
  20         67648  
138             our @ISA = qw(Mail::SpamAssassin::Plugin);
139              
140             my $msg = shift;
141             Mail::SpamAssassin::Plugin::dbg("HashBL: $msg", @_);
142 60     60 1 184 }
143 60         348  
144             my ($class, $mailsa) = @_;
145              
146             $class = ref($class) || $class;
147 61     61 1 216 my $self = $class->SUPER::new($mailsa);
148             bless ($self, $class);
149 61   33     421  
150 61         311 # are network tests enabled?
151 61         163 if ($mailsa->{local_tests_only}) {
152             $self->{hashbl_available} = 0;
153             dbg("local tests only, disabling HashBL");
154 61 100       292 } else {
155 60         291 $self->{hashbl_available} = 1;
156 60         268 }
157              
158 1         4 $self->register_eval_rule("check_hashbl_emails");
159             $self->register_eval_rule("check_hashbl_uris");
160             $self->register_eval_rule("check_hashbl_bodyre");
161 61         308 $self->set_config($mailsa->{conf});
162 61         186  
163 61         202 return $self;
164 61         247 }
165              
166 61         527 my($self, $conf) = @_;
167             my @cmds;
168              
169             push (@cmds, {
170 61     61 0 184 setting => 'hashbl_ignore',
171 61         124 is_admin => 1,
172             type => $Mail::SpamAssassin::Conf::CONF_TYPE_HASH_KEY_VALUE,
173             default => {},
174             code => sub {
175             my ($self, $key, $value, $line) = @_;
176             if (!defined $value || $value eq '') {
177             return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
178             }
179 0     0   0 foreach my $str (split (/\s+/, $value)) {
180 0 0 0     0 $self->{hashbl_ignore}->{lc $str} = 1;
181 0         0 }
182             }
183 0         0 });
184 0         0  
185             $conf->{parser}->register_commands(\@cmds);
186             }
187 61         670  
188             my ($self, $acl) = @_;
189 61         316  
190             if (not defined $acl) {
191             return ();
192             }
193 0     0   0 $acl =~ s/\s+//g;
194             if ($acl !~ /^[a-z0-9]{1,32}$/) {
195 0 0       0 warn("invalid acl name: $acl");
196 0         0 return ();
197             }
198 0         0 if ($acl eq 'all') {
199 0 0       0 return ();
200 0         0 }
201 0         0 if (defined $self->{hashbl_acl}{$acl}) {
202             warn("no such acl defined: $acl");
203 0 0       0 return ();
204 0         0 }
205             }
206 0 0       0  
207 0         0 my ($self, $opt) = @_;
208 0         0  
209             if ($opt->{key} =~ /^hashbl_acl_([a-z0-9]{1,32})$/i) {
210             $self->inhibit_further_callbacks();
211             return 1 unless $self->{hashbl_available};
212              
213 0     0 1 0 my $acl = lc($1);
214             my @opts = split(/\s+/, $opt->{value});
215 0 0       0 foreach my $tmp (@opts)
216 0         0 {
217 0 0       0 if ($tmp =~ /^(\!)?(\S+)$/i) {
218             my $neg = $1;
219 0         0 my $value = lc($2);
220 0         0  
221 0         0 if (defined $neg) {
222             $self->{hashbl_acl}{$acl}{$value} = 0;
223 0 0       0 } else {
224 0         0 next if $acl eq 'all';
225 0         0 # exclusions overrides
226             if ( not defined $self->{hashbl_acl}{$acl}{$value} ) {
227 0 0       0 $self->{hashbl_acl}{$acl}{$value} = 1
228 0         0 }
229             }
230 0 0       0 } else {
231             warn("invalid acl: $tmp");
232 0 0       0 }
233 0         0 }
234             return 1;
235             }
236             return 0;
237 0         0 }
238              
239             my ($self, $opts) = @_;
240 0         0  
241             return 0 if !$self->{hashbl_available};
242 0         0  
243             # valid_tlds_re will be available at finish_parsing_end, compile it now,
244             # we only need to do it once and before possible forking
245             if (!exists $self->{email_re}) {
246 61     61 1 170 $self->_init_email_re();
247             }
248 61 100       363  
249             return 0;
250             }
251              
252 1 50       3 my ($self) = @_;
253 1         4  
254             # Some regexp tips courtesy of http://www.regular-expressions.info/email.html
255             # full email regex v0.02
256 1         4 $self->{email_re} = qr/
257             (?=.{0,64}\@) # limit userpart to 64 chars (and speed up searching?)
258             (?<![a-z0-9!#\$%&'*+\/=?^_`{|}~-]) # start boundary
259             ( # capture email
260 1     1   2 [a-z0-9!#\$%&'*+\/=?^_`{|}~-]+ # no dot in beginning
261             (?:\.[a-z0-9!#\$%&'*+\/=?^_`{|}~-]+)* # no consecutive dots, no ending dot
262             \@
263             (?:[a-z0-9](?:[a-z0-9-]{0,59}[a-z0-9])?\.){1,4} # max 4x61 char parts (should be enough?)
264 1         4367 $self->{main}->{registryboundaries}->{valid_tlds_re} # ends with valid tld
265             )
266             /xi;
267              
268             # default email whitelist
269             $self->{email_whitelist} = qr/
270             ^(?:
271             abuse|support|sales|info|helpdesk|contact|kontakt
272             | (?:post|host|domain)master
273             | undisclosed.* # yahoo.com etc(?)
274             | request-[a-f0-9]{16} # live.com
275             | bounced?- # yahoo.com etc
276             | [a-f0-9]{8}(?:\.[a-f0-9]{8}|-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}) # gmail msgids?
277 1         8 | .+=.+=.+ # gmail forward
278             )\@
279             /xi;
280             }
281              
282             my ($self, $pms, $opts, $from, $acl) = @_;
283              
284             my @emails; # keep find order
285             my %seen;
286             my @tmp_email;
287             my $domain;
288              
289             foreach my $hdr (split(/\//, $from)) {
290             my $parsed_emails = $self->_parse_emails($pms, $opts, $hdr);
291 0     0     foreach (@$parsed_emails) {
292             next if exists $seen{$_};
293 0           my @tmp_email = split('@', $_);
294             my $domain = $tmp_email[1];
295 0           if (defined($acl) and ($acl ne "all") and defined($domain)) {
296 0           if (defined($self->{hashbl_acl}{$acl}{$domain}) and ($self->{hashbl_acl}{$acl}{$domain} eq 1)) {
297             push @emails, $_;
298 0           $seen{$_} = 1;
299 0           }
300 0           } else {
301 0 0         push @emails, $_;
302 0           $seen{$_} = 1;
303 0           }
304 0 0 0       }
      0        
305 0 0 0       }
306 0            
307 0           return \@emails;
308             }
309              
310 0           my ($self, $pms, $opts, $hdr) = @_;
311 0            
312             if (exists $pms->{hashbl_email_cache}{$hdr}) {
313             return $pms->{hashbl_email_cache}{$hdr};
314             }
315              
316 0           if ($hdr eq 'ALLFROM') {
317             my @emails = $pms->all_from_addrs();
318             return $pms->{hashbl_email_cache}{$hdr} = \@emails;
319             }
320 0     0      
321             if (not defined $pms->{hashbl_whitelist}) {
322 0 0         %{$pms->{hashbl_whitelist}} = map { lc($_) => 1 }
323 0           ( $pms->get("X-Original-To:addr"),
324             $pms->get("Apparently-To:addr"),
325             $pms->get("Delivered-To:addr"),
326 0 0         $pms->get("Envelope-To:addr"),
327 0           );
328 0           if ( defined $pms->{hashbl_whitelist}{''} ) {
329             delete $pms->{hashbl_whitelist}{''};
330             }
331 0 0         }
332 0            
  0            
  0            
333             my $str = '';
334             if ($hdr eq 'ALL') {
335             $str = join("\n", $pms->get('ALL'));
336             } elsif ($hdr eq 'body') {
337             # get all <a href="mailto:", since they don't show up on stripped_body
338 0 0         my $uris = $pms->get_uri_detail_list();
339 0           while (my($uri, $info) = each %{$uris}) {
340             if (defined $info->{types}->{a} && !defined $info->{types}->{parsed}) {
341             if ($uri =~ /^mailto:(.+)/i) {
342             $str .= "$1\n";
343 0           }
344 0 0         }
    0          
345 0           }
346             my $body = join('', @{$pms->get_decoded_stripped_body_text_array()});
347             if ($opts =~ /\bnouri\b/) {
348 0           # strip urls with possible emails inside
349 0           $body =~ s#<?https?://\S{0,255}(?:\@|%40)\S{0,255}# #gi;
  0            
350 0 0 0       }
351 0 0         if ($opts =~ /\bnoquote\b/) {
352 0           # strip emails contained in <>, not mailto:
353             # also strip ones followed by quote-like "wrote:" (but not fax: and tel: etc)
354             $body =~ s#<?(?<!mailto:)$self->{email_re}(?:>|\s{1,10}(?!(?:fa(?:x|csi)|tel|phone|e?-?mail))[a-z]{2,11}:)# #gi;
355             }
356 0           $str .= $body;
  0            
357 0 0         } else {
358             $str .= join("\n", $pms->get($hdr));
359 0           }
360              
361 0 0         my @emails; # keep find order
362             my %seen;
363              
364 0           while ($str =~ /($self->{email_re})/g) {
365             next if exists $seen{$1};
366 0           push @emails, $1;
367             }
368 0            
369             return $pms->{hashbl_email_cache}{$hdr} = \@emails;
370             }
371 0            
372             my ($self, $pms, $list, $opts, $from, $subtest, $acl) = @_;
373              
374 0           return 0 if !$self->{hashbl_available};
375 0 0         return 0 if !$pms->is_dns_available();
376 0           return 0 if !$self->{email_re};
377              
378             my $rulename = $pms->get_current_eval_rule_name();
379 0            
380             if (!defined $list) {
381             warn "HashBL: $rulename blocklist argument missing\n";
382             return 0;
383 0     0 1   }
384              
385 0 0         if ($subtest) {
386 0 0         my ($rec, $err) = compile_regexp($subtest, 0);
387 0 0         if (!$rec) {
388             warn "HashBL: $rulename invalid subtest regex: $@\n";
389 0           return 0;
390             }
391 0 0         $subtest = $rec;
392 0           }
393 0            
394             # Defaults
395             $opts = 'sha1/notag/noquote/max=10/shuffle' if !$opts;
396 0 0          
397 0           $from = 'ALLFROM/Reply-To/body' if !$from;
398 0 0          
399 0           # Find all emails
400 0           my $emails = $self->_get_emails($pms, $opts, $from, $acl);
401             if (!@$emails) {
402 0           if(defined $acl) {
403             dbg("$rulename: no emails found ($from) on acl $acl");
404             } else {
405             dbg("$rulename: no emails found ($from)");
406 0 0         }
407             return 0;
408 0 0         } else {
409             dbg("$rulename: raw emails found: ".join(', ', @$emails));
410             }
411 0            
412 0 0         # Filter list
413 0 0         my $keep_case = $opts =~ /\bcase\b/i;
414 0           my $nodot = $opts =~ /\bnodot\b/i;
415             my $notag = $opts =~ /\bnotag\b/i;
416 0           my @filtered_emails; # keep order
417             my %seen;
418 0           foreach my $email (@$emails) {
419             next if exists $seen{$email};
420 0           next if $email !~ /.*\@.*/;
421             if (($email =~ $self->{email_whitelist}) or defined ($pms->{hashbl_whitelist}{$email})) {
422             dbg("Address whitelisted: $email");
423             next;
424 0           }
425 0           if ($nodot || $notag) {
426 0           my ($username, $domain) = ($email =~ /(.*)(\@.*)/);
427 0           $username =~ tr/.//d if $nodot;
428             $username =~ s/\+.*// if $notag;
429 0           $email = $username.$domain;
430 0 0         }
431 0 0         push @filtered_emails, $keep_case ? $email : lc($email);
432 0 0 0       $seen{$email} = 1;
433 0           }
434 0            
435             # Randomize order
436 0 0 0       if ($opts =~ /\bshuffle\b/) {
437 0           Mail::SpamAssassin::Util::fisher_yates_shuffle(\@filtered_emails);
438 0 0         }
439 0 0          
440 0           # Truncate list
441             my $max = $opts =~ /\bmax=(\d+)\b/ ? $1 : 10;
442 0 0         $#filtered_emails = $max-1 if scalar @filtered_emails > $max;
443 0            
444             foreach my $email (@filtered_emails) {
445             $self->_submit_query($pms, $rulename, $email, $list, $opts, $subtest);
446             }
447 0 0          
448 0           return 0;
449             }
450              
451             my ($self, $pms, $list, $opts, $subtest) = @_;
452 0 0          
453 0 0         return 0 if !$self->{hashbl_available};
454             return 0 if !$pms->is_dns_available();
455 0            
456 0           my $rulename = $pms->get_current_eval_rule_name();
457              
458             if (!defined $list) {
459 0           warn "HashBL: $rulename blocklist argument missing\n";
460             return 0;
461             }
462              
463 0     0 1   if ($subtest) {
464             my ($rec, $err) = compile_regexp($subtest, 0);
465 0 0         if (!$rec) {
466 0 0         warn "HashBL: $rulename invalid subtest regex: $@\n";
467             return 0;
468 0           }
469             $subtest = $rec;
470 0 0         }
471 0            
472 0           # Defaults
473             $opts = 'sha1/max=10/shuffle' if !$opts;
474              
475 0 0         # Filter list
476 0           my $keep_case = $opts =~ /\bcase\b/i;
477 0 0          
478 0           if ($opts =~ /raw/) {
479 0           warn "HashBL: $rulename raw option invalid\n";
480             return 0;
481 0           }
482              
483             my $uris = $pms->get_uri_detail_list();
484             my %seen;
485 0 0         my @filtered_uris;
486              
487             while (my($uri, $info) = each %{$uris}) {
488 0           # we want to skip mailto: uris
489             next if ($uri =~ /^mailto:/i);
490 0 0         next if exists $seen{$uri};
491 0            
492 0           # no hosts/domains were found via this uri, so skip
493             next unless $info->{hosts};
494             next unless $info->{cleaned};
495 0           next unless $info->{types}->{a} || $info->{types}->{parsed};
496 0           foreach my $uri (@{$info->{cleaned}}) {
497             # check url
498             push @filtered_uris, $keep_case ? $uri : lc($uri);
499 0           }
  0            
500             $seen{$uri} = 1;
501 0 0         }
502 0 0          
503             # Randomize order
504             if ($opts =~ /\bshuffle\b/) {
505 0 0         Mail::SpamAssassin::Util::fisher_yates_shuffle(\@filtered_uris);
506 0 0         }
507 0 0 0        
508 0           # Truncate list
  0            
509             my $max = $opts =~ /\bmax=(\d+)\b/ ? $1 : 10;
510 0 0         $#filtered_uris = $max-1 if scalar @filtered_uris > $max;
511              
512 0           foreach my $furi (@filtered_uris) {
513             $self->_submit_query($pms, $rulename, $furi, $list, $opts, $subtest);
514             }
515              
516 0 0         return 0;
517 0           }
518              
519             my ($self, $pms, $bodyref, $list, $opts, $re, $subtest) = @_;
520              
521 0 0         return 0 if !$self->{hashbl_available};
522 0 0         return 0 if !$pms->is_dns_available();
523              
524 0           my $rulename = $pms->get_current_eval_rule_name();
525 0            
526             if (!defined $list) {
527             warn "HashBL: $rulename blocklist argument missing\n";
528 0           return 0;
529             }
530              
531             if (!$re) {
532 0     0 1   warn "HashBL: $rulename missing body regex\n";
533             return 0;
534 0 0         }
535 0 0         my ($rec, $err) = compile_regexp($re, 0);
536             if (!$rec) {
537 0           warn "HashBL: $rulename invalid body regex: $@\n";
538             return 0;
539 0 0         }
540 0           $re = $rec;
541 0            
542             if ($subtest) {
543             my ($rec, $err) = compile_regexp($subtest, 0);
544 0 0         if (!$rec) {
545 0           warn "HashBL: $rulename invalid subtest regex: $@\n";
546 0           return 0;
547             }
548 0           $subtest = $rec;
549 0 0         }
550 0            
551 0           # Defaults
552             $opts = 'sha1/max=10/shuffle' if !$opts;
553 0            
554             my $keep_case = $opts =~ /\bcase\b/i;
555 0 0          
556 0           # Search body
557 0 0         my @matches;
558 0           my %seen;
559 0           if (ref($bodyref) eq 'ARRAY') {
560             # body, rawbody
561 0           foreach (@$bodyref) {
562             while ($_ =~ /$re/gs) {
563             next if !defined $1;
564             my $match = $keep_case ? $1 : lc($1);
565 0 0         next if exists $seen{$match};
566             $seen{$match} = 1;
567 0           push @matches, $match;
568             }
569             }
570 0           } else {
571             # full
572 0 0         while ($$bodyref =~ /$re/gs) {
573             next if !defined $1;
574 0           my $match = $keep_case ? $1 : lc($1);
575 0           next if exists $seen{$match};
576 0 0         $seen{$match} = 1;
577 0 0         push @matches, $match;
578 0 0         }
579 0           }
580 0            
581             if (!@matches) {
582             dbg("$rulename: no matches found");
583             return 0;
584             } else {
585 0           dbg("$rulename: matches found: '".join("', '", @matches)."'");
586 0 0         }
587 0 0          
588 0 0         # Randomize order
589 0           if ($opts =~ /\bshuffle\b/) {
590 0           Mail::SpamAssassin::Util::fisher_yates_shuffle(\@matches);
591             }
592              
593             # Truncate list
594 0 0         my $max = $opts =~ /\bmax=(\d+)\b/ ? $1 : 10;
595 0           $#matches = $max-1 if scalar @matches > $max;
596 0            
597             foreach my $match (@matches) {
598 0           $self->_submit_query($pms, $rulename, $match, $list, $opts, $subtest);
599             }
600              
601             return 0;
602 0 0         }
603 0            
604             my ($self, $opts, $value) = @_;
605              
606             my $hashtype = $opts =~ /\b(raw|sha1|md5)\b/i ? lc($1) : 'sha1';
607 0 0         if ($hashtype eq 'sha1') {
608 0 0         return sha1_hex($value);
609             } elsif ($hashtype eq 'md5') {
610 0           return md5_hex($value);
611 0           } else {
612             return $value;
613             }
614 0           }
615              
616             my ($self, $pms, $rulename, $value, $list, $opts, $subtest) = @_;
617              
618 0     0     if (exists $pms->{conf}->{hashbl_ignore}->{lc $value}) {
619             dbg("query skipped, ignored string: $value");
620 0 0         return 1;
621 0 0         }
    0          
622 0            
623             my $hash = $self->_hash($opts, $value);
624 0           dbg("querying $value ($hash) from $list");
625              
626 0           if (exists $pms->{conf}->{hashbl_ignore}->{$hash}) {
627             dbg("query skipped, ignored hash: $value");
628             return 1;
629             }
630              
631 0     0     my $type = $list =~ s,/(A|TXT)$,,i ? uc($1) : 'A';
632             my $lookup = "$hash.$list";
633 0 0          
634 0           my $key = "HASHBL_EMAIL:$lookup";
635 0           my $ent = {
636             key => $key,
637             zone => $list,
638 0           rulename => $rulename,
639 0           type => "HASHBL",
640             hash => $hash,
641 0 0         value => $value,
642 0           subtest => $subtest,
643 0           };
644             $ent = $pms->{async}->bgsend_and_start_lookup($lookup, $type, undef, $ent,
645             sub { my ($ent, $pkt) = @_; $self->_finish_query($pms, $ent, $pkt); },
646 0 0         master_deadline => $pms->{master_deadline}
647 0           );
648             $pms->register_async_rule_start($rulename) if $ent;
649 0           }
650 0            
651             my ($self, $pms, $ent, $pkt) = @_;
652              
653             if (!$pkt) {
654             # $pkt will be undef if the DNS query was aborted (e.g. timed out)
655             dbg("lookup was aborted: $ent->{rulename} $ent->{key}");
656             return;
657             }
658              
659             my $dnsmatch = $ent->{subtest} ? $ent->{subtest} : qr/^127\./;
660 0     0     my @answer = $pkt->answer;
  0            
661             foreach my $rr (@answer) {
662 0           if ($rr->address =~ $dnsmatch) {
663 0 0         dbg("$ent->{rulename}: $ent->{zone} hit '$ent->{value}'");
664             $ent->{value} =~ s/\@/[at]/g;
665             $pms->test_log($ent->{value});
666             $pms->got_hit($ent->{rulename}, '', ruletype => 'eval');
667 0     0     $pms->register_async_rule_finish($ent->{rulename});
668             return;
669 0 0         }
670             }
671 0           }
672 0            
673             # Version features
674              
675 0 0         1;