NAME

Mail::SPF::Query - query Sender Permitted From for an IP,email

SYNOPSIS

my $query = new Mail::SPF::Query (ip => "127.0.0.1", sender=>'foo@example.com');
my ($result, $comment) = $query->result();

if    ($result eq "pass") { ... } # mail is not spam
elsif ($result eq "deny") { ... } # mail may be spam
else                      { ... } # sender domain has not implemented SPF

ABSTRACT

The SPF protocol relies on sender domains to publish a DNS whitelist of their designated outbound mailers. Given an envelope sender, Mail::SPF::Query determines the legitimacy of an SMTP client IP.

Mail::SPF::Query->new()

my $query = eval { new Mail::SPF::Query (ip => "127.0.0.1", sender=>'foo@example.com') };

                  optional parameters:   fallbacks => ["spf.mailzone.com", ...],
                                         debug => 1,

if ($@) { warn "bad input to Mail::SPF::Query: $@" }

$query->result()

my ($result, $comment) = $query->result();

$result will be one of pass, fail, softfail, unknown, or error.

pass means the client IP is a designated mailer for the sender's domain.

error means the client IP is not.

unknown means the domain does not publish SPF data.

error means the DNS lookup encountered an error during processing.

$query->debuglog()

Subclasses may override this with their own debug logger.  I recommend Log::Dispatch.

Algorithm

input: SEARCH_STACK = ([domain_name, is_fallback], ...)

returns: one of PASS | SOFTFAIL | FAIL | DEFER_PASS | DEFER_SOFTFAIL | DEFER_FAIL | UNKNOWN | ERROR , TEXT

data: LOOKUP_RESULT = PASS | FAIL | DEFER_PASS | DEFER_FAIL | UNKNOWN | ERROR , TEXT SPFQUERY_RESULT = PASS | FAIL | DEFER_PASS | DEFER_FAIL | UNKNOWN | ERROR , TEXT

pop a DOMAIN off the top of the stack and run

LOOKUP_RESULT, LOOKUP_TEXT = LOOKUP(DOMAIN, SEARCH_STACK).

as a side effect, LOOKUP may push new domains onto the top of the SEARCH_STACK on the basis of SPFinclude replies.

They will be pushed with the attribute includehardenfail=1, because SOFTDENY makes everything more complicated. It should be relevant for the top-level search but not in any included domains.

If LOOKUP returns a PASS, a FAIL, or a SOFT_FAIL, short-circuit the query by returning LOOKUP_RESULT, LOOKUP_TEXT immediately. That result will propagate all the way back up the recursion stack.

If LOOKUP found any includes, it will return DEFER_FAIL or DEFER_SOFTFAIL instead of FAIL or SOFTFAIL. So try the includes also before returning the current value.

If the search stack is empty, return the LOOKUP_RESULT, LOOKUP_TEXT.

To exhaust the search stack, we will recurse:

SPFQUERY_RESULT, SPFQUERY_TEXT = SPFQUERY(SEARCH_STACK)

return the severer of LOOKUP_RESULT vs SPFQUERY_RESULT, together with the appropriate TEXT. Severity is defined according to the following table:

PASS	 
FAIL	 
SOFTFAIL	 
ERROR	 
DEFER_PASS
DEFER_FAIL
DEFER_SOFTFAIL
UNKNOWN    

SEARCH ALGORITHM: lookup

global IP global DOMAINS_QUERIED

lookup(DOMAIN, SEARCH_STACK):

Pop a domain off the top of the stack.

Have we queried this domain already? If so, return nothing.

Perform a TXT query. If the result contains

CNAME: push the CNAME's target onto the SEARCH_STACK and return nothing.
TXT SPF=allow: return PASS.
TXT SPFinclude=domain.com: push all matching domain.com onto the SEARCH_STACK in reverse order of their [:priority].
TXT SPF=fail: return FAIL if there were no includes; if there were includes, return DEFER_FAIL.
TXT SPF=softfail: return SOFTFAIL if there were no includes; if there were includes, return DEFER_SOFTFAIL.

If the query failed or returned unknown, if the domain IS NOT FALLBACK,

push the fallback versions of the current domain onto the
top of the search stack:

  SEARCH_STACK = SEARCH_STACK map { "domain_name.$_" } FALLBACK_LIST

Then return unknown.

EXPORT

None by default.

AUTHOR

Meng Weng Wong, <mengwong+spf@pobox.com>

SEE ALSO

http://spf.pobox.com/