[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/horde/framework/Horde/Mime/ -> Part.php (source)

   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  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1