Mercurial > hg > rc2
comparison program/lib/Roundcube/rcube_mime.php @ 0:4681f974d28b
vanilla 1.3.3 distro, I hope
author | Charlie Root |
---|---|
date | Thu, 04 Jan 2018 15:52:31 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4681f974d28b |
---|---|
1 <?php | |
2 | |
3 /** | |
4 +-----------------------------------------------------------------------+ | |
5 | This file is part of the Roundcube Webmail client | | |
6 | Copyright (C) 2005-2016, The Roundcube Dev Team | | |
7 | Copyright (C) 2011-2016, Kolab Systems AG | | |
8 | | | |
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. | | |
12 | | | |
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 * | |
24 * @package Framework | |
25 * @subpackage Storage | |
26 * @author Thomas Bruederli <roundcube@gmail.com> | |
27 * @author Aleksander Machniak <alec@alec.pl> | |
28 */ | |
29 class rcube_mime | |
30 { | |
31 private static $default_charset; | |
32 | |
33 | |
34 /** | |
35 * Object constructor. | |
36 */ | |
37 function __construct($default_charset = null) | |
38 { | |
39 self::$default_charset = $default_charset; | |
40 } | |
41 | |
42 /** | |
43 * Returns message/object character set name | |
44 * | |
45 * @return string Character set name | |
46 */ | |
47 public static function get_charset() | |
48 { | |
49 if (self::$default_charset) { | |
50 return self::$default_charset; | |
51 } | |
52 | |
53 if ($charset = rcube::get_instance()->config->get('default_charset')) { | |
54 return $charset; | |
55 } | |
56 | |
57 return RCUBE_CHARSET; | |
58 } | |
59 | |
60 /** | |
61 * Parse the given raw message source and return a structure | |
62 * of rcube_message_part objects. | |
63 * | |
64 * It makes use of the rcube_mime_decode library | |
65 * | |
66 * @param string $raw_body The message source | |
67 * | |
68 * @return object rcube_message_part The message structure | |
69 */ | |
70 public static function parse_message($raw_body) | |
71 { | |
72 $conf = array( | |
73 'include_bodies' => true, | |
74 'decode_bodies' => true, | |
75 'decode_headers' => false, | |
76 'default_charset' => self::get_charset(), | |
77 ); | |
78 | |
79 $mime = new rcube_mime_decode($conf); | |
80 | |
81 return $mime->decode($raw_body); | |
82 } | |
83 | |
84 /** | |
85 * Split an address list into a structured array list | |
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 | |
91 * @param boolean $addronly Return flat array with e-mail addresses only | |
92 * | |
93 * @return array Indexed list of addresses | |
94 */ | |
95 static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false) | |
96 { | |
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 | |
104 if (!is_array($a)) { | |
105 return $out; | |
106 } | |
107 | |
108 foreach ($a as $val) { | |
109 $j++; | |
110 $address = trim($val['address']); | |
111 | |
112 if ($addronly) { | |
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; | |
123 | |
124 $out[$j] = array('name' => $name, 'mailto' => $address, 'string' => $string); | |
125 } | |
126 | |
127 if ($max && $j==$max) | |
128 break; | |
129 } | |
130 | |
131 return $out; | |
132 } | |
133 | |
134 /** | |
135 * Decode a message header value | |
136 * | |
137 * @param string $input Header value | |
138 * @param string $fallback Fallback charset if none specified | |
139 * | |
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 { | |
159 $default_charset = $fallback ?: self::get_charset(); | |
160 | |
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); | |
187 $out .= rcube_charset::convert($substr, $default_charset); | |
188 $start = $pos; | |
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 | |
228 $out .= rcube_charset::convert($text, $charset); | |
229 $tmp = array(); | |
230 } | |
231 | |
232 // add the last part of the input string | |
233 if ($start != strlen($input)) { | |
234 $out .= rcube_charset::convert(substr($input, $start), $default_charset); | |
235 } | |
236 | |
237 // return the results | |
238 return $out; | |
239 } | |
240 | |
241 // no encoding information, use fallback | |
242 return rcube_charset::convert($input, $default_charset); | |
243 } | |
244 | |
245 /** | |
246 * Decode a mime part | |
247 * | |
248 * @param string $input Input string | |
249 * @param string $encoding Part encoding | |
250 * | |
251 * @return string Decoded string | |
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(); | |
277 $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers); | |
278 $lines = explode("\n", $headers); | |
279 $count = count($lines); | |
280 | |
281 for ($i=0; $i<$count; $i++) { | |
282 if ($p = strpos($lines[$i], ': ')) { | |
283 $field = strtolower(substr($lines[$i], 0, $p)); | |
284 $value = trim(substr($lines[$i], $p+1)); | |
285 if (!empty($value)) { | |
286 $a_headers[$field] = $value; | |
287 } | |
288 } | |
289 } | |
290 | |
291 return $a_headers; | |
292 } | |
293 | |
294 /** | |
295 * E-mail address list parser | |
296 */ | |
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 } | |
322 // special case (#1489092) | |
323 else if (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) { | |
324 $address = 'MAILER-DAEMON'; | |
325 $name = substr($val, 0, -strlen($m[1])); | |
326 } | |
327 else if (preg_match('/('.$email_rx.')/', $val, $m)) { | |
328 $name = $m[1]; | |
329 } | |
330 else { | |
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); | |
342 // some clients encode addressee name with quotes around it | |
343 if ($name[0] == '"' && $name[strlen($name)-1] == '"') { | |
344 $name = substr($name, 1, -1); | |
345 } | |
346 } | |
347 } | |
348 | |
349 if (!$address && $name) { | |
350 $address = $name; | |
351 $name = ''; | |
352 } | |
353 | |
354 if ($address) { | |
355 $address = self::fix_email($address); | |
356 $result[$key] = array('name' => $name, 'address' => $address); | |
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 * | |
440 * @param string $text Raw body formatted as flowed text | |
441 * @param string $mark Mark each flowed line with specified character | |
442 * @param boolean $delsp Remove the trailing space of each flowed line | |
443 * | |
444 * @return string Interpreted text with unwrapped lines and stuffed space removed | |
445 */ | |
446 public static function unfold_flowed($text, $mark = null, $delsp = false) | |
447 { | |
448 $text = preg_split('/\r?\n/', $text); | |
449 $last = -1; | |
450 $q_level = 0; | |
451 $marks = array(); | |
452 | |
453 foreach ($text as $idx => $line) { | |
454 if ($q = strspn($line, '>')) { | |
455 // remove quote chars | |
456 $line = substr($line, $q); | |
457 // remove (optional) space-staffing | |
458 if ($line[0] === ' ') $line = substr($line, 1); | |
459 | |
460 // The same paragraph (We join current line with the previous one) when: | |
461 // - the same level of quoting | |
462 // - previous line was flowed | |
463 // - previous line contains more than only one single space (and quote char(s)) | |
464 if ($q == $q_level | |
465 && isset($text[$last]) && $text[$last][strlen($text[$last])-1] == ' ' | |
466 && !preg_match('/^>+ {0,1}$/', $text[$last]) | |
467 ) { | |
468 if ($delsp) { | |
469 $text[$last] = substr($text[$last], 0, -1); | |
470 } | |
471 $text[$last] .= $line; | |
472 unset($text[$idx]); | |
473 | |
474 if ($mark) { | |
475 $marks[$last] = true; | |
476 } | |
477 } | |
478 else { | |
479 $last = $idx; | |
480 } | |
481 } | |
482 else { | |
483 if ($line == '-- ') { | |
484 $last = $idx; | |
485 } | |
486 else { | |
487 // remove space-stuffing | |
488 if ($line[0] === ' ') $line = substr($line, 1); | |
489 | |
490 if (isset($text[$last]) && $line && !$q_level | |
491 && $text[$last] != '-- ' | |
492 && $text[$last][strlen($text[$last])-1] == ' ' | |
493 ) { | |
494 if ($delsp) { | |
495 $text[$last] = substr($text[$last], 0, -1); | |
496 } | |
497 $text[$last] .= $line; | |
498 unset($text[$idx]); | |
499 | |
500 if ($mark) { | |
501 $marks[$last] = true; | |
502 } | |
503 } | |
504 else { | |
505 $text[$idx] = $line; | |
506 $last = $idx; | |
507 } | |
508 } | |
509 } | |
510 $q_level = $q; | |
511 } | |
512 | |
513 if (!empty($marks)) { | |
514 foreach (array_keys($marks) as $mk) { | |
515 $text[$mk] = $mark . $text[$mk]; | |
516 } | |
517 } | |
518 | |
519 return implode("\r\n", $text); | |
520 } | |
521 | |
522 /** | |
523 * Wrap the given text to comply with RFC 2646 | |
524 * | |
525 * @param string $text Text to wrap | |
526 * @param int $length Length | |
527 * @param string $charset Character encoding of $text | |
528 * | |
529 * @return string Wrapped text | |
530 */ | |
531 public static function format_flowed($text, $length = 72, $charset=null) | |
532 { | |
533 $text = preg_split('/\r?\n/', $text); | |
534 | |
535 foreach ($text as $idx => $line) { | |
536 if ($line != '-- ') { | |
537 if ($level = strspn($line, '>')) { | |
538 // remove quote chars | |
539 $line = substr($line, $level); | |
540 // remove (optional) space-staffing and spaces before the line end | |
541 $line = rtrim($line, ' '); | |
542 if ($line[0] === ' ') $line = substr($line, 1); | |
543 | |
544 $prefix = str_repeat('>', $level) . ' '; | |
545 $line = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset); | |
546 } | |
547 else if ($line) { | |
548 $line = self::wordwrap(rtrim($line), $length - 2, " \r\n", false, $charset); | |
549 // space-stuffing | |
550 $line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line); | |
551 } | |
552 | |
553 $text[$idx] = $line; | |
554 } | |
555 } | |
556 | |
557 return implode("\r\n", $text); | |
558 } | |
559 | |
560 /** | |
561 * Improved wordwrap function with multibyte support. | |
562 * The code is based on Zend_Text_MultiByte::wordWrap(). | |
563 * | |
564 * @param string $string Text to wrap | |
565 * @param int $width Line width | |
566 * @param string $break Line separator | |
567 * @param bool $cut Enable to cut word | |
568 * @param string $charset Charset of $string | |
569 * @param bool $wrap_quoted When enabled quoted lines will not be wrapped | |
570 * | |
571 * @return string Text | |
572 */ | |
573 public static function wordwrap($string, $width=75, $break="\n", $cut=false, $charset=null, $wrap_quoted=true) | |
574 { | |
575 // Note: Never try to use iconv instead of mbstring functions here | |
576 // Iconv's substr/strlen are 100x slower (#1489113) | |
577 | |
578 if ($charset && $charset != RCUBE_CHARSET) { | |
579 mb_internal_encoding($charset); | |
580 } | |
581 | |
582 // Convert \r\n to \n, this is our line-separator | |
583 $string = str_replace("\r\n", "\n", $string); | |
584 $separator = "\n"; // must be 1 character length | |
585 $result = array(); | |
586 | |
587 while (($stringLength = mb_strlen($string)) > 0) { | |
588 $breakPos = mb_strpos($string, $separator, 0); | |
589 | |
590 // quoted line (do not wrap) | |
591 if ($wrap_quoted && $string[0] == '>') { | |
592 if ($breakPos === $stringLength - 1 || $breakPos === false) { | |
593 $subString = $string; | |
594 $cutLength = null; | |
595 } | |
596 else { | |
597 $subString = mb_substr($string, 0, $breakPos); | |
598 $cutLength = $breakPos + 1; | |
599 } | |
600 } | |
601 // next line found and current line is shorter than the limit | |
602 else if ($breakPos !== false && $breakPos < $width) { | |
603 if ($breakPos === $stringLength - 1) { | |
604 $subString = $string; | |
605 $cutLength = null; | |
606 } | |
607 else { | |
608 $subString = mb_substr($string, 0, $breakPos); | |
609 $cutLength = $breakPos + 1; | |
610 } | |
611 } | |
612 else { | |
613 $subString = mb_substr($string, 0, $width); | |
614 | |
615 // last line | |
616 if ($breakPos === false && $subString === $string) { | |
617 $cutLength = null; | |
618 } | |
619 else { | |
620 $nextChar = mb_substr($string, $width, 1); | |
621 | |
622 if ($nextChar === ' ' || $nextChar === $separator) { | |
623 $afterNextChar = mb_substr($string, $width + 1, 1); | |
624 | |
625 // Note: mb_substr() does never return False | |
626 if ($afterNextChar === false || $afterNextChar === '') { | |
627 $subString .= $nextChar; | |
628 } | |
629 | |
630 $cutLength = mb_strlen($subString) + 1; | |
631 } | |
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 } | |
639 else if ($cut === false) { | |
640 $spacePos = mb_strpos($string, ' ', 0); | |
641 | |
642 if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) { | |
643 $subString = mb_substr($string, 0, $spacePos); | |
644 $cutLength = $spacePos + 1; | |
645 } | |
646 else if ($breakPos === false) { | |
647 $subString = $string; | |
648 $cutLength = null; | |
649 } | |
650 else { | |
651 $subString = mb_substr($string, 0, $breakPos); | |
652 $cutLength = $breakPos + 1; | |
653 } | |
654 } | |
655 else { | |
656 $cutLength = $width; | |
657 } | |
658 } | |
659 } | |
660 } | |
661 | |
662 $result[] = $subString; | |
663 | |
664 if ($cutLength !== null) { | |
665 $string = mb_substr($string, $cutLength, ($stringLength - $cutLength)); | |
666 } | |
667 else { | |
668 break; | |
669 } | |
670 } | |
671 | |
672 if ($charset && $charset != RCUBE_CHARSET) { | |
673 mb_internal_encoding(RCUBE_CHARSET); | |
674 } | |
675 | |
676 return implode($break, $result); | |
677 } | |
678 | |
679 /** | |
680 * A method to guess the mime_type of an attachment. | |
681 * | |
682 * @param string $path Path to the file or file contents | |
683 * @param string $name File name (with suffix) | |
684 * @param string $failover Mime type supplied for failover | |
685 * @param boolean $is_stream Set to True if $path contains file contents | |
686 * @param boolean $skip_suffix Set to True if the config/mimetypes.php mappig should be ignored | |
687 * | |
688 * @return string | |
689 * @author Till Klampaeckel <till@php.net> | |
690 * @see http://de2.php.net/manual/en/ref.fileinfo.php | |
691 * @see http://de2.php.net/mime_content_type | |
692 */ | |
693 public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false) | |
694 { | |
695 static $mime_ext = array(); | |
696 | |
697 $mime_type = null; | |
698 $config = rcube::get_instance()->config; | |
699 $mime_magic = $config->get('mime_magic'); | |
700 | |
701 if (!$skip_suffix && empty($mime_ext)) { | |
702 foreach ($config->resolve_paths('mimetypes.php') as $fpath) { | |
703 $mime_ext = array_merge($mime_ext, (array) @include($fpath)); | |
704 } | |
705 } | |
706 | |
707 // use file name suffix with hard-coded mime-type map | |
708 if (!$skip_suffix && is_array($mime_ext) && $name) { | |
709 if ($suffix = substr($name, strrpos($name, '.')+1)) { | |
710 $mime_type = $mime_ext[strtolower($suffix)]; | |
711 } | |
712 } | |
713 | |
714 // try fileinfo extension if available | |
715 if (!$mime_type && function_exists('finfo_open')) { | |
716 // null as a 2nd argument should be the same as no argument | |
717 // this however is not true on all systems/versions | |
718 if ($mime_magic) { | |
719 $finfo = finfo_open(FILEINFO_MIME, $mime_magic); | |
720 } | |
721 else { | |
722 $finfo = finfo_open(FILEINFO_MIME); | |
723 } | |
724 | |
725 if ($finfo) { | |
726 if ($is_stream) | |
727 $mime_type = finfo_buffer($finfo, $path); | |
728 else | |
729 $mime_type = finfo_file($finfo, $path); | |
730 finfo_close($finfo); | |
731 } | |
732 } | |
733 | |
734 // try PHP's mime_content_type | |
735 if (!$mime_type && !$is_stream && function_exists('mime_content_type')) { | |
736 $mime_type = @mime_content_type($path); | |
737 } | |
738 | |
739 // fall back to user-submitted string | |
740 if (!$mime_type) { | |
741 $mime_type = $failover; | |
742 } | |
743 else { | |
744 // Sometimes (PHP-5.3?) content-type contains charset definition, | |
745 // Remove it (#1487122) also "charset=binary" is useless | |
746 $mime_type = array_shift(preg_split('/[; ]/', $mime_type)); | |
747 } | |
748 | |
749 return $mime_type; | |
750 } | |
751 | |
752 /** | |
753 * Get mimetype => file extension mapping | |
754 * | |
755 * @param string Mime-Type to get extensions for | |
756 * | |
757 * @return array List of extensions matching the given mimetype or a hash array | |
758 * with ext -> mimetype mappings if $mimetype is not given | |
759 */ | |
760 public static function get_mime_extensions($mimetype = null) | |
761 { | |
762 static $mime_types, $mime_extensions; | |
763 | |
764 // return cached data | |
765 if (is_array($mime_types)) { | |
766 return $mimetype ? $mime_types[$mimetype] : $mime_extensions; | |
767 } | |
768 | |
769 // load mapping file | |
770 $file_paths = array(); | |
771 | |
772 if ($mime_types = rcube::get_instance()->config->get('mime_types')) { | |
773 $file_paths[] = $mime_types; | |
774 } | |
775 | |
776 // try common locations | |
777 if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') { | |
778 $file_paths[] = 'C:/xampp/apache/conf/mime.types.'; | |
779 } | |
780 else { | |
781 $file_paths[] = '/etc/mime.types'; | |
782 $file_paths[] = '/etc/httpd/mime.types'; | |
783 $file_paths[] = '/etc/httpd2/mime.types'; | |
784 $file_paths[] = '/etc/apache/mime.types'; | |
785 $file_paths[] = '/etc/apache2/mime.types'; | |
786 $file_paths[] = '/etc/nginx/mime.types'; | |
787 $file_paths[] = '/usr/local/etc/httpd/conf/mime.types'; | |
788 $file_paths[] = '/usr/local/etc/apache/conf/mime.types'; | |
789 $file_paths[] = '/usr/local/etc/apache24/mime.types'; | |
790 } | |
791 | |
792 foreach ($file_paths as $fp) { | |
793 if (@is_readable($fp)) { | |
794 $lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); | |
795 break; | |
796 } | |
797 } | |
798 | |
799 $mime_types = $mime_extensions = array(); | |
800 $regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i"; | |
801 foreach ((array)$lines as $line) { | |
802 // skip comments or mime types w/o any extensions | |
803 if ($line[0] == '#' || !preg_match($regex, $line, $matches)) | |
804 continue; | |
805 | |
806 $mime = $matches[1]; | |
807 foreach (explode(' ', $matches[2]) as $ext) { | |
808 $ext = trim($ext); | |
809 $mime_types[$mime][] = $ext; | |
810 $mime_extensions[$ext] = $mime; | |
811 } | |
812 } | |
813 | |
814 // fallback to some well-known types most important for daily emails | |
815 if (empty($mime_types)) { | |
816 foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) { | |
817 $mime_extensions = array_merge($mime_extensions, (array) @include($fpath)); | |
818 } | |
819 | |
820 foreach ($mime_extensions as $ext => $mime) { | |
821 $mime_types[$mime][] = $ext; | |
822 } | |
823 } | |
824 | |
825 // Add some known aliases that aren't included by some mime.types (#1488891) | |
826 // the order is important here so standard extensions have higher prio | |
827 $aliases = array( | |
828 'image/gif' => array('gif'), | |
829 'image/png' => array('png'), | |
830 'image/x-png' => array('png'), | |
831 'image/jpeg' => array('jpg', 'jpeg', 'jpe'), | |
832 'image/jpg' => array('jpg', 'jpeg', 'jpe'), | |
833 'image/pjpeg' => array('jpg', 'jpeg', 'jpe'), | |
834 'image/tiff' => array('tif'), | |
835 'message/rfc822' => array('eml'), | |
836 'text/x-mail' => array('eml'), | |
837 ); | |
838 | |
839 foreach ($aliases as $mime => $exts) { | |
840 $mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts)); | |
841 | |
842 foreach ($exts as $ext) { | |
843 if (!isset($mime_extensions[$ext])) { | |
844 $mime_extensions[$ext] = $mime; | |
845 } | |
846 } | |
847 } | |
848 | |
849 return $mimetype ? $mime_types[$mimetype] : $mime_extensions; | |
850 } | |
851 | |
852 /** | |
853 * Detect image type of the given binary data by checking magic numbers. | |
854 * | |
855 * @param string $data Binary file content | |
856 * | |
857 * @return string Detected mime-type or jpeg as fallback | |
858 */ | |
859 public static function image_content_type($data) | |
860 { | |
861 $type = 'jpeg'; | |
862 if (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png'; | |
863 else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif'; | |
864 else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico'; | |
865 // else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg'; | |
866 | |
867 return 'image/' . $type; | |
868 } | |
869 | |
870 /** | |
871 * Try to fix invalid email addresses | |
872 */ | |
873 public static function fix_email($email) | |
874 { | |
875 $parts = rcube_utils::explode_quoted_string('@', $email); | |
876 foreach ($parts as $idx => $part) { | |
877 // remove redundant quoting (#1490040) | |
878 if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) { | |
879 $parts[$idx] = $m[1]; | |
880 } | |
881 } | |
882 | |
883 return implode('@', $parts); | |
884 } | |
885 } |