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