Aleksander Machniak
2015-02-02 e8fc8d303a30658abd70419917a1373131802e28
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
eaf9d8 159         $filename = ($message->subject ? $message->subject : 'roundcube') . '.zip';
AM 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
ed1cee 214             foreach ($uids as $uid) {
bde3ba 215                 $headers = $imap->get_message_headers($uid);
dafb50 216
ed1cee 217                 if ($mode == 'mbox') {
AM 218                     $from = rcube_mime::decode_address_list($headers->from, null, true, $headers->charset, true);
219                     $from = array_shift($from);
bde3ba 220
ed1cee 221                     // Mbox format header
AM 222                     // @FIXME: \r\n or \n
223                     // @FIXME: date format
224                     $header = sprintf("From %s %s\r\n",
225                         // replace spaces with hyphens
226                         $from ? preg_replace('/\s/', '-', $from) : 'MAILER-DAEMON',
227                         // internaldate
228                         $headers->internaldate
229                     );
230
231                     fwrite($tmpfp, $header);
232
233                     // Use stream filter to quote "From " in the message body
234                     stream_filter_register('mbox_filter', 'zipdownload_mbox_filter');
235                     $filter = stream_filter_append($tmpfp, 'mbox_filter');
236                     $imap->get_raw_body($uid, $tmpfp);
237                     stream_filter_remove($filter);
238                     fwrite($tmpfp, "\r\n");
239                 }
240                 else { // maildir
241                     $subject = rcube_mime::decode_mime_string((string)$headers->subject);
242                     $subject = $this->_convert_filename($subject);
243                     $subject = substr($subject, 0, 16);
244
245                     $disp_name = ($subject ? $subject : 'message_rfc822') . ".eml";
246                     $disp_name = $path . $uid . "_" . $disp_name;
247
248                     $tmpfn = tempnam($temp_dir, 'zipmessage');
249                     $tmpfp = fopen($tmpfn, 'w');
250                     $imap->get_raw_body($uid, $tmpfp);
251                     $tempfiles[] = $tmpfn;
252                     fclose($tmpfp);
253                     $zip->addFile($tmpfn, $disp_name);
254                 }
bde3ba 255             }
ed1cee 256         }
AM 257
258         $filename = $folders ? 'messages' : $imap->get_folder();
259
260         if ($mode == 'mbox') {
261             $tempfiles[] = $tmpfname . '.mbox';
262             fclose($tmpfp);
263             $zip->addFile($tmpfname . '.mbox', $filename . '.mbox');
eaf9d8 264         }
70bbab 265
eaf9d8 266         $zip->close();
70bbab 267
bde3ba 268         $this->_deliver_zipfile($tmpfname, $filename . '.zip');
70bbab 269
eaf9d8 270         // delete temporary files from disk
AM 271         foreach ($tempfiles as $tmpfn) {
272             unlink($tmpfn);
273         }
70bbab 274
eaf9d8 275         exit;
AM 276     }
70bbab 277
eaf9d8 278     /**
AM 279      * Helper method to send the zip archive to the browser
280      */
281     private function _deliver_zipfile($tmpfname, $filename)
282     {
283         $browser = new rcube_browser;
284         $rcmail  = rcmail::get_instance();
61be82 285
eaf9d8 286         $rcmail->output->nocacheing_headers();
70bbab 287
ed1cee 288         if ($browser->ie)
eaf9d8 289             $filename = rawurlencode($filename);
AM 290         else
291             $filename = addcslashes($filename, '"');
70bbab 292
eaf9d8 293         // send download headers
AM 294         header("Content-Type: application/octet-stream");
295         if ($browser->ie) {
296             header("Content-Type: application/force-download");
297         }
70bbab 298
eaf9d8 299         // don't kill the connection if download takes more than 30 sec.
AM 300         @set_time_limit(0);
301         header("Content-Disposition: attachment; filename=\"". $filename ."\"");
302         header("Content-length: " . filesize($tmpfname));
303         readfile($tmpfname);
304     }
70bbab 305
eaf9d8 306     /**
AM 307      * Helper function to convert filenames to the configured charset
308      */
309     private function _convert_filename($str)
310     {
492247 311         $str = rcube_charset::convert($str, RCUBE_CHARSET, $this->charset);
dafb50 312
ed1cee 313         return strtr($str, array(':' => '', '/' => '-'));
AM 314     }
315 }
316
317 class zipdownload_mbox_filter extends php_user_filter
318 {
319     function filter($in, $out, &$consumed, $closing)
320     {
321         while ($bucket = stream_bucket_make_writeable($in)) {
322             // messages are read line by line
323             if (preg_match('/^>*From /', $bucket->data)) {
324                 $bucket->data     = '>' . $bucket->data;
325                 $bucket->datalen += 1;
326             }
327
328             $consumed += $bucket->datalen;
329             stream_bucket_append($out, $bucket);
330         }
331
332         return PSFS_PASS_ON;
eaf9d8 333     }
70bbab 334 }