From 45ddf6d5c60662ab86290c8322d2da305806e7f8 Mon Sep 17 00:00:00 2001 From: Christopher Gurnee Date: Sat, 18 Apr 2026 10:56:24 -0400 Subject: [PATCH 1/4] Zipdownload: buffer at most 512KiB during download --- plugins/zipdownload/composer.json | 2 +- plugins/zipdownload/zipdownload.php | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/zipdownload/composer.json b/plugins/zipdownload/composer.json index 7639225275..f6fe39ca18 100644 --- a/plugins/zipdownload/composer.json +++ b/plugins/zipdownload/composer.json @@ -3,7 +3,7 @@ "type": "roundcube-plugin", "description": "Adds an option to download all attachments to a message in one zip file, when a message has multiple attachments. Also allows the download of a selection of messages in one zip file. Supports mbox and maildir format.", "license": "GPL-3.0-or-later", - "version": "3.6", + "version": "3.8", "authors": [ { "name": "Thomas Bruederli", diff --git a/plugins/zipdownload/zipdownload.php b/plugins/zipdownload/zipdownload.php index d3aa92baed..4c323e6d68 100644 --- a/plugins/zipdownload/zipdownload.php +++ b/plugins/zipdownload/zipdownload.php @@ -369,7 +369,20 @@ private function _deliver_zipfile($tmpfname, $filename) $rcmail->output->download_headers($filename, ['length' => filesize($tmpfname)]); - readfile($tmpfname); + $tmpfp = fopen($tmpfname, 'r'); + if (!$tmpfp) { + return; + } + while (true) { + $data = fread($tmpfp, 512 * 1024); + if (strlen($data) == 0) { + break; + } + echo $data; + ob_flush(); + flush(); + } + fclose($tmpfp); } /** From 9bd686c1c13a9341fd59fa50f2e7d43312d0cb4c Mon Sep 17 00:00:00 2001 From: Christopher Gurnee Date: Mon, 20 Apr 2026 10:33:41 -0400 Subject: [PATCH 2/4] Avoid compression and stripping of Content-Length * Apache doesn't trust Content-Length headers by default; it will strip them and, only if length is known (on disk/all buffered), add it back * Brotli already avoided compressing files already compressed, do the same with the deflate method * Don't recompress zips created by the zipdownload plugin --- public_html/.htaccess | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/public_html/.htaccess b/public_html/.htaccess index e6d4b02a55..0f50a831a8 100644 --- a/public_html/.htaccess +++ b/public_html/.htaccess @@ -5,15 +5,21 @@ RewriteEngine On RewriteRule ^favicon\.ico$ static.php/skins/elastic/images/favicon.ico +# https://httpd.apache.org/docs/2.4/env.html#cgilike +SetEnv ap_trust_cgilike_cl 1 + SetOutputFilter DEFLATE +# some assets have been compressed, so no need to do it again +SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|web[pm]|woff2?)$ no-gzip +SetEnvIfExpr "%{QUERY_STRING} =~ /(?:^|&)_action=plugin\.zipdownload\.(?:attachments|messages)(?:&|$)/" no-gzip # prefer to brotli over gzip if brotli is available SetOutputFilter BROTLI_COMPRESS -# some assets have been compressed, so no need to do it again SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|web[pm]|woff2?)$ no-brotli +SetEnvIfExpr "%{QUERY_STRING} =~ /(?:^|&)_action=plugin\.zipdownload\.(?:attachments|messages)(?:&|$)/" no-brotli From 5b72a5b6a6a17d24a96410c6088e5fcb2214cf67 Mon Sep 17 00:00:00 2001 From: Christopher Gurnee Date: Sat, 18 Apr 2026 07:46:08 -0400 Subject: [PATCH 3/4] Zipdownload: add mtime to files in maildir exports --- plugins/zipdownload/zipdownload.php | 51 ++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/plugins/zipdownload/zipdownload.php b/plugins/zipdownload/zipdownload.php index 4c323e6d68..3334209100 100644 --- a/plugins/zipdownload/zipdownload.php +++ b/plugins/zipdownload/zipdownload.php @@ -19,6 +19,7 @@ class zipdownload extends rcube_plugin private $charset = 'ASCII'; private $names = []; private $default_limit = '50MB'; + private $timezone; // RFC4155: mbox date format public const MBOX_DATE_FORMAT = 'D M d H:i:s Y'; @@ -251,14 +252,15 @@ private function _download_messages($messageset) foreach ($uids as $uid) { $headers = $imap->get_message_headers($uid); + // Received (internal) date + $date = rcube_utils::anytodatetime($headers->internaldate); + if ($mode == 'mbox') { // Sender address $from = rcube_mime::decode_address_list($headers->from, null, true, $headers->charset, true); $from = array_shift($from); $from = preg_replace('/\s/', '-', $from); - // Received (internal) date - $date = rcube_utils::anytodatetime($headers->internaldate); if ($date) { $date = $date->setTimezone($timezone)->format(self::MBOX_DATE_FORMAT); } else { @@ -277,7 +279,7 @@ private function _download_messages($messageset) $path = $folders ? str_replace($delimiter, '/', $mbox) . '/' : ''; $disp_name = $path . $uid . ($subject ? " {$subject}" : '') . '.eml'; - $messages[$uid . ':' . $mbox] = $disp_name; + $messages[$uid . ':' . $mbox] = ($date ? $date->getTimestamp() : '') . ':' . $disp_name; } $size += $headers->size; @@ -305,6 +307,7 @@ private function _download_messages($messageset) } // open zip file + putenv('TZ=UTC'); // see _datetime_to_ziplocal() comments $zip = new \ZipArchive(); $zip->open($tmpfname, \ZipArchive::OVERWRITE); @@ -331,12 +334,17 @@ private function _download_messages($messageset) fwrite($tmpfp, "\r\n"); } } else { // maildir + [$date, $filename] = explode(':', $value, 2); $tmpfn = rcube_utils::temp_filename('zipmessage'); $fp = fopen($tmpfn, 'w'); $imap->get_raw_body($uid, $fp); $tempfiles[] = $tmpfn; fclose($fp); - $zip->addFile($tmpfn, $value); + $zip->addFile($tmpfn, $filename); + if ($date) { + $date = $this->_datetime_to_ziplocal(new \DateTime('@' . $date)); + $zip->setMtimeName($filename, $date->getTimestamp()); + } } } @@ -360,6 +368,41 @@ private function _download_messages($messageset) exit; } + /** + * Zip files do not store timezones; most extraction tools extract times + * as though they were local times. This converts the UTC times inside + * DateTime objects into a user's preferred local time (despite claiming + * to still be UTC) so that when added and later extracted to/from a zip + * file, they will be in that user's local time. + * + * Also, ZipArchive creation is affected the system's default timezone + * (NOT date_default_timezone_set); to mitigate this, putenv('TZ=UTC'). + * + * @param \DateTimeInterface $real The accurate DateTime of a file (is not changed) + * + * @return \DateTimeInterface A "fake" DateTimeImmutable for inclusion into a zip + */ + private function _datetime_to_ziplocal($real) + { + if (!$this->timezone) { + if ($this->timezone === false) { + return $real; + } + $rcmail = rcmail::get_instance(); + try { + $this->timezone = new \DateTimeZone($rcmail->config->get('timezone')); + } catch (\DateInvalidTimeZoneException) { + $this->timezone = false; + return $real; + } + } + + $real = \DateTime::createFromInterface($real); + $real->setTimezone($this->timezone); + $local = max($real->format('Y/m/d H:i:s'), '1980/01/01 00:00:00'); // Earliest supported by zip + return \DateTimeImmutable::createFromFormat('Y/m/d H:i:s O', $local . ' +0000'); + } + /** * Helper method to send the zip archive to the browser */ From a6b857fb0f17edd1f3d89020d2fe98007eca2a9d Mon Sep 17 00:00:00 2001 From: Christopher Gurnee Date: Mon, 4 May 2026 16:30:07 -0400 Subject: [PATCH 4/4] Zipdownload: Change charset in ex. config to UTF-8 It's already the default if unspecified, and the zip format has supported UTF-8 since 2006, so there's little reason to do otherwise. https://www.loc.gov/preservation/digital/formats/digformatspecs/APPNOTE(20060929)_Version_6.3.0.txt --- plugins/zipdownload/config.inc.php.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/zipdownload/config.inc.php.dist b/plugins/zipdownload/config.inc.php.dist index a6911067fc..98332d5213 100644 --- a/plugins/zipdownload/config.inc.php.dist +++ b/plugins/zipdownload/config.inc.php.dist @@ -16,4 +16,4 @@ $config['zipdownload_attachments'] = 1; $config['zipdownload_selection'] = '50MB'; // Charset to use for filenames inside the zip -$config['zipdownload_charset'] = 'ISO-8859-1'; +$config['zipdownload_charset'] = 'UTF-8';