Every check reads DNS through a resolver, a module that implements MailAuth.Resolver. Pick one with the :resolver option.

Inet

MailAuth.Resolver.Inet, the default, asks DNS servers with OTP's :inet_res, using EDNS0 so that 2048-bit DKIM keys fit. Pick the servers with nameservers: [{{192, 0, 2, 53}, 53}].

Cached

MailAuth.Resolver.Cached keeps answers in ETS for their TTL, capped at one hour. "No such record" answers are kept too; failures are not.

children = [
  {MailAuth.Resolver.Cached, resolver: MailAuth.Resolver.Inet, max_size: 10_000}
]

Static

MailAuth.Resolver.Static answers from a map. Use it in tests:

records = %{
  "example.com" => [txt: "v=spf1 ip4:192.0.2.0/24 -all", mx: {10, "mx.example.com"}],
  "mx.example.com" => [a: {192, 0, 2, 1}],
  "broken.example" => :temperror
}

MailAuth.SPF.check("192.0.2.1", "a@example.com", "mx.example.com",
  resolver: MailAuth.Resolver.Static,
  records: records
)

MailAuth.Resolver.Static.parse_file/1 reads records from a file, as mix mail_auth.verify --dns-file does.

Limits

DNS answers come from strangers, so everything is bounded:

  • :deadline (default 15000 ms) for the whole call; a check still waiting at the deadline is :temperror, reason :deadline
  • :timeout (default 5000 ms) for each query
  • TXT records over 4 KB are ignored
  • SPF's lookup limits, and at most 8 queries for a DMARC tree walk

A failure to reach DNS is :temperror, never :fail.

Your own resolver

defmodule MyResolver do
  @behaviour MailAuth.Resolver

  @impl true
  def lookup(name, :txt, _opts) do
    case MyDNS.txt(name) do
      {:ok, records} -> {:ok, records, 300}
      :nxdomain -> {:error, :nxdomain, 300}
      _ -> {:error, :temperror, 0}
    end
  end

  def lookup(_name, _type, _opts), do: {:error, :temperror, 0}
end