From 096b24646ee9ab11fdabcb7d229eac3b566526b6 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sun, 10 Aug 2025 05:38:47 +0200 Subject: [PATCH 01/22] PHP 8.5 | Tests: prevent deprecation notice for Reflection*::setAccessible() Since PHP 8.1, calling the `Reflection*::setAccessible()` methods is no longer necessary as reflected properties/methods/etc will always be accessible. However, the method calls are still needed for PHP < 8.1. As of PHP 8.5, calling the `Reflection*::setAccessible()` methods is now formally deprecated and will yield a deprecation notice, which will fail test runs. As of PHP 9.0, the `setAccessible()` method(s) will be removed. With the latter in mind, this commit prevents the deprecation notice by making the calls to `setAccessible()` conditional. Silencing the deprecation would mean, this would need to be "fixed" again come PHP 9.0, while the current solution should be stable, including for PHP 9.0. Ref: https://wiki.php.net/rfc/deprecations_php_8_5#extreflection_deprecations --- test/OAuth/OAuthTest.php | 2 +- test/PHPMailer/FileIsAccessibleTest.php | 8 ++++---- test/PHPMailer/IsPermittedPathTest.php | 4 ++-- test/PHPMailer/LocalizationTest.php | 4 ++-- test/PHPMailer/PHPMailerTest.php | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/OAuth/OAuthTest.php b/test/OAuth/OAuthTest.php index 4b640ced..5043808a 100644 --- a/test/OAuth/OAuthTest.php +++ b/test/OAuth/OAuthTest.php @@ -36,7 +36,7 @@ final class OAuthTest extends TestCase $PHPMailer = new PHPMailer(true); $reflection = new \ReflectionClass($PHPMailer); $property = $reflection->getProperty('oauth'); - $property->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $property->setAccessible(true); $property->setValue($PHPMailer, true); self::assertTrue($PHPMailer->getOAuth(), 'Initial value of oauth property is not true'); diff --git a/test/PHPMailer/FileIsAccessibleTest.php b/test/PHPMailer/FileIsAccessibleTest.php index 8989643a..45f0837d 100644 --- a/test/PHPMailer/FileIsAccessibleTest.php +++ b/test/PHPMailer/FileIsAccessibleTest.php @@ -39,9 +39,9 @@ final class FileIsAccessibleTest extends TestCase public function testFileIsAccessible($input, $expected) { $reflMethod = new ReflectionMethod(PHPMailer::class, 'fileIsAccessible'); - $reflMethod->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); $result = $reflMethod->invoke(null, $input); - $reflMethod->setAccessible(false); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); self::assertSame($expected, $result); } @@ -91,9 +91,9 @@ final class FileIsAccessibleTest extends TestCase chmod($file, octdec('0')); $reflMethod = new ReflectionMethod(PHPMailer::class, 'fileIsAccessible'); - $reflMethod->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); $result = $reflMethod->invoke(null, $file); - $reflMethod->setAccessible(false); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); // Reset to the default for git files before running assertions. chmod($file, octdec('644')); diff --git a/test/PHPMailer/IsPermittedPathTest.php b/test/PHPMailer/IsPermittedPathTest.php index 93823a44..f3f17537 100644 --- a/test/PHPMailer/IsPermittedPathTest.php +++ b/test/PHPMailer/IsPermittedPathTest.php @@ -35,9 +35,9 @@ final class IsPermittedPathTest extends TestCase public function testIsPermittedPath($input, $expected) { $reflMethod = new ReflectionMethod(PHPMailer::class, 'isPermittedPath'); - $reflMethod->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); $result = $reflMethod->invoke(null, $input); - $reflMethod->setAccessible(false); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); self::assertSame($expected, $result); } diff --git a/test/PHPMailer/LocalizationTest.php b/test/PHPMailer/LocalizationTest.php index c67ce578..6347ac6a 100644 --- a/test/PHPMailer/LocalizationTest.php +++ b/test/PHPMailer/LocalizationTest.php @@ -423,9 +423,9 @@ final class LocalizationTest extends TestCase } $reflMethod = new ReflectionMethod($this->Mail, 'lang'); - $reflMethod->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); $result = $reflMethod->invoke($this->Mail, $input); - $reflMethod->setAccessible(false); + (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); self::assertSame($expected, $result); } diff --git a/test/PHPMailer/PHPMailerTest.php b/test/PHPMailer/PHPMailerTest.php index 1cc9ba14..fb787504 100644 --- a/test/PHPMailer/PHPMailerTest.php +++ b/test/PHPMailer/PHPMailerTest.php @@ -253,7 +253,7 @@ EOT; $PHPMailer = new PHPMailer(); $reflection = new \ReflectionClass($PHPMailer); $property = $reflection->getProperty('message_type'); - $property->setAccessible(true); + (\PHP_VERSION_ID < 80100) && $property->setAccessible(true); $property->setValue($PHPMailer, 'inline'); self::assertIsString($PHPMailer->createBody()); From 5b970527ae5f32c4425ad8411b77912ca68d5cf9 Mon Sep 17 00:00:00 2001 From: Christian Seel Date: Mon, 11 Aug 2025 14:09:44 +0200 Subject: [PATCH 02/22] Enhance interrupted system call check for non-english locale on applications with a different locale than english, the message "interrupted system call" is not found because it's translated. So we also check for the SOCKET_EINTR constant which is defined under Windows and UNIX-like platforms (if available on the platform). --- src/SMTP.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/SMTP.php b/src/SMTP.php index e6170f71..4181eaea 100644 --- a/src/SMTP.php +++ b/src/SMTP.php @@ -1340,7 +1340,16 @@ class SMTP //stream_select returns false when the `select` system call is interrupted //by an incoming signal, try the select again - if (stripos($message, 'interrupted system call') !== false) { + if ( + stripos($message, 'interrupted system call') !== false || + ( + // on applications with a different locale than english, the message above is not found because + // it's translated. So we also check for the SOCKET_EINTR constant which is defined under + // Windows and UNIX-like platforms (if available on the platform). + defined('SOCKET_EINTR') && + stripos($message, 'stream_select(): Unable to select [' . SOCKET_EINTR . ']') !== false + ) + ) { $this->edebug( 'SMTP -> get_lines(): retrying stream_select', self::DEBUG_LOWLEVEL From 97f4e58e1842e363de41e2438faea63b5b4718d2 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 17 Aug 2025 13:18:53 +0200 Subject: [PATCH 03/22] Sorting the Array problem in doCallback --- src/PHPMailer.php | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/PHPMailer.php b/src/PHPMailer.php index 49fce31e..d1912e57 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -1842,16 +1842,18 @@ class PHPMailer fwrite($mail, $body); $result = pclose($mail); $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); - $this->doCallback( - ($result === 0), - [[$addrinfo['address'], $addrinfo['name']]], - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); + foreach ($addrinfo as $addr) { + $this->doCallback( + ($result === 0), + [[$addr['address'], $addr['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + } $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); @@ -2017,16 +2019,18 @@ class PHPMailer foreach ($toArr as $toAddr) { $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); - $this->doCallback( - $result, - [[$addrinfo['address'], $addrinfo['name']]], - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); + foreach ($addrinfo as $addr) { + $this->doCallback( + $result, + [[$addr['address'], $addr['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); + } } } else { $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); From 855bf067fcbf34162bef4eca37e33329b1b1553b Mon Sep 17 00:00:00 2001 From: jrfnl Date: Fri, 22 Aug 2025 02:19:37 +0200 Subject: [PATCH 04/22] Fix linting issue on PHP 8.5 As per the options described in https://github.com/PHPMailer/PHPMailer/pull/3202#issuecomment-3212478928. Note: the linting ignore comment triggers some PHPCS errors (_sigh_), so I'm selectively excluding those. Alternatively, it could be considered to exclude test fixture files completely from the PHPCS scan. --- phpcs.xml.dist | 17 +++++++++ .../LocalizationTest/phpmailer.lang-yy.php | 1 - .../LocalizationTest/phpmailer.lang-yz.php | 14 +++++++ test/PHPMailer/LocalizationTest.php | 38 +++++++++++++++---- 4 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 test/Fixtures/LocalizationTest/phpmailer.lang-yz.php diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 426618c7..e2d1e5c6 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -36,7 +36,24 @@ + + + */language/phpmailer\.lang*\.php$ + + + + */test/Fixtures/LocalizationTest/phpmailer.lang-yz\.php + + + */test/Fixtures/LocalizationTest/phpmailer.lang-yz\.php + + diff --git a/test/Fixtures/LocalizationTest/phpmailer.lang-yy.php b/test/Fixtures/LocalizationTest/phpmailer.lang-yy.php index eb485dbf..d3af6be6 100644 --- a/test/Fixtures/LocalizationTest/phpmailer.lang-yy.php +++ b/test/Fixtures/LocalizationTest/phpmailer.lang-yy.php @@ -12,6 +12,5 @@ echo $composer; $PHPMAILER_LANG['extension_missing'] = 'Confirming that test fixture was loaded correctly (yy).'; $PHPMAILER_LANG['empty_message'] = $composer; -$PHPMAILER_LANG['encoding'] = `ls -l`; $PHPMAILER_LANG['execute'] = exec('some harmful command'); $PHPMAILER_LANG['signing'] = "Double quoted but not interpolated $composer"; diff --git a/test/Fixtures/LocalizationTest/phpmailer.lang-yz.php b/test/Fixtures/LocalizationTest/phpmailer.lang-yz.php new file mode 100644 index 00000000..2b0f8aa3 --- /dev/null +++ b/test/Fixtures/LocalizationTest/phpmailer.lang-yz.php @@ -0,0 +1,14 @@ +Mail->setLanguage( + 'yz', // Unassigned lang code. + dirname(__DIR__) . '/Fixtures/LocalizationTest/' + ); + $lang = $this->Mail->getTranslations(); + + self::assertTrue($result, 'Setting the language failed. Translations set to: ' . var_export($lang, true)); + self::assertIsArray($lang, 'Translations is not an array'); + + // Verify that the fixture file was loaded. + self::assertArrayHasKey('extension_missing', $lang, 'The "extension_missing" translation key was not found'); + self::assertSame( + 'Confirming that test fixture was loaded correctly (yz).', + $lang['extension_missing'], + 'The "extension_missing" translation is not as expected' + ); + + // Verify that arbitrary code in a translation file does not get processed. + self::assertArrayHasKey('encoding', $lang, 'The "encoding" translation key was not found'); + self::assertSame( + 'Unknown encoding: ', + $lang['encoding'], + 'The "encoding" translation is not as expected' + ); + } + /** * Test that text strings passed in from a language file for arbitrary keys do not get processed. */ From 19f17f8aebdbf28966aae770daf0066c9cad3f88 Mon Sep 17 00:00:00 2001 From: Marcus Bointon Date: Fri, 22 Aug 2025 15:12:14 +0200 Subject: [PATCH 05/22] CS --- src/PHPMailer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PHPMailer.php b/src/PHPMailer.php index d1912e57..5b0ba5c0 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -1851,7 +1851,7 @@ class PHPMailer $this->Subject, $body, $this->From, - [] + [] ); } $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); From b41dd255a207af2e9481123b274d4a4d77ef9f68 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sat, 23 Aug 2025 16:16:14 +0200 Subject: [PATCH 06/22] Staticfying the Language Pack --- src/PHPMailer.php | 112 ++++++++++++++-------------- test/PHPMailer/LocalizationTest.php | 7 +- test/TestCase.php | 1 + 3 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/PHPMailer.php b/src/PHPMailer.php index 5b0ba5c0..9e5afe95 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -711,7 +711,7 @@ class PHPMailer * * @var array */ - protected $language = []; + protected static $language = []; /** * The number of errors encountered. @@ -1102,7 +1102,7 @@ class PHPMailer //At-sign is missing. $error_message = sprintf( '%s (%s): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $kind, $address ); @@ -1187,7 +1187,7 @@ class PHPMailer if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { $error_message = sprintf( '%s: %s', - $this->lang('Invalid recipient kind'), + self::lang('Invalid recipient kind'), $kind ); $this->setError($error_message); @@ -1201,7 +1201,7 @@ class PHPMailer if (!static::validateAddress($address)) { $error_message = sprintf( '%s (%s): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $kind, $address ); @@ -1349,7 +1349,7 @@ class PHPMailer ) { $error_message = sprintf( '%s (From): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $address ); $this->setError($error_message); @@ -1605,7 +1605,7 @@ class PHPMailer && ini_get('mail.add_x_header') === '1' && stripos(PHP_OS, 'WIN') === 0 ) { - trigger_error($this->lang('buggy_php'), E_USER_WARNING); + trigger_error(self::lang('buggy_php'), E_USER_WARNING); } try { @@ -1635,7 +1635,7 @@ class PHPMailer call_user_func_array([$this, 'addAnAddress'], $params); } if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { - throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); + throw new Exception(self::lang('provide_address'), self::STOP_CRITICAL); } //Validate From, Sender, and ConfirmReadingTo addresses @@ -1652,7 +1652,7 @@ class PHPMailer if (!static::validateAddress($this->{$address_kind})) { $error_message = sprintf( '%s (%s): %s', - $this->lang('invalid_address'), + self::lang('invalid_address'), $address_kind, $this->{$address_kind} ); @@ -1674,7 +1674,7 @@ class PHPMailer $this->setMessageType(); //Refuse to send an empty message unless we are specifically allowing it if (!$this->AllowEmpty && empty($this->Body)) { - throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + throw new Exception(self::lang('empty_message'), self::STOP_CRITICAL); } //Trim subject consistently @@ -1834,7 +1834,7 @@ class PHPMailer foreach ($this->SingleToArray as $toAddr) { $mail = @popen($sendmail, 'w'); if (!$mail) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } $this->edebug("To: {$toAddr}"); fwrite($mail, 'To: ' . $toAddr . "\n"); @@ -1856,13 +1856,13 @@ class PHPMailer } $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } } else { $mail = @popen($sendmail, 'w'); if (!$mail) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } fwrite($mail, $header); fwrite($mail, $body); @@ -1879,7 +1879,7 @@ class PHPMailer ); $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { - throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } @@ -2040,7 +2040,7 @@ class PHPMailer ini_set('sendmail_from', $old_from); } if (!$result) { - throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); + throw new Exception(self::lang('instantiate'), self::STOP_CRITICAL); } return true; @@ -2126,12 +2126,12 @@ class PHPMailer $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; $bad_rcpt = []; if (!$this->smtpConnect($this->SMTPOptions)) { - throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); + throw new Exception(self::lang('smtp_connect_failed'), self::STOP_CRITICAL); } //If we have recipient addresses that need Unicode support, //but the server doesn't support it, stop here if ($this->UseSMTPUTF8 && !$this->smtp->getServerExt('SMTPUTF8')) { - throw new Exception($this->lang('no_smtputf8'), self::STOP_CRITICAL); + throw new Exception(self::lang('no_smtputf8'), self::STOP_CRITICAL); } //Sender already validated in preSend() if ('' === $this->Sender) { @@ -2143,7 +2143,7 @@ class PHPMailer $this->smtp->xclient($this->SMTPXClient); } if (!$this->smtp->mail($smtp_from)) { - $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + $this->setError(self::lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); } @@ -2165,7 +2165,7 @@ class PHPMailer //Only send the DATA command if we have viable recipients if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) { - throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); + throw new Exception(self::lang('data_not_accepted'), self::STOP_CRITICAL); } $smtp_transaction_id = $this->smtp->getLastTransactionID(); @@ -2196,7 +2196,7 @@ class PHPMailer foreach ($bad_rcpt as $bad) { $errstr .= $bad['to'] . ': ' . $bad['error']; } - throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE); + throw new Exception(self::lang('recipients_failed') . $errstr, self::STOP_CONTINUE); } return true; @@ -2250,7 +2250,7 @@ class PHPMailer $hostinfo ) ) { - $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry)); + $this->edebug(self::lang('invalid_hostentry') . ' ' . trim($hostentry)); //Not a valid host entry continue; } @@ -2262,7 +2262,7 @@ class PHPMailer //Check the host name is a valid name or IP address before trying to use it if (!static::isValidHost($hostinfo[2])) { - $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); + $this->edebug(self::lang('invalid_host') . ' ' . $hostinfo[2]); continue; } $prefix = ''; @@ -2282,7 +2282,7 @@ class PHPMailer if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) { //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled if (!$sslext) { - throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); + throw new Exception(self::lang('extension_missing') . 'openssl', self::STOP_CRITICAL); } } $host = $hostinfo[2]; @@ -2334,7 +2334,7 @@ class PHPMailer $this->oauth ) ) { - throw new Exception($this->lang('authenticate')); + throw new Exception(self::lang('authenticate')); } return true; @@ -2384,7 +2384,7 @@ class PHPMailer * * @return bool Returns true if the requested language was loaded, false otherwise. */ - public function setLanguage($langcode = 'en', $lang_path = '') + public static function setLanguage($langcode = 'en', $lang_path = '') { //Backwards compatibility for renamed language codes $renamed_langcodes = [ @@ -2499,7 +2499,7 @@ class PHPMailer } } } - $this->language = $PHPMAILER_LANG; + self::$language = $PHPMAILER_LANG; return $foundlang; //Returns false if language not found } @@ -2511,11 +2511,11 @@ class PHPMailer */ public function getTranslations() { - if (empty($this->language)) { - $this->setLanguage(); // Set the default language. + if (empty(self::$language)) { + self::setLanguage(); // Set the default language. } - return $this->language; + return self::$language; } /** @@ -3154,12 +3154,12 @@ class PHPMailer if ($this->isError()) { $body = ''; if ($this->exceptions) { - throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); + throw new Exception(self::lang('empty_message'), self::STOP_CRITICAL); } } elseif ($this->sign_key_file) { try { if (!defined('PKCS7_TEXT')) { - throw new Exception($this->lang('extension_missing') . 'openssl'); + throw new Exception(self::lang('extension_missing') . 'openssl'); } $file = tempnam(sys_get_temp_dir(), 'srcsign'); @@ -3197,7 +3197,7 @@ class PHPMailer $body = $parts[1]; } else { @unlink($signed); - throw new Exception($this->lang('signing') . openssl_error_string()); + throw new Exception(self::lang('signing') . openssl_error_string()); } } catch (Exception $exc) { $body = ''; @@ -3342,7 +3342,7 @@ class PHPMailer ) { try { if (!static::fileIsAccessible($path)) { - throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name @@ -3355,7 +3355,7 @@ class PHPMailer $name = $filename; } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } $this->attachment[] = [ @@ -3516,11 +3516,11 @@ class PHPMailer { try { if (!static::fileIsAccessible($path)) { - throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = file_get_contents($path); if (false === $file_buffer) { - throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = $this->encodeString($file_buffer, $encoding); @@ -3573,9 +3573,9 @@ class PHPMailer $encoded = $this->encodeQP($str); break; default: - $this->setError($this->lang('encoding') . $encoding); + $this->setError(self::lang('encoding') . $encoding); if ($this->exceptions) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } break; } @@ -3850,7 +3850,7 @@ class PHPMailer } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } //Append to $attachment array @@ -3909,7 +3909,7 @@ class PHPMailer ) { try { if (!static::fileIsAccessible($path)) { - throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); + throw new Exception(self::lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name @@ -3918,7 +3918,7 @@ class PHPMailer } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); @@ -3984,7 +3984,7 @@ class PHPMailer } if (!$this->validateEncoding($encoding)) { - throw new Exception($this->lang('encoding') . $encoding); + throw new Exception(self::lang('encoding') . $encoding); } //Append to $attachment array @@ -4241,7 +4241,7 @@ class PHPMailer } if (strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { - throw new Exception($this->lang('invalid_header')); + throw new Exception(self::lang('invalid_header')); } return false; @@ -4265,15 +4265,15 @@ class PHPMailer if ('smtp' === $this->Mailer && null !== $this->smtp) { $lasterror = $this->smtp->getError(); if (!empty($lasterror['error'])) { - $msg .= ' ' . $this->lang('smtp_error') . $lasterror['error']; + $msg .= ' ' . self::lang('smtp_error') . $lasterror['error']; if (!empty($lasterror['detail'])) { - $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail']; + $msg .= ' ' . self::lang('smtp_detail') . $lasterror['detail']; } if (!empty($lasterror['smtp_code'])) { - $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code']; + $msg .= ' ' . self::lang('smtp_code') . $lasterror['smtp_code']; } if (!empty($lasterror['smtp_code_ex'])) { - $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex']; + $msg .= ' ' . self::lang('smtp_code_ex') . $lasterror['smtp_code_ex']; } } } @@ -4398,21 +4398,21 @@ class PHPMailer * * @return string */ - protected function lang($key) + protected static function lang($key) { - if (count($this->language) < 1) { - $this->setLanguage(); //Set the default language + if (count(self::$language) < 1) { + self::setLanguage(); //Set the default language } - if (array_key_exists($key, $this->language)) { + if (array_key_exists($key, self::$language)) { if ('smtp_connect_failed' === $key) { //Include a link to troubleshooting docs on SMTP connection failure. //This is by far the biggest cause of support questions //but it's usually not PHPMailer's fault. - return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + return self::$language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; } - return $this->language[$key]; + return self::$language[$key]; } //Return the key as a fallback @@ -4427,7 +4427,7 @@ class PHPMailer */ private function getSmtpErrorMessage($base_key) { - $message = $this->lang($base_key); + $message = self::lang($base_key); $error = $this->smtp->getError(); if (!empty($error['error'])) { $message .= ' ' . $error['error']; @@ -4471,7 +4471,7 @@ class PHPMailer //Ensure name is not empty, and that neither name nor value contain line breaks if (empty($name) || strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { - throw new Exception($this->lang('invalid_header')); + throw new Exception(self::lang('invalid_header')); } return false; @@ -4864,7 +4864,7 @@ class PHPMailer return true; } - $this->setError($this->lang('variable_set') . $name); + $this->setError(self::lang('variable_set') . $name); return false; } @@ -5002,7 +5002,7 @@ class PHPMailer { if (!defined('PKCS7_TEXT')) { if ($this->exceptions) { - throw new Exception($this->lang('extension_missing') . 'openssl'); + throw new Exception(self::lang('extension_missing') . 'openssl'); } return ''; diff --git a/test/PHPMailer/LocalizationTest.php b/test/PHPMailer/LocalizationTest.php index d24a424d..4af752a3 100644 --- a/test/PHPMailer/LocalizationTest.php +++ b/test/PHPMailer/LocalizationTest.php @@ -15,6 +15,7 @@ namespace PHPMailer\Test\PHPMailer; use ReflectionMethod; use PHPMailer\Test\TestCase; +use PHPMailer\PHPMailer\PHPMailer; /** * Test localized error message functionality. @@ -443,12 +444,12 @@ final class LocalizationTest extends TestCase public function testLang($input, $expected, $langCode = null) { if (isset($langCode)) { - $this->Mail->setLanguage($langCode); + PHPMailer::setLanguage($langCode); } - $reflMethod = new ReflectionMethod($this->Mail, 'lang'); + $reflMethod = new ReflectionMethod(PHPMailer::class, 'lang'); (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true); - $result = $reflMethod->invoke($this->Mail, $input); + $result = $reflMethod->invoke(null, $input); (\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false); self::assertSame($expected, $result); diff --git a/test/TestCase.php b/test/TestCase.php index 40fbc8e5..c9ec7a26 100644 --- a/test/TestCase.php +++ b/test/TestCase.php @@ -119,6 +119,7 @@ abstract class TestCase extends PolyfillTestCase private $PHPMailerStaticProps = [ 'LE' => PHPMailer::CRLF, 'validator' => 'php', + 'language' => [], ]; /** From 22885eaf4a8e380a3fb8dbe48fd390778e0c135a Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sat, 23 Aug 2025 16:40:10 +0200 Subject: [PATCH 07/22] Preparing new version for no-IMAP, Tests Pending --- composer.json | 3 +- language/phpmailer.lang-es.php | 5 +- src/PHPMailer.php | 92 +++++++++++++++++++++------------- 3 files changed, 61 insertions(+), 39 deletions(-) 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 5b0ba5c0..5388f4fc 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -1280,47 +1280,65 @@ 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 + * of 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, + ]; + } + } + } + } + /** * Set the From and FromName properties. * @@ -2433,6 +2451,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 From 69a2b8038f1e61749dde494c3f773fcd98dca731 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 24 Aug 2025 15:37:42 +0200 Subject: [PATCH 08/22] Removing Tests. Reintroducing them fixed in #3197 --- src/PHPMailer.php | 3 +- test/PHPMailer/ParseAddressesTest.php | 62 --------------------------- 2 files changed, 2 insertions(+), 63 deletions(-) diff --git a/src/PHPMailer.php b/src/PHPMailer.php index f82cad59..69bd3fd3 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -1288,7 +1288,7 @@ class PHPMailer /** * Parse a string containing one or more RFC822-style comma-separated email addresses - * of the form "display name
" into an array of name/address pairs. + * 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. * @@ -1301,6 +1301,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); + $list = explode(',', $addrstr); foreach ($list as $address) { $address = trim($address); diff --git a/test/PHPMailer/ParseAddressesTest.php b/test/PHPMailer/ParseAddressesTest.php index f6188b90..1df2e9c2 100644 --- a/test/PHPMailer/ParseAddressesTest.php +++ b/test/PHPMailer/ParseAddressesTest.php @@ -28,36 +28,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 +59,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. From e72170c6ba41abde45f640ef8742e39ed0710bb3 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 24 Aug 2025 15:38:03 +0200 Subject: [PATCH 09/22] Little separator line --- src/PHPMailer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PHPMailer.php b/src/PHPMailer.php index 69bd3fd3..577d33f6 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -1301,7 +1301,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); - + $list = explode(',', $addrstr); foreach ($list as $address) { $address = trim($address); From 3c93e8d6f3df3875ef8a112216fa154cb0ed81a1 Mon Sep 17 00:00:00 2001 From: SirLouen Date: Sun, 24 Aug 2025 16:03:51 +0200 Subject: [PATCH 10/22] Adding Temporary Specific Tests for Native Function --- src/PHPMailer.php | 2 ++ test/PHPMailer/ParseAddressesTest.php | 47 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/PHPMailer.php b/src/PHPMailer.php index 577d33f6..92203715 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -1338,6 +1338,8 @@ class PHPMailer } } } + + return $addresses; } /** diff --git a/test/PHPMailer/ParseAddressesTest.php b/test/PHPMailer/ParseAddressesTest.php index 1df2e9c2..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; /** @@ -93,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. * From fd9f8d33cb7faa87a7a2b7a4ee579bdc05dbbe1f Mon Sep 17 00:00:00 2001 From: Georg Sieber Date: Thu, 11 Sep 2025 16:07:09 +0200 Subject: [PATCH 11/22] fix encoding header for SMIME signed messages with long lines --- src/PHPMailer.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/PHPMailer.php b/src/PHPMailer.php index 92203715..0596d166 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -2961,10 +2961,6 @@ class PHPMailer //Create unique IDs and preset boundaries $this->setBoundaries(); - if ($this->sign_key_file) { - $body .= $this->getMailMIME() . static::$LE; - } - $this->setWordWrap(); $bodyEncoding = $this->Encoding; @@ -2996,6 +2992,12 @@ class PHPMailer if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) { $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE; } + + if ($this->sign_key_file) { + $this->Encoding = $bodyEncoding; + $body .= $this->getMailMIME() . static::$LE; + } + //Use this as a preamble in all multipart message types $mimepre = ''; switch ($this->message_type) { From 5470c1e7952d5cf5caf5c196b288f48f05b9f157 Mon Sep 17 00:00:00 2001 From: Georg Sieber Date: Thu, 11 Sep 2025 22:52:01 +0200 Subject: [PATCH 12/22] add test --- .../HasLineLongerThanMaxTest/cert.pem | 31 +++++++++++ .../Fixtures/HasLineLongerThanMaxTest/key.pem | 52 +++++++++++++++++++ test/PHPMailer/HasLineLongerThanMaxTest.php | 40 ++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 test/Fixtures/HasLineLongerThanMaxTest/cert.pem create mode 100644 test/Fixtures/HasLineLongerThanMaxTest/key.pem diff --git a/test/Fixtures/HasLineLongerThanMaxTest/cert.pem b/test/Fixtures/HasLineLongerThanMaxTest/cert.pem new file mode 100644 index 00000000..e7c90b30 --- /dev/null +++ b/test/Fixtures/HasLineLongerThanMaxTest/cert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIUPVCD/ME/tR7lrcNY0eLMignHqywwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNTA5MTExOTI4MDdaFw0zNTA5 +MDkxOTI4MDdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDHDtSUM73CqVfUt5hhBIbz56ENE746CqgqCqYpKypQ +frCDbcaRWagg9JOy4k9BChB4/B8wZilF9vsmfFoIa0H+LmQWQLN1pVx2tuSWI9rw +CdmTm6cXFZCxleOQMFxzmzV53gK9y2YRxAYL/hm6mcWp6Rblv0SqyxBz+GPJLLrr +cVRIgkktEia7ENA56DWpLoi49xYUwnDN3o+PwtrPGEzwsH/25zhEyS1LlcfRM3pY +W9UGX8HtU1LB94dWoVWNvISFvjicCWhVsuNw1Z0tIko499iEQG+zezbmh++n9a2G +bkCaI6dFZL5pHakmKOTYKyZ1sprE4799KDSTd8hlPfHboC4ClWqIiI6ou3kEpJln +sdsNZP5vPHrDgjuW/oE+zsQjmkaJiWaZphpthyYkR32Xu7HPvtQT4MHfkrs/SFE0 +43ml8CqrGSa+IjSjI+HXMwsf0mRmEtK7PcqVLhdSWAGjMNPjJ+er/O+PX3ZAWrbl +GzJfYU5LAk1ES/8uKpB+TjAXDL8xyM0+aP0axEeU57SyTNqbVfimrA250KZ+Q3hk +dpTWlTEjCXhxGHXdiJJwFPyanCNstFuKgNHTbmdRTMKIQ+Wmu5EgUSH2GcRZg9oO +t2veQP3EIc9dIxzijUFETWuBqzi80D6rKJJ1KowJE0rdh7owI/SCHNOYgjSN5a0G +XQIDAQABo1MwUTAdBgNVHQ4EFgQUzSwRCSiJnYQsy99FkcsdWzHJjIwwHwYDVR0j +BBgwFoAUzSwRCSiJnYQsy99FkcsdWzHJjIwwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAVZApbeRypzpwv2d8B/kPcIRcq5Kot0HhTDr9CNvGqU0G +TwQrVyIVAzi0uX+Ki7flj3+bo1br9xR/ocKbnTbEA3ofCxEbf0KGEjiwvB7tAg22 +UeFBxdAZG2IJcwwmY779IHKmjmFgrWGbXTirrN2a3i5TYU/nrTp7yY3GFQFujt5q +hQXBnkEvubS3n9ImdA0ByWCgmYiS08v8HGgsgGs9xVe1idkDkD+5I1imCADvUh7I +0ZksoB/XpdHRaqTRF0h6G2EUXznOG7x04uG4tiHkim1W4IkBBVTLxp6iul9n8GAe +QoZadHGaPIeytwl7A986Qo78WIltxZC+SBjJeQJG7/qHt/MvB8dBXZ49zg1SmHeV +ZtBWdtC0LBGcLoImm9m7DCyA9xMqSKSoOqmzXTlWcKQnPi3MeI5dqfWzuvk9LyLg +71hXXF4EnTgZpHw1ZWJBI47jEfsH2G0c7X46HPYjD4XcDCChNG81d0xQpZMbq5J7 +jy6PSbE/iGghEOuiF1NpQsrAnlf0UAzA27bUPyX0NFOmQmAejc8b6NqIljSQE/Xm +MOZlE4RpIa63EzzDo1fas8hhUhtz3loYysHN+nmw4N0BRjVenxatJXiunIbsDkzJ +VeBTrddQlqszd0qOlGCpJM0uhHuVDPntHKmFLo7O32aH2TgvYakpp54Xd+4xIqM= +-----END CERTIFICATE----- diff --git a/test/Fixtures/HasLineLongerThanMaxTest/key.pem b/test/Fixtures/HasLineLongerThanMaxTest/key.pem new file mode 100644 index 00000000..6d7ec066 --- /dev/null +++ b/test/Fixtures/HasLineLongerThanMaxTest/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +ujKpHkhiv0V4kA8yZpxQDcXp84JE +-----END PRIVATE KEY----- diff --git a/test/PHPMailer/HasLineLongerThanMaxTest.php b/test/PHPMailer/HasLineLongerThanMaxTest.php index af6118d6..f9618df9 100644 --- a/test/PHPMailer/HasLineLongerThanMaxTest.php +++ b/test/PHPMailer/HasLineLongerThanMaxTest.php @@ -56,6 +56,46 @@ final class HasLineLongerThanMaxTest extends PreSendTestCase ); } + /** + * Test constructing a SMIME signed message that contains lines that are too long for RFC compliance. + * + * @covers \PHPMailer\PHPMailer\PHPMailer::hasLineLongerThanMax + */ + public function testLongBodySmime() + { + $oklen = str_repeat(str_repeat('0', PHPMailer::MAX_LINE_LENGTH) . PHPMailer::getLE(), 2); + // Use +2 to ensure line length is over limit - LE may only be 1 char. + $badlen = str_repeat(str_repeat('1', PHPMailer::MAX_LINE_LENGTH + 2) . PHPMailer::getLE(), 2); + + $this->Mail->Body = 'This message contains lines that are too long.' . + PHPMailer::getLE() . $oklen . $badlen . $oklen; + self::assertTrue( + PHPMailer::hasLineLongerThanMax($this->Mail->Body), + 'Test content does not contain long lines!' + ); + + $this->Mail->isHTML(); + $this->buildBody(); + #$this->Mail->AltBody = $this->Mail->Body; + $this->Mail->Encoding = '8bit'; + $this->Mail->sign( + __DIR__.'/../Fixtures/HasLineLongerThanMaxTest/cert.pem', + __DIR__.'/../Fixtures/HasLineLongerThanMaxTest/key.pem', + null, + ); + $this->Mail->preSend(); + $message = $this->Mail->getSentMIMEMessage(); + self::assertFalse( + PHPMailer::hasLineLongerThanMax($message), + 'Long line not corrected (Max: ' . (PHPMailer::MAX_LINE_LENGTH + strlen(PHPMailer::getLE())) . ' chars)' + ); + self::assertStringContainsString( + 'Content-Transfer-Encoding: quoted-printable', + $message, + 'Long line did not cause transfer encoding switch.' + ); + } + /** * Test constructing a message that does NOT contain lines that are too long for RFC compliance. * From 4c917a9b46130945b655901c9e0e0cac1bcbb236 Mon Sep 17 00:00:00 2001 From: Marcus Bointon Date: Thu, 11 Sep 2025 23:16:50 +0200 Subject: [PATCH 13/22] CS --- test/PHPMailer/HasLineLongerThanMaxTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/PHPMailer/HasLineLongerThanMaxTest.php b/test/PHPMailer/HasLineLongerThanMaxTest.php index f9618df9..7fffee6d 100644 --- a/test/PHPMailer/HasLineLongerThanMaxTest.php +++ b/test/PHPMailer/HasLineLongerThanMaxTest.php @@ -79,9 +79,9 @@ final class HasLineLongerThanMaxTest extends PreSendTestCase #$this->Mail->AltBody = $this->Mail->Body; $this->Mail->Encoding = '8bit'; $this->Mail->sign( - __DIR__.'/../Fixtures/HasLineLongerThanMaxTest/cert.pem', - __DIR__.'/../Fixtures/HasLineLongerThanMaxTest/key.pem', - null, + __DIR__ . '/../Fixtures/HasLineLongerThanMaxTest/cert.pem', + __DIR__ . '/../Fixtures/HasLineLongerThanMaxTest/key.pem', + null ); $this->Mail->preSend(); $message = $this->Mail->getSentMIMEMessage(); From 9f0387fb376fc288d07eca81ef6c3d2e92957913 Mon Sep 17 00:00:00 2001 From: Marcus Bointon Date: Thu, 18 Sep 2025 17:24:32 +0200 Subject: [PATCH 14/22] Changelog --- changelog.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelog.md b/changelog.md index 272c73be..ccfb0133 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,11 @@ * Add support for [RFC4954](https://www.rfc-editor.org/rfc/rfc4954#section-4) two-part authentication for large XOAUTH2 tokens. * Also support empty tokens. * Avoid bogus static analyser deprecation warnings in `setFrom`. +* Make language loading entirely static, thanks to @SirLouen. +* Emit warnings when `parseAddresses()` is used without IMAP extension. +* Fix PHP 8.5 linting issue. +* Don't use `-t` switch when calling qmail. +* Checking for interrupted system calls now works in languages other than English. ## Version 6.10.0 (April 24th, 2025) * Add support for [RFC 6530 SMTPUTF8](https://www.rfc-editor.org/rfc/rfc6530), permitting use of UTF-8 Unicode characters everywhere, thanks to @arnt and ICANN. See `SMTPUTF8.md` for details. From 6f0f7a7f68d776890cbfca985917fb2ba39123b3 Mon Sep 17 00:00:00 2001 From: Marcus Bointon Date: Thu, 18 Sep 2025 17:25:26 +0200 Subject: [PATCH 15/22] Extract gmail transaction IDs, fixes #3224 --- changelog.md | 1 + src/SMTP.php | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.md b/changelog.md index ccfb0133..dd585eb9 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ * Fix PHP 8.5 linting issue. * Don't use `-t` switch when calling qmail. * Checking for interrupted system calls now works in languages other than English. +* Add support for extracting gmail transaction IDs after sending. ## Version 6.10.0 (April 24th, 2025) * Add support for [RFC 6530 SMTPUTF8](https://www.rfc-editor.org/rfc/rfc6530), permitting use of UTF-8 Unicode characters everywhere, thanks to @arnt and ICANN. See `SMTPUTF8.md` for details. diff --git a/src/SMTP.php b/src/SMTP.php index 4181eaea..7c359cf7 100644 --- a/src/SMTP.php +++ b/src/SMTP.php @@ -205,6 +205,7 @@ class SMTP 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/', 'ZoneMTA' => '/[\d]{3} Message queued as (.*)/', 'Mailjet' => '/[\d]{3} OK queued as (.*)/', + 'Gsmtp' => '/[\d]{3} 2\.0\.0 OK (.*) - gsmtp/', ]; /** From 63540d8cf33f55fcec637f98b14b53f8022d1983 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 20 Sep 2025 05:08:06 +0200 Subject: [PATCH 16/22] GH Actions/scorecard: update permissions ... to match the current recommendations. I've removed the "read" permissions as those should only be needed for "private" repos. Ref: https://github.com/ossf/scorecard-action#additional-permissions-for-private-repositories --- .github/workflows/scorecards.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 06fa35ac..5d11aaa4 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -7,8 +7,7 @@ on: push: branches: [ "master" ] -# Declare default permissions as read only. -permissions: read-all +permissions: {} jobs: analysis: @@ -17,15 +16,12 @@ jobs: name: Scorecards analysis runs-on: ubuntu-latest + permissions: - # Needed to upload the results to code-scanning dashboard. + # Required when publishing results (badge / API / code scanning) security-events: write - # Used to receive a badge. (Upcoming feature) id-token: write - # Needs for private repositories. - contents: read - actions: read - + steps: - name: "Checkout code" uses: actions/checkout@v4 From 086dfbe727e88792528ff96685cc76c6fc995434 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 20 Sep 2025 04:54:05 +0200 Subject: [PATCH 17/22] GH Actions: "pin" all action runners Recently there has been more and more focus on securing GH Actions workflows - in part due to some incidents. The problem with "unpinned" action runners is as follows: * Tags are mutable, which means that a tag could point to a safe commit today, but to a malicious commit tomorrow. Note that GitHub is currently beta-testing a new "immutable releases" feature (= tags and release artifacts can not be changed anymore once the release is published), but whether that has much effect depends on the ecosystem of the packages using the feature. Aside from that, it will likely take years before all projects adopt _immutable releases_. * Action runners often don't even point to a tag, but to a branch, making the used action runner a moving target. _Note: this type of "floating major" for action runners used to be promoted as good practice when the ecosystem was "young". Insights have since changed._ While it is convenient to use "floating majors" of action runners, as this means you only need to update the workflows on a new major release of the action runner, the price is higher risk of malicious code being executed in workflows. Dependabot, by now, can automatically submit PRs to update pinned action runners too, as long as the commit-hash pinned runner is followed by a comment listing the released version the commit is pointing to. So, what with Dependabot being capable of updating workflows with pinned action runners, I believe it is time to update the workflows to the _current_ best practice of using commit-hash pinned action runners. The downside of this change is that there will be more frequent Dependabot PRs. If this would become a burden/irritating, the following mitigations can be implemented: 1. Updating the Dependabot config to group updates instead of sending individual PRs per action runner. 2. A workflow to automatically merge Dependabot PRs as long as CI passes. Includes updating the version for `ossf/scorecard-action` as it was a couple of version behind. Ref: https://docs.github.com/en/actions/reference/security/secure-use#using-third-party-actions --- .github/workflows/docs.yaml | 4 ++-- .github/workflows/scorecards.yml | 8 ++++---- .github/workflows/tests.yml | 24 ++++++++++++------------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index f30a48e9..e2e15846 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -14,13 +14,13 @@ jobs: if: github.repository == 'PHPMailer/PHPMailer' steps: - name: Checkout sources - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: fetch-depth: 1 - name: Build Docs uses: ./.github/actions/build-docs - name: Publish Docs to gh-pages - uses: JamesIves/github-pages-deploy-action@v4 + uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # v4.7.3 with: branch: gh-pages folder: docs diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 06fa35ac..6bf1531b 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -28,12 +28,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 + uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif @@ -52,7 +52,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif @@ -60,6 +60,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 with: sarif_file: results.sarif diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ec6e7ddb..5c7e6f92 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,10 +17,10 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Set up PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 with: php-version: 'latest' coverage: none @@ -29,7 +29,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -57,10 +57,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - name: Install PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 with: php-version: ${{ matrix.php }} ini-values: error_reporting=-1, display_errors=On, display_startup_errors=On @@ -70,7 +70,7 @@ jobs: # Install dependencies and handle caching in one go. # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install Composer dependencies - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1 with: # Bust the cache at least once a month - output format: YYYY-MM. custom-cache-suffix: $(date -u "+%Y-%m") @@ -127,7 +127,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@v4 + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 # About the "extensions": # @@ -157,7 +157,7 @@ jobs: fi - name: Set up PHP - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 with: php-version: ${{ matrix.php }} coverage: ${{ matrix.coverage && 'xdebug' || 'none' }} @@ -168,7 +168,7 @@ jobs: # @link https://github.com/marketplace/actions/install-php-dependencies-with-composer - name: Install PHP packages - normal if: ${{ matrix.php != '8.5' }} - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1 with: composer-options: ${{ steps.set_extensions.outputs.COMPOSER_OPTIONS }} # Bust the cache at least once a month - output format: YYYY-MM. @@ -176,7 +176,7 @@ jobs: - name: Install PHP packages - ignore-platform-reqs if: ${{ matrix.php == '8.5' }} - uses: "ramsey/composer-install@v3" + uses: "ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520" # 3.1.1 with: composer-options: --ignore-platform-reqs ${{ steps.set_extensions.outputs.COMPOSER_OPTIONS }} # Bust the cache at least once a month - output format: YYYY-MM. @@ -185,7 +185,7 @@ jobs: # Install postfix and automatically retry if the install failed, which happens reguarly. # @link https://github.com/marketplace/actions/retry-step - name: Install postfix - uses: nick-invision/retry@v3 + uses: nick-invision/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2 with: timeout_minutes: 2 max_attempts: 3 @@ -214,7 +214,7 @@ jobs: - name: Send coverage report to Codecov if: ${{ success() && matrix.coverage == true }} - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: From 5ce9b04aae68d37ad4ff60b0b08557e765485e14 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 20 Sep 2025 04:54:51 +0200 Subject: [PATCH 18/22] Dependabot: update config This commit makes the following change to the Dependabot config: * It introduces a "group". By default Dependabot raises individual PRs for each update. Now, it will group updates to new minor or patch release for all action runners into a single PR. Updates to new major releases of action runners will still be raised as individual PRs. Refs: * https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/optimizing-pr-creation-version-updates * https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d727b17c..8a7997b3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,9 @@ updates: open-pull-requests-limit: 5 commit-message: prefix: "GH Actions:" + groups: + action-runners: + applies-to: version-updates + update-types: + - "minor" + - "patch" From 0d6eaeb3a9e1c4052a6fccec5dff08b7d14bc5f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Sep 2025 10:20:29 +0000 Subject: [PATCH 19/22] GH Actions: Bump actions/checkout from 4.3.0 to 5.0.0 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.3.0 to 5.0.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/08eba0b27e820071cde6df949e0beb9ba4906955...08c6903cd8c0fde910a37f88322edcfb5dd907a8) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 5.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/docs.yaml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/tests.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index e2e15846..a60dec0e 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -14,7 +14,7 @@ jobs: if: github.repository == 'PHPMailer/PHPMailer' steps: - name: Checkout sources - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: fetch-depth: 1 - name: Build Docs diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 6bf1531b..a8f3b55d 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -28,7 +28,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5c7e6f92..27578e83 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Set up PHP uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 @@ -57,7 +57,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Install PHP uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 @@ -127,7 +127,7 @@ jobs: steps: - name: Check out code - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 # About the "extensions": # From a20929910526e341cc9ba0805a2ab2aa53cd1a03 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 20 Sep 2025 05:01:54 +0200 Subject: [PATCH 20/22] GH Actions: do not persist credentials > By default, using `actions/checkout` causes a credential to be persisted in the checked-out repo's `.git/config`, so that subsequent `git` operations can be authenticated. > > Subsequent steps may accidentally publicly persist `.git/config`, e.g. by including it in a publicly accessible artifact via `actions/upload-artifact`. > > However, even without this, persisting the credential in the `.git/config` is non-ideal unless actually needed. > > **Remediation** > > Unless needed for `git` operations, `actions/checkout` should be used with `persist-credentials: false`. > > If the persisted credential is needed, it should be made explicit with `persist-credentials: true`. This has now been addressed in all workflows. Refs: * https://unit42.paloaltonetworks.com/github-repo-artifacts-leak-tokens/ * https://docs.zizmor.sh/audits/#artipacked --- .github/workflows/docs.yaml | 1 + .github/workflows/tests.yml | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index e2e15846..1e058a11 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -17,6 +17,7 @@ jobs: uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 with: fetch-depth: 1 + persist-credentials: false - name: Build Docs uses: ./.github/actions/build-docs - name: Publish Docs to gh-pages diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5c7e6f92..87638646 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,6 +18,8 @@ jobs: steps: - name: Check out code uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Set up PHP uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 @@ -58,6 +60,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false - name: Install PHP uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 @@ -128,6 +132,8 @@ jobs: steps: - name: Check out code uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 + with: + persist-credentials: false # About the "extensions": # From eef3fef3ae1a48b4e42ba585f2bdce8e7ef31a80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 21 Sep 2025 13:59:33 +0000 Subject: [PATCH 21/22] GH Actions: Bump codecov/codecov-action from 4 to 5 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 4 to 5. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v4...v5) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 27578e83..e2196b77 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -214,7 +214,7 @@ jobs: - name: Send coverage report to Codecov if: ${{ success() && matrix.coverage == true }} - uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: From c8fdd4178e907f17779446c4ffbde76f06d69d99 Mon Sep 17 00:00:00 2001 From: jrfnl Date: Sat, 20 Sep 2025 05:08:24 +0200 Subject: [PATCH 22/22] GH Actions: set permissions for each workflow/job > Users frequently over-scope their workflow and job permissions, or set broad workflow-level permissions without realizing that all jobs inherit those permissions. > > Furthermore, users often don't realize that the _default_ `GITHUB_TOKEN` permissions can be very broad, meaning that workflows that don't configure any permissions at all can _still_ provide excessive credentials to their individual jobs. > > **Remediation** > In general, permissions should be declared as minimally as possible, and as close to their usage site as possible. > > In practice, this means that workflows should almost always set `permissions: {}` at the workflow level to disable all permissions by default, and then set specific job-level permissions as needed. This was already addressed for the other two workflows, just not for the `tests` one. As far as I can see, the jobs here do not need the `GITHUB_TOKEN` secret and even if they do, only for `content: read`, which for public repos does not need to be set explicitly, though it doesn't do any harm to have that set anyway. Refs: * https://docs.zizmor.sh/audits/#excessive-permissions --- .github/workflows/tests.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 65127019..30ba6233 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,8 +6,7 @@ on: # Allow manually triggering the workflow. workflow_dispatch: -permissions: - contents: read # to fetch code (actions/checkout) +permissions: {} jobs: @@ -15,6 +14,9 @@ jobs: runs-on: ubuntu-22.04 name: Coding standards + permissions: + contents: read # to fetch code (actions/checkout) + steps: - name: Check out code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -57,6 +59,9 @@ jobs: name: "Lint: PHP ${{ matrix.php }}" continue-on-error: ${{ matrix.experimental }} + permissions: + contents: read # to fetch code (actions/checkout) + steps: - name: Checkout code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -129,6 +134,9 @@ jobs: continue-on-error: ${{ matrix.experimental }} + permissions: + contents: read # to fetch code (actions/checkout) + steps: - name: Check out code uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0