You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2528 lines
75 KiB

<?php
/**
* Copyright 1999-2017 Horde LLC (http://www.horde.org/)
*
* See the enclosed file COPYING 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]);
}
}