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.

536 lines
15 KiB

<?php
/**
* Copyright 2002-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 2002-2017 Horde LLC
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Mime
*/
/**
* This class represents the collection of header values for a single mail
* message part.
*
* It supports the base e-mail spec (RFC 5322) and the MIME extensions to that
* spec (RFC 2045).
*
* @author Michael Slusarz <slusarz@horde.org>
* @category Horde
* @copyright 2002-2017 Horde LLC
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Mime
*/
class Horde_Mime_Headers
implements ArrayAccess, IteratorAggregate, Serializable
{
/* Serialized version. */
const VERSION = 3;
/**
* 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';
/**
* Cached handler information for Header Element objects.
*
* @var array
*/
protected static $_handlers = array();
/**
* The internal headers array.
*
* @var Horde_Support_CaseInsensitiveArray
*/
protected $_headers;
/**
* Constructor.
*/
public function __construct()
{
$this->_headers = new Horde_Support_CaseInsensitiveArray();
}
/**
*/
public function __clone()
{
$copy = new Horde_Support_CaseInsensitiveArray();
foreach ($this->_headers as $key => $val) {
$copy[$key] = clone $val;
}
$this->_headers = $copy;
}
/**
* Returns the headers in array format.
*
* @param array $opts Optional parameters:
* <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
* DEFAULT: false
* - canonical: (boolean) Use canonical (RFC 822/2045) CRLF EOLs?
* DEFAULT: Uses "\n"
* - charset: (string) Encodes the headers using this charset. If empty,
* encodes using UTF-8.
* DEFAULT: No encoding.
* - defserver: (string) The default domain to append to mailboxes.
* DEFAULT: No default name.
* - lang: (string) The language to use when encoding.
* DEFAULT: None specified
* - nowrap: (integer) Don't wrap the headers.
* DEFAULT: Headers are wrapped.
* </pre>
*
* @return array The headers in array format. Keys are header names, but
* case sensitivity cannot be guaranteed. Values are
* header values.
*/
public function toArray(array $opts = array())
{
$charset = array_key_exists('charset', $opts)
? (empty($opts['charset']) ? 'UTF-8' : $opts['charset'])
: null;
$eol = empty($opts['canonical'])
? $this->_eol
: "\r\n";
$ret = array();
foreach ($this->_headers as $ob) {
$sopts = array(
'charset' => $charset
);
if (($ob instanceof Horde_Mime_Headers_Addresses) ||
($ob instanceof Horde_Mime_Headers_AddressesMulti)) {
if (!empty($opts['defserver'])) {
$sopts['defserver'] = $opts['defserver'];
}
} elseif ($ob instanceof Horde_Mime_Headers_ContentParam) {
$sopts['broken_rfc2231'] = !empty($opts['broken_rfc2231']);
if (!empty($opts['lang'])) {
$sopts['lang'] = $opts['lang'];
}
}
$tmp = array();
foreach ($ob->sendEncode(array_filter($sopts)) as $val) {
if (empty($opts['nowrap'])) {
/* Remove any existing linebreaks and wrap the line. */
$htext = $ob->name . ': ';
$val = ltrim(
substr(
wordwrap(
$htext . strtr(trim($val), array("\r" => '', "\n" => '')),
76,
$eol . ' '
),
strlen($htext)
)
);
}
$tmp[] = $val;
}
$ret[$ob->name] = (count($tmp) == 1)
? reset($tmp)
: $tmp;
}
return $ret;
}
/**
* Returns all headers concatenated into a single string.
*
* @param array $opts See toArray().
*
* @return string The headers in string format.
*/
public function toString(array $opts = array())
{
$eol = empty($opts['canonical'])
? $this->_eol
: "\r\n";
$text = '';
foreach ($this->toArray($opts) as $key => $val) {
foreach ((is_array($val) ? $val : array($val)) as $entry) {
$text .= $key . ': ' . $entry . $eol;
}
}
return $text . $eol;
}
/**
* Add/append/replace a header.
*
* @param string $header The header name.
* @param string $value The header value (UTF-8).
* @param array $opts DEPRECATED
*/
public function addHeader($header, $value, array $opts = array())
{
/* Existing header? Add to that object. */
$header = trim($header);
if ($hdr = $this[$header]) {
$hdr->setValue($value);
return;
}
$classname = $this->_getHeaderClassName($header);
try {
$ob = new $classname($header, $value);
} catch (InvalidArgumentException $e) {
/* Ignore an invalid header. */
return;
} catch (Horde_Mime_Exception $e) {
return;
}
switch ($classname) {
case 'Horde_Mime_Headers_ContentParam_ContentDisposition':
case 'Horde_Mime_Headers_ContentParam_ContentType':
/* BC */
if (!empty($opts['params'])) {
foreach ($opts['params'] as $key => $val) {
$ob[$key] = $val;
}
}
break;
}
$this->_headers[$ob->name] = $ob;
}
/**
* Add a Horde_Mime_Headers_Element object to the current header list.
*
* @since 2.5.0
*
* @param Horde_Mime_Headers_Element $ob Header object to add.
* @param boolean $check Check that the header and object
* type match?
*
* @throws InvalidArgumentException
*/
public function addHeaderOb(Horde_Mime_Headers_Element $ob, $check = false)
{
if ($check) {
$cname = $this->_getHeaderClassName($ob->name);
if (!($ob instanceof $cname)) {
throw new InvalidArgumentException(sprintf(
'Object is not correct class: %s',
$cname
));
}
}
/* Existing header? Add to that object. */
if ($hdr = $this[$ob->name]) {
$hdr->setValue($ob);
} else {
$this->_headers[$ob->name] = $ob;
}
}
/**
* Return the header class to use for a header name.
*
* @param string $header The header name.
*
* @return string The Horde_Mime_Headers_* class to use.
*/
protected function _getHeaderClassName($header)
{
if (empty(self::$_handlers)) {
$search = array(
'Horde_Mime_Headers_Element_Single',
'Horde_Mime_Headers_AddressesMulti',
'Horde_Mime_Headers_Addresses',
'Horde_Mime_Headers_ContentDescription',
'Horde_Mime_Headers_ContentId',
'Horde_Mime_Headers_ContentLanguage',
'Horde_Mime_Headers_ContentParam_ContentDisposition',
'Horde_Mime_Headers_ContentParam_ContentType',
'Horde_Mime_Headers_ContentTransferEncoding',
'Horde_Mime_Headers_Date',
'Horde_Mime_Headers_Identification',
'Horde_Mime_Headers_MessageId',
'Horde_Mime_Headers_Mime',
'Horde_Mime_Headers_MimeVersion',
'Horde_Mime_Headers_Received',
'Horde_Mime_Headers_Subject',
'Horde_Mime_Headers_UserAgent'
);
foreach ($search as $val) {
foreach ($val::getHandles() as $hdr) {
self::$_handlers[$hdr] = $val;
}
}
}
$header = Horde_String::lower($header);
return isset(self::$_handlers[$header])
? self::$_handlers[$header]
: 'Horde_Mime_Headers_Element_Multiple';
}
/**
* Get a header from the header array.
*
* @param string $header The header name.
*
* @return Horde_Mime_Headers_Element Element object, or null if not
* found.
*/
public function getHeader($header)
{
return $this[$header];
}
/**
* Remove a header from the header array.
*
* @param string $header The header name.
*/
public function removeHeader($header)
{
unset($this[$header]);
}
/* Static methods. */
/**
* Builds a Horde_Mime_Headers object from header text.
*
* @param mixed $text A text string (or, as of 2.3.0, a Horde_Stream
* object or stream resource) containing the headers.
*
* @return Horde_Mime_Headers A new Horde_Mime_Headers object.
*/
public static function parseHeaders($text)
{
$curr = null;
$headers = new Horde_Mime_Headers();
$hdr_list = array();
if ($text instanceof Horde_Stream) {
$stream = $text;
$stream->rewind();
} else {
$stream = new Horde_Stream_Temp();
$stream->add($text, true);
}
while (!$stream->eof()) {
if (!($val = rtrim($stream->getToChar("\n", false), "\r"))) {
break;
}
if ($curr && (($val[0] == ' ') || ($val[0] == "\t"))) {
$curr->text .= ' ' . ltrim($val);
} else {
$pos = strpos($val, ':');
$curr = new stdClass;
$curr->header = substr($val, 0, $pos);
$curr->text = ltrim(substr($val, $pos + 1));
$hdr_list[] = $curr;
}
}
foreach ($hdr_list as $val) {
/* When parsing, only keep the FIRST header seen for single value
* text-only headers, since newer headers generally are appended
* to the top of the message. */
if (!($ob = $headers[$val->header]) ||
!($ob instanceof Horde_Mime_Headers_Element_Single) ||
($ob instanceof Horde_Mime_Headers_Addresses)) {
$headers->addHeader($val->header, rtrim($val->text));
}
}
if (!($text instanceof Horde_Stream)) {
$stream->close();
}
return $headers;
}
/* Serializable methods. */
/**
* Serialization.
*
* @return string Serialized data.
*/
public function serialize()
{
$data = array(
// Serialized data ID.
self::VERSION,
$this->_headers->getArrayCopy(),
// TODO: BC
$this->_eol
);
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)) {
throw new Horde_Mime_Exception('Cache version change');
}
$this->_headers = new Horde_Support_CaseInsensitiveArray($data[1]);
// TODO: BC
$this->_eol = $data[2];
}
/* ArrayAccess methods. */
/**
* Does header exist?
*
* @since 2.5.0
*
* @param string $header Header name.
*
* @return boolean True if header exists.
*/
public function offsetExists($offset)
{
return isset($this->_headers[trim($offset)]);
}
/**
* Return header element object.
*
* @since 2.5.0
*
* @param string $header Header name.
*
* @return Horde_Mime_Headers_Element Element object, or null if not
* found.
*/
public function offsetGet($offset)
{
return $this->_headers[trim($offset)];
}
/**
* Store a header element object.
*
* @since 2.5.0
*
* @param string $offset Not used.
* @param Horde_Mime_Headers_Element $elt Header element.
*/
public function offsetSet($offset, $value)
{
$this->addHeaderOb($value);
}
/**
* Remove a header element object.
*
* @since 2.5.0
*
* @param string $offset Header name.
*/
public function offsetUnset($offset)
{
unset($this->_headers[trim($offset)]);
}
/* IteratorAggregate function */
/**
* @since 2.5.0
*/
public function getIterator()
{
return new ArrayIterator($this->_headers);
}
/* Deprecated functions */
/**
* Handle deprecated methods.
*/
public function __call($name, $arguments)
{
$d = new Horde_Mime_Headers_Deprecated($this);
return call_user_func_array(array($d, $name), $arguments);
}
/**
* Handle deprecated static methods.
*/
public static function __callStatic($name, $arguments)
{
$d = new Horde_Mime_Headers_Deprecated();
return call_user_func_array(array($d, $name), $arguments);
}
/**
* @deprecated
*/
protected $_eol = "\n";
/**
* @deprecated
*/
public function setEOL($eol)
{
$this->_eol = $eol;
}
/**
* @deprecated
*/
public function getEOL()
{
return $this->_eol;
}
/* Constants for getValue(). @deprecated */
const VALUE_STRING = 1;
const VALUE_BASE = 2;
const VALUE_PARAMS = 3;
}