send.webforms.toRead the docs →

how-toemaildeliverability

Contact form emails going to spam: the five causes, and how to fix each

Nearly every case comes down to whose address is on the message and who is allowed to send it. How to read the headers, fix the cause, and what we decided for our own notifications.

WebForms 8 min read

A contact form notification is about the most innocent email there is: your own website writing to your own inbox about a message someone typed into it. It still lands in junk, or never arrives, with depressing regularity. The receiving server has no idea the message came from your form. It sees an email claiming to be from somebody, sent by some server, and it checks whether those two facts agree. When they do not, it does exactly what it was built to do.

There are five usual causes. Before working through them, spend two minutes finding out which one you have, because the fixes are different and installing a plugin at random often fixes the wrong thing.

Start with the headers, not a plugin

Open one notification that landed in spam and look at its raw source. In Gmail on a computer that is More, next to Reply, then Show original; other clients call it view source or message details. Find the Authentication-Results header. It is a standard field (RFC 8601) where the receiving server records what it checked, and it usually looks something like this:

a typical failing notification
Authentication-Results: mx.example.net;
  spf=pass smtp.mailfrom=server42.sharedhost.example;
  dkim=none;
  dmarc=fail header.from=yahoo.com

Three results, three questions. spf says whether the server that connected is allowed to send for the envelope domain. dkim says whether the message carries a valid signature, and for which domain. dmarc says whether either of those passing domains matches the domain in the From line the reader actually sees. The example is the classic contact form failure in one screen: SPF passes, but for the hosting company's machine; nothing is signed; and the From line says yahoo.com, which is neither.

1. The form sends From the visitor's address

Check this one first. Plenty of form plugins and hand-written handlers want Reply to reach the person who wrote in, so they put that person's address in From: a message claiming to be from jane@yahoo.com, sent by your web server. Your server is not one Yahoo authorises and cannot sign for yahoo.com, so DMARC fails, and the receiving server applies whatever policy Yahoo publishes for its domain.

Those policies differ by provider, which explains the most confusing symptom of all: some submissions arrive and some never do. When we checked on 24 October 2026, yahoo.com published p=reject (refuse the message), icloud.com published p=quarantine (deliver it to junk), and gmail.com published p=none for the domain itself, while Google's sender guidelines tell senders plainly not to impersonate Gmail From headers. A form that forges From succeeds or fails depending on which provider each visitor happens to use. You can look up any domain's policy yourself:

Terminal
dig +short TXT _dmarc.yahoo.com
"v=DMARC1; p=reject; pct=100; ..."

The fix is the convention every careful sender follows. From is an address on a domain you control and have authenticated, and the visitor goes in Reply-To. Authentication passes, and Reply still reaches the visitor, because RFC 5322 defines Reply-To as the address the author suggests replies be sent to, and mail clients honour it.

the headers, before and after
Before:
From: Jane Doe <jane@yahoo.com>
To: hello@yourcompany.com

After:
From: Website <forms@yourcompany.com>
Reply-To: Jane Doe <jane@yahoo.com>
To: hello@yourcompany.com

2. Unauthenticated mail() on shared hosting

WordPress sends email through wp_mail(), and out of the box that hands the message to PHP's mail() function rather than an authenticated mail server. On shared hosting, mail() means the machine's own mail software, sending under whatever hostname the host gave the server, usually with no DKIM signature for your domain. Even with a correct From address on your own domain, the message is authenticated as the hosting server if at all, and DMARC alignment usually fails because that server's domain is not yours.

The fix is to stop sending through the web server. Send through an authenticated mailbox or a transactional email provider, over SMTP or its API, with SPF and DKIM set up for your domain at that provider. On WordPress an SMTP plugin does the site side; the DNS side is a handful of records your provider tells you to publish.

3. Someone else's reputation

Receiving servers score the sending IP address as well as the domain. On shared hosting your mail leaves from the same address as every other site on the machine, and if one of them is a compromised install sending pharmacy spam, your notifications inherit its record. Nothing in your own DNS fixes that, and nobody tells you it happened. It is the strongest argument for the fix above: once mail goes out through a provider that manages its sending reputation, the web server's IP is no longer part of the story.

4. SPF without DKIM, or no DMARC at all

SPF on its own is fragile. It checks the server that connected, so it breaks as soon as a message is forwarded, which is precisely what happens when hello@ forwards to someone's personal inbox. DKIM signs the message itself, and the signature survives forwarding as long as the body is not rewritten. Google's sender guidelines require SPF or DKIM from everyone sending to Gmail accounts, and SPF, DKIM and DMARC from anyone sending more than 5,000 messages a day to Gmail accounts. A contact form is nowhere near that volume, but a message carrying both is simply easier to trust.

If your domain has no DMARC record, publish one in monitoring mode first. p=none changes nothing about delivery, and the aggregate reports show you every server that sends as your domain before you tighten anything:

DNS
_dmarc.yourcompany.com.  TXT  "v=DMARC1; p=none; rua=mailto:dmarc@yourcompany.com"

Move to quarantine or reject only once the reports show every legitimate sender passing. Tighten it first and the contact form is the least of what breaks: invoicing tools, CRMs and newsletter platforms that send as your domain all need authorising too.

5. The content, and the spam your form relays

Content scoring matters less than authentication, but contact forms have a content problem of their own: they relay whatever a bot types. A notification stuffed with links and pharmacy keywords looks like spam because it is spam, wrapped in your template. When you or a colleague mark those as spam, the complaints count against the sender, and next week's real enquiry arrives carrying a reputation the bots built. Filter before sending, not after. A honeypot field stops the laziest bots, and anything suspicious that gets past it should be held back rather than emailed.

WordPress and Contact Form 7, specifically

Contact Form 7's own documentation gives the same advice as cause 1. Its best-practice page for mail ends with "in the From field, use an email address that belongs to the same domain as the site", and shows the visitor's address going in Reply-To through the Additional headers field on the Mail tab. Its configuration validator flags a From address outside the site's domain as an error. A Mail tab set up that way looks like this:

Contact Form 7, Mail tab
To:                  hello@yourcompany.com
From:                Website <wordpress@yourcompany.com>
Subject:             [your-subject]
Additional headers:  Reply-To: [your-email]

That fixes the From line. It does nothing for cause 2. Contact Form 7, Elementor and Divi all hand their mail to wp_mail(), and wp_mail() in turn defaults to wordpress@ your domain as the sender and to PHP's mail() as the transport. WordPress's own reference for the function adds that a true return value does not automatically mean the email was received. One SMTP plugin connected to a properly authenticated provider fixes the sending side for every form plugin on the site at once, and it is worth doing even after the From line is right.

What we decided for WebForms notifications

We send the notifications for every form that uses us, so we had to make the From and Reply-To decision once, for everyone. These are the rules, followed by what they cost you.

  • From is always a WebForms address, on sending domains that publish SPF and DKIM, aligned with the From domain, and a DMARC policy of reject. No customer can change the address, on any plan, because a customer-chosen address would fail authentication and spend a reputation every other customer depends on. Pro and Agency can change the display name, so a notification reads as your brand; the address behind it stays ours.
  • Reply-To comes from the _replyto field and nowhere else. Line breaks and other control characters are stripped first, because a line break inside a header is how header injection works, and what remains is used only if it is exactly one valid address. A list or a malformed address is dropped rather than guessed at.
  • Spam is filtered before anything is sent. Quarantined submissions are stored and readable in the dashboard but never emailed, so the junk your form attracts does not reach your inbox to be reported against the sender.
  • Account mail such as password resets goes out from a different sender address than form notifications, so complaints against one stream cannot drag down the other.
  • When a receiving server refuses an address, that address is suppressed for every kind of email we send, so a dead mailbox cannot keep costing reputation that every customer's notifications share.

Setting Reply-To on a plain HTML form

Fields whose names start with an underscore are instructions, not data, so they are never stored as part of the submission. Keep the visitor's address in an ordinary email field, where it is stored and shown in the notification, and copy it into _replyto as the form is sent:

contact.html
<form id="contact" action="https://send.webforms.to/wft_your_key" method="POST">
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <input type="hidden" name="_replyto">
  <button>Send</button>
</form>

<script>
  const form = document.getElementById("contact");
  form.addEventListener("submit", () => {
    form.elements["_replyto"].value = form.elements["email"].value;
  });
</script>

Without _replyto the notification is still delivered and still shows the visitor's address in the body. It simply has no Reply-To, so pressing Reply addresses our sending address rather than the visitor. If you post from your own code instead of a browser form, set _replyto in the request body the same way.

What this costs you

Notifications come from our address, not yours, and that is not configurable. Inbox rules have to match our sender, and a colleague who has never seen one of our notifications may not recognise the first. And no sender, us included, can see what a receiving server does after it accepts a message: if it files a fully authenticated notification under junk anyway, mark it as not spam once in each inbox that does so. If notifications must come from your own domain, the honest route is a webhook destination (paid plans) into a mail provider you control, which puts the authentication work back on your side of the fence, where this post started.

Get your endpoint

Free for 300 submissions a month. No card required.

Start free