1<?php 2 3/* 4 * This file is part of SwiftMailer. 5 * (c) 2004-2009 Chris Corbyn 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11//@require 'Swift/Mime/HeaderSet.php'; 12//@require 'Swift/OutputByteStream.php'; 13//@require 'Swift/Mime/ContentEncoder.php'; 14//@require 'Swift/KeyCache.php'; 15 16/** 17 * A MIME entity, in a multipart message. 18 * @package Swift 19 * @subpackage Mime 20 * @author Chris Corbyn 21 */ 22class Swift_Mime_SimpleMimeEntity implements Swift_Mime_MimeEntity 23{ 24 25 /** A collection of Headers for this mime entity */ 26 private $_headers; 27 28 /** The body as a string, or a stream */ 29 private $_body; 30 31 /** The encoder that encodes the body into a streamable format */ 32 private $_encoder; 33 34 /** A mime bounary, if any is used */ 35 private $_boundary; 36 37 /** Mime types to be used based on the nesting level */ 38 private $_compositeRanges = array( 39 'multipart/mixed' => array(self::LEVEL_TOP, self::LEVEL_MIXED), 40 'multipart/alternative' => array(self::LEVEL_MIXED, self::LEVEL_ALTERNATIVE), 41 'multipart/related' => array(self::LEVEL_ALTERNATIVE, self::LEVEL_RELATED) 42 ); 43 44 /** A set of filter rules to define what level an entity should be nested at */ 45 private $_compoundLevelFilters = array(); 46 47 /** The nesting level of this entity */ 48 private $_nestingLevel = self::LEVEL_ALTERNATIVE; 49 50 /** A KeyCache instance used during encoding and streaming */ 51 private $_cache; 52 53 /** Direct descendants of this entity */ 54 private $_immediateChildren = array(); 55 56 /** All descendants of this entity */ 57 private $_children = array(); 58 59 /** The maximum line length of the body of this entity */ 60 private $_maxLineLength = 78; 61 62 /** The order in which alternative mime types should appear */ 63 private $_alternativePartOrder = array( 64 'text/plain' => 1, 65 'text/html' => 2, 66 'multipart/related' => 3 67 ); 68 69 /** The CID of this entity */ 70 private $_id; 71 72 /** The key used for accessing the cache */ 73 private $_cacheKey; 74 75 protected $_userContentType; 76 77 /** 78 * Create a new SimpleMimeEntity with $headers, $encoder and $cache. 79 * @param Swift_Mime_HeaderSet $headers 80 * @param Swift_Mime_ContentEncoder $encoder 81 * @param Swift_KeyCache $cache 82 */ 83 public function __construct(Swift_Mime_HeaderSet $headers, 84 Swift_Mime_ContentEncoder $encoder, Swift_KeyCache $cache) 85 { 86 $this->_cacheKey = uniqid(); 87 $this->_cache = $cache; 88 $this->_headers = $headers; 89 $this->setEncoder($encoder); 90 $this->_headers->defineOrdering( 91 array('Content-Type', 'Content-Transfer-Encoding') 92 ); 93 94 // This array specifies that, when the entire MIME document contains 95 // $compoundLevel, then for each child within $level, if its Content-Type 96 // is $contentType then it should be treated as if it's level is 97 // $neededLevel instead. I tried to write that unambiguously! :-\ 98 // Data Structure: 99 // array ( 100 // $compoundLevel => array( 101 // $level => array( 102 // $contentType => $neededLevel 103 // ) 104 // ) 105 // ) 106 107 $this->_compoundLevelFilters = array( 108 (self::LEVEL_ALTERNATIVE + self::LEVEL_RELATED) => array( 109 self::LEVEL_ALTERNATIVE => array( 110 'text/plain' => self::LEVEL_ALTERNATIVE, 111 'text/html' => self::LEVEL_RELATED 112 ) 113 ) 114 ); 115 $this->generateId(); 116 } 117 118 /** 119 * Generate a new Content-ID or Message-ID for this MIME entity. 120 * @return string 121 */ 122 public function generateId() 123 { 124 $idLeft = time() . '.' . uniqid(); 125 $idRight = !empty($_SERVER['SERVER_NAME']) 126 ? $_SERVER['SERVER_NAME'] 127 : 'swift.generated'; 128 $this->_id = $idLeft . '@' . $idRight; 129 return $this->getId(); 130 } 131 132 /** 133 * Get the {@link Swift_Mime_HeaderSet} for this entity. 134 * @return Swift_Mime_HeaderSet 135 */ 136 public function getHeaders() 137 { 138 return $this->_headers; 139 } 140 141 /** 142 * Get the nesting level of this entity. 143 * @return int 144 * @see LEVEL_TOP, LEVEL_MIXED, LEVEL_RELATED, LEVEL_ALTERNATIVE 145 */ 146 public function getNestingLevel() 147 { 148 return $this->_nestingLevel; 149 } 150 151 /** 152 * Get the Content-type of this entity. 153 * @return string 154 */ 155 public function getContentType() 156 { 157 return $this->_getHeaderFieldModel('Content-Type'); 158 } 159 160 /** 161 * Set the Content-type of this entity. 162 * @param string $type 163 */ 164 public function setContentType($type) 165 { 166 $this->_setContentTypeInHeaders($type); 167 // Keep track of the value so that if the content-type changes automatically 168 // due to added child entities, it can be restored if they are later removed 169 $this->_userContentType = $type; 170 return $this; 171 } 172 173 /** 174 * Get the CID of this entity. 175 * The CID will only be present in headers if a Content-ID header is present. 176 * @return string 177 */ 178 public function getId() 179 { 180 return $this->_headers->has($this->_getIdField()) 181 ? current((array) $this->_getHeaderFieldModel($this->_getIdField())) 182 : $this->_id; 183 } 184 185 /** 186 * Set the CID of this entity. 187 * @param string $id 188 */ 189 public function setId($id) 190 { 191 if (!$this->_setHeaderFieldModel($this->_getIdField(), $id)) 192 { 193 $this->_headers->addIdHeader($this->_getIdField(), $id); 194 } 195 $this->_id = $id; 196 return $this; 197 } 198 199 /** 200 * Get the description of this entity. 201 * This value comes from the Content-Description header if set. 202 * @return string 203 */ 204 public function getDescription() 205 { 206 return $this->_getHeaderFieldModel('Content-Description'); 207 } 208 209 /** 210 * Set the description of this entity. 211 * This method sets a value in the Content-ID header. 212 * @param string $description 213 */ 214 public function setDescription($description) 215 { 216 if (!$this->_setHeaderFieldModel('Content-Description', $description)) 217 { 218 $this->_headers->addTextHeader('Content-Description', $description); 219 } 220 return $this; 221 } 222 223 /** 224 * Get the maximum line length of the body of this entity. 225 * @return int 226 */ 227 public function getMaxLineLength() 228 { 229 return $this->_maxLineLength; 230 } 231 232 /** 233 * Set the maximum line length of lines in this body. 234 * Though not enforced by the library, lines should not exceed 1000 chars. 235 * @param int $length 236 */ 237 public function setMaxLineLength($length) 238 { 239 $this->_maxLineLength = $length; 240 return $this; 241 } 242 243 /** 244 * Get all children added to this entity. 245 * @return array of Swift_Mime_Entity 246 */ 247 public function getChildren() 248 { 249 return $this->_children; 250 } 251 252 /** 253 * Set all children of this entity. 254 * @param array $children Swiift_Mime_Entity instances 255 * @param int $compoundLevel For internal use only 256 */ 257 public function setChildren(array $children, $compoundLevel = null) 258 { 259 //TODO: Try to refactor this logic 260 261 $compoundLevel = isset($compoundLevel) 262 ? $compoundLevel 263 : $this->_getCompoundLevel($children) 264 ; 265 266 $immediateChildren = array(); 267 $grandchildren = array(); 268 $newContentType = $this->_userContentType; 269 270 foreach ($children as $child) 271 { 272 $level = $this->_getNeededChildLevel($child, $compoundLevel); 273 if (empty($immediateChildren)) //first iteration 274 { 275 $immediateChildren = array($child); 276 } 277 else 278 { 279 $nextLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel); 280 if ($nextLevel == $level) 281 { 282 $immediateChildren[] = $child; 283 } 284 elseif ($level < $nextLevel) 285 { 286 //Re-assign immediateChildren to grandchilden 287 $grandchildren = array_merge($grandchildren, $immediateChildren); 288 //Set new children 289 $immediateChildren = array($child); 290 } 291 else 292 { 293 $grandchildren[] = $child; 294 } 295 } 296 } 297 298 if (!empty($immediateChildren)) 299 { 300 $lowestLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel); 301 302 //Determine which composite media type is needed to accomodate the 303 // immediate children 304 foreach ($this->_compositeRanges as $mediaType => $range) 305 { 306 if ($lowestLevel > $range[0] 307 && $lowestLevel <= $range[1]) 308 { 309 $newContentType = $mediaType; 310 break; 311 } 312 } 313 314 //Put any grandchildren in a subpart 315 if (!empty($grandchildren)) 316 { 317 $subentity = $this->_createChild(); 318 $subentity->_setNestingLevel($lowestLevel); 319 $subentity->setChildren($grandchildren, $compoundLevel); 320 array_unshift($immediateChildren, $subentity); 321 } 322 } 323 324 $this->_immediateChildren = $immediateChildren; 325 $this->_children = $children; 326 $this->_setContentTypeInHeaders($newContentType); 327 $this->_fixHeaders(); 328 $this->_sortChildren(); 329 330 return $this; 331 } 332 333 /** 334 * Get the body of this entity as a string. 335 * @return string 336 */ 337 public function getBody() 338 { 339 return ($this->_body instanceof Swift_OutputByteStream) 340 ? $this->_readStream($this->_body) 341 : $this->_body; 342 } 343 344 /** 345 * Set the body of this entity, either as a string, or as an instance of 346 * {@link Swift_OutputByteStream}. 347 * @param mixed $body 348 * @param string $contentType optional 349 */ 350 public function setBody($body, $contentType = null) 351 { 352 if ($body !== $this->_body) 353 { 354 $this->_clearCache(); 355 } 356 357 $this->_body = $body; 358 if (isset($contentType)) 359 { 360 $this->setContentType($contentType); 361 } 362 return $this; 363 } 364 365 /** 366 * Get the encoder used for the body of this entity. 367 * @return Swift_Mime_ContentEncoder 368 */ 369 public function getEncoder() 370 { 371 return $this->_encoder; 372 } 373 374 /** 375 * Set the encoder used for the body of this entity. 376 * @param Swift_Mime_ContentEncoder $encoder 377 */ 378 public function setEncoder(Swift_Mime_ContentEncoder $encoder) 379 { 380 if ($encoder !== $this->_encoder) 381 { 382 $this->_clearCache(); 383 } 384 385 $this->_encoder = $encoder; 386 $this->_setEncoding($encoder->getName()); 387 $this->_notifyEncoderChanged($encoder); 388 return $this; 389 } 390 391 /** 392 * Get the boundary used to separate children in this entity. 393 * @return string 394 */ 395 public function getBoundary() 396 { 397 if (!isset($this->_boundary)) 398 { 399 $this->_boundary = '_=_swift_v4_' . time() . uniqid() . '_=_'; 400 } 401 return $this->_boundary; 402 } 403 404 /** 405 * Set the boundary used to separate children in this entity. 406 * @param string $boundary 407 * @throws Swift_RfcComplianceException 408 */ 409 public function setBoundary($boundary) 410 { 411 $this->_assertValidBoundary($boundary); 412 $this->_boundary = $boundary; 413 return $this; 414 } 415 416 /** 417 * Receive notification that the charset of this entity, or a parent entity 418 * has changed. 419 * @param string $charset 420 */ 421 public function charsetChanged($charset) 422 { 423 $this->_notifyCharsetChanged($charset); 424 } 425 426 /** 427 * Receive notification that the encoder of this entity or a parent entity 428 * has changed. 429 * @param Swift_Mime_ContentEncoder $encoder 430 */ 431 public function encoderChanged(Swift_Mime_ContentEncoder $encoder) 432 { 433 $this->_notifyEncoderChanged($encoder); 434 } 435 436 /** 437 * Get this entire entity as a string. 438 * @return string 439 */ 440 public function toString() 441 { 442 $string = $this->_headers->toString(); 443 if (isset($this->_body) && empty($this->_immediateChildren)) 444 { 445 if ($this->_cache->hasKey($this->_cacheKey, 'body')) 446 { 447 $body = $this->_cache->getString($this->_cacheKey, 'body'); 448 } 449 else 450 { 451 $body = "\r\n" . $this->_encoder->encodeString($this->getBody(), 0, 452 $this->getMaxLineLength() 453 ); 454 $this->_cache->setString($this->_cacheKey, 'body', $body, 455 Swift_KeyCache::MODE_WRITE 456 ); 457 } 458 $string .= $body; 459 } 460 461 if (!empty($this->_immediateChildren)) 462 { 463 foreach ($this->_immediateChildren as $child) 464 { 465 $string .= "\r\n\r\n--" . $this->getBoundary() . "\r\n"; 466 $string .= $child->toString(); 467 } 468 $string .= "\r\n\r\n--" . $this->getBoundary() . "--\r\n"; 469 } 470 471 return $string; 472 } 473 474 /** 475 * Returns a string representation of this object. 476 * 477 * @return string 478 * 479 * @see toString() 480 */ 481 public function __toString() 482 { 483 return $this->toString(); 484 } 485 486 /** 487 * Write this entire entity to a {@link Swift_InputByteStream}. 488 * @param Swift_InputByteStream 489 */ 490 public function toByteStream(Swift_InputByteStream $is) 491 { 492 $is->write($this->_headers->toString()); 493 $is->commit(); 494 495 if (empty($this->_immediateChildren)) 496 { 497 if (isset($this->_body)) 498 { 499 if ($this->_cache->hasKey($this->_cacheKey, 'body')) 500 { 501 $this->_cache->exportToByteStream($this->_cacheKey, 'body', $is); 502 } 503 else 504 { 505 $cacheIs = $this->_cache->getInputByteStream($this->_cacheKey, 'body'); 506 if ($cacheIs) 507 { 508 $is->bind($cacheIs); 509 } 510 511 $is->write("\r\n"); 512 513 if ($this->_body instanceof Swift_OutputByteStream) 514 { 515 $this->_body->setReadPointer(0); 516 517 $this->_encoder->encodeByteStream($this->_body, $is, 0, 518 $this->getMaxLineLength() 519 ); 520 } 521 else 522 { 523 $is->write($this->_encoder->encodeString( 524 $this->getBody(), 0, $this->getMaxLineLength() 525 )); 526 } 527 528 if ($cacheIs) 529 { 530 $is->unbind($cacheIs); 531 } 532 } 533 } 534 } 535 536 if (!empty($this->_immediateChildren)) 537 { 538 foreach ($this->_immediateChildren as $child) 539 { 540 $is->write("\r\n\r\n--" . $this->getBoundary() . "\r\n"); 541 $child->toByteStream($is); 542 } 543 $is->write("\r\n\r\n--" . $this->getBoundary() . "--\r\n"); 544 } 545 } 546 547 // -- Protected methods 548 549 /** 550 * Get the name of the header that provides the ID of this entity */ 551 protected function _getIdField() 552 { 553 return 'Content-ID'; 554 } 555 556 /** 557 * Get the model data (usually an array or a string) for $field. 558 */ 559 protected function _getHeaderFieldModel($field) 560 { 561 if ($this->_headers->has($field)) 562 { 563 return $this->_headers->get($field)->getFieldBodyModel(); 564 } 565 } 566 567 /** 568 * Set the model data for $field. 569 */ 570 protected function _setHeaderFieldModel($field, $model) 571 { 572 if ($this->_headers->has($field)) 573 { 574 $this->_headers->get($field)->setFieldBodyModel($model); 575 return true; 576 } 577 else 578 { 579 return false; 580 } 581 } 582 583 /** 584 * Get the parameter value of $parameter on $field header. 585 */ 586 protected function _getHeaderParameter($field, $parameter) 587 { 588 if ($this->_headers->has($field)) 589 { 590 return $this->_headers->get($field)->getParameter($parameter); 591 } 592 } 593 594 /** 595 * Set the parameter value of $parameter on $field header. 596 */ 597 protected function _setHeaderParameter($field, $parameter, $value) 598 { 599 if ($this->_headers->has($field)) 600 { 601 $this->_headers->get($field)->setParameter($parameter, $value); 602 return true; 603 } 604 else 605 { 606 return false; 607 } 608 } 609 610 /** 611 * Re-evaluate what content type and encoding should be used on this entity. 612 */ 613 protected function _fixHeaders() 614 { 615 if (count($this->_immediateChildren)) 616 { 617 $this->_setHeaderParameter('Content-Type', 'boundary', 618 $this->getBoundary() 619 ); 620 $this->_headers->remove('Content-Transfer-Encoding'); 621 } 622 else 623 { 624 $this->_setHeaderParameter('Content-Type', 'boundary', null); 625 $this->_setEncoding($this->_encoder->getName()); 626 } 627 } 628 629 /** 630 * Get the KeyCache used in this entity. 631 */ 632 protected function _getCache() 633 { 634 return $this->_cache; 635 } 636 637 /** 638 * Empty the KeyCache for this entity. 639 */ 640 protected function _clearCache() 641 { 642 $this->_cache->clearKey($this->_cacheKey, 'body'); 643 } 644 645 // -- Private methods 646 647 private function _readStream(Swift_OutputByteStream $os) 648 { 649 $string = ''; 650 while (false !== $bytes = $os->read(8192)) 651 { 652 $string .= $bytes; 653 } 654 return $string; 655 } 656 657 private function _setEncoding($encoding) 658 { 659 if (!$this->_setHeaderFieldModel('Content-Transfer-Encoding', $encoding)) 660 { 661 $this->_headers->addTextHeader('Content-Transfer-Encoding', $encoding); 662 } 663 } 664 665 private function _assertValidBoundary($boundary) 666 { 667 if (!preg_match( 668 '/^[a-z0-9\'\(\)\+_\-,\.\/:=\?\ ]{0,69}[a-z0-9\'\(\)\+_\-,\.\/:=\?]$/Di', 669 $boundary)) 670 { 671 throw new Exception('Mime boundary set is not RFC 2046 compliant.'); 672 } 673 } 674 675 private function _setContentTypeInHeaders($type) 676 { 677 if (!$this->_setHeaderFieldModel('Content-Type', $type)) 678 { 679 $this->_headers->addParameterizedHeader('Content-Type', $type); 680 } 681 } 682 683 private function _setNestingLevel($level) 684 { 685 $this->_nestingLevel = $level; 686 } 687 688 private function _getCompoundLevel($children) 689 { 690 $level = 0; 691 foreach ($children as $child) 692 { 693 $level |= $child->getNestingLevel(); 694 } 695 return $level; 696 } 697 698 private function _getNeededChildLevel($child, $compoundLevel) 699 { 700 $filter = array(); 701 foreach ($this->_compoundLevelFilters as $bitmask => $rules) 702 { 703 if (($compoundLevel & $bitmask) === $bitmask) 704 { 705 $filter = $rules + $filter; 706 } 707 } 708 709 $realLevel = $child->getNestingLevel(); 710 $lowercaseType = strtolower($child->getContentType()); 711 712 if (isset($filter[$realLevel]) 713 && isset($filter[$realLevel][$lowercaseType])) 714 { 715 return $filter[$realLevel][$lowercaseType]; 716 } 717 else 718 { 719 return $realLevel; 720 } 721 } 722 723 private function _createChild() 724 { 725 return new self($this->_headers->newInstance(), 726 $this->_encoder, $this->_cache); 727 } 728 729 private function _notifyEncoderChanged(Swift_Mime_ContentEncoder $encoder) 730 { 731 foreach ($this->_immediateChildren as $child) 732 { 733 $child->encoderChanged($encoder); 734 } 735 } 736 737 private function _notifyCharsetChanged($charset) 738 { 739 $this->_encoder->charsetChanged($charset); 740 $this->_headers->charsetChanged($charset); 741 foreach ($this->_immediateChildren as $child) 742 { 743 $child->charsetChanged($charset); 744 } 745 } 746 747 private function _sortChildren() 748 { 749 $shouldSort = false; 750 foreach ($this->_immediateChildren as $child) 751 { 752 //NOTE: This include alternative parts moved into a related part 753 if ($child->getNestingLevel() == self::LEVEL_ALTERNATIVE) 754 { 755 $shouldSort = true; 756 break; 757 } 758 } 759 760 //Sort in order of preference, if there is one 761 if ($shouldSort) 762 { 763 usort($this->_immediateChildren, array($this, '_childSortAlgorithm')); 764 } 765 } 766 767 private function _childSortAlgorithm($a, $b) 768 { 769 $typePrefs = array(); 770 $types = array( 771 strtolower($a->getContentType()), 772 strtolower($b->getContentType()) 773 ); 774 foreach ($types as $type) 775 { 776 $typePrefs[] = (array_key_exists($type, $this->_alternativePartOrder)) 777 ? $this->_alternativePartOrder[$type] 778 : (max($this->_alternativePartOrder) + 1); 779 } 780 return ($typePrefs[0] >= $typePrefs[1]) ? 1 : -1; 781 } 782 783 // -- Destructor 784 785 /** 786 * Empties it's own contents from the cache. 787 */ 788 public function __destruct() 789 { 790 $this->_cache->clearAll($this->_cacheKey); 791 } 792 793} 794