diff --git a/changelog.md b/changelog.md index f3fbbbf7..241e1df8 100644 --- a/changelog.md +++ b/changelog.md @@ -54,6 +54,7 @@ This is a major update that breaks backwards compatibility. * Replaced all use of MD5 and SHA1 hash functions with SHA256. * Now checks for invalid host strings when sending via SMTP. * Include timestamps in HTML-format debug output +* More reliable folding of message headers ## Version 5.2.23 (March 15th 2017) * Improve trapping of TLS errors during connection so that they don't cause warnings, and are reported better in debug output diff --git a/src/PHPMailer.php b/src/PHPMailer.php index c3a5086f..90829622 100644 --- a/src/PHPMailer.php +++ b/src/PHPMailer.php @@ -153,6 +153,7 @@ class PHPMailer /** * Word-wrap the message body to this number of chars. * Set to 0 to not wrap. A useful value here is 78, for RFC2822 section 2.1.1 compliance. + * @see static::STD_LINE_LENGTH * * @var integer */ @@ -682,6 +683,13 @@ class PHPMailer */ const MAX_LINE_LENGTH = 998; + /** + * The lower maximum line length allowed by RFC 2822 section 2.1.1 + * + * @var integer + */ + const STD_LINE_LENGTH = 78; + /** * Constructor. * @@ -2812,7 +2820,11 @@ class PHPMailer $encoded = ''; switch (strtolower($encoding)) { case 'base64': - $encoded = chunk_split(base64_encode($str), 76, static::$LE); + $encoded = chunk_split( + base64_encode($str), + static::STD_LINE_LENGTH - strlen(static::$LE), + static::$LE + ); break; case '7bit': case '8bit': @@ -2837,7 +2849,7 @@ class PHPMailer /** * Encode a header value (not including its label) optimally. - * Picks shortest of Q, B, or none. + * Picks shortest of Q, B, or none. Result includes folding if needed. * * @param string $str The header value to encode. * @param string $position What context the string will be used in. @@ -2874,16 +2886,20 @@ class PHPMailer break; } - //There are no chars that need encoding - if (0 == $matchcount) { - return ($str); - } - - $maxlen = 75 - 7 - strlen($this->CharSet); + //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 ("` =??[QB]??=`") + // - 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 @@ -2893,17 +2909,31 @@ class PHPMailer $maxlen -= $maxlen % 4; $encoded = trim(chunk_split($encoded, $maxlen, "\n")); } - } else { + $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); + } else { + //No reformatting needed + return $str; } - //The leading space in the replacement pattern is critical - //as it is used to designate header folding - $encoded = preg_replace('/^(.*)$/m', ' =?' . $this->CharSet . "?$encoding?\\1?=", $encoded); - $encoded = trim(str_replace("\n", static::$LE, $encoded)); + $encoded = trim(static::normalizeBreaks($encoded)); return $encoded; } diff --git a/test/phpmailerTest.php b/test/phpmailerTest.php index 4cd57562..945f180a 100644 --- a/test/phpmailerTest.php +++ b/test/phpmailerTest.php @@ -838,6 +838,69 @@ class PHPMailerTest extends \PHPUnit_Framework_TestCase ); } + /** + * Test header encoding & folding. + */ + public function testHeaderEncoding() + { + $this->Mail->CharSet = 'UTF-8'; + //This should select B-encoding automatically and should fold + $bencode = str_repeat('é', PHPMailer::STD_LINE_LENGTH + 1); + //This should select Q-encoding automatically and should fold + $qencode = str_repeat('e', PHPMailer::STD_LINE_LENGTH) . 'é'; + //This should select B-encoding automatically and should not fold + $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 not change + $noencode = 'eeeeeeeeee'; + $this->Mail->isMail(); + //Expected results + $bencoderes = '=?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6k=?='.PHPMailer::getLE(). + ' =?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6k=?=' . PHPMailer::getLE() . + ' =?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6k=?=' . PHPMailer::getLE() . + ' =?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6nDqcOpw6k=?='; + $qencoderes = '=?UTF-8?Q?eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee?=' . PHPMailer::getLE() . + ' =?UTF-8?Q?eeeeeeeeeeeeeeeeeeeeeeeeee=C3=A9?='; + $bencodenofoldres = '=?UTF-8?B?w6nDqcOpw6nDqcOpw6nDqcOpw6k=?='; + $qencodenofoldres = '=?UTF-8?Q?eeeeeeeee=C3=A9?='; + $justfoldres = 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee'. + 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' . PHPMailer::getLE() . ' eeeeeeeeee'; + $noencoderes = 'eeeeeeeeee'; + $this->assertEquals( + $bencoderes, + $this->Mail->encodeHeader($bencode), + 'Folded B-encoded header value incorrect' + ); + $this->assertEquals( + $qencoderes, + $this->Mail->encodeHeader($qencode), + 'Folded Q-encoded header value incorrect' + ); + $this->assertEquals( + $bencodenofoldres, + $this->Mail->encodeHeader($bencodenofold), + 'B-encoded header value incorrect' + ); + $this->assertEquals( + $qencodenofoldres, + $this->Mail->encodeHeader($qencodenofold), + 'Q-encoded header value incorrect' + ); + $this->assertEquals( + $justfoldres, + $this->Mail->encodeHeader($justfold), + 'Folded header value incorrect' + ); + $this->assertEquals( + $noencoderes, + $this->Mail->encodeHeader($noencode), + 'Unencoded header value incorrect' + ); + } + /** * Send an HTML message. */