Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This file includes only changes we consider noteworthy for users, admins and plu
- Fix bug where dates could get displayed shifted back one day in some places (#9403)
- Fix regression where it wasn't possible to hide a skin logo image anymore (#10254)
- Fix decoding of multi-segment RFC2231 extended attachment filenames (#10268)
- Fix Mbox export (zipdownload) not quoting message-body lines starting with "From " (#10150)
- Fix vCard import silently dropping properties with a non-item group prefix (#10271)

## Release 1.7.2
Expand Down
60 changes: 60 additions & 0 deletions plugins/zipdownload/tests/ZipdownloadTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,64 @@ public function test_constructor()
$this->assertInstanceOf('zipdownload', $plugin);
$this->assertInstanceOf('rcube_plugin', $plugin);
}

/**
* Every line starting with "From " must be quoted in the Mbox output,
* regardless of how the message body is split into stream chunks.
*
* Regression test for the case where the body is written in large chunks
* (rcube_imap_generic::handlePartBody() writes up to 1 MB at a time), not
* line by line, so interior "From " lines were left unescaped.
*/
public function test_mbox_from_escaping()
{
// Loading the plugin also defines the mbox stream filter class
$this->assertTrue(class_exists('zipdownload'));
$this->assertTrue(class_exists('zipdownload_mbox_filter'));

stream_filter_register('zipdownload_mbox_test', 'zipdownload_mbox_filter');

$body = "Received: from mx\r\n"
. "\r\n"
. "Hello,\r\n"
. "From all of us, hi\r\n" // must be quoted
. "Fromage is not a delimiter\r\n" // must NOT be quoted
. ">From already quoted\r\n" // must get one more quote (mboxrd)
. "Bye\r\n";

$expected = "Received: from mx\r\n"
. "\r\n"
. "Hello,\r\n"
. ">From all of us, hi\r\n"
. "Fromage is not a delimiter\r\n"
. ">>From already quoted\r\n"
. "Bye\r\n";

// Test a single write and a byte-by-byte write (which splits lines,
// and even the word "From ", across filter buckets).
$chunkings = [
[strlen($body)],
array_fill(0, strlen($body), 1),
];

foreach ($chunkings as $chunks) {
$tmp = tempnam(sys_get_temp_dir(), 'mboxtest');
$fp = fopen($tmp, 'w');
$filter = stream_filter_append($fp, 'zipdownload_mbox_test', \STREAM_FILTER_WRITE);

$offset = 0;
foreach ($chunks as $len) {
fwrite($fp, substr($body, $offset, $len));
$offset += $len;
}

stream_filter_remove($filter);
fclose($fp);

$result = file_get_contents($tmp);
unlink($tmp);

$this->assertSame($expected, $result);
}
}
}
41 changes: 35 additions & 6 deletions plugins/zipdownload/zipdownload.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,19 +395,48 @@ private function _filename_from_subject($str)

class zipdownload_mbox_filter extends \php_user_filter
{
/** @var string Buffer holding an incomplete trailing line between chunks */
private $buffer = '';

#[\Override]
#[\ReturnTypeWillChange]
public function filter($in, $out, &$consumed, $closing)
{
// The message body is written to the stream in arbitrary chunks (see
// rcube_imap_generic::handlePartBody(), which writes up to 1 MB at a
// time), not line by line. We therefore buffer across chunks and quote
// every complete line that starts with "From " (mboxrd escaping),
// keeping any incomplete trailing line for the next chunk.
while ($bucket = stream_bucket_make_writeable($in)) {
// messages are read line by line
if (preg_match('/^>*From /', $bucket->data)) {
$bucket->data = '>' . $bucket->data;
$bucket->datalen++;
$consumed += (int) $bucket->datalen;
$this->buffer .= $bucket->data;
}

$data = '';

while (($pos = strpos($this->buffer, "\n")) !== false) {
$line = substr($this->buffer, 0, $pos + 1);
$this->buffer = substr($this->buffer, $pos + 1);

if (preg_match('/^>*From /', $line)) {
$line = '>' . $line;
}

$consumed += (int) $bucket->datalen;
stream_bucket_append($out, $bucket);
$data .= $line;
}

// Flush the last line (which may have no trailing newline) on close.
if ($closing && $this->buffer !== '') {
if (preg_match('/^>*From /', $this->buffer)) {
$this->buffer = '>' . $this->buffer;
}

$data .= $this->buffer;
$this->buffer = '';
}

if ($data !== '') {
stream_bucket_append($out, stream_bucket_new($this->stream, $data));
}

return \PSFS_PASS_ON;
Expand Down
Loading