Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

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