Mercurial > hg > rc1
comparison plugins/zipdownload/zipdownload.php @ 0:1e000243b222
vanilla 1.3.3 distro, I hope
| author | Charlie Root |
|---|---|
| date | Thu, 04 Jan 2018 15:50:29 -0500 |
| parents | |
| children | b80b258cc02c |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:1e000243b222 |
|---|---|
| 1 <?php | |
| 2 | |
| 3 /** | |
| 4 * ZipDownload | |
| 5 * | |
| 6 * Plugin to allow the download of all message attachments in one zip file | |
| 7 * and also download of many messages in one go. | |
| 8 * | |
| 9 * @requires php_zip extension (including ZipArchive class) | |
| 10 * | |
| 11 * @author Philip Weir | |
| 12 * @author Thomas Bruderli | |
| 13 * @author Aleksander Machniak | |
| 14 */ | |
| 15 class zipdownload extends rcube_plugin | |
| 16 { | |
| 17 public $task = 'mail'; | |
| 18 | |
| 19 private $charset = 'ASCII'; | |
| 20 | |
| 21 private $names = []; | |
| 22 | |
| 23 // RFC4155: mbox date format | |
| 24 const MBOX_DATE_FORMAT = 'D M d H:i:s Y'; | |
| 25 | |
| 26 /** | |
| 27 * Plugin initialization | |
| 28 */ | |
| 29 public function init() | |
| 30 { | |
| 31 // check requirements first | |
| 32 if (!class_exists('ZipArchive', false)) { | |
| 33 rcmail::raise_error(array( | |
| 34 'code' => 520, | |
| 35 'file' => __FILE__, | |
| 36 'line' => __LINE__, | |
| 37 'message' => "php_zip extension is required for the zipdownload plugin"), true, false); | |
| 38 return; | |
| 39 } | |
| 40 | |
| 41 $rcmail = rcmail::get_instance(); | |
| 42 | |
| 43 $this->load_config(); | |
| 44 $this->charset = $rcmail->config->get('zipdownload_charset', RCUBE_CHARSET); | |
| 45 $this->add_texts('localization'); | |
| 46 | |
| 47 if ($rcmail->config->get('zipdownload_attachments', 1) > -1 && ($rcmail->action == 'show' || $rcmail->action == 'preview')) { | |
| 48 $this->add_hook('template_object_messageattachments', array($this, 'attachment_ziplink')); | |
| 49 } | |
| 50 | |
| 51 $this->register_action('plugin.zipdownload.attachments', array($this, 'download_attachments')); | |
| 52 $this->register_action('plugin.zipdownload.messages', array($this, 'download_messages')); | |
| 53 | |
| 54 if (!$rcmail->action && $rcmail->config->get('zipdownload_selection')) { | |
| 55 $this->download_menu(); | |
| 56 } | |
| 57 } | |
| 58 | |
| 59 /** | |
| 60 * Place a link/button after attachments listing to trigger download | |
| 61 */ | |
| 62 public function attachment_ziplink($p) | |
| 63 { | |
| 64 $rcmail = rcmail::get_instance(); | |
| 65 | |
| 66 // only show the link if there is more than the configured number of attachments | |
| 67 if (substr_count($p['content'], '<li') > $rcmail->config->get('zipdownload_attachments', 1)) { | |
| 68 $href = $rcmail->url(array( | |
| 69 '_action' => 'plugin.zipdownload.attachments', | |
| 70 '_mbox' => $rcmail->output->env['mailbox'], | |
| 71 '_uid' => $rcmail->output->env['uid'], | |
| 72 ), false, false, true); | |
| 73 | |
| 74 $link = html::a(array('href' => $href, 'class' => 'button zipdownload'), | |
| 75 rcube::Q($this->gettext('downloadall')) | |
| 76 ); | |
| 77 | |
| 78 // append link to attachments list, slightly different in some skins | |
| 79 switch (rcmail::get_instance()->config->get('skin')) { | |
| 80 case 'classic': | |
| 81 $p['content'] = str_replace('</ul>', html::tag('li', array('class' => 'zipdownload'), $link) . '</ul>', $p['content']); | |
| 82 break; | |
| 83 | |
| 84 default: | |
| 85 $p['content'] .= $link; | |
| 86 break; | |
| 87 } | |
| 88 | |
| 89 $this->include_stylesheet($this->local_skin_path() . '/zipdownload.css'); | |
| 90 } | |
| 91 | |
| 92 return $p; | |
| 93 } | |
| 94 | |
| 95 /** | |
| 96 * Adds download options menu to the page | |
| 97 */ | |
| 98 public function download_menu() | |
| 99 { | |
| 100 $this->include_script('zipdownload.js'); | |
| 101 $this->add_label('download'); | |
| 102 | |
| 103 $rcmail = rcmail::get_instance(); | |
| 104 $menu = array(); | |
| 105 $ul_attr = array('role' => 'menu', 'aria-labelledby' => 'aria-label-zipdownloadmenu'); | |
| 106 if ($rcmail->config->get('skin') != 'classic') { | |
| 107 $ul_attr['class'] = 'toolbarmenu'; | |
| 108 } | |
| 109 | |
| 110 foreach (array('eml', 'mbox', 'maildir') as $type) { | |
| 111 $menu[] = html::tag('li', null, $rcmail->output->button(array( | |
| 112 'command' => "download-$type", | |
| 113 'label' => "zipdownload.download$type", | |
| 114 'classact' => 'active', | |
| 115 ))); | |
| 116 } | |
| 117 | |
| 118 $rcmail->output->add_footer(html::div(array('id' => 'zipdownload-menu', 'class' => 'popupmenu', 'aria-hidden' => 'true'), | |
| 119 html::tag('h2', array('class' => 'voice', 'id' => 'aria-label-zipdownloadmenu'), "Message Download Options Menu") . | |
| 120 html::tag('ul', $ul_attr, implode('', $menu)))); | |
| 121 } | |
| 122 | |
| 123 /** | |
| 124 * Handler for attachment download action | |
| 125 */ | |
| 126 public function download_attachments() | |
| 127 { | |
| 128 $rcmail = rcmail::get_instance(); | |
| 129 | |
| 130 // require CSRF protected request | |
| 131 $rcmail->request_security_check(rcube_utils::INPUT_GET); | |
| 132 | |
| 133 $imap = $rcmail->get_storage(); | |
| 134 $temp_dir = $rcmail->config->get('temp_dir'); | |
| 135 $tmpfname = tempnam($temp_dir, 'zipdownload'); | |
| 136 $tempfiles = array($tmpfname); | |
| 137 $message = new rcube_message(rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET)); | |
| 138 | |
| 139 // open zip file | |
| 140 $zip = new ZipArchive(); | |
| 141 $zip->open($tmpfname, ZIPARCHIVE::OVERWRITE); | |
| 142 | |
| 143 foreach ($message->attachments as $part) { | |
| 144 $pid = $part->mime_id; | |
| 145 $part = $message->mime_parts[$pid]; | |
| 146 $disp_name = $this->_create_displayname($part); | |
| 147 | |
| 148 $tmpfn = tempnam($temp_dir, 'zipattach'); | |
| 149 $tmpfp = fopen($tmpfn, 'w'); | |
| 150 $tempfiles[] = $tmpfn; | |
| 151 | |
| 152 $message->get_part_body($part->mime_id, false, 0, $tmpfp); | |
| 153 $zip->addFile($tmpfn, $disp_name); | |
| 154 fclose($tmpfp); | |
| 155 } | |
| 156 | |
| 157 $zip->close(); | |
| 158 | |
| 159 $filename = ($this->_filename_from_subject($message->subject) ?: 'attachments') . '.zip'; | |
| 160 | |
| 161 $this->_deliver_zipfile($tmpfname, $filename); | |
| 162 | |
| 163 // delete temporary files from disk | |
| 164 foreach ($tempfiles as $tmpfn) { | |
| 165 unlink($tmpfn); | |
| 166 } | |
| 167 | |
| 168 exit; | |
| 169 } | |
| 170 | |
| 171 /** | |
| 172 * Handler for message download action | |
| 173 */ | |
| 174 public function download_messages() | |
| 175 { | |
| 176 $rcmail = rcmail::get_instance(); | |
| 177 | |
| 178 if ($rcmail->config->get('zipdownload_selection') && !empty($_POST['_uid'])) { | |
| 179 $messageset = rcmail::get_uids(); | |
| 180 if (count($messageset)) { | |
| 181 $this->_download_messages($messageset); | |
| 182 } | |
| 183 } | |
| 184 } | |
| 185 | |
| 186 /** | |
| 187 * Create and get display name of attachment part to add on zip file | |
| 188 * | |
| 189 * @param $part stdClass Part of attachment on message | |
| 190 * | |
| 191 * @return string Display name of attachment part | |
| 192 */ | |
| 193 private function _create_displayname($part) | |
| 194 { | |
| 195 $rcmail = rcmail::get_instance(); | |
| 196 $filename = $part->filename; | |
| 197 | |
| 198 if ($filename === null || $filename === '') { | |
| 199 $ext = (array) rcube_mime::get_mime_extensions($part->mimetype); | |
| 200 $ext = array_shift($ext); | |
| 201 $filename = $rcmail->gettext('messagepart') . ' ' . $part->mime_id; | |
| 202 if ($ext) { | |
| 203 $filename .= '.' . $ext; | |
| 204 } | |
| 205 } | |
| 206 | |
| 207 $displayname = $this->_convert_filename($filename); | |
| 208 | |
| 209 /** | |
| 210 * Adding a number before dot of extension on a name of file with same name on zip | |
| 211 * Ext: attach(1).txt on attach filename that has a attach.txt filename on same zip | |
| 212 */ | |
| 213 if (isset($this->name[$displayname])) { | |
| 214 list($filename, $ext) = preg_split("/\.(?=[^\.]*$)/", $displayname); | |
| 215 $displayname = $filename . '(' . ($this->names[$displayname]++) . ').' . $ext; | |
| 216 $this->names[$displayname] = 1; | |
| 217 } | |
| 218 else { | |
| 219 $this->names[$displayname] = 1; | |
| 220 } | |
| 221 | |
| 222 return $displayname; | |
| 223 } | |
| 224 | |
| 225 /** | |
| 226 * Helper method to packs all the given messages into a zip archive | |
| 227 * | |
| 228 * @param array List of message UIDs to download | |
| 229 */ | |
| 230 private function _download_messages($messageset) | |
| 231 { | |
| 232 $rcmail = rcmail::get_instance(); | |
| 233 $imap = $rcmail->get_storage(); | |
| 234 $mode = rcube_utils::get_input_value('_mode', rcube_utils::INPUT_POST); | |
| 235 $temp_dir = $rcmail->config->get('temp_dir'); | |
| 236 $tmpfname = tempnam($temp_dir, 'zipdownload'); | |
| 237 $tempfiles = array($tmpfname); | |
| 238 $folders = count($messageset) > 1; | |
| 239 | |
| 240 // @TODO: file size limit | |
| 241 | |
| 242 // open zip file | |
| 243 $zip = new ZipArchive(); | |
| 244 $zip->open($tmpfname, ZIPARCHIVE::OVERWRITE); | |
| 245 | |
| 246 if ($mode == 'mbox') { | |
| 247 $tmpfp = fopen($tmpfname . '.mbox', 'w'); | |
| 248 } | |
| 249 | |
| 250 foreach ($messageset as $mbox => $uids) { | |
| 251 $imap->set_folder($mbox); | |
| 252 $path = $folders ? str_replace($imap->get_hierarchy_delimiter(), '/', $mbox) . '/' : ''; | |
| 253 | |
| 254 if ($uids === '*') { | |
| 255 $index = $imap->index($mbox, null, null, true); | |
| 256 $uids = $index->get(); | |
| 257 } | |
| 258 | |
| 259 foreach ($uids as $uid) { | |
| 260 $headers = $imap->get_message_headers($uid); | |
| 261 | |
| 262 if ($mode == 'mbox') { | |
| 263 // Sender address | |
| 264 $from = rcube_mime::decode_address_list($headers->from, null, true, $headers->charset, true); | |
| 265 $from = array_shift($from); | |
| 266 $from = preg_replace('/\s/', '-', $from); | |
| 267 | |
| 268 // Received (internal) date | |
| 269 $date = rcube_utils::anytodatetime($headers->internaldate); | |
| 270 if ($date) { | |
| 271 $date->setTimezone(new DateTimeZone('UTC')); | |
| 272 $date = $date->format(self::MBOX_DATE_FORMAT); | |
| 273 } | |
| 274 | |
| 275 // Mbox format header (RFC4155) | |
| 276 $header = sprintf("From %s %s\r\n", | |
| 277 $from ?: 'MAILER-DAEMON', | |
| 278 $date ?: '' | |
| 279 ); | |
| 280 | |
| 281 fwrite($tmpfp, $header); | |
| 282 | |
| 283 // Use stream filter to quote "From " in the message body | |
| 284 stream_filter_register('mbox_filter', 'zipdownload_mbox_filter'); | |
| 285 $filter = stream_filter_append($tmpfp, 'mbox_filter'); | |
| 286 $imap->get_raw_body($uid, $tmpfp); | |
| 287 stream_filter_remove($filter); | |
| 288 fwrite($tmpfp, "\r\n"); | |
| 289 } | |
| 290 else { // maildir | |
| 291 $subject = rcube_mime::decode_header($headers->subject, $headers->charset); | |
| 292 $subject = $this->_filename_from_subject(mb_substr($subject, 0, 16)); | |
| 293 $subject = $this->_convert_filename($subject); | |
| 294 | |
| 295 $disp_name = $path . $uid . ($subject ? " $subject" : '') . '.eml'; | |
| 296 | |
| 297 $tmpfn = tempnam($temp_dir, 'zipmessage'); | |
| 298 $tmpfp = fopen($tmpfn, 'w'); | |
| 299 $imap->get_raw_body($uid, $tmpfp); | |
| 300 $tempfiles[] = $tmpfn; | |
| 301 fclose($tmpfp); | |
| 302 $zip->addFile($tmpfn, $disp_name); | |
| 303 } | |
| 304 } | |
| 305 } | |
| 306 | |
| 307 $filename = $folders ? 'messages' : $imap->get_folder(); | |
| 308 | |
| 309 if ($mode == 'mbox') { | |
| 310 $tempfiles[] = $tmpfname . '.mbox'; | |
| 311 fclose($tmpfp); | |
| 312 $zip->addFile($tmpfname . '.mbox', $filename . '.mbox'); | |
| 313 } | |
| 314 | |
| 315 $zip->close(); | |
| 316 | |
| 317 $this->_deliver_zipfile($tmpfname, $filename . '.zip'); | |
| 318 | |
| 319 // delete temporary files from disk | |
| 320 foreach ($tempfiles as $tmpfn) { | |
| 321 unlink($tmpfn); | |
| 322 } | |
| 323 | |
| 324 exit; | |
| 325 } | |
| 326 | |
| 327 /** | |
| 328 * Helper method to send the zip archive to the browser | |
| 329 */ | |
| 330 private function _deliver_zipfile($tmpfname, $filename) | |
| 331 { | |
| 332 $browser = new rcube_browser; | |
| 333 $rcmail = rcmail::get_instance(); | |
| 334 | |
| 335 $rcmail->output->nocacheing_headers(); | |
| 336 | |
| 337 if ($browser->ie) | |
| 338 $filename = rawurlencode($filename); | |
| 339 else | |
| 340 $filename = addcslashes($filename, '"'); | |
| 341 | |
| 342 // send download headers | |
| 343 header("Content-Type: application/octet-stream"); | |
| 344 if ($browser->ie) { | |
| 345 header("Content-Type: application/force-download"); | |
| 346 } | |
| 347 | |
| 348 // don't kill the connection if download takes more than 30 sec. | |
| 349 @set_time_limit(0); | |
| 350 header("Content-Disposition: attachment; filename=\"". $filename ."\""); | |
| 351 header("Content-length: " . filesize($tmpfname)); | |
| 352 readfile($tmpfname); | |
| 353 } | |
| 354 | |
| 355 /** | |
| 356 * Helper function to convert filenames to the configured charset | |
| 357 */ | |
| 358 private function _convert_filename($str) | |
| 359 { | |
| 360 $str = strtr($str, array(':' => '', '/' => '-')); | |
| 361 | |
| 362 return rcube_charset::convert($str, RCUBE_CHARSET, $this->charset); | |
| 363 } | |
| 364 | |
| 365 /** | |
| 366 * Helper function to convert message subject into filename | |
| 367 */ | |
| 368 private function _filename_from_subject($str) | |
| 369 { | |
| 370 $str = preg_replace('/[\t\n\r\0\x0B]+\s*/', ' ', $str); | |
| 371 | |
| 372 return trim($str, " ./_"); | |
| 373 } | |
| 374 } | |
| 375 | |
| 376 class zipdownload_mbox_filter extends php_user_filter | |
| 377 { | |
| 378 function filter($in, $out, &$consumed, $closing) | |
| 379 { | |
| 380 while ($bucket = stream_bucket_make_writeable($in)) { | |
| 381 // messages are read line by line | |
| 382 if (preg_match('/^>*From /', $bucket->data)) { | |
| 383 $bucket->data = '>' . $bucket->data; | |
| 384 $bucket->datalen += 1; | |
| 385 } | |
| 386 | |
| 387 $consumed += $bucket->datalen; | |
| 388 stream_bucket_append($out, $bucket); | |
| 389 } | |
| 390 | |
| 391 return PSFS_PASS_ON; | |
| 392 } | |
| 393 } |
