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") { ... } # domain is not forged
elsif ($result eq "fail") { ... } # domain is forged
elsif ($result eq "softfail") { ... } # domain may be forged
else { ... } # 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,
no_explicit_wildcard_workaround => 1,
if ($@) { warn "bad input to Mail::SPF::Query: $@" }
Set debug=
1> to watch the queries happen.
We expect an SPF-conformant nameserver to respond with an "spf=allow/deny/softdeny" response when we query reversed-ip.in-addr._smtp_client.domain
. If we receive an NXDOMAIN response upon the initial query, we will try to find a default status by querying _default._smtp_client
. We do this because certain DNS servers require explicit wildcarding at each level of a subdomain hierarchy; this behaviour is RFC1034-conformant, but undesirable for our purposes! Set no_explicit_wildcard_workaround=
1, fallbacks=>[]> if your nameserver needs explicit wildcarding. If it does, see http://spf.pobox.com/explicit_wildcards.html
$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. The mail should be accepted subject to local policy regarding the sender domain.
fail
means the client IP is not a designated mailer, and the sender domain wants you to reject the transaction for fear of forgery.
softfail
means the transaction should be accepted but subject to further scrutiny because the domain is still transitioning toward SPF adoption.
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 | UNKNOWN | ERROR , TEXT
data: LOOKUP_RESULT = PASS | FAIL | UNKNOWN | ERROR , TEXT SPFQUERY_RESULT = PASS | 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, 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
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. spfquery will try the includes before using the FAIL response.
TXT SPF=softfail: return SOFTFAIL. spfquery will try the includes before using the FAIL response.
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/