From ced34cb15e095836767971aa4d27b141fb1d7ec9 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak <alec@alec.pl> Date: Sat, 18 Oct 2014 08:47:54 -0400 Subject: [PATCH] Merge pull request #230 from bytesatwork-xx/master --- program/lib/Roundcube/rcube_mime.php | 297 +++++++++++++++++++++++++++++++++++++++++++---------------- 1 files changed, 215 insertions(+), 82 deletions(-) diff --git a/program/lib/Roundcube/rcube_mime.php b/program/lib/Roundcube/rcube_mime.php index ac4be95..f66cf14 100644 --- a/program/lib/Roundcube/rcube_mime.php +++ b/program/lib/Roundcube/rcube_mime.php @@ -127,10 +127,11 @@ * @param int $max List only this number of addresses * @param boolean $decode Decode address strings * @param string $fallback Fallback charset if none specified + * @param boolean $addronly Return flat array with e-mail addresses only * - * @return array Indexed list of addresses + * @return array Indexed list of addresses */ - static function decode_address_list($input, $max = null, $decode = true, $fallback = null) + static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false) { $a = self::parse_address_list($input, $decode, $fallback); $out = array(); @@ -145,20 +146,21 @@ foreach ($a as $val) { $j++; $address = trim($val['address']); - $name = trim($val['name']); - if ($name && $address && $name != $address) - $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address); - else if ($address) - $string = $address; - else if ($name) - $string = $name; + if ($addronly) { + $out[$j] = $address; + } + else { + $name = trim($val['name']); + if ($name && $address && $name != $address) + $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address); + else if ($address) + $string = $address; + else if ($name) + $string = $name; - $out[$j] = array( - 'name' => $name, - 'mailto' => $address, - 'string' => $string - ); + $out[$j] = array('name' => $name, 'mailto' => $address, 'string' => $string); + } if ($max && $j==$max) break; @@ -359,6 +361,14 @@ $address = $m[1]; $name = ''; } + // special case (#1489092) + else if (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) { + $address = 'MAILER-DAEMON'; + $name = substr($val, 0, -strlen($m[1])); + } + else if (preg_match('/('.$email_rx.')/', $val, $m)) { + $name = $m[1]; + } else { $name = $val; } @@ -371,14 +381,20 @@ } if ($decode) { $name = self::decode_header($name, $fallback); + // some clients encode addressee name with quotes around it + if ($name[0] == '"' && $name[strlen($name)-1] == '"') { + $name = substr($name, 1, -1); + } } } if (!$address && $name) { $address = $name; + $name = ''; } if ($address) { + $address = self::fix_email($address); $result[$key] = array('name' => $name, 'address' => $address); } } @@ -465,20 +481,23 @@ /** * Interpret a format=flowed message body according to RFC 2646 * - * @param string $text Raw body formatted as flowed text + * @param string $text Raw body formatted as flowed text + * @param string $mark Mark each flowed line with specified character * * @return string Interpreted text with unwrapped lines and stuffed space removed */ - public static function unfold_flowed($text) + public static function unfold_flowed($text, $mark = null) { $text = preg_split('/\r?\n/', $text); $last = -1; $q_level = 0; + $marks = array(); foreach ($text as $idx => $line) { - if ($line[0] == '>') { - // remove quote chars, store level in $q - $line = preg_replace('/^>+/', '', $line, -1, $q); + if (preg_match('/^(>+)/', $line, $m)) { + // remove quote chars + $q = strlen($m[1]); + $line = preg_replace('/^>+/', '', $line); // remove (optional) space-staffing $line = preg_replace('/^ /', '', $line); @@ -492,6 +511,10 @@ ) { $text[$last] .= $line; unset($text[$idx]); + + if ($mark) { + $marks[$last] = true; + } } else { $last = $idx; @@ -504,7 +527,7 @@ } else { // remove space-stuffing - $line = preg_replace('/^\s/', '', $line); + $line = preg_replace('/^ /', '', $line); if (isset($text[$last]) && $line && $text[$last] != '-- ' @@ -512,6 +535,10 @@ ) { $text[$last] .= $line; unset($text[$idx]); + + if ($mark) { + $marks[$last] = true; + } } else { $text[$idx] = $line; @@ -520,6 +547,12 @@ } } $q_level = $q; + } + + if (!empty($marks)) { + foreach (array_keys($marks) as $mk) { + $text[$mk] = $mark . $text[$mk]; + } } return implode("\r\n", $text); @@ -541,9 +574,10 @@ foreach ($text as $idx => $line) { if ($line != '-- ') { - if ($line[0] == '>') { - // remove quote chars, store level in $level - $line = preg_replace('/^>+/', '', $line, -1, $level); + if (preg_match('/^(>+)/', $line, $m)) { + // remove quote chars + $level = strlen($m[1]); + $line = preg_replace('/^>+/', '', $line); // remove (optional) space-staffing and spaces before the line end $line = preg_replace('/(^ | +$)/', '', $line); $prefix = str_repeat('>', $level) . ' '; @@ -564,82 +598,122 @@ /** - * Improved wordwrap function. + * Improved wordwrap function with multibyte support. + * The code is based on Zend_Text_MultiByte::wordWrap(). * - * @param string $string Text to wrap - * @param int $width Line width - * @param string $break Line separator - * @param bool $cut Enable to cut word - * @param string $charset Charset of $string + * @param string $string Text to wrap + * @param int $width Line width + * @param string $break Line separator + * @param bool $cut Enable to cut word + * @param string $charset Charset of $string + * @param bool $wrap_quoted When enabled quoted lines will not be wrapped * * @return string Text */ - public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null) + public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null, $wrap_quoted=true) { - if ($charset && function_exists('mb_internal_encoding')) { + // Note: Never try to use iconv instead of mbstring functions here + // Iconv's substr/strlen are 100x slower (#1489113) + + if ($charset && $charset != RCUBE_CHARSET && function_exists('mb_internal_encoding')) { mb_internal_encoding($charset); } - $para = preg_split('/\r?\n/', $string); - $string = ''; + // Convert \r\n to \n, this is our line-separator + $string = str_replace("\r\n", "\n", $string); + $separator = "\n"; // must be 1 character length + $result = array(); - while (count($para)) { - $line = array_shift($para); - if ($line[0] == '>') { - $string .= $line . (count($para) ? $break : ''); - continue; - } + while (($stringLength = mb_strlen($string)) > 0) { + $breakPos = mb_strpos($string, $separator, 0); - $list = explode(' ', $line); - $len = 0; - while (count($list)) { - $line = array_shift($list); - $l = mb_strlen($line); - $space = $len ? 1 : 0; - $newlen = $len + $l + $space; - - if ($newlen <= $width) { - $string .= ($space ? ' ' : '').$line; - $len += ($space + $l); + // quoted line (do not wrap) + if ($wrap_quoted && $string[0] == '>') { + if ($breakPos === $stringLength - 1 || $breakPos === false) { + $subString = $string; + $cutLength = null; } else { - if ($l > $width) { - if ($cut) { - $start = 0; - while ($l) { - $str = mb_substr($line, $start, $width); - $strlen = mb_strlen($str); - $string .= ($len ? $break : '').$str; - $start += $strlen; - $l -= $strlen; - $len = $strlen; + $subString = mb_substr($string, 0, $breakPos); + $cutLength = $breakPos + 1; + } + } + // next line found and current line is shorter than the limit + else if ($breakPos !== false && $breakPos < $width) { + if ($breakPos === $stringLength - 1) { + $subString = $string; + $cutLength = null; + } + else { + $subString = mb_substr($string, 0, $breakPos); + $cutLength = $breakPos + 1; + } + } + else { + $subString = mb_substr($string, 0, $width); + + // last line + if ($breakPos === false && $subString === $string) { + $cutLength = null; + } + else { + $nextChar = mb_substr($string, $width, 1); + + if ($nextChar === ' ' || $nextChar === $separator) { + $afterNextChar = mb_substr($string, $width + 1, 1); + + // Note: mb_substr() does never return False + if ($afterNextChar === false || $afterNextChar === '') { + $subString .= $nextChar; + } + + $cutLength = mb_strlen($subString) + 1; + } + else { + $spacePos = mb_strrpos($subString, ' ', 0); + + if ($spacePos !== false) { + $subString = mb_substr($subString, 0, $spacePos); + $cutLength = $spacePos + 1; + } + else if ($cut === false) { + $spacePos = mb_strpos($string, ' ', 0); + + if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) { + $subString = mb_substr($string, 0, $spacePos); + $cutLength = $spacePos + 1; + } + else if ($breakPos === false) { + $subString = $string; + $cutLength = null; + } + else { + $subString = mb_substr($string, 0, $breakPos); + $cutLength = $breakPos + 1; } } else { - $string .= ($len ? $break : '').$line; - if (count($list)) { - $string .= $break; - } - $len = 0; + $cutLength = $width; } - } - else { - $string .= $break.$line; - $len = $l; } } } - if (count($para)) { - $string .= $break; + $result[] = $subString; + + if ($cutLength !== null) { + $string = mb_substr($string, $cutLength, ($stringLength - $cutLength)); + } + else { + break; } } - if ($charset && function_exists('mb_internal_encoding')) { + if ($charset && $charset != RCUBE_CHARSET && function_exists('mb_internal_encoding')) { mb_internal_encoding(RCUBE_CHARSET); } - return $string; + return implode($break, $result); } @@ -659,12 +733,20 @@ */ public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false) { + static $mime_ext = array(); + $mime_type = null; - $mime_magic = rcube::get_instance()->config->get('mime_magic'); - $mime_ext = $skip_suffix ? null : @include(RCUBE_CONFIG_DIR . '/mimetypes.php'); + $config = rcube::get_instance()->config; + $mime_magic = $config->get('mime_magic'); + + if (!$skip_suffix && empty($mime_ext)) { + foreach ($config->resolve_paths('mimetypes.php') as $fpath) { + $mime_ext = array_merge($mime_ext, (array) @include($fpath)); + } + } // use file name suffix with hard-coded mime-type map - if (is_array($mime_ext) && $name) { + if (!$skip_suffix && is_array($mime_ext) && $name) { if ($suffix = substr($name, strrpos($name, '.')+1)) { $mime_type = $mime_ext[strtolower($suffix)]; } @@ -672,7 +754,16 @@ // try fileinfo extension if available if (!$mime_type && function_exists('finfo_open')) { - if ($finfo = finfo_open(FILEINFO_MIME, $mime_magic)) { + // null as a 2nd argument should be the same as no argument + // this however is not true on all systems/versions + if ($mime_magic) { + $finfo = finfo_open(FILEINFO_MIME, $mime_magic); + } + else { + $finfo = finfo_open(FILEINFO_MIME); + } + + if ($finfo) { if ($is_stream) $mime_type = finfo_buffer($finfo, $path); else @@ -732,19 +823,20 @@ $file_paths[] = '/etc/httpd2/mime.types'; $file_paths[] = '/etc/apache/mime.types'; $file_paths[] = '/etc/apache2/mime.types'; + $file_paths[] = '/etc/nginx/mime.types'; $file_paths[] = '/usr/local/etc/httpd/conf/mime.types'; $file_paths[] = '/usr/local/etc/apache/conf/mime.types'; } foreach ($file_paths as $fp) { - if (is_readable($fp)) { + if (@is_readable($fp)) { $lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); break; } } $mime_types = $mime_extensions = array(); - $regex = "/([\w\+\-\.\/]+)\t+([\w\s]+)/i"; + $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i"; foreach((array)$lines as $line) { // skip comments or mime types w/o any extensions if ($line[0] == '#' || !preg_match($regex, $line, $matches)) @@ -760,11 +852,37 @@ // fallback to some well-known types most important for daily emails if (empty($mime_types)) { - $mime_extensions = @include(RCUBE_CONFIG_DIR . '/mimetypes.php'); - $mime_extensions += array('gif' => 'image/gif', 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'tif' => 'image/tiff'); + foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) { + $mime_extensions = array_merge($mime_extensions, (array) @include($fpath)); + } - foreach ($mime_extensions as $ext => $mime) + foreach ($mime_extensions as $ext => $mime) { $mime_types[$mime][] = $ext; + } + } + + // Add some known aliases that aren't included by some mime.types (#1488891) + // the order is important here so standard extensions have higher prio + $aliases = array( + 'image/gif' => array('gif'), + 'image/png' => array('png'), + 'image/x-png' => array('png'), + 'image/jpeg' => array('jpg', 'jpeg', 'jpe'), + 'image/jpg' => array('jpg', 'jpeg', 'jpe'), + 'image/pjpeg' => array('jpg', 'jpeg', 'jpe'), + 'image/tiff' => array('tif'), + 'message/rfc822' => array('eml'), + 'text/x-mail' => array('eml'), + ); + + foreach ($aliases as $mime => $exts) { + $mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts)); + + foreach ($exts as $ext) { + if (!isset($mime_extensions[$ext])) { + $mime_extensions[$ext] = $mime; + } + } } return $mimetype ? $mime_types[$mimetype] : $mime_extensions; @@ -789,4 +907,19 @@ return 'image/' . $type; } + /** + * Try to fix invalid email addresses + */ + public static function fix_email($email) + { + $parts = rcube_utils::explode_quoted_string('@', $email); + foreach ($parts as $idx => $part) { + // remove redundant quoting (#1490040) + if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) { + $parts[$idx] = $m[1]; + } + } + + return implode('@', $parts); + } } -- Gitblit v1.9.1