Macros are the part of SPF that almost no course covers, and they are how the largest senders on the internet stay inside the lookup budget. Once you can expand one by hand, records that looked like line noise become readable.
The shape
%{ir}
%opens a macro.{…}delimit it.iis the macro letter — which value to substitute.ris a transformer — here, reverse the parts.
The letters worth knowing
| Letter | Expands to | For 1.2.3.4 sending as user@acme.com |
|---|---|---|
%{i} | The connecting IP. | 1.2.3.4 |
%{d} | The domain being evaluated. | acme.com |
%{s} | The whole sender address. | user@acme.com |
%{l} | The local part. | user |
%{o} | The sender’s domain. | acme.com |
%{h} | The HELO name. | mail.acme.com |
%{v} | The literal "in-addr" or "ip6". | in-addr |
RFC 7208 §7.1 has the full set. These seven cover everything you will meet in the wild.
Transformers
r— reverse.%{ir}turns 1.2.3.4 into 4.3.2.1, which is the order reverse-DNS names use.- A digit — keep the rightmost N parts.
%{d2}onmail.eu.acme.comgivesacme.com. - Both together, digit after r.
%{ir4}is reverse, then keep four. - Uppercase means URL-escape.
%{S}is the sender percent-encoded — for macros that end up in anexpURL.
Three literals you will see
%% is a literal percent sign, %_ a space, and %- an escaped space (%20). They exist because a macro string may end up inside a URL.
Expanding one by hand
Take the record booking.com published on 2026-09-15:
v=spf1 include:%{ir}.%{v}.%{d}.spf.has.pphosted.com -allFor a connection from 1.2.3.4 evaluating booking.com:
%{ir} -> 4.3.2.1 (i reversed)
%{v} -> in-addr (IPv4)
%{d} -> booking.com
include:4.3.2.1.in-addr.booking.com.spf.has.pphosted.comAnd that name really resolves — which is how you check your expansion is right rather than merely plausible:
"v=spf1 exists:%{i}._spf.sparkpostmail.com exists:%{ir}.spf.mail.service-now.com -all"One include in the published record, and behind it a per-connection answer assembled from the connecting address itself. The next lesson is about why anyone would build that.