Commit 371014d75e8 for woocommerce

commit 371014d75e84fc61e444f8e5c9eece8488836832
Author: Ján Mikláš <neosinner@gmail.com>
Date:   Wed Jul 8 11:04:43 2026 +0200

    Validate mail callback return type (#66346)

    * Validate mail callback return type

    * Coerce scalar mail callback returns to bool instead of forcing false

diff --git a/plugins/woocommerce/includes/emails/class-wc-email.php b/plugins/woocommerce/includes/emails/class-wc-email.php
index fa15ee82766..475b4f450e7 100644
--- a/plugins/woocommerce/includes/emails/class-wc-email.php
+++ b/plugins/woocommerce/includes/emails/class-wc-email.php
@@ -1233,7 +1233,21 @@ class WC_Email extends WC_Settings_API {
 		$message              = apply_filters( 'woocommerce_mail_content', $this->style_inline( $message ) );
 		$mail_callback        = apply_filters( 'woocommerce_mail_callback', 'wp_mail', $this );
 		$mail_callback_params = apply_filters( 'woocommerce_mail_callback_params', array( $to, wp_specialchars_decode( $subject ), $message, $headers, $attachments ), $this );
-		$return               = (bool) $mail_callback( ...$mail_callback_params );
+		$return               = $mail_callback( ...$mail_callback_params );
+		if ( ! is_bool( $return ) ) {
+			$original_type = gettype( $return );
+
+			$return = is_scalar( $return ) ? wc_string_to_bool( (string) $return ) : false;
+
+			wc_doing_it_wrong(
+				__METHOD__,
+				sprintf(
+					'The callback registered to the woocommerce_mail_callback filter should return a boolean; %s returned.',
+					$original_type
+				),
+				'11.1.0'
+			);
+		}

 		remove_filter( 'wp_mail_from', array( $this, 'get_from_address' ) );
 		remove_filter( 'wp_mail_from_name', array( $this, 'get_from_name' ) );
diff --git a/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php b/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php
index 5a7e746a376..33084fa7503 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php
@@ -910,23 +910,101 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox send() coerces a non-bool return from the mail callback into a bool.
+	 * @testdox send() returns false and warns when the mail callback returns a non-scalar.
 	 */
-	public function test_send_coerces_non_bool_mail_callback_return_to_bool(): void {
+	public function test_send_returns_false_and_warns_when_mail_callback_returns_non_scalar(): void {
 		add_filter(
 			'woocommerce_mail_callback',
 			function () {
-					return static function () {
-							return null;
-					};
+				return static function () {
+					return new \WP_Error( 'failed', 'Mail failed' );
+				};
 			}
 		);
+		add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		$this->setExpectedIncorrectUsage( 'WC_Email::send' );
+
+		$notices = array();
+		$action  = function ( $function_name, $message, $version ) use ( &$notices ) {
+			$notices[] = array(
+				'function_name' => $function_name,
+				'message'       => $message,
+				'version'       => $version,
+			);
+		};
+		add_action( 'doing_it_wrong_run', $action, 10, 3 );

-		$email  = new \WC_Email();
-		$result = $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+		try {
+			$email  = new \WC_Email();
+			$result = $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+		} finally {
+			remove_action( 'doing_it_wrong_run', $action, 10 );
+			remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		}

-		$this->assertIsBool( $result, 'send() should always return a bool, even if the mail callback does not' );
-		$this->assertFalse( $result, 'A non-bool (null) callback return should coerce to false' );
+		$this->assertFalse( $result, 'A non-scalar callback return should resolve to false' );
+		$this->assertNotEmpty( $notices );
+		$this->assertSame( 'WC_Email::send', $notices[0]['function_name'] );
+		$this->assertStringContainsString( 'woocommerce_mail_callback filter should return a boolean; object returned.', $notices[0]['message'] );
+		$this->assertSame( '11.1.0', $notices[0]['version'] );
+	}
+
+	/**
+	 * @testdox send() coerces a scalar mail callback return to bool and warns.
+	 *
+	 * @dataProvider provider_scalar_callback_returns
+	 *
+	 * @param mixed  $callback_return Value returned by the mail callback.
+	 * @param bool   $expected        Expected coerced send result.
+	 * @param string $expected_type   gettype() of the returned value, as reported in the notice.
+	 */
+	public function test_send_coerces_scalar_callback_return_and_warns( $callback_return, bool $expected, string $expected_type ): void {
+		add_filter(
+			'woocommerce_mail_callback',
+			function () use ( $callback_return ) {
+				return static function () use ( $callback_return ) {
+					return $callback_return;
+				};
+			}
+		);
+		add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		$this->setExpectedIncorrectUsage( 'WC_Email::send' );
+
+		$notices = array();
+		$action  = function ( $function_name, $message, $version ) use ( &$notices ) {
+			$notices[] = array(
+				'function_name' => $function_name,
+				'message'       => $message,
+				'version'       => $version,
+			);
+		};
+		add_action( 'doing_it_wrong_run', $action, 10, 3 );
+
+		try {
+			$email  = new \WC_Email();
+			$result = $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+		} finally {
+			remove_action( 'doing_it_wrong_run', $action, 10 );
+			remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		}
+
+		$this->assertSame( $expected, $result );
+		$this->assertNotEmpty( $notices );
+		$this->assertStringContainsString( "woocommerce_mail_callback filter should return a boolean; {$expected_type} returned.", $notices[0]['message'] );
+	}
+
+	/**
+	 * Scalar mail callback return values and their expected coercion.
+	 *
+	 * @return array<string, array<mixed>>
+	 */
+	public function provider_scalar_callback_returns(): array {
+		return array(
+			'integer 1 is a success'     => array( 1, true, 'integer' ),
+			'string yes is a success'    => array( 'yes', true, 'string' ),
+			'integer 0 is a failure'     => array( 0, false, 'integer' ),
+			'random string is a failure' => array( 'sent', false, 'string' ),
+		);
 	}

 	/**
@@ -936,32 +1014,40 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
 		add_filter(
 			'woocommerce_mail_callback',
 			function () {
-					return static function () {
-							return null;
-					};
+				return static function () {
+					return new \WP_Error( 'failed', 'Mail failed' );
+				};
 			}
 		);
+		add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		$this->setExpectedIncorrectUsage( 'WC_Email::send' );

 		$email = $this->create_testable_email( 'my_email', 'admin@example.com', true, false, true );

-		$result = $email->run_send_notification();
+		try {
+			$result = $email->run_send_notification();
+		} finally {
+			remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		}

 		$this->assertIsBool( $result, 'send_notification() should return a bool instead of fataling' );
-		$this->assertFalse( $result, 'Should resolve to false when the underlying mail callback returns null' );
+		$this->assertFalse( $result, 'Should resolve to false when the underlying mail callback returns a non-bool' );
 	}

 	/**
-	 * @testdox send() casts the mail callback return to bool before firing woocommerce_email_sent.
+	 * @testdox send() passes false to woocommerce_email_sent when the mail callback returns a non-bool.
 	 */
 	public function test_send_fires_email_sent_action_with_bool_when_callback_returns_non_bool(): void {
 		add_filter(
 			'woocommerce_mail_callback',
 			function () {
-					return static function () {
-							return null;
-					};
+				return static function () {
+					return new \WP_Error( 'failed', 'Mail failed' );
+				};
 			}
 		);
+		add_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		$this->setExpectedIncorrectUsage( 'WC_Email::send' );

 		$received_return = null;
 		add_action(
@@ -974,9 +1060,13 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
 		);

 		$email = new \WC_Email();
-		$email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+		try {
+			$email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+		} finally {
+			remove_filter( 'doing_it_wrong_trigger_error', '__return_false' );
+		}

-		$this->assertIsBool( $received_return, 'woocommerce_email_sent should receive a bool even when the mail callback returns null' );
+		$this->assertIsBool( $received_return, 'woocommerce_email_sent should receive a bool even when the mail callback returns a non-bool' );
 		$this->assertFalse( $received_return );

 		remove_all_actions( 'woocommerce_email_sent' );