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