diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php
index 2ddfaadc4b39d..5e9d9c5a73e69 100644
--- a/src/wp-admin/includes/class-wp-site-health.php
+++ b/src/wp-admin/includes/class-wp-site-health.php
@@ -84,6 +84,98 @@ public static function get_instance() {
return self::$instance;
}
+ /**
+ * Sends a test email to the current user's email address.
+ *
+ * A successful result means that WordPress handed the message to the mail
+ * system without an immediate error. It does not confirm delivery.
+ *
+ * @since 7.2.0
+ *
+ * @return true|WP_Error True on success, or a WP_Error on failure.
+ */
+ public function send_email_delivery_test() {
+ $user = wp_get_current_user();
+ $email = $user->user_email;
+
+ if ( ! $user->exists() || ! is_email( $email ) ) {
+ return new WP_Error( 'invalid-address' );
+ }
+
+ $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
+ $subject = sprintf(
+ /* translators: %s: Site title. */
+ __( '[%s] Email delivery test' ),
+ $site_name
+ );
+ $message = sprintf(
+ /* translators: 1: Site title. 2: Site URL. */
+ __( "This is a test email sent from your WordPress site, %1\$s.\n\nReceiving this message confirms that it reached this inbox. Review the message headers to confirm that the sender details are correct.\n\nSite URL: %2\$s" ),
+ $site_name,
+ home_url( '/' )
+ );
+
+ if ( ! wp_mail( $email, $subject, $message ) ) {
+ return new WP_Error( 'send-failed' );
+ }
+
+ update_option(
+ 'email_delivery_last_tested',
+ array(
+ 'timestamp' => time(),
+ 'user_id' => $user->ID,
+ 'email' => $email,
+ ),
+ false
+ );
+
+ return true;
+ }
+
+ /**
+ * Gets the timestamp of the most recent valid email delivery test.
+ *
+ * @since 7.2.0
+ *
+ * @return int The test timestamp, or 0 if no valid timestamp exists.
+ */
+ private function get_email_delivery_last_tested() {
+ $last_tested = get_option( 'email_delivery_last_tested' );
+
+ if (
+ ! is_array( $last_tested ) ||
+ ! isset( $last_tested['timestamp'], $last_tested['user_id'], $last_tested['email'] ) ||
+ ! is_numeric( $last_tested['timestamp'] ) ||
+ 0 >= (int) $last_tested['timestamp'] ||
+ (int) $last_tested['timestamp'] > time() ||
+ ! is_numeric( $last_tested['user_id'] ) ||
+ 0 >= (int) $last_tested['user_id'] ||
+ ! is_string( $last_tested['email'] ) ||
+ ! is_email( $last_tested['email'] )
+ ) {
+ return 0;
+ }
+
+ $user = get_userdata( (int) $last_tested['user_id'] );
+
+ if ( ! $user || 0 !== strcasecmp( $user->user_email, $last_tested['email'] ) ) {
+ return 0;
+ }
+
+ return (int) $last_tested['timestamp'];
+ }
+
+ /**
+ * Determines whether the most recent email delivery test is current.
+ *
+ * @since 7.2.0
+ *
+ * @return bool Whether email delivery was tested within the required period.
+ */
+ private function is_email_delivery_test_current() {
+ return $this->get_email_delivery_last_tested() > time() - ( 3 * MONTH_IN_SECONDS );
+ }
+
/**
* Enqueues the site health scripts.
*
@@ -2791,6 +2883,49 @@ public function get_test_search_engine_visibility() {
return $result;
}
+ /**
+ * Tests whether email delivery has been tested recently.
+ *
+ * @since 7.2.0
+ *
+ * @return array The test results.
+ */
+ public function get_test_email_delivery() {
+ $was_tested_recently = $this->is_email_delivery_test_current();
+ $action = sprintf(
+ '
',
+ esc_url( admin_url( 'site-health.php' ) ),
+ wp_nonce_field( 'verify-email-delivery', '_wpnonce', true, false ),
+ esc_html__( 'Verify email delivery' )
+ );
+
+ $result = array(
+ 'label' => __( 'Email delivery was recently tested' ),
+ 'status' => 'good',
+ 'badge' => array(
+ 'label' => __( 'Performance' ),
+ 'color' => 'blue',
+ ),
+ 'description' => sprintf(
+ '%s
',
+ __( 'WordPress recently handed a test email to the mail system without an immediate error. This does not guarantee that the message reached the inbox.' )
+ ),
+ 'actions' => $action,
+ 'test' => 'email_delivery',
+ );
+
+ if ( ! $was_tested_recently ) {
+ $result['label'] = __( 'Email delivery should be tested' );
+ $result['status'] = 'recommended';
+ $result['description'] = sprintf(
+ '%s
',
+ __( 'WordPress relies on email for important site administration messages. Send a test email to your user account, then confirm that it reaches the inbox and that its sender headers are correct.' )
+ );
+ }
+
+ return $result;
+ }
+
/**
* Tests if opcode cache is enabled and available.
*
@@ -2896,6 +3031,10 @@ public static function get_tests() {
'label' => __( 'HTTP Requests' ),
'test' => 'http_requests',
),
+ 'email_delivery' => array(
+ 'label' => __( 'Email delivery' ),
+ 'test' => 'email_delivery',
+ ),
'rest_availability' => array(
'label' => __( 'REST API availability' ),
'test' => 'rest_availability',
diff --git a/src/wp-admin/site-health.php b/src/wp-admin/site-health.php
index 1a70e2e1dd8f1..da8d155377d0a 100644
--- a/src/wp-admin/site-health.php
+++ b/src/wp-admin/site-health.php
@@ -74,6 +74,26 @@
$health_check_site_status = WP_Site_Health::get_instance();
+if ( 'verify_email_delivery' === $action ) {
+ check_admin_referer( 'verify-email-delivery' );
+
+ $result = $health_check_site_status->send_email_delivery_test();
+ $status = 'success';
+
+ if ( is_wp_error( $result ) ) {
+ $status = $result->get_error_code();
+ }
+
+ wp_safe_redirect(
+ add_query_arg(
+ 'email-delivery-test',
+ $status,
+ admin_url( 'site-health.php' )
+ )
+ );
+ exit;
+}
+
get_current_screen()->add_help_tab(
array(
'id' => 'overview',
@@ -124,6 +144,43 @@
);
}
}
+
+ $email_delivery_test_status = '';
+ // The query parameter only selects a notice to display.
+ // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ if ( isset( $_GET['email-delivery-test'] ) && is_string( $_GET['email-delivery-test'] ) ) {
+ $email_delivery_test_status = sanitize_key( wp_unslash( $_GET['email-delivery-test'] ) );
+ }
+
+ if ( 'success' === $email_delivery_test_status ) {
+ wp_admin_notice(
+ sprintf(
+ /* translators: %s: Current user's email address. */
+ __( 'WordPress accepted a request to send a test email to %s. Check that it arrived in your inbox and review its headers to confirm that the sender details are correct.' ),
+ '' . esc_html( wp_get_current_user()->user_email ) . ''
+ ),
+ array(
+ 'type' => 'success',
+ 'dismissible' => true,
+ )
+ );
+ } elseif ( 'invalid-address' === $email_delivery_test_status ) {
+ wp_admin_notice(
+ __( 'Your user account does not have a valid email address. Update it before testing email delivery.' ),
+ array(
+ 'type' => 'error',
+ 'dismissible' => true,
+ )
+ );
+ } elseif ( 'send-failed' === $email_delivery_test_status ) {
+ wp_admin_notice(
+ __( 'The test email could not be sent. Your site may not be correctly configured to send emails.' ),
+ array(
+ 'type' => 'error',
+ 'dismissible' => true,
+ )
+ );
+ }
?>
diff --git a/tests/phpunit/tests/admin/wpSiteHealth.php b/tests/phpunit/tests/admin/wpSiteHealth.php
index 6080b477f54c3..89df631418695 100644
--- a/tests/phpunit/tests/admin/wpSiteHealth.php
+++ b/tests/phpunit/tests/admin/wpSiteHealth.php
@@ -31,6 +31,34 @@ public function set_up() {
$this->instance = new WP_Site_Health();
}
+ /**
+ * Performs cleanup tasks after every test.
+ */
+ public function tear_down() {
+ delete_option( 'email_delivery_last_tested' );
+ wp_set_current_user( 0 );
+
+ parent::tear_down();
+ }
+
+ /**
+ * Creates and sets the current user for an email delivery test.
+ *
+ * @param string $email User email address.
+ * @return WP_User The current user.
+ */
+ private function set_email_delivery_test_user( $email = 'site-health@example.org' ) {
+ $user = self::factory()->user->create_and_get(
+ array(
+ 'role' => 'administrator',
+ 'user_email' => $email,
+ )
+ );
+ wp_set_current_user( $user->ID );
+
+ return $user;
+ }
+
/**
* @ticket 55791
* @covers ::__construct()
@@ -649,6 +677,243 @@ public static function set_autoloaded_option( $bytes = 800000 ) {
add_option( 'test_set_autoloaded_option', $heavy_option_string, '', true );
}
+ /**
+ * Tests the email delivery Site Health status.
+ *
+ * @ticket 65891
+ * @dataProvider data_email_delivery_status
+ * @covers ::get_email_delivery_last_tested()
+ * @covers ::get_test_email_delivery()
+ *
+ * @param string $state The stored timestamp state.
+ * @param string $expected_status The expected Site Health status.
+ */
+ public function test_email_delivery_status( $state, $expected_status ) {
+ $user = $this->set_email_delivery_test_user();
+ $test_data = array(
+ 'timestamp' => time() - DAY_IN_SECONDS,
+ 'user_id' => $user->ID,
+ 'email' => $user->user_email,
+ );
+
+ switch ( $state ) {
+ case 'malformed':
+ update_option( 'email_delivery_last_tested', 'not-a-timestamp', false );
+ break;
+ case 'future':
+ $test_data['timestamp'] = time() + HOUR_IN_SECONDS;
+ update_option( 'email_delivery_last_tested', $test_data, false );
+ break;
+ case 'expired':
+ $test_data['timestamp'] = time() - ( 3 * MONTH_IN_SECONDS );
+ update_option( 'email_delivery_last_tested', $test_data, false );
+ break;
+ case 'recent':
+ update_option( 'email_delivery_last_tested', $test_data, false );
+ break;
+ }
+
+ $result = $this->instance->get_test_email_delivery();
+
+ $this->assertSame( $expected_status, $result['status'] );
+ $this->assertSame( 'email_delivery', $result['test'] );
+ $this->assertSame(
+ array(
+ 'label' => __( 'Performance' ),
+ 'color' => 'blue',
+ ),
+ $result['badge']
+ );
+
+ $this->assertStringContainsString( 'site-health.php', $result['actions'] );
+ $this->assertStringNotContainsString( 'options-general.php', $result['actions'] );
+ }
+
+ /**
+ * Data provider for test_email_delivery_status().
+ *
+ * @return array[] Test parameters.
+ */
+ public function data_email_delivery_status() {
+ return array(
+ 'missing timestamp' => array( 'missing', 'recommended' ),
+ 'malformed timestamp' => array( 'malformed', 'recommended' ),
+ 'future timestamp' => array( 'future', 'recommended' ),
+ 'expired timestamp' => array( 'expired', 'recommended' ),
+ 'recent timestamp' => array( 'recent', 'good' ),
+ );
+ }
+
+ /**
+ * Tests a successful email delivery test.
+ *
+ * @ticket 65891
+ * @covers ::send_email_delivery_test()
+ */
+ public function test_send_email_delivery_test_success() {
+ global $wpdb;
+
+ $user = $this->set_email_delivery_test_user();
+ $mail_data = null;
+ $callback = static function ( $short_circuit, $atts ) use ( &$mail_data ) {
+ $mail_data = $atts;
+ return true;
+ };
+ add_filter( 'pre_wp_mail', $callback, 10, 2 );
+
+ $result = $this->instance->send_email_delivery_test();
+
+ remove_filter( 'pre_wp_mail', $callback );
+
+ $this->assertTrue( $result );
+ $this->assertSame( $user->user_email, $mail_data['to'] );
+ $this->assertStringContainsString( get_option( 'blogname' ), $mail_data['subject'] );
+ $this->assertStringContainsString( home_url( '/' ), $mail_data['message'] );
+
+ $test_data = get_option( 'email_delivery_last_tested' );
+ $this->assertGreaterThan( 0, $test_data['timestamp'] );
+ $this->assertSame( $user->ID, $test_data['user_id'] );
+ $this->assertSame( $user->user_email, $test_data['email'] );
+ $this->assertSame( 'good', $this->instance->get_test_email_delivery()['status'] );
+ $this->assertSame(
+ 'off',
+ $wpdb->get_var(
+ $wpdb->prepare(
+ "SELECT autoload FROM $wpdb->options WHERE option_name = %s",
+ 'email_delivery_last_tested'
+ )
+ )
+ );
+ }
+
+ /**
+ * Tests an immediate email sending failure.
+ *
+ * @ticket 65891
+ * @covers ::send_email_delivery_test()
+ */
+ public function test_send_email_delivery_test_failure() {
+ $this->set_email_delivery_test_user();
+ add_filter( 'pre_wp_mail', '__return_false' );
+
+ $result = $this->instance->send_email_delivery_test();
+
+ remove_filter( 'pre_wp_mail', '__return_false' );
+
+ $this->assertWPError( $result );
+ $this->assertSame( 'send-failed', $result->get_error_code() );
+ $this->assertFalse( get_option( 'email_delivery_last_tested', false ) );
+ }
+
+ /**
+ * Tests a current user without a valid email address.
+ *
+ * @ticket 65891
+ * @covers ::send_email_delivery_test()
+ */
+ public function test_send_email_delivery_test_invalid_address() {
+ $result = $this->instance->send_email_delivery_test();
+
+ $this->assertWPError( $result );
+ $this->assertSame( 'invalid-address', $result->get_error_code() );
+ $this->assertFalse( get_option( 'email_delivery_last_tested', false ) );
+ }
+
+ /**
+ * Tests that changing the tested user's email invalidates the result.
+ *
+ * @ticket 65891
+ * @covers ::get_email_delivery_last_tested()
+ * @covers ::get_test_email_delivery()
+ */
+ public function test_email_delivery_status_is_invalid_after_user_email_changes() {
+ $user = $this->set_email_delivery_test_user( 'before-change@example.org' );
+ update_option(
+ 'email_delivery_last_tested',
+ array(
+ 'timestamp' => time() - DAY_IN_SECONDS,
+ 'user_id' => $user->ID,
+ 'email' => $user->user_email,
+ ),
+ false
+ );
+
+ wp_update_user(
+ array(
+ 'ID' => $user->ID,
+ 'user_email' => 'after-change@example.org',
+ )
+ );
+
+ $this->assertSame( 'recommended', $this->instance->get_test_email_delivery()['status'] );
+ }
+
+ /**
+ * Tests that deleting the tested user invalidates the result.
+ *
+ * @ticket 65891
+ * @covers ::get_email_delivery_last_tested()
+ * @covers ::get_test_email_delivery()
+ */
+ public function test_email_delivery_status_is_invalid_after_user_is_deleted() {
+ $user = $this->set_email_delivery_test_user();
+ update_option(
+ 'email_delivery_last_tested',
+ array(
+ 'timestamp' => time() - DAY_IN_SECONDS,
+ 'user_id' => $user->ID,
+ 'email' => $user->user_email,
+ ),
+ false
+ );
+
+ if ( is_multisite() ) {
+ wpmu_delete_user( $user->ID );
+ } else {
+ wp_delete_user( $user->ID );
+ }
+ wp_set_current_user( 0 );
+
+ $this->assertSame( 'recommended', $this->instance->get_test_email_delivery()['status'] );
+ }
+
+ /**
+ * Tests that a recent email delivery result applies site-wide.
+ *
+ * @ticket 65891
+ * @covers ::get_email_delivery_last_tested()
+ * @covers ::get_test_email_delivery()
+ */
+ public function test_email_delivery_status_applies_to_other_administrators() {
+ $user = $this->set_email_delivery_test_user();
+ update_option(
+ 'email_delivery_last_tested',
+ array(
+ 'timestamp' => time() - DAY_IN_SECONDS,
+ 'user_id' => $user->ID,
+ 'email' => $user->user_email,
+ ),
+ false
+ );
+
+ $this->set_email_delivery_test_user( 'another-admin@example.org' );
+
+ $this->assertSame( 'good', $this->instance->get_test_email_delivery()['status'] );
+ }
+
+ /**
+ * Tests that the email delivery check is registered as a direct test.
+ *
+ * @ticket 65891
+ * @covers ::get_tests()
+ */
+ public function test_email_delivery_test_is_registered() {
+ $tests = WP_Site_Health::get_tests();
+
+ $this->assertArrayHasKey( 'email_delivery', $tests['direct'] );
+ $this->assertSame( 'email_delivery', $tests['direct']['email_delivery']['test'] );
+ }
+
/**
* Tests get_test_opcode_cache() return structure.
*