Disable All Email
Stops every email WordPress would send, so nothing leaves the server's IP and its address cannot end up on a spam blacklist. Meant as a must-use plugin on staging clones, migrations, and any site where outgoing mail has to be impossible rather than merely unconfigured.
by Konstantin Sorokin · github.com/kostyasorokin/wp-disable-all-email · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/kostyasorokin/wp-disable-all-email/archive/refs/heads/main.zipReadme
Disable All Email
Stops every email WordPress would send. One file, no settings screen, no database writes.
Issues and pull requests: https://github.com/kostyasorokin/wp-disable-all-email/issues
Why
A WordPress site that cannot deliver mail is not merely quiet — it is a liability. Contact forms and registration forms get filled by bots, the site politely emails a confirmation to whatever address the bot supplied, and those addresses belong to real people who press "spam". Bounces pile up behind them. The result is a server IP on a blocklist and a message from the host asking you to fix it.
The reliable fix is to make sending impossible rather than merely unconfigured.
Turning off the MTA daemon does not do that: on a typical host /usr/sbin/sendmail
is a symlink to the mail binary, so PHP's mail() still hands messages over and
still opens outbound connections. This file removes the WordPress half of that
problem.
What it does
Two layers, and only one of them ever runs:
wp_mail()is redefined. As a must-use plugin this file loads beforewp-includes/pluggable.php, so core never declares its ownwp_mail(). There is no filter left to remove and no plugin that can outrank it.pre_wp_mailis filtered atPHP_INT_MAX. This only matters when another must-use plugin sorted earlier and already definedwp_mail(), in which case layer 1 was skipped and core's sender needs stopping instead.
The load order both layers depend on is in wp-settings.php: must-use plugins are
included first, then regular plugins, and pluggable.php last. That is also why the
file works unchanged as an ordinary plugin — regular plugins still load before
pluggable.php.
It also switches off Post SMTP through
that plugin's own two filters. Post SMTP replaces PHPMailer::send() on the global
$phpmailer, which is a send path that never passes through wp_mail() at all. The
filters do nothing when Post SMTP is not installed.
What it cannot do
No plugin can intercept these, this one included:
- a plugin that calls PHP's
mail()itself, - a plugin that opens its own SMTP socket,
- a plugin that posts to a mail API with raw
curl_exec().
Those are closed at the server: disable_functions=mail in php.ini, an empty
sendmail_path, or blocking outbound 25/465/587. If the goal is the reputation of a
specific IP address, do that as well — this file is the WordPress half, not the
whole.
Installation
Drop disable-all-email.php into wp-content/mu-plugins/. There is nothing to
activate.
mu-plugins does not scan subdirectories — wp_get_mu_plugins() reads one level and
takes only files ending in .php — so if you keep the repository as a folder there,
add a loader beside it:
<?php
/**
* Plugin Name: Disable All Email (must-use loader)
*/
require_once WPMU_PLUGIN_DIR . '/wp-disable-all-email/disable-all-email.php';
It also works as an ordinary plugin in wp-content/plugins/, where it can be
switched off from the admin — which is either the point or the problem, depending on
why you installed it.
Configuration
All three constants go in wp-config.php. None of them is stored in the database.
| Constant | Default | Effect |
|---|---|---|
DISABLE_ALL_EMAIL_RESULT |
true |
What a caller is told after its message is dropped. |
DISABLE_ALL_EMAIL_LOG |
false |
Log recipient and subject of each dropped message to the PHP error log. |
DISABLE_ALL_EMAIL_BLOCK_API |
false |
Also block outgoing mail sent through a provider's HTTP API. |
DISABLE_ALL_EMAIL_RESULT
true tells the caller the message was sent. Contact Form 7 reports success to the
visitor, WooCommerce finishes the order, nothing looks broken.
false tells the caller it failed. That is the honest answer when there is no other
delivery channel, but Contact Form 7 then shows every visitor an error, and core
re-queues the same auto-update email on every cron tick because it only records the
"already reported" flag on a successful send.
On a WooCommerce store true has a cost of its own: _new_order_email_sent is
stamped on every order placed while mail is off, so those orders never produce their
notification even after mail is restored.
DISABLE_ALL_EMAIL_BLOCK_API
Off by default and deliberately. Mail sent through SendGrid, Mailgun, Brevo and the rest leaves the provider's IP address, not this server's, so it is no threat to this machine's reputation and blocking it is usually the wrong trade. Turn it on when the requirement is that no mail leaves at all.
It matches the request URL against a list of provider endpoints, filterable through
disable_all_email_api_needles, and refuses those requests with a WP_Error.
Requests to anything else are untouched.
Hooks
// Fires in place of sending. The one place to see what was dropped,
// and the hook to use when a message body is needed.
add_action( 'disable_all_email_suppressed', function ( array $mail ): void {
// $mail: to, subject, message, headers, attachments, embeds
} );
// Fires with a usable recovery-mode URL. Route it somewhere you can read.
add_action( 'disable_all_email_recovery_url', function ( string $url ): void {
// e.g. telegram_integration_send_message( null, $url );
} );
// Adjust which URLs count as a mail API.
add_filter( 'disable_all_email_api_needles', function ( array $needles ): array {
$needles[] = 'api.example-esp.com';
return $needles;
} );
Recovery mode
WordPress emails you a recovery link when a plugin or theme fatals. It stores only a hash of the key, so the usable link exists nowhere but in that email — dropping it silently would leave a broken site fixable by SSH alone.
This file hooks generate_recovery_mode_key, rebuilds the link and writes it to the
PHP error log, as well as passing it to disable_all_email_recovery_url. The link
grants entry to recovery mode; it is in the log because the alternative is that it
exists nowhere at all. If that trade is wrong for your host, use the action to send
it somewhere else and stop logging it.
Known consequences
These are the plugin working as intended, not faults:
- The admin email cannot be changed from Settings → General. Core stores the new
address in the
adminhashoption and only applies it when the confirmation link is opened — and that link was in the dropped mail. The screen shows a pending-change notice indefinitely. Usewp option update admin_email ..., or open/wp-admin/options.php?adminhash=<hash from the adminhash option>. - Password resets never arrive. With the default
truethe login screen says to check your email and nothing comes. Reset withwp user update. - Post SMTP reports a bind error and stays permanently unbound. It cannot send while this file is present, so on such a site it is better deactivated.
- Mail-logging plugins record nothing. Suppression happens before the point where
wp_mail_succeededandwp_mail_failedfire, so nothing downstream observes it. Usedisable_all_email_suppressedinstead.
Site Health
The plugin registers one Site Health test, because a must-use plugin is easy to forget and "email stopped working" gets investigated from Tools → Site Health rather than from the Must-Use tab. It reports which layer is doing the work, what callers are being told, and how to switch it off.
Requirements
- PHP 8.0 or newer.
- WordPress 5.7 or newer for the
pre_wp_mailfallback layer. The primary layer works on any version.
License
GPL-3.0-or-later.