The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

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

SYNOPSIS

  my $query = new Mail::SPF::Query (ip => "127.0.0.1", sender=>'foo@example.com', helo=>"somehost.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.

METHODS

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

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

                    optional parameters:   fallbacks => ["spf.mailzone.com", ...],
                                           debug => 1, debuglog => sub { print STDERR "@_\n" },
                                           no_explicit_wildcard_workaround => 1,

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

Set debug=>1 to watch the queries happen.

$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.

Results are cached internally for a default of 120 seconds. You can call ->result() repeatedly; subsequent lookups won't hit your DNS.

$query->debuglog()

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

  Alternatively, pass the C<new()> constructor a
  C<debuglog => sub { ... }> callback, and we'll pass
  debugging lines to that.

Regarding Explicit Wildcards

We expect an SPF-conformant nameserver to respond with an "spf=allow/deny/softdeny" response when we query reversed-ip.in-addr._smtp_client.host.example.com and reversed-ip.in-addr._smtp_client.example.com. If we receive an NXDOMAIN response upon the initial query, we will try to find a default status by querying _default._smtp_client. This behaviour will be removed in future releases. See http://spf.pobox.com/explicit_wildcards.html for more information.

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/

http://develooper.com/code/qpsmtpd/ is a Perl replacement for qmail-smtpd. It also works as a Postfix content-filter and smtpd proxy pass-through. qpsmtpd supports SPF.