From 1e8697a49f56b08cfb9f84a793280f0f9313aa8e Mon Sep 17 00:00:00 2001 From: Zachary Date: Wed, 21 Jan 2026 15:53:44 +0900 Subject: [PATCH 1/3] feat(email): Add reply-to header support for Bento emails (#13) Implement reply-to header parsing and forwarding for Bento transactional emails. Add new configuration option to enable or disable reply-to header support. Enhance email header parsing to extract headers from various input formats. --- .../class_bento_email_handler.php | 40 ++++++++++++++++++- tests/Unit/BentoEmailHandlerTest.php | 32 +++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/inc/events-controllers/class_bento_email_handler.php b/inc/events-controllers/class_bento_email_handler.php index 3e91eb2..326e083 100644 --- a/inc/events-controllers/class_bento_email_handler.php +++ b/inc/events-controllers/class_bento_email_handler.php @@ -179,6 +179,10 @@ private function send_via_bento($email_data) { $bento_secret_key = $options['bento_secret_key'] ?? ''; $from_email = $options['bento_from_email'] ?? get_option('admin_email'); $transactional_override = !empty($options['bento_transactional_override']) && $options['bento_transactional_override'] === '1'; + $reply_to_enabled = !isset($options['bento_enable_reply_to']) || $options['bento_enable_reply_to'] !== '0'; + + $parsed_headers = $this->parse_headers($email_data['headers'] ?? []); + $reply_to = $parsed_headers['Reply-To'] ?? ($parsed_headers['reply-to'] ?? null); Bento_Logger::log('Bento Email Handler: Sending via API'); @@ -208,6 +212,10 @@ private function send_via_bento($email_data) { ) ); + if ($reply_to_enabled && !empty($reply_to)) { + $body['emails'][0]['reply_to'] = $reply_to; + } + Bento_Logger::log('Request body: ' . print_r($body, true)); // Get plugin version @@ -249,4 +257,34 @@ private function send_via_bento($email_data) { return true; } -} \ No newline at end of file + + /** + * Parse email headers into a name/value array. + */ + private function parse_headers($headers) { + if (empty($headers)) { + return []; + } + + if (is_string($headers)) { + $headers = explode("\n", str_replace("\r\n", "\n", $headers)); + } + + if (!is_array($headers)) { + return []; + } + + $parsed = []; + + foreach ($headers as $header) { + if (strpos($header, ':') === false) { + continue; + } + + [$name, $value] = explode(':', $header, 2); + $parsed[trim($name)] = trim($value); + } + + return $parsed; + } +} diff --git a/tests/Unit/BentoEmailHandlerTest.php b/tests/Unit/BentoEmailHandlerTest.php index 749e73b..dfafe95 100644 --- a/tests/Unit/BentoEmailHandlerTest.php +++ b/tests/Unit/BentoEmailHandlerTest.php @@ -114,3 +114,35 @@ $queue = get_option('bento_email_queue', []); expect($queue)->toBe([]); }); + +test('Bento email handler forwards reply-to headers', function () { + update_option('bento_settings', [ + 'bento_enable_transactional' => '1', + 'bento_site_key' => 'site', + 'bento_publishable_key' => 'pub', + 'bento_secret_key' => 'sec', + 'bento_enable_reply_to' => '1', + ]); + + $handler = new Bento_Email_Handler(); + + $email = [ + 'to' => 'reply@example.com', + 'subject' => 'Subject', + 'message' => 'Body', + 'headers' => ['Reply-To: reply@example.com'], + ]; + + update_option('bento_email_queue', [[ + 'email_data' => $email, + 'timestamp' => time(), + 'hash' => md5('reply@example.com|Subject'), + ]]); + + $handler->process_email_queue(); + + global $__wp_test_state; + $payload = json_decode($__wp_test_state['remote_posts'][0]['args']['body'], true); + + expect($payload['emails'][0]['reply_to'])->toBe('reply@example.com'); +}); From 0702a19e49b9d6ea88a2867813ab99c5dc31b6fd Mon Sep 17 00:00:00 2001 From: Zachary Date: Wed, 21 Jan 2026 17:29:48 +0900 Subject: [PATCH 2/3] Feat reply to header support (#14) * feat(email): Add reply-to header support for Bento emails Implement reply-to header parsing and forwarding for Bento transactional emails. Add new configuration option to enable or disable reply-to header support. Enhance email header parsing to extract headers from various input formats. * feat(email): Improve email recipient and header parsing Enhance email handling by adding robust recipient normalization, email validation, and reply-to parsing. Implement stricter input validation and error handling to prevent potential issues with malformed email addresses and headers. Key improvements: - Add recipient normalization method - Validate email addresses - Improve reply-to header parsing - Add logging for invalid recipient scenarios - Ensure consistent email address formatting --- .../class_bento_email_handler.php | 114 +++++++++++++++++- tests/Unit/BentoEmailHandlerTest.php | 95 +++++++++++++++ 2 files changed, 205 insertions(+), 4 deletions(-) diff --git a/inc/events-controllers/class_bento_email_handler.php b/inc/events-controllers/class_bento_email_handler.php index 326e083..e6662fc 100644 --- a/inc/events-controllers/class_bento_email_handler.php +++ b/inc/events-controllers/class_bento_email_handler.php @@ -158,7 +158,14 @@ public function process_email_queue() { */ private function create_email_hash($email_data) { // Convert recipient(s) to a consistent format - $to = is_array($email_data['to']) ? implode(',', $email_data['to']) : $email_data['to']; + $normalized = $this->normalize_recipients($email_data['to'] ?? []); + if (!empty($normalized)) { + $to = implode(',', $normalized); + } else { + $to = is_array($email_data['to'] ?? null) + ? implode(',', $email_data['to']) + : ($email_data['to'] ?? ''); + } // Combine key elements of the email $hash_input = $to . '|' . @@ -181,8 +188,16 @@ private function send_via_bento($email_data) { $transactional_override = !empty($options['bento_transactional_override']) && $options['bento_transactional_override'] === '1'; $reply_to_enabled = !isset($options['bento_enable_reply_to']) || $options['bento_enable_reply_to'] !== '0'; + $recipients = $this->normalize_recipients($email_data['to'] ?? []); + if (empty($recipients)) { + Bento_Logger::log('Bento Email Handler: No valid recipients found'); + return false; + } + + $recipient_list = implode(', ', $recipients); + $parsed_headers = $this->parse_headers($email_data['headers'] ?? []); - $reply_to = $parsed_headers['Reply-To'] ?? ($parsed_headers['reply-to'] ?? null); + $reply_to = $this->parse_reply_to($parsed_headers); Bento_Logger::log('Bento Email Handler: Sending via API'); @@ -203,7 +218,7 @@ private function send_via_bento($email_data) { $body = array( 'emails' => array( array( - 'to' => $email_data['to'], + 'to' => $recipient_list, 'from' => $from_email, 'subject' => $email_data['subject'], 'html_body' => $email_data['message'], @@ -277,14 +292,105 @@ private function parse_headers($headers) { $parsed = []; foreach ($headers as $header) { + if (!is_string($header) || trim($header) === '') { + continue; + } + if (strpos($header, ':') === false) { continue; } [$name, $value] = explode(':', $header, 2); - $parsed[trim($name)] = trim($value); + $parsed[strtolower(trim($name))] = trim($value); } return $parsed; } + + /** + * Extract and validate reply-to address from parsed headers. + */ + private function parse_reply_to($headers) { + $value = $headers['reply-to'] ?? null; + if (empty($value)) { + return null; + } + + $candidates = preg_split('/[,;]/', $value); + if ($candidates === false) { + $candidates = [$value]; + } + + foreach ($candidates as $candidate) { + $candidate = trim($candidate); + if ($candidate === '') { + continue; + } + + if (preg_match('/<([^>]+)>/', $candidate, $matches)) { + $email = trim($matches[1]); + } else { + $email = $candidate; + } + + if ($this->is_valid_email($email)) { + return $email; + } + } + + return null; + } + + /** + * Normalize recipient list into valid email addresses. + */ + private function normalize_recipients($recipients) { + if (empty($recipients)) { + return []; + } + + if (is_string($recipients)) { + $recipients = preg_split('/[,;]/', $recipients); + } + + if (!is_array($recipients)) { + $recipients = (array) $recipients; + } + + $normalized = []; + + foreach ($recipients as $recipient) { + if (!is_string($recipient)) { + continue; + } + + $recipient = trim($recipient); + if ($recipient === '') { + continue; + } + + if (preg_match('/<([^>]+)>/', $recipient, $matches)) { + $email = trim($matches[1]); + } else { + $email = $recipient; + } + + if ($this->is_valid_email($email)) { + $normalized[] = $email; + } + } + + return array_values(array_unique($normalized)); + } + + /** + * Validate email address using WordPress helpers when available. + */ + private function is_valid_email($email) { + if (function_exists('is_email')) { + return is_email($email); + } + + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + } } diff --git a/tests/Unit/BentoEmailHandlerTest.php b/tests/Unit/BentoEmailHandlerTest.php index dfafe95..8267d20 100644 --- a/tests/Unit/BentoEmailHandlerTest.php +++ b/tests/Unit/BentoEmailHandlerTest.php @@ -146,3 +146,98 @@ expect($payload['emails'][0]['reply_to'])->toBe('reply@example.com'); }); + +test('Bento email handler skips invalid reply-to headers', function () { + update_option('bento_settings', [ + 'bento_enable_transactional' => '1', + 'bento_site_key' => 'site', + 'bento_publishable_key' => 'pub', + 'bento_secret_key' => 'sec', + 'bento_enable_reply_to' => '1', + ]); + + $handler = new Bento_Email_Handler(); + + $email = [ + 'to' => 'reply@example.com', + 'subject' => 'Subject', + 'message' => 'Body', + 'headers' => ['Reply-To: not-an-email'], + ]; + + update_option('bento_email_queue', [[ + 'email_data' => $email, + 'timestamp' => time(), + 'hash' => md5('reply@example.com|Subject'), + ]]); + + $handler->process_email_queue(); + + global $__wp_test_state; + $payload = json_decode($__wp_test_state['remote_posts'][0]['args']['body'], true); + + expect($payload['emails'][0])->not->toHaveKey('reply_to'); +}); + +test('Bento email handler normalizes recipient list', function () { + update_option('bento_settings', [ + 'bento_enable_transactional' => '1', + 'bento_site_key' => 'site', + 'bento_publishable_key' => 'pub', + 'bento_secret_key' => 'sec', + ]); + + $handler = new Bento_Email_Handler(); + + $email = [ + 'to' => 'Jane Doe , invalid, john@example.com', + 'subject' => 'Subject', + 'message' => 'Body', + 'headers' => [], + ]; + + update_option('bento_email_queue', [[ + 'email_data' => $email, + 'timestamp' => time(), + 'hash' => md5('jane@example.com,john@example.com|Subject'), + ]]); + + $handler->process_email_queue(); + + global $__wp_test_state; + $payload = json_decode($__wp_test_state['remote_posts'][0]['args']['body'], true); + + expect($payload['emails'][0]['to'])->toBe('jane@example.com, john@example.com'); +}); + +test('Bento email handler accepts case-insensitive reply-to headers', function () { + update_option('bento_settings', [ + 'bento_enable_transactional' => '1', + 'bento_site_key' => 'site', + 'bento_publishable_key' => 'pub', + 'bento_secret_key' => 'sec', + 'bento_enable_reply_to' => '1', + ]); + + $handler = new Bento_Email_Handler(); + + $email = [ + 'to' => 'reply@example.com', + 'subject' => 'Subject', + 'message' => 'Body', + 'headers' => ['REPLY-TO: reply@example.com'], + ]; + + update_option('bento_email_queue', [[ + 'email_data' => $email, + 'timestamp' => time(), + 'hash' => md5('reply@example.com|Subject'), + ]]); + + $handler->process_email_queue(); + + global $__wp_test_state; + $payload = json_decode($__wp_test_state['remote_posts'][0]['args']['body'], true); + + expect($payload['emails'][0]['reply_to'])->toBe('reply@example.com'); +}); From 949fe18084f02810d9281fc7b6491d353f95cb41 Mon Sep 17 00:00:00 2001 From: Zachary Date: Wed, 21 Jan 2026 17:30:45 +0900 Subject: [PATCH 3/3] Gitbutler/workspace (#15) * feat(email): Add reply-to header support for Bento emails Implement reply-to header parsing and forwarding for Bento transactional emails. Add new configuration option to enable or disable reply-to header support. Enhance email header parsing to extract headers from various input formats. * feat(email): Improve email recipient and header parsing Enhance email handling by adding robust recipient normalization, email validation, and reply-to parsing. Implement stricter input validation and error handling to prevent potential issues with malformed email addresses and headers. Key improvements: - Add recipient normalization method - Validate email addresses - Improve reply-to header parsing - Add logging for invalid recipient scenarios - Ensure consistent email address formatting