Merge pull request #3217 from SirLouen/patch/3210

Modularizing and Simplifying the Address Parser
This commit is contained in:
Marcus Bointon 2025-09-02 21:11:31 +02:00 committed by GitHub
commit 5ddea0610b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 108 additions and 98 deletions

View File

@ -56,7 +56,8 @@
"league/oauth2-google": "Needed for Google XOAUTH2 authentication", "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"psr/log": "For optional PSR-3 debug logging", "psr/log": "For optional PSR-3 debug logging",
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication", "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": { "autoload": {
"psr-4": { "psr-4": {

View File

@ -9,7 +9,7 @@
*/ */
$PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.'; $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['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.'; $PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.';
$PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.'; $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['extension_missing'] = 'Extensión faltante: ';
$PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: '; $PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: ';
$PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el 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['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_address'] = 'Imposible enviar: dirección de email inválido: ';
$PHPMAILER_LANG['invalid_header'] = 'Nombre o valor de encabezado no vá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_detail'] = 'Detalle: ';
$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; $PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: '; $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.';

View File

@ -1280,40 +1280,61 @@ class PHPMailer
} }
} else { } else {
//Use this simpler parser //Use this simpler parser
$list = explode(',', $addrstr); $addresses = self::parseSimplerAddresses($addrstr, $charset);
foreach ($list as $address) { }
$address = trim($address);
//Is there a separate name part? return $addresses;
if (strpos($address, '<') === false) { }
//No separate name, just use the whole thing
if (static::validateAddress($address)) { /**
$addresses[] = [ * Parse a string containing one or more RFC822-style comma-separated email addresses
'name' => '', * with the form "display name <address>" into an array of name/address pairs.
'address' => $address, * 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.
} *
} else { * @param string $addrstr The address list string
list($name, $email) = explode('<', $address); * @param string $charset The charset to use when decoding the address list string.
$email = trim(str_replace('>', '', $email)); *
$name = trim($name); * @return array
if (static::validateAddress($email)) { */
//Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled protected static function parseSimplerAddresses($addrstr, $charset)
//If this name is encoded, decode it {
if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { // Emit a runtime notice to recommend using the IMAP extension for full RFC822 parsing
$origCharset = mb_internal_encoding(); trigger_error(self::lang('imap_recommended'), E_USER_NOTICE);
mb_internal_encoding($charset);
//Undo any RFC2047-encoded spaces-as-underscores $list = explode(',', $addrstr);
$name = str_replace('_', '=20', $name); foreach ($list as $address) {
//Decode the name $address = trim($address);
$name = mb_decode_mimeheader($name); //Is there a separate name part?
mb_internal_encoding($origCharset); if (strpos($address, '<') === false) {
} //No separate name, just use the whole thing
$addresses[] = [ if (static::validateAddress($address)) {
//Remove any surrounding quotes and spaces from the name $addresses[] = [
'name' => trim($name, '\'" '), 'name' => '',
'address' => $email, '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: ', 'smtp_error' => 'SMTP server error: ',
'variable_set' => 'Cannot set or reset variable: ', 'variable_set' => 'Cannot set or reset variable: ',
'no_smtputf8' => 'Server does not support SMTPUTF8 needed to send to Unicode addresses', '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)) { if (empty($lang_path)) {
//Calculate an absolute path so it can work if CWD is not here //Calculate an absolute path so it can work if CWD is not here

View File

@ -14,6 +14,7 @@
namespace PHPMailer\Test\PHPMailer; namespace PHPMailer\Test\PHPMailer;
use PHPMailer\PHPMailer\PHPMailer; use PHPMailer\PHPMailer\PHPMailer;
use ReflectionMethod;
use Yoast\PHPUnitPolyfills\TestCases\TestCase; use Yoast\PHPUnitPolyfills\TestCases\TestCase;
/** /**
@ -28,36 +29,6 @@ use Yoast\PHPUnitPolyfills\TestCases\TestCase;
*/ */
final class ParseAddressesTest extends 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 * Test RFC822 address splitting using the IMAP implementation
* with the Mbstring extension available. * with the Mbstring extension available.
@ -89,38 +60,6 @@ final class ParseAddressesTest extends TestCase
$this->verifyExpectations($parsed, $expectedOutput); $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 * Test RFC822 address splitting using the IMAP implementation
* without the Mbstring extension. * without the Mbstring extension.
@ -155,6 +94,52 @@ final class ParseAddressesTest extends TestCase
$this->verifyExpectations($parsed, $expectedOutput); $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 <joe@example.com>, Jill User <jill@example.net>',
'expected' => [
['name' => 'Joe User', 'address' => 'joe@example.com'],
['name' => 'Jill User', 'address' => 'jill@example.net'],
],
],
];
}
/** /**
* Verify the expectations. * Verify the expectations.
* *