diff --git a/composer.json b/composer.json index 82da669a..e4dd7ddd 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,8 @@ "league/oauth2-google": "Needed for Google XOAUTH2 authentication", "psr/log": "For optional PSR-3 debug logging", "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication", - "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", + "ext-imap": "Needed to support advanced email address parsing according to RFC822" }, "autoload": { "psr-4": { diff --git a/language/phpmailer.lang-es.php b/language/phpmailer.lang-es.php index 4e74bfb7..35ce5b8e 100644 --- a/language/phpmailer.lang-es.php +++ b/language/phpmailer.lang-es.php @@ -9,7 +9,7 @@ */ $PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.'; -$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP está afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.'; +$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP ha sido afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.'; $PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.'; $PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.'; $PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.'; @@ -18,7 +18,7 @@ $PHPMAILER_LANG['execute'] = 'Imposible ejecutar: '; $PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: '; $PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: '; $PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: '; -$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: '; +$PHPMAILER_LANG['from_failed'] = 'La siguiente dirección de remitente falló: '; $PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.'; $PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: '; $PHPMAILER_LANG['invalid_header'] = 'Nombre o valor de encabezado no válido'; @@ -34,3 +34,4 @@ $PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.'; $PHPMAILER_LANG['smtp_detail'] = 'Detalle: '; $PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; $PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: '; +$PHPMAILER_LANG['imap_recommended'] = 'No se recomienda usar el analizador de direcciones simplificado. Instala la extensión IMAP de PHP para un análisis RFC822 más completo.'; diff --git a/src/PHPMailer.php b/src/PHPMailer.php index 9e5afe95..92203715 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -1280,40 +1280,61 @@ class PHPMailer } } else { //Use this simpler parser - $list = explode(',', $addrstr); - foreach ($list as $address) { - $address = trim($address); - //Is there a separate name part? - if (strpos($address, '<') === false) { - //No separate name, just use the whole thing - if (static::validateAddress($address)) { - $addresses[] = [ - 'name' => '', - 'address' => $address, - ]; - } - } else { - list($name, $email) = explode('<', $address); - $email = trim(str_replace('>', '', $email)); - $name = trim($name); - 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); - } - $addresses[] = [ - //Remove any surrounding quotes and spaces from the name - 'name' => trim($name, '\'" '), - 'address' => $email, - ]; + $addresses = self::parseSimplerAddresses($addrstr, $charset); + } + + return $addresses; + } + + /** + * Parse a string containing one or more RFC822-style comma-separated email addresses + * with the form "display name
" into an array of name/address pairs. + * Uses a simpler parser that does not require the IMAP extension but doesnt support + * the full RFC822 spec. For full RFC822 support, use the PHP IMAP extension. + * + * @param string $addrstr The address list string + * @param string $charset The charset to use when decoding the address list string. + * + * @return array + */ + protected static function parseSimplerAddresses($addrstr, $charset) + { + // Emit a runtime notice to recommend using the IMAP extension for full RFC822 parsing + trigger_error(self::lang('imap_recommended'), E_USER_NOTICE); + + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if (static::validateAddress($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + $name = trim($name); + 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); } + $addresses[] = [ + //Remove any surrounding quotes and spaces from the name + 'name' => trim($name, '\'" '), + 'address' => $email, + ]; } } } @@ -2433,6 +2454,8 @@ class PHPMailer 'smtp_error' => 'SMTP server error: ', 'variable_set' => 'Cannot set or reset variable: ', 'no_smtputf8' => 'Server does not support SMTPUTF8 needed to send to Unicode addresses', + 'imap_recommended' => 'Using simplified address parser is not recommended. ' . + 'Install the PHP IMAP extension for full RFC822 parsing.', ]; if (empty($lang_path)) { //Calculate an absolute path so it can work if CWD is not here diff --git a/test/PHPMailer/ParseAddressesTest.php b/test/PHPMailer/ParseAddressesTest.php index f6188b90..ee9064f1 100644 --- a/test/PHPMailer/ParseAddressesTest.php +++ b/test/PHPMailer/ParseAddressesTest.php @@ -14,6 +14,7 @@ namespace PHPMailer\Test\PHPMailer; use PHPMailer\PHPMailer\PHPMailer; +use ReflectionMethod; use Yoast\PHPUnitPolyfills\TestCases\TestCase; /** @@ -28,36 +29,6 @@ use Yoast\PHPUnitPolyfills\TestCases\TestCase; */ final class ParseAddressesTest extends TestCase { - /** - * Test RFC822 address splitting using the PHPMailer native implementation - * with the Mbstring extension available. - * - * @requires extension mbstring - * - * @dataProvider dataAddressSplitting - * - * @param string $addrstr The address list string. - * @param array $expected The expected function output. - * @param string $charset Optional. The charset to use. - */ - public function testAddressSplittingNative($addrstr, $expected, $charset = null) - { - if (isset($charset)) { - $parsed = PHPMailer::parseAddresses($addrstr, false, $charset); - } else { - $parsed = PHPMailer::parseAddresses($addrstr, false); - } - - $expectedOutput = $expected['default']; - if (empty($expected['native+mbstring']) === false) { - $expectedOutput = $expected['native+mbstring']; - } elseif (empty($expected['native']) === false) { - $expectedOutput = $expected['native']; - } - - $this->verifyExpectations($parsed, $expectedOutput); - } - /** * Test RFC822 address splitting using the IMAP implementation * with the Mbstring extension available. @@ -89,38 +60,6 @@ final class ParseAddressesTest extends TestCase $this->verifyExpectations($parsed, $expectedOutput); } - /** - * Test RFC822 address splitting using the PHPMailer native implementation - * without the Mbstring extension. - * - * @dataProvider dataAddressSplitting - * - * @param string $addrstr The address list string. - * @param array $expected The expected function output. - * @param string $charset Optional. The charset to use. - */ - public function testAddressSplittingNativeNoMbstring($addrstr, $expected, $charset = null) - { - if (extension_loaded('mbstring')) { - self::markTestSkipped('Test requires MbString *not* to be available'); - } - - if (isset($charset)) { - $parsed = PHPMailer::parseAddresses($addrstr, false, $charset); - } else { - $parsed = PHPMailer::parseAddresses($addrstr, false); - } - - $expectedOutput = $expected['default']; - if (empty($expected['native--mbstring']) === false) { - $expectedOutput = $expected['native--mbstring']; - } elseif (empty($expected['native']) === false) { - $expectedOutput = $expected['native']; - } - - $this->verifyExpectations($parsed, $expectedOutput); - } - /** * Test RFC822 address splitting using the IMAP implementation * without the Mbstring extension. @@ -155,6 +94,52 @@ final class ParseAddressesTest extends TestCase $this->verifyExpectations($parsed, $expectedOutput); } + /** + * Test RFC822 address splitting using the native implementation + * + * @dataProvider dataAddressSplittingNative + * + * @param string $addrstr The address list string. + * @param array $expected The expected function output. + * @param string $charset Optional.The charset to use. + */ + public function testAddressSplittingNative($addrstr, $expected, $charset = PHPMailer::CHARSET_ISO88591) + { + error_reporting(E_ALL & ~E_USER_NOTICE); + $reflMethod = new ReflectionMethod(PHPMailer::class, 'parseSimplerAddresses'); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); + $parsed = $reflMethod->invoke(null, $addrstr, $charset); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); + $this->verifyExpectations($parsed, $expected); + } + + /** + * Data provider for testAddressSplittingNative. + * + * @return array + * addrstr: string, + * expected: array{name: string, address: string}[] + * charset: string + */ + public function dataAddressSplittingNative() + { + return [ + 'Valid address: single address without name' => [ + 'addrstr' => 'joe@example.com', + 'expected' => [ + ['name' => '', 'address' => 'joe@example.com'], + ], + ], + 'Valid address: two addresses with names' => [ + 'addrstr' => 'Joe User , Jill User ', + 'expected' => [ + ['name' => 'Joe User', 'address' => 'joe@example.com'], + ['name' => 'Jill User', 'address' => 'jill@example.net'], + ], + ], + ]; + } + /** * Verify the expectations. *