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:
*
* - 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.
*
*
* @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:
*
* - 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.
*
*
* @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]);
}
}