Mail::S...Iterator(3)

πŸ“¦
version 1.121 Β· 2 versions
, in fedora 43

πŸ’– Mail::SPF::Iterator πŸ’–

okay bestie, welcome to the manual page for Mail::SPF::Iterator β€” it's part of the User Contributed Perl Documentation and it's genuinely one of those modules that makes you go "wait, that's actually so clever". it does iterative SPF lookup, and by the end of this page you're gonna understand it fr fr. πŸ¦‹


✨ NAME ✨

Mail::SPF::Iterator β€” iterative SPF lookup. that's it, that's the whole vibe. πŸ’…


🌸 SYNOPSIS 🌸

no notes, just look at it:

    use Net::DNS;
    use Mail::SPF::Iterator;
    use Mail::SPF::Iterator Debug =>1; # enable debugging
    my $spf = Mail::SPF::Iterator->new(
        $ip,       # IP4|IP6 of client
        $mailfrom, # from MAIL FROM:
        $helo,     # from HELO|EHLO
        $myname,   # optional: my hostname
        {
            default_spf => 'mx/24 ?all', # in case no record was found in DNS
            pass_all => SPF_SoftFail,    # treat records like '+all' as error
            # rfc4408 => 1,              # for compatibility only
        }
    );

    # could be other resolvers too
    my $resolver = Net::DNS::Resolver->new;

    ### with nonblocking, but still in loop
    ### (callbacks are preferred with non-blocking)
    my ($result,@ans) = $spf->next; # initial query
    while ( ! $result ) {
        my @query = @ans;
        die "no queries" if ! @query;
        for my $q (@query) {
            # resolve query
            my $socket = $resolver->bgsend( $q );
            ... wait...
            my $answer = $resolver->bgread($socket);
            ($result,@ans) = $spf->next(
                $answer                             # valid answer
                || [ $q, $resolver->errorstring ]   # or DNS problem
            );
            last if $result; # got final result
            last if @ans;    # got more DNS queries
        }
    }

    ### OR with blocking:
    ### ($result,@ans) = $spf->lookup_blocking( undef,$resolver );

    ### print mailheader
    print "Received-SPF: ".$spf->mailheader;

    # $result = Fail|Pass|...
    # $ans[0] = comment for Received-SPF
    # $ans[1] = %hash with infos for Received-SPF
    # $ans[2] = explanation in case of Fail

like, the loop is doing the most and honestly it ate. πŸ”₯


πŸ’« DESCRIPTION πŸ’«

so this module gives you iterative resolving of SPF records. and here's the tea: unlike Mail::SPF, which does blocking DNS lookups (imagine waiting around, not very main character of it), this module just hands you the DNS queries and later expects the responses back. you're in charge, you're driving, you're the main character. πŸ‘‘

the lookup of the DNS records happens outside of the module, and it can totally be done in an event driven way. you can also run many parallel SPF checks in parallel without needing multiple threads or processes. no cap, that's the whole point. ✨

this module can also make use of SenderID records for checking the "mfrom" part, but it will prefer SPF. it will only use DNS TXT records for looking up SPF policies unless compatibility with RFC 4408 is explicitly enabled.

see RFC 7208 (old RFC 4408) for SPF and RFC 4406 for SenderID. iykyk. πŸ“š


πŸŽ€ METHODS πŸŽ€

new( IP, MAILFROM, HELO, [ MYNAME ], [ \%OPT ] )

constructs a shiny new Mail::SPF::Iterator object, which maintains the state between the steps of the iteration. important: for each new SPF check you gotta create a new object. no recycling bestie, it doesn't work like that. πŸ™ˆ

  • IP β€” the IP if the client as string (IP4 or IP6).
  • MAILFROM β€” the user@domain part from the MAIL FROM handshake, e.g. '<', '>' and any parameters removed. if only '<>' was given (like in bounces) the value is empty.
  • HELO β€” the string send within the HELO|EHLO dialog which should be a domain according to the RFC but often is not. 😭 (we've all seen it)
  • MYNAME β€” the name of the local host. it's only used if required by macros inside the SPF record.
  • OPT β€” used for additional arguments. currently:
    • default_spf can be used to set a default SPF record in case no SPF/TXT records are returned from DNS (useful values are for example 'mx ?all' or 'mx/24 ?all').
    • rfc4408 can be set to true in case stricter compatibility is needed with RFC 4408 instead of RFC 7208, i.e. lookup of DNS SPF records, no limit on void DNS lookups etc.
    • pass_all can be set to the expected outcome in case a SPF policy gets found, which would pass everything. such policies are common used domains used by spammers. 🚩

returns the new object. πŸ’–


next([ ANSWER ])

"next" will be initially called with no arguments to get initial DNS queries, and then will be called with the DNS answers. it's giving "round trip" but make it DNS. ✈️

ANSWER is either:

  • a DNS packet with the response to a former query, or
  • [ QUERY, REASON ] on failures, where QUERY is the DNS packet containing the failed query and REASON the reason why the query failed (like TIMEOUT). 😡

if a final result was achieved it will return ( RESULT, COMMENT, HASH, EXPLAIN ):

  • RESULT is the result, e.g. "Fail", "Pass",….
  • COMMENT is the comment for the Received-SPF header.
  • HASH contains information about problem, mechanism for the Received-SPF header.
  • EXPLAIN will be set to the explain string if RESULT is Fail.

the following fields are in HASH:

  • client-ip β€” the clients IP address
  • helo β€” the helo string from the client
  • identity β€” how the identity of the sender was given, i.e. either "mailfrom" or "helo".
  • envelope-from β€” the sender, either based on the mail from in the SMTP dialog (with "identity" being "mailfrom") or the HELO/EHLO.

if no final result was achieved yet it will either return:

  • (undef,@QUERIES) with a list of new queries to continue,
  • ('') in case the ANSWER produced an error but got ignored, because there are other queries open, or
  • () in case the ANSWER was ignored because it did not match any open queries.

three different flavors of "not yet", pick your fighter. πŸ’…


mailheader

creates value for Received-SPF header based on the final answer from next(). returns header as string (one line, no folding) or undef, if no final result was found. this creates only the value, not the 'Received-SPF' prefix. so don't forget to glue that on yourself. πŸŽ€


result

returns ( RESULT, COMMENT, HASH, EXPLAIN ) like the final "next" does, or () if the final result wasn't found yet.

heads up bestie: if the SPF record had an explain modifier, which needed DNS lookups to resolve, this method might return the result (although with incomplete explain) before "next" does it. so like… read the room before you trust it fully. πŸ‘€

explain_default ( [ EXPLAIN ] )

sets default explanation string if EXPLAIN is given. if it's called as a class method the default explanation string for the class will be set, otherwise the default explanation string for the object. so it's giving "depends who's asking". 🫢

returns the current default explanation string for the object, or β€” if non given, or if called as a class method β€” the default explanation string for the class.


lookup_blocking ( [ TIMEOUT, RESOLVER ] )

quick way to get the SPF status. this will simply call "next" until it gets a final result. the "i don't feel like managing state today" option. 😌

  • TIMEOUT limits the lookup time and defaults to 20.
  • RESOLVER is a Net::DNS::Resolver object (or similar) and defaults to Net::DNS::Resolver->new.

returns ( RESULT, COMMENT, HASH ) like the final "next" does.

warning bestie, red flag: this is not the preferred way to use this module, because it's blocking, so no lookups can be done in parallel in a single process/thread. it works, but it's giving "i chose violence against my own throughput". πŸ’€

πŸ¦‹ EXPORTED SYMBOLS πŸ¦‹

for convenience the constants SPF_TempError, SPF_PermError, SPF_Pass, SPF_Fail, SPF_SoftFail, SPF_Neutral, SPF_None are by default exported, which have the values "TempError", "PermError" … you get the pattern, slay. ✨


🌈 Arguments to use/import 🌈

the "SPF_*" symbols are available for import and are exported if no arguments are given to "use" or "import". same effect with adding ":DEFAULT" as an argument. additionally the following arguments are supported:

  • DebugFunc => \&coderef β€” sets a custom debug function, which just takes on argument. if given it will be called on all debug messages when debugging is active. this function takes as the only argument the debug message.
  • Debug => 1|0 β€” switches debugging on/off. πŸ›

πŸ‘‘ AUTHOR πŸ‘‘

Steffen Ullrich <sullr@cpan.org>


copyright by Steffen Ullrich.

this module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.


and that's the whole page bestie, you're officially educated πŸ’…βœ¨ this beauty is running under perl v5.42.0, dated 2025-07-25, and it lives at Mail::S...Iterator(3). go forth and resolve iteratively, you absolute icon πŸ«ΆπŸŽ€

πŸ“¦ where this page comes from