From ae3ce13d2c77a91ac2a9e5d0c7d30afa508a2b1b Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Mon, 24 Nov 2025 13:52:36 -0600 Subject: [PATCH 1/4] Fix false mention notifications for users in CC without mention tags Users were incorrectly receiving mention email notifications when they appeared in the CC field of an activity but were not actually mentioned in the content. This commonly occurred when users followed an actor and were added to CC for federation purposes. The fix verifies that a user's actor ID appears in the activity's tag array with type "Mention" before sending a mention notification, rather than just checking if they're in the CC field. Changes: - Add validation in Mailer::mention() to check for actual mention tags - Add object_to_uri() support for Mention type objects - Add test case for users in CC without mention tags (should not notify) - Add test case for users properly mentioned (should notify) --- includes/class-mailer.php | 9 +- includes/functions.php | 1 + .../tests/includes/class-test-mailer.php | 132 ++++++++++++++++++ 3 files changed, 140 insertions(+), 2 deletions(-) diff --git a/includes/class-mailer.php b/includes/class-mailer.php index 1acd0e229..c27e66830 100644 --- a/includes/class-mailer.php +++ b/includes/class-mailer.php @@ -349,8 +349,13 @@ public static function mention( $activity, $user_ids ) { } $actor_id = $actor->get_id(); - if ( \in_array( $actor_id, (array) $activity['cc'], true ) ) { - $recipients[ $user_id ] = $actor_id; + if ( \in_array( $actor_id, (array) $activity['cc'], true ) && isset( $activity['object']['tag'] ) ) { + foreach ( (array) $activity['object']['tag'] as $tag ) { + if ( object_to_uri( $tag ) === $actor_id ) { + $recipients[ $user_id ] = $actor_id; + break; + } + } } } diff --git a/includes/functions.php b/includes/functions.php index 55eaa8efd..ba61b65c7 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -808,6 +808,7 @@ function object_to_uri( $data ) { $data = object_to_uri( $data['url'] ); break; case 'Link': + case 'Mention': $data = $data['href']; break; default: diff --git a/tests/phpunit/tests/includes/class-test-mailer.php b/tests/phpunit/tests/includes/class-test-mailer.php index a395debbe..1dce8267e 100644 --- a/tests/phpunit/tests/includes/class-test-mailer.php +++ b/tests/phpunit/tests/includes/class-test-mailer.php @@ -832,6 +832,13 @@ public function test_mention_with_array() { 'object' => array( 'id' => 'https://example.com/post/1', 'content' => 'Test mention', + 'tag' => array( + array( + 'type' => 'Mention', + 'href' => Actors::get_by_id( $user_id )->get_id(), + 'name' => '@test', + ), + ), ), 'cc' => array( Actors::get_by_id( $user_id )->get_id() ), ); @@ -954,6 +961,13 @@ public function test_mention_filters_recipients() { 'object' => array( 'id' => 'https://example.com/post/1', 'content' => 'Test mention', + 'tag' => array( + array( + 'type' => 'Mention', + 'href' => Actors::get_by_id( $user_id )->get_id(), + 'name' => '@test', + ), + ), ), // Only user_id is in CC, not other_user_id. 'cc' => array( Actors::get_by_id( $user_id )->get_id() ), @@ -1135,4 +1149,122 @@ public function test_respect_existing_notification_settings() { // Clean up. \delete_option( 'activitypub_create_posts' ); } + + /** + * Test that users in CC without actual mention tags do not receive mention notifications. + * + * This tests the bug fix where users added to CC (e.g., because they follow the actor) + * were incorrectly receiving mention notifications even when not actually mentioned. + * + * @covers ::mention + */ + public function test_mention_requires_tag_not_just_cc() { + $user_id = self::$user_id; + + // Activity with user in CC but NOT mentioned in tags. + $activity = array( + 'actor' => 'https://example.com/sports-account', + 'object' => array( + 'id' => 'https://example.com/sports-account/posts/123', + 'type' => 'Note', + 'content' => '

Join @user1 and @user2 on our stream...

', + 'tag' => array( + // Other users mentioned, but NOT the local user. + array( + 'type' => 'Mention', + 'href' => 'https://example.com/user1', + 'name' => 'user1@example.com', + ), + array( + 'type' => 'Mention', + 'href' => 'https://example.com/user2', + 'name' => 'user2@example.com', + ), + ), + ), + // User is in CC (e.g., because they follow the actor). + 'cc' => array( Actors::get_by_id( $user_id )->get_id() ), + ); + + // Mock remote metadata. + $metadata_filter = function () { + return array( + 'name' => 'Sports Account', + 'url' => 'https://example.com/sports-account', + ); + }; + \add_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter ); + + $mock = new \MockAction(); + \add_filter( 'wp_mail', array( $mock, 'filter' ), 1 ); + + // Trigger mention notification. + Mailer::mention( $activity, $user_id ); + + // Should NOT send any email because user is not actually mentioned in tags. + $this->assertEquals( 0, $mock->get_call_count(), 'User in CC without mention tag should not receive notification' ); + + // Clean up. + \remove_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter ); + \remove_filter( 'wp_mail', array( $mock, 'filter' ), 1 ); + } + + /** + * Test that users with actual mention tags DO receive mention notifications. + * + * @covers ::mention + */ + public function test_mention_with_tag_sends_notification() { + $user_id = self::$user_id; + + // Activity with user properly mentioned in both CC and tags. + $activity = array( + 'actor' => 'https://example.com/author', + 'object' => array( + 'id' => 'https://example.com/post/1', + 'type' => 'Note', + 'content' => '

Hello @testuser, how are you?

', + 'tag' => array( + array( + 'type' => 'Mention', + 'href' => Actors::get_by_id( $user_id )->get_id(), + 'name' => '@testuser', + ), + ), + ), + 'cc' => array( Actors::get_by_id( $user_id )->get_id() ), + ); + + // Mock remote metadata. + $metadata_filter = function () { + return array( + 'name' => 'Test Author', + 'url' => 'https://example.com/author', + ); + }; + \add_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter ); + + $mock = new \MockAction(); + \add_filter( 'wp_mail', array( $mock, 'filter' ), 1 ); + + // Capture email. + $mail_filter = function ( $args ) use ( $user_id ) { + $this->assertStringContainsString( 'Mention', $args['subject'] ); + $this->assertStringContainsString( 'Test Author', $args['subject'] ); + $this->assertEquals( \get_user_by( 'id', $user_id )->user_email, $args['to'] ); + return $args; + }; + \add_filter( 'wp_mail', $mail_filter ); + + // Trigger mention notification. + Mailer::mention( $activity, $user_id ); + + // Should send 1 email because user is properly mentioned. + $this->assertEquals( 1, $mock->get_call_count(), 'User properly mentioned in tags should receive notification' ); + + // Clean up. + \remove_filter( 'pre_get_remote_metadata_by_actor', $metadata_filter ); + \remove_filter( 'wp_mail', array( $mock, 'filter' ), 1 ); + \remove_filter( 'wp_mail', $mail_filter ); + } } From 186d72ed91d5f36dbf1b62f32496e0e2e5353993 Mon Sep 17 00:00:00 2001 From: Automattic Bot Date: Mon, 24 Nov 2025 21:04:41 +0100 Subject: [PATCH 2/4] Add changelog --- .github/changelog/2532-from-description | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/changelog/2532-from-description diff --git a/.github/changelog/2532-from-description b/.github/changelog/2532-from-description new file mode 100644 index 000000000..15fa5fd9d --- /dev/null +++ b/.github/changelog/2532-from-description @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +False mention email notifications for users in CC field without actual mention tags. From f2f08b2e115e86eed1c23206a9814dd42397b3b6 Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 26 Nov 2025 10:38:10 -0600 Subject: [PATCH 3/4] Refactor mention handling in Mailer class Update mention logic to use 'object.tag' for mentions instead of 'cc' recipients. Simplifies recipient mapping by filtering and mapping mention tags. --- includes/class-mailer.php | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/includes/class-mailer.php b/includes/class-mailer.php index c27e66830..3c359ac9b 100644 --- a/includes/class-mailer.php +++ b/includes/class-mailer.php @@ -327,8 +327,8 @@ public static function direct_message( $activity, $user_ids ) { * @param int|int[] $user_ids The id(s) of the local blog-user(s). */ public static function mention( $activity, $user_ids ) { - // Early return if activity has no cc recipients. - if ( empty( $activity['cc'] ) ) { + // Early return if activity has no mentions. + if ( empty( $activity['object']['tag'] ) ) { return; } @@ -337,25 +337,18 @@ public static function mention( $activity, $user_ids ) { return; } - // Normalize to array. - $user_ids = (array) $user_ids; - - // Build a map of user_id => actor_id and filter to only users in the "cc" field. $recipients = array(); - foreach ( $user_ids as $user_id ) { + $mentions = wp_list_filter( (array) $activity['object']['tag'], array( 'type' => 'Mention' ) ); + $mentions = array_map( __NAMESPACE__ . '\object_to_uri', $mentions ); + foreach ( (array) $user_ids as $user_id ) { $actor = Actors::get_by_id( $user_id ); if ( \is_wp_error( $actor ) ) { continue; } $actor_id = $actor->get_id(); - if ( \in_array( $actor_id, (array) $activity['cc'], true ) && isset( $activity['object']['tag'] ) ) { - foreach ( (array) $activity['object']['tag'] as $tag ) { - if ( object_to_uri( $tag ) === $actor_id ) { - $recipients[ $user_id ] = $actor_id; - break; - } - } + if ( \in_array( $actor_id, $mentions, true ) ) { + $recipients[ $user_id ] = $actor_id; } } From 5adf897c8db9b6b5ab8f33880f397c2f483572de Mon Sep 17 00:00:00 2001 From: Konstantin Obenland Date: Wed, 26 Nov 2025 10:40:10 -0600 Subject: [PATCH 4/4] Improve readability --- includes/class-mailer.php | 2 +- includes/functions.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/class-mailer.php b/includes/class-mailer.php index 3c359ac9b..052cb6b5a 100644 --- a/includes/class-mailer.php +++ b/includes/class-mailer.php @@ -339,7 +339,7 @@ public static function mention( $activity, $user_ids ) { $recipients = array(); $mentions = wp_list_filter( (array) $activity['object']['tag'], array( 'type' => 'Mention' ) ); - $mentions = array_map( __NAMESPACE__ . '\object_to_uri', $mentions ); + $mentions = array_map( '\Activitypub\object_to_uri', $mentions ); foreach ( (array) $user_ids as $user_id ) { $actor = Actors::get_by_id( $user_id ); if ( \is_wp_error( $actor ) ) { diff --git a/includes/functions.php b/includes/functions.php index ba61b65c7..3bc83532e 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -807,10 +807,12 @@ function object_to_uri( $data ) { // See https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image. $data = object_to_uri( $data['url'] ); break; + case 'Link': case 'Mention': $data = $data['href']; break; + default: $data = $data['id']; break;