The symptom: silent failure
You submit a contact form and nothing arrives. You request a password reset and the inbox stays empty. A WooCommerce order goes through but the customer never gets a confirmation (if that is your specific symptom, the WooCommerce order emails not sending article covers the WooCommerce-specific causes like order status, HPOS, and the background queue). There's no error on screen, no warning in the admin, just silence. That silence is what makes WordPress mail issues so frustrating: WordPress reports success even when nothing was actually delivered to the recipient.
What "not sending" actually means
WordPress sends mail through its wp_mail() function, which on a Linux server hands the message off to PHP's mail() function. PHP mail() then writes the message to a local sendmail-compatible binary (postfix, exim, sendmail). The PHP manual is explicit about the catch: "Just because the mail was accepted for delivery, it does NOT mean the mail will actually reach the intended destination." So WordPress can return true, the host can accept the message, and the recipient mail server can still drop it on the floor without anyone telling you.
That means "not sending" usually maps to one of three failure layers:
- WordPress never builds or hands off the message. A plugin swallows the call, the recipient address is wrong, or a fatal PHP error stops execution before the mail line runs.
- The host accepts the message but never delivers it. PHP
mail()is disabled, the local MTA is not configured, or the host's outbound port 25 is blocked. - The receiving mail server rejects or quietly junks the message. SPF, DKIM or DMARC fail, the sending IP is on a blocklist, or the From address is forged.
The right fix depends on which layer is failing. The diagnosis below will tell you which one.
Common causes, ordered by likelihood
In my hands-on experience with shared and managed WordPress hosting, the rough order is:
- The host has disabled or rate-limited
mail(), or routes outgoing mail to a local mailbox you'll never check. - The default From address is
wordpress@yoursite.tld, which fails DMARC alignment with strict providers like Gmail and Outlook, so it's quietly junked. - SPF, DKIM or DMARC records are missing or wrong for the sending domain.
- A contact form plugin uses the visitor's address as the From header, which most providers now reject as spoofing.
- A security plugin or SMTP plugin is misconfigured and silently blocks outbound mail.
- The site has a critical PHP error on the page that triggers the email, so
wp_mail()is never reached. - The site has an error establishing a database connection, so transactional mails never get queued in the first place.
Diagnosis: figure out which layer is failing
Before changing anything, run these three checks in order. Each one is non-destructive.
Step 1: send a test email and capture the result
Install Check & Log Email (free, 100,000+ active installs as of April 2026). It logs every wp_mail() call, records whether PHP returned success or failure, and stores the headers, body and any error messages. This is the single most useful diagnostic tool for this problem because it tells you whether the message ever left WordPress.
Open Check & Log Email > Status and send a test email to an address on a different domain than your WordPress site. Then check the log.
You'll know it worked when: the log entry shows a green status and the test email actually arrives in the inbox you sent it to. If the log shows green but no mail arrives, the failure is at layer 2 or 3 (hosting or DNS). If the log shows red or "failed", the failure is at layer 1 (WordPress).
Step 2: capture the actual error with wp_mail_failed
If Check & Log Email shows a failure but doesn't surface a useful message, hook into the wp_mail_failed action to see the underlying PHPMailer exception. This action was introduced in WordPress 4.4 and fires after a PHPMailer exception is caught.
Create a file called log-mail-errors.php in wp-content/mu-plugins/ using your hosting panel's file manager (or download via SFTP, edit locally, and upload). If the mu-plugins folder doesn't exist yet, create it. Paste this into the file:
<?php
// Logs every wp_mail() failure to wp-content/debug.log
add_action( 'wp_mail_failed', function ( WP_Error $error ) {
error_log( 'wp_mail_failed: ' . $error->get_error_message() );
error_log( print_r( $error->get_error_data(), true ) );
} );
Trigger another mail (resubmit the contact form, request a password reset). Then open wp-content/debug.log through your hosting panel's file manager (or download it via SFTP). You should see the PHPMailer exception text, which usually names the exact reason: SMTP authentication failed, recipient rejected, connection refused, and so on.
You'll know it worked when: debug.log shows a wp_mail_failed: line containing a concrete error string. If no line appears, wp_mail() is not being called at all, which points to a critical error or plugin filter swallowing the call.
Step 3: check the host actually accepted the message
If layers 1 and 2 look healthy but mail still doesn't arrive at the recipient, check the host's outbound mail log. On most cPanel hosts this is Email > Track Delivery. On DirectAdmin it's E-mail Manager > Mailqueue Administration. On managed WordPress hosts it's usually surfaced under a "Mail" or "Email log" panel in the dashboard.
You're looking for a line that names your sender, names the recipient, and reports either "sent", "deferred" or "bounced". A "bounced" line will quote the receiving server's exact rejection reason ("550 5.7.1 SPF check failed", "DMARC policy", "Recipient address rejected").
You'll know it worked when: you can read the host log and see the exact disposition of your test message. If the host log is empty even though Check & Log Email reported success, the host quietly dropped the message before it ever hit a real MTA. Skip to the hosting solutions below.
Solutions by layer
Layer 1: WordPress is not sending the message
Cause: a plugin or filter is intercepting wp_mail().
Disable any security, SMTP or anti-spam plugin you have installed. If mail starts working again, re-enable them one at a time until the culprit shows itself. The most common offenders are misconfigured SMTP plugins that point at a dead mail server, and security plugins that block outgoing mail by default.
You'll know it worked when: a fresh test email through Check & Log Email arrives in the recipient inbox with all third-party mail-related plugins disabled.
Cause: the contact form plugin is misusing the From header.
Open your contact form plugin (Contact Form 7, Gravity Forms, Fluent Forms, etc.) and look at the form's mail settings. The From address must be on a domain you control, for example forms@yoursite.tld, not the visitor's email. Put the visitor's address in Reply-To instead. Without this, every modern receiver will fail DMARC alignment and either junk or reject the mail.
You'll know it worked when: a test submission produces a Check & Log Email entry whose From header is on your own domain, and the mail arrives.
Cause: a critical error on the page that triggers the mail.
Run the page that should trigger the email (the contact form, the password-reset form, the WooCommerce checkout) and open wp-content/debug.log through your hosting panel's file manager to check for fatal errors. If the page is silently 500-ing, see there has been a critical error on this website.
You'll know it worked when: the page loads cleanly, the debug log shows no fatal errors during the request, and a fresh submission produces a mail log entry.
Layer 2: the host is not delivering the message
Cause: PHP mail() is disabled or rate-limited by the host.
Many shared hosts disable PHP mail() for abuse reasons or rate-limit it to a few dozen messages per hour. Open a support ticket and ask, in plain words: "Is PHP mail() enabled on my account, and what is the per-hour outbound limit?" If it's disabled, you have to switch to SMTP (see below). If it's rate-limited and you're hitting the cap, the same fix applies.
You'll know it worked when: the host either confirms mail() is unrestricted, or you've moved to SMTP and your test mail arrives.
Cause: mail routing is set to "local" but you use external mail. If your domain's MX record points at Google Workspace, Microsoft 365, or another external mail provider, your hosting panel's mail routing for the domain must be set to Remote, not Local. With local routing the host tries to deliver to a mailbox on its own server, which doesn't exist, so the message lands in nothing. In cPanel this is Email Routing. In DirectAdmin it's under MX Records > Use this server to handle my emails: No.
You'll know it worked when: the host's mail log shows the message handed off to an external smarthost (or shows the recipient as a remote address), and the test mail arrives.
Cause: the shared sending IP is on a blocklist. Take the IP address of the outgoing mail line in your host's log and check it against MultiRBL or a similar aggregator. If you see hits on Spamhaus, SORBS or Barracuda, the cleanest fix is not to fight the blocklist: switch to authenticated SMTP through a transactional sender (see Layer 3) so your delivery no longer depends on the shared IP.
You'll know it worked when: outbound mail goes via the transactional sender, your host's outbound IP is no longer in the message path, and the test mail lands in the inbox.
Layer 3: the receiving mail server is rejecting or junking the message
This is where most modern WordPress mail problems actually live. Since Google and Yahoo's bulk sender requirements took effect in February 2024, the bar for being accepted by Gmail (and Outlook, and Yahoo) is much higher than it used to be. Even for low-volume senders, the practical baseline is now: send via authenticated SMTP, with SPF, DKIM and DMARC aligned to your sending domain.
The fix is two parts: route mail through an authenticated SMTP service, then add the matching DNS records.
Step 1: pick an SMTP service. Use a service that signs outbound mail with DKIM for your domain and gives you a bounce-tracking dashboard. Common options at the time of writing: Postmark, Mailgun, SendGrid, Amazon SES, Brevo (formerly Sendinblue). For low-volume transactional mail (form notifications, password resets, order confirmations), most have a free tier of a few hundred messages per day.
Step 2: install an SMTP plugin and configure it.
WP Mail SMTP, FluentSMTP, and Post SMTP all do the same job: they hook phpmailer_init to override WordPress's default mail transport with an authenticated SMTP connection. The relevant action is documented at phpmailer_init. Pick one, enter the host, port (usually 587 with STARTTLS or 465 with implicit TLS), username and password from your SMTP service, and set the From address to a real address on your own domain. Save and send a test. For the full plugin setup walkthrough, see how to configure a WordPress SMTP plugin.
Step 3: add the DNS records the SMTP service tells you to. Every SMTP service gives you the exact SPF, DKIM and (sometimes) DMARC records to add to your domain's DNS zone. Add them as TXT records in your DNS provider. For background:
- SPF (RFC 7208) authorises which mail servers may send for your domain.
- DKIM (RFC 6376) lets the sender cryptographically sign the message so the receiver can verify it wasn't tampered with.
- DMARC (RFC 7489) tells receivers what to do if SPF or DKIM fail, and requires the From-header domain to align with one of them.
A minimum DMARC record to start with, while you're checking that everything aligns, is:
_dmarc.yoursite.tld. IN TXT "v=DMARC1; p=none; rua=mailto:dmarc@yoursite.tld"
p=none means "monitor only, don't reject". Once you've watched the DMARC reports for a week or two and confirmed your legitimate mail is passing, raise the policy to p=quarantine and eventually p=reject.
You'll know all three steps worked when: a test email from your WordPress site arrives in a Gmail inbox, you click "Show original" and you see SPF: PASS, DKIM: PASS and DMARC: PASS in the Authentication-Results header. If any of those three say FAIL or NEUTRAL, the corresponding record is wrong and needs adjusting before the mail will be reliably accepted.
One more cause that catches people out: the default From address
WordPress's default From address is wordpress@<your hostname>, built from $_SERVER['SERVER_NAME'] in wp-includes/pluggable.php. If your site runs on www.yoursite.tld and you don't override the From header, every WordPress-generated email goes out as wordpress@yoursite.tld. Two things go wrong with this:
- That mailbox doesn't exist, so any reply or bounce vanishes.
- If you also use a separate provider for
@yoursite.tldmail, your SMTP path probably doesn't have permission to send as@yoursite.tld, so DMARC fails.
The fix is to set an explicit, real From address in your SMTP plugin (or via the wp_mail_from and wp_mail_from_name filters), pointing at a mailbox you actually monitor.
You'll know it worked when: Check & Log Email shows the new From address on every outbound message, replies to that address actually reach you, and DMARC passes.
When to escalate
If you've worked through all three layers and mail still doesn't go out reliably, ask your host for help. Before you open the ticket, gather:
- Your PHP version (Tools > Site Health > Info in WordPress, or
php -vif you have shell access). - Your hosting tier and provider name.
- The exact error string from
wp-content/debug.log(the line starting withwp_mail_failed:). - The headers and disposition of one failed test message from Check & Log Email's log view.
- A copy of the relevant lines from your host's outbound mail log for that test message.
- The full list of active plugins (Tools > Site Health > Info > Active Plugins).
- Your current SPF, DKIM and DMARC records as published in DNS (look them up with a free tool like MXToolbox, or use
dig TXT yoursite.tldanddig TXT _dmarc.yoursite.tldif you have shell access). - The From address WordPress is sending with, and the MX records for the same domain.
A host support engineer can resolve this in minutes if they have all of the above. Without it, expect a multi-day back-and-forth.
If the problem is specifically that password-reset emails don't arrive, the diagnosis is the same as above, but the broader user-facing context is covered in password reset not working in WordPress.
How to prevent it from coming back
- Send all WordPress mail through an authenticated SMTP service from day one. Don't rely on PHP
mail()on shared hosting in 2026. - Keep your SPF, DKIM and DMARC records under version control or at least documented somewhere outside DNS, so you can reproduce them after a registrar change. For the full DNS record setup, see the SPF, DKIM and DMARC walkthrough for WordPress.
- Set DMARC to
p=nonefirst and only raise it top=quarantine/p=rejectafter a couple of weeks of clean reports. - Monitor mail volume monthly. If you cross the 5,000-message-per-day bulk-sender threshold for Gmail, the requirements get stricter (one-click unsubscribe, spam-complaint rate below 0.30%).
- Keep Check & Log Email installed even on healthy sites, with retention set to about 30 days. The first time something breaks, you'll have a log to read instead of a guess to make.