Use Q-encode to wrap too long headers (#1840)
* Always Q-encode headers exceeding maximum length Previously, headers exceeding the maximum line length without any special characters were only folded. This lead to problems with long filenames (#1469) and long headers in general (#1525). Now, long headers are always Q-encoded (and still folded). * Use ASCII as Q-encoding charset if applicable Previously, headers were Q-encoded using the message charset, e.g. UTF-8. This is excessive for ASCII values, as it requires a unicode engine. Now, we use ASCII if we only find 7-bit characters. * Separate header encoding from encoding selection * Use ASCII for B-encoding as well * Refactor max line length calculation Previously, we calculated the maximum line length for header encoding both for B- and Q-encoding, even though they share the same limits. Now, we calculate these once for both.
This commit is contained in:
parent
a4f6fe3879
commit
21b35dc49b
|
|
@ -30,6 +30,7 @@ namespace PHPMailer\PHPMailer;
|
|||
*/
|
||||
class PHPMailer
|
||||
{
|
||||
const CHARSET_ASCII = 'us-ascii';
|
||||
const CHARSET_ISO88591 = 'iso-8859-1';
|
||||
const CHARSET_UTF8 = 'utf-8';
|
||||
|
||||
|
|
@ -747,6 +748,16 @@ class PHPMailer
|
|||
*/
|
||||
protected static $LE = "\r\n";
|
||||
|
||||
/**
|
||||
* The maximum line length supported by mail().
|
||||
*
|
||||
* Background: mail() will sometimes corrupt messages
|
||||
* with headers headers longer than 65 chars, see #818.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAIL_MAX_LINE_LENGTH = 63;
|
||||
|
||||
/**
|
||||
* The maximum line length allowed by RFC 2822 section 2.1.1.
|
||||
*
|
||||
|
|
@ -2530,7 +2541,7 @@ class PHPMailer
|
|||
if (static::ENCODING_8BIT == $bodyEncoding and !$this->has8bitChars($this->Body)) {
|
||||
$bodyEncoding = static::ENCODING_7BIT;
|
||||
//All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
|
||||
$bodyCharSet = 'us-ascii';
|
||||
$bodyCharSet = static::CHARSET_ASCII;
|
||||
}
|
||||
//If lines are too long, and we're not already using an encoding that will shorten them,
|
||||
//change to quoted-printable transfer encoding for the body part only
|
||||
|
|
@ -2544,7 +2555,7 @@ class PHPMailer
|
|||
if (static::ENCODING_8BIT == $altBodyEncoding and !$this->has8bitChars($this->AltBody)) {
|
||||
$altBodyEncoding = static::ENCODING_7BIT;
|
||||
//All ISO 8859, Windows codepage and UTF-8 charsets are ascii compatible up to 7-bit
|
||||
$altBodyCharSet = 'us-ascii';
|
||||
$altBodyCharSet = static::CHARSET_ASCII;
|
||||
}
|
||||
//If lines are too long, and we're not already using an encoding that will shorten them,
|
||||
//change to quoted-printable transfer encoding for the alt body part only
|
||||
|
|
@ -3133,51 +3144,57 @@ class PHPMailer
|
|||
break;
|
||||
}
|
||||
|
||||
//RFCs specify a maximum line length of 78 chars, however mail() will sometimes
|
||||
//corrupt messages with headers longer than 65 chars. See #818
|
||||
$lengthsub = 'mail' == $this->Mailer ? 13 : 0;
|
||||
$maxlen = static::STD_LINE_LENGTH - $lengthsub;
|
||||
// Try to select the encoding which should produce the shortest output
|
||||
if ($matchcount > strlen($str) / 3) {
|
||||
// More than a third of the content will need encoding, so B encoding will be most efficient
|
||||
$encoding = 'B';
|
||||
//This calculation is:
|
||||
// max line length
|
||||
// - shorten to avoid mail() corruption
|
||||
// - Q/B encoding char overhead ("` =?<charset>?[QB]?<content>?=`")
|
||||
// - charset name length
|
||||
$maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet);
|
||||
if ($this->hasMultiBytes($str)) {
|
||||
// Use a custom function which correctly encodes and wraps long
|
||||
// multibyte strings without breaking lines within a character
|
||||
$encoded = $this->base64EncodeWrapMB($str, "\n");
|
||||
} else {
|
||||
$encoded = base64_encode($str);
|
||||
$maxlen -= $maxlen % 4;
|
||||
$encoded = trim(chunk_split($encoded, $maxlen, "\n"));
|
||||
}
|
||||
$encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded);
|
||||
} elseif ($matchcount > 0) {
|
||||
//1 or more chars need encoding, use Q-encode
|
||||
$encoding = 'Q';
|
||||
//Recalc max line length for Q encoding - see comments on B encode
|
||||
$maxlen = static::STD_LINE_LENGTH - $lengthsub - 8 - strlen($this->CharSet);
|
||||
$encoded = $this->encodeQ($str, $position);
|
||||
$encoded = $this->wrapText($encoded, $maxlen, true);
|
||||
$encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
|
||||
$encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded);
|
||||
} elseif (strlen($str) > $maxlen) {
|
||||
//No chars need encoding, but line is too long, so fold it
|
||||
$encoded = trim($this->wrapText($str, $maxlen, false));
|
||||
if ($str == $encoded) {
|
||||
//Wrapping nicely didn't work, wrap hard instead
|
||||
$encoded = trim(chunk_split($str, static::STD_LINE_LENGTH, static::$LE));
|
||||
}
|
||||
$encoded = str_replace(static::$LE, "\n", trim($encoded));
|
||||
$encoded = preg_replace('/^(.*)$/m', ' \\1', $encoded);
|
||||
if ($this->has8bitChars($str)) {
|
||||
$charset = $this->CharSet;
|
||||
} else {
|
||||
//No reformatting needed
|
||||
return $str;
|
||||
$charset = static::CHARSET_ASCII;
|
||||
}
|
||||
|
||||
// Q/B encoding adds 8 chars and the charset ("` =?<charset>?[QB]?<content>?=`").
|
||||
$overhead = 8 + strlen($charset);
|
||||
|
||||
if ('mail' == $this->Mailer) {
|
||||
$maxlen = static::MAIL_MAX_LINE_LENGTH - $overhead;
|
||||
} else {
|
||||
$maxlen = static::STD_LINE_LENGTH - $overhead;
|
||||
}
|
||||
|
||||
// Select the encoding that produces the shortest output and/or prevents corruption.
|
||||
if ($matchcount > strlen($str) / 3) {
|
||||
// More than 1/3 of the content needs encoding, use B-encode.
|
||||
$encoding = 'B';
|
||||
} elseif ($matchcount > 0) {
|
||||
// Less than 1/3 of the content needs encoding, use Q-encode.
|
||||
$encoding = 'Q';
|
||||
} elseif (strlen($str) > $maxlen) {
|
||||
// No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
|
||||
$encoding = 'Q';
|
||||
} else {
|
||||
// No reformatting needed
|
||||
$encoding = false;
|
||||
}
|
||||
|
||||
switch ($encoding) {
|
||||
case 'B':
|
||||
if ($this->hasMultiBytes($str)) {
|
||||
// Use a custom function which correctly encodes and wraps long
|
||||
// multibyte strings without breaking lines within a character
|
||||
$encoded = $this->base64EncodeWrapMB($str, "\n");
|
||||
} else {
|
||||
$encoded = base64_encode($str);
|
||||
$maxlen -= $maxlen % 4;
|
||||
$encoded = trim(chunk_split($encoded, $maxlen, "\n"));
|
||||
}
|
||||
$encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
|
||||
break;
|
||||
case 'Q':
|
||||
$encoded = $this->encodeQ($str, $position);
|
||||
$encoded = $this->wrapText($encoded, $maxlen, true);
|
||||
$encoded = str_replace('=' . static::$LE, "\n", trim($encoded));
|
||||
$encoded = preg_replace('/^(.*)$/m', ' =?' . $charset . "?$encoding?\\1?=", $encoded);
|
||||
break;
|
||||
default:
|
||||
return $str;
|
||||
}
|
||||
|
||||
return trim(static::normalizeBreaks($encoded));
|
||||
|
|
|
|||
|
|
@ -877,8 +877,10 @@ final class PHPMailerTest extends TestCase
|
|||
$bencodenofold = str_repeat('é', 10);
|
||||
//This should select Q-encoding automatically and should not fold
|
||||
$qencodenofold = str_repeat('e', 9) . 'é';
|
||||
//This should not encode, but just fold automatically
|
||||
$justfold = str_repeat('e', PHPMailer::STD_LINE_LENGTH + 10);
|
||||
//This should Q-encode as ASCII and fold (previously, this did not encode)
|
||||
$longheader = str_repeat('e', PHPMailer::STD_LINE_LENGTH + 10);
|
||||
//This should Q-encode as UTF-8 and fold
|
||||
$longutf8 = str_repeat('é', PHPMailer::STD_LINE_LENGTH + 10);
|
||||
//This should not change
|
||||
$noencode = 'eeeeeeeeee';
|
||||
$this->Mail->isMail();
|
||||
|
|
@ -896,8 +898,12 @@ final class PHPMailerTest extends TestCase
|
|||
' =?UTF-8?Q?eeeeeeeeeeeeeeeeeeeeeeeeee=C3=A9?=';
|
||||
$bencodenofoldres = '=?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6k=?=';
|
||||
$qencodenofoldres = '=?UTF-8?Q?eeeeeeeee=C3=A9?=';
|
||||
$justfoldres = 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' .
|
||||
PHPMailer::getLE() . ' eeeeeeeeee';
|
||||
$longheaderres = '=?us-ascii?Q?eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee?=' .
|
||||
PHPMailer::getLE() . ' =?us-ascii?Q?eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee?=';
|
||||
$longutf8res = '=?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6k=?=' .
|
||||
PHPMailer::getLE() . ' =?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6k=?=' .
|
||||
PHPMailer::getLE() . ' =?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6k=?=' .
|
||||
PHPMailer::getLE() . ' =?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqQ==?=';
|
||||
$noencoderes = 'eeeeeeeeee';
|
||||
$this->assertEquals(
|
||||
$bencoderes,
|
||||
|
|
@ -920,9 +926,14 @@ final class PHPMailerTest extends TestCase
|
|||
'Q-encoded header value incorrect'
|
||||
);
|
||||
$this->assertEquals(
|
||||
$justfoldres,
|
||||
$this->Mail->encodeHeader($justfold),
|
||||
'Folded header value incorrect'
|
||||
$longheaderres,
|
||||
$this->Mail->encodeHeader($longheader),
|
||||
'Long header value incorrect'
|
||||
);
|
||||
$this->assertEquals(
|
||||
$longutf8res,
|
||||
$this->Mail->encodeHeader($longutf8),
|
||||
'Long UTF-8 header value incorrect'
|
||||
);
|
||||
$this->assertEquals(
|
||||
$noencoderes,
|
||||
|
|
|
|||
Loading…
Reference in New Issue