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/