Aleksander Machniak
2015-11-22 9f1f754daf4b57a0d0d3aea95d2321716d218cf5
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  *
ed1cee 9  * @version 3.0
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'],
66             ));
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     {
122         $rcmail    = rcmail::get_instance();
123         $imap      = $rcmail->get_storage();
124         $temp_dir  = $rcmail->config->get('temp_dir');
125         $tmpfname  = tempnam($temp_dir, 'zipdownload');
126         $tempfiles = array($tmpfname);
127         $message   = new rcube_message(rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET));
70bbab 128
eaf9d8 129         // open zip file
AM 130         $zip = new ZipArchive();
131         $zip->open($tmpfname, ZIPARCHIVE::OVERWRITE);
70bbab 132
eaf9d8 133         foreach ($message->attachments as $part) {
AM 134             $pid      = $part->mime_id;
135             $part     = $message->mime_parts[$pid];
136             $filename = $part->filename;
2eef77 137
AM 138             if ($filename === null || $filename === '') {
139                 $ext      = (array) rcube_mime::get_mime_extensions($part->mimetype);
140                 $ext      = array_shift($ext);
141                 $filename = $rcmail->gettext('messagepart') . ' ' . $pid;
142                 if ($ext) {
143                     $filename .= '.' . $ext;
144                 }
145             }
146
48ba44 147             $disp_name   = $this->_convert_filename($filename);
AM 148             $tmpfn       = tempnam($temp_dir, 'zipattach');
149             $tmpfp       = fopen($tmpfn, 'w');
150             $tempfiles[] = $tmpfn;
70bbab 151
48ba44 152             $message->get_part_body($part->mime_id, false, 0, $tmpfp);
AM 153             $zip->addFile($tmpfn, $disp_name);
154             fclose($tmpfp);
eaf9d8 155         }
70bbab 156
eaf9d8 157         $zip->close();
70bbab 158
9f1f75 159         $filename = ($message->subject ?: 'roundcube') . '.zip';
eaf9d8 160         $this->_deliver_zipfile($tmpfname, $filename);
70bbab 161
eaf9d8 162         // delete temporary files from disk
AM 163         foreach ($tempfiles as $tmpfn) {
164             unlink($tmpfn);
165         }
70bbab 166
eaf9d8 167         exit;
AM 168     }
70bbab 169
eaf9d8 170     /**
AM 171      * Handler for message download action
172      */
ed1cee 173     public function download_messages()
eaf9d8 174     {
ed1cee 175         $rcmail = rcmail::get_instance();
70bbab 176
ed1cee 177         if ($rcmail->config->get('zipdownload_selection') && !empty($_POST['_uid'])) {
AM 178             $messageset = rcmail::get_uids();
179             if (sizeof($messageset)) {
bde3ba 180                 $this->_download_messages($messageset);
eaf9d8 181             }
AM 182         }
183     }
70bbab 184
eaf9d8 185     /**
AM 186      * Helper method to packs all the given messages into a zip archive
187      *
188      * @param array List of message UIDs to download
189      */
bde3ba 190     private function _download_messages($messageset)
eaf9d8 191     {
AM 192         $rcmail    = rcmail::get_instance();
193         $imap      = $rcmail->get_storage();
ed1cee 194         $mode      = rcube_utils::get_input_value('_mode', rcube_utils::INPUT_POST);
eaf9d8 195         $temp_dir  = $rcmail->config->get('temp_dir');
AM 196         $tmpfname  = tempnam($temp_dir, 'zipdownload');
197         $tempfiles = array($tmpfname);
bde3ba 198         $folders   = count($messageset) > 1;
70bbab 199
ed1cee 200         // @TODO: file size limit
AM 201
eaf9d8 202         // open zip file
AM 203         $zip = new ZipArchive();
204         $zip->open($tmpfname, ZIPARCHIVE::OVERWRITE);
70bbab 205
ed1cee 206         if ($mode == 'mbox') {
AM 207             $tmpfp = fopen($tmpfname . '.mbox', 'w');
208         }
209
210         foreach ($messageset as $mbox => $uids) {
bde3ba 211             $imap->set_folder($mbox);
TB 212             $path = $folders ? str_replace($imap->get_hierarchy_delimiter(), '/', $mbox) . '/' : '';
70bbab 213
bb2113 214             if ($uids === '*') {
AM 215                 $index = $imap->index($mbox, null, null, true);
216                 $uids  = $index->get();
217             }
218
ed1cee 219             foreach ($uids as $uid) {
bde3ba 220                 $headers = $imap->get_message_headers($uid);
dafb50 221
ed1cee 222                 if ($mode == 'mbox') {
AM 223                     $from = rcube_mime::decode_address_list($headers->from, null, true, $headers->charset, true);
224                     $from = array_shift($from);
bde3ba 225
ed1cee 226                     // Mbox format header
AM 227                     // @FIXME: \r\n or \n
228                     // @FIXME: date format
229                     $header = sprintf("From %s %s\r\n",
230                         // replace spaces with hyphens
231                         $from ? preg_replace('/\s/', '-', $from) : 'MAILER-DAEMON',
232                         // internaldate
233                         $headers->internaldate
234                     );
235
236                     fwrite($tmpfp, $header);
237
238                     // Use stream filter to quote "From " in the message body
239                     stream_filter_register('mbox_filter', 'zipdownload_mbox_filter');
240                     $filter = stream_filter_append($tmpfp, 'mbox_filter');
241                     $imap->get_raw_body($uid, $tmpfp);
242                     stream_filter_remove($filter);
243                     fwrite($tmpfp, "\r\n");
244                 }
245                 else { // maildir
246                     $subject = rcube_mime::decode_mime_string((string)$headers->subject);
247                     $subject = $this->_convert_filename($subject);
248                     $subject = substr($subject, 0, 16);
249
9f1f75 250                     $disp_name = ($subject ?: 'message_rfc822') . ".eml";
ed1cee 251                     $disp_name = $path . $uid . "_" . $disp_name;
AM 252
253                     $tmpfn = tempnam($temp_dir, 'zipmessage');
254                     $tmpfp = fopen($tmpfn, 'w');
255                     $imap->get_raw_body($uid, $tmpfp);
256                     $tempfiles[] = $tmpfn;
257                     fclose($tmpfp);
258                     $zip->addFile($tmpfn, $disp_name);
259                 }
bde3ba 260             }
ed1cee 261         }
AM 262
263         $filename = $folders ? 'messages' : $imap->get_folder();
264
265         if ($mode == 'mbox') {
266             $tempfiles[] = $tmpfname . '.mbox';
267             fclose($tmpfp);
268             $zip->addFile($tmpfname . '.mbox', $filename . '.mbox');
eaf9d8 269         }
70bbab 270
eaf9d8 271         $zip->close();
70bbab 272
bde3ba 273         $this->_deliver_zipfile($tmpfname, $filename . '.zip');
70bbab 274
eaf9d8 275         // delete temporary files from disk
AM 276         foreach ($tempfiles as $tmpfn) {
277             unlink($tmpfn);
278         }
70bbab 279
eaf9d8 280         exit;
AM 281     }
70bbab 282
eaf9d8 283     /**
AM 284      * Helper method to send the zip archive to the browser
285      */
286     private function _deliver_zipfile($tmpfname, $filename)
287     {
288         $browser = new rcube_browser;
289         $rcmail  = rcmail::get_instance();
61be82 290
eaf9d8 291         $rcmail->output->nocacheing_headers();
70bbab 292
ed1cee 293         if ($browser->ie)
eaf9d8 294             $filename = rawurlencode($filename);
AM 295         else
296             $filename = addcslashes($filename, '"');
70bbab 297
eaf9d8 298         // send download headers
AM 299         header("Content-Type: application/octet-stream");
300         if ($browser->ie) {
301             header("Content-Type: application/force-download");
302         }
70bbab 303
eaf9d8 304         // don't kill the connection if download takes more than 30 sec.
AM 305         @set_time_limit(0);
306         header("Content-Disposition: attachment; filename=\"". $filename ."\"");
307         header("Content-length: " . filesize($tmpfname));
308         readfile($tmpfname);
309     }
70bbab 310
eaf9d8 311     /**
AM 312      * Helper function to convert filenames to the configured charset
313      */
314     private function _convert_filename($str)
315     {
492247 316         $str = rcube_charset::convert($str, RCUBE_CHARSET, $this->charset);
dafb50 317
ed1cee 318         return strtr($str, array(':' => '', '/' => '-'));
AM 319     }
320 }
321
322 class zipdownload_mbox_filter extends php_user_filter
323 {
324     function filter($in, $out, &$consumed, $closing)
325     {
326         while ($bucket = stream_bucket_make_writeable($in)) {
327             // messages are read line by line
328             if (preg_match('/^>*From /', $bucket->data)) {
329                 $bucket->data     = '>' . $bucket->data;
330                 $bucket->datalen += 1;
331             }
332
333             $consumed += $bucket->datalen;
334             stream_bucket_append($out, $bucket);
335         }
336
337         return PSFS_PASS_ON;
eaf9d8 338     }
70bbab 339 }