RFC 4408

Sender Policy Framework (SPF) for Authorizing Use of Domains in E-Mail, Version 1

๐Ÿ“… published April 2006, from ietf

RFC 4408 ๐ŸŽ€ Sender Policy Framework (SPF) for Authorizing Use of Domains in E-Mail, Version 1 (Part 1 of 9)

okay bestie, here's the title block tea โ˜• This is RFC 4408, from the Network Working Group, and it's written by M. Wong and W. Schlitt. It's in the Experimental category, dated April 2006. It doesn't list anything it obsoletes or updates, so no drama there ๐Ÿซถ

  • RFC number: 4408
  • Title: Sender Policy Framework (SPF) for Authorizing Use of Domains in E-Mail, Version 1
  • Authors: M. Wong, W. Schlitt
  • Category: Experimental
  • Date: April 2006
  • Obsoletes: none listed
  • Updates: none listed

Status of This Memo ๐Ÿ“œ

this memo defines an Experimental Protocol for the Internet community ๐Ÿฆ‹ it does not specify an Internet standard of any kind, so don't go calling it one lol. they're asking for discussion and suggestions for improvement, and distribution of the memo is unlimited โœจ


Copyright (C) The Internet Society (2006). that's the whole vibe ๐Ÿ’…

IESG Note ๐Ÿ“

ok this part is lowkey important, so read it ๐Ÿ‘€

the following documents, RFC 4405, RFC 4406, RFC 4407, and RFC 4408, were published at the same time as Experimental RFCs. there is no general technical consensus, and efforts to reconcile the two approaches have failed ๐Ÿ’€ so these documents did not get full IETF review. they're published "AS-IS" to document the different approaches as they were considered in the MARID working group ๐ŸŒธ

the IESG takes no position on which approach is better, and it warns that there are serious open issues with each approach, plus concerns about using them together ๐Ÿ™ˆ they think documenting the different approaches does less harm than not documenting them, which, honestly, same ๐Ÿซถ

now the spicy bit ๐ŸŒถ๏ธ the Sender ID experiment may use DNS records that were created for the current SPF experiment or earlier versions in this set of experiments. depending on what's in the record, Sender-ID heuristics might get applied incorrectly to a message. and depending on what actions the recipient ties to those heuristics, the message may not be delivered, or it may be discarded when it arrives ๐Ÿ˜ญ

so participants relying on Sender ID experiment DNS records are warned that they may lose valid messages in this situation. participants publishing SPF experiment DNS records should consider the advice in section 3.4 of RFC 4406, and they may wish to publish both v=spf1 and spf2.0 records to avoid the conflict ๐Ÿงš

โš ๏ธ Resent-* header fields: the Sender-ID experiment uses Resent-* header fields in a way that results in failure to receive legitimate email when interacting with standards-compliant systems. that includes automatic forwarders that comply with the standards by not adding Resent-* headers, and systems that comply with RFC 822 but haven't implemented RFC 2822 Resent-* semantics yet. it would be inappropriate to advance Sender-ID on the standards track without resolving this interoperability problem ๐Ÿ’€

finally, the community is invited to watch how the two approaches do during the two years after publication, so a community consensus can be reached later ๐ŸŒˆ

Abstract ๐Ÿฆ‹

e-mail on the Internet can be forged in a bunch of ways ๐Ÿ™ƒ the big problem is that existing protocols place no restriction on what a sending host can use as the reverse-path of a message, or the domain given on the SMTP HELO/EHLO commands. this document describes version 1 of the Sender Policy Framework (SPF) protocol. with it, a domain can explicitly authorize the hosts that are allowed to use its domain name, and a receiving host can check that authorization โœจ

Table of Contents ๐Ÿงš

   1. Introduction ....................................................4
      1.1. Protocol Status ............................................4
      1.2. Terminology ................................................5
   2. Operation .......................................................5
      2.1. The HELO Identity ..........................................5
      2.2. The MAIL FROM Identity .....................................5
      2.3. Publishing Authorization ...................................6
      2.4. Checking Authorization .....................................6
      2.5. Interpreting the Result ....................................7
           2.5.1. None ................................................8
           2.5.2. Neutral .............................................8
           2.5.3. Pass ................................................8
           2.5.4. Fail ................................................8
           2.5.5. SoftFail ............................................9
           2.5.6. TempError ...........................................9
           2.5.7. PermError ...........................................9
   3. SPF Records .....................................................9
      3.1. Publishing ................................................10
           3.1.1. DNS Resource Record Types ..........................10
           3.1.2. Multiple DNS Records ...............................11
           3.1.3. Multiple Strings in a Single DNS record ............11
           3.1.4. Record Size ........................................11
           3.1.5. Wildcard Records ...................................11

   4. The check_host() Function ......................................12
      4.1. Arguments .................................................12
      4.2. Results ...................................................13
      4.3. Initial Processing ........................................13
      4.4. Record Lookup .............................................13
      4.5. Selecting Records .........................................13
      4.6. Record Evaluation .........................................14
           4.6.1. Term Evaluation ....................................14
           4.6.2. Mechanisms .........................................15
           4.6.3. Modifiers ..........................................15
      4.7. Default Result ............................................16
      4.8. Domain Specification ......................................16
   5. Mechanism Definitions ..........................................16
      5.1. "all" .....................................................17
      5.2. "include" .................................................18
      5.3. "a" .......................................................19
      5.4. "mx" ......................................................20
      5.5. "ptr" .....................................................20
      5.6. "ip4" and "ip6" ...........................................21
      5.7. "exists" ..................................................22
   6. Modifier Definitions ...........................................22
      6.1. redirect: Redirected Query ................................23
      6.2. exp: Explanation ..........................................23
   7. The Received-SPF Header Field ..................................25
   8. Macros .........................................................27
      8.1. Macro Definitions .........................................27
      8.2. Expansion Examples ........................................30
   9. Implications ...................................................31
      9.1. Sending Domains ...........................................31
      9.2. Mailing Lists .............................................32
      9.3. Forwarding Services and Aliases ...........................32
      9.4. Mail Services .............................................34
      9.5. MTA Relays ................................................34
   10. Security Considerations .......................................35
      10.1. Processing Limits ........................................35
      10.2. SPF-Authorized E-Mail May Contain Other False
            Identities ...............................................37
      10.3. Spoofed DNS and IP Data ..................................37
      10.4. Cross-User Forgery .......................................37
      10.5. Untrusted Information Sources ............................38
      10.6. Privacy Exposure .........................................38
   11. Contributors and Acknowledgements .............................38
   12. IANA Considerations ...........................................39
      12.1. The SPF DNS Record Type ..................................39
      12.2. The Received-SPF Mail Header Field .......................39
   13. References ....................................................39
      13.1. Normative References .....................................39
      13.2. Informative References ...................................40

   Appendix A.  Collected ABNF .......................................42
   Appendix B.  Extended Examples ....................................44
      B.1.  Simple Examples ..........................................44
      B.2.  Multiple Domain Example ..................................45
      B.3.  DNSBL Style Example ......................................46
      B.4.  Multiple Requirements Example ............................46

1. Introduction โœจ

ok so the current E-Mail infrastructure has this property: any host injecting mail into the mail system can claim to be any domain name it wants ๐Ÿ˜ญ hosts can do this at a bunch of levels, mainly the session, the envelope, and the mail headers. this is sometimes desirable, but it's a major obstacle to reducing Unsolicited Bulk E-Mail (UBE, aka spam). and honestly? lots of domain name holders are understandably worried about how easily other entities can use their domain names, often with malicious intent ๐Ÿ™…โ€โ™€๏ธ

so this document defines a protocol where domain owners can authorize hosts to use their domain name in the "MAIL FROM" or "HELO" identity ๐Ÿ’– compliant domain holders publish Sender Policy Framework (SPF) records that say which hosts are allowed to use their names. compliant mail receivers use those published records to test whether a sending Mail Transfer Agent (MTA) is authorized to use a given "HELO" or "MAIL FROM" identity during a mail transaction ๐ŸŽ€

and here's the bonus for receivers ๐Ÿซถ once an identity has been verified, local policy decisions about the mail can be made based on the sender's domain, not the host's IP address. that's a big deal because domain name reputation is likely to be more accurate than IP address reputation ๐Ÿ’… and if a claimed identity fails verification, local policy can take stronger action against that email, like rejecting it ๐Ÿ”ฅ

1.1. Protocol Status ๐Ÿ’…

so SPF has been in development since summer 2003 and has been deployed beyond the developers since December 2003 ๐ŸŒธ its design slowly evolved until spring 2004, and then it stabilized. there have been quite a few forms of SPF, some written up as documents, some submitted as Internet Drafts, and loads discussed in development forums ๐Ÿงš

the goal of this document is to clearly document the protocol defined by earlier draft specifications of SPF, as used in existing implementations. this version of SPF is sometimes called "SPF Classic" ๐Ÿฆ‹ it's understood that particular implementations and deployments may differ from this work and build on it. it's hoped that we still captured the common understanding of SPF version 1 ๐ŸŒˆ

1.2. Terminology ๐Ÿ“–โœจ

okay so the key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document get read the way [RFC2119] describes, so these ten words are the ones with real weight ๐Ÿ’…๐ŸŽ€

This doc is about the bit of a mail message that people call the "envelope sender", "return path", "reverse path", "bounce address", "2821 FROM", or "MAIL FROM" ๐Ÿ“ฌ Those names are either not well defined or get tossed around super casually, so this doc defines the "MAIL FROM" identity properly in Section 2.2. Other terms that look like the common ones, like "reverse-path", only mean what the normative documents say they mean. No mixing it up, bestie ๐Ÿงš


2. Operation ๐Ÿฆ‹

2.1. The HELO Identity ๐Ÿ‘‹

the "HELO" identity comes from either the SMTP HELO or EHLO command (see [RFC2821]) ๐Ÿ’Œ Those commands tell you who the SMTP client is, aka the sending host for the session. Here's the tea: the rules for what goes in the EHLO or HELO command aren't always clear to the sending party, so SPF clients need to be ready for the "HELO" identity to be malformed or an IP address literal ๐Ÿ™ˆ At the time of this writing, lots of legit E-Mails get delivered with invalid HELO domains, which is so messy but also so real ๐Ÿ˜ญ

It is RECOMMENDED that SPF clients not only check the "MAIL FROM" identity, but also check the "HELO" identity separately by running the check_host() function (Section 4) on the "HELO" identity as the <sender> โœจ

2.2. The MAIL FROM Identity ๐Ÿ“ฎ

the "MAIL FROM" identity comes from the SMTP MAIL command (see [RFC2821]) ๐Ÿ’• That command gives you the "reverse-path" for a message, which is usually the sender mailbox. It's also where notification messages get sent if the message runs into delivery problems ๐Ÿšจ

[RFC2821] lets the reverse-path be null (see Section 4.5.5 in RFC 2821) ๐ŸŒ™ When it's null, there's no explicit sender mailbox, so the message can be assumed to be a notification from the mail system itself, which is giving "automated and proud of it" ๐Ÿค– In that case, this doc defines the "MAIL FROM" identity as the mailbox made up of the localpart "postmaster" and the "HELO" identity (which may or may not have been checked separately before) ๐Ÿ’œ

SPF clients MUST check the "MAIL FROM" identity. They check it by running the check_host() function on the "MAIL FROM" identity as the <sender> ๐Ÿซถ

2.3. Publishing Authorization ๐Ÿ“ฃ

An SPF-compliant domain MUST publish a valid SPF record as described in Section 3. Main character energy for the domain, honestly ๐Ÿ‘‘ That record authorizes the use of the domain name in the "HELO" and "MAIL FROM" identities by the MTAs it names ๐ŸŽ€

If domain owners choose to publish SPF records, it is RECOMMENDED that they end in "-all", or redirect to other records that do, so that a definitive determination of authorization can be made ๐Ÿ”ฅ

Domain holders can publish SPF records that explicitly authorize no hosts if mail should never come from that domain ๐Ÿ™…โ€โ™€๏ธ Very dramatic, very valid ๐Ÿ’€

When you change SPF records, be careful to leave a transition period, so the old policy stays valid until all the legitimate E-Mail has been checked ๐Ÿฆ‹ Don't yeet the old record too early, bestie ๐Ÿ˜ญ

2.4. Checking Authorization ๐Ÿ”

A mail receiver can run a set of SPF checks on every mail message it gets ๐Ÿ“จ An SPF check tests whether a client host is authorized to send mail with a given identity. Usually a receiving MTA does these checks, but they can happen somewhere else in the mail processing chain, as long as the info needed is available and reliable ๐ŸŒธ At minimum the "MAIL FROM" identity MUST be checked, and it is RECOMMENDED that the "HELO" identity also gets checked beforehand ๐Ÿ’…

Without explicit approval from the domain owner, checking other identities against SPF version 1 records is NOT RECOMMENDED, because there are known cases that give incorrect results ๐Ÿ™ƒ For example, almost all mailing lists rewrite the "MAIL FROM" identity (see Section 9.2), but some don't change any other identities in the message. The scenario in Section 9.3, sub-section 1.2, is another example ๐Ÿง Documents that define other identities should define the method for that explicit approval โœจ

Mail receivers might use the SPF check as part of a bigger set of tests on incoming mail ๐Ÿ’– The results of other tests can affect whether a particular SPF check gets done at all. For example, if the sending host's IP address is on a local white list, that might skip all the other tests and let every bit of mail from that host through ๐ŸŒˆ

When a mail receiver decides to do an SPF check, it MUST use a correctly-implemented check_host() function (Section 4) evaluated with the correct parameters. Even though the test as a whole is optional, once someone's decided to run it, it has to be run as specified so the correct semantics are kept between publisher and receiver ๐Ÿซถ No half-measures, bestie ๐Ÿ˜ค

To make the test, the mail receiver MUST evaluate the check_host() function with the arguments set like this:

  • <ip> - the IP address of the SMTP client that is emitting the mail, either IPv4 or IPv6 ๐ŸŒ
  • <domain> - the domain portion of the "MAIL FROM" or "HELO" identity ๐Ÿ“
  • <sender> - the "MAIL FROM" or "HELO" identity ๐Ÿ’œ

Heads up: the <domain> argument might not be a well-formed domain name ๐Ÿ‘€ For example, if the reverse-path was null, then the EHLO/HELO domain gets used, along with its associated problems (see Section 2.1). In those cases, check_host() is defined in Section 4.3 to return a "None" result ๐Ÿฆ‹

Invalid, malformed, or non-existent domains make SPF checks return "None", because no SPF record can be found ๐Ÿซ  Even so, it's long been the policy of many MTAs to reject E-Mail from those domains, especially when the "MAIL FROM" is invalid ๐Ÿ™…โ€โ™€๏ธ To stop people getting around SPF records, rejecting E-Mail from invalid domains should be considered โœจ

Implementations must take care to correctly pull the <domain> out of the data given with the SMTP MAIL FROM command, because lots of MTAs will still accept things like source routes (see [RFC2821], Appendix C), the %-hack (see [RFC1123]), and bang paths (see [RFC1983]) ๐Ÿ™ˆ These archaic features have been used maliciously to get around security systems, so this is a serious one, bestie ๐Ÿ˜ญ๐Ÿ”ฅ

2.5. Interpreting the Result ๐Ÿ’…

This section explains how software that does the authorization should read the results of the check_host() function ๐Ÿ“– The authorization check SHOULD be done while the SMTP transaction that sends the mail is being processed. That way, errors can be sent straight back to the sending MTA through SMTP replies ๐Ÿ’Œ

Doing the authorization after the SMTP transaction is finished can cause problems, like: (1) it can be hard to pull the required info accurately out of potentially deceptive headers; (2) legit E-Mail can fail because the sender's policy may have changed since then ๐Ÿ™ƒ

Generating non-delivery notifications to forged identities that failed the authorization check is generally abusive, and it goes against the explicit wishes of the identity owner ๐Ÿšซ Not it, forgers ๐Ÿ’…

2.5.1. None ๐ŸŒซ๏ธ

A "None" result means that either no records were published by the domain, or no checkable sender domain could be figured out from the given identity. The checking software can't tell whether or not the client host is authorized ๐Ÿคทโ€โ™€๏ธ

2.5.2. Neutral ๐Ÿ˜

The domain owner has explicitly said they cannot or do not want to state whether or not the IP address is authorized ๐Ÿ™Š A "Neutral" result MUST be treated exactly like the "None" result. The only difference is for informational purposes. Treating "Neutral" more harshly than "None" would discourage domain owners from testing the use of SPF records (see Section 9.1), and nobody wants that ๐Ÿงช

2.5.3. Pass ๐ŸŽ‰

A "Pass" result means the client is authorized to inject mail with the given identity ๐Ÿ’– In the sense of reputation, the domain can now be considered responsible for sending the message. Further policy checks can go ahead with confidence in the legit use of the identity ๐Ÿ‘‘

2.5.4. Fail โŒ

A "Fail" result is an explicit statement that the client is NOT authorized to use the domain in the given identity ๐Ÿšซ Hard no, bestie. The checking software can choose to mark the mail based on this, or reject it outright ๐Ÿ’…

If the checking software chooses to reject the mail during the SMTP transaction, it SHOULD use an SMTP reply code of 550 (see [RFC2821]) and, if it's supported, the 5.7.1 Delivery Status Notification (DSN) code (see [RFC3464]), along with an appropriate reply text โœจ The check_host() function can return either a default explanation string, or one from the domain that published the SPF records (see Section 6.2). If the info didn't come from the checking software, it should be made clear that the text was provided by the sender's domain ๐Ÿซถ For example:

550-5.7.1 SPF MAIL FROM check failed:
550-5.7.1 The domain example.com explains:
550 5.7.1 Please see http://www.example.com/mailpolicy.html

2.5.5. SoftFail ๐ŸŒท

A "SoftFail" result should be treated as somewhere between a "Fail" and a "Neutral" ๐Ÿฅน The domain believes the host is not authorized, but it isn't willing to make that strong of a statement. Receiving software SHOULD NOT reject the message based solely on this result, but it MAY subject the message to closer scrutiny than normal ๐Ÿ”

The domain owner wants to discourage use of this host, so they want limited feedback when a "SoftFail" result happens ๐Ÿคซ For example, the recipient's Mail User Agent (MUA) could highlight the "SoftFail" status ๐ŸŒˆ Or the receiving MTA could use a technique called "greylisting", where it issues an SMTP reply code of 451 (4.3.0 DSN code) with a note the first time the message comes in, but accepts it the second time ๐Ÿงš

2.5.6. TempError โณ

A "TempError" result means the SPF client ran into a transient error while doing the check โ˜๏ธ The checking software can choose to accept the message or temporarily reject it. If the message gets rejected during the SMTP transaction for this reason, the software SHOULD use an SMTP reply code of 451 and, if it's supported, the 4.4.3 DSN code ๐Ÿฆ‹

2.5.7. PermError ๐Ÿšง

A "PermError" result means the domain's published records couldn't be interpreted correctly. This is an error condition that needs manual intervention to be resolved, unlike the TempError result ๐Ÿ› ๏ธ Be aware that if the domain owner uses macros (Section 8), this result might be because the checked identities have an unexpected format ๐Ÿ‘€


3. SPF Records ๐Ÿ“œ

An SPF record is a DNS Resource Record (RR) that declares which hosts are, and are not, authorized to use a domain name for the "HELO" and "MAIL FROM" identities ๐ŸŒ Loosely, the record splits all hosts into permitted and not-permitted sets, though some hosts might land in neither category ๐Ÿซง

The SPF record is a single string of text ๐Ÿ’Œ An example record is the following:

v=spf1 +mx a:colo.example.com/28 -all

This record has a version of "spf1" and three directives: "+mx", "a:colo.example.com/28" (the + is implied), and "-all" ๐Ÿ’– Version, directives, done, bestie ๐ŸŽ€

3.1. Publishing ๐Ÿ“ฎ

okay bestie, so domain owners who want to be SPF compliant HAVE to publish SPF records for the hosts used in the "MAIL FROM" and "HELO" identities ๐Ÿ’… The records go at the host name they're about, NOT at a subdomain under it (unlike SRV records, which do the subdomain thing ๐Ÿ™ƒ). And it's the same whether you use the TXT or the SPF RR type (more on that in Section 3.1.1) โœจ

The example from Section 3 could get published with these lines in a domain zone file ๐ŸŒธ

example.com.          TXT "v=spf1 +mx a:colo.example.com/28 -all"
smtp-out.example.com. TXT "v=spf1 a -all"
โš ๏ธ If you publish with TXT records, watch out for OTHER TXT records already sitting there for other reasons ๐Ÿ˜ญ They can cause size-limit drama (see Section 3.1.4).

3.1.1. DNS Resource Record Types ๐ŸŽ€

This doc makes a brand-new DNS RR type called SPF, code 99 ๐Ÿฆ‹ It has the exact same format as the TXT RR [RFC1035], and for either type, the record's characters are encoded as [US-ASCII] ๐Ÿ’œ

Real talk: using a TXT record is NOT the ideal situation ๐Ÿ™ˆ But it's necessary, because a bunch of DNS server and resolver implementations in common use can't handle the new RR type yet. The two-type setup is basically a forward path toward the better solution, which is using an RR type that's reserved just for this purpose ๐Ÿงš

An SPF-compliant domain name SHOULD have SPF records of BOTH RR types ๐Ÿ’– A compliant domain name MUST have a record of at least one type. And if a domain has both types, they MUST have identical content ๐Ÿซถ So instead of publishing just one record like in Section 3.1, it's better to do this:

example.com. IN TXT "v=spf1 +mx a:colo.example.com/28 -all"
example.com. IN SPF "v=spf1 +mx a:colo.example.com/28 -all"

The example RRs in this doc are shown with the TXT type, but they could be published with the SPF type or with both types โœจ


3.1.2. Multiple DNS Records ๐Ÿšซ

A domain name MUST NOT have multiple records that would make an authorization check select more than one record ๐Ÿ˜ญ Check Section 4.5 for the selection rules.


3.1.3. Multiple Strings in a Single DNS record ๐Ÿงฉ

As defined in [RFC1035] sections 3.3.14 and 3.3, a single text DNS record (either TXT or SPF RR types) can be made up of more than one string ๐Ÿ’… If a published record has multiple strings, the record MUST be treated as if those strings are glued together with NO spaces added between them ๐Ÿ”ฅ For example:

IN TXT "v=spf1 .... first" "second string..."

MUST be treated as equivalent to

IN TXT "v=spf1 .... firstsecond string..."

Multi-string SPF or TXT records are super useful for building records that would otherwise go over the 255-byte max length of one string inside a single TXT or SPF RR ๐Ÿ‘‘


3.1.4. Record Size ๐Ÿ“

The published SPF record for a given domain name SHOULD stay small enough that the query result fits within 512 octets ๐ŸŒˆ That keeps even older DNS implementations from falling over to TCP ๐Ÿ˜ฉ The answer size depends on a lot of stuff outside this doc's scope, so all we can give you is this guideline: if the combined length of the DNS name and the text of ALL the records of a given type (TXT or SPF) is under 450 characters, the DNS answers should fit in UDP packets ๐Ÿฆ‹ Also, when you're computing sizes for TXT-format queries, remember to count any OTHER TXT records published at that domain name ๐Ÿ‘€ Records that are too long to fit in a single UDP packet MAY be silently ignored by SPF clients ๐Ÿ™ˆ


3.1.5. Wildcard Records ๐ŸŒŸ

Using wildcard records for publishing is not recommended, and you gotta be careful if you do ๐Ÿ’€ If a domain publishes wildcard MX records, it may want to publish wildcard declarations too, subject to the same requirements and problems. The big one: the declaration must be repeated for any host that has ANY RR records at all, and for subdomains of that host. For example, the example in [RFC1034], Section 4.3.3, could be extended with the following:

    X.COM.          MX      10      A.X.COM
    X.COM.          TXT     "v=spf1 a:A.X.COM -all"

    *.X.COM.        MX      10      A.X.COM
    *.X.COM.        TXT     "v=spf1 a:A.X.COM -all"

    A.X.COM.        A       1.2.3.4
    A.X.COM.        MX      10      A.X.COM
    A.X.COM.        TXT     "v=spf1 a:A.X.COM -all"

    *.A.X.COM.      MX      10      A.X.COM
    *.A.X.COM.      TXT     "v=spf1 a:A.X.COM -all"

Notice that SPF records have to be repeated TWICE for every name in the domain: once for the name itself, and once with a wildcard to cover the tree under that name ๐Ÿ˜ญ๐Ÿซถ

Wildcards are generally discouraged, bestie, because they make every name under the domain exist, so queries for random names will never return RCODE 3 (Name Error) ๐Ÿฅฒ


4. The check_host() Function โœจ

The check_host() function fetches SPF records, parses them, and interprets them to figure out whether a particular host is or isn't permitted to send mail with a given identity ๐Ÿ•ต๏ธโ€โ™€๏ธ Mail receivers that do this check MUST correctly evaluate the check_host() function as described here ๐Ÿ’–

Implementations MAY use a different algorithm than the canonical one defined here, as long as the results are the same in all cases ๐Ÿฆ‹


4.1. Arguments ๐Ÿงพ

The check_host() function takes these arguments:

<ip>     - the IP address of the SMTP client that is emitting the
           mail, either IPv4 or IPv6.

<domain> - the domain that provides the sought-after authorization
           information; initially, the domain portion of the "MAIL
           FROM" or "HELO" identity.

<sender> - the "MAIL FROM" or "HELO" identity.

The domain portion of <sender> will usually be the same as the <domain> argument when check_host() is first evaluated. However, this will generally NOT be true for recursive evaluations (see Section 5.2 below) ๐Ÿ‘€

Actual implementations of check_host() may need additional arguments ๐ŸŒธ


4.2. Results ๐ŸŽฏ

The check_host() function can return one of several results, described in Section 2.5. Based on the result, the receiver's local policies decide what action gets taken ๐Ÿ’…


4.3. Initial Processing ๐Ÿšฆ

If the <domain> is malformed (label longer than 63 characters, zero-length label not at the end, etc.) or is not a fully qualified domain name, OR if the DNS lookup returns "domain does not exist" (RCODE 3), check_host() immediately returns the result "None" ๐Ÿ™…โ€โ™€๏ธ

If the <sender> has no localpart, substitute the string "postmaster" for the localpart ๐Ÿ“ฌ


4.4. Record Lookup ๐Ÿ”

Based on how the records are published (see Section 3.1 above), a DNS query needs to be made for the <domain> name, asking for RR type TXT, SPF, or both ๐Ÿงš If both SPF and TXT RRs are looked up, the queries MAY be done in parallel โšก

If ALL the DNS lookups that get made return a server failure (RCODE 2), or some other error (RCODE other than 0 or 3), or they time out, then check_host() exits immediately with the result "TempError" ๐Ÿ˜ฉ


4.5. Selecting Records ๐Ÿงน

Records begin with a version section:

record           = version terms *SP
version          = "v=spf1"

Starting with the set of records returned by the lookup, record selection goes through two steps ๐Ÿซง

  1. Records that do NOT begin with a version section of exactly "v=spf1" get discarded. Note that the version section is terminated either by an SP character or the end of the record. A record with a version section of "v=spf10" does not match and must be discarded ๐Ÿ™ƒ
  2. If any records of type SPF are in the set, then ALL records of type TXT get discarded ๐Ÿ’œ

After those steps, there should be exactly one record left, and then evaluation can proceed โœจ If there are two or more records left, check_host() exits immediately with the result of "PermError" ๐Ÿ’€

If no matching records come back, an SPF client MUST assume that the domain makes no SPF declarations. SPF processing MUST stop and return "None" ๐Ÿซถ


4.6. Record Evaluation ๐Ÿ”ฌ

After one SPF record has been selected, check_host() parses and interprets it to find a result for the current test ๐Ÿ’… If there are any syntax errors, check_host() returns immediately with the result "PermError" ๐Ÿ˜ญ

Implementations MAY choose to parse the entire record first and return "PermError" if the record isn't syntactically well formed ๐ŸŒˆ However, in ALL cases, any syntax errors anywhere in the record MUST be detected ๐Ÿ”ฅ


4.6.1. Term Evaluation ๐Ÿง

There are two types of terms: mechanisms and modifiers ๐Ÿฆ‹ A record has an ordered list of these, written in the following Augmented Backus-Naur Form (ABNF) ๐Ÿ“œ

terms            = *( 1*SP ( directive / modifier ) )

directive        = [ qualifier ] mechanism
qualifier        = "+" / "-" / "?" / "~"
mechanism        = ( all / include
                   / A / MX / PTR / IP4 / IP6 / exists )
modifier         = redirect / explanation / unknown-modifier
unknown-modifier = name "=" macro-string

name             = ALPHA *( ALPHA / DIGIT / "-" / "_" / "." )

Most mechanisms allow a ":" or "/" character after the name ๐ŸŽ€

Modifiers ALWAYS have an equals ('=') character immediately after the name, and before any ":" or "/" characters that may be part of the macro-string ๐Ÿ’œ

Terms that don't contain any of "=", ":", or "/" are mechanisms, as defined in Section 5 โœจ

As per the definition of the ABNF notation in [RFC4234], mechanism and modifier names are case-insensitive ๐Ÿ™Œ


4.6.2. Mechanisms ๐Ÿ› ๏ธ

Each mechanism gets considered in turn, from left to right. If there are no more mechanisms, the result is specified in Section 4.7 ๐ŸŒธ

When a mechanism is evaluated, one of three things can happen: it can match, not match, or throw an exception ๐ŸŽฒ

If it matches, processing ends and the qualifier value is returned as the result of that record. If it doesn't match, processing continues with the next mechanism. If it throws an exception, mechanism processing ends and the exception value is returned ๐Ÿ’ฅ

The possible qualifiers and the results they return are:

"+" Pass
"-" Fail
"~" SoftFail
"?" Neutral

The qualifier is optional and defaults to "+" ๐Ÿซถ

When a mechanism matches and the qualifier is "-", a "Fail" result is returned, and the explanation string is computed as described in Section 6.2 ๐Ÿ™ˆ

The specific mechanisms are described in Section 5 ๐Ÿ“–


4.6.3. Modifiers ๐Ÿงƒ

Modifiers aren't mechanisms: they don't return match or not-match. Instead, they provide additional information ๐Ÿ’… Even though modifiers don't directly affect how the record is evaluated, the "redirect" modifier does have an effect after ALL the mechanisms have been evaluated ๐Ÿ‘‘


4.7. Default Result ๐Ÿ

If none of the mechanisms match and there's no "redirect" modifier, check_host() returns a result of "Neutral", just as if "?all" were specified as the last directive ๐Ÿซง If there IS a "redirect" modifier, check_host() proceeds as defined in Section 6.1 โœจ

Note that records SHOULD always use either a "redirect" modifier or an "all" mechanism to explicitly terminate processing ๐Ÿ›‘

For example:

v=spf1 +mx -all

or

v=spf1 +mx redirect=_spf.example.com

4.8. Domain Specification ๐Ÿงš

Okay bestie, so several of these mechanisms and modifiers have a <domain-spec> section. That <domain-spec> string gets macro expanded (go check Section 8 ๐Ÿ’…). The result is the common presentation form of a fully-qualified DNS name, which is a series of labels separated by periods. For the rest of the doc, we call this domain the <target-name> ๐ŸŽ€.

Note: The result of the macro expansion is not subject to any further escaping. Hence, this facility cannot produce all characters that are legal in a DNS label (e.g., the control characters). However, this facility is powerful enough to express legal host names and common utility labels (such as "_spf") that are used in DNS. ๐Ÿ™ˆ

For several mechanisms the <domain-spec> is optional. If it's not provided, the <domain> gets used as the <target-name> โœจ.

5. Mechanism Definitions ๐ŸŽ€

This section defines two types of mechanisms, and honestly the difference is so cute ๐Ÿฆ‹.

Basic mechanisms contribute to the language framework. They do not specify a particular type of authorization scheme. ๐ŸŒธ

   all
   include

Designated sender mechanisms are used to designate a set of <ip> addresses as being permitted or not permitted to use the <domain> for sending mail ๐Ÿ’Œ.

   a
   mx
   ptr
   ip4
   ip6
   exists

The following conventions apply to all mechanisms that perform a comparison between <ip> and an IP address at any point ๐Ÿ’–:

If no CIDR-length is given in the directive, then <ip> and the IP address are compared for equality. (Here, CIDR is Classless Inter-Domain Routing ๐Ÿง .)

If a CIDR-length is specified, then only the specified number of high-order bits of <ip> and the IP address are compared for equality ๐Ÿ”ฅ.

When any mechanism fetches host addresses to compare with <ip>, A records are fetched when <ip> is an IPv4 address, and AAAA records are fetched when <ip> is an IPv6 address. Even if the SMTP connection is via IPv6, an IPv4-mapped IPv6 IP address (see [RFC3513], Section 2.5.5) MUST still be considered an IPv4 address. Main character energy, no notes ๐Ÿ‘‘.

Several mechanisms rely on information fetched from DNS. For these DNS queries, except where noted, if the DNS server returns an error (RCODE other than 0 or 3) or the query times out, the mechanism throws the exception "TempError" ๐Ÿ˜ญ. If the server returns "domain does not exist" (RCODE 3), then evaluation of the mechanism continues as if the server returned no error (RCODE 0) and zero answer records. Lowkey the most chill outcome ever ๐Ÿซถ.

5.1. "all"

   all              = "all"

The "all" mechanism is a test that always matches ๐Ÿ’ฏ. It's used as the rightmost mechanism in a record to provide an explicit default.

For example:

   v=spf1 a mx -all

Mechanisms after "all" will never be tested ๐Ÿšซ. Any "redirect" modifier (Section 6.1) has no effect when there is an "all" mechanism. Iconic behaviour, no cap ๐Ÿ’….

5.2. "include"

   include          = "include"  ":" domain-spec

The "include" mechanism triggers a recursive evaluation of check_host() ๐Ÿ”. The domain-spec is expanded as per Section 8. Then check_host() is evaluated with the resulting string as the <domain>. The <ip> and <sender> arguments remain the same as in the current evaluation of check_host() ๐ŸŒˆ.

In hindsight, the name "include" was poorly chosen ๐Ÿ™ˆ. Only the evaluated result of the referenced SPF record is used, rather than acting as if the referenced SPF record was literally included in the first. For example, evaluating a "-all" directive in the referenced record does not terminate the overall processing and does not necessarily result in an overall "Fail". (Better names for this mechanism would have been "if-pass", "on-pass", etc. ๐Ÿ’€)

The "include" mechanism makes it possible for one domain to designate multiple administratively-independent domains ๐Ÿงš. For example, a vanity domain "example.net" might send mail using the servers of administratively-independent domains example.com and example.org.

Example.net could say:

   IN TXT "v=spf1 include:example.com include:example.org -all"

This would direct check_host() to, in effect, check the records of example.com and example.org for a "Pass" result. Only if the host were not permitted for either of those domains would the result be "Fail" โœจ.

Whether this mechanism matches, does not match, or throws an exception depends on the result of the recursive evaluation of check_host() ๐Ÿ“Š:

   +---------------------------------+---------------------------------+
   | A recursive check_host() result | Causes the "include" mechanism  |
   | of:                             | to:                             |
   +---------------------------------+---------------------------------+
   | Pass                            | match                           |
   |                                 |                                 |
   | Fail                            | not match                       |
   |                                 |                                 |
   | SoftFail                        | not match                       |
   |                                 |                                 |
   | Neutral                         | not match                       |
   |                                 |                                 |
   | TempError                       | throw TempError                 |
   |                                 |                                 |
   | PermError                       | throw PermError                 |
   |                                 |                                 |
   | None                            | throw PermError                 |
   +---------------------------------+---------------------------------+

The "include" mechanism is intended for crossing administrative boundaries ๐ŸŒ‰. Although it is possible to use includes to consolidate multiple domains that share the same set of designated hosts, domains are encouraged to use redirects where possible, and to minimize the number of includes within a single administrative domain. For example, if example.com and example.org were managed by the same entity, and if the permitted set of hosts for both domains was "mx:example.com", it would be possible for example.org to specify "include:example.com", but it would be preferable to specify "redirect=example.com" or even "mx:example.com" ๐Ÿ“.

5.3. "a"

This mechanism matches if <ip> is one of the <target-name>'s IP addresses ๐ŸŽฏ.

   A                = "a"      [ ":" domain-spec ] [ dual-cidr-length ]

An address lookup is done on the <target-name>. The <ip> is compared to the returned address(es). If any address matches, the mechanism matches ๐Ÿฅณ.

5.4. "mx"

This mechanism matches if <ip> is one of the MX hosts for a domain name ๐Ÿ“ฌ.

   MX               = "mx"     [ ":" domain-spec ] [ dual-cidr-length ]

check_host() first performs an MX lookup on the <target-name>. Then it performs an address lookup on each MX name returned. The <ip> is compared to each returned IP address. To prevent Denial of Service (DoS) attacks, more than 10 MX names MUST NOT be looked up during the evaluation of an "mx" mechanism (see Section 10) ๐Ÿ›ก๏ธ. If any address matches, the mechanism matches.

Note regarding implicit MXs: If the <target-name> has no MX records, check_host() MUST NOT pretend the target is its single MX, and MUST NOT default to an A lookup on the <target-name> directly. This behavior breaks with the legacy "implicit MX" rule. See [RFC2821], Section 5. If such behavior is desired, the publisher should specify an "a" directive ๐Ÿ™…โ€โ™€๏ธ.

5.5. "ptr"

This mechanism tests whether the DNS reverse-mapping for <ip> exists and correctly points to a domain name within a particular domain ๐Ÿ”.

   PTR              = "ptr"    [ ":" domain-spec ]

First, the <ip>'s name is looked up using this procedure: perform a DNS reverse-mapping for <ip>, looking up the corresponding PTR record in "in-addr.arpa." if the address is an IPv4 one and in "ip6.arpa." if it is an IPv6 address. For each record returned, validate the domain name by looking up its IP address. To prevent DoS attacks, more than 10 PTR names MUST NOT be looked up during the evaluation of a "ptr" mechanism (see Section 10). If <ip> is among the returned IP addresses, then that domain name is validated. In pseudocode:

   sending-domain_names := ptr_lookup(sending-host_IP); if more than 10
   sending-domain_names are found, use at most 10.  for each name in
   (sending-domain_names) {
     IP_addresses := a_lookup(name);
     if the sending-domain_IP is one of the IP_addresses {
       validated-sending-domain_names += name;
     } }

Check all validated domain names to see if they end in the <target-name> domain. If any do, this mechanism matches. If no validated domain name can be found, or if none of the validated domain names end in the <target-name>, this mechanism fails to match. If a DNS error occurs while doing the PTR RR lookup, then this mechanism fails to match. If a DNS error occurs while doing an A RR lookup, then that domain name is skipped and the search continues ๐Ÿ™ƒ.

Pseudocode:

   for each name in (validated-sending-domain_names) {
     if name ends in <domain-spec>, return match.
     if name is <domain-spec>, return match.
   }
   return no-match.

This mechanism matches if the <target-name> is either an ancestor of a validated domain name or if the <target-name> and a validated domain name are the same. For example: "mail.example.com" is within the domain "example.com", but "mail.bad-example.com" is not ๐ŸŒท.

Note: Use of this mechanism is discouraged because it is slow, it is not as reliable as other mechanisms in cases of DNS errors, and it places a large burden on the arpa name servers. If used, proper PTR records must be in place for the domain's hosts and the "ptr" mechanism should be one of the last mechanisms checked. Ngl, this one is giving "use with caution" energy ๐Ÿ˜ญ๐Ÿ’€.

5.6. "ip4" and "ip6"

These mechanisms test whether <ip> is contained within a given IP network ๐ŸŒ.

   IP4              = "ip4"      ":" ip4-network   [ ip4-cidr-length ]
   IP6              = "ip6"      ":" ip6-network   [ ip6-cidr-length ]

   ip4-cidr-length  = "/" 1*DIGIT
   ip6-cidr-length  = "/" 1*DIGIT
   dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ]

   ip4-network      = qnum "." qnum "." qnum "." qnum
   qnum             = DIGIT                 ; 0-9
                      / %x31-39 DIGIT       ; 10-99
                      / "1" 2DIGIT          ; 100-199
                      / "2" %x30-34 DIGIT   ; 200-249
                      / "25" %x30-35        ; 250-255
            ; as per conventional dotted quad notation.  e.g., 192.0.2.0
   ip6-network      = <as per [RFC 3513], section 2.2>
            ; e.g., 2001:DB8::CD30

The <ip> is compared to the given network. If CIDR-length high-order bits match, the mechanism matches ๐ŸŽฏ.

If ip4-cidr-length is omitted, it is taken to be "/32". If ip6-cidr-length is omitted, it is taken to be "/128". It is not permitted to omit parts of the IP address instead of using CIDR notations. That is, use 192.0.2.0/24 instead of 192.0.2 ๐Ÿ’–. No shortcuts, bestie, the full address hits different โœจ.

5.7. "exists" โœจ

okay bestie, "exists" is literally so unhinged in the best way ๐Ÿ’… It builds a whole custom domain name out of pieces of the mail envelope and then does a DNS A record lookup on it. If anything comes back, the mechanism matches. It's giving full choose-your-own-adventure for who gets to send mail ๐Ÿฆ‹

exists = "exists" ":" domain-spec

The domain-spec gets expanded using Section 8 ๐ŸŒธ The result is the domain name used for the DNS A RR lookup. If any A record comes back, the mechanism matches. And here's the twist: the lookup type is A even when the connection is IPv6 ๐Ÿ‘€ no cap, that one's a plot twist.

Domains can use this to write super complicated queries ๐Ÿงš For example, say example.com publishes this record:

v=spf1 exists:%{ir}.%{l1r+-}._spf.%{d} -all

The <target-name> might expand to "1.2.0.192.someuser._spf.example.com" ๐Ÿ’– That means you can make really fine-grained decisions based on the user and the client IP address. Lowkey iconic โœจ

This mechanism also lets you copy the style of tests that existing anti-spam DNS blacklists (DNSBL) use ๐Ÿ”ฅ So if you've ever seen a DNSBL, this is giving familiar vibes with way more control.


6. Modifier Definitions ๐ŸŽ€

okay so modifiers are name/value pairs that add extra info ๐Ÿ“ They always have an "=" between the name and the value, no exceptions bestie ๐Ÿซถ

The modifiers defined in this document ("redirect" and "exp") MAY appear anywhere in the record, but they SHOULD appear at the end, after all the mechanisms ๐Ÿ’… Their order doesn't matter. And each of these two MUST NOT appear in a record more than once ๐Ÿ˜ญ If they do, check_host() exits with a result of "PermError". Not it ๐Ÿ’€

Unrecognized modifiers MUST be ignored no matter where they show up in a record, or how often ๐ŸŒˆ This is what lets implementations of this document gracefully handle records with modifiers that come from other specifications. Main character energy for future-proofing โœจ

6.1. redirect: Redirected Query ๐Ÿฆ‹

If all the mechanisms fail to match, and a "redirect" modifier is there, then processing goes like this:

redirect = "redirect" "=" domain-spec

The domain-spec in the redirect gets expanded using the macro rules in Section 8 ๐ŸŒธ Then check_host() gets evaluated again, using the resulting string as the <domain>. The <ip> and <sender> arguments stay the same as the current evaluation of check_host().

Whatever result that new check_host() evaluation gives is considered the result of the current evaluation ๐Ÿ’– The one exception: if no SPF record is found, or the target-name is malformed, the result is a "PermError" instead of "None". Plot twist, but a fair one ๐Ÿ‘‘

Note that the newly-queried domain may itself specify redirect processing. Redirects inside redirects, we are SO in it ๐ŸŒ€

This is meant for organizations that want to apply the same record to multiple domains ๐Ÿงš For example:

la.example.com. TXT "v=spf1 redirect=_spf.example.com" ny.example.com. TXT "v=spf1 redirect=_spf.example.com" sf.example.com. TXT "v=spf1 redirect=_spf.example.com" _spf.example.com. TXT "v=spf1 mx:example.com -all"

In this example, mail from any of the three domains is described by the same record. Administratively, that's a whole vibe ๐Ÿ“

Note: In general, the domain "A" cannot reliably use a redirect to another domain "B" that isn't under the same administrative control ๐Ÿ™ˆ Since the <sender> stays the same, there's no guarantee that the record at domain "B" will work correctly for mailboxes in domain "A", especially if domain "B" uses mechanisms involving localparts. An "include" directive may be more appropriate, bestie.

For clarity, it is RECOMMENDED that any "redirect" modifier appears as the very last term in a record ๐Ÿ’œ Clean and easy to read, periodt โœจ

6.2. exp: Explanation ๐Ÿ’ฌ

explanation = "exp" "=" domain-spec

If check_host() results in a "Fail" because of a mechanism match (like "-all"), and the "exp" modifier is present, then the explanation string gets computed the way described below ๐Ÿ”ฅ If there's no "exp" modifier, then either a default explanation string or an empty explanation string may be returned.

The <domain-spec> is macro expanded (see Section 8) and becomes the <target-name> ๐ŸŒธ Then the DNS TXT record for the <target-name> is fetched.

If the <domain-spec> is empty, or there are any DNS processing errors (any RCODE other than 0), or no records come back, or more than one record comes back, or there are syntax errors in the explanation string, then proceed as if no exp modifier was given ๐Ÿ˜ญ Not a crash, just a vibe reset.

The strings in the fetched TXT record are concatenated with no spaces, then treated as an <explain-string>, which gets macro-expanded ๐Ÿฆ‹ That final result is the explanation string. Implementations MAY limit the length of the resulting explanation string to allow for other protocol constraints and/or reasonable processing limits ๐ŸŽ€ And since the explanation string is meant for an SMTP response, and [RFC2821] Section 2.4 says responses are in [US-ASCII], the explanation string is also limited to US-ASCII. Keep it simple, bestie ๐Ÿ’…

Software evaluating check_host() can use this string to pass along info from the publishing domain, like a short message or a URL โœจ Software SHOULD make it clear that the explanation string comes from a third party ๐Ÿซถ For example, it can prepend the macro string "%{o} explains: " to the explanation, like what's shown in Section 2.5.4.

Say example.com has this record:

v=spf1 mx -all exp=explain._spf.%{d}

Here are some examples of possible explanation TXT records at explain._spf.example.com:

"Mail from example.com should only be sent by its own servers." -- a simple, constant message

"%{i} is not one of %{d}'s designated mail servers." -- a message with a little more information, including the IP address that failed the check

"See http://%{d}/why.html?s=%{S}&i=%{I}" -- a complicated example that constructs a URL with the arguments to check_host() so that a web page can be generated with detailed, custom instructions

Note: During recursion into an "include" mechanism, an exp= modifier from the <target-name> MUST NOT be used ๐Ÿ™…โ€โ™€๏ธ On the flip side, when executing a "redirect" modifier, an exp= modifier from the original domain MUST NOT be used. No mixing the explanations, bestie ๐Ÿ’–

7. The Received-SPF Header Field ๐Ÿ“ฌ

It is RECOMMENDED that SMTP receivers record the result of SPF processing in the message header ๐ŸŒˆ If an SMTP receiver chooses to do so, it SHOULD use the "Received-SPF" header field defined here for each identity that was checked. This info is meant for the recipient. (Info meant for the sender is in Section 6.2, Explanation.)

The Received-SPF header field is a trace field (see [RFC2822] Section 3.6.7) and it SHOULD be prepended to the existing header, above the Received: field that the SMTP receiver generates ๐Ÿฆ‹ It MUST appear above all other Received-SPF fields in the message. Here's the format:

header-field = "Received-SPF:" [CFWS] result FWS [comment FWS] [ key-value-list ] CRLF

result = "Pass" / "Fail" / "SoftFail" / "Neutral" / "None" / "TempError" / "PermError"

key-value-list = key-value-pair *( ";" [CFWS] key-value-pair ) [";"]

key-value-pair = key [CFWS] "=" ( dot-atom / quoted-string )

key = "client-ip" / "envelope-from" / "helo" / "problem" / "receiver" / "identity" / mechanism / "x-" name / name

identity = "mailfrom" ; for the "MAIL FROM" identity / "helo" ; for the "HELO" identity / name ; other identities

dot-atom = <unquoted word as per [RFC2822]> quoted-string = <quoted string as per [RFC2822]> comment = <comment string as per [RFC2822]> CFWS = <comment or folding white space as per [RFC2822]> FWS = <folding white space as per [RFC2822]> CRLF = <standard end-of-line token as per [RFC2822]>

The header field SHOULD include a "(...)" style <comment> after the result ๐Ÿ’œ It conveys supporting info for the result, like <ip>, <sender>, and <domain>.

The following key-value pairs are designed for later machine parsing ๐Ÿค– SPF clients SHOULD give enough info so the SPF results can be verified. That means at least "client-ip", "helo", and, if the "MAIL FROM" identity was checked, "envelope-from" ๐Ÿซถ

client-ip the IP address of the SMTP client

envelope-from the envelope sender mailbox

helo the host name given in the HELO or EHLO command

mechanism the mechanism that matched (if no mechanisms matched, substitute the word "default")

problem if an error was returned, details about the error

receiver the host name of the SPF client

identity the identity that was checked; see the <identity> ABNF rule

Other keys may be defined by SPF clients ๐ŸŒธ Until a new key name becomes widely accepted, new key names should start with "x-".

SPF clients MUST make sure that the Received-SPF header field doesn't contain invalid characters, isn't excessively long, and doesn't contain malicious data that was provided by the sender ๐Ÿ˜ญ This one's serious, bestie, no shade, just safety ๐Ÿ”ฅ

Here are some examples of the header styles that could get generated ๐Ÿ’…

Received-SPF: Pass (mybox.example.org: domain of myname@example.com designates 192.0.2.1 as permitted sender) receiver=mybox.example.org; client-ip=192.0.2.1; envelope-from=<myname@example.com>; helo=foo.example.com;

Received-SPF: Fail (mybox.example.org: domain of myname@example.com does not designate 192.0.2.1 as permitted sender) identity=mailfrom; client-ip=192.0.2.1; envelope-from=<myname@example.com>;


8. Macros โœจ

8.1. Macro Definitions โœจ

okay so macros are where the magic happens ๐Ÿ”ฎ Lots of mechanisms and modifiers do macro expansion on part of their term, and this is the part of the spec that explains exactly how it all works bestie ๐Ÿ’…

Here's the grammar, copy-pasted exactly so nobody can accuse me of altering it ๐Ÿซถ

domain-spec      = macro-string domain-end
domain-end       = ( "." toplabel [ "." ] ) / macro-expand

toplabel         = ( *alphanum ALPHA *alphanum ) /
                   ( 1*alphanum "-" *( alphanum / "-" ) alphanum )
                   ; LDH rule plus additional TLD restrictions
                   ; (see [RFC3696], Section 2)
alphanum         = ALPHA / DIGIT

explain-string   = *( macro-string / SP )

macro-string     = *( macro-expand / macro-literal )
macro-expand     = ( "%{" macro-letter transformers *delimiter "}" )
                   / "%%" / "%_" / "%-"
macro-literal    = %x21-24 / %x26-7E
                   ; visible characters except "%"
macro-letter     = "s" / "l" / "o" / "d" / "i" / "p" / "h" /
                   "c" / "r" / "t"
transformers     = *DIGIT [ "r" ]
delimiter        = "." / "-" / "+" / "," / "/" / "_" / "="

A literal "%" is written as %% ๐ŸŒธ

%_ expands to a single " " space.
%- expands to a URL-encoded space, viz., "%20".

These macro letters get expanded in term arguments ๐Ÿฆ‹

s = <sender>
l = local-part of <sender>
o = domain of <sender>
d = <domain>
i = <ip>
p = the validated domain name of <ip>
v = the string "in-addr" if <ip> is ipv4, or "ip6" if <ip> is ipv6
h = HELO/EHLO domain

These ones are only allowed in "exp" text, so don't try them anywhere else ๐Ÿ™ˆ

c = SMTP client IP (easily readable format)
r = domain name of host performing the check
t = current timestamp

Now the big one: a % that isn't followed by a {, %, -, or _ is a syntax error ๐Ÿ’€ So this is WRONG and will make check_host() return a "PermError" ๐Ÿ˜ญ

-exists:%(ir).sbl.spamhaus.example.org

Instead, do this ๐ŸŽ€

-exists:%{ir}.sbl.spamhaus.example.org

The optional transformers are:

*DIGIT = zero or more digits
'r'    = reverse value, splitting on dots by default

If you use transformers or delimiters, the replacement value for a macro letter gets split into parts ๐Ÿงฉ After any reversal and/or removal of left-hand parts, the parts get rejoined with "." (not the original splitting characters, so pay attention โœจ)

By default, strings split on "." (dots). Note that there's no special treatment for leading, trailing, or consecutive delimiters, so the list of parts might contain empty strings ๐Ÿซ  Older implementations of SPF prohibit trailing dots in domain names, so trailing dots shouldn't be published by domain owners, although they must be accepted by implementations conforming to this document. Macros can also specify delimiter characters that get used instead of "." ๐Ÿ“

The 'r' transformer means reversal ๐Ÿ”„ If the client IP address were 192.0.2.1, then %{i} expands to "192.0.2.1" and %{ir} expands to "1.2.0.192" ๐Ÿ˜ญ iconic

The DIGIT transformer tells you how many right-hand parts to use, after the optional reversal. If a DIGIT is specified, the value MUST be nonzero. If no DIGITs are specified, or if the value asks for more parts than are available, then all the available parts get used. If the DIGIT was 5 but only 3 parts were available, the macro interpreter pretends the DIGIT was 3 ๐Ÿ’… Implementations MUST support at least a value of 128, since that's the maximum number of labels in a domain name ๐ŸŒˆ

The "s" macro expands to the <sender> argument. It's an E-Mail address with a localpart, an "@" character, and a domain. The "l" macro expands to just the localpart, and the "o" macro expands to just the domain part ๐Ÿ‘‘ These values stay the same during recursive and chained evaluations because of "include" and/or "redirect". Also, if the original <sender> had no localpart, the localpart was set to "postmaster" during initial processing (see Section 4.3) ๐Ÿ“ฌ

For IPv4 addresses, both the "i" and "c" macros expand to the standard dotted-quad format ๐ŸŒธ

For IPv6 addresses, the "i" macro expands to a dot-format address, and it's meant for use in %{ir} ๐Ÿงš The "c" macro may expand to any of the hexadecimal colon-format addresses specified in [RFC3513], Section 2.2. It's meant for humans to read ๐Ÿ‘€

The "p" macro expands to the validated domain name of <ip>. The procedure for finding that validated domain name is defined in Section 5.5. If the <domain> is present in the list of validated domains, it SHOULD be used. Otherwise, if a subdomain of the <domain> is present, it SHOULD be used. Otherwise, any name from the list may be used. If there are no validated domain names, or if a DNS error occurs, the string "unknown" is used ๐Ÿซถ

The "r" macro expands to the name of the receiving MTA. This SHOULD be a fully qualified domain name. But if one doesn't exist (like when the checking is done by a MUA), or if policy restrictions dictate otherwise, the word "unknown" SHOULD be substituted ๐Ÿ™ƒ The domain name might be different from the name found in the MX record that the client MTA used to locate the receiving MTA โœจ

The "t" macro expands to the decimal representation of the approximate number of seconds since the Epoch (Midnight, January 1, 1970, UTC). It's the same value POSIX time() gives you in most standards-compliant libraries ๐Ÿ•ฐ๏ธ

When the result of macro expansion gets used in a domain name query, and the expanded domain name is longer than 253 characters (the maximum length of a domain name), the left side gets truncated to fit ๐Ÿ”ช You do this by removing successive domain labels until the total length doesn't exceed 253 characters.

Uppercased macros expand exactly like their lowercased equivalents, and then get URL escaped ๐Ÿ’– URL escaping must be performed for characters not in the "uric" set, which is defined in [RFC3986].

Note: Care must be taken so that macro expansion for legitimate E-Mail doesn't go over the 63-character limit on DNS labels. The localpart of E-Mail addresses, in particular, can have more than 63 characters between dots ๐Ÿ˜ญ
Note: Domains should avoid using the "s", "l", "o", or "h" macros in conjunction with any mechanism directive. Even though these macros are powerful and allow per-user records to be published, they seriously limit how well implementations can cache results of check_host(), and they reduce the effectiveness of DNS caches ๐Ÿ’€

Implementations should be aware that if no directive processed during the evaluation of check_host() contains an "s", "l", "o", or "h" macro, then the results of the evaluation can be cached based on <domain> and <ip> alone, for as long as the shortest Time To Live (TTL) of all the DNS records involved ๐Ÿง  โœจ


8.2. Expansion Examples ๐ŸŽ€

Here's the setup for all the examples, so follow along ๐Ÿ’…

The <sender> is strong-bad@email.example.com.
The IPv4 SMTP client IP is 192.0.2.3.
The IPv6 SMTP client IP is 2001:DB8::CB01.
The PTR domain name of the client IP is mx.example.org.

And here's the expansion table, which is honestly so satisfying ๐Ÿคฉ

macro                       expansion
-------  ----------------------------
%{s}     strong-bad@email.example.com
%{o}                email.example.com
%{d}                email.example.com
%{d4}               email.example.com
%{d3}               email.example.com
%{d2}                     example.com
%{d1}                             com
%{dr}               com.example.email
%{d2r}                  example.email
%{l}                       strong-bad
%{l-}                      strong.bad
%{lr}                      strong-bad
%{lr-}                     bad.strong
%{l1r-}                        strong
macro-string                                               expansion
--------------------------------------------------------------------
%{ir}.%{v}._spf.%{d2}             3.2.0.192.in-addr._spf.example.com
%{lr-}.lp._spf.%{d2}                  bad.strong.lp._spf.example.com

%{lr-}.lp.%{ir}.%{v}._spf.%{d2}
                    bad.strong.lp.3.2.0.192.in-addr._spf.example.com

%{ir}.%{v}.%{l1r-}.lp._spf.%{d2}
                        3.2.0.192.in-addr.strong.lp._spf.example.com

%{d2}.trusted-domains.example.net
                             example.com.trusted-domains.example.net

IPv6:
%{ir}.%{v}._spf.%{d2}                               1.0.B.C.0.0.0.0.
0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.B.D.0.1.0.0.2.ip6._spf.example.com

9. Implications ๐ŸŒˆ

This section lays out the big implications that adopting this document will have on the various entities involved in Internet E-Mail ๐Ÿ“จ It's meant to make it clear where this document knowingly affects how those entities work. This section is NOT a "how-to" manual or a "best practices" document, and it isn't a comprehensive list of what those entities should do because of this document ๐Ÿ™…โ€โ™€๏ธ

This section is non-normative. ๐Ÿ“

9.1. Sending Domains ๐Ÿ’Œ

Domains that want to be compliant with this specification will need to figure out the list of hosts they allow to use their domain name in the "HELO" and "MAIL FROM" identities. It's recognized that forming that list isn't just a simple technical exercise. It involves policy decisions with both technical and administrative considerations ๐Ÿง

It can be helpful to publish records that include a "tracking exists:" mechanism ๐Ÿ•ต๏ธโ€โ™€๏ธ By looking at the name server logs, you can then generate a rough list. For example:

v=spf1 exists:_h.%{h}._l.%{l}._o.%{o}._i.%{i}._spf.%{d} ?all

9.2. Mailing Lists ๐Ÿ“ฌ

Mailing lists must be aware of how they re-inject mail that's sent to the list. Mailing lists MUST comply with the requirements in [RFC2821], Section 3.10, and [RFC1123], Section 5.3.6, which say that the reverse-path MUST be changed to be the mailbox of a person or other entity who administers the list ๐Ÿซก The reasons for changing the reverse-path are many and long-standing, but SPF adds enforcement to this requirement ๐Ÿ’ช

In practice, almost all mailing list software in use already complies with this requirement ๐ŸŽ‰ Mailing lists that don't comply may or may not run into problems, depending on how access to the list is restricted. Lists that are entirely internal to a domain (where only people in the domain can send to or receive from the list) aren't affected ๐ŸŒธ

9.3. Forwarding Services and Aliases โœจ

Okay so forwarding services take mail that lands in a mailbox and pass it along to some external mailbox ๐Ÿ’Œ Right now, basically every forwarding service re-sends the message using the original "MAIL FROM" when it delivers to that external mailbox ๐Ÿซ  [RFC1123] and [RFC2821] call this an "alias" instead of a "mail list". So the external mailbox's MTA sees all this mail coming in on a connection from a host that belongs to the forwarding service, which means the "MAIL FROM" identity generally won't pass authorization ๐Ÿ’€

There are three spots where you can try to fix this ๐Ÿ› ๏ธ

  1. The beginning, when E-Mail is first sent.
    1. "Neutral" results could be given for IP addresses that might be forwarders, instead of "Fail" results. For example: "v=spf1 mx -exists:%{ir}.sbl.spamhaus.example.org ?all" This would do a lookup on an anti-spam DNS blacklist (DNSBL) and only give a "Fail" result for E-Mail from listed sources. Everything else, including E-Mail sent through forwarders, gets a "Neutral" result. Checking the DNSBL after the known good sources means problems from incorrect DNSBL listings are greatly reduced ๐Ÿ™Œ
    2. The "MAIL FROM" identity could carry extra info in the localpart that cryptographically proves the mail came from an authorized source ๐Ÿ” In that case, an SPF record like "v=spf1 mx exists:%{l}._spf_verify.%{d} -all" could be used. Then a specialized DNS server can be set up to serve the _spf_verify subdomain and validate the localpart. Yes, this needs an extra DNS lookup, but it only happens when the E-Mail would otherwise be rejected as not coming from a known good source โœจ
Note that because domain labels are capped at 63 characters, this approach only works reliably if the localpart signature scheme is guaranteed either to only produce localparts with a maximum of 63 characters or to gracefully handle truncated localparts. ๐Ÿ“
  1. Similarly, a specialized DNS server could be set up to rate-limit E-Mail coming from unexpected IP addresses ๐Ÿšฆ "v=spf1 mx exists:%{ir}._spf_rate.%{d} -all"
  2. SPF allows per-user policies for special cases ๐Ÿ‘‘ For example, this SPF record plus appropriate wildcard DNS records can do the job: "v=spf1 mx redirect=%{l1r+}._at_.%{o}._spf.%{d}"
  1. The middle, when E-Mail is forwarded.
    1. Forwarding services can solve the problem by rewriting the "MAIL FROM" to be in their own domain. The catch is that mail bounced from the external mailbox will have to be re-bounced by the forwarding service ๐Ÿ” Various schemes to do this exist, though they vary widely in complexity and in the resource requirements on the forwarding service's side.
    2. Several popular MTAs can be forced from "alias" semantics to "mailing list" semantics by adding an extra alias with "owner-" prepended to the original alias name ๐Ÿงš For example, an alias of "friends: george@example.com, fred@example.org" would need another alias of the form "owner-friends: localowner"
  2. The end, when E-Mail is received.
    1. If the owner of the external mailbox trusts the forwarding service, they can tell the external mailbox's MTA to skip SPF tests when the client host belongs to that forwarding service ๐Ÿซถ
    2. Tests against other identities, such as the "HELO" identity, may be used to override a failed test against the "MAIL FROM" identity ๐Ÿ’œ
    3. For larger domains, it may not be possible to have a complete or accurate list of the forwarding services used by the owners of the domain's mailboxes ๐Ÿ˜ตโ€๐Ÿ’ซ In those cases, whitelists of generally-recognized forwarding services could be employed ๐ŸŒˆ

9.4. Mail Services

Service providers that offer mail services to third-party domains, like sending bulk mail ๐Ÿ“ฌ, may want to adjust their setup in light of the authorization check described in this document. If the "MAIL FROM" identity used for that E-Mail uses the provider's own domain, then the provider only needs to make sure its sending host is authorized by its own SPF record, if it has one ๐Ÿ’–

If the "MAIL FROM" identity does not use the mail service provider's domain, then extra care must be taken ๐Ÿง The SPF record format has several options for the third-party domain to authorize the service provider's MTAs to send mail on its behalf. For mail service providers, like ISPs, that have a wide variety of customers using the same MTA, steps should be taken to prevent cross-customer forgery (see Section 10.4) ๐Ÿšจ

9.5. MTA Relays

The authorization check generally rules out arbitrary MTA relays between the sender and receiver of an E-Mail message ๐Ÿšซ

Inside an organization, MTA relays can be effectively deployed ๐Ÿข For the purposes of this document, though, those relays are effectively transparent. The SPF authorization check is a check between border MTAs of different domains ๐ŸŒ

For mail senders, this means that published SPF records must authorize any MTAs that actually send across the Internet ๐Ÿ“ค Usually, these are just the border MTAs, since internal MTAs simply forward mail to them for delivery.

Mail receivers will generally want to perform the authorization check at the border MTAs, specifically including all secondary MXs ๐Ÿ‘‹ This lets mail that fails get rejected during the SMTP session rather than bounced. Internal MTAs then skip the authorization test. To perform the test anywhere other than the border, the host that first transferred the message into the organization has to be determined, and that can be tricky to pull out of the message header ๐Ÿซ  Testing other than at the border is not recommended ๐Ÿ™…

10. Security Considerations ๐Ÿšจ

10.1. Processing Limits

Like most aspects of E-Mail, there are a bunch of ways bad actors could use the protocol as an avenue for a Denial-of-Service (DoS) attack ๐Ÿ˜ˆ The processing limits outlined here are designed to prevent attacks like these:

  • A malicious party could create an SPF record with many references to a victim's domain and send many E-Mails to different SPF clients. Those SPF clients would then create a DoS attack. In effect, the SPF clients get used to amplify the attacker's bandwidth, since they use fewer bytes in the SMTP session than the DNS queries do. Using SPF clients also lets the attacker hide the true source of the attack ๐Ÿ•ต๏ธ
  • Whereas implementations of check_host() are supposed to limit the number of DNS lookups, malicious domains could publish records that exceed these limits to waste computation effort at their targets when they send them mail. Malicious domains could also design SPF records that cause particular implementations to use excessive memory or CPU, or to trigger bugs ๐Ÿ›
  • Malicious parties could send a large volume of mail purporting to come from the intended target to a wide variety of legitimate mail hosts. Those legitimate machines would then put a DNS load on the target as they fetch the relevant records ๐Ÿ“‰

Of these, the case of a third party referenced in the SPF record is the easiest for a DoS attack to effectively exploit ๐Ÿ˜ญ As a result, limits that may seem reasonable for an individual mail server can still allow an unreasonable amount of bandwidth amplification. Therefore, the processing limits need to be quite low โฌ‡๏ธ

SPF implementations MUST limit the number of mechanisms and modifiers that do DNS lookups to at most 10 per SPF check, including any lookups caused by the use of the "include" mechanism or the "redirect" modifier. If this number is exceeded during a check, a PermError MUST be returned. The "include", "a", "mx", "ptr", and "exists" mechanisms as well as the "redirect" modifier do count against this limit. The "all", "ip4", and "ip6" mechanisms do not require DNS lookups and therefore do not count against this limit. The "exp" modifier does not count against this limit because the DNS lookup to fetch the explanation string occurs after the SPF record has been evaluated โฑ๏ธ

When evaluating the "mx" and "ptr" mechanisms, or the %{p} macro, there MUST be a limit of no more than 10 MX or PTR RRs looked up and checked ๐Ÿ”ข

SPF implementations SHOULD limit the total amount of data obtained from the DNS queries. For example, when DNS over TCP or EDNS0 are available, there may need to be an explicit limit to how much data will be accepted, to prevent excessive bandwidth usage or memory usage and DoS attacks ๐Ÿ“ฆ

MTAs or other processors MAY also impose a limit on the maximum amount of elapsed time to evaluate check_host(). Such a limit SHOULD allow at least 20 seconds. If such a limit is exceeded, the result of authorization SHOULD be "TempError" โณ

Domains publishing records SHOULD try to keep the number of "include" mechanisms and chained "redirect" modifiers to a minimum. Domains SHOULD also try to minimize the amount of other DNS information needed to evaluate a record. This can be done by choosing directives that require less DNS information and by placing lower-cost mechanisms earlier in the SPF record ๐Ÿงน

For example, consider a domain set up as follows:

example.com.      IN MX   10 mx.example.com.
mx.example.com.   IN A    192.0.2.1
a.example.com.    IN TXT  "v=spf1 mx:example.com -all"
b.example.com.    IN TXT  "v=spf1 a:mx.example.com -all"
c.example.com.    IN TXT  "v=spf1 ip4:192.0.2.1 -all"

Evaluating check_host() for the domain "a.example.com" requires the MX records for "example.com", and then the A records for the listed hosts. Evaluating for "b.example.com" requires only the A records. Evaluating for "c.example.com" requires none ๐Ÿง 

However, there may be administrative considerations: using "a" over "ip4" allows hosts to be renumbered easily, and using "mx" over "a" allows the set of mail hosts to be changed easily ๐Ÿ”„

10.2. SPF-Authorized E-Mail May Contain Other False Identities

The "MAIL FROM" and "HELO" identity authorizations must not be construed to provide more assurance than they do ๐Ÿซฃ It's entirely possible for a malicious sender to inject a message using their own domain in the identities used by SPF, get that domain's SPF record to authorize the sending host, and yet the message can easily list other identities in its header. Unless the user or the MUA takes care to note that the authorized identity does not match the other, more commonly-presented identities (such as the From: header field), the user may be lulled into a false sense of security ๐Ÿ˜ฌ

10.3. Spoofed DNS and IP Data

okay bestie, there are two spots where a baddie could mess with check_host() ๐Ÿ‘€

  • The whole check leans super hard on DNS ๐ŸŒ. An attacker could go after the DNS infrastructure so check_host() sees spoofed data and returns wrong results. That could even mean Pass for an <ip> where the domain's real record would say Fail ๐Ÿ’€. For a rundown of DNS weaknesses, see [RFC3833].
  • The client IP address, <ip>, is assumed to be correct ๐Ÿซฃ. An attacker could spoof TCP sequence numbers so mail looks like it's coming from a permitted host, for a domain they're impersonating ๐Ÿ™ˆ

10.4. Cross-User Forgery

SPF policies are only about domain names. They map domains to sets of authorized MTAs, not whole E-Mail addresses to sets of authorized users ๐Ÿ“ฎ The l macro (Section 8) gives you a limited way to define separate sets of authorized MTAs for specific E-Mail addresses, but it's generally impossible to verify through SPF that individual users of the same MTA are really the ones using specific addresses ๐Ÿคทโ€โ™€๏ธ

So mail services and their MTAs have to stop cross-user forgery directly ๐Ÿ›ก๏ธ Based on SMTP AUTH ([RFC2554]), users should only be allowed to use E-Mail addresses that are actually under their control (see [RFC4409], Section 6.1). Another way to verify individual users is message cryptography, like PGP ([RFC2440]) or S/MIME ([RFC3851]) ๐Ÿ”

10.5. Untrusted Information Sources

SPF runs on info from third parties, like the HELO domain name, the MAIL FROM address, and SPF records ๐Ÿ“ฌ That info then gets passed to the receiver in the Received-SPF: trace fields and maybe sent back to the client MTA as part of an SMTP rejection message. This info must be checked for invalid characters and excessively long lines ๐Ÿงน

When the authorization check fails, an explanation string may be included in the reject response. Both the sender and the rejecting receiver need to be aware that the explanation was written by the publisher of the SPF record that got checked, and in general not by the receiver ๐Ÿšฉ The explanation may contain malicious URLs, or it may be offensive or misleading ๐Ÿ’€

This is probably less of a concern than it first seems ๐Ÿ’… Those messages go back to the sender, and the explanation strings come from the sender policy published by the domain in the identity that very sender claimed. As long as the DSN isn't redirected to someone other than the actual sender, the only people who see malicious explanation strings are people whose messages claim to be from domains that publish those strings in their SPF records. In practice, DSNs can get misdirected, like when an MTA accepts an E-Mail and then later generates a DSN to a forged address, or when an E-Mail forwarder doesn't send the DSN back to the original sender ๐Ÿ“จ

10.6. Privacy Exposure

Checking SPF records sends DNS queries to the domain owner ๐Ÿ‘€ Those queries, especially the ones caused by the exists mechanism, can reveal who is sending E-Mail and likely which MTA the E-Mail is going to ๐Ÿซข That can raise some privacy concerns. How much of an issue it is depends on local laws and on the relationship between the domain owner and the person sending the E-Mail โš–๏ธ

11. Contributors and Acknowledgements

This document is largely based on the work of Meng Weng Wong and Mark Lentczner ๐Ÿ‘‘ Lots of people contributed, but a very large portion of the writing and editing is thanks to Meng and Mark ๐Ÿซถ

This design owes a debt of parentage to [RMX] by Hadmut Danisch and to [DMP] by Gordon Fecyk ๐Ÿงš The idea of using a DNS record to check whether an E-Mail address is legit has roots even further back, in messages on the namedroppers mailing list by Paul Vixie [Vixie] (based on a suggestion by Jim Miller) and by David Green [Green] ๐Ÿฆ‹

Philip Gladstone added the concept of macros to the specification โœจ That multiplied how expressive the language is and made per-user and per-IP lookups possible ๐Ÿคฏ

The authors also want to thank the literally hundreds of people who helped develop this design ๐Ÿ’– There are way too many to name, but they include:

  • The folks on the spf-discuss mailing list.
  • The folks on the SPAM-L mailing list.
  • The folks on the IRTF ASRG mailing list.
  • The folks on the IETF MARID mailing list.
  • The folks on #perl.

12. IANA Considerations

12.1. The SPF DNS Record Type

IANA assigned a new Resource Record Type and Qtype from the DNS Parameters Registry for the SPF RR type, with code 99 ๐ŸŽ€

12.2. The Received-SPF Mail Header Field

Per [RFC3864], the Received-SPF: header field is added to the IANA Permanent Message Header Field Registry ๐Ÿ“ Here's the registration template:

      Header field name: Received-SPF
      Applicable protocol: mail ([RFC2822])
      Status: Experimental
      Author/Change controller: IETF
      Specification document(s): RFC 4408
      Related information:
      Requesting SPF Council review of any proposed changes and
      additions to this field are recommended.  For information about
      the SPF Council see http://www.openspf.org/Council

13. References

13.1. Normative References

   [RFC1035]  Mockapetris, P., "Domain names - implementation and
              specification", STD 13, RFC 1035, November 1987.

   [RFC1123]  Braden, R., "Requirements for Internet Hosts - Application
              and Support", STD 3, RFC 1123, October 1989.

   [RFC2119]  Bradner, S., "Key words for use in RFCs to Indicate
              Requirement Levels", BCP 14, RFC 2119, March 1997.

   [RFC2821]  Klensin, J., "Simple Mail Transfer Protocol", RFC 2821,
              April 2001.

   [RFC2822]  Resnick, P., "Internet Message Format", RFC 2822, April
              2001.

   [RFC3464]  Moore, K. and G. Vaudreuil, "An Extensible Message Format
              for Delivery Status Notifications", RFC 3464, January
              2003.

   [RFC3513]  Hinden, R. and S. Deering, "Internet Protocol Version 6
              (IPv6) Addressing Architecture", RFC 3513, April 2003.

   [RFC3864]  Klyne, G., Nottingham, M., and J. Mogul, "Registration
              Procedures for Message Header Fields", BCP 90, RFC 3864,
              September 2004.

   [RFC3986]  Berners-Lee, T., Fielding, R., and L. Masinter, "Uniform
              Resource Identifier (URI): Generic Syntax", STD 66, RFC
              3986, January 2005.

   [RFC4234]  Crocker, D. and P. Overell, "Augmented BNF for Syntax
              Specifications: ABNF", RFC 4234, October 2005.

   [US-ASCII] American National Standards Institute (formerly United
              States of America Standards Institute), "USA Code for
              Information Interchange, X3.4", 1968.

   ANSI X3.4-1968 has been replaced by newer versions with slight
              modifications, but the 1968 version remains definitive for
              the Internet.

13.2 Informative References

   [RFC1034]  Mockapetris, P., "Domain names - concepts and facilities",
              STD 13, RFC 1034, November 1987.

   [RFC1983]  Malkin, G., "Internet Users' Glossary", RFC 1983, August
              1996.

   [RFC2440]  Callas, J., Donnerhacke, L., Finney, H., and R. Thayer,
              "OpenPGP Message Format", RFC 2440, November 1998.

   [RFC2554]  Myers, J., "SMTP Service Extension for Authentication",
              RFC 2554, March 1999.

   [RFC3696]  Klensin, J., "Application Techniques for Checking and
              Transformation of Names", RFC 3696, February 2004.

   [RFC3833]  Atkins, D. and R. Austein, "Threat Analysis of the Domain
              Name System (DNS)", RFC 3833, August 2004.

   [RFC3851]  Ramsdell, B., "Secure/Multipurpose Internet Mail
              Extensions (S/MIME) Version 3.1 Message Specification",
              RFC 3851, July 2004.

   [RFC4409]  Gellens, R. and J. Klensin, "Message Submission for Mail",
              RFC 4409, April 2006.

   [RMX]      Danish, H., "The RMX DNS RR Type for light weight sender
              authentication", Work In Progress

   [DMP]      Fecyk, G., "Designated Mailers Protocol", Work In Progress

   [Vixie]    Vixie, P., "Repudiating MAIL FROM", 2002.

   [Green]    Green, D., "Domain-Authorized SMTP Mail", 2002.

Appendix A. Collected ABNF โœจ

okay so this section is normative, which means it's the final word ๐Ÿ’… if any ABNF fragment earlier in the doc doesn't match this grammar, this one wins, no debate. ๐Ÿ”ฅ

quick heads up: this is ABNF (the notation from [RFC4234]), and literal text in quotes is case-insensitive. so "mx" matches "mx", "MX", "mX", and "Mx" ๐Ÿ™ˆ literally every version slays.

   record           = version terms *SP
   version          = "v=spf1"

   terms            = *( 1*SP ( directive / modifier ) )

   directive        = [ qualifier ] mechanism
   qualifier        = "+" / "-" / "?" / "~"
   mechanism        = ( all / include
                      / A / MX / PTR / IP4 / IP6 / exists )

   all              = "all"
   include          = "include"  ":" domain-spec
   A                = "a"      [ ":" domain-spec ] [ dual-cidr-length ]
   MX               = "mx"     [ ":" domain-spec ] [ dual-cidr-length ]
   PTR              = "ptr"    [ ":" domain-spec ]
   IP4              = "ip4"      ":" ip4-network   [ ip4-cidr-length ]
   IP6              = "ip6"      ":" ip6-network   [ ip6-cidr-length ]
   exists           = "exists"   ":" domain-spec

   modifier         = redirect / explanation / unknown-modifier
   redirect         = "redirect" "=" domain-spec
   explanation      = "exp" "=" domain-spec
   unknown-modifier = name "=" macro-string

   ip4-cidr-length  = "/" 1*DIGIT
   ip6-cidr-length  = "/" 1*DIGIT
   dual-cidr-length = [ ip4-cidr-length ] [ "/" ip6-cidr-length ]

   ip4-network      = qnum "." qnum "." qnum "." qnum
   qnum             = DIGIT                 ; 0-9
                      / %x31-39 DIGIT       ; 10-99
                      / "1" 2DIGIT          ; 100-199
                      / "2" %x30-34 DIGIT   ; 200-249
                      / "25" %x30-35        ; 250-255
             ; conventional dotted quad notation.  e.g., 192.0.2.0
   ip6-network      = <as per [RFC 3513], section 2.2>
             ; e.g., 2001:DB8::CD30

   domain-spec      = macro-string domain-end
   domain-end       = ( "." toplabel [ "." ] ) / macro-expand
   toplabel         = ( *alphanum ALPHA *alphanum ) /
                      ( 1*alphanum "-" *( alphanum / "-" ) alphanum )
                      ; LDH rule plus additional TLD restrictions
                      ; (see [RFC3696], Section 2)

   alphanum         = ALPHA / DIGIT

   explain-string   = *( macro-string / SP )

   macro-string     = *( macro-expand / macro-literal )
   macro-expand     = ( "%{" macro-letter transformers *delimiter "}" )
                      / "%%" / "%_" / "%-"
   macro-literal    = %x21-24 / %x26-7E
                      ; visible characters except "%"
   macro-letter     = "s" / "l" / "o" / "d" / "i" / "p" / "h" /
                      "c" / "r" / "t"
   transformers     = *DIGIT [ "r" ]
   delimiter        = "." / "-" / "+" / "," / "/" / "_" / "="

   name             = ALPHA *( ALPHA / DIGIT / "-" / "_" / "." )

   header-field     = "Received-SPF:" [CFWS] result FWS [comment FWS]
                      [ key-value-list ] CRLF

   result           = "Pass" / "Fail" / "SoftFail" / "Neutral" /
                      "None" / "TempError" / "PermError"

   key-value-list   = key-value-pair *( ";" [CFWS] key-value-pair )
                      [";"]

   key-value-pair   = key [CFWS] "=" ( dot-atom / quoted-string )

   key              = "client-ip" / "envelope-from" / "helo" /
                      "problem" / "receiver" / "identity" /
                       mechanism / "x-" name / name

   identity         = "mailfrom"   ; for the "MAIL FROM" identity
                      / "helo"     ; for the "HELO" identity
                      / name       ; other identities

   dot-atom         = <unquoted word as per [RFC2822]>
   quoted-string    = <quoted string as per [RFC2822]>
   comment          = <comment string as per [RFC2822]>
   CFWS             = <comment or folding white space as per [RFC2822]>
   FWS              = <folding white space as per [RFC2822]>
   CRLF             = <standard end-of-line token as per [RFC2822]>

Appendix B. Extended Examples ๐Ÿงš

time for the fun part ๐ŸŽ€ these examples all run on one shared DNS setup, so here's the zone we're working with:

   ; A domain with two mail servers, two hosts
   ; and two servers at the domain name
   $ORIGIN example.com.
   @           MX  10 mail-a
               MX  20 mail-b
               A   192.0.2.10
               A   192.0.2.11
   amy         A   192.0.2.65
   bob         A   192.0.2.66
   mail-a      A   192.0.2.129
   mail-b      A   192.0.2.130
   www         CNAME example.com.

   ; A related domain
   $ORIGIN example.org.
   @           MX  10 mail-c
   mail-c      A   192.0.2.140

   ; The reverse IP for those addresses
   $ORIGIN 2.0.192.in-addr.arpa.
   10          PTR example.com.
   11          PTR example.com.
   65          PTR amy.example.com.
   66          PTR bob.example.com.
   129         PTR mail-a.example.com.
   130         PTR mail-b.example.com.
   140         PTR mail-c.example.org.

   ; A rogue reverse IP domain that claims to be
   ; something it's not
   $ORIGIN 0.0.10.in-addr.arpa.
   4           PTR bob.example.com.

B.1. Simple Examples

these show different records you could publish for example.com, and which values of <ip> would make check_host() return "Pass". โœจ just note that <domain> is "example.com" here.

   v=spf1 +all
      -- any <ip> passes

   v=spf1 a -all
      -- hosts 192.0.2.10 and 192.0.2.11 pass

   v=spf1 a:example.org -all
      -- no sending hosts pass since example.org has no A records

   v=spf1 mx -all
      -- sending hosts 192.0.2.129 and 192.0.2.130 pass

   v=spf1 mx:example.org -all
      -- sending host 192.0.2.140 passes

   v=spf1 mx mx:example.org -all
      -- sending hosts 192.0.2.129, 192.0.2.130, and 192.0.2.140 pass

   v=spf1 mx/30 mx:example.org/30 -all
      -- any sending host in 192.0.2.128/30 or 192.0.2.140/30 passes

   v=spf1 ptr -all
      -- sending host 192.0.2.65 passes (reverse DNS is valid and is in
         example.com)
      -- sending host 192.0.2.140 fails (reverse DNS is valid, but not
         in example.com)
      -- sending host 10.0.0.4 fails (reverse IP is not valid)

   v=spf1 ip4:192.0.2.128/28 -all
      -- sending host 192.0.2.65 fails
      -- sending host 192.0.2.129 passes

B.2. Multiple Domain Example

these show what happens when records are related to each other ๐Ÿ‘ฏโ€โ™€๏ธ

      example.org: "v=spf1 include:example.com include:example.net -all"

this record would be used if mail from example.org actually came through servers at example.com and example.net. basically example.org's designated servers are the union of example.com's and example.net's designated servers. iconic teamwork ๐Ÿซถ

      la.example.org: "v=spf1 redirect=example.org"
      ny.example.org: "v=spf1 redirect=example.org"
      sf.example.org: "v=spf1 redirect=example.org"

these records let a bunch of domains that all use the same mail system share that system's record ๐Ÿ’… so when the mail setup changes, only the mail system's record needs updating. these domains' records never have to change. lowkey genius ๐Ÿง 

B.3. DNSBL Style Example

imagine that, on top of the domain records we already looked at, these exist too:

   $ORIGIN _spf.example.com.  mary.mobile-users                   A
   127.0.0.2 fred.mobile-users                   A 127.0.0.2
   15.15.168.192.joel.remote-users     A 127.0.0.2
   16.15.168.192.joel.remote-users     A 127.0.0.2

the following records describe users at example.com who mail from arbitrary servers, or who mail from personal servers ๐Ÿ“ฑ

   example.com:

   v=spf1 mx
          include:mobile-users._spf.%{d}
          include:remote-users._spf.%{d}
          -all

   mobile-users._spf.example.com:

   v=spf1 exists:%{l1r+}.%{d}

   remote-users._spf.example.com:

   v=spf1 exists:%{ir}.%{l1r+}.%{d}

B.4. Multiple Requirements Example

say your sender policy needs BOTH that the IP address sits inside a certain range AND that the reverse DNS for the IP matches ๐Ÿฅน there are a few ways to set that up, and here's one of them:

   example.com.           SPF  ( "v=spf1 "
                                 "-include:ip4._spf.%{d} "
                                 "-include:ptr._spf.%{d} "
                                 "+all" )
   ip4._spf.example.com.  SPF  "v=spf1 -ip4:192.0.2.0/24 +all"
   ptr._spf.example.com.  SPF  "v=spf1 -ptr +all"

this example shows how the "-include" mechanism can be useful, how an SPF record that ends in "+all" can be very restrictive, and the use of De Morgan's Law. galaxy brain moment ๐ŸŒŒ


Authors' Addresses ๐Ÿฆ‹

meet the authors ๐Ÿ’œ

   Meng Weng Wong
   Singapore

   EMail: mengwong+spf@pobox.com
   Wayne Schlitt
   4615 Meredeth #9
   Lincoln Nebraska, NE  68506
   United States of America

   EMail: wayne@schlitt.net
   URI:   http://www.schlitt.net/spf/

Copyright (C) The Internet Society (2006).

this document is subject to the rights, licenses and restrictions contained in BCP 78, and except as set forth therein, the authors retain all their rights.

this document and the information contained herein are provided on an "AS IS" basis and THE CONTRIBUTOR, THE ORGANIZATION HE/SHE REPRESENTS OR IS SPONSORED BY (IF ANY), THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK FORCE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.

the disclaimer above is serious business. the document is provided "AS IS", with no warranties at all, express or implied.

Intellectual Property ๐Ÿ”ฅ

the IETF takes no position on the validity or scope of any Intellectual Property Rights or other rights that might be claimed to pertain to the implementation or use of the technology described in this document, or on whether any license under such rights might or might not be available. it also doesn't claim to have made any independent effort to identify any such rights. info on the procedures for rights in RFC documents can be found in BCP 78 and BCP 79.

copies of IPR disclosures made to the IETF Secretariat, any assurances of licenses to be made available, or the result of an attempt to get a general license or permission for implementers or users to use these proprietary rights can be found in the IETF online IPR repository at http://www.ietf.org/ipr.

the IETF invites any interested party to bring to its attention any copyrights, patents or patent applications, or other proprietary rights that may cover technology required to implement this standard. send that info to the IETF at ietf-ipr@ietf.org ๐Ÿ“ฌ


Acknowledgement ๐Ÿซถ

funding for the RFC Editor function is provided by the IETF Administrative Support Activity (IASA). thank you, IASA, for keeping the lights on โœจ

๐Ÿ“ฆ where this page comes from