PluginProbe
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress / 10.2.2
AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress v10.2.2
11.1.1 11.1.0 11.0.5 11.0.4 11.0.3 11.0.2 11.0.1 11.0.0 10.11.1 10.11.0 10.10.2 10.10.1 10.10.0 10.9.1 trunk 10.0.0 10.0.1 10.1.0 10.1.1 10.1.2 10.1.3 10.1.4 10.2.0 10.2.1 10.2.2 All 62 releases
acymailing / back / Libraries / Imap2 / Roundcube / Mime.php

Mime.php in AcyMailing – An Ultimate Newsletter Plugin and Marketing Automation Solution for WordPress 10.2.2, at back/Libraries/Imap2/Roundcube/Mime.php

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