File Coverage

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