From 13ca41d9f9b71039d37b26bfdc14177d27cfb1ba Mon Sep 17 00:00:00 2001 From: PhilW Date: Sun, 10 May 2026 09:55:57 +0100 Subject: [PATCH 1/4] identity_select: match received header in reverse order (#10158) --- plugins/identity_select/identity_select.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/identity_select/identity_select.php b/plugins/identity_select/identity_select.php index 51ad152351..b827394a86 100644 --- a/plugins/identity_select/identity_select.php +++ b/plugins/identity_select/identity_select.php @@ -85,8 +85,9 @@ protected function get_email_from_header($message, $header) if (strtolower($header) == 'received') { // find first email address in all Received headers $email = null; - foreach ((array) $value as $entry) { - if (preg_match('/[\s\t]+for[\s\t]+<([^>]+)>/', $entry, $matches)) { + // reverse the header array as RFC 5321 requires additional Received headers are prepended + foreach (array_reverse((array) $value) as $entry) { + if (preg_match('/[\s\t]+for\s+@]+@[^\s]+\.[^\s>;]+)>?/u', $entry, $matches)) { $email = $matches[1]; break; } From 8191d729e5cbf7e94016e798a23cd6d82fa31b1d Mon Sep 17 00:00:00 2001 From: PhilW Date: Sun, 10 May 2026 13:26:30 +0100 Subject: [PATCH 2/4] add a test for get_email_from_header() --- .../tests/IdentitySelectTest.php | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/plugins/identity_select/tests/IdentitySelectTest.php b/plugins/identity_select/tests/IdentitySelectTest.php index 86284e2716..4f12e0517b 100644 --- a/plugins/identity_select/tests/IdentitySelectTest.php +++ b/plugins/identity_select/tests/IdentitySelectTest.php @@ -4,6 +4,8 @@ use PHPUnit\Framework\TestCase; +use function Roundcube\Tests\invokeMethod; + class IdentitySelectTest extends TestCase { /** @@ -17,4 +19,47 @@ public function test_constructor() $this->assertInstanceOf('identity_select', $plugin); $this->assertInstanceOf('rcube_plugin', $plugin); } + + /** + * Test get_email_from_header() method + */ + public function test_get_email_from_header() + { + $rcube = \rcube::get_instance(); + $plugin = new \identity_select($rcube->plugins); + $message = new \stdClass(); + + $headers = [ + 'Delivered-To' => 'user@example.com', + 'Received' => 'from github.com ([10.48.109.45]) by smtp.github.com (Postfix) with ESMTPA id 8C9B4E0075' + . ' for ; Sat, 28 Nov 2020 22:45:44 -0800 (PST)', + ]; + $message->headers = \rcube_message_header::from_array($headers); + $result = invokeMethod($plugin, 'get_email_from_header', [$message, 'Delivered-To']); + $this->assertSame('user@example.com', $result[0]); + $result = invokeMethod($plugin, 'get_email_from_header', [$message, 'Received']); + $this->assertSame('john@domain.tld', $result[0]); + + $headers = [ + 'Received' => [ + 'from mail.aliasprovider.com (mail.aliasprovider.com [198.51.100.20]) by smtp.example.com (Postfix) with ESMTP id 555AAA777' + . ' for chris.smith@example.com; Sun, 10 May 2026 16:00:01 -0400 (EDT)', + 'from mail.external.com (mail.external.com [203.0.113.45]) by mail.aliasprovider.com (Postfix) with ESMTP id 444BBB666' + . ' for bob.smith@example.com; Sun, 10 May 2026 16:00:00 -0400 (EDT)', + 'from client.mail.com (client.mail.com [192.0.2.10]) by mail.external.com (Postfix) with ESMTP id 333CCC555' + . ' for sales@example.com; Sun, 10 May 2026 15:59:58 -0400 (EDT)', + ], + ]; + $message->headers = \rcube_message_header::from_array($headers); + $result = invokeMethod($plugin, 'get_email_from_header', [$message, 'Received']); + $this->assertSame('sales@example.com', $result[0]); + + $headers = [ + 'Received' => 'from mail.aliasprovider.com (mail.aliasprovider.com [198.51.100.20]) by smtp.example.com (Postfix) with ESMTP id 555AAA777' + . ' for ; Sun, 10 May 2026 16:30:01 -0400 (EDT)', + ]; + $message->headers = \rcube_message_header::from_array($headers); + $result = invokeMethod($plugin, 'get_email_from_header', [$message, 'Received']); + $this->assertSame('josé.müller@example.com', $result[0]); + } } From d653d55d16dbe3a2831cc4979c4458f2052bd9e7 Mon Sep 17 00:00:00 2001 From: PhilW Date: Wed, 13 May 2026 18:48:48 +0100 Subject: [PATCH 3/4] identity_select: return all received headers --- plugins/identity_select/identity_select.php | 17 ++++++++++------- .../tests/IdentitySelectTest.php | 8 ++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/plugins/identity_select/identity_select.php b/plugins/identity_select/identity_select.php index b827394a86..1377c5275f 100644 --- a/plugins/identity_select/identity_select.php +++ b/plugins/identity_select/identity_select.php @@ -60,11 +60,15 @@ public function select($p) } $rcmail = rcmail::get_instance(); + $identities = []; + foreach ($p['identities'] as $idx => $ident) { + $identities[$idx] = $ident['email']; + } foreach ((array) $rcmail->config->get('identity_select_headers', []) as $header) { if ($emails = $this->get_email_from_header($p['message'], $header)) { - foreach ($p['identities'] as $idx => $ident) { - if (in_array($ident['email_ascii'], $emails)) { + foreach ($emails as $email) { + if (($idx = array_search($email, $identities)) !== false) { $p['selected'] = $idx; break 2; } @@ -83,17 +87,16 @@ protected function get_email_from_header($message, $header) $value = $message->headers->get($header, false); if (strtolower($header) == 'received') { - // find first email address in all Received headers - $email = null; + // find all email addresses in all Received headers + $emails = []; // reverse the header array as RFC 5321 requires additional Received headers are prepended foreach (array_reverse((array) $value) as $entry) { if (preg_match('/[\s\t]+for\s+@]+@[^\s]+\.[^\s>;]+)>?/u', $entry, $matches)) { - $email = $matches[1]; - break; + $emails[] = $matches[1]; } } - $value = $email; + $value = $emails; } return (array) $value; diff --git a/plugins/identity_select/tests/IdentitySelectTest.php b/plugins/identity_select/tests/IdentitySelectTest.php index 4f12e0517b..b036bdafa2 100644 --- a/plugins/identity_select/tests/IdentitySelectTest.php +++ b/plugins/identity_select/tests/IdentitySelectTest.php @@ -36,9 +36,9 @@ public function test_get_email_from_header() ]; $message->headers = \rcube_message_header::from_array($headers); $result = invokeMethod($plugin, 'get_email_from_header', [$message, 'Delivered-To']); - $this->assertSame('user@example.com', $result[0]); + $this->assertSame(['user@example.com'], $result); $result = invokeMethod($plugin, 'get_email_from_header', [$message, 'Received']); - $this->assertSame('john@domain.tld', $result[0]); + $this->assertSame(['john@domain.tld'], $result); $headers = [ 'Received' => [ @@ -52,7 +52,7 @@ public function test_get_email_from_header() ]; $message->headers = \rcube_message_header::from_array($headers); $result = invokeMethod($plugin, 'get_email_from_header', [$message, 'Received']); - $this->assertSame('sales@example.com', $result[0]); + $this->assertSame(['sales@example.com', 'bob.smith@example.com', 'chris.smith@example.com'], $result); $headers = [ 'Received' => 'from mail.aliasprovider.com (mail.aliasprovider.com [198.51.100.20]) by smtp.example.com (Postfix) with ESMTP id 555AAA777' @@ -60,6 +60,6 @@ public function test_get_email_from_header() ]; $message->headers = \rcube_message_header::from_array($headers); $result = invokeMethod($plugin, 'get_email_from_header', [$message, 'Received']); - $this->assertSame('josé.müller@example.com', $result[0]); + $this->assertSame(['josé.müller@example.com'], $result); } } From a2e3a1c14d540658c1972b1edc4fd4c23ee4a2fd Mon Sep 17 00:00:00 2001 From: PhilW Date: Wed, 13 May 2026 19:15:37 +0100 Subject: [PATCH 4/4] make matching case insensitive --- plugins/identity_select/identity_select.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/identity_select/identity_select.php b/plugins/identity_select/identity_select.php index 1377c5275f..0f6798bfdf 100644 --- a/plugins/identity_select/identity_select.php +++ b/plugins/identity_select/identity_select.php @@ -62,13 +62,13 @@ public function select($p) $rcmail = rcmail::get_instance(); $identities = []; foreach ($p['identities'] as $idx => $ident) { - $identities[$idx] = $ident['email']; + $identities[$idx] = mb_strtolower($ident['email']); } foreach ((array) $rcmail->config->get('identity_select_headers', []) as $header) { if ($emails = $this->get_email_from_header($p['message'], $header)) { foreach ($emails as $email) { - if (($idx = array_search($email, $identities)) !== false) { + if (($idx = array_search(mb_strtolower($email), $identities)) !== false) { $p['selected'] = $idx; break 2; }