rcube_mime.php 33.5 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992
<?php

/**
 +-----------------------------------------------------------------------+
 | This file is part of the Roundcube Webmail client                     |
 |                                                                       |
 | Copyright (C) The Roundcube Dev Team                                  |
 | Copyright (C) Kolab Systems AG                                        |
 |                                                                       |
 | Licensed under the GNU General Public License version 3 or            |
 | any later version with exceptions for skins & plugins.                |
 | See the README file for a full license statement.                     |
 |                                                                       |
 | PURPOSE:                                                              |
 |   MIME message parsing utilities                                      |
 +-----------------------------------------------------------------------+
 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
 | Author: Aleksander Machniak <alec@alec.pl>                            |
 +-----------------------------------------------------------------------+
*/

/**
 * Class for parsing MIME messages
 *
 * @package    Framework
 * @subpackage Storage
 */
class rcube_mime
{
    private static $default_charset;


    /**
     * Object constructor.
     */
    function __construct($default_charset = null)
    {
        self::$default_charset = $default_charset;
    }

    /**
     * Returns message/object character set name
     *
     * @return string Character set name
     */
    public static function get_charset()
    {
        if (self::$default_charset) {
            return self::$default_charset;
        }

        if ($charset = rcube::get_instance()->config->get('default_charset')) {
            return $charset;
        }

        return RCUBE_CHARSET;
    }

    /**
     * Parse the given raw message source and return a structure
     * of rcube_message_part objects.
     *
     * It makes use of the rcube_mime_decode library
     *
     * @param string $raw_body The message source
     *
     * @return object rcube_message_part The message structure
     */
    public static function parse_message($raw_body)
    {
        $conf = [
            'include_bodies'  => true,
            'decode_bodies'   => true,
            'decode_headers'  => false,
            'default_charset' => self::get_charset(),
        ];

        $mime = new rcube_mime_decode($conf);

        return $mime->decode($raw_body);
    }

    /**
     * Split an address list into a structured array list
     *
     * @param string|array $input    Input string (or list of strings)
     * @param int          $max      List only this number of addresses
     * @param bool         $decode   Decode address strings
     * @param string       $fallback Fallback charset if none specified
     * @param bool         $addronly Return flat array with e-mail addresses only
     *
     * @return array Indexed list of addresses
     */
    static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false)
    {
        // A common case when the same header is used many times in a mail message
        if (is_array($input)) {
            $input = implode(', ', $input);
        }

        $a   = self::parse_address_list($input, $decode, $fallback);
        $out = [];
        $j   = 0;

        // Special chars as defined by RFC 822 need to in quoted string (or escaped).
        $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';

        if (!is_array($a)) {
            return $out;
        }

        foreach ($a as $val) {
            $j++;
            $address = trim($val['address']);

            if ($addronly) {
                $out[$j] = $address;
            }
            else {
                $name   = trim($val['name']);
                $string = '';

                if ($name && $address && $name != $address) {
                    $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
                }
                else if ($address) {
                    $string = $address;
                }
                else if ($name) {
                    $string = $name;
                }

                $out[$j] = ['name' => $name, 'mailto' => $address, 'string' => $string];
            }

            if ($max && $j == $max) {
                break;
            }
        }

        return $out;
    }

    /**
     * Decode a message header value
     *
     * @param string  $input    Header value
     * @param string  $fallback Fallback charset if none specified
     *
     * @return string Decoded string
     */
    public static function decode_header($input, $fallback = null)
    {
        $str = self::decode_mime_string((string)$input, $fallback);

        return $str;
    }

    /**
     * Decode a mime-encoded string to internal charset
     *
     * @param string $input    Header value
     * @param string $fallback Fallback charset if none specified
     *
     * @return string Decoded string
     */
    public static function decode_mime_string($input, $fallback = null)
    {
        $default_charset = $fallback ?: self::get_charset();

        // rfc: all line breaks or other characters not found
        // in the Base64 Alphabet must be ignored by decoding software
        // delete all blanks between MIME-lines, differently we can
        // receive unnecessary blanks and broken utf-8 symbols
        $input = preg_replace("/\?=\s+=\?/", '?==?', $input);

        // encoded-word regexp
        $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';

        // Find all RFC2047's encoded words
        if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
            // Initialize variables
            $tmp   = [];
            $out   = '';
            $start = 0;

            foreach ($matches as $idx => $m) {
                $pos      = $m[0][1];
                $charset  = $m[1][0];
                $encoding = $m[2][0];
                $text     = $m[3][0];
                $length   = strlen($m[0][0]);

                // Append everything that is before the text to be decoded
                if ($start != $pos) {
                    $substr = substr($input, $start, $pos-$start);
                    $out   .= rcube_charset::convert($substr, $default_charset);
                    $start  = $pos;
                }
                $start += $length;

                // Per RFC2047, each string part "MUST represent an integral number
                // of characters . A multi-octet character may not be split across
                // adjacent encoded-words." However, some mailers break this, so we
                // try to handle characters spanned across parts anyway by iterating
                // through and aggregating sequential encoded parts with the same
                // character set and encoding, then perform the decoding on the
                // aggregation as a whole.

                $tmp[] = $text;
                if (!empty($matches[$idx+1]) && ($next_match = $matches[$idx+1])) {
                    if ($next_match[0][1] == $start
                        && $next_match[1][0] == $charset
                        && $next_match[2][0] == $encoding
                    ) {
                        continue;
                    }
                }

                $count = count($tmp);
                $text  = '';

                // Decode and join encoded-word's chunks
                if ($encoding == 'B' || $encoding == 'b') {
                    $rest  = '';
                    // base64 must be decoded a segment at a time.
                    // However, there are broken implementations that continue
                    // in the following word, we'll handle that (#6048)
                    for ($i=0; $i<$count; $i++) {
                        $chunk  = $rest . $tmp[$i];
                        $length = strlen($chunk);
                        if ($length % 4) {
                            $length = floor($length / 4) * 4;
                            $rest   = substr($chunk, $length);
                            $chunk  = substr($chunk, 0, $length);
                        }

                        $text .= base64_decode($chunk);
                    }
                }
                else { // if ($encoding == 'Q' || $encoding == 'q') {
                    // quoted printable can be combined and processed at once
                    for ($i=0; $i<$count; $i++) {
                        $text .= $tmp[$i];
                    }

                    $text = str_replace('_', ' ', $text);
                    $text = quoted_printable_decode($text);
                }

                $out .= rcube_charset::convert($text, $charset);
                $tmp = [];
            }

            // add the last part of the input string
            if ($start != strlen($input)) {
                $out .= rcube_charset::convert(substr($input, $start), $default_charset);
            }

            // return the results
            return $out;
        }

        // no encoding information, use fallback
        return rcube_charset::convert($input, $default_charset);
    }

    /**
     * Decode a mime part
     *
     * @param string $input    Input string
     * @param string $encoding Part encoding
     *
     * @return string Decoded string
     */
    public static function decode($input, $encoding = '7bit')
    {
        switch (strtolower($encoding)) {
        case 'quoted-printable':
            return quoted_printable_decode($input);
        case 'base64':
            return base64_decode($input);
        case 'x-uuencode':
        case 'x-uue':
        case 'uue':
        case 'uuencode':
            return convert_uudecode($input);
        case '7bit':
        default:
            return $input;
        }
    }

    /**
     * Split RFC822 header string into an associative array
     */
    public static function parse_headers($headers)
    {
        $result  = [];
        $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
        $lines   = explode("\n", $headers);
        $count   = count($lines);

        for ($i=0; $i<$count; $i++) {
            if ($p = strpos($lines[$i], ': ')) {
                $field = strtolower(substr($lines[$i], 0, $p));
                $value = trim(substr($lines[$i], $p+1));
                if (!empty($value)) {
                    $result[$field] = $value;
                }
            }
        }

        return $result;
    }

    /**
     * E-mail address list parser
     */
    private static function parse_address_list($str, $decode = true, $fallback = null)
    {
        // remove any newlines and carriage returns before
        $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);

        // extract list items, remove comments
        $str = self::explode_header_string(',;', $str, true);

        // simplified regexp, supporting quoted local part
        $email_rx = '([^\s:]+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';

        $result = [];

        foreach ($str as $key => $val) {
            $name    = '';
            $address = '';
            $val     = trim($val);

            // First token might be a group name, ignore it
            $tokens = self::explode_header_string(' ', $val);
            if (isset($tokens[0]) && $tokens[0][strlen($tokens[0])-1] == ':') {
                $val = substr($val, strlen($tokens[0]));
            }

            if (preg_match('/(.*)<('.$email_rx.')$/', $val, $m)) {
                // Note: There are cases like "Test<test@domain.tld" with no closing bracket,
                // therefor we do not include it in the regexp above, but we have to
                // remove it later, because $email_rx will catch it (#8164)
                $address = rtrim($m[2], '>');
                $name    = trim($m[1]);
            }
            else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
                $address = $m[1];
                $name    = '';
            }
            // special case (#1489092)
            else if (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) {
                $address = 'MAILER-DAEMON';
                $name    = substr($val, 0, -strlen($m[1]));
            }
            else if (preg_match('/('.$email_rx.')/', $val, $m)) {
                $name = $m[1];
            }
            else {
                $name = $val;
            }

            // unquote and/or decode name
            if ($name) {
                // An unquoted name ending with colon is a address group name, ignore it
                if ($name[strlen($name)-1] == ':') {
                    $name = '';
                }

                if (strlen($name) > 1 && $name[0] == '"' && $name[strlen($name)-1] == '"') {
                    $name = substr($name, 1, -1);
                    $name = stripslashes($name);
                }

                if ($decode) {
                    $name = self::decode_header($name, $fallback);
                    // some clients encode addressee name with quotes around it
                    if (strlen($name) > 1 && $name[0] == '"' && $name[strlen($name)-1] == '"') {
                        $name = substr($name, 1, -1);
                    }
                }
            }

            if (!$address && $name) {
                $address = $name;
                $name    = '';
            }

            if ($address) {
                $address      = self::fix_email($address);
                $result[$key] = ['name' => $name, 'address' => $address];
            }
        }

        return $result;
    }

    /**
     * Explodes header (e.g. address-list) string into array of strings
     * using specified separator characters with proper handling
     * of quoted-strings and comments (RFC2822)
     *
     * @param string $separator       String containing separator characters
     * @param string $str             Header string
     * @param bool   $remove_comments Enable to remove comments
     *
     * @return array Header items
     */
    public static function explode_header_string($separator, $str, $remove_comments = false)
    {
        $length  = strlen($str);
        $result  = [];
        $quoted  = false;
        $comment = 0;
        $out     = '';

        for ($i=0; $i<$length; $i++) {
            // we're inside a quoted string
            if ($quoted) {
                if ($str[$i] == '"') {
                    $quoted = false;
                }
                else if ($str[$i] == "\\") {
                    if ($comment <= 0) {
                        $out .= "\\";
                    }
                    $i++;
                }
            }
            // we are inside a comment string
            else if ($comment > 0) {
                if ($str[$i] == ')') {
                    $comment--;
                }
                else if ($str[$i] == '(') {
                    $comment++;
                }
                else if ($str[$i] == "\\") {
                    $i++;
                }
                continue;
            }
            // separator, add to result array
            else if (strpos($separator, $str[$i]) !== false) {
                if ($out) {
                    $result[] = $out;
                }
                $out = '';
                continue;
            }
            // start of quoted string
            else if ($str[$i] == '"') {
                $quoted = true;
            }
            // start of comment
            else if ($remove_comments && $str[$i] == '(') {
                $comment++;
            }

            if ($comment <= 0) {
                $out .= $str[$i];
            }
        }

        if ($out && $comment <= 0) {
            $result[] = $out;
        }

        return $result;
    }

    /**
     * Interpret a format=flowed message body according to RFC 2646
     *
     * @param string $text  Raw body formatted as flowed text
     * @param string $mark  Mark each flowed line with specified character
     * @param bool   $delsp Remove the trailing space of each flowed line
     *
     * @return string Interpreted text with unwrapped lines and stuffed space removed
     */
    public static function unfold_flowed($text, $mark = null, $delsp = false)
    {
        $text    = preg_split('/\r?\n/', $text);
        $last    = -1;
        $q_level = 0;
        $marks   = [];

        foreach ($text as $idx => $line) {
            if ($q = strspn($line, '>')) {
                // remove quote chars
                $line = substr($line, $q);
                // remove (optional) space-staffing
                if (isset($line[0]) && $line[0] === ' ') {
                    $line = substr($line, 1);
                }

                // The same paragraph (We join current line with the previous one) when:
                // - the same level of quoting
                // - previous line was flowed
                // - previous line contains more than only one single space (and quote char(s))
                if ($q == $q_level
                    && isset($text[$last]) && $text[$last][strlen($text[$last])-1] == ' '
                    && !preg_match('/^>+ {0,1}$/', $text[$last])
                ) {
                    if ($delsp) {
                        $text[$last] = substr($text[$last], 0, -1);
                    }
                    $text[$last] .= $line;
                    unset($text[$idx]);

                    if ($mark) {
                        $marks[$last] = true;
                    }
                }
                else {
                    $last = $idx;
                }
            }
            else {
                if ($line == '-- ') {
                    $last = $idx;
                }
                else {
                    // remove space-stuffing
                    if (isset($line[0]) && $line[0] === ' ') {
                        $line = substr($line, 1);
                    }

                    $last_len = isset($text[$last]) ? strlen($text[$last]) : 0;

                    if (
                        $last_len && $line && !$q_level && $text[$last] != '-- '
                        && isset($text[$last][$last_len-1]) && $text[$last][$last_len-1] == ' '
                    ) {
                        if ($delsp) {
                            $text[$last] = substr($text[$last], 0, -1);
                        }
                        $text[$last] .= $line;
                        unset($text[$idx]);

                        if ($mark) {
                            $marks[$last] = true;
                        }
                    }
                    else {
                        $text[$idx] = $line;
                        $last = $idx;
                    }
                }
            }
            $q_level = $q;
        }

        if (!empty($marks)) {
            foreach (array_keys($marks) as $mk) {
                $text[$mk] = $mark . $text[$mk];
            }
        }

        return implode("\r\n", $text);
    }

    /**
     * Wrap the given text to comply with RFC 2646
     *
     * @param string $text    Text to wrap
     * @param int    $length  Length
     * @param string $charset Character encoding of $text
     *
     * @return string Wrapped text
     */
    public static function format_flowed($text, $length = 72, $charset = null)
    {
        $text = preg_split('/\r?\n/', $text);

        foreach ($text as $idx => $line) {
            if ($line != '-- ') {
                if ($level = strspn($line, '>')) {
                    // remove quote chars
                    $line = substr($line, $level);
                    // remove (optional) space-staffing and spaces before the line end
                    $line = rtrim($line, ' ');
                    if (isset($line[0]) && $line[0] === ' ') {
                        $line = substr($line, 1);
                    }

                    $prefix = str_repeat('>', $level) . ' ';
                    $line   = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset);
                }
                else if ($line) {
                    $line = self::wordwrap(rtrim($line), $length - 2, " \r\n", false, $charset);
                    // space-stuffing
                    $line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);
                }

                $text[$idx] = $line;
            }
        }

        return implode("\r\n", $text);
    }

    /**
     * Improved wordwrap function with multibyte support.
     * The code is based on Zend_Text_MultiByte::wordWrap().
     *
     * @param string $string      Text to wrap
     * @param int    $width       Line width
     * @param string $break       Line separator
     * @param bool   $cut         Enable to cut word
     * @param string $charset     Charset of $string
     * @param bool   $wrap_quoted When enabled quoted lines will not be wrapped
     *
     * @return string Text
     */
    public static function wordwrap($string, $width = 75, $break = "\n", $cut = false, $charset = null, $wrap_quoted = true)
    {
        // Note: Never try to use iconv instead of mbstring functions here
        //       Iconv's substr/strlen are 100x slower (#1489113)

        if ($charset && $charset != RCUBE_CHARSET) {
            $charset = rcube_charset::parse_charset($charset);
            mb_internal_encoding($charset);
        }

        // Convert \r\n to \n, this is our line-separator
        $string       = str_replace("\r\n", "\n", $string);
        $separator    = "\n"; // must be 1 character length
        $result       = [];

        while (($stringLength = mb_strlen($string)) > 0) {
            $breakPos = mb_strpos($string, $separator, 0);

            // quoted line (do not wrap)
            if ($wrap_quoted && $string[0] == '>') {
                if ($breakPos === $stringLength - 1 || $breakPos === false) {
                    $subString = $string;
                    $cutLength = null;
                }
                else {
                    $subString = mb_substr($string, 0, $breakPos);
                    $cutLength = $breakPos + 1;
                }
            }
            // next line found and current line is shorter than the limit
            else if ($breakPos !== false && $breakPos < $width) {
                if ($breakPos === $stringLength - 1) {
                    $subString = $string;
                    $cutLength = null;
                }
                else {
                    $subString = mb_substr($string, 0, $breakPos);
                    $cutLength = $breakPos + 1;
                }
            }
            else {
                $subString = mb_substr($string, 0, $width);

                // last line
                if ($breakPos === false && $subString === $string) {
                    $cutLength = null;
                }
                else {
                    $nextChar = mb_substr($string, $width, 1);

                    if ($nextChar === ' ' || $nextChar === $separator) {
                        $afterNextChar = mb_substr($string, $width + 1, 1);

                        // Note: mb_substr() does never return False
                        if ($afterNextChar === false || $afterNextChar === '') {
                            $subString .= $nextChar;
                        }

                        $cutLength = mb_strlen($subString) + 1;
                    }
                    else {
                        $spacePos = mb_strrpos($subString, ' ', 0);

                        if ($spacePos !== false) {
                            $subString = mb_substr($subString, 0, $spacePos);
                            $cutLength = $spacePos + 1;
                        }
                        else if ($cut === false) {
                            $spacePos = mb_strpos($string, ' ', 0);

                            if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) {
                                $subString = mb_substr($string, 0, $spacePos);
                                $cutLength = $spacePos + 1;
                            }
                            else if ($breakPos === false) {
                                $subString = $string;
                                $cutLength = null;
                            }
                            else {
                                $subString = mb_substr($string, 0, $breakPos);
                                $cutLength = $breakPos + 1;
                            }
                        }
                        else {
                            $cutLength = $width;
                        }
                    }
                }
            }

            $result[] = $subString;

            if ($cutLength !== null) {
                $string = mb_substr($string, $cutLength, ($stringLength - $cutLength));
            }
            else {
                break;
            }
        }

        if ($charset && $charset != RCUBE_CHARSET) {
            mb_internal_encoding(RCUBE_CHARSET);
        }

        return implode($break, $result);
    }

    /**
     * A method to guess the mime_type of an attachment.
     *
     * @param string  $path        Path to the file or file contents
     * @param string  $name        File name (with suffix)
     * @param string  $failover    Mime type supplied for failover
     * @param bool    $is_stream   Set to True if $path contains file contents
     * @param bool    $skip_suffix Set to True if the config/mimetypes.php map should be ignored
     *
     * @return string
     * @author Till Klampaeckel <till@php.net>
     * @see    http://de2.php.net/manual/en/ref.fileinfo.php
     * @see    http://de2.php.net/mime_content_type
     */
    public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false)
    {
        $mime_type = null;
        $config    = rcube::get_instance()->config;

        // Detect mimetype using filename extension
        if (!$skip_suffix) {
            $mime_type = self::file_ext_type($name);
        }

        // try fileinfo extension if available
        if (!$mime_type && function_exists('finfo_open')) {
            $mime_magic = $config->get('mime_magic');
            // null as a 2nd argument should be the same as no argument
            // this however is not true on all systems/versions
            if ($mime_magic) {
                $finfo = finfo_open(FILEINFO_MIME, $mime_magic);
            }
            else {
                $finfo = finfo_open(FILEINFO_MIME);
            }

            if ($finfo) {
                $func      = $is_stream ? 'finfo_buffer' : 'finfo_file';
                $mime_type = $func($finfo, $path, FILEINFO_MIME_TYPE);
                finfo_close($finfo);
            }
        }

        // try PHP's mime_content_type
        if (!$mime_type && !$is_stream && function_exists('mime_content_type')) {
            $mime_type = @mime_content_type($path);
        }

        // fall back to user-submitted string
        if (!$mime_type) {
            $mime_type = $failover;
        }

        return $mime_type;
    }

    /**
     * File type detection based on file name only.
     *
     * @param string $filename Path to the file or file contents
     *
     * @return string|null Mimetype label
     */
    public static function file_ext_type($filename)
    {
        static $mime_ext = [];

        if (empty($mime_ext)) {
            foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
                $mime_ext = array_merge($mime_ext, (array) @include($fpath));
            }
        }

        // use file name suffix with hard-coded mime-type map
        if (!empty($mime_ext) && $filename) {
            $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
            if ($ext && !empty($mime_ext[$ext])) {
                return $mime_ext[$ext];
            }
        }
    }

    /**
     * Get mimetype => file extension mapping
     *
     * @param string Mime-Type to get extensions for
     *
     * @return array List of extensions matching the given mimetype or a hash array
     *               with ext -> mimetype mappings if $mimetype is not given
     */
    public static function get_mime_extensions($mimetype = null)
    {
        static $mime_types, $mime_extensions;

        // return cached data
        if (is_array($mime_types)) {
            return $mimetype ? (isset($mime_types[$mimetype]) ? $mime_types[$mimetype] : []) : $mime_extensions;
        }

        // load mapping file
        $file_paths = [];

        if ($mime_types = rcube::get_instance()->config->get('mime_types')) {
            $file_paths[] = $mime_types;
        }

        // try common locations
        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $file_paths[] = 'C:/xampp/apache/conf/mime.types.';
        }
        else {
            $file_paths[] = '/etc/mime.types';
            $file_paths[] = '/etc/httpd/mime.types';
            $file_paths[] = '/etc/httpd2/mime.types';
            $file_paths[] = '/etc/apache/mime.types';
            $file_paths[] = '/etc/apache2/mime.types';
            $file_paths[] = '/etc/nginx/mime.types';
            $file_paths[] = '/usr/local/etc/httpd/conf/mime.types';
            $file_paths[] = '/usr/local/etc/apache/conf/mime.types';
            $file_paths[] = '/usr/local/etc/apache24/mime.types';
        }

        $mime_types      = [];
        $mime_extensions = [];
        $lines = [];
        $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";

        foreach ($file_paths as $fp) {
            if (@is_readable($fp)) {
                $lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                break;
            }
        }

        foreach ($lines as $line) {
             // skip comments or mime types w/o any extensions
            if ($line[0] == '#' || !preg_match($regex, $line, $matches)) {
                continue;
            }

            $mime = $matches[1];

            foreach (explode(' ', $matches[2]) as $ext) {
                $ext = trim($ext);
                $mime_types[$mime][]   = $ext;
                $mime_extensions[$ext] = $mime;
            }
        }

        // fallback to some well-known types most important for daily emails
        if (empty($mime_types)) {
            foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
                $mime_extensions = array_merge($mime_extensions, (array) @include($fpath));
            }

            foreach ($mime_extensions as $ext => $mime) {
                $mime_types[$mime][] = $ext;
            }
        }

        // Add some known aliases that aren't included by some mime.types (#1488891)
        // the order is important here so standard extensions have higher prio
        $aliases = [
            'image/gif'      => ['gif'],
            'image/png'      => ['png'],
            'image/x-png'    => ['png'],
            'image/jpeg'     => ['jpg', 'jpeg', 'jpe'],
            'image/jpg'      => ['jpg', 'jpeg', 'jpe'],
            'image/pjpeg'    => ['jpg', 'jpeg', 'jpe'],
            'image/tiff'     => ['tif'],
            'image/bmp'      => ['bmp'],
            'image/x-ms-bmp' => ['bmp'],
            'message/rfc822' => ['eml'],
            'text/x-mail'    => ['eml'],
        ];

        foreach ($aliases as $mime => $exts) {
            if (isset($mime_types[$mime])) {
                $mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts));
            }
            else {
                $mime_types[$mime] = $exts;
            }

            foreach ($exts as $ext) {
                if (!isset($mime_extensions[$ext])) {
                    $mime_extensions[$ext] = $mime;
                }
            }
        }

        if ($mimetype) {
            return !empty($mime_types[$mimetype]) ? $mime_types[$mimetype] : [];
        }

        return $mime_extensions;
    }

    /**
     * Detect image type of the given binary data by checking magic numbers.
     *
     * @param string $data  Binary file content
     *
     * @return string Detected mime-type or jpeg as fallback
     */
    public static function image_content_type($data)
    {
        $type = 'jpeg';
        if      (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png';
        else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif';
        else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
    //  else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';

        return 'image/' . $type;
    }

    /**
     * Try to fix invalid email addresses
     */
    public static function fix_email($email)
    {
        $parts = rcube_utils::explode_quoted_string('@', $email);

        foreach ($parts as $idx => $part) {
            // remove redundant quoting (#1490040)
            if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) {
                $parts[$idx] = $m[1];
            }
        }

        return implode('@', $parts);
    }

    /**
     * Fix mimetype name.
     *
     * @param string $type Mimetype
     *
     * @return string Mimetype
     */
    public static function fix_mimetype($type)
    {
        $type    = strtolower(trim($type));
        $aliases = [
            'image/x-ms-bmp' => 'image/bmp',        // #4771
            'pdf'            => 'application/pdf',  // #6816
        ];

        if (!empty($aliases[$type])) {
            return $aliases[$type];
        }

        // Some versions of Outlook create garbage Content-Type:
        // application/pdf.A520491B_3BF7_494D_8855_7FAC2C6C0608
        if (preg_match('/^application\/pdf.+/', $type)) {
            return 'application/pdf';
        }

        // treat image/pjpeg (image/pjpg, image/jpg) as image/jpeg (#4196)
        if (preg_match('/^image\/p?jpe?g$/', $type)) {
            return 'image/jpeg';
        }

        return $type;
    }
}