Overview

Packages

  • Mime

Classes

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