Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions data-machine.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ function () {
require_once __DIR__ . '/inc/Abilities/Fetch/QueryWordPressPostsAbility.php';
require_once __DIR__ . '/inc/Abilities/Publish/PublishWordPressAbility.php';
require_once __DIR__ . '/inc/Abilities/Publish/SendEmailAbility.php';
require_once __DIR__ . '/inc/Abilities/Publish/SendEmailQueuedAbility.php';
require_once __DIR__ . '/inc/Abilities/Update/UpdateWordPressAbility.php';
require_once __DIR__ . '/inc/Abilities/Handler/TestHandlerAbility.php';
// Register ability hooks immediately during plugins_loaded.
Expand Down Expand Up @@ -327,6 +328,7 @@ function () {
new \DataMachine\Abilities\Fetch\QueryWordPressPostsAbility();
new \DataMachine\Abilities\Publish\PublishWordPressAbility();
new \DataMachine\Abilities\Publish\SendEmailAbility();
new \DataMachine\Abilities\Publish\SendEmailQueuedAbility();
new \DataMachine\Abilities\Update\UpdateWordPressAbility();
new \DataMachine\Abilities\Handler\TestHandlerAbility();

Expand Down
93 changes: 88 additions & 5 deletions inc/Abilities/Publish/SendEmailAbility.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,14 @@ private function registerAbilities(): void {
'datamachine/send-email',
array(
'label' => __( 'Send Email', 'data-machine' ),
'description' => __( 'Send an email with optional attachments via wp_mail()', 'data-machine' ),
'description' => __( 'Send an email with optional attachments via wp_mail(). Body can be supplied directly or rendered from a registered template via the datamachine_email_templates filter.', 'data-machine' ),
'category' => 'datamachine-publishing',
'input_schema' => array(
'type' => 'object',
'required' => array( 'to', 'subject', 'body' ),
// Either `body` or `template` must be provided. Both branches
// are validated in execute(); the schema marks only `to` and
// `subject` as universally required.
'required' => array( 'to', 'subject' ),
'properties' => array(
'to' => array(
'type' => 'string',
Expand All @@ -60,11 +63,22 @@ private function registerAbilities(): void {
),
'subject' => array(
'type' => 'string',
'description' => __( 'Email subject line. Supports {month}, {year}, {site_name}, {date} placeholders.', 'data-machine' ),
'description' => __( 'Email subject line. Supports {month}, {year}, {site_name}, {date}, {admin_email} placeholders (replaced after template render).', 'data-machine' ),
),
'body' => array(
'type' => 'string',
'description' => __( 'Email body content (HTML or plain text)', 'data-machine' ),
'default' => '',
'description' => __( 'Email body content (HTML or plain text). Used verbatim when `template` is omitted. Supports {month}, {year}, {site_name}, {date}, {admin_email} placeholders (replaced after template render).', 'data-machine' ),
),
'template' => array(
'type' => 'string',
'default' => '',
'description' => __( 'Optional template id. Resolved via the datamachine_email_templates filter; the resolved callable receives the `context` array and returns the body. Placeholder replacement runs AFTER template render so templates may emit {site_name}, {date}, etc.', 'data-machine' ),
),
'context' => array(
'type' => 'object',
'default' => array(),
'description' => __( 'Opaque context object passed to the template renderer. Ignored when `template` is omitted.', 'data-machine' ),
),
'content_type' => array(
'type' => 'string',
Expand Down Expand Up @@ -158,6 +172,66 @@ public function execute( array $input ): array {
);
}

// 1b. Resolve template (if provided) → body. Runs BEFORE placeholder
// replacement so templates may themselves emit {site_name}, {date}, etc.
if ( '' !== $config['template'] ) {
$templates = apply_filters( 'datamachine_email_templates', array() );
if ( ! is_array( $templates ) || ! isset( $templates[ $config['template'] ] ) || ! is_callable( $templates[ $config['template'] ] ) ) {
$error = sprintf( 'Unknown email template: %s', $config['template'] );
$logs[] = array(
'level' => 'error',
'message' => 'Email: ' . $error,
'data' => array(
'template' => $config['template'],
'registered_templates' => is_array( $templates ) ? array_keys( $templates ) : array(),
),
);
return array(
'success' => false,
'error' => $error,
'logs' => $logs,
);
}

$rendered = call_user_func( $templates[ $config['template'] ], (array) $config['context'] );
if ( ! is_string( $rendered ) ) {
$error = sprintf( 'Email template renderer did not return a string: %s', $config['template'] );
$logs[] = array(
'level' => 'error',
'message' => 'Email: ' . $error,
'data' => array(
'template' => $config['template'],
'return_type' => gettype( $rendered ),
),
);
return array(
'success' => false,
'error' => $error,
'logs' => $logs,
);
}

$config['body'] = $rendered;
$logs[] = array(
'level' => 'debug',
'message' => 'Email: Template rendered',
'data' => array(
'template' => $config['template'],
'body_length' => strlen( $rendered ),
),
);
} elseif ( '' === (string) $config['body'] ) {
$logs[] = array(
'level' => 'error',
'message' => 'Email: No body provided and no template specified',
);
return array(
'success' => false,
'error' => 'Either `body` or `template` must be provided.',
'logs' => $logs,
);
}

// 2. Build headers.
$headers = array();

Expand Down Expand Up @@ -289,14 +363,23 @@ private function normalizeConfig( array $input ): array {
'bcc' => '',
'subject' => '',
'body' => '',
'template' => '',
'context' => array(),
'content_type' => 'text/html',
'from_name' => '',
'from_email' => '',
'reply_to' => '',
'attachments' => array(),
);

return array_merge( $defaults, $input );
$merged = array_merge( $defaults, $input );

// Normalize types defensively — REST/JSON callers may pass nulls.
$merged['template'] = is_string( $merged['template'] ) ? trim( $merged['template'] ) : '';
$merged['context'] = is_array( $merged['context'] ) ? $merged['context'] : array();
$merged['body'] = is_string( $merged['body'] ) ? $merged['body'] : '';

return $merged;
}

/**
Expand Down
Loading
Loading