DEV Community

Cover image for SPF, DKIM and DMARC for Developers: Why Your Contact Form Emails Land in Spam
Sara Casciaro
Sara Casciaro

Posted on

SPF, DKIM and DMARC for Developers: Why Your Contact Form Emails Land in Spam

The contact form submits. The success message appears. The server log says the mail was handed off without errors. And the client, three weeks later, asks why nobody ever answered their enquiry.

Nothing crashed. The email was delivered straight to spam, or silently rejected by the receiving server, because the domain it claimed to come from had never said the sending server was allowed to use its name.

This is one of the most common silent failures on small business websites, and it sits in a blind spot: developers treat email as "the server's problem", hosting providers treat it as "the site's problem", and nobody reads the headers.

Let's read them.

The three records, in one sentence each

  • SPF says which servers are allowed to send mail for a domain.
  • DKIM adds a cryptographic signature to each message, proving the content wasn't altered and linking it to a domain.
  • DMARC tells receiving servers what to do when SPF and DKIM fail, and checks that the passing domain actually matches the one in the visible From address.

All three are DNS TXT records, which means you can inspect any of them from a terminal.

Reading SPF

$ dig +short TXT example.com
"v=spf1 include:_spf.google.com include:spf.brevo.com ~all"
"google-site-verification=pQ3n1b0rK..."
Enter fullscreen mode Exit fullscreen mode

The SPF record is the one starting with v=spf1. This one authorises Google Workspace and Brevo, and ends with ~all, a soft fail for everything else.

Now the classic bug. The site's contact form sends through the web hosting server itself, whose outgoing IP appears nowhere in that record. Every form notification fails SPF.

The second classic bug is the lookup limit. SPF allows at most 10 DNS lookups while evaluating a record, and every include can trigger more. Add a newsletter tool, a CRM, a helpdesk and an invoicing platform over the years, and eventually the record breaks for everyone. The receiving server then reports:

Enter fullscreen mode Exit fullscreen mode

A permerror is worse than a fail: the record is effectively broken for all senders, including the legitimate ones.

There can also be only one SPF record per domain. Two separate v=spf1 TXT records don't combine, they invalidate each other. This happens constantly when a second service's setup guide says "add this SPF record" and someone adds instead of merging.

Reading DKIM

DKIM keys live under a selector, which you find in the headers of a signed message (s= in the DKIM-Signature header). With the selector, you can query the public key:

$ dig +short TXT google._domainkey.example.com
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu3hX..."
Enter fullscreen mode Exit fullscreen mode

If that query returns nothing, the signature on outgoing mail can't be verified and DKIM fails. Every sending service, whether your mailbox provider, your newsletter tool or a transactional email API, has its own selector and its own key to publish. Setting up DKIM for your mailbox does nothing for the form notifications if they go out through a different service.

Reading DMARC

$ dig +short TXT _dmarc.example.com
"v=DMARC1; p=none; rua=mailto:dmarc-reports@example.com; fo=1"
Enter fullscreen mode Exit fullscreen mode

p=none means "monitor, don't act". p=quarantine asks receivers to send failing mail to spam, and p=reject asks them to refuse it outright. The rua address receives daily aggregate reports, which are XML files listing every server that sent mail claiming to be your domain. Reading a few of these is usually how you discover the forgotten service that has been sending on your behalf for years.

The part most people miss is alignment. DMARC doesn't just ask whether SPF or DKIM passed. It asks whether they passed for the same domain that appears in the From header. A message can pass SPF for the hosting provider's domain and still fail DMARC for yours.

The header that tells you everything

Send a test from your form to a Gmail address, open the message, choose Show original, and look for this block:

Enter fullscreen mode Exit fullscreen mode

Three passes, all for example.com. That's a healthy message.

Here is what a typical broken contact form produces:

Enter fullscreen mode Exit fullscreen mode

No DKIM line at all, because nothing signed the message. SPF soft-fails, because the hosting server's IP isn't in the record. DMARC fails, and since the domain publishes p=quarantine, the receiver does exactly what it was told: straight to spam. Nothing in the site's own logs will ever show this.

The form-specific trap: sending "from" the visitor

A lot of contact forms set the From header to the visitor's email address, so that hitting "reply" goes straight to them. It feels convenient. It's also a guaranteed DMARC failure whenever the visitor uses a domain with a strict policy, because your server is not authorised to send mail as @yahoo.com, and Yahoo publishes p=reject.

The correct pattern is:

Enter fullscreen mode Exit fullscreen mode

The message is sent by your domain, authenticated as your domain, and replying still reaches the visitor. Same convenience, no spoofing.

Why this matters more since 2024

In February 2024, Google and Yahoo started enforcing stricter requirements for bulk senders: SPF and DKIM on the sending domain, a DMARC record (even at p=none), and one-click unsubscribe for marketing mail. A contact form isn't bulk mail, but the direction is clear. Unauthenticated mail is increasingly treated as suspicious by default, and "it used to arrive" is no longer evidence that it will.

What the DMARC reports actually show

The rua address in your DMARC record starts receiving aggregate reports, usually once a day per receiving provider, as compressed XML attachments. They look intimidating, but a single record tells you most of what you need:

<record>
  <row>
    <source_ip>185.62.xx.xx</source_ip>
    <count>47</count>
    <policy_evaluated>
      <disposition>none</disposition>
      <dkim>fail</dkim>
      <spf>fail</spf>
    </policy_evaluated>
  </row>
  <identifiers>
    <header_from>example.com</header_from>
  </identifiers>
  <auth_results>
    <spf>
      <domain>server42.hosting-provider.net</domain>
      <result>pass</result>
    </spf>
  </auth_results>
</record>
Enter fullscreen mode Exit fullscreen mode

Read it from the top. Forty-seven messages came from an IP you don't recognise, claiming to be example.com. SPF actually passed, but for the hosting provider's own domain, not yours, so DMARC counts it as a failure: that's alignment in action. Because the policy is still p=none, the disposition was none and the mail was delivered anyway.

That IP is very often the web server itself, sending form notifications and order confirmations nobody ever thought of as "email infrastructure". This is exactly why you start at p=none: the reports reveal every legitimate sender you forgot, you fix them, and only then do you move to quarantine and eventually reject. Tightening the policy before reading the reports is how companies block their own invoices.

A reverse lookup usually identifies the source in seconds:

$ dig +short -x 185.62.xx.xx
server42.hosting-provider.net.
Enter fullscreen mode Exit fullscreen mode

If the report shows sources you truly can't account for, that's the other thing DMARC is for: someone else sending mail in your name. Moving to p=reject is what stops them.

A checklist you can run in ten minutes

  1. dig +short TXT yourdomain.com and confirm there is exactly one v=spf1 record.
  2. Make sure every service that sends as your domain is in it: mailbox, newsletter, form delivery, invoicing, helpdesk.
  3. Count the include mechanisms and remember the 10 lookup ceiling.
  4. For each sending service, publish its DKIM key and confirm the selector resolves with dig.
  5. Publish a DMARC record, starting at p=none with a rua address, and read the reports for a couple of weeks before tightening the policy.
  6. Send a test from the form to Gmail, open Show original, and read Authentication-Results line by line.
  7. Make sure the form sends from your domain and puts the visitor in Reply-To.

Email authentication isn't glamorous and rarely appears in a project brief. But a website whose contact form quietly loses leads is failing at the one job most small businesses built it for, and the fix is a few DNS records and ten minutes of reading headers.


Written by Sara Casciaro, founder of Sabriel Agency, digital studio in Ugento (LE), Italy.

Top comments (0)