Mercurial > hg > rc2
comparison program/lib/Roundcube/rcube_message.php @ 0:4681f974d28b
vanilla 1.3.3 distro, I hope
| author | Charlie Root |
|---|---|
| date | Thu, 04 Jan 2018 15:52:31 -0500 |
| parents | |
| children | b6a96bdd6b29 |
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) 2008-2014, The Roundcube Dev Team | | |
| 7 | | | |
| 8 | Licensed under the GNU General Public License version 3 or | | |
| 9 | any later version with exceptions for skins & plugins. | | |
| 10 | See the README file for a full license statement. | | |
| 11 | | | |
| 12 | PURPOSE: | | |
| 13 | Logical representation of a mail message with all its data | | |
| 14 | and related functions | | |
| 15 +-----------------------------------------------------------------------+ | |
| 16 | Author: Thomas Bruederli <roundcube@gmail.com> | | |
| 17 +-----------------------------------------------------------------------+ | |
| 18 */ | |
| 19 | |
| 20 /** | |
| 21 * Logical representation of a mail message with all its data | |
| 22 * and related functions | |
| 23 * | |
| 24 * @package Framework | |
| 25 * @subpackage Storage | |
| 26 * @author Thomas Bruederli <roundcube@gmail.com> | |
| 27 */ | |
| 28 class rcube_message | |
| 29 { | |
| 30 /** | |
| 31 * Instance of framework class. | |
| 32 * | |
| 33 * @var rcube | |
| 34 */ | |
| 35 private $app; | |
| 36 | |
| 37 /** | |
| 38 * Instance of storage class | |
| 39 * | |
| 40 * @var rcube_storage | |
| 41 */ | |
| 42 private $storage; | |
| 43 | |
| 44 /** | |
| 45 * Instance of mime class | |
| 46 * | |
| 47 * @var rcube_mime | |
| 48 */ | |
| 49 private $mime; | |
| 50 private $opt = array(); | |
| 51 private $parse_alternative = false; | |
| 52 | |
| 53 public $uid; | |
| 54 public $folder; | |
| 55 public $headers; | |
| 56 public $sender; | |
| 57 public $context; | |
| 58 public $parts = array(); | |
| 59 public $mime_parts = array(); | |
| 60 public $inline_parts = array(); | |
| 61 public $attachments = array(); | |
| 62 public $subject = ''; | |
| 63 public $is_safe = false; | |
| 64 | |
| 65 const BODY_MAX_SIZE = 1048576; // 1MB | |
| 66 | |
| 67 | |
| 68 /** | |
| 69 * __construct | |
| 70 * | |
| 71 * Provide a uid, and parse message structure. | |
| 72 * | |
| 73 * @param string $uid The message UID. | |
| 74 * @param string $folder Folder name | |
| 75 * @param bool $is_safe Security flag | |
| 76 * | |
| 77 * @see self::$app, self::$storage, self::$opt, self::$parts | |
| 78 */ | |
| 79 function __construct($uid, $folder = null, $is_safe = false) | |
| 80 { | |
| 81 // decode combined UID-folder identifier | |
| 82 if (preg_match('/^[0-9.]+-.+/', $uid)) { | |
| 83 list($uid, $folder) = explode('-', $uid, 2); | |
| 84 } | |
| 85 | |
| 86 if (preg_match('/^([0-9]+)\.([0-9.]+)$/', $uid, $matches)) { | |
| 87 $uid = $matches[1]; | |
| 88 $context = $matches[2]; | |
| 89 } | |
| 90 | |
| 91 $this->uid = $uid; | |
| 92 $this->context = $context; | |
| 93 $this->app = rcube::get_instance(); | |
| 94 $this->storage = $this->app->get_storage(); | |
| 95 $this->folder = strlen($folder) ? $folder : $this->storage->get_folder(); | |
| 96 | |
| 97 // Set current folder | |
| 98 $this->storage->set_folder($this->folder); | |
| 99 $this->storage->set_options(array('all_headers' => true)); | |
| 100 | |
| 101 $this->headers = $this->storage->get_message($uid); | |
| 102 | |
| 103 if (!$this->headers) { | |
| 104 return; | |
| 105 } | |
| 106 | |
| 107 $this->set_safe($is_safe || $_SESSION['safe_messages'][$this->folder.':'.$uid]); | |
| 108 $this->opt = array( | |
| 109 'safe' => $this->is_safe, | |
| 110 'prefer_html' => $this->app->config->get('prefer_html'), | |
| 111 'get_url' => $this->app->url(array( | |
| 112 'action' => 'get', | |
| 113 'mbox' => $this->folder, | |
| 114 'uid' => $uid), | |
| 115 false, false, true) | |
| 116 ); | |
| 117 | |
| 118 if (!empty($this->headers->structure)) { | |
| 119 $this->get_mime_numbers($this->headers->structure); | |
| 120 $this->parse_structure($this->headers->structure); | |
| 121 } | |
| 122 else if ($this->context === null) { | |
| 123 $this->body = $this->storage->get_body($uid); | |
| 124 } | |
| 125 | |
| 126 $this->mime = new rcube_mime($this->headers->charset); | |
| 127 $this->subject = $this->headers->get('subject'); | |
| 128 $from = $this->mime->decode_address_list($this->headers->from, 1); | |
| 129 $this->sender = current($from); | |
| 130 | |
| 131 // notify plugins and let them analyze this structured message object | |
| 132 $this->app->plugins->exec_hook('message_load', array('object' => $this)); | |
| 133 } | |
| 134 | |
| 135 /** | |
| 136 * Return a (decoded) message header | |
| 137 * | |
| 138 * @param string $name Header name | |
| 139 * @param bool $row Don't mime-decode the value | |
| 140 * @return string Header value | |
| 141 */ | |
| 142 public function get_header($name, $raw = false) | |
| 143 { | |
| 144 if (empty($this->headers)) { | |
| 145 return null; | |
| 146 } | |
| 147 | |
| 148 return $this->headers->get($name, !$raw); | |
| 149 } | |
| 150 | |
| 151 /** | |
| 152 * Set is_safe var and session data | |
| 153 * | |
| 154 * @param bool $safe enable/disable | |
| 155 */ | |
| 156 public function set_safe($safe = true) | |
| 157 { | |
| 158 $_SESSION['safe_messages'][$this->folder.':'.$this->uid] = $this->is_safe = $safe; | |
| 159 } | |
| 160 | |
| 161 /** | |
| 162 * Compose a valid URL for getting a message part | |
| 163 * | |
| 164 * @param string $mime_id Part MIME-ID | |
| 165 * @param mixed $embed Mimetype class for parts to be embedded | |
| 166 * @return string URL or false if part does not exist | |
| 167 */ | |
| 168 public function get_part_url($mime_id, $embed = false) | |
| 169 { | |
| 170 if ($this->mime_parts[$mime_id]) | |
| 171 return $this->opt['get_url'] . '&_part=' . $mime_id . ($embed ? '&_embed=1&_mimeclass=' . $embed : ''); | |
| 172 else | |
| 173 return false; | |
| 174 } | |
| 175 | |
| 176 /** | |
| 177 * Get content of a specific part of this message | |
| 178 * | |
| 179 * @param string $mime_id Part MIME-ID | |
| 180 * @param resource $fp File pointer to save the message part | |
| 181 * @param boolean $skip_charset_conv Disables charset conversion | |
| 182 * @param int $max_bytes Only read this number of bytes | |
| 183 * @param boolean $formatted Enables formatting of text/* parts bodies | |
| 184 * | |
| 185 * @return string Part content | |
| 186 * @deprecated | |
| 187 */ | |
| 188 public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false, $max_bytes = 0, $formatted = true) | |
| 189 { | |
| 190 if ($part = $this->mime_parts[$mime_id]) { | |
| 191 // stored in message structure (winmail/inline-uuencode) | |
| 192 if (!empty($part->body) || $part->encoding == 'stream') { | |
| 193 if ($fp) { | |
| 194 fwrite($fp, $part->body); | |
| 195 } | |
| 196 return $fp ? true : $part->body; | |
| 197 } | |
| 198 | |
| 199 // get from IMAP | |
| 200 $this->storage->set_folder($this->folder); | |
| 201 | |
| 202 return $this->storage->get_message_part($this->uid, $mime_id, $part, | |
| 203 NULL, $fp, $skip_charset_conv, $max_bytes, $formatted); | |
| 204 } | |
| 205 } | |
| 206 | |
| 207 /** | |
| 208 * Get content of a specific part of this message | |
| 209 * | |
| 210 * @param string $mime_id Part ID | |
| 211 * @param boolean $formatted Enables formatting of text/* parts bodies | |
| 212 * @param int $max_bytes Only return/read this number of bytes | |
| 213 * @param mixed $mode NULL to return a string, -1 to print body | |
| 214 * or file pointer to save the body into | |
| 215 * | |
| 216 * @return string|bool Part content or operation status | |
| 217 */ | |
| 218 public function get_part_body($mime_id, $formatted = false, $max_bytes = 0, $mode = null) | |
| 219 { | |
| 220 if (!($part = $this->mime_parts[$mime_id])) { | |
| 221 return; | |
| 222 } | |
| 223 | |
| 224 // allow plugins to modify part body | |
| 225 $plugin = $this->app->plugins->exec_hook('message_part_body', | |
| 226 array('object' => $this, 'part' => $part)); | |
| 227 | |
| 228 // only text parts can be formatted | |
| 229 $formatted = $formatted && $part->ctype_primary == 'text'; | |
| 230 | |
| 231 // part body not fetched yet... save in memory if it's small enough | |
| 232 if ($part->body === null && is_numeric($mime_id) && $part->size < self::BODY_MAX_SIZE) { | |
| 233 $this->storage->set_folder($this->folder); | |
| 234 // Warning: body here should be always unformatted | |
| 235 $part->body = $this->storage->get_message_part($this->uid, $mime_id, $part, | |
| 236 null, null, true, 0, false); | |
| 237 } | |
| 238 | |
| 239 // body stored in message structure (winmail/inline-uuencode) | |
| 240 if ($part->body !== null || $part->encoding == 'stream') { | |
| 241 $body = $part->body; | |
| 242 | |
| 243 if ($formatted && $body) { | |
| 244 $body = self::format_part_body($body, $part, $this->headers->charset); | |
| 245 } | |
| 246 | |
| 247 if ($max_bytes && strlen($body) > $max_bytes) { | |
| 248 $body = substr($body, 0, $max_bytes); | |
| 249 } | |
| 250 | |
| 251 if (is_resource($mode)) { | |
| 252 if ($body !== false) { | |
| 253 fwrite($mode, $body); | |
| 254 rewind($mode); | |
| 255 } | |
| 256 | |
| 257 return $body !== false; | |
| 258 } | |
| 259 | |
| 260 if ($mode === -1) { | |
| 261 if ($body !== false) { | |
| 262 print($body); | |
| 263 } | |
| 264 | |
| 265 return $body !== false; | |
| 266 } | |
| 267 | |
| 268 return $body; | |
| 269 } | |
| 270 | |
| 271 // get the body from IMAP | |
| 272 $this->storage->set_folder($this->folder); | |
| 273 | |
| 274 $body = $this->storage->get_message_part($this->uid, $mime_id, $part, | |
| 275 $mode === -1, is_resource($mode) ? $mode : null, | |
| 276 !($mode && $formatted), $max_bytes, $mode && $formatted); | |
| 277 | |
| 278 if (is_resource($mode)) { | |
| 279 rewind($mode); | |
| 280 return $body !== false; | |
| 281 } | |
| 282 | |
| 283 if (!$mode && $body && $formatted) { | |
| 284 $body = self::format_part_body($body, $part, $this->headers->charset); | |
| 285 } | |
| 286 | |
| 287 return $body; | |
| 288 } | |
| 289 | |
| 290 /** | |
| 291 * Format text message part for display | |
| 292 * | |
| 293 * @param string $body Part body | |
| 294 * @param rcube_message_part $part Part object | |
| 295 * @param string $default_charset Fallback charset if part charset is not specified | |
| 296 * | |
| 297 * @return string Formatted body | |
| 298 */ | |
| 299 public static function format_part_body($body, $part, $default_charset = null) | |
| 300 { | |
| 301 // remove useless characters | |
| 302 $body = preg_replace('/[\t\r\0\x0B]+\n/', "\n", $body); | |
| 303 | |
| 304 // remove NULL characters if any (#1486189) | |
| 305 if (strpos($body, "\x00") !== false) { | |
| 306 $body = str_replace("\x00", '', $body); | |
| 307 } | |
| 308 | |
| 309 // detect charset... | |
| 310 if (!$part->charset || strtoupper($part->charset) == 'US-ASCII') { | |
| 311 // try to extract charset information from HTML meta tag (#1488125) | |
| 312 if ($part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) { | |
| 313 $part->charset = strtoupper($m[1]); | |
| 314 } | |
| 315 else if ($default_charset) { | |
| 316 $part->charset = $default_charset; | |
| 317 } | |
| 318 else { | |
| 319 $rcube = rcube::get_instance(); | |
| 320 $part->charset = $rcube->config->get('default_charset', RCUBE_CHARSET); | |
| 321 } | |
| 322 } | |
| 323 | |
| 324 // ..convert charset encoding | |
| 325 $body = rcube_charset::convert($body, $part->charset); | |
| 326 | |
| 327 return $body; | |
| 328 } | |
| 329 | |
| 330 /** | |
| 331 * Determine if the message contains a HTML part. This must to be | |
| 332 * a real part not an attachment (or its part) | |
| 333 * | |
| 334 * @param bool $enriched Enables checking for text/enriched parts too | |
| 335 * @param rcube_message_part &$part Reference to the part if found | |
| 336 * | |
| 337 * @return bool True if a HTML is available, False if not | |
| 338 */ | |
| 339 public function has_html_part($enriched = false, &$part = null) | |
| 340 { | |
| 341 // check all message parts | |
| 342 foreach ($this->mime_parts as $part) { | |
| 343 if ($part->mimetype == 'text/html' || ($enriched && $part->mimetype == 'text/enriched')) { | |
| 344 // Skip if part is an attachment, don't use is_attachment() here | |
| 345 if ($part->filename) { | |
| 346 continue; | |
| 347 } | |
| 348 | |
| 349 if (!$part->size) { | |
| 350 continue; | |
| 351 } | |
| 352 | |
| 353 if (!$this->check_context($part)) { | |
| 354 continue; | |
| 355 } | |
| 356 | |
| 357 $level = explode('.', $part->mime_id); | |
| 358 $depth = count($level); | |
| 359 $last = ''; | |
| 360 | |
| 361 // Check if the part belongs to higher-level's multipart part | |
| 362 // this can be alternative/related/signed/encrypted or mixed | |
| 363 while (array_pop($level) !== null) { | |
| 364 $parent_depth = count($level); | |
| 365 if (!$parent_depth) { | |
| 366 return true; | |
| 367 } | |
| 368 | |
| 369 $parent = $this->mime_parts[join('.', $level)]; | |
| 370 | |
| 371 if (!$this->check_context($parent)) { | |
| 372 return true; | |
| 373 } | |
| 374 | |
| 375 $max_delta = $depth - (1 + ($last == 'multipart/alternative' ? 1 : 0)); | |
| 376 $last = $parent->real_mimetype ?: $parent->mimetype; | |
| 377 | |
| 378 if (!preg_match('/^multipart\/(alternative|related|signed|encrypted|mixed)$/', $last) | |
| 379 || ($last == 'multipart/mixed' && $parent_depth < $max_delta)) { | |
| 380 continue 2; | |
| 381 } | |
| 382 } | |
| 383 | |
| 384 return true; | |
| 385 } | |
| 386 } | |
| 387 | |
| 388 $part = null; | |
| 389 | |
| 390 return false; | |
| 391 } | |
| 392 | |
| 393 /** | |
| 394 * Determine if the message contains a text/plain part. This must to be | |
| 395 * a real part not an attachment (or its part) | |
| 396 * | |
| 397 * @param rcube_message_part &$part Reference to the part if found | |
| 398 * | |
| 399 * @return bool True if a plain text part is available, False if not | |
| 400 */ | |
| 401 public function has_text_part(&$part = null) | |
| 402 { | |
| 403 // check all message parts | |
| 404 foreach ($this->mime_parts as $part) { | |
| 405 if ($part->mimetype == 'text/plain') { | |
| 406 // Skip if part is an attachment, don't use is_attachment() here | |
| 407 if ($part->filename) { | |
| 408 continue; | |
| 409 } | |
| 410 | |
| 411 if (!$part->size) { | |
| 412 continue; | |
| 413 } | |
| 414 | |
| 415 if (!$this->check_context($part)) { | |
| 416 continue; | |
| 417 } | |
| 418 | |
| 419 $level = explode('.', $part->mime_id); | |
| 420 | |
| 421 // Check if the part belongs to higher-level's alternative/related | |
| 422 while (array_pop($level) !== null) { | |
| 423 if (!count($level)) { | |
| 424 return true; | |
| 425 } | |
| 426 | |
| 427 $parent = $this->mime_parts[join('.', $level)]; | |
| 428 | |
| 429 if (!$this->check_context($parent)) { | |
| 430 return true; | |
| 431 } | |
| 432 | |
| 433 if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') { | |
| 434 continue 2; | |
| 435 } | |
| 436 } | |
| 437 | |
| 438 return true; | |
| 439 } | |
| 440 } | |
| 441 | |
| 442 $part = null; | |
| 443 | |
| 444 return false; | |
| 445 } | |
| 446 | |
| 447 /** | |
| 448 * Return the first HTML part of this message | |
| 449 * | |
| 450 * @param rcube_message_part &$part Reference to the part if found | |
| 451 * @param bool $enriched Enables checking for text/enriched parts too | |
| 452 * | |
| 453 * @return string HTML message part content | |
| 454 */ | |
| 455 public function first_html_part(&$part = null, $enriched = false) | |
| 456 { | |
| 457 if ($this->has_html_part($enriched, $part)) { | |
| 458 $body = $this->get_part_body($part->mime_id, true); | |
| 459 | |
| 460 if ($part->mimetype == 'text/enriched') { | |
| 461 $body = rcube_enriched::to_html($body); | |
| 462 } | |
| 463 | |
| 464 return $body; | |
| 465 } | |
| 466 } | |
| 467 | |
| 468 /** | |
| 469 * Return the first text part of this message. | |
| 470 * If there's no text/plain part but $strict=true and text/html part | |
| 471 * exists, it will be returned in text/plain format. | |
| 472 * | |
| 473 * @param rcube_message_part &$part Reference to the part if found | |
| 474 * @param bool $strict Check only text/plain parts | |
| 475 * | |
| 476 * @return string Plain text message/part content | |
| 477 */ | |
| 478 public function first_text_part(&$part = null, $strict = false) | |
| 479 { | |
| 480 // no message structure, return complete body | |
| 481 if (empty($this->parts)) { | |
| 482 return $this->body; | |
| 483 } | |
| 484 | |
| 485 if ($this->has_text_part($part)) { | |
| 486 return $this->get_part_body($part->mime_id, true); | |
| 487 } | |
| 488 | |
| 489 if (!$strict && ($body = $this->first_html_part($part, true))) { | |
| 490 // create instance of html2text class | |
| 491 $h2t = new rcube_html2text($body); | |
| 492 return $h2t->get_text(); | |
| 493 } | |
| 494 } | |
| 495 | |
| 496 /** | |
| 497 * Return message parts in current context | |
| 498 */ | |
| 499 public function mime_parts() | |
| 500 { | |
| 501 if ($this->context === null) { | |
| 502 return $this->mime_parts; | |
| 503 } | |
| 504 | |
| 505 $parts = array(); | |
| 506 | |
| 507 foreach ($this->mime_parts as $part_id => $part) { | |
| 508 if ($this->check_context($part)) { | |
| 509 $parts[$part_id] = $part; | |
| 510 } | |
| 511 } | |
| 512 | |
| 513 return $parts; | |
| 514 } | |
| 515 | |
| 516 /** | |
| 517 * Checks if part of the message is an attachment (or part of it) | |
| 518 * | |
| 519 * @param rcube_message_part $part Message part | |
| 520 * | |
| 521 * @return bool True if the part is an attachment part | |
| 522 */ | |
| 523 public function is_attachment($part) | |
| 524 { | |
| 525 foreach ($this->attachments as $att_part) { | |
| 526 if ($att_part->mime_id == $part->mime_id) { | |
| 527 return true; | |
| 528 } | |
| 529 | |
| 530 // check if the part is a subpart of another attachment part (message/rfc822) | |
| 531 if ($att_part->mimetype == 'message/rfc822') { | |
| 532 if (in_array($part, (array)$att_part->parts)) { | |
| 533 return true; | |
| 534 } | |
| 535 } | |
| 536 } | |
| 537 | |
| 538 return false; | |
| 539 } | |
| 540 | |
| 541 /** | |
| 542 * In a multipart/encrypted encrypted message, | |
| 543 * find the encrypted message payload part. | |
| 544 * | |
| 545 * @return rcube_message_part | |
| 546 */ | |
| 547 public function get_multipart_encrypted_part() | |
| 548 { | |
| 549 foreach ($this->mime_parts as $mime_id => $mpart) { | |
| 550 if ($mpart->mimetype == 'multipart/encrypted') { | |
| 551 $this->pgp_mime = true; | |
| 552 } | |
| 553 if ($this->pgp_mime && ($mpart->mimetype == 'application/octet-stream' || | |
| 554 (!empty($mpart->filename) && $mpart->filename != 'version.txt'))) { | |
| 555 $this->encrypted_part = $mime_id; | |
| 556 return $mpart; | |
| 557 } | |
| 558 } | |
| 559 | |
| 560 return false; | |
| 561 } | |
| 562 | |
| 563 /** | |
| 564 * Read the message structure returend by the IMAP server | |
| 565 * and build flat lists of content parts and attachments | |
| 566 * | |
| 567 * @param rcube_message_part $structure Message structure node | |
| 568 * @param bool $recursive True when called recursively | |
| 569 */ | |
| 570 private function parse_structure($structure, $recursive = false) | |
| 571 { | |
| 572 // real content-type of message/rfc822 part | |
| 573 if ($structure->mimetype == 'message/rfc822' && $structure->real_mimetype) { | |
| 574 $mimetype = $structure->real_mimetype; | |
| 575 | |
| 576 // parse headers from message/rfc822 part | |
| 577 if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) { | |
| 578 list($headers, ) = explode("\r\n\r\n", $this->get_part_body($structure->mime_id, false, 32768)); | |
| 579 $structure->headers = rcube_mime::parse_headers($headers); | |
| 580 | |
| 581 if ($this->context == $structure->mime_id) { | |
| 582 $this->headers = rcube_message_header::from_array($structure->headers); | |
| 583 } | |
| 584 } | |
| 585 } | |
| 586 else { | |
| 587 $mimetype = $structure->mimetype; | |
| 588 } | |
| 589 | |
| 590 // show message headers | |
| 591 if ($recursive && is_array($structure->headers) && | |
| 592 (isset($structure->headers['subject']) || $structure->headers['from'] || $structure->headers['to']) | |
| 593 ) { | |
| 594 $c = new stdClass; | |
| 595 $c->type = 'headers'; | |
| 596 $c->headers = $structure->headers; | |
| 597 $this->add_part($c); | |
| 598 } | |
| 599 | |
| 600 // Allow plugins to handle message parts | |
| 601 $plugin = $this->app->plugins->exec_hook('message_part_structure', | |
| 602 array('object' => $this, 'structure' => $structure, | |
| 603 'mimetype' => $mimetype, 'recursive' => $recursive)); | |
| 604 | |
| 605 if ($plugin['abort']) { | |
| 606 return; | |
| 607 } | |
| 608 | |
| 609 $structure = $plugin['structure']; | |
| 610 $mimetype = $plugin['mimetype']; | |
| 611 $recursive = $plugin['recursive']; | |
| 612 | |
| 613 list($message_ctype_primary, $message_ctype_secondary) = explode('/', $mimetype); | |
| 614 | |
| 615 // print body if message doesn't have multiple parts | |
| 616 if ($message_ctype_primary == 'text' && !$recursive) { | |
| 617 // parts with unsupported type add to attachments list | |
| 618 if (!in_array($message_ctype_secondary, array('plain', 'html', 'enriched'))) { | |
| 619 $this->add_part($structure, 'attachment'); | |
| 620 return; | |
| 621 } | |
| 622 | |
| 623 $structure->type = 'content'; | |
| 624 $this->add_part($structure); | |
| 625 | |
| 626 // Parse simple (plain text) message body | |
| 627 if ($message_ctype_secondary == 'plain') { | |
| 628 foreach ((array)$this->uu_decode($structure) as $uupart) { | |
| 629 $this->mime_parts[$uupart->mime_id] = $uupart; | |
| 630 $this->add_part($uupart, 'attachment'); | |
| 631 } | |
| 632 } | |
| 633 } | |
| 634 // the same for pgp signed messages | |
| 635 else if ($mimetype == 'application/pgp' && !$recursive) { | |
| 636 $structure->type = 'content'; | |
| 637 $this->add_part($structure); | |
| 638 } | |
| 639 // message contains (more than one!) alternative parts | |
| 640 else if ($mimetype == 'multipart/alternative' | |
| 641 && is_array($structure->parts) && count($structure->parts) > 1 | |
| 642 ) { | |
| 643 // get html/plaintext parts, other add to attachments list | |
| 644 foreach ($structure->parts as $p => $sub_part) { | |
| 645 $sub_mimetype = $sub_part->mimetype; | |
| 646 $is_multipart = preg_match('/^multipart\/(related|relative|mixed|alternative)/', $sub_mimetype); | |
| 647 | |
| 648 // skip empty text parts | |
| 649 if (!$sub_part->size && !$is_multipart) { | |
| 650 continue; | |
| 651 } | |
| 652 | |
| 653 // We've encountered (malformed) messages with more than | |
| 654 // one text/plain or text/html part here. There's no way to choose | |
| 655 // which one is better, so we'll display first of them and add | |
| 656 // others as attachments (#1489358) | |
| 657 | |
| 658 // check if sub part is | |
| 659 if ($is_multipart) | |
| 660 $related_part = $p; | |
| 661 else if ($sub_mimetype == 'text/plain' && !$plain_part) | |
| 662 $plain_part = $p; | |
| 663 else if ($sub_mimetype == 'text/html' && !$html_part) { | |
| 664 $html_part = $p; | |
| 665 $this->got_html_part = true; | |
| 666 } | |
| 667 else if ($sub_mimetype == 'text/enriched' && !$enriched_part) | |
| 668 $enriched_part = $p; | |
| 669 else { | |
| 670 // add unsupported/unrecognized parts to attachments list | |
| 671 $this->add_part($sub_part, 'attachment'); | |
| 672 } | |
| 673 } | |
| 674 | |
| 675 // parse related part (alternative part could be in here) | |
| 676 if ($related_part !== null && !$this->parse_alternative) { | |
| 677 $this->parse_alternative = true; | |
| 678 $this->parse_structure($structure->parts[$related_part], true); | |
| 679 $this->parse_alternative = false; | |
| 680 | |
| 681 // if plain part was found, we should unset it if html is preferred | |
| 682 if ($this->opt['prefer_html'] && count($this->parts)) { | |
| 683 $plain_part = null; | |
| 684 } | |
| 685 } | |
| 686 | |
| 687 // choose html/plain part to print | |
| 688 if ($html_part !== null && $this->opt['prefer_html']) { | |
| 689 $print_part = $structure->parts[$html_part]; | |
| 690 } | |
| 691 else if ($enriched_part !== null) { | |
| 692 $print_part = $structure->parts[$enriched_part]; | |
| 693 } | |
| 694 else if ($plain_part !== null) { | |
| 695 $print_part = $structure->parts[$plain_part]; | |
| 696 } | |
| 697 | |
| 698 // add the right message body | |
| 699 if (is_object($print_part)) { | |
| 700 $print_part->type = 'content'; | |
| 701 | |
| 702 // Allow plugins to handle also this part | |
| 703 $plugin = $this->app->plugins->exec_hook('message_part_structure', | |
| 704 array('object' => $this, 'structure' => $print_part, | |
| 705 'mimetype' => $print_part->mimetype, 'recursive' => true)); | |
| 706 | |
| 707 if (!$plugin['abort']) { | |
| 708 $this->add_part($print_part); | |
| 709 } | |
| 710 } | |
| 711 // show plaintext warning | |
| 712 else if ($html_part !== null && empty($this->parts)) { | |
| 713 $c = new stdClass; | |
| 714 $c->type = 'content'; | |
| 715 $c->ctype_primary = 'text'; | |
| 716 $c->ctype_secondary = 'plain'; | |
| 717 $c->mimetype = 'text/plain'; | |
| 718 $c->realtype = 'text/html'; | |
| 719 | |
| 720 $this->add_part($c); | |
| 721 } | |
| 722 } | |
| 723 // this is an ecrypted message -> create a plaintext body with the according message | |
| 724 else if ($mimetype == 'multipart/encrypted') { | |
| 725 $p = new stdClass; | |
| 726 $p->type = 'content'; | |
| 727 $p->ctype_primary = 'text'; | |
| 728 $p->ctype_secondary = 'plain'; | |
| 729 $p->mimetype = 'text/plain'; | |
| 730 $p->realtype = 'multipart/encrypted'; | |
| 731 $p->mime_id = $structure->mime_id; | |
| 732 | |
| 733 $this->add_part($p); | |
| 734 | |
| 735 // add encrypted payload part as attachment | |
| 736 if (is_array($structure->parts)) { | |
| 737 for ($i=0; $i < count($structure->parts); $i++) { | |
| 738 $subpart = $structure->parts[$i]; | |
| 739 if ($subpart->mimetype == 'application/octet-stream' || !empty($subpart->filename)) { | |
| 740 $this->add_part($subpart, 'attachment'); | |
| 741 } | |
| 742 } | |
| 743 } | |
| 744 } | |
| 745 // this is an S/MIME ecrypted message -> create a plaintext body with the according message | |
| 746 else if ($mimetype == 'application/pkcs7-mime') { | |
| 747 $p = new stdClass; | |
| 748 $p->type = 'content'; | |
| 749 $p->ctype_primary = 'text'; | |
| 750 $p->ctype_secondary = 'plain'; | |
| 751 $p->mimetype = 'text/plain'; | |
| 752 $p->realtype = 'application/pkcs7-mime'; | |
| 753 $p->mime_id = $structure->mime_id; | |
| 754 | |
| 755 $this->add_part($p); | |
| 756 | |
| 757 if (!empty($structure->filename)) { | |
| 758 $this->add_part($structure, 'attachment'); | |
| 759 } | |
| 760 } | |
| 761 // message contains multiple parts | |
| 762 else if (is_array($structure->parts) && !empty($structure->parts)) { | |
| 763 // iterate over parts | |
| 764 for ($i=0; $i < count($structure->parts); $i++) { | |
| 765 $mail_part = &$structure->parts[$i]; | |
| 766 $primary_type = $mail_part->ctype_primary; | |
| 767 $secondary_type = $mail_part->ctype_secondary; | |
| 768 $part_mimetype = $mail_part->mimetype; | |
| 769 | |
| 770 // multipart/alternative or message/rfc822 | |
| 771 if ($primary_type == 'multipart' || $part_mimetype == 'message/rfc822') { | |
| 772 $this->parse_structure($mail_part, true); | |
| 773 | |
| 774 // list message/rfc822 as attachment as well (mostly .eml) | |
| 775 if ($primary_type == 'message' && !empty($mail_part->filename)) { | |
| 776 $this->add_part($mail_part, 'attachment'); | |
| 777 } | |
| 778 } | |
| 779 // part text/[plain|html] or delivery status | |
| 780 else if ((($part_mimetype == 'text/plain' || $part_mimetype == 'text/html') && $mail_part->disposition != 'attachment') || | |
| 781 in_array($part_mimetype, array('message/delivery-status', 'text/rfc822-headers', 'message/disposition-notification')) | |
| 782 ) { | |
| 783 // Allow plugins to handle also this part | |
| 784 $plugin = $this->app->plugins->exec_hook('message_part_structure', | |
| 785 array('object' => $this, 'structure' => $mail_part, | |
| 786 'mimetype' => $part_mimetype, 'recursive' => true)); | |
| 787 | |
| 788 if ($plugin['abort']) { | |
| 789 continue; | |
| 790 } | |
| 791 | |
| 792 if ($part_mimetype == 'text/html' && $mail_part->size) { | |
| 793 $this->got_html_part = true; | |
| 794 } | |
| 795 | |
| 796 $mail_part = $plugin['structure']; | |
| 797 list($primary_type, $secondary_type) = explode('/', $plugin['mimetype']); | |
| 798 | |
| 799 // add text part if it matches the prefs | |
| 800 if (!$this->parse_alternative || | |
| 801 ($secondary_type == 'html' && $this->opt['prefer_html']) || | |
| 802 ($secondary_type == 'plain' && !$this->opt['prefer_html']) | |
| 803 ) { | |
| 804 $mail_part->type = 'content'; | |
| 805 $this->add_part($mail_part); | |
| 806 } | |
| 807 | |
| 808 // list as attachment as well | |
| 809 if (!empty($mail_part->filename)) { | |
| 810 $this->add_part($mail_part, 'attachment'); | |
| 811 } | |
| 812 } | |
| 813 // ignore "virtual" protocol parts | |
| 814 else if ($primary_type == 'protocol') { | |
| 815 continue; | |
| 816 } | |
| 817 // part is Microsoft Outlook TNEF (winmail.dat) | |
| 818 else if ($part_mimetype == 'application/ms-tnef') { | |
| 819 $tnef_parts = (array) $this->tnef_decode($mail_part); | |
| 820 foreach ($tnef_parts as $tpart) { | |
| 821 $this->mime_parts[$tpart->mime_id] = $tpart; | |
| 822 $this->add_part($tpart, 'attachment'); | |
| 823 } | |
| 824 | |
| 825 // add winmail.dat to the list if it's content is unknown | |
| 826 if (empty($tnef_parts) && !empty($mail_part->filename)) { | |
| 827 $this->mime_parts[$mail_part->mime_id] = $mail_part; | |
| 828 $this->add_part($mail_part, 'attachment'); | |
| 829 } | |
| 830 } | |
| 831 // part is a file/attachment | |
| 832 else if (preg_match('/^(inline|attach)/', $mail_part->disposition) || | |
| 833 $mail_part->headers['content-id'] || | |
| 834 ($mail_part->filename && | |
| 835 (empty($mail_part->disposition) || preg_match('/^[a-z0-9!#$&.+^_-]+$/i', $mail_part->disposition))) | |
| 836 ) { | |
| 837 // skip apple resource forks | |
| 838 if ($message_ctype_secondary == 'appledouble' && $secondary_type == 'applefile') | |
| 839 continue; | |
| 840 | |
| 841 // part belongs to a related message and is linked | |
| 842 if (preg_match('/^multipart\/(related|relative)/', $mimetype) | |
| 843 && ($mail_part->headers['content-id'] || $mail_part->headers['content-location']) | |
| 844 ) { | |
| 845 if ($mail_part->headers['content-id']) | |
| 846 $mail_part->content_id = preg_replace(array('/^</', '/>$/'), '', $mail_part->headers['content-id']); | |
| 847 if ($mail_part->headers['content-location']) | |
| 848 $mail_part->content_location = $mail_part->headers['content-base'] . $mail_part->headers['content-location']; | |
| 849 | |
| 850 $this->add_part($mail_part, 'inline'); | |
| 851 } | |
| 852 // regular attachment with valid content type | |
| 853 // (content-type name regexp according to RFC4288.4.2) | |
| 854 else if (preg_match('/^[a-z0-9!#$&.+^_-]+\/[a-z0-9!#$&.+^_-]+$/i', $part_mimetype)) { | |
| 855 $this->add_part($mail_part, 'attachment'); | |
| 856 } | |
| 857 // attachment with invalid content type | |
| 858 // replace malformed content type with application/octet-stream (#1487767) | |
| 859 else if ($mail_part->filename) { | |
| 860 $mail_part->ctype_primary = 'application'; | |
| 861 $mail_part->ctype_secondary = 'octet-stream'; | |
| 862 $mail_part->mimetype = 'application/octet-stream'; | |
| 863 | |
| 864 $this->add_part($mail_part, 'attachment'); | |
| 865 } | |
| 866 } | |
| 867 // calendar part not marked as attachment (#1490325) | |
| 868 else if ($part_mimetype == 'text/calendar') { | |
| 869 if (!$mail_part->filename) { | |
| 870 $mail_part->filename = 'calendar.ics'; | |
| 871 } | |
| 872 | |
| 873 $this->add_part($mail_part, 'attachment'); | |
| 874 } | |
| 875 } | |
| 876 | |
| 877 // if this was a related part try to resolve references | |
| 878 if (preg_match('/^multipart\/(related|relative)/', $mimetype) && count($this->inline_parts)) { | |
| 879 $a_replaces = array(); | |
| 880 $img_regexp = '/^image\/(gif|jpe?g|png|tiff|bmp|svg)/'; | |
| 881 | |
| 882 foreach ($this->inline_parts as $inline_object) { | |
| 883 $part_url = $this->get_part_url($inline_object->mime_id, $inline_object->ctype_primary); | |
| 884 if (isset($inline_object->content_id)) | |
| 885 $a_replaces['cid:'.$inline_object->content_id] = $part_url; | |
| 886 if ($inline_object->content_location) { | |
| 887 $a_replaces[$inline_object->content_location] = $part_url; | |
| 888 } | |
| 889 | |
| 890 if (!empty($inline_object->filename)) { | |
| 891 // MS Outlook sends sometimes non-related attachments as related | |
| 892 // In this case multipart/related message has only one text part | |
| 893 // We'll add all such attachments to the attachments list | |
| 894 if (!isset($this->got_html_part)) { | |
| 895 $this->add_part($inline_object, 'attachment'); | |
| 896 } | |
| 897 // MS Outlook sometimes also adds non-image attachments as related | |
| 898 // We'll add all such attachments to the attachments list | |
| 899 // Warning: some browsers support pdf in <img/> | |
| 900 else if (!preg_match($img_regexp, $inline_object->mimetype)) { | |
| 901 $this->add_part($inline_object, 'attachment'); | |
| 902 } | |
| 903 // @TODO: we should fetch HTML body and find attachment's content-id | |
| 904 // to handle also image attachments without reference in the body | |
| 905 // @TODO: should we list all image attachments in text mode? | |
| 906 } | |
| 907 } | |
| 908 | |
| 909 // add replace array to each content part | |
| 910 // (will be applied later when part body is available) | |
| 911 foreach ($this->parts as $i => $part) { | |
| 912 if ($part->type == 'content') | |
| 913 $this->parts[$i]->replaces = $a_replaces; | |
| 914 } | |
| 915 } | |
| 916 } | |
| 917 // message is a single part non-text | |
| 918 else if ($structure->filename || preg_match('/^application\//i', $mimetype)) { | |
| 919 $this->add_part($structure, 'attachment'); | |
| 920 } | |
| 921 } | |
| 922 | |
| 923 /** | |
| 924 * Fill a flat array with references to all parts, indexed by part numbers | |
| 925 * | |
| 926 * @param rcube_message_part $part Message body structure | |
| 927 */ | |
| 928 private function get_mime_numbers(&$part) | |
| 929 { | |
| 930 if (strlen($part->mime_id)) | |
| 931 $this->mime_parts[$part->mime_id] = &$part; | |
| 932 | |
| 933 if (is_array($part->parts)) | |
| 934 for ($i=0; $i<count($part->parts); $i++) | |
| 935 $this->get_mime_numbers($part->parts[$i]); | |
| 936 } | |
| 937 | |
| 938 /** | |
| 939 * Add a part to object parts array(s) (with context check) | |
| 940 */ | |
| 941 private function add_part($part, $type = null) | |
| 942 { | |
| 943 if ($this->check_context($part)) { | |
| 944 switch ($type) { | |
| 945 case 'inline': $this->inline_parts[] = $part; break; | |
| 946 case 'attachment': $this->attachments[] = $part; break; | |
| 947 default: $this->parts[] = $part; break; | |
| 948 } | |
| 949 } | |
| 950 } | |
| 951 | |
| 952 /** | |
| 953 * Check if specified part belongs to the current context | |
| 954 */ | |
| 955 private function check_context($part) | |
| 956 { | |
| 957 return $this->context === null || strpos($part->mime_id, $this->context . '.') === 0; | |
| 958 } | |
| 959 | |
| 960 /** | |
| 961 * Decode a Microsoft Outlook TNEF part (winmail.dat) | |
| 962 * | |
| 963 * @param rcube_message_part $part Message part to decode | |
| 964 * @return array | |
| 965 */ | |
| 966 function tnef_decode(&$part) | |
| 967 { | |
| 968 // @TODO: attachment may be huge, handle body via file | |
| 969 $body = $this->get_part_body($part->mime_id); | |
| 970 $tnef = new rcube_tnef_decoder; | |
| 971 $tnef_arr = $tnef->decompress($body); | |
| 972 $parts = array(); | |
| 973 | |
| 974 unset($body); | |
| 975 | |
| 976 foreach ($tnef_arr as $pid => $winatt) { | |
| 977 $tpart = new rcube_message_part; | |
| 978 | |
| 979 $tpart->filename = $this->fix_attachment_name(trim($winatt['name']), $part); | |
| 980 $tpart->encoding = 'stream'; | |
| 981 $tpart->ctype_primary = trim(strtolower($winatt['type'])); | |
| 982 $tpart->ctype_secondary = trim(strtolower($winatt['subtype'])); | |
| 983 $tpart->mimetype = $tpart->ctype_primary . '/' . $tpart->ctype_secondary; | |
| 984 $tpart->mime_id = 'winmail.' . $part->mime_id . '.' . $pid; | |
| 985 $tpart->size = $winatt['size']; | |
| 986 $tpart->body = $winatt['stream']; | |
| 987 | |
| 988 $parts[] = $tpart; | |
| 989 unset($tnef_arr[$pid]); | |
| 990 } | |
| 991 | |
| 992 return $parts; | |
| 993 } | |
| 994 | |
| 995 /** | |
| 996 * Parse message body for UUencoded attachments bodies | |
| 997 * | |
| 998 * @param rcube_message_part $part Message part to decode | |
| 999 * @return array | |
| 1000 */ | |
| 1001 function uu_decode(&$part) | |
| 1002 { | |
| 1003 // @TODO: messages may be huge, handle body via file | |
| 1004 $part->body = $this->get_part_body($part->mime_id); | |
| 1005 $parts = array(); | |
| 1006 $pid = 0; | |
| 1007 | |
| 1008 // FIXME: line length is max.65? | |
| 1009 $uu_regexp_begin = '/begin [0-7]{3,4} ([^\r\n]+)\r?\n/s'; | |
| 1010 $uu_regexp_end = '/`\r?\nend((\r?\n)|($))/s'; | |
| 1011 | |
| 1012 while (preg_match($uu_regexp_begin, $part->body, $matches, PREG_OFFSET_CAPTURE)) { | |
| 1013 $startpos = $matches[0][1]; | |
| 1014 | |
| 1015 if (!preg_match($uu_regexp_end, $part->body, $m, PREG_OFFSET_CAPTURE, $startpos)) { | |
| 1016 break; | |
| 1017 } | |
| 1018 | |
| 1019 $endpos = $m[0][1]; | |
| 1020 $begin_len = strlen($matches[0][0]); | |
| 1021 $end_len = strlen($m[0][0]); | |
| 1022 | |
| 1023 // extract attachment body | |
| 1024 $filebody = substr($part->body, $startpos + $begin_len, $endpos - $startpos - $begin_len - 1); | |
| 1025 $filebody = str_replace("\r\n", "\n", $filebody); | |
| 1026 | |
| 1027 // remove attachment body from the message body | |
| 1028 $part->body = substr_replace($part->body, '', $startpos, $endpos + $end_len - $startpos); | |
| 1029 // mark body as modified so it will not be cached by rcube_imap_cache | |
| 1030 $part->body_modified = true; | |
| 1031 | |
| 1032 // add attachments to the structure | |
| 1033 $uupart = new rcube_message_part; | |
| 1034 $uupart->filename = trim($matches[1][0]); | |
| 1035 $uupart->encoding = 'stream'; | |
| 1036 $uupart->body = convert_uudecode($filebody); | |
| 1037 $uupart->size = strlen($uupart->body); | |
| 1038 $uupart->mime_id = 'uu.' . $part->mime_id . '.' . $pid; | |
| 1039 | |
| 1040 $ctype = rcube_mime::file_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true); | |
| 1041 $uupart->mimetype = $ctype; | |
| 1042 list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype); | |
| 1043 | |
| 1044 $parts[] = $uupart; | |
| 1045 $pid++; | |
| 1046 } | |
| 1047 | |
| 1048 return $parts; | |
| 1049 } | |
| 1050 | |
| 1051 /** | |
| 1052 * Fix attachment name encoding if needed/possible | |
| 1053 */ | |
| 1054 protected function fix_attachment_name($name, $part) | |
| 1055 { | |
| 1056 if ($name == rcube_charset::clean($name)) { | |
| 1057 return $name; | |
| 1058 } | |
| 1059 | |
| 1060 // find charset from part or its parent(s) | |
| 1061 if ($part->charset) { | |
| 1062 $charsets[] = $part->charset; | |
| 1063 } | |
| 1064 else { | |
| 1065 // check first part (common case) | |
| 1066 $n = strpos($part->mime_id, '.') ? preg_replace('/\.[0-9]+$/', '', $part->mime_id) . '.1' : 1; | |
| 1067 if (($_part = $this->mime_parts[$n]) && $_part->charset) { | |
| 1068 $charsets[] = $_part->charset; | |
| 1069 } | |
| 1070 | |
| 1071 // check parents' charset | |
| 1072 $items = explode('.', $part->mime_id); | |
| 1073 for ($i = count($items)-1; $i > 0; $i--) { | |
| 1074 $last = array_pop($items); | |
| 1075 $parent = $this->mime_parts[join('.', $items)]; | |
| 1076 | |
| 1077 if ($parent && $parent->charset) { | |
| 1078 $charsets[] = $parent->charset; | |
| 1079 } | |
| 1080 } | |
| 1081 } | |
| 1082 | |
| 1083 if ($this->headers->charset) { | |
| 1084 $charsets[] = $this->headers->charset; | |
| 1085 } | |
| 1086 | |
| 1087 if (empty($charsets)) { | |
| 1088 $rcube = rcube::get_instance(); | |
| 1089 $charsets[] = rcube_charset::detect($name, $rcube->config->get('default_charset', RCUBE_CHARSET)); | |
| 1090 } | |
| 1091 | |
| 1092 foreach (array_unique($charsets) as $charset) { | |
| 1093 $_name = rcube_charset::convert($name, $charset); | |
| 1094 | |
| 1095 if ($_name == rcube_charset::clean($_name)) { | |
| 1096 if (!$part->charset) { | |
| 1097 $part->charset = $charset; | |
| 1098 } | |
| 1099 | |
| 1100 return $_name; | |
| 1101 } | |
| 1102 } | |
| 1103 | |
| 1104 return $name; | |
| 1105 } | |
| 1106 | |
| 1107 /** | |
| 1108 * Deprecated methods (to be removed) | |
| 1109 */ | |
| 1110 | |
| 1111 public static function unfold_flowed($text) | |
| 1112 { | |
| 1113 return rcube_mime::unfold_flowed($text); | |
| 1114 } | |
| 1115 | |
| 1116 public static function format_flowed($text, $length = 72) | |
| 1117 { | |
| 1118 return rcube_mime::format_flowed($text, $length); | |
| 1119 } | |
| 1120 } |
