Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

rcube_mime.php 31KB

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