The actual reason your form emails land in spam
Most people set up a contact form, get the "email sent" success message, and then notice their own notification arrived in spam. The instinct is to blame the form host. It's almost never the form host. It's DNS.
Here's what actually happens. Your form backend collects a submission and tries to email you@yourdomain.com. The notification email has a From header — that From claims a domain. Gmail, Outlook, and every other modern mail provider then ask three questions in DNS, in milliseconds, before the message hits your inbox:
- SPF: Is the IP address that connected to me allowed to send mail for the From domain?
- DKIM: Is this message cryptographically signed by a key published in DNS for the From domain?
- DMARC: If SPF or DKIM fail, what policy does the From domain ask receivers to apply (none, quarantine, reject)?
If any of those answers comes back wrong, the message is statistically more likely to be spam than legitimate, and into the spam folder it goes. The fix is to make all three line up. That's the entire post.
If the form itself never delivered, that's a different problem — see contact form not working.
The six root causes, in order of frequency
After diagnosing this for hundreds of indie sites, here's the ranked list of what causes deliverability failure on contact forms:
- Mismatched From header. The form puts the visitor's email in the From field. The visitor's domain didn't authorize your SMTP server, so SPF instantly fails. This is the single most common mistake in DIY contact forms.
- Missing SPF/DKIM/DMARC alignment. You publish SPF for one domain but send From a different one. Or you publish DKIM but the signing domain doesn't match the From domain. DMARC requires both authentication AND alignment.
- No PTR record (reverse DNS). If you run your own SMTP on a VPS, and the connecting IP doesn't reverse-resolve, Gmail downgrades you. Generic cloud hostnames (
ec2-...amazonaws.com) are nearly as bad as no PTR. - Shared SMTP reputation. Free PHP mail() relays, hostinger's shared SMTP, default Heroku/Render mail — all share IP pools with thousands of other senders. One spammer poisons the pool.
- Reply-To set as From. The form developer wanted "hit Reply to respond to the visitor" behavior, so they jammed the visitor's email into From. The right field is Reply-To.
- No List-Unsubscribe header. Required for bulk senders since Feb 2024. Not strictly enforced for transactional one-offs, but absence is one more spam-score point against you.
The top two account for ~80% of the cases I see. If your contact form emails are landing in spam, start with From and SPF.
How to diagnose with dig and Gmail's "Show original"
Before you change anything, find out what's actually failing. Two tools: dig on the command line, and Gmail's built-in header viewer.
Step A: Check current DNS with dig
Open a terminal and run these against the domain in your From header:
# SPF (look for "v=spf1 ..." in the answer)
dig +short TXT yourdomain.com
# DKIM (selector depends on provider; "google", "k1", "s1" are common)
dig +short TXT google._domainkey.yourdomain.com
dig +short TXT k1._domainkey.yourdomain.com
# DMARC
dig +short TXT _dmarc.yourdomain.comIf any of those returns empty, that record doesn't exist. That's your problem.
Step B: Read the actual headers in Gmail
In Gmail, open the email that went to spam. Three-dot menu in the top right of the message → Show original. A new tab opens with the full headers. Near the top you'll see a summary block like this:
SPF: PASS with IP 209.85.220.41
DKIM: 'PASS' with domain mg.yourdomain.com
DMARC: 'FAIL' p=NONE sp=NONE dis=NONEDMARC FAIL with SPF PASS and DKIM PASS means an alignment failure — the From domain doesn't match the SPF/DKIM domains. That's the fix in 99% of these cases: change From to match the authenticated domain, or authenticate the From domain.
Also scroll down to the Received-SPF: and Authentication-Results: headers — they explain in plain English what passed and what didn't.
Step-by-step fix for SPF, DKIM, and DMARC
Pick the registrar where your domain lives — Cloudflare, Route 53, Google Domains (now Squarespace Domains), Namecheap, anything else. The TXT record values are identical across registrars; only the UI differs.
SPF (one record, one line)
SPF is a single TXT record at the apex of your domain. You can only have one SPF record — multiple records is itself a failure. Combine sources with includes:
# Name: @ (or yourdomain.com)
# Type: TXT
# Value: v=spf1 include:_spf.google.com include:mailgun.org ~allCommon includes: _spf.google.com (Gmail/Workspace), amazonses.com (SES), mailgun.org (Mailgun), spf.mandrillapp.com (Mandrill), sendgrid.net (SendGrid). End with ~all (softfail) until you've verified, then move to -all (hardfail).
DKIM (provider-specific, paste their record)
DKIM uses a public key published in DNS. Your SMTP provider gives you the exact record. Examples:
- SES: AWS console → SES → Verified identities → DKIM tab. You get three CNAME records like
xxxx._domainkey.yourdomain.com → xxxx.dkim.amazonses.com. Paste all three. - Mailgun: Domain settings shows one TXT record at
k1._domainkey.yourdomain.com. - Postmark: Sender Signatures → DNS records → one TXT at
20240101pm._domainkey.yourdomain.com(selector includes a date). - Google Workspace: Admin → Apps → Google Workspace → Gmail → Authenticate email → Generate new record. Publishes at
google._domainkey.yourdomain.com.
On Cloudflare, paste these as TXT records and turn the proxy off (grey cloud). Cloudflare's proxy is HTTP only — it doesn't affect TXT records, but make sure you're creating TXT not CNAME unless the provider explicitly says CNAME.
DMARC (one record, start soft)
Publish at _dmarc.yourdomain.com:
# Name: _dmarc
# Type: TXT
# Value: v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; aspf=r; adkim=rStart with p=none — "monitor only, don't reject." You'll get weekly aggregate reports at the rua address showing exactly who's sending mail claiming to be your domain. After two weeks of clean reports, tighten to p=quarantine, then eventually p=reject.
From vs Reply-To best practice
This is the single most misunderstood concept in contact forms. Read this section twice.
From is an authentication claim. It must be a domain you control with proper DNS. Receivers verify against it.
Reply-To is a routing hint for human reply behavior. When you hit Reply in your email client, that's where the response goes. It has zero impact on deliverability or spam scoring.
The right pattern for a contact form notification is:
From: notifications@yourdomain.com (or noreply@formhost.com)
Reply-To: visitor@gmail.com (whatever they typed)
Subject: New contact form submission
To: you@yourdomain.comNow when the notification hits your inbox, you hit Reply and your reply goes directly to the visitor. SPF and DKIM authenticate against yourdomain.com, which you control. Everyone wins.
The wrong pattern (and the cause of most spam issues):
From: visitor@gmail.com <-- WRONG: you don't control gmail.com SPF
Reply-To: (not set)
Subject: New contact form submission
To: you@yourdomain.comNow SPF tries to validate that your SMTP server is allowed to send for gmail.com. It isn't. Gmail knows it isn't. Spam folder, every time.
Custom SMTP via SES, Mailgun, or Postmark
Once you understand the From/Reply-To split, the next question is which SMTP service to send through. For contact forms (transactional, low volume), three good options:
AWS SES
Cheapest by far at $0.10 per 1,000 emails. Excellent deliverability. The setup friction: you start in sandbox mode (can only send to verified addresses) and have to request production access via a support ticket. Approval takes a few hours to a day. SES gives you three DKIM CNAMEs to publish — once verified, it's rock solid.
Mailgun
Free tier: 100 emails/day forever (as of 2026-05). Friendlier UI than SES. The DKIM setup is one TXT record. Pay-as-you-go after that at $0.80 per 1,000. Good for small sites that don't want to deal with AWS support tickets.
Postmark
Premium pricing ($15/month minimum for 10k emails) but the best deliverability and support I've used. They aggressively police their IP reputation. If you're sending password resets and contact notifications and absolutely cannot afford spam folder, Postmark is the safe choice.
Gmail App Password (cheapest if you already have Workspace)
If your domain is already on Google Workspace, generate an App Password from your account security settings and use smtp.gmail.com:587 as your SMTP server. Google handles SPF, DKIM, and DMARC for you. The catch: From must be a Gmail address or a verified alias on the same Workspace account. Hard limit: 2,000 emails/day for Workspace, 500/day for free Gmail.
For most contact forms (under 100 submissions/day), Gmail App Password or SES is the right answer. Don't over-engineer this.
How splitforms handles this by default
The form-to-email category has historically forced you into one of two bad choices: either use the form host's shared SMTP (subject to their reputation, often middling) or run your own from-scratch SMTP setup (high friction). Splitforms goes a third way.
By default, splitforms sends through your own SMTP. Plug in any of the following on signup:
- Gmail App Password (1-minute setup, free up to 500/day)
- AWS SES credentials (cheapest at scale)
- Mailgun, Postmark, SendGrid, or any standard SMTP
This means SPF, DKIM, and DMARC are evaluated against your sending domain — the one you already control and authenticate. Splitforms doesn't put a third-party header in the From line, so there's no alignment surprise.
The other deliberate choice: splitforms sets From to a domain you've verified, and Reply-To to the visitor's submitted email. That single decision avoids the most common contact-form deliverability bug. You hit Reply on the notification and it goes to the visitor; SPF and DKIM still pass because the From domain is yours.
Plans: free up to 1,000 submissions/month, $5/mo Pro for 5,000, or $59 for 4 years if you prefer a one-time payment. Webhooks, AI spam filtering, and custom SMTP are on every plan. See the free contact form template or compare to Formspree, Web3Forms, and Basin.
For framework-specific setups, see Next.js, React, Astro, and Svelte integration guides.
Verify the fix with mail-tester.com
After publishing DNS changes, wait 5–10 minutes for propagation, then verify. The fastest tool is mail-tester.com:
- Go to
mail-tester.com(no signup) - Copy the generated test address (e.g.
test-xyz123@srv1.mail-tester.com) - Submit your contact form with that address as the visitor email — but actually, you want the notification email sent through your stack, so paste the test address into your To: recipient field in the form host's settings, then trigger a submission
- Click Then check your score
- You get a score out of 10 with line-by-line breakdown
Aim for 9/10 or 10/10. Common deductions and quick fixes:
- -1.5: SPF doesn't pass → check From domain matches a domain in your SPF record
- -1: DKIM not signed → publish the DKIM record your provider gave you
- -0.5: No DMARC → publish the DMARC record from earlier in this post
- -0.1: Missing List-Unsubscribe → for transactional contact-form emails this is usually ignorable
- -1+: Body / HTML formatting issues → less common but worth fixing if mail-tester flags specific bytes
Re-run mail-tester after each DNS change. Don't change five things at once — change one, retest. After you hit 9+/10, send a real test to Gmail, Outlook, and (if your audience uses them) iCloud. Inbox placement on all three is your real success metric.
If you're still landing in spam at 9/10 mail-tester score, the remaining variable is recipient-side reputation — recipients who've previously marked similar emails as spam. Time and Not Spam clicks fix that.
FAQ
Why does Gmail mark my own contact form emails as spam?
Because the notification email's From header claims to be from your domain (e.g. hello@yoursite.com) but the actual SMTP server sending it is not authorized to send for your domain in DNS. Gmail checks SPF, DKIM, and DMARC alignment in milliseconds. If even one check fails or is missing, Gmail's filter is statistically right more often than not, so it lands in spam. Fix is to either send From your form host's verified domain (with Reply-To set to the visitor), or add SPF/DKIM/DMARC records for your sending domain.
What's the difference between From and Reply-To for contact forms?
From is who Gmail/Outlook authenticates against DNS — it must be a domain whose SMTP you control. Reply-To is who gets the message when you hit Reply — this should be the visitor's email. The classic mistake is putting the visitor's address in From, which immediately fails SPF because the visitor's domain didn't authorize your server to send. Always set From to a domain you control (notifications@yoursite.com or noreply@formhost.com) and Reply-To to the visitor.
Do I need all three: SPF, DKIM, and DMARC?
In 2026, yes. Since Gmail and Yahoo's February 2024 bulk-sender enforcement, having only SPF is no longer enough for reliable inbox placement. DKIM is the cryptographic signature that survives forwarding; SPF only checks the connecting IP. DMARC tells receivers what to do when SPF/DKIM fail and gives you reporting. For low-volume contact forms you can survive with just SPF + DKIM aligned, but adding DMARC with `p=none` costs nothing and gives you visibility into what's failing.
Can I use Gmail SMTP to send my form notifications?
Yes, and it's the simplest option for low volume (under ~500/day). Generate an App Password from your Google account, use smtp.gmail.com:587 with your Gmail address as username. Google handles SPF, DKIM, and DMARC for you because the email is genuinely sent by Google. The catch: From must be your Gmail address (or a verified alias) — you can't send From hello@yourdomain.com via Gmail SMTP unless that domain is on Google Workspace and configured as a Send Mail As alias.
How do I check what's actually failing in Gmail?
Open the email in Gmail, click the three-dot menu, choose Show original. You get the full raw headers. Look for three lines near the top: SPF, DKIM, and DMARC. Each says PASS, FAIL, NEUTRAL, or NONE. PASS on all three with `header.from=yourdomain.com` aligned is what you want. If SPF passes but DKIM fails, your signing record is wrong. If both pass but DMARC fails, the From domain doesn't match the signed domain (alignment failure). Fix whichever one says FAIL.
What's a PTR record and do I really need one?
PTR (reverse DNS) maps an IP address back to a hostname. Big mail providers run a reverse lookup on the connecting IP — if it doesn't resolve, or resolves to something generic like `ec2-3-21-44-99.compute.amazonaws.com`, deliverability tanks. If you run your own SMTP, ask your hosting provider to set a PTR record pointing at your mail hostname. If you use SES, Mailgun, Postmark, SendGrid, or Gmail SMTP, the provider has already set PTRs for their IPs — you don't have to touch this.
Will adding SPF/DKIM/DMARC fix already-delivered spam emails?
No. Once an email lands in spam, that delivery is final. But once you fix DNS, future emails to that same recipient have a clean shot. Recipients can also mark your previous emails Not Spam, which feeds back into Gmail's per-user reputation model and accelerates recovery. After a DNS fix, send a fresh test, mark it Not Spam if it lands wrong, then send another. Within a few sends Gmail starts placing them in the inbox.
Does splitforms handle this automatically?
Splitforms sends via your configured SMTP — bring your own Gmail App Password, AWS SES, Mailgun, or Postmark — so the SPF/DKIM/DMARC records belong to a domain you authenticated. We default to setting From to a domain you control and Reply-To to the visitor, which avoids the most common alignment failure. The dashboard also surfaces the last 10 bounce/spam events from your provider so you can spot a deliverability regression within minutes instead of weeks.
More troubleshooting reads: contact form not working, all posts, and the splitforms FAQ. Docs: /docs and /api-reference.