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
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 }