Compare commits

...
10 Commits
Author SHA1 Message Date
dualfroz 124f69fae3 Describe this repository 2026-10-22 21:02:15 +02:00
dualfroz 5f8d078d57 Keep the charset label for 7-bit ISO-2022 header values
encodeHeader() fell back to us-ascii for any header value without 8-bit
chars, which mislabelled ISO-2022-JP text. Values containing ESC escape
sequences now keep the message charset.

Fixes #2185
2026-10-14 18:51:49 +02:00
dualfroz 4ce8260f80 Spell out the folding arithmetic in addrAppend()
Name the separators that make up the line length instead of the bare
3, and refer to RFC 2822 like MAX_LINE_LENGTH does.
2026-10-03 18:30:36 +02:00
upstream ce1b3ed5e6 Upstream PHPMailer/PHPMailer state of the touched files before 5b7250d8da 2026-10-03 18:30:36 +02:00
dualfroz 1ac61eecb6 Fold the To list passed to mail()
mailSend() joined the To addresses for mail() with its own implode, so
a long list still went out as one line, while the To header used for
getSentMIMEMessage() and DKIM signing is folded by addrAppend(). mail()
keeps CRLF + space folds in its to argument, so build it with
addrAppend() too.
2026-10-04 12:54:41 +02:00
upstream 95d2378b6a Upstream PHPMailer/PHPMailer state of the touched files before 7956cb743f 2026-10-03 01:37:31 +02:00
dualfroz 0825c8cc83 Don't send QUIT while DATA is in progress
If the connection is closed in the middle of a DATA command, for example
from a destructor after SIGTERM, the server takes QUIT as part of the
message and the connection only ends when it times out. SMTP now keeps
the last command it sent in a state property, and quit() just closes the
connection when that command is DATA, as discussed in #3121.

Fixes #2994
2026-10-02 16:51:33 +02:00
upstream a1bc7fa8d1 Upstream PHPMailer/PHPMailer state of the touched files before b1849ac763 2026-10-02 16:51:33 +02:00
dualfroz 94041263a0 Fold long address headers
addrAppend() joined all addresses onto one line, so a To, Cc, Bcc or
Reply-To header with many recipients could go past the 998 character
limit in RFC 5322. Fold between addresses when the next one would not
fit. Short lists are unchanged.

Fixes #2690
2026-10-02 16:44:53 +02:00
dualfroz 709e2d5b62 Normalise line breaks in S/MIME signed messages
openssl_pkcs7_sign() returns its headers and body with bare LF line
breaks. They were copied into the message verbatim, so signed mail was
rejected by SMTP servers that enforce RFC 5321 line endings with
550 5.6.11 SMTPSEND.BareLinefeedsAreIllegal.

Fixes #3067
2026-09-10 08:11:24 +02:00
7 changed files with 2136 additions and 4 deletions

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# Contributions to PHPMailer/PHPMailer
The classic email sending library for PHP
This repository holds only my commits to the upstream project, with their original author, date and diff.
Each of my commits is preceded by a snapshot of the upstream files it touched, so every diff shows exactly what I changed.
## Merged pull requests
- [Don't send QUIT while DATA is in progress](https://github.com/PHPMailer/PHPMailer/pull/3362) (2026-10-04)
- [Fold long address headers](https://github.com/PHPMailer/PHPMailer/pull/3361) (2026-10-04)
- [Normalise line breaks in S/MIME signed messages](https://github.com/PHPMailer/PHPMailer/pull/3358) (2026-09-10)
+21 -4
View File
@@ -2647,7 +2647,22 @@ class PHPMailer
$addresses[] = $this->addrFormat($address);
}
return $type . ': ' . implode(', ', $addresses) . static::$LE;
//Fold between addresses so that no line exceeds the RFC 2822 line length limit
$header = $type . ': ' . array_shift($addresses);
$lineLength = strlen($header);
foreach ($addresses as $address) {
//The address goes after ', ' and must still leave room for the ',' that ends
//the line if the next address gets folded
if ($lineLength + strlen(', ') + strlen($address) + strlen(',') > static::MAX_LINE_LENGTH) {
$header .= ',' . static::$LE . ' ' . $address;
$lineLength = strlen(' ') + strlen($address);
} else {
$header .= ', ' . $address;
$lineLength += strlen(', ') + strlen($address);
}
}
return $header . static::$LE;
}
/**
@@ -3320,8 +3335,9 @@ class PHPMailer
@unlink($signed);
//The message returned by openssl contains both headers and body, so need to split them up
$parts = explode("\n\n", $body, 2);
$this->MIMEHeader .= $parts[0] . static::$LE . static::$LE;
$body = $parts[1];
//openssl emits bare LF breaks, which SMTP servers may reject
$this->MIMEHeader .= static::normalizeBreaks($parts[0]) . static::$LE . static::$LE;
$body = static::normalizeBreaks($parts[1]);
} else {
@unlink($signed);
throw new Exception(self::lang('signing') . openssl_error_string());
@@ -3752,7 +3768,8 @@ class PHPMailer
break;
}
if ($this->has8bitChars($str)) {
//ISO-2022 charsets such as ISO-2022-JP are 7-bit, but text using their escape sequences isn't us-ascii
if ($this->has8bitChars($str) || strpos($str, "\x1B") !== false) {
$charset = $this->CharSet;
} else {
$charset = static::CHARSET_ASCII;
+1633
View File
File diff suppressed because it is too large. Load diff
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* PHPMailer - PHP email transport unit tests.
* PHP version 5.5.
*
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
* @author Andy Prevost
* @copyright 2012 - 2020 Marcus Bointon
* @copyright 2004 - 2009 Andy Prevost
* @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
*/
namespace PHPMailer\Test\PHPMailer;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\Test\TestCase;
/**
* Test creating address headers.
*
* @covers \PHPMailer\PHPMailer\PHPMailer::addrAppend
*/
final class AddrAppendTest extends TestCase
{
/**
* A short address list stays on one line.
*/
public function testShortListIsNotFolded()
{
$header = $this->Mail->addrAppend('To', [['joe@example.com', 'Joe'], ['zoe@example.com', '']]);
self::assertSame('To: Joe <joe@example.com>, zoe@example.com' . PHPMailer::CRLF, $header);
}
/**
* A long address list is folded between addresses so no line exceeds the RFC 5322 limit.
*/
public function testLongListIsFolded()
{
$addresses = [];
$formatted = [];
for ($i = 1; $i <= 100; ++$i) {
$addresses[] = ['recipient' . $i . '@example.com', 'Recipient ' . $i];
$formatted[] = 'Recipient ' . $i . ' <recipient' . $i . '@example.com>';
}
$header = $this->Mail->addrAppend('Cc', $addresses);
$lines = explode(PHPMailer::CRLF, substr($header, 0, -strlen(PHPMailer::CRLF)));
self::assertGreaterThan(1, count($lines), 'The header was not folded');
foreach ($lines as $index => $line) {
self::assertLessThanOrEqual(PHPMailer::MAX_LINE_LENGTH, strlen($line), "Line $index is too long");
if ($index > 0) {
self::assertSame(' ', $line[0], "Line $index is not a folded continuation");
}
}
self::assertSame(
'Cc: ' . implode(', ', $formatted) . PHPMailer::CRLF,
str_replace(PHPMailer::CRLF . ' ', ' ', $header),
'Unfolding the header did not give back the address list'
);
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
/**
* PHPMailer - PHP email transport unit tests.
* PHP version 5.5.
*
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
* @author Andy Prevost
* @copyright 2012 - 2020 Marcus Bointon
* @copyright 2004 - 2009 Andy Prevost
* @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
*/
namespace PHPMailer\Test\PHPMailer;
use PHPMailer\Test\TestCase;
/**
* Test the charset label chosen when encoding header values.
*
* @covers \PHPMailer\PHPMailer\PHPMailer::encodeHeader
*/
final class EncodeHeaderTest extends TestCase
{
/**
* Test the charset label used for 7-bit header values.
*
* @dataProvider dataEncodeHeaderCharset
*
* @param string $input The header value to encode.
* @param string $expected The expected function return value.
* @param string $charset The charset to use.
*/
public function testEncodeHeaderCharset($input, $expected, $charset)
{
$this->Mail->CharSet = $charset;
self::assertSame($expected, $this->Mail->encodeHeader($input));
}
/**
* Data provider.
*
* @return array
*/
public static function dataEncodeHeaderCharset()
{
return [
'ISO-2022-JP text keeps its charset label even though it is 7-bit' => [
//"Test" in Japanese katakana, converted to ISO-2022-JP
'input' => "\x1B\$B%F%9%H\x1B(B",
'expected' => '=?ISO-2022-JP?Q?=1B$B%F%9%H=1B(B?=',
'charset' => 'ISO-2022-JP',
],
'ISO-2022-JP text in the middle of ASCII text keeps its charset label' => [
'input' => "Re: \x1B\$B%F%9%H\x1B(B",
'expected' => '=?ISO-2022-JP?Q?Re:_=1B$B%F%9%H=1B(B?=',
'charset' => 'ISO-2022-JP',
],
'Plain ASCII text with an ISO-2022-JP charset is not encoded' => [
'input' => 'Test',
'expected' => 'Test',
'charset' => 'ISO-2022-JP',
],
'ASCII text with a control character still falls back to us-ascii' => [
'input' => "Joe\x7FBloggs",
'expected' => '=?us-ascii?Q?Joe=7FBloggs?=',
'charset' => 'utf-8',
],
];
}
}
+298
View File
@@ -0,0 +1,298 @@
<?php
/**
* PHPMailer - PHP email transport unit tests.
* PHP version 5.5.
*
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
* @author Andy Prevost
* @copyright 2012 - 2020 Marcus Bointon
* @copyright 2004 - 2009 Andy Prevost
* @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
*/
namespace PHPMailer\Test\PHPMailer;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\Test\SendTestCase;
/**
* Test sending mail using the various available mail transport options.
*/
final class MailTransportTest extends SendTestCase
{
/** @var string */
private $originalSendmailFrom = '';
protected function set_up()
{
parent::set_up();
$from = ini_get('sendmail_from');
$this->originalSendmailFrom = $from === false ? '' : $from;
}
protected function tear_down()
{
ini_set('sendmail_from', $this->originalSendmailFrom);
parent::tear_down();
}
/**
* Test sending using SendMail.
*
* @covers \PHPMailer\PHPMailer\PHPMailer::isSendmail
*/
public function testSendmailSend()
{
$this->Mail->Body = 'Sending via sendmail';
$this->buildBody();
$subject = $this->Mail->Subject;
$this->Mail->Subject = $subject . ': sendmail';
$this->Mail->isSendmail();
self::assertTrue($this->Mail->send(), $this->Mail->ErrorInfo);
}
/**
* Test sending using Qmail.
*
* @covers \PHPMailer\PHPMailer\PHPMailer::isQmail
*/
public function testQmailSend()
{
// Only run if we have qmail installed.
if (file_exists('/var/qmail/bin/qmail-inject') === false) {
self::markTestSkipped('Qmail is not installed');
}
$this->Mail->Body = 'Sending via qmail';
$this->buildBody();
$subject = $this->Mail->Subject;
$this->Mail->Subject = $subject . ': qmail';
$this->Mail->isQmail();
self::assertTrue($this->Mail->send(), $this->Mail->ErrorInfo);
}
/**
* Test sending using PHP mail() function.
*
* @covers \PHPMailer\PHPMailer\PHPMailer::isMail
*/
public function testMailSend()
{
$this->Mail->Body = 'Sending via mail()';
$this->buildBody();
$this->Mail->Subject = $this->Mail->Subject . ': mail()';
$this->Mail->clearAddresses();
$this->Mail->clearCCs();
$this->Mail->clearBCCs();
$this->setAddress('testmailsend@example.com', 'totest');
$this->setAddress('cctestmailsend@example.com', 'cctest', $sType = 'cc');
$this->setAddress('bcctestmailsend@example.com', 'bcctest', $sType = 'bcc');
self::assertContains('testmailsend@example.com', $this->Mail->getToAddresses()[0], 'To address not found');
self::assertContains('cctestmailsend@example.com', $this->Mail->getCcAddresses()[0], 'CC address not found');
self::assertContains('bcctestmailsend@example.com', $this->Mail->getBccAddresses()[0], 'BCC address not found');
self::assertTrue(
$this->Mail->getAllRecipientAddresses()['testmailsend@example.com'],
'To address not in recipient addresses'
);
self::assertTrue(
$this->Mail->getAllRecipientAddresses()['cctestmailsend@example.com'],
'CC address not in recipient addresses'
);
self::assertTrue(
$this->Mail->getAllRecipientAddresses()['bcctestmailsend@example.com'],
'BCC address not in recipient addresses'
);
$this->Mail->createHeader();
$this->Mail->isMail();
self::assertTrue($this->Mail->send(), $this->Mail->ErrorInfo);
$msg = $this->Mail->getSentMIMEMessage();
self::assertStringNotContainsString("\r\n\r\nMIME-Version:", $msg, 'Incorrect MIME headers');
}
/**
* Test that a long To list is folded before it is passed to mail().
*
* @covers \PHPMailer\PHPMailer\PHPMailer::mailSend
*/
public function testMailSendFoldsLongToList()
{
$this->Mail->Body = 'Sending via mail()';
$this->buildBody();
$this->Mail->Subject = $this->Mail->Subject . ': mail()';
$this->Mail->clearAddresses();
for ($i = 1; $i <= 100; ++$i) {
$this->Mail->addAddress('recipient' . $i . '@example.com', 'Recipient ' . $i);
}
$to = '';
$this->Mail->Debugoutput = static function ($str) use (&$to) {
if (strpos($str, 'To: ') === 0) {
$to = substr($str, 4);
}
};
$this->Mail->isMail();
self::assertTrue($this->Mail->send(), $this->Mail->ErrorInfo);
$lines = explode(PHPMailer::getLE(), 'To: ' . $to);
self::assertGreaterThan(1, count($lines), 'The To list was not folded');
foreach ($lines as $index => $line) {
self::assertLessThanOrEqual(PHPMailer::MAX_LINE_LENGTH, strlen($line), "Line $index is too long");
}
self::assertStringContainsString(
'To: ' . $to . PHPMailer::getLE(),
$this->Mail->getSentMIMEMessage(),
'mail() did not get the same To list as the message header'
);
}
/**
* Test sending using PHP mail() function with Sender address
* and explicit sendmail_from ini set.
* Test running required with:
* php -d sendmail_path="/usr/sbin/sendmail -t -i -frpath@example.org" ./vendor/bin/phpunit
*
* @group sendmailparams
* @covers \PHPMailer\PHPMailer\PHPMailer::isMail
*/
public function testMailSendWithSendmailParams()
{
$sender = 'rpath@example.org';
if (strpos(ini_get('sendmail_path'), $sender) === false) {
self::markTestSkipped('Custom Sendmail php.ini not available');
}
$this->Mail->Body = 'Sending via mail()';
$this->buildBody();
$this->Mail->Subject = $this->Mail->Subject . ': mail()';
$this->Mail->clearAddresses();
$this->setAddress('testmailsend@example.com', 'totest');
ini_set('sendmail_from', $sender);
$this->Mail->createHeader();
$this->Mail->isMail();
self::assertTrue($this->Mail->send(), $this->Mail->ErrorInfo);
}
/**
* Test sending using SendMail with Sender address
* and explicit sendmail_from ini set.
* Test running required with:
* php -d sendmail_path="/usr/sbin/sendmail -t -i -frpath@example.org" ./vendor/bin/phpunit
*
* @group sendmailparams
* @covers \PHPMailer\PHPMailer\PHPMailer::isSendmail
*/
public function testSendmailSendWithSendmailParams()
{
$sender = 'rpath@example.org';
if (strpos(ini_get('sendmail_path'), $sender) === false) {
self::markTestSkipped('Custom Sendmail php.ini not available');
}
$this->Mail->Body = 'Sending via sendmail';
$this->buildBody();
$subject = $this->Mail->Subject;
$this->Mail->Subject = $subject . ': sendmail';
ini_set('sendmail_from', $sender);
$this->Mail->isSendmail();
self::assertTrue($this->Mail->send(), $this->Mail->ErrorInfo);
}
/**
* Test parsing of sendmail path and with certain parameters.
*
* @group sendmailparams
* @covers \PHPMailer\PHPMailer\PHPMailer::parseSendmailPath
* @dataProvider sendmailPathProvider
*
* @param string $sendmailPath The sendmail path to parse.
* @param string $expectedCommand The expected command after parsing.
* @param string $expectedSender The expected Sender (-f parameter) after parsing.
*/
public function testParseSendmailPath($sendmailPath, $expectedCommand, $expectedSender)
{
$mailer = $this->Mail;
$parseSendmailPath = \Closure::bind(
function ($path) {
return $this->{'parseSendmailPath'}($path);
},
$mailer,
\PHPMailer\PHPMailer\PHPMailer::class
);
$command = $parseSendmailPath($sendmailPath);
self::assertSame($expectedCommand, $command, 'Sendmail command not parsed correctly');
self::assertSame($expectedSender, $mailer->Sender, 'Sender property not set correctly');
}
/**
* Data provider for testParseSendmailPath.
*
* @return array{
* 0: string, // The sendmail path to parse.
* 1: string, // The expected command after parsing.
* 2: string // The expected Sender (-f parameter) after parsing.
* }
*/
public static function sendmailPathProvider()
{
return [
'path only' => [
'/usr/sbin/sendmail',
'/usr/sbin/sendmail',
''
],
'with i and t' => [
'/usr/sbin/sendmail -i -t',
'/usr/sbin/sendmail',
''
],
'with f concatenated' => [
'/usr/sbin/sendmail -frpath@example.org -i',
'/usr/sbin/sendmail',
'rpath@example.org'
],
'with f separated' => [
'/usr/sbin/sendmail -f rpath@example.org -t',
'/usr/sbin/sendmail',
'rpath@example.org',
],
'with extra flags preserved' => [
'/opt/sendmail -x -y -fuser@example.org',
'/opt/sendmail -x -y',
'user@example.org',
],
"extra flags with values preserved" => [
'/opt/sendmail -X /path/to/logfile -fuser@example.org',
'/opt/sendmail -X /path/to/logfile',
'user@example.org',
],
"extra flags concatenated preserved" => [
'/opt/sendmail -X/path/to/logfile -t -i',
'/opt/sendmail -X/path/to/logfile',
'',
],
"option values with regular parameters" => [
'/opt/sendmail -oi -t',
'/opt/sendmail',
'',
],
];
}
}
+36
View File
@@ -971,6 +971,11 @@ EOT;
$msg = $this->Mail->getSentMIMEMessage();
self::assertStringNotContainsString("\r\n\r\nMIME-Version:", $msg, 'Incorrect MIME headers');
self::assertSame(
0,
preg_match_all("/(?<!\r)\n/", $msg),
'Signed message contains bare line feeds'
);
unlink($certfile);
unlink($keyfile);
}
@@ -1111,6 +1116,37 @@ EOT;
self::assertFalse($smtp->mail("somewhere\nbad"), 'Bad SMTP command containing breaks accepted');
}
/**
* Closing a connection in the middle of DATA must not send QUIT, as the server would take it as message content.
*/
public function testQuitDuringData()
{
self::assertTrue($this->Mail->smtpConnect(), 'SMTP connect failed');
$smtp = $this->Mail->getSMTPInstance();
//Don't wait long for a reply that should never be requested
$smtp->Timelimit = 5;
self::assertTrue($smtp->mail('phpunit@example.com'));
self::assertTrue($smtp->recipient('somebody@example.com'));
//Start DATA without sending the message, as if the send had been interrupted
$reflMethod = new \ReflectionMethod($smtp, 'sendCommand');
(\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(true);
self::assertTrue($reflMethod->invoke($smtp, 'DATA', 'DATA', 354));
(\PHP_VERSION_ID < 80100) && $reflMethod->setAccessible(false);
$sent = '';
$smtp->setDebugLevel(SMTP::DEBUG_CLIENT);
$smtp->setDebugOutput(
static function ($str) use (&$sent) {
$sent .= $str;
}
);
$smtp->quit();
self::assertStringNotContainsString('QUIT', $sent, 'QUIT was sent during DATA');
self::assertFalse($smtp->connected(), 'Connection was not closed');
}
/**
* Tests setting and retrieving ConfirmReadingTo address, also known as "read receipt" address.
*/