Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Count number of lookups in an SPF record.
#!/usr/bin/env ruby
# 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.
# Another safeguard is the maximum of ten mechanisms querying DNS, i.e.
# any mechanism except from IP4, IP6, and ALL. Implementations can abort
# the evaluation with result SOFTERROR when it takes too long or a DNS
# query times out, but they must return PERMERROR if the policy directly
# or indirectly needs more than ten queries for mechanisms.
# Any redirect= also counts towards this processing limit.
# A typical SPF HELO policy v=spf1 a -all may execute up to three DNS
# queries: (1) TXT, (2) SPF (obsoleted by RFC 7208), and (3) A or AAAA.
# This last query counts as the first mechanism towards the limit (10).
# In this example it is also the last, because ALL needs no DNS lookup.
require "resolv"
def lookup(domain, depth=0)
records =, Resolv::DNS::Resource::IN::TXT)
record = records.find { |r|"v=spf")
if !record
puts "Warning: Missing SPF record for #{domain}"
return 0
record =
puts "#{domain}: #{record}" if depth == 0 || $verbose
mechanisms = record.split(" ")[1..-1].map do |m|
# remove qualifiers
if %w[+ ? ~ -].include?(m[0])
end do |m|
# everything except "ip4:", "ip6:", and "all" count towards a limit
!m.start_with?("ip4:","ip6:") && m != "all"
# puts "mechanisms: #{mechanisms}"
lookups = 1 # count the lookup we just did
mechanisms.each do |m|
if m.start_with?("include:", "redirect=")
new_domain = m.match(/^(?:include:|redirect=)(.+)$/)[1]
domain_lookups = lookup(new_domain, depth+1)
puts "#{new_domain}: #{domain_lookups} lookup#{"s" if domain_lookups != 1}" if depth == 0 || $verbose
lookups += domain_lookups
elsif m == "a"
lookups += 1
abort("Unsupported: #{m}")
return lookups
$verbose = false
if ARGV.first == "-v"
$verbose = true
if !ARGV.first
puts "Please supply the domain name as the first argument."
puts "Total: #{lookup(ARGV.first)} lookups"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment