Two integration surfaces. This guide covers the in-process PHP API — WordPress hooks and PHP service classes for code running inside the same WordPress install. If you instead need to drive the plugin over HTTP from an external system (for example to schedule newsletters from another application), see the JWT-authenticated REST API. For an overview of how the layers fit together, see architecture.md.
Overview#
The Mail System provides a hook-based architecture that allows third-party plugins to:
- Register external lists - Add automated/dynamic subscriber lists
- Register external subscribers - Add subscribers from external sources (CRM, WooCommerce, etc.)
- Control editability - Lock specific lists or subscribers from being modified
- Extend email functionality - Modify email content, add custom placeholders
- Manage email templates - Create, edit, and use reusable email templates
Key Concepts#
| Concept | Description |
|---|---|
| Database entities | Lists and subscribers stored in mskd_* tables |
| External entities | Lists and subscribers provided dynamically via hooks |
| External ID prefix | All external IDs are prefixed with ext_ automatically |
| Provider | Name of the plugin/source providing external entities |
Quick Start#
Register an External List#
add_filter( 'mskd_register_external_lists', 'my_plugin_register_lists' );
function my_plugin_register_lists( $lists ) {
$lists[] = array(
'id' => 'my_plugin_vip_users',
'name' => 'VIP Users',
'description' => 'Users with VIP membership',
'provider' => 'My Plugin',
'subscriber_callback' => 'my_plugin_get_vip_subscribers',
);
return $lists;
}
function my_plugin_get_vip_subscribers() {
// Return array of subscriber arrays
// 'email' is required, 'first_name' and 'last_name' are optional
return array(
array(
'email' => 'john@example.com',
'first_name' => 'John',
'last_name' => 'Doe',
),
array(
'email' => 'jane@example.com',
'first_name' => 'Jane',
'last_name' => 'Smith',
),
array(
'email' => 'user@example.com', // Minimal - only email
),
);
}
Register External Subscribers#
add_filter( 'mskd_register_external_subscribers', 'my_plugin_register_subscribers', 10, 2 );
function my_plugin_register_subscribers( $subscribers, $args ) {
$subscribers[] = array(
'id' => 'crm_contact_123',
'email' => 'john@example.com',
'first_name' => 'John',
'last_name' => 'Doe',
'status' => 'active',
'provider' => 'My CRM Plugin',
);
return $subscribers;
}
Hooks Reference#
Lists Hooks#
mskd_register_external_lists#
Register external/automated subscriber lists that appear alongside database lists.
| Property | Type | Required | Description |
|---|---|---|---|
id |
string|int | Yes | Unique identifier (auto-prefixed with ext_) |
name |
string | Yes | Display name in admin interface |
description |
string | No | Description shown in lists table |
provider |
string | No | Plugin/provider name. Default: "External" |
subscriber_callback |
callable | No | Returns array of subscriber arrays (see format below) |
Example:
add_filter( 'mskd_register_external_lists', function( $lists ) {
$lists[] = array(
'id' => 'recent_buyers',
'name' => 'Recent Buyers',
'description' => 'Customers who purchased in the last 30 days',
'provider' => 'WooCommerce',
'subscriber_callback' => 'get_recent_buyer_emails',
);
return $lists;
});
mskd_list_is_editable#
Control whether a database list can be edited. External lists are always non-editable.
Parameters:
$is_editable(bool) - Current editable status$list_id(int) - The list ID
Example:
add_filter( 'mskd_list_is_editable', function( $is_editable, $list_id ) {
// Lock system lists
$locked = array( 1, 2, 3 );
return in_array( $list_id, $locked, true ) ? false : $is_editable;
}, 10, 2 );
Subscribers Hooks#
mskd_register_external_subscribers#
Register external subscribers that appear in the Subscribers admin page.
| Property | Type | Required | Description |
|---|---|---|---|
id |
string|int | Yes | Unique identifier (auto-prefixed with ext_) |
email |
string | Yes | Valid email address |
first_name |
string | No | Subscriber's first name |
last_name |
string | No | Subscriber's last name |
status |
string | No | active, inactive, or unsubscribed |
provider |
string | No | Plugin/provider name |
lists |
array | No | Array of list IDs subscriber belongs to |
Parameters:
$subscribers(array) - Current external subscribers$args(array) - Query arguments (status filter, pagination)
Example:
add_filter( 'mskd_register_external_subscribers', function( $subscribers, $args ) {
// Respect status filter
$my_subs = get_my_external_subscribers();
if ( ! empty( $args['status'] ) ) {
$my_subs = array_filter( $my_subs, function( $sub ) use ( $args ) {
return $sub['status'] === $args['status'];
});
}
return array_merge( $subscribers, $my_subs );
}, 10, 2 );
mskd_subscriber_is_editable#
Control whether a database subscriber can be edited.
Parameters:
$is_editable(bool) - Current editable status$subscriber_id(int) - The subscriber ID
Email Hooks#
mskd_process_queue#
Action hook triggered by WP-Cron to process the email queue.
mskd_subscriber_unsubscribed#
Fired when a subscriber unsubscribes.
Parameters:
$email(string) - Subscriber's email$token(string) - Unsubscribe token
Example:
add_action( 'mskd_subscriber_unsubscribed', function( $email, $token ) {
// Sync unsubscribe with external system
if ( strpos( $token, 'ext_' ) === 0 ) {
my_crm_mark_unsubscribed( $email );
}
}, 10, 2 );
Services API#
MSKD_List_Provider#
Static class for list and subscriber management.
require_once MSKD_PLUGIN_DIR . 'includes/services/class-mskd-list-provider.php';
List Methods#
| Method | Description | Returns |
|---|---|---|
get_all_lists() |
Get all lists (database + external) | array |
get_database_lists() |
Get only database lists | array |
get_external_lists() |
Get external lists from filter | array |
get_list( $list_id ) |
Get a single list by ID | object|null |
is_list_editable( $list_id ) |
Check if list can be edited | bool |
list_exists( $list_id ) |
Check if list exists | bool |
get_list_subscriber_count( $list ) |
Get total subscriber count | int |
get_list_active_subscriber_count( $list ) |
Get active subscriber count | int |
get_list_subscriber_ids( $list ) |
Get active subscriber IDs | array |
get_list_subscribers_full( $list ) |
Get full subscriber objects | array |
Subscriber Methods#
| Method | Description | Returns |
|---|---|---|
get_all_subscribers( $args ) |
Get all subscribers | array |
get_database_subscribers( $args ) |
Get database subscribers | array |
get_external_subscribers( $args ) |
Get external subscribers | array |
get_subscriber( $id ) |
Get subscriber by ID | object|null |
is_external_id( $id ) |
Check if ID is external (ext_*) |
bool |
is_subscriber_editable( $id ) |
Check if subscriber can be edited | bool |
get_total_subscriber_count( $status ) |
Get total count by status | int |
Utility Methods#
| Method | Description |
|---|---|
invalidate_cache() |
Clear external lists cache |
Usage Example:
// Get all lists
$all_lists = MSKD_List_Provider::get_all_lists();
// Get external list by ID
$list = MSKD_List_Provider::get_list( 'ext_my_plugin_users' );
// Get subscriber count
$count = MSKD_List_Provider::get_list_active_subscriber_count( $list );
// Get subscribers for a list
$subscriber_ids = MSKD_List_Provider::get_list_subscriber_ids( $list );
MSKD_SMTP_Mailer#
SMTP email sending service using PHPMailer.
require_once MSKD_PLUGIN_DIR . 'includes/services/class-smtp-mailer.php';
$mailer = new MSKD_SMTP_Mailer();
| Method | Description | Returns |
|---|---|---|
is_enabled() |
Check if SMTP is configured | bool |
send( $to, $subject, $body, $headers ) |
Send an email | bool |
test_connection() |
Test SMTP connection | array |
get_last_error() |
Get last error message | string |
get_debug_log() |
Get debug log array | array |
Template_Service#
Service for managing email templates.
use MSKD\Services\Template_Service;
$template_service = new Template_Service();
Methods#
| Method | Description | Returns |
|---|---|---|
get_all( $args ) |
Get all templates with optional filters | array |
get_by_id( $id ) |
Get template by ID | object|null |
get_by_name( $name ) |
Get template by name | object|null |
create( $data ) |
Create a new template | int|false |
update( $id, $data ) |
Update a template | bool |
delete( $id ) |
Delete a template | bool |
duplicate( $id ) |
Duplicate a template | int|false |
count( $type, $status ) |
Get template count | int |
get_predefined() |
Get all predefined templates | array |
get_custom() |
Get all custom templates | array |
get_active() |
Get all active templates | array |
install_defaults() |
Install default predefined templates | void |
Usage Example#
use MSKD\Services\Template_Service;
$template_service = new Template_Service();
// Create a custom template
$template_id = $template_service->create( array(
'name' => 'My Newsletter Template',
'subject' => 'Weekly Newsletter',
'content' => '<h1>Hello {first_name}!</h1><p>Your content here...</p>',
'type' => 'custom',
'status' => 'active',
) );
// Get a template by ID
$template = $template_service->get_by_id( $template_id );
// Update a template
$template_service->update( $template_id, array(
'subject' => 'Updated Weekly Newsletter',
) );
// Duplicate a template
$new_id = $template_service->duplicate( $template_id );
// Delete a template (only custom templates can be deleted)
$template_service->delete( $template_id );
Database Schema#
Tables#
All tables use the {$wpdb->prefix}mskd_ prefix.
mskd_subscribers#
| Column | Type | Description |
|---|---|---|
id |
bigint(20) | Primary key |
email |
varchar(255) | Unique email address |
first_name |
varchar(100) | First name |
last_name |
varchar(100) | Last name |
status |
enum | active, inactive, unsubscribed |
unsubscribe_token |
varchar(64) | Unique unsubscribe token |
created_at |
datetime | Creation timestamp |
updated_at |
datetime | Last update timestamp |
mskd_lists#
| Column | Type | Description |
|---|---|---|
id |
bigint(20) | Primary key |
name |
varchar(255) | List name |
description |
text | List description |
created_at |
datetime | Creation timestamp |
mskd_subscriber_list#
| Column | Type | Description |
|---|---|---|
id |
bigint(20) | Primary key |
subscriber_id |
bigint(20) | FK to subscribers |
list_id |
bigint(20) | FK to lists |
subscribed_at |
datetime | Subscription timestamp |
mskd_templates#
| Column | Type | Description |
|---|---|---|
id |
bigint(20) | Primary key |
name |
varchar(255) | Template name |
subject |
varchar(255) | Default email subject |
content |
longtext | HTML email content |
json_content |
longtext | JSON content for visual editor |
thumbnail |
varchar(500) | Thumbnail image URL |
type |
enum | predefined, custom |
status |
enum | active, inactive |
created_at |
datetime | Creation timestamp |
updated_at |
datetime | Last update timestamp |
mskd_queue#
| Column | Type | Description |
|---|---|---|
id |
bigint(20) | Primary key |
subscriber_id |
bigint(20) | FK to subscribers (0 for external) |
subscriber_data |
text | JSON data for external subscribers |
subject |
varchar(255) | Email subject |
body |
longtext | Email body (HTML) |
status |
enum | pending, processing, sent, failed |
scheduled_at |
datetime | Scheduled send time |
sent_at |
datetime | Actual send time |
attempts |
int | Retry attempt count |
error_message |
text | Last error message |
created_at |
datetime | Creation timestamp |
Email Templates#
The Mail System includes a comprehensive email templates feature that allows users to create reusable email templates.
Template Types#
| Type | Description |
|---|---|
| Predefined | Built-in templates provided by the system. Cannot be deleted but can be duplicated. |
| Custom | User-created templates. Can be edited, duplicated, and deleted. |
Default Templates#
The system includes the following predefined templates:
- Blank Template - An empty starting point
- Newsletter - Standard newsletter layout with header, content area, and footer
- Welcome Email - Template for welcoming new subscribers
- Promotional - Template for promotional offers with call-to-action buttons
Using Templates#
Templates can be used when composing a new email:
- Navigate to Emails > Templates
- Click "Use" on any template
- The compose form will be pre-filled with the template's subject and content
- Modify as needed and send
Template Placeholders#
All templates support the standard email placeholders:
| Placeholder | Description |
|---|---|
{first_name} |
Subscriber's first name |
{last_name} |
Subscriber's last name |
{email} |
Subscriber's email address |
{unsubscribe_link} |
HTML link to unsubscribe |
{unsubscribe_url} |
Raw unsubscribe URL |
Programmatic Template Usage#
use MSKD\Services\Template_Service;
$template_service = new Template_Service();
// Get a template
$template = $template_service->get_by_id( 1 );
// Use the template content
$subject = $template->subject;
$content = $template->content;
// Apply placeholders
$content = str_replace(
array( '{first_name}', '{last_name}', '{email}' ),
array( $subscriber->first_name, $subscriber->last_name, $subscriber->email ),
$content
);
Examples#
WooCommerce Integration#
Complete example of integrating WooCommerce customers:
<?php
/**
* Plugin Name: MSKD WooCommerce Integration
* Description: Adds WooCommerce customer lists to Mail System
*/
class MSKD_WooCommerce_Integration {
public function __construct() {
add_filter( 'mskd_register_external_lists', array( $this, 'register_lists' ) );
add_filter( 'mskd_register_external_subscribers', array( $this, 'register_subscribers' ), 10, 2 );
}
public function register_lists( $lists ) {
if ( ! class_exists( 'WooCommerce' ) ) {
return $lists;
}
// Recent Customers
$lists[] = array(
'id' => 'wc_recent_customers',
'name' => 'Recent Customers (30 days)',
'description' => 'Customers with orders in the last 30 days',
'provider' => 'WooCommerce',
'subscriber_callback' => array( $this, 'get_recent_customers' ),
);
// VIP Customers
$lists[] = array(
'id' => 'wc_vip_customers',
'name' => 'VIP Customers',
'description' => 'Customers with total spend over $500',
'provider' => 'WooCommerce',
'subscriber_callback' => array( $this, 'get_vip_customers' ),
);
return $lists;
}
public function register_subscribers( $subscribers, $args ) {
if ( ! class_exists( 'WooCommerce' ) ) {
return $subscribers;
}
$customers = get_users( array( 'role' => 'customer', 'number' => 100 ) );
foreach ( $customers as $customer ) {
$sub = array(
'id' => 'wc_customer_' . $customer->ID,
'email' => $customer->user_email,
'first_name' => get_user_meta( $customer->ID, 'billing_first_name', true ),
'last_name' => get_user_meta( $customer->ID, 'billing_last_name', true ),
'status' => 'active',
'provider' => 'WooCommerce',
);
// Apply status filter if provided
if ( empty( $args['status'] ) || $sub['status'] === $args['status'] ) {
$subscribers[] = $sub;
}
}
return $subscribers;
}
public function get_recent_customers() {
global $wpdb;
return $wpdb->get_col( $wpdb->prepare(
"SELECT DISTINCT pm.meta_value
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE pm.meta_key = '_billing_email'
AND p.post_type = 'shop_order'
AND p.post_date > %s",
date( 'Y-m-d', strtotime( '-30 days' ) )
) );
}
public function get_vip_customers() {
global $wpdb;
if ( ! class_exists( 'WC_Customer' ) ) {
return array();
}
return $wpdb->get_col(
"SELECT email FROM {$wpdb->prefix}wc_customer_lookup
WHERE total_spend > 500"
);
}
}
new MSKD_WooCommerce_Integration();
CRM Integration with External Subscribers#
<?php
/**
* Plugin Name: MSKD CRM Integration
*/
add_filter( 'mskd_register_external_lists', function( $lists ) {
$lists[] = array(
'id' => 'crm_leads',
'name' => 'CRM Leads',
'description' => 'Active leads from CRM system',
'provider' => 'My CRM',
'subscriber_callback' => 'get_crm_leads',
);
return $lists;
});
function get_crm_leads() {
// Fetch from CRM API and return subscriber arrays
$leads = my_crm_api_get_leads();
return array_map( function( $lead ) {
return array(
'email' => $lead->email,
'first_name' => $lead->first_name,
'last_name' => $lead->last_name,
);
}, $leads );
}
Best Practices#
1. Use Unique Prefixes#
Prevent ID collisions by prefixing with your plugin slug:
// ✅ Good
'id' => 'myplugin_premium_users'
// ❌ Bad - may collide
'id' => 'premium_users'
2. Cache Expensive Queries#
function get_premium_subscribers() {
$cache_key = 'myplugin_premium_subs';
$cached = wp_cache_get( $cache_key, 'mskd_external' );
if ( false !== $cached ) {
return $cached;
}
global $wpdb;
$result = $wpdb->get_col( /* expensive query */ );
wp_cache_set( $cache_key, $result, 'mskd_external', HOUR_IN_SECONDS );
return $result;
}
3. Always Return Arrays#
function get_my_subscribers() {
$result = some_function();
return is_array( $result ) ? $result : array();
}
4. Respect Query Arguments#
add_filter( 'mskd_register_external_subscribers', function( $subscribers, $args ) {
$my_subs = get_my_subscribers();
// Apply status filter
if ( ! empty( $args['status'] ) ) {
$my_subs = array_filter( $my_subs, function( $sub ) use ( $args ) {
return $sub['status'] === $args['status'];
});
}
return array_merge( $subscribers, $my_subs );
}, 10, 2 );
5. Document Your Lists#
$lists[] = array(
'id' => 'wc_repeat_buyers',
'name' => 'Repeat Buyers',
'description' => 'Customers with 3+ orders. Updated hourly via cron.',
'provider' => 'WooCommerce Pro',
);
6. Handle Unsubscribes#
add_action( 'mskd_subscriber_unsubscribed', function( $email, $token ) {
// Sync with external system
my_crm_mark_unsubscribed( $email );
}, 10, 2 );
Troubleshooting#
List Not Appearing#
- Ensure your filter runs before admin pages load (
plugins_loadedor earlier) - Verify list has required
idandnameproperties - Check PHP error log for exceptions
Subscriber Count Shows 0#
- Verify callback returns array of arrays with
emailkey - Test callback directly:
var_dump( your_callback() ); - Check for PHP errors in the callback function
Emails Not Sending#
- Verify
subscriber_callbackis callable - Check queue table for error messages
- Ensure SMTP is configured in Settings
- Review
MSKD_Cron_Handler::MAX_ATTEMPTS(default: 3)
External Subscriber Can't Receive Emails#
- Ensure
subscriber_callbackreturns array of arrays withemailkey - Optionally include
first_nameandlast_namefor personalization - Data is stored as JSON in
subscriber_datacolumn
Constants#
| Constant | Default | Description |
|---|---|---|
MSKD_VERSION |
1.1.2 |
Plugin version |
MSKD_BATCH_SIZE |
10 |
Emails per cron run (configurable via settings) |
MSKD_PLUGIN_DIR |
— | Plugin directory path |
MSKD_PLUGIN_URL |
— | Plugin URL |
Support#
For questions, bug reports, or feature requests:
- Website: katsarov.design
- GitHub: Repository Issues