diff --git a/class.phpmailer.php b/class.phpmailer.php index 8d6c291fe..c891c3d03 100644 --- a/class.phpmailer.php +++ b/class.phpmailer.php @@ -570,6 +570,12 @@ class PHPMailer */ const CRLF = "\r\n"; + /** + * The maximum line length allowed by RFC 2822 section 2.1.1 + * @type integer + */ + const MAX_LINE_LENGTH = 998; + /** * Constructor. * @param boolean $exceptions Should we throw external exceptions? @@ -1001,8 +1007,9 @@ public function preSend() throw new phpmailerException($this->lang('empty_message'), self::STOP_CRITICAL); } - $this->MIMEHeader = $this->createHeader(); + //Create body before headers in case body makes changes to headers (e.g. altering transfer encoding) $this->MIMEBody = $this->createBody(); + $this->MIMEHeader = $this->createHeader(); // To capture the complete message when using mail(), create // an extra header list which createHeader() doesn't fold in @@ -1823,16 +1830,30 @@ public function createBody() $bodyEncoding = $this->Encoding; $bodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? if ($bodyEncoding == '8bit' and !$this->has8bitChars($this->Body)) { $bodyEncoding = '7bit'; $bodyCharSet = 'us-ascii'; } + //If lines are too long, change to quoted-printable transfer encoding + if (self::hasLineLongerThanMax($this->Body)) { + $this->Encoding = 'quoted-printable'; + $bodyEncoding = 'quoted-printable'; + $bodyCharSet = 'us-ascii'; //qp always fits into ascii + } + $altBodyEncoding = $this->Encoding; $altBodyCharSet = $this->CharSet; + //Can we do a 7-bit downgrade? if ($altBodyEncoding == '8bit' and !$this->has8bitChars($this->AltBody)) { $altBodyEncoding = '7bit'; $altBodyCharSet = 'us-ascii'; } + //If lines are too long, change to quoted-printable transfer encoding + if (self::hasLineLongerThanMax($this->AltBody)) { + $altBodyEncoding = 'quoted-printable'; + $altBodyCharSet = 'us-ascii'; + } switch ($this->message_type) { case 'inline': $body .= $this->getBoundary($this->boundary[1], $bodyCharSet, '', $bodyEncoding); @@ -3371,6 +3392,18 @@ public function DKIM_Add($headers_line, $subject, $body) return $dkimhdrs . $signed . "\r\n"; } + /** + * Detect if a string contains a line longer than the maximum line length allowed. + * @param $str + * @return boolean + * @static + */ + public static function hasLineLongerThanMax($str) + { + //+2 to include CRLF line break for a 1000 total + return (boolean)preg_match('/^(.{'.(self::MAX_LINE_LENGTH + 2).',})/m', $str); + } + /** * Allows for public read access to 'to' property. * @access public diff --git a/test/phpmailerTest.php b/test/phpmailerTest.php index 4d519cb4f..c3247dad7 100644 --- a/test/phpmailerTest.php +++ b/test/phpmailerTest.php @@ -146,12 +146,12 @@ public function buildBody() // Determine line endings for message if ($this->Mail->ContentType == 'text/html' || strlen($this->Mail->AltBody) > 0) { - $eol = '
'; + $eol = "
". PHPMailer::CRLF; $bullet = '
  • '; $bullet_start = ''; } else { - $eol = "\n"; + $eol = PHPMailer::CRLF; $bullet = ' - '; $bullet_start = ''; $bullet_end = ''; @@ -1040,6 +1040,57 @@ public function testEmptyBody() $this->assertFalse($this->Mail->send(), $this->Mail->ErrorInfo); } + /** + * Test constructing a message that contains lines that are too long for RFC compliance. + */ + public function testLongBody() + { + $oklen = str_repeat(str_repeat('0', PHPMailer::MAX_LINE_LENGTH) . PHPMailer::CRLF, 10); + $badlen = str_repeat(str_repeat('1', PHPMailer::MAX_LINE_LENGTH + 1) . PHPMailer::CRLF, 2); + + $this->Mail->Body = "This message contains lines that are too long.". + PHPMailer::CRLF . PHPMailer::CRLF . $oklen . $badlen . $oklen; + $this->assertTrue( + PHPMailer::hasLineLongerThanMax($this->Mail->Body), + 'Test content does not contain long lines!' + ); + $this->buildBody(); + $this->Mail->Encoding = '8bit'; + $this->Mail->preSend(); + $message = $this->Mail->getSentMIMEMessage(); + $this->assertFalse(PHPMailer::hasLineLongerThanMax($message), 'Long line not corrected.'); + $this->assertContains( + 'Content-Transfer-Encoding: quoted-printable', + $message, + 'Long line did not cause transfer encoding switch.' + ); + } + + /** + * Test constructing a message that does NOT contain lines that are too long for RFC compliance. + */ + public function testShortBody() + { + $oklen = str_repeat(str_repeat('0', PHPMailer::MAX_LINE_LENGTH) . PHPMailer::CRLF, 10); + + $this->Mail->Body = "This message does not contain lines that are too long.". + PHPMailer::CRLF . PHPMailer::CRLF . $oklen; + $this->assertFalse( + PHPMailer::hasLineLongerThanMax($this->Mail->Body), + 'Test content contains long lines!' + ); + $this->buildBody(); + $this->Mail->Encoding = '8bit'; + $this->Mail->preSend(); + $message = $this->Mail->getSentMIMEMessage(); + $this->assertFalse(PHPMailer::hasLineLongerThanMax($message), 'Long line not corrected.'); + $this->assertNotContains( + 'Content-Transfer-Encoding: quoted-printable', + $message, + 'Short line caused transfer encoding switch.' + ); + } + /** * Test keepalive (sending multiple messages in a single connection) */ @@ -1288,6 +1339,20 @@ public function testLineBreaks() $this->assertEquals($target, PHPMailer::normalizeBreaks($mixedsrc), 'Mixed break reformatting failed'); } + /** + * Test line length detection + */ + public function testLineLength() + { + $oklen = str_repeat(str_repeat('0', PHPMailer::MAX_LINE_LENGTH)."\r\n", 10); + $badlen = str_repeat(str_repeat('1', PHPMailer::MAX_LINE_LENGTH + 1) . "\r\n", 2); + $this->assertTrue(PHPMailer::hasLineLongerThanMax($badlen), 'Long line not detected (only)'); + $this->assertTrue(PHPMailer::hasLineLongerThanMax($oklen . $badlen), 'Long line not detected (first)'); + $this->assertTrue(PHPMailer::hasLineLongerThanMax($badlen . $oklen), 'Long line not detected (last)'); + $this->assertTrue(PHPMailer::hasLineLongerThanMax($oklen . $badlen . $oklen), 'Long line not detected (middle)'); + $this->assertFalse(PHPMailer::hasLineLongerThanMax($oklen), 'Long line false positive'); + } + /** * Test setting and retrieving message ID */