PageRenderTime 48ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/etc/apps/webmail/program/lib/Roundcube/rcube_mime.php

https://github.com/raiman264/zpanelx
PHP | 892 lines | 541 code | 124 blank | 227 comment | 194 complexity | e2e71878810fd2c15146fef0bc99192c MD5 | raw file
Possible License(s): GPL-2.0, LGPL-3.0, LGPL-2.1, CC-BY-SA-4.0, GPL-3.0
  1. <?php
  2. /*
  3. +-----------------------------------------------------------------------+
  4. | This file is part of the Roundcube Webmail client |
  5. | Copyright (C) 2005-2012, The Roundcube Dev Team |
  6. | Copyright (C) 2011-2012, Kolab Systems AG |
  7. | |
  8. | Licensed under the GNU General Public License version 3 or |
  9. | any later version with exceptions for skins & plugins. |
  10. | See the README file for a full license statement. |
  11. | |
  12. | PURPOSE: |
  13. | MIME message parsing utilities |
  14. +-----------------------------------------------------------------------+
  15. | Author: Thomas Bruederli <roundcube@gmail.com> |
  16. | Author: Aleksander Machniak <alec@alec.pl> |
  17. +-----------------------------------------------------------------------+
  18. */
  19. /**
  20. * Class for parsing MIME messages
  21. *
  22. * @package Framework
  23. * @subpackage Storage
  24. * @author Thomas Bruederli <roundcube@gmail.com>
  25. * @author Aleksander Machniak <alec@alec.pl>
  26. */
  27. class rcube_mime
  28. {
  29. private static $default_charset;
  30. /**
  31. * Object constructor.
  32. */
  33. function __construct($default_charset = null)
  34. {
  35. self::$default_charset = $default_charset;
  36. }
  37. /**
  38. * Returns message/object character set name
  39. *
  40. * @return string Characted set name
  41. */
  42. public static function get_charset()
  43. {
  44. if (self::$default_charset) {
  45. return self::$default_charset;
  46. }
  47. if ($charset = rcube::get_instance()->config->get('default_charset')) {
  48. return $charset;
  49. }
  50. return RCUBE_CHARSET;
  51. }
  52. /**
  53. * Parse the given raw message source and return a structure
  54. * of rcube_message_part objects.
  55. *
  56. * It makes use of the PEAR:Mail_mimeDecode library
  57. *
  58. * @param string The message source
  59. * @return object rcube_message_part The message structure
  60. */
  61. public static function parse_message($raw_body)
  62. {
  63. $mime = new Mail_mimeDecode($raw_body);
  64. $struct = $mime->decode(array('include_bodies' => true, 'decode_bodies' => true));
  65. return self::structure_part($struct);
  66. }
  67. /**
  68. * Recursive method to convert a Mail_mimeDecode part into a rcube_message_part object
  69. *
  70. * @param object A message part struct
  71. * @param int Part count
  72. * @param string Parent MIME ID
  73. *
  74. * @return object rcube_message_part
  75. */
  76. private static function structure_part($part, $count=0, $parent='')
  77. {
  78. $struct = new rcube_message_part;
  79. $struct->mime_id = $part->mime_id ? $part->mime_id : (empty($parent) ? (string)$count : "$parent.$count");
  80. $struct->headers = $part->headers;
  81. $struct->ctype_primary = $part->ctype_primary;
  82. $struct->ctype_secondary = $part->ctype_secondary;
  83. $struct->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
  84. $struct->ctype_parameters = $part->ctype_parameters;
  85. if ($part->headers['content-transfer-encoding'])
  86. $struct->encoding = $part->headers['content-transfer-encoding'];
  87. if ($part->ctype_parameters['charset'])
  88. $struct->charset = $part->ctype_parameters['charset'];
  89. $part_charset = $struct->charset ? $struct->charset : self::get_charset();
  90. // determine filename
  91. if (($filename = $part->d_parameters['filename']) || ($filename = $part->ctype_parameters['name'])) {
  92. $struct->filename = rcube_mime::decode_mime_string($filename, $part_charset);
  93. }
  94. // copy part body and convert it to UTF-8 if necessary
  95. $struct->body = $part->ctype_primary == 'text' || !$part->ctype_parameters['charset'] ? rcube_charset::convert($part->body, $part_charset) : $part->body;
  96. $struct->size = strlen($part->body);
  97. $struct->disposition = $part->disposition;
  98. foreach ((array)$part->parts as $child_part) {
  99. $struct->parts[] = self::structure_part($child_part, ++$count, $struct->mime_id);
  100. }
  101. return $struct;
  102. }
  103. /**
  104. * Split an address list into a structured array list
  105. *
  106. * @param string $input Input string
  107. * @param int $max List only this number of addresses
  108. * @param boolean $decode Decode address strings
  109. * @param string $fallback Fallback charset if none specified
  110. * @param boolean $addronly Return flat array with e-mail addresses only
  111. *
  112. * @return array Indexed list of addresses
  113. */
  114. static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false)
  115. {
  116. $a = self::parse_address_list($input, $decode, $fallback);
  117. $out = array();
  118. $j = 0;
  119. // Special chars as defined by RFC 822 need to in quoted string (or escaped).
  120. $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
  121. if (!is_array($a))
  122. return $out;
  123. foreach ($a as $val) {
  124. $j++;
  125. $address = trim($val['address']);
  126. if ($addronly) {
  127. $out[$j] = $address;
  128. }
  129. else {
  130. $name = trim($val['name']);
  131. if ($name && $address && $name != $address)
  132. $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
  133. else if ($address)
  134. $string = $address;
  135. else if ($name)
  136. $string = $name;
  137. $out[$j] = array('name' => $name, 'mailto' => $address, 'string' => $string);
  138. }
  139. if ($max && $j==$max)
  140. break;
  141. }
  142. return $out;
  143. }
  144. /**
  145. * Decode a message header value
  146. *
  147. * @param string $input Header value
  148. * @param string $fallback Fallback charset if none specified
  149. *
  150. * @return string Decoded string
  151. */
  152. public static function decode_header($input, $fallback = null)
  153. {
  154. $str = self::decode_mime_string((string)$input, $fallback);
  155. return $str;
  156. }
  157. /**
  158. * Decode a mime-encoded string to internal charset
  159. *
  160. * @param string $input Header value
  161. * @param string $fallback Fallback charset if none specified
  162. *
  163. * @return string Decoded string
  164. */
  165. public static function decode_mime_string($input, $fallback = null)
  166. {
  167. $default_charset = !empty($fallback) ? $fallback : self::get_charset();
  168. // rfc: all line breaks or other characters not found
  169. // in the Base64 Alphabet must be ignored by decoding software
  170. // delete all blanks between MIME-lines, differently we can
  171. // receive unnecessary blanks and broken utf-8 symbols
  172. $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
  173. // encoded-word regexp
  174. $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
  175. // Find all RFC2047's encoded words
  176. if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
  177. // Initialize variables
  178. $tmp = array();
  179. $out = '';
  180. $start = 0;
  181. foreach ($matches as $idx => $m) {
  182. $pos = $m[0][1];
  183. $charset = $m[1][0];
  184. $encoding = $m[2][0];
  185. $text = $m[3][0];
  186. $length = strlen($m[0][0]);
  187. // Append everything that is before the text to be decoded
  188. if ($start != $pos) {
  189. $substr = substr($input, $start, $pos-$start);
  190. $out .= rcube_charset::convert($substr, $default_charset);
  191. $start = $pos;
  192. }
  193. $start += $length;
  194. // Per RFC2047, each string part "MUST represent an integral number
  195. // of characters . A multi-octet character may not be split across
  196. // adjacent encoded-words." However, some mailers break this, so we
  197. // try to handle characters spanned across parts anyway by iterating
  198. // through and aggregating sequential encoded parts with the same
  199. // character set and encoding, then perform the decoding on the
  200. // aggregation as a whole.
  201. $tmp[] = $text;
  202. if ($next_match = $matches[$idx+1]) {
  203. if ($next_match[0][1] == $start
  204. && $next_match[1][0] == $charset
  205. && $next_match[2][0] == $encoding
  206. ) {
  207. continue;
  208. }
  209. }
  210. $count = count($tmp);
  211. $text = '';
  212. // Decode and join encoded-word's chunks
  213. if ($encoding == 'B' || $encoding == 'b') {
  214. // base64 must be decoded a segment at a time
  215. for ($i=0; $i<$count; $i++)
  216. $text .= base64_decode($tmp[$i]);
  217. }
  218. else { //if ($encoding == 'Q' || $encoding == 'q') {
  219. // quoted printable can be combined and processed at once
  220. for ($i=0; $i<$count; $i++)
  221. $text .= $tmp[$i];
  222. $text = str_replace('_', ' ', $text);
  223. $text = quoted_printable_decode($text);
  224. }
  225. $out .= rcube_charset::convert($text, $charset);
  226. $tmp = array();
  227. }
  228. // add the last part of the input string
  229. if ($start != strlen($input)) {
  230. $out .= rcube_charset::convert(substr($input, $start), $default_charset);
  231. }
  232. // return the results
  233. return $out;
  234. }
  235. // no encoding information, use fallback
  236. return rcube_charset::convert($input, $default_charset);
  237. }
  238. /**
  239. * Decode a mime part
  240. *
  241. * @param string $input Input string
  242. * @param string $encoding Part encoding
  243. * @return string Decoded string
  244. */
  245. public static function decode($input, $encoding = '7bit')
  246. {
  247. switch (strtolower($encoding)) {
  248. case 'quoted-printable':
  249. return quoted_printable_decode($input);
  250. case 'base64':
  251. return base64_decode($input);
  252. case 'x-uuencode':
  253. case 'x-uue':
  254. case 'uue':
  255. case 'uuencode':
  256. return convert_uudecode($input);
  257. case '7bit':
  258. default:
  259. return $input;
  260. }
  261. }
  262. /**
  263. * Split RFC822 header string into an associative array
  264. * @access private
  265. */
  266. public static function parse_headers($headers)
  267. {
  268. $a_headers = array();
  269. $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
  270. $lines = explode("\n", $headers);
  271. $c = count($lines);
  272. for ($i=0; $i<$c; $i++) {
  273. if ($p = strpos($lines[$i], ': ')) {
  274. $field = strtolower(substr($lines[$i], 0, $p));
  275. $value = trim(substr($lines[$i], $p+1));
  276. if (!empty($value))
  277. $a_headers[$field] = $value;
  278. }
  279. }
  280. return $a_headers;
  281. }
  282. /**
  283. * @access private
  284. */
  285. private static function parse_address_list($str, $decode = true, $fallback = null)
  286. {
  287. // remove any newlines and carriage returns before
  288. $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
  289. // extract list items, remove comments
  290. $str = self::explode_header_string(',;', $str, true);
  291. $result = array();
  292. // simplified regexp, supporting quoted local part
  293. $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
  294. foreach ($str as $key => $val) {
  295. $name = '';
  296. $address = '';
  297. $val = trim($val);
  298. if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
  299. $address = $m[2];
  300. $name = trim($m[1]);
  301. }
  302. else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
  303. $address = $m[1];
  304. $name = '';
  305. }
  306. // special case (#1489092)
  307. else if (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) {
  308. $address = 'MAILER-DAEMON';
  309. $name = substr($val, 0, -strlen($m[1]));
  310. }
  311. else if (preg_match('/('.$email_rx.')/', $val, $m)) {
  312. $name = $m[1];
  313. }
  314. else {
  315. $name = $val;
  316. }
  317. // dequote and/or decode name
  318. if ($name) {
  319. if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
  320. $name = substr($name, 1, -1);
  321. $name = stripslashes($name);
  322. }
  323. if ($decode) {
  324. $name = self::decode_header($name, $fallback);
  325. // some clients encode addressee name with quotes around it
  326. if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
  327. $name = substr($name, 1, -1);
  328. }
  329. }
  330. }
  331. if (!$address && $name) {
  332. $address = $name;
  333. $name = '';
  334. }
  335. if ($address) {
  336. $result[$key] = array('name' => $name, 'address' => $address);
  337. }
  338. }
  339. return $result;
  340. }
  341. /**
  342. * Explodes header (e.g. address-list) string into array of strings
  343. * using specified separator characters with proper handling
  344. * of quoted-strings and comments (RFC2822)
  345. *
  346. * @param string $separator String containing separator characters
  347. * @param string $str Header string
  348. * @param bool $remove_comments Enable to remove comments
  349. *
  350. * @return array Header items
  351. */
  352. public static function explode_header_string($separator, $str, $remove_comments = false)
  353. {
  354. $length = strlen($str);
  355. $result = array();
  356. $quoted = false;
  357. $comment = 0;
  358. $out = '';
  359. for ($i=0; $i<$length; $i++) {
  360. // we're inside a quoted string
  361. if ($quoted) {
  362. if ($str[$i] == '"') {
  363. $quoted = false;
  364. }
  365. else if ($str[$i] == "\\") {
  366. if ($comment <= 0) {
  367. $out .= "\\";
  368. }
  369. $i++;
  370. }
  371. }
  372. // we are inside a comment string
  373. else if ($comment > 0) {
  374. if ($str[$i] == ')') {
  375. $comment--;
  376. }
  377. else if ($str[$i] == '(') {
  378. $comment++;
  379. }
  380. else if ($str[$i] == "\\") {
  381. $i++;
  382. }
  383. continue;
  384. }
  385. // separator, add to result array
  386. else if (strpos($separator, $str[$i]) !== false) {
  387. if ($out) {
  388. $result[] = $out;
  389. }
  390. $out = '';
  391. continue;
  392. }
  393. // start of quoted string
  394. else if ($str[$i] == '"') {
  395. $quoted = true;
  396. }
  397. // start of comment
  398. else if ($remove_comments && $str[$i] == '(') {
  399. $comment++;
  400. }
  401. if ($comment <= 0) {
  402. $out .= $str[$i];
  403. }
  404. }
  405. if ($out && $comment <= 0) {
  406. $result[] = $out;
  407. }
  408. return $result;
  409. }
  410. /**
  411. * Interpret a format=flowed message body according to RFC 2646
  412. *
  413. * @param string $text Raw body formatted as flowed text
  414. *
  415. * @return string Interpreted text with unwrapped lines and stuffed space removed
  416. */
  417. public static function unfold_flowed($text)
  418. {
  419. $text = preg_split('/\r?\n/', $text);
  420. $last = -1;
  421. $q_level = 0;
  422. foreach ($text as $idx => $line) {
  423. if (preg_match('/^(>+)/', $line, $m)) {
  424. // remove quote chars
  425. $q = strlen($m[1]);
  426. $line = preg_replace('/^>+/', '', $line);
  427. // remove (optional) space-staffing
  428. $line = preg_replace('/^ /', '', $line);
  429. // The same paragraph (We join current line with the previous one) when:
  430. // - the same level of quoting
  431. // - previous line was flowed
  432. // - previous line contains more than only one single space (and quote char(s))
  433. if ($q == $q_level
  434. && isset($text[$last]) && $text[$last][strlen($text[$last])-1] == ' '
  435. && !preg_match('/^>+ {0,1}$/', $text[$last])
  436. ) {
  437. $text[$last] .= $line;
  438. unset($text[$idx]);
  439. }
  440. else {
  441. $last = $idx;
  442. }
  443. }
  444. else {
  445. $q = 0;
  446. if ($line == '-- ') {
  447. $last = $idx;
  448. }
  449. else {
  450. // remove space-stuffing
  451. $line = preg_replace('/^\s/', '', $line);
  452. if (isset($text[$last]) && $line
  453. && $text[$last] != '-- '
  454. && $text[$last][strlen($text[$last])-1] == ' '
  455. ) {
  456. $text[$last] .= $line;
  457. unset($text[$idx]);
  458. }
  459. else {
  460. $text[$idx] = $line;
  461. $last = $idx;
  462. }
  463. }
  464. }
  465. $q_level = $q;
  466. }
  467. return implode("\r\n", $text);
  468. }
  469. /**
  470. * Wrap the given text to comply with RFC 2646
  471. *
  472. * @param string $text Text to wrap
  473. * @param int $length Length
  474. * @param string $charset Character encoding of $text
  475. *
  476. * @return string Wrapped text
  477. */
  478. public static function format_flowed($text, $length = 72, $charset=null)
  479. {
  480. $text = preg_split('/\r?\n/', $text);
  481. foreach ($text as $idx => $line) {
  482. if ($line != '-- ') {
  483. if (preg_match('/^(>+)/', $line, $m)) {
  484. // remove quote chars
  485. $level = strlen($m[1]);
  486. $line = preg_replace('/^>+/', '', $line);
  487. // remove (optional) space-staffing and spaces before the line end
  488. $line = preg_replace('/(^ | +$)/', '', $line);
  489. $prefix = str_repeat('>', $level) . ' ';
  490. $line = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset);
  491. }
  492. else if ($line) {
  493. $line = self::wordwrap(rtrim($line), $length - 2, " \r\n", false, $charset);
  494. // space-stuffing
  495. $line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);
  496. }
  497. $text[$idx] = $line;
  498. }
  499. }
  500. return implode("\r\n", $text);
  501. }
  502. /**
  503. * Improved wordwrap function with multibyte support.
  504. * The code is based on Zend_Text_MultiByte::wordWrap().
  505. *
  506. * @param string $string Text to wrap
  507. * @param int $width Line width
  508. * @param string $break Line separator
  509. * @param bool $cut Enable to cut word
  510. * @param string $charset Charset of $string
  511. * @param bool $wrap_quoted When enabled quoted lines will not be wrapped
  512. *
  513. * @return string Text
  514. */
  515. public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null, $wrap_quoted=true)
  516. {
  517. // Note: Never try to use iconv instead of mbstring functions here
  518. // Iconv's substr/strlen are 100x slower (#1489113)
  519. if ($charset && $charset != RCUBE_CHARSET && function_exists('mb_internal_encoding')) {
  520. mb_internal_encoding($charset);
  521. }
  522. // Convert \r\n to \n, this is our line-separator
  523. $string = str_replace("\r\n", "\n", $string);
  524. $separator = "\n"; // must be 1 character length
  525. $result = array();
  526. while (($stringLength = mb_strlen($string)) > 0) {
  527. $breakPos = mb_strpos($string, $separator, 0);
  528. // quoted line (do not wrap)
  529. if ($wrap_quoted && $string[0] == '>') {
  530. if ($breakPos === $stringLength - 1 || $breakPos === false) {
  531. $subString = $string;
  532. $cutLength = null;
  533. }
  534. else {
  535. $subString = mb_substr($string, 0, $breakPos);
  536. $cutLength = $breakPos + 1;
  537. }
  538. }
  539. // next line found and current line is shorter than the limit
  540. else if ($breakPos !== false && $breakPos < $width) {
  541. if ($breakPos === $stringLength - 1) {
  542. $subString = $string;
  543. $cutLength = null;
  544. }
  545. else {
  546. $subString = mb_substr($string, 0, $breakPos);
  547. $cutLength = $breakPos + 1;
  548. }
  549. }
  550. else {
  551. $subString = mb_substr($string, 0, $width);
  552. // last line
  553. if ($breakPos === false && $subString === $string) {
  554. $cutLength = null;
  555. }
  556. else {
  557. $nextChar = mb_substr($string, $width, 1);
  558. if ($nextChar === ' ' || $nextChar === $separator) {
  559. $afterNextChar = mb_substr($string, $width + 1, 1);
  560. // Note: mb_substr() does never return False
  561. if ($afterNextChar === false || $afterNextChar === '') {
  562. $subString .= $nextChar;
  563. }
  564. $cutLength = mb_strlen($subString) + 1;
  565. }
  566. else {
  567. $spacePos = mb_strrpos($subString, ' ', 0);
  568. if ($spacePos !== false) {
  569. $subString = mb_substr($subString, 0, $spacePos);
  570. $cutLength = $spacePos + 1;
  571. }
  572. else if ($cut === false) {
  573. $spacePos = mb_strpos($string, ' ', 0);
  574. if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) {
  575. $subString = mb_substr($string, 0, $spacePos);
  576. $cutLength = $spacePos + 1;
  577. }
  578. else if ($breakPos === false) {
  579. $subString = $string;
  580. $cutLength = null;
  581. }
  582. else {
  583. $subString = mb_substr($string, 0, $breakPos);
  584. $cutLength = $breakPos + 1;
  585. }
  586. }
  587. else {
  588. $cutLength = $width;
  589. }
  590. }
  591. }
  592. }
  593. $result[] = $subString;
  594. if ($cutLength !== null) {
  595. $string = mb_substr($string, $cutLength, ($stringLength - $cutLength));
  596. }
  597. else {
  598. break;
  599. }
  600. }
  601. if ($charset && $charset != RCUBE_CHARSET && function_exists('mb_internal_encoding')) {
  602. mb_internal_encoding(RCUBE_CHARSET);
  603. }
  604. return implode($break, $result);
  605. }
  606. /**
  607. * A method to guess the mime_type of an attachment.
  608. *
  609. * @param string $path Path to the file or file contents
  610. * @param string $name File name (with suffix)
  611. * @param string $failover Mime type supplied for failover
  612. * @param boolean $is_stream Set to True if $path contains file contents
  613. * @param boolean $skip_suffix Set to True if the config/mimetypes.php mappig should be ignored
  614. *
  615. * @return string
  616. * @author Till Klampaeckel <till@php.net>
  617. * @see http://de2.php.net/manual/en/ref.fileinfo.php
  618. * @see http://de2.php.net/mime_content_type
  619. */
  620. public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false)
  621. {
  622. static $mime_ext = array();
  623. $mime_type = null;
  624. $config = rcube::get_instance()->config;
  625. $mime_magic = $config->get('mime_magic');
  626. if (!$skip_suffix && empty($mime_ext)) {
  627. foreach ($config->resolve_paths('mimetypes.php') as $fpath) {
  628. $mime_ext = array_merge($mime_ext, (array) @include($fpath));
  629. }
  630. }
  631. // use file name suffix with hard-coded mime-type map
  632. if (!$skip_suffix && is_array($mime_ext) && $name) {
  633. if ($suffix = substr($name, strrpos($name, '.')+1)) {
  634. $mime_type = $mime_ext[strtolower($suffix)];
  635. }
  636. }
  637. // try fileinfo extension if available
  638. if (!$mime_type && function_exists('finfo_open')) {
  639. // null as a 2nd argument should be the same as no argument
  640. // this however is not true on all systems/versions
  641. if ($mime_magic) {
  642. $finfo = finfo_open(FILEINFO_MIME, $mime_magic);
  643. }
  644. else {
  645. $finfo = finfo_open(FILEINFO_MIME);
  646. }
  647. if ($finfo) {
  648. if ($is_stream)
  649. $mime_type = finfo_buffer($finfo, $path);
  650. else
  651. $mime_type = finfo_file($finfo, $path);
  652. finfo_close($finfo);
  653. }
  654. }
  655. // try PHP's mime_content_type
  656. if (!$mime_type && !$is_stream && function_exists('mime_content_type')) {
  657. $mime_type = @mime_content_type($path);
  658. }
  659. // fall back to user-submitted string
  660. if (!$mime_type) {
  661. $mime_type = $failover;
  662. }
  663. else {
  664. // Sometimes (PHP-5.3?) content-type contains charset definition,
  665. // Remove it (#1487122) also "charset=binary" is useless
  666. $mime_type = array_shift(preg_split('/[; ]/', $mime_type));
  667. }
  668. return $mime_type;
  669. }
  670. /**
  671. * Get mimetype => file extension mapping
  672. *
  673. * @param string Mime-Type to get extensions for
  674. * @return array List of extensions matching the given mimetype or a hash array with ext -> mimetype mappings if $mimetype is not given
  675. */
  676. public static function get_mime_extensions($mimetype = null)
  677. {
  678. static $mime_types, $mime_extensions;
  679. // return cached data
  680. if (is_array($mime_types)) {
  681. return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
  682. }
  683. // load mapping file
  684. $file_paths = array();
  685. if ($mime_types = rcube::get_instance()->config->get('mime_types')) {
  686. $file_paths[] = $mime_types;
  687. }
  688. // try common locations
  689. if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
  690. $file_paths[] = 'C:/xampp/apache/conf/mime.types.';
  691. }
  692. else {
  693. $file_paths[] = '/etc/mime.types';
  694. $file_paths[] = '/etc/httpd/mime.types';
  695. $file_paths[] = '/etc/httpd2/mime.types';
  696. $file_paths[] = '/etc/apache/mime.types';
  697. $file_paths[] = '/etc/apache2/mime.types';
  698. $file_paths[] = '/usr/local/etc/httpd/conf/mime.types';
  699. $file_paths[] = '/usr/local/etc/apache/conf/mime.types';
  700. }
  701. foreach ($file_paths as $fp) {
  702. if (@is_readable($fp)) {
  703. $lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  704. break;
  705. }
  706. }
  707. $mime_types = $mime_extensions = array();
  708. $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";
  709. foreach((array)$lines as $line) {
  710. // skip comments or mime types w/o any extensions
  711. if ($line[0] == '#' || !preg_match($regex, $line, $matches))
  712. continue;
  713. $mime = $matches[1];
  714. foreach (explode(' ', $matches[2]) as $ext) {
  715. $ext = trim($ext);
  716. $mime_types[$mime][] = $ext;
  717. $mime_extensions[$ext] = $mime;
  718. }
  719. }
  720. // fallback to some well-known types most important for daily emails
  721. if (empty($mime_types)) {
  722. foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
  723. $mime_extensions = array_merge($mime_extensions, (array) @include($fpath));
  724. }
  725. foreach ($mime_extensions as $ext => $mime) {
  726. $mime_types[$mime][] = $ext;
  727. }
  728. }
  729. // Add some known aliases that aren't included by some mime.types (#1488891)
  730. // the order is important here so standard extensions have higher prio
  731. $aliases = array(
  732. 'image/gif' => array('gif'),
  733. 'image/png' => array('png'),
  734. 'image/x-png' => array('png'),
  735. 'image/jpeg' => array('jpg', 'jpeg', 'jpe'),
  736. 'image/jpg' => array('jpg', 'jpeg', 'jpe'),
  737. 'image/pjpeg' => array('jpg', 'jpeg', 'jpe'),
  738. 'image/tiff' => array('tif'),
  739. 'message/rfc822' => array('eml'),
  740. 'text/x-mail' => array('eml'),
  741. );
  742. foreach ($aliases as $mime => $exts) {
  743. $mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts));
  744. foreach ($exts as $ext) {
  745. if (!isset($mime_extensions[$ext])) {
  746. $mime_extensions[$ext] = $mime;
  747. }
  748. }
  749. }
  750. return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
  751. }
  752. /**
  753. * Detect image type of the given binary data by checking magic numbers.
  754. *
  755. * @param string $data Binary file content
  756. *
  757. * @return string Detected mime-type or jpeg as fallback
  758. */
  759. public static function image_content_type($data)
  760. {
  761. $type = 'jpeg';
  762. if (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png';
  763. else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif';
  764. else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
  765. // else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';
  766. return 'image/' . $type;
  767. }
  768. }