File Coverage

blib/lib/Mail/SpamAssassin/Plugin/HashBL.pm
Criterion Covered Total %
statement 45 304 14.8
branch 5 184 2.7
condition 1 27 3.7
subroutine 11 27 40.7
pod 7 12 58.3
total 69 554 12.4


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