From 95ff455f9d4f124f897d8899be7d2422ac862105 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Fri, 26 Sep 2025 01:07:01 +0200 Subject: [PATCH] Adding #3235 and reorganizing tests --- src/PHPMailer.php | 62 ++++++++---- test/PHPMailer/PHPMailerTest.php | 2 + test/PHPMailer/ParseAddressesTest.php | 137 +++++++++++++++++--------- 3 files changed, 136 insertions(+), 65 deletions(-) diff --git a/src/PHPMailer.php b/src/PHPMailer.php index 02dd65a4..7a0e750a 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -1298,6 +1298,7 @@ class PHPMailer // Emit a runtime notice to recommend using the IMAP extension for full RFC822 parsing trigger_error(self::lang('imap_recommended'), E_USER_NOTICE); + $addresses = []; $list = explode(',', $addrstr); foreach ($list as $address) { $address = trim($address); @@ -1311,21 +1312,10 @@ class PHPMailer ]; } } else { - list($name, $email) = explode('<', $address); - $email = trim(str_replace('>', '', $email)); - $name = trim($name); + $parsed = self::parseEmailString($address); + $email = $parsed['email']; if (static::validateAddress($email)) { - //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled - //If this name is encoded, decode it - if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { - $origCharset = mb_internal_encoding(); - mb_internal_encoding($charset); - //Undo any RFC2047-encoded spaces-as-underscores - $name = str_replace('_', '=20', $name); - //Decode the name - $name = mb_decode_mimeheader($name); - mb_internal_encoding($origCharset); - } + $name = static::decodeHeader($parsed['name'], $charset); $addresses[] = [ //Remove any surrounding quotes and spaces from the name 'name' => trim($name, '\'" '), @@ -1338,6 +1328,42 @@ class PHPMailer return $addresses; } + /** + * Parse a string containing an email address with an optional name + * and divide it into a name and email address. + * + * @param string $input The email with name. + * + * @return array{name: string, email: string} + */ + private static function parseEmailString($input) + { + $input = trim((string)$input); + + if ($input === '') { + return ['name' => '', 'email' => '']; + } + + $pattern = '/^\s*(?:(?:"([^"]*)"|\'([^\']*)\'|([^<]*?))\s*)?<\s*([^>]+)\s*>\s*$/'; + if (preg_match($pattern, $input, $matches)) { + $name = ''; + // Double quotes including special scenarios. + if (isset($matches[1]) && $matches[1] !== '') { + $name = $matches[1]; + // Single quotes including special scenarios. + } elseif (isset($matches[2]) && $matches[2] !== '') { + $name = $matches[2]; + // Simplest scenario, name and email are in the format "Name ". + } elseif (isset($matches[3])) { + $name = trim($matches[3]); + } + + return ['name' => $name, 'email' => trim($matches[4])]; + } + + return ['name' => '', 'email' => $input]; + } + /** * Set the From and FromName properties. * @@ -3727,14 +3753,8 @@ class PHPMailer // Decode the header value $value = mb_decode_mimeheader($value); mb_internal_encoding($origCharset); - } elseif ($hasEncodedWord && function_exists('iconv_mime_decode')) { - // Use iconv as a fallback when mbstring is not available - $mode = defined('ICONV_MIME_DECODE_CONTINUE_ON_ERROR') ? ICONV_MIME_DECODE_CONTINUE_ON_ERROR : 0; - $decoded = @iconv_mime_decode($value, $mode, $charset); - if ($decoded !== false) { - $value = $decoded; - } } + return $value; } diff --git a/test/PHPMailer/PHPMailerTest.php b/test/PHPMailer/PHPMailerTest.php index cee80dd3..00933187 100644 --- a/test/PHPMailer/PHPMailerTest.php +++ b/test/PHPMailer/PHPMailerTest.php @@ -278,6 +278,8 @@ EOT; /** * Send a message containing ISO-8859-1 text. + * + * @requires extension mbstring */ public function testHtmlIso8859() { diff --git a/test/PHPMailer/ParseAddressesTest.php b/test/PHPMailer/ParseAddressesTest.php index 11fc14fb..922b8616 100644 --- a/test/PHPMailer/ParseAddressesTest.php +++ b/test/PHPMailer/ParseAddressesTest.php @@ -19,52 +19,32 @@ use Yoast\PHPUnitPolyfills\TestCases\TestCase; /** * Test RFC822 address splitting. - * - * @todo Additional tests need to be added to verify the correct handling of inputs which - * include a different encoding than UTF8 or even mixed encoding. For more information - * on what these test cases should look like and should test, please see - * {@link https://github.com/PHPMailer/PHPMailer/pull/2449} for context. - * - * @covers \PHPMailer\PHPMailer\PHPMailer::parseAddresses */ final class ParseAddressesTest extends TestCase { /** - * Test RFC822 address splitting using the PHPMailer native implementation + * Verify the expectations. * - * @dataProvider dataAddressSplitting + * Abstracted out as the same verification needs to be done for every test, just with different data. * - * @param string $addrstr The address list string. - * @param array $expected The expected function output. - * @param string $charset Optional. The charset to use. - */ - public function testAddressSplitting($addrstr, $expected) - { - $parsed = PHPMailer::parseAddresses($addrstr, null, PHPMailer::CHARSET_UTF8); - - $this->verifyExpectations($parsed, $expected); - } - - /** - * Test decodeHeader using the PHPMailer - * with the Mbstring extension available. - * - * @dataProvider dataDecodeHeader - * - * @param string $addrstr The header string. + * @param string $actual The actual function output. * @param array $expected The expected function output. */ - public function testDecodeHeader($str, $expected) + protected function verifyExpectations($actual, $expected) { - $parsed = PHPMailer::decodeHeader($str, PHPMailer::CHARSET_UTF8); - - $this->assertEquals($parsed, $expected); + self::assertIsArray($actual, 'parseAddresses() did not return an array'); + self::assertSame( + $expected, + $actual, + 'The return value from parseAddresses() did not match the expected output' + ); } /** * Test RFC822 address splitting using the native implementation * * @dataProvider dataAddressSplittingNative + * @covers \PHPMailer\PHPMailer\PHPMailer::parseSimplerAddresses * * @param string $addrstr The address list string. * @param array $expected The expected function output. @@ -72,6 +52,7 @@ final class ParseAddressesTest extends TestCase */ public function testAddressSplittingNative($addrstr, $expected, $charset = PHPMailer::CHARSET_ISO88591) { + xdebug_break(); error_reporting(E_ALL & ~E_USER_NOTICE); $reflMethod = new ReflectionMethod(PHPMailer::class, 'parseSimplerAddresses'); (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); @@ -107,22 +88,71 @@ final class ParseAddressesTest extends TestCase ]; } - /** - * Verify the expectations. + /** + * Test if email addresses are parsed and split into a name and address. * - * Abstracted out as the same verification needs to be done for every test, just with different data. - * - * @param string $actual The actual function output. - * @param array $expected The expected function output. + * @dataProvider dataParseEmailString + * @covers \PHPMailer\PHPMailer\PHPMailer::parseEmailString + * @param mixed $addrstr + * @param mixed $expected */ - protected function verifyExpectations($actual, $expected) + public function testParseEmailString($addrstr, $expected) { - self::assertIsArray($actual, 'parseAddresses() did not return an array'); - self::assertSame( - $expected, - $actual, - 'The return value from parseAddresses() did not match the expected output' - ); + $reflMethod = new ReflectionMethod(PHPMailer::class, 'parseEmailString'); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); + $parsed = $reflMethod->invoke(null, $addrstr); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); + $this->assertEquals($parsed, $expected); + } + + /** + * Data provider for testParseEmailString. + * + * @return array The array is expected to have an `addrstr` and an `expected` key. + */ + public function dataParseEmailString() + { + return [ + 'Valid address: simple address' => [ + 'addrstr' => 'Joe User ', + 'expected' => ['name' => 'Joe User', 'email' => 'joe@example.com'], + ], + 'Valid address: simple address with double quotes' => [ + 'addrstr' => '"Joe User" ', + 'expected' => ['name' => 'Joe User', 'email' => 'joe@example.com'], + ], + 'Valid address: simple address with single quotes' => [ + 'addrstr' => '\'Joe User\' ', + 'expected' => ['name' => 'Joe User', 'email' => 'joe@example.com'], + ], + 'Valid address: complex address with single quotes' => [ + 'addrstr' => '\'Joe', + 'expected' => ['name' => 'Joe 'joe@example.com'], + ], + 'Valid address: complex address with triangle bracket' => [ + 'addrstr' => '"test', + 'expected' => ['name' => 'test 'test@example.com'], + ], + ]; + } + + /** + * Test RFC822 address splitting using the PHPMailer native implementation + * + * @dataProvider dataAddressSplitting + * @covers \PHPMailer\PHPMailer\PHPMailer::parseAddresses + * + * @requires extension mbstring + * + * @param string $addrstr The address list string. + * @param array $expected The expected function output. + * @param string $charset Optional. The charset to use. + */ + public function testAddressSplitting($addrstr, $expected) + { + $parsed = PHPMailer::parseAddresses($addrstr, null, PHPMailer::CHARSET_UTF8); + + $this->verifyExpectations($parsed, $expected); } /** @@ -218,6 +248,25 @@ final class ParseAddressesTest extends TestCase ]; } + /** + * Test decodeHeader using the PHPMailer + * with the Mbstring extension available. + * + * @dataProvider dataDecodeHeader + * @covers \PHPMailer\PHPMailer\PHPMailer::decodeHeader + * + * @requires extension mbstring + * + * @param string $addrstr The header string. + * @param array $expected The expected function output. + */ + public function testDecodeHeader($str, $expected) + { + $parsed = PHPMailer::decodeHeader($str, PHPMailer::CHARSET_UTF8); + + $this->assertEquals($parsed, $expected); + } + /** * Data provider for decodeHeader. *