Skip to content

Commit

Permalink
More reliable folding of message headers
Browse files Browse the repository at this point in the history
  • Loading branch information
Synchro committed May 22, 2017
1 parent d95ef49 commit b25f93e
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 13 deletions.
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 43 additions & 13 deletions src/PHPMailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -2812,7 +2820,11 @@ public function encodeString($str, $encoding = 'base64')
$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':
Expand All @@ -2837,7 +2849,7 @@ public function encodeString($str, $encoding = 'base64')

/**
* 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.
Expand Down Expand Up @@ -2874,16 +2886,20 @@ public function encodeHeader($str, $position = 'text')
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 ("` =?<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
Expand All @@ -2893,17 +2909,31 @@ public function encodeHeader($str, $position = 'text')
$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;
}
Expand Down
63 changes: 63 additions & 0 deletions test/phpmailerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,69 @@ public function testQuotedPrintable()
);
}

/**
* 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.
*/
Expand Down

0 comments on commit b25f93e

Please sign in to comment.