From 4bde475ea1a43b87066e53e244d608e24765fab9 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Wed, 1 Jan 2025 13:27:30 +0100 Subject: [PATCH] Fix handling of binary mail parts (e.g. PDF) encoded with quoted-printable (#9728) --- CHANGELOG.md | 1 + program/lib/Roundcube/rcube_imap_generic.php | 36 +++++++++----------- tests/Framework/ImapGenericTest.php | 17 +++++---- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff05a3540..2ae37d517 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ - Fix PHP fatal error when parsing some malformed BODYSTRUCTURE responses (#9689) - Fix insert_or_update() and reading database server config on PostgreSQL (#9710) - Fix Oauth issues with use_secure_urls=true (#9722) +- Fix handling of binary mail parts (e.g. PDF) encoded with quoted-printable (#9728) ## Release 1.6.9 diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index 6af4878bb..cd49acc27 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -2939,7 +2939,7 @@ class rcube_imap_generic } if ($result !== false) { - $result = $this->decodeContent($result, $mode, true); + $result = $this->decodeContent($result, $mode, true, '', $formatted); } } // response with string literal @@ -2973,7 +2973,7 @@ class rcube_imap_generic } $bytes -= $len; - $chunk = $this->decodeContent($chunk, $mode, $bytes <= 0, $prev); + $chunk = $this->decodeContent($chunk, $mode, $bytes <= 0, $prev, $formatted); if ($file) { if (($result = fwrite($file, $chunk)) === false) { @@ -3008,14 +3008,15 @@ class rcube_imap_generic /** * Decodes a chunk of a message part content from a FETCH response. * - * @param string $chunk Content - * @param int $mode Encoding mode - * @param bool $is_last Whether it is a last chunk of data - * @param string $prev Extra content from the previous chunk + * @param string $chunk Content + * @param int $mode Encoding mode + * @param bool $is_last Whether it is a last chunk of data + * @param string $prev Extra content from the previous chunk + * @param bool $formatted Format the content for output * * @return string Encoded string */ - protected static function decodeContent($chunk, $mode, $is_last = false, &$prev = '') + protected static function decodeContent($chunk, $mode, $is_last = false, &$prev = '', $formatted = false) { // BASE64 if ($mode == 1) { @@ -3039,22 +3040,18 @@ class rcube_imap_generic $result .= base64_decode($_chunk); } - return $result; + $chunk = $result; } - // QUOTED-PRINTABLE - if ($mode == 2) { + elseif ($mode == 2) { if (!self::decodeContentChunk($chunk, $prev, $is_last)) { return ''; } - $chunk = preg_replace('/[\t\r\0\x0B]+\n/', "\n", $chunk); - - return quoted_printable_decode($chunk); + $chunk = quoted_printable_decode($chunk); } - // X-UUENCODE - if ($mode == 3) { + elseif ($mode == 3) { if (!self::decodeContentChunk($chunk, $prev, $is_last)) { return ''; } @@ -3069,12 +3066,11 @@ class rcube_imap_generic return ''; } - return convert_uudecode($chunk); + $chunk = convert_uudecode($chunk); } - // Plain text formatted // TODO: Formatting should be handled outside of this class - if ($mode == 4) { + elseif ($mode == 4) { if (!self::decodeContentChunk($chunk, $prev, $is_last)) { return ''; } @@ -3082,8 +3078,10 @@ class rcube_imap_generic if ($is_last) { $chunk = rtrim($chunk, "\t\r\n\0\x0B"); } + } - return preg_replace('/[\t\r\0\x0B]+\n/', "\n", $chunk); + if ($formatted) { + $chunk = preg_replace('/[\t\r\0\x0B]+\n/', "\n", $chunk); } return $chunk; diff --git a/tests/Framework/ImapGenericTest.php b/tests/Framework/ImapGenericTest.php index 4089ceba6..17715ff28 100644 --- a/tests/Framework/ImapGenericTest.php +++ b/tests/Framework/ImapGenericTest.php @@ -177,10 +177,16 @@ class ImapGenericTest extends TestCase */ public function test_decode_content_qp() { - $content = "test quoted-printable\n\n żąśźć encoded content\ntest quoted-printable żąśźć encoded content"; - $encoded = \Mail_mimePart::quotedPrintableEncode($content, 12); + $content = "test quoted-printable\r\n\r\n żąśźć encoded content\r\ntest quoted-printable żąśźć encoded content"; + $encoded = \Mail_mimePart::quotedPrintableEncode($content, 12, "\r\n"); $this->runDecodeContent($content, $encoded, 2); + + $content = file_get_contents(TESTS_DIR . '/src/test.pdf'); + $encoded = \Mail_mimePart::quotedPrintableEncode($content, 76, "\r\n"); + + $this->runDecodeContent($content, $encoded, 2, strlen($encoded)); + $this->runDecodeContent($content, $encoded, 2, strlen($encoded) / 2); } /** @@ -207,16 +213,15 @@ class ImapGenericTest extends TestCase public function test_decode_content_formatted() { $content = "test \r\n plain text\tcontent\t\r\n test plain text content\t"; - $expected = "test \n plain text\tcontent\n test plain text content"; - $this->runDecodeContent($expected, $content, 4); + $this->runDecodeContent(rtrim($content), $content, 4, true); } /** * Helper to execute decodeCOntent() method in multiple variations of an input * and assert with the expected output */ - public function runDecodeContent($expected, $encoded, $mode, $size = null) + public function runDecodeContent($expected, $encoded, $mode, $size = null, $formatted = false) { $method = new \ReflectionMethod('rcube_imap_generic', 'decodeContent'); $method->setAccessible(true); @@ -231,7 +236,7 @@ class ImapGenericTest extends TestCase $chunks = str_split($encoded, $x); foreach ($chunks as $idx => $chunk) { - $decoded .= $method->invokeArgs(null, [$chunk, $mode, $idx == count($chunks) - 1, &$prev]); + $decoded .= $method->invokeArgs(null, [$chunk, $mode, $idx == count($chunks) - 1, &$prev, $formatted]); } $this->assertSame($expected, $decoded, "Failed on chunk size of {$x}");