Skip to content
Merged
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
67 changes: 67 additions & 0 deletions inc/Abilities/Email/EmailAbilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,10 @@ public function executeReply( array $input ): array {
$sent = wp_mail( $to, $input['subject'], $input['body'], $headers );

if ( $sent ) {
// Save a copy to the IMAP Sent folder so the message appears in
// the user's email client (e.g. Gmail "Sent Mail" thread view).
$this->saveToSentFolder( $to, $input['subject'], $input['body'], $headers );

return array(
'success' => true,
'message' => 'Reply sent to ' . implode( ', ', $to ),
Expand Down Expand Up @@ -1287,4 +1291,67 @@ private function buildMailboxString( string $host, int $port, string $encryption

return sprintf( '{%s:%d%s}%s', $host, $port, $flags, $folder );
}

/**
* Save a sent message to the IMAP Sent folder.
*
* After wp_mail() sends via SMTP, the message only exists on the recipient's
* server. This appends a copy to the sender's Sent folder so it appears in
* their email client (e.g. Gmail thread view).
*
* @param array $to Recipient addresses.
* @param string $subject Email subject.
* @param string $body Email body.
* @param array $headers Email headers (Content-Type, In-Reply-To, References, etc.).
*/
private function saveToSentFolder( array $to, string $subject, string $body, array $headers ): void {
// Determine the Sent folder name. Gmail uses "[Gmail]/Sent Mail".
$sent_folder = '[Gmail]/Sent Mail';

$connection = $this->connect( $sent_folder );
if ( is_array( $connection ) && ! ( $connection['success'] ?? true ) ) {
// Non-Gmail server or folder not found — try common alternatives.
foreach ( array( 'Sent', 'Sent Items', 'INBOX.Sent' ) as $fallback ) {
$connection = $this->connect( $fallback );
if ( ! is_array( $connection ) ) {
$sent_folder = $fallback;
break;
}
}

// If still no connection, silently skip — sending succeeded, saving is best-effort.
if ( is_array( $connection ) ) {
return;
}
}

$auth = $this->getAuthProvider();
$from = $auth ? $auth->getUser() : 'noreply@extrachill.com';
$to_str = implode( ', ', $to );
$date = gmdate( 'r' );

// Build the RFC822 message.
$message = "From: {$from}\r\n";
$message .= "To: {$to_str}\r\n";
$message .= "Subject: {$subject}\r\n";
$message .= "Date: {$date}\r\n";

foreach ( $headers as $header ) {
$message .= $header . "\r\n";
}

$message .= "MIME-Version: 1.0\r\n";
$message .= "\r\n";
$message .= $body;

// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@imap_append( $connection, $this->buildMailboxString(
$auth->getHost(),
$auth->getPort(),
$auth->getEncryption(),
$sent_folder
), $message, '\\Seen' );

imap_close( $connection );
}
}
48 changes: 44 additions & 4 deletions inc/Abilities/Flow/FlowHelpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -517,18 +517,58 @@ protected function applyStepConfigsToFlow( int $flow_id, array $step_configs ):
continue;
}

// Normalize plural forms to the singular keys that UpdateFlowStepAbility expects.
// CLI and admin UI naturally pass handler_slugs (array) and handler_configs (keyed object),
// but the ability expects handler_slug (string) and handler_config (object).
$handler_slugs = $config['handler_slugs'] ?? array();
$handler_configs = $config['handler_configs'] ?? array();

// If singular forms are provided, use those directly (backward compat with --handler-config path).
$single_slug = $config['handler_slug'] ?? '';
$single_config = $config['handler_config'] ?? array();

// When handler_slugs is provided, add each handler with its config from handler_configs.
if ( ! empty( $handler_slugs ) ) {
foreach ( $handler_slugs as $slug ) {
$slug_config = $handler_configs[ $slug ] ?? array();
$add_result = $flow_step_abilities->executeUpdateFlowStep(
array(
'flow_step_id' => $flow_step_id,
'add_handler' => $slug,
'add_handler_config' => $slug_config,
)
);
if ( ! $add_result['success'] ) {
$errors[] = array(
'step_type' => $step_type,
'flow_step_id' => $flow_step_id,
'handler' => $slug,
'error' => $add_result['error'] ?? 'Failed to add handler',
);
}
}
}

// Build the base update input for singular handler_slug / handler_config / user_message.
$update_input = array( 'flow_step_id' => $flow_step_id );

if ( ! empty( $config['handler_slug'] ) ) {
$update_input['handler_slug'] = $config['handler_slug'];
if ( ! empty( $single_slug ) ) {
$update_input['handler_slug'] = $single_slug;
}
if ( ! empty( $config['handler_config'] ) ) {
$update_input['handler_config'] = $config['handler_config'];
if ( ! empty( $single_config ) ) {
$update_input['handler_config'] = $single_config;
}
if ( ! empty( $config['user_message'] ) ) {
$update_input['user_message'] = $config['user_message'];
}

// Only call update if there's something beyond the flow_step_id to apply.
if ( count( $update_input ) <= 1 && ! empty( $handler_slugs ) ) {
// Already handled via add_handler above — mark as applied.
$applied[] = $flow_step_id;
continue;
}

$result = $flow_step_abilities->executeUpdateFlowStep( $update_input );

if ( $result['success'] ) {
Expand Down
Loading