You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

zipdownload.php 13KB

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