Mercurial > hg > rc2
annotate program/lib/Roundcube/rcube_mime.php @ 18:b631c1c7a3ce
More cleaning up more php8 Warnings/deprecations,
including _many_ ediff-based fixes on rcmail_output_html.php
| author | Charlie Root |
|---|---|
| date | Tue, 07 Oct 2025 11:20:40 -0400 |
| parents | 4681f974d28b |
| children | bea5a38be938 |
| rev | line source |
|---|---|
| 0 | 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; | |
|
18
b631c1c7a3ce
More cleaning up more php8 Warnings/deprecations,
Charlie Root
parents:
0
diff
changeset
|
201 if ($next_match = ($matches[$idx+1]??null)) { |
| 0 | 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 } |
