From 97e6065897435ffce420b41de4838eca9b8d33c9 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sun, 19 Jan 2020 18:46:02 +0100 Subject: [PATCH] Extract RFC2231 attachment name from message headers (#6729) (#6783) * Extract RFC2231 attachment name from message headers (#6729) * Workaround for attachments with invalid content type (e.g. PDF) (#6816) --- program/lib/Roundcube/rcube_imap.php | 148 ++++++++++----------------- program/lib/Roundcube/rcube_mime.php | 33 ++++++ program/steps/mail/func.inc | 27 +---- program/steps/mail/get.inc | 4 +- program/steps/mail/show.inc | 2 +- 5 files changed, 90 insertions(+), 124 deletions(-) diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index 43c23350b..ee2d49d26 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -2054,18 +2054,23 @@ class rcube_imap extends rcube_storage // build parts list for headers pre-fetching for ($i=0; $imime_id ? $struct->mime_id.'.'.($i+1) : $i+1; if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') { $mime_part_headers[] = $tmp_part_id; } - else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) { - $mime_part_headers[] = $tmp_part_id; + else if (!empty($part[$i][2]) && empty($part[$i][3])) { + $params = array_map('strtolower', (array) $part[$i][2]); + $find = array('name', 'filename', 'name*', 'filename*', 'name*0', 'filename*0', 'name*0*', 'filename*0*'); + + // In case of malformed header check disposition. E.g. some servers for + // "Content-Type: PDF; name=test.pdf" may return text/plain and ignore name argument + if (count(array_intersect($params, $find)) > 0 + || (is_array($part[$i][9]) && stripos($part[$i][9][0], 'attachment') === 0) + ) { + $mime_part_headers[] = $tmp_part_id; + } } } } @@ -2113,9 +2118,9 @@ class rcube_imap extends rcube_storage */ // regular part - $struct->ctype_primary = strtolower($part[0]); + $struct->ctype_primary = strtolower($part[0]); $struct->ctype_secondary = strtolower($part[1]); - $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary; + $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary; // read content type parameters if (is_array($part[2])) { @@ -2239,96 +2244,37 @@ class rcube_imap extends rcube_storage */ protected function set_part_filename(&$part, $headers = null) { - if (!empty($part->d_parameters['filename'])) { - $filename_mime = $part->d_parameters['filename']; - } - else if (!empty($part->d_parameters['filename*'])) { - $filename_encoded = $part->d_parameters['filename*']; - } - else if (!empty($part->ctype_parameters['name*'])) { - $filename_encoded = $part->ctype_parameters['name*']; - } - // RFC2231 value continuations - // TODO: this should be rewrited to support RFC2231 4.1 combinations - else if (!empty($part->d_parameters['filename*0'])) { - $i = 0; - while (isset($part->d_parameters['filename*'.$i])) { - $filename_mime .= $part->d_parameters['filename*'.$i]; - $i++; - } - // some servers (eg. dovecot-1.x) have no support for parameter value continuations - // we must fetch and parse headers "manually" - if ($i<2) { - if (!$headers) { - $headers = $this->conn->fetchPartHeader( - $this->folder, $this->msg_uid, true, $part->mime_id); - } - $filename_mime = ''; - $i = 0; - while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { - $filename_mime .= $matches[1]; - $i++; - } + // Some IMAP servers do not support RFC2231, if we have + // part headers we'll get attachment name from them, not the BODYSTRUCTURE + $rfc2231_params = array(); + if (!empty($headers) || !empty($part->headers)) { + if (is_object($headers)) { + $headers = get_object_vars($headers); } - } - else if (!empty($part->d_parameters['filename*0*'])) { - $i = 0; - while (isset($part->d_parameters['filename*'.$i.'*'])) { - $filename_encoded .= $part->d_parameters['filename*'.$i.'*']; - $i++; + else { + $headers = !empty($headers) ? rcube_mime::parse_headers($headers) : $part->headers; } - if ($i<2) { - if (!$headers) { - $headers = $this->conn->fetchPartHeader( - $this->folder, $this->msg_uid, true, $part->mime_id); - } - $filename_encoded = ''; - $i = 0; $matches = array(); - while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { - $filename_encoded .= $matches[1]; - $i++; + + $tokens = preg_split('/;[\s\r\n\t]*/', $headers['content-type'] . ';' . $headers['content-disposition']); + + foreach ($tokens as $token) { + // TODO: Use order defined by the parameter name not order of occurrence in the header + if (preg_match('/^(name|filename)\*([0-9]*)\*?="*([^"]+)"*/i', $token, $matches)) { + $rfc2231_params[strtolower($matches[1])] .= $matches[3]; } } } - else if (!empty($part->ctype_parameters['name*0'])) { - $i = 0; - while (isset($part->ctype_parameters['name*'.$i])) { - $filename_mime .= $part->ctype_parameters['name*'.$i]; - $i++; - } - if ($i<2) { - if (!$headers) { - $headers = $this->conn->fetchPartHeader( - $this->folder, $this->msg_uid, true, $part->mime_id); - } - $filename_mime = ''; - $i = 0; $matches = array(); - while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { - $filename_mime .= $matches[1]; - $i++; - } - } + + if (isset($rfc2231_params['name'])) { + $filename_encoded = $rfc2231_params['name']; } - else if (!empty($part->ctype_parameters['name*0*'])) { - $i = 0; - while (isset($part->ctype_parameters['name*'.$i.'*'])) { - $filename_encoded .= $part->ctype_parameters['name*'.$i.'*']; - $i++; - } - if ($i<2) { - if (!$headers) { - $headers = $this->conn->fetchPartHeader( - $this->folder, $this->msg_uid, true, $part->mime_id); - } - $filename_encoded = ''; - $i = 0; $matches = array(); - while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) { - $filename_encoded .= $matches[1]; - $i++; - } - } + else if (isset($rfc2231_params['filename'])) { + $filename_encoded = $rfc2231_params['filename']; + } + else if (!empty($part->d_parameters['filename'])) { + $filename_mime = $part->d_parameters['filename']; } - // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird) + // read 'name' after rfc2231 parameters as it may contain truncated filename (from Thunderbird) else if (!empty($part->ctype_parameters['name'])) { $filename_mime = $part->ctype_parameters['name']; } @@ -2341,7 +2287,7 @@ class rcube_imap extends rcube_storage } // decode filename - if (!empty($filename_mime)) { + if (isset($filename_mime)) { if (!empty($part->charset)) { $charset = $part->charset; } @@ -2354,7 +2300,7 @@ class rcube_imap extends rcube_storage $part->filename = rcube_mime::decode_mime_string($filename_mime, $charset); } - else if (!empty($filename_encoded)) { + else if (isset($filename_encoded)) { // decode filename according to RFC 2231, Section 4 if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) { $filename_charset = $fmatches[1]; @@ -2363,6 +2309,18 @@ class rcube_imap extends rcube_storage $part->filename = rcube_charset::convert(urldecode($filename_encoded), $filename_charset); } + + // Workaround for invalid Content-Type (#6816) + // Some servers for "Content-Type: PDF; name=test.pdf" may return text/plain and ignore name argument + if ($part->mimetype == 'text/plain' && !empty($headers['content-type'])) { + $tokens = preg_split('/;[\s\r\n\t]*/', $headers['content-type']); + $type = rcube_mime::fix_mimetype($tokens[0]); + + if ($type != $part->mimetype) { + $part->mimetype = $type; + list($part->ctype_primary, $part->ctype_secondary) = explode('/', $part->mimetype); + } + } } /** diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php index d3848d507..7d75d6b00 100644 --- a/program/lib/Roundcube/rcube_mime.php +++ b/program/lib/Roundcube/rcube_mime.php @@ -893,4 +893,37 @@ class rcube_mime return implode('@', $parts); } + + /** + * Fix mimetype name. + * + * @param string $type Mimetype + * + * @return string Mimetype + */ + public static function fix_mimetype($type) + { + $type = strtolower(trim($type)); + $aliases = array( + 'image/x-ms-bmp' => 'image/bmp', // #4771 + 'pdf' => 'application/pdf', // #6816 + ); + + if ($alias = $aliases[$type]) { + return $alias; + } + + // Some versions of Outlook create garbage Content-Type: + // application/pdf.A520491B_3BF7_494D_8855_7FAC2C6C0608 + if (preg_match('/^application\/pdf.+/', $type)) { + return 'application/pdf'; + } + + // treat image/pjpeg (image/pjpg, image/jpg) as image/jpeg (#4196) + if (preg_match('/^image\/p?jpe?g$/', $type)) { + return 'image/jpeg'; + } + + return $type; + } } diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index 6d01afef2..4e2aa0799 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -1046,7 +1046,7 @@ function rcmail_part_image_type($part) // Content-Type: image/*... if (strpos($mimetype, 'image/') === 0) { - return rcmail_fix_mimetype($mimetype); + return $mimetype; } // Many clients use application/octet-stream, we'll detect mimetype @@ -1552,31 +1552,6 @@ function rcmail_identity_select($MESSAGE, $identities = null, $compose_mode = 'r return rcmail_sendmail::identity_select($MESSAGE, $identities, $compose_mode); } -// Fixes some content-type names -function rcmail_fix_mimetype($name) -{ - $map = array( - 'image/x-ms-bmp' => 'image/bmp', // #1490282 - ); - - $name = strtolower($name); - - if ($alias = $map[$name]) { - $name = $alias; - } - // Some versions of Outlook create garbage Content-Type: - // application/pdf.A520491B_3BF7_494D_8855_7FAC2C6C0608 - else if (preg_match('/^application\/pdf.+/', $name)) { - $name = 'application/pdf'; - } - // treat image/pjpeg (image/pjpg, image/jpg) as image/jpeg (#1489097) - else if (preg_match('/^image\/p?jpe?g$/', $name)) { - $name = 'image/jpeg'; - } - - return $name; -} - // return attachment filename, handle empty filename case function rcmail_attachment_name($attachment, $display = false) { diff --git a/program/steps/mail/get.inc b/program/steps/mail/get.inc index bca413228..9327307f5 100644 --- a/program/steps/mail/get.inc +++ b/program/steps/mail/get.inc @@ -160,7 +160,7 @@ if (empty($_GET['_thumb']) && $attachment->is_valid()) { } // "fix" real mimetype the same way the original is before comparison - $real_mimetype = rcmail_fix_mimetype($real_mimetype); + $real_mimetype = rcube_mime::fix_mimetype($real_mimetype); $valid = $valid_extension && rcmail_mimetype_compare($real_mimetype, $mimetype); } @@ -446,7 +446,7 @@ class rcmail_attachment_handler // check connection status self::check_storage_status(); - $this->mimetype = rcmail_fix_mimetype($this->mimetype); + $this->mimetype = rcube_mime::fix_mimetype($this->mimetype); } /** diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc index b7d949b76..03a26585a 100644 --- a/program/steps/mail/show.inc +++ b/program/steps/mail/show.inc @@ -194,7 +194,7 @@ function rcmail_message_attachments($attrib) foreach ($MESSAGE->attachments as $attach_prop) { $filename = rcmail_attachment_name($attach_prop, true); $filesize = $RCMAIL->message_part_size($attach_prop); - $mimetype = rcmail_fix_mimetype($attach_prop->mimetype); + $mimetype = $attach_prop->mimetype; $class = rcube_utils::file2class($mimetype, $filename); $id = 'attach' . $attach_prop->mime_id;