File Coverage

blib/lib/Mail/SPF/Iterator.pm
Criterion Covered Total %
statement 655 797 82.1
branch 428 682 62.7
condition 64 112 57.1
subroutine 46 50 92.0
pod 6 8 75.0
total 1199 1649 72.7


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Mail::SPF::Iterator - iterative SPF lookup
4              
5             =head1 SYNOPSIS
6              
7             use Net::DNS;
8             use Mail::SPF::Iterator;
9             use Mail::SPF::Iterator Debug =>1; # enable debugging
10             my $spf = Mail::SPF::Iterator->new(
11             $ip, # IP4|IP6 of client
12             $mailfrom, # from MAIL FROM:
13             $helo, # from HELO|EHLO
14             $myname, # optional: my hostname
15             {
16             default_spf => 'mx/24 ?all', # in case no record was found in DNS
17             pass_all => SPF_SoftFail, # treat records like '+all' as error
18             # rfc4408 => 1, # for compatibility only
19             }
20             );
21              
22             # could be other resolvers too
23             my $resolver = Net::DNS::Resolver->new;
24              
25             ### with nonblocking, but still in loop
26             ### (callbacks are preferred with non-blocking)
27             my ($result,@ans) = $spf->next; # initial query
28             while ( ! $result ) {
29             my @query = @ans;
30             die "no queries" if ! @query;
31             for my $q (@query) {
32             # resolve query
33             my $socket = $resolver->bgsend( $q );
34             ... wait...
35             my $answer = $resolver->bgread($socket);
36             ($result,@ans) = $spf->next(
37             $answer # valid answer
38             || [ $q, $resolver->errorstring ] # or DNS problem
39             );
40             last if $result; # got final result
41             last if @ans; # got more DNS queries
42             }
43             }
44              
45             ### OR with blocking:
46             ### ($result,@ans) = $spf->lookup_blocking( undef,$resolver );
47              
48             ### print mailheader
49             print "Received-SPF: ".$spf->mailheader;
50              
51             # $result = Fail|Pass|...
52             # $ans[0] = comment for Received-SPF
53             # $ans[1] = %hash with infos for Received-SPF
54             # $ans[2] = explanation in case of Fail
55              
56              
57              
58             =head1 DESCRIPTION
59              
60             This module provides an iterative resolving of SPF records. Contrary to
61             Mail::SPF, which does blocking DNS lookups, this module just returns the DNS
62             queries and later expects the responses.
63              
64             Lookup of the DNS records will be done outside of the module and can be done
65             in a event driven way. It is also possible to do many parallel SPF checks
66             in parallel without needing multiple threads or processes.
67              
68             This module can also make use of SenderID records for checking the C
69             part, but it will prefer SPF. It will only use DNS TXT records for looking up
70             SPF policies unless compatibility with RFC 4408 is explicitly enabled.
71              
72             See RFC 7208 (old RFC 4408) for SPF and RFC 4406 for SenderID.
73              
74             =head1 METHODS
75              
76             =over 4
77              
78             =item new( IP, MAILFROM, HELO, [ MYNAME ], [ \%OPT ] )
79              
80             Construct a new Mail::SPF::Iterator object, which maintains the state
81             between the steps of the iteration. For each new SPF check a new object has
82             to be created.
83              
84             IP is the IP if the client as string (IP4 or IP6).
85              
86             MAILFROM is the user@domain part from the MAIL FROM handshake, e.g. '<','>'
87             and any parameters removed. If only '<>' was given (like in bounces) the
88             value is empty.
89              
90             HELO is the string send within the HELO|EHLO dialog which should be a domain
91             according to the RFC but often is not.
92              
93             MYNAME is the name of the local host. It's only used if required by macros
94             inside the SPF record.
95              
96             OPT is used for additional arguments. Currently B can be used
97             to set a default SPF record in case no SPF/TXT records are
98             returned from DNS (useful values are for example 'mx ?all' or 'mx/24 ?all').
99             B can be set to true in case stricter compatibility is needed with RFC
100             4408 instead of RFC 7208, i.e. lookup of DNS SPF records, no limit on void DNS
101             lookups etc.
102             B can be set to the expected outcome in case a SPF policy gets found,
103             which would pass everything. Such policies are common used domains used by
104             spammers.
105              
106             Returns the new object.
107              
108             =item next([ ANSWER ])
109              
110             C will be initially called with no arguments to get initial DNS queries
111             and then will be called with the DNS answers.
112              
113             ANSWER is either a DNS packet with the response to a former query or C<< [
114             QUERY, REASON ] >> on failures, where QUERY is the DNS packet containing the
115             failed query and REASON the reason, why the query failed (like TIMEOUT).
116              
117             If a final result was achieved it will return
118             C<< ( RESULT, COMMENT, HASH, EXPLAIN ) >>. RESULT is the result, e.g. "Fail",
119             "Pass",.... COMMENT is the comment for the Received-SPF header. HASH contains
120             information about problem, mechanism for the Received-SPF header.
121             EXPLAIN will be set to the explain string if RESULT is Fail.
122              
123             If no final result was achieved yet it will either return
124             C<< (undef,@QUERIES) >> with a list of new queries to continue, C<< ('') >>
125             in case the ANSWER produced an error but got ignored, because there are
126             other queries open, or C<< () >> in case the ANSWER was ignored because it
127             did not match any open queries.
128              
129             =item mailheader
130              
131             Creates value for Received-SPF header based on the final answer from next().
132             Returns header as string (one line, no folding) or undef, if no final result
133             was found.
134             This creates only the value, not the 'Received-SPF' prefix.
135              
136             =item result
137              
138             Returns ( RESULT, COMMENT, HASH, EXPLAIN ) like the final C does or () if
139             the final result wasn't found yet.
140              
141             If the SPF record had an explain modifier, which needed DNS lookups to resolve
142             this method might return the result (although with incomplete explain) before
143             C does it.
144              
145             =item explain_default ( [ EXPLAIN ] )
146              
147             Sets default explanation string if EXPLAIN is given.
148             If it's called as a class method the default explanation string for the class
149             will be set, otherwise the default explanation string for the object.
150              
151             Returns the current default explanation string for the object or if non
152             given or if called as a class method the default explanation string for the
153             class.
154              
155             =item lookup_blocking ( [ TIMEOUT, RESOLVER ] )
156              
157             Quick way to get the SPF status.
158             This will simply call C until it gets a final result.
159              
160             TIMEOUT limits the lookup time and defaults to 20.
161             RESOLVER is a Net::DNS::Resolver object (or similar) and defaults to
162             C<< Net::DNS::Resolver->new >>.
163             Returns ( RESULT, COMMENT, HASH ) like the final C does.
164              
165             This is not the preferred way to use this module, because it's blocking, so
166             no lookups can be done in parallel in a single process/thread.
167              
168             =back
169              
170             =head1 EXPORTED SYMBOLS
171              
172             For convenience the constants SPF_TempError, SPF_PermError, SPF_Pass, SPF_Fail,
173             SPF_SoftFail, SPF_Neutral, SPF_None are by default exported, which have the values
174             C<"TempError">, C<"PermError"> ...
175              
176             =head2 Arguments to C/C
177              
178             The C symbols are available for import and are exported if no arguments
179             are given to C or C. Same effect with adding C<:DEFAULT> as an
180             argument. Additionally the following arguments are supported:
181              
182             =over 4
183              
184             =item DebugFunc => \&coderef
185              
186             Sets a custom debug function, which just takes on argument. If given it will be
187             called on all debug messages when debugging is active. This function takes as
188             the only argument the debug message.
189              
190             =item Debug => 1|0
191              
192             Switches debugging on/off.
193              
194             =back
195              
196             =head1 AUTHOR
197              
198             Steffen Ullrich
199              
200             =head1 COPYRIGHT
201              
202             Copyright by Steffen Ullrich.
203              
204             This module is free software; you can redistribute it and/or
205             modify it under the same terms as Perl itself.
206              
207             =cut
208              
209              
210 5     5   4677 use strict;
  5         16  
  5         123  
211 5     5   23 use warnings;
  5         7  
  5         484  
212              
213             package Mail::SPF::Iterator;
214              
215             our $VERSION = '1.118';
216              
217             use fields (
218             # values given in or derived from params to new()
219 5         21 'helo', # helo given in new()
220             'myname', # myname given in new()
221             'clientip4', # packed ip from new() if IP4
222             'clientip6', # packed ip from new() if IP6
223             'sender', # mailfrom|helo given in new()
224             'domain', # extracted from mailfrom|helo
225             'identity', # 'mailfrom' if sender is mailfrom, else 'helo'
226             'opt', # additional options like default_spf
227             # internal states and values
228             'mech', # list of unhandled mechanism for current SPF
229             'include_stack', # stack for handling includes
230             'redirect', # set to domain of redirect modifier of current SPF
231             'explain', # set to explain modifier of current SPF
232             'cb', # [$sub,@arg] for callback to DNS replies
233             'cbq', # list of queries from last mech incl state
234             'validated', # cache used in validation of hostnames for ptr and %{p}
235             'limit_dns_mech', # countdown for number of mechanism using DNS queries
236             'limit_dns_void', # countdown for number of void DNS queries
237             'explain_default', # default explanation of object specific
238             'result', # contains final result
239             'tmpresult', # contains the best result we have so far
240             'used_default_spf', # set to the default_spf from opt if used
241 5     5   1990 );
  5         7334  
242              
243 5     5   3015 use Net::DNS;
  5         429757  
  5         838  
244 5     5   88 use Socket;
  5         23  
  5         3302  
245 5     5   2232 use URI::Escape 'uri_escape';
  5         7979  
  5         360  
246 5     5   2723 use Data::Dumper;
  5         28258  
  5         373  
247 5     5   42 use base 'Exporter';
  5         11  
  5         1164  
248              
249              
250             ### check if IPv6 support is in Socket, otherwise try Socket6
251             my $can_ip6;
252             BEGIN {
253             $can_ip6 = eval {
254             require Socket;
255             Socket->import(qw(inet_pton inet_ntop));
256             Socket->import('AF_INET6') if ! defined &AF_INET6;
257             1;
258 5   33 5   14 } || eval {
259             require Socket6;
260             Socket6->import(qw( inet_pton inet_ntop));
261             Socket6->import('AF_INET6') if ! defined &AF_INET6;
262             1;
263             };
264 5 50       238 if ( ! $can_ip6 ) {
265 5     5   34 no strict 'refs';
  5         11  
  5         372  
266 0         0 *{'AF_INET6'} = *{'inet_pton'} = *{'inet_ntop'}
  0         0  
  0         0  
267 0         0 = sub { die "no IPv6 support" };
  0         0  
268             }
269             }
270              
271             ### create SPF_* constants and export them
272             our @EXPORT;
273             our @EXPORT_OK = '$DEBUG';
274 5     5   29 use constant SPF_Noop => '_NOOP';
  5         10  
  5         489  
275             my %ResultQ;
276             BEGIN {
277 5     5   16 my $i = 0;
278 5         18 $ResultQ{ &SPF_Noop } = $i++;
279 5         11 for (qw(None PermError TempError Neutral SoftFail Fail Pass)) {
280 5     5   32 no strict 'refs';
  5         112  
  5         418  
281 35         1817 *{"SPF_$_"} = eval "sub () { '$_' }";
  35         150  
282 35         145 push @EXPORT, "SPF_$_";
283 35         51748 $ResultQ{$_} = $i++;
284             }
285             }
286              
287             my $DEBUGFUNC;
288             our $DEBUG=0;
289             sub import {
290 12 100   12   33758 goto &Exporter::import if @_ == 1; # implicit :DEFAULT
291 6         26 my $i = 1;
292 6         30 while ( $i<@_ ) {
293 6 50       40 if ( $_[$i] eq 'DebugFunc' ) {
    50          
294 0         0 $DEBUGFUNC = $_[$i+1];
295 0         0 splice( @_,$i,2 );
296 0         0 next;
297             } elsif ( $_[$i] eq 'Debug' ) {
298 6         19 $DEBUG = $_[$i+1];
299 6         17 splice( @_,$i,2 );
300 6         19 next;
301             }
302 0         0 ++$i;
303             }
304 6 50       38 goto &Exporter::import if @_ >1; # not implicit :DEFAULT
305             }
306              
307              
308              
309             ### Debugging
310             sub DEBUG {
311 6878 50   6878 0 77233 $DEBUG or return; # check against debug level
312 6878 50       11256 goto &$DEBUGFUNC if $DEBUGFUNC;
313 6878         18574 my ($pkg,$file,$line) = caller;
314 6878         11055 my $msg = shift;
315 6878 50       11675 $msg = sprintf $msg,@_ if @_;
316 6878         22785 print STDERR "DEBUG: $pkg#$line: $msg\n";
317             }
318              
319             ### pre-compute masks for IP4, IP6
320             my (@mask4,@mask6);
321             {
322             my $m = '0' x 32;
323             $mask4[0] = pack( "B32",$m);
324             for (1..32) {
325             substr( $m,$_-1,1) = '1';
326             $mask4[$_] = pack( "B32",$m);
327             }
328              
329             $m = '0' x 128;
330             $mask6[0] = pack( "B32",$m);
331             for (1..128) {
332             substr( $m,$_-1,1) = '1';
333             $mask6[$_] = pack( "B128",$m);
334             }
335             }
336              
337             ### mapping char to result
338             my %qual2rv = (
339             '+' => SPF_Pass,
340             '-' => SPF_Fail,
341             '~' => SPF_SoftFail,
342             '?' => SPF_Neutral,
343             );
344              
345             ############################################################################
346             # NEW
347             # creates new SPF processing object
348             # Args: ($class,$ip,$mailfrom,$helo,?$myname,?\%opt)
349             # $ip: IP4/IP6 as string
350             # $mailfrom: user@domain of "mail from"
351             # $helo: info from helo|ehlo - should be domain name
352             # $myname: local name, used only for expanding macros (optional)
353             # %opt: optional additional arguments
354             # default_spf => ... : default SPF record if none from DNS
355             # Returns: $self
356             ############################################################################
357             sub new {
358 994     994 1 205082 my ($class,$ip,$mailfrom,$helo,$myname,$opt) = @_;
359 994         3222 my Mail::SPF::Iterator $self = fields::new($class);
360              
361 994 50       147590 my $domain =
    50          
    100          
    100          
362             $mailfrom =~m{\@([\w\-.]+)$} ? $1 :
363             $mailfrom =~m{\@\[([\da-f:\.]+)\]$}i ? $1 :
364             $helo =~m{\@([\w\-.]+)$} ? $1 :
365             $helo =~m{\@\[([\da-f:\.]+)\]$}i ? $1 :
366             $helo;
367 994 100       3230 my ($sender,$identity) = $mailfrom ne ''
368             ? ( $mailfrom,'mailfrom' )
369             : ( $helo,'helo' );
370              
371 994         1650 my $ip4 = eval { inet_aton($ip) };
  994         3860  
372 994   33     3109 my $ip6 = ! $ip4 && $can_ip6 && eval { inet_pton(AF_INET6,$ip) };
373 994 0 33     1965 die "no client IP4 or IP6 known (can_ip6=$can_ip6): $ip"
374             if ! $ip4 and ! $ip6;
375              
376 994 50       1921 if ( $ip6 ) {
377 0         0 my $m = inet_pton( AF_INET6,'::ffff:0.0.0.0' );
378 0 0       0 if ( ($ip6 & $m) eq $m ) {
379             # mapped IPv4
380 0         0 $ip4 = substr( $ip6,-4 );
381 0         0 $ip6 = undef;
382             }
383             }
384              
385 994         6789 %$self = (
386             clientip4 => $ip4, # IP of client
387             clientip6 => $ip6, # IP of client
388             domain => $domain, # current domain
389             sender => $sender, # sender (mailfrom|helo)
390             helo => $helo, # helo
391             identity => $identity, # 'helo'|'mailfrom'
392             myname => $myname, # name of mail host itself
393             include_stack => [], # stack in case of include
394             cb => undef, # callback for next DNS reply
395             cbq => [], # the DNS queries for cb
396             validated => {}, # validated IP/domain names for PTR and %{p}
397             limit_dns_mech => 10, # Limit on Number of DNS mechanism
398             limit_dns_void => 2, # Limit on Number of void DNS answers
399             mech => undef, # list of spf mechanism
400             redirect => undef, # redirect from SPF record
401             explain => undef, # explain from SPF record
402             result => undef, # final result [ SPF_*, info, \%hash ]
403             opt => $opt,
404             );
405 994         2997 return $self;
406             }
407              
408             ############################################################################
409             # return result
410             # Args: $self
411             # Returns: ($status,$info,$hash,$explain)
412             # $status: SPF_Pass|SPF_Fail|...
413             # $info: comment for Received-SPF header
414             # $hash: param for Received-SPF header
415             # $explain: explanation string on SPF_Fail
416             ############################################################################
417             sub result {
418 0     0 1 0 my Mail::SPF::Iterator $self = shift;
419 0 0       0 my $r = $self->{result} or return;
420 0         0 return @$r;
421             }
422              
423             ############################################################################
424             # get/set default explanation string
425             # Args: ($self,[$explain])
426             # $explain: default explanation string (will be set)
427             # Returns: $explain
428             # $explain: default explanation string
429             ############################################################################
430             {
431             my $default = 'SPF Check Failed';
432             sub explain_default {
433 266 50   266 1 929 if ( ref $_[0] ) {
434 266         425 my Mail::SPF::Iterator $self = shift;
435 266 50       615 $self->{explain_default} = shift if @_;
436             return defined $self->{explain_default}
437             ? $self->{explain_default}
438 266 50       1039 : $default;
439             } else {
440 0         0 shift; # class
441 0 0       0 $default = shift if @_;
442 0         0 return $default;
443             }
444             }
445             }
446              
447             ############################################################################
448             # lookup blocking
449             # not the intended way to use the module, but sometimes one needs to quickly
450             # lookup something, even if it's blocking
451             # Args: ($self,[$timeout,$resolver])
452             # $timeout: total timeout for lookups, default 20
453             # $resolver: Resolver object compatible to Net::DNS::Resolver, if not
454             # given a new Net::DNS::Resolver object will be created
455             # Returns: ($status,$info,$hash,$explain)
456             # see result()
457             ############################################################################
458             sub lookup_blocking {
459 0     0 1 0 my Mail::SPF::Iterator $self = shift;
460 0         0 my ($timeout,$resolver) = @_;
461              
462 0   0     0 my $expire = time() + ( $timeout || 20 ); # 20s: RFC4408, 10.1
463 0   0     0 $resolver ||= Net::DNS::Resolver->new;
464              
465 0         0 my ($status,@ans) = $self->next; # get initial queries
466 0         0 while ( ! $status ) {
467              
468             # expired ?
469 0         0 $timeout = $expire - time();
470 0 0       0 last if $timeout < 0;
471              
472 0         0 my @query = @ans;
473 0 0       0 die "no more queries but no final status" if ! @query;
474 0         0 for my $q (@query) {
475             #DEBUG( "next query: ".$q->string );
476 0         0 my $socket = $resolver->bgsend( $q );
477              
478 0         0 my $rin = '';
479 0         0 vec( $rin,fileno($socket),1) = 1;
480 0 0       0 select( $rin,undef,undef,$timeout ) or last;
481              
482 0         0 my $answer = $resolver->bgread( $socket );
483 0   0     0 ($status,@ans) = $self->next(
484             $answer || [ $q, $resolver->errorstring ]
485             );
486 0 0 0     0 last if $status or @ans;
487             }
488             }
489 0 0       0 my @rv = ! $status
490             ? ( SPF_TempError,'', { problem => 'DNS lookups timed out' } )
491             : ($status,@ans);
492 0 0       0 return wantarray ? @rv : $status;
493             }
494              
495             ############################################################################
496             # mailheader
497             # create value for Received-SPF header for final response
498             # Args: $self
499             # Returns: $hdrvalue
500             ############################################################################
501             sub mailheader {
502 994     994 1 30049 my Mail::SPF::Iterator $self = shift;
503 994 50       1331 my ($result,$info,$hash) = @{ $self->{result} || return };
  994         3405  
504             $result .= " (using default SPF of \"$self->{used_default_spf}\")"
505 994 50       2228 if $self->{used_default_spf};
506             return $result ." ". join( "; ", map {
507             # Quote: this is not exactly rfc2822 but should be enough
508 994         5835 my $v = $hash->{$_};
  4725         7036  
509 4725         7422 $v =~ s{([\"\\])}{\\$1}g;
510 4725         7040 $v =~ s{[\r\n]+}{ }g;
511 4725         7464 $v =~ s{^\s+}{};
512 4725         7800 $v =~ s{\s+$}{};
513 4725 100 66     15024 $v = qq("$v") if $v =~ m{[\s;()]} or $v eq '';
514 4725         13517 "$_=$v"
515             } sort keys %$hash );
516             }
517              
518              
519             ############################################################################
520             # next step in SPF lookup
521             # - verify that there are open queries for the DNS reply and that parameter
522             # in query match question+answer in reply
523             # - process dnsresp by the current callback
524             # - process callbacks result using _next_process_cbrv which returns either
525             # final result or more DNS questions
526             # Args: ($self,$dnsresp)
527             # $dnsresp: DNS reply
528             # Returns: (undef,@dnsq) | ($status,$info,\%param,$explain) | ()
529             # (undef,@dnsq): @dnsq are more DNS questions
530             # ($status,$info,\%param,$explain): final response
531             # (''): reply processed, but answer ignored (likely error)
532             # (): reply ignored, does not matching outstanding request
533             ############################################################################
534             sub next {
535 2861     2861 1 1909410 my Mail::SPF::Iterator $self = shift;
536 2861         3964 my $dnsresp = shift;
537              
538 2861 100       5777 if ( ! $dnsresp ) {
539             # no DNS response - must be initial call to next
540 994 50       2154 die "no DNS reply but callback given" if $self->{cb};
541 994         2171 return $self->_next_process_cbrv( $self->_query_txt_spf );
542             }
543              
544             # handle DNS reply
545 1867 50       4337 my $callback = $self->{cb} or die "no callback but DNS reply";
546 1867         2795 my $cb_queries = $self->{cbq};
547 1867 50       3512 if ( ! @$cb_queries ) {
548             # we've got a reply, but no outstanding queries - ignore
549 0 0       0 $DEBUG && DEBUG( "got reply w/o queries, ignoring" );
550 0         0 return;
551             }
552              
553             # extract query from reply
554 1867         2985 my ($question,$err,$qid);
555 1867 100       5933 if ( ! UNIVERSAL::isa( $dnsresp, 'Net::DNS::Packet' )) {
556             # probably [ $question, $errorstring ]
557 34         73 (my $query,$err) = @$dnsresp;
558 34         98 ($question) = $query->question;
559 34         195 $qid = $query->header->id;
560 34   50     325 $err ||= 'unknown error';
561 34         59 $dnsresp = $err;
562 34 50       191 $DEBUG && DEBUG( "error '$err' to query ".$question->string );
563             } else {
564 1833         4115 ($question) = $dnsresp->question;
565 1833         9320 $qid = $dnsresp->header->id;
566             }
567 1867         17551 my $qtype = $question->qtype;
568              
569             # check if the reply matches one of the open queries
570 1867         17640 my $found;
571 1867         3245 for (@$cb_queries) {
572 1891 100       4208 next if $qid != $_->{id}; # ID mismatch
573 1867 50       3580 next if $qtype ne $_->{q}->qtype; # type mismatch
574              
575 1867 50       17606 if ( lc($question->qname) eq lc($_->{q}->qname) ) {
576 1867         29439 $found = $_;
577 1867         2909 last;
578             }
579              
580             # in case of special characters the names might have the
581             # wire presentation \DDD or the raw presentation
582             # actual behavior depends on the Net::DNS version, so normalize
583 0         0 my $rname = lc($question->qname);
584 0         0 my $qname = lc($_->{q}->qname);
585 0 0       0 s{\\(?:(\d\d\d)|(.))}{ $2 || chr($1) }esg for($rname,$qname);
  0         0  
586 0 0       0 if ( $rname eq $qname ) {
587 0         0 $found = $_;
588 0         0 last;
589             }
590             }
591              
592 1867 50       5362 if ( ! $found ) {
    50          
593             # packet does not match our queries
594 0 0       0 $DEBUG && DEBUG( "found no open query for ".$question->string );
595 0         0 return; # ignore problem
596             } elsif ( ! $found->{pkt} ) {
597             # duplicate response - ignore
598 0 0       0 $DEBUG && DEBUG( "duplicate response, ignoring" );
599 0         0 return;
600             }
601              
602 1867         3307 delete $found->{pkt}; # no longer needed
603              
604             # found matching query
605             # check for error
606 1867 100       3322 if ( $err ) {
607             # if this temporary error is the best we have so far set it as tmpresult
608 34 100 66     134 if (! $self->{tmpresult} or
609             $ResultQ{ $self->{tmpresult}[0] } < $ResultQ{ &SPF_TempError }) {
610             $self->{tmpresult} = [ SPF_TempError,
611             "getting ".$found->{q}->qtype." for ".$found->{q}->qname,
612 30         91 { problem => "error getting DNS response: $err" }
613             ]
614             }
615              
616 34 100       582 if ( grep { $_->{pkt} } @$cb_queries ) {
  46 100       161  
617             # we still have outstanding queries, so we might still get answers
618             # -> return ('') as a sign, that we got an error to an outstanding
619             # request, but otherwise ignore this error
620 6 50       28 $DEBUG && DEBUG( "ignore error '$err', we still have oustanding queries" );
621 6         21 return ('');
622              
623             } elsif ( my $r = $self->{result} ) {
624             # we have a final result already, so this error occured only while
625             # trying to expand %{p} for explain
626             # -> ignore error, set to default explain and return final result
627 6 50       35 $DEBUG && DEBUG( "error looking up data for explain: $err" );
628 6         27 return @$r;
629              
630             } else {
631             # we have no final result - pick the best error we have so far
632 22 50       122 $DEBUG && DEBUG( "TempError: $err" );
633 22         41 $self->{result} = $self->{tmpresult};
634 22         62 _update_result_info($self);
635 22         47 return @{$self->{result}};
  22         92  
636             }
637             }
638              
639             # call callback with no records on error
640 1833         3414 my $rcode = $dnsresp->header->rcode;
641 1833         57186 my @answer = $dnsresp->answer;
642 1833 100 66     12729 if (!@answer or $rcode ne 'NOERROR') {
643 248         702 my ($sub,@arg) = @$callback;
644 248 100 100     1590 if ($sub != \&_got_TXT_exp
      100        
645             and ! $self->{opt}{rfc4408}
646             and --$self->{limit_dns_void} < 0) {
647 8         39 $self->{result} = [ SPF_PermError, "",
648             { problem => "Number of void DNS queries exceeded" }];
649 8         29 _update_result_info($self);
650 8         16 return @{$self->{result}};
  8         44  
651             }
652              
653 240         801 return $self->_next_process_cbrv(
654             $sub->($self,$qtype,$rcode,[],[],@arg));
655             }
656              
657             # extract answer and additional data
658             # verify if names and types in answer records match query
659             # handle CNAMEs
660 1585         3140 my $qname = $question->qname;
661 1585 50       14247 $qname =~s{\\(?:(\d\d\d)|(.))}{ $2 || chr($1) }esg; # presentation -> raw
  12         89  
662 1585         2915 $qname = lc($qname);
663 1585         2273 my (%cname,%ans);
664 1585         2574 for my $rr (@answer) {
665 1891         4591 my $rtype = $rr->type;
666             # changed between Net::DNS 0.63 and 0.64
667             # it reports now the presentation name instead of the raw name
668 1891 50       15892 ( my $name = $rr->name ) =~s{\\(?:(\d\d\d)|(.))}{ $2 || chr($1) }esg;
  12         135  
669 1891         18488 $name = lc($name);
670 1891 100       4435 if ( $rtype eq 'CNAME' ) {
    50          
671             # remember CNAME so that we can check that the answer record
672             # for $qtype matches name from query or CNAME which is an alias
673             # for name
674 4 50       16 if ( exists $cname{$name} ) {
675 0 0       0 $DEBUG && DEBUG( "more than one CNAME for same name" );
676 0         0 next; # XXX should we TempError instead of ignoring?
677             }
678 4         13 $cname{$name} = $rr->cname;
679             } elsif ( $rtype eq $qtype ) {
680 1887         2260 push @{ $ans{$name}},$rr;
  1887         6048  
681             } else {
682             # XXXX should we TempError instead of ignoring?
683 0 0       0 $DEBUG && DEBUG( "unexpected answer record for $qtype:$qname" );
684             }
685             }
686              
687             # find all valid names, usually there should be at most one CNAME
688             # works by starting with name from query, finding CNAMEs for it,
689             # adding these to set and finding next CNAMEs etc
690             # if there are unconnected CNAMEs they will be left in %cname
691 1585         3132 my @names = ($qname);
692 1585         3281 while ( %cname ) {
693 4 50       13 my @n = grep { defined $_ } delete @cname{@names} or last;
  4         20  
694 4         10 push @names, map { lc($_) } @n;
  4         15  
695             }
696 1585 50       2747 if ( %cname ) {
697             # Report but ignore - XXX should we TempError instead?
698 0 0       0 $DEBUG && DEBUG( "unrelated CNAME records ".Dumper(\%cname));
699             }
700              
701             # collect the RR for all valid names
702 1585         2131 my @ans;
703 1585         2583 for (@names) {
704 1589 100       3898 my $rrs = delete $ans{$_} or next;
705 1585         4381 push @ans,@$rrs;
706             }
707 1585 50       2933 if ( %ans ) {
708             # answer records which don't match name from query or via CNAME
709             # derived names
710             # Report but ignore - XXX should we TempError instead?
711 0 0       0 $DEBUG && DEBUG( "unrelated answer records for $qtype names=@names ".Dumper(\%ans));
712             }
713              
714 1585 50 33     3376 if ( ! @ans and @names>1 ) {
715             # according to RFC1034 all RR for the type should be put into
716             # the answer section together with the CNAMEs
717             # so if there are no RRs in this answer, we should assume, that
718             # there will be no RRs at all
719 0 0       0 $DEBUG && DEBUG( "no answer records for $qtype, but names @names" );
720             }
721              
722 1585         3078 my ($sub,@arg) = @$callback;
723 1585         4003 return $self->_next_process_cbrv(
724             $sub->($self,$qtype,$rcode,\@ans,[ $dnsresp->additional ],@arg));
725             }
726              
727             ############################################################################
728             # return list of DNS queries which are still open
729             # Args: ($self)
730             # Returns: @dnsq
731             ############################################################################
732             sub todo {
733             return
734 0 0       0 map { $_->{pkt} ? ($_->{pkt}):() }
735 0     0 0 0 @{ shift->{cbq} }
  0         0  
736             }
737              
738             ############################################################################
739             # fill information in hash of final result
740             # Args: ($self)
741             ############################################################################
742             sub _update_result_info {
743 1100     1100   1534 my Mail::SPF::Iterator $self = shift;
744 1100 50       2241 my $h = $self->{result} or return;
745 1100 100       2173 $h = $h->[2] or return;
746             $h->{'client-ip'} = $self->{clientip4}
747             ? inet_ntoa($self->{clientip4})
748 1051 50       5797 : inet_ntop(AF_INET6,$self->{clientip6});
749 1051         2554 $h->{helo} = $self->{helo};
750 1051         1824 $h->{identity} = $self->{identity};
751 1051 50       3471 $h->{'envelope-from'} = "<$self->{sender}>" if $self->{sender};
752             }
753              
754             ############################################################################
755             # process results from callback to DNS reply, called from next
756             # Args: ($self,@rv)
757             # @rv: result from callback, either
758             # @query - List of new Net::DNS::Packet queries for next step
759             # () - no result (go on with next step)
760             # (status,...) - final response
761             # Returns: ... - see sub next
762             ############################################################################
763             sub _next_process_cbrv {
764 2819     2819   79239 my Mail::SPF::Iterator $self = shift;
765 2819         5105 my @rv = @_; # results from callback to _mech*
766              
767             # resolving of %{p} in exp= mod or explain TXT results in @rv = ()
768             # see sub _validate_*
769 2819 100 100     6774 if ( $self->{result} && ! @rv ) {
770             # set to final result
771 20         35 @rv = @{ $self->{result}};
  20         65  
772             }
773              
774             # if the last mech (which was called with the DNS reply in sub next) got
775             # no match and no further questions we need to find the match or questions
776             # either by processing the next mech in the current SPF record, following
777             # a redirect or going the include stack up
778 2819 100       6296 @rv = $self->_next_mech if ! @rv;
779              
780 2819 100       17867 if ( UNIVERSAL::isa( $rv[0],'Net::DNS::Packet' )) {
781             # @rv is list of DNS packets
782 1735         3563 return $self->_next_rv_dnsq(@rv)
783             }
784              
785             # @rv is (status,...)
786             # status of SPF_Noop is special in that it returns nothing as a sign, that
787             # it just waits for more input
788             # Only used when we could get multiple responses, e.g when multiple DNS
789             # requests were send like in the query for SPF+TXT
790 1084 100       2547 if ( $rv[0] eq SPF_Noop ) {
791             die "NOOP but no open queries"
792 18 50       30 if ! grep { $_->{pkt} } @{$self->{cbq}};
  36         62  
  18         42  
793 18         80 return ('');
794             }
795              
796             # inside include the response is only pre-final,
797             # propagate it the include stack up:
798             # see RFC4408, 5.2 for propagation of results
799 1066         1599 while ( my $top = pop @{ $self->{include_stack} } ) {
  1180         3142  
800 116 50       504 $DEBUG && DEBUG( "pre-final response $rv[0]" );
801              
802 116 100 66     626 if ( $rv[0] eq SPF_TempError || $rv[0] eq SPF_PermError ) {
    50          
803             # keep
804             } elsif ( $rv[0] eq SPF_None ) {
805 0         0 $rv[0] = SPF_PermError; # change None to PermError
806             } else {
807             # go stack up, restore saved data
808 44         115 my $qual = delete $top->{qual};
809 44         199 while ( my ($k,$v) = each %$top ) {
810 176         511 $self->{$k} = $v;
811             }
812 44 100       117 if ( $rv[0] eq SPF_Pass ) {
813             # Pass == match -> set status to $qual
814 16         59 $rv[0] = $qual;
815             } else {
816             # ! Pass == non-match
817             # -> restart with @rv=() and go on with next mech
818 28         73 @rv = $self->_next_mech;
819 28 100       185 if ( UNIVERSAL::isa( $rv[0],'Net::DNS::Packet' )) {
820             # @rv is list of DNS packets
821 2         6 return $self->_next_rv_dnsq(@rv)
822             }
823             }
824             }
825             }
826              
827             # no more include stack
828             # -> @rv is the probably the final result, but check if we had a better
829             # one already
830 1064         1655 my $final;
831 1064 50 66     2666 if ($self->{tmpresult} and
832             $ResultQ{ $self->{tmpresult}[0] } > $ResultQ{ $rv[0] }) {
833 0         0 $final = $self->{result} = $self->{tmpresult};
834             } else {
835 1064         2519 $final = $self->{result} = [ @rv ];
836             }
837 1064         2601 _update_result_info($self);
838              
839             # now the only things left is to handle explain in case of SPF_Fail
840 1064 100       5822 return @$final if $final->[0] ne SPF_Fail; # finally done
841              
842             # set default explanation
843 324 100       1367 $final->[3] = $self->explain_default if ! defined $final->[3];
844              
845             # lookup TXT record for explain
846 324 100       964 if ( my $exp = delete $self->{explain} ) {
847 106 100       287 if (ref $exp) {
848 4 100       15 if ( my @dnsq = $self->_resolve_macro_p($exp)) {
849             # we need to do more DNS lookups for resolving %{p} macros
850             # inside the exp=... modifier, before we get the domain name
851             # which contains the TXT for explain
852 2 50       216 $DEBUG && DEBUG( "need to resolve %{p} in $exp->{macro}" );
853 2         6 $self->{explain} = $exp; # put back until resolved
854 2         8 return $self->_next_rv_dnsq(@dnsq)
855             }
856 2         7 $exp = $exp->{expanded};
857             }
858 104 50       288 if ( my @err = _check_domain( $exp, "explain:$exp" )) {
859             # bad domain: return unmodified final
860 0         0 return @$final;
861             }
862 104 50       500 $DEBUG && DEBUG( "lookup TXT for '$exp' for explain" );
863 104         344 $self->{cb} = [ \&_got_TXT_exp ];
864 104         430 return $self->_next_rv_dnsq( Net::DNS::Packet->new($exp,'TXT','IN'));
865             }
866              
867             # resolve macros in TXT record for explain
868 218 100       599 if ( my $exp = delete $final->[4] ) {
869             # we had a %{p} to resolve in the TXT we got for explain,
870             # see _got_TXT_exp -> should be expanded now
871 12         23 $final->[3] = $exp->{expanded};
872              
873             }
874              
875             # This was the last action needed
876 218         1341 return @$final;
877             }
878              
879             ############################################################################
880             # try to match or give more questions by
881             # - trying the next mechanism in the current SPF record
882             # - if there is no next mech try to redirect to another SPF record
883             # - if there is no redirect try to go include stack up
884             # - if there is no include stack return SPF_Neutral
885             # Args: $self
886             # Returns: @query|@final
887             # @query: new queries as list of Net::DNS::Packets
888             # @final: final SPF result (see sub next)
889             ############################################################################
890             sub _next_mech {
891 1077     1077   1507 my Mail::SPF::Iterator $self = shift;
892              
893 1077         1832 for my $dummy (1) {
894              
895             # if we have more mechanisms in the current SPF record take next
896 1201 100       1509 if ( my $next = shift @{$self->{mech}} ) {
  1201         3134  
897 1036         2981 my ($sub,$id,@arg) = @$next;
898 1036         2370 my @rv = $sub->($self,@arg);
899 1036 100       33316 redo if ! @rv; # still no match and no queries
900 920         3266 return @rv;
901             }
902              
903             # if no mechanisms in current SPF record but we have a redirect
904             # continue with the SPF record from the new location
905 165 100       458 if ( my $domain = $self->{redirect} ) {
906 108 50       262 if ( ref $domain ) {
907             # need to resolve %{p}
908 0 0 0     0 if ( $domain->{macro} and
909             ( my @rv = $self->_resolve_macro_p($domain))) {
910 0         0 return @rv;
911             }
912 0         0 $self->{redirect} = $domain = $domain->{expanded};
913             }
914 108 50       284 if ( my @err = _check_domain($domain,"redirect:$domain" )) {
915 0         0 return @err;
916             }
917              
918             return ( SPF_PermError, "",
919             { problem => "Number of DNS mechanism exceeded" })
920 108 100       371 if --$self->{limit_dns_mech} < 0;
921              
922             # reset state information
923 102         218 $self->{mech} = [];
924 102         180 $self->{explain} = undef;
925 102         159 $self->{redirect} = undef;
926              
927             # set domain to domain from redirect
928 102         190 $self->{domain} = $domain;
929              
930             # restart with new SPF record
931 102         225 return $self->_query_txt_spf;
932             }
933              
934             # if there are still no more mechanisms available and we are inside
935             # an include go up the include stack
936 57         104 my $st = $self->{include_stack};
937 57 100       162 if (@$st) {
938 8         22 my $top = pop @$st;
939 8         21 delete $top->{qual};
940 8         48 while ( my ($k,$v) = each %$top ) {
941 32         92 $self->{$k} = $v;
942             }
943             # continue with mech or redirect of upper SPF record
944 8         27 redo;
945             }
946             }
947              
948             # no mech, no redirect and no include stack
949             # -> give up finally and return SPF_Neutral
950 49         115 return ( SPF_Neutral,'no matches' );
951             }
952              
953             ############################################################################
954             # if @rv is list of DNS packets return them as (undef,@dnspkt)
955             # remember the queries so that the answers can later (sub next) verified
956             # against the queries
957             # Args: ($self,@dnsq)
958             # @dnsq: list of Net::DNS::Packet's
959             # Returns: (undef,@dnsq)
960             ############################################################################
961             sub _next_rv_dnsq {
962 1843     1843   10212 my Mail::SPF::Iterator $self = shift;
963 1843         3152 my @dnsq = @_;
964             # track queries for later verification
965             $self->{cbq} = [ map {
966 1843         2910 { q => ($_->question)[0], id => $_->header->id, pkt => $_ }
  2213         10620  
967             } @dnsq ];
968             $DEBUG && DEBUG( "need to lookup ".join( " | ",
969 1843 50       33864 map { "'".$_->{id}.'/'.$_->{q}->string."'" } @{$self->{cbq}}));
  2213         18663  
  1843         3445  
970 1843         10846 return ( undef,@dnsq );
971             }
972              
973             ############################################################################
974             # check if the domain has the right format
975             # this checks the domain before the macros got expanded
976             ############################################################################
977             sub _check_macro_domain {
978 458     458   1006 my ($domain,$why) = @_;
979             # 'domain-spec': see RFC4408 Appendix A for ABNF
980 458         1432 my $rx = qr{
981             # macro-string
982             (?:
983             [^%\s]+ |
984             % (?: { [slodipvh] \d* r? [.\-+,/_=]* } | [%\-_] )
985             )*
986             # domain-end
987             (?:(?:
988             # toplabel
989             \. [\da-z]*[a-z][\da-z]* |
990             \. [\da-z]+-[\-a-z\d]*[\da-z]
991             ) | (?:
992             # macro-expand
993             % (?: { [slodipvh] \d* r? [.\-+,/_=]* } | [%\-_] )
994             ))
995             }xi;
996 458         1117 _check_domain( $domain,$why,$rx);
997             }
998              
999             ############################################################################
1000             # check if the domain has the right format
1001             # this checks the domain after the macros got expanded
1002             ############################################################################
1003             sub _check_domain {
1004 2462     2462   4784 my ($domain,$why,$rx) = @_;
1005 2462 100       4697 $why = '' if ! defined $why;
1006              
1007             # domain name according to RFC2181 can be anything binary!
1008             # this is not only for host names
1009 2462   66     12535 $rx ||= qr{.*?};
1010              
1011 2462         3341 my @rv;
1012 2462 100 100     95777 if ( $domain =~m{[^\d.]}
1013             && $domain =~s{^($rx)\.?$}{$1} ) {
1014             # looks like valid domain name
1015 2378 100       8159 if ( grep { length == 0 || length>63 } split( m{\.},$domain,-1 )) {
  7394 100       24381  
    50          
1016 32         156 @rv = ( SPF_PermError,"query $why", { problem =>
1017             "DNS labels limited to 63 chars and should not be empty." });
1018             } elsif ( length($domain)>253 ) {
1019 0         0 @rv = ( SPF_PermError,"query $why",
1020             { problem => "Domain names limited to 253 chars." });
1021             } else {
1022             #DEBUG( "domain name ist OK" );
1023             return
1024 2346         10995 }
1025             } else {
1026 84         524 @rv = ( SPF_PermError, "query $why",
1027             { problem => "Invalid domain name" });
1028             }
1029              
1030 116 50       808 $DEBUG && DEBUG( "error with '$domain': ".$rv[2]{problem} );
1031 116         586 return @rv; # have error
1032             }
1033              
1034             ############################################################################
1035             # initial query
1036             # returns queries for SPF and TXT record, next state is _got_txt_spf
1037             ############################################################################
1038             sub _query_txt_spf {
1039 1226     1226   1656 my Mail::SPF::Iterator $self = shift;
1040 1226 50       4568 $DEBUG && DEBUG( "want SPF/TXT for $self->{domain}" );
1041             # return query for SPF and TXT, we see what we get first
1042 1226 100       3135 if ( my @err = _check_domain( $self->{domain}, "SPF/TXT record" )) {
1043 20 50       60 if ( ! $self->{cb} ) {
1044             # for initial query return SPF_None on errors
1045 20         40 $err[0] = SPF_None;
1046             }
1047 20         82 return @err;
1048             }
1049              
1050 1206         3085 $self->{cb} = [ \&_got_txt_spf ];
1051             return (
1052             # use SPF DNS record only if rfc4408 compatibility is required
1053             $self->{opt}{rfc4408}
1054             ? (scalar(Net::DNS::Packet->new( $self->{domain}, 'SPF','IN' ))):(),
1055 1206 100       7490 scalar(Net::DNS::Packet->new( $self->{domain}, 'TXT','IN' )),
1056             );
1057             }
1058              
1059             ############################################################################
1060             # processes response to SPF|TXT query
1061             # parses response and starts processing
1062             ############################################################################
1063             sub _got_txt_spf {
1064 1202     1202   6059 my Mail::SPF::Iterator $self = shift;
1065 1202         2321 my ($qtype,$rcode,$ans,$add) = @_;
1066              
1067             {
1068 1202 100       1583 last if ! @$ans;
  1202         2433  
1069              
1070             # RFC4408 says in 4.5:
1071             # 2. If any records of type SPF are in the set, then all records of
1072             # type TXT are discarded.
1073             # But it says that if both SPF and TXT are given they should be the
1074             # same (3.1.1)
1075             # so I think we can ignore the requirement 4.5.2 and just use the
1076             # first record which is valid SPF, if the admin of the domain sets
1077             # TXT and SPF to different values it's his own problem
1078              
1079 1160         1691 my (@spfdata,@senderid);
1080 1160         1906 for my $rr (@$ans) {
1081 1194         2747 my $txtdata = join( '', $rr->char_str_list );
1082 1194 100       37083 $txtdata =~m{^
1083             (?:
1084             (v=spf1)
1085             | spf2\.\d/(?:[\w,]*\bmfrom\b[\w,]*)
1086             )
1087             (?:$|\040\s*)(.*)
1088             }xi or next;
1089 1164 100       3393 if ( $1 ) {
1090 1156         2636 push @spfdata,$2;
1091 1156 50       4198 $DEBUG && DEBUG( "got spf data for $qtype: $txtdata" );
1092             } else {
1093 8         24 push @senderid,$2;
1094 8 50       36 $DEBUG && DEBUG( "got senderid data for $qtype: $txtdata" );
1095             }
1096             }
1097              
1098             # if SenderID and SPF are given prefer SPF, else use any
1099 1160 100       2426 @spfdata = @senderid if ! @spfdata;
1100              
1101 1160 100       2085 @spfdata or last; # no usable SPF reply
1102 1144 100       2540 if (@spfdata>1) {
1103 18         133 return ( SPF_PermError,
1104             "checking $qtype for $self->{domain}",
1105             { problem => "multiple SPF records" }
1106             );
1107             }
1108 1126 100       1660 unless ( eval { $self->_parse_spf( $spfdata[0] ) }) {
  1126         2750  
1109             # this is an invalid SPF record
1110             # make it a permanent error
1111             # it does not matter if the other type of record is good
1112             # because according to RFC if both provide SPF (v=spf1..)
1113             # they should be the same, so the other one should be bad too
1114 354         2431 return ( SPF_PermError,
1115             "checking $qtype for $self->{domain}",
1116             { problem => "invalid SPF record: $@" }
1117             );
1118             }
1119              
1120             # looks good, return so that next() processes the next query
1121 772         2752 return;
1122             }
1123              
1124             # If this is the first response, wait for the other
1125 58 50       291 $DEBUG && DEBUG( "no records for $qtype ($rcode)" );
1126 58 100       102 if ( grep { $_->{pkt} } @{ $self->{cbq}} ) {
  88         239  
  58         135  
1127 18         56 return (SPF_Noop);
1128             }
1129              
1130             # otherwise it means that we got no SPF or TXT records
1131              
1132             # if we have a default record and we are at the first level use this
1133 40 50 66     202 if (!$self->{mech} and my $default = $self->{opt}{default_spf}) {
1134 0 0       0 if (eval { $self->_parse_spf($default) }) {
  0         0  
1135             # good
1136 0         0 $self->{used_default_spf} = $default;
1137 0         0 return;
1138             }
1139 0         0 return (SPF_PermError,
1140             "checking default SPF for $self->{domain}",
1141             { problem => "invalid default SPF record: $@" }
1142             );
1143             }
1144              
1145             # return SPF_None if this was the initial query ($self->{mech} is undef)
1146             # and SPF_PermError if as a result from redirect or include
1147             # ($self->{mech} is [])
1148 40 50       151 $DEBUG && DEBUG( "no usable SPF/TXT records" );
1149 40 100       236 return ( $self->{mech} ? SPF_PermError : SPF_None,
1150             'query SPF/TXT record',
1151             { problem => 'no SPF records found' });
1152             }
1153              
1154              
1155             ############################################################################
1156             # parse SPF record, returns 1 if record looks valid,
1157             # otherwise die()s with somewhat helpful error message
1158             ############################################################################
1159             sub _parse_spf {
1160 1126     1126   1648 my Mail::SPF::Iterator $self = shift;
1161 1126         1540 my $data = shift;
1162              
1163 1126         1687 my (@mech,$redirect,$explain);
1164 1126         2933 for ( split( ' ', $data )) {
1165 1954 100       11850 my ($qual,$mech,$mod,$arg) = m{^(?:
1166             ([~\-+?]?) # Qualifier
1167             (all|ip[46]|a|mx|ptr|exists|include) # Mechanism
1168             |(redirect|exp) # Modifier
1169             |[a-zA-Z][\w.\-]*= # unknown modifier + '='
1170             )([ \t\x20-\x7e]*) # Arguments
1171             $}x
1172             or die "bad SPF part: $_\n";
1173              
1174 1900 100       4118 if ( $mech ) {
    100          
1175 1568   100     4781 $qual = $qual2rv{ $qual || '+' };
1176              
1177 1568 100 100     5765 if ( $mech eq 'all' ) {
    100          
    100          
    100          
    100          
    100          
    50          
1178 520 100       1141 die "no arguments allowed with mechanism 'all': '$_'\n"
1179             if $arg ne '';
1180 502         1612 push @mech, [ \&_mech_all, $_, $qual ]
1181              
1182             } elsif ( $mech eq 'ip4' ) {
1183 238 100       1541 my ($ip,$plen) =
1184             $arg =~m{^:(\d+\.\d+\.\d+\.\d+)(?:/([1-9]\d*|0))?$}
1185             or die "bad argument for mechanism 'ip4' in '$_'\n";
1186 208 100       589 $plen = 32 if ! defined $plen;
1187 208 100       576 $plen>32 and die "invalid prefix len >32 in '$_'\n";
1188 202 50       331 eval { $ip = inet_aton( $ip ) }
  202         985  
1189             or die "bad ip '$ip' in '$_'\n";
1190 202 50       569 next if ! $self->{clientip4}; # don't use for IP6
1191 202         810 push @mech, [ \&_mech_ip4, $_, $qual, $ip,$plen ];
1192              
1193             } elsif ( $mech eq 'ip6' ) {
1194 24 100       162 my ($ip,$plen) =
1195             $arg =~m{^:([\da-fA-F:\.]+)(?:/([1-9]\d*|0))?$}
1196             or die "bad argument for mechanism 'ip6' in '$_'\n";
1197 18 50       84 $plen = 128 if ! defined $plen;
1198 18 100       88 $plen>128 and die "invalid prefix len >128 in '$_'\n";
1199 12 50 50     32 eval { $ip = inet_pton( AF_INET6,$ip ) }
  12         76  
1200             or die "bad ip '$ip' in '$_'\n"
1201             if $can_ip6;
1202 12 50       49 next if ! $self->{clientip6}; # don't use for IP4
1203 0         0 push @mech, [ \&_mech_ip6, $_, $qual, $ip,$plen ];
1204              
1205             } elsif ( $mech eq 'a' or $mech eq 'mx' ) {
1206 514   100     1321 $arg ||= '';
1207 514 100       3263 my ($domain,$plen4,$plen6) =
1208             $arg =~m{^
1209             (?: : (.+?))? # [ ":" domain-spec ]
1210             (?: / (?: ([1-9]\d*|0) ))? # [ ip4-cidr-length ]
1211             (?: // (?: ([1-9]\d*|0) ))? # [ "/" ip6-cidr-length ]
1212             $}x or die "bad argument for mechanism '$mech' in '$_'\n";
1213              
1214 498 100       1126 $plen4 = 32 if ! defined $plen4;
1215 498 100       905 $plen6 = 128 if ! defined $plen6;
1216 498 100       1008 die "invalid prefix len >32 in '$_'\n" if $plen4>32;
1217 486 100       916 die "invalid prefix len >128 in '$_'\n" if $plen6>128;
1218 474 100       808 if ( ! $domain ) {
1219 288         497 $domain = $self->{domain};
1220             } else {
1221 186 100       434 if ( my @err = _check_macro_domain($domain)) {
1222 72   50     581 die(($err[2]->{problem}||"Invalid domain name")."\n");
1223             }
1224 114         387 $domain = $self->_macro_expand($domain);
1225             }
1226 402 100       1018 my $sub = $mech eq 'a' ? \&_mech_a : \&_mech_mx;
1227 402 50       865 push @mech, [ \&_resolve_macro_p, $domain ] if ref($domain);
1228             push @mech, [ $sub, $_, $qual, $domain,
1229 402 50       1732 $self->{clientip4} ? $plen4:$plen6 ];
1230              
1231             } elsif ( $mech eq 'ptr' ) {
1232 50 100 100     2887 my ($domain) = ( $arg || '' )=~m{^(?::([^/]+))?$}
1233             or die "bad argument for mechanism '$mech' in '$_'\n";
1234             $domain = $domain
1235             ? $self->_macro_expand($domain)
1236 38 100       146 : $self->{domain};
1237 38 50       108 push @mech, [ \&_resolve_macro_p, $_, $domain ] if ref($domain);
1238 38         177 push @mech, [ \&_mech_ptr, $_, $qual, $domain ];
1239              
1240             } elsif ( $mech eq 'exists' ) {
1241 58 100 100     426 my ($domain) = ( $arg || '' )=~m{^:([^/]+)$}
1242             or die "bad argument for mechanism '$mech' in '$_'\n";
1243 40         124 $domain = $self->_macro_expand($domain);
1244 26 100       119 push @mech, [ \&_resolve_macro_p, $_, $domain ] if ref($domain);
1245 26         114 push @mech, [ \&_mech_exists, $_, $qual, $domain ];
1246              
1247             } elsif ( $mech eq 'include' ) {
1248 164 100 100     988 my ($domain) = ( $arg || '' )=~m{^:([^/]+)$}
1249             or die "bad argument for mechanism '$mech' in '$_'\n";
1250 140         447 $domain = $self->_macro_expand($domain);
1251 140 50       378 push @mech, [ \&_resolve_macro_p, $_, $domain ] if ref($domain);
1252 140         581 push @mech, [ \&_mech_include, $_, $qual, $domain ];
1253              
1254             } else {
1255 0         0 die "unhandled mechanism '$mech'\n"
1256             }
1257              
1258             } elsif ( $mod ) {
1259             # multiple redirect or explain will be considered an error
1260 302 100       836 if ( $mod eq 'redirect' ) {
    50          
    0          
1261 144 100       372 die "redirect was specified more than once\n" if $redirect;
1262 138 100 50     743 my ($domain) = ( $arg || '' )=~m{^=([^/]+)$}
1263             or die "bad argument for modifier '$mod' in '$_'\n";
1264 126 100       291 if ( my @err = _check_macro_domain($domain)) {
1265 6   50     49 die(( $err[2]->{problem} || "Invalid domain name" )."\n" );
1266             }
1267 120         383 $redirect = $self->_macro_expand($domain);
1268              
1269             } elsif ( $mod eq 'exp' ) {
1270 158 100       392 die "$explain was specified more than once\n" if $explain;
1271 152 100 50     818 my ($domain) = ( $arg || '' )=~m{^=([^/]+)$}
1272             or die "bad argument for modifier '$mod' in '$_'\n";
1273 146 100       390 if ( my @err = _check_macro_domain($domain)) {
1274 12   50     102 die(( $err[2]->{problem} || "Invalid domain name" )."\n" );
1275             }
1276 134         422 $explain = $self->_macro_expand($domain);
1277              
1278             } elsif ( $mod ) {
1279 0         0 die "unhandled modifier '$mod'\n"
1280             }
1281             } else {
1282             # unknown modifier - check if arg is valid macro-string
1283             # (will die() on error) but ignore modifier
1284 30   50     123 $self->_macro_expand($arg || '');
1285             }
1286             }
1287              
1288 772 100       2260 if ($self->{opt}{pass_all}) {
1289 256         444 my $r = 0;
1290 256         531 for (@mech) {
1291 380         661 my $qual = $_->[2];
1292 380 100       962 last if $_->[0] == \&_mech_include;
1293 334 100       842 $r=-1,last if $qual eq SPF_Fail;
1294 226 100 100     898 $r=+1,last if $qual eq SPF_Pass and $_->[0] == \&_mech_all;
1295             }
1296 256 100       653 if ($r == 1) {
1297             # looks like a pass all rule
1298             $self->{result} = [
1299 6         26 $self->{opt}{pass_all}, "",
1300             { problem => "record designed to allow every sender" }
1301             ];
1302 6         24 _update_result_info($self);
1303             }
1304             }
1305 772         1471 $self->{mech} = \@mech;
1306 772         1320 $self->{explain} = $explain;
1307 772         1242 $self->{redirect} = $redirect;
1308 772         2153 return 1;
1309             }
1310              
1311             ############################################################################
1312             # handles mechanism 'all'
1313             # matches all time
1314             ############################################################################
1315             sub _mech_all {
1316 302     302   472 my Mail::SPF::Iterator $self = shift;
1317 302         503 my $qual = shift;
1318 302 50       1099 $DEBUG && DEBUG( "match mech all with qual=$qual" );
1319 302         1166 return ( $qual,'matches default', { mechanism => 'all' });
1320             }
1321              
1322             ############################################################################
1323             # handle mechanism 'ip4'
1324             # matches if clients IP4 address is in ip/mask
1325             ############################################################################
1326             sub _mech_ip4 {
1327 162     162   245 my Mail::SPF::Iterator $self = shift;
1328 162         389 my ($qual,$ip,$plen) = @_;
1329 162 50       438 defined $self->{clientip4} or return (); # ignore rule, no IP4 address
1330 162 100       625 if ( ($self->{clientip4} & $mask4[$plen]) eq ($ip & $mask4[$plen]) ) {
1331             # rules matches
1332 46 50       409 $DEBUG && DEBUG( "match mech ip4:".inet_ntoa($ip)."/$plen with qual=$qual" );
1333 46         353 return ($qual,"matches ip4:".inet_ntoa($ip)."/$plen",
1334             { mechanism => 'ip4' } )
1335             }
1336 116 50       901 $DEBUG && DEBUG( "no match mech ip4:".inet_ntoa($ip)."/$plen" );
1337 116         307 return (); # ignore, no match
1338             }
1339              
1340             ############################################################################
1341             # handle mechanism 'ip6'
1342             # matches if clients IP6 address is in ip/mask
1343             ############################################################################
1344             sub _mech_ip6 {
1345 0     0   0 my Mail::SPF::Iterator $self = shift;
1346 0         0 my ($qual,$ip,$plen) = @_;
1347 0 0       0 defined $self->{clientip6} or return (); # ignore rule, no IP6 address
1348 0 0       0 if ( ($self->{clientip6} & $mask6[$plen]) eq ($ip & $mask6[$plen])) {
1349             # rules matches
1350 0 0       0 $DEBUG && DEBUG( "match mech ip6:".inet_ntop(AF_INET6,$ip)."/$plen with qual=$qual" );
1351 0         0 return ($qual,"matches ip6:".inet_ntop(AF_INET6,$ip)."/$plen",
1352             { mechanism => 'ip6' } )
1353             }
1354 0 0       0 $DEBUG && DEBUG( "no match ip6:".inet_ntop(AF_INET6,$ip)."/$plen" );
1355 0         0 return (); # ignore, no match
1356             }
1357              
1358             ############################################################################
1359             # handle mechanism 'a'
1360             # check if one of the A/AAAA records for $domain resolves to
1361             # clientip/plen,
1362             ############################################################################
1363             sub _mech_a {
1364 266     266   404 my Mail::SPF::Iterator $self = shift;
1365 266         538 my ($qual,$domain,$plen) = @_;
1366 266 50       540 $domain = $domain->{expanded} if ref $domain;
1367 266 50       1205 $DEBUG && DEBUG( "check mech a:$domain/$plen with qual=$qual" );
1368 266 100       781 if ( my @err = _check_domain($domain, "a:$domain/$plen")) {
1369             # spec is not clear here:
1370             # variante1: no match on invalid domain name -> return
1371             # variante2: propagate err -> return @err
1372             # we use variante2 for now
1373 6 50       50 $DEBUG && DEBUG( "no match mech a:$domain/$plen - @err" );
1374 6         22 return @err;
1375             }
1376              
1377             return ( SPF_PermError, "",
1378             { problem => "Number of DNS mechanism exceeded" })
1379 260 100       814 if --$self->{limit_dns_mech} < 0;
1380              
1381 252 50       599 my $typ = $self->{clientip4} ? 'A':'AAAA';
1382 252         853 $self->{cb} = [ \&_got_A, $qual,$plen,[ $domain ],'a' ];
1383 252         973 return scalar(Net::DNS::Packet->new( $domain, $typ,'IN' ));
1384             }
1385              
1386             ############################################################################
1387             # this is used in _mech_a and in _mech_mx if the address for an MX is not
1388             # sent inside the additional data
1389             # in the case of MX $names might contain more than one name to resolve, it
1390             # will try to resolve names to addresses and to match them until @$names
1391             # is empty
1392             ############################################################################
1393             sub _got_A {
1394 299     299   1110 my Mail::SPF::Iterator $self = shift;
1395 299         798 my ($qtype,$rcode,$ans,$add,$qual,$plen,$names,$mech) = @_;
1396 299         602 my $domain = shift(@$names);
1397              
1398 299 50       1458 $DEBUG && DEBUG( "got response to $qtype for $domain: $rcode" );
1399 299 100       1010 if ( $rcode eq 'NXDOMAIN' ) {
    50          
1400 48 50       217 $DEBUG && DEBUG( "no match mech a:$domain/$plen - $rcode" );
1401             # no records found
1402             } elsif ( $rcode ne 'NOERROR' ) {
1403 0 0       0 $DEBUG && DEBUG( "temperror mech a:$domain/$plen - $rcode" );
1404 0         0 return ( SPF_TempError,
1405             "getting $qtype for $domain",
1406             { problem => "error resolving $domain" }
1407             );
1408             }
1409              
1410 299         587 my @addr = map { $_->address } @$ans;
  245         1044  
1411 299         1994 return _check_A_match($self,$qual,$domain,$plen,\@addr,$names,$mech);
1412             }
1413              
1414             sub _check_A_match {
1415 365     365   543 my Mail::SPF::Iterator $self = shift;
1416 365         878 my ($qual,$domain,$plen,$addr,$names,$mech) = @_;
1417              
1418             # process all found addresses
1419 365 50       785 if ( $self->{clientip4} ) {
1420 365 50       742 $plen = 32 if ! defined $plen;
1421 365         657 my $mask = $mask4[$plen];
1422 365         761 for my $addr (@$addr) {
1423 285 50       1096 $DEBUG && DEBUG( "check a:$domain($addr)/$plen for mech $mech" );
1424 285 50 33     1418 my $packed = $addr=~m{^[\d.]+$} && eval { inet_aton($addr) }
1425             or return ( SPF_TempError,
1426             "getting A for $domain",
1427             { problem => "bad address in A record" }
1428             );
1429              
1430 285 100       1061 if ( ($packed & $mask) eq ($self->{clientip4} & $mask) ) {
1431             # match!
1432 87 50       382 $DEBUG && DEBUG( "match mech a:.../$plen for mech $mech with qual $qual" );
1433 87         647 return ($qual,"matches domain: $domain/$plen with IP4 $addr",
1434             { mechanism => $mech })
1435             }
1436             }
1437             } else { # AAAA
1438 0 0       0 $plen = 128 if ! defined $plen;
1439 0         0 my $mask = $mask6[$plen];
1440 0         0 for my $addr (@$addr) {
1441 0 0       0 $DEBUG && DEBUG( "check a:$domain($addr)//$plen for mech $mech" );
1442 0 0       0 my $packed = eval { inet_pton(AF_INET6,$addr) }
  0         0  
1443             or return ( SPF_TempError,
1444             "getting AAAA for $domain",
1445             { problem => "bad address in AAAA record" }
1446             );
1447 0 0       0 if ( ($packed & $mask) eq ($self->{clientip6} & $mask) ) {
1448             # match!
1449 0 0       0 $DEBUG && DEBUG( "match mech a:...//$plen for mech $mech with qual $qual" );
1450 0         0 return ($qual,"matches domain: $domain//$plen with IP6 $addr",
1451             { mechanism => $mech })
1452             }
1453             }
1454             }
1455              
1456             # no match yet, can we resolve another name?
1457 278 100       639 if ( @$names ) {
1458 51 50       138 my $typ = $self->{clientip4} ? 'A':'AAAA';
1459 51 50       273 $DEBUG && DEBUG( "check mech a:$names->[0]/$plen for mech $mech with qual $qual" );
1460 51         182 $self->{cb} = [ \&_got_A, $qual,$plen,$names,$mech ];
1461 51         174 return scalar(Net::DNS::Packet->new( $names->[0], $typ,'IN' ));
1462             }
1463              
1464             # finally no match
1465 227 50       860 $DEBUG && DEBUG( "no match mech $mech:$domain/$plen" );
1466 227         798 return;
1467             }
1468              
1469              
1470              
1471             ############################################################################
1472             # handle mechanism 'mx'
1473             # similar to mech 'a', we expect the A/AAAA records for the MX in the
1474             # additional section of the DNS response
1475             ############################################################################
1476             sub _mech_mx {
1477 110     110   174 my Mail::SPF::Iterator $self = shift;
1478 110         219 my ($qual,$domain,$plen) = @_;
1479 110 50       259 $domain = $domain->{expanded} if ref $domain;
1480 110 50       429 if ( my @err = _check_domain($domain,
    50          
1481             "mx:$domain".( defined $plen ? "/$plen":"" ))) {
1482 0 0       0 $DEBUG && DEBUG( "no mech mx:$domain/$plen - @err" );
1483             return @err
1484 0         0 }
1485              
1486             return ( SPF_PermError, "",
1487             { problem => "Number of DNS mechanism exceeded" })
1488 110 50       303 if --$self->{limit_dns_mech} < 0;
1489              
1490 110         367 $self->{cb} = [ \&_got_MX,$qual,$domain,$plen ];
1491 110         391 return scalar(Net::DNS::Packet->new( $domain, 'MX','IN' ));
1492             }
1493              
1494             sub _got_MX {
1495 106     106   477 my Mail::SPF::Iterator $self = shift;
1496 106         282 my ($qtype,$rcode,$ans,$add,$qual,$domain,$plen) = @_;
1497              
1498 106 50       411 if ( $rcode eq 'NXDOMAIN' ) {
    50          
    100          
1499 0 0       0 $DEBUG && DEBUG( "no match mech mx:$domain/$plen - $rcode" );
1500             # no records found
1501             } elsif ( $rcode ne 'NOERROR' ) {
1502 0 0       0 $DEBUG && DEBUG( "no match mech mx:$domain/$plen - $rcode" );
1503 0         0 return ( SPF_TempError,
1504             "getting MX form $domain",
1505             { problem => "error resolving $domain" }
1506             );
1507             } elsif ( ! @$ans ) {
1508 36 50       175 $DEBUG && DEBUG( "no match mech mx:$domain/$plen - no MX records" );
1509 36         133 return; # domain has no MX -> no match
1510             }
1511              
1512             # all MX, with best (lowest) preference first
1513 130         2407 my @mx = map { $_->[0] }
1514 138         270 sort { $a->[1] <=> $b->[1] }
1515 70         151 map { [ $_->exchange, $_->preference ] }
  130         872  
1516             @$ans;
1517 70         164 my %mx = map { $_ => [] } @mx;
  130         311  
1518              
1519 70 100       260 if (!$self->{opt}{rfc4408}) {
1520             # RFC 4408 limited the number of MX to query to 10
1521             # RFC 7208 instead said that ALL returned MX should count
1522             # against the limit and the test suite suggest that this limit
1523             # should be enforced before even asking the MX
1524             return ( SPF_PermError, "",
1525             { problem => "Number of DNS mechanism exceeded" })
1526 52 100       186 if $self->{limit_dns_mech}-@mx < 0;
1527             }
1528              
1529             # try to find A|AAAA records in additional data
1530 66 50       180 my $atyp = $self->{clientip4} ? 'A':'AAAA';
1531 66         137 for my $rr (@$add) {
1532 46 100 66     355 if ( $rr->type eq $atyp && exists $mx{$rr->name} ) {
1533 40         729 push @{$mx{$rr->name}},$rr->address;
  40         73  
1534             }
1535             }
1536             $DEBUG && DEBUG( "found mx for $domain: ".join( " ",
1537 66 50       604 map { $mx{$_} ? "$_(".join(",",@{$mx{$_}}).")" : $_ } @mx ));
  86 50       222  
  86         361  
1538              
1539             # remove from @mx where I've found addresses
1540 66         134 @mx = grep { ! @{$mx{$_}} } @mx;
  86         113  
  86         260  
1541             # limit the Rest to 10 records (rfc4408,10.1)
1542 66 100       171 splice(@mx,10) if @mx>10;
1543              
1544 66         151 my @addr = map { @$_ } values %mx;
  68         155  
1545 66         242 return _check_A_match( $self,$qual,"(mx)".$domain,$plen,\@addr,\@mx,'mx');
1546             }
1547              
1548             ############################################################################
1549             # handle mechanis 'exists'
1550             # just check, if I get any A record for the domain (lookup for A even if
1551             # I use IP6 - this is RBL style)
1552             ############################################################################
1553             sub _mech_exists {
1554 20     20   36 my Mail::SPF::Iterator $self = shift;
1555 20         49 my ($qual,$domain) = @_;
1556 20 100       57 $domain = $domain->{expanded} if ref $domain;
1557 20 50       72 if ( my @err = _check_domain($domain, "exists:$domain" )) {
1558 0 0       0 $DEBUG && DEBUG( "no match mech exists:$domain - @err" );
1559             return @err
1560 0         0 }
1561              
1562             return ( SPF_PermError, "",
1563             { problem => "Number of DNS mechanism exceeded" })
1564 20 50       68 if --$self->{limit_dns_mech} < 0;
1565              
1566 20         72 $self->{cb} = [ \&_got_A_exists,$qual,$domain ];
1567 20         84 return scalar(Net::DNS::Packet->new( $domain, 'A','IN' ));
1568             }
1569              
1570             sub _got_A_exists {
1571 20     20   121 my Mail::SPF::Iterator $self = shift;
1572 20         81 my ($qtype,$rcode,$ans,$add,$qual,$domain) = @_;
1573              
1574 20 50       92 if ( $rcode ne 'NOERROR' ) {
    50          
1575 0 0       0 $DEBUG && DEBUG( "no match mech exists:$domain - $rcode" );
1576 0         0 return;
1577             } elsif ( ! @$ans ) {
1578 0 0       0 $DEBUG && DEBUG( "no match mech exists:$domain - no A records" );
1579 0         0 return;
1580             }
1581 20 50       117 $DEBUG && DEBUG( "match mech exists:$domain with qual $qual" );
1582 20         112 return ($qual,"domain $domain exists", { mechanism => 'exists' } )
1583             }
1584              
1585              
1586              
1587             ############################################################################
1588             # PTR
1589             # this is the most complex and most expensive mechanism:
1590             # - first get domains from PTR records for IP (clientip4|clientip6)
1591             # - filter for domains which match $domain (because only these are interesting
1592             # for matching)
1593             # - then verify the domains, if they point back to the IP by doing A|AAAA
1594             # lookups until one domain can be validated
1595             ############################################################################
1596             sub _mech_ptr {
1597 34     34   61 my Mail::SPF::Iterator $self = shift;
1598 34         77 my ($qual,$domain) = @_;
1599 34 50       96 $domain = $domain->{expanded} if ref $domain;
1600 34 50       112 if ( my @err = _check_domain($domain, "ptr:$domain" )) {
1601 0 0       0 $DEBUG && DEBUG( "no match mech ptr:$domain - @err" );
1602             return @err
1603 0         0 }
1604              
1605             return ( SPF_PermError, "",
1606             { problem => "Number of DNS mechanism exceeded" })
1607 34 50       113 if --$self->{limit_dns_mech} < 0;
1608              
1609 34   33     106 my $ip = $self->{clientip4} || $self->{clientip6};
1610 34 50       141 if ( exists $self->{validated}{$ip}{$domain} ) {
1611             # already checked
1612 0 0       0 if ( ! $self->{validated}{$ip}{$domain} ) {
1613             # could not be validated
1614 0 0       0 $DEBUG && DEBUG( "no match mech ptr:$domain - cannot validate $ip/$domain" );
1615 0         0 return; # ignore
1616             } else {
1617 0 0       0 $DEBUG && DEBUG( "match mech ptr:$domain with qual $qual" );
1618 0         0 return ($qual,"$domain validated" );
1619             }
1620             }
1621              
1622 34         58 my $query;
1623 34 50       84 if ( $self->{clientip4} ) {
1624             $query = join( '.', reverse split( m/\./,
1625 34         303 inet_ntoa($self->{clientip4}) ))
1626             .'.in-addr.arpa'
1627             } else {
1628             $query = join( '.', split( //,
1629 0         0 reverse unpack("H*",$self->{clientip6}) ))
1630             .'.ip6.arpa';
1631             }
1632              
1633 34         137 $self->{cb} = [ \&_got_PTR,$qual,$query,$domain ];
1634 34         151 return scalar(Net::DNS::Packet->new( $query, 'PTR','IN' ));
1635             }
1636              
1637             sub _got_PTR {
1638 34     34   178 my Mail::SPF::Iterator $self = shift;
1639 34         137 my ($qtype,$rcode,$ans,$add,$qual,$query,$domain) = @_;
1640              
1641             # ignore mech if it can not be validated
1642 34 100       100 $rcode eq 'NOERROR' or do {
1643 8 50       57 $DEBUG && DEBUG( "no match mech ptr:$domain - $rcode" );
1644 8         27 return;
1645             };
1646 26 50       55 my @names = map { $_->ptrdname } @$ans or do {
  122         824  
1647 0 0       0 $DEBUG && DEBUG( "no match mech ptr:$domain - no names in PTR lookup" );
1648 0         0 return;
1649             };
1650              
1651             # strip records, which do not end in $domain
1652 26 100       263 @names = grep { $_ eq $domain || m{\.\Q$domain\E$} } @names;
  122         1074  
1653 26 50       81 if ( ! @names ) {
1654 0 0       0 $DEBUG && DEBUG( "no match mech ptr:$domain - no names in PTR lookup match $domain" );
1655             # return if no matches inside $domain
1656 0         0 return;
1657             }
1658              
1659             # limit to no more then 10 names (see RFC4408, 10.1)
1660 26 50       69 splice(@names,10) if @names>10;
1661              
1662             # validate the rest by looking up the IP and verifying it
1663             # with the original IP (clientip)
1664 26 50       83 my $typ = $self->{clientip4} ? 'A':'AAAA';
1665              
1666 26         120 $self->{cb} = [ \&_got_A_ptr, $qual,\@names ];
1667 26         2793 return scalar(Net::DNS::Packet->new( $names[0], $typ,'IN' ));
1668             }
1669              
1670             sub _got_A_ptr {
1671 26     26   185 my Mail::SPF::Iterator $self = shift;
1672 26         74 my ($qtype,$rcode,$ans,$add,$qual,$names) = @_;
1673              
1674 26 50       93 for my $dummy ( $rcode eq 'NOERROR' ? (1):() ) {
1675 26 100       90 @$ans or last; # no addr for domain? - try next
1676 20         46 my @addr = map { $_->address } @$ans;
  20         70  
1677              
1678             # check if @addr contains clientip
1679 20         205 my ($match,$ip);
1680 20 50       67 if ( $ip = $self->{clientip4} ) {
1681 20         47 for(@addr) {
1682 20 50       131 m{^[\d\.]+$} or next;
1683 20 50       41 eval { inet_aton($_) } eq $ip or next;
  20         128  
1684 20         45 $match = 1;
1685 20         59 last;
1686             }
1687             } else {
1688 0         0 $ip = $self->{clientip6};
1689 0         0 for(@addr) {
1690 0 0       0 eval { inet_pton(AF_INET6,$_) } eq $ip or next;
  0         0  
1691 0         0 $match = 1;
1692 0         0 last;
1693             }
1694             }
1695              
1696             # cache verification status
1697 20         68 $self->{validated}{$ip}{$names->[0]} = $match;
1698              
1699             # return $qual if we have verified the ptr
1700 20 50       55 if ($match) {
1701 20 50       96 $DEBUG && DEBUG( "match mech ptr:... with qual $qual" );
1702 20         105 return ( $qual,"verified clientip with ptr", { mechanism => 'ptr' })
1703             }
1704             }
1705              
1706             # try next
1707 6         13 shift @$names;
1708 6 50       22 @$names or do {
1709             # no next
1710 6 50       26 $DEBUG && DEBUG( "no match mech ptr:... - no more names for clientip" );
1711 6         20 return;
1712             };
1713              
1714             # cb stays the same
1715 0         0 return scalar(Net::DNS::Packet->new( $names->[0], $qtype,'IN' ));
1716             }
1717              
1718              
1719             ############################################################################
1720             # mechanism include
1721             # include SPF from other domain, propagate errors and consider Pass
1722             # from this inner SPF as match for the include mechanism
1723             ############################################################################
1724             sub _mech_include {
1725 136     136   219 my Mail::SPF::Iterator $self = shift;
1726 136         266 my ($qual,$domain) = @_;
1727 136 50       288 $domain = $domain->{expanded} if ref $domain;
1728 136 50       383 if ( my @err = _check_domain($domain, "include:$domain" )) {
1729 0 0       0 $DEBUG && DEBUG( "failed mech include:$domain - @err" );
1730             return @err
1731 0         0 }
1732              
1733 136 50       572 $DEBUG && DEBUG( "mech include:$domain with qual=$qual" );
1734              
1735             return ( SPF_PermError, "",
1736             { problem => "Number of DNS mechanism exceeded" })
1737 136 100       417 if --$self->{limit_dns_mech} < 0;
1738              
1739             # push and reset current domain and SPF record
1740 130         715 push @{$self->{include_stack}}, {
1741             domain => $self->{domain},
1742             mech => $self->{mech},
1743             explain => $self->{explain},
1744             redirect => $self->{redirect},
1745 130         187 qual => $qual,
1746             };
1747 130         258 $self->{domain} = $domain;
1748 130         226 $self->{mech} = [];
1749 130         210 $self->{explain} = undef;
1750 130         197 $self->{redirect} = undef;
1751              
1752             # start with new SPF record
1753 130         284 return $self->_query_txt_spf;
1754             }
1755              
1756              
1757             ############################################################################
1758             # create explain message from TXT record
1759             ############################################################################
1760             sub _got_TXT_exp {
1761 98     98   534 my Mail::SPF::Iterator $self = shift;
1762 98         262 my ($qtype,$rcode,$ans,$add) = @_;
1763 98         201 my $final = $self->{result};
1764              
1765 98 100       272 if ( $rcode ne 'NOERROR' ) {
1766 4 50       18 $DEBUG && DEBUG( "DNS error for exp TXT lookup" );
1767             # just return the final rv
1768 4         16 return @$final;
1769             }
1770              
1771 94         209 my ($txtdata,$t2) = grep { length } map { $_->txtdata } @$ans;;
  98         2260  
  98         472  
1772 94 100       496 if ( $t2 ) {
    100          
1773             # only one record should be returned
1774 10 50       46 $DEBUG && DEBUG( "got more than one TXT -> error" );
1775 10         85 return @$final;
1776             } elsif ( ! $txtdata ) {
1777 6 50       27 $DEBUG && DEBUG( "no text in TXT for explain" );
1778 6         25 return @$final;
1779             }
1780              
1781 78 50       364 $DEBUG && DEBUG( "got TXT $txtdata" );
1782              
1783             # valid TXT record found -> expand macros
1784 78         135 my $exp = eval { $self->_macro_expand( $txtdata,'exp' ) };
  78         207  
1785 78 100       212 if ($@) {
1786 6 50       38 $DEBUG && DEBUG( "macro expansion of '$txtdata' failed: $@" );
1787 6         29 return @$final;
1788             }
1789              
1790             # explain
1791 72 100       184 if (ref $exp) {
1792 12 50       47 if ( my @xrv = $self->_resolve_macro_p($exp)) {
1793             # we need to do more DNS lookups for resolving %{p} macros
1794 12 50       1021 $DEBUG && DEBUG( "need to resolve %{p} in $exp->{macro}" );
1795 12         33 $final->[4] = $exp;
1796 12         45 return @xrv;
1797             }
1798 0         0 $exp = $exp->{expanded};
1799             }
1800              
1801             # result should be limited to US-ASCII!
1802             # further limit to printable chars
1803 60 100       237 $final->[3] = $exp if $exp !~m{[\x00-\x1f\x7e-\xff]};
1804              
1805 60         271 return @$final;
1806             }
1807              
1808             ############################################################################
1809             # expand Macros
1810             ############################################################################
1811             sub _macro_expand {
1812 698     698   1180 my Mail::SPF::Iterator $self = shift;
1813 698         1442 my ($domain,$explain) = @_;
1814 698         1039 my $new_domain = '';
1815 698 100       2286 my $mchars = $explain ? qr{[slodipvhcrt]}i : qr{[slodipvh]}i;
1816 698         1039 my $need_validated;
1817             #DEBUG( Carp::longmess("no domain" )) if ! $domain;
1818             #DEBUG( "domain=$domain" );
1819 698         13489 while ( $domain =~ m{\G (?:
1820             ([^%]+) | # text
1821             %(?:
1822             ([%_\-]) | # char: %_, %-, %%
1823             {
1824             # macro: l1r+- -> (l)(1)(r)(+-)
1825             ($mchars) (\d*)(r?) ([.\-+,/_=]*)
1826             } |
1827             (.|$) # bad char
1828             ))}xg ) {
1829 1028         4410 my ($text,$char,$macro,$macro_n,$macro_r,$macro_delim,$bad)
1830             = ($1,$2,$3,$4,$5,$6,$7);
1831              
1832 1028 100       2254 if ( defined $text ) {
    100          
    100          
1833 746         3909 $new_domain .= $text;
1834              
1835             } elsif ( defined $char ) {
1836 24 100       138 $new_domain .=
    100          
1837             $char eq '%' ? '%' :
1838             $char eq '_' ? ' ' :
1839             '%20'
1840              
1841             } elsif ( $macro ) {
1842 232   100     959 $macro_delim ||= '.';
1843 232         402 my $imacro = lc($macro);
1844             my $expand =
1845             $imacro eq 's' ? $self->{sender} :
1846             $imacro eq 'l' ? $self->{sender} =~m{^([^@]+)\@}
1847             ? $1 : 'postmaster' :
1848             $imacro eq 'o' ? $self->{sender} =~m{\@(.*)}
1849             ? $1 : $self->{sender} :
1850             $imacro eq 'd' ? $self->{domain} :
1851             $imacro eq 'i' ? $self->{clientip4} ?
1852             inet_ntoa($self->{clientip4}) :
1853 0         0 join('.',map { uc } split(//,
1854             unpack( "H*",$self->{clientip6}))) :
1855             $imacro eq 'v' ? $self->{clientip4} ? 'in-addr' : 'ip6':
1856             $imacro eq 'h' ? $self->{helo} :
1857             $imacro eq 'c' ? $self->{clientip4} ?
1858             inet_ntoa($self->{clientip4}) :
1859             inet_ntop(AF_INET6,$self->{clientip6}) :
1860             $imacro eq 'r' ? $self->{myname} || 'unknown' :
1861             $imacro eq 't' ? time() :
1862 232 100 0     1664 $imacro eq 'p' ? do {
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
1863 62   33     165 my $ip = $self->{clientip4} || $self->{clientip6};
1864 62         111 my $v = $self->{validated}{$ip};
1865 62         96 my $d = $self->{domain};
1866 62 100       167 if ( ! $v ) {
    50          
    50          
1867             # nothing validated pointing to IP
1868 46         142 $need_validated = { ip => $ip, domain => $d };
1869 46         102 'unknown'
1870             } elsif ( $v->{$d} ) {
1871             # itself is validated
1872 0         0 $d;
1873 16         73 } elsif ( my @xd = grep { $v->{$_} } keys %$v ) {
1874 16 100       31 if ( my @sd = grep { m{\.\Q$d\E$} } @xd ) {
  16         247  
1875             # subdomain if is validated
1876 6         22 $sd[0]
1877             } else {
1878             # any other domain pointing to IP
1879 10         35 $xd[0]
1880             }
1881             } else {
1882 0         0 'unknown'
1883             }
1884             } :
1885             die "unknown macro $macro\n";
1886              
1887 232         16082 my $rx = eval "qr{[$macro_delim]}";
1888 232         1422 my @parts = split( $rx, $expand );
1889 232 100       648 @parts = reverse @parts if $macro_r;
1890 232 100       491 if ( length $macro_n ) {
1891 38 50       94 die "bad macro definition '$domain'\n"
1892             if ! $macro_n; # must be != 0
1893 38 100       175 @parts = splice( @parts,-$macro_n ) if @parts>$macro_n;
1894             }
1895 232 100       514 if ( $imacro ne $macro ) {
1896             # upper case - URI escape
1897 36         81 @parts = map { uri_escape($_) } @parts;
  78         660  
1898             }
1899 232         2673 $new_domain .= join('.',@parts);
1900              
1901             } else {
1902 26         244 die "bad macro definition '$domain'\n";
1903             }
1904             }
1905              
1906 672 100       1498 if ( ! $explain ) {
1907             # should be less than 253 bytes
1908 600         1422 while ( length($new_domain)>253 ) {
1909 6 50       45 $new_domain =~s{^[^.]*\.}{} or last;
1910             }
1911 600 50       1208 $new_domain = '' if length($new_domain)>253;
1912             }
1913              
1914 672 100       1306 if ( $need_validated ) {
1915 46         364 return { expanded => $new_domain, %$need_validated, macro => $domain }
1916             } else {
1917 626         2178 return $new_domain;
1918             }
1919             }
1920              
1921             ############################################################################
1922             # resolve macro %{p}, e.g. find validated domain name for IP and replace
1923             # %{p} with it. This has many thing similar with the ptr: method
1924             ############################################################################
1925             sub _resolve_macro_p {
1926 22     22   44 my Mail::SPF::Iterator $self = shift;
1927 22         35 my $rec = shift;
1928 22 100 66     136 my $ip = ref($rec) && $rec->{ip} or return; # nothing to resolve
1929              
1930             # could it already be resolved w/o further lookups?
1931 20         34 my $d = eval { $self->_macro_expand( $rec->{macro} ) };
  20         47  
1932 20 50       71 if ( ! ref $d ) {
1933 0 0       0 %$rec = ( expanded => $d ) if ! $@;
1934 0         0 return;
1935             }
1936              
1937 20         29 my $query;
1938 20 50       55 if ( length($ip) == 4 ) {
1939 20         157 $query = join( '.', reverse split( m/\./,
1940             inet_ntoa($ip) )) .'.in-addr.arpa'
1941             } else {
1942 0         0 $query = join( '.', split( //,
1943             reverse unpack("H*",$ip) )) .'.ip6.arpa';
1944             }
1945              
1946 20         75 $self->{cb} = [ \&_validate_got_PTR, $rec ];
1947 20         86 return scalar(Net::DNS::Packet->new( $query, 'PTR','IN' ));
1948             }
1949              
1950             sub _validate_got_PTR {
1951 20     20   120 my Mail::SPF::Iterator $self = shift;
1952 20         52 my ($qtype,$rcode,$ans,$add,$rec ) = @_;
1953              
1954             # no validation possible if no records
1955 20 50 33     113 return if $rcode ne 'NOERROR' or ! @$ans;
1956              
1957 20         52 my @names = map { lc($_->ptrdname) } @$ans;
  26         113  
1958              
1959             # prefer records, which are $domain or end in $domain
1960 20 50       234 if ( my $domain = $rec->{domain} ) {
1961 20         51 unshift @names, grep { $_ eq $domain } @names;
  26         67  
1962 20         36 unshift @names, grep { m{\.\Q$domain\E$} } @names;
  26         296  
1963 20         38 { my %n; @names = grep { !$n{$_}++ } @names } # uniq
  20         31  
  20         40  
  32         118  
1964             }
1965              
1966             # limit to no more then 10 names (RFC4408, 10.1)
1967 20 50       61 splice(@names,10) if @names>10;
1968              
1969             # validate the rest by looking up the IP and verifying it
1970             # with the original IP (clientip)
1971 20 50       64 my $typ = length($rec->{ip}) == 4 ? 'A':'AAAA';
1972              
1973 20         67 $self->{cb} = [ \&_validate_got_A_ptr, $rec,\@names ];
1974 20         73 return scalar(Net::DNS::Packet->new( $names[0], $typ,'IN' ));
1975             }
1976              
1977             sub _validate_got_A_ptr {
1978 20     20   117 my Mail::SPF::Iterator $self = shift;
1979 20         55 my ($qtype,$rcode,$ans,$add,$rec,$names) = @_;
1980              
1981 20 50       63 if ( $rcode eq 'NOERROR' ) {
1982 20 50       44 my @addr = map { $_->address } @$ans or do {
  32         155  
1983             # no addr for domain? -> ignore - maybe
1984             # the domain only provides the other kind of records?
1985 0         0 return;
1986             };
1987              
1988             # check if @addr contains clientip
1989 20         207 my $match;
1990 20         46 my $ip = $rec->{ip};
1991 20 50       54 if ( length($ip) == 4 ) {
1992 20         40 for(@addr) {
1993 26 50       147 m{^[\d\.]+$} or next;
1994 26 100       51 eval { inet_aton($_) } eq $ip or next;
  26         136  
1995 14         38 $match = 1;
1996 14         27 last;
1997             }
1998             } else {
1999 0         0 for(@addr) {
2000 0 0       0 eval { inet_pton(AF_INET6,$_) } eq $ip or next;
  0         0  
2001 0         0 $match = 1;
2002 0         0 last;
2003             }
2004             }
2005              
2006             # cache verification status
2007 20         92 $self->{validated}{$ip}{$names->[0]} = $match;
2008              
2009             # expand macro if we have verified the ptr
2010 20 100       52 if ( $match ) {
2011 14 50       24 if ( my $t = eval { $self->_macro_expand( $rec->{macro} ) }) {
  14         41  
2012 14         55 %$rec = ( expanded => $t );
2013             }
2014 14         59 return;
2015             }
2016             }
2017              
2018             # try next
2019 6         11 shift @$names;
2020 6 50       25 @$names or return; # no next
2021              
2022             # cb stays the same
2023 0           return scalar(Net::DNS::Packet->new( $names->[0], $qtype,'IN' ));
2024             }
2025              
2026              
2027             1;