Commit 2cfebf73b69 for woocommerce
commit 2cfebf73b69190a768741326debd4f3a4935d174
Author: Anuj Singh <80690679+Anuj-Rathore24@users.noreply.github.com>
Date: Thu Jul 2 17:56:31 2026 +0530
Fix uncaught TypeError in WC_Email::send_notification() when mail callback returns non-bool (#66186)
* Fix: TypeError in WC_Email::send() when mail callback returns non-bool
* feat: Add tests for send() bool coercion
- test_send_coerces_non_bool_mail_callback_return_to_bool: verifies send() returns a bool even when the filtered mail callback returns null.
- test_send_notification_does_not_fatal_when_mail_callback_returns_non_bool: verifies send_notification() does not throw when the underlying callback returns a non-bool.
* Fix: Add missing class-wc-email.php require in EmailLoggerTest setUp()
Prevents `MethodCannotBeConfiguredException` when running this file standalone. Matches the pattern in other email test files.
* feat: Add changelog
* feat: Cast mail callback return to bool before firing `woocommerce_email_sent`
* feat: Adds a test asserting a `woocommerce_email_sent` listener
receives a real bool when the mail callback returns null.
diff --git a/plugins/woocommerce/changelog/fix-email-send-notification-bool-typeerror b/plugins/woocommerce/changelog/fix-email-send-notification-bool-typeerror
new file mode 100644
index 00000000000..2275250704f
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-email-send-notification-bool-typeerror
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix a fatal error in WC_Email::send_notification() when a filtered mail callback returns a non-bool value.
diff --git a/plugins/woocommerce/includes/emails/class-wc-email.php b/plugins/woocommerce/includes/emails/class-wc-email.php
index 004980c8b47..fa15ee82766 100644
--- a/plugins/woocommerce/includes/emails/class-wc-email.php
+++ b/plugins/woocommerce/includes/emails/class-wc-email.php
@@ -1233,7 +1233,7 @@ 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 = $mail_callback( ...$mail_callback_params );
+ $return = (bool) $mail_callback( ...$mail_callback_params );
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 ab6ec225229..5a7e746a376 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Email/EmailLoggerTest.php
@@ -29,6 +29,8 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
*/
public function setUp(): void {
parent::setUp();
+ $bootstrap = \WC_Unit_Tests_Bootstrap::instance();
+ require_once $bootstrap->plugin_dir . '/includes/emails/class-wc-email.php';
$this->sut = new EmailLogger();
}
@@ -39,6 +41,7 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
remove_all_filters( 'woocommerce_email_log_enabled' );
remove_all_filters( 'woocommerce_email_log_context' );
remove_all_filters( 'woocommerce_email_log_add_order_note' );
+ remove_all_filters( 'woocommerce_mail_callback' );
remove_all_actions( 'woocommerce_email_disabled' );
remove_all_actions( 'woocommerce_email_skipped' );
remove_action( 'woocommerce_email_sent', array( $this->sut, 'handle_woocommerce_email_sent' ) );
@@ -769,15 +772,16 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
*
* Exposes both protected helpers as public `run_*` wrappers and records whether send() was called.
*
- * @param string $email_id Email type ID.
- * @param string $recipient Recipient email address (empty string = no recipient).
- * @param bool $is_enabled Return value for is_enabled().
- * @param bool $send_return Return value for the stubbed send().
+ * @param string $email_id Email type ID.
+ * @param string $recipient Recipient email address (empty string = no recipient).
+ * @param bool $is_enabled Return value for is_enabled().
+ * @param bool $send_return Return value for the stubbed send() (ignored if $use_real_send is true).
+ * @param bool $use_real_send If true, keep the real WC_Email::send() instead of stubbing it.
* @return object Anonymous class instance with `run_send_notification()`, `run_send_if_recipient()`,
* `send_called`, and `send_args` properties.
*/
- private function create_testable_email( string $email_id, string $recipient, bool $is_enabled, bool $send_return = false ): object {
- return new class( $email_id, $recipient, $is_enabled, $send_return ) extends \WC_Email {
+ private function create_testable_email( string $email_id, string $recipient, bool $is_enabled, bool $send_return = false, bool $use_real_send = false ): object {
+ return new class( $email_id, $recipient, $is_enabled, $send_return, $use_real_send ) extends \WC_Email {
/** @var bool Whether send() has been invoked. */
public bool $send_called = false;
/** @var array Arguments captured from the most recent send() call. */
@@ -789,6 +793,8 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
private bool $test_is_enabled;
/** @var bool Value returned by send(). */
private bool $test_send_return;
+ /** @var bool Whether to delegate to the real WC_Email::send() instead of returning $test_send_return. */
+ private bool $use_real_send;
/**
* Construct the test double.
@@ -797,13 +803,15 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
* @param string $recipient Recipient string for get_recipient().
* @param bool $is_enabled Value to return from is_enabled().
* @param bool $send_return Value to return from send().
+ * @param bool $use_real_send Whether to delegate to the real send() instead of the stub.
*/
- public function __construct( string $email_id, string $recipient, bool $is_enabled, bool $send_return ) {
+ public function __construct( string $email_id, string $recipient, bool $is_enabled, bool $send_return, bool $use_real_send ) {
// Deliberately skip parent::__construct() to avoid side-effects in tests.
$this->id = $email_id;
$this->test_recipient = $recipient;
$this->test_is_enabled = $is_enabled;
$this->test_send_return = $send_return;
+ $this->use_real_send = $use_real_send;
}
/**
@@ -861,6 +869,11 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
public function send( $to, $subject, $message, $headers, $attachments ): bool {
$this->send_called = true;
$this->send_args = array( $to, $subject, $message, $headers, $attachments );
+
+ if ( $this->use_real_send ) {
+ return parent::send( $to, $subject, $message, $headers, $attachments );
+ }
+
return $this->test_send_return;
}
@@ -895,4 +908,77 @@ class EmailLoggerTest extends WC_Unit_Test_Case {
return $email;
}
+
+ /**
+ * @testdox send() coerces a non-bool return from the mail callback into a bool.
+ */
+ public function test_send_coerces_non_bool_mail_callback_return_to_bool(): void {
+ add_filter(
+ 'woocommerce_mail_callback',
+ function () {
+ return static function () {
+ return null;
+ };
+ }
+ );
+
+ $email = new \WC_Email();
+ $result = $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+
+ $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' );
+ }
+
+ /**
+ * @testdox send_notification() does not throw a TypeError when the mail callback returns a non-bool.
+ */
+ public function test_send_notification_does_not_fatal_when_mail_callback_returns_non_bool(): void {
+ add_filter(
+ 'woocommerce_mail_callback',
+ function () {
+ return static function () {
+ return null;
+ };
+ }
+ );
+
+ $email = $this->create_testable_email( 'my_email', 'admin@example.com', true, false, true );
+
+ $result = $email->run_send_notification();
+
+ $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' );
+ }
+
+ /**
+ * @testdox send() casts the mail callback return to bool before firing woocommerce_email_sent.
+ */
+ 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;
+ };
+ }
+ );
+
+ $received_return = null;
+ add_action(
+ 'woocommerce_email_sent',
+ function ( $result ) use ( &$received_return ) {
+ $received_return = $result;
+ },
+ 10,
+ 1
+ );
+
+ $email = new \WC_Email();
+ $email->send( 'test@example.com', 'Subject', 'Message', '', array() );
+
+ $this->assertIsBool( $received_return, 'woocommerce_email_sent should receive a bool even when the mail callback returns null' );
+ $this->assertFalse( $received_return );
+
+ remove_all_actions( 'woocommerce_email_sent' );
+ }
}