Fix Contact Form Emails Going to Spam
Three different faults look identical from the inbox: never sent, rejected on arrival, or delivered and filtered. How to tell which one you have, with no terminal.

Three faults that look the same from the inbox
Somebody fills in the enquiry form on your website. The page says thank you. Nothing arrives. Or it arrives three days later, in spam, after the customer has already called someone else.
Before you read on, you can see what your domain publishes in one step: the DMARC and SPF checker reads the live records and warns when more than one SPF or DMARC record is published.
From where you are sitting those are the same event, which is why the usual advice is the same advice for all of them: set up SPF, DKIM and DMARC, install an SMTP plugin, pay somebody. That advice is correct for exactly one of the three things that could be happening, and useless for the other two.
Underneath, there are three different faults:
- The message was never sent. Your server refused the job, or dropped it quietly. Nothing was ever handed to anybody.
- It was sent and the receiving server refused it. There is a rejection, with a number attached, and a bounce message that almost certainly did not come to you.
- It was delivered and the filter moved it. The message is sitting in a spam folder with a verdict attached, which you can read.
Only the third one, and part of the second, is an authentication problem. Publish a flawless set of DNS records while you are in the first case and the result is a flawless set of DNS records and no email.
So the order below is diagnosis first. Work out which branch you are on, then fix that branch. Everything here can be done from a browser: no dig, no terminal, no SSH.
Branch one: it never left your server
This is the branch almost nobody writes about, and it is the one to rule out first, because ruling it out is free and because every authentication fix below is wasted effort while you are in it.
The classic cause is PHP's built-in mail(). It does not speak to the recipient's server. It hands the message to whatever mail agent the host has configured and returns. The PHP manual is unusually blunt about what the return value means: mail() returns true "if the mail was successfully accepted for delivery", and "It is important to note that just because the mail was accepted for delivery, it does NOT mean the mail will actually reach the intended destination". Your form saw true, showed the thank-you page, and told you nothing.
Three things commonly happen after that true:
- The host rate-limits it. Hostinger, to pick a host a great many Indian small businesses are on, caps mail sent this way at a maximum of 10 emails per minute and 100 a day, and says plainly that emails sent this way "often skip necessary authentication (SPF, DKIM, and DMARC) checks". Hit the cap and later submissions go nowhere.
- SMTP credentials are wrong. A form configured to send through authenticated SMTP with the wrong password, the wrong port or an expired app password usually fails silently, because most form plugins and most application code do not surface the error to anyone. Port 587 with STARTTLS and port 465 with implicit TLS are not interchangeable, and picking the wrong one is a connection that never completes.
- The destination address cannot receive mail at all. The form sends to
enquiries@yourdomain.in, the domain has no MX records because mail was never set up or was moved, and there is nothing at the other end.
How to tell, without a terminal
Start with the destination address, because it is the cheapest check and it rules out a whole class of embarrassment. Put the address your form sends to into the email validator. It checks the format locally and then queries live DNS for MX records on the domain. No MX and no fallback means mail to that address cannot be delivered by anybody, and no amount of SPF will change it. The DNS checker shows the same MX records next to everything else the domain publishes, which is the view you want when the answer is "the MX points at a provider we stopped paying for".
Then check whether anything was sent:
- Look in the sending account's Sent folder. If the form sends through a real mailbox over SMTP, a successfully sent message is usually there. An empty Sent folder over several test submissions is branch one.
- Send a test to an address on a different provider. Submit the form twice, once to your normal address and once to an address at a different mail provider. If neither arrives and neither bounces, the message is not leaving. If one arrives, you are on branch three and the problem is filtering, not sending.
- Ask your host for the mail log for that minute. Every shared host keeps one, and support will read it for you. The useful question is specific: "a message was submitted at 14:32 IST on this date from my site, does your outbound mail log show it, and what happened to it?" A log entry with a rejection is branch two. No log entry at all is branch one.
- Check your form's own log. Most form plugins and frameworks can record every attempted send, and turning that on costs nothing. If yours cannot, see the section on persisting the enquiry below, because that is the real fix.
The repair for branch one is nearly always the same: stop using unauthenticated mail() and send through authenticated SMTP, either your own mailbox provider's server or a transactional mail service. That is also the step that drags you onto branch three, where authentication records start to matter, so do them in that order.
Branch two: it was sent, and the receiver said no
Here the message reached the receiving server and the server refused it with a 5xx code, most often 550. RFC 5321 defines a 5yz reply as a permanent negative completion: the command "will not be accepted again if retried without change". Retrying does not help. Something has to change.
The reason this branch disguises itself as branch one is where the rejection goes. When a server accepts mail for relay and then cannot deliver it, RFC 5321 says it "MUST construct an 'undeliverable mail' notification message and send it to the originator of the undeliverable mail (as indicated by the reverse-path)". The reverse-path is the SMTP envelope sender, which is not the From: line a human sees.
So if your form sets the envelope sender to the visitor's own address, which is a very common default, the bounce goes to the visitor. They get a confusing delivery failure for a message they never knowingly sent, assume it is spam, delete it, and never mention it to you. You get silence, and conclude nothing was sent.
Two fixes, both worth doing:
- Make the envelope sender an address at your own domain. Then bounces come back to you and branch two stops being invisible.
- Read the rejection text. A bounce says which kind of refusal it was. "No such user" or "recipient address rejected" is an address problem: fix the address, and check it with the email validator. A refusal that mentions SPF, DKIM, DMARC, policy or reputation is really branch three arriving one step earlier, and the next section is the one you want.
Branch three: it arrived, and the filter moved it
This is the branch everyone writes about, and it is the only one where editing DNS is the answer. For the fuller record syntax, the policy ladder from p=none to p=reject, and what the big three providers actually require, why business emails go to spam covers that ground in depth; here is the version compressed for a contact form specifically.
Google's email sender guidelines are the clearest public statement of the floor. Every sender, at any volume, is asked to set up SPF or DKIM for their sending domains, to have valid forward and reverse DNS for the sending IP, to use a TLS connection, to follow the RFC 5322 message format, and to keep spam rates in Postmaster Tools below 0.3%. Senders of more than 5,000 messages a day get stricter rules on top: DMARC, and a From: domain aligned with either the SPF domain or the DKIM domain.
A website contact form is nowhere near 5,000 messages a day. The baseline still applies to it, and the alignment rule is worth adopting anyway, because it is what stops the one mistake in the next section.
What your DNS actually says
Three records do three different jobs, and the quickest way to see all three is from a browser.
SPF lists the servers allowed to send for your domain. Run your domain through the DMARC and SPF checker: it reads live DNS, shows the record it found, and tells you whether the record ends in a strict -all or a soft ~all. Two details from RFC 7208 decide most SPF arguments:
- One record, and only one. Section 3.2 says a domain "MUST NOT have multiple records that would cause an authorization check to select more than one record", and two records produce a
permerror, which is not a stricter SPF but no passing SPF at all. When you add a new sender, you merge aninclude:into the record you already have. You never add a secondv=spf1line. - Ten DNS lookups, maximum. Section 4.6.4 requires implementations to "limit the total number of those terms to 10 during SPF evaluation", and to return
permerrorpast that. Everyinclude:you stack up, for the website, the CRM, the newsletter and the invoicing tool, spends from that budget.
DKIM signs the message so the receiver can verify it was not altered, using a public key published at <selector>._domainkey.yourdomain.in. The catch is the selector: a domain can publish several, and only the one the sender actually signs with matters. The DKIM checker confirms a given selector exists and carries a real public key. You find the selector in one of two places: your provider's settings, or the s= tag inside the DKIM-Signature header of a message you have already received. Common ones are google for Google Workspace, zoho for Zoho Mail, selector1 and selector2 for Microsoft 365, and default.
DMARC ties the first two to the address the reader sees. RFC 7489 section 3.1 puts it precisely: DMARC "authenticates use of the RFC5322.From domain by requiring that it match (be aligned with) an Authenticated Identifier". Section 4.2 is the part people get wrong in both directions: a message passes when at least one of the mechanisms produces a pass and does so on an identifier that is in alignment. You do not need both SPF and DKIM to pass. You do need one of them to pass for the domain in the visible From:.
And there is one failure mode that makes all of this moot. Section 6.6.3: if policy discovery finds multiple records at _dmarc, "policy discovery terminates and DMARC processing is not applied to this message". Two DMARC records is not stricter than one. It is none. The commonest way this happens is a host or mail provider's setup wizard quietly adding its own record next to yours, and the giveaway is a second record whose rua= reporting address belongs to the host rather than to you. The DNS checker lists every TXT record for a name in one view, which is how a duplicate SPF or a duplicate DMARC becomes visible instead of staying theoretical.
Reading the verdict in Gmail
If the message is in a Gmail spam folder, the receiving server has already written down what it thought. Open the message, click More next to Reply, and choose Show original. Gmail shows the full header, with a Copy to clipboard button.
The line to find is Authentication-Results. Read three verdicts and two domains.
| What you see | What it means | What to do |
|---|---|---|
| spf=pass | That server is listed | Nothing |
| spf=fail | That server is not listed | Add it to the one SPF record |
| spf=permerror | Two records, or over ten lookups | Merge to one, flatten includes |
| spf=none | No SPF published | Publish one |
| dkim=none | Not signed at all | Turn DKIM on at the sender |
| dkim=fail | Signed, did not verify | Check the key at that selector |
| dmarc=fail | Nothing passed in alignment | Fix the From header first |
Two details decide most cases. SPF is checked against the envelope sender, not the address shown to the reader: RFC 7208 section 2.4 has verifiers check the MAIL FROM identity, and the specification never looks at the header From:. And DMARC only cares about the header From:. That is why a message can show spf=pass and dmarc=fail on the same line, and it is the single most confusing thing about reading these headers.
Google's Admin Toolbox includes a Messageheader tool that parses a pasted header and lays out the hops, which is useful when the delay rather than the verdict is the question.
If all three say pass and the message still went to spam, stop editing DNS. The filter had some other reason: a brand-new sending domain with no history, a shared hosting IP with poor reputation, or content the filter did not like. Those are reputation and content problems, and no record you add will touch them.
The From and Reply-To rule
There is one mistake that causes more of this than any other, and it is built into the default configuration of a lot of form software: putting the visitor's email address in the From: header so that Reply works.
Do not. When your server sends a message whose From: is priya@gmail.com, it is asserting that the message came from gmail.com. SPF fails, because your web server is not in gmail.com's SPF record. DKIM fails, because you cannot sign for a domain whose private key you do not have. DMARC therefore fails, and what happens next is decided by the policy the visitor's provider publishes, not by you.
Those policies differ, which is exactly why this bug is so hard to catch. Checked in October 2026, yahoo.com publishes p=reject, so a form configured this way never reaches you when the visitor is on Yahoo. gmail.com publishes p=none, so the same form often works for Gmail visitors and quietly fails for others. You can confirm either of those, or any customer's domain, with the DMARC and SPF checker.
The correct shape costs one line of configuration. From: is an address at your own domain, the one domain you can publish records for. The visitor goes in Reply-To:, which RFC 5322 section 3.6.2 defines as "the address(es) to which the author of the message suggests that replies be sent". Your mail client honours it, so Reply still goes to the customer, and your authentication is now about a domain you control.
While you are in that configuration, set the envelope sender to your own domain too, so branch two bounces come back to you.
Where to change DNS: Hostinger, BigRock or GoDaddy
Before you edit anything, find out which nameservers are actually authoritative for the domain. A great many wasted afternoons are somebody editing a zone at the registrar while the nameservers point at the host, or the other way round. The NS records in the DNS checker answer that in one query: whatever they name is the panel you need to be in.
- Hostinger. In hPanel, go to Domains, then Domain portfolio, select the domain, then DNS / Nameservers and DNS records. TXT records take
@as the host for the root domain. Hostinger's own guide to managing DNS records has the screens. - GoDaddy. Sign in to the Domain Portfolio, select the domain, open DNS, then Add New Record and pick TXT. The Name field is the prefix without the domain, and
@means the root. - BigRock. Sign in at the orders dashboard, open the domain under Orders, then DNS Records, then Add Record. This panel only applies when the domain is on BigRock's own nameservers, which is the first thing the NS check above will tell you.
All three share one trap worth stating plainly, because it is where most DMARC and DKIM records get published to the wrong name: the Name or Host field does not include your domain. Enter _dmarc, not _dmarc.yourdomain.in. Enter zoho._domainkey, not the full hostname. The panel appends the domain for you, and typing it twice creates a record at _dmarc.yourdomain.in.yourdomain.in, which looks right in the panel and does not exist as far as the internet is concerned. Zoho's documentation notes this directly: several providers append the domain name automatically, so you supply only <selector>._domainkey.
Expect propagation. The published TTL is how long resolvers keep the old answer, so give a change up to a day before concluding it did not work, and re-check with the tools rather than with your laptop, which caches too.
If your mail is on Zoho Mail or Google Workspace
Both are common for Indian small businesses, and both have the same gotcha: your website is a second sender. The provider's include: covers mail sent from their mailbox servers. It does not cover a message your web host sends directly, even though the From: address is the same.
You have two honest options. Either send the form's mail through the same provider's authenticated SMTP, so the existing SPF and DKIM already cover it, which is the simpler answer for low volume. Or add the web sender to SPF and arrange signing for it. What you must not do is publish a second SPF record for the second sender.
Google Workspace. The SPF record Google documents is v=spf1 include:_spf.google.com ~all, with one record per domain. DKIM is generated in the Admin console under Apps, Google Workspace, Gmail, Authenticate email, with the default prefix selector google; you copy the host name and value into DNS, then come back and start authentication. Google's DMARC guidance is the matching third step.
Zoho Mail. Zoho documents v=spf1 include:zohomail.com -all, or include:one.zoho.com when several Zoho services send for you, published as a TXT record at @, and says to use ~all instead of -all if anything other than Zoho also sends for the domain (SPF configuration). A website that sends its own mail is exactly that case. DKIM is three steps in their admin console: generate the key against a selector, publish the TXT record at <selector>._domainkey, then validate and enable it (DKIM configuration). Until that last step, the key exists and nothing is being signed with it.
Whichever you are on, verify from outside rather than from the provider's own green tick: the DKIM checker against the selector you just published, and the DMARC and SPF checker for the other two.
Make the form stop depending on email
Everything above makes delivery more likely. None of it makes delivery certain, and an enquiry that exists only as an email is one filter away from never having happened.
The structural fix is to stop treating the notification as the record. Save the submission to your own database first, return the thank-you page on the strength of that, and send the notification afterwards as a separate step that is allowed to fail. Then record whether it succeeded.
That is how the form on this site works. The enquiry is written to storage and given an id before any mail is attempted, the notification goes out on a background job so a slow SMTP handshake never makes the visitor wait, and each enquiry carries a flag recording whether its notification actually went out. The From: address is the authenticated account doing the sending, never the visitor. The practical benefit is not elegance: it is that when branch one happens, the enquiry is still there and the flag says so, instead of the whole thing having evaporated between a thank-you page and a spam filter.
Add a simple list of submissions that you can open in a browser, and the question "did we miss an enquiry" stops depending on email at all.
The order to work through it
- Put the address the form sends to into the email validator. No MX, no delivery, nothing else matters.
- Submit the form twice, to your own address and to an address at a different provider. Note which arrive, and where.
- If neither arrives and nothing bounces, you are on branch one. Ask your host for the mail log for that exact minute, and move the form onto authenticated SMTP.
- If something bounced, read the rejection. Address problems and policy rejections need different fixes.
- If it arrived in spam, open it and use Show original. Read
spf=,dkim=anddmarc=, and which domain each one is talking about. - Check what you publish: the DMARC and SPF checker for the policy, the DKIM checker for the selector your mail actually signs with, and the DNS checker for the duplicate TXT record neither of those will see as a duplicate.
- Fix the
From:header before you fix anything in DNS. If the visitor's address is inFrom:, nothing you publish can make it pass. - Re-test, and allow for the TTL before deciding a change did not work.
What none of this fixes
Authentication is identity, not reputation. Passing SPF, DKIM and DMARC proves the message is from who it claims to be, and a filter is perfectly entitled to junk a message that is authentically from a sender it does not trust yet. A domain that has never sent email before has no history, and the first few weeks are the hardest.
Shared hosting IP reputation is mostly out of your hands, which is the real argument for sending through a mailbox provider or a transactional service rather than from the web server. Content filters still apply, and a form that forwards raw visitor text including links and attachments is handing the filter plenty to work with. Moving to p=reject protects your domain from spoofing and does nothing for your own deliverability on its own. And Postmaster Tools, which is where Google's 0.3% spam rate figure is read, needs real volume before it reports anything, so a contact form alone will not fill it.
We have published no numbers of our own here on purpose. Deliverability statistics are easy to invent and we have not measured any. What is checkable is in the records, in the headers, and in your host's mail log.
Frequently asked questions
Why do contact form emails go to spam?
Usually because the message is not authenticated as your domain, or because the form puts the visitor's address in the From: header, which cannot pass SPF, DKIM or DMARC for that visitor's domain. Put an address at your own domain in From: and the visitor in Reply-To:.
Why am I not receiving contact form emails at all?
Nothing arriving is a different fault from mail in spam. The message may never have been sent, or the receiving server may have refused it, so check the branches above, and your host's mail log for that minute, before you change DNS.
Why does my contact form work for some visitors and not others?
Because the receiving side applies the visitor's own domain policy when your form uses their address in From:. As checked in October 2026, yahoo.com publishes p=reject, while gmail.com publishes p=none, so the same form can work for one and fail for the other.
How do I stop website form emails going to spam?
Send them from an address at your own domain through authenticated SMTP, publish SPF, DKIM and DMARC for that domain, and use Reply-To for the visitor. Then test with a real submission to a Gmail address and read the verdict with Show original.
Do I need an SMTP plugin for my contact form?
Sending through your mailbox provider with authentication is more reliable than letting the web server send directly, because the web server's shared address carries other sites' reputation. A plugin or a mail service is one way to do that.
Is a contact form email going to spam an SPF problem?
It can be, but check the From: header first. If the visitor's address is in From:, nothing you publish can make it pass. Then look at SPF, DKIM and DMARC with the free checkers, and look for a duplicate TXT record.
How do I make sure I never lose an enquiry?
Save each submission to your own database before sending any email, show the thank-you page once it is saved, and record whether the notification went out. Then a failed email no longer means a lost customer.
If you want the state of your own domain written down rather than guessed at, our security headers checklist covers the other half of the same surface, and moving a website without losing search traffic covers the DNS cut-over where these records most often get left behind.
Mail landing in spam is fixable by somebody who has done it before, and the result shows up in the next message you send rather than in a report nobody reads. If you would rather hand it over, we measure what is actually failing first and then quote a fixed price for the fix: that is website fixes and maintenance.
Tools mentioned in this post
- DMARC & SPF CheckerCheck whether your domain’s email is protected by SPF and DMARC, and how strictly.
- DKIM CheckerVerify your DKIM public key is published correctly at its selector.
- DNS CheckerLook up A, AAAA, CNAME, MX, NS and TXT records for any domain in one shot.
- Email ValidatorCheck an address’s format and whether its domain can actually receive mail.


