Skip to content

Commit

Permalink
Init new custom mailer class (#1068)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
pfefferle and obenland authored Dec 11, 2024
1 parent 6d5c76f commit 29053f3
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* `icon` support for `Audio` and `Video` attachments
* Send "new follower" emails
* Send "direct message" emails

### Improved

Expand Down
1 change: 1 addition & 0 deletions activitypub.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ function plugin_init() {
\add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Link', 'init' ) );
\add_action( 'init', array( __NAMESPACE__ . '\Mailer', 'init' ) );

if ( site_supports_blocks() ) {
\add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) );
Expand Down
129 changes: 90 additions & 39 deletions includes/class-mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,41 @@
namespace Activitypub;

use Activitypub\Collection\Actors;

/**
* Mailer Class
* Mailer Class.
*/
class Mailer {
/**
* Initialize the Mailer.
*/
public static function init() {
add_filter( 'comment_notification_subject', array( self::class, 'comment_notification_subject' ), 10, 2 );
add_filter( 'comment_notification_text', array( self::class, 'comment_notification_text' ), 10, 2 );
\add_filter( 'comment_notification_subject', array( self::class, 'comment_notification_subject' ), 10, 2 );
\add_filter( 'comment_notification_text', array( self::class, 'comment_notification_text' ), 10, 2 );

// New follower notification.
add_action( 'activitypub_notification_follow', array( self::class, 'new_follower' ) );
\add_action( 'activitypub_notification_follow', array( self::class, 'new_follower' ) );

// Direct message notification.
\add_action( 'activitypub_inbox_create', array( self::class, 'direct_message' ), 10, 2 );
}

/**
* Filter the mail-subject for Like and Announce notifications.
* Filter the subject line for Like and Announce notifications.
*
* @param string $subject The default mail-subject.
* @param string $subject The default subject line.
* @param int|string $comment_id The comment ID.
*
* @return string The filtered mail-subject
* @return string The filtered subject line.
*/
public static function comment_notification_subject( $subject, $comment_id ) {
$comment = get_comment( $comment_id );
$comment = \get_comment( $comment_id );

if ( ! $comment ) {
return $subject;
}

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

if ( 'activitypub' !== $type ) {
return $subject;
Expand All @@ -50,28 +54,28 @@ public static function comment_notification_subject( $subject, $comment_id ) {
return $subject;
}

$post = get_post( $comment->comment_post_ID );
$post = \get_post( $comment->comment_post_ID );

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

/**
* Filter the mail-content for Like and Announce notifications.
* Filter the notification text for Like and Announce notifications.
*
* @param string $message The default mail-content.
* @param string $message The default notification text.
* @param int|string $comment_id The comment ID.
*
* @return string The filtered mail-content
* @return string The filtered notification text.
*/
public static function comment_notification_text( $message, $comment_id ) {
$comment = get_comment( $comment_id );
$comment = \get_comment( $comment_id );

if ( ! $comment ) {
return $message;
}

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

if ( 'activitypub' !== $type ) {
return $message;
Expand All @@ -83,24 +87,24 @@ public static function comment_notification_text( $message, $comment_id ) {
return $message;
}

$post = get_post( $comment->comment_post_ID );
$comment_author_domain = gethostbyaddr( $comment->comment_author_IP );
$post = \get_post( $comment->comment_post_ID );
$comment_author_domain = \gethostbyaddr( $comment->comment_author_IP );

/* translators: %1$s: Comment type, %2$s: Post title */
$notify_message = \sprintf( __( 'New %1$s on your post "%2$s"', 'activitypub' ), $comment_type['singular'], $post->post_title ) . "\r\n";
/* translators: 1: Trackback/pingback website name, 2: Website IP address, 3: Website hostname. */
$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";
/* translators: %s: Trackback/pingback/comment author URL. */
$notify_message .= \sprintf( __( 'URL: %s', 'activitypub' ), $comment->comment_author_url ) . "\r\n\r\n";
/* translators: %s: Comment type label */
$notify_message .= \sprintf( __( 'You can see all %s on this post here:', 'activitypub' ), $comment_type['label'] ) . "\r\n";
$notify_message .= \get_permalink( $comment->comment_post_ID ) . '#' . $comment_type['singular'] . "\r\n\r\n";
/* translators: 1: Comment type, 2: Post title */
$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";
/* translators: 1: Website name, 2: Website IP address, 3: Website hostname. */
$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";
/* translators: Reaction author URL. */
$notify_message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $comment->comment_author_url ) ) . "\r\n\r\n";
/* translators: Comment type label */
$notify_message .= \sprintf( \esc_html__( 'You can see all %s on this post here:', 'activitypub' ), \esc_html( $comment_type['label'] ) ) . "\r\n";
$notify_message .= \get_permalink( $comment->comment_post_ID ) . '#' . \esc_attr( $comment_type['type'] ) . "\r\n\r\n";

return $notify_message;
}

/**
* Send a Mail for every new follower.
* Send a notification email for every new follower.
*
* @param Notification $notification The notification object.
*/
Expand All @@ -111,26 +115,73 @@ public static function new_follower( $notification ) {
return;
}

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

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

if ( ! $user ) {
return;
}

$email = $user->user_email;
$admin_url = '/users.php?page=activitypub-followers-list';
}

/* translators: 1: Blog name, 2: Follower name */
$subject = \sprintf( \esc_html__( '[%1$s] Follower: %2$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
/* translators: 1: Blog name, 2: Follower name */
$message = \sprintf( \esc_html__( 'New Follower: %2$s.', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $actor['name'] ) ) . "\r\n\r\n";
/* translators: Follower URL */
$message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";
$message .= \esc_html__( 'You can see all followers here:', 'activitypub' ) . "\r\n";
$message .= \esc_url( \admin_url( $admin_url ) ) . "\r\n\r\n";

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

/**
* Send a direct message.
*
* @param array $activity The activity object.
* @param int $user_id The id of the local blog-user.
*/
public static function direct_message( $activity, $user_id ) {
// Check if Activity is public or not.
if (
is_activity_public( $activity ) &&
is_activity_reply( $activity )
) {
return;
}

$actor = get_remote_metadata_by_actor( $activity['actor'] );

if ( ! $actor || \is_wp_error( $actor ) || empty( $activity['object']['content'] ) ) {
return;
}

$email = \get_option( 'admin_email' );

if ( (int) $user_id > Actors::BLOG_USER_ID ) {
$user = \get_user_by( 'id', $user_id );

if ( ! $user ) {
return;
}

$email = $user->user_email;
}

/* translators: %1$s: Blog name, %2$s: Follower name */
$subject = \sprintf( \__( '[%1$s] Follower: %2$s', 'activitypub' ), get_option( 'blogname' ), $actor['name'] );
/* translators: %1$s: Blog name, %2$s: Follower name */
$message = \sprintf( \__( 'New follower: %2$s', 'activitypub' ), get_option( 'blogname' ), $actor['name'] ) . "\r\n";
/* translators: %s: Follower URL */
$message .= \sprintf( \__( 'URL: %s', 'activitypub' ), $actor['url'] ) . "\r\n\r\n";
$message .= \sprintf( \__( 'You can see all followers here:', 'activitypub' ) ) . "\r\n";
$message .= \esc_url( \admin_url( '/users.php?page=activitypub-followers-list' ) ) . "\r\n\r\n";
/* translators: 1: Blog name, 2 Actor name */
$subject = \sprintf( \esc_html__( '[%1$s] Direct Message from: %2$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
/* translators: 1: Blog name, 2: Actor name */
$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";
/* translators: Actor name */
$message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
/* translators: Actor URL */
$message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";

\wp_mail( $email, $subject, $message );
}
Expand Down
11 changes: 11 additions & 0 deletions includes/class-notification.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,18 @@ public function __construct( $type, $actor, $activity, $target ) {
public function send() {
$type = \strtolower( $this->type );

/**
* Action to send ActivityPub notifications.
*
* @param Notification $this The notification object.
*/
do_action( 'activitypub_notification', $this );

/**
* Type-specific action to send ActivityPub notifications.
*
* @param Notification $this The notification object.
*/
do_action( "activitypub_notification_{$type}", $this );
}
}
11 changes: 11 additions & 0 deletions includes/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,17 @@ function is_activity_public( $data ) {
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
}

/**
* Check if passed Activity is a reply.
*
* @param array $data The Activity object as array.
*
* @return boolean True if a reply, false if not.
*/
function is_activity_reply( $data ) {
return ! empty( $data['object']['inReplyTo'] );
}

/**
* Get active users based on a given duration.
*
Expand Down
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ For reasons of data protection, it is not possible to see the followers of other

* Added: `icon` support for `Audio` and `Video` attachments
* Added: Send "new follower" emails
* Added: Send "direct message" emails
* Improved: Email templates for Likes and Reposts
* Improved: Interactions moderation
* Improved: Compatibility with Akismet
Expand Down
69 changes: 69 additions & 0 deletions tests/includes/class-test-mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,73 @@ public function test_init() {
$this->assertEquals( 10, has_filter( 'comment_notification_text', array( Mailer::class, 'comment_notification_text' ) ) );
$this->assertEquals( 10, has_action( 'activitypub_notification_follow', array( Mailer::class, 'new_follower' ) ) );
}

/**
* Test direct message notification.
*
* @covers ::direct_message
*/
public function test_direct_message() {
$user_id = self::$user_id;

$activity = array(
'actor' => 'https://example.com/author',
'object' => array(
'content' => 'Test direct message',
),
);

// Mock remote metadata.
add_filter(
'pre_get_remote_metadata_by_actor',
function () {
return array(
'name' => 'Test Sender',
'url' => 'https://example.com/author',
);
}
);

// Capture email.
add_filter(
'wp_mail',
function ( $args ) use ( $user_id ) {
$this->assertStringContainsString( 'Direct Message', $args['subject'] );
$this->assertStringContainsString( 'Test Sender', $args['subject'] );
$this->assertStringContainsString( 'Test direct message', $args['message'] );
$this->assertStringContainsString( 'https://example.com/author', $args['message'] );
$this->assertEquals( get_user_by( 'id', $user_id )->user_email, $args['to'] );
return $args;
}
);

Mailer::direct_message( $activity, $user_id );

// Test public activity (should not send email).
$public_activity = array(
'actor' => 'https://example.com/author',
'object' => array(
'content' => 'Test public message',
'inReplyTo' => 'https://example.com/post/1',
),
'to' => array( 'https://www.w3.org/ns/activitystreams#Public' ),
);

// Reset email capture.
remove_all_filters( 'wp_mail' );
add_filter(
'wp_mail',
function ( $args ) {
$this->fail( 'Email should not be sent for public activity' );
return $args;
}
);

Mailer::direct_message( $public_activity, $user_id );

// Clean up.
remove_all_filters( 'pre_get_remote_metadata_by_actor' );
remove_all_filters( 'wp_mail' );
wp_delete_user( $user_id );
}
}

0 comments on commit 29053f3

Please sign in to comment.