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.

rcube_mime.php 30KB

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