Skip to content

Commit

Permalink
Use Q-encode to wrap too long headers (#1840)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
caugner authored and Synchro committed Sep 25, 2019
1 parent a4f6fe3 commit 21b35dc
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 48 deletions.
99 changes: 58 additions & 41 deletions src/PHPMailer.php
Expand Up @@ -30,6 +30,7 @@
*/
class PHPMailer
{
const CHARSET_ASCII = 'us-ascii';
const CHARSET_ISO88591 = 'iso-8859-1';
const CHARSET_UTF8 = 'utf-8';

Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -2530,7 +2541,7 @@ public function createBody()
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
Expand All @@ -2544,7 +2555,7 @@ public function createBody()
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
Expand Down Expand Up @@ -3133,51 +3144,57 @@ public function encodeHeader($str, $position = 'text')
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 ($this->has8bitChars($str)) {
$charset = $this->CharSet;
} else {
$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 a third of the content will need encoding, so B encoding will be most efficient
// More than 1/3 of the content needs encoding, use B-encode.
$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
// Less than 1/3 of the content needs 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);
// No encoding needed, but value exceeds max line length, use Q-encode to prevent corruption.
$encoding = 'Q';
} else {
//No reformatting needed
return $str;
// 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));
Expand Down
25 changes: 18 additions & 7 deletions test/PHPMailerTest.php
Expand Up @@ -877,8 +877,10 @@ public function testHeaderEncoding()
$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();
Expand All @@ -896,8 +898,12 @@ public function testHeaderEncoding()
' =?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,
Expand All @@ -920,9 +926,14 @@ public function testHeaderEncoding()
'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,
Expand Down

0 comments on commit 21b35dc

Please sign in to comment.