WordPress security releases now get attacked within hours. After the recent core security release, attackers were probing sites the same day the patch shipped. If you look after client sites, or if you’ve hidden update notices from editors, you can easily miss a pending update for days. WordPress only emails you after it runs a background auto-update. It doesn’t email you when an update is waiting for you to install it.
This snippet emails the site admin when a core, plugin or theme update is pending. It lists each item with its installed and new versions and links to Dashboard → Updates. It sends one email per new set of updates, not one every day, so people keep reading it.
The snippet
Save this as wp-content/mu-plugins/dpc-update-alerts.php. Using an mu-plugin means nobody can deactivate it from the Plugins screen. It also works in a normal plugin or your theme’s functions.php.
<?php
/**
* Plugin Name: DPC Update Alerts
* Description: Emails the site admin once when core, plugin or theme updates are pending.
*/
defined( 'ABSPATH' ) || exit;
add_action( 'init', 'dpc_update_alert_schedule' );
function dpc_update_alert_schedule(): void {
if ( ! wp_next_scheduled( 'dpc_update_alert_check' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'twicedaily', 'dpc_update_alert_check' );
}
}
add_action( 'dpc_update_alert_check', 'dpc_update_alert_run' );
/**
* Collect pending updates from the transients WordPress already maintains.
*
* @return string[] One human-readable line per pending update.
*/
function dpc_update_alert_pending(): array {
$items = array();
$core = get_site_transient( 'update_core' );
if ( isset( $core->updates ) && is_array( $core->updates ) ) {
foreach ( $core->updates as $offer ) {
if ( isset( $offer->response, $offer->current ) && 'upgrade' === $offer->response ) {
$items[] = sprintf( 'WordPress core: %s -> %s', get_bloginfo( 'version' ), $offer->current );
break;
}
}
}
$plugins = get_site_transient( 'update_plugins' );
if ( ! empty( $plugins->response ) && is_array( $plugins->response ) ) {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$installed = get_plugins();
foreach ( $plugins->response as $file => $data ) {
$data = (object) $data;
$name = $installed[ $file ]['Name'] ?? ( $data->slug ?? $file );
$current = $installed[ $file ]['Version'] ?? '?';
$items[] = sprintf( 'Plugin %s: %s -> %s', $name, $current, $data->new_version ?? '?' );
}
}
$themes = get_site_transient( 'update_themes' );
if ( ! empty( $themes->response ) && is_array( $themes->response ) ) {
foreach ( $themes->response as $stylesheet => $data ) {
$data = (array) $data;
$theme = wp_get_theme( $stylesheet );
$name = $theme->exists() ? $theme->get( 'Name' ) : $stylesheet;
$current = $theme->exists() ? $theme->get( 'Version' ) : '?';
$items[] = sprintf( 'Theme %s: %s -> %s', $name, $current, $data['new_version'] ?? '?' );
}
}
return array_map( 'wp_strip_all_tags', $items );
}
/**
* Email the pending list, but only when it differs from the last email.
*
* @return bool True when an email was sent.
*/
function dpc_update_alert_run(): bool {
$items = dpc_update_alert_pending();
if ( ! $items ) {
delete_option( 'dpc_update_alert_last' );
return false;
}
$fingerprint = md5( implode( '|', $items ) );
if ( get_option( 'dpc_update_alert_last' ) === $fingerprint ) {
return false;
}
$recipients = (array) apply_filters( 'dpc_update_alert_recipients', array( get_option( 'admin_email' ) ) );
$recipients = array_filter( array_map( 'sanitize_email', $recipients ), 'is_email' );
if ( ! $recipients ) {
return false;
}
$site = wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
$subject = sprintf( '[%s] %d update(s) waiting', $site, count( $items ) );
$body = sprintf(
'Pending updates on %1$s:%2$s%2$s- %3$s%2$s%2$sReview and install them: %4$s',
home_url( '/' ),
PHP_EOL,
implode( PHP_EOL . '- ', $items ),
admin_url( 'update-core.php' )
);
$sent = wp_mail( $recipients, $subject, $body );
if ( $sent ) {
update_option( 'dpc_update_alert_last', $fingerprint, false );
}
return $sent;
}
How it works
- WordPress already checks WordPress.org for updates about twice a day. It stores the results in the
update_core,update_pluginsandupdate_themessite transients. The snippet only reads those transients, so it makes no extra HTTP requests. dpc_update_alert_schedule()registers atwicedailyWP-Cron event, the same interval as the core check.dpc_update_alert_pending()turns the transients into one line per item, such asPlugin Contact Form: 5.9.1 -> 5.9.2. It gets names and installed versions fromget_plugins()andwp_get_theme(), and strips tags in case a header contains markup.dpc_update_alert_run()hashes the list and saves the hash in a non-autoloaded option. It sends an email only when the hash changes. When everything is up to date, it deletes the option, so the next update triggers a new email.
Customise it
To send the alert to an agency inbox instead of, or as well as, the admin email, add this filter:
add_filter( 'dpc_update_alert_recipients', function ( array $to ): array {
$to[] = 'alerts@example.com';
return $to;
} );
- For faster alerts, change
twicedailytohourly. The data only changes when WordPress runs its own update check. - To send a daily reminder until updates are installed, remove the fingerprint check.
Gotchas
- WP-Cron only runs when someone visits the site. On low-traffic sites, set
DISABLE_WP_CRONand callwp-cron.phpfrom a real server cron job. - The update data doesn’t say which releases are security fixes. For official announcements, follow the Security category on WordPress.org News, which has its own RSS feed.
- If a vulnerable plugin has no fix yet, deactivating it is usually safer than waiting. This alert tells you the fix has shipped.
- If you used Disable WordPress core, plugins and themes updates to block update checks, the transients stay empty and this snippet never sends anything. If you only hid the admin notices, it still works.
- In a normal plugin, clear the event on deactivation with
wp_clear_scheduled_hook( 'dpc_update_alert_check' ). - For another admin email built the same way, see Notify Admin When New Account Is Created.