| | Author: Aleksander Machniak | +-----------------------------------------------------------------------+ */ class rcmail_action_mail_get extends rcmail_action_mail_index { protected static $attachment; /** * Request handler. * * @param array $args Arguments from the previous step(s) */ #[Override] public function run($args = []) { $rcmail = rcmail::get_instance(); // This resets X-Frame-Options for framed output (#6688) $rcmail->output->page_headers(); $attachment = new rcmail_attachment_handler(); $mimetype = $attachment->mimetype; $filename = $attachment->filename; self::$attachment = $attachment; // show part page if (!empty($_GET['_frame'])) { $rcmail->output->set_pagetitle($filename); // register UI objects $rcmail->output->add_handlers([ 'messagepartframe' => [$this, 'message_part_frame'], 'messagepartcontrols' => [$this, 'message_part_controls'], ]); $part_id = rcube_utils::get_input_string('_part', rcube_utils::INPUT_GET); $uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GET); // message/rfc822 preview (Note: handle also multipart/ parts, they can // come from Enigma, which replaces message/rfc822 with real mimetype) if ($part_id && ($mimetype == 'message/rfc822' || str_starts_with($mimetype, 'multipart/'))) { $uid = preg_replace('/\.[0-9.]+/', '', $uid); $uid .= '.' . $part_id; $rcmail->output->set_env('is_message', true); } $rcmail->output->set_env('mailbox', $rcmail->storage->get_folder()); $rcmail->output->set_env('uid', $uid); $rcmail->output->set_env('part', $part_id); $rcmail->output->set_env('filename', $filename); $rcmail->output->set_env('mimetype', $mimetype); $rcmail->output->send('messagepart'); } // render thumbnail of an image attachment if (!empty($_GET['_thumb']) && $attachment->is_valid()) { $thumbnail_size = $rcmail->config->get('image_thumbnail_size', 240); $file_ident = $attachment->ident; $thumb_name = 'thumb' . md5($file_ident . ':' . $rcmail->user->ID . ':' . $thumbnail_size); $cache_file = rcube_utils::temp_filename($thumb_name, false, false); // render thumbnail image if not done yet if (!is_file($cache_file) && $attachment->body_to_file($orig_name = rcube_utils::temp_filename('attmnt'))) { $image = new rcube_image($orig_name); if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) { $mimetype = 'image/' . $imgtype; } else { // Resize failed, we need to check the file mimetype // So, we do not exit here, but goto generic file body handler below $_GET['_thumb'] = 0; $_REQUEST['_embed'] = 1; } } if (!empty($_GET['_thumb'])) { if (is_file($cache_file)) { $rcmail->output->future_expire_header(3600); header('Content-Type: ' . $mimetype); header('Content-Length: ' . filesize($cache_file)); readfile($cache_file); } $rcmail->output->sendExit(); } } // Handle attachment body (display or download) if (empty($_GET['_thumb']) && $attachment->is_valid()) { // require CSRF protected url for downloads if (!empty($_GET['_download'])) { $rcmail->request_security_check(rcube_utils::INPUT_GET); } // compare file mimetype with the stated content-type headers and file extension to avoid malicious operations if (!empty($_REQUEST['_embed']) && empty($_REQUEST['_nocheck'])) { [$valid, $mimetype] = self::check_mimetype_against_content_type_and_filename($attachment, $mimetype); // show warning if validity checks failed if (!$valid) { // send blocked.gif for expected images if (str_starts_with($mimetype, 'image/')) { // Do not cache. Failure might be the result of a misconfiguration, // thus real content should be returned once fixed. $content = self::get_resource_content('blocked.gif'); $rcmail->output->nocacheing_headers(); header('Content-Type: image/gif'); header('Content-Transfer-Encoding: binary'); header('Content-Length: ' . strlen($content)); echo $content; } $rcmail->output->sendExit(); } } // TIFF/WEBP to JPEG conversion, if needed foreach (['tiff', 'webp'] as $type) { $img_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps'][$type]); if ( !empty($_REQUEST['_embed']) && !$img_support && $attachment->image_type() == 'image/' . $type && rcube_image::is_convertable('image/' . $type) ) { $convert2jpeg = true; $mimetype = 'image/jpeg'; break; } } // Deliver plaintext with HTML-markup if ($mimetype == 'text/plain' && empty($_GET['_download']) && empty($_REQUEST['_embed'])) { $body = $attachment->print_body(); $stylesheet_tag = null; $styles_path = $rcmail->output->get_skin_file('/styles/styles.css', $_dummy, null, true); if ($styles_path) { $stylesheet_tag = html::tag('link', ['rel' => 'stylesheet', 'href' => $rcmail->output->asset_url($styles_path)]); } $body = html::tag('html', ['class' => 'message-part'], html::tag('head', [], $stylesheet_tag) . html::tag('body', [], $body) ); $rcmail->output->sendExit($body); } // deliver part content if ($mimetype == 'text/html' && empty($_GET['_download'])) { // Check if we have enough memory to handle the message in it // #1487424: we need up to 10x more memory than the body if (!rcube_utils::mem_check($attachment->size * 10)) { $inline_warning = $this->make_inline_warning( // TODO: Make this work with iframed content parts (the interactivity must happen outside of the iframe) $rcmail->gettext('messagetoobig'), $rcmail->gettext('download'), $rcmail->url(array_merge($_GET, ['_download' => 1])) ); $this->send_html('', $inline_warning); } else { // render HTML body $this->send_html($attachment->html()); } } // add filename extension if missing if (!pathinfo($filename, \PATHINFO_EXTENSION) && ($extensions = rcube_mime::get_mime_extensions($mimetype))) { $filename .= '.' . $extensions[0]; } $rcmail->output->download_headers($filename, [ 'type' => $mimetype, 'type_charset' => $attachment->charset, 'disposition' => !empty($_GET['_download']) ? 'attachment' : 'inline', ]); // handle tiff to jpeg conversion if (!empty($convert2jpeg)) { $file_path = rcube_utils::temp_filename('attmnt'); // convert image to jpeg and send it to the browser if ($attachment->body_to_file($file_path)) { $image = new rcube_image($file_path); if ($image->convert(rcube_image::TYPE_JPG, $file_path)) { header('Content-Length: ' . filesize($file_path)); readfile($file_path); } } } else { $attachment->output($mimetype); } $rcmail->output->sendExit(); } // if we arrive here, the requested part was not found http_response_code(404); $rcmail->output->sendExit(); } protected static function check_mimetype_against_content_type_and_filename($attachment, $mimetype): array { $extensions = rcube_mime::get_mime_extensions($mimetype); $file_extension = strtolower(pathinfo(self::$attachment->filename, \PATHINFO_EXTENSION)); // 1. compare filename suffix with expected suffix derived from mimetype $valid = $file_extension && in_array($file_extension, (array) $extensions) || empty($extensions) || !empty($_REQUEST['_mimeclass']); // 2. detect the real mimetype of the attachment part and compare it with the stated mimetype and filename extension if ($valid || !$file_extension || $mimetype == 'application/octet-stream' || stripos($mimetype, 'text/') === 0) { $tmp_body = $attachment->body(2048); // detect message part mimetype $real_mimetype = rcube_mime::file_content_type($tmp_body, self::$attachment->filename, $mimetype, true, true); [$real_ctype_primary, $real_ctype_secondary] = explode('/', $real_mimetype); // accept text/plain with any extension if ($real_mimetype == 'text/plain' && self::mimetype_compare($real_mimetype, $mimetype)) { $valid_extension = true; } // ignore differences in text/* mimetypes. Filetype detection isn't very reliable here elseif ($real_ctype_primary == 'text' && str_starts_with($mimetype, $real_ctype_primary)) { $real_mimetype = $mimetype; $valid_extension = true; } // ignore filename extension if mimeclass matches (#1489029) elseif (!empty($_REQUEST['_mimeclass']) && $real_ctype_primary == $_REQUEST['_mimeclass']) { $valid_extension = true; } else { // get valid file extensions $extensions = rcube_mime::get_mime_extensions($real_mimetype); $valid_extension = !$file_extension || empty($extensions) || in_array($file_extension, (array) $extensions); } if ( // fix mimetype for files wrongly declared as octet-stream ($mimetype == 'application/octet-stream' && $valid_extension) // force detected mimetype for images (#8158) || str_starts_with($real_mimetype, 'image/') ) { $mimetype = $real_mimetype; } // "fix" real mimetype the same way the original is before comparison $real_mimetype = rcube_mime::fix_mimetype($real_mimetype); $valid = $valid_extension && self::mimetype_compare($real_mimetype, $mimetype); } else { $real_mimetype = $mimetype; } return [$valid, $mimetype, $real_mimetype, $extensions]; } /** * Compares two mimetype strings with making sure that * e.g. image/bmp and image/x-ms-bmp are treated as equal. */ public static function mimetype_compare($type1, $type2) { $regexp = '~/(x-ms-|x-)~'; $type1 = preg_replace($regexp, '/', $type1); $type2 = preg_replace($regexp, '/', $type2); return $type1 === $type2; } /** * Attachment properties table */ public static function message_part_controls($attrib) { if (!self::$attachment->is_valid()) { return ''; } $rcmail = rcmail::get_instance(); $table = new html_table(['cols' => 2]); $table->add('title', rcube::Q($rcmail->gettext('namex')) . ':'); $table->add('header', rcube::Q(self::$attachment->filename)); $table->add('title', rcube::Q($rcmail->gettext('type')) . ':'); $table->add('header', rcube::Q(self::$attachment->mimetype)); $table->add('title', rcube::Q($rcmail->gettext('size')) . ':'); $table->add('header', rcube::Q(self::$attachment->size())); return $table->show($attrib); } /** * Attachment preview frame */ public static function message_part_frame($attrib) { $rcmail = rcmail::get_instance(); $mimetype = self::$attachment->mimetype; $file_extension = strtolower(pathinfo(self::$attachment->filename, \PATHINFO_EXTENSION)); [$valid, $mimetype, $real_mimetype, $extensions] = self::check_mimetype_against_content_type_and_filename(self::$attachment, $mimetype); if (!$valid) { $inline_warning = self::make_inline_warning( $rcmail->gettext([ 'name' => 'attachmentvalidationerror', 'vars' => [ 'expected' => $mimetype . (!empty($file_extension) ? rcube::Q(" (.{$file_extension})") : ''), 'detected' => $real_mimetype . (!empty($extensions[0]) ? " (.{$extensions[0]})" : ''), ], ]), $rcmail->gettext('showanyway'), $rcmail->url(array_merge($_GET, ['_nocheck' => 1])) ); } $rcmail = rcmail::get_instance(); if ($rcmail->output->get_env('is_message')) { $url = [ 'task' => 'mail', 'action' => 'preview', 'uid' => $rcmail->output->get_env('uid'), 'mbox' => $rcmail->output->get_env('mailbox'), ]; } else { // What's is the mimetype assignment from/for? $mimetype = $rcmail->output->get_env('mimetype'); $url = $_GET; $url['_nocheck'] = 1; $url['_embed'] = 1; unset($url['_frame']); } $url['_framed'] = 1; // For proper X-Frame-Options:deny handling $attrib['data-src'] = $rcmail->url($url); $attrib['class'] = 'framed-message-part attachment-view'; // 'allow-modals' is necessary to allow printing button in attachment view. $attrib['sandbox'] = 'allow-same-origin allow-modals'; $rcmail->output->add_gui_object('messagepartframe', $attrib['id']); if ($valid) { $prefix = self::message_loading_notice(); } else { $prefix = $inline_warning; } return html::div([], [ $prefix, html::iframe($attrib) ]); } /** * @param $contents string Content to send as HTTP body * @param $inline_warning string Something to inject into the beginning of the content */ protected function send_html($contents, $inline_warning = null): void { $rcmail = rcmail::get_instance(); $rcmail->output->reset(true, false); if (empty($_REQUEST['_embed'])) { $css_path = $rcmail->output->get_skin_file('/styles/styles.css', $_dummy, null, true); } else { $embed_css = $rcmail->config->get('embed_css_location', '/embed.css'); if ($embed_path = $rcmail->output->get_skin_file($embed_css, $_dummy, null, true)) { $css_path = $embed_path; } else { // set default styles for warning blocks inside the attachment part frame $rcmail->output->add_header(html::tag('style', ['type' => 'text/css'], '.rcmail-inline-message { font-family: sans-serif; border:2px solid #ffdf0e;' . "background:#fef893; padding:0.6em 1em; margin-bottom:0.6em }\n" . '.rcmail-inline-buttons { margin-bottom:0 }' )); } } if ($css_path) { $rcmail->output->include_css($rcmail->output->asset_url($css_path)); } if (empty($contents)) { $contents = ''; } if ($inline_warning) { $body_start = 0; if ($body_pos = strpos($contents, '', $body_pos) + 1; } $contents = substr_replace($contents, $inline_warning, $body_start, 0); } // TODO: If we actually want to style HTML email content, find a better way than this (currently a possibly // exisiting class-attribute makes our attribute be ignored). $html_elem_start = strpos($contents, '', $html_elem_start); $contents = substr_replace($contents, ' class="message-htmlpart"', $html_inject_pos, 0); $rcmail->output->write_blank_slate($contents); $rcmail->output->sendExit(); } /** * @param $text string Text content * @param $button_label string Text for the optional button to append to the content * @param $button_url string URL of the button * * @return string HTML code as string */ protected static function make_inline_warning($text, $button_label = null, $button_url = null): string { $text = html::span(null, $text); if ($button_label) { $button = html::tag('button', [], rcube::Q($button_label)); $text .= html::p(['class' => 'rcmail-inline-buttons'], $button); } return html::div(['class' => 'rcmail-inline-message rcmail-inline-warning'], $text); } }