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