| @@ -1,877 +1,0 @@ | ||
| 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 | -} | |