Order confirmations that never arrive. Password resets stuck in spam. Contact form notifications that vanish. A WooCommerce store whose customers email to ask “did my order go through?” because the receipt never landed. Almost every WordPress site eventually runs into this, and almost every time the same underlying problem is to blame: the site is trying to send mail the way a piece of software sent mail in 2005, and every major inbox provider has spent the last two decades learning to reject that.
This guide walks through the actual fix – an SMTP relay plugin plus the three DNS records (SPF, DKIM, DMARC) that prove to Gmail, Outlook, and Yahoo that your mail is legitimate – and explains why each piece matters, how to verify it is working, and why “contact form not sending email” is almost never a contact form problem.
Why WordPress emails end up in spam#
Out of the box, WordPress sends mail using PHP’s
mail()
function. That function hands the message to the local sendmail binary on the web server, which opens a direct connection to the recipient’s mail server on port 25 and delivers it. This was the correct architecture in 1998. Today it fails for three compounding reasons.
The server’s IP has no sending reputation. Inbox providers score every sending IP against a track record: how many emails has it sent, how often do recipients mark those emails as spam, how long has the IP been sending mail. A shared hosting IP has usually been seen sending a mix of transactional mail, newsletters, forum notifications, and whatever else a few hundred neighbouring WordPress sites happen to emit. From Gmail’s perspective, that reputation is noise at best and actively bad at worst. Your legitimate password reset gets filed in the same bucket as the bulk mail those other sites sent.
The mail is unauthenticated. Gmail, Outlook, and Yahoo all now require authentication – SPF, DKIM, or ideally both – before they will accept mail as anything other than suspicious. A
mail()
call from a random PHP process does none of this by default. The receiving server sees an email claiming to be from
you@yourdomain.com
, checks your DNS for authentication records, finds none that cover the sending IP, and quietly drops the message into spam. From WordPress’s perspective,
mail()
returned success. From the user’s perspective, the email never arrived.
Port 25 is blocked. Most modern hosts, including Hostney, block outbound port 25 entirely. That is the port servers use to hand mail directly to recipient mail servers. Blocking it prevents compromised sites from being used as spam relays, but it also means PHP’s
mail()
cannot deliver mail at all on these hosts. The function returns success because it hands the message to sendmail, but sendmail cannot reach anything, and the message sits in a queue until it is dropped.
The fix is not to find a way around these restrictions. The fix is to stop using
mail()
entirely and route your WordPress mail through a proper SMTP relay service that has a dedicated sending IP, signs mail with your domain, and has spent years building a reputation with every major inbox provider.
The fix stack#
Five steps, in order. Do them in this order – skipping the authentication records and just installing the plugin is the single most common mistake.
1. Install an SMTP plugin
In your WordPress admin, install WP Mail SMTP (the free version from the WordPress.org plugin directory is fine) or an equivalent – FluentSMTP, Post SMTP, Easy WP SMTP. They all do the same core thing: they intercept the
wp_mail()
function and route every outgoing message through SMTP with authentication, instead of handing it to the local
mail()
binary.
WP Mail SMTP is the most widely used and has direct integrations (“mailers”) for every common relay service, so the rest of this guide uses its terminology. The principle is identical in any of the alternatives.
Do not configure it yet. Install and activate, then move on.
2. Pick an SMTP relay
This is the service that will actually deliver your mail. For a typical WordPress or WooCommerce site sending under 40,000 emails a month, any of the following work well and have generous free or cheap tiers:
- SendGrid – 100/day free, wide adoption, solid deliverability
- Mailgun – pay-as-you-go, strong developer tooling, good for higher volumes
- Postmark – transactional-only (strict, which is why it has excellent deliverability), paid from the first email
- SMTP2GO – generous free tier (1,000/month), straightforward setup
- Brevo (formerly Sendinblue) – 300/day free, popular with European sites
- Amazon SES – cheapest at volume, more setup overhead
The important thing is that you pick one and stick with it. Each relay has its own SPF include, its own DKIM records, and its own DMARC interaction. Switching later means re-doing the DNS changes.
For a small WordPress site that sends only transactional mail (password resets, form submissions, order receipts), any of these is fine. For a WooCommerce store with higher volumes, Postmark or SES tend to produce the best inbox placement.
3. Configure SPF to authorize the relay
SPF is a DNS TXT record that tells receiving mail servers which IPs are allowed to send mail for your domain. Without it, your chosen relay’s servers look exactly like a spammer impersonating you – there is no DNS record saying “yes, these servers are mine.”
The relay provides an
include:
mechanism that references all of their sending IPs. You add that to a single SPF record on your domain:
v=spf1 include:sendgrid.net ~all
or for Mailgun:
v=spf1 include:mailgun.org ~all
or for SMTP2GO:
v=spf1 include:spf.smtp2go.com ~all
The record goes on your root domain (
yourdomain.com
) as a TXT record. You can have only one
v=spf1
record per domain – if one already exists (because you also use Google Workspace, Microsoft 365, or another service), you merge the includes into a single record:
v=spf1 include:spf.protection.outlook.com include:sendgrid.net ~all
The
~all
at the end is a softfail – receiving servers should treat unauthorized mail as suspicious but not reject it outright. Once everything else is in place you can tighten this to
-all
(hardfail), which explicitly tells receivers to reject anything not covered by the record. SPF also has a 10-DNS-lookup limit that trips up domains using too many services; the full SPF setup guide walks through how each mechanism counts and how to stay under the limit.
4. Configure DKIM via the relay
Where SPF says “this server is allowed to send for my domain,” DKIM adds a cryptographic signature to each message proving that the message has not been tampered with and genuinely originated from an authorized source. In practice, DKIM is what actually moves the needle on inbox placement. Gmail in particular weights a valid DKIM signature heavily – a valid DKIM from a reputable relay is the single biggest factor in landing in the inbox instead of spam.
Every major SMTP relay provides DKIM records during onboarding. SendGrid generates two CNAME records. Mailgun gives you a TXT record. Postmark gives you a CNAME. The exact form does not matter – you add whatever records they provide to your DNS under the hostname they specify (usually something like
s1._domainkey.yourdomain.com
), wait for propagation, and click “verify” in the relay’s dashboard.
The full mechanics of DKIM signing, selector naming, and key rotation are covered in the DKIM setup guide. For the WordPress fix here, you do not need to understand the cryptography – you just need to paste the records the relay provides into your DNS and verify.
5. Publish a DMARC policy
DMARC sits on top of SPF and DKIM and does two things: it tells receiving servers what to do when authentication fails, and it gives you visibility into who is sending mail as your domain (including spammers who are trying to spoof you).
Start with a monitoring-only policy. Add a TXT record at
_dmarc.yourdomain.com
:
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourdomain.com
p=none
means “do not take action on unauthenticated mail yet, just tell me about it.” The
rua
address receives daily aggregate reports from every major inbox provider listing every message they saw claiming to be from your domain, whether it passed or failed, and from which IP. This is invaluable: it tells you if SendGrid is signing correctly, if a forgotten service is still sending as your domain, or if someone is attempting to spoof you.
Run at
p=none
for two to four weeks. Read the reports. Once every legitimate sender you see is passing both SPF and DKIM, tighten to
p=quarantine
(send failing mail to spam) and finally
p=reject
(drop it entirely). The full DMARC guide walks through the progression in more detail, including how to use the
pct=
tag to roll out quarantine gradually.
Testing the setup#
Once the plugin is configured and the DNS records are live, verify each layer independently before declaring victory.
Send a test email. Most SMTP plugins have a “send test email” button in settings. Send one to a Gmail address and one to an Outlook address. Both should land in the inbox, not spam. If they land in spam, check the message source – specifically the
Authentication-Results
header – to see which check failed.
Check the authentication records externally. Use tools.hostney.com/dmarc to run a combined SPF, DKIM, and DMARC lookup against your domain. The checker walks your SPF record, counts the DNS lookups (important for staying under the 10-lookup limit), probes roughly 40 common DKIM selectors including all the major relays, and parses your DMARC policy. Anything misconfigured shows up immediately, which saves a lot of DNS-propagation roundtripping.
Run the mail through mail-tester.com. Go to mail-tester.com, copy the randomly-generated address it shows you, and send a test email to that address from your WordPress site (use the SMTP plugin’s test-email feature, or trigger a real event like a password reset). The site scores your mail out of 10 and breaks down each factor: SPF, DKIM, DMARC, reverse DNS, spam trigger words, blocklist presence, HTML quality. Aim for 9/10 or better. A fresh WordPress install with the fix stack above typically scores 10/10.
Check an email’s headers on arrival. In Gmail, open a received test message and click “Show original.” The headers include the full authentication result –
spf=pass
,
dkim=pass
,
dmarc=pass
. If any of them show
fail
or
none
, the corresponding layer is not set up correctly. SiteProbe’s email header analyzer will parse the raw headers for you and flag anything suspicious, including delay breakdowns per hop and TLS on each leg of the delivery.
"Contact form not sending email" is an SMTP problem#
Almost every “contact form not sending email” support thread on the WordPress forums has the same underlying cause. The form plugin – Contact Form 7, WPForms, Gravity Forms, Formidable, whatever – calls
wp_mail()
to deliver the notification.
wp_mail()
falls back to PHP’s
mail()
.
mail()
fails silently. The form submits successfully from the visitor’s perspective (because the submission itself works), the admin notification never arrives, and the form plugin gets blamed.
The fix is exactly the same as everything above. Install an SMTP plugin, configure it with a relay, publish SPF and DKIM, publish DMARC. Do not try to debug the contact form itself – the contact form is doing exactly what it is supposed to do, which is hand the message to
wp_mail()
. The failure is one layer down.
This is also why switching form plugins almost never solves the problem. Every form plugin uses
wp_mail()
. Every form plugin fails in the same way on a site that has not fixed SMTP. The only form plugin setting worth checking first is the “from” address – some hosts reject mail where the
From
address does not match the sending domain (for example, setting the
From
on a notification to the submitter’s email, rather than an address on your own domain). That is a DMARC-alignment failure, and the fix is to set the
From
header to something on your own domain and put the submitter’s email in
Reply-To
instead. Most form plugins have a setting for this.
Why wp_mail > PHPMailer > SMTP relay is the right architecture#
WordPress internally uses a class called PHPMailer to actually compose and send messages.
wp_mail()
is a thin wrapper that calls PHPMailer with sensible defaults. By default, PHPMailer is configured to use PHP’s
mail()
function as the transport, which is what triggers all the problems above.
What an SMTP plugin does is hook into WordPress’s
phpmailer_init
action and reconfigure PHPMailer to use SMTP as the transport instead, with the relay’s credentials, host, port, and TLS settings. Nothing else changes – WordPress still calls
wp_mail()
,
wp_mail()
still calls PHPMailer, PHPMailer still composes the message – but the delivery path is now SMTP to an authenticated relay instead of an unauthenticated handoff to sendmail.
This is why “write your own mail plugin” or “call PHPMailer directly from functions.php” is a waste of effort. The SMTP plugins do exactly the right thing already. Install one, configure the relay, publish the DNS records, and move on. The WordPress mail layer is fine – it was always fine. The only thing wrong was the transport underneath it.
Common mistakes#
- Configuring the plugin but skipping SPF and DKIM. Mail sends. Mail lands in spam. “The plugin does not work.” The plugin works – it is the authentication that is missing, and Gmail is quietly filtering.
- Multiple SPF records on the same domain. Publishing two
v=spf1records is a hard failure per spec – receiving servers treat the SPF check as permerror and most treat that as equivalent to no record at all. Merge into one. - Forgetting DKIM on an existing relay. Some people find their
mail()calls working and stop there. Without DKIM, a valid SPF alone will survive Gmail’s filters unevenly – sometimes inbox, often spam. - Jumping straight to
p=rejectin DMARC. Legitimate mail from a forgotten sender (your CRM, a plugin that emails reports, a cron job using a different address) suddenly starts being rejected. Runp=nonefirst, watch reports for two weeks, then tighten in stages. - Setting up authentication and never testing delivery. The DNS records exist but are subtly wrong – a typo in the SendGrid CNAME, a stray character in the SPF include. The plugin works, mail still goes to spam, and nobody notices until a customer complains. Use mail-tester.com or tools.hostney.com/dmarc to catch it before users do.
- Relying on the server’s own SPF. Some hosts configure a default SPF that authorizes the shared server’s IP. This does nothing for a relay setup – you still need the relay’s include, and mixing the two eats into your 10-lookup limit.
How Hostney handles WordPress email#
Outbound port 25 is blocked on every Hostney server. This is deliberate: it prevents compromised sites from being used as spam relays and forces every site on the platform to authenticate properly. The practical consequence for WordPress is that the SMTP-relay setup above is not optional – it is how email actually works on the platform – and it is a one-time setup that results in dramatically better inbox placement than the alternative of leaving port 25 open and trusting the server’s IP reputation (which, on any shared host, is not something to trust).
The DNS records needed for the setup (SPF, DKIM CNAMEs, DMARC) are all managed through the DNS Zone Editor in the control panel. Ellie, the in-control-panel AI assistant, can walk you through adding each record if you share the values from your relay’s dashboard. For domains hosted elsewhere – if your DNS is at Cloudflare, GoDaddy, or Namecheap – the records go in whichever registrar you use; the DNS zone does not need to live at Hostney for any of this to work.
Beyond the DNS side, Hostney’s bot-detection stack filters the kind of reconnaissance traffic that ends up generating stale authentication failures in the first place. When a legitimate customer submits a contact form, the request gets through to WordPress and mail goes out. When a scraper hits
/wp-admin/admin-ajax.php
with junk payloads, it is stopped at the edge before it can generate a fake notification email to pollute your DMARC reports. The cleaner the traffic, the cleaner the authentication data, the tighter you can set your DMARC policy without worrying about false alarms.
Summary#
WordPress mail fails by default because
mail()
sends from an IP with no sending reputation, signs nothing, and – on most modern hosts – cannot even reach port 25. The fix is the same on every WordPress site: install an SMTP plugin, route mail through a relay service, publish SPF and DKIM records that authorize and authenticate the relay, and layer DMARC on top to catch failures and protect your domain from spoofing. Test with mail-tester.com and tools.hostney.com/dmarc to confirm each layer is working before calling it done. “Contact form not sending email” is the same problem wearing different clothes – the contact form is fine, the transport underneath it is not, and the fix is identical. Do the setup once per site, and email stops being something you have to think about again.