Why Your Contact Form Emails Land in Spam and How to Fix It

Why Your Contact Form Emails Land in Spam and How to Fix It

Contact form emails going to spam is almost always an authentication or envelope problem, not a content problem. Your form hands a message to a program on your server, that program opens an SMTP conversation, and the receiving mail server decides whether to trust what it sees based on who the message claims to come from and whether the sending IP is allowed to make that claim. Fix those two things and most filtering disappears.

What actually happens when the form submits

A form submission does not send email. It sends an HTTP POST to a script, and that script invokes a mail transport. On a typical PHP host that is the mail() function, which shells out to a local MTA or a sendmail-compatible binary. On a Node or Python app it is usually an SMTP client library talking to a relay. Either way, the script constructs a message and chooses a sender address, and that choice is where most of the damage gets done.

The critical detail is that there are two sender addresses, and they are not the same thing. The From: header is what the recipient sees. The envelope sender, also called the return path, is what the receiving server uses for bounce handling and, more importantly, for authentication checks. Many form scripts set From: to the visitor's address so the site owner can hit reply, and leave the envelope sender as whatever the web server's default is, often something like www-data@localhost or a bare local user. That mismatch is a red flag. You are claiming to be a domain you do not control while sending from an IP that has no authorization to send for it.

Why contact form emails going to spam is usually an SPF alignment failure

Receivers evaluate three published records. SPF is a TXT record listing which IP addresses may send mail for a domain. DKIM is a cryptographic signature added to the message and verified against a public key in DNS. DMARC ties the two together and tells the receiver what to do when neither passes, and it is the record that governs alignment.

Alignment means the domain in the visible From: header has to match the domain that passed SPF or DKIM. If your form sends as the visitor's address, the visible domain is the visitor's, but SPF is being evaluated against your server's IP and your domain. Nothing aligns. DMARC then applies its policy, which for many domains is p=none at first and later p=quarantine or p=reject. Quarantine means spam folder. That is your bug, and no amount of rewriting the subject line will fix it.

You can see the whole picture by asking a receiving server what it thinks. Send a test message to a mailbox you control at a large provider and read the headers, or query the records directly:

dig +short TXT example.com
dig +short TXT _dmarc.example.com
dig +short TXT selector1._domainkey.example.com

The first line returns your SPF policy, the second your DMARC policy, and the third the DKIM public key. If the DKIM query returns nothing, signing is not configured, and you are relying on SPF alone.

Configure the form to send from your own domain

The fix is to stop using the visitor's address as the sender. Send from an address on a domain you control and can authenticate, then put the visitor's address in Reply-To. That keeps reply working while making the message align.

In PHP, the fifth argument to mail() sets the envelope sender, and the headers carry the rest:

$headers = "From: Website <[email protected]>\r\n";
$headers .= "Reply-To: " . $visitor . "\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
mail($to, $subject, $body, $headers, "[email protected]");

The -f flag is the part people forget. Without it the envelope sender defaults to the web server user and the alignment you just built in the headers is thrown away. Note also that mail() gives you no feedback. If the local MTA rejects the message you will not know. For anything beyond a hobby site, use an SMTP client library and point it at a relay you have authenticated.

Whatever relay you choose, it must sign your mail with DKIM and its sending IPs must appear in your SPF record. Add the relay's include directive to SPF and publish the DKIM key it gives you at the selector it specifies. Keep the SPF record to a single TXT record and keep the number of DNS lookups it triggers low, because SPF has a lookup limit and exceeding it is a permanent error, not a soft fail.

Headers that get messages filtered

Beyond authentication, certain header patterns are treated as suspicious by content filters. A From: address that does not exist and cannot receive mail is one. A Subject: that is entirely the visitor's input is another, because spam bots fill forms too. Missing Date: and Message-ID: headers, which some minimal scripts omit, make a message look hand-forged.

If your form sends HTML, send a proper multipart message with a text alternative. A message that claims Content-Type: text/html but contains no HTML structure, or one that has an HTML body with no plain text part, scores badly. Add a List-Unsubscribe header only if the mail is genuinely a list; putting it on transactional form mail is confusing and can hurt.

Finally, make sure the sending domain has a real postmaster@ and abuse@ mailbox, or at least a catch-all, and that the domain resolves forward and backward. A forward-confirmed reverse DNS entry on the sending IP is expected by most large receivers. You can check it yourself:

dig +short -x 203.0.113.10
dig +short mail.example.com

The name returned by the first command should match the name in the second. If it does not, or if the first returns nothing, your mail is arriving from an IP with no verifiable identity and will be treated accordingly.

Test before you assume it is fixed

Send a submission through the live form to a mailbox at a large provider, then open the raw source and read the Authentication-Results header. It will state plainly whether SPF, DKIM and DMARC passed or failed and whether they aligned. Fix alignment first, then DKIM, then look at content. If you cannot see the headers in your mail client, fetch the message over IMAP and read it there, or temporarily route form mail to a mailbox you can inspect at the protocol level. Change one thing at a time and re-test, because these records propagate and a stale cached answer will send you chasing a problem you already solved.

Related articles

Subscribe to our newsletter

Get the latest hosting tips, performance insights, and industry news.