WordPress Not Sending Emails: Find Where the Message Dies

Reviewed by Mohammad HumzaPublished 12 min readSecurity & Maintenance
A row of metal mailboxes with one envelope left on top, for a guide to WordPress not sending emails

The first email I sent for this article never reached a mail server. I called wp_mail() on a local WordPress 7.1.2 install, and WordPress rejected its own default From address, wordpress@localhost, before it tried to connect to anything. The function returned false and nothing else happened unless you were listening for the error.

That small failure is a good map of the whole problem. A message from WordPress has to clear three checkpoints: WordPress has to build a valid message, something on the server has to hand it off, and the receiving mailbox has to trust it. “WordPress not sending emails” means something different at each checkpoint, and so does the fix. Below I follow one message through all three, with the output from tests I ran on that local install today (October 17, 2026) and what Gmail and Yahoo now require at the far end.

The short version of fixing WordPress not sending emails: log the failure first, then send through an authenticated SMTP or API service using an address on your own domain, and confirm SPF, DKIM and DMARC pass on a real inbox. The rest of this page explains why each of those steps exists.

How wp_mail() builds and sends a message

The emails WordPress core sends go through wp_mail() in wp-includes/pluggable.php: password resets, new user notices, the admin’s fatal error “recovery mode” message, comment notifications. Most plugins use it too. WooCommerce order emails go through it by default; its WC_Email::send() method calls wp_mail through a filterable callback. Reading the 7.1.2 source, the order of operations is what matters:

  1. WordPress creates a PHPMailer object (version 7.1.1 is bundled with 7.1.2) and, since WordPress 5.5, tells it to validate addresses with WordPress’s own is_email().
  2. If no plugin passed a From header, it builds one: wordpress@ plus the host of your site URL, with a leading www. removed. The wp_mail_from and wp_mail_from_name filters can change it.
  3. It calls setFrom(). If the address fails validation, WordPress fires wp_mail_failed and returns false right there.
  4. It switches PHPMailer to isMail(), which means PHP’s built-in mail() function.
  5. Only now does it fire the phpmailer_init action, where SMTP plugins and custom code change the transport.
  6. It sends. Success fires wp_mail_succeeded; an exception fires wp_mail_failed.

Two details from that list explain a lot of forum threads. First, a true return value only means the hand-off worked. The wp_mail() reference says so directly: it “does not automatically mean that the user received the email successfully.” Second, anything hooked to phpmailer_init never runs if step 3 fails. I hit that in my own test, which I’ll show below.

First, make WordPress tell you why

When you find WordPress not sending emails, add a log before changing anything, because WordPress has no screen that shows failed emails. The wp_mail_failed action (added in WordPress 4.4.0) receives a WP_Error with PHPMailer’s message plus the recipient, subject and headers. Hooking it takes a few lines. Put this in a file in wp-content/mu-plugins/ so a theme switch can’t remove it:

<?php
/**
 * Plugin Name: Log failed emails
 */
add_action( 'wp_mail_failed', function ( $error ) {
	$data = $error->get_error_data();
	error_log( sprintf(
		'wp_mail failed to %s, subject "%s": %s',
		implode( ', ', (array) $data['to'] ),
		$data['subject'],
		$error->get_error_message()
	) );
} );

With no destination argument, error_log() writes to the PHP error log; with WP_DEBUG and WP_DEBUG_LOG on, that is wp-content/debug.log. I deliberately leave the message body out, because contact form submissions carry other people’s personal details. In my test I ran the same callback with a timestamp added, writing to a scratch file, and aimed SMTP at a port nothing was listening on. WordPress gave up after 2.1 seconds and the log line read:

[2026-10-17T12:20:25+00:00] wp_mail failed to test@example.com, subject "Test 6: wrong SMTP port": SMTP Error: Could not connect to SMTP host. Failed to connect to server

That one line usually tells you which checkpoint failed. Here is how the common forms of WordPress not sending emails, including the case where nothing is logged, map to the three stages:

What you see Where the message died Look at
Invalid address: (From): ... in the log Inside WordPress, before sending The From address and your site URL
Could not instantiate mail function. The hand-off to PHP mail() Whether your host allows mail() at all
SMTP Error: Could not connect to SMTP host. The hand-off to an SMTP server Host, port, encryption, outbound port blocking
Nothing logged, nothing received, not in spam After the hand-off The receiving server’s verdict, then the host’s mail queue
Arrives, but in spam At the receiving server SPF, DKIM and DMARC alignment
Only form notifications go missing Inside WordPress The form’s From field

Checkpoint 1: the From address WordPress picks

On my test install the site URL is http://localhost:8909, so core built wordpress@localhost. Here is the output with a wp_mail_failed listener attached and nothing listening on the mail port, trimmed to the relevant lines:

wp_mail_failed fired
  code:    wp_mail_failed
  message: Invalid address:  (From): wordpress@localhost
  data keys: to,subject,message,headers,attachments,phpmailer_exception_code
wp_mail() returned: false

The cause is in is_email(): it rejects any domain without a dot (the domain_no_periods check). So any install whose URL host has no dot, such as localhost or an intranet name, cannot send a single core email until something supplies a real From address. That is a common reason for WordPress not sending emails on a local or internal staging copy, and an SMTP plugin alone won’t fix it if the plugin only acts on phpmailer_init. I tested exactly that: with an SMTP hook in place but no From filter, the result was the same “Invalid address” error and my SMTP catcher received nothing.

On a live site, wordpress@yourdomain.com passes validation, but core’s own source comment admits the weakness: “Some hosts will block outgoing mail from this address if it doesn’t exist.” If that mailbox doesn’t exist, replies to it go nowhere.

Contact forms add a second From problem. Many forms can be set to send “from” the visitor, so a notification claims to come from someone@gmail.com while actually leaving your server. Google’s email sender guidelines warn senders not to impersonate Gmail From headers and say Gmail will apply a DMARC quarantine policy to them. The fix is to send from your own domain and put the visitor in Reply-To, so you can still hit reply. When I sent a form-style message with From: Visitor <visitor@gmail.com> through the setup in the next section, the captured headers came out like this:

From: Example Site <hello@example.com>
Reply-To: Visitor <visitor@gmail.com>

If the recovery mode message from a crashed plugin never reached you, this checkpoint is a likely reason; my white screen of death walkthrough covers what that email contains and how to get in without it.

Checkpoint 2: the hand-off from your server

By default WordPress hands the message to PHP’s mail(). On a typical Linux host that pipes it into the server’s local mail program; on Windows, PHP talks SMTP to whatever php.ini names, which on my machine was localhost:25 with nothing running. With a valid From address set through wp_mail_from, the second test got further and failed here instead:

phpmailer_init fired. Mailer: mail | From: site@example.com | Sender: ''
wp_mail_failed fired
  message: Could not instantiate mail function.
wp_mail() returned: false

This is the classic shared hosting version of WordPress not sending emails at all. “Could not instantiate mail function” is PHPMailer’s generic message when mail() returns false. On a live host it usually means the host disabled mail() or has no working local mail program, and only the host can change that. Even PHPMailer’s own README says mail() “should be avoided when possible” and that it is faster and safer to use SMTP, even to localhost.

Notice Sender: '' in that output. Core calls setFrom( $from_email, $from_name, false ), and that false stops PHPMailer copying the From address into the envelope sender. So on mail(), the envelope sender, which the receiving server turns into the Return-Path, is whatever the host’s mail program decides. Remember that for checkpoint 3.

A plugin-free SMTP setup

Most guides stop at “install an SMTP plugin.” Plugins are fine, especially if you want a log screen, but the WordPress side of WordPress SMTP is only a dozen lines, and the phpmailer_init hook exists for exactly this. Credentials go in wp-config.php, above the line that loads wp-settings.php, so they stay out of the database:

define( 'SMTP_HOST', 'smtp.your-provider.example' );
define( 'SMTP_PORT', 587 );
define( 'SMTP_SECURE', 'tls' );   // 'tls' on 587, 'ssl' on 465
define( 'SMTP_USER', 'your-smtp-username' );
define( 'SMTP_PASS', 'your-smtp-password' );
define( 'SMTP_FROM', 'hello@yourdomain.com' );
define( 'SMTP_FROM_NAME', 'Your Site' );

With a mail password now in that file, the wp-config.php permissions in my WordPress security checklist matter more than before. Then add a must-use plugin, wp-content/mu-plugins/smtp-from-config.php. This is the code I ran, apart from one comment and the test values living in the constants:

<?php
/**
 * Plugin Name: SMTP from wp-config constants
 */
add_action( 'phpmailer_init', function ( $phpmailer ) {
	if ( ! defined( 'SMTP_HOST' ) ) {
		return; // No constants, leave WordPress on PHP mail().
	}
	$phpmailer->isSMTP();
	$phpmailer->Host       = SMTP_HOST;
	$phpmailer->Port       = SMTP_PORT;
	$phpmailer->SMTPSecure = SMTP_SECURE;
	$phpmailer->SMTPAuth   = true;
	$phpmailer->Username   = SMTP_USER;
	$phpmailer->Password   = SMTP_PASS;
	$phpmailer->From       = SMTP_FROM;
	$phpmailer->FromName   = SMTP_FROM_NAME;
	$phpmailer->Sender     = SMTP_FROM; // Envelope sender, becomes Return-Path.
} );

// wp_mail() validates From before phpmailer_init runs, so set it here too.
add_filter( 'wp_mail_from', function ( $from ) {
	return defined( 'SMTP_FROM' ) ? SMTP_FROM : $from;
} );

The port pairs come from PHPMailer: ENCRYPTION_SMTPS (the string ssl) for implicit TLS on 465, ENCRYPTION_STARTTLS (tls) on 587. The wp_mail_from filter at the bottom is there because of my third test: without it, the site died at checkpoint 1 and the SMTP code never ran. Over SMTP, PHPMailer already falls back to From for the envelope sender when Sender is empty, so that line just makes the Return-Path address explicit.

To check it without sending real mail, I ran a 48-line SMTP listener written with Python’s standard library on 127.0.0.1:2525, which accepts any login and saves what it receives. Pointed at it, wp_mail() returned true and fired wp_mail_succeeded. The client side of the session, trimmed, with credentials masked by my listener:

C: EHLO Hamza-Lenovo
C: AUTH LOGIN <hidden>
C: MAIL FROM:<hello@example.com>
C: RCPT TO:<test@example.com>
C: DATA

And the headers WordPress produced:

From: Example Site <hello@example.com>
Subject: Test 4: SMTP via mu-plugin
Message-ID: <Ccm1ajOSyebUh4a4xfuPOyLocSdNencWVBUz2IynMyU@Hamza-Lenovo>
X-Mailer: PHPMailer 7.1.1 (https://github.com/PHPMailer/PHPMailer)

There is no Return-Path header in what WordPress sends; the receiving server writes one from MAIL FROM, which is now your own address. The Message-ID ends in my computer’s name because PHPMailer falls back to the machine hostname when it has no web server name to use; set $phpmailer->Hostname to your domain if you want that tidier.

Two trade-offs to know. This code forces one From address for every plugin, which is usually what you want. WooCommerce adds its own wp_mail_from filter while sending, but phpmailer_init runs after that, so the constant still wins; set the store’s sender address under WooCommerce’s email settings to the same mailbox so the two don’t disagree. On a store, WordPress not sending emails means customers without order confirmations, so if those emails need per-type senders or more complex routing, that is a job for WooCommerce development rather than a dozen lines in an mu-plugin.

When SMTP itself is blocked: use the provider’s API

Some servers can’t open SMTP connections at all. DigitalOcean’s documentation says ports 25, 465 and 587 are blocked on Droplets by default, and Google Cloud blocks connections to external destination port 25 by default. If the cause of WordPress not sending emails turns out to be a connection error like my test 6 on every port, sending over the provider’s HTTPS API is the way around it. That replaces the transport rather than configuring PHPMailer, so it needs the provider’s plugin or custom code; WordPress has offered a pre_wp_mail filter since 5.7 that lets code take over sending completely. The authentication rules below apply either way.

Checkpoint 3: why the receiving server sends it to spam

Plenty of reports of WordPress not sending emails are really emails that were sent and then filtered. When a message is handed off and still doesn’t show up, the receiving server is the next suspect. Gmail and Yahoo both check three things against your domain:

  • SPF checks whether the server that delivered the message is allowed to send for the domain in the envelope sender (the Return-Path).
  • DKIM checks a signature added by the sending server, tied to the domain in its d= tag.
  • DMARC passes only when SPF or DKIM passes for a domain that is aligned with the visible From address. The current specification, RFC 9989 (May 2026), defines relaxed alignment as both domains sharing the same organizational domain.

Now the earlier details pay off. A shared server’s mail() can pass SPF for the host’s own domain and still fail DMARC, because that domain isn’t yours. A contact form sending as visitor@gmail.com fails because your server isn’t Gmail. Sending through a provider that has verified your domain, with From and Sender on that domain, is what makes alignment possible. The provider gives you the DNS records to add; until they are live, the SMTP fix moves the failure instead of removing it.

The big mailbox providers made this mandatory in 2024. Google’s guidelines say that starting February 1, 2024, everyone sending to Gmail accounts must set up SPF or DKIM, have valid forward and reverse DNS, use TLS, and keep the spam rate reported in Postmaster Tools below 0.3%. Senders of more than 5,000 messages a day to Gmail also need SPF and DKIM, a DMARC record, a From domain aligned with SPF or DKIM, and one-click unsubscribe on marketing mail. Yahoo’s sender requirements, enforced from February 2024, match closely: SPF or DKIM for everyone, and for bulk senders both, plus a DMARC policy of at least p=none that passes, with relaxed alignment accepted. A brochure site sending a few notices a day isn’t a bulk sender, but the all-sender rules still apply.

Testing a real message

The only proof that you have fixed WordPress not sending emails is a message in a real inbox, and my local test stops at the hand-off; it can’t show what Gmail decides. On a live site, trigger a real email (a password reset to a Gmail address you control is the simplest), then open it, click More, then Show original. Gmail shows the full header. Look for spf=pass, dkim=pass and dmarc=pass, and check that the domains named there match the domain in From. If DKIM shows your provider’s domain instead of yours, domain verification at the provider isn’t finished yet.

The order I would work through it

If you are dealing with WordPress not sending emails on a live site, this is the sequence, each step depending on the one before:

  1. Add the wp_mail_failed logger and trigger a password reset. Read the log line before changing anything else.
  2. If the log shows an invalid From, set one on your own domain with wp_mail_from, and change any form that sends “from” the visitor to use Reply-To instead.
  3. If mail() can’t be instantiated or is unreliable, move to an authenticated provider over SMTP (the constants and mu-plugin above, or a plugin) or over its API when the host blocks SMTP ports.
  4. Add the provider’s SPF and DKIM records and a DMARC record for your domain, wait for DNS, and confirm all three pass in Show original.
  5. Keep the logger; email breaks again when a password changes or a provider suspends an account.

Being honest about my test: it ran on a local install on Windows with a fake SMTP server, so it proves what WordPress and PHPMailer hand off, not what any inbox accepts. I kept every command and output in a test log, and restored the install byte for byte afterwards. If you’d rather not own any of this, mail delivery checks are part of my WordPress maintenance work, alongside updates and backups.

Share this article

Copied

Start a project

Working on something like this? Send a line and get a written quote within one business day.