On a test site, a wp_mail() call ends up in one of three places. It fails silently and you never find out, it reaches a real inbox belonging to a real person, or it is captured somewhere you can read it.
Most guides about WordPress not sending email are written about production, where the goal is delivery. On a copy of a site the goal is the opposite, and the two failure modes pull in different directions.
The two failure modes, and why they need different fixes
The first failure mode is the quiet one. You submit the contact form on your staging copy, nothing arrives, and you cannot tell whether the form is broken or the environment simply has no way to send mail.
The second is the expensive one. You clone production to test a checkout change, place three test orders, and three customers get order confirmations for orders that do not exist.
The first costs you an afternoon. The second costs you a support queue, and in a membership or WooCommerce site it can cost you a refund request.
Every fix below is aimed at one or the other, so it is worth knowing which one you are actually solving.
Why a test site usually cannot send mail at all
WordPress does not send email. It hands the message to PHP's mail() function, which hands it to a local mail transfer agent, which is the thing that actually talks to the recipient's mail server.
On a laptop, in a Docker container, or in most disposable environments, that last piece does not exist. There is no MTA installed, so the handoff fails at the first step.
This is why "WordPress is not sending email" on a test environment is almost never a WordPress problem. It is an infrastructure problem that WordPress reports badly.
Even where an MTA does exist, a fresh server IP with no SPF, DKIM, or DMARC record sends mail that gets filed as spam or rejected outright. So "it sent" and "it arrived" are two different questions, and a test environment rarely answers the second one honestly.
Why WordPress makes the failure hard to see
wp_mail() returns a boolean, but a true only means the mailer accepted the message for sending. It says nothing about whether the recipient's server ever took delivery.
The failure path is a hook rather than a return value. wp_mail_failed "fires after a PHPMailer exception is caught" and passes a WP_Error carrying the exception message plus the recipient, subject, message, headers and attachments (WordPress developer reference).
Nothing in core surfaces that anywhere you would look. If no plugin is listening on wp_mail_failed, a failed send produces no admin notice, no log entry, and no visible difference from a successful one.
That is the whole reason the quiet failure mode exists. The information is there, and by default nobody is reading it.
Disabling email on a clone is harder than it looks
The obvious answer to the second failure mode is a plugin that turns email off. The popular one is Disable Emails, which stops a site "from sending any emails using the standard wp_mail() function".
Read that sentence carefully, because the qualifier is the problem. Its own page is blunt about the gap: "You probably have a plugin that is sending emails via some other method, like directly using the PHP mail() function, or directly implementing an SMTP client."
wp_mail() is pluggable, which means any plugin can replace it wholesale. A transactional-email plugin that replaces it and posts to a provider's HTTP API never touches PHPMailer at all, so anything that works by swapping out the PHPMailer instance sends nothing and blocks nothing (Gabor Javorszky, 2016).
So the honest version of the advice is two steps, not one. Disable email, and separately deactivate every SMTP or transactional-email plugin on the copy, because those are exactly the ones that bypass the disabling.
A routine for a copied site
Do these in order, before you touch anything that could trigger a send.
- Deactivate SMTP and transactional-email plugins first. Anything named for a provider (Mailgun, SendGrid, Postmark, Resend) or ending in SMTP goes off before the site is used.
- Change the admin email and the store or "from" address to something you own, so anything that escapes lands on you.
- Scrub or shorten the user table if you are testing anything that mails users in bulk. A clone carries every real customer address with it.
- Trigger one known send and confirm where it went before running the real test.
- Only then run the thing you actually came to test.
Step four is the one people skip, and it is the only one that proves the other three worked.
What we do instead, and why
SandyWP sandboxes capture mail rather than blocking it. Every wp_mail() call is recorded with its subject, recipient, headers, attachments and rendered HTML body, and you read the message in the Email Log tab on the site.
Capture is on the moment the sandbox boots, with no configuration. The point is that "did the form fire?" becomes a thing you can look at rather than infer.
Two implementation details are worth stating plainly, because they are what make the guarantee real.
The capture hooks pre_wp_mail at priority 1, so a plugin that short-circuits that filter to send over its own HTTP API still lands in the log. Logging first is deliberate: a plugin that bypasses core is exactly the one you most want a record of.
And sendmail_path in the sandbox PHP image is pinned to /bin/false. Even a plugin that falls back to PHP's mail() cannot put a worker IP in front of a recipient's mail server, which protects both you and everyone else on the platform.
The one case where mail genuinely leaves is when you configure a real relay yourself. If a plugin owns delivery through a configured SMTP host, or has replaced wp_mail() with its own transport, the message goes out under your credentials and your domain, and it is still recorded in the log.
That distinction is the useful one. Blocking a working relay would make the plugin report a successful send of a message that never left, which is a worse lie than either sending or failing.
What a sandbox will not tell you
A capture log answers whether the message was generated and what it contained. It does not answer whether it would have arrived.
Deliverability is a property of the sending domain, not of the code. Your SPF, DKIM and DMARC records, your domain's reputation, and the receiving provider's filters are all attached to production and none of them are exercised by a copy.
Nor will a sandbox reproduce rate limits at your provider, a bounce-handling webhook that only points at the live site, or a template that renders differently in a specific desktop client. For those, send to a seed inbox from the real domain or use a dedicated deliverability tool.
The split is clean enough to state as a rule. Use a sandbox to confirm the message fires and reads correctly, and use production or a staging site on the real domain to confirm it lands.
Where this fits
If you are copying a production site to test an update, the email question is one item on a longer checklist. We wrote that routine up separately in testing a plugin update without breaking your live site, and the same clone-and-check shape applies to support reproduction work.
If mail is failing and you cannot tell why, the debug tools are the other half of this. WP_DEBUG_LOG plus a log viewer will show you the PHPMailer exception that wp_mail_failed carries, which is usually a one-line answer to a problem that reads like a mystery.
