File Coverage

blib/lib/Mail/SPF/Query.pm
Criterion Covered Total %
statement 265 672 39.4
branch 97 340 28.5
condition 32 125 25.6
subroutine 35 69 50.7
pod 9 52 17.3
total 438 1258 34.8


line stmt bran cond sub pod time code
1             package Mail::SPF::Query;
2              
3             # ----------------------------------------------------------
4             # Mail::SPF::Query
5             # Test an IP / sender address pair for SPF authorization
6             #
7             # http://www.openspf.org
8             # http://search.cpan.org/dist/Mail-SPF-Query
9             #
10             # Copyright (C) 2003-2005 Meng Weng Wong
11             # Contributions by various members of the SPF project
12             # License: like Perl, i.e. GPL-2 and Artistic License
13             #
14             # $Id: Query.pm 143 2006-02-26 17:41:10Z julian $
15             # ----------------------------------------------------------
16              
17 1     1   44823 use 5.006;
  1         4  
  1         43  
18              
19 1     1   5 use strict;
  1         2  
  1         30  
20 1     1   5 use warnings;
  1         6  
  1         32  
21 1     1   4 no warnings 'uninitialized';
  1         2  
  1         57  
22              
23             our $VERSION = '1.999.1'; # fake version for EU::MM and CPAN
24             $VERSION = '1.999001'; # real numerical version
25              
26 1     1   1194 use Sys::Hostname::Long;
  1         4001  
  1         74  
27 1     1   1035 use Net::DNS qw(); # by default it exports mx, which we define.
  1         103006  
  1         30  
28 1     1   991 use Net::CIDR::Lite;
  1         4738  
  1         33  
29 1     1   1070 use URI::Escape;
  1         1595  
  1         6619  
30              
31             # ----------------------------------------------------------
32             # initialization
33             # ----------------------------------------------------------
34              
35             my $GUESS_MECHS = "a/24 mx/24 ptr";
36             my $TRUSTED_FORWARDER = "include:spf.trusted-forwarder.org";
37              
38             my $DEFAULT_EXPLANATION = "Please see http://www.openspf.org/why.html?sender=%{S}&ip=%{I}&receiver=%{R}";
39             my @KNOWN_MECHANISMS = qw( a mx ptr include ip4 ip6 exists all );
40             my $MAX_LOOKUP_COUNT = 10;
41              
42             my $Domains_Queried = {};
43              
44             our $CACHE_TIMEOUT = 120;
45             our $DNS_RESOLVER_TIMEOUT = 15;
46              
47             # ----------------------------------------------------------
48             # no user-serviceable parts below this line
49             # ----------------------------------------------------------
50              
51             my $looks_like_ipv4 = qr/\d+\.\d+\.\d+\.\d+/;
52             my $looks_like_email = qr/\S+\@\S+/;
53              
54             =head1 NAME
55              
56             Mail::SPF::Query - query Sender Policy Framework for an IP,email,helo
57              
58             =head1 VERSION
59              
60             1.999.1
61              
62             =head1 SYNOPSIS
63              
64             my $query = new Mail::SPF::Query (ip => "127.0.0.1", sender=>'foo@example.com', helo=>"somehost.example.com", trusted=>0, guess=>0);
65             my ($result, # pass | fail | softfail | neutral | none | error | unknown [mechanism]
66             $smtp_comment, # "please see http://www.openspf.org/why.html?..." when rejecting, return this string to the SMTP client
67             $header_comment, # prepend_header("Received-SPF" => "$result ($header_comment)")
68             $spf_record, # "v=spf1 ..." original SPF record for the domain
69             ) = $query->result();
70              
71             if ($result eq "pass") { "Domain is not forged. Apply RHSBL and content filters." }
72             elsif ($result eq "fail") { "Domain is forged. Reject or save to spambox." }
73              
74             =head1 ABSTRACT
75              
76             The SPF protocol relies on sender domains to describe their designated outbound
77             mailers in DNS. Given an email address, Mail::SPF::Query determines the
78             legitimacy of an SMTP client IP address.
79              
80             =head1 DESCRIPTION
81              
82             There are two ways to use Mail::SPF::Query. Your choice depends on whether the
83             domains your server is an MX for have secondary MXes which your server doesn't
84             know about.
85              
86             The first and more common style, calling ->result(), is suitable when all mail
87             is received directly from the originator's MTA. If the domains you receive do
88             not have secondary MX entries, this is appropriate. This style of use is
89             outlined in the SYNOPSIS above. This is the common case.
90              
91             The second style is more complex, but works when your server receives mail from
92             secondary MXes. This performs checks as each recipient is handled. If the
93             message is coming from a valid MX secondary for a recipient, then the SPF check
94             is not performed, and a "pass" response is returned right away. To do this,
95             call C and C instead of C.
96              
97             If you do not know what a secondary MX is, you probably don't have one. Use
98             the first style.
99              
100             You can try out Mail::SPF::Query on the command line with the following
101             command:
102              
103             perl -MMail::SPF::Query -le 'print for Mail::SPF::Query->new(
104             helo => shift, ipv4 => shift, sender => shift)->result' \
105             helohost.example.com 1.2.3.4 user@example.com
106              
107             =head1 BUGS
108              
109             Mail::SPF::Query tries to implement the SPF specification (see L)
110             as close as reasonably possible given that M:S:Q has been the very first SPF
111             implementation and has changed with the SPF specification over time. As a
112             result, M:S:Q has various known deficiencies that cannot be corrected with
113             reasonably little effort:
114              
115             =over
116              
117             =item *
118              
119             B M:S:Q is not designed to
120             support the I querying of the HELO and MAIL FROM identities. Passing
121             the HELO identity as the C argument for a stand-alone HELO check might
122             generally work but could yield unexpected results.
123              
124             =item *
125              
126             B IPv6 is not supported. C mechanisms in SPF records
127             and everywhere else are simply ignored.
128              
129             =item *
130              
131             B If a
132             query result was caused by anything other than a real SPF record (i.e. local
133             policy, overrides, fallbacks, etc.), and no custom C was
134             specified, the domain's explanation or M:S:Q's hard-coded default explanation
135             will still be returned. Be aware that in this case the explanation may not
136             correctly explain the reason for such an artificial result.
137              
138             =for comment
139             INTERNAL NOTE: If the spf_source is not 'original-spf-record' (but e.g. a
140             local policy source), do not return the "why.html" default explanation, because
141             "why.html" will not be able to reproduce the local policy.
142              
143             =back
144              
145             =head1 NON-STANDARD FEATURES
146              
147             Also due to its long history, M:S:Q does have some legacy features that are not
148             parts of the official SPF specification, most notably I
149             and I. Please be careful when using
150             these I features or when reproducing them in your own SPF
151             implementation, as they may cause unexpected results.
152              
153             =head1 METHODS
154              
155             =head2 C<< Mail::SPF::Query->new() >>
156              
157             my $query = eval { new Mail::SPF::Query (
158             ip => '127.0.0.1',
159             sender => 'foo@example.com',
160             helo => 'host.example.com',
161              
162             # Optional parameters:
163             debug => 1, debuglog => sub { print STDERR "@_\n" },
164             local => 'extra mechanisms',
165             trusted => 1, # do trusted forwarder processing
166             guess => 1, # do best guess if no SPF record
167             default_explanation => 'Please see http://spf.my.isp/spferror.html for details',
168             max_lookup_count => 10, # total number of SPF includes/redirects
169             sanitize => 0, # do not sanitize all returned strings
170             myhostname => 'foo.example.com', # prepended to header_comment
171             override => { 'example.net' => 'v=spf1 a mx -all',
172             '*.example.net' => 'v=spf1 a mx -all' },
173             fallback => { 'example.org' => 'v=spf1 a mx -all',
174             '*.example.org' => 'v=spf1 a mx -all' }
175             ) };
176              
177             if ($@) { warn "bad input to Mail::SPF::Query: $@" }
178              
179             Set C1> to turned on C accreditation
180             checking. The mechanism C is used just
181             before a C<-all> or C. The precise circumstances are somewhat more
182             complicated, but it does get the case of C right -- i.e.
183             C is not checked. B
184              
185             Set C1> to turned on automatic best guess processing. This will
186             use the best_guess SPF record when one cannot be found in the DNS. Note that
187             this can only return C or C. The C and C flags
188             also operate when the best_guess is being used. B
189             feature.>
190              
191             Set C'include:local.domain'> to include some extra processing just
192             before a C<-all> or C. The local processing happens just before the
193             trusted forwarder processing. B
194              
195             Set C to a string to be used if the SPF record does not
196             provide a specific explanation. The default value will direct the user to a
197             page at www.openspf.org with the following message:
198              
199             Please see http://www.openspf.org/why.html?sender=%{S}&ip=%{I}&receiver=%{R}
200              
201             Note that the string has macro substitution performed.
202              
203             Set C to 0 to get all the returned strings unsanitized.
204             Alternatively, pass a function reference and this function will be used to
205             sanitize the returned values. The function must take a single string argument
206             and return a single string which contains the sanitized result.
207              
208             Set C1> to watch the queries happen.
209              
210             Set C to define SPF records for domains that do publish but which you
211             want to override anyway. Wildcards are supported. B
212             feature.>
213              
214             Set C to define "pretend" SPF records for domains that don't publish
215             them yet. Wildcards are supported. B
216              
217             Note: domain name arguments to override and fallback need to be in all
218             lowercase.
219              
220             =cut
221              
222             # ----------------------------------------------------------
223             # new
224             # ----------------------------------------------------------
225              
226             sub new {
227 165     165 1 212900 my $class = shift;
228 165         12734 my $query = bless { @_ }, $class;
229              
230 165         896 $query->{lookup_count} = 0;
231              
232 165 50 33     1219 $query->{ipv4} = delete $query->{ip}
233             if defined($query->{ip}) and $query->{ip} =~ $looks_like_ipv4;
234 165 50       515 $query->{helo} = delete $query->{ehlo}
235             if defined($query->{ehlo});
236              
237 165 50       2576 $query->{local} .= ' ' . $TRUSTED_FORWARDER if ($query->{trusted});
238              
239 165         906 $query->{trusted} = undef;
240              
241 165   50     2754 $query->{spf_error_explanation} ||= "SPF record error";
242              
243 165   66     470 $query->{default_explanation} ||= $DEFAULT_EXPLANATION;
244              
245 165 50       479 $query->{default_record} = $GUESS_MECHS if ($query->{guess});
246              
247 165 50 33     2962 if (($query->{sanitize} && !ref($query->{sanitize})) || !defined($query->{sanitize})) {
      33        
248             # Apply default sanitizer
249 165         467 $query->{sanitize} = \&strict_sanitize;
250             }
251              
252 165         579 $query->{sender} =~ s/<(.*)>/$1/g;
253              
254 165 50 33     1362 if (not ($query->{ipv4} and length $query->{ipv4})) {
255 0         0 die "no IP address given";
256             }
257              
258 165         602 for ($query->{sender}) { s/^\s+//; s/\s+$//; }
  165         506  
  165         1297  
259              
260 165         2673 $query->{spf_source} = "domain of $query->{sender}";
261 165         555 $query->{spf_source_type} = "original-spf-record";
262              
263 165         1270 ($query->{domain}) = $query->{sender} =~ /([^@]+)$/; # given foo@bar@baz.com, the domain is baz.com, not bar@baz.com.
264              
265             # the domain should not be an address literal --- [1.2.3.4]
266 165 50       2269 if ($query->{domain} =~ /^\[\d+\.\d+\.\d+\.\d+\]$/) {
267 0         0 die "sender domain should be an FQDN, not an address literal";
268             }
269              
270 165 50       668 if (not $query->{helo}) { require Carp; import Carp qw(cluck); cluck ("Mail::SPF::Query: ->new() requires a \"helo\" argument.\n");
  0         0  
  0         0  
  0         0  
271 0         0 $query->{helo} = $query->{domain};
272             }
273              
274 165         1055 $query->debuglog("new: ipv4=$query->{ipv4}, sender=$query->{sender}, helo=$query->{helo}");
275              
276 165         1641 ($query->{helo}) =~ s/.*\@//; # strip localpart from helo
277              
278 165 50       550 if (not $query->{domain}) {
279 0         0 $query->debuglog("sender $query->{sender} has no domain, using HELO domain $query->{helo} instead.");
280 0         0 $query->{domain} = $query->{helo};
281 0         0 $query->{sender} = $query->{helo};
282             }
283              
284 165 50       592 if (not length $query->{domain}) { die "unable to identify domain of sender $query->{sender}" }
  0         0  
285              
286 165         528 $query->{orig_domain} = $query->{domain};
287              
288 165         1122 $query->{loop_report} = [$query->{domain}];
289              
290 165         5328 ($query->{localpart}) = $query->{sender} =~ /(.+)\@/;
291 165 100       660 $query->{localpart} = "postmaster" if not length $query->{localpart};
292              
293 165         1148 $query->debuglog("localpart is $query->{localpart}");
294              
295 165 0       1409 $query->{Reversed_IP} = ($query->{ipv4} ? reverse_in_addr($query->{ipv4}) :
    50          
296             $query->{ipv6} ? die "IPv6 not supported" : "");
297              
298 165 50       658 if (not $query->{myhostname}) {
299 165         1332 $query->{myhostname} = Sys::Hostname::Long::hostname_long();
300             }
301 165   50     45012 $query->{myhostname} ||= "localhost";
302              
303             # Unfold legacy { 'domain' => { record => '...' } } override and fallback
304             # structures to just { 'domain' => '...' }:
305 165         11016 foreach ('override', 'fallback') {
306 330 50       1526 if (ref(my $domains_hash = $query->{$_}) eq 'HASH') {
307 0         0 foreach my $domain (keys(%$domains_hash)) {
308 0 0       0 $domains_hash->{$domain} = $domains_hash->{$domain}->{record}
309             if ref($domains_hash->{$domain}) eq 'HASH';
310             }
311             }
312             }
313              
314 165 50       2395 $query->post_new(@_) if $class->can("post_new");
315              
316 165         1370 return $query;
317             }
318              
319             =head2 C<< $query->result() >>
320              
321             my ($result, $smtp_comment, $header_comment, $spf_record, $detail) = $query->result();
322              
323             C<$result> will be one of C, C, C, C, C,
324             C or C:
325              
326             =over
327              
328             =item C
329              
330             The client IP address is an authorized mailer for the sender. The mail should
331             be accepted subject to local policy regarding the sender.
332              
333             =item C
334              
335             The client IP address is not an authorized mailer, and the sender wants you to
336             reject the transaction for fear of forgery.
337              
338             =item C
339              
340             The client IP address is not an authorized mailer, but the sender prefers that
341             you accept the transaction because it isn't absolutely sure all its users are
342             mailing through approved servers. The C status is often used during
343             initial deployment of SPF records by a domain.
344              
345             =item C
346              
347             The sender makes no assertion about the status of the client IP.
348              
349             =item C
350              
351             There is no SPF record for this domain.
352              
353             =item C
354              
355             The DNS lookup encountered a temporary error during processing.
356              
357             =item C
358              
359             The domain has a configuration error in the published data or defines a
360             mechanism that this library does not understand. If the data contained an
361             unrecognized mechanism, it will be presented following "unknown". You should
362             test for unknown using a regexp C rather than C.
363              
364             =back
365              
366             Results are cached internally for a default of 120 seconds. You can call
367             C<-Eresult()> repeatedly; subsequent lookups won't hit your DNS.
368              
369             C should be displayed to the SMTP client.
370              
371             C goes into a C header, like so:
372              
373             Received-SPF: $result ($header_comment)
374              
375             C shows the original SPF record fetched for the query. If there is
376             no SPF record, it is blank. Otherwise, it will start with C and
377             contain the SPF mechanisms and such that describe the domain.
378              
379             Note that the strings returned by this method (and most of the other methods)
380             are (at least partially) under the control of the sender's domain. This means
381             that, if the sender is an attacker, the contents can be assumed to be hostile.
382             The various methods that return these strings make sure that (by default) the
383             strings returned contain only characters in the range 32 - 126. This behavior
384             can be changed by setting C to 0 to turn off sanitization entirely.
385             You can also set C to a function reference to perform custom
386             sanitization. In particular, assume that C might contain a
387             newline character.
388              
389             C is a hash of all the foregoing result elements, plus extra data
390             returned by the SPF result.
391              
392             I In the beginning, C returned only one
393             value, the C<$result>. Then C<$smtp_comment> and C<$header_comment> came
394             along. Then C<$spf_record>. Past a certain number of positional results, it
395             makes more sense to have a hash. But we didn't want to break backwards
396             compatibility, so we just declared that the fifth result would be a hash and
397             future return value would go in there.
398              
399             The keys of the hash are:
400              
401             result
402             smtp_comment
403             header_comment
404             header_pairs
405             spf_record
406             modifiers
407              
408             =cut
409              
410             # ----------------------------------------------------------
411             # result
412             # ----------------------------------------------------------
413              
414             sub result {
415 165     165 1 350 my $query = shift;
416 165         342 my %result_set;
417              
418 165 50       825 my ($result, $smtp_explanation, $smtp_why, $orig_txt) = $query->spfquery(
419             $query->{best_guess} ? $query->{guess_mechs} : ()
420             );
421              
422 165 50       10522 $smtp_why = "" if $smtp_why eq "default";
423              
424 165 100 66     1877 my $smtp_comment = ($smtp_explanation && $smtp_why) ? "$smtp_explanation: $smtp_why" : ($smtp_explanation || $smtp_why);
      33        
425              
426 165         535 $query->{smtp_comment} = $smtp_comment;
427              
428 165         921 my $header_comment = "$query->{myhostname}: ". $query->header_comment($result);
429              
430             # $result =~ s/\s.*$//; # this regex truncates "unknown some:mechanism" to just "unknown"
431              
432 165         585 $query->{result} = $result;
433              
434 165         1102 my $hash = { result => $query->sanitize(lc $result),
435             smtp_comment => $query->sanitize($smtp_comment),
436             header_comment => $query->sanitize($header_comment),
437             spf_record => $query->sanitize($orig_txt),
438             modifiers => $query->{modifiers},
439             header_pairs => $query->sanitize(scalar $query->header_pairs()),
440             };
441              
442 165 50       2211 return ($hash->{result},
443             $hash->{smtp_comment},
444             $hash->{header_comment},
445             $hash->{spf_record},
446             $hash,
447             ) if wantarray;
448              
449 0         0 return $query->sanitize(lc $result);
450             }
451              
452             sub header_comment {
453 165     165 0 401 my $query = shift;
454 165         406 my $result = shift;
455 165         902 my $ip = $query->ip;
456 165 100 66     657 if ($result eq "pass" and $query->{smtp_comment} eq "localhost is always allowed.") { return $query->{smtp_comment} }
  1         12  
457              
458 164         1051 $query->debuglog("header_comment: spf_source = $query->{spf_source}");
459 164         1535 $query->debuglog("header_comment: spf_source_type = $query->{spf_source_type}");
460              
461 164 50       1197 if ($query->{spf_source_type} eq "original-spf-record") {
462             return
463 164 0       13030 ( $result eq "pass" ? "$query->{spf_source} designates $ip as permitted sender"
    0          
    0          
    50          
    50          
    50          
    50          
    50          
464             : $result eq "fail" ? "$query->{spf_source} does not designate $ip as permitted sender"
465             : $result eq "softfail" ? "transitioning $query->{spf_source} does not designate $ip as permitted sender"
466             : $result =~ /^unknown / ? "encountered unrecognized mechanism during SPF processing of $query->{spf_source}"
467             : $result eq "unknown" ? "error in processing during lookup of $query->{sender}"
468             : $result eq "neutral" ? "$ip is neither permitted nor denied by domain of $query->{sender}"
469             : $result eq "error" ? "encountered temporary error during SPF processing of $query->{spf_source}"
470             : $result eq "none" ? "$query->{spf_source} does not designate permitted sender hosts"
471             : "could not perform SPF query for $query->{spf_source}" );
472             }
473              
474 0         0 return $query->{spf_source};
475              
476             }
477              
478             sub header_pairs {
479 165     165 0 384 my $query = shift;
480             # from spf-draft-200404.txt
481             # SPF clients may append zero or more of the following key-value-pairs
482             # at their discretion:
483             #
484             # receiver the hostname of the SPF client
485             # client-ip the IP address of the SMTP client
486             # envelope-from the envelope sender address
487             # helo the hostname given in the HELO or EHLO command
488             # mechanism the mechanism that matched (if no mechanisms
489             # matched, substitute the word "default".)
490             # problem if an error was returned, details about the error
491             #
492             # Other key-value pairs may be defined by SPF clients. Until a new key
493             # name becomes widely accepted, new key names should start with "x-".
494              
495 165 50 50     2469 my @pairs = (
    50          
    50          
496             "receiver" => $query->{myhostname},
497             "client-ip" => ($query->{ipv4} || $query->{ipv6} || ""),
498             "envelope-from" => $query->{sender},
499             "helo" => $query->{helo},
500             mechanism => ($query->{matched_mechanism} ? display_mechanism($query->{matched_mechanism}) : "default"),
501             ($query->{result} eq "error"
502             ? (problem => $query->{spf_error_explanation})
503             : ()),
504             ($query->{spf_source_type} ne "original-spf-record" ? ("x-spf-source" => $query->{spf_source}) : ()),
505             );
506              
507 165 50       600 if (wantarray) { return @pairs; }
  0         0  
508 165         451 my @pair_text;
509 165         456 while (@pairs) {
510 825         3184 my ($key, $val) = (shift(@pairs), shift (@pairs));
511 825         3900 push @pair_text, "$key=$val;";
512             }
513 165         4977 return join " ", @pair_text;
514             }
515              
516             =head2 C<< $query->result2() >>
517              
518             my ($result, $smtp_comment, $header_comment, $spf_record) = $query->result2('recipient@domain', 'recipient2@domain');
519              
520             C does everything that C does, but it first checks to see if
521             the sending system is a recognized MX secondary for the recipient(s). If so,
522             then it returns C and does not perform the SPF query. Note that the
523             sending system may be a MX secondary for some (but not all) of the recipients
524             for a multi-recipient message, which is why result2 takes an argument list.
525             See also C.
526              
527             B B
528             exemption of trusted relays, such as secondary MXes, should really be performed
529             by the software that uses this library before doing an SPF check.>
530              
531             C<$result> will be one of C, C, C, or C.
532             See C above for meanings.
533              
534             If you have secondary MXes and if you are unable to explicitly white-list them
535             before SPF tests occur, you can use this method in place of C,
536             calling it as many times as there are recipients, or just providing all the
537             recipients at one time.
538              
539             C can be displayed to the SMTP client.
540              
541             For example:
542              
543             my $query = new Mail::SPF::Query (ip => "127.0.0.1",
544             sender=>'foo@example.com',
545             helo=>"somehost.example.com");
546              
547             ...
548              
549             my ($result, $smtp_comment, $header_comment);
550              
551             ($result, $smtp_comment, $header_comment) = $query->result2('recip1@example.com');
552             # return suitable error code based on $result eq 'fail' or not
553              
554             ($result, $smtp_comment, $header_comment) = $query->result2('recip2@example.org');
555             # return suitable error code based on $result eq 'fail' or not
556              
557             ($result, $smtp_comment, $header_comment) = $query->message_result2();
558             # return suitable error if $result eq 'fail'
559             # prefix message with "Received-SPF: $result ($header_comment)"
560              
561             =cut
562              
563             # ----------------------------------------------------------
564             # result2
565             # ----------------------------------------------------------
566              
567             sub result2 {
568 13     13 1 2511 my $query = shift;
569 13         30 my @recipients = @_;
570              
571 13 100       47 if (!$query->{result2}) {
572 9         21 my $all_mx_secondary = 'neutral';
573              
574 9         31 foreach my $recip (@recipients) {
575 9         66 my ($rhost) = $recip =~ /([^@]+)$/;
576              
577 9         60 $query->debuglog("result2: Checking status of recipient $recip (at host $rhost)");
578              
579 9         68 my $cache_result = $query->{mx_cache}->{$rhost};
580 9 50       31 if (not defined($cache_result)) {
581 9 50       36 $cache_result = $query->{mx_cache}->{$rhost} = is_secondary_for($rhost, $query->{ipv4}) ? 'yes' : 'no';
582 9         62 $query->debuglog("result2: $query->{ipv4} is a MX for $rhost: $cache_result");
583             }
584              
585 9 50       69 if ($cache_result eq 'yes') {
586 0         0 $query->{is_mx_good} = [$query->sanitize('pass'),
587             $query->sanitize('message from secondary MX'),
588             $query->sanitize("$query->{myhostname}: message received from $query->{ipv4} which is an MX secondary for $recip"),
589             undef];
590 0         0 $all_mx_secondary = 'yes';
591             } else {
592 9         21 $all_mx_secondary = 'no';
593 9         29 last;
594             }
595             }
596              
597 9 50       37 if ($all_mx_secondary eq 'yes') {
598 0 0       0 return @{$query->{is_mx_good}} if wantarray;
  0         0  
599 0         0 return $query->{is_mx_good}->[0];
600             }
601              
602 9         48 my @result = $query->result();
603              
604 9         86 $query->{result2} = \@result;
605             }
606              
607 13 50       37 return @{$query->{result2}} if wantarray;
  13         108  
608 0         0 return $query->{result2}->[0];
609             }
610              
611             sub is_secondary_for {
612 9     9 0 25 my ($host, $addr) = @_;
613              
614 9         162 my $resolver = Net::DNS::Resolver->new(
615             tcp_timeout => $DNS_RESOLVER_TIMEOUT,
616             udp_timeout => $DNS_RESOLVER_TIMEOUT,
617             )
618             ;
619 9 50       744 if ($resolver) {
620 9         40 my $mx = $resolver->send($host, 'MX');
621 9 50       248175 if ($mx) {
622 9         55 my @mxlist = sort { $a->preference <=> $b->preference } (grep { $_->type eq 'MX' } $mx->answer);
  8         83  
  8         82  
623             # discard the first entry (top priority) - we shouldn't get mail from them
624 9         68 shift @mxlist;
625 9         99 foreach my $rr (@mxlist) {
626 6         260 my $a = $resolver->send($rr->exchange, 'A');
627 6 50       27043 if ($a) {
628 6         29 foreach my $rra ($a->answer) {
629 6 50       56 if ($rra->type eq 'A') {
630 6 50       82 if ($rra->address eq $addr) {
631 0         0 return 1;
632             }
633             }
634             }
635             }
636             }
637             }
638             }
639              
640 9         291 return undef;
641             }
642              
643             =head2 C<< $query->message_result2() >>
644              
645             my ($result, $smtp_comment, $header_comment, $spf_record) = $query->message_result2();
646              
647             C returns an overall status for the message after zero or
648             more calls to C. It will always be the last status returned by
649             C, or the status returned by C if C was never
650             called.
651              
652             C<$result> will be one of C, C, C, or C. See
653             C above for meanings.
654              
655             =cut
656              
657             # ----------------------------------------------------------
658             # message_result2
659             # ----------------------------------------------------------
660              
661             sub message_result2 {
662 9     9 1 2842 my $query = shift;
663              
664 9 50       36 if (!$query->{result2}) {
665 0 0       0 if ($query->{is_mx_good}) {
666 0 0       0 return @{$query->{is_mx_good}} if wantarray;
  0         0  
667 0         0 return $query->{is_mx_good}->[0];
668             }
669              
670             # we are very unlikely to get here -- unless result2 was not called.
671              
672 0         0 my @result = $query->result();
673              
674 0         0 $query->{result2} = \@result;
675             }
676              
677 9 50       27 return @{$query->{result2}} if wantarray;
  9         130  
678 0         0 return $query->{result2}->[0];
679             }
680              
681             =head2 C<< $query->best_guess() >>
682              
683             my ($result, $smtp_comment, $header_comment) = $query->best_guess();
684              
685             When a domain does not publish an SPF record, this library can produce an
686             educated guess anyway.
687              
688             It pretends the domain defined A, MX, and PTR mechanisms, plus a few others.
689             The default set of directives is
690              
691             a/24 mx/24 ptr
692              
693             That default set will return either "pass" or "neutral".
694              
695             If you want to experiment with a different default, you can pass it as an
696             argument: C<< $query->best_guess("a mx ptr") >>
697              
698             B B You
699             should set C1> on the C method instead.
700              
701             =head2 C<< $query->trusted_forwarder() >>
702              
703             my ($result, $smtp_comment, $header_comment) = $query->best_guess();
704              
705             It is possible that the message is coming through a known-good relay like
706             C or C. During the transitional period, many legitimate
707             services may appear to forge a sender address: for example, a news website may
708             have a "send me this article in email" link.
709              
710             The C domain is a white-list of known-good hosts that
711             either forward mail or perform benign envelope sender forgery:
712              
713             include:spf.trusted-forwarder.org
714              
715             This will return either "pass" or "neutral".
716              
717             B B You
718             should set C1> on the C method instead.
719              
720             =cut
721              
722             sub clone {
723 0     0 0 0 my $query = shift;
724 0         0 my $class = ref $query;
725              
726 0         0 my %guts = (%$query, @_, parent=>$query);
727              
728 0         0 my $clone = bless \%guts, $class;
729              
730 0         0 push @{$clone->{loop_report}}, delete $clone->{reason};
  0         0  
731              
732 0         0 $query->debuglog(" clone: new object:");
733 0         0 for ($clone->show) { $clone->debuglog( "clone: $_" ) }
  0         0  
734              
735 0         0 return $clone;
736             }
737              
738             sub top {
739 2024     2024 0 3640 my $query = shift;
740 2024 50       6789 if ($query->{parent}) { return $query->{parent}->top }
  0         0  
741 2024         13354 return $query;
742             }
743              
744             sub set_temperror {
745 0     0 0 0 my $query = shift;
746 0         0 $query->{error} = shift;
747             }
748              
749             sub show {
750 0     0 0 0 my $query = shift;
751              
752 0         0 return map { sprintf ("%20s = %s", $_, $query->{$_}) } keys %$query;
  0         0  
753             }
754              
755             sub best_guess {
756 0     0 1 0 my $query = shift;
757 0   0     0 my $guess_mechs = shift || $GUESS_MECHS;
758              
759             # clone the query object with best_guess mode turned on.
760 0         0 my $guess_query = $query->clone( best_guess => 1,
761             guess_mechs => $guess_mechs,
762             reason => "has no data. best guess",
763             );
764              
765 0         0 $guess_query->top->{lookup_count} = 0;
766              
767             # if result is not defined, the domain has no SPF.
768             # perform fallback lookups.
769             # perform trusted-forwarder lookups.
770             # perform guess lookups.
771             #
772             # if result is defined, return it.
773              
774 0         0 my ($result, $smtp_comment, $header_comment) = $guess_query->result();
775 0 0 0     0 if (defined $result and $result eq "pass") {
776 0         0 my $ip = $query->ip;
777 0         0 $header_comment = $query->sanitize("seems reasonable for $query->{sender} to mail through $ip");
778 0 0       0 return ($result, $smtp_comment, $header_comment) if wantarray;
779 0         0 return $result;
780             }
781              
782 0         0 return $query->sanitize("neutral");
783             }
784              
785             sub trusted_forwarder {
786 0     0 1 0 my $query = shift;
787 0   0     0 my $guess_mechs = shift || $TRUSTED_FORWARDER;
788 0         0 return $query->best_guess($guess_mechs);
789             }
790              
791             # ----------------------------------------------------------
792              
793             =head2 C<< $query->sanitize('string') >>
794              
795             This applies the sanitization rules for the particular query object. These
796             rules are controlled by the C parameter to the c method.
797              
798             =cut
799              
800             sub sanitize {
801 825     825 1 1312 my $query = shift;
802 825         1206 my $txt = shift;
803              
804 825 50       2311 if (ref($query->{sanitize})) {
805 825         1976 $txt = $query->{sanitize}->($txt);
806             }
807              
808 825         3681 return $txt;
809             }
810              
811             # ----------------------------------------------------------
812              
813             =head2 C<< strict_sanitize('string') >>
814              
815             This ensures that all the characters in the returned string are printable. All
816             whitespace is converted into spaces, and all other non-printable characters are
817             converted into question marks. This is probably over-aggressive for many
818             applications.
819              
820             This function is used by default when the C option is passed to the
821             C method.
822              
823             B
824              
825             =cut
826              
827             sub strict_sanitize {
828 825     825 1 1178 my $txt = shift;
829              
830 825         8989 $txt =~ s/\s/ /g;
831 825         8003 $txt =~ s/[^[:print:]]/?/g;
832              
833 825         1623 return $txt;
834             }
835              
836             # ----------------------------------------------------------
837              
838             =head2 C<< $query->debuglog() >>
839              
840             Subclasses may override this with their own debug logger. C is
841             recommended.
842              
843             Alternatively, pass the C constructor a C<< debuglog => sub { ... } >>
844             callback, and we'll pass debugging lines to that.
845              
846             =cut
847              
848             sub debuglog {
849 1696     1696 1 2752 my $query = shift;
850 1696 50 33     11131 return if ref $query and not $query->{debug};
851            
852 1696         5657 my $toprint = join (" ", @_);
853 1696         4016 chomp $toprint;
854 1696         4978 $toprint = sprintf ("%-8s %s %s %s",
855             ("|" x ($query->top->{lookup_count}+1)),
856             $query->{localpart},
857             $query->{domain},
858             $toprint);
859              
860 1696 50 33     16843 if (exists $query->{debuglog} and ref $query->{debuglog} eq "CODE") {
861 1696         2629 eval { $query->{debuglog}->($toprint) };
  1696         8138  
862             }
863             else {
864 0         0 printf STDERR "%s", "$toprint\n";
865             }
866             }
867              
868             # ----------------------------------------------------------
869             # spfquery
870             # ----------------------------------------------------------
871              
872             sub spfquery {
873             #
874             # usage: my ($result, $explanation, $text, $time) = $query->spfquery( [ GUESS_MECHS ] )
875             #
876             # performs a full SPF resolution using the data in $query. to use different data, clone the object.
877             #
878             # if GUESS_MECHS is present, we are operating in "guess" mode so we will not actually query the domain for TXT; we will use the guess_mechs instead.
879             #
880 165     165 0 250 my $query = shift;
881 165         321 my $guess_mechs = shift;
882              
883 165 100 66     1416 if ($query->{ipv4} and
884 1         4 $query->{ipv4}=~ /^127\./) { return "pass", "localhost is always allowed." }
885              
886 164         703 $query->top->{lookup_count}++;
887              
888 164 50       649 if ($query->is_looping) { return "unknown", $query->{spf_error_explanation}, $query->is_looping }
  0         0  
889 164 50       581 if ($query->can_use_cached_result) { return $query->cached_result; }
  0         0  
890 164         591 else { $query->tell_cache_that_lookup_is_underway; }
891              
892 164         1889 my $directive_set = DirectiveSet->new($query->{domain}, $query, $guess_mechs, $query->{local}, $query->{default_record});
893              
894 164 50       1054 if (not defined $directive_set) {
895 164         933 $query->debuglog("no SPF record found for $query->{domain}");
896 164         1664 $query->delete_cache_point;
897 164 50       870 if ($query->{domain} ne $query->{orig_domain}) {
898 0 0       0 if ($query->{error}) {
899 0         0 return "error", $query->{spf_error_explanation}, $query->{error};
900             }
901 0         0 return "unknown", $query->{spf_error_explanation}, "Missing SPF record at $query->{domain}";
902             }
903 164 50       582 if ($query->{last_dns_error} eq 'NXDOMAIN') {
904 164         717 my $explanation = $query->macro_substitute($query->{default_explanation});
905 164         1323 return "unknown", $explanation, "domain of sender $query->{sender} does not exist";
906             }
907 0         0 return "none", "SPF", "domain of sender $query->{sender} does not designate mailers";
908             }
909              
910 0 0       0 if ($directive_set->{hard_syntax_error}) {
911 0         0 $query->debuglog(" syntax error while parsing $directive_set->{txt}");
912 0         0 $query->delete_cache_point;
913 0         0 return "unknown", $query->{spf_error_explanation}, $directive_set->{hard_syntax_error};
914             }
915              
916 0         0 $query->{directive_set} = $directive_set;
917              
918 0         0 foreach my $mechanism ($directive_set->mechanisms) {
919 0         0 my ($result, $comment) = $query->evaluate_mechanism($mechanism);
920              
921 0 0       0 if ($query->{error}) {
922 0         0 $query->debuglog(" returning temporary error: $query->{error}");
923 0         0 $query->delete_cache_point;
924 0         0 return "error", $query->{spf_error_explanation}, $query->{error};
925             }
926              
927 0 0       0 if (defined $result) {
928 0         0 $query->debuglog(" saving result $result to cache point and returning.");
929 0 0       0 my $explanation = $query->interpolate_explanation(
930             ($result =~ /^unknown/)
931             ? $query->{spf_error_explanation} : $query->{default_explanation});
932 0         0 $query->save_result_to_cache($result,
933             $explanation,
934             $comment,
935             $query->{directive_set}->{orig_txt});
936 0         0 $query->{matched_mechanism} = $mechanism;
937 0         0 return $result, $explanation, $comment, $query->{directive_set}->{orig_txt};
938             }
939             }
940              
941             # run the redirect modifier
942 0 0       0 if ($query->{directive_set}->redirect) {
943 0         0 my $new_domain = $query->macro_substitute($query->{directive_set}->redirect);
944              
945 0         0 $query->debuglog(" executing redirect=$new_domain");
946              
947 0         0 my $inner_query = $query->clone(domain => $new_domain,
948             reason => "redirects to $new_domain",
949             );
950              
951 0         0 my @inner_result = $inner_query->spfquery();
952              
953 0         0 $query->delete_cache_point;
954              
955 0         0 $query->debuglog(" executed redirect=$new_domain, got result @inner_result");
956              
957 0         0 $query->{spf_source} = $inner_query->{spf_source};
958 0         0 $query->{spf_source_type} = $inner_query->{spf_source_type};
959 0         0 $query->{matched_mechanism} = $inner_query->{matched_mechanism};
960              
961 0         0 return @inner_result;
962             }
963              
964 0         0 $query->debuglog(" no mechanisms matched; deleting cache point and using neutral");
965 0         0 $query->delete_cache_point;
966 0         0 return "neutral", $query->interpolate_explanation($query->{default_explanation}), $directive_set->{soft_syntax_error};
967             }
968              
969             # ----------------------------------------------------------
970             # we cache into $Domains_Queried.
971             # ----------------------------------------------------------
972              
973             sub cache_point {
974 656     656 0 1122 my $query = shift;
975 656   50     48002 return my $cache_point = join "/", ($query->{best_guess} || 0,
      50        
976             $query->{guess_mechs} || "",
977             $query->{ipv4},
978             $query->{localpart},
979             $query->{domain},
980             $query->{default_record},
981             $query->{local});
982             }
983              
984             sub is_looping {
985 164     164 0 888 my $query = shift;
986 164         603 my $cache_point = $query->cache_point;
987              
988 164 50 33     752 return join(" ", "loop encountered:", @{$query->{loop_report}})
  0         0  
989             if exists $Domains_Queried->{$cache_point}
990             and not defined $Domains_Queried->{$cache_point}->[0];
991              
992 164 50 33     842 return join(" ", "query caused more than" . $query->max_lookup_count . " lookups:", @{$query->{loop_report}})
  0         0  
993             if $query->max_lookup_count and $query->top->{lookup_count} > $query->max_lookup_count;
994              
995 164         757 return 0;
996             }
997              
998             sub max_lookup_count {
999 328     328 0 853 my $query = shift;
1000 328   33     7627 return $query->{max_lookup_count} || $MAX_LOOKUP_COUNT;
1001             }
1002              
1003             sub can_use_cached_result {
1004 164     164 0 3795 my $query = shift;
1005 164         369 my $cache_point = $query->cache_point;
1006              
1007 164 50       1717 if ($Domains_Queried->{$cache_point}) {
1008 0         0 $query->debuglog(" lookup: we have already processed $query->{domain} before with $query->{ipv4}.");
1009 0         0 my @cached = @{ $Domains_Queried->{$cache_point} };
  0         0  
1010 0 0 0     0 if (not defined $CACHE_TIMEOUT
1011             or time - $cached[-1] > $CACHE_TIMEOUT) {
1012 0         0 $query->debuglog(" lookup: but its cache entry is stale; deleting it.");
1013 0         0 delete $Domains_Queried->{$cache_point};
1014 0         0 return 0;
1015             }
1016              
1017 0         0 $query->debuglog(" lookup: the cache entry is fresh; returning it.");
1018 0         0 return 1;
1019             }
1020 164         553 return 0;
1021             }
1022              
1023             sub tell_cache_that_lookup_is_underway {
1024 164     164 0 199 my $query = shift;
1025              
1026             # define an entry here so we don't loop endlessly in an Include loop.
1027 164         4211 $Domains_Queried->{$query->cache_point} = [undef, undef, undef, undef, time];
1028             }
1029              
1030             sub save_result_to_cache {
1031 0     0 0 0 my $query = shift;
1032 0         0 my ($result, $explanation, $comment, $orig_txt) = (shift, shift, shift, shift);
1033              
1034             # define an entry here so we don't loop endlessly in an Include loop.
1035 0         0 $Domains_Queried->{$query->cache_point} = [$result, $explanation, $comment, $orig_txt, time];
1036             }
1037              
1038             sub cached_result {
1039 0     0 0 0 my $query = shift;
1040 0         0 my $cache_point = $query->cache_point;
1041              
1042 0 0       0 if ($Domains_Queried->{$cache_point}) {
1043 0         0 return @{ $Domains_Queried->{$cache_point} };
  0         0  
1044             }
1045 0         0 return;
1046             }
1047              
1048             sub delete_cache_point {
1049 164     164 0 279 my $query = shift;
1050 164         964 delete $Domains_Queried->{$query->cache_point};
1051             }
1052              
1053             sub clear_cache {
1054 9     9 0 10894 $Domains_Queried = {};
1055             }
1056              
1057             sub get_ptr_domain {
1058 0     0 0 0 my ($query) = shift;
1059              
1060 0 0       0 return $query->{ptr_domain} if ($query->{ptr_domain});
1061            
1062 0         0 foreach my $ptrdname ($query->myquery(reverse_in_addr($query->{ipv4}) . ".in-addr.arpa", "PTR", "ptrdname")) {
1063 0         0 $query->debuglog(" get_ptr_domain: $query->{ipv4} is $ptrdname");
1064            
1065 0         0 $query->debuglog(" get_ptr_domain: checking hostname $ptrdname for legitimacy.");
1066            
1067             # check for legitimacy --- PTR -> hostname A -> PTR
1068 0         0 foreach my $ptr_to_a ($query->myquery($ptrdname, "A", "address")) {
1069            
1070 0         0 $query->debuglog(" get_ptr_domain: hostname $ptrdname -> $ptr_to_a");
1071            
1072 0 0       0 if ($ptr_to_a eq $query->{ipv4}) {
1073 0         0 return $query->{ptr_domain} = $ptrdname;
1074             }
1075             }
1076             }
1077              
1078 0         0 return undef;
1079             }
1080              
1081             sub macro_substitute_item {
1082 27     27 0 51 my $query = shift;
1083 27         70 my $arg = shift;
1084              
1085 27 50       72 if ($arg eq "%") { return "%" }
  0         0  
1086 27 50       52 if ($arg eq "_") { return " " }
  0         0  
1087 27 50       74 if ($arg eq "-") { return "%20" }
  0         0  
1088              
1089 27         153 $arg =~ s/^{(.*)}$/$1/;
1090              
1091 27         592 my ($field, $num, $reverse, $delim) = $arg =~ /^(x?\w)(\d*)(r?)(.*)$/;
1092              
1093 27 50       91 $delim = '.' if not length $delim;
1094              
1095 27         181 my $newval = $arg;
1096 27         36 my $timestamp = time;
1097              
1098 27 50       68 $newval = $query->{localpart} if (lc $field eq 'u');
1099 27 50       64 $newval = $query->{localpart} if (lc $field eq 'l');
1100 27 50       60 $newval = $query->{domain} if (lc $field eq 'd');
1101 27 100       394 $newval = $query->{sender} if (lc $field eq 's');
1102 27 50       53 $newval = $query->{orig_domain} if (lc $field eq 'o');
1103 27 100       93 $newval = $query->ip if (lc $field eq 'i');
1104 27 50       70 $newval = $timestamp if (lc $field eq 't');
1105 27 50       104 $newval = $query->{helo} if (lc $field eq 'h');
1106 27 50       59 $newval = $query->get_ptr_domain if (lc $field eq 'p');
1107 27 100       77 $newval = $query->{myhostname} if (lc $field eq 'r'); # only used in explanation
1108 27 0       60 $newval = $query->{ipv4} ? 'in-addr' : 'ip6'
    50          
1109             if (lc $field eq 'v');
1110              
1111             # We need to escape a bunch of characters inside a character class
1112 27         65 $delim =~ s/([\^\-\]\:\\])/\\$1/g;
1113              
1114 27 50       59 if (length $delim) {
1115 27         306 my @parts = split /[$delim]/, $newval;
1116              
1117 27 50       60 @parts = reverse @parts if ($reverse);
1118              
1119 27 50       53 if ($num) {
1120 0         0 while (@parts > $num) { shift @parts }
  0         0  
1121             }
1122              
1123 27         302 $newval = join ".", @parts;
1124             }
1125              
1126 27 50       139 $newval = uri_escape($newval) if ($field ne lc $field);
1127              
1128 27         729 $query->debuglog(" macro_substitute_item: $arg: field=$field, num=$num, reverse=$reverse, delim=$delim, newval=$newval");
1129              
1130 27         362 return $newval;
1131             }
1132              
1133             sub macro_substitute {
1134 164     164 0 279 my $query = shift;
1135 164         256 my $arg = shift;
1136 164         279 my $maxlen = shift;
1137              
1138 164         327 my $original = $arg;
1139              
1140             # macro-char = ( '%{' alpha *digit [ 'r' ] *delim '}' )
1141             # / '%%'
1142             # / '%_'
1143             # / '%-'
1144              
1145 164         621 $arg =~ s/%([%_-]|{(\w[^}]*)})/$query->macro_substitute_item($1)/ge;
  27         78  
1146              
1147 164 50 33     781 if ($maxlen && length $arg > $maxlen) {
1148 0         0 $arg = substr($arg, -$maxlen); # super.long.string -> er.long.string
1149 0         0 $arg =~ s/[^.]*\.//; # er.long.string -> long.string
1150             }
1151 164 100       491 $query->debuglog(" macro_substitute: $original -> $arg") if ($original ne $arg);
1152 164         546 return $arg;
1153             }
1154              
1155             # ----------------------------------------------------------
1156             # display_mechanism
1157             #
1158             # in human-readable form; used in header_pairs above.
1159             # ----------------------------------------------------------
1160              
1161             sub display_mechanism {
1162 0     0 0 0 my ($modifier, $mechanism, $argument, $source) = @{shift()};
  0         0  
1163              
1164 0 0       0 return "$modifier$mechanism" . (length($argument) ? ":$argument" : "");
1165             }
1166              
1167             # ----------------------------------------------------------
1168             # evaluate_mechanism
1169             # ----------------------------------------------------------
1170              
1171             sub evaluate_mechanism {
1172 0     0 0 0 my $query = shift;
1173 0         0 my ($modifier, $mechanism, $argument, $source) = @{shift()};
  0         0  
1174              
1175 0 0       0 $modifier = "+" if not length $modifier;
1176              
1177 0         0 $query->debuglog(" evaluate_mechanism: $modifier$mechanism($argument) for domain=$query->{domain}");
1178              
1179 0 0       0 if ({ map { $_=>1 } @KNOWN_MECHANISMS }->{$mechanism}) {
  0         0  
1180 0         0 my $mech_sub = "mech_$mechanism";
1181 0         0 my ($hit, $text) = $query->$mech_sub($query->macro_substitute($argument, 255));
1182 1     1   14 no warnings 'uninitialized';
  1         3  
  1         6315  
1183 0         0 $query->debuglog(" evaluate_mechanism: $modifier$mechanism($argument) returned $hit $text");
1184              
1185 0 0       0 return if not $hit;
1186              
1187 0 0       0 return ($hit, $text) if ($hit ne "hit");
1188            
1189 0 0       0 if ($source) {
1190 0         0 $query->{spf_source} = $source;
1191 0         0 $query->{spf_source_type} = "from mechanism $mechanism";
1192             }
1193              
1194 0         0 return $query->shorthand2value($modifier), $text;
1195             }
1196             else {
1197 0 0       0 my $unrecognized_mechanism = join ("",
    0          
1198             ($modifier eq "+" ? "" : $modifier),
1199             $mechanism,
1200             ($argument ? ":" : ""),
1201             $argument);
1202 0         0 my $error_string = "unknown $unrecognized_mechanism";
1203 0         0 $query->debuglog(" evaluate_mechanism: unrecognized mechanism $unrecognized_mechanism, returning $error_string");
1204 0         0 return $error_string => "unrecognized mechanism $unrecognized_mechanism";
1205             }
1206              
1207 0         0 return ("neutral", "evaluate-mechanism: neutral");
1208             }
1209              
1210             # ----------------------------------------------------------
1211             # myquery wraps DNS resolver queries
1212             #
1213             # ----------------------------------------------------------
1214              
1215             sub myquery {
1216 164     164 0 239 my $query = shift;
1217 164         412 my $label = shift;
1218 164         204 my $qtype = shift;
1219 164         256 my $method = shift;
1220 164         751 my $sortby = shift;
1221              
1222 164         584 $query->debuglog(" myquery: doing $qtype query on $label");
1223              
1224 164         1244 for ($label) {
1225 164 50 33     7420 if (/\.\./ or /^\./) {
1226             # convert .foo..com to foo.com, etc.
1227 0         0 $query->debuglog(" myquery: fixing up invalid syntax in $label");
1228 0         0 s/\.\.+/\./g;
1229 0         0 s/^\.//;
1230 0         0 $query->debuglog(" myquery: corrected label is $label");
1231             }
1232             }
1233 164         460 my $resquery = $query->resolver->query($label, $qtype);
1234              
1235 164         2370621 my $errorstring = $query->resolver->errorstring;
1236 164 50 33     2334 if (not $resquery and $errorstring eq "NOERROR") {
1237 0         0 return;
1238             }
1239              
1240 164         718 $query->{last_dns_error} = $errorstring;
1241              
1242 164 50       565 if (not $resquery) {
1243 164 50       1114 if ($errorstring eq "NXDOMAIN") {
1244 164         1149 $query->debuglog(" myquery: $label $qtype failed: NXDOMAIN.");
1245 164         2898 return;
1246             }
1247              
1248 0         0 $query->debuglog(" myquery: $label $qtype lookup error: $errorstring");
1249 0         0 $query->debuglog(" myquery: will set error condition.");
1250 0         0 $query->set_temperror("DNS error while looking up $label $qtype: $errorstring");
1251 0         0 return;
1252             }
1253              
1254 0         0 my @answers = grep { lc $_->type eq lc $qtype } $resquery->answer;
  0         0  
1255              
1256             # $query->debuglog(" myquery: found $qtype response: @answers");
1257              
1258 0         0 my @toreturn;
1259 0 0       0 if ($sortby) { @toreturn = map { rr_method($_,$method) } sort { $a->$sortby() <=> $b->$sortby() } @answers; }
  0         0  
  0         0  
  0         0  
1260 0         0 else { @toreturn = map { rr_method($_,$method) } @answers; }
  0         0  
1261              
1262 0 0       0 if (not @toreturn) {
1263 0         0 $query->debuglog(" myquery: result had no data.");
1264 0         0 return;
1265             }
1266              
1267 0         0 return @toreturn;
1268             }
1269              
1270             sub rr_method {
1271 0     0 0 0 my ($answer, $method) = @_;
1272 0 0       0 if ($method ne "char_str_list") { return $answer->$method() }
  0         0  
1273              
1274             # long TXT records can't be had with txtdata; they need to be pulled out with char_str_list which returns a list of strings
1275             # that need to be joined.
1276              
1277 0         0 my @char_str_list = $answer->$method();
1278             # print "rr_method returning join of @char_str_list\n";
1279              
1280 0         0 return join "", @char_str_list;
1281             }
1282              
1283             #
1284             # Mechanisms return one of the following:
1285             #
1286             # undef mechanism did not match
1287             # "hit" mechanism matched
1288             # "unknown" some error happened during processing
1289             # "error" some temporary error
1290             #
1291             # ----------------------------------------------------------
1292             # all
1293             # ----------------------------------------------------------
1294              
1295             sub mech_all {
1296 0     0 0 0 my $query = shift;
1297 0         0 return "hit" => "default";
1298             }
1299              
1300             # ----------------------------------------------------------
1301             # include
1302             # ----------------------------------------------------------
1303              
1304             sub mech_include {
1305 0     0 0 0 my $query = shift;
1306 0         0 my $argument = shift;
1307              
1308 0 0       0 if (not $argument) {
1309 0         0 $query->debuglog(" mechanism include: no argument given.");
1310 0         0 return "unknown", "include mechanism not given an argument";
1311             }
1312              
1313 0         0 $query->debuglog(" mechanism include: recursing into $argument");
1314              
1315 0         0 my $inner_query = $query->clone(domain => $argument,
1316             reason => "includes $argument",
1317             local => undef,
1318             trusted => undef,
1319             guess => undef,
1320             default_record => undef,
1321             );
1322              
1323 0         0 my ($result, $explanation, $text, $orig_txt, $time) = $inner_query->spfquery();
1324              
1325 0         0 $query->debuglog(" mechanism include: got back result $result / $text / $time");
1326              
1327 0 0       0 if ($result eq "pass") { return hit => $text, $time; }
  0         0  
1328 0 0       0 if ($result eq "error") { return $result => $text, $time; }
  0         0  
1329 0 0       0 if ($result eq "unknown") { return $result => $text, $time; }
  0         0  
1330 0 0       0 if ($result eq "none") { return unknown => $text, $time; } # fail-safe mode. convert an included NONE into an UNKNOWN error.
  0         0  
1331 0 0 0     0 if ($result eq "fail" ||
      0        
1332             $result eq "neutral" ||
1333 0         0 $result eq "softfail") { return undef, $text, $time; }
1334            
1335 0         0 $query->debuglog(" mechanism include: reducing result $result to unknown");
1336 0         0 return "unknown", $text, $time;
1337             }
1338              
1339             # ----------------------------------------------------------
1340             # a
1341             # ----------------------------------------------------------
1342              
1343             sub mech_a {
1344 0     0 0 0 my $query = shift;
1345 0         0 my $argument = shift;
1346            
1347 0 0       0 my $ip4_cidr_length = ($argument =~ s/ \/(\d+)//x) ? $1 : 32;
1348 0 0       0 my $ip6_cidr_length = ($argument =~ s/\/\/(\d+)//x) ? $1 : 128;
1349              
1350 0   0     0 my $domain_to_use = $argument || $query->{domain};
1351              
1352             # see code below in ip4 for more validation
1353 0 0       0 if ($domain_to_use !~ / \. [a-z] (?: [a-z0-9-]* [a-z0-9] ) $ /ix) {
1354 0         0 return ("unknown" => "bad argument to a: $domain_to_use not a valid FQDN");
1355             }
1356              
1357 0         0 foreach my $a ($query->myquery($domain_to_use, "A", "address")) {
1358 0         0 $query->debuglog(" mechanism a: $a");
1359 0 0       0 if ($a eq $query->{ipv4}) {
    0          
1360 0         0 $query->debuglog(" mechanism a: match found: $domain_to_use A $a == $query->{ipv4}");
1361 0         0 return "hit", "$domain_to_use A $query->{ipv4}";
1362             }
1363             elsif ($ip4_cidr_length < 32) {
1364 0         0 my $cidr = Net::CIDR::Lite->new("$a/$ip4_cidr_length");
1365              
1366 0         0 $query->debuglog(" mechanism a: looking for $query->{ipv4} in $a/$ip4_cidr_length");
1367            
1368 0 0       0 return (hit => "$domain_to_use A $a /$ip4_cidr_length contains $query->{ipv4}")
1369             if $cidr->find($query->{ipv4});
1370             }
1371             }
1372 0         0 return;
1373             }
1374              
1375             # ----------------------------------------------------------
1376             # mx
1377             # ----------------------------------------------------------
1378              
1379             sub mech_mx {
1380 0     0 0 0 my $query = shift;
1381 0         0 my $argument = shift;
1382              
1383 0 0       0 my $ip4_cidr_length = ($argument =~ s/ \/(\d+)//x) ? $1 : 32;
1384 0 0       0 my $ip6_cidr_length = ($argument =~ s/\/\/(\d+)//x) ? $1 : 128;
1385              
1386 0   0     0 my $domain_to_use = $argument || $query->{domain};
1387              
1388 0 0       0 if ($domain_to_use !~ / \. [a-z] (?: [a-z0-9-]* [a-z0-9] ) $ /ix) {
1389 0         0 return ("unknown" => "bad argument to mx: $domain_to_use not a valid FQDN");
1390             }
1391              
1392 0         0 my @mxes = $query->myquery($domain_to_use, "MX", "exchange", "preference");
1393              
1394 0         0 foreach my $mx (@mxes) {
1395             # $query->debuglog(" mechanism mx: $mx");
1396              
1397 0         0 foreach my $a ($query->myquery($mx, "A", "address")) {
1398 0 0       0 if ($a eq $query->{ipv4}) {
    0          
1399 0         0 $query->debuglog(" mechanism mx: we have a match; $domain_to_use MX $mx A $a == $query->{ipv4}");
1400 0         0 return "hit", "$domain_to_use MX $mx A $a";
1401             }
1402             elsif ($ip4_cidr_length < 32) {
1403 0         0 my $cidr = Net::CIDR::Lite->new("$a/$ip4_cidr_length");
1404              
1405 0         0 $query->debuglog(" mechanism mx: looking for $query->{ipv4} in $a/$ip4_cidr_length");
1406              
1407 0 0       0 return (hit => "$domain_to_use MX $mx A $a /$ip4_cidr_length contains $query->{ipv4}")
1408             if $cidr->find($query->{ipv4});
1409              
1410             }
1411             }
1412             }
1413 0         0 return;
1414             }
1415              
1416             # ----------------------------------------------------------
1417             # ptr
1418             # ----------------------------------------------------------
1419              
1420             sub mech_ptr {
1421 0     0 0 0 my $query = shift;
1422 0         0 my $argument = shift;
1423              
1424 0 0       0 if ($query->{ipv6}) { return "neutral", "ipv6 not yet supported"; }
  0         0  
1425              
1426 0   0     0 my $domain_to_use = $argument || $query->{domain};
1427              
1428 0         0 foreach my $ptrdname ($query->myquery(reverse_in_addr($query->{ipv4}) . ".in-addr.arpa", "PTR", "ptrdname")) {
1429 0         0 $query->debuglog(" mechanism ptr: $query->{ipv4} is $ptrdname");
1430            
1431 0         0 $query->debuglog(" mechanism ptr: checking hostname $ptrdname for legitimacy.");
1432            
1433             # check for legitimacy --- PTR -> hostname A -> PTR
1434 0         0 foreach my $ptr_to_a ($query->myquery($ptrdname, "A", "address")) {
1435            
1436 0         0 $query->debuglog(" mechanism ptr: hostname $ptrdname -> $ptr_to_a");
1437            
1438 0 0       0 if ($ptr_to_a eq $query->{ipv4}) {
1439 0         0 $query->debuglog(" mechanism ptr: we have a valid PTR: $query->{ipv4} PTR $ptrdname A $ptr_to_a");
1440 0         0 $query->debuglog(" mechanism ptr: now we see if $ptrdname ends in $domain_to_use.");
1441            
1442 0 0       0 if ($ptrdname =~ /(^|\.)\Q$domain_to_use\E$/i) {
1443 0         0 $query->debuglog(" mechanism ptr: $query->{ipv4} PTR $ptrdname does end in $domain_to_use.");
1444 0         0 return hit => "$query->{ipv4} PTR $ptrdname matches $domain_to_use";
1445             }
1446             else {
1447 0         0 $query->debuglog(" mechanism ptr: $ptrdname does not end in $domain_to_use. no match.");
1448             }
1449             }
1450             }
1451             }
1452 0         0 return;
1453             }
1454              
1455             # ----------------------------------------------------------
1456             # exists
1457             # ----------------------------------------------------------
1458              
1459             sub mech_exists {
1460 0     0 0 0 my $query = shift;
1461 0         0 my $argument = shift;
1462              
1463 0 0       0 return if (!$argument);
1464              
1465 0         0 my $domain_to_use = $argument;
1466              
1467 0         0 $query->debuglog(" mechanism exists: looking up $domain_to_use");
1468            
1469 0         0 foreach ($query->myquery($domain_to_use, "A", "address")) {
1470 0         0 $query->debuglog(" mechanism exists: $_");
1471 0         0 $query->debuglog(" mechanism exists: we have a match.");
1472 0         0 my @txt = map { s/^"//; s/"$//; $_ } $query->myquery($domain_to_use, "TXT", "char_str_list");
  0         0  
  0         0  
  0         0  
1473 0 0       0 if (@txt) {
1474 0         0 return hit => join(" ", @txt);
1475             }
1476 0         0 return hit => "$domain_to_use found";
1477             }
1478 0         0 return;
1479             }
1480              
1481             # ----------------------------------------------------------
1482             # ip4
1483             # ----------------------------------------------------------
1484              
1485             sub mech_ip4 {
1486 0     0 0 0 my $query = shift;
1487 0         0 my $cidr_spec = shift;
1488              
1489 0 0       0 if ($cidr_spec eq '') {
1490 0         0 return ("unknown" => "no argument given to ip4");
1491             }
1492              
1493 0         0 my ($network, $cidr_length) = split (/\//, $cidr_spec, 2);
1494              
1495 0 0 0     0 if (
      0        
1496             $network !~ /^\d+\.\d+\.\d+\.\d+$/ ||
1497             (defined($cidr_length) && $cidr_length !~ /^\d+$/)
1498 0         0 ) { return ("unknown" => "bad argument to ip4: $cidr_spec"); }
1499            
1500 0 0       0 $cidr_length = "32" if not defined $cidr_length;
1501              
1502 0         0 local $@;
1503 0         0 my $cidr = eval { Net::CIDR::Lite->new("$network/$cidr_length") };
  0         0  
1504 0 0       0 if ($@) { return ("unknown" => "unable to parse ip4:$cidr_spec"); }
  0         0  
1505              
1506 0         0 $query->debuglog(" mechanism ip4: looking for $query->{ipv4} in $cidr_spec");
1507              
1508 0 0       0 return (hit => "$cidr_spec contains $query->{ipv4}") if $cidr->find($query->{ipv4});
1509              
1510 0         0 return;
1511             }
1512              
1513             # ----------------------------------------------------------
1514             # ip6
1515             # ----------------------------------------------------------
1516              
1517             sub mech_ip6 {
1518 0     0 0 0 my $query = shift;
1519              
1520 0         0 return;
1521             }
1522              
1523             # ----------------------------------------------------------
1524             # functions
1525             # ----------------------------------------------------------
1526              
1527             sub ip { # accessor
1528 174     174 0 360 my $query = shift;
1529 174   33     782 return $query->{ipv4} || $query->{ipv6};
1530             }
1531              
1532             sub reverse_in_addr {
1533 165     165 0 1447 return join (".", (reverse split /\./, shift));
1534             }
1535              
1536             sub resolver {
1537 328     328 0 929 my $query = shift;
1538 328   66     3817 return $query->{res} ||= Net::DNS::Resolver->new(
1539             tcp_timeout => $DNS_RESOLVER_TIMEOUT,
1540             udp_timeout => $DNS_RESOLVER_TIMEOUT,
1541             );
1542             }
1543              
1544             sub fallbacks {
1545 0     0 0 0 my $query = shift;
1546 0         0 return @{$query->{fallbacks}};
  0         0  
1547             }
1548              
1549             sub shorthand2value {
1550 0     0 0 0 my $query = shift;
1551 0         0 my $shorthand = shift;
1552 0   0     0 return { "-" => "fail",
1553             "+" => "pass",
1554             "~" => "softfail",
1555             "?" => "neutral" } -> {$shorthand} || $shorthand;
1556             }
1557              
1558             sub value2shorthand {
1559 0     0 0 0 my $query = shift;
1560 0         0 my $value = lc shift;
1561 0   0     0 return { "fail" => "-",
1562             "pass" => "+",
1563             "softfail" => "~",
1564             "deny" => "-",
1565             "allow" => "+",
1566             "softdeny" => "~",
1567             "unknown" => "?",
1568             "neutral" => "?" } -> {$value} || $value;
1569             }
1570              
1571             sub interpolate_explanation {
1572 0     0 0 0 my $query = shift;
1573 0         0 my $txt = shift;
1574              
1575 0 0       0 if ($query->{directive_set}->explanation) {
1576 0         0 my @txt = map { s/^"//; s/"$//; $_ } $query->myquery($query->macro_substitute($query->{directive_set}->explanation), "TXT", "char_str_list");
  0         0  
  0         0  
  0         0  
1577 0         0 $txt = join " ", @txt;
1578             }
1579              
1580 0         0 return $query->macro_substitute($txt);
1581             }
1582              
1583             sub find_ancestor {
1584 0     0 0 0 my $query = shift;
1585 0         0 my $which_hash = shift;
1586 0         0 my $current_domain = shift;
1587              
1588 0 0       0 return if not exists $query->{$which_hash};
1589              
1590 0         0 $current_domain =~ s/\.$//g;
1591 0         0 my @current_domain = split /\./, $current_domain;
1592              
1593 0         0 foreach my $ancestor_level (0 .. @current_domain) {
1594 0         0 my @ancestor = @current_domain;
1595 0         0 for (1 .. $ancestor_level) { shift @ancestor }
  0         0  
1596 0         0 my $ancestor = join ".", @ancestor;
1597              
1598 0 0       0 for my $match ($ancestor_level > 0 ? "*.$ancestor" : $ancestor) {
1599 0         0 $query->debuglog(" DirectiveSet $which_hash: is $match in the $which_hash hash?");
1600 0 0       0 if (my $record = $query->{$which_hash}->{lc $match}) {
1601 0         0 $query->debuglog(" DirectiveSet $which_hash: yes, it is.");
1602 0 0       0 return wantarray ? ($which_hash, $match, $record) : $record;
1603             }
1604             }
1605             }
1606 0         0 return;
1607             }
1608              
1609             sub found_record_for {
1610 0     0 0 0 my $query = shift;
1611 0         0 my ($which_hash, $matched_domain_glob, $record) = $query->find_ancestor(@_);
1612 0 0       0 return if not $record;
1613 0         0 $query->{spf_source} = "explicit $which_hash found: $matched_domain_glob defines $record";
1614 0         0 $query->{spf_source_type} = "full-explanation";
1615 0 0       0 $record = "v=spf1 $record" if $record !~ /^v=spf1\b/i;
1616 0         0 return $record;
1617             }
1618              
1619             sub try_override {
1620 0     0 0 0 my $query = shift;
1621 0         0 return $query->found_record_for("override", @_);
1622             }
1623              
1624             sub try_fallback {
1625 0     0 0 0 my $query = shift;
1626 0         0 return $query->found_record_for("fallback", @_);
1627             }
1628              
1629             # ----------------------------------------------------------
1630             # algo
1631             # ----------------------------------------------------------
1632              
1633             {
1634             package DirectiveSet;
1635              
1636             sub new {
1637 164     164   303 my $class = shift;
1638 164         501 my $current_domain = shift;
1639 164         328 my $query = shift;
1640 164         276 my $override_text = shift;
1641 164         211 my $localpolicy = shift;
1642 164         332 my $default_record = shift;
1643              
1644 164         253 my $txt;
1645              
1646             # Overrides can come from two places:
1647             # - When operating in best_guess mode, spfquery may be called with a $guess_mechs argument, which comes in as $override_text.
1648             # - When operating with ->new(..., override => { ... }) we need to load the override dynamically.
1649 164 50       703 if ($override_text) {
    50          
1650 0         0 $txt = "v=spf1 $override_text ?all";
1651 0         0 $query->{spf_source} = "local policy";
1652 0         0 $query->{spf_source_type} = "full-explanation";
1653             }
1654             elsif (exists $query->{override}) {
1655 0         0 $txt = $query->try_override($current_domain);
1656             }
1657              
1658             # Retrieve a record from DNS:
1659 164 50       598 if (!defined $txt) {
1660 164         221 my @txt;
1661 164         660 $query->debuglog(" DirectiveSet->new(): doing TXT query on $current_domain");
1662 164         2386 @txt = $query->myquery($current_domain, "TXT", "char_str_list");
1663 164         1683 $query->debuglog(" DirectiveSet->new(): TXT query on $current_domain returned error=$query->{error}, last_dns_error=$query->{last_dns_error}");
1664              
1665             # Combine multiple TXT strings into a single string:
1666 164         1808 foreach (@txt) {
1667 0 0       0 $txt .= $1 if /^v=spf1\s*(.*)$/;
1668             }
1669              
1670             $txt = undef
1671 164 50 33     1292 if $query->{error} or $query->{last_dns_error} eq 'NXDOMAIN';
1672             }
1673              
1674             # Try the fallbacks:
1675 164 50 33     1019 if (!defined $txt and exists $query->{fallback}) {
1676 0         0 $query->debuglog(" DirectiveSet->new(): will try fallbacks.");
1677 0         0 $txt = $query->try_fallback($current_domain, "fallback");
1678 0 0       0 defined($txt)
1679             or $query->debuglog(" DirectiveSet->new(): fallback search failed.");
1680             }
1681              
1682 164 50 33     1179 if (!defined $txt and defined $default_record) {
1683 0         0 $txt = "v=spf1 $default_record ?all";
1684 0         0 $query->{spf_source} = "local policy";
1685 0         0 $query->{spf_source_type} = "full-explanation";
1686             }
1687              
1688 164         2132 $query->debuglog(" DirectiveSet->new(): SPF policy: $txt");
1689              
1690 164 50       1584 return if not defined $txt;
1691              
1692             # TODO: the prepending of the v=spf1 is a massive hack; get it right by saving the actual raw orig_txt.
1693 0 0         my $directive_set = bless { orig_txt => ($txt =~ /^v=spf1/ ? $txt : "v=spf1 $txt"), txt => $txt } , $class;
1694              
1695             TXT_RESPONSE:
1696 0           for ($txt) {
1697 0           $query->debuglog(" lookup: TXT $_");
1698              
1699             # parse the policy record
1700            
1701 0           while (/\S/) {
1702 0           s/^\s*(\S+)\s*//;
1703 0           my $word = $1;
1704             # $query->debuglog(" lookup: word parsing word $word");
1705 0 0         if ($word =~ /^v=(\S+)/i) {
1706 0           my $version = $1;
1707 0           $query->debuglog(" lookup: TXT version=$version");
1708 0           $directive_set->{version} = $version;
1709 0 0         next TXT_RESPONSE if ($version ne "spf1");
1710 0           next;
1711             }
1712              
1713             # modifiers always have an = sign.
1714 0 0         if (my ($lhs, $rhs) = $word =~ /^([^:\/]+)=(\S*)$/) {
1715             # $query->debuglog(" lookup: TXT modifier found: $lhs = $rhs");
1716              
1717             # if we ever come to support multiple of the same modifier, we need to make this a list.
1718 0           $directive_set->{modifiers}->{lc $lhs} = $rhs;
1719 0           next;
1720             }
1721              
1722             # RHS optional, defaults to domain.
1723             # [:/] matches a:foo and a/24
1724 0 0         if (my ($prefix, $lhs, $rhs) = $word =~ /^([-~+?]?)([\w_-]+)([\/:]\S*)?$/i) {
1725 0           $rhs =~ s/^://;
1726 0   0       $prefix ||= "+";
1727 0           $query->debuglog(" lookup: TXT prefix=$prefix, lhs=$lhs, rhs=$rhs");
1728 0           push @{$directive_set->{mechanisms}}, [$prefix => lc $lhs => $rhs];
  0            
1729 0           next;
1730             }
1731              
1732             }
1733             }
1734              
1735 0 0         if (my $rhs = delete $directive_set->{modifiers}->{default}) {
1736 0           push @{$directive_set->{mechanisms}}, [ $query->value2shorthand($rhs), all => undef ];
  0            
1737             }
1738              
1739 0 0         $directive_set->{mechanisms} = [] if not $directive_set->{mechanisms};
1740 0 0         if ($localpolicy) {
1741 0           my $mechanisms = $directive_set->{mechanisms};
1742 0           my $lastmech = $mechanisms->[$#$mechanisms];
1743 0 0 0       if (($lastmech->[0] eq '-' || $lastmech->[0] eq '?') &&
      0        
1744             $lastmech->[1] eq 'all') {
1745 0           my $index;
1746              
1747 0           for ($index = $#$mechanisms - 1; $index >= 0; $index--) {
1748 0 0         last if ($lastmech->[0] ne $mechanisms->[$index]->[0]);
1749             }
1750 0 0         if ($index >= 0) {
1751             # We want to insert the localpolicy just *after* $index
1752 0           $query->debuglog(" inserting local policy mechanisms into @{[$directive_set->show_mechanisms]} after position $index");
  0            
1753 0           my $localset = DirectiveSet->new($current_domain, $query->clone, $localpolicy);
1754              
1755 0 0         if ($localset) {
1756 0           my @locallist = $localset->mechanisms;
1757             # Get rid of the ?all at the end of the list
1758 0           pop @locallist;
1759             # $_->[3] goes into $query->{spf_source}.
1760 0 0         map { $_->[3] = ($_->[1] eq 'include'
  0            
1761             ? "local policy includes SPF record at " . $query->macro_substitute($_->[2])
1762             : "local policy") }
1763             @locallist;
1764 0           splice(@$mechanisms, $index + 1, 0, @locallist);
1765             }
1766             }
1767             }
1768             }
1769 0           $query->debuglog(" lookup: mec mechanisms=@{[$directive_set->show_mechanisms]}");
  0            
1770 0           return $directive_set;
1771             }
1772              
1773 0     0     sub version { shift->{version} }
1774 0     0     sub mechanisms { @{shift->{mechanisms}} }
  0            
1775 0     0     sub explanation { shift->{modifiers}->{exp} }
1776 0     0     sub redirect { shift->{modifiers}->{redirect} }
1777 0     0     sub get_modifier { shift->{modifiers}->{shift()} }
1778 0     0     sub syntax_error { shift->{syntax_error} }
1779              
1780             sub show_mechanisms {
1781 0     0     my $directive_set = shift;
1782 0   0       my @toreturn = map { $_->[0] . $_->[1] . "(" . ($_->[2]||"") . ")" } $directive_set->mechanisms;
  0            
1783             # print STDERR ("showing mechanisms @toreturn: " . Dumper($directive_set)); use Data::Dumper;
1784 0           return @toreturn;
1785             }
1786             }
1787              
1788             1;
1789              
1790             =head1 WARNINGS
1791              
1792             Mail::Query::SPF should only be used at the point where messages are received
1793             from the Internet. The underlying assumption is that the sender of the e-mail
1794             is sending the message directly to you or one of your secondary MXes. If your
1795             MTA does not have an exhaustive list of secondary MXes, then the C
1796             and C methods can be used. These methods take care to
1797             permit mail from secondary MXes.
1798              
1799             =head1 AUTHORS
1800              
1801             Meng Weng Wong , Philip Gladstone, Julian Mehnle
1802            
1803              
1804             =head1 SEE ALSO
1805              
1806             About SPF: L
1807              
1808             Mail::SPF::Query: L
1809              
1810             The latest release of the SPF specification: L
1811              
1812             =cut
1813              
1814             # vim:et sts=4 sw=4