diff --git a/src/Message/Transcoder.php b/src/Message/Transcoder.php index aec4ae8d..5e9fe7b0 100644 --- a/src/Message/Transcoder.php +++ b/src/Message/Transcoder.php @@ -247,19 +247,50 @@ public static function decode(string $text, string $fromCharset): string $fromCharset = self::$charsetAliases[$lowercaseFromCharset]; } - \set_error_handler(function ($nr, $message) use ($originalFromCharset, $fromCharset) { + $iconvDecodedText = self::iconvDecode($text, $originalFromCharset, $fromCharset); + if (false !== $iconvDecodedText) { + return $iconvDecodedText; + } + + \error_clear_last(); + $decodedText = @\mb_convert_encoding($text, 'UTF-8', $fromCharset); + if (null !== ($lastError = \error_get_last())) { throw new UnsupportedCharsetException(\sprintf( 'Unsupported charset "%s"%s: %s', $originalFromCharset, ($fromCharset !== $originalFromCharset) ? \sprintf(' (alias found: "%s")', $fromCharset) : '', - $message - ), $nr); - }); + $lastError['message'] + ), $lastError['type']); + } + + return $decodedText; + } + + /** + * Decode text to UTF-8 with iconv. + * + * @param string $text Text to decode + * @param string $originalFromCharset Original charset + * @param string $fromCharset Aliased charset + * + * @return bool|string + */ + private static function iconvDecode($text, $originalFromCharset, $fromCharset) + { + static $iconvLoaded; + if (null === $iconvLoaded) { + $iconvLoaded = \function_exists('iconv'); + } - $decodedText = \mb_convert_encoding($text, 'UTF-8', $fromCharset); + if (false === $iconvLoaded) { + return false; + } - \restore_error_handler(); + $iconvDecodedText = @\iconv($fromCharset, 'UTF-8', $text); + if (false === $iconvDecodedText) { + $iconvDecodedText = @\iconv($originalFromCharset, 'UTF-8', $text); + } - return $decodedText; + return $iconvDecodedText; } } diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 878ff3ca..01ef4f67 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -29,6 +29,13 @@ */ final class MessageTest extends AbstractTest { + private static $encodings = [ + Mime\Mime::ENCODING_7BIT, + Mime\Mime::ENCODING_8BIT, + Mime\Mime::ENCODING_QUOTEDPRINTABLE, + Mime\Mime::ENCODING_BASE64, + ]; + private static $charsets = [ 'ASCII' => '! "#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~', 'GB18030' => " 、。〃々〆〇〈〉《》「」『』【】〒〓〔〕〖〗〝〞〡〢〣〤〥〦〧〨〩〾一\u{200b}丁\u{200b}丂踰\u{200b}踱\u{200b}踲\u{200b}", @@ -40,6 +47,11 @@ final class MessageTest extends AbstractTest 'Windows-1252' => 'ƒŠŒŽšœžŸªºÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ', ]; + private static $iconvOnlyCharsets = [ + 'macintosh' => '†°¢£§•¶ß®©™´¨≠ÆØ∞±≤≥¥µ∂∑∏π∫ªºΩæø¿¡¬√ƒ≈«»…ÀÃÕŒœ–—“”‘’÷◊ÿŸ⁄€‹›fifl‡·‚„‰ÂÊÁËÈÍÎÏÌÓÔ', + 'Windows-1250' => 'ŚŤŹśťźˇ˘ŁĄŞŻ˛łąşĽ˝ľż', + ]; + protected function setUp() { $this->mailbox = $this->createMailbox(); @@ -100,16 +112,8 @@ public function provideCharsets(): array // This first data set mimics "us-ascii" imap server default settings $provider[] = [null, self::$charsets['ASCII'], null]; - - $encodings = [ - Mime\Mime::ENCODING_7BIT, - Mime\Mime::ENCODING_8BIT, - Mime\Mime::ENCODING_QUOTEDPRINTABLE, - Mime\Mime::ENCODING_BASE64, - ]; - foreach (self::$charsets as $charset => $charList) { - foreach ($encodings as $encoding) { + foreach (self::$encodings as $encoding) { $provider[] = [$charset, $charList, $encoding]; } } @@ -177,6 +181,38 @@ public function testSpecialCharsetOnHeaders() $this->assertSame('김 현진', $from->getName()); } + /** + * @dataProvider provideIconvCharsets + */ + public function testIconvFallback(string $charset, string $charList, string $encoding) + { + $subject = \sprintf('[%s:%s]', $charset, $encoding); + $this->createTestMessage( + $this->mailbox, + $subject, + \iconv('UTF-8', $charset, $charList), + $encoding, + $charset + ); + + $message = $this->mailbox->getMessage(1); + + $this->assertSame($subject, $message->getSubject()); + $this->assertSame($charList, \rtrim($message->getBodyText())); + } + + public function provideIconvCharsets(): array + { + $provider = []; + foreach (self::$iconvOnlyCharsets as $charset => $charList) { + foreach (self::$encodings as $encoding) { + $provider[] = [$charset, $charList, $encoding]; + } + } + + return $provider; + } + public function testEmailAddress() { $this->mailbox->addMessage($this->getFixture('email_address'));