/lib/horde/framework/Horde/Mime/Part.php
PHP | 2528 lines | 1390 code | 286 blank | 852 comment | 224 complexity | caee7b98b9a202c510bf9de7715d2f15 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
- <?php
- /**
- * Copyright 1999-2017 Horde LLC (http://www.horde.org/)
- *
- * See the enclosed file LICENSE for license information (LGPL). If you
- * did not receive this file, see http://www.horde.org/licenses/lgpl21.
- *
- * @category Horde
- * @copyright 1999-2017 Horde LLC
- * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
- * @package Mime
- */
- /**
- * Object-oriented representation of a MIME part (RFC 2045-2049).
- *
- * @author Chuck Hagenbuch <chuck@horde.org>
- * @author Michael Slusarz <slusarz@horde.org>
- * @category Horde
- * @copyright 1999-2017 Horde LLC
- * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
- * @package Mime
- */
- class Horde_Mime_Part
- implements ArrayAccess, Countable, RecursiveIterator, Serializable
- {
- /* Serialized version. */
- const VERSION = 2;
- /* The character(s) used internally for EOLs. */
- const EOL = "\n";
- /* The character string designated by RFC 2045 to designate EOLs in MIME
- * messages. */
- const RFC_EOL = "\r\n";
- /* The default encoding. */
- const DEFAULT_ENCODING = 'binary';
- /* Constants indicating the valid transfer encoding allowed. */
- const ENCODE_7BIT = 1;
- const ENCODE_8BIT = 2;
- const ENCODE_BINARY = 4;
- /* MIME nesting limit. */
- const NESTING_LIMIT = 100;
- /* Status mask value: Need to reindex the current part. */
- const STATUS_REINDEX = 1;
- /* Status mask value: This is the base MIME part. */
- const STATUS_BASEPART = 2;
- /**
- * The default charset to use when parsing text parts with no charset
- * information.
- *
- * @todo Make this a non-static property or pass as parameter to static
- * methods in Horde 6.
- *
- * @var string
- */
- public static $defaultCharset = 'us-ascii';
- /**
- * The memory limit for use with the PHP temp stream.
- *
- * @var integer
- */
- public static $memoryLimit = 2097152;
- /**
- * Parent object. Value only accurate when iterating.
- *
- * @since 2.8.0
- *
- * @var Horde_Mime_Part
- */
- public $parent = null;
- /**
- * Default value for this Part's size.
- *
- * @var integer
- */
- protected $_bytes;
- /**
- * The body of the part. Always stored in binary format.
- *
- * @var resource
- */
- protected $_contents;
- /**
- * The sequence to use as EOL for this part.
- *
- * The default is currently to output the EOL sequence internally as
- * just "\n" instead of the canonical "\r\n" required in RFC 822 & 2045.
- * To be RFC complaint, the full <CR><LF> EOL combination should be used
- * when sending a message.
- *
- * @var string
- */
- protected $_eol = self::EOL;
- /**
- * The MIME headers for this part.
- *
- * @var Horde_Mime_Headers
- */
- protected $_headers;
- /**
- * The charset to output the headers in.
- *
- * @var string
- */
- protected $_hdrCharset = null;
- /**
- * Metadata.
- *
- * @var array
- */
- protected $_metadata = array();
- /**
- * The MIME ID of this part.
- *
- * @var string
- */
- protected $_mimeid = null;
- /**
- * The subparts of this part.
- *
- * @var array
- */
- protected $_parts = array();
- /**
- * Status mask for this part.
- *
- * @var integer
- */
- protected $_status = 0;
- /**
- * Temporary array.
- *
- * @var array
- */
- protected $_temp = array();
- /**
- * The desired transfer encoding of this part.
- *
- * @var string
- */
- protected $_transferEncoding = self::DEFAULT_ENCODING;
- /**
- * Flag to detect if a message failed to send at least once.
- *
- * @var boolean
- */
- protected $_failed = false;
- /**
- * Constructor.
- */
- public function __construct()
- {
- $this->_headers = new Horde_Mime_Headers();
- /* Mandatory MIME headers. */
- $this->_headers->addHeaderOb(
- new Horde_Mime_Headers_ContentParam_ContentDisposition(null, '')
- );
- $ct = Horde_Mime_Headers_ContentParam_ContentType::create();
- $ct['charset'] = self::$defaultCharset;
- $this->_headers->addHeaderOb($ct);
- }
- /**
- * Function to run on clone.
- */
- public function __clone()
- {
- foreach ($this->_parts as $k => $v) {
- $this->_parts[$k] = clone $v;
- }
- $this->_headers = clone $this->_headers;
- if (!empty($this->_contents)) {
- $this->_contents = $this->_writeStream($this->_contents);
- }
- }
- /**
- * Set the content-disposition of this part.
- *
- * @param string $disposition The content-disposition to set ('inline',
- * 'attachment', or an empty value).
- */
- public function setDisposition($disposition = null)
- {
- $this->_headers['content-disposition']->setContentParamValue(
- strval($disposition)
- );
- }
- /**
- * Get the content-disposition of this part.
- *
- * @return string The part's content-disposition. An empty string means
- * no desired disposition has been set for this part.
- */
- public function getDisposition()
- {
- return $this->_headers['content-disposition']->value;
- }
- /**
- * Add a disposition parameter to this part.
- *
- * @param string $label The disposition parameter label.
- * @param string $data The disposition parameter data. If null, removes
- * the parameter (@since 2.8.0).
- */
- public function setDispositionParameter($label, $data)
- {
- $cd = $this->_headers['content-disposition'];
- if (is_null($data)) {
- unset($cd[$label]);
- } elseif (strlen($data)) {
- $cd[$label] = $data;
- if (strcasecmp($label, 'size') === 0) {
- // RFC 2183 [2.7] - size parameter
- $this->_bytes = $cd[$label];
- } elseif ((strcasecmp($label, 'filename') === 0) &&
- !strlen($cd->value)) {
- /* Set part to attachment if not already explicitly set to
- * 'inline'. */
- $cd->setContentParamValue('attachment');
- }
- }
- }
- /**
- * Get a disposition parameter from this part.
- *
- * @param string $label The disposition parameter label.
- *
- * @return string The data requested.
- * Returns null if $label is not set.
- */
- public function getDispositionParameter($label)
- {
- $cd = $this->_headers['content-disposition'];
- return $cd[$label];
- }
- /**
- * Get all parameters from the Content-Disposition header.
- *
- * @return array An array of all the parameters
- * Returns the empty array if no parameters set.
- */
- public function getAllDispositionParameters()
- {
- return $this->_headers['content-disposition']->params;
- }
- /**
- * Set the name of this part.
- *
- * @param string $name The name to set.
- */
- public function setName($name)
- {
- $this->setDispositionParameter('filename', $name);
- $this->setContentTypeParameter('name', $name);
- }
- /**
- * Get the name of this part.
- *
- * @param boolean $default If the name parameter doesn't exist, should we
- * use the default name from the description
- * parameter?
- *
- * @return string The name of the part.
- */
- public function getName($default = false)
- {
- if (!($name = $this->getDispositionParameter('filename')) &&
- !($name = $this->getContentTypeParameter('name')) &&
- $default) {
- $name = preg_replace('|\W|', '_', $this->getDescription(false));
- }
- return $name;
- }
- /**
- * Set the body contents of this part.
- *
- * @param mixed $contents The part body. Either a string or a stream
- * resource, or an array containing both.
- * @param array $options Additional options:
- * - encoding: (string) The encoding of $contents.
- * DEFAULT: Current transfer encoding value.
- * - usestream: (boolean) If $contents is a stream, should we directly
- * use that stream?
- * DEFAULT: $contents copied to a new stream.
- */
- public function setContents($contents, $options = array())
- {
- if (is_resource($contents) && ($contents === $this->_contents)) {
- return;
- }
- if (empty($options['encoding'])) {
- $options['encoding'] = $this->_transferEncoding;
- }
- $fp = (empty($options['usestream']) || !is_resource($contents))
- ? $this->_writeStream($contents)
- : $contents;
- /* Properly close the existing stream. */
- $this->clearContents();
- $this->setTransferEncoding($options['encoding']);
- $this->_contents = $this->_transferDecode($fp, $options['encoding']);
- }
- /**
- * Add to the body contents of this part.
- *
- * @param mixed $contents The part body. Either a string or a stream
- * resource, or an array containing both.
- * - encoding: (string) The encoding of $contents.
- * DEFAULT: Current transfer encoding value.
- * - usestream: (boolean) If $contents is a stream, should we directly
- * use that stream?
- * DEFAULT: $contents copied to a new stream.
- */
- public function appendContents($contents, $options = array())
- {
- if (empty($this->_contents)) {
- $this->setContents($contents, $options);
- } else {
- $fp = (empty($options['usestream']) || !is_resource($contents))
- ? $this->_writeStream($contents)
- : $contents;
- $this->_writeStream((empty($options['encoding']) || ($options['encoding'] == $this->_transferEncoding)) ? $fp : $this->_transferDecode($fp, $options['encoding']), array('fp' => $this->_contents));
- unset($this->_temp['sendTransferEncoding']);
- }
- }
- /**
- * Clears the body contents of this part.
- */
- public function clearContents()
- {
- if (!empty($this->_contents)) {
- fclose($this->_contents);
- $this->_contents = null;
- unset($this->_temp['sendTransferEncoding']);
- }
- }
- /**
- * Return the body of the part.
- *
- * @param array $options Additional options:
- * - canonical: (boolean) Returns the contents in strict RFC 822 &
- * 2045 output - namely, all newlines end with the
- * canonical <CR><LF> sequence.
- * DEFAULT: No
- * - stream: (boolean) Return the body as a stream resource.
- * DEFAULT: No
- *
- * @return mixed The body text (string) of the part, null if there is no
- * contents, and a stream resource if 'stream' is true.
- */
- public function getContents($options = array())
- {
- return empty($options['canonical'])
- ? (empty($options['stream']) ? $this->_readStream($this->_contents) : $this->_contents)
- : $this->replaceEOL($this->_contents, self::RFC_EOL, !empty($options['stream']));
- }
- /**
- * Decodes the contents of the part to binary encoding.
- *
- * @param resource $fp A stream containing the data to decode.
- * @param string $encoding The original file encoding.
- *
- * @return resource A new file resource with the decoded data.
- */
- protected function _transferDecode($fp, $encoding)
- {
- /* If the contents are empty, return now. */
- fseek($fp, 0, SEEK_END);
- if (ftell($fp)) {
- switch ($encoding) {
- case 'base64':
- try {
- return $this->_writeStream($fp, array(
- 'error' => true,
- 'filter' => array(
- 'convert.base64-decode' => array()
- )
- ));
- } catch (ErrorException $e) {}
- rewind($fp);
- return $this->_writeStream(base64_decode(stream_get_contents($fp)));
- case 'quoted-printable':
- try {
- return $this->_writeStream($fp, array(
- 'error' => true,
- 'filter' => array(
- 'convert.quoted-printable-decode' => array()
- )
- ));
- } catch (ErrorException $e) {}
- // Workaround for Horde Bug #8747
- rewind($fp);
- return $this->_writeStream(quoted_printable_decode(stream_get_contents($fp)));
- case 'uuencode':
- case 'x-uuencode':
- case 'x-uue':
- /* Support for uuencoded encoding - although not required by
- * RFCs, some mailers may still encode this way. */
- $res = Horde_Mime::uudecode($this->_readStream($fp));
- return $this->_writeStream($res[0]['data']);
- }
- }
- return $fp;
- }
- /**
- * Encodes the contents of the part as necessary for transport.
- *
- * @param resource $fp A stream containing the data to encode.
- * @param string $encoding The encoding to use.
- *
- * @return resource A new file resource with the encoded data.
- */
- protected function _transferEncode($fp, $encoding)
- {
- $this->_temp['transferEncodeClose'] = true;
- switch ($encoding) {
- case 'base64':
- /* Base64 Encoding: See RFC 2045, section 6.8 */
- return $this->_writeStream($fp, array(
- 'filter' => array(
- 'convert.base64-encode' => array(
- 'line-break-chars' => $this->getEOL(),
- 'line-length' => 76
- )
- )
- ));
- case 'quoted-printable':
- // PHP Bug 65776 - Must normalize the EOL characters.
- stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol');
- $stream = new Horde_Stream_Existing(array(
- 'stream' => $fp
- ));
- $stream->stream = $this->_writeStream($stream->stream, array(
- 'filter' => array(
- 'horde_eol' => array('eol' => $stream->getEOL()
- )
- )));
- /* Quoted-Printable Encoding: See RFC 2045, section 6.7 */
- return $this->_writeStream($fp, array(
- 'filter' => array(
- 'convert.quoted-printable-encode' => array_filter(array(
- 'line-break-chars' => $stream->getEOL(),
- 'line-length' => 76
- ))
- )
- ));
- default:
- $this->_temp['transferEncodeClose'] = false;
- return $fp;
- }
- }
- /**
- * Set the MIME type of this part.
- *
- * @param string $type The MIME type to set (ex.: text/plain).
- */
- public function setType($type)
- {
- /* RFC 2045: Any entity with unrecognized encoding must be treated
- * as if it has a Content-Type of "application/octet-stream"
- * regardless of what the Content-Type field actually says. */
- if (!is_null($this->_transferEncoding)) {
- $this->_headers['content-type']->setContentParamValue($type);
- }
- }
- /**
- * Get the full MIME Content-Type of this part.
- *
- * @param boolean $charset Append character set information to the end
- * of the content type if this is a text/* part?
- *`
- * @return string The MIME type of this part.
- */
- public function getType($charset = false)
- {
- $ct = $this->_headers['content-type'];
- return $charset
- ? $ct->type_charset
- : $ct->value;
- }
- /**
- * If the subtype of a MIME part is unrecognized by an application, the
- * default type should be used instead (See RFC 2046). This method
- * returns the default subtype for a particular primary MIME type.
- *
- * @return string The default MIME type of this part (ex.: text/plain).
- */
- public function getDefaultType()
- {
- switch ($this->getPrimaryType()) {
- case 'text':
- /* RFC 2046 (4.1.4): text parts default to text/plain. */
- return 'text/plain';
- case 'multipart':
- /* RFC 2046 (5.1.3): multipart parts default to multipart/mixed. */
- return 'multipart/mixed';
- default:
- /* RFC 2046 (4.2, 4.3, 4.4, 4.5.3, 5.2.4): all others default to
- application/octet-stream. */
- return 'application/octet-stream';
- }
- }
- /**
- * Get the primary type of this part.
- *
- * @return string The primary MIME type of this part.
- */
- public function getPrimaryType()
- {
- return $this->_headers['content-type']->ptype;
- }
- /**
- * Get the subtype of this part.
- *
- * @return string The MIME subtype of this part.
- */
- public function getSubType()
- {
- return $this->_headers['content-type']->stype;
- }
- /**
- * Set the character set of this part.
- *
- * @param string $charset The character set of this part.
- */
- public function setCharset($charset)
- {
- $this->setContentTypeParameter('charset', $charset);
- }
- /**
- * Get the character set to use for this part.
- *
- * @return string The character set of this part (lowercase). Returns
- * null if there is no character set.
- */
- public function getCharset()
- {
- return $this->getContentTypeParameter('charset')
- ?: (($this->getPrimaryType() === 'text') ? 'us-ascii' : null);
- }
- /**
- * Set the character set to use when outputting MIME headers.
- *
- * @param string $charset The character set.
- */
- public function setHeaderCharset($charset)
- {
- $this->_hdrCharset = $charset;
- }
- /**
- * Get the character set to use when outputting MIME headers.
- *
- * @return string The character set. If no preferred character set has
- * been set, returns null.
- */
- public function getHeaderCharset()
- {
- return is_null($this->_hdrCharset)
- ? $this->getCharset()
- : $this->_hdrCharset;
- }
- /**
- * Set the language(s) of this part.
- *
- * @param mixed $lang A language string, or an array of language
- * strings.
- */
- public function setLanguage($lang)
- {
- $this->_headers->addHeaderOb(
- new Horde_Mime_Headers_ContentLanguage('', $lang)
- );
- }
- /**
- * Get the language(s) of this part.
- *
- * @param array The list of languages.
- */
- public function getLanguage()
- {
- return $this->_headers['content-language']->langs;
- }
- /**
- * Set the content duration of the data contained in this part (see RFC
- * 3803).
- *
- * @param integer $duration The duration of the data, in seconds. If
- * null, clears the duration information.
- */
- public function setDuration($duration)
- {
- if (is_null($duration)) {
- unset($this->_headers['content-duration']);
- } else {
- if (!($hdr = $this->_headers['content-duration'])) {
- $hdr = new Horde_Mime_Headers_Element_Single(
- 'Content-Duration',
- ''
- );
- $this->_headers->addHeaderOb($hdr);
- }
- $hdr->setValue($duration);
- }
- }
- /**
- * Get the content duration of the data contained in this part (see RFC
- * 3803).
- *
- * @return integer The duration of the data, in seconds. Returns null if
- * there is no duration information.
- */
- public function getDuration()
- {
- return ($hdr = $this->_headers['content-duration'])
- ? intval($hdr->value)
- : null;
- }
- /**
- * Set the description of this part.
- *
- * @param string $description The description of this part. If null,
- * deletes the description (@since 2.8.0).
- */
- public function setDescription($description)
- {
- if (is_null($description)) {
- unset($this->_headers['content-description']);
- } else {
- if (!($hdr = $this->_headers['content-description'])) {
- $hdr = new Horde_Mime_Headers_ContentDescription(null, '');
- $this->_headers->addHeaderOb($hdr);
- }
- $hdr->setValue($description);
- }
- }
- /**
- * Get the description of this part.
- *
- * @param boolean $default If the description parameter doesn't exist,
- * should we use the name of the part?
- *
- * @return string The description of this part.
- */
- public function getDescription($default = false)
- {
- if (($ob = $this->_headers['content-description']) &&
- strlen($ob->value)) {
- return $ob->value;
- }
- return $default
- ? $this->getName()
- : '';
- }
- /**
- * Set the transfer encoding to use for this part.
- *
- * Only needed in the following circumstances:
- * 1.) Indicate what the transfer encoding is if the data has not yet been
- * set in the object (can only be set if there presently are not
- * any contents).
- * 2.) Force the encoding to a certain type on a toString() call (if
- * 'send' is true).
- *
- * @param string $encoding The transfer encoding to use.
- * @param array $options Additional options:
- * - send: (boolean) If true, use $encoding as the sending encoding.
- * DEFAULT: $encoding is used to change the base encoding.
- */
- public function setTransferEncoding($encoding, $options = array())
- {
- if (empty($encoding) ||
- (empty($options['send']) && !empty($this->_contents))) {
- return;
- }
- switch ($encoding = Horde_String::lower($encoding)) {
- case '7bit':
- case '8bit':
- case 'base64':
- case 'binary':
- case 'quoted-printable':
- // Non-RFC types, but old mailers may still use
- case 'uuencode':
- case 'x-uuencode':
- case 'x-uue':
- if (empty($options['send'])) {
- $this->_transferEncoding = $encoding;
- } else {
- $this->_temp['sendEncoding'] = $encoding;
- }
- break;
- default:
- if (empty($options['send'])) {
- /* RFC 2045: Any entity with unrecognized encoding must be
- * treated as if it has a Content-Type of
- * "application/octet-stream" regardless of what the
- * Content-Type field actually says. */
- $this->setType('application/octet-stream');
- $this->_transferEncoding = null;
- }
- break;
- }
- }
- /**
- * Get a list of all MIME subparts.
- *
- * @return array An array of the Horde_Mime_Part subparts.
- */
- public function getParts()
- {
- return $this->_parts;
- }
- /**
- * Add/remove a content type parameter to this part.
- *
- * @param string $label The content-type parameter label.
- * @param string $data The content-type parameter data. If null, removes
- * the parameter (@since 2.8.0).
- */
- public function setContentTypeParameter($label, $data)
- {
- $ct = $this->_headers['content-type'];
- if (is_null($data)) {
- unset($ct[$label]);
- } elseif (strlen($data)) {
- $ct[$label] = $data;
- }
- }
- /**
- * Get a content type parameter from this part.
- *
- * @param string $label The content type parameter label.
- *
- * @return string The data requested.
- * Returns null if $label is not set.
- */
- public function getContentTypeParameter($label)
- {
- $ct = $this->_headers['content-type'];
- return $ct[$label];
- }
- /**
- * Get all parameters from the Content-Type header.
- *
- * @return array An array of all the parameters
- * Returns the empty array if no parameters set.
- */
- public function getAllContentTypeParameters()
- {
- return $this->_headers['content-type']->params;
- }
- /**
- * Sets a new string to use for EOLs.
- *
- * @param string $eol The string to use for EOLs.
- */
- public function setEOL($eol)
- {
- $this->_eol = $eol;
- }
- /**
- * Get the string to use for EOLs.
- *
- * @return string The string to use for EOLs.
- */
- public function getEOL()
- {
- return $this->_eol;
- }
- /**
- * Returns a Horde_Mime_Header object containing all MIME headers needed
- * for the part.
- *
- * @param array $options Additional options:
- * - encode: (integer) A mask of allowable encodings.
- * DEFAULT: Auto-determined
- * - headers: (Horde_Mime_Headers) The object to add the MIME headers
- * to.
- * DEFAULT: Add headers to a new object
- *
- * @return Horde_Mime_Headers A Horde_Mime_Headers object.
- */
- public function addMimeHeaders($options = array())
- {
- if (empty($options['headers'])) {
- $headers = new Horde_Mime_Headers();
- } else {
- $headers = $options['headers'];
- $headers->removeHeader('Content-Disposition');
- $headers->removeHeader('Content-Transfer-Encoding');
- }
- /* Add the mandatory Content-Type header. */
- $ct = $this->_headers['content-type'];
- $headers->addHeaderOb($ct);
- /* Add the language(s), if set. (RFC 3282 [2]) */
- if ($hdr = $this->_headers['content-language']) {
- $headers->addHeaderOb($hdr);
- }
- /* Get the description, if any. */
- if ($hdr = $this->_headers['content-description']) {
- $headers->addHeaderOb($hdr);
- }
- /* Set the duration, if it exists. (RFC 3803) */
- if ($hdr = $this->_headers['content-duration']) {
- $headers->addHeaderOb($hdr);
- }
- /* Per RFC 2046[4], this MUST appear in the base message headers. */
- if ($this->_status & self::STATUS_BASEPART) {
- $headers->addHeaderOb(Horde_Mime_Headers_MimeVersion::create());
- }
- /* message/* parts require no additional header information. */
- if ($ct->ptype === 'message') {
- return $headers;
- }
- /* RFC 2183 [2] indicates that default is no requested disposition -
- * the receiving MUA is responsible for display choice. */
- $cd = $this->_headers['content-disposition'];
- if (!$cd->isDefault()) {
- $headers->addHeaderOb($cd);
- }
- /* Add transfer encoding information. RFC 2045 [6.1] indicates that
- * default is 7bit. No need to send the header in this case. */
- $cte = new Horde_Mime_Headers_ContentTransferEncoding(
- null,
- $this->_getTransferEncoding(
- empty($options['encode']) ? null : $options['encode']
- )
- );
- if (!$cte->isDefault()) {
- $headers->addHeaderOb($cte);
- }
- /* Add content ID information. */
- if ($hdr = $this->_headers['content-id']) {
- $headers->addHeaderOb($hdr);
- }
- return $headers;
- }
- /**
- * Return the entire part in MIME format.
- *
- * @param array $options Additional options:
- * - canonical: (boolean) Returns the encoded part in strict RFC 822 &
- * 2045 output - namely, all newlines end with the
- * canonical <CR><LF> sequence.
- * DEFAULT: false
- * - defserver: (string) The default server to use when creating the
- * header string.
- * DEFAULT: none
- * - encode: (integer) A mask of allowable encodings.
- * DEFAULT: self::ENCODE_7BIT
- * - headers: (mixed) Include the MIME headers? If true, create a new
- * headers object. If a Horde_Mime_Headers object, add MIME
- * headers to this object. If a string, use the string
- * verbatim.
- * DEFAULT: true
- * - id: (string) Return only this MIME ID part.
- * DEFAULT: Returns the base part.
- * - stream: (boolean) Return a stream resource.
- * DEFAULT: false
- *
- * @return mixed The MIME string (returned as a resource if $stream is
- * true).
- */
- public function toString($options = array())
- {
- $eol = $this->getEOL();
- $isbase = true;
- $oldbaseptr = null;
- $parts = $parts_close = array();
- if (isset($options['id'])) {
- $id = $options['id'];
- if (!($part = $this[$id])) {
- return $part;
- }
- unset($options['id']);
- $contents = $part->toString($options);
- $prev_id = Horde_Mime::mimeIdArithmetic($id, 'up', array('norfc822' => true));
- $prev_part = ($prev_id == $this->getMimeId())
- ? $this
- : $this[$prev_id];
- if (!$prev_part) {
- return $contents;
- }
- $boundary = trim($this->getContentTypeParameter('boundary'), '"');
- $parts = array(
- $eol . '--' . $boundary . $eol,
- $contents
- );
- if (!isset($this[Horde_Mime::mimeIdArithmetic($id, 'next')])) {
- $parts[] = $eol . '--' . $boundary . '--' . $eol;
- }
- } else {
- if ($isbase = empty($options['_notbase'])) {
- $headers = !empty($options['headers'])
- ? $options['headers']
- : false;
- if (empty($options['encode'])) {
- $options['encode'] = null;
- }
- if (empty($options['defserver'])) {
- $options['defserver'] = null;
- }
- $options['headers'] = true;
- $options['_notbase'] = true;
- } else {
- $headers = true;
- $oldbaseptr = &$options['_baseptr'];
- }
- $this->_temp['toString'] = '';
- $options['_baseptr'] = &$this->_temp['toString'];
- /* Any information about a message is embedded in the message
- * contents themself. Simply output the contents of the part
- * directly and return. */
- $ptype = $this->getPrimaryType();
- if ($ptype == 'message') {
- $parts[] = $this->_contents;
- } else {
- if (!empty($this->_contents)) {
- $encoding = $this->_getTransferEncoding($options['encode']);
- switch ($encoding) {
- case '8bit':
- if (empty($options['_baseptr'])) {
- $options['_baseptr'] = '8bit';
- }
- break;
- case 'binary':
- $options['_baseptr'] = 'binary';
- break;
- }
- $parts[] = $this->_transferEncode($this->_contents, $encoding);
- /* If not using $this->_contents, we can close the stream
- * when finished. */
- if ($this->_temp['transferEncodeClose']) {
- $parts_close[] = end($parts);
- }
- }
- /* Deal with multipart messages. */
- if ($ptype == 'multipart') {
- if (empty($this->_contents)) {
- $parts[] = 'This message is in MIME format.' . $eol;
- }
- $boundary = trim($this->getContentTypeParameter('boundary'), '"');
- /* If base part is multipart/digest, children should not
- * have content-type (automatically treated as
- * message/rfc822; RFC 2046 [5.1.5]). */
- if ($this->getSubType() === 'digest') {
- $options['is_digest'] = true;
- }
- foreach ($this as $part) {
- $parts[] = $eol . '--' . $boundary . $eol;
- $tmp = $part->toString($options);
- if ($part->getEOL() != $eol) {
- $tmp = $this->replaceEOL($tmp, $eol, !empty($options['stream']));
- }
- if (!empty($options['stream'])) {
- $parts_close[] = $tmp;
- }
- $parts[] = $tmp;
- }
- $parts[] = $eol . '--' . $boundary . '--' . $eol;
- }
- }
- if (is_string($headers)) {
- array_unshift($parts, $headers);
- } elseif ($headers) {
- $hdr_ob = $this->addMimeHeaders(array(
- 'encode' => $options['encode'],
- 'headers' => ($headers === true) ? null : $headers
- ));
- if (!$isbase && !empty($options['is_digest'])) {
- unset($hdr_ob['content-type']);
- }
- if (!empty($this->_temp['toString'])) {
- $hdr_ob->addHeader(
- 'Content-Transfer-Encoding',
- $this->_temp['toString']
- );
- }
- array_unshift($parts, $hdr_ob->toString(array(
- 'canonical' => ($eol == self::RFC_EOL),
- 'charset' => $this->getHeaderCharset(),
- 'defserver' => $options['defserver']
- )));
- }
- }
- $newfp = $this->_writeStream($parts);
- array_map('fclose', $parts_close);
- if (!is_null($oldbaseptr)) {
- switch ($this->_temp['toString']) {
- case '8bit':
- if (empty($oldbaseptr)) {
- $oldbaseptr = '8bit';
- }
- break;
- case 'binary':
- $oldbaseptr = 'binary';
- break;
- }
- }
- if ($isbase && !empty($options['canonical'])) {
- return $this->replaceEOL($newfp, self::RFC_EOL, !empty($options['stream']));
- }
- return empty($options['stream'])
- ? $this->_readStream($newfp)
- : $newfp;
- }
- /**
- * Get the transfer encoding for the part based on the user requested
- * transfer encoding and the current contents of the part.
- *
- * @param integer $encode A mask of allowable encodings.
- *
- * @return string The transfer-encoding of this part.
- */
- protected function _getTransferEncoding($encode = self::ENCODE_7BIT)
- {
- if (!empty($this->_temp['sendEncoding'])) {
- return $this->_temp['sendEncoding'];
- } elseif (!empty($this->_temp['sendTransferEncoding'][$encode])) {
- return $this->_temp['sendTransferEncoding'][$encode];
- }
- if (empty($this->_contents)) {
- $encoding = '7bit';
- } else {
- switch ($this->getPrimaryType()) {
- case 'message':
- case 'multipart':
- /* RFC 2046 [5.2.1] - message/rfc822 messages only allow 7bit,
- * 8bit, and binary encodings. If the current encoding is
- * either base64 or q-p, switch it to 8bit instead.
- * RFC 2046 [5.2.2, 5.2.3, 5.2.4] - All other messages
- * only allow 7bit encodings.
- *
- * TODO: What if message contains 8bit characters and we are
- * in strict 7bit mode? Not sure there is anything we can do
- * in that situation, especially for message/rfc822 parts.
- *
- * These encoding will be figured out later (via toString()).
- * They are limited to 7bit, 8bit, and binary. Default to
- * '7bit' per RFCs. */
- $default_8bit = 'base64';
- $encoding = '7bit';
- break;
- case 'text':
- $default_8bit = 'quoted-printable';
- $encoding = '7bit';
- break;
- default:
- $default_8bit = 'base64';
- /* If transfer encoding has changed from the default, use that
- * value. */
- $encoding = ($this->_transferEncoding == self::DEFAULT_ENCODING)
- ? 'base64'
- : $this->_transferEncoding;
- break;
- }
- switch ($encoding) {
- case 'base64':
- case 'binary':
- break;
- default:
- $encoding = $this->_scanStream($this->_contents);
- break;
- }
- switch ($encoding) {
- case 'base64':
- case 'binary':
- /* If the text is longer than 998 characters between
- * linebreaks, use quoted-printable encoding to ensure the
- * text will not be chopped (i.e. by sendmail if being
- * sent as mail text). */
- $encoding = $default_8bit;
- break;
- case '8bit':
- $encoding = (($encode & self::ENCODE_8BIT) || ($encode & self::ENCODE_BINARY))
- ? '8bit'
- : $default_8bit;
- break;
- }
- }
- $this->_temp['sendTransferEncoding'][$encode] = $encoding;
- return $encoding;
- }
- /**
- * Replace newlines in this part's contents with those specified by either
- * the given newline sequence or the part's current EOL setting.
- *
- * @param mixed $text The text to replace. Either a string or a
- * stream resource. If a stream, and returning
- * a string, will close the stream when done.
- * @param string $eol The EOL sequence to use. If not present, uses
- * the part's current EOL setting.
- * @param boolean $stream If true, returns a stream resource.
- *
- * @return string The text with the newlines replaced by the desired
- * newline sequence (returned as a stream resource if
- * $stream is true).
- */
- public function replaceEOL($text, $eol = null, $stream = false)
- {
- if (is_null($eol)) {
- $eol = $this->getEOL();
- }
- stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol');
- $fp = $this->_writeStream($text, array(
- 'filter' => array(
- 'horde_eol' => array('eol' => $eol)
- )
- ));
- return $stream ? $fp : $this->_readStream($fp, true);
- }
- /**
- * Determine the size of this MIME part and its child members.
- *
- * @todo Remove $approx parameter.
- *
- * @param boolean $approx If true, determines an approximate size for
- * parts consisting of base64 encoded data.
- *
- * @return integer Size of the part, in bytes.
- */
- public function getBytes($approx = false)
- {
- if ($this->getPrimaryType() == 'multipart') {
- if (isset($this->_bytes)) {
- return $this->_bytes;
- }
- $bytes = 0;
- foreach ($this as $part) {
- $bytes += $part->getBytes($approx);
- }
- return $bytes;
- }
- if ($this->_contents) {
- fseek($this->_contents, 0, SEEK_END);
- $bytes = ftell($this->_contents);
- } else {
- $bytes = $this->_bytes;
- /* Base64 transfer encoding is approx. 33% larger than original
- * data size (RFC 2045 [6.8]). */
- if ($approx && ($this->_transferEncoding == 'base64')) {
- $bytes *= 0.75;
- }
- }
- return intval($bytes);
- }
- /**
- * Explicitly set the size (in bytes) of this part. This value will only
- * be returned (via getBytes()) if there are no contents currently set.
- *
- * This function is useful for setting the size of the part when the
- * contents of the part are not fully loaded (i.e. creating a
- * Horde_Mime_Part object from IMAP header information without loading the
- * data of the part).
- *
- * @param integer $bytes The size of this part in bytes.
- */
- public function setBytes($bytes)
- {
- /* Consider 'size' disposition parameter to be the canonical size.
- * Only set bytes if that value doesn't exist. */
- if (!$this->getDispositionParameter('size')) {
- $this->setDispositionParameter('size', $bytes);
- }
- }
- /**
- * Output the size of this MIME part in KB.
- *
- * @todo Remove $approx parameter.
- *
- * @param boolean $approx If true, determines an approximate size for
- * parts consisting of base64 encoded data.
- *
- * @return string Size of the part in KB.
- */
- public function getSize($approx = false)
- {
- if (!($bytes = $this->getBytes($approx))) {
- return 0;
- }
- $localeinfo = Horde_Nls::getLocaleInfo();
- // TODO: Workaround broken number_format() prior to PHP 5.4.0.
- return str_replace(
- array('X', 'Y'),
- array($localeinfo['decimal_point'], $localeinfo['thousands_sep']),
- number_format(ceil($bytes / 1024), 0, 'X', 'Y')
- );
- }
- /**
- * Sets the Content-ID header for this part.
- *
- * @param string $cid Use this CID (if not already set). Else, generate
- * a random CID.
- *
- * @return string The Content-ID for this part.
- */
- public function setContentId($cid = null)
- {
- if (!is_null($id = $this->getContentId())) {
- return $id;
- }
- $this->_headers->addHeaderOb(
- is_null($cid)
- ? Horde_Mime_Headers_ContentId::create()
- : new Horde_Mime_Headers_ContentId(null, $cid)
- );
- return $this->getContentId();
- }
- /**
- * Returns the Content-ID for this part.
- *
- * @return string The Content-ID for this part (null if not set).
- */
- public function getContentId()
- {
- return ($hdr = $this->_headers['content-id'])
- ? trim($hdr->value, '<>')
- : null;
- }
- /**
- * Alter the MIME ID of this part.
- *
- * @param string $mimeid The MIME ID.
- */
- public function setMimeId($mimeid)
- {
- $this->_mimeid = $mimeid;
- }
- /**
- * Returns the MIME ID of this part.
- *
- * @return string The MIME ID.
- */
- public function getMimeId()
- {
- return $this->_mimeid;
- }
- /**
- * Build the MIME IDs for this part and all subparts.
- *
- * @param string $id The ID of this part.
- * @param boolean $rfc822 Is this a message/rfc822 part?
- */
- public function buildMimeIds($id = null, $rfc822 = false)
- {
- $this->_status &= ~self::STATUS_REINDEX;
- if (is_null($id)) {
- $rfc822 = true;
- $id = '';
- }
- if ($rfc822) {
- if (empty($this->_parts) &&
- ($this->getPrimaryType() != 'multipart')) {
- $this->setMimeId($id . '1');
- } else {
- if (empty($id) && ($this->getType() == 'message/rfc822')) {
- $this->setMimeId('1.0');
- } else {
- $this->setMimeId($id . '0');
- }
- $i = 1;
- foreach ($this as $val) {
- $val->buildMimeIds($id . ($i++));
- }
- }
- } else {
- $this->setMimeId($id);
- $id = $id
- ? ((substr($id, -2) === '.0') ? substr($id, 0, -1) : ($id . '.'))
- : '';
- if (count($this)) {
- if ($this->getType() == 'message/rfc822') {
- $this->rewind();
- $this->current()->buildMimeIds($id, true);
- } else {
- $i = 1;
- foreach ($this as $val) {
- $val->buildMimeIds($id . ($i++));
- }
- }
- }
- }
- }
- /**
- * Is this the base MIME part?
- *
- * @param boolean $base True if this is the base MIME part.
- */
- public function isBasePart($base)
- {
- if (empty($base)) {
- $this->_status &= ~self::STATUS_BASEPART;
- } else {
- $this->_status |= self::STATUS_BASEPART;
- }
- }
- /**
- * Determines if this MIME part is an attachment for display purposes.
- *
- * @since Horde_Mime 2.10.0
- *
- * @return boolean True if this part should be considered an attachment.
- */
- public function isAttachment()
- {
- $type = $this->getType();
- switch ($type) {
- case 'application/ms-tnef':
- case 'application/pgp-keys':
- case 'application/vnd.ms-tnef':
- return false;
- }
- if ($this->parent) {
- switch ($this->parent->getType()) {
- case 'multipart/encrypted':
- switch ($type) {
- case 'application/octet-stream':
- return false;
- }
- break;
- case 'multipart/signed':
- switch ($type) {
- case 'application/pgp-signature':
- case 'application/pkcs7-signature':
- case 'application/x-pkcs7-signature':
- return false;
- }
- break;
- }
- }
- switch ($this->getDisposition()) {
- case 'attachment':
- return true;
- }
- switch ($this->getPrimaryType()) {
- case 'application':
- if (strlen($this->getName())) {
- return true;
- }
- break;
- case 'audio':
- case 'video':
- return true;
- case 'multipart':
- return false;
- }
- return false;
- }
- /**
- * Set a piece of metadata on this object.
- *
- * @param string $key The metadata key.
- * @param mixed $data The metadata. If null, clears the key.
- */
- public function setMetadata($key, $data = null)
- {
- if (is_null($data)) {
- unset($this->_metadata[$key]);
- } else {
- $this->_metadata[$key] = $data;
- }
- }
- /**
- * Retrieves metadata from this object.
- *
- * @param string $key The metadata key.
- *
- * @return mixed The metadata, or null if it doesn't exist.
- */
- public function getMetadata($key)
- {
- return isset($this->_metadata[$key])
- ? $this->_metadata[$key]
- : null;
- }
- /**
- * Sends this message.
- *
- * @param string $email The address list to send to.
- * @param Horde_Mime_Headers $headers The Horde_Mime_Headers object
- * holding this message's headers.
- * @param Horde_Mail_Transport $mailer A Horde_Mail_Transport object.
- * @param array $opts Additional options:
- * <pre>
- * - broken_rfc2231: (boolean) Attempt to work around non-RFC
- * 2231-compliant MUAs by generating both a RFC
- * 2047-like parameter name and also the correct RFC
- * 2231 parameter (@since 2.5.0).
- * DEFAULT: false
- * - encode: (integer) The encoding to use. A mask of self::ENCODE_*
- * values.
- * DEFAULT: Auto-determined based on transport driver.
- * </pre>
- *
- * @throws Horde_Mime_Exception
- * @throws InvalidArgumentException
- */
- public function send($email, $headers, Horde_Mail_Transport $mailer,
- array $opts = array())
- {
- $old_status = $this->_status;
- $this->isBasePart(true);
- /* Does the SMTP backend support 8BITMIME (RFC 1652)? */
- $canonical = true;
- $encode = self::ENCODE_7BIT;
- if (isset($opts['encode'])) {
- /* Always allow 7bit encoding. */
- $encode |= $opts['encode'];
- } elseif ($mailer instanceof Horde_Mail_Transport_Smtp) {
- try {
- $smtp_ext = $mailer->getSMTPObject()->getServiceExtensions();
- if (isset($smtp_ext['8BITMIME'])) {
- $encode |= self::ENCODE_8BIT;
- }
- } catch (Horde_Mail_Exception $e) {}
- $canonical = false;
- } elseif ($mailer instanceof Horde_Mail_Transport_Smtphorde) {
- try {
- if ($mailer->getSMTPObject()->data_8bit) {
- $encode |= self::ENCODE_8BIT;
- }
- } catch (Horde_Mail_Exception $e) {}
- $canonical = false;
- }
- $msg = $this->toString(array(
- 'canonical' => $canonical,
- 'encode' => $encode,
- 'headers' => false,
- 'stream' => true
- ));
- /* Add MIME Headers if they don't already exist. */
- if (!isset($headers['MIME-Version'])) {
- $headers = $this->addMimeHeaders(array(
- 'encode' => $encode,
- 'headers' => $headers
- ));
- }
- if (!empty($this->_temp['toString'])) {
- $headers->addHeader(
- 'Content-Transfer-Encoding',
- $this->_temp['toString']
- );
- switch ($this->_temp['toString']) {
- case '8bit':
- if ($mailer instanceof Horde_Mail_Transport_Smtp) {
- $mailer->addServiceExtensionParameter('BODY', '8BITMIME');
- }
- break;
- }
- }
- $this->_status = $old_status;
- $rfc822 = new Horde_Mail_Rfc822();
- try {
- $mailer->send($rfc822->parseAddressList($email)->writeAddress(array(
- 'encode' => $this->getHeaderCharset() ?: true,
- 'idn' => true
- )), $headers->toArray(array(
- 'broken_rfc2231' => !empty($opts['broken_rfc2231']),
- 'canonical' => $canonical,
- 'charset' => $this->getHeaderCharset()
- )), $msg);
- } catch (InvalidArgumentException $e) {
- // Try to rebuild the part in case it was due to
- // an invalid line length in a rfc822/message attachment.
- if ($this->_failed) {
- throw $e;
- }
- $this->_failed = true;
- $this->_sanityCheckRfc822Attachments();
- try {
- $this->send($email, $headers, $mailer, $opts);
- } catch (Horde_Mail_Exception $e) {
- throw new Horde_Mime_Exception($e);
- }
- } catch (Horde_Mail_Exception $e) {
- throw new Horde_Mime_Exception($e);
- }
- }
- /**
- * Finds the main "body" text part (if any) in a message.
- * "Body" data is the first text part under this part.
- *
- * @param string $subtype Specifically search for this subtype.
- *
- * @return mixed The MIME ID of the main body part, or null if a body
- * part is not found.
- */
- public function findBody($subtype = null)
- {
- $this->buildMimeIds();
- foreach ($this->partIterator() as $val) {
- $id = $val->getMimeId();
- if (($val->getPrimaryType() == 'text') &&
- ((intval($id) === 1) || !$this->getMimeId()) &&
- (is_null($subtype) || ($val->getSubType() == $subtype)) &&
- ($val->getDisposition() !== 'attachment')) {
- return $id;
- }
- }
- return null;
- }
- /**
- * Returns the recursive iterator needed to iterate through this part.
- *
- * @since 2.8.0
- *
- * @param boolean $current Include the current part as the base?
- *
- * @return Iterator Recursive iterator.
- */
- public function partIterator($current = true)
- {
- $this->_reindex(true);
- return new Horde_Mime_Part_Iterator($this, $current);
- }
- /**
- * Returns a subpart by index.
- *
- * @return Horde_Mime_Part Part, or null if not found.
- */
- public function getPartByIndex($index)
- {
- if (!isset($this->_parts[$index])) {
- return null;
- }
- $part = $this->_parts[$index];
- $part->parent = $this;
- return $part;
- }
- /**
- * Reindexes the MIME IDs, if necessary.
- *
- * @param boolean $force Reindex if the current part doesn't have an ID.
- */
- protected function _reindex($force = false)
- {
- $id = $this->getMimeId();
- if (($this->_status & self::STATUS_REINDEX) ||
- ($force && is_null($id))) {
- $this->buildMimeIds(
- is_null($id)
- ? (($this->getPrimaryType() === 'multipart') ? '0' : '1')
- : $id
- );
- }
- }
- /**
- * Write data to a stream.
- *
- * @param array $data The data to write. Either a stream resource or
- * a string.
- * @param array $options Additional options:
- * - error: (boolean) Catch errors when writing to the stream. Throw an
- * ErrorException if an error is found.
- * DEFAULT: false
- * - filter: (array) Filter(s) to apply to the string. Keys are the
- * filter names, values are filter params.
- * - fp: (resource) Use this stream instead of creating a new one.
- *
- * @return resource The stream resource.
- * @throws ErrorException
- */
- protected function _writeStream($data, $options = array())
- {
- if (empty($options['fp'])) {
- $fp = fopen('php://temp/maxmemory:' . self::$memoryLimit, 'r+');
- } else {
- $fp = $options['fp'];
- fseek($fp, 0, SEEK_END);
- }
- if (!is_array($data)) {
- $data = array($data);
- }
- $append_filter = array();
- if (!empty($options['filter'])) {
- foreach ($options['filter'] as $key => $val) {
- $append_filter[] = stream_filter_append($fp, $key, STREAM_FILTER_WRITE, $val);
- }
- }
- if (!empty($options['error'])) {
- set_error_handler(function($errno, $errstr) {
- throw new ErrorException($errstr, $errno);
- });
- $error = null;
- }
- try {
- foreach ($data as $d) {
- if (is_resource($d)) {
- rewind($d);
- while (!feof($d)) {
- fwrite($fp, fread($d, 8192));
- }
- } elseif (is_string($d)) {
- $len = strlen($d);
- $i = 0;
- while ($i < $len) {
- fwrite($fp, substr($d, $i, 8192));
- $i += 8192;
- }
- }
- }
- } catch (ErrorException $e) {
- $error = $e;
- }
- foreach ($append_filter as $val) {
- stream_filter_remove($val);
- }
- if (!empty($options['error'])) {
- restore_error_handler();
- if ($error) {
- throw $error;
- }
- }
- return $fp;
- }
- /**
- * Read data from a stream.
- *
- * @param resource $fp An active stream.
- * @param boolean $close Close the stream when done reading?
- *
- * @return string The data from the stream.
- */
- protected function _readStream($fp, $close = false)
- {
- $out = '';
- if (!is_resource($fp)) {
- return $out;
- }
- rewind($fp);
- while (!feof($fp)) {
- $out .= fread($fp, 8192);
- }
- if ($close) {
- fclose($fp);
- }
- return $out;
- }
- /**
- * Scans a stream for content type.
- *
- * @param resource $fp A stream resource.
- *
- * @return mixed Either 'binary', '8bit', or false.
- */
- protected function _scanStream($fp)
- {
- rewind($fp);
- stream_filter_register(
- 'horde_mime_scan_stream',
- 'Horde_Mime_Filter_Encoding'
- );
- $filter_params = new stdClass;
- $filter = stream_filter_append(
- $fp,
- 'horde_mime_scan_stream',
- STREAM_FILTER_READ,
- $filter_params
- );
- while (!feof($fp)) {
- fread($fp, 8192);
- }
- stream_filter_remove($filter);
- return $filter_params->body;
- }
- /* Static methods. */
- /**
- * Attempts to build a Horde_Mime_Part object from message text.
- *
- * @param string $text The text of the MIME message.
- * @param array $opts Additional options:
- * - forcemime: (boolean) If true, the message data is assumed to be
- * MIME data. If not, a MIME-Version header must exist (RFC
- * 2045 [4]) to be parsed as a MIME message.
- * DEFAULT: false
- * - level: (integer) Current nesting level of the MIME data.
- * DEFAULT: 0
- * - no_body: (boolean) If true, don't set body contents of parts (since
- * 2.2.0).
- * DEFAULT: false
- *
- * @return Horde_Mime_Part A MIME Part object.
- * @throws Horde_Mime_Exception
- */
- public static function parseMessage($text, array $opts = array())
- {
- /* Mini-hack to get a blank Horde_Mime part so we can call
- * replaceEOL(). Convert to EOL, since that is the expected EOL for
- * use internally within a Horde_Mime_Part object. */
- $part = new Horde_Mime_Part();
- $rawtext = $part->replaceEOL($text, self::EOL);
- /* Find the header. */
- $hdr_pos = self::_findHeader($rawtext, self::EOL);
- unset($opts['ctype']);
- $ob = self::_getStructure(substr($rawtext, 0, $hdr_pos), substr($rawtext, $hdr_pos + 2), $opts);
- $ob->buildMimeIds();
- return $ob;
- }
- /**
- * Creates a MIME object from the text of one part of a MIME message.
- *
- * @param string $header The header text.
- * @param string $body The body text.
- * @param array $opts Additional options:
- * <pre>
- * - ctype: (string) The default content-type.
- * - forcemime: (boolean) If true, the message data is assumed to be
- * MIME data. If not, a MIME-Version header must exist to
- * be parsed as a MIME message.
- * - level: (integer) Current nesting level.
- * - no_body: (boolean) If true, don't set body contents of parts.
- * </pre>
- *
- * @return Horde_Mime_Part The MIME part object.
- */
- protected static function _getStructure($header, $body,
- array $opts = array())
- {
- $opts = array_merge(array(
- 'ctype' => 'text/plain',
- 'forcemime' => false,
- 'level' => 0,
- 'no_body' => false
- ), $opts);
- /* Parse headers text into a Horde_Mime_Headers object. */
- $hdrs = Horde_Mime_Headers::parseHeaders($header);
- $ob = new Horde_Mime_Part();
- /* This is not a MIME message. */
- if (!$opts['forcemime'] && !isset($hdrs['MIME-Version'])) {
- $ob->setType('text/plain');
- if ($len = strlen($body)) {
- if ($opts['no_body']) {
- $ob->setBytes($len);
- } else {
- $ob->setContents($body);
- }
- }
- return $ob;
- }
- /* Content type. */
- if ($tmp = $hdrs['Content-Type']) {
- $ob->setType($tmp->value);
- foreach ($tmp->params as $key => $val) {
- $ob->setContentTypeParameter($key, $val);
- }
- } else {
- $ob->setType($opts['ctype']);
- }
- /* Content transfer encoding. */
- if ($tmp = $hdrs['Content-Transfer-Encoding']) {
- $ob->setTransferEncoding(strval($tmp));
- }
- /* Content-Description. */
- if ($tmp = $hdrs['Content-Description']) {
- $ob->setDescription(strval($tmp));
- }
- /* Content-Disposition. */
- if ($tmp = $hdrs['Content-Disposition']) {
- $ob->setDisposition($tmp->value);
- foreach ($tmp->params as $key => $val) {
- $ob->setDispositionParameter($key, $val);
- }
- }
- /* Content-Duration */
- if ($tmp = $hdrs['Content-Duration']) {
- $ob->setDuration(strval($tmp));
- }
- /* Content-ID. */
- if ($tmp = $hdrs['Content-Id']) {
- $ob->setContentId(strval($tmp));
- }
- if (($len = strlen($body)) && ($ob->getPrimaryType() != 'multipart')) {
- if ($opts['no_body']) {
- $ob->setBytes($len);
- } else {
- $ob->setContents($body);
- }
- }
- if (++$opts['level'] >= self::NESTING_LIMIT) {
- return $ob;
- }
- /* Process subparts. */
- switch ($ob->getPrimaryType()) {
- case 'message':
- if ($ob->getSubType() == 'rfc822') {
- $ob[] = self::parseMessage($body, array(
- 'forcemime' => true,
- 'no_body' => $opts['no_body']
- ));
- }
- break;
- case 'multipart':
- $boundary = $ob->getContentTypeParameter('boundary');
- if (!is_null($boundary)) {
- foreach (self::_findBoundary($body, 0, $boundary) as $val) {
- if (!isset($val['length'])) {
- break;
- }
- $subpart = substr($body, $val['start'], $val['length']);
- $hdr_pos = self::_findHeader($subpart, self::EOL);
- $ob[] = self::_getStructure(
- substr($subpart, 0, $hdr_pos),
- substr($subpart, $hdr_pos + 2),
- array(
- 'ctype' => ($ob->getSubType() == 'digest') ? 'message/rfc822' : 'text/plain',
- 'forcemime' => true,
- 'level' => $opts['level'],
- 'no_body' => $opts['no_body']
- )
- );
- }
- }
- break;
- }
- return $ob;
- }
- /**
- * Attempts to obtain the raw text of a MIME part.
- *
- * @param mixed $text The full text of the MIME message. The text is
- * assumed to be MIME data (no MIME-Version checking
- * is performed). It can be either a stream or a
- * string.
- * @param string $type Either 'header' or 'body'.
- * @param string $id The MIME ID.
- *
- * @return string The raw text.
- * @throws Horde_Mime_Exception
- */
- public static function getRawPartText($text, $type, $id)
- {
- /* Mini-hack to get a blank Horde_Mime part so we can call
- * replaceEOL(). From an API perspective, getRawPartText() should be
- * static since it is not working on MIME part data. */
- $part = new Horde_Mime_Part();
- $rawtext = $part->replaceEOL($text, self::RFC_EOL);
- /* We need to carry around the trailing "\n" because this is needed
- * to correctly find the boundary string. */
- $hdr_pos = self::_findHeader($rawtext, self::RFC_EOL);
- $curr_pos = $hdr_pos + 3;
- if ($id == 0) {
- switch ($type) {
- case 'body':
- return substr($rawtext, $curr_pos + 1);
- case 'header':
- return trim(substr($rawtext, 0, $hdr_pos));
- }
- }
- $hdr_ob = Horde_Mime_Headers::parseHeaders(trim(substr($rawtext, 0, $hdr_pos)));
- /* If this is a message/rfc822, pass the body into the next loop.
- * Don't decrement the ID here. */
- if (($ct = $hdr_ob['Content-Type']) && ($ct == 'message/rfc822')) {
- return self::getRawPartText(
- substr($rawtext, $curr_pos + 1),
- $type,
- $id
- );
- }
- $base_pos = strpos($id, '.');
- $orig_id = $id;
- if ($base_pos !== false) {
- $id = substr($id, $base_pos + 1);
- $base_pos = substr($orig_id, 0, $base_pos);
- } else {
- $base_pos = $id;
- $id = 0;
- }
- if ($ct && !isset($ct->params['boundary'])) {
- if ($orig_id == '1') {
- return substr($rawtext, $curr_pos + 1);
- }
- throw new Horde_Mime_Exception('Could not find MIME part.');
- }
- $b_find = self::_findBoundary(
- $rawtext,
- $curr_pos,
- $ct->params['boundary'],
- $base_pos
- );
- if (!isset($b_find[$base_pos])) {
- throw new Horde_Mime_Exception('Could not find MIME part.');
- }
- return self::getRawPartText(
- substr(
- $rawtext,
- $b_find[$base_pos]['start'],
- $b_find[$base_pos]['length'] - 1
- ),
- $type,
- $id
- );
- }
- /**
- * Find the location of the end of the header text.
- *
- * @param string $text The text to search.
- * @param string $eol The EOL string.
- *
- * @return integer Header position.
- */
- protected static function _findHeader($text, $eol)
- {
- $hdr_pos = strpos($text, $eol . $eol);
- return ($hdr_pos === false)
- ? strlen($text)
- : $hdr_pos;
- }
- /**
- * Find the location of the next boundary string.
- *
- * @param string $text The text to search.
- * @param integer $pos The current position in $text.
- * @param string $boundary The boundary string.
- * @param integer $end If set, return after matching this many
- * boundaries.
- *
- * @return array Keys are the boundary number, values are an array with
- * two elements: 'start' and 'length'.
- */
- protected static function _findBoundary($text, $pos, $boundary,
- $end = null)
- {
- $i = 0;
- $out = array();
- $search = "--" . $boundary;
- $search_len = strlen($search);
- while (($pos = strpos($text, $search, $pos)) !== false) {
- /* Boundary needs to appear at beginning of string or right after
- * a LF. */
- if (($pos != 0) && ($text[$pos - 1] != "\n")) {
- continue;
- }
- if (isset($out[$i])) {
- $out[$i]['length'] = $pos - $out[$i]['start'] - 1;
- }
- if (!is_null($end) && ($end == $i)) {
- break;
- }
- $pos += $search_len;
- if (isset($text[$pos])) {
- switch ($text[$pos]) {
- case "\r":
- $pos += 2;
- $out[++$i] = array('start' => $pos);
- break;
- case "\n":
- $out[++$i] = array('start' => ++$pos);
- break;
- case '-':
- return $out;
- }
- }
- }
- return $out;
- }
- /**
- * Re-enocdes message/rfc822 parts in case there was e.g., some broken
- * line length in the headers of the message in the part. Since we shouldn't
- * alter the original message in any way, we simply reset cause the part to
- * be encoded as base64 and sent as a application/octet part.
- */
- protected function _sanityCheckRfc822Attachments()
- {
- if ($this->getType() == 'message/rfc822') {
- $this->_reEncodeMessageAttachment($this);
- return;
- }
- foreach ($this->getParts() as $part) {
- if ($part->getType() == 'message/rfc822') {
- $this->_reEncodeMessageAttachment($part);
- }
- }
- return;
- }
- /**
- * Rebuilds $part and forces it to be a base64 encoded
- * application/octet-stream part.
- *
- * @param Horde_Mime_Part $part The MIME part.
- */
- protected function _reEncodeMessageAttachment(Horde_Mime_Part $part)
- {
- $new_part = Horde_Mime_Part::parseMessage($part->getContents());
- $part->setContents($new_part->getContents(array('stream' => true)), array('encoding' => self::ENCODE_BINARY));
- $part->setTransferEncoding('base64', array('send' => true));
- }
- /* ArrayAccess methods. */
- /**
- */
- public function offsetExists($offset)
- {
- return ($this[$offset] !== null);
- }
- /**
- */
- public function offsetGet($offset)
- {
- $this->_reindex();
- if (strcmp($offset, $this->getMimeId()) === 0) {
- $this->parent = null;
- return $this;
- }
- foreach ($this->_parts as $val) {
- if (strcmp($offset, $val->getMimeId()) === 0) {
- $val->parent = $this;
- return $val;
- }
- if ($found = $val[$offset]) {
- return $found;
- }
- }
- return null;
- }
- /**
- */
- public function offsetSet($offset, $value)
- {
- if (is_null($offset)) {
- $this->_parts[] = $value;
- $this->_status |= self::STATUS_REINDEX;
- } elseif ($part = $this[$offset]) {
- if ($part->parent === $this) {
- if (($k = array_search($part, $this->_parts, true)) !== false) {
- $value->setMimeId($part->getMimeId());
- $this->_parts[$k] = $value;
- }
- } else {
- $this->parent[$offset] = $value;
- }
- }
- }
- /**
- */
- public function offsetUnset($offset)
- {
- if ($part = $this[$offset]) {
- if ($part->parent === $this) {
- if (($k = array_search($part, $this->_parts, true)) !== false) {
- unset($this->_parts[$k]);
- $this->_parts = array_values($this->_parts);
- }
- } else {
- unset($part->parent[$offset]);
- }
- $this->_status |= self::STATUS_REINDEX;
- }
- }
- /* Countable methods. */
- /**
- * Returns the number of child message parts (doesn't include
- * grandchildren or more remote ancestors).
- *
- * @return integer Number of message parts.
- */
- public function count()
- {
- return count($this->_parts);
- }
- /* RecursiveIterator methods. */
- /**
- * @since 2.8.0
- */
- public function current()
- {
- return (($key = $this->key()) === null)
- ? null
- : $this->getPartByIndex($key);
- }
- /**
- * @since 2.8.0
- */
- public function key()
- {
- return (isset($this->_temp['iterate']) && isset($this->_parts[$this->_temp['iterate']]))
- ? $this->_temp['iterate']
- : null;
- }
- /**
- * @since 2.8.0
- */
- public function next()
- {
- ++$this->_temp['iterate'];
- }
- /**
- * @since 2.8.0
- */
- public function rewind()
- {
- $this->_reindex();
- reset($this->_parts);
- $this->_temp['iterate'] = key($this->_parts);
- }
- /**
- * @since 2.8.0
- */
- public function valid()
- {
- return ($this->key() !== null);
- }
- /**
- * @since 2.8.0
- */
- public function hasChildren()
- {
- return (($curr = $this->current()) && count($curr));
- }
- /**
- * @since 2.8.0
- */
- public function getChildren()
- {
- return $this->current();
- }
- /* Serializable methods. */
- /**
- * Serialization.
- *
- * @return string Serialized data.
- */
- public function serialize()
- {
- $data = array(
- // Serialized data ID.
- self::VERSION,
- $this->_bytes,
- $this->_eol,
- $this->_hdrCharset,
- $this->_headers,
- $this->_metadata,
- $this->_mimeid,
- $this->_parts,
- $this->_status,
- $this->_transferEncoding
- );
- if (!empty($this->_contents)) {
- $data[] = $this->_readStream($this->_contents);
- }
- return serialize($data);
- }
- /**
- * Unserialization.
- *
- * @param string $data Serialized data.
- *
- * @throws Exception
- */
- public function unserialize($data)
- {
- $data = @unserialize($data);
- if (!is_array($data) ||
- !isset($data[0]) ||
- ($data[0] != self::VERSION)) {
- switch ($data[0]) {
- case 1:
- $convert = new Horde_Mime_Part_Upgrade_V1($data);
- $data = $convert->data;
- break;
- default:
- $data = null;
- break;
- }
- if (is_null($data)) {
- throw new Exception('Cache version change');
- }
- }
- $key = 0;
- $this->_bytes = $data[++$key];
- $this->_eol = $data[++$key];
- $this->_hdrCharset = $data[++$key];
- $this->_headers = $data[++$key];
- $this->_metadata = $data[++$key];
- $this->_mimeid = $data[++$key];
- $this->_parts = $data[++$key];
- $this->_status = $data[++$key];
- $this->_transferEncoding = $data[++$key];
- if (isset($data[++$key])) {
- $this->setContents($data[$key]);
- }
- }
- /* Deprecated elements. */
- /**
- * @deprecated
- */
- const UNKNOWN = 'x-unknown';
- /**
- * @deprecated
- */
- public static $encodingTypes = array(
- '7bit', '8bit', 'base64', 'binary', 'quoted-printable',
- // Non-RFC types, but old mailers may still use
- 'uuencode', 'x-uuencode', 'x-uue'
- );
- /**
- * @deprecated
- */
- public static $mimeTypes = array(
- 'text', 'multipart', 'message', 'application', 'audio', 'image',
- 'video', 'model'
- );
- /**
- * @deprecated Use setContentTypeParameter with a null $data value.
- */
- public function clearContentTypeParameter($label)
- {
- $this->setContentTypeParam($label, null);
- }
- /**
- * @deprecated Use iterator instead.
- */
- public function contentTypeMap($sort = true)
- {
- $map = array();
- foreach ($this->partIterator() as $val) {
- $map[$val->getMimeId()] = $val->getType();
- }
- return $map;
- }
- /**
- * @deprecated Use array access instead.
- */
- public function addPart($mime_part)
- {
- $this[] = $mime_part;
- }
- /**
- * @deprecated Use array access instead.
- */
- public function getPart($id)
- {
- return $this[$id];
- }
- /**
- * @deprecated Use array access instead.
- */
- public function alterPart($id, $mime_part)
- {
- $this[$id] = $mime_part;
- }
- /**
- * @deprecated Use array access instead.
- */
- public function removePart($id)
- {
- unset($this[$id]);
- }
- }