Action Scheduler Deferred Emails Snippet
This code can be used as a standalone plugin, or mu-plugin file. It's an accompaniment to a set of tutorials about using Action Scheduler to defer the dispatch of emails (the first part of that tutorial can be found here).
<?php
/**
* Plugin name: Async Emails with Action Scheduler.
* Description: Uses Action Scheduler to send emails asynchronously, with a minimum 10 second gap between dispatches.
* Plugin URI: https://codingkills.me/action-scheduler-deferred-emails-snippet
* Author URI: https://codingkills.me
* Version: 2024.02.01
* License: GPL-3.0
*
* This plugin/snippet can live as a top-level plugin, or a mu-plugin. It is an
* accompaniment to this 3-part tutorial, written for Action Scheduler 3.7.1:
*
* 1. https://codingkills.me/deferring-emails-with-action-scheduler-1
* 2. https://codingkills.me/deferring-emails-with-action-scheduler-2
* 3. https://codingkills.me/deferring-emails-with-action-scheduler-3
*
* Take it, test it, change it, and improve it! It is a sparsely commented
* starting point, not a highly polished piece of production-ready code. Pay
* attention to the `todo` items sprinkled throughout :-)
*/
class My_Async_Emails {
private static bool $within_scheduled_action = false;
public static function setup(): void {
add_action( 'action_scheduler_init', [ __CLASS__, 'register_async_tasks' ] );
}
public static function register_async_tasks(): void {
if ( version_compare( ActionScheduler_Versions::instance()->latest_version(), '3.7.1', '<' ) ) {
return;
}
add_filter( 'pre_wp_mail', [ __CLASS__, 'defer_emails' ], 10, 2 );
add_action( 'my_async_emails_send_email', [ __CLASS__, 'send_emails' ], 10, 5 );
}
public static function defer_emails( $null, array $attributes ): mixed {
if ( false === self::$within_scheduled_action ) {
// @todo $attributes is a filtered value: consider verifying it has the expected shape.
if ( ! My_Stored_Emails::save( ...$attributes ) ) {
return false;
}
return (bool) as_schedule_recurring_action(
timestamp: time(),
interval_in_seconds: 10,
hook: 'my_async_emails_send_email',
group: 'deferred-emails',
unique: true,
);
}
return $null;
}
public static function send_emails(): void {
self::$within_scheduled_action = true;
$email_id = My_Stored_Emails::get_next_id();
if ( ! $email_id ) {
self::unhook_self();
return;
}
$email_array = My_Stored_Emails::get( $email_id );
if ( $email_array ) {
// @todo consider checking if wp_mail() succeeds: should we log failures?
wp_mail( ...$email_array );
}
My_Stored_Emails::delete( $email_id );
self::$within_scheduled_action = false;
}
private static function unhook_self(): void {
add_action( 'action_scheduler_after_process_queue', function () {
as_unschedule_all_actions(
hook: 'my_async_emails_send_email',
group: 'deferred-emails',
);
} );
}
}
class My_Stored_Emails {
private const POST_TYPE = 'My_Stored_Emails';
public static function register_post_type(): void {
register_post_type( self::POST_TYPE, [
'public' => false,
] );
}
public static function save( string $to, string $subject, string $message, string $headers = '', $attachments = [] ): int|false {
$payload = wp_json_encode( [
'to' => $to,
'subject' => $subject,
'message' => $message,
'headers' => $headers,
'attachments' => $attachments,
] );
$inserted_post_id = wp_insert_post( [
'post_type' => self::POST_TYPE,
'post_content' => base64_encode( $payload ),
'post_mime_type' => 'application/data',
] );
return is_int( $inserted_post_id ) && $inserted_post_id > 0 ? $inserted_post_id : false;
}
public static function get_next_id(): int|false {
$email_posts = get_posts( [
'fields' => 'ids',
'order' => 'ASC',
'orderby' => 'date',
'post_status' => 'any',
'post_type' => self::POST_TYPE,
'posts_per_page' => 1
] );
return is_array( $email_posts ) && count( $email_posts ) === 1 ? (int) current( $email_posts ) : false;
}
public static function get( int $id ): array|false {
$retrieved_email = get_posts( [
'post__in' => [ $id ],
'post_status' => 'any',
'post_type' => self::POST_TYPE
] );
if ( ! is_array( $retrieved_email ) || count( $retrieved_email ) !== 1 ) {
return false;
}
$email_array = json_decode( base64_decode( $retrieved_email[0]->post_content ), true );
return is_array( $email_array ) ? $email_array : false;
}
public static function delete( int $email_id ): void {
// @todo instead of deleting, we could consider making better use of post statuses.
wp_delete_post( $email_id, true );
}
}
add_action( 'init', [ My_Stored_Emails::class, 'register_post_type' ] );
My_Async_Emails::setup();