WP Manifestindependent plugin directory
manifest / email / wp-disable-all-email

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

0stars
0forks

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.zip

Readme

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:

  1. wp_mail() is redefined. As a must-use plugin this file loads before wp-includes/pluggable.php, so core never declares its own wp_mail(). There is no filter left to remove and no plugin that can outrank it.
  2. pre_wp_mail is filtered at PHP_INT_MAX. This only matters when another must-use plugin sorted earlier and already defined wp_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 adminhash option 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. Use wp option update admin_email ..., or open /wp-admin/options.php?adminhash=<hash from the adminhash option>.
  • Password resets never arrive. With the default true the login screen says to check your email and nothing comes. Reset with wp 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_succeeded and wp_mail_failed fire, so nothing downstream observes it. Use disable_all_email_suppressed instead.

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_mail fallback layer. The primary layer works on any version.

License

GPL-3.0-or-later.

Read the full README on GitHub →