rcube_mime.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  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 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. }
  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. // base64 must be decoded a segment at a time
  186. for ($i=0; $i<$count; $i++)
  187. $text .= base64_decode($tmp[$i]);
  188. }
  189. else { //if ($encoding == 'Q' || $encoding == 'q') {
  190. // quoted printable can be combined and processed at once
  191. for ($i=0; $i<$count; $i++)
  192. $text .= $tmp[$i];
  193. $text = str_replace('_', ' ', $text);
  194. $text = quoted_printable_decode($text);
  195. }
  196. $out .= rcube_charset::convert($text, $charset);
  197. $tmp = array();
  198. }
  199. // add the last part of the input string
  200. if ($start != strlen($input)) {
  201. $out .= rcube_charset::convert(substr($input, $start), $default_charset);
  202. }
  203. // return the results
  204. return $out;
  205. }
  206. // no encoding information, use fallback
  207. return rcube_charset::convert($input, $default_charset);
  208. }
  209. /**
  210. * Decode a mime part
  211. *
  212. * @param string $input Input string
  213. * @param string $encoding Part encoding
  214. *
  215. * @return string Decoded string
  216. */
  217. public static function decode($input, $encoding = '7bit')
  218. {
  219. switch (strtolower($encoding)) {
  220. case 'quoted-printable':
  221. return quoted_printable_decode($input);
  222. case 'base64':
  223. return base64_decode($input);
  224. case 'x-uuencode':
  225. case 'x-uue':
  226. case 'uue':
  227. case 'uuencode':
  228. return convert_uudecode($input);
  229. case '7bit':
  230. default:
  231. return $input;
  232. }
  233. }
  234. /**
  235. * Split RFC822 header string into an associative array
  236. */
  237. public static function parse_headers($headers)
  238. {
  239. $a_headers = array();
  240. $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
  241. $lines = explode("\n", $headers);
  242. $count = count($lines);
  243. for ($i=0; $i<$count; $i++) {
  244. if ($p = strpos($lines[$i], ': ')) {
  245. $field = strtolower(substr($lines[$i], 0, $p));
  246. $value = trim(substr($lines[$i], $p+1));
  247. if (!empty($value)) {
  248. $a_headers[$field] = $value;
  249. }
  250. }
  251. }
  252. return $a_headers;
  253. }
  254. /**
  255. * E-mail address list parser
  256. */
  257. private static function parse_address_list($str, $decode = true, $fallback = null)
  258. {
  259. // remove any newlines and carriage returns before
  260. $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
  261. // extract list items, remove comments
  262. $str = self::explode_header_string(',;', $str, true);
  263. $result = array();
  264. // simplified regexp, supporting quoted local part
  265. $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
  266. foreach ($str as $key => $val) {
  267. $name = '';
  268. $address = '';
  269. $val = trim($val);
  270. if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
  271. $address = $m[2];
  272. $name = trim($m[1]);
  273. }
  274. else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
  275. $address = $m[1];
  276. $name = '';
  277. }
  278. // special case (#1489092)
  279. else if (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) {
  280. $address = 'MAILER-DAEMON';
  281. $name = substr($val, 0, -strlen($m[1]));
  282. }
  283. else if (preg_match('/('.$email_rx.')/', $val, $m)) {
  284. $name = $m[1];
  285. }
  286. else {
  287. $name = $val;
  288. }
  289. // dequote and/or decode name
  290. if ($name) {
  291. if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
  292. $name = substr($name, 1, -1);
  293. $name = stripslashes($name);
  294. }
  295. if ($decode) {
  296. $name = self::decode_header($name, $fallback);
  297. // some clients encode addressee name with quotes around it
  298. if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
  299. $name = substr($name, 1, -1);
  300. }
  301. }
  302. }
  303. if (!$address && $name) {
  304. $address = $name;
  305. $name = '';
  306. }
  307. if ($address) {
  308. $address = self::fix_email($address);
  309. $result[$key] = array('name' => $name, 'address' => $address);
  310. }
  311. }
  312. return $result;
  313. }
  314. /**
  315. * Explodes header (e.g. address-list) string into array of strings
  316. * using specified separator characters with proper handling
  317. * of quoted-strings and comments (RFC2822)
  318. *
  319. * @param string $separator String containing separator characters
  320. * @param string $str Header string
  321. * @param bool $remove_comments Enable to remove comments
  322. *
  323. * @return array Header items
  324. */
  325. public static function explode_header_string($separator, $str, $remove_comments = false)
  326. {
  327. $length = strlen($str);
  328. $result = array();
  329. $quoted = false;
  330. $comment = 0;
  331. $out = '';
  332. for ($i=0; $i<$length; $i++) {
  333. // we're inside a quoted string
  334. if ($quoted) {
  335. if ($str[$i] == '"') {
  336. $quoted = false;
  337. }
  338. else if ($str[$i] == "\\") {
  339. if ($comment <= 0) {
  340. $out .= "\\";
  341. }
  342. $i++;
  343. }
  344. }
  345. // we are inside a comment string
  346. else if ($comment > 0) {
  347. if ($str[$i] == ')') {
  348. $comment--;
  349. }
  350. else if ($str[$i] == '(') {
  351. $comment++;
  352. }
  353. else if ($str[$i] == "\\") {
  354. $i++;
  355. }
  356. continue;
  357. }
  358. // separator, add to result array
  359. else if (strpos($separator, $str[$i]) !== false) {
  360. if ($out) {
  361. $result[] = $out;
  362. }
  363. $out = '';
  364. continue;
  365. }
  366. // start of quoted string
  367. else if ($str[$i] == '"') {
  368. $quoted = true;
  369. }
  370. // start of comment
  371. else if ($remove_comments && $str[$i] == '(') {
  372. $comment++;
  373. }
  374. if ($comment <= 0) {
  375. $out .= $str[$i];
  376. }
  377. }
  378. if ($out && $comment <= 0) {
  379. $result[] = $out;
  380. }
  381. return $result;
  382. }
  383. /**
  384. * Interpret a format=flowed message body according to RFC 2646
  385. *
  386. * @param string $text Raw body formatted as flowed text
  387. * @param string $mark Mark each flowed line with specified character
  388. *
  389. * @return string Interpreted text with unwrapped lines and stuffed space removed
  390. */
  391. public static function unfold_flowed($text, $mark = null)
  392. {
  393. $text = preg_split('/\r?\n/', $text);
  394. $last = -1;
  395. $q_level = 0;
  396. $marks = array();
  397. foreach ($text as $idx => $line) {
  398. if ($q = strspn($line, '>')) {
  399. // remove quote chars
  400. $line = substr($line, $q);
  401. // remove (optional) space-staffing
  402. if ($line[0] === ' ') $line = substr($line, 1);
  403. // The same paragraph (We join current line with the previous one) when:
  404. // - the same level of quoting
  405. // - previous line was flowed
  406. // - previous line contains more than only one single space (and quote char(s))
  407. if ($q == $q_level
  408. && isset($text[$last]) && $text[$last][strlen($text[$last])-1] == ' '
  409. && !preg_match('/^>+ {0,1}$/', $text[$last])
  410. ) {
  411. $text[$last] .= $line;
  412. unset($text[$idx]);
  413. if ($mark) {
  414. $marks[$last] = true;
  415. }
  416. }
  417. else {
  418. $last = $idx;
  419. }
  420. }
  421. else {
  422. if ($line == '-- ') {
  423. $last = $idx;
  424. }
  425. else {
  426. // remove space-stuffing
  427. if ($line[0] === ' ') $line = substr($line, 1);
  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 ($level = strspn($line, '>')) {
  468. // remove quote chars
  469. $line = substr($line, $level);
  470. // remove (optional) space-staffing and spaces before the line end
  471. $line = rtrim($line, ' ');
  472. if ($line[0] === ' ') $line = substr($line, 1);
  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. $file_paths[] = '/usr/local/etc/apache24/mime.types';
  688. }
  689. foreach ($file_paths as $fp) {
  690. if (@is_readable($fp)) {
  691. $lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
  692. break;
  693. }
  694. }
  695. $mime_types = $mime_extensions = array();
  696. $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";
  697. foreach((array)$lines as $line) {
  698. // skip comments or mime types w/o any extensions
  699. if ($line[0] == '#' || !preg_match($regex, $line, $matches))
  700. continue;
  701. $mime = $matches[1];
  702. foreach (explode(' ', $matches[2]) as $ext) {
  703. $ext = trim($ext);
  704. $mime_types[$mime][] = $ext;
  705. $mime_extensions[$ext] = $mime;
  706. }
  707. }
  708. // fallback to some well-known types most important for daily emails
  709. if (empty($mime_types)) {
  710. foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
  711. $mime_extensions = array_merge($mime_extensions, (array) @include($fpath));
  712. }
  713. foreach ($mime_extensions as $ext => $mime) {
  714. $mime_types[$mime][] = $ext;
  715. }
  716. }
  717. // Add some known aliases that aren't included by some mime.types (#1488891)
  718. // the order is important here so standard extensions have higher prio
  719. $aliases = array(
  720. 'image/gif' => array('gif'),
  721. 'image/png' => array('png'),
  722. 'image/x-png' => array('png'),
  723. 'image/jpeg' => array('jpg', 'jpeg', 'jpe'),
  724. 'image/jpg' => array('jpg', 'jpeg', 'jpe'),
  725. 'image/pjpeg' => array('jpg', 'jpeg', 'jpe'),
  726. 'image/tiff' => array('tif'),
  727. 'message/rfc822' => array('eml'),
  728. 'text/x-mail' => array('eml'),
  729. );
  730. foreach ($aliases as $mime => $exts) {
  731. $mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts));
  732. foreach ($exts as $ext) {
  733. if (!isset($mime_extensions[$ext])) {
  734. $mime_extensions[$ext] = $mime;
  735. }
  736. }
  737. }
  738. return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
  739. }
  740. /**
  741. * Detect image type of the given binary data by checking magic numbers.
  742. *
  743. * @param string $data Binary file content
  744. *
  745. * @return string Detected mime-type or jpeg as fallback
  746. */
  747. public static function image_content_type($data)
  748. {
  749. $type = 'jpeg';
  750. if (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png';
  751. else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif';
  752. else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
  753. // else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';
  754. return 'image/' . $type;
  755. }
  756. /**
  757. * Try to fix invalid email addresses
  758. */
  759. public static function fix_email($email)
  760. {
  761. $parts = rcube_utils::explode_quoted_string('@', $email);
  762. foreach ($parts as $idx => $part) {
  763. // remove redundant quoting (#1490040)
  764. if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) {
  765. $parts[$idx] = $m[1];
  766. }
  767. }
  768. return implode('@', $parts);
  769. }
  770. }