WooCommerce 11.1 Order Emails Not Arriving: A Three-Layer Debug from wp_mail to SPF/DKIM/DMARC
I actually hit this one on a WooCommerce store I run myself. The site had just moved from WooCommerce 10.9 to 11.1 (11.1.1, the September 18, 2026 security patch), and the next day customers started asking the same question: "I placed an order — why didn't I get a confirmation?" The order list looked fine. The SMTP plugin's log was entirely green. I cringed, used WP-CLI to send a test message, and wp_mail() returned true.
It took me three days to unwind the whole path. The uncomfortable headline: whether your WooCommerce order emails arrive has far less to do with which SMTP plugin you installed than most guides suggest. The root cause in my case was in the outermost layer — DNS alignment for the sending domain — not the relay. I tested the layers in the wrong order and burned two days for it.
Here is the short version. An order email reaches a customer's inbox only when all three of these are true at the same time: the message was actually handed to a relay that receivers trust; your sending domain passes SPF/DKIM/DMARC; and your sender reputation is good enough to keep it out of the spam folder. Miss any one and the symptom is identical — the customer says nothing arrived.
⏳ TL;DR
🔍 **The three-layer method — work through it in order, do not skip ahead**: Use WooCommerce → Status → Logs to confirm "did WooCommerce even try to send?" → then use phpmailer_init to capture the SMTP transcript and confirm "did the relay accept it?" → then check the relay's delivery log plus dig to confirm "did the receiving domain accept it?"
🖥️ Desk gear for long nights staring at order dashboards and mail logs (both bought with my own money):
- **Dell UltraSharp U2723QE 27" 4K USB-C Hub monitor** — one USB-C cable carries 4K and 90W of power, and the built-in KVM lets me flip between my laptop terminal and a jump-host log window without reaching for a cable. 💰 Roughly $429–529 as of writing; prices swing with promotions, so check the live listing.
👉 Check the Dell U2723QE on Amazon >>
- **BenQ ScreenBar Halo 2 monitor light bar** — its three-zone backlight fills in the screen-to-wall contrast gap, which matters at 2 a.m. when you are reading an SMTP transcript. 💰 Around $179–199.
👉 Check the BenQ ScreenBar Halo 2 on Amazon >>
The one-line rule: fix the relay (SMTP), then fix the domain (SPF/DKIM/DMARC), and only then worry about message content and reputation. Get that order wrong and you will spend a week rewriting email templates while the real problem sits in your DNS zone.
About this write-up and my affiliate disclosure
> Affiliate disclosure: this post contains Amazon Associates links (tag=techpassive-20). If you buy through them I earn a small commission and you pay exactly the same price you would going straight to Amazon. Both pieces of desk gear above are things I bought myself, not review samples. The version numbers, SMTP response codes and protocol behaviour in the technical sections come from vendor documentation and my own server logs; versions and prices are accurate as of publication.
Three kinds of "not delivered" — confuse them and you will search the wrong layer
The entry point for each failure class is completely different, and mixing them up is the biggest time sink in this whole exercise.
| Layer | Typical symptom | Where to look | Usual cause |
|---|---|---|---|
| Trigger | Order exists, but no send was ever attempted | WooCommerce → Status → Logs, order notes | Order stuck in pending payment, notification disabled, wp-cron not running |
| Transport | A send was attempted, but the relay refused it or could not be reached | SMTP plugin log, PHPMailer transcript | Wrong credentials, blocked port, relay region mismatch |
| DNS and reputation | Relay log says 250 OK, customer still has nothing | Relay delivery log, receiver rejection codes, DMARC reports | SPF/DKIM failing, DMARC missing or unaligned, message filed as spam |
My failure was in the third layer. The relay log read 250 Ok: queued as ..., which looks like a perfect outcome — but the DKIM signature was d=fail, because the selector record for it had never been published.
The 2026 entry ticket: you trigger bulk sender rules at roughly 5,000 messages a day
This is not a big-store-only concern. The bulk sender rules Gmail and Yahoo have enforced since February 2024 kick in at close to 5,000 messages or more sent to personal inboxes (@gmail.com, @googlemail.com) within 24 hours — and the classification is permanent. Cross the line once, even on a Black Friday blast, and you are held to the bulk requirements forever. Microsoft followed in May 2025 with a near-identical rule set for outlook.com, hotmail.com and live.com.
What matters more is how much teeth enforcement has grown. Since November 2025, Gmail has escalated non-compliance from "route it to spam" to temporary and permanent rejections. Order confirmations that a customer could once have rescued from a spam folder now may simply never be delivered at all.
The requirement list, per each provider's own documentation:
| Requirement | What it demands | How to verify |
|---|---|---|
| SPF | Must exist and pass | `dig +short TXT yourdomain.com` |
| DKIM | Must exist and pass; key ≥1024 bits (Yahoo explicitly rejects 512-bit keys) | `dig +short TXT selector._domainkey.yourdomain.com` |
| DMARC | Must exist, `p=none` at minimum | `dig +short TXT _dmarc.yourdomain.com` |
| Alignment | The From domain must align with SPF or DKIM (relaxed is fine) | DMARC reports, authentication-results headers |
| One-click unsubscribe | RFC 8058 for marketing mail, honoured within two days | `List-Unsubscribe` header |
| Complaint rate | Below 0.30%; providers recommend targeting under 0.10% | Google Postmaster Tools and similar |
| PTR and TLS | Valid forward and reverse DNS, TLS on the connection | `dig -x your_sending_ip`, SMTP transcript |
| Formatting | RFC 5322 compliant, with a Message-ID | Inspect a raw message's headers |
Rejection codes you will actually see (exact numbers vary by receiver — trust the one you get): 550 5.7.26 when both SPF and DKIM fail, 421 4.7.40 when the DMARC record is missing, 421 4.7.32 on DMARC alignment failure, 5.7.25 for PTR problems and 5.7.29 for TLS problems. On Microsoft's side you will commonly see 550 5.7.515; on Yahoo's, 550 5.7.9.
Step 1: confirm WooCommerce actually tried to send the email
Starting with WooCommerce 10.9 (released June 23, 2026), transactional email logging landed in core: WooCommerce → Status → Logs now shows whether a given order email was sent, and where available, why it was not. One caveat worth knowing: 10.9.0 was briefly reverted after release, with 10.9.1 through 10.9.4 (July 7, 2026) filling the gaps — so if your store is still on 10.8 or earlier, that panel may not exist. Check your WooCommerce version first.
Three unglamorous things decide this layer:
1. Order status. WooCommerce only fires the matching notification when the status advances to processing, completed, and so on. If the payment gateway's callback never returns, the order sits in pending payment and the confirmation email was never triggered at all. Debugging SMTP at that point is wasted effort — fix the gateway.
2. Notification toggles. Under WooCommerce → Settings → Emails, open each email type's Manage screen and confirm "Enable this email notification" is ticked, and that admin recipients are correct.
3. From address. In the sender options at the bottom of the same screen, the From address must be on your own domain (say, orders@yourstore.com). A free Gmail or Yahoo address as the From is the fastest route to the spam folder or outright rejection.
A quick WP-CLI sanity check that scheduled jobs are firing:
wp cron event list --fields=hook,next_run_relative | grep -i woocommerce
Set your expectations here too: WooCommerce does not retry a failed email. It fires once and moves on. Retry queues are a capability of the relay (transactional email provider), not of WooCommerce — which becomes a hard requirement once you pass a few hundred orders a day.
Step 2: prove wp_mail really sent it, instead of "succeeding" silently
This layer lies to you more than any other. wp_mail() returning true means "the message was handed to the configured transport and nothing threw an exception." It does **not** mean the receiving server accepted it. It will mislead you in three ways: handed to local PHP mail() and queued by an MTA that later bounces it; handed to a relay's API that accepted the call and rejected the message at the receiver; or short-circuited entirely by a plugin filter.
There is one mechanism here that you have to know in 2026: the **pre_wp_mail filter** (WordPress 5.7+). Returning anything non-null from it ends wp_mail() immediately — none of the code after it runs. And the mainstream SMTP plugins — WP Mail SMTP, FluentSMTP, Post SMTP — **all take over sending by hooking pre_wp_mail**. The direct consequence is that **once an SMTP plugin is in charge, wp_mail_failed never fires again**, because it is watching a code path your mail no longer takes. The failure-logging callback you carefully wrote will sit there in total silence.
So instrument three points at once. Drop this into a site-specific mu-plugin under wp-content/mu-plugins/:
SMTPDebug = 2; // 1 = errors and messages, 2 = client<->server, 3-4 = verbose
$phpmailer->Debugoutput = function ($str, $level) {
error_log("[wp_mail/$level] $str");
};
}, 999);
// 2) Capture failures (note: will NOT fire once pre_wp_mail takes over)
add_action('wp_mail_failed', function ($error) {
error_log('[wp_mail_failed] ' . $error->get_error_message());
});
// 3) Capture successes, to confirm recipients and headers
add_action('wp_mail_succeeded', function ($mail_data) {
error_log('[wp_mail_succeeded] to=' . implode(',', (array) $mail_data['to']));
});
// 4) Fix the default sender: wp_mail defaults to wordpress@yourdomain, which many hosts refuse
add_filter('wp_mail_from', fn() => 'orders@yourstore.com');
add_filter('wp_mail_from_name', fn() => 'Your Store');
Then enable logging in wp-config.php:
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
Finally send a controlled test message and follow the log:
wp eval 'var_dump( wp_mail( "you@example.com", "tp mail test", "body" ) );'
tail -f wp-content/debug.log
Reading the result is refreshingly unambiguous. If the transcript shows **nothing at all**, something short-circuited wp_mail — almost certainly your SMTP plugin's pre_wp_mail hook — so go look in that plugin's own log instead. If the transcript reaches 250 Ok and the message still never arrives, the failure is past PHPMailer entirely; jump straight to Step 4.
Step 3: hand your mail to a relay you trust (and the 2026 port reality)
Do not send order email through your host's PHP mail(). Shared hosts routinely throttle or disable it, and receivers distrust unauthenticated direct submission anyway.
Once you have picked a relay, ports are the first wall:
- **25** — blocked by default almost everywhere. On Amazon EC2, for example, outbound port 25 is throttled by default, and the symptom is a **silent timeout** rather than a clear error, so it looks like a network problem when it is really a policy one. You have to file a separate request to remove the email sending limitation.
- **587 (STARTTLS)** — the standard recommendation.
- **465 (TLS wrapper / SMTPS)** — an older protocol, but widely supported by clients. Note that TLS and SSL are not interchangeable choices: the port and the encryption mode have to match.
- **Fallback ports** — Amazon SES also accepts STARTTLS on 2587 and TLS wrapper on 2465, with endpoints in the form `email-smtp.{region}.amazonaws.com`. When the standard ports are blocked upstream, this is the fastest way around it.
Three SES-specific traps are worth knowing before you hit them:
1. **SES SMTP credentials are not your IAM access keys.** They are a separate pair generated from the SES console's "Create SMTP credentials": a 20-character username (starting AKIA...) and a longer Base64 password. Paste an IAM secret access key into the SMTP password field and authentication will fail, every time.
2. **Credentials are region-bound.** The SMTP password is derived through an HMAC signature that includes the region, so a credential minted in us-east-1 connecting to the eu-west-1 endpoint fails reliably with a 535 — even though the underlying IAM user is global.
3. New accounts start in the sandbox: 200 messages per 24 hours, one per second, verified recipients only, counted per region. Sending real order email requires a production access request.
Step 4: SPF, DKIM, DMARC, and the word that actually matters — alignment
This is where my root cause lived, and it is the step most guides skip.
SPF is one TXT record declaring which servers may send mail for your domain. Two hard constraints: a domain may only have one SPF record (multiple records is a permanent error), and you get at most 10 DNS lookups before SPF fails outright. The usual shape:
yourstore.com. TXT "v=spf1 include:your-relay-spf-domain ~all"
**DKIM** signs every message. Your relay gives you a TXT record at something like selector._domainkey.yourdomain.com. Here is the detail that bit me: **when you change relays or regions, the selector usually changes too.** If the old record lingers and the new one never gets published, the signature reports d=fail — while SPF may still pass, leaving the overall authentication incomplete and, at strict receivers, the message judged suspicious anyway.
DMARC tells receivers what to do when SPF or DKIM fails, and collects reports:
_dmarc.yourdomain.com. TXT "v=DMARC1; p=none; rua=mailto:dmarc@yourstore.com"
Verify all three in one go:
dig +short TXT yourstore.com
dig +short TXT 20260901._domainkey.yourstore.com
dig +short TXT _dmarc.yourstore.com
Then handshake directly with the relay to confirm the port and TLS work (SES in us-east-1 as the example):
openssl s_client -connect email-smtp.us-east-1.amazonaws.com:587 -starttls smtp -crlf
Treat p=none as a starting point, not a destination: it collects reports and satisfies the minimum, but it blocks nothing. When the industry baseline moves to p=quarantine, you will at least have weeks of DMARC reports to work from. Register for Google Postmaster Tools, Yahoo Sender Hub and Microsoft SNDS in the meantime — they are the only place you can see your **actual** complaint rate.
Troubleshooting: five real errors and how to fix them
Error 1: `SMTP Error: Could not authenticate.` / `535 Authentication Credentials Invalid`
Cause: three candidates, in order of frequency — IAM access keys pasted in as SMTP credentials; credentials and endpoint in different regions (the most common SES case by far); or a stray space or newline copied into the password.
**Fix**: regenerate a dedicated SMTP credential pair in the relay console, strictly in the **same region as the sending endpoint**; paste rather than type both username and password; if you are on SES, confirm the From address is a **verified identity in that same region**. Then run the openssl s_client handshake again — you want to see 235 Authentication successful before you move on.
Error 2: `550 5.7.26 This mail has been blocked because the sender is unauthenticated`
**Cause**: Gmail's rejection when SPF and DKIM **both** fail. Seen most often when a store changed its sending domain but left the DNS records on the old one, or the relay's SPF include was never added, or the DKIM selector record is still unpublished (which usually shows up first as 421 4.7.32 alignment failure before escalating to 550).
**Fix**: dig all three records and confirm they exist and point at the current relay; confirm the From domain aligns with SPF or DKIM; then send a message to a scoring address and read the authentication results it reports. Watch the subdomain trap as well: only the primary domain counts and subdomains roll up into it, so giving mail. or news. its own record set does not keep you under the threshold.
Error 3: `wp_mail()` returns `true`, the relay log shows `250 Ok`, the customer still has nothing, and `wp_mail_failed` never fired once
**Cause**: the "silent success" from the top of this article, with two independent contributors. First, once an SMTP plugin takes over via pre_wp_mail, wp_mail_failed never fires, so your failure log is decorative. Second, 250 Ok only means the relay **accepted** the message; a later asynchronous bounce or a spam-folder filing at the receiving domain never propagates back into WordPress.
**Fix**: stop trusting WordPress-side logging alone. Read three places together — the relay's activity and delivery panels, its bounce feedback channel (for example an SNS subscription or webhook), and your DMARC reports. The mnemonic that resolves this fastest: if the transcript shows 250 but the receiver has nothing, the problem is on the receiving side; if the transcript is blank, a plugin short-circuited wp_mail and the answer is in the SMTP plugin's own log.
Error 4: `SMTP connect() failed` or a connection timeout, with no explicit rejection
Cause: the port is blocked upstream. Cloud providers throttle outbound port 25 by default (presenting as a silent timeout), and some hosts also block 465 or 587.
Fix: switch to port 2587 (STARTTLS) or 2465 (TLS wrapper) to test whether connectivity comes back; if it does, you have confirmed a port policy problem, and can then decide between requesting an unblock or standardising on the fallback port. Check that your encryption mode matches the port too — do not select STARTTLS on 465.
Error 5: `PHP Fatal error: Uncaught PHPMailer\PHPMailer\Exception: Could not instantiate mail function`
**Cause**: PHP's mail() is disabled in your execution environment (common on hardened shared hosting and containers) while the SMTP plugin sits in a "partially configured" state — connection details incomplete, so the plugin quietly falls back to PHP mail().
**Fix**: complete the relay configuration (host, port, encryption, username, password, authentication switch), and confirm the plugin reports "connected" rather than merely "active". Check whether disable_functions includes mail. Then rerun the Step 2 wp eval test message.
A 7-item pre-launch checklist
1. The From address is on your own domain and aligns with SPF or DKIM.
2. dig returns expected content for all three records; exactly one SPF record exists; DNS lookups stay under 10.
3. Relay credentials and endpoint are in the **same region**, and the handshake returns 235 followed by 250.
4. A WP-CLI test message produces log lines from both wp_mail_failed and wp_mail_succeeded as appropriate.
5. The test message is confirmed in two places at once — the relay panel and a Gmail inbox — and it is not in spam.
6. At least one sender reputation dashboard is registered and showing a visible complaint rate.
7. A real order (not a manually changed status) triggers the expected email. Treat WordPress 7.1's client-side media processing — which moves resizing and thumbnail generation into the browser and relieves PHP — as a welcome side benefit, not as a fix for a mail problem.
The takeaway
The most expensive lesson here is that **wp_mail() returning true, an all-green SMTP plugin, and a 250 OK in the relay log together still do not prove the customer received anything.** Order email sits on a revenue-critical path, and the only way to guarantee it is to instrument all three layers at once: WooCommerce's trigger log, the PHPMailer transcript, and the DNS plus receiver-side authentication results. Walk them from the inside out and you will usually find the cause in an afternoon. Walk them backwards and three days may not be enough to find the one DKIM selector record that never went live.
👉 Join MiniMax Token Plan: AI coding acceleration for businesses
👉 Join Xiaomi MiMo Platform: Leading AI model platform with cost-effective inference
👉 Join Aliyun AI: Top AI products with exclusive coupons for business innovation
📌 This article was AI-assisted generated and human-reviewed | TechPassive — An AI-driven content testing site focused on real tool reviews
🔗 Recommended Tools
These are carefully selected tools. Using our affiliate links supports us to keep producing quality content: