--- class.phpmailer.php.orig 2005-06-29 04:23:45.000000000 +0200 +++ class.phpmailer.php 2007-11-13 19:14:18.000000000 +0100 @@ -656,6 +656,9 @@ */ function WrapText($message, $length, $qp_mode = false) { $soft_break = ($qp_mode) ? sprintf(" =%s", $this->LE) : $this->LE; + // If utf-8 encoding is used, we will need to make sure we don't + // split multibyte characters when we wrap + $is_utf8 = (strtolower($this->CharSet) == "utf-8"); $message = $this->FixEOL($message); if (substr($message, -1) == $this->LE) @@ -678,9 +681,11 @@ if ($space_left > 20) { $len = $space_left; - if (substr($word, $len - 1, 1) == "=") + if ($is_utf8) { + $len = $this->UTF8CharBoundary($word, $len); + } elseif (substr($word, $len - 1, 1) == "=") { $len--; - elseif (substr($word, $len - 2, 1) == "=") + } elseif (substr($word, $len - 2, 1) == "=") $len -= 2; $part = substr($word, 0, $len); $word = substr($word, $len); @@ -696,9 +701,11 @@ while (strlen($word) > 0) { $len = $length; - if (substr($word, $len - 1, 1) == "=") + if ($is_utf8) { + $len = $this->UTF8CharBoundary($word, $len); + } elseif (substr($word, $len - 1, 1) == "=") { $len--; - elseif (substr($word, $len - 2, 1) == "=") + } elseif (substr($word, $len - 2, 1) == "=") $len -= 2; $part = substr($word, 0, $len); $word = substr($word, $len); @@ -727,6 +734,65 @@ return $message; } + /** + * Finds last character boundary prior to maxLength in a utf-8 + * quoted (printable) encoded string. + * Original written by Colin Brown. + * + * @access private + * + * @param string $encodedText utf-8 QP text + * @param int $maxLength find last character boundary prior to this length + * + * @return int + */ + function UTF8CharBoundary($encodedText, $maxLength) + { + $foundSplitPos = false; + $lookBack = 3; + + while (!$foundSplitPos) + { + $lastChunk = substr($encodedText, $maxLength - $lookBack, $lookBack); + $encodedCharPos = strpos($lastChunk, "="); + + if ($encodedCharPos !== false) { + // Found start of encoded character byte within $lookBack block. + // Check the encoded byte value (the 2 chars after the '=') + $hex = substr($encodedText, $maxLength - $lookBack + $encodedCharPos + 1, 2); + $dec = hexdec($hex); + if ($dec < 128) + { + // Single byte character. + + // If the encoded char was found at pos 0, it will fit + // otherwise reduce maxLength to start of the encoded char + $maxLength = ($encodedCharPos == 0) ? $maxLength : + $maxLength - ($lookBack - $encodedCharPos); + $foundSplitPos = true; + } + elseif ($dec >= 192) + { + // First byte of a multi byte character + + // Reduce maxLength to split at start of character + $maxLength = $maxLength - ($lookBack - $encodedCharPos); + $foundSplitPos = true; + } + elseif ($dec < 192) + { + // Middle byte of a multi byte character, look further back + $lookBack += 3; + } + } else { + // No encoded character found + $foundSplitPos = true; + } + } + + return $maxLength; + } + /** * Set the body wrapping. * @access private @@ -1166,9 +1232,15 @@ // Try to select the encoding which should produce the shortest output if (strlen($str)/3 < $x) { $encoding = 'B'; - $encoded = base64_encode($str); - $maxlen -= $maxlen % 4; - $encoded = trim(chunk_split($encoded, $maxlen, "\n")); + if (function_exists('mb_strlen') && $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); + } else { + $encoded = base64_encode($str); + $maxlen -= $maxlen % 4; + $encoded = trim(chunk_split($encoded, $maxlen, "\n")); + } } else { $encoding = 'Q'; $encoded = $this->EncodeQ($str, $position); @@ -1182,6 +1254,74 @@ return $encoded; } + + /** + * Checks if a string contains multibyte characters. + * + * @access private + * + * @param string $str multi-byte text to wrap encode + * + * @return bool + */ + function HasMultiBytes($str) + { + if (function_exists('mb_strlen')) { + return (strlen($str) > mb_strlen($str, $this->CharSet)); + } else { + // Assume no multibytes (we can't handle without mbstring functions anyway) + return False; + } + } + + /** + * Correctly encodes and wraps long multibyte strings for mail headers + * without breaking lines within a character. + * + * Adapted from a function by paravoid at http://uk.php.net/manual/en/function.mb-encode-mimeheader.php + * + * @access private + * + * @param string $str multi-byte text to wrap encode + * + * @return string + */ + function Base64EncodeWrapMB($str) + { + $start = "=?".$this->CharSet."?B?"; + $end = "?="; + $encoded = ""; + + $mb_length = mb_strlen($str, $this->CharSet); + // Each line must have length <= 75, including $start and $end + $length = 75 - strlen($start) - strlen($end); + // Average multi-byte ratio + $ratio = $mb_length / strlen($str); + // Base64 has a 4:3 ratio + $offset = $avgLength = floor($length * $ratio * .75); + + for ($i = 0; $i < $mb_length; $i += $offset) + { + $lookBack = 0; + + do { + $offset = $avgLength - $lookBack; + $chunk = mb_substr($str, $i, $offset, $this->CharSet); + $chunk = base64_encode($chunk); + $lookBack++; + } + while (strlen($chunk) > $length); + + $encoded .= $chunk . $this->LE; + } + + // Chomp the last linefeed + $encoded = substr($encoded, 0, -strlen($this->LE)); + + return $encoded; + } + + /** * Encode string to quoted-printable. * @access private