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