home *** CD-ROM | disk | FTP | other *** search
/ ftp.f-secure.com / 2014.06.ftp.f-secure.com.tar / ftp.f-secure.com / support / hotfix / fsis / IS-SpamControl.fsfix / iufssc / lib / Mail / SPF / Query.pm
Text File  |  2006-11-29  |  65KB  |  1,933 lines

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