Skip to content

Commit 29053f3

Browse files
pfefferleobenland
andauthored
Init new custom mailer class (#1068)
* Load mailer I forgot to load the mailer And added some line breaks * Bonus: Direct-Messages * added changelog * Update includes/class-mailer.php Co-authored-by: Konstantin Obenland <[email protected]> * Update includes/class-mailer.php Co-authored-by: Konstantin Obenland <[email protected]> * Update includes/class-mailer.php Co-authored-by: Konstantin Obenland <[email protected]> * add escaping * this is not wp_kses * add unit tests * phpcs * Add more escaping and docs * Keep bottom line URLs in separate line * Escape blog names * link appropriate follower page --------- Co-authored-by: Konstantin Obenland <[email protected]>
1 parent 6d5c76f commit 29053f3

File tree

7 files changed

+184
-39
lines changed

7 files changed

+184
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
* `icon` support for `Audio` and `Video` attachments
1313
* Send "new follower" emails
14+
* Send "direct message" emails
1415

1516
### Improved
1617

activitypub.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ function plugin_init() {
7272
\add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) );
7373
\add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) );
7474
\add_action( 'init', array( __NAMESPACE__ . '\Link', 'init' ) );
75+
\add_action( 'init', array( __NAMESPACE__ . '\Mailer', 'init' ) );
7576

7677
if ( site_supports_blocks() ) {
7778
\add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) );

includes/class-mailer.php

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,41 @@
88
namespace Activitypub;
99

1010
use Activitypub\Collection\Actors;
11+
1112
/**
12-
* Mailer Class
13+
* Mailer Class.
1314
*/
1415
class Mailer {
1516
/**
1617
* Initialize the Mailer.
1718
*/
1819
public static function init() {
19-
add_filter( 'comment_notification_subject', array( self::class, 'comment_notification_subject' ), 10, 2 );
20-
add_filter( 'comment_notification_text', array( self::class, 'comment_notification_text' ), 10, 2 );
20+
\add_filter( 'comment_notification_subject', array( self::class, 'comment_notification_subject' ), 10, 2 );
21+
\add_filter( 'comment_notification_text', array( self::class, 'comment_notification_text' ), 10, 2 );
2122

2223
// New follower notification.
23-
add_action( 'activitypub_notification_follow', array( self::class, 'new_follower' ) );
24+
\add_action( 'activitypub_notification_follow', array( self::class, 'new_follower' ) );
25+
26+
// Direct message notification.
27+
\add_action( 'activitypub_inbox_create', array( self::class, 'direct_message' ), 10, 2 );
2428
}
2529

2630
/**
27-
* Filter the mail-subject for Like and Announce notifications.
31+
* Filter the subject line for Like and Announce notifications.
2832
*
29-
* @param string $subject The default mail-subject.
33+
* @param string $subject The default subject line.
3034
* @param int|string $comment_id The comment ID.
3135
*
32-
* @return string The filtered mail-subject
36+
* @return string The filtered subject line.
3337
*/
3438
public static function comment_notification_subject( $subject, $comment_id ) {
35-
$comment = get_comment( $comment_id );
39+
$comment = \get_comment( $comment_id );
3640

3741
if ( ! $comment ) {
3842
return $subject;
3943
}
4044

41-
$type = get_comment_meta( $comment->comment_ID, 'protocol', true );
45+
$type = \get_comment_meta( $comment->comment_ID, 'protocol', true );
4246

4347
if ( 'activitypub' !== $type ) {
4448
return $subject;
@@ -50,28 +54,28 @@ public static function comment_notification_subject( $subject, $comment_id ) {
5054
return $subject;
5155
}
5256

53-
$post = get_post( $comment->comment_post_ID );
57+
$post = \get_post( $comment->comment_post_ID );
5458

55-
/* translators: %1$s: Blog name, %2$s: Post title */
56-
return sprintf( __( '[%1$s] %2$s: %3$s', 'activitypub' ), get_option( 'blogname' ), $singular, $post->post_title );
59+
/* translators: 1: Blog name, 2: Like or Repost, 3: Post title */
60+
return \sprintf( \esc_html__( '[%1$s] %2$s: %3$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $singular ), \esc_html( $post->post_title ) );
5761
}
5862

5963
/**
60-
* Filter the mail-content for Like and Announce notifications.
64+
* Filter the notification text for Like and Announce notifications.
6165
*
62-
* @param string $message The default mail-content.
66+
* @param string $message The default notification text.
6367
* @param int|string $comment_id The comment ID.
6468
*
65-
* @return string The filtered mail-content
69+
* @return string The filtered notification text.
6670
*/
6771
public static function comment_notification_text( $message, $comment_id ) {
68-
$comment = get_comment( $comment_id );
72+
$comment = \get_comment( $comment_id );
6973

7074
if ( ! $comment ) {
7175
return $message;
7276
}
7377

74-
$type = get_comment_meta( $comment->comment_ID, 'protocol', true );
78+
$type = \get_comment_meta( $comment->comment_ID, 'protocol', true );
7579

7680
if ( 'activitypub' !== $type ) {
7781
return $message;
@@ -83,24 +87,24 @@ public static function comment_notification_text( $message, $comment_id ) {
8387
return $message;
8488
}
8589

86-
$post = get_post( $comment->comment_post_ID );
87-
$comment_author_domain = gethostbyaddr( $comment->comment_author_IP );
90+
$post = \get_post( $comment->comment_post_ID );
91+
$comment_author_domain = \gethostbyaddr( $comment->comment_author_IP );
8892

89-
/* translators: %1$s: Comment type, %2$s: Post title */
90-
$notify_message = \sprintf( __( 'New %1$s on your post "%2$s"', 'activitypub' ), $comment_type['singular'], $post->post_title ) . "\r\n";
91-
/* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */
92-
$notify_message .= \sprintf( __( 'Website: %1$s (IP address: %2$s, %3$s)', 'activitypub' ), $comment->comment_author, $comment->comment_author_IP, $comment_author_domain ) . "\r\n";
93-
/* translators: %s: Trackback/pingback/comment author URL. */
94-
$notify_message .= \sprintf( __( 'URL: %s', 'activitypub' ), $comment->comment_author_url ) . "\r\n\r\n";
95-
/* translators: %s: Comment type label */
96-
$notify_message .= \sprintf( __( 'You can see all %s on this post here:', 'activitypub' ), $comment_type['label'] ) . "\r\n";
97-
$notify_message .= \get_permalink( $comment->comment_post_ID ) . '#' . $comment_type['singular'] . "\r\n\r\n";
93+
/* translators: 1: Comment type, 2: Post title */
94+
$notify_message = \sprintf( html_entity_decode( esc_html__( 'New %1$s on your post &#8220;%2$s&#8221;.', 'activitypub' ) ), \esc_html( $comment_type['singular'] ), \esc_html( $post->post_title ) ) . "\r\n\r\n";
95+
/* translators: 1: Website name, 2: Website IP address, 3: Website hostname. */
96+
$notify_message .= \sprintf( \esc_html__( 'From: %1$s (IP address: %2$s, %3$s)', 'activitypub' ), \esc_html( $comment->comment_author ), \esc_html( $comment->comment_author_IP ), \esc_html( $comment_author_domain ) ) . "\r\n";
97+
/* translators: Reaction author URL. */
98+
$notify_message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $comment->comment_author_url ) ) . "\r\n\r\n";
99+
/* translators: Comment type label */
100+
$notify_message .= \sprintf( \esc_html__( 'You can see all %s on this post here:', 'activitypub' ), \esc_html( $comment_type['label'] ) ) . "\r\n";
101+
$notify_message .= \get_permalink( $comment->comment_post_ID ) . '#' . \esc_attr( $comment_type['type'] ) . "\r\n\r\n";
98102

99103
return $notify_message;
100104
}
101105

102106
/**
103-
* Send a Mail for every new follower.
107+
* Send a notification email for every new follower.
104108
*
105109
* @param Notification $notification The notification object.
106110
*/
@@ -111,26 +115,73 @@ public static function new_follower( $notification ) {
111115
return;
112116
}
113117

114-
$email = \get_option( 'admin_email' );
118+
$email = \get_option( 'admin_email' );
119+
$admin_url = '/options-general.php?page=activitypub&tab=followers';
115120

116-
if ( (int) $notification->target > Actors::BLOG_USER_ID ) {
121+
if ( $notification->target > Actors::BLOG_USER_ID ) {
117122
$user = \get_user_by( 'id', $notification->target );
118123

119124
if ( ! $user ) {
120125
return;
121126
}
122127

128+
$email = $user->user_email;
129+
$admin_url = '/users.php?page=activitypub-followers-list';
130+
}
131+
132+
/* translators: 1: Blog name, 2: Follower name */
133+
$subject = \sprintf( \esc_html__( '[%1$s] Follower: %2$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
134+
/* translators: 1: Blog name, 2: Follower name */
135+
$message = \sprintf( \esc_html__( 'New Follower: %2$s.', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $actor['name'] ) ) . "\r\n\r\n";
136+
/* translators: Follower URL */
137+
$message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";
138+
$message .= \esc_html__( 'You can see all followers here:', 'activitypub' ) . "\r\n";
139+
$message .= \esc_url( \admin_url( $admin_url ) ) . "\r\n\r\n";
140+
141+
\wp_mail( $email, $subject, $message );
142+
}
143+
144+
/**
145+
* Send a direct message.
146+
*
147+
* @param array $activity The activity object.
148+
* @param int $user_id The id of the local blog-user.
149+
*/
150+
public static function direct_message( $activity, $user_id ) {
151+
// Check if Activity is public or not.
152+
if (
153+
is_activity_public( $activity ) &&
154+
is_activity_reply( $activity )
155+
) {
156+
return;
157+
}
158+
159+
$actor = get_remote_metadata_by_actor( $activity['actor'] );
160+
161+
if ( ! $actor || \is_wp_error( $actor ) || empty( $activity['object']['content'] ) ) {
162+
return;
163+
}
164+
165+
$email = \get_option( 'admin_email' );
166+
167+
if ( (int) $user_id > Actors::BLOG_USER_ID ) {
168+
$user = \get_user_by( 'id', $user_id );
169+
170+
if ( ! $user ) {
171+
return;
172+
}
173+
123174
$email = $user->user_email;
124175
}
125176

126-
/* translators: %1$s: Blog name, %2$s: Follower name */
127-
$subject = \sprintf( \__( '[%1$s] Follower: %2$s', 'activitypub' ), get_option( 'blogname' ), $actor['name'] );
128-
/* translators: %1$s: Blog name, %2$s: Follower name */
129-
$message = \sprintf( \__( 'New follower: %2$s', 'activitypub' ), get_option( 'blogname' ), $actor['name'] ) . "\r\n";
130-
/* translators: %s: Follower URL */
131-
$message .= \sprintf( \__( 'URL: %s', 'activitypub' ), $actor['url'] ) . "\r\n\r\n";
132-
$message .= \sprintf( \__( 'You can see all followers here:', 'activitypub' ) ) . "\r\n";
133-
$message .= \esc_url( \admin_url( '/users.php?page=activitypub-followers-list' ) ) . "\r\n\r\n";
177+
/* translators: 1: Blog name, 2 Actor name */
178+
$subject = \sprintf( \esc_html__( '[%1$s] Direct Message from: %2$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
179+
/* translators: 1: Blog name, 2: Actor name */
180+
$message = \sprintf( \esc_html__( 'New Direct Message: %2$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \wp_strip_all_tags( $activity['object']['content'] ) ) . "\r\n\r\n";
181+
/* translators: Actor name */
182+
$message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
183+
/* translators: Actor URL */
184+
$message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";
134185

135186
\wp_mail( $email, $subject, $message );
136187
}

includes/class-notification.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,18 @@ public function __construct( $type, $actor, $activity, $target ) {
6060
public function send() {
6161
$type = \strtolower( $this->type );
6262

63+
/**
64+
* Action to send ActivityPub notifications.
65+
*
66+
* @param Notification $this The notification object.
67+
*/
6368
do_action( 'activitypub_notification', $this );
69+
70+
/**
71+
* Type-specific action to send ActivityPub notifications.
72+
*
73+
* @param Notification $this The notification object.
74+
*/
6475
do_action( "activitypub_notification_{$type}", $this );
6576
}
6677
}

includes/functions.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,17 @@ function is_activity_public( $data ) {
689689
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
690690
}
691691

692+
/**
693+
* Check if passed Activity is a reply.
694+
*
695+
* @param array $data The Activity object as array.
696+
*
697+
* @return boolean True if a reply, false if not.
698+
*/
699+
function is_activity_reply( $data ) {
700+
return ! empty( $data['object']['inReplyTo'] );
701+
}
702+
692703
/**
693704
* Get active users based on a given duration.
694705
*

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ For reasons of data protection, it is not possible to see the followers of other
136136

137137
* Added: `icon` support for `Audio` and `Video` attachments
138138
* Added: Send "new follower" emails
139+
* Added: Send "direct message" emails
139140
* Improved: Email templates for Likes and Reposts
140141
* Improved: Interactions moderation
141142
* Improved: Compatibility with Akismet

tests/includes/class-test-mailer.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,4 +209,73 @@ public function test_init() {
209209
$this->assertEquals( 10, has_filter( 'comment_notification_text', array( Mailer::class, 'comment_notification_text' ) ) );
210210
$this->assertEquals( 10, has_action( 'activitypub_notification_follow', array( Mailer::class, 'new_follower' ) ) );
211211
}
212+
213+
/**
214+
* Test direct message notification.
215+
*
216+
* @covers ::direct_message
217+
*/
218+
public function test_direct_message() {
219+
$user_id = self::$user_id;
220+
221+
$activity = array(
222+
'actor' => 'https://example.com/author',
223+
'object' => array(
224+
'content' => 'Test direct message',
225+
),
226+
);
227+
228+
// Mock remote metadata.
229+
add_filter(
230+
'pre_get_remote_metadata_by_actor',
231+
function () {
232+
return array(
233+
'name' => 'Test Sender',
234+
'url' => 'https://example.com/author',
235+
);
236+
}
237+
);
238+
239+
// Capture email.
240+
add_filter(
241+
'wp_mail',
242+
function ( $args ) use ( $user_id ) {
243+
$this->assertStringContainsString( 'Direct Message', $args['subject'] );
244+
$this->assertStringContainsString( 'Test Sender', $args['subject'] );
245+
$this->assertStringContainsString( 'Test direct message', $args['message'] );
246+
$this->assertStringContainsString( 'https://example.com/author', $args['message'] );
247+
$this->assertEquals( get_user_by( 'id', $user_id )->user_email, $args['to'] );
248+
return $args;
249+
}
250+
);
251+
252+
Mailer::direct_message( $activity, $user_id );
253+
254+
// Test public activity (should not send email).
255+
$public_activity = array(
256+
'actor' => 'https://example.com/author',
257+
'object' => array(
258+
'content' => 'Test public message',
259+
'inReplyTo' => 'https://example.com/post/1',
260+
),
261+
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
262+
);
263+
264+
// Reset email capture.
265+
remove_all_filters( 'wp_mail' );
266+
add_filter(
267+
'wp_mail',
268+
function ( $args ) {
269+
$this->fail( 'Email should not be sent for public activity' );
270+
return $args;
271+
}
272+
);
273+
274+
Mailer::direct_message( $public_activity, $user_id );
275+
276+
// Clean up.
277+
remove_all_filters( 'pre_get_remote_metadata_by_actor' );
278+
remove_all_filters( 'wp_mail' );
279+
wp_delete_user( $user_id );
280+
}
212281
}

0 commit comments

Comments
 (0)