Developer Reference
Technical reference for developers extending or integrating with Super Speedy Emails.
Last updated for DB version 2.10.0 / plugin version 1.16.0. If
get_option( 'sse_db_version' )reports a higher number, this doc may be behind.
Database Tables
All tables use the WordPress prefix (e.g. wp_sse_lists). Schema changes go through SSE_Activator::create_tables() via dbDelta.
Phase 1 tables (signup / forms / subscription management)
sse_lists
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
name | varchar(255) | List name |
slug | varchar(255) | URL-friendly slug (unique) |
description | text | Optional description |
status | varchar(20) | active or archived |
settings | longtext | JSON for misc settings |
requires_confirmation | tinyint(1) | Double opt-in flag (default 1) – Phase 2B |
is_newsletter | tinyint(1) | 1 = this list is the article-newsletter audience (single-select) |
category | varchar(40) | Email category this list maps to (list_broadcast default) – Phase 2C |
created_at / updated_at | datetime |
sse_subscribers
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
email | varchar(255) | Email address (unique) |
first_name / last_name | varchar(255) | |
wp_user_id | bigint(20) unsigned | Linked WordPress user ID (nullable) |
cookie_id | varchar(64) | Guest identification cookie value |
ip_address | varchar(45) | IP at time of subscription |
status | varchar(20) | pending / active / bounced / complained / suppressed – Phase 2A |
confirmed_at | datetime | Double opt-in confirmation timestamp – Phase 2B |
marketing_opted_out_at | datetime | When marketing-opt-out happened |
marketing_opt_out_reason | varchar(64) | user_request / complaint / bounce / migrated_unsubscribed |
digest_frequency | varchar(20) | instant / digest – Phase 2C |
unsubscribe_token | varchar(64) | Long random token for one-click unsubscribe URLs |
source | varchar(64) | form / migration_mailpoet / migration_mailerlite / wc_purchase / admin |
created_at / updated_at | datetime |
sse_subscriber_lists
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
subscriber_id | bigint(20) unsigned | FK to sse_subscribers |
list_id | bigint(20) unsigned | FK to sse_lists |
status | varchar(20) | subscribed, unsubscribed, or pending |
subscribed_at | datetime | |
unsubscribed_at | datetime | nullable |
Unique on (subscriber_id, list_id).
sse_forms
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
name | varchar(255) | Internal form name |
list_ids | longtext | JSON array of list IDs |
fields | longtext | JSON: {"first_name": true, "last_name": false} |
style | longtext | JSON: preset, colors, border radius, button text |
popup_settings | longtext | JSON popup config (null if not a popup) |
success_message | text | |
status | varchar(20) | active or inactive |
created_at / updated_at | datetime |
Phase 2 tables (sending pipeline)
sse_suppressions
The authoritative do-not-send store. Scopes: global, marketing, category:<term_id>, list:<list_id>, product_marketing:<product_id>.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
email | varchar(255) | Normalized lowercase |
scope | varchar(80) | See above |
reason | varchar(64) | user_unsubscribe / complaint / bounce_hard / bounce_soft_repeated / manual / migrated |
source | varchar(64) | Where the suppression came from |
created_at | datetime |
Unique on (email, scope).
sse_newsletter_prefs
Per-subscriber, per-category preferences. Missing rows mean defaulted-in.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
subscriber_id | bigint(20) unsigned | FK to sse_subscribers |
term_id | bigint(20) unsigned | WP term_id of a top-level post category |
subscribed | tinyint(1) | 1 = wants this category |
updated_at | datetime |
Unique on (subscriber_id, term_id).
sse_campaigns
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
name | varchar(255) | Internal name |
category | varchar(40) | See Email Categories |
type | varchar(20) | broadcast / automation_step / digest |
subject | varchar(255) | |
from_name / from_email / reply_to | varchar(255) | Optional per-campaign overrides |
body_text | longtext | Plain-text body (with merge tags) |
body_html | longtext | HTML body (optional; auto-generated when omitted) |
audience_query | longtext | JSON segment query – see Segments |
audience_snapshot | longtext | Frozen recipient list at sending transition |
automation_id | bigint(20) unsigned | FK to sse_automations (null for broadcasts) |
schedule_at | datetime | nullable |
status | varchar(20) | draft / scheduled / sending / sent / cancelled |
sent_at | datetime | |
recipient_count | int unsigned | Frozen count from snapshot |
created_at / updated_at | datetime |
sse_automations
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
name | varchar(255) | |
trigger_type | varchar(40) | wc_product_purchased / manual / custom (via filter) |
trigger_config | longtext | JSON, e.g. { "product_ids": [...], "match": "any" } |
category | varchar(40) | Email category for all steps |
status | varchar(20) | active / paused |
created_at / updated_at | datetime |
sse_automation_steps
Ordered steps for an automation. Cumulative offsets from enrolment timestamp.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
automation_id | bigint(20) unsigned | FK |
position | int unsigned | Sort order (0-indexed) |
offset_days / offset_hours | int | Cumulative offset from enrollment_created_at |
subject | varchar(255) | |
body_text | longtext | |
body_html | longtext | |
created_at / updated_at | datetime |
Index on (automation_id, position).
sse_automation_enrollments
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
automation_id | bigint(20) unsigned | FK |
subscriber_id | bigint(20) unsigned | FK |
context | longtext | JSON, e.g. { "order_id": 123, "product_id": 456 } |
context_hash | varchar(32) | md5 of dedup-relevant context (e.g. order_id) |
current_step | int unsigned | Position of the next step to send |
next_send_at | datetime | When the current step is due |
status | varchar(20) | active / complete / cancelled |
created_at / updated_at | datetime |
Unique on (automation_id, subscriber_id, context_hash). Index on (next_send_at, status).
sse_email_log
One row per attempted send. Drives reporting + compliance audit trail.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
subscriber_id | bigint(20) unsigned | nullable for fully ad-hoc |
email | varchar(255) | Captured at send-time |
campaign_id / automation_id | bigint(20) unsigned | nullable |
category | varchar(40) | Denormalised for fast filtering |
legal_basis | varchar(40) | consent / legitimate_interest / service |
subject | varchar(255) | |
mailgun_message_id | varchar(255) | |
status | varchar(20) | queued / sent / delivered / bounced / complained / failed |
opened_at / clicked_at / bounced_at / complained_at / unsubscribed_at / delivered_at | datetime | First-event timestamps |
open_count / click_count | int unsigned | Cumulative counters |
error | text | Provider error text |
context | longtext | JSON merge-data snapshot |
source_plugin / source_reference | varchar | For 3rd-party integrations (e.g. ssp-product-renewals / renewal:order:123) |
test_mode | varchar(20) | live / log_only / redirect |
created_at / updated_at | datetime |
Indexes: (subscriber_id, created_at), (campaign_id, status), mailgun_message_id, category, test_mode.
sse_email_events
Raw event stream from MailGun webhooks. Append-only.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
email_log_id | bigint(20) unsigned | FK (nullable – events can arrive before match) |
mailgun_message_id | varchar(255) | |
event | varchar(40) | delivered / opened / clicked / unsubscribed / complained / permanent_fail / temporary_fail |
payload | longtext | Full webhook payload (JSON) |
occurred_at | datetime | From the webhook payload |
received_at | datetime | When we received it |
sse_email_queue
Cron-drained send queue. Small (rows live only while pending).
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
email_log_id | bigint(20) unsigned | FK – log row is created up-front |
priority | int | Lower = sooner (transactional 1, marketing 10) |
attempts | int unsigned | |
next_attempt_at | datetime | Exponential backoff (1m → 4m → 16m → 64m → 256m) |
locked_by | varchar(64) | Worker ID |
locked_until | datetime | |
last_error | text | |
created_at | datetime |
sse_product_owners
Derived cache populated from completed/processing WC orders. Bundle expansion applied at write time.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
subscriber_id | bigint(20) unsigned | FK |
product_id | bigint(20) unsigned | The resolved product (direct line item OR bundle component) |
direct | tinyint(1) | 1 = bought it directly, 0 = via a bundle |
first_purchased_at / last_purchased_at | datetime |
Unique on (subscriber_id, product_id, direct).
sse_product_owners is a derived cache with bundle expansion applied at write time, so “owners of product X” segments resolve with a single indexed lookup rather than walking orders at send time. Rebuild it with SSE_Product_Owners::rebuild_all() after a bulk import or a data fix.
sse_confirmation_tokens
Double opt-in tokens. 7-day TTL.
| Column | Type | Description |
|---|---|---|
id | bigint(20) unsigned | Primary key |
token | varchar(64) | Unique |
subscriber_id | bigint(20) unsigned | FK |
list_ids | longtext | JSON array of list IDs being confirmed |
form_id | bigint(20) unsigned | nullable |
expires_at | datetime | |
consumed_at | datetime | nullable |
created_at | datetime |
PHP Classes
Phase 1
SSE_Lists
SSE_Lists::create( array $data ): int|WP_Error
SSE_Lists::get( int $id ): object|null
SSE_Lists::get_all( array $args ): array
SSE_Lists::count( array $args ): int
SSE_Lists::update( int $id, array $data ): bool|WP_Error
SSE_Lists::delete( int $id ): bool
SSE_Lists::get_subscriber_count( int $id ): int
SSE_Subscribers
SSE_Subscribers::create( array $data ): int|WP_Error
SSE_Subscribers::get( int $id ): object|null
SSE_Subscribers::get_by_email( string $email ): object|null
SSE_Subscribers::get_by_cookie( string $cookie_id ): object|null
SSE_Subscribers::get_by_user_id( int $user_id ): object|null
SSE_Subscribers::update( int $id, array $data ): bool
SSE_Subscribers::delete( int $id ): bool
SSE_Subscribers::get_all( array $args ): array
SSE_Subscribers::count( array $args ): int
SSE_Subscribers::subscribe_to_list( int $subscriber_id, int $list_id, string $status = 'subscribed' ): bool
SSE_Subscribers::unsubscribe_from_list( int $subscriber_id, int $list_id ): bool
SSE_Subscribers::get_lists( int $subscriber_id ): array
SSE_Subscribers::export_csv( array $args ): void // Streams CSV
SSE_Subscribers::total_count(): int
SSE_Forms
SSE_Forms::create( array $data ): int|WP_Error
SSE_Forms::get( int $id ): object|null // JSON fields auto-decoded
SSE_Forms::get_all( array $args ): array
SSE_Forms::count( array $args ): int
SSE_Forms::update( int $id, array $data ): bool
SSE_Forms::delete( int $id ): bool
SSE_Forms::render( int $id, string $context = 'inline' ): string
SSE_Forms::get_popups_for_page( int $page_id, string $url ): array
Phase 2 – sending pipeline
sse_send() (global function)
The public send API. Used by ssp-product-renewals and any other plugin that wants SSE to deliver one message.
sse_send( array $args ): int|WP_Error
$args keys:
| Key | Type | Description |
|---|---|---|
email (or subscriber_email) | string | Recipient email (required) |
category | string | One of: newsletter / list_broadcast / product_marketing / product_support / onboarding / transactional. Default: transactional. |
subject | string | Subject line (required) |
body_text | string | Plain-text body (required) |
body_html | string | Optional HTML body |
from_name / from_email / reply_to | string | Optional per-message overrides |
subscriber_id | int | Optional; resolves automatically from email if omitted |
campaign_id / automation_id | int | Optional FKs for log row |
gate_context | array | Per-call hints for the suppression gate (e.g. { category_term_id: 12 } for newsletter sends) |
context | array | Snapshot data persisted to sse_email_log.context |
source_plugin / source_reference | string | For third-party integrations |
priority | int | Queue priority (lower = sooner). Default 10 for marketing, 1 for transactional. |
Returns: the new sse_email_log row ID, or WP_Error (invalid email, blocked by suppression gate, queue insert failure).
A return of WP_Error( 'sse_suppressed', ... ) is normal – it means the recipient is suppressed for that category and the send was correctly skipped.
Third-party plugins should send through sse_send(), never wp_mail() directly. Routing through sse_send() gets you the suppression gate, the per-category From address, queueing with retry, and the full audit log for free. Treat a WP_Error( 'sse_suppressed', ... ) as an expected outcome, not a failure.
SSE_Suppressions
SSE_Suppressions::add( string $email, string $scope = 'global', string $reason = '', string $source = '' ): int|false
SSE_Suppressions::remove( string $email, string $scope = 'global' ): bool
SSE_Suppressions::is_suppressed( string $email, string $scope ): bool
SSE_Suppressions::is_globally_suppressed( string $email ): bool
SSE_Suppressions::get_scopes_for_email( string $email ): string[]
SSE_Suppressions::get_all( array $args ): array
SSE_Suppressions::count( array $args ): int
Constants: REASON_USER_UNSUBSCRIBE, REASON_COMPLAINT, REASON_BOUNCE_HARD, REASON_BOUNCE_SOFT_REPEAT, REASON_MANUAL, REASON_MIGRATED.
SSE_Suppression_Gate
SSE_Suppression_Gate::check( string $email, string $category, array $context = array() ): array
// Returns: [ 'allowed' => bool, 'reason' => string|null ]
Encapsulates the category × suppression matrix. Every send goes through this.
SSE_Email_Log
SSE_Email_Log::create( array $data ): int // Always reads test_mode from option
SSE_Email_Log::get( int $id ): object|null
SSE_Email_Log::record_event( int $log_id, string $event, string|null $occurred_at = null ): bool
SSE_Email_Log::find_by_mailgun_id( string $message_id ): object|null
SSE_Email_Log::for_subscriber( int $subscriber_id, array $args = array() ): array
SSE_Email_Log::campaign_stats( int $campaign_id ): array
SSE_Email_Queue
SSE_Email_Queue::init(): void // Registers cron schedule + drain hook
SSE_Email_Queue::enqueue( int $log_id, int $priority = 10 ): int|false
SSE_Email_Queue::drain( int|null $batch_size = null ): int // Returns messages sent
SSE_Email_Queue::pending_count(): int
SSE_Sender
SSE_Sender::send( array $args ): bool|WP_Error
// Internal - wraps wp_mail with category-aware From + Mailgun header injection.
// You don't call this directly; sse_send() / the drainer do.
SSE_Campaigns
SSE_Campaigns::create( array $data ): int|WP_Error
SSE_Campaigns::get( int $id ): object|null
SSE_Campaigns::get_all( array $args ): array
SSE_Campaigns::update( int $id, array $data ): bool|WP_Error
SSE_Campaigns::delete( int $id ): bool
SSE_Campaigns::send( int $id ): array|WP_Error // Refuses unless status in draft/scheduled
SSE_Campaigns::send_test( int $id, string $to_email ): int|WP_Error
SSE_Campaigns::reset_to_draft( int $id ): bool
SSE_Campaigns::resolve_audience( object $campaign ): int[] // Subscriber IDs
Status constants: STATUS_DRAFT, STATUS_SCHEDULED, STATUS_SENDING, STATUS_SENT, STATUS_CANCELLED.
SSE_Confirmations
SSE_Confirmations::issue( int $subscriber_id, int[] $list_ids, int|null $form_id = null ): string // Token
SSE_Confirmations::consume( string $token ): object|WP_Error // Returns the subscriber row
SSE_Confirmations::send_confirmation_email( int $subscriber_id, string $token ): int|WP_Error
SSE_Newsletter
SSE_Newsletter::init(): void // Registers transition_post_status + hourly tick
SSE_Newsletter::enabled_terms(): int[] // Allowed top-level term_ids
SSE_Newsletter::dispatch_instant( int $post_id ): int // Recipients queued
SSE_Newsletter::run_digest( int|null $now_gmt = null ): int // Recipients queued
SSE_Newsletter::resolve_top_level_terms( int $post_id ): int[]
SSE_Newsletter::is_schedule_match( int $now_gmt = null ): bool
SSE_Bundles
SSE_Bundles::components_of( int $product_id ): int[] // [] if not a bundle
SSE_Bundles::is_available(): bool // True if WC Product Bundles tables present
SSE_Product_Owners
SSE_Product_Owners::init(): void // Registers WC hooks
SSE_Product_Owners::on_status_changed( int $order_id, string $old, string $new, $order = null ): void
SSE_Product_Owners::rebuild_for_order( int $order_id ): void
SSE_Product_Owners::rebuild_all( int $days = 0 ): int // Backfill (used by migration)
SSE_Product_Owners::subscriber_ids_owning( int $product_id, bool $include_bundles = true ): int[]
Constant: PAID_STATUSES = [ 'completed', 'processing' ].
SSE_Segment
SSE_Segment::resolve( array $query, string $category = 'list_broadcast' ): int[] // Wraps with suppression mask
SSE_Segment::resolve_raw( array $query ): int[] // No mask - for testing
See Product Audiences & Segments for the predicate language.
SSE_Automations
SSE_Automations::create( array $data ): int|WP_Error
SSE_Automations::get( int $id ): object|null // trigger_config auto-decoded
SSE_Automations::get_all( array $args = array() ): array
SSE_Automations::update( int $id, array $data ): bool
SSE_Automations::delete( int $id ): bool
SSE_Automations::add_step( int $automation_id, array $data ): int
SSE_Automations::update_step( int $step_id, array $data ): bool
SSE_Automations::delete_step( int $step_id ): bool
SSE_Automations::get_steps( int $automation_id ): array
SSE_Automations::count_steps( int $automation_id ): int
SSE_Automation_Scheduler
SSE_Automation_Scheduler::init(): void // Registers WC hooks + tick
SSE_Automation_Scheduler::enroll( int $automation_id, int $subscriber_id, array $context = [] ): int|WP_Error // Idempotent on (auto, sub, context_hash)
SSE_Automation_Scheduler::tick(): int // Number of steps fired
SSE_Automation_Scheduler::on_wc_order_status_changed( int $order_id, string $old, string $new, $order = null ): void
SSE_Automation_Scheduler::on_wc_order_refunded( int $order_id ): void
SSE_Automation_Scheduler::match_wc_product_purchased( object $automation, array $purchase_data ): bool
SSE_Automation_Scheduler::registered_triggers(): array // Filterable via 'sse_automation_triggers'
SSE_Mailgun_Webhook
SSE_Mailgun_Webhook::handle( WP_REST_Request $request ): WP_REST_Response
// Registered at /wp-json/sse/v1/mailgun-webhook. HMAC-verified.
SSE_Migrate (Phase 2F)
SSE_Migrate::apply_subscriber( array $rec, bool $commit = false ): array
SSE_Migrate::import_wc_customers( int $days = 0, bool $commit = false ): array
Action constants: ACTION_ACTIVE, ACTION_PENDING, ACTION_UNSUBSCRIBED, ACTION_BOUNCED, ACTION_COMPLAINED.
SSE_Migrate_MailPoet::is_installed(): bool
SSE_Migrate_MailPoet::build_list_map( bool $commit = false, array $existing_map = [] ): array
SSE_Migrate_MailPoet::run( bool $commit = false ): array // Report
SSE_Migrate_MailerLite::parse_csv( string $path ): array
SSE_Migrate_MailerLite::run( string $path, bool $commit = false ): array // Report
Filters & Actions
apply_filters( 'sse_automation_triggers', array $triggers ): array
Register custom triggers for SSE automations. Used by ssp-product-renewals (and anyone else).
add_filter( 'sse_automation_triggers', function( $triggers ) {
$triggers['renewal_due_t30'] = array(
'label' => 'Renewal due in 30 days',
'matcher' => 'my_plugin_match_renewal_t30', // callable
);
return $triggers;
} );
Your matcher is called by the scheduler when external code invokes SSE_Automation_Scheduler::enroll() for that trigger type. See class-sse-automation-scheduler.php for the calling convention.
AJAX Endpoints
sse_subscribe
Form submissions. Registered for both logged-in and anonymous users.
POST: sse_form_id, sse_email, sse_first_name, sse_last_name, sse_nonce, sse_hp (honeypot)
Response: { success, data }
sse_update_subscriptions
Subscription preference updates from the management page. Also handles newsletter category prefs + frequency.
POST: nonce, list_ids[], newsletter_terms[], digest_frequency
Response: { success, data: { message } }
REST API
GET /wp-json/sse/v1/forms
Returns active forms. Used by the Gutenberg block editor.
Permission: edit_posts Response: [ { id, name }, ... ]
GET /wp-json/sse/v1/confirm?token=...
Double opt-in confirmation landing. Consumes the token, flips subscriber to active.
Permission: public
POST /wp-json/sse/v1/mailgun-webhook
MailGun event ingestion endpoint. HMAC-verified using sse_mailgun_webhook_key.
Permission: public (signature is the security)
WP-CLI Commands
wp sse migrate mailpoet [--dry-run|--commit]
wp sse migrate mailerlite --file=<path> [--dry-run|--commit]
wp sse migrate wc-customers [--days=<n>] [--dry-run|--commit]
wp sse migrate report
Default mode is --dry-run. --commit writes. Re-runs are idempotent.
See Email Migration Guide for usage.
Cron Schedules
Registered by SSE_Activator::schedule_cron() on activation:
| Hook | Interval | Class |
|---|---|---|
sse_drain_email_queue | every minute (sse_every_minute) | SSE_Email_Queue::drain() |
sse_newsletter_digest_tick | hourly | SSE_Newsletter::run_digest_if_due() |
sse_automation_tick | every minute | SSE_Automation_Scheduler::tick() |
Custom interval sse_every_minute is added via the cron_schedules filter in SSE_Email_Queue::init().
Constants
Defined in super-speedy-emails.php:
| Constant | Value |
|---|---|
SSE_VERSION | 1.04 (read from plugin header) |
SSE_PLUGIN_DIR | Absolute path to plugin directory |
SSE_PLUGIN_URL | URL to plugin directory |
SSE_PLUGIN_FILE | Path to main plugin file |
SSE_PLUGIN_BASENAME | Plugin basename for hooks |
Options
Phase 1
| Option | Default | Description |
|---|---|---|
sse_db_version | 2.5.0 | Database schema version |
sse_from_name | Site name | Global sender name |
sse_from_email | Admin email | Global sender email |
sse_manage_page_id | 0 | ID of the subscription management page |
sse_wc_myaccount_enabled | false | WooCommerce My Account tab toggle |
sse_wc_endpoint_slug | email-preferences | WooCommerce endpoint slug |
sse_cookie_lifetime | 365 | Cookie lifetime (days) |
sse_delete_data_on_uninstall | false | Delete all data on uninstall |
Phase 2 – sending
| Option | Default | Description |
|---|---|---|
sse_test_mode | live | live / log_only / redirect |
sse_test_redirect_email | empty | Destination for redirect mode |
sse_mailgun_webhook_key | empty | MailGun webhook signing key |
sse_from_name__<category> | empty | Per-category sender name override |
sse_from_email__<category> | empty | Per-category sender email override |
sse_reply_to__<category> | empty | Per-category Reply-To override |
sse_newsletter_enabled_terms | [] | JSON array of term_ids enabled for the newsletter |
sse_newsletter_schedule | [{day:tue,hour:12},{day:sat,hour:12}] | Digest schedule (GMT) |
sse_newsletter_digest_intro | empty | Optional intro text above the post list |
sse_newsletter_last_run_at | 0 | Unix timestamp of last digest run |
sse_migrate_mailpoet_prefix | empty | Override MailPoet table prefix (testing) |
sse_support_emails_paused | 1 | Scheduled Support Email sending is paused. Defaults to paused; the manual “Send now” button is unaffected |
Support Emails (product-update / changelog engine)
A Support Email is a record in sse_support_rules. The engine fans out over its product checklist (one email per product to that product’s owners), applying a per-day guard, the suppression gate and recency rules. Policy comes from the record’s type, which a plugin registers in code. Full guide: Automated Support Emails – Developer Guide.
Table sse_support_rules: id, name, status (draft/active/paused), type (registered type key), uid, product_ids (JSON – the fan-out checklist), match_mode (all/any), conditions (JSON recency rules), subject, body, stages (JSON – the authored reminder sequence), settings (JSON – the type’s own per-record options), footer, freq_cap_max, freq_cap_days, stage_reset_days, last_run_at, created_at, updated_at.
Registering a type:
sse_register_support_email_type( 'my-type', array(
'label' => 'My email type',
'mode' => 'per_product', // or 'per_user'
'stages' => 1, // N for an escalating reminder sequence
'stage_reset_days' => 0,
'placeholders' => array( '{token}' => 'What it is' ),
'freq_cap' => array( 'max' => 0, 'days' => 0 ),
'footer' => "You're receiving this because you own {product_name}.",
'admin_fields' => array( /* per-record options - see below */ ),
'callbacks' => array( /* below */ ),
) );
Register on the sse_register_support_email_types action (fired on init, priority 5) or on plugins_loaded.
Callbacks (each gets ($value, SSE_Support_Context $context), except the actions which get ($context)):
| Callback | Kind | Runs | Default |
|---|---|---|---|
should_send | filter | Once per product, before owners load | true |
content | filter | Per product, then per recipient. Returns { body, name } and optionally subject | passthrough |
should_send_to_user | filter | Once per owner | true |
on_sent | action | Per queued email, real sends only | – |
on_complete | action | Once per product, after its fan-out finished | – |
Advance a per-product baseline in on_complete, not on_sent: on_sent fires on recipient one of the run, so a run that dies partway strands everyone it never reached until the next baseline change.
admin_fields – per-record options the type declares, rendered in the editor, stored on the record and resolved onto $context->settings:
'admin_fields' => array(
'audience_scope' => array(
'type' => 'select', // select | text | number | checkbox
'label' => 'Send to',
'description' => 'Who this reaches.',
'default' => 'all',
'options' => array(
array( 'value' => 'all', 'label' => 'All owners' ),
array( 'value' => 'active_licence', 'label' => 'Active licence only' ),
),
),
),
Read with $context->setting( 'audience_scope', 'all' ). Values are validated against the declaration on every write path (editor, config import, MCP): undeclared keys are discarded and an out-of-range value falls back to the declared default. When the type is not registered, stored settings are left untouched rather than wiped.
Classes:
SSE_Support_Types– the type registry:register(),get(),all(),fields(),callback(),record_type(),sanitize_settings(),resolve_settings().SSE_Support_Rules– CRUD for the records +passes_audience($record,$context)(recency rules) +describe().SSE_Support_Emails– the engine:plan_support_email($record,$is_preview,$only_product),run_support_email($record,$only_product),preview_content($record,$product_id),daily_check()(cron),is_paused()/set_paused(), AJAXajax_preview()/ajax_preview_content()/ajax_send().SSE_Support_Context– the object passed to every callback (support_email_id,product_id,subscriber,matching_product,matching_products[],all_products[],settings,is_preview,name,rule; helpersis_product_mode(),setting(),owner_meta(),owns(),bought_within(),owned_row(),wc_customer()).
Ownership rows carry owner_id and meta – whatever any plugin attached to that purchase with SSE_Product_Owners::set_owner_meta() – batch-loaded once per fan-out.
Sending is blocked when a record’s type is not registered, or when the type’s own declared placeholders are still present in the finished subject or body. The reason is reported in the preview.
Repeat behaviour is the per-day guard (sse_email_log.source_reference = support:<record_id>:<product_id>), or the record’s frequency cap when one is set. Changelog behaviour lives in the separate ssp-changelog-emails companion plugin.
AJAX actions: sse_support_email_preview (per-product reach from the unsaved editor form), sse_support_email_preview_content (the rendered email for a real recipient), sse_support_email_send (send one product of a saved record).
CSS Classes
Frontend form classes for custom styling – unchanged from Phase 1.
| Class | Element |
|---|---|
.sse-form | Form wrapper |
.sse-form--minimal / --clean / --branded | Preset modifier |
.sse-form--loading | Applied during AJAX submission |
.sse-form--success | Applied after successful submission |
.sse-form__inner | The <form> element |
.sse-form__fields | Fields container (flexbox) |
.sse-form__field | Individual field wrapper |
.sse-form__input | Input elements |
.sse-form__input.sse-has-error | Input with validation error |
.sse-form__submit | Submit button |
.sse-form__message | Error/info message area |
.sse-form__success | Success message (hidden until success) |
[data-form-id="X"] | Target a specific form by ID |
Enqueued Assets
| Handle | Type | When loaded |
|---|---|---|
sse-forms | CSS + JS | When a form is rendered |
sse-popup | CSS + JS | When a popup form matches the current page |
sse-admin | CSS + JS | Admin pages with sse- in the hook |
Extending: a worked example
A third-party plugin (e.g. ssp-product-renewals) that wants to send a renewal email through SSE:
// In your plugin:
$result = sse_send( array(
'email' => $customer_email,
'category' => 'product_marketing',
'subject' => 'Your Scalability Pro renewal is due',
'body_text' => $rendered_body,
'subscriber_id' => $sub_id,
'context' => array(
'order_id' => $order_id,
'product_id' => $product_id,
'renewal_due_at' => $due_at,
),
'source_plugin' => 'ssp-product-renewals',
'source_reference' => "renewal:order:{$order_id}:t-{$days_until}",
) );
if ( is_wp_error( $result ) ) {
if ( $result->get_error_code() === 'sse_suppressed' ) {
// Recipient is suppressed for this category - that's a normal outcome.
// Log it for your own audit if needed, but don't treat as a failure.
} else {
// Genuine error - invalid email, DB failure, etc.
error_log( 'SSE send failed: ' . $result->get_error_message() );
}
}
// $result is the sse_email_log row ID on success.
SSE handles the gate check, queue insert, From: address per category, audit logging, and delivery via the cron drainer. The other plugin doesn’t need to know about MailGun, suppressions, or queues – just compose and call.
Further reading
wp sse command group
Comments