Skip to content

Add asynchronous email queue#28

Open
Nimeshan wants to merge 4 commits into
fossasia:mainfrom
Nimeshan:fix/async-email-queue
Open

Add asynchronous email queue#28
Nimeshan wants to merge 4 commits into
fossasia:mainfrom
Nimeshan:fix/async-email-queue

Conversation

@Nimeshan
Copy link
Copy Markdown
Contributor

@Nimeshan Nimeshan commented May 10, 2026

Fixes #10

Adds an asynchronous email queue backed by a dedicated queue table and WP Cron processing.

Summary:

  • Adds queue table creation and migration support.
  • Adds an Enable Email Queuing setting.
  • Intercepts wp_mail through pre_wp_mail when queueing is enabled.
  • Processes queued mail in batches with retry handling.
  • Links queue status back to email logs and shows queue counts in the logs screen.

Summary by Sourcery

Introduce an asynchronous email queuing system backed by a new queue table, WP Cron processing, and extended logging to decouple wp_mail calls from immediate delivery.

New Features:

  • Add an email queue manager that stores wp_mail payloads in a dedicated database table for background processing.
  • Provide an Enable Email Queuing setting and Email Queue admin section to control queue behaviour.
  • Hook into pre_wp_mail to enqueue outgoing emails instead of sending them synchronously when queuing is enabled.
  • Expose queue status metrics (queued, processing, sent, failed) on the Email Logs screen via a status widget.

Enhancements:

  • Extend email logging to support queued status, status metadata, and deterministic mail hashing shared with the queue.
  • Wire the queue processor into WP Cron with a custom five‑minute schedule and batch processing with retry handling.
  • Update activation, deactivation, and migration routines to create the queue table, manage cron events, and bump plugin and DB versions to 1.3.0.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 10, 2026

Reviewer's Guide

Implements an asynchronous email queuing system backed by a new queue table and WP-Cron processor, wires it into existing SMTP/logging flows, and surfaces queue status in admin logs, including schema migration and lifecycle hooks for activation/deactivation and upgrades.

Sequence diagram for wp_mail queuing and asynchronous processing

sequenceDiagram
    actor User
    participant WP as WordPress_wp_mail
    participant SMTP as Wpfa_Mailconnect_SMTP
    participant Queue as Wpfa_Mailconnect_Queue
    participant Logger as Wpfa_Mailconnect_Logger
    participant DB as WordPress_DB
    participant Cron as WP_Cron

    User->>WP: wp_mail(to, subject, message, headers, attachments)
    WP->>SMTP: pre_wp_mail filter queue_email(pre, args)
    SMTP->>SMTP: get_option smtp_options
    alt queue_enabled and not Wpfa_Mailconnect_Queue::is_processing
        SMTP->>Queue: add_to_queue(args)
        Queue->>Queue: normalize_mail_args(args)
        Queue->>Logger: generate_mail_hash(args)
        Logger-->>Queue: hash
        Queue->>Logger: insert_pending(hash, to, subject, message, body_html, headers, status_details)
        Logger->>DB: INSERT log_row(status pending)
        Queue->>Logger: update_status(hash, queued, "", status_details)
        Logger->>DB: UPDATE log_row(status queued)
        Queue->>DB: INSERT queue_row(status queued)
        DB-->>Queue: insert_id
        Queue-->>SMTP: queued_id
        SMTP->>Queue: Wpfa_Mailconnect_Queue::schedule_processing()
        Queue->>Cron: wp_schedule_event(PROCESS_CRON_HOOK)
        SMTP-->>WP: true (short-circuit wp_mail)
    else queue disabled or processing
        SMTP-->>WP: pre (continue normal wp_mail)
    end

    Cron->>Queue: process_queue_batch()
    Queue->>DB: SELECT queued rows WHERE next_attempt_at <= now LIMIT BATCH_SIZE
    DB-->>Queue: queue_items
    loop each queue_item
        Queue->>DB: UPDATE queue_row SET status processing
        Queue->>WP: add_action wp_mail_failed(error_capture)
        Queue->>Queue: self::$processing = true
        Queue->>WP: wp_mail(to, subject, message, headers, attachments)
        WP-->>Queue: sent_flag
        Queue->>Queue: self::$processing = false
        Queue->>WP: remove_action wp_mail_failed
        alt sent_flag true
            Queue->>DB: UPDATE queue_row SET status sent
            Queue->>Logger: update_status(hash, success, "", details)
            Logger->>DB: UPDATE log_row(status success)
        else sent_flag false
            Queue->>DB: UPDATE retries, status(queued or failed), next_attempt_at
            Queue->>Logger: update_status(hash, queued or failed, error_message, details)
            Logger->>DB: UPDATE log_row(status queued or failed)
        end
    end
Loading

Entity relationship diagram for email logs and queue tables

erDiagram
    email_logs {
        bigint id PK
        varchar hash
        longtext to_email
        text subject
        longtext message
        longtext body_html
        varchar status
        text error_message
        longtext status_details
        longtext headers
        datetime created_at
    }

    mail_queue {
        bigint id PK
        varchar status
        varchar hash
        longtext to_email
        text subject
        longtext message
        longtext headers
        longtext attachments
        int retries
        datetime created_at
        datetime next_attempt_at
        datetime updated_at
    }

    email_logs ||--o{ mail_queue : hash_links_queue_items_to_logs
Loading

Class diagram for email queue, SMTP, and logger integration

classDiagram
    class Wpfa_Mailconnect {
        -Wpfa_Mailconnect_SMTP smtp
        -Wpfa_Mailconnect_Logger email_logger_service
        -Wpfa_Mailconnect_Queue queue
        +load_dependencies()
        +define_admin_hooks()
    }

    class Wpfa_Mailconnect_SMTP {
        -Wpfa_Mailconnect_Logger logger
        -Wpfa_Mailconnect_Queue queue
        -array fields
        +__construct(plugin_name, version)
        +settings_init()
        +logging_section_callback()
        +queue_section_callback()
        +sanitize_smtp_options(input)
        +test_email_form()
        +queue_email(pre, args)
        +phpmailer_override(phpmailer)
    }

    class Wpfa_Mailconnect_Queue {
        <<service>>
        +const TABLE_SUFFIX
        +const PROCESS_CRON_HOOK
        +const CRON_INTERVAL
        +const BATCH_SIZE
        +const MAX_RETRIES
        -static bool processing
        -string table_name
        -Wpfa_Mailconnect_Logger logger
        +__construct(logger)
        +add_cron_schedules(schedules) array
        +add_to_queue(args) int
        +process_queue_batch() int
        +get_status_counts() array
        +is_queue_enabled() bool
        +schedule_processing() void
        +unschedule_processing() void
        +is_processing() bool
        -create_table() void
        -mark_processing(id) void
        -mark_sent(item) void
        -mark_failed_or_retry(item, error) void
        -normalize_mail_args(args) array
        -format_recipients(recipients) string
        -is_html_content(args) bool
        -get_attachment_details(attachments) array
    }

    class Wpfa_Mailconnect_Logger {
        +const ALLOWED_STATUSES
        +create_log_table() void
        +generate_mail_hash(args) string
        -get_attachment_hash_details(attachments) array
        +insert_pending(hash, to, subject, message, body_html, headers, status_details) bool
        +update_status(hash, status, error_message, status_details) bool
        +clear_old_logs(days) int
    }

    class Wpfa_Mailconnect_Admin {
        +render_logs_page() void
        -render_queue_status_widget() void
        +handle_clear_logs() void
    }

    class Wpfa_Mailconnect_Activator {
        +activate() void
    }

    class Wpfa_Mailconnect_Deactivator {
        +deactivate() void
    }

    class Wpfa_Mailconnect_Updater {
        -run_migrations(installed_version) void
        -update_to_1_2_0() void
        -update_to_1_3_0() void
        -update_to_1_0_1() void
    }

    Wpfa_Mailconnect --> Wpfa_Mailconnect_SMTP : composes
    Wpfa_Mailconnect --> Wpfa_Mailconnect_Queue : composes
    Wpfa_Mailconnect_SMTP --> Wpfa_Mailconnect_Logger : uses
    Wpfa_Mailconnect_SMTP --> Wpfa_Mailconnect_Queue : composes
    Wpfa_Mailconnect_Queue --> Wpfa_Mailconnect_Logger : uses
    Wpfa_Mailconnect_Admin --> Wpfa_Mailconnect_Queue : uses
    Wpfa_Mailconnect_Activator --> Wpfa_Mailconnect_Logger : uses
    Wpfa_Mailconnect_Activator --> Wpfa_Mailconnect_Queue : uses
    Wpfa_Mailconnect_Deactivator --> Wpfa_Mailconnect_Queue : uses
    Wpfa_Mailconnect_Updater --> Wpfa_Mailconnect_Queue : uses
Loading

File-Level Changes

Change Details Files
Introduce queue table and asynchronous processing pipeline for wp_mail calls using WP Cron.
  • Add Wpfa_Mailconnect_Queue class to store wp_mail payloads, track status/retries, and process batches via process_queue_batch using wp_mail.
  • Define queue table schema, creation helper, and status-count helper plus retry/backoff logic and processing guard flag.
  • Register custom cron schedule, queue processing hook, activator/deactivator integration, and migration to create the queue table on upgrade to 1.3.0.
includes/class-wpfa-mailconnect-queue.php
includes/class-wpfa-mailconnect.php
includes/class-wpfa-mailconnect-activator.php
includes/class-wpfa-mailconnect-deactivator.php
includes/class-wpfa-mailconnect-updater.php
Wire email queue into SMTP settings and wp_mail interception, including enabling/disabling and scheduling behavior.
  • Add enable_email_queue setting, dedicated settings section/description, and ensure it is excluded from the main SMTP field loop.
  • Intercept wp_mail using pre_wp_mail to enqueue messages when queueing is enabled and not currently processing, then schedule processing.
  • On settings save, schedule or unschedule the queue processor based on the enable_email_queue option, and slightly refactor numeric sanitization.
includes/class-wpfa-mailconnect-smtp.php
Extend logging to support queued email lifecycle and deterministic correlation with queue entries.
  • Add 'queued' to allowed log statuses and extend insert_pending to accept and persist status_details.
  • Introduce deterministic generate_mail_hash plus attachment hashing helper to normalize wp_mail payloads and link queue rows to log entries.
  • Update queue code to insert pending logs with queue metadata and then transition status to queued/success/failed with enriched status_details on send, retry, or failure.
includes/class-wpfa-mailconnect-logger.php
includes/class-wpfa-mailconnect-queue.php
Expose queue state in the admin email logs UI.
  • Add 'Queued' filter option and map queued rows to the pending row CSS class for consistent styling.
  • Render a queue status widget on the logs page that shows counts for queued, processing, sent, and failed items via the queue status-counts helper.
admin/class-wpfa-mailconnect-admin.php
Bump plugin and DB versions to trigger new migration path for the queue feature.
  • Increase plugin version constant to 1.3.0 and DB schema version to 1.3.0 to align with the new queue table migration.
  • Wire the 1.3.0 migration into the updater to create the queue table during upgrades and log in WP_DEBUG mode.
wpfa-mailconnect.php
includes/class-wpfa-mailconnect-updater.php

Assessment against linked issues

Issue Objective Addressed Explanation
#10 Create a dedicated email queue database table and integrate it with the existing logger using a deterministic hash to link queue items to log entries.
#10 Modify the mail dispatch logic to intercept wp_mail calls and, when queuing is enabled, store the email data in the queue table and immediately return success instead of sending synchronously.
#10 Implement a background queue processor driven by WP Cron (with configuration in plugin settings) that processes queued emails in batches, handles retries, updates log statuses, and exposes queue status in the admin UI.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The cron_schedules registration for the custom 5‑minute interval is split between the activator (anonymous filter) and Wpfa_Mailconnect_Queue::add_cron_schedules; consider consolidating to a single location, since adding the filter only during activation is not persisted across requests and may cause wp_schedule_event with the custom interval to fail.
  • The attachment metadata logic is implemented twice (Wpfa_Mailconnect_Logger::get_attachment_hash_details and Wpfa_Mailconnect_Queue::get_attachment_details) with slightly different outputs; extracting a shared helper would reduce drift and make it easier to keep the logged and hashed attachment information consistent.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `cron_schedules` registration for the custom 5‑minute interval is split between the activator (anonymous filter) and `Wpfa_Mailconnect_Queue::add_cron_schedules`; consider consolidating to a single location, since adding the filter only during activation is not persisted across requests and may cause `wp_schedule_event` with the custom interval to fail.
- The attachment metadata logic is implemented twice (`Wpfa_Mailconnect_Logger::get_attachment_hash_details` and `Wpfa_Mailconnect_Queue::get_attachment_details`) with slightly different outputs; extracting a shared helper would reduce drift and make it easier to keep the logged and hashed attachment information consistent.

## Individual Comments

### Comment 1
<location path="includes/class-wpfa-mailconnect-activator.php" line_range="54-62" />
<code_context>
 			wp_schedule_event( time(), 'daily', $cleanup_hook );
 		}
+
+		add_filter(
+			'cron_schedules',
+			function( $schedules ) {
+				$schedules[ Wpfa_Mailconnect_Queue::CRON_INTERVAL ] = array(
+					'interval' => 5 * MINUTE_IN_SECONDS,
+					'display'  => __( 'Every 5 Minutes', 'wpfa-mailconnect' ),
+				);
+				return $schedules;
+			}
+		);
+		if ( Wpfa_Mailconnect_Queue::is_queue_enabled() ) {
</code_context>
<issue_to_address>
**issue (bug_risk):** Avoid registering cron_schedules filter inside activation, as it won’t persist beyond the activation request and duplicates runtime logic.

Because this filter is added only during activation, the custom schedule won’t exist on normal requests, so the interval isn’t reliably registered. It also duplicates the schedule configuration already handled in Wpfa_Mailconnect_Queue::add_cron_schedules. Please remove this activator filter and rely on the queue class’ add_cron_schedules hook instead.
</issue_to_address>

### Comment 2
<location path="includes/class-wpfa-mailconnect-queue.php" line_range="357-360" />
<code_context>
+	private function mark_failed_or_retry( $item, $error ) {
+		global $wpdb;
+
+		$retries = absint( $item->retries ) + 1;
+		$failed  = $retries >= self::MAX_RETRIES;
+		$status  = $failed ? 'failed' : 'queued';
+		$delay   = min( 60, 5 * $retries );
+
+		$wpdb->update(
</code_context>
<issue_to_address>
**issue (bug_risk):** Align next_attempt_at time calculation with WordPress time helpers to avoid potential timezone inconsistencies.

`current_time( 'timestamp' )` already applies the site timezone, while `date()` uses the server timezone, which may differ and cause scheduling drift. Use a consistent helper chain, for example:

```php
$timestamp    = current_time( 'timestamp' );
$next_ts      = $timestamp + ( $delay * MINUTE_IN_SECONDS );
$next_attempt = gmdate( 'Y-m-d H:i:s', $next_ts );
```

(or `current_time( 'mysql', true )` plus a manual offset) so this value stays aligned with how `created_at`/`updated_at` are generated.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread includes/class-wpfa-mailconnect-activator.php Outdated
Comment thread includes/class-wpfa-mailconnect-queue.php
Nimeshan and others added 3 commits May 10, 2026 12:33
Resolve conflicts: keep 1.0.0 version constants and retain
render_queue_status_widget() for the async queue UI on logs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve conflicts keeping both async queue (PR fossasia#28) and HTML email
templates (PR fossasia#29) features:
- Preserve render_queue_status_widget() alongside new format_attachment_log_summary()
- Keep queue class require and cron hooks alongside template wp_mail filter
- Combine queue and template settings sections/fields in SMTP class
- Retain both queue_email() and apply_html_template() methods

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Implement Asynchronous Email Queue System

1 participant