diff --git a/inc/Abilities/Email/EmailAbilities.php b/inc/Abilities/Email/EmailAbilities.php index 3cb952d89..2802ae05c 100644 --- a/inc/Abilities/Email/EmailAbilities.php +++ b/inc/Abilities/Email/EmailAbilities.php @@ -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 ), @@ -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 ); + } } diff --git a/inc/Abilities/Flow/FlowHelpers.php b/inc/Abilities/Flow/FlowHelpers.php index b06fa871a..7e8e447b0 100644 --- a/inc/Abilities/Flow/FlowHelpers.php +++ b/inc/Abilities/Flow/FlowHelpers.php @@ -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'] ) {