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   5290 use strict;
  5         16  
  5         140  
211 5     5   27 use warnings;
  5         9  
  5         581  
212              
213             package Mail::SPF::Iterator;
214              
215             our $VERSION = '1.119';
216              
217             use fields (
218             # values given in or derived from params to new()
219 5         20 '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   2122 );
  5         7897  
242              
243 5     5   3425 use Net::DNS;
  5         460819  
  5         908  
244 5     5   86 use Socket;
  5         33  
  5         3632  
245 5     5   2309 use URI::Escape 'uri_escape';
  5         8767  
  5         401  
246 5     5   3061 use Data::Dumper;
  5         32468  
  5         357  
247 5     5   40 use base 'Exporter';
  5         17  
  5         1016  
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   17 } || 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       279 if ( ! $can_ip6 ) {
265 5     5   48 no strict 'refs';
  5         15  
  5         374  
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   42 use constant SPF_Noop => '_NOOP';
  5         10  
  5         544  
275             my %ResultQ;
276             BEGIN {
277 5     5   18 my $i = 0;
278 5         35 $ResultQ{ &SPF_Noop } = $i++;
279 5         12 for (qw(None PermError TempError Neutral SoftFail Fail Pass)) {
280 5     5   46 no strict 'refs';
  5         128  
  5         476  
281 35         2135 *{"SPF_$_"} = eval "sub () { '$_' }";
  35         183  
282 35         142 push @EXPORT, "SPF_$_";
283 35         59820 $ResultQ{$_} = $i++;
284             }
285             }
286              
287             my $DEBUGFUNC;
288             our $DEBUG=0;
289             sub import {
290 12 100   12   36243 goto &Exporter::import if @_ == 1; # implicit :DEFAULT
291 6         29 my $i = 1;
292 6         29 while ( $i<@_ ) {
293 6 50       35 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         18 $DEBUG = $_[$i+1];
299 6         16 splice( @_,$i,2 );
300 6         19 next;
301             }
302 0         0 ++$i;
303             }
304 6 50       33 goto &Exporter::import if @_ >1; # not implicit :DEFAULT
305             }
306              
307              
308              
309             ### Debugging
310             sub DEBUG {
311 6878 50   6878 0 84026 $DEBUG or return; # check against debug level
312 6878 50       12762 goto &$DEBUGFUNC if $DEBUGFUNC;
313 6878         20964 my ($pkg,$file,$line) = caller;
314 6878         12041 my $msg = shift;
315 6878 50       13302 $msg = sprintf $msg,@_ if @_;
316 6878         25783 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 204874 my ($class,$ip,$mailfrom,$helo,$myname,$opt) = @_;
359 994         3097 my Mail::SPF::Iterator $self = fields::new($class);
360              
361 994 50       161326 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       3366 my ($sender,$identity) = $mailfrom ne ''
368             ? ( $mailfrom,'mailfrom' )
369             : ( $helo,'helo' );
370              
371 994         1570 my $ip4 = eval { inet_aton($ip) };
  994         4070  
372 994   33     3219 my $ip6 = ! $ip4 && $can_ip6 && eval { inet_pton(AF_INET6,$ip) };
373 994 0 33     2167 die "no client IP4 or IP6 known (can_ip6=$can_ip6): $ip"
374             if ! $ip4 and ! $ip6;
375              
376 994 50       2065 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         7132 %$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         3294 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 967 if ( ref $_[0] ) {
434 266         421 my Mail::SPF::Iterator $self = shift;
435 266 50       572 $self->{explain_default} = shift if @_;
436             return defined $self->{explain_default}
437             ? $self->{explain_default}
438 266 50       1072 : $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 32585 my Mail::SPF::Iterator $self = shift;
503 994 50       1476 my ($result,$info,$hash) = @{ $self->{result} || return };
  994         3297  
504             $result .= " (using default SPF of \"$self->{used_default_spf}\")"
505 994 50       2456 if $self->{used_default_spf};
506             return $result ." ". join( "; ", map {
507 994         6090 my $v = $hash->{$_};
  4725         8650  
508 4725         8245 $v =~ s{([\"\\])}{\\$1}g;
509 4725         7311 $v =~ s{[\r\n]+}{ }g;
510 4725         8531 $v =~ s{^\s+}{};
511 4725         8649 $v =~ s{\s+$}{};
512 4725 100 66     28072 $v = qq("$v") if $v eq '' or $v =~ m{[^0-9a-zA-Z!#$%&'*+\-/=?^_`{|}~]};
513 4725         16194 "$_=$v"
514             } sort keys %$hash );
515             }
516              
517              
518             ############################################################################
519             # next step in SPF lookup
520             # - verify that there are open queries for the DNS reply and that parameter
521             # in query match question+answer in reply
522             # - process dnsresp by the current callback
523             # - process callbacks result using _next_process_cbrv which returns either
524             # final result or more DNS questions
525             # Args: ($self,$dnsresp)
526             # $dnsresp: DNS reply
527             # Returns: (undef,@dnsq) | ($status,$info,\%param,$explain) | ()
528             # (undef,@dnsq): @dnsq are more DNS questions
529             # ($status,$info,\%param,$explain): final response
530             # (''): reply processed, but answer ignored (likely error)
531             # (): reply ignored, does not matching outstanding request
532             ############################################################################
533             sub next {
534 2861     2861 1 2108629 my Mail::SPF::Iterator $self = shift;
535 2861         4519 my $dnsresp = shift;
536              
537 2861 100       6659 if ( ! $dnsresp ) {
538             # no DNS response - must be initial call to next
539 994 50       2183 die "no DNS reply but callback given" if $self->{cb};
540 994         2297 return $self->_next_process_cbrv( $self->_query_txt_spf );
541             }
542              
543             # handle DNS reply
544 1867 50       4520 my $callback = $self->{cb} or die "no callback but DNS reply";
545 1867         2956 my $cb_queries = $self->{cbq};
546 1867 50       4067 if ( ! @$cb_queries ) {
547             # we've got a reply, but no outstanding queries - ignore
548 0 0       0 $DEBUG && DEBUG( "got reply w/o queries, ignoring" );
549 0         0 return;
550             }
551              
552             # extract query from reply
553 1867         3132 my ($question,$err,$qid);
554 1867 100       6669 if ( ! UNIVERSAL::isa( $dnsresp, 'Net::DNS::Packet' )) {
555             # probably [ $question, $errorstring ]
556 34         80 (my $query,$err) = @$dnsresp;
557 34         100 ($question) = $query->question;
558 34         230 $qid = $query->header->id;
559 34   50     369 $err ||= 'unknown error';
560 34         63 $dnsresp = $err;
561 34 50       187 $DEBUG && DEBUG( "error '$err' to query ".$question->string );
562             } else {
563 1833         4208 ($question) = $dnsresp->question;
564 1833         10534 $qid = $dnsresp->header->id;
565             }
566 1867         19389 my $qtype = $question->qtype;
567              
568             # check if the reply matches one of the open queries
569 1867         19733 my $found;
570 1867         3797 for (@$cb_queries) {
571 1891 100       4462 next if $qid != $_->{id}; # ID mismatch
572 1867 50       4008 next if $qtype ne $_->{q}->qtype; # type mismatch
573              
574 1867 50       20057 if ( lc($question->qname) eq lc($_->{q}->qname) ) {
575 1867         33834 $found = $_;
576 1867         3212 last;
577             }
578              
579             # in case of special characters the names might have the
580             # wire presentation \DDD or the raw presentation
581             # actual behavior depends on the Net::DNS version, so normalize
582 0         0 my $rname = lc($question->qname);
583 0         0 my $qname = lc($_->{q}->qname);
584 0 0       0 s{\\(?:(\d\d\d)|(.))}{ $2 || chr($1) }esg for($rname,$qname);
  0         0  
585 0 0       0 if ( $rname eq $qname ) {
586 0         0 $found = $_;
587 0         0 last;
588             }
589             }
590              
591 1867 50       5775 if ( ! $found ) {
    50          
592             # packet does not match our queries
593 0 0       0 $DEBUG && DEBUG( "found no open query for ".$question->string );
594 0         0 return; # ignore problem
595             } elsif ( ! $found->{pkt} ) {
596             # duplicate response - ignore
597 0 0       0 $DEBUG && DEBUG( "duplicate response, ignoring" );
598 0         0 return;
599             }
600              
601 1867         3506 delete $found->{pkt}; # no longer needed
602              
603             # found matching query
604             # check for error
605 1867 100       3438 if ( $err ) {
606             # if this temporary error is the best we have so far set it as tmpresult
607 34 100 66     170 if (! $self->{tmpresult} or
608             $ResultQ{ $self->{tmpresult}[0] } < $ResultQ{ &SPF_TempError }) {
609             $self->{tmpresult} = [ SPF_TempError,
610             "getting ".$found->{q}->qtype." for ".$found->{q}->qname,
611 30         108 { problem => "error getting DNS response: $err" }
612             ]
613             }
614              
615 34 100       673 if ( grep { $_->{pkt} } @$cb_queries ) {
  46 100       189  
616             # we still have outstanding queries, so we might still get answers
617             # -> return ('') as a sign, that we got an error to an outstanding
618             # request, but otherwise ignore this error
619 6 50       42 $DEBUG && DEBUG( "ignore error '$err', we still have oustanding queries" );
620 6         27 return ('');
621              
622             } elsif ( my $r = $self->{result} ) {
623             # we have a final result already, so this error occured only while
624             # trying to expand %{p} for explain
625             # -> ignore error, set to default explain and return final result
626 6 50       41 $DEBUG && DEBUG( "error looking up data for explain: $err" );
627 6         35 return @$r;
628              
629             } else {
630             # we have no final result - pick the best error we have so far
631 22 50       124 $DEBUG && DEBUG( "TempError: $err" );
632 22         63 $self->{result} = $self->{tmpresult};
633 22         62 _update_result_info($self);
634 22         46 return @{$self->{result}};
  22         98  
635             }
636             }
637              
638             # call callback with no records on error
639 1833         3965 my $rcode = $dnsresp->header->rcode;
640 1833         63279 my @answer = $dnsresp->answer;
641 1833 100 66     14158 if (!@answer or $rcode ne 'NOERROR') {
642 248         733 my ($sub,@arg) = @$callback;
643 248 100 100     1627 if ($sub != \&_got_TXT_exp
      100        
644             and ! $self->{opt}{rfc4408}
645             and --$self->{limit_dns_void} < 0) {
646 8         38 $self->{result} = [ SPF_PermError, "",
647             { problem => "Number of void DNS queries exceeded" }];
648 8         30 _update_result_info($self);
649 8         18 return @{$self->{result}};
  8         55  
650             }
651              
652 240         802 return $self->_next_process_cbrv(
653             $sub->($self,$qtype,$rcode,[],[],@arg));
654             }
655              
656             # extract answer and additional data
657             # verify if names and types in answer records match query
658             # handle CNAMEs
659 1585         3551 my $qname = $question->qname;
660 1585 50       15432 $qname =~s{\\(?:(\d\d\d)|(.))}{ $2 || chr($1) }esg; # presentation -> raw
  12         97  
661 1585         3002 $qname = lc($qname);
662 1585         2694 my (%cname,%ans);
663 1585         2865 for my $rr (@answer) {
664 1891         4314 my $rtype = $rr->type;
665             # changed between Net::DNS 0.63 and 0.64
666             # it reports now the presentation name instead of the raw name
667 1891 50       17501 ( my $name = $rr->name ) =~s{\\(?:(\d\d\d)|(.))}{ $2 || chr($1) }esg;
  12         151  
668 1891         20870 $name = lc($name);
669 1891 100       4703 if ( $rtype eq 'CNAME' ) {
    50          
670             # remember CNAME so that we can check that the answer record
671             # for $qtype matches name from query or CNAME which is an alias
672             # for name
673 4 50       14 if ( exists $cname{$name} ) {
674 0 0       0 $DEBUG && DEBUG( "more than one CNAME for same name" );
675 0         0 next; # XXX should we TempError instead of ignoring?
676             }
677 4         12 $cname{$name} = $rr->cname;
678             } elsif ( $rtype eq $qtype ) {
679 1887         2690 push @{ $ans{$name}},$rr;
  1887         6783  
680             } else {
681             # XXXX should we TempError instead of ignoring?
682 0 0       0 $DEBUG && DEBUG( "unexpected answer record for $qtype:$qname" );
683             }
684             }
685              
686             # find all valid names, usually there should be at most one CNAME
687             # works by starting with name from query, finding CNAMEs for it,
688             # adding these to set and finding next CNAMEs etc
689             # if there are unconnected CNAMEs they will be left in %cname
690 1585         3491 my @names = ($qname);
691 1585         3663 while ( %cname ) {
692 4 50       13 my @n = grep { defined $_ } delete @cname{@names} or last;
  4         18  
693 4         12 push @names, map { lc($_) } @n;
  4         14  
694             }
695 1585 50       3156 if ( %cname ) {
696             # Report but ignore - XXX should we TempError instead?
697 0 0       0 $DEBUG && DEBUG( "unrelated CNAME records ".Dumper(\%cname));
698             }
699              
700             # collect the RR for all valid names
701 1585         2180 my @ans;
702 1585         2711 for (@names) {
703 1589 100       4376 my $rrs = delete $ans{$_} or next;
704 1585         4877 push @ans,@$rrs;
705             }
706 1585 50       3217 if ( %ans ) {
707             # answer records which don't match name from query or via CNAME
708             # derived names
709             # Report but ignore - XXX should we TempError instead?
710 0 0       0 $DEBUG && DEBUG( "unrelated answer records for $qtype names=@names ".Dumper(\%ans));
711             }
712              
713 1585 50 33     3892 if ( ! @ans and @names>1 ) {
714             # according to RFC1034 all RR for the type should be put into
715             # the answer section together with the CNAMEs
716             # so if there are no RRs in this answer, we should assume, that
717             # there will be no RRs at all
718 0 0       0 $DEBUG && DEBUG( "no answer records for $qtype, but names @names" );
719             }
720              
721 1585         3406 my ($sub,@arg) = @$callback;
722 1585         4289 return $self->_next_process_cbrv(
723             $sub->($self,$qtype,$rcode,\@ans,[ $dnsresp->additional ],@arg));
724             }
725              
726             ############################################################################
727             # return list of DNS queries which are still open
728             # Args: ($self)
729             # Returns: @dnsq
730             ############################################################################
731             sub todo {
732             return
733 0 0       0 map { $_->{pkt} ? ($_->{pkt}):() }
734 0     0 0 0 @{ shift->{cbq} }
  0         0  
735             }
736              
737             ############################################################################
738             # fill information in hash of final result
739             # Args: ($self)
740             ############################################################################
741             sub _update_result_info {
742 1100     1100   1618 my Mail::SPF::Iterator $self = shift;
743 1100 50       2416 my $h = $self->{result} or return;
744 1100 100       2457 $h = $h->[2] or return;
745             $h->{'client-ip'} = $self->{clientip4}
746             ? inet_ntoa($self->{clientip4})
747 1051 50       6280 : inet_ntop(AF_INET6,$self->{clientip6});
748 1051         2160 $h->{helo} = $self->{helo};
749 1051         1806 $h->{identity} = $self->{identity};
750 1051 50       3688 $h->{'envelope-from'} = "<$self->{sender}>" if $self->{sender};
751             }
752              
753             ############################################################################
754             # process results from callback to DNS reply, called from next
755             # Args: ($self,@rv)
756             # @rv: result from callback, either
757             # @query - List of new Net::DNS::Packet queries for next step
758             # () - no result (go on with next step)
759             # (status,...) - final response
760             # Returns: ... - see sub next
761             ############################################################################
762             sub _next_process_cbrv {
763 2819     2819   86893 my Mail::SPF::Iterator $self = shift;
764 2819         5658 my @rv = @_; # results from callback to _mech*
765              
766             # resolving of %{p} in exp= mod or explain TXT results in @rv = ()
767             # see sub _validate_*
768 2819 100 100     7347 if ( $self->{result} && ! @rv ) {
769             # set to final result
770 20         34 @rv = @{ $self->{result}};
  20         77  
771             }
772              
773             # if the last mech (which was called with the DNS reply in sub next) got
774             # no match and no further questions we need to find the match or questions
775             # either by processing the next mech in the current SPF record, following
776             # a redirect or going the include stack up
777 2819 100       6952 @rv = $self->_next_mech if ! @rv;
778              
779 2819 100       19961 if ( UNIVERSAL::isa( $rv[0],'Net::DNS::Packet' )) {
780             # @rv is list of DNS packets
781 1735         4033 return $self->_next_rv_dnsq(@rv)
782             }
783              
784             # @rv is (status,...)
785             # status of SPF_Noop is special in that it returns nothing as a sign, that
786             # it just waits for more input
787             # Only used when we could get multiple responses, e.g when multiple DNS
788             # requests were send like in the query for SPF+TXT
789 1084 100       2661 if ( $rv[0] eq SPF_Noop ) {
790             die "NOOP but no open queries"
791 18 50       28 if ! grep { $_->{pkt} } @{$self->{cbq}};
  36         90  
  18         40  
792 18         110 return ('');
793             }
794              
795             # inside include the response is only pre-final,
796             # propagate it the include stack up:
797             # see RFC4408, 5.2 for propagation of results
798 1066         1550 while ( my $top = pop @{ $self->{include_stack} } ) {
  1180         3307  
799 116 50       422 $DEBUG && DEBUG( "pre-final response $rv[0]" );
800              
801 116 100 66     657 if ( $rv[0] eq SPF_TempError || $rv[0] eq SPF_PermError ) {
    50          
802             # keep
803             } elsif ( $rv[0] eq SPF_None ) {
804 0         0 $rv[0] = SPF_PermError; # change None to PermError
805             } else {
806             # go stack up, restore saved data
807 44         140 my $qual = delete $top->{qual};
808 44         221 while ( my ($k,$v) = each %$top ) {
809 176         528 $self->{$k} = $v;
810             }
811 44 100       127 if ( $rv[0] eq SPF_Pass ) {
812             # Pass == match -> set status to $qual
813 16         56 $rv[0] = $qual;
814             } else {
815             # ! Pass == non-match
816             # -> restart with @rv=() and go on with next mech
817 28         72 @rv = $self->_next_mech;
818 28 100       189 if ( UNIVERSAL::isa( $rv[0],'Net::DNS::Packet' )) {
819             # @rv is list of DNS packets
820 2         8 return $self->_next_rv_dnsq(@rv)
821             }
822             }
823             }
824             }
825              
826             # no more include stack
827             # -> @rv is the probably the final result, but check if we had a better
828             # one already
829 1064         1747 my $final;
830 1064 50 66     2949 if ($self->{tmpresult} and
831             $ResultQ{ $self->{tmpresult}[0] } > $ResultQ{ $rv[0] }) {
832 0         0 $final = $self->{result} = $self->{tmpresult};
833             } else {
834 1064         2724 $final = $self->{result} = [ @rv ];
835             }
836 1064         2760 _update_result_info($self);
837              
838             # now the only things left is to handle explain in case of SPF_Fail
839 1064 100       6269 return @$final if $final->[0] ne SPF_Fail; # finally done
840              
841             # set default explanation
842 324 100       1091 $final->[3] = $self->explain_default if ! defined $final->[3];
843              
844             # lookup TXT record for explain
845 324 100       1035 if ( my $exp = delete $self->{explain} ) {
846 106 100       271 if (ref $exp) {
847 4 100       12 if ( my @dnsq = $self->_resolve_macro_p($exp)) {
848             # we need to do more DNS lookups for resolving %{p} macros
849             # inside the exp=... modifier, before we get the domain name
850             # which contains the TXT for explain
851 2 50       202 $DEBUG && DEBUG( "need to resolve %{p} in $exp->{macro}" );
852 2         7 $self->{explain} = $exp; # put back until resolved
853 2         8 return $self->_next_rv_dnsq(@dnsq)
854             }
855 2         6 $exp = $exp->{expanded};
856             }
857 104 50       312 if ( my @err = _check_domain( $exp, "explain:$exp" )) {
858             # bad domain: return unmodified final
859 0         0 return @$final;
860             }
861 104 50       523 $DEBUG && DEBUG( "lookup TXT for '$exp' for explain" );
862 104         338 $self->{cb} = [ \&_got_TXT_exp ];
863 104         434 return $self->_next_rv_dnsq( Net::DNS::Packet->new($exp,'TXT','IN'));
864             }
865              
866             # resolve macros in TXT record for explain
867 218 100       609 if ( my $exp = delete $final->[4] ) {
868             # we had a %{p} to resolve in the TXT we got for explain,
869             # see _got_TXT_exp -> should be expanded now
870 12         29 $final->[3] = $exp->{expanded};
871              
872             }
873              
874             # This was the last action needed
875 218         1464 return @$final;
876             }
877              
878             ############################################################################
879             # try to match or give more questions by
880             # - trying the next mechanism in the current SPF record
881             # - if there is no next mech try to redirect to another SPF record
882             # - if there is no redirect try to go include stack up
883             # - if there is no include stack return SPF_Neutral
884             # Args: $self
885             # Returns: @query|@final
886             # @query: new queries as list of Net::DNS::Packets
887             # @final: final SPF result (see sub next)
888             ############################################################################
889             sub _next_mech {
890 1077     1077   1625 my Mail::SPF::Iterator $self = shift;
891              
892 1077         2024 for my $dummy (1) {
893              
894             # if we have more mechanisms in the current SPF record take next
895 1201 100       1685 if ( my $next = shift @{$self->{mech}} ) {
  1201         3346  
896 1036         2924 my ($sub,$id,@arg) = @$next;
897 1036         2450 my @rv = $sub->($self,@arg);
898 1036 100       37202 redo if ! @rv; # still no match and no queries
899 920         3701 return @rv;
900             }
901              
902             # if no mechanisms in current SPF record but we have a redirect
903             # continue with the SPF record from the new location
904 165 100       505 if ( my $domain = $self->{redirect} ) {
905 108 50       284 if ( ref $domain ) {
906             # need to resolve %{p}
907 0 0 0     0 if ( $domain->{macro} and
908             ( my @rv = $self->_resolve_macro_p($domain))) {
909 0         0 return @rv;
910             }
911 0         0 $self->{redirect} = $domain = $domain->{expanded};
912             }
913 108 50       331 if ( my @err = _check_domain($domain,"redirect:$domain" )) {
914 0         0 return @err;
915             }
916              
917             return ( SPF_PermError, "",
918             { problem => "Number of DNS mechanism exceeded" })
919 108 100       433 if --$self->{limit_dns_mech} < 0;
920              
921             # reset state information
922 102         247 $self->{mech} = [];
923 102         197 $self->{explain} = undef;
924 102         180 $self->{redirect} = undef;
925              
926             # set domain to domain from redirect
927 102         201 $self->{domain} = $domain;
928              
929             # restart with new SPF record
930 102         302 return $self->_query_txt_spf;
931             }
932              
933             # if there are still no more mechanisms available and we are inside
934             # an include go up the include stack
935 57         125 my $st = $self->{include_stack};
936 57 100       167 if (@$st) {
937 8         20 my $top = pop @$st;
938 8         24 delete $top->{qual};
939 8         62 while ( my ($k,$v) = each %$top ) {
940 32         103 $self->{$k} = $v;
941             }
942             # continue with mech or redirect of upper SPF record
943 8         28 redo;
944             }
945             }
946              
947             # no mech, no redirect and no include stack
948             # -> give up finally and return SPF_Neutral
949 49         127 return ( SPF_Neutral,'no matches' );
950             }
951              
952             ############################################################################
953             # if @rv is list of DNS packets return them as (undef,@dnspkt)
954             # remember the queries so that the answers can later (sub next) verified
955             # against the queries
956             # Args: ($self,@dnsq)
957             # @dnsq: list of Net::DNS::Packet's
958             # Returns: (undef,@dnsq)
959             ############################################################################
960             sub _next_rv_dnsq {
961 1843     1843   11279 my Mail::SPF::Iterator $self = shift;
962 1843         3260 my @dnsq = @_;
963             # track queries for later verification
964             $self->{cbq} = [ map {
965 1843         3075 { q => ($_->question)[0], id => $_->header->id, pkt => $_ }
  2213         11769  
966             } @dnsq ];
967             $DEBUG && DEBUG( "need to lookup ".join( " | ",
968 1843 50       36139 map { "'".$_->{id}.'/'.$_->{q}->string."'" } @{$self->{cbq}}));
  2213         22224  
  1843         3714  
969 1843         11829 return ( undef,@dnsq );
970             }
971              
972             ############################################################################
973             # check if the domain has the right format
974             # this checks the domain before the macros got expanded
975             ############################################################################
976             sub _check_macro_domain {
977 458     458   991 my ($domain,$why) = @_;
978             # 'domain-spec': see RFC4408 Appendix A for ABNF
979 458         1537 my $rx = qr{
980             # macro-string
981             (?:
982             [^%\s]+ |
983             % (?: { [slodipvh] \d* r? [.\-+,/_=]* } | [%\-_] )
984             )*
985             # domain-end
986             (?:(?:
987             # toplabel
988             \. [\da-z]*[a-z][\da-z]* |
989             \. [\da-z]+-[\-a-z\d]*[\da-z]
990             ) | (?:
991             # macro-expand
992             % (?: { [slodipvh] \d* r? [.\-+,/_=]* } | [%\-_] )
993             ))
994             }xi;
995 458         1076 _check_domain( $domain,$why,$rx);
996             }
997              
998             ############################################################################
999             # check if the domain has the right format
1000             # this checks the domain after the macros got expanded
1001             ############################################################################
1002             sub _check_domain {
1003 2462     2462   5303 my ($domain,$why,$rx) = @_;
1004 2462 100       5219 $why = '' if ! defined $why;
1005              
1006             # domain name according to RFC2181 can be anything binary!
1007             # this is not only for host names
1008 2462   66     13345 $rx ||= qr{.*?};
1009              
1010 2462         3593 my @rv;
1011 2462 100 100     105311 if ( $domain =~m{[^\d.]}
1012             && $domain =~s{^($rx)\.?$}{$1} ) {
1013             # looks like valid domain name
1014 2378 100       9056 if ( grep { length == 0 || length>63 } split( m{\.},$domain,-1 )) {
  7394 100       27822  
    50          
1015 32         161 @rv = ( SPF_PermError,"query $why", { problem =>
1016             "DNS labels limited to 63 chars and should not be empty." });
1017             } elsif ( length($domain)>253 ) {
1018 0         0 @rv = ( SPF_PermError,"query $why",
1019             { problem => "Domain names limited to 253 chars." });
1020             } else {
1021             #DEBUG( "domain name ist OK" );
1022             return
1023 2346         12364 }
1024             } else {
1025 84         569 @rv = ( SPF_PermError, "query $why",
1026             { problem => "Invalid domain name" });
1027             }
1028              
1029 116 50       877 $DEBUG && DEBUG( "error with '$domain': ".$rv[2]{problem} );
1030 116         669 return @rv; # have error
1031             }
1032              
1033             ############################################################################
1034             # initial query
1035             # returns queries for SPF and TXT record, next state is _got_txt_spf
1036             ############################################################################
1037             sub _query_txt_spf {
1038 1226     1226   1891 my Mail::SPF::Iterator $self = shift;
1039 1226 50       4939 $DEBUG && DEBUG( "want SPF/TXT for $self->{domain}" );
1040             # return query for SPF and TXT, we see what we get first
1041 1226 100       3444 if ( my @err = _check_domain( $self->{domain}, "SPF/TXT record" )) {
1042 20 50       63 if ( ! $self->{cb} ) {
1043             # for initial query return SPF_None on errors
1044 20         43 $err[0] = SPF_None;
1045             }
1046 20         77 return @err;
1047             }
1048              
1049 1206         3133 $self->{cb} = [ \&_got_txt_spf ];
1050             return (
1051             # use SPF DNS record only if rfc4408 compatibility is required
1052             $self->{opt}{rfc4408}
1053             ? (scalar(Net::DNS::Packet->new( $self->{domain}, 'SPF','IN' ))):(),
1054 1206 100       7980 scalar(Net::DNS::Packet->new( $self->{domain}, 'TXT','IN' )),
1055             );
1056             }
1057              
1058             ############################################################################
1059             # processes response to SPF|TXT query
1060             # parses response and starts processing
1061             ############################################################################
1062             sub _got_txt_spf {
1063 1202     1202   6452 my Mail::SPF::Iterator $self = shift;
1064 1202         2650 my ($qtype,$rcode,$ans,$add) = @_;
1065              
1066             {
1067 1202 100       1641 last if ! @$ans;
  1202         2468  
1068              
1069             # RFC4408 says in 4.5:
1070             # 2. If any records of type SPF are in the set, then all records of
1071             # type TXT are discarded.
1072             # But it says that if both SPF and TXT are given they should be the
1073             # same (3.1.1)
1074             # so I think we can ignore the requirement 4.5.2 and just use the
1075             # first record which is valid SPF, if the admin of the domain sets
1076             # TXT and SPF to different values it's his own problem
1077              
1078 1160         1783 my (@spfdata,@senderid);
1079 1160         2037 for my $rr (@$ans) {
1080 1194         3020 my $txtdata = join( '', $rr->char_str_list );
1081 1194 100       39941 $txtdata =~m{^
1082             (?:
1083             (v=spf1)
1084             | spf2\.\d/(?:[\w,]*\bmfrom\b[\w,]*)
1085             )
1086             (?:$|\040\s*)(.*)
1087             }xi or next;
1088 1164 100       3816 if ( $1 ) {
1089 1156         2893 push @spfdata,$2;
1090 1156 50       4387 $DEBUG && DEBUG( "got spf data for $qtype: $txtdata" );
1091             } else {
1092 8         25 push @senderid,$2;
1093 8 50       36 $DEBUG && DEBUG( "got senderid data for $qtype: $txtdata" );
1094             }
1095             }
1096              
1097             # if SenderID and SPF are given prefer SPF, else use any
1098 1160 100       2748 @spfdata = @senderid if ! @spfdata;
1099              
1100 1160 100       2444 @spfdata or last; # no usable SPF reply
1101 1144 100       2500 if (@spfdata>1) {
1102 18         120 return ( SPF_PermError,
1103             "checking $qtype for $self->{domain}",
1104             { problem => "multiple SPF records" }
1105             );
1106             }
1107 1126 100       1845 unless ( eval { $self->_parse_spf( $spfdata[0] ) }) {
  1126         2769  
1108             # this is an invalid SPF record
1109             # make it a permanent error
1110             # it does not matter if the other type of record is good
1111             # because according to RFC if both provide SPF (v=spf1..)
1112             # they should be the same, so the other one should be bad too
1113 354         2615 return ( SPF_PermError,
1114             "checking $qtype for $self->{domain}",
1115             { problem => "invalid SPF record: $@" }
1116             );
1117             }
1118              
1119             # looks good, return so that next() processes the next query
1120 772         2831 return;
1121             }
1122              
1123             # If this is the first response, wait for the other
1124 58 50       296 $DEBUG && DEBUG( "no records for $qtype ($rcode)" );
1125 58 100       111 if ( grep { $_->{pkt} } @{ $self->{cbq}} ) {
  88         262  
  58         168  
1126 18         63 return (SPF_Noop);
1127             }
1128              
1129             # otherwise it means that we got no SPF or TXT records
1130              
1131             # if we have a default record and we are at the first level use this
1132 40 50 66     204 if (!$self->{mech} and my $default = $self->{opt}{default_spf}) {
1133 0 0       0 if (eval { $self->_parse_spf($default) }) {
  0         0  
1134             # good
1135 0         0 $self->{used_default_spf} = $default;
1136 0         0 return;
1137             }
1138 0         0 return (SPF_PermError,
1139             "checking default SPF for $self->{domain}",
1140             { problem => "invalid default SPF record: $@" }
1141             );
1142             }
1143              
1144             # return SPF_None if this was the initial query ($self->{mech} is undef)
1145             # and SPF_PermError if as a result from redirect or include
1146             # ($self->{mech} is [])
1147 40 50       156 $DEBUG && DEBUG( "no usable SPF/TXT records" );
1148 40 100       235 return ( $self->{mech} ? SPF_PermError : SPF_None,
1149             'query SPF/TXT record',
1150             { problem => 'no SPF records found' });
1151             }
1152              
1153              
1154             ############################################################################
1155             # parse SPF record, returns 1 if record looks valid,
1156             # otherwise die()s with somewhat helpful error message
1157             ############################################################################
1158             sub _parse_spf {
1159 1126     1126   1860 my Mail::SPF::Iterator $self = shift;
1160 1126         1758 my $data = shift;
1161              
1162 1126         1810 my (@mech,$redirect,$explain);
1163 1126         3209 for ( split( ' ', $data )) {
1164 1954 100       12906 my ($qual,$mech,$mod,$arg) = m{^(?:
1165             ([~\-+?]?) # Qualifier
1166             (all|ip[46]|a|mx|ptr|exists|include) # Mechanism
1167             |(redirect|exp) # Modifier
1168             |[a-zA-Z][\w.\-]*= # unknown modifier + '='
1169             )([ \t\x20-\x7e]*) # Arguments
1170             $}x
1171             or die "bad SPF part: $_\n";
1172              
1173 1900 100       4751 if ( $mech ) {
    100          
1174 1568   100     5017 $qual = $qual2rv{ $qual || '+' };
1175              
1176 1568 100 100     5967 if ( $mech eq 'all' ) {
    100          
    100          
    100          
    100          
    100          
    50          
1177 520 100       1138 die "no arguments allowed with mechanism 'all': '$_'\n"
1178             if $arg ne '';
1179 502         1694 push @mech, [ \&_mech_all, $_, $qual ]
1180              
1181             } elsif ( $mech eq 'ip4' ) {
1182 238 100       1588 my ($ip,$plen) =
1183             $arg =~m{^:(\d+\.\d+\.\d+\.\d+)(?:/([1-9]\d*|0))?$}
1184             or die "bad argument for mechanism 'ip4' in '$_'\n";
1185 208 100       645 $plen = 32 if ! defined $plen;
1186 208 100       520 $plen>32 and die "invalid prefix len >32 in '$_'\n";
1187 202 50       310 eval { $ip = inet_aton( $ip ) }
  202         1020  
1188             or die "bad ip '$ip' in '$_'\n";
1189 202 50       607 next if ! $self->{clientip4}; # don't use for IP6
1190 202         871 push @mech, [ \&_mech_ip4, $_, $qual, $ip,$plen ];
1191              
1192             } elsif ( $mech eq 'ip6' ) {
1193 24 100       186 my ($ip,$plen) =
1194             $arg =~m{^:([\da-fA-F:\.]+)(?:/([1-9]\d*|0))?$}
1195             or die "bad argument for mechanism 'ip6' in '$_'\n";
1196 18 50       52 $plen = 128 if ! defined $plen;
1197 18 100       111 $plen>128 and die "invalid prefix len >128 in '$_'\n";
1198 12 50 50     36 eval { $ip = inet_pton( AF_INET6,$ip ) }
  12         83  
1199             or die "bad ip '$ip' in '$_'\n"
1200             if $can_ip6;
1201 12 50       58 next if ! $self->{clientip6}; # don't use for IP4
1202 0         0 push @mech, [ \&_mech_ip6, $_, $qual, $ip,$plen ];
1203              
1204             } elsif ( $mech eq 'a' or $mech eq 'mx' ) {
1205 514   100     1441 $arg ||= '';
1206 514 100       3415 my ($domain,$plen4,$plen6) =
1207             $arg =~m{^
1208             (?: : (.+?))? # [ ":" domain-spec ]
1209             (?: / (?: ([1-9]\d*|0) ))? # [ ip4-cidr-length ]
1210             (?: // (?: ([1-9]\d*|0) ))? # [ "/" ip6-cidr-length ]
1211             $}x or die "bad argument for mechanism '$mech' in '$_'\n";
1212              
1213 498 100       1168 $plen4 = 32 if ! defined $plen4;
1214 498 100       952 $plen6 = 128 if ! defined $plen6;
1215 498 100       1116 die "invalid prefix len >32 in '$_'\n" if $plen4>32;
1216 486 100       976 die "invalid prefix len >128 in '$_'\n" if $plen6>128;
1217 474 100       847 if ( ! $domain ) {
1218 288         548 $domain = $self->{domain};
1219             } else {
1220 186 100       430 if ( my @err = _check_macro_domain($domain)) {
1221 72   50     611 die(($err[2]->{problem}||"Invalid domain name")."\n");
1222             }
1223 114         404 $domain = $self->_macro_expand($domain);
1224             }
1225 402 100       1054 my $sub = $mech eq 'a' ? \&_mech_a : \&_mech_mx;
1226 402 50       869 push @mech, [ \&_resolve_macro_p, $domain ] if ref($domain);
1227             push @mech, [ $sub, $_, $qual, $domain,
1228 402 50       1838 $self->{clientip4} ? $plen4:$plen6 ];
1229              
1230             } elsif ( $mech eq 'ptr' ) {
1231 50 100 100     3688 my ($domain) = ( $arg || '' )=~m{^(?::([^/]+))?$}
1232             or die "bad argument for mechanism '$mech' in '$_'\n";
1233             $domain = $domain
1234             ? $self->_macro_expand($domain)
1235 38 100       144 : $self->{domain};
1236 38 50       118 push @mech, [ \&_resolve_macro_p, $_, $domain ] if ref($domain);
1237 38         188 push @mech, [ \&_mech_ptr, $_, $qual, $domain ];
1238              
1239             } elsif ( $mech eq 'exists' ) {
1240 58 100 100     471 my ($domain) = ( $arg || '' )=~m{^:([^/]+)$}
1241             or die "bad argument for mechanism '$mech' in '$_'\n";
1242 40         139 $domain = $self->_macro_expand($domain);
1243 26 100       111 push @mech, [ \&_resolve_macro_p, $_, $domain ] if ref($domain);
1244 26         123 push @mech, [ \&_mech_exists, $_, $qual, $domain ];
1245              
1246             } elsif ( $mech eq 'include' ) {
1247 164 100 100     1011 my ($domain) = ( $arg || '' )=~m{^:([^/]+)$}
1248             or die "bad argument for mechanism '$mech' in '$_'\n";
1249 140         733 $domain = $self->_macro_expand($domain);
1250 140 50       377 push @mech, [ \&_resolve_macro_p, $_, $domain ] if ref($domain);
1251 140         625 push @mech, [ \&_mech_include, $_, $qual, $domain ];
1252              
1253             } else {
1254 0         0 die "unhandled mechanism '$mech'\n"
1255             }
1256              
1257             } elsif ( $mod ) {
1258             # multiple redirect or explain will be considered an error
1259 302 100       876 if ( $mod eq 'redirect' ) {
    50          
    0          
1260 144 100       364 die "redirect was specified more than once\n" if $redirect;
1261 138 100 50     768 my ($domain) = ( $arg || '' )=~m{^=([^/]+)$}
1262             or die "bad argument for modifier '$mod' in '$_'\n";
1263 126 100       343 if ( my @err = _check_macro_domain($domain)) {
1264 6   50     58 die(( $err[2]->{problem} || "Invalid domain name" )."\n" );
1265             }
1266 120         416 $redirect = $self->_macro_expand($domain);
1267              
1268             } elsif ( $mod eq 'exp' ) {
1269 158 100       401 die "$explain was specified more than once\n" if $explain;
1270 152 100 50     856 my ($domain) = ( $arg || '' )=~m{^=([^/]+)$}
1271             or die "bad argument for modifier '$mod' in '$_'\n";
1272 146 100       387 if ( my @err = _check_macro_domain($domain)) {
1273 12   50     110 die(( $err[2]->{problem} || "Invalid domain name" )."\n" );
1274             }
1275 134         432 $explain = $self->_macro_expand($domain);
1276              
1277             } elsif ( $mod ) {
1278 0         0 die "unhandled modifier '$mod'\n"
1279             }
1280             } else {
1281             # unknown modifier - check if arg is valid macro-string
1282             # (will die() on error) but ignore modifier
1283 30   50     162 $self->_macro_expand($arg || '');
1284             }
1285             }
1286              
1287 772 100       2351 if ($self->{opt}{pass_all}) {
1288 256         388 my $r = 0;
1289 256         508 for (@mech) {
1290 380         663 my $qual = $_->[2];
1291 380 100       883 last if $_->[0] == \&_mech_include;
1292 334 100       736 $r=-1,last if $qual eq SPF_Fail;
1293 226 100 100     811 $r=+1,last if $qual eq SPF_Pass and $_->[0] == \&_mech_all;
1294             }
1295 256 100       554 if ($r == 1) {
1296             # looks like a pass all rule
1297             $self->{result} = [
1298 6         23 $self->{opt}{pass_all}, "",
1299             { problem => "record designed to allow every sender" }
1300             ];
1301 6         18 _update_result_info($self);
1302             }
1303             }
1304 772         1492 $self->{mech} = \@mech;
1305 772         1391 $self->{explain} = $explain;
1306 772         1252 $self->{redirect} = $redirect;
1307 772         2356 return 1;
1308             }
1309              
1310             ############################################################################
1311             # handles mechanism 'all'
1312             # matches all time
1313             ############################################################################
1314             sub _mech_all {
1315 302     302   514 my Mail::SPF::Iterator $self = shift;
1316 302         535 my $qual = shift;
1317 302 50       1137 $DEBUG && DEBUG( "match mech all with qual=$qual" );
1318 302         1197 return ( $qual,'matches default', { mechanism => 'all' });
1319             }
1320              
1321             ############################################################################
1322             # handle mechanism 'ip4'
1323             # matches if clients IP4 address is in ip/mask
1324             ############################################################################
1325             sub _mech_ip4 {
1326 162     162   280 my Mail::SPF::Iterator $self = shift;
1327 162         362 my ($qual,$ip,$plen) = @_;
1328 162 50       471 defined $self->{clientip4} or return (); # ignore rule, no IP4 address
1329 162 100       628 if ( ($self->{clientip4} & $mask4[$plen]) eq ($ip & $mask4[$plen]) ) {
1330             # rules matches
1331 46 50       445 $DEBUG && DEBUG( "match mech ip4:".inet_ntoa($ip)."/$plen with qual=$qual" );
1332 46         378 return ($qual,"matches ip4:".inet_ntoa($ip)."/$plen",
1333             { mechanism => 'ip4' } )
1334             }
1335 116 50       921 $DEBUG && DEBUG( "no match mech ip4:".inet_ntoa($ip)."/$plen" );
1336 116         332 return (); # ignore, no match
1337             }
1338              
1339             ############################################################################
1340             # handle mechanism 'ip6'
1341             # matches if clients IP6 address is in ip/mask
1342             ############################################################################
1343             sub _mech_ip6 {
1344 0     0   0 my Mail::SPF::Iterator $self = shift;
1345 0         0 my ($qual,$ip,$plen) = @_;
1346 0 0       0 defined $self->{clientip6} or return (); # ignore rule, no IP6 address
1347 0 0       0 if ( ($self->{clientip6} & $mask6[$plen]) eq ($ip & $mask6[$plen])) {
1348             # rules matches
1349 0 0       0 $DEBUG && DEBUG( "match mech ip6:".inet_ntop(AF_INET6,$ip)."/$plen with qual=$qual" );
1350 0         0 return ($qual,"matches ip6:".inet_ntop(AF_INET6,$ip)."/$plen",
1351             { mechanism => 'ip6' } )
1352             }
1353 0 0       0 $DEBUG && DEBUG( "no match ip6:".inet_ntop(AF_INET6,$ip)."/$plen" );
1354 0         0 return (); # ignore, no match
1355             }
1356              
1357             ############################################################################
1358             # handle mechanism 'a'
1359             # check if one of the A/AAAA records for $domain resolves to
1360             # clientip/plen,
1361             ############################################################################
1362             sub _mech_a {
1363 266     266   457 my Mail::SPF::Iterator $self = shift;
1364 266         610 my ($qual,$domain,$plen) = @_;
1365 266 50       597 $domain = $domain->{expanded} if ref $domain;
1366 266 50       1165 $DEBUG && DEBUG( "check mech a:$domain/$plen with qual=$qual" );
1367 266 100       920 if ( my @err = _check_domain($domain, "a:$domain/$plen")) {
1368             # spec is not clear here:
1369             # variante1: no match on invalid domain name -> return
1370             # variante2: propagate err -> return @err
1371             # we use variante2 for now
1372 6 50       60 $DEBUG && DEBUG( "no match mech a:$domain/$plen - @err" );
1373 6         23 return @err;
1374             }
1375              
1376             return ( SPF_PermError, "",
1377             { problem => "Number of DNS mechanism exceeded" })
1378 260 100       842 if --$self->{limit_dns_mech} < 0;
1379              
1380 252 50       650 my $typ = $self->{clientip4} ? 'A':'AAAA';
1381 252         923 $self->{cb} = [ \&_got_A, $qual,$plen,[ $domain ],'a' ];
1382 252         1001 return scalar(Net::DNS::Packet->new( $domain, $typ,'IN' ));
1383             }
1384              
1385             ############################################################################
1386             # this is used in _mech_a and in _mech_mx if the address for an MX is not
1387             # sent inside the additional data
1388             # in the case of MX $names might contain more than one name to resolve, it
1389             # will try to resolve names to addresses and to match them until @$names
1390             # is empty
1391             ############################################################################
1392             sub _got_A {
1393 299     299   1171 my Mail::SPF::Iterator $self = shift;
1394 299         861 my ($qtype,$rcode,$ans,$add,$qual,$plen,$names,$mech) = @_;
1395 299         596 my $domain = shift(@$names);
1396              
1397 299 50       1466 $DEBUG && DEBUG( "got response to $qtype for $domain: $rcode" );
1398 299 100       1045 if ( $rcode eq 'NXDOMAIN' ) {
    50          
1399 48 50       240 $DEBUG && DEBUG( "no match mech a:$domain/$plen - $rcode" );
1400             # no records found
1401             } elsif ( $rcode ne 'NOERROR' ) {
1402 0 0       0 $DEBUG && DEBUG( "temperror mech a:$domain/$plen - $rcode" );
1403 0         0 return ( SPF_TempError,
1404             "getting $qtype for $domain",
1405             { problem => "error resolving $domain" }
1406             );
1407             }
1408              
1409 299         652 my @addr = map { $_->address } @$ans;
  245         1131  
1410 299         2135 return _check_A_match($self,$qual,$domain,$plen,\@addr,$names,$mech);
1411             }
1412              
1413             sub _check_A_match {
1414 365     365   600 my Mail::SPF::Iterator $self = shift;
1415 365         935 my ($qual,$domain,$plen,$addr,$names,$mech) = @_;
1416              
1417             # process all found addresses
1418 365 50       864 if ( $self->{clientip4} ) {
1419 365 50       842 $plen = 32 if ! defined $plen;
1420 365         703 my $mask = $mask4[$plen];
1421 365         793 for my $addr (@$addr) {
1422 285 50       1183 $DEBUG && DEBUG( "check a:$domain($addr)/$plen for mech $mech" );
1423 285 50 33     1582 my $packed = $addr=~m{^[\d.]+$} && eval { inet_aton($addr) }
1424             or return ( SPF_TempError,
1425             "getting A for $domain",
1426             { problem => "bad address in A record" }
1427             );
1428              
1429 285 100       1106 if ( ($packed & $mask) eq ($self->{clientip4} & $mask) ) {
1430             # match!
1431 87 50       415 $DEBUG && DEBUG( "match mech a:.../$plen for mech $mech with qual $qual" );
1432 87         696 return ($qual,"matches domain: $domain/$plen with IP4 $addr",
1433             { mechanism => $mech })
1434             }
1435             }
1436             } else { # AAAA
1437 0 0       0 $plen = 128 if ! defined $plen;
1438 0         0 my $mask = $mask6[$plen];
1439 0         0 for my $addr (@$addr) {
1440 0 0       0 $DEBUG && DEBUG( "check a:$domain($addr)//$plen for mech $mech" );
1441 0 0       0 my $packed = eval { inet_pton(AF_INET6,$addr) }
  0         0  
1442             or return ( SPF_TempError,
1443             "getting AAAA for $domain",
1444             { problem => "bad address in AAAA record" }
1445             );
1446 0 0       0 if ( ($packed & $mask) eq ($self->{clientip6} & $mask) ) {
1447             # match!
1448 0 0       0 $DEBUG && DEBUG( "match mech a:...//$plen for mech $mech with qual $qual" );
1449 0         0 return ($qual,"matches domain: $domain//$plen with IP6 $addr",
1450             { mechanism => $mech })
1451             }
1452             }
1453             }
1454              
1455             # no match yet, can we resolve another name?
1456 278 100       698 if ( @$names ) {
1457 51 50       135 my $typ = $self->{clientip4} ? 'A':'AAAA';
1458 51 50       241 $DEBUG && DEBUG( "check mech a:$names->[0]/$plen for mech $mech with qual $qual" );
1459 51         180 $self->{cb} = [ \&_got_A, $qual,$plen,$names,$mech ];
1460 51         195 return scalar(Net::DNS::Packet->new( $names->[0], $typ,'IN' ));
1461             }
1462              
1463             # finally no match
1464 227 50       939 $DEBUG && DEBUG( "no match mech $mech:$domain/$plen" );
1465 227         832 return;
1466             }
1467              
1468              
1469              
1470             ############################################################################
1471             # handle mechanism 'mx'
1472             # similar to mech 'a', we expect the A/AAAA records for the MX in the
1473             # additional section of the DNS response
1474             ############################################################################
1475             sub _mech_mx {
1476 110     110   183 my Mail::SPF::Iterator $self = shift;
1477 110         243 my ($qual,$domain,$plen) = @_;
1478 110 50       245 $domain = $domain->{expanded} if ref $domain;
1479 110 50       487 if ( my @err = _check_domain($domain,
    50          
1480             "mx:$domain".( defined $plen ? "/$plen":"" ))) {
1481 0 0       0 $DEBUG && DEBUG( "no mech mx:$domain/$plen - @err" );
1482             return @err
1483 0         0 }
1484              
1485             return ( SPF_PermError, "",
1486             { problem => "Number of DNS mechanism exceeded" })
1487 110 50       366 if --$self->{limit_dns_mech} < 0;
1488              
1489 110         369 $self->{cb} = [ \&_got_MX,$qual,$domain,$plen ];
1490 110         436 return scalar(Net::DNS::Packet->new( $domain, 'MX','IN' ));
1491             }
1492              
1493             sub _got_MX {
1494 106     106   507 my Mail::SPF::Iterator $self = shift;
1495 106         297 my ($qtype,$rcode,$ans,$add,$qual,$domain,$plen) = @_;
1496              
1497 106 50       456 if ( $rcode eq 'NXDOMAIN' ) {
    50          
    100          
1498 0 0       0 $DEBUG && DEBUG( "no match mech mx:$domain/$plen - $rcode" );
1499             # no records found
1500             } elsif ( $rcode ne 'NOERROR' ) {
1501 0 0       0 $DEBUG && DEBUG( "no match mech mx:$domain/$plen - $rcode" );
1502 0         0 return ( SPF_TempError,
1503             "getting MX form $domain",
1504             { problem => "error resolving $domain" }
1505             );
1506             } elsif ( ! @$ans ) {
1507 36 50       194 $DEBUG && DEBUG( "no match mech mx:$domain/$plen - no MX records" );
1508 36         118 return; # domain has no MX -> no match
1509             }
1510              
1511             # all MX, with best (lowest) preference first
1512 130         2425 my @mx = map { $_->[0] }
1513 138         310 sort { $a->[1] <=> $b->[1] }
1514 70         157 map { [ $_->exchange, $_->preference ] }
  130         994  
1515             @$ans;
1516 70         203 my %mx = map { $_ => [] } @mx;
  130         334  
1517              
1518 70 100       226 if (!$self->{opt}{rfc4408}) {
1519             # RFC 4408 limited the number of MX to query to 10
1520             # RFC 7208 instead said that ALL returned MX should count
1521             # against the limit and the test suite suggest that this limit
1522             # should be enforced before even asking the MX
1523             return ( SPF_PermError, "",
1524             { problem => "Number of DNS mechanism exceeded" })
1525 52 100       192 if $self->{limit_dns_mech}-@mx < 0;
1526             }
1527              
1528             # try to find A|AAAA records in additional data
1529 66 50       182 my $atyp = $self->{clientip4} ? 'A':'AAAA';
1530 66         155 for my $rr (@$add) {
1531 46 100 66     414 if ( $rr->type eq $atyp && exists $mx{$rr->name} ) {
1532 40         851 push @{$mx{$rr->name}},$rr->address;
  40         80  
1533             }
1534             }
1535             $DEBUG && DEBUG( "found mx for $domain: ".join( " ",
1536 66 50       684 map { $mx{$_} ? "$_(".join(",",@{$mx{$_}}).")" : $_ } @mx ));
  86 50       239  
  86         396  
1537              
1538             # remove from @mx where I've found addresses
1539 66         164 @mx = grep { ! @{$mx{$_}} } @mx;
  86         117  
  86         265  
1540             # limit the Rest to 10 records (rfc4408,10.1)
1541 66 100       190 splice(@mx,10) if @mx>10;
1542              
1543 66         155 my @addr = map { @$_ } values %mx;
  68         172  
1544 66         246 return _check_A_match( $self,$qual,"(mx)".$domain,$plen,\@addr,\@mx,'mx');
1545             }
1546              
1547             ############################################################################
1548             # handle mechanis 'exists'
1549             # just check, if I get any A record for the domain (lookup for A even if
1550             # I use IP6 - this is RBL style)
1551             ############################################################################
1552             sub _mech_exists {
1553 20     20   46 my Mail::SPF::Iterator $self = shift;
1554 20         57 my ($qual,$domain) = @_;
1555 20 100       68 $domain = $domain->{expanded} if ref $domain;
1556 20 50       71 if ( my @err = _check_domain($domain, "exists:$domain" )) {
1557 0 0       0 $DEBUG && DEBUG( "no match mech exists:$domain - @err" );
1558             return @err
1559 0         0 }
1560              
1561             return ( SPF_PermError, "",
1562             { problem => "Number of DNS mechanism exceeded" })
1563 20 50       72 if --$self->{limit_dns_mech} < 0;
1564              
1565 20         75 $self->{cb} = [ \&_got_A_exists,$qual,$domain ];
1566 20         93 return scalar(Net::DNS::Packet->new( $domain, 'A','IN' ));
1567             }
1568              
1569             sub _got_A_exists {
1570 20     20   129 my Mail::SPF::Iterator $self = shift;
1571 20         67 my ($qtype,$rcode,$ans,$add,$qual,$domain) = @_;
1572              
1573 20 50       90 if ( $rcode ne 'NOERROR' ) {
    50          
1574 0 0       0 $DEBUG && DEBUG( "no match mech exists:$domain - $rcode" );
1575 0         0 return;
1576             } elsif ( ! @$ans ) {
1577 0 0       0 $DEBUG && DEBUG( "no match mech exists:$domain - no A records" );
1578 0         0 return;
1579             }
1580 20 50       117 $DEBUG && DEBUG( "match mech exists:$domain with qual $qual" );
1581 20         120 return ($qual,"domain $domain exists", { mechanism => 'exists' } )
1582             }
1583              
1584              
1585              
1586             ############################################################################
1587             # PTR
1588             # this is the most complex and most expensive mechanism:
1589             # - first get domains from PTR records for IP (clientip4|clientip6)
1590             # - filter for domains which match $domain (because only these are interesting
1591             # for matching)
1592             # - then verify the domains, if they point back to the IP by doing A|AAAA
1593             # lookups until one domain can be validated
1594             ############################################################################
1595             sub _mech_ptr {
1596 34     34   68 my Mail::SPF::Iterator $self = shift;
1597 34         90 my ($qual,$domain) = @_;
1598 34 50       96 $domain = $domain->{expanded} if ref $domain;
1599 34 50       110 if ( my @err = _check_domain($domain, "ptr:$domain" )) {
1600 0 0       0 $DEBUG && DEBUG( "no match mech ptr:$domain - @err" );
1601             return @err
1602 0         0 }
1603              
1604             return ( SPF_PermError, "",
1605             { problem => "Number of DNS mechanism exceeded" })
1606 34 50       131 if --$self->{limit_dns_mech} < 0;
1607              
1608 34   33     120 my $ip = $self->{clientip4} || $self->{clientip6};
1609 34 50       164 if ( exists $self->{validated}{$ip}{$domain} ) {
1610             # already checked
1611 0 0       0 if ( ! $self->{validated}{$ip}{$domain} ) {
1612             # could not be validated
1613 0 0       0 $DEBUG && DEBUG( "no match mech ptr:$domain - cannot validate $ip/$domain" );
1614 0         0 return; # ignore
1615             } else {
1616 0 0       0 $DEBUG && DEBUG( "match mech ptr:$domain with qual $qual" );
1617 0         0 return ($qual,"$domain validated" );
1618             }
1619             }
1620              
1621 34         62 my $query;
1622 34 50       81 if ( $self->{clientip4} ) {
1623             $query = join( '.', reverse split( m/\./,
1624 34         305 inet_ntoa($self->{clientip4}) ))
1625             .'.in-addr.arpa'
1626             } else {
1627             $query = join( '.', split( //,
1628 0         0 reverse unpack("H*",$self->{clientip6}) ))
1629             .'.ip6.arpa';
1630             }
1631              
1632 34         149 $self->{cb} = [ \&_got_PTR,$qual,$query,$domain ];
1633 34         147 return scalar(Net::DNS::Packet->new( $query, 'PTR','IN' ));
1634             }
1635              
1636             sub _got_PTR {
1637 34     34   188 my Mail::SPF::Iterator $self = shift;
1638 34         132 my ($qtype,$rcode,$ans,$add,$qual,$query,$domain) = @_;
1639              
1640             # ignore mech if it can not be validated
1641 34 100       102 $rcode eq 'NOERROR' or do {
1642 8 50       57 $DEBUG && DEBUG( "no match mech ptr:$domain - $rcode" );
1643 8         30 return;
1644             };
1645 26 50       70 my @names = map { $_->ptrdname } @$ans or do {
  122         902  
1646 0 0       0 $DEBUG && DEBUG( "no match mech ptr:$domain - no names in PTR lookup" );
1647 0         0 return;
1648             };
1649              
1650             # strip records, which do not end in $domain
1651 26 100       303 @names = grep { $_ eq $domain || m{\.\Q$domain\E$} } @names;
  122         1012  
1652 26 50       91 if ( ! @names ) {
1653 0 0       0 $DEBUG && DEBUG( "no match mech ptr:$domain - no names in PTR lookup match $domain" );
1654             # return if no matches inside $domain
1655 0         0 return;
1656             }
1657              
1658             # limit to no more then 10 names (see RFC4408, 10.1)
1659 26 50       76 splice(@names,10) if @names>10;
1660              
1661             # validate the rest by looking up the IP and verifying it
1662             # with the original IP (clientip)
1663 26 50       91 my $typ = $self->{clientip4} ? 'A':'AAAA';
1664              
1665 26         94 $self->{cb} = [ \&_got_A_ptr, $qual,\@names ];
1666 26         3385 return scalar(Net::DNS::Packet->new( $names[0], $typ,'IN' ));
1667             }
1668              
1669             sub _got_A_ptr {
1670 26     26   154 my Mail::SPF::Iterator $self = shift;
1671 26         97 my ($qtype,$rcode,$ans,$add,$qual,$names) = @_;
1672              
1673 26 50       95 for my $dummy ( $rcode eq 'NOERROR' ? (1):() ) {
1674 26 100       81 @$ans or last; # no addr for domain? - try next
1675 20         60 my @addr = map { $_->address } @$ans;
  20         65  
1676              
1677             # check if @addr contains clientip
1678 20         244 my ($match,$ip);
1679 20 50       78 if ( $ip = $self->{clientip4} ) {
1680 20         49 for(@addr) {
1681 20 50       157 m{^[\d\.]+$} or next;
1682 20 50       45 eval { inet_aton($_) } eq $ip or next;
  20         114  
1683 20         50 $match = 1;
1684 20         43 last;
1685             }
1686             } else {
1687 0         0 $ip = $self->{clientip6};
1688 0         0 for(@addr) {
1689 0 0       0 eval { inet_pton(AF_INET6,$_) } eq $ip or next;
  0         0  
1690 0         0 $match = 1;
1691 0         0 last;
1692             }
1693             }
1694              
1695             # cache verification status
1696 20         77 $self->{validated}{$ip}{$names->[0]} = $match;
1697              
1698             # return $qual if we have verified the ptr
1699 20 50       71 if ($match) {
1700 20 50       114 $DEBUG && DEBUG( "match mech ptr:... with qual $qual" );
1701 20         127 return ( $qual,"verified clientip with ptr", { mechanism => 'ptr' })
1702             }
1703             }
1704              
1705             # try next
1706 6         16 shift @$names;
1707 6 50       25 @$names or do {
1708             # no next
1709 6 50       29 $DEBUG && DEBUG( "no match mech ptr:... - no more names for clientip" );
1710 6         26 return;
1711             };
1712              
1713             # cb stays the same
1714 0         0 return scalar(Net::DNS::Packet->new( $names->[0], $qtype,'IN' ));
1715             }
1716              
1717              
1718             ############################################################################
1719             # mechanism include
1720             # include SPF from other domain, propagate errors and consider Pass
1721             # from this inner SPF as match for the include mechanism
1722             ############################################################################
1723             sub _mech_include {
1724 136     136   244 my Mail::SPF::Iterator $self = shift;
1725 136         292 my ($qual,$domain) = @_;
1726 136 50       297 $domain = $domain->{expanded} if ref $domain;
1727 136 50       407 if ( my @err = _check_domain($domain, "include:$domain" )) {
1728 0 0       0 $DEBUG && DEBUG( "failed mech include:$domain - @err" );
1729             return @err
1730 0         0 }
1731              
1732 136 50       635 $DEBUG && DEBUG( "mech include:$domain with qual=$qual" );
1733              
1734             return ( SPF_PermError, "",
1735             { problem => "Number of DNS mechanism exceeded" })
1736 136 100       457 if --$self->{limit_dns_mech} < 0;
1737              
1738             # push and reset current domain and SPF record
1739 130         742 push @{$self->{include_stack}}, {
1740             domain => $self->{domain},
1741             mech => $self->{mech},
1742             explain => $self->{explain},
1743             redirect => $self->{redirect},
1744 130         214 qual => $qual,
1745             };
1746 130         272 $self->{domain} = $domain;
1747 130         245 $self->{mech} = [];
1748 130         213 $self->{explain} = undef;
1749 130         217 $self->{redirect} = undef;
1750              
1751             # start with new SPF record
1752 130         285 return $self->_query_txt_spf;
1753             }
1754              
1755              
1756             ############################################################################
1757             # create explain message from TXT record
1758             ############################################################################
1759             sub _got_TXT_exp {
1760 98     98   603 my Mail::SPF::Iterator $self = shift;
1761 98         248 my ($qtype,$rcode,$ans,$add) = @_;
1762 98         203 my $final = $self->{result};
1763              
1764 98 100       265 if ( $rcode ne 'NOERROR' ) {
1765 4 50       22 $DEBUG && DEBUG( "DNS error for exp TXT lookup" );
1766             # just return the final rv
1767 4         19 return @$final;
1768             }
1769              
1770 94         203 my ($txtdata,$t2) = grep { length } map { $_->txtdata } @$ans;;
  98         2441  
  98         521  
1771 94 100       336 if ( $t2 ) {
    100          
1772             # only one record should be returned
1773 10 50       56 $DEBUG && DEBUG( "got more than one TXT -> error" );
1774 10         50 return @$final;
1775             } elsif ( ! $txtdata ) {
1776 6 50       31 $DEBUG && DEBUG( "no text in TXT for explain" );
1777 6         31 return @$final;
1778             }
1779              
1780 78 50       374 $DEBUG && DEBUG( "got TXT $txtdata" );
1781              
1782             # valid TXT record found -> expand macros
1783 78         150 my $exp = eval { $self->_macro_expand( $txtdata,'exp' ) };
  78         238  
1784 78 100       235 if ($@) {
1785 6 50       46 $DEBUG && DEBUG( "macro expansion of '$txtdata' failed: $@" );
1786 6         34 return @$final;
1787             }
1788              
1789             # explain
1790 72 100       219 if (ref $exp) {
1791 12 50       47 if ( my @xrv = $self->_resolve_macro_p($exp)) {
1792             # we need to do more DNS lookups for resolving %{p} macros
1793 12 50       1124 $DEBUG && DEBUG( "need to resolve %{p} in $exp->{macro}" );
1794 12         38 $final->[4] = $exp;
1795 12         50 return @xrv;
1796             }
1797 0         0 $exp = $exp->{expanded};
1798             }
1799              
1800             # result should be limited to US-ASCII!
1801             # further limit to printable chars
1802 60 100       267 $final->[3] = $exp if $exp !~m{[\x00-\x1f\x7e-\xff]};
1803              
1804 60         278 return @$final;
1805             }
1806              
1807             ############################################################################
1808             # expand Macros
1809             ############################################################################
1810             sub _macro_expand {
1811 698     698   1231 my Mail::SPF::Iterator $self = shift;
1812 698         1599 my ($domain,$explain) = @_;
1813 698         1151 my $new_domain = '';
1814 698 100       2596 my $mchars = $explain ? qr{[slodipvhcrt]}i : qr{[slodipvh]}i;
1815 698         1149 my $need_validated;
1816             #DEBUG( Carp::longmess("no domain" )) if ! $domain;
1817             #DEBUG( "domain=$domain" );
1818 698         14853 while ( $domain =~ m{\G (?:
1819             ([^%]+) | # text
1820             %(?:
1821             ([%_\-]) | # char: %_, %-, %%
1822             {
1823             # macro: l1r+- -> (l)(1)(r)(+-)
1824             ($mchars) (\d*)(r?) ([.\-+,/_=]*)
1825             } |
1826             (.|$) # bad char
1827             ))}xg ) {
1828 1028         4729 my ($text,$char,$macro,$macro_n,$macro_r,$macro_delim,$bad)
1829             = ($1,$2,$3,$4,$5,$6,$7);
1830              
1831 1028 100       2453 if ( defined $text ) {
    100          
    100          
1832 746         4326 $new_domain .= $text;
1833              
1834             } elsif ( defined $char ) {
1835 24 100       159 $new_domain .=
    100          
1836             $char eq '%' ? '%' :
1837             $char eq '_' ? ' ' :
1838             '%20'
1839              
1840             } elsif ( $macro ) {
1841 232   100     982 $macro_delim ||= '.';
1842 232         419 my $imacro = lc($macro);
1843             my $expand =
1844             $imacro eq 's' ? $self->{sender} :
1845             $imacro eq 'l' ? $self->{sender} =~m{^([^@]+)\@}
1846             ? $1 : 'postmaster' :
1847             $imacro eq 'o' ? $self->{sender} =~m{\@(.*)}
1848             ? $1 : $self->{sender} :
1849             $imacro eq 'd' ? $self->{domain} :
1850             $imacro eq 'i' ? $self->{clientip4} ?
1851             inet_ntoa($self->{clientip4}) :
1852 0         0 join('.',map { uc } split(//,
1853             unpack( "H*",$self->{clientip6}))) :
1854             $imacro eq 'v' ? $self->{clientip4} ? 'in-addr' : 'ip6':
1855             $imacro eq 'h' ? $self->{helo} :
1856             $imacro eq 'c' ? $self->{clientip4} ?
1857             inet_ntoa($self->{clientip4}) :
1858             inet_ntop(AF_INET6,$self->{clientip6}) :
1859             $imacro eq 'r' ? $self->{myname} || 'unknown' :
1860             $imacro eq 't' ? time() :
1861 232 100 0     1727 $imacro eq 'p' ? do {
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
1862 62   33     176 my $ip = $self->{clientip4} || $self->{clientip6};
1863 62         120 my $v = $self->{validated}{$ip};
1864 62         97 my $d = $self->{domain};
1865 62 100       187 if ( ! $v ) {
    50          
    50          
1866             # nothing validated pointing to IP
1867 46         169 $need_validated = { ip => $ip, domain => $d };
1868 46         119 'unknown'
1869             } elsif ( $v->{$d} ) {
1870             # itself is validated
1871 0         0 $d;
1872 16         102 } elsif ( my @xd = grep { $v->{$_} } keys %$v ) {
1873 16 100       33 if ( my @sd = grep { m{\.\Q$d\E$} } @xd ) {
  16         264  
1874             # subdomain if is validated
1875 6         25 $sd[0]
1876             } else {
1877             # any other domain pointing to IP
1878 10         38 $xd[0]
1879             }
1880             } else {
1881 0         0 'unknown'
1882             }
1883             } :
1884             die "unknown macro $macro\n";
1885              
1886 232         16914 my $rx = eval "qr{[$macro_delim]}";
1887 232         1591 my @parts = split( $rx, $expand );
1888 232 100       669 @parts = reverse @parts if $macro_r;
1889 232 100       537 if ( length $macro_n ) {
1890 38 50       100 die "bad macro definition '$domain'\n"
1891             if ! $macro_n; # must be != 0
1892 38 100       194 @parts = splice( @parts,-$macro_n ) if @parts>$macro_n;
1893             }
1894 232 100       542 if ( $imacro ne $macro ) {
1895             # upper case - URI escape
1896 36         86 @parts = map { uri_escape($_) } @parts;
  78         740  
1897             }
1898 232         3043 $new_domain .= join('.',@parts);
1899              
1900             } else {
1901 26         219 die "bad macro definition '$domain'\n";
1902             }
1903             }
1904              
1905 672 100       1667 if ( ! $explain ) {
1906             # should be less than 253 bytes
1907 600         1375 while ( length($new_domain)>253 ) {
1908 6 50       83 $new_domain =~s{^[^.]*\.}{} or last;
1909             }
1910 600 50       1240 $new_domain = '' if length($new_domain)>253;
1911             }
1912              
1913 672 100       1278 if ( $need_validated ) {
1914 46         414 return { expanded => $new_domain, %$need_validated, macro => $domain }
1915             } else {
1916 626         2447 return $new_domain;
1917             }
1918             }
1919              
1920             ############################################################################
1921             # resolve macro %{p}, e.g. find validated domain name for IP and replace
1922             # %{p} with it. This has many thing similar with the ptr: method
1923             ############################################################################
1924             sub _resolve_macro_p {
1925 22     22   44 my Mail::SPF::Iterator $self = shift;
1926 22         37 my $rec = shift;
1927 22 100 66     119 my $ip = ref($rec) && $rec->{ip} or return; # nothing to resolve
1928              
1929             # could it already be resolved w/o further lookups?
1930 20         40 my $d = eval { $self->_macro_expand( $rec->{macro} ) };
  20         53  
1931 20 50       74 if ( ! ref $d ) {
1932 0 0       0 %$rec = ( expanded => $d ) if ! $@;
1933 0         0 return;
1934             }
1935              
1936 20         35 my $query;
1937 20 50       53 if ( length($ip) == 4 ) {
1938 20         156 $query = join( '.', reverse split( m/\./,
1939             inet_ntoa($ip) )) .'.in-addr.arpa'
1940             } else {
1941 0         0 $query = join( '.', split( //,
1942             reverse unpack("H*",$ip) )) .'.ip6.arpa';
1943             }
1944              
1945 20         81 $self->{cb} = [ \&_validate_got_PTR, $rec ];
1946 20         83 return scalar(Net::DNS::Packet->new( $query, 'PTR','IN' ));
1947             }
1948              
1949             sub _validate_got_PTR {
1950 20     20   128 my Mail::SPF::Iterator $self = shift;
1951 20         58 my ($qtype,$rcode,$ans,$add,$rec ) = @_;
1952              
1953             # no validation possible if no records
1954 20 50 33     104 return if $rcode ne 'NOERROR' or ! @$ans;
1955              
1956 20         46 my @names = map { lc($_->ptrdname) } @$ans;
  26         123  
1957              
1958             # prefer records, which are $domain or end in $domain
1959 20 50       264 if ( my $domain = $rec->{domain} ) {
1960 20         44 unshift @names, grep { $_ eq $domain } @names;
  26         65  
1961 20         37 unshift @names, grep { m{\.\Q$domain\E$} } @names;
  26         296  
1962 20         43 { my %n; @names = grep { !$n{$_}++ } @names } # uniq
  20         28  
  20         40  
  32         153  
1963             }
1964              
1965             # limit to no more then 10 names (RFC4408, 10.1)
1966 20 50       67 splice(@names,10) if @names>10;
1967              
1968             # validate the rest by looking up the IP and verifying it
1969             # with the original IP (clientip)
1970 20 50       70 my $typ = length($rec->{ip}) == 4 ? 'A':'AAAA';
1971              
1972 20         65 $self->{cb} = [ \&_validate_got_A_ptr, $rec,\@names ];
1973 20         94 return scalar(Net::DNS::Packet->new( $names[0], $typ,'IN' ));
1974             }
1975              
1976             sub _validate_got_A_ptr {
1977 20     20   126 my Mail::SPF::Iterator $self = shift;
1978 20         55 my ($qtype,$rcode,$ans,$add,$rec,$names) = @_;
1979              
1980 20 50       60 if ( $rcode eq 'NOERROR' ) {
1981 20 50       44 my @addr = map { $_->address } @$ans or do {
  32         175  
1982             # no addr for domain? -> ignore - maybe
1983             # the domain only provides the other kind of records?
1984 0         0 return;
1985             };
1986              
1987             # check if @addr contains clientip
1988 20         214 my $match;
1989 20         45 my $ip = $rec->{ip};
1990 20 50       52 if ( length($ip) == 4 ) {
1991 20         50 for(@addr) {
1992 26 50       153 m{^[\d\.]+$} or next;
1993 26 100       44 eval { inet_aton($_) } eq $ip or next;
  26         149  
1994 14         34 $match = 1;
1995 14         28 last;
1996             }
1997             } else {
1998 0         0 for(@addr) {
1999 0 0       0 eval { inet_pton(AF_INET6,$_) } eq $ip or next;
  0         0  
2000 0         0 $match = 1;
2001 0         0 last;
2002             }
2003             }
2004              
2005             # cache verification status
2006 20         87 $self->{validated}{$ip}{$names->[0]} = $match;
2007              
2008             # expand macro if we have verified the ptr
2009 20 100       52 if ( $match ) {
2010 14 50       26 if ( my $t = eval { $self->_macro_expand( $rec->{macro} ) }) {
  14         44  
2011 14         60 %$rec = ( expanded => $t );
2012             }
2013 14         62 return;
2014             }
2015             }
2016              
2017             # try next
2018 6         14 shift @$names;
2019 6 50       27 @$names or return; # no next
2020              
2021             # cb stays the same
2022 0           return scalar(Net::DNS::Packet->new( $names->[0], $qtype,'IN' ));
2023             }
2024              
2025              
2026             1;