File Coverage

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


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