Why WooCommerce Order Confirmation And Password Reset Emails Land In Spam

Published

A client asked me why their WooCommerce order confirmations kept going to spam. The marketing emails were fine, sent through Klaviyo, authenticated, landing in the inbox. The order confirmation was a different story.

Why Transactional Email Breaks Differently

That's the part people miss. A WooCommerce store usually has two separate email systems, not one. Marketing email gets the setup and attention. Transactional email, the order confirmations, shipping updates, and password resets, often gets none.

This is a different failure mode from a marketing campaign getting flagged, and it needs its own check.

Marketing platforms like Klaviyo handle their own sending infrastructure. You connect a domain, Klaviyo walks you through SPF and DKIM, and the platform refuses to send a campaign until that's done.

WooCommerce doesn't work that way. Out of the box, it sends transactional email straight from your web server using PHP's built-in mail function. Nobody walks you through authentication. Nothing refuses to send if it's missing.

So a store can have a fully authenticated Klaviyo setup and a completely unauthenticated transactional stream running in parallel, from the same domain. Gmail and Outlook see two different senders behaving two different ways.

WordPress's Default Mail Function Is The Actual Problem

`wp_mail()`, the function WooCommerce calls for every transactional email, is a thin wrapper around PHP's `mail()`. It hands the message to whatever mail transfer agent is running on the server and does not sign it with DKIM or check for an SPF record first.

Most shared hosting environments do have some local MTA installed, so the email does go out. That's exactly why this breaks quietly. The order confirmation leaves the server, nothing errors, and it shows up as sent in WooCommerce's own order notes.

What happens after that depends entirely on whether the receiving mail server trusts an unsigned message from your server's IP, which is the part nobody checks until a customer complains.

The DNS Check That Tells You If Your WooCommerce Emails Are Even Configured

Before changing how WooCommerce sends anything, I check what's actually published in DNS for the domain. SPF and DMARC are both plain TXT records, so this is a direct lookup, no WordPress admin access needed.

import dns.resolver

def get_txt_records(name):
    try:
        answers = dns.resolver.resolve(name, "TXT")
        return [b"".join(r.strings).decode() for r in answers]
    except dns.resolver.NXDOMAIN:
        return []
    except dns.resolver.NoAnswer:
        return []

def check_spf(domain):
    records = get_txt_records(domain)
    return [r for r in records if r.startswith("v=spf1")]

def check_dmarc(domain):
    records = get_txt_records(f"_dmarc.{domain}")
    return [r for r in records if r.startswith("v=DMARC1")]

Running `check_spf` and `check_dmarc` against a domain gives a clean yes or no. An empty list for either one means exactly what it looks like: nothing is published, so there's nothing for a receiving server to validate against.

DKIM is the one record I can't check this way, since the selector (the part before `._domainkey` in the record name) depends entirely on whatever service is actually signing the message. For a default WooCommerce setup sending through PHP `mail()`, there usually isn't one, because nothing in that path signs anything.

flags = []
if not check_spf(domain):
    flags.append("No SPF record published")
if not check_dmarc(domain):
    flags.append("No DMARC record published")

for flag in flags:
    print(flag)

On the client's domain, both came back empty. The marketing subdomain Klaviyo sent from had a correct SPF include and a DMARC policy. The root domain WooCommerce was sending transactional mail from had neither.

What 'Configured' Actually Means Here

A record existing is not the same as it being correct. SPF needs to include the actual sending source, whether that's your host's mail server, a transactional provider, or both. A stale SPF record that only lists an old provider fails just as hard as having none.

DMARC needs a policy value, `p=none`, `p=quarantine`, or `p=reject`, not just the record's presence. `p=none` publishes visibility without enforcing anything, which is a reasonable starting point, not a finished setup.

Most WooCommerce installs I've looked into fall into one of two groups: no SPF or DMARC at all, or a record that was correct for a mail setup that changed since. Either way, the check above tells you which group a given domain is actually in.

Fixing It

The fix is routing transactional email through a sending method that's actually authenticated, instead of PHP `mail()` guessing its way through. No plugin that just promises 'better deliverability' changes that on its own.

That usually means an SMTP plugin configured against a real transactional provider, Amazon SES, Postmark, SendGrid, whichever one the host already uses for marketing or already has credentials for, pointed at WooCommerce's transactional stream specifically.

The provider handles DKIM signing on its end. Your job is making sure SPF includes that provider and DMARC has a policy that isn't silently set to none forever.

This is also why mixing transactional and marketing traffic on the same sending domain without separating the paths causes exactly the kind of shared-reputation problem a stricter DMARC policy is meant to contain. Fixing the WooCommerce side without checking the rest of the domain's authentication just moves the gap somewhere else.

What This Doesn't Fix

Authenticating the transactional stream stops Gmail and Outlook from distrusting the sender. It does not fix a store that's already built up a bad sending reputation from months of unauthenticated mail, since that takes time and consistent clean sending to recover from, not a DNS change alone.

It also doesn't replace an actual deliverability audit of the marketing side. SPF, DKIM, and DMARC being correct on the transactional stream says nothing about list hygiene, engagement, or content triggers on the campaign side, which is a separate diagnostic in its own right.

Once the DNS records are actually correct, I still check one more thing before calling it done: whether the host's shared sending IP already has a bad reputation from other tenants on the same server. A new, correct SPF and DMARC setup on a flagged IP still lands in spam, and that's a hosting conversation, not a DNS one.

Not sure if your store's transactional email is even authenticated?

I run the full email marketing setup, Klaviyo included, and I check the parts of a store's email most agencies never look at.