WordPress Contact Form Not Sending? The Worse Case Is When It Says It Did.
We built a site where mail is accepted and discarded. wp_mail returned TRUE, the visitor was thanked, and the enquiry reached nobody.
Short answer: a wordpress contact form not sending is usually easy to spot, because the visitor gets an error. The dangerous version is the opposite. wp_mail() returns TRUE, the visitor is thanked, and the message reaches nobody. Nothing is logged, nothing bounces, and the first sign is a client asking why enquiries dried up.
I reproduced it deliberately this week, and the part that stayed with me is that nobody in the chain lies.
The reproduction
I built a WordPress site with a working contact form, then gave the server a mail transport that accepts every message and writes it nowhere. That is not a contrived setup. It is what a suspended SMTP account, an over-quota mailbox or a misconfigured relay looks like to PHP.

which is the point.*
Then I submitted it as a visitor would.

the bottom, and every report above it is technically correct. That is exactly why nothing raises an alarm.*
The visitor saw: “Thank you for your message. It has been sent.”
The enquiry went to /dev/null.
Contact Form 7 is not the problem
I want to be fair here, because this failure gets blamed on form plugins constantly.
With a genuinely broken transport, the same form behaved perfectly honestly.

visitor knows and picks up the phone instead. Row two is the expensive one. Contact Form 7 behaves correctly in both, which is why swapping form plugins never fixes this.*
In the first case the form told the truth because PHP told it the truth. In the second case it told the truth too. The lie starts one layer below anything WordPress can see.
Four reasons a WordPress contact form is not sending while reporting success
Four transport states produce a TRUE from wp_mail() and no delivery:
- The relay accepts and drops. Exactly what I simulated. Common with a local sendmail that
was never wired to a real mail service.
- The SMTP account is suspended or over quota. The plugin authenticates, the provider
accepts, then the message is refused downstream and the bounce goes to an address nobody reads.
- Authentication passes and the message is filed as spam. Delivery succeeded. Nobody sees
it. This is where SPF and DMARC come in, and where an SPF record that quietly went over its lookup limit will cause exactly this pattern.
- The mail is queued to a scheduler that is not running. Plenty of plugins send
asynchronously, so a stopped WP-Cron means the message is never dispatched at all while every screen reports success.
That last one is worth checking first, because it is the cheapest to rule out.
There is a fifth, and it is the one I have seen bite agencies hardest after a migration. The site sends from a From address on a domain it is not authorised to send for. Everything succeeds locally. The receiving server checks SPF, finds the sending host is not listed, and files the message as spam or drops it outright. The site reports success because, from its own point of view, it did succeed. Nothing on the WordPress side can see the decision, because the decision is made by a machine you do not control, after the handoff.
This is why form debugging that stays inside WordPress so often ends in a shrug. The evidence you need is in the receiving server’s logs, and you have no access to those. The only thing you can genuinely control is whether the submission survives the attempt.
How to actually prove forms work

form that records submissions to the database turns a silent, permanent loss into a recoverable one, whatever the mail transport does that week.*
Three commands that tell you where you stand, in the order I run them:
# 1. does wp_mail even claim success on this site
wp eval '$ok = wp_mail("[email protected]","protuno test","body");
echo $ok ? "TRUE" : "FALSE";'
# 2. if it returns FALSE, get the actual reason
wp eval 'add_action("wp_mail_failed", function($e){ echo $e->get_error_message(); });
wp_mail("[email protected]","protuno test","body");'
# 3. what address is the site sending as, and is it authorised to
wp eval 'echo apply_filters("wp_mail_from", "wordpress@" . parse_url(home_url(), PHP_URL_HOST));'
Command three is the one that most often produces the surprise. If it prints something like [email protected] and the client’s mail is on Google Workspace, the site is sending as a domain whose SPF record does not list the web server. That message will authenticate as nothing and be treated accordingly.
TRUE from command one is not good news on its own. It only means the message left. Whether it arrived is a question WordPress cannot answer, which is the whole argument of this post.
Two practical notes from doing this.
A test email from the plugin’s settings screen proves almost nothing. It usually goes to you, often from a different sender address, and lands in a mailbox that already trusts the domain. The failure you are hunting affects a stranger’s message to a client’s inbox.
Turn on database storage today, before anything else. Whatever form plugin the site uses, there is a setting that keeps submissions. It costs nothing and it means the next silent outage is an inconvenience rather than a quarter of lost leads.
Why this is worth automating
The check is conceptually simple and genuinely annoying to do by hand: submit the real form, wait, confirm arrival at a monitored address, and do it on every site, repeatedly, because the thing that breaks it is usually a change somewhere else.
So it gets tested once at launch and trusted forever. That is the unbilled half hour again, and this time the cost is measured in enquiries the client never knew existed.
Echo, Protuno’s Marketing agent, is built to submit forms on a schedule and confirm they still post. Straight with you as on every post here: Echo is built and named but not live yet. The free audit checks the mail posture from the domain today, which catches the SPF and DMARC causes above but cannot see whether a form arrives.
Go and check the database-storage setting on your busiest client’s contact form. If it is off, that is the ten minutes with the highest return in this entire post.
Comments