<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DMARCguard</title>
    <description>The latest articles on DEV Community by DMARCguard (dmarcguard).</description>
    <link>https://dev.to/dmarcguard</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F15969%2F390f8509-8005-414f-a064-3e98f652b238.png</url>
      <title>DEV Community: DMARCguard</title>
      <link>https://dev.to/dmarcguard</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmarcguard"/>
    <language>en</language>
    <item>
      <title>SPF Record Syntax: Mechanisms, Qualifiers, Modifiers, and Macros</title>
      <dc:creator>Meysam</dc:creator>
      <pubDate>Wed, 11 Nov 2026 09:51:02 +0000</pubDate>
      <link>https://dev.to/dmarcguard/spf-record-syntax-mechanisms-qualifiers-modifiers-and-macros-i7k</link>
      <guid>https://dev.to/dmarcguard/spf-record-syntax-mechanisms-qualifiers-modifiers-and-macros-i7k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Short answer&lt;/p&gt;

&lt;p&gt;SPF record syntax follows one shape: a single DNS TXT record starting with &lt;code&gt;v=spf1&lt;/code&gt;, followed by space-separated terms — mechanisms with optional qualifiers, then modifiers — evaluated left to right until the first match. A complete record such as &lt;code&gt;v=spf1 ip4:192.0.2.0/24 include:_spf.example.com -all&lt;/code&gt; authorizes a /24 network and a third party’s servers, then fails everything else. Every rule governing it lives in RFC 7208. Break a record down term by term with the &lt;a href="https://dmarcguard.io/tools/spf-syntax-inspector/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;SPF syntax inspector&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;SPF record syntax follows one shape: a single DNS TXT record that starts with &lt;code&gt;v=spf1&lt;/code&gt;, followed by space-separated terms — mechanisms with optional qualifiers, then modifiers — evaluated left to right until the first match. Here is a complete record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 ip4:192.0.2.0/24 include:_spf.example.com -all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one line authorizes a /24 network and a third party’s servers, then fails everything else. Every rule governing it lives in &lt;a href="https://datatracker.ietf.org/doc/html/rfc7208" rel="noopener noreferrer"&gt;RFC 7208&lt;/a&gt;, the SPF standard published in April 2014.&lt;/p&gt;

&lt;p&gt;This page is the full reference: every mechanism, every qualifier, both modifiers, the complete macro table, evaluation order, DNS lookup limits, and record placement rules — each with the RFC 7208 section that defines it. If you want protocol fundamentals first — why SPF exists and how it fits with DKIM and DMARC — start with our &lt;a href="https://dmarcguard.io/learn/spf/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;SPF guide&lt;/a&gt;. If you build and maintain records, bookmark this.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPF Record Syntax at a Glance
&lt;/h2&gt;

&lt;p&gt;An SPF record is one string of text in the RDATA of a single TXT record, and its grammar has exactly three kinds of parts: a version tag, mechanisms (each with an optional qualifier), and modifiers. The version tag must be exactly &lt;code&gt;v=spf1&lt;/code&gt; — a record starting &lt;code&gt;v=spf10&lt;/code&gt; is discarded, not partially matched (§4.5).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdmarcguard.io%2Fimages%2Fblog%2Fspf-record-syntax%2Fspf-record-syntax_anatomy_diagram.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdmarcguard.io%2Fimages%2Fblog%2Fspf-record-syntax%2Fspf-record-syntax_anatomy_diagram.svg" alt="Labeled anatomy of an SPF record: version tag, ip4 mechanism with argument, include mechanism, minus qualifier, and terminal all mechanism" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every SPF record is the same three kinds of parts: a version tag, mechanisms with optional qualifiers, and modifiers.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The grammar in brief, from §3 and §4.6.1:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Version&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;v=spf1&lt;/code&gt;, always first, exact&lt;/td&gt;
&lt;td&gt;Selects the record (§4.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mechanism&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[qualifier]name[:arg][/cidr]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tested against the client IP; can match or not match (§4.6.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qualifier&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;+&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;~&lt;/code&gt; &lt;code&gt;?&lt;/code&gt; before mechanism&lt;/td&gt;
&lt;td&gt;The result returned when its mechanism matches (§4.6.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modifier&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;name=value&lt;/code&gt;, at most once each&lt;/td&gt;
&lt;td&gt;Extra information; never matched (§6)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The three kinds of parts in SPF record syntax&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Terms are separated by spaces. Mechanism names are case-insensitive, and terms containing none of &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;:&lt;/code&gt;, or &lt;code&gt;/&lt;/code&gt; are mechanisms (§4.6.1). One syntax error anywhere invalidates the whole record: check_host() — the receiver’s evaluation routine, as the RFC names it — validates SPF syntax first and returns PermError immediately if anything is malformed, without evaluating a single term (§4.6). That is why one stray character can fail authentication for every message a domain sends.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPF Mechanisms
&lt;/h2&gt;

&lt;p&gt;Eight mechanisms exist, and each either matches the connecting IP or does not. RFC 7208 §5 splits them into basic framework mechanisms (&lt;code&gt;all&lt;/code&gt;, &lt;code&gt;include&lt;/code&gt;) and designated-sender mechanisms (&lt;code&gt;a&lt;/code&gt;, &lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;ptr&lt;/code&gt;, &lt;code&gt;ip4&lt;/code&gt;, &lt;code&gt;ip6&lt;/code&gt;, &lt;code&gt;exists&lt;/code&gt;). Before the full table: most real-world records use only &lt;code&gt;include&lt;/code&gt;, &lt;code&gt;ip4&lt;/code&gt;, &lt;code&gt;ip6&lt;/code&gt;, and &lt;code&gt;all&lt;/code&gt; — the rest of this reference exists so you can read other people’s records, not because your own needs them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Syntax&lt;/th&gt;
&lt;th&gt;Matches when…&lt;/th&gt;
&lt;th&gt;Counts toward 10-lookup limit?&lt;/th&gt;
&lt;th&gt;RFC §&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Always&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;§5.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;include:domain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The referenced record returns Pass&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;§5.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a[:domain][/cidr]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client IP is among the domain’s A/AAAA addresses&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;§5.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mx[:domain][/cidr]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client IP is an address of one of the domain’s MX hosts&lt;/td&gt;
&lt;td&gt;Yes (plus per-MX address caps)&lt;/td&gt;
&lt;td&gt;§5.4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ptr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ptr[:domain]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reverse DNS validates into the target domain&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;§5.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip4:network[/cidr]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client IP is inside the IPv4 network&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;§5.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ip6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip6:network[/cidr]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client IP is inside the IPv6 network&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;§5.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exists&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;exists:domain&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The constructed domain has any A record&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;§5.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;All eight SPF mechanisms with syntax, match condition, and DNS lookup cost&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can parse any published record term-by-term — with each mechanism’s cost and RFC cite attached — using the &lt;a href="https://dmarcguard.io/tools/spf-syntax-inspector/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;SPF syntax inspector&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;all&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;all&lt;/code&gt; always matches, which is why it belongs at the end as the explicit default (§5.1). Everything after it is dead text: “Mechanisms listed after &lt;code&gt;all&lt;/code&gt; MUST be ignored,” and any &lt;code&gt;redirect=&lt;/code&gt; modifier is ignored whenever &lt;code&gt;all&lt;/code&gt; appears anywhere in the record (§5.1). A record without a trailing &lt;code&gt;all&lt;/code&gt; or &lt;code&gt;redirect=&lt;/code&gt; silently defaults to Neutral (§4.7).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;include&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;include:domain&lt;/code&gt; recursively evaluates the referenced domain’s SPF record and matches only when that evaluation returns Pass (§5.2). It is not a splice. The RFC itself concedes the name was “poorly chosen”: a &lt;code&gt;-all&lt;/code&gt; inside an included record does &lt;strong&gt;not&lt;/strong&gt; fail your outer record — a Fail, Softfail, or Neutral inside simply means “no match here, keep going” (§5.2). Better mental model: &lt;code&gt;if-match&lt;/code&gt;. One consequence worth knowing: if the included domain publishes no SPF record at all, &lt;code&gt;include&lt;/code&gt; returns PermError (§5.2).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;mx&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;a&lt;/code&gt; matches when the client IP is one of the target domain’s A or AAAA addresses; &lt;code&gt;mx&lt;/code&gt; matches when it is an address of one of the domain’s MX hosts (§5.3, §5.4). Both default to the current domain when no argument is given, and both accept dual CIDR suffixes — &lt;code&gt;a/24&lt;/code&gt; compares only the top 24 bits, and &lt;code&gt;a:example.com/24//64&lt;/code&gt; sets IPv4 and IPv6 prefixes separately (§5.3). Note the cost asymmetry: &lt;code&gt;mx&lt;/code&gt; is one term against the 10-lookup limit, but evaluating it triggers one MX query plus an address query per MX host, capped at 10 address records before PermError (§4.6.4).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ip4&lt;/code&gt; and &lt;code&gt;ip6&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ip4:&lt;/code&gt; and &lt;code&gt;ip6:&lt;/code&gt; test whether the client IP falls inside a literal network, using a colon — &lt;code&gt;ip4:192.0.2.0/24&lt;/code&gt;, never &lt;code&gt;ip4=&lt;/code&gt; (§5.6). Omitted CIDR lengths default to &lt;code&gt;/32&lt;/code&gt; and &lt;code&gt;/128&lt;/code&gt;, i.e., exact-address match, and truncated addresses like &lt;code&gt;192.0.2&lt;/code&gt; are not permitted (§5.6). These are the only designated-sender mechanisms with zero DNS cost, which makes them the cheapest terms in any record.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;exists&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;exists:domain&lt;/code&gt; constructs a domain name, queries it for an A record, and matches if any A record comes back — regardless of its value, and always an A query even on IPv6 connections (§5.7). One lookup, arbitrary logic. Combined with macros (§7), it powers dynamic authorization: publish per-IP hostnames in a zone you control, and the record authorizes exactly those IPs without ever listing them. This is the pattern behind Salesforce’s current record, shown in the examples below.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ptr&lt;/code&gt; (do not use)
&lt;/h3&gt;

&lt;p&gt;RFC 7208’s own heading for §5.5 is literally &lt;code&gt;"ptr" (do not use)&lt;/code&gt;: the mechanism “SHOULD NOT be published” because it is slow, unreliable under DNS errors, and burdens the &lt;code&gt;.arpa&lt;/code&gt; name servers. Receivers must still support it, but you should never publish it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key finding:&lt;/strong&gt; 41,728 domains — 1.4% of SPF-enabled domains — still publish the deprecated ptr mechanism (Source: DMARCguard SPF Supply Chain study — our scan of 5,499,028 Tranco domains, March 15, 2026)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  SPF Qualifiers: +, -, ~, ?
&lt;/h2&gt;

&lt;p&gt;A qualifier is a single prefix character that sets the result returned when its mechanism matches — and when you omit it, the default is &lt;code&gt;+&lt;/code&gt; (§4.6.2). &lt;code&gt;mx&lt;/code&gt; and &lt;code&gt;+mx&lt;/code&gt; are the same term.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Qualifier&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Meaning (§2.6)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pass&lt;/td&gt;
&lt;td&gt;The client is authorized (default when omitted)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fail&lt;/td&gt;
&lt;td&gt;The client is explicitly not authorized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Softfail&lt;/td&gt;
&lt;td&gt;Probably not authorized; the domain isn’t willing to state a hard Fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Neutral&lt;/td&gt;
&lt;td&gt;The domain asserts nothing about the client&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The four SPF qualifiers and their check_host() results&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In practice the choice comes down to the final term: &lt;code&gt;-all&lt;/code&gt; versus &lt;code&gt;~all&lt;/code&gt;. Receivers treat them differently, and mail flows through forwarders complicate the picture — we cover the trade-off in depth in &lt;a href="https://dmarcguard.io/blog/spf-softfail-vs-hardfail/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;softfail vs hardfail&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The two largest mailbox providers disagree on the qualifier&lt;/p&gt;

&lt;p&gt;Google’s setup doc tells single-provider domains to publish &lt;code&gt;v=spf1 include:_spf.google.com ~all&lt;/code&gt; (softfail), while Microsoft recommends &lt;code&gt;-all&lt;/code&gt; “because we also recommend DKIM and DMARC for the domain.” (Google, &lt;a href="https://knowledge.workspace.google.com/admin/security/set-up-spf" rel="noopener noreferrer"&gt;Set up SPF&lt;/a&gt;; Microsoft, &lt;a href="https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure" rel="noopener noreferrer"&gt;Set up SPF for Microsoft 365&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On a domain that sends mail, &lt;code&gt;~all&lt;/code&gt; alongside DKIM and DMARC enforcement is the usual choice; publish &lt;code&gt;-all&lt;/code&gt; on parked and non-sending domains. The &lt;a href="https://dmarcguard.io/blog/spf-softfail-vs-hardfail/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;softfail vs hardfail&lt;/a&gt; post walks through the trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPF Modifiers: redirect and exp
&lt;/h2&gt;

&lt;p&gt;Modifiers are &lt;code&gt;name=value&lt;/code&gt; pairs — always an equals sign, never a colon — that provide information rather than being matched (§6). RFC 7208 defines two, each allowed at most once; a duplicated &lt;code&gt;redirect=&lt;/code&gt; or &lt;code&gt;exp=&lt;/code&gt; is a PermError, while unrecognized modifiers are ignored no matter how often they appear (§6).&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;redirect=&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;redirect=domain&lt;/code&gt; hands the entire evaluation to another domain’s record — but only after every mechanism has failed to match (§6.1). The other record’s result becomes your result, with one sharpening: where a missing record would normally give None, a &lt;code&gt;redirect=&lt;/code&gt; to a domain with no SPF record gives PermError (§6.1). Two rules people miss: &lt;code&gt;redirect=&lt;/code&gt; counts as one DNS lookup (§4.6.4), and it “MUST be ignored” whenever an &lt;code&gt;all&lt;/code&gt; mechanism appears anywhere in the record (§6.1).&lt;/p&gt;

&lt;p&gt;How it differs from &lt;code&gt;include&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;include:&lt;/code&gt; (§5.2)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;redirect=&lt;/code&gt; (§6.1)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Matches on&lt;/td&gt;
&lt;td&gt;Pass only; otherwise evaluation continues&lt;/td&gt;
&lt;td&gt;Not a mechanism; applies after all misses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effect of referenced result&lt;/td&gt;
&lt;td&gt;Only Pass propagates as a match&lt;/td&gt;
&lt;td&gt;Result replaces yours entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With &lt;code&gt;all&lt;/code&gt; in the record&lt;/td&gt;
&lt;td&gt;Unaffected&lt;/td&gt;
&lt;td&gt;Ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Referenced record missing&lt;/td&gt;
&lt;td&gt;PermError&lt;/td&gt;
&lt;td&gt;PermError&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intended use&lt;/td&gt;
&lt;td&gt;Crossing administrative boundaries&lt;/td&gt;
&lt;td&gt;Sharing one policy across your own domains&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;include: versus redirect= behavior side by side&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;exp=&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;exp=domain&lt;/code&gt; names a TXT record whose macro-expanded string is returned as the explanation when a message Fails (§6.2). It is the only term in the language that never counts toward the lookup limit — §4.6.4 exempts &lt;code&gt;exp&lt;/code&gt; explicitly, because its lookup happens after evaluation, and only on Fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPF Macros Reference
&lt;/h2&gt;

&lt;p&gt;Macros are &lt;code&gt;%{...}&lt;/code&gt; sequences expanded at evaluation time from properties of the message — and they are the least-covered corner of SPF record syntax despite being fully specified in RFC 7208 §7. The ABNF defines exactly eleven macro letters: &lt;code&gt;s&lt;/code&gt;, &lt;code&gt;l&lt;/code&gt;, &lt;code&gt;o&lt;/code&gt;, &lt;code&gt;d&lt;/code&gt;, &lt;code&gt;i&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;h&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;, &lt;code&gt;r&lt;/code&gt;, &lt;code&gt;t&lt;/code&gt;, &lt;code&gt;v&lt;/code&gt; (§7.1).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Macro&lt;/th&gt;
&lt;th&gt;Expands to (§7.2)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{s}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The full sender, e.g. &lt;code&gt;user@example.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{l}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local-part of the sender (&lt;code&gt;user&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Limits caching (§7.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{o}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Domain of the sender&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{d}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The domain being evaluated&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{i}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The connecting client IP&lt;/td&gt;
&lt;td&gt;Dotted-quad IPv4 / dot-separated nibbles IPv6 (§7.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{p}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Validated reverse-DNS name of the IP&lt;/td&gt;
&lt;td&gt;”do not use” (§7.2, §5.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{v}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;in-addr&lt;/code&gt; for IPv4, &lt;code&gt;ip6&lt;/code&gt; for IPv6&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{h}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HELO/EHLO domain&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{c}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client IP, human-readable&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;exp&lt;/code&gt; text only (§7.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{r}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Domain of the host performing the check&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;exp&lt;/code&gt; text only (§7.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%{t}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Current timestamp (seconds since epoch)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;exp&lt;/code&gt; text only (§7.2)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;All eleven SPF macro letters and their expansions (RFC 7208 §7.2)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three literal escapes complete the set: &lt;code&gt;%%&lt;/code&gt; is a percent sign, &lt;code&gt;%_&lt;/code&gt; a space, &lt;code&gt;%-&lt;/code&gt; a URL-encoded space (§7.1).&lt;/p&gt;

&lt;p&gt;Transformers modify the expansion inside the braces (§7.3). A digit keeps only that many right-hand parts: &lt;code&gt;%{d2}&lt;/code&gt; on &lt;code&gt;email.example.com&lt;/code&gt; gives &lt;code&gt;example.com&lt;/code&gt;. The letter &lt;code&gt;r&lt;/code&gt; reverses the parts on dots: with client IP 192.0.2.1, &lt;code&gt;%{i}&lt;/code&gt; is &lt;code&gt;192.0.2.1&lt;/code&gt; and &lt;code&gt;%{ir}&lt;/code&gt; is &lt;code&gt;1.2.0.192&lt;/code&gt;. Custom delimiters re-split on other characters — &lt;code&gt;%{l-}&lt;/code&gt; splits the local-part on hyphens — and parts are always rejoined with dots.&lt;/p&gt;

&lt;p&gt;The RFC’s own worked example in §7.4 ties it together. With client IP 192.0.2.3 and domain &lt;code&gt;email.example.com&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Macro expansion — RFC 7208 §7.4 worked example&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;%{ir}.%{v}._spf.%{d2}
→ 3.2.0.192.in-addr._spf.example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pair that with &lt;code&gt;exists:&lt;/code&gt; and you get per-IP authorization in a single lookup — the §5.7 example &lt;code&gt;v=spf1 exists:%{ir}.%{l1r+-}._spf.%{d} -all&lt;/code&gt; makes decisions “at the level of the user and client IP address.” So what does &lt;code&gt;%{i}&lt;/code&gt; mean in an SPF record? It is the connecting client’s IP address — dotted-quad for IPv4, dot-separated nibble format for IPv6 — and it is the macro that makes dynamic gateway records work (§7.2). You can expand any macro against a live IP with the &lt;a href="https://dmarcguard.io/tools/spf-macro-debugger/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;SPF macro debugger&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One caution from the RFC itself: records using &lt;code&gt;s&lt;/code&gt;, &lt;code&gt;l&lt;/code&gt;, &lt;code&gt;o&lt;/code&gt;, or &lt;code&gt;h&lt;/code&gt; macros defeat result caching, so §7.3 advises sending domains to avoid them with mechanism directives. Before publishing any macro, expand it against a real client IP in the &lt;a href="https://dmarcguard.io/tools/spf-macro-debugger/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;SPF macro debugger&lt;/a&gt; and check the constructed name resolves the way you expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a Receiver Evaluates Your Record
&lt;/h2&gt;

&lt;p&gt;Evaluation is first-match-wins: mechanisms are considered left to right, the first one that matches ends processing, and its qualifier becomes the result (§4.6.2). Order is not stylistic — it is the semantics.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdmarcguard.io%2Fimages%2Fblog%2Fspf-record-syntax%2Fspf-record-syntax_evaluation-order_flowchart.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdmarcguard.io%2Fimages%2Fblog%2Fspf-record-syntax%2Fspf-record-syntax_evaluation-order_flowchart.svg" alt="First-match-wins evaluation of the record v=spf1 ip4:192.0.2.0/24 -a mx ~all for client IP 198.51.100.7: ip4 and -a do not match, mx matches and evaluation stops with result Pass" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First-match-wins evaluation of an SPF record, step by step. The ~all is never reached.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Trace client IP 198.51.100.7 through this record:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;First-match-wins evaluation trace&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 ip4:192.0.2.0/24 -a mx ~all

1. ip4:192.0.2.0/24  → 198.51.100.7 not in range          → no match, continue
2. -a                → IP not among the domain's A records → no match, continue
3. mx                → IP matches an MX host's address     → MATCH — stop here
Result: Pass (the matching mechanism's implicit "+")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;~all&lt;/code&gt; is never reached. Now invert the case: if the IP &lt;strong&gt;had&lt;/strong&gt; matched step 2, the result would be Fail — and no later mechanism could rescue it. A &lt;code&gt;-&lt;/code&gt; qualifier early in the record is final for any IP it matches, no matter what an &lt;code&gt;include&lt;/code&gt; further right would have said. This also answers the ordering question directly: yes, mechanism order matters, because evaluation stops at the first match (§4.6.2).&lt;/p&gt;

&lt;p&gt;If nothing matches and there is no &lt;code&gt;redirect=&lt;/code&gt;, the default result is Neutral, “just as if &lt;code&gt;?all&lt;/code&gt; were specified as the last directive” (§4.7). The RFC recommends terminating explicitly with &lt;code&gt;all&lt;/code&gt; or &lt;code&gt;redirect=&lt;/code&gt; anyway — implicit defaults make debugging harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  DNS Lookup Limits: What Counts and What Doesn’t
&lt;/h2&gt;

&lt;p&gt;The rule, verbatim from §4.6.4: “SPF implementations MUST limit the total number of those terms to 10 during SPF evaluation … If this limit is exceeded, the implementation MUST return ‘permerror’.” Which terms count is fully enumerated:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Counts toward the 10?&lt;/th&gt;
&lt;th&gt;Extra caps (§4.6.4)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;include&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Nested lookups inside it also count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;≤10 address (A/AAAA) queries per MX evaluation, else PermError&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ptr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;≤10 address queries per PTR evaluation; excess records ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exists&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;redirect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ip4&lt;/code&gt;, &lt;code&gt;ip6&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Never query DNS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Looked up later, only on Fail — explicitly exempt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Which SPF terms count toward the 10-DNS-lookup limit (RFC 7208 §4.6.4)&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key finding:&lt;/strong&gt; 148,655 of 3,077,219 SPF-enabled domains — 4.8% — exceed the 10-lookup limit and risk PermError on every message they send (Source: DMARCguard SPF Supply Chain study — our scan of 5,499,028 Tranco domains, March 15, 2026)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That table is the whole of what this reference covers on limits. The second, separate cap on void lookups (§4.6.4), how to count a real record’s include chain, and the step-by-step reduction path when you are over budget all live in &lt;a href="https://dmarcguard.io/blog/spf-too-many-dns-lookups/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;fix SPF too many DNS lookups&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record Placement Rules: One Record, TXT Only, 255-Byte Strings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Exactly one SPF record per name
&lt;/h3&gt;

&lt;p&gt;A domain name “MUST NOT have multiple records that would cause an authorization check to select more than one record” (§3.2) — and the consequence is explicit, not vague: when record selection finds more than one &lt;code&gt;v=spf1&lt;/code&gt; record, check_host() “produces the ‘permerror’ result” (§4.5). Two SPF records don’t merge, don’t race, and don’t half-work. They fail authentication outright. If you have two, merge the mechanisms into one record.&lt;/p&gt;

&lt;h3&gt;
  
  
  TXT type only
&lt;/h3&gt;

&lt;p&gt;“SPF records MUST be published as a DNS TXT (type 16) Resource Record (RR) only” (§3.1). The dedicated SPF record type (type 99) from SPF’s experimental phase was dropped by the SPFbis working group, which concluded the dual-record-type model “was fundamentally flawed” (§3.1).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key finding:&lt;/strong&gt; 107,646 domains still publish the deprecated type-99 SPF record (Source: Czybik, Horlboge &amp;amp; Rieck, "Lazy Gatekeepers: A Large-Scale Study on SPF Configuration in the Wild," ACM IMC 2023)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The 255-byte limit is per string, not per record
&lt;/h3&gt;

&lt;p&gt;This is the rule the SERP most consistently gets wrong. The 255-octet maximum applies to a single &lt;em&gt;character-string&lt;/em&gt; inside a TXT record — not to the record. A TXT record can hold multiple quoted strings, and §3.3 is unambiguous: “If a published record contains multiple character-strings, then the record MUST be treated as if those strings are concatenated together without adding spaces.” So this:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One TXT record split across two quoted strings (§3.3)&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com. IN TXT "v=spf1 ip4:192.0.2.0/24 include:_spf.exam" "ple.com -all"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;evaluates as &lt;code&gt;v=spf1 ip4:192.0.2.0/24 include:_spf.example.com -all&lt;/code&gt; — note the split lands mid-token and concatenation heals it, because no space is inserted. That is the correct way to publish a record longer than 255 characters. The practical ceiling is elsewhere: §3.4 recommends keeping the whole DNS answer small enough to fit a 512-octet UDP packet (roughly 450 octets of name-plus-records), since oversized answers can be silently dropped by middleboxes that mishandle DNS over TCP.&lt;/p&gt;

&lt;h2&gt;
  
  
  SPF Record Examples
&lt;/h2&gt;

&lt;p&gt;Four annotated records, from minimal to macro-based. Lookup counts come from walking each chain with &lt;code&gt;dig +short TXT&lt;/code&gt; — provider records change (Google flattened &lt;code&gt;_spf.google.com&lt;/code&gt; from 4 lookups to 1 in December 2025), so always count again rather than trust a blog’s cached number.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Single provider (Google’s own published string — &lt;a href="https://knowledge.workspace.google.com/admin/security/set-up-spf" rel="noopener noreferrer"&gt;Set up SPF&lt;/a&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 include:_spf.google.com ~all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reads as: authorize Google’s servers; softfail everything else. Cost: 1 lookup — &lt;code&gt;_spf.google.com&lt;/code&gt; now resolves to a flat &lt;code&gt;ip4&lt;/code&gt;/&lt;code&gt;ip6&lt;/code&gt; list. The &lt;a href="https://dmarcguard.io/blog/google-workspace-spf-record/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;Google Workspace SPF record guide&lt;/a&gt; covers that include’s full expansion, merged records for third-party senders, and the &lt;code&gt;~all&lt;/code&gt; vs &lt;code&gt;-all&lt;/code&gt; question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Provider plus your own mail server:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 ip4:192.0.2.10 mx include:_spf.google.com -all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reads as: authorize one fixed IP, your MX hosts, and Google; fail everything else. Cost: 2 lookups (&lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;include&lt;/code&gt; — the &lt;code&gt;ip4&lt;/code&gt; is free).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Multi-SaaS — Google plus Microsoft 365 (a combination Google’s doc publishes verbatim):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 include:_spf.google.com include:spf.protection.outlook.com ~all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reads as: authorize both providers; softfail the rest. Cost: 2 lookups — both includes currently resolve to flat address lists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Macro-based dynamic authorization (Salesforce’s live record for &lt;code&gt;_spf.salesforce.com&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 exists:%{i}._spf.mta.salesforce.com -all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reads as: for each connection, ask whether &lt;code&gt;&amp;lt;client-ip&amp;gt;._spf.mta.salesforce.com&lt;/code&gt; has an A record; pass if yes, fail otherwise. One &lt;code&gt;exists&lt;/code&gt; lookup authorizes an entire fleet without listing a single address — the §5.7 pattern running in production.&lt;/p&gt;

&lt;p&gt;Want to assemble your own? The &lt;a href="https://dmarcguard.io/tools/spf-generator/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;SPF generator&lt;/a&gt; builds a record from your providers, and the &lt;a href="https://dmarcguard.io/tools/spf-checker/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;SPF checker&lt;/a&gt; validates the result against every rule on this page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common SPF Syntax Mistakes
&lt;/h2&gt;

&lt;p&gt;Genuine syntax errors are rarer than vendor marketing implies — and the honest numbers are worth stating. The largest peer-reviewed study of SPF configuration found that only 2.9% of 7,251,736 SPF-enabled domains had any error at all, and true syntax mistakes were 18.15% of those errors — roughly 0.5% of all SPF-enabled domains (Czybik et al., ACM IMC 2023, &lt;a href="https://arxiv.org/abs/2502.08240" rel="noopener noreferrer"&gt;arxiv.org/abs/2502.08240&lt;/a&gt;). Keep the categories separate: syntax errors, lookup-limit PermErrors, and permissive policy choices are three different problems.&lt;/p&gt;

&lt;p&gt;The mistakes that do occur are consistent. Each row below is an observed error class with its share of syntax errors where the IMC 2023 study measured it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mistake&lt;/th&gt;
&lt;th&gt;Wrong&lt;/th&gt;
&lt;th&gt;Right&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ipv4&lt;/code&gt; instead of &lt;code&gt;ip4&lt;/code&gt; (11.0% of syntax errors, IMC 2023)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ipv4:192.0.2.0/24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ip4:192.0.2.0/24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;§5.6 defines &lt;code&gt;ip4&lt;/code&gt;/&lt;code&gt;ip6&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Space after the colon (16.6%, IMC 2023)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;include: example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;include:example.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;§4.6.1 ABNF permits none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;=&lt;/code&gt; where &lt;code&gt;:&lt;/code&gt; belongs (Microsoft-documented common error)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ip4=192.0.2.0&lt;/code&gt; / &lt;code&gt;include=example.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ip4:192.0.2.0&lt;/code&gt; / &lt;code&gt;include:example.com&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Mechanisms take &lt;code&gt;:&lt;/code&gt;; only modifiers take &lt;code&gt;=&lt;/code&gt; (§4.6.1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More than one &lt;code&gt;v=spf1&lt;/code&gt; record (15.3%, IMC 2023)&lt;/td&gt;
&lt;td&gt;Two TXT records&lt;/td&gt;
&lt;td&gt;One merged record&lt;/td&gt;
&lt;td&gt;§3.2 / §4.5 → PermError&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terms after &lt;code&gt;all&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=spf1 -all include:...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=spf1 include:... -all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;§5.1 — silently ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPF concatenated with a verification string (7.0%, IMC 2023)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=spf1 ...google-site-verification=...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Separate TXT records&lt;/td&gt;
&lt;td&gt;§4.5 selection breaks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Observed SPF syntax mistakes: wrong form, right form, and the governing rule&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;=&lt;/code&gt;-for-&lt;code&gt;:&lt;/code&gt; confusion is documented by Microsoft among the most common errors its support teams see, alongside stray spaces and trailing periods (&lt;a href="https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure" rel="noopener noreferrer"&gt;Microsoft SPF doc&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If your DMARC reports already show PermError and you need the diagnosis path rather than the reference, that flow lives in &lt;a href="https://dmarcguard.io/blog/spf-permerror-fix/?utm_source=devto&amp;amp;utm_medium=content&amp;amp;utm_campaign=crosspost-2026&amp;amp;utm_content=spf-record-syntax" rel="noopener noreferrer"&gt;fix SPF PermError&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Can a domain have two SPF records?
&lt;/h3&gt;

&lt;p&gt;No. RFC 7208 permits exactly one SPF record per domain name (§3.2), and when record selection finds more than one &lt;code&gt;v=spf1&lt;/code&gt; TXT record, the result is PermError (§4.5) — authentication fails for every message. Merge the mechanisms from both records into a single &lt;code&gt;v=spf1&lt;/code&gt; record instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between include and redirect in SPF?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;include:&lt;/code&gt; matches only when the referenced record returns Pass, and evaluation continues if it doesn’t (§5.2). &lt;code&gt;redirect=&lt;/code&gt; applies only after every mechanism misses, replaces your record’s result entirely with the referenced domain’s, and is ignored whenever an &lt;code&gt;all&lt;/code&gt; mechanism is present (§6.1).&lt;/p&gt;

&lt;h3&gt;
  
  
  Which SPF mechanisms count toward the 10-lookup limit?
&lt;/h3&gt;

&lt;p&gt;Per RFC 7208 §4.6.4, the terms that count are &lt;code&gt;include&lt;/code&gt;, &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;mx&lt;/code&gt;, &lt;code&gt;ptr&lt;/code&gt;, and &lt;code&gt;exists&lt;/code&gt;, plus the &lt;code&gt;redirect&lt;/code&gt; modifier. The &lt;code&gt;ip4&lt;/code&gt;, &lt;code&gt;ip6&lt;/code&gt;, and &lt;code&gt;all&lt;/code&gt; mechanisms and the &lt;code&gt;exp&lt;/code&gt; modifier never query DNS during evaluation and are exempt. A separate cap of two void lookups also applies.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does %{i} mean in an SPF record?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;%{i}&lt;/code&gt; is a macro that expands to the connecting client’s IP address — dotted-quad format for IPv4, dot-separated nibble format for IPv6 (§7.2, §7.3). It is most often paired with &lt;code&gt;exists:&lt;/code&gt; to build per-IP hostnames, letting one lookup authorize a dynamic set of senders.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the order of mechanisms in an SPF record matter?
&lt;/h3&gt;

&lt;p&gt;Yes. Mechanisms are evaluated left to right, the first match stops processing, and that mechanism’s qualifier becomes the result (§4.6.2). An early &lt;code&gt;-&lt;/code&gt; qualifier is final for any IP it matches — no later &lt;code&gt;include&lt;/code&gt; can override it — and terms after &lt;code&gt;all&lt;/code&gt; are never evaluated (§5.1).&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I publish an SPF record longer than 255 characters?
&lt;/h3&gt;

&lt;p&gt;Split it into multiple quoted strings of at most 255 bytes each, inside one TXT record. Receivers must concatenate the strings without adding spaces (§3.3), so the split can even fall mid-token. Keep the full DNS answer under roughly 450 octets so it fits a single UDP packet (§3.4).&lt;/p&gt;

&lt;h2&gt;
  
  
  The reference, in four lines
&lt;/h2&gt;

&lt;p&gt;SPF record syntax rewards precision, and the whole standard reduces to a short list worth keeping:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One record, TXT only.&lt;/strong&gt; Exactly one &lt;code&gt;v=spf1&lt;/code&gt; TXT record per name; a second one is a PermError, not a merge (§3.1, §3.2).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First match wins.&lt;/strong&gt; Left to right, stop at the first matching mechanism, return its qualifier (§4.6.2). Order is policy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Know your lookup budget.&lt;/strong&gt; Ten DNS-querying terms, two void lookups — exceed either and every message fails (§4.6.4).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Macros are standard, not folklore.&lt;/strong&gt; Eleven letters in §7 power the dynamic records Salesforce runs in production today.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This article was created with the help of AI.&lt;/p&gt;

&lt;p&gt;Originally published at &lt;a href="https://dmarcguard.io/blog/spf-record-syntax/" rel="noopener noreferrer"&gt;dmarcguard.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>email</category>
      <category>dns</category>
      <category>security</category>
      <category>sysadmin</category>
    </item>
  </channel>
</rss>
