Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403]
1 <?php 2 /** 3 * Copyright 1999-2017 Horde LLC (http://www.horde.org/) 4 * 5 * See the enclosed file LICENSE for license information (LGPL). If you 6 * did not receive this file, see http://www.horde.org/licenses/lgpl21. 7 * 8 * @category Horde 9 * @copyright 1999-2017 Horde LLC 10 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 11 * @package Mime 12 */ 13 14 /** 15 * Object-oriented representation of a MIME part (RFC 2045-2049). 16 * 17 * @author Chuck Hagenbuch <chuck@horde.org> 18 * @author Michael Slusarz <slusarz@horde.org> 19 * @category Horde 20 * @copyright 1999-2017 Horde LLC 21 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 22 * @package Mime 23 */ 24 class Horde_Mime_Part 25 implements ArrayAccess, Countable, RecursiveIterator, Serializable 26 { 27 /* Serialized version. */ 28 const VERSION = 2; 29 30 /* The character(s) used internally for EOLs. */ 31 const EOL = "\n"; 32 33 /* The character string designated by RFC 2045 to designate EOLs in MIME 34 * messages. */ 35 const RFC_EOL = "\r\n"; 36 37 /* The default encoding. */ 38 const DEFAULT_ENCODING = 'binary'; 39 40 /* Constants indicating the valid transfer encoding allowed. */ 41 const ENCODE_7BIT = 1; 42 const ENCODE_8BIT = 2; 43 const ENCODE_BINARY = 4; 44 45 /* MIME nesting limit. */ 46 const NESTING_LIMIT = 100; 47 48 /* Status mask value: Need to reindex the current part. */ 49 const STATUS_REINDEX = 1; 50 /* Status mask value: This is the base MIME part. */ 51 const STATUS_BASEPART = 2; 52 53 /** 54 * The default charset to use when parsing text parts with no charset 55 * information. 56 * 57 * @todo Make this a non-static property or pass as parameter to static 58 * methods in Horde 6. 59 * 60 * @var string 61 */ 62 public static $defaultCharset = 'us-ascii'; 63 64 /** 65 * The memory limit for use with the PHP temp stream. 66 * 67 * @var integer 68 */ 69 public static $memoryLimit = 2097152; 70 71 /** 72 * Parent object. Value only accurate when iterating. 73 * 74 * @since 2.8.0 75 * 76 * @var Horde_Mime_Part 77 */ 78 public $parent = null; 79 80 /** 81 * Default value for this Part's size. 82 * 83 * @var integer 84 */ 85 protected $_bytes; 86 87 /** 88 * The body of the part. Always stored in binary format. 89 * 90 * @var resource 91 */ 92 protected $_contents; 93 94 /** 95 * The sequence to use as EOL for this part. 96 * 97 * The default is currently to output the EOL sequence internally as 98 * just "\n" instead of the canonical "\r\n" required in RFC 822 & 2045. 99 * To be RFC complaint, the full <CR><LF> EOL combination should be used 100 * when sending a message. 101 * 102 * @var string 103 */ 104 protected $_eol = self::EOL; 105 106 /** 107 * The MIME headers for this part. 108 * 109 * @var Horde_Mime_Headers 110 */ 111 protected $_headers; 112 113 /** 114 * The charset to output the headers in. 115 * 116 * @var string 117 */ 118 protected $_hdrCharset = null; 119 120 /** 121 * Metadata. 122 * 123 * @var array 124 */ 125 protected $_metadata = array(); 126 127 /** 128 * The MIME ID of this part. 129 * 130 * @var string 131 */ 132 protected $_mimeid = null; 133 134 /** 135 * The subparts of this part. 136 * 137 * @var array 138 */ 139 protected $_parts = array(); 140 141 /** 142 * Status mask for this part. 143 * 144 * @var integer 145 */ 146 protected $_status = 0; 147 148 /** 149 * Temporary array. 150 * 151 * @var array 152 */ 153 protected $_temp = array(); 154 155 /** 156 * The desired transfer encoding of this part. 157 * 158 * @var string 159 */ 160 protected $_transferEncoding = self::DEFAULT_ENCODING; 161 162 /** 163 * Flag to detect if a message failed to send at least once. 164 * 165 * @var boolean 166 */ 167 protected $_failed = false; 168 169 /** 170 * Constructor. 171 */ 172 public function __construct() 173 { 174 $this->_headers = new Horde_Mime_Headers(); 175 176 /* Mandatory MIME headers. */ 177 $this->_headers->addHeaderOb( 178 new Horde_Mime_Headers_ContentParam_ContentDisposition(null, '') 179 ); 180 181 $ct = Horde_Mime_Headers_ContentParam_ContentType::create(); 182 $ct['charset'] = self::$defaultCharset; 183 $this->_headers->addHeaderOb($ct); 184 } 185 186 /** 187 * Function to run on clone. 188 */ 189 public function __clone() 190 { 191 foreach ($this->_parts as $k => $v) { 192 $this->_parts[$k] = clone $v; 193 } 194 195 $this->_headers = clone $this->_headers; 196 197 if (!empty($this->_contents)) { 198 $this->_contents = $this->_writeStream($this->_contents); 199 } 200 } 201 202 /** 203 * Set the content-disposition of this part. 204 * 205 * @param string $disposition The content-disposition to set ('inline', 206 * 'attachment', or an empty value). 207 */ 208 public function setDisposition($disposition = null) 209 { 210 $this->_headers['content-disposition']->setContentParamValue( 211 strval($disposition) 212 ); 213 } 214 215 /** 216 * Get the content-disposition of this part. 217 * 218 * @return string The part's content-disposition. An empty string means 219 * no desired disposition has been set for this part. 220 */ 221 public function getDisposition() 222 { 223 return $this->_headers['content-disposition']->value; 224 } 225 226 /** 227 * Add a disposition parameter to this part. 228 * 229 * @param string $label The disposition parameter label. 230 * @param string $data The disposition parameter data. If null, removes 231 * the parameter (@since 2.8.0). 232 */ 233 public function setDispositionParameter($label, $data) 234 { 235 $cd = $this->_headers['content-disposition']; 236 237 if (is_null($data)) { 238 unset($cd[$label]); 239 } elseif (strlen($data)) { 240 $cd[$label] = $data; 241 242 if (strcasecmp($label, 'size') === 0) { 243 // RFC 2183 [2.7] - size parameter 244 $this->_bytes = $cd[$label]; 245 } elseif ((strcasecmp($label, 'filename') === 0) && 246 !strlen($cd->value)) { 247 /* Set part to attachment if not already explicitly set to 248 * 'inline'. */ 249 $cd->setContentParamValue('attachment'); 250 } 251 } 252 } 253 254 /** 255 * Get a disposition parameter from this part. 256 * 257 * @param string $label The disposition parameter label. 258 * 259 * @return string The data requested. 260 * Returns null if $label is not set. 261 */ 262 public function getDispositionParameter($label) 263 { 264 $cd = $this->_headers['content-disposition']; 265 return $cd[$label]; 266 } 267 268 /** 269 * Get all parameters from the Content-Disposition header. 270 * 271 * @return array An array of all the parameters 272 * Returns the empty array if no parameters set. 273 */ 274 public function getAllDispositionParameters() 275 { 276 return $this->_headers['content-disposition']->params; 277 } 278 279 /** 280 * Set the name of this part. 281 * 282 * @param string $name The name to set. 283 */ 284 public function setName($name) 285 { 286 $this->setDispositionParameter('filename', $name); 287 $this->setContentTypeParameter('name', $name); 288 } 289 290 /** 291 * Get the name of this part. 292 * 293 * @param boolean $default If the name parameter doesn't exist, should we 294 * use the default name from the description 295 * parameter? 296 * 297 * @return string The name of the part. 298 */ 299 public function getName($default = false) 300 { 301 if (!($name = $this->getDispositionParameter('filename')) && 302 !($name = $this->getContentTypeParameter('name')) && 303 $default) { 304 $name = preg_replace('|\W|', '_', $this->getDescription(false)); 305 } 306 307 return $name; 308 } 309 310 /** 311 * Set the body contents of this part. 312 * 313 * @param mixed $contents The part body. Either a string or a stream 314 * resource, or an array containing both. 315 * @param array $options Additional options: 316 * - encoding: (string) The encoding of $contents. 317 * DEFAULT: Current transfer encoding value. 318 * - usestream: (boolean) If $contents is a stream, should we directly 319 * use that stream? 320 * DEFAULT: $contents copied to a new stream. 321 */ 322 public function setContents($contents, $options = array()) 323 { 324 if (is_resource($contents) && ($contents === $this->_contents)) { 325 return; 326 } 327 328 if (empty($options['encoding'])) { 329 $options['encoding'] = $this->_transferEncoding; 330 } 331 332 $fp = (empty($options['usestream']) || !is_resource($contents)) 333 ? $this->_writeStream($contents) 334 : $contents; 335 336 /* Properly close the existing stream. */ 337 $this->clearContents(); 338 339 $this->setTransferEncoding($options['encoding']); 340 $this->_contents = $this->_transferDecode($fp, $options['encoding']); 341 } 342 343 /** 344 * Add to the body contents of this part. 345 * 346 * @param mixed $contents The part body. Either a string or a stream 347 * resource, or an array containing both. 348 * - encoding: (string) The encoding of $contents. 349 * DEFAULT: Current transfer encoding value. 350 * - usestream: (boolean) If $contents is a stream, should we directly 351 * use that stream? 352 * DEFAULT: $contents copied to a new stream. 353 */ 354 public function appendContents($contents, $options = array()) 355 { 356 if (empty($this->_contents)) { 357 $this->setContents($contents, $options); 358 } else { 359 $fp = (empty($options['usestream']) || !is_resource($contents)) 360 ? $this->_writeStream($contents) 361 : $contents; 362 363 $this->_writeStream((empty($options['encoding']) || ($options['encoding'] == $this->_transferEncoding)) ? $fp : $this->_transferDecode($fp, $options['encoding']), array('fp' => $this->_contents)); 364 unset($this->_temp['sendTransferEncoding']); 365 } 366 } 367 368 /** 369 * Clears the body contents of this part. 370 */ 371 public function clearContents() 372 { 373 if (!empty($this->_contents)) { 374 fclose($this->_contents); 375 $this->_contents = null; 376 unset($this->_temp['sendTransferEncoding']); 377 } 378 } 379 380 /** 381 * Return the body of the part. 382 * 383 * @param array $options Additional options: 384 * - canonical: (boolean) Returns the contents in strict RFC 822 & 385 * 2045 output - namely, all newlines end with the 386 * canonical <CR><LF> sequence. 387 * DEFAULT: No 388 * - stream: (boolean) Return the body as a stream resource. 389 * DEFAULT: No 390 * 391 * @return mixed The body text (string) of the part, null if there is no 392 * contents, and a stream resource if 'stream' is true. 393 */ 394 public function getContents($options = array()) 395 { 396 return empty($options['canonical']) 397 ? (empty($options['stream']) ? $this->_readStream($this->_contents) : $this->_contents) 398 : $this->replaceEOL($this->_contents, self::RFC_EOL, !empty($options['stream'])); 399 } 400 401 /** 402 * Decodes the contents of the part to binary encoding. 403 * 404 * @param resource $fp A stream containing the data to decode. 405 * @param string $encoding The original file encoding. 406 * 407 * @return resource A new file resource with the decoded data. 408 */ 409 protected function _transferDecode($fp, $encoding) 410 { 411 /* If the contents are empty, return now. */ 412 fseek($fp, 0, SEEK_END); 413 if (ftell($fp)) { 414 switch ($encoding) { 415 case 'base64': 416 try { 417 return $this->_writeStream($fp, array( 418 'error' => true, 419 'filter' => array( 420 'convert.base64-decode' => array() 421 ) 422 )); 423 } catch (ErrorException $e) {} 424 425 rewind($fp); 426 return $this->_writeStream(base64_decode(stream_get_contents($fp))); 427 428 case 'quoted-printable': 429 try { 430 return $this->_writeStream($fp, array( 431 'error' => true, 432 'filter' => array( 433 'convert.quoted-printable-decode' => array() 434 ) 435 )); 436 } catch (ErrorException $e) {} 437 438 // Workaround for Horde Bug #8747 439 rewind($fp); 440 return $this->_writeStream(quoted_printable_decode(stream_get_contents($fp))); 441 442 case 'uuencode': 443 case 'x-uuencode': 444 case 'x-uue': 445 /* Support for uuencoded encoding - although not required by 446 * RFCs, some mailers may still encode this way. */ 447 $res = Horde_Mime::uudecode($this->_readStream($fp)); 448 return $this->_writeStream($res[0]['data']); 449 } 450 } 451 452 return $fp; 453 } 454 455 /** 456 * Encodes the contents of the part as necessary for transport. 457 * 458 * @param resource $fp A stream containing the data to encode. 459 * @param string $encoding The encoding to use. 460 * 461 * @return resource A new file resource with the encoded data. 462 */ 463 protected function _transferEncode($fp, $encoding) 464 { 465 $this->_temp['transferEncodeClose'] = true; 466 467 switch ($encoding) { 468 case 'base64': 469 /* Base64 Encoding: See RFC 2045, section 6.8 */ 470 return $this->_writeStream($fp, array( 471 'filter' => array( 472 'convert.base64-encode' => array( 473 'line-break-chars' => $this->getEOL(), 474 'line-length' => 76 475 ) 476 ) 477 )); 478 479 case 'quoted-printable': 480 // PHP Bug 65776 - Must normalize the EOL characters. 481 stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol'); 482 $stream = new Horde_Stream_Existing(array( 483 'stream' => $fp 484 )); 485 $stream->stream = $this->_writeStream($stream->stream, array( 486 'filter' => array( 487 'horde_eol' => array('eol' => $stream->getEOL() 488 ) 489 ))); 490 491 /* Quoted-Printable Encoding: See RFC 2045, section 6.7 */ 492 return $this->_writeStream($fp, array( 493 'filter' => array( 494 'convert.quoted-printable-encode' => array_filter(array( 495 'line-break-chars' => $stream->getEOL(), 496 'line-length' => 76 497 )) 498 ) 499 )); 500 501 default: 502 $this->_temp['transferEncodeClose'] = false; 503 return $fp; 504 } 505 } 506 507 /** 508 * Set the MIME type of this part. 509 * 510 * @param string $type The MIME type to set (ex.: text/plain). 511 */ 512 public function setType($type) 513 { 514 /* RFC 2045: Any entity with unrecognized encoding must be treated 515 * as if it has a Content-Type of "application/octet-stream" 516 * regardless of what the Content-Type field actually says. */ 517 if (!is_null($this->_transferEncoding)) { 518 $this->_headers['content-type']->setContentParamValue($type); 519 } 520 } 521 522 /** 523 * Get the full MIME Content-Type of this part. 524 * 525 * @param boolean $charset Append character set information to the end 526 * of the content type if this is a text/* part? 527 *` 528 * @return string The MIME type of this part. 529 */ 530 public function getType($charset = false) 531 { 532 $ct = $this->_headers['content-type']; 533 534 return $charset 535 ? $ct->type_charset 536 : $ct->value; 537 } 538 539 /** 540 * If the subtype of a MIME part is unrecognized by an application, the 541 * default type should be used instead (See RFC 2046). This method 542 * returns the default subtype for a particular primary MIME type. 543 * 544 * @return string The default MIME type of this part (ex.: text/plain). 545 */ 546 public function getDefaultType() 547 { 548 switch ($this->getPrimaryType()) { 549 case 'text': 550 /* RFC 2046 (4.1.4): text parts default to text/plain. */ 551 return 'text/plain'; 552 553 case 'multipart': 554 /* RFC 2046 (5.1.3): multipart parts default to multipart/mixed. */ 555 return 'multipart/mixed'; 556 557 default: 558 /* RFC 2046 (4.2, 4.3, 4.4, 4.5.3, 5.2.4): all others default to 559 application/octet-stream. */ 560 return 'application/octet-stream'; 561 } 562 } 563 564 /** 565 * Get the primary type of this part. 566 * 567 * @return string The primary MIME type of this part. 568 */ 569 public function getPrimaryType() 570 { 571 return $this->_headers['content-type']->ptype; 572 } 573 574 /** 575 * Get the subtype of this part. 576 * 577 * @return string The MIME subtype of this part. 578 */ 579 public function getSubType() 580 { 581 return $this->_headers['content-type']->stype; 582 } 583 584 /** 585 * Set the character set of this part. 586 * 587 * @param string $charset The character set of this part. 588 */ 589 public function setCharset($charset) 590 { 591 $this->setContentTypeParameter('charset', $charset); 592 } 593 594 /** 595 * Get the character set to use for this part. 596 * 597 * @return string The character set of this part (lowercase). Returns 598 * null if there is no character set. 599 */ 600 public function getCharset() 601 { 602 return $this->getContentTypeParameter('charset') 603 ?: (($this->getPrimaryType() === 'text') ? 'us-ascii' : null); 604 } 605 606 /** 607 * Set the character set to use when outputting MIME headers. 608 * 609 * @param string $charset The character set. 610 */ 611 public function setHeaderCharset($charset) 612 { 613 $this->_hdrCharset = $charset; 614 } 615 616 /** 617 * Get the character set to use when outputting MIME headers. 618 * 619 * @return string The character set. If no preferred character set has 620 * been set, returns null. 621 */ 622 public function getHeaderCharset() 623 { 624 return is_null($this->_hdrCharset) 625 ? $this->getCharset() 626 : $this->_hdrCharset; 627 } 628 629 /** 630 * Set the language(s) of this part. 631 * 632 * @param mixed $lang A language string, or an array of language 633 * strings. 634 */ 635 public function setLanguage($lang) 636 { 637 $this->_headers->addHeaderOb( 638 new Horde_Mime_Headers_ContentLanguage('', $lang) 639 ); 640 } 641 642 /** 643 * Get the language(s) of this part. 644 * 645 * @param array The list of languages. 646 */ 647 public function getLanguage() 648 { 649 return $this->_headers['content-language']->langs; 650 } 651 652 /** 653 * Set the content duration of the data contained in this part (see RFC 654 * 3803). 655 * 656 * @param integer $duration The duration of the data, in seconds. If 657 * null, clears the duration information. 658 */ 659 public function setDuration($duration) 660 { 661 if (is_null($duration)) { 662 unset($this->_headers['content-duration']); 663 } else { 664 if (!($hdr = $this->_headers['content-duration'])) { 665 $hdr = new Horde_Mime_Headers_Element_Single( 666 'Content-Duration', 667 '' 668 ); 669 $this->_headers->addHeaderOb($hdr); 670 } 671 $hdr->setValue($duration); 672 } 673 } 674 675 /** 676 * Get the content duration of the data contained in this part (see RFC 677 * 3803). 678 * 679 * @return integer The duration of the data, in seconds. Returns null if 680 * there is no duration information. 681 */ 682 public function getDuration() 683 { 684 return ($hdr = $this->_headers['content-duration']) 685 ? intval($hdr->value) 686 : null; 687 } 688 689 /** 690 * Set the description of this part. 691 * 692 * @param string $description The description of this part. If null, 693 * deletes the description (@since 2.8.0). 694 */ 695 public function setDescription($description) 696 { 697 if (is_null($description)) { 698 unset($this->_headers['content-description']); 699 } else { 700 if (!($hdr = $this->_headers['content-description'])) { 701 $hdr = new Horde_Mime_Headers_ContentDescription(null, ''); 702 $this->_headers->addHeaderOb($hdr); 703 } 704 $hdr->setValue($description); 705 } 706 } 707 708 /** 709 * Get the description of this part. 710 * 711 * @param boolean $default If the description parameter doesn't exist, 712 * should we use the name of the part? 713 * 714 * @return string The description of this part. 715 */ 716 public function getDescription($default = false) 717 { 718 if (($ob = $this->_headers['content-description']) && 719 strlen($ob->value)) { 720 return $ob->value; 721 } 722 723 return $default 724 ? $this->getName() 725 : ''; 726 } 727 728 /** 729 * Set the transfer encoding to use for this part. 730 * 731 * Only needed in the following circumstances: 732 * 1.) Indicate what the transfer encoding is if the data has not yet been 733 * set in the object (can only be set if there presently are not 734 * any contents). 735 * 2.) Force the encoding to a certain type on a toString() call (if 736 * 'send' is true). 737 * 738 * @param string $encoding The transfer encoding to use. 739 * @param array $options Additional options: 740 * - send: (boolean) If true, use $encoding as the sending encoding. 741 * DEFAULT: $encoding is used to change the base encoding. 742 */ 743 public function setTransferEncoding($encoding, $options = array()) 744 { 745 if (empty($encoding) || 746 (empty($options['send']) && !empty($this->_contents))) { 747 return; 748 } 749 750 switch ($encoding = Horde_String::lower($encoding)) { 751 case '7bit': 752 case '8bit': 753 case 'base64': 754 case 'binary': 755 case 'quoted-printable': 756 // Non-RFC types, but old mailers may still use 757 case 'uuencode': 758 case 'x-uuencode': 759 case 'x-uue': 760 if (empty($options['send'])) { 761 $this->_transferEncoding = $encoding; 762 } else { 763 $this->_temp['sendEncoding'] = $encoding; 764 } 765 break; 766 767 default: 768 if (empty($options['send'])) { 769 /* RFC 2045: Any entity with unrecognized encoding must be 770 * treated as if it has a Content-Type of 771 * "application/octet-stream" regardless of what the 772 * Content-Type field actually says. */ 773 $this->setType('application/octet-stream'); 774 $this->_transferEncoding = null; 775 } 776 break; 777 } 778 } 779 780 /** 781 * Get a list of all MIME subparts. 782 * 783 * @return array An array of the Horde_Mime_Part subparts. 784 */ 785 public function getParts() 786 { 787 return $this->_parts; 788 } 789 790 /** 791 * Add/remove a content type parameter to this part. 792 * 793 * @param string $label The content-type parameter label. 794 * @param string $data The content-type parameter data. If null, removes 795 * the parameter (@since 2.8.0). 796 */ 797 public function setContentTypeParameter($label, $data) 798 { 799 $ct = $this->_headers['content-type']; 800 801 if (is_null($data)) { 802 unset($ct[$label]); 803 } elseif (strlen($data)) { 804 $ct[$label] = $data; 805 } 806 } 807 808 /** 809 * Get a content type parameter from this part. 810 * 811 * @param string $label The content type parameter label. 812 * 813 * @return string The data requested. 814 * Returns null if $label is not set. 815 */ 816 public function getContentTypeParameter($label) 817 { 818 $ct = $this->_headers['content-type']; 819 return $ct[$label]; 820 } 821 822 /** 823 * Get all parameters from the Content-Type header. 824 * 825 * @return array An array of all the parameters 826 * Returns the empty array if no parameters set. 827 */ 828 public function getAllContentTypeParameters() 829 { 830 return $this->_headers['content-type']->params; 831 } 832 833 /** 834 * Sets a new string to use for EOLs. 835 * 836 * @param string $eol The string to use for EOLs. 837 */ 838 public function setEOL($eol) 839 { 840 $this->_eol = $eol; 841 } 842 843 /** 844 * Get the string to use for EOLs. 845 * 846 * @return string The string to use for EOLs. 847 */ 848 public function getEOL() 849 { 850 return $this->_eol; 851 } 852 853 /** 854 * Returns a Horde_Mime_Header object containing all MIME headers needed 855 * for the part. 856 * 857 * @param array $options Additional options: 858 * - encode: (integer) A mask of allowable encodings. 859 * DEFAULT: Auto-determined 860 * - headers: (Horde_Mime_Headers) The object to add the MIME headers 861 * to. 862 * DEFAULT: Add headers to a new object 863 * 864 * @return Horde_Mime_Headers A Horde_Mime_Headers object. 865 */ 866 public function addMimeHeaders($options = array()) 867 { 868 if (empty($options['headers'])) { 869 $headers = new Horde_Mime_Headers(); 870 } else { 871 $headers = $options['headers']; 872 $headers->removeHeader('Content-Disposition'); 873 $headers->removeHeader('Content-Transfer-Encoding'); 874 } 875 876 /* Add the mandatory Content-Type header. */ 877 $ct = $this->_headers['content-type']; 878 $headers->addHeaderOb($ct); 879 880 /* Add the language(s), if set. (RFC 3282 [2]) */ 881 if ($hdr = $this->_headers['content-language']) { 882 $headers->addHeaderOb($hdr); 883 } 884 885 /* Get the description, if any. */ 886 if ($hdr = $this->_headers['content-description']) { 887 $headers->addHeaderOb($hdr); 888 } 889 890 /* Set the duration, if it exists. (RFC 3803) */ 891 if ($hdr = $this->_headers['content-duration']) { 892 $headers->addHeaderOb($hdr); 893 } 894 895 /* Per RFC 2046[4], this MUST appear in the base message headers. */ 896 if ($this->_status & self::STATUS_BASEPART) { 897 $headers->addHeaderOb(Horde_Mime_Headers_MimeVersion::create()); 898 } 899 900 /* message/* parts require no additional header information. */ 901 if ($ct->ptype === 'message') { 902 return $headers; 903 } 904 905 /* RFC 2183 [2] indicates that default is no requested disposition - 906 * the receiving MUA is responsible for display choice. */ 907 $cd = $this->_headers['content-disposition']; 908 if (!$cd->isDefault()) { 909 $headers->addHeaderOb($cd); 910 } 911 912 /* Add transfer encoding information. RFC 2045 [6.1] indicates that 913 * default is 7bit. No need to send the header in this case. */ 914 $cte = new Horde_Mime_Headers_ContentTransferEncoding( 915 null, 916 $this->_getTransferEncoding( 917 empty($options['encode']) ? null : $options['encode'] 918 ) 919 ); 920 if (!$cte->isDefault()) { 921 $headers->addHeaderOb($cte); 922 } 923 924 /* Add content ID information. */ 925 if ($hdr = $this->_headers['content-id']) { 926 $headers->addHeaderOb($hdr); 927 } 928 929 return $headers; 930 } 931 932 /** 933 * Return the entire part in MIME format. 934 * 935 * @param array $options Additional options: 936 * - canonical: (boolean) Returns the encoded part in strict RFC 822 & 937 * 2045 output - namely, all newlines end with the 938 * canonical <CR><LF> sequence. 939 * DEFAULT: false 940 * - defserver: (string) The default server to use when creating the 941 * header string. 942 * DEFAULT: none 943 * - encode: (integer) A mask of allowable encodings. 944 * DEFAULT: self::ENCODE_7BIT 945 * - headers: (mixed) Include the MIME headers? If true, create a new 946 * headers object. If a Horde_Mime_Headers object, add MIME 947 * headers to this object. If a string, use the string 948 * verbatim. 949 * DEFAULT: true 950 * - id: (string) Return only this MIME ID part. 951 * DEFAULT: Returns the base part. 952 * - stream: (boolean) Return a stream resource. 953 * DEFAULT: false 954 * 955 * @return mixed The MIME string (returned as a resource if $stream is 956 * true). 957 */ 958 public function toString($options = array()) 959 { 960 $eol = $this->getEOL(); 961 $isbase = true; 962 $oldbaseptr = null; 963 $parts = $parts_close = array(); 964 965 if (isset($options['id'])) { 966 $id = $options['id']; 967 if (!($part = $this[$id])) { 968 return $part; 969 } 970 unset($options['id']); 971 $contents = $part->toString($options); 972 973 $prev_id = Horde_Mime::mimeIdArithmetic($id, 'up', array('norfc822' => true)); 974 $prev_part = ($prev_id == $this->getMimeId()) 975 ? $this 976 : $this[$prev_id]; 977 if (!$prev_part) { 978 return $contents; 979 } 980 981 $boundary = trim($this->getContentTypeParameter('boundary'), '"'); 982 $parts = array( 983 $eol . '--' . $boundary . $eol, 984 $contents 985 ); 986 987 if (!isset($this[Horde_Mime::mimeIdArithmetic($id, 'next')])) { 988 $parts[] = $eol . '--' . $boundary . '--' . $eol; 989 } 990 } else { 991 if ($isbase = empty($options['_notbase'])) { 992 $headers = !empty($options['headers']) 993 ? $options['headers'] 994 : false; 995 996 if (empty($options['encode'])) { 997 $options['encode'] = null; 998 } 999 if (empty($options['defserver'])) { 1000 $options['defserver'] = null; 1001 } 1002 $options['headers'] = true; 1003 $options['_notbase'] = true; 1004 } else { 1005 $headers = true; 1006 $oldbaseptr = &$options['_baseptr']; 1007 } 1008 1009 $this->_temp['toString'] = ''; 1010 $options['_baseptr'] = &$this->_temp['toString']; 1011 1012 /* Any information about a message is embedded in the message 1013 * contents themself. Simply output the contents of the part 1014 * directly and return. */ 1015 $ptype = $this->getPrimaryType(); 1016 if ($ptype == 'message') { 1017 $parts[] = $this->_contents; 1018 } else { 1019 if (!empty($this->_contents)) { 1020 $encoding = $this->_getTransferEncoding($options['encode']); 1021 switch ($encoding) { 1022 case '8bit': 1023 if (empty($options['_baseptr'])) { 1024 $options['_baseptr'] = '8bit'; 1025 } 1026 break; 1027 1028 case 'binary': 1029 $options['_baseptr'] = 'binary'; 1030 break; 1031 } 1032 1033 $parts[] = $this->_transferEncode($this->_contents, $encoding); 1034 1035 /* If not using $this->_contents, we can close the stream 1036 * when finished. */ 1037 if ($this->_temp['transferEncodeClose']) { 1038 $parts_close[] = end($parts); 1039 } 1040 } 1041 1042 /* Deal with multipart messages. */ 1043 if ($ptype == 'multipart') { 1044 if (empty($this->_contents)) { 1045 $parts[] = 'This message is in MIME format.' . $eol; 1046 } 1047 1048 $boundary = trim($this->getContentTypeParameter('boundary'), '"'); 1049 1050 /* If base part is multipart/digest, children should not 1051 * have content-type (automatically treated as 1052 * message/rfc822; RFC 2046 [5.1.5]). */ 1053 if ($this->getSubType() === 'digest') { 1054 $options['is_digest'] = true; 1055 } 1056 1057 foreach ($this as $part) { 1058 $parts[] = $eol . '--' . $boundary . $eol; 1059 $tmp = $part->toString($options); 1060 if ($part->getEOL() != $eol) { 1061 $tmp = $this->replaceEOL($tmp, $eol, !empty($options['stream'])); 1062 } 1063 if (!empty($options['stream'])) { 1064 $parts_close[] = $tmp; 1065 } 1066 $parts[] = $tmp; 1067 } 1068 $parts[] = $eol . '--' . $boundary . '--' . $eol; 1069 } 1070 } 1071 1072 if (is_string($headers)) { 1073 array_unshift($parts, $headers); 1074 } elseif ($headers) { 1075 $hdr_ob = $this->addMimeHeaders(array( 1076 'encode' => $options['encode'], 1077 'headers' => ($headers === true) ? null : $headers 1078 )); 1079 if (!$isbase && !empty($options['is_digest'])) { 1080 unset($hdr_ob['content-type']); 1081 } 1082 if (!empty($this->_temp['toString'])) { 1083 $hdr_ob->addHeader( 1084 'Content-Transfer-Encoding', 1085 $this->_temp['toString'] 1086 ); 1087 } 1088 array_unshift($parts, $hdr_ob->toString(array( 1089 'canonical' => ($eol == self::RFC_EOL), 1090 'charset' => $this->getHeaderCharset(), 1091 'defserver' => $options['defserver'] 1092 ))); 1093 } 1094 } 1095 1096 $newfp = $this->_writeStream($parts); 1097 1098 array_map('fclose', $parts_close); 1099 1100 if (!is_null($oldbaseptr)) { 1101 switch ($this->_temp['toString']) { 1102 case '8bit': 1103 if (empty($oldbaseptr)) { 1104 $oldbaseptr = '8bit'; 1105 } 1106 break; 1107 1108 case 'binary': 1109 $oldbaseptr = 'binary'; 1110 break; 1111 } 1112 } 1113 1114 if ($isbase && !empty($options['canonical'])) { 1115 return $this->replaceEOL($newfp, self::RFC_EOL, !empty($options['stream'])); 1116 } 1117 1118 return empty($options['stream']) 1119 ? $this->_readStream($newfp) 1120 : $newfp; 1121 } 1122 1123 /** 1124 * Get the transfer encoding for the part based on the user requested 1125 * transfer encoding and the current contents of the part. 1126 * 1127 * @param integer $encode A mask of allowable encodings. 1128 * 1129 * @return string The transfer-encoding of this part. 1130 */ 1131 protected function _getTransferEncoding($encode = self::ENCODE_7BIT) 1132 { 1133 if (!empty($this->_temp['sendEncoding'])) { 1134 return $this->_temp['sendEncoding']; 1135 } elseif (!empty($this->_temp['sendTransferEncoding'][$encode])) { 1136 return $this->_temp['sendTransferEncoding'][$encode]; 1137 } 1138 1139 if (empty($this->_contents)) { 1140 $encoding = '7bit'; 1141 } else { 1142 switch ($this->getPrimaryType()) { 1143 case 'message': 1144 case 'multipart': 1145 /* RFC 2046 [5.2.1] - message/rfc822 messages only allow 7bit, 1146 * 8bit, and binary encodings. If the current encoding is 1147 * either base64 or q-p, switch it to 8bit instead. 1148 * RFC 2046 [5.2.2, 5.2.3, 5.2.4] - All other messages 1149 * only allow 7bit encodings. 1150 * 1151 * TODO: What if message contains 8bit characters and we are 1152 * in strict 7bit mode? Not sure there is anything we can do 1153 * in that situation, especially for message/rfc822 parts. 1154 * 1155 * These encoding will be figured out later (via toString()). 1156 * They are limited to 7bit, 8bit, and binary. Default to 1157 * '7bit' per RFCs. */ 1158 $default_8bit = 'base64'; 1159 $encoding = '7bit'; 1160 break; 1161 1162 case 'text': 1163 $default_8bit = 'quoted-printable'; 1164 $encoding = '7bit'; 1165 break; 1166 1167 default: 1168 $default_8bit = 'base64'; 1169 /* If transfer encoding has changed from the default, use that 1170 * value. */ 1171 $encoding = ($this->_transferEncoding == self::DEFAULT_ENCODING) 1172 ? 'base64' 1173 : $this->_transferEncoding; 1174 break; 1175 } 1176 1177 switch ($encoding) { 1178 case 'base64': 1179 case 'binary': 1180 break; 1181 1182 default: 1183 $encoding = $this->_scanStream($this->_contents); 1184 break; 1185 } 1186 1187 switch ($encoding) { 1188 case 'base64': 1189 case 'binary': 1190 /* If the text is longer than 998 characters between 1191 * linebreaks, use quoted-printable encoding to ensure the 1192 * text will not be chopped (i.e. by sendmail if being 1193 * sent as mail text). */ 1194 $encoding = $default_8bit; 1195 break; 1196 1197 case '8bit': 1198 $encoding = (($encode & self::ENCODE_8BIT) || ($encode & self::ENCODE_BINARY)) 1199 ? '8bit' 1200 : $default_8bit; 1201 break; 1202 } 1203 } 1204 1205 $this->_temp['sendTransferEncoding'][$encode] = $encoding; 1206 1207 return $encoding; 1208 } 1209 1210 /** 1211 * Replace newlines in this part's contents with those specified by either 1212 * the given newline sequence or the part's current EOL setting. 1213 * 1214 * @param mixed $text The text to replace. Either a string or a 1215 * stream resource. If a stream, and returning 1216 * a string, will close the stream when done. 1217 * @param string $eol The EOL sequence to use. If not present, uses 1218 * the part's current EOL setting. 1219 * @param boolean $stream If true, returns a stream resource. 1220 * 1221 * @return string The text with the newlines replaced by the desired 1222 * newline sequence (returned as a stream resource if 1223 * $stream is true). 1224 */ 1225 public function replaceEOL($text, $eol = null, $stream = false) 1226 { 1227 if (is_null($eol)) { 1228 $eol = $this->getEOL(); 1229 } 1230 1231 stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol'); 1232 $fp = $this->_writeStream($text, array( 1233 'filter' => array( 1234 'horde_eol' => array('eol' => $eol) 1235 ) 1236 )); 1237 1238 return $stream ? $fp : $this->_readStream($fp, true); 1239 } 1240 1241 /** 1242 * Determine the size of this MIME part and its child members. 1243 * 1244 * @todo Remove $approx parameter. 1245 * 1246 * @param boolean $approx If true, determines an approximate size for 1247 * parts consisting of base64 encoded data. 1248 * 1249 * @return integer Size of the part, in bytes. 1250 */ 1251 public function getBytes($approx = false) 1252 { 1253 if ($this->getPrimaryType() == 'multipart') { 1254 if (isset($this->_bytes)) { 1255 return $this->_bytes; 1256 } 1257 1258 $bytes = 0; 1259 foreach ($this as $part) { 1260 $bytes += $part->getBytes($approx); 1261 } 1262 return $bytes; 1263 } 1264 1265 if ($this->_contents) { 1266 fseek($this->_contents, 0, SEEK_END); 1267 $bytes = ftell($this->_contents); 1268 } else { 1269 $bytes = $this->_bytes; 1270 1271 /* Base64 transfer encoding is approx. 33% larger than original 1272 * data size (RFC 2045 [6.8]). */ 1273 if ($approx && ($this->_transferEncoding == 'base64')) { 1274 $bytes *= 0.75; 1275 } 1276 } 1277 1278 return intval($bytes); 1279 } 1280 1281 /** 1282 * Explicitly set the size (in bytes) of this part. This value will only 1283 * be returned (via getBytes()) if there are no contents currently set. 1284 * 1285 * This function is useful for setting the size of the part when the 1286 * contents of the part are not fully loaded (i.e. creating a 1287 * Horde_Mime_Part object from IMAP header information without loading the 1288 * data of the part). 1289 * 1290 * @param integer $bytes The size of this part in bytes. 1291 */ 1292 public function setBytes($bytes) 1293 { 1294 /* Consider 'size' disposition parameter to be the canonical size. 1295 * Only set bytes if that value doesn't exist. */ 1296 if (!$this->getDispositionParameter('size')) { 1297 $this->setDispositionParameter('size', $bytes); 1298 } 1299 } 1300 1301 /** 1302 * Output the size of this MIME part in KB. 1303 * 1304 * @todo Remove $approx parameter. 1305 * 1306 * @param boolean $approx If true, determines an approximate size for 1307 * parts consisting of base64 encoded data. 1308 * 1309 * @return string Size of the part in KB. 1310 */ 1311 public function getSize($approx = false) 1312 { 1313 if (!($bytes = $this->getBytes($approx))) { 1314 return 0; 1315 } 1316 1317 $localeinfo = Horde_Nls::getLocaleInfo(); 1318 1319 // TODO: Workaround broken number_format() prior to PHP 5.4.0. 1320 return str_replace( 1321 array('X', 'Y'), 1322 array($localeinfo['decimal_point'], $localeinfo['thousands_sep']), 1323 number_format(ceil($bytes / 1024), 0, 'X', 'Y') 1324 ); 1325 } 1326 1327 /** 1328 * Sets the Content-ID header for this part. 1329 * 1330 * @param string $cid Use this CID (if not already set). Else, generate 1331 * a random CID. 1332 * 1333 * @return string The Content-ID for this part. 1334 */ 1335 public function setContentId($cid = null) 1336 { 1337 if (!is_null($id = $this->getContentId())) { 1338 return $id; 1339 } 1340 1341 $this->_headers->addHeaderOb( 1342 is_null($cid) 1343 ? Horde_Mime_Headers_ContentId::create() 1344 : new Horde_Mime_Headers_ContentId(null, $cid) 1345 ); 1346 1347 return $this->getContentId(); 1348 } 1349 1350 /** 1351 * Returns the Content-ID for this part. 1352 * 1353 * @return string The Content-ID for this part (null if not set). 1354 */ 1355 public function getContentId() 1356 { 1357 return ($hdr = $this->_headers['content-id']) 1358 ? trim($hdr->value, '<>') 1359 : null; 1360 } 1361 1362 /** 1363 * Alter the MIME ID of this part. 1364 * 1365 * @param string $mimeid The MIME ID. 1366 */ 1367 public function setMimeId($mimeid) 1368 { 1369 $this->_mimeid = $mimeid; 1370 } 1371 1372 /** 1373 * Returns the MIME ID of this part. 1374 * 1375 * @return string The MIME ID. 1376 */ 1377 public function getMimeId() 1378 { 1379 return $this->_mimeid; 1380 } 1381 1382 /** 1383 * Build the MIME IDs for this part and all subparts. 1384 * 1385 * @param string $id The ID of this part. 1386 * @param boolean $rfc822 Is this a message/rfc822 part? 1387 */ 1388 public function buildMimeIds($id = null, $rfc822 = false) 1389 { 1390 $this->_status &= ~self::STATUS_REINDEX; 1391 1392 if (is_null($id)) { 1393 $rfc822 = true; 1394 $id = ''; 1395 } 1396 1397 if ($rfc822) { 1398 if (empty($this->_parts) && 1399 ($this->getPrimaryType() != 'multipart')) { 1400 $this->setMimeId($id . '1'); 1401 } else { 1402 if (empty($id) && ($this->getType() == 'message/rfc822')) { 1403 $this->setMimeId('1.0'); 1404 } else { 1405 $this->setMimeId($id . '0'); 1406 } 1407 $i = 1; 1408 foreach ($this as $val) { 1409 $val->buildMimeIds($id . ($i++)); 1410 } 1411 } 1412 } else { 1413 $this->setMimeId($id); 1414 $id = $id 1415 ? ((substr($id, -2) === '.0') ? substr($id, 0, -1) : ($id . '.')) 1416 : ''; 1417 1418 if (count($this)) { 1419 if ($this->getType() == 'message/rfc822') { 1420 $this->rewind(); 1421 $this->current()->buildMimeIds($id, true); 1422 } else { 1423 $i = 1; 1424 foreach ($this as $val) { 1425 $val->buildMimeIds($id . ($i++)); 1426 } 1427 } 1428 } 1429 } 1430 } 1431 1432 /** 1433 * Is this the base MIME part? 1434 * 1435 * @param boolean $base True if this is the base MIME part. 1436 */ 1437 public function isBasePart($base) 1438 { 1439 if (empty($base)) { 1440 $this->_status &= ~self::STATUS_BASEPART; 1441 } else { 1442 $this->_status |= self::STATUS_BASEPART; 1443 } 1444 } 1445 1446 /** 1447 * Determines if this MIME part is an attachment for display purposes. 1448 * 1449 * @since Horde_Mime 2.10.0 1450 * 1451 * @return boolean True if this part should be considered an attachment. 1452 */ 1453 public function isAttachment() 1454 { 1455 $type = $this->getType(); 1456 1457 switch ($type) { 1458 case 'application/ms-tnef': 1459 case 'application/pgp-keys': 1460 case 'application/vnd.ms-tnef': 1461 return false; 1462 } 1463 1464 if ($this->parent) { 1465 switch ($this->parent->getType()) { 1466 case 'multipart/encrypted': 1467 switch ($type) { 1468 case 'application/octet-stream': 1469 return false; 1470 } 1471 break; 1472 1473 case 'multipart/signed': 1474 switch ($type) { 1475 case 'application/pgp-signature': 1476 case 'application/pkcs7-signature': 1477 case 'application/x-pkcs7-signature': 1478 return false; 1479 } 1480 break; 1481 } 1482 } 1483 1484 switch ($this->getDisposition()) { 1485 case 'attachment': 1486 return true; 1487 } 1488 1489 switch ($this->getPrimaryType()) { 1490 case 'application': 1491 if (strlen($this->getName())) { 1492 return true; 1493 } 1494 break; 1495 1496 case 'audio': 1497 case 'video': 1498 return true; 1499 1500 case 'multipart': 1501 return false; 1502 } 1503 1504 return false; 1505 } 1506 1507 /** 1508 * Set a piece of metadata on this object. 1509 * 1510 * @param string $key The metadata key. 1511 * @param mixed $data The metadata. If null, clears the key. 1512 */ 1513 public function setMetadata($key, $data = null) 1514 { 1515 if (is_null($data)) { 1516 unset($this->_metadata[$key]); 1517 } else { 1518 $this->_metadata[$key] = $data; 1519 } 1520 } 1521 1522 /** 1523 * Retrieves metadata from this object. 1524 * 1525 * @param string $key The metadata key. 1526 * 1527 * @return mixed The metadata, or null if it doesn't exist. 1528 */ 1529 public function getMetadata($key) 1530 { 1531 return isset($this->_metadata[$key]) 1532 ? $this->_metadata[$key] 1533 : null; 1534 } 1535 1536 /** 1537 * Sends this message. 1538 * 1539 * @param string $email The address list to send to. 1540 * @param Horde_Mime_Headers $headers The Horde_Mime_Headers object 1541 * holding this message's headers. 1542 * @param Horde_Mail_Transport $mailer A Horde_Mail_Transport object. 1543 * @param array $opts Additional options: 1544 * <pre> 1545 * - broken_rfc2231: (boolean) Attempt to work around non-RFC 1546 * 2231-compliant MUAs by generating both a RFC 1547 * 2047-like parameter name and also the correct RFC 1548 * 2231 parameter (@since 2.5.0). 1549 * DEFAULT: false 1550 * - encode: (integer) The encoding to use. A mask of self::ENCODE_* 1551 * values. 1552 * DEFAULT: Auto-determined based on transport driver. 1553 * </pre> 1554 * 1555 * @throws Horde_Mime_Exception 1556 * @throws InvalidArgumentException 1557 */ 1558 public function send($email, $headers, Horde_Mail_Transport $mailer, 1559 array $opts = array()) 1560 { 1561 $old_status = $this->_status; 1562 $this->isBasePart(true); 1563 1564 /* Does the SMTP backend support 8BITMIME (RFC 1652)? */ 1565 $canonical = true; 1566 $encode = self::ENCODE_7BIT; 1567 1568 if (isset($opts['encode'])) { 1569 /* Always allow 7bit encoding. */ 1570 $encode |= $opts['encode']; 1571 } elseif ($mailer instanceof Horde_Mail_Transport_Smtp) { 1572 try { 1573 $smtp_ext = $mailer->getSMTPObject()->getServiceExtensions(); 1574 if (isset($smtp_ext['8BITMIME'])) { 1575 $encode |= self::ENCODE_8BIT; 1576 } 1577 } catch (Horde_Mail_Exception $e) {} 1578 $canonical = false; 1579 } elseif ($mailer instanceof Horde_Mail_Transport_Smtphorde) { 1580 try { 1581 if ($mailer->getSMTPObject()->data_8bit) { 1582 $encode |= self::ENCODE_8BIT; 1583 } 1584 } catch (Horde_Mail_Exception $e) {} 1585 $canonical = false; 1586 } 1587 1588 $msg = $this->toString(array( 1589 'canonical' => $canonical, 1590 'encode' => $encode, 1591 'headers' => false, 1592 'stream' => true 1593 )); 1594 1595 /* Add MIME Headers if they don't already exist. */ 1596 if (!isset($headers['MIME-Version'])) { 1597 $headers = $this->addMimeHeaders(array( 1598 'encode' => $encode, 1599 'headers' => $headers 1600 )); 1601 } 1602 1603 if (!empty($this->_temp['toString'])) { 1604 $headers->addHeader( 1605 'Content-Transfer-Encoding', 1606 $this->_temp['toString'] 1607 ); 1608 switch ($this->_temp['toString']) { 1609 case '8bit': 1610 if ($mailer instanceof Horde_Mail_Transport_Smtp) { 1611 $mailer->addServiceExtensionParameter('BODY', '8BITMIME'); 1612 } 1613 break; 1614 } 1615 } 1616 1617 $this->_status = $old_status; 1618 $rfc822 = new Horde_Mail_Rfc822(); 1619 try { 1620 $mailer->send($rfc822->parseAddressList($email)->writeAddress(array( 1621 'encode' => $this->getHeaderCharset() ?: true, 1622 'idn' => true 1623 )), $headers->toArray(array( 1624 'broken_rfc2231' => !empty($opts['broken_rfc2231']), 1625 'canonical' => $canonical, 1626 'charset' => $this->getHeaderCharset() 1627 )), $msg); 1628 } catch (InvalidArgumentException $e) { 1629 // Try to rebuild the part in case it was due to 1630 // an invalid line length in a rfc822/message attachment. 1631 if ($this->_failed) { 1632 throw $e; 1633 } 1634 $this->_failed = true; 1635 $this->_sanityCheckRfc822Attachments(); 1636 try { 1637 $this->send($email, $headers, $mailer, $opts); 1638 } catch (Horde_Mail_Exception $e) { 1639 throw new Horde_Mime_Exception($e); 1640 } 1641 } catch (Horde_Mail_Exception $e) { 1642 throw new Horde_Mime_Exception($e); 1643 } 1644 } 1645 1646 /** 1647 * Finds the main "body" text part (if any) in a message. 1648 * "Body" data is the first text part under this part. 1649 * 1650 * @param string $subtype Specifically search for this subtype. 1651 * 1652 * @return mixed The MIME ID of the main body part, or null if a body 1653 * part is not found. 1654 */ 1655 public function findBody($subtype = null) 1656 { 1657 $this->buildMimeIds(); 1658 1659 foreach ($this->partIterator() as $val) { 1660 $id = $val->getMimeId(); 1661 1662 if (($val->getPrimaryType() == 'text') && 1663 ((intval($id) === 1) || !$this->getMimeId()) && 1664 (is_null($subtype) || ($val->getSubType() == $subtype)) && 1665 ($val->getDisposition() !== 'attachment')) { 1666 return $id; 1667 } 1668 } 1669 1670 return null; 1671 } 1672 1673 /** 1674 * Returns the recursive iterator needed to iterate through this part. 1675 * 1676 * @since 2.8.0 1677 * 1678 * @param boolean $current Include the current part as the base? 1679 * 1680 * @return Iterator Recursive iterator. 1681 */ 1682 public function partIterator($current = true) 1683 { 1684 $this->_reindex(true); 1685 return new Horde_Mime_Part_Iterator($this, $current); 1686 } 1687 1688 /** 1689 * Returns a subpart by index. 1690 * 1691 * @return Horde_Mime_Part Part, or null if not found. 1692 */ 1693 public function getPartByIndex($index) 1694 { 1695 if (!isset($this->_parts[$index])) { 1696 return null; 1697 } 1698 1699 $part = $this->_parts[$index]; 1700 $part->parent = $this; 1701 1702 return $part; 1703 } 1704 1705 /** 1706 * Reindexes the MIME IDs, if necessary. 1707 * 1708 * @param boolean $force Reindex if the current part doesn't have an ID. 1709 */ 1710 protected function _reindex($force = false) 1711 { 1712 $id = $this->getMimeId(); 1713 1714 if (($this->_status & self::STATUS_REINDEX) || 1715 ($force && is_null($id))) { 1716 $this->buildMimeIds( 1717 is_null($id) 1718 ? (($this->getPrimaryType() === 'multipart') ? '0' : '1') 1719 : $id 1720 ); 1721 } 1722 } 1723 1724 /** 1725 * Write data to a stream. 1726 * 1727 * @param array $data The data to write. Either a stream resource or 1728 * a string. 1729 * @param array $options Additional options: 1730 * - error: (boolean) Catch errors when writing to the stream. Throw an 1731 * ErrorException if an error is found. 1732 * DEFAULT: false 1733 * - filter: (array) Filter(s) to apply to the string. Keys are the 1734 * filter names, values are filter params. 1735 * - fp: (resource) Use this stream instead of creating a new one. 1736 * 1737 * @return resource The stream resource. 1738 * @throws ErrorException 1739 */ 1740 protected function _writeStream($data, $options = array()) 1741 { 1742 if (empty($options['fp'])) { 1743 $fp = fopen('php://temp/maxmemory:' . self::$memoryLimit, 'r+'); 1744 } else { 1745 $fp = $options['fp']; 1746 fseek($fp, 0, SEEK_END); 1747 } 1748 1749 if (!is_array($data)) { 1750 $data = array($data); 1751 } 1752 1753 $append_filter = array(); 1754 if (!empty($options['filter'])) { 1755 foreach ($options['filter'] as $key => $val) { 1756 $append_filter[] = stream_filter_append($fp, $key, STREAM_FILTER_WRITE, $val); 1757 } 1758 } 1759 1760 if (!empty($options['error'])) { 1761 set_error_handler(function($errno, $errstr) { 1762 throw new ErrorException($errstr, $errno); 1763 }); 1764 $error = null; 1765 } 1766 1767 try { 1768 foreach ($data as $d) { 1769 if (is_resource($d)) { 1770 rewind($d); 1771 while (!feof($d)) { 1772 fwrite($fp, fread($d, 8192)); 1773 } 1774 } elseif (is_string($d)) { 1775 $len = strlen($d); 1776 $i = 0; 1777 while ($i < $len) { 1778 fwrite($fp, substr($d, $i, 8192)); 1779 $i += 8192; 1780 } 1781 } 1782 } 1783 } catch (ErrorException $e) { 1784 $error = $e; 1785 } 1786 1787 foreach ($append_filter as $val) { 1788 stream_filter_remove($val); 1789 } 1790 1791 if (!empty($options['error'])) { 1792 restore_error_handler(); 1793 if ($error) { 1794 throw $error; 1795 } 1796 } 1797 1798 return $fp; 1799 } 1800 1801 /** 1802 * Read data from a stream. 1803 * 1804 * @param resource $fp An active stream. 1805 * @param boolean $close Close the stream when done reading? 1806 * 1807 * @return string The data from the stream. 1808 */ 1809 protected function _readStream($fp, $close = false) 1810 { 1811 $out = ''; 1812 1813 if (!is_resource($fp)) { 1814 return $out; 1815 } 1816 1817 rewind($fp); 1818 while (!feof($fp)) { 1819 $out .= fread($fp, 8192); 1820 } 1821 1822 if ($close) { 1823 fclose($fp); 1824 } 1825 1826 return $out; 1827 } 1828 1829 /** 1830 * Scans a stream for content type. 1831 * 1832 * @param resource $fp A stream resource. 1833 * 1834 * @return mixed Either 'binary', '8bit', or false. 1835 */ 1836 protected function _scanStream($fp) 1837 { 1838 rewind($fp); 1839 1840 stream_filter_register( 1841 'horde_mime_scan_stream', 1842 'Horde_Mime_Filter_Encoding' 1843 ); 1844 $filter_params = new stdClass; 1845 $filter = stream_filter_append( 1846 $fp, 1847 'horde_mime_scan_stream', 1848 STREAM_FILTER_READ, 1849 $filter_params 1850 ); 1851 1852 while (!feof($fp)) { 1853 fread($fp, 8192); 1854 } 1855 1856 stream_filter_remove($filter); 1857 1858 return $filter_params->body; 1859 } 1860 1861 /* Static methods. */ 1862 1863 /** 1864 * Attempts to build a Horde_Mime_Part object from message text. 1865 * 1866 * @param string $text The text of the MIME message. 1867 * @param array $opts Additional options: 1868 * - forcemime: (boolean) If true, the message data is assumed to be 1869 * MIME data. If not, a MIME-Version header must exist (RFC 1870 * 2045 [4]) to be parsed as a MIME message. 1871 * DEFAULT: false 1872 * - level: (integer) Current nesting level of the MIME data. 1873 * DEFAULT: 0 1874 * - no_body: (boolean) If true, don't set body contents of parts (since 1875 * 2.2.0). 1876 * DEFAULT: false 1877 * 1878 * @return Horde_Mime_Part A MIME Part object. 1879 * @throws Horde_Mime_Exception 1880 */ 1881 public static function parseMessage($text, array $opts = array()) 1882 { 1883 /* Mini-hack to get a blank Horde_Mime part so we can call 1884 * replaceEOL(). Convert to EOL, since that is the expected EOL for 1885 * use internally within a Horde_Mime_Part object. */ 1886 $part = new Horde_Mime_Part(); 1887 $rawtext = $part->replaceEOL($text, self::EOL); 1888 1889 /* Find the header. */ 1890 $hdr_pos = self::_findHeader($rawtext, self::EOL); 1891 1892 unset($opts['ctype']); 1893 $ob = self::_getStructure(substr($rawtext, 0, $hdr_pos), substr($rawtext, $hdr_pos + 2), $opts); 1894 $ob->buildMimeIds(); 1895 return $ob; 1896 } 1897 1898 /** 1899 * Creates a MIME object from the text of one part of a MIME message. 1900 * 1901 * @param string $header The header text. 1902 * @param string $body The body text. 1903 * @param array $opts Additional options: 1904 * <pre> 1905 * - ctype: (string) The default content-type. 1906 * - forcemime: (boolean) If true, the message data is assumed to be 1907 * MIME data. If not, a MIME-Version header must exist to 1908 * be parsed as a MIME message. 1909 * - level: (integer) Current nesting level. 1910 * - no_body: (boolean) If true, don't set body contents of parts. 1911 * </pre> 1912 * 1913 * @return Horde_Mime_Part The MIME part object. 1914 */ 1915 protected static function _getStructure($header, $body, 1916 array $opts = array()) 1917 { 1918 $opts = array_merge(array( 1919 'ctype' => 'text/plain', 1920 'forcemime' => false, 1921 'level' => 0, 1922 'no_body' => false 1923 ), $opts); 1924 1925 /* Parse headers text into a Horde_Mime_Headers object. */ 1926 $hdrs = Horde_Mime_Headers::parseHeaders($header); 1927 1928 $ob = new Horde_Mime_Part(); 1929 1930 /* This is not a MIME message. */ 1931 if (!$opts['forcemime'] && !isset($hdrs['MIME-Version'])) { 1932 $ob->setType('text/plain'); 1933 1934 if ($len = strlen($body)) { 1935 if ($opts['no_body']) { 1936 $ob->setBytes($len); 1937 } else { 1938 $ob->setContents($body); 1939 } 1940 } 1941 1942 return $ob; 1943 } 1944 1945 /* Content type. */ 1946 if ($tmp = $hdrs['Content-Type']) { 1947 $ob->setType($tmp->value); 1948 foreach ($tmp->params as $key => $val) { 1949 $ob->setContentTypeParameter($key, $val); 1950 } 1951 } else { 1952 $ob->setType($opts['ctype']); 1953 } 1954 1955 /* Content transfer encoding. */ 1956 if ($tmp = $hdrs['Content-Transfer-Encoding']) { 1957 $ob->setTransferEncoding(strval($tmp)); 1958 } 1959 1960 /* Content-Description. */ 1961 if ($tmp = $hdrs['Content-Description']) { 1962 $ob->setDescription(strval($tmp)); 1963 } 1964 1965 /* Content-Disposition. */ 1966 if ($tmp = $hdrs['Content-Disposition']) { 1967 $ob->setDisposition($tmp->value); 1968 foreach ($tmp->params as $key => $val) { 1969 $ob->setDispositionParameter($key, $val); 1970 } 1971 } 1972 1973 /* Content-Duration */ 1974 if ($tmp = $hdrs['Content-Duration']) { 1975 $ob->setDuration(strval($tmp)); 1976 } 1977 1978 /* Content-ID. */ 1979 if ($tmp = $hdrs['Content-Id']) { 1980 $ob->setContentId(strval($tmp)); 1981 } 1982 1983 if (($len = strlen($body)) && ($ob->getPrimaryType() != 'multipart')) { 1984 if ($opts['no_body']) { 1985 $ob->setBytes($len); 1986 } else { 1987 $ob->setContents($body); 1988 } 1989 } 1990 1991 if (++$opts['level'] >= self::NESTING_LIMIT) { 1992 return $ob; 1993 } 1994 1995 /* Process subparts. */ 1996 switch ($ob->getPrimaryType()) { 1997 case 'message': 1998 if ($ob->getSubType() == 'rfc822') { 1999 $ob[] = self::parseMessage($body, array( 2000 'forcemime' => true, 2001 'no_body' => $opts['no_body'] 2002 )); 2003 } 2004 break; 2005 2006 case 'multipart': 2007 $boundary = $ob->getContentTypeParameter('boundary'); 2008 if (!is_null($boundary)) { 2009 foreach (self::_findBoundary($body, 0, $boundary) as $val) { 2010 if (!isset($val['length'])) { 2011 break; 2012 } 2013 $subpart = substr($body, $val['start'], $val['length']); 2014 $hdr_pos = self::_findHeader($subpart, self::EOL); 2015 $ob[] = self::_getStructure( 2016 substr($subpart, 0, $hdr_pos), 2017 substr($subpart, $hdr_pos + 2), 2018 array( 2019 'ctype' => ($ob->getSubType() == 'digest') ? 'message/rfc822' : 'text/plain', 2020 'forcemime' => true, 2021 'level' => $opts['level'], 2022 'no_body' => $opts['no_body'] 2023 ) 2024 ); 2025 } 2026 } 2027 break; 2028 } 2029 2030 return $ob; 2031 } 2032 2033 /** 2034 * Attempts to obtain the raw text of a MIME part. 2035 * 2036 * @param mixed $text The full text of the MIME message. The text is 2037 * assumed to be MIME data (no MIME-Version checking 2038 * is performed). It can be either a stream or a 2039 * string. 2040 * @param string $type Either 'header' or 'body'. 2041 * @param string $id The MIME ID. 2042 * 2043 * @return string The raw text. 2044 * @throws Horde_Mime_Exception 2045 */ 2046 public static function getRawPartText($text, $type, $id) 2047 { 2048 /* Mini-hack to get a blank Horde_Mime part so we can call 2049 * replaceEOL(). From an API perspective, getRawPartText() should be 2050 * static since it is not working on MIME part data. */ 2051 $part = new Horde_Mime_Part(); 2052 $rawtext = $part->replaceEOL($text, self::RFC_EOL); 2053 2054 /* We need to carry around the trailing "\n" because this is needed 2055 * to correctly find the boundary string. */ 2056 $hdr_pos = self::_findHeader($rawtext, self::RFC_EOL); 2057 $curr_pos = $hdr_pos + 3; 2058 2059 if ($id == 0) { 2060 switch ($type) { 2061 case 'body': 2062 return substr($rawtext, $curr_pos + 1); 2063 2064 case 'header': 2065 return trim(substr($rawtext, 0, $hdr_pos)); 2066 } 2067 } 2068 2069 $hdr_ob = Horde_Mime_Headers::parseHeaders(trim(substr($rawtext, 0, $hdr_pos))); 2070 2071 /* If this is a message/rfc822, pass the body into the next loop. 2072 * Don't decrement the ID here. */ 2073 if (($ct = $hdr_ob['Content-Type']) && ($ct == 'message/rfc822')) { 2074 return self::getRawPartText( 2075 substr($rawtext, $curr_pos + 1), 2076 $type, 2077 $id 2078 ); 2079 } 2080 2081 $base_pos = strpos($id, '.'); 2082 $orig_id = $id; 2083 2084 if ($base_pos !== false) { 2085 $id = substr($id, $base_pos + 1); 2086 $base_pos = substr($orig_id, 0, $base_pos); 2087 } else { 2088 $base_pos = $id; 2089 $id = 0; 2090 } 2091 2092 if ($ct && !isset($ct->params['boundary'])) { 2093 if ($orig_id == '1') { 2094 return substr($rawtext, $curr_pos + 1); 2095 } 2096 2097 throw new Horde_Mime_Exception('Could not find MIME part.'); 2098 } 2099 2100 $b_find = self::_findBoundary( 2101 $rawtext, 2102 $curr_pos, 2103 $ct->params['boundary'], 2104 $base_pos 2105 ); 2106 2107 if (!isset($b_find[$base_pos])) { 2108 throw new Horde_Mime_Exception('Could not find MIME part.'); 2109 } 2110 2111 return self::getRawPartText( 2112 substr( 2113 $rawtext, 2114 $b_find[$base_pos]['start'], 2115 $b_find[$base_pos]['length'] - 1 2116 ), 2117 $type, 2118 $id 2119 ); 2120 } 2121 2122 /** 2123 * Find the location of the end of the header text. 2124 * 2125 * @param string $text The text to search. 2126 * @param string $eol The EOL string. 2127 * 2128 * @return integer Header position. 2129 */ 2130 protected static function _findHeader($text, $eol) 2131 { 2132 $hdr_pos = strpos($text, $eol . $eol); 2133 return ($hdr_pos === false) 2134 ? strlen($text) 2135 : $hdr_pos; 2136 } 2137 2138 /** 2139 * Find the location of the next boundary string. 2140 * 2141 * @param string $text The text to search. 2142 * @param integer $pos The current position in $text. 2143 * @param string $boundary The boundary string. 2144 * @param integer $end If set, return after matching this many 2145 * boundaries. 2146 * 2147 * @return array Keys are the boundary number, values are an array with 2148 * two elements: 'start' and 'length'. 2149 */ 2150 protected static function _findBoundary($text, $pos, $boundary, 2151 $end = null) 2152 { 2153 $i = 0; 2154 $out = array(); 2155 2156 $search = "--" . $boundary; 2157 $search_len = strlen($search); 2158 2159 while (($pos = strpos($text, $search, $pos)) !== false) { 2160 /* Boundary needs to appear at beginning of string or right after 2161 * a LF. */ 2162 if (($pos != 0) && ($text[$pos - 1] != "\n")) { 2163 continue; 2164 } 2165 2166 if (isset($out[$i])) { 2167 $out[$i]['length'] = $pos - $out[$i]['start'] - 1; 2168 } 2169 2170 if (!is_null($end) && ($end == $i)) { 2171 break; 2172 } 2173 2174 $pos += $search_len; 2175 if (isset($text[$pos])) { 2176 switch ($text[$pos]) { 2177 case "\r": 2178 $pos += 2; 2179 $out[++$i] = array('start' => $pos); 2180 break; 2181 2182 case "\n": 2183 $out[++$i] = array('start' => ++$pos); 2184 break; 2185 2186 case '-': 2187 return $out; 2188 } 2189 } 2190 } 2191 2192 return $out; 2193 } 2194 2195 /** 2196 * Re-enocdes message/rfc822 parts in case there was e.g., some broken 2197 * line length in the headers of the message in the part. Since we shouldn't 2198 * alter the original message in any way, we simply reset cause the part to 2199 * be encoded as base64 and sent as a application/octet part. 2200 */ 2201 protected function _sanityCheckRfc822Attachments() 2202 { 2203 if ($this->getType() == 'message/rfc822') { 2204 $this->_reEncodeMessageAttachment($this); 2205 return; 2206 } 2207 foreach ($this->getParts() as $part) { 2208 if ($part->getType() == 'message/rfc822') { 2209 $this->_reEncodeMessageAttachment($part); 2210 } 2211 } 2212 return; 2213 } 2214 2215 /** 2216 * Rebuilds $part and forces it to be a base64 encoded 2217 * application/octet-stream part. 2218 * 2219 * @param Horde_Mime_Part $part The MIME part. 2220 */ 2221 protected function _reEncodeMessageAttachment(Horde_Mime_Part $part) 2222 { 2223 $new_part = Horde_Mime_Part::parseMessage($part->getContents()); 2224 $part->setContents($new_part->getContents(array('stream' => true)), array('encoding' => self::ENCODE_BINARY)); 2225 $part->setTransferEncoding('base64', array('send' => true)); 2226 } 2227 2228 /* ArrayAccess methods. */ 2229 2230 /** 2231 */ 2232 public function offsetExists($offset) 2233 { 2234 return ($this[$offset] !== null); 2235 } 2236 2237 /** 2238 */ 2239 public function offsetGet($offset) 2240 { 2241 $this->_reindex(); 2242 2243 if (strcmp($offset, $this->getMimeId()) === 0) { 2244 $this->parent = null; 2245 return $this; 2246 } 2247 2248 foreach ($this->_parts as $val) { 2249 if (strcmp($offset, $val->getMimeId()) === 0) { 2250 $val->parent = $this; 2251 return $val; 2252 } 2253 2254 if ($found = $val[$offset]) { 2255 return $found; 2256 } 2257 } 2258 2259 return null; 2260 } 2261 2262 /** 2263 */ 2264 public function offsetSet($offset, $value) 2265 { 2266 if (is_null($offset)) { 2267 $this->_parts[] = $value; 2268 $this->_status |= self::STATUS_REINDEX; 2269 } elseif ($part = $this[$offset]) { 2270 if ($part->parent === $this) { 2271 if (($k = array_search($part, $this->_parts, true)) !== false) { 2272 $value->setMimeId($part->getMimeId()); 2273 $this->_parts[$k] = $value; 2274 } 2275 } else { 2276 $this->parent[$offset] = $value; 2277 } 2278 } 2279 } 2280 2281 /** 2282 */ 2283 public function offsetUnset($offset) 2284 { 2285 if ($part = $this[$offset]) { 2286 if ($part->parent === $this) { 2287 if (($k = array_search($part, $this->_parts, true)) !== false) { 2288 unset($this->_parts[$k]); 2289 $this->_parts = array_values($this->_parts); 2290 } 2291 } else { 2292 unset($part->parent[$offset]); 2293 } 2294 $this->_status |= self::STATUS_REINDEX; 2295 } 2296 } 2297 2298 /* Countable methods. */ 2299 2300 /** 2301 * Returns the number of child message parts (doesn't include 2302 * grandchildren or more remote ancestors). 2303 * 2304 * @return integer Number of message parts. 2305 */ 2306 public function count() 2307 { 2308 return count($this->_parts); 2309 } 2310 2311 /* RecursiveIterator methods. */ 2312 2313 /** 2314 * @since 2.8.0 2315 */ 2316 public function current() 2317 { 2318 return (($key = $this->key()) === null) 2319 ? null 2320 : $this->getPartByIndex($key); 2321 } 2322 2323 /** 2324 * @since 2.8.0 2325 */ 2326 public function key() 2327 { 2328 return (isset($this->_temp['iterate']) && isset($this->_parts[$this->_temp['iterate']])) 2329 ? $this->_temp['iterate'] 2330 : null; 2331 } 2332 2333 /** 2334 * @since 2.8.0 2335 */ 2336 public function next() 2337 { 2338 ++$this->_temp['iterate']; 2339 } 2340 2341 /** 2342 * @since 2.8.0 2343 */ 2344 public function rewind() 2345 { 2346 $this->_reindex(); 2347 reset($this->_parts); 2348 $this->_temp['iterate'] = key($this->_parts); 2349 } 2350 2351 /** 2352 * @since 2.8.0 2353 */ 2354 public function valid() 2355 { 2356 return ($this->key() !== null); 2357 } 2358 2359 /** 2360 * @since 2.8.0 2361 */ 2362 public function hasChildren() 2363 { 2364 return (($curr = $this->current()) && count($curr)); 2365 } 2366 2367 /** 2368 * @since 2.8.0 2369 */ 2370 public function getChildren() 2371 { 2372 return $this->current(); 2373 } 2374 2375 /* Serializable methods. */ 2376 2377 /** 2378 * Serialization. 2379 * 2380 * @return string Serialized data. 2381 */ 2382 public function serialize() 2383 { 2384 $data = array( 2385 // Serialized data ID. 2386 self::VERSION, 2387 $this->_bytes, 2388 $this->_eol, 2389 $this->_hdrCharset, 2390 $this->_headers, 2391 $this->_metadata, 2392 $this->_mimeid, 2393 $this->_parts, 2394 $this->_status, 2395 $this->_transferEncoding 2396 ); 2397 2398 if (!empty($this->_contents)) { 2399 $data[] = $this->_readStream($this->_contents); 2400 } 2401 2402 return serialize($data); 2403 } 2404 2405 /** 2406 * Unserialization. 2407 * 2408 * @param string $data Serialized data. 2409 * 2410 * @throws Exception 2411 */ 2412 public function unserialize($data) 2413 { 2414 $data = @unserialize($data); 2415 if (!is_array($data) || 2416 !isset($data[0]) || 2417 ($data[0] != self::VERSION)) { 2418 switch ($data[0]) { 2419 case 1: 2420 $convert = new Horde_Mime_Part_Upgrade_V1($data); 2421 $data = $convert->data; 2422 break; 2423 2424 default: 2425 $data = null; 2426 break; 2427 } 2428 2429 if (is_null($data)) { 2430 throw new Exception('Cache version change'); 2431 } 2432 } 2433 2434 $key = 0; 2435 $this->_bytes = $data[++$key]; 2436 $this->_eol = $data[++$key]; 2437 $this->_hdrCharset = $data[++$key]; 2438 $this->_headers = $data[++$key]; 2439 $this->_metadata = $data[++$key]; 2440 $this->_mimeid = $data[++$key]; 2441 $this->_parts = $data[++$key]; 2442 $this->_status = $data[++$key]; 2443 $this->_transferEncoding = $data[++$key]; 2444 2445 if (isset($data[++$key])) { 2446 $this->setContents($data[$key]); 2447 } 2448 } 2449 2450 /* Deprecated elements. */ 2451 2452 /** 2453 * @deprecated 2454 */ 2455 const UNKNOWN = 'x-unknown'; 2456 2457 /** 2458 * @deprecated 2459 */ 2460 public static $encodingTypes = array( 2461 '7bit', '8bit', 'base64', 'binary', 'quoted-printable', 2462 // Non-RFC types, but old mailers may still use 2463 'uuencode', 'x-uuencode', 'x-uue' 2464 ); 2465 2466 /** 2467 * @deprecated 2468 */ 2469 public static $mimeTypes = array( 2470 'text', 'multipart', 'message', 'application', 'audio', 'image', 2471 'video', 'model' 2472 ); 2473 2474 /** 2475 * @deprecated Use setContentTypeParameter with a null $data value. 2476 */ 2477 public function clearContentTypeParameter($label) 2478 { 2479 $this->setContentTypeParam($label, null); 2480 } 2481 2482 /** 2483 * @deprecated Use iterator instead. 2484 */ 2485 public function contentTypeMap($sort = true) 2486 { 2487 $map = array(); 2488 2489 foreach ($this->partIterator() as $val) { 2490 $map[$val->getMimeId()] = $val->getType(); 2491 } 2492 2493 return $map; 2494 } 2495 2496 /** 2497 * @deprecated Use array access instead. 2498 */ 2499 public function addPart($mime_part) 2500 { 2501 $this[] = $mime_part; 2502 } 2503 2504 /** 2505 * @deprecated Use array access instead. 2506 */ 2507 public function getPart($id) 2508 { 2509 return $this[$id]; 2510 } 2511 2512 /** 2513 * @deprecated Use array access instead. 2514 */ 2515 public function alterPart($id, $mime_part) 2516 { 2517 $this[$id] = $mime_part; 2518 } 2519 2520 /** 2521 * @deprecated Use array access instead. 2522 */ 2523 public function removePart($id) 2524 { 2525 unset($this[$id]); 2526 } 2527 2528 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body