WordPress Contact Form Not Sending Email? The Fix That Survives Shared Hosting
Your WordPress contact form is not sending email even though it shows success. Here is the diagnosis funnel I use, and the fix that survives shared hosting.
The form works. You fill it in, hit send, the green success message appears right on cue. Somewhere a potential client thinks they just reached you, while you're refreshing an inbox that stays empty.
That's the nasty version of this problem. A form that throws an error tells you something is wrong. A form that reports success while the email quietly dies sends your best prospect straight to the next developer on their list.
I rebuilt the contact form on this site a few weeks ago, and the exercise was a good reminder of how many ways a simple form can lose an email between the send button and a human being. So if your WordPress contact form is not sending email, this is the diagnosis path I follow. It applies whether the form came from WPForms, Contact Form 7, Gravity Forms, Fluent Forms, or a developer who hand-rolled a PHP handler in 2019.
Every form funnels into one pipe
Here's the part most tutorials skip. It doesn't matter which form plugin you picked. Under the hood they all end up in the same place: wp_mail(). That function hands the message to PHPMailer, and PHPMailer picks a transport.
On a default WordPress install, that transport is your web server's mail() function. That one detail sits behind most of the contact form tickets I've ever seen.
Form plugin → wp_mail() → PHPMailer → mail() → the server's mail agent → ???
The question marks are the problem. PHP's mail() fires and forgets. It passes the message to the local mail agent and reports success if the handoff was accepted. Whether the email goes anywhere after that point is nobody's job to check, so the failure stays invisible end to end.
When a form goes quiet, one of three things is happening. The form never fires. The form fires but wp_mail() never gets called, which is what hand-rolled handlers do. Or the mail layer sends and the message dies further down the chain. Each case gets treated differently, so the first job is figuring out which one you have.
Step 1: catch the layer that is lying
Install WP Mail Logging, or any plugin that records outgoing mail, then submit your own form once and open the log.
If the log shows your message with a sent status and it still never lands, the failure is below WordPress. Move straight to step 2.
If the log stays empty, one of two things is true. Either the form isn't firing at all, maybe a captcha is silently blocking real humans or the notification event is disabled. Or you're dealing with a hand-rolled handler that calls mail() directly and never touches wp_mail(). Logging plugins only see what passes through wp_mail(), so a custom script is invisible to them. That second case is more common than people think, and it gets its own section below.
There's also a quick way to test the server itself without creating any file in the web root. From the terminal (cPanel has one built in, or use SSH):
php -r "var_dump(mail('you@example.com', 'Server mail test', 'If you can read this, mail() works here.', 'From: test@yourdomain.com'));"
If that prints true and the message still never lands, WordPress is innocent. The server's mail stack is where you keep digging.
Step 2: the mail() reality on shared hosting
PHP's mail() doesn't send anything by itself. It hands the message to the server's local mail agent, usually Sendmail or Postfix, and hopes for the best. The handoff is fire and forget: once the local agent accepts the message, mail() returns true and the script moves on.
On shared hosting, hope isn't a delivery strategy. Sometimes outgoing port 25 is throttled or firewalled by the host, in which case messages pile up in the local queue and never leave the building. Sometimes port 25 works fine but the messages that do leave carry no authentication, come from an IP with a rough shared reputation, or lack valid reverse DNS. Receiving servers see a suspicious anonymous message claiming to be from your domain and quietly bin it.
Both failure modes look identical from the outside: no bounce, no error on the visitor's screen, and a message that simply never shows up.
Step 3: the From-address trap
Now the single most common deliverability bug in contact forms, and it has nothing to do with your code quality.
Plenty of form setups send the message From the visitor's own address. It feels intuitive: hit reply and the reply goes straight to the person, no copy-paste needed. The problem is what receiving servers do with it. Yahoo and Microsoft publish strict DMARC policies for their addresses (p=reject), so a message claiming to be from a yahoo.com address but sent by your hosting provider's machine gets binned on sight. Gmail publishes a softer record, but it still treats the failed alignment as a strike, and its own inbound checks catch spoofed gmail.com senders anyway.
The fix feels backwards and works everywhere:
From: form@yourdomain.com ← your domain sends, so the visitor's DMARC never trips
Reply-To: visitor@gmail.com ← replies still go to the human
One honest caveat. Switching the From header stops the visitor's domain from rejecting the message, but it doesn't magically make your own domain pass SPF and DKIM. That depends on the transport you pick in step 4, plus the DNS records behind it. What the From fix does immediately is shut down the most violent failure mode, and it costs nothing.
Your domain sends, the visitor's address rides along in Reply-To. You keep the one-click reply behaviour.
A security aside: the custom handler trap
If your form doesn't come from a plugin, if some developer wrote a bespoke PHP handler back when "a quick script" felt like a good idea, this section is for you.
A handler that builds mail headers from raw user input turns your form into a spam cannon. The live vector is the additional headers string. If the handler writes From: $name <$email> using unvalidated input, an attacker submits a name containing a line break followed by Bcc: and fifty addresses, and the message goes to all of them. One newline, fifty victims.
Modern PHP rejects newlines in the To and Subject parameters of mail(), so the language closes those doors itself. The headers parameter is where user input still gets in. htmlspecialchars doesn't help, because it escapes quotes and tags but leaves carriage returns and line feeds untouched. WordPress's own sanitize_text_field() does strip control characters, and the defence is exactly that boring: sanitize every value, validate the address with a real email filter, and keep user input out of headers you don't need.
I know because I pulled an old handler off my own live site during this rebuild, and it had exactly this flaw. Probably had it for years. The scary part is how completely normal that file looked sitting in the repo.
Step 4: the transport that holds up
Once the diagnosis points at the mail stack, here's what I land on, in order of preference.
A transactional HTTP API. Mailgun, Postmark, Amazon SES, Brevo. WordPress or a small custom handler talks to the provider over HTTPS. No dependency on the local mail stack, no port 25, no mystery MTA configuration. The provider's entire business is getting email delivered, which is more than a shared host can promise. This is what this site uses now.
SMTP through a plugin. WP Mail SMTP, FluentSMTP or Post SMTP rewrite PHPMailer's transport to authenticate against a real mail server over port 587. Massively better than mail(). Two caveats from experience: the SMTP password usually lives in your database, so use a provider credential you can rotate without pain, and some shared hosts block outbound SMTP ports anyway, so test before you celebrate.
Either way, keep the From alignment rule from step 3, and make sure SPF, DKIM and DMARC exist for the domain doing the sending. I covered that layer in depth in a separate post: Why Your Emails Aren't Being Delivered. Read it after this one and the whole stack clicks into place.
One more thing worth adding once the fix is in: run your address through mail-tester.com and aim for a 10. It scores SPF, DKIM, spam patterns and content in one shot, and it's the fastest sanity check I know.
Where the settings hide in popular plugins
Whichever form builder you run, the fix is the same pattern. Only the location of the setting changes.
| Plugin | Where the mail settings live | Watch out for | |---|---|---| | Contact Form 7 | Mail tab, per form | The default From is safe (your site title and domain), but people often swap the visitor's address in to make replies easy. CF7 flags that as a config error. Keep From on your domain and use Reply-To. | | WPForms | Form builder, Settings, then Notifications | From Email defaults to the admin address. Keep it that way and put the visitor in Reply-To. | | Gravity Forms | Per form, Settings, Notifications | The From Email field accepts merge tags. A visitor tag there wrecks alignment. | | Fluent Forms | Settings, Notification settings | Same pattern as the rest: your domain sends, the visitor rides in Reply-To. | | Elementor Forms | Actions After Submit, then the Email action | The From Email setting hides inside the action. Verify it isn't the visitor. |
The pre-flight checklist
Run through this before you blame the form plugin for anything:
- Log outgoing mail and submit one test entry yourself. Remember that a custom handler won't show up in the log.
- Bare
mail()test from the terminal, with a From header. Fails? It was never WordPress. - From address on your own domain, visitor in Reply-To.
- SPF, DKIM and DMARC verified for the sending domain, guide linked above.
- Replace
mail()with an authenticated transport. HTTP API first, SMTP second. - Custom handler in play? Sanitize every header value and strip CR and LF.
- Send a final test through mail-tester.com and read the score, not just the inbox.
Before you go
If you take one thing from this post, make it the order of operations. Diagnose the layer first, fix the From header second, change the transport third. Most people do it backwards: they reinstall form plugins for hours while the mail stack quietly eats every message.
And when you send your first test after the fix, check the spam folder as well as the inbox. Getting delivered is the win. Landing in the primary inbox is the proof.
Keep Reading
-
Why Your Emails Aren't Being Delivered (And How to Fix It): the SPF, DKIM and DMARC deep dive this post leans on.
-
Securing wp-config.php: 5 Rules Every Agency Ignores: because the box sending your email should not be an open door.
Working on a WordPress project?
Let's talk about what you're building.
I'm available for custom plugin development, performance optimization, and headless WordPress projects.
