From e023fa5e7befcc044515497af370f12ead657f2b Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Date: Wed, 8 Apr 2026 22:43:26 +0530 Subject: [PATCH 1/2] Add jetpack_account_protection_send_auth_email filter to allow custom email handling Add a filter in Email_Service::api_send_auth_email() that fires before the WPCOM API call, allowing sites to handle the verification email locally (e.g. via wp_mail()). When the filter returns truthy, the API call is skipped. Default behavior is unchanged. Fixes #47999 --- .../changelog/add-send-auth-email-filter | 4 ++ .../src/class-email-service.php | 19 ++++++++ .../tests/php/Email_Service_Test.php | 45 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 projects/packages/account-protection/changelog/add-send-auth-email-filter diff --git a/projects/packages/account-protection/changelog/add-send-auth-email-filter b/projects/packages/account-protection/changelog/add-send-auth-email-filter new file mode 100644 index 000000000000..09cede443b70 --- /dev/null +++ b/projects/packages/account-protection/changelog/add-send-auth-email-filter @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add `jetpack_account_protection_send_auth_email` filter to allow custom handling of the verification email. diff --git a/projects/packages/account-protection/src/class-email-service.php b/projects/packages/account-protection/src/class-email-service.php index ddd447d0d7eb..a54fa2de9889 100644 --- a/projects/packages/account-protection/src/class-email-service.php +++ b/projects/packages/account-protection/src/class-email-service.php @@ -44,6 +44,25 @@ public function __construct( public function api_send_auth_email( int $user_id, string $auth_code ) { $blog_id = Jetpack_Options::get_option( 'id' ); + /** + * Filters whether the Account Protection verification email should be handled externally. + * + * When the filter returns a truthy value, the default WPCOM API email send is skipped, + * allowing sites to deliver the email locally (e.g. via `wp_mail()`). + * + * @since $$next-version$$ + * + * @param bool $handled Whether the email has been handled. Default false. + * @param int $user_id The user ID. + * @param string $auth_code The authentication code. + * @param int $blog_id The blog ID, or false if not available. + */ + $handled = apply_filters( 'jetpack_account_protection_send_auth_email', false, $user_id, $auth_code, $blog_id ); + + if ( $handled ) { + return true; + } + if ( ! $blog_id || ! $this->connection_manager->is_connected() ) { return new \WP_Error( 'jetpack_connection_error', __( 'Jetpack is not connected. Please connect and try again.', 'jetpack-account-protection' ) ); } diff --git a/projects/packages/account-protection/tests/php/Email_Service_Test.php b/projects/packages/account-protection/tests/php/Email_Service_Test.php index 9a667454355f..309f16a0b073 100644 --- a/projects/packages/account-protection/tests/php/Email_Service_Test.php +++ b/projects/packages/account-protection/tests/php/Email_Service_Test.php @@ -79,6 +79,51 @@ public function test_resend_auth_mail_sends_mail_and_remembers_2fa_token_success $this->assertMatchesRegularExpression( '/^[0-9]{6}$/', $new_transient['auth_code'], 'Auth code should be 6 digits.' ); } + public function test_api_send_auth_email_skips_api_call_when_filter_returns_truthy(): void { + $sut = new Email_Service(); + $user = new \WP_User(); + $user->ID = 1; + + $filter_args = array(); + $callback = function ( $handled, $user_id, $auth_code, $blog_id ) use ( &$filter_args ) { + $filter_args = compact( 'handled', 'user_id', 'auth_code', 'blog_id' ); + return true; + }; + + add_filter( 'jetpack_account_protection_send_auth_email', $callback, 10, 4 ); + + $result = $sut->api_send_auth_email( $user->ID, '123456' ); + + remove_filter( 'jetpack_account_protection_send_auth_email', $callback, 10 ); + + $this->assertTrue( $result, 'api_send_auth_email should return true when the filter short-circuits.' ); + $this->assertNotEmpty( $filter_args, 'Filter callback should have been called.' ); + $this->assertFalse( $filter_args['handled'], 'Default handled value should be false.' ); + $this->assertSame( 1, $filter_args['user_id'], 'Filter should receive the user ID.' ); + $this->assertSame( '123456', $filter_args['auth_code'], 'Filter should receive the auth code.' ); + } + + public function test_api_send_auth_email_proceeds_normally_when_filter_returns_falsy(): void { + Jetpack_Options::delete_option( 'id' ); + $sut = new Email_Service(); + $user = new \WP_User(); + $user->ID = 1; + + $callback = function () { + return false; + }; + + add_filter( 'jetpack_account_protection_send_auth_email', $callback, 10, 4 ); + + $result = $sut->api_send_auth_email( $user->ID, '123456' ); + + remove_filter( 'jetpack_account_protection_send_auth_email', $callback, 10 ); + + // Should continue to the normal flow and fail because blog_id is not set + $this->assertInstanceOf( \WP_Error::class, $result ); + $this->assertEquals( 'jetpack_connection_error', $result->get_error_code() ); + } + public function test_api_send_auth_email_returns_error_if_blog_id_not_available(): void { Jetpack_Options::delete_option( 'id' ); $sut = new Email_Service(); From 6e3ffa6ecfc6c00fc81946a33f606b1fc51b799a Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Date: Fri, 10 Apr 2026 20:54:34 +0530 Subject: [PATCH 2/2] Fix PhanTypeInvalidDimOffset by initializing filter_args with expected keys --- .../account-protection/tests/php/Email_Service_Test.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/projects/packages/account-protection/tests/php/Email_Service_Test.php b/projects/packages/account-protection/tests/php/Email_Service_Test.php index 309f16a0b073..74cf00918b88 100644 --- a/projects/packages/account-protection/tests/php/Email_Service_Test.php +++ b/projects/packages/account-protection/tests/php/Email_Service_Test.php @@ -84,7 +84,12 @@ public function test_api_send_auth_email_skips_api_call_when_filter_returns_trut $user = new \WP_User(); $user->ID = 1; - $filter_args = array(); + $filter_args = array( + 'handled' => null, + 'user_id' => null, + 'auth_code' => null, + 'blog_id' => null, + ); $callback = function ( $handled, $user_id, $auth_code, $blog_id ) use ( &$filter_args ) { $filter_args = compact( 'handled', 'user_id', 'auth_code', 'blog_id' ); return true;