<?php
/**
- * The Horde_Mime:: class provides methods for dealing with various MIME (see, e.g.,
- * RFC 2045) standards.
+ * The Horde_Mime:: class provides methods for dealing with various MIME (see,
+ * e.g., RFC 2045-2049; 2183; 2231) standards.
*
* Copyright 1999-2008 The Horde Project (http://www.horde.org/)
*
class Horde_Mime
{
/**
+ * 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. See:
+ * http://lists.horde.org/archives/dev/Week-of-Mon-20040426/014240.html
+ *
+ * @var boolean
+ */
+ static public $brokenRFC2231 = false;
+
+ /**
* Determines if a string contains 8-bit (non US-ASCII) characters.
*
* @param string $string The string to check.
}
/**
- * Encodes a parameter string pursuant to RFC 2231.
+ * Encodes a MIME parameter string pursuant to RFC 2183 & 2231
+ * (Content-Type and Content-Disposition headers).
*
* @param string $name The parameter name.
- * @param string $string The string to encode.
+ * @param string $val The parameter value.
* @param string $charset The charset the text should be encoded with.
* @param string $lang The language to use when encoding.
*
* @return array The encoded parameter string.
*/
- static public function encodeParamString($name, $string, $charset,
- $lang = null)
+ static public function encodeParam($name, $val, $charset, $lang = null)
{
$encode = $wrap = false;
+ $i = 0;
$output = array();
- if (self::is8bit($string, $charset)) {
- $string = String::lower($charset) . '\'' . (is_null($lang) ? '' : String::lower($lang)) . '\'' . rawurlencode($string);
+ if (self::is8bit($val, $charset)) {
+ $string = String::lower($charset) . '\'' . (is_null($lang) ? '' : String::lower($lang)) . '\'' . rawurlencode($val);
$encode = true;
+ } else {
+ $string = $val;
}
// 4 = '*', 2x '"', ';'
$pre_len = strlen($name) + 4 + (($encode) ? 1 : 0);
- if (($pre_len + strlen($string)) > 76) {
+ if (($pre_len + strlen($string)) > 75) {
while ($string) {
- $chunk = 76 - $pre_len;
+ $chunk = 75 - $pre_len;
$pos = min($chunk, strlen($string) - 1);
if (($chunk == $pos) && ($pos > 2)) {
- for ($i = 0; $i <= 2; $i++) {
- if ($string[$pos-$i] == '%') {
+ for ($i = 0; $i <= 2; ++$i) {
+ if ($string[$pos - $i] == '%') {
$pos -= $i + 1;
break;
}
$lines = array($string);
}
- $i = 0;
- foreach ($lines as $val) {
- $output[] =
- $name .
- (($wrap) ? ('*' . $i++) : '') .
- (($encode) ? '*' : '') .
- '="' . $val . '"';
+ foreach ($lines as $line) {
+ $output[$name . (($wrap) ? ('*' . $i++) : '') . (($encode) ? '*' : '')] = $line;
}
- return implode('; ', $output);
+ if (self::$brokenRFC2231 && !isset($output[$name])) {
+ $output[$name] = self::encode($val, $charset);
+ }
+
+ return $output;
}
/**
- * Decodes a parameter string encoded pursuant to RFC 2231.
+ * Decodes a header string encoded pursuant to RFC 2231.
*
- * @param string $string The entire string to decode, including the
- * parameter name.
+ * @param string $string The string to decode.
* @param string $to_charset The charset the text should be decoded to.
*
- * @return array The decoded text, or the original string if it was not
- * encoded.
+ * @return array An array with the following entries:
+ * <pre>
+ * </pre>
*/
static public function decodeParamString($string, $to_charset = null)
{
return false;
}
- if (!isset($to_charset)) {
- require_once 'Horde/NLS.php';
- $to_charset = NLS::getCharset();
- }
-
$attribute = substr($string, 0, $pos);
$charset = $lang = null;
$output = '';
* in RFC 2822 [3.2.5].
*
* @param string $str The string to be quoted and escaped.
- * @param string $type Either 'address' or 'personal'.
+ * @param string $type Either 'address', 'personal', or null.
*
* @return string The correctly quoted and escaped string.
*/
* <pre>
* 'charset' => (string) Encodes the headers using this charset.
* DEFAULT: No encoding.
- * 'defserver' => (string) TODO
- * DEFAULT: NO
+ * 'defserver' => (string) The default domain to append to mailboxes.
+ * DEFAULT: No default name.
* 'nowrap' => (integer) Don't wrap the headers.
* DEFAULT: Headers are wrapped.
* </pre>
*/
public function toArray($options = array())
{
+ $charset = empty($options['charset']) ? null : $options['charset'];
+ $address_keys = $charset ? array() : $this->addressFields();
$ret = array();
- $address_keys = empty($options['charset'])
- ? array()
- : $this->addressFields();
foreach ($this->_headers as $header => $ob) {
$val = is_array($ob['value']) ? $ob['value'] : array($ob['value']);
foreach (array_keys($val) as $key) {
- if (!empty($address_keys)) {
- if (in_array($header, $address_keys)) {
- $text = Horde_Mime::encodeAddress($val[$key], empty($options['charset']) ? null : $options['charset'], empty($options['defserver']) ? null : $options['defserver']);
- if (is_a($text, 'PEAR_Error')) {
- $text = $val[$key];
- }
- } else {
- $text = Horde_Mime::encode($val[$key], $options['charset']);
+ if (in_array($header, $address_keys) ) {
+ /* Address encoded headers. */
+ $text = Horde_Mime::encodeAddress($val[$key], $charset, empty($options['defserver']) ? null : $options['defserver']);
+ if (is_a($text, 'PEAR_Error')) {
+ $text = $val[$key];
}
} else {
- $text = $val[$key];
+ $text = $charset
+ ? Horde_Mime::encode($val[$key], $charset)
+ : $val[$key];
+
+ /* MIME encoded headers (RFC 2231). */
+ if (in_array($header, array('content-type', 'content-disposition')) &&
+ !empty($ob['params'])) {
+ foreach ($ob['params'] as $name => $param) {
+ foreach (Horde_Mime::encodeParam($name, $param, $charset) as $name2 => $param2) {
+ /* MIME parameter quoting is identical to RFC
+ * 822 quoted-string encoding. See RFC 2045
+ * [Appendix A]. */
+ $text .= '; ' . $name2 . '=' . Horde_Mime_Address::encode($param2, null);
+ }
+ }
+ }
}
if (empty($options['nowrap'])) {
- $text = $this->wrapHeaders($header, $text);
+ /* Remove any existing linebreaks and wrap the line. */
+ $header_text = $ob['header'] . ': ';
+ $text = substr(wordwrap($header_text . strtr(trim($text), array("\r" => '', "\n" => '')), 76, $this->_eol . ' '), strlen($header_text));
}
$val[$key] = $text;
* <pre>
* 'charset' => (string) Encodes the headers using this charset.
* DEFAULT: No encoding.
- * 'defserver' => (string) TODO
- * DEFAULT: NO
+ * 'defserver' => (string) The default domain to append to mailboxes.
+ * DEFAULT: No default name.
* 'nowrap' => (integer) Don't wrap the headers.
* DEFAULT: Headers are wrapped.
* </pre>
/**
* Add a header to the header array.
*
- * @param string $header The header name.
- * @param string $value The header value.
- * @param boolean $decode MIME decode the value?
+ * @param string $header The header name.
+ * @param string $value The header value.
+ * @param array $options Additional options:
+ * <pre>
+ * 'decode' - (boolean) MIME decode the value?
+ * 'params' - (array) MIME parameters for Content-Type or
+ * Content-Disposition
+ * </pre>
*/
- public function addHeader($header, $value, $decode = false)
+ public function addHeader($header, $value, $options = array())
{
require_once 'Horde/String.php';
}
$ptr = &$this->_headers[$lcHeader];
- if ($decode) {
+ if (!empty($options['decode'])) {
// Fields defined in RFC 2822 that contain address information
if (in_array($lcHeader, $this->addressFields())) {
$value = Horde_Mime::decodeAddrString($value);
} else {
$ptr['value'] = $value;
}
+
+ if (!empty($options['params'])) {
+ $ptr['params'] = $options['params'];
+ }
}
/**
/**
* Replace a value of a header.
*
- * @param string $header The header name.
- * @param string $value The header value.
- * @param boolean $decode MIME decode the value?
+ * @param string $header The header name.
+ * @param string $value The header value.
+ * @param array $options Additional options:
+ * <pre>
+ * 'decode' - (boolean) MIME decode the value?
+ * 'params' - (array) MIME parameters for Content-Type or
+ * Content-Disposition
+ * </pre>
*/
- public function replaceHeader($header, $value, $decode = false)
+ public function replaceHeader($header, $value, $options = array())
{
$this->removeHeader($header);
- $this->addHeader($header, $value, $decode);
+ $this->addHeader($header, $value, $options);
}
/**
* Set a value for a particular header ONLY if that header is set.
*
- * @param string $header The header name.
- * @param string $value The header value.
- * @param boolean $decode MIME decode the value?
+ * @param string $header The header name.
+ * @param string $value The header value.
+ * @param array $options Additional options:
+ * <pre>
+ * 'decode' - (boolean) MIME decode the value?
+ * 'params' - (array) MIME parameters for Content-Type or
+ * Content-Disposition
+ * </pre>
*
* @return boolean True if value was set.
*/
- public function setValue($header, $value, $decode = false)
+ public function setValue($header, $value, $options = array())
{
require_once 'Horde/String.php';
}
/**
- * Adds proper linebreaks to a header string.
- * RFC 2822 says headers SHOULD only be 78 characters a line, but also
- * says that a header line MUST not be more than 998 characters.
- *
- * @param string $header The header name.
- * @param string $text The text of the header field.
- *
- * @return string The header value, with linebreaks inserted.
- */
- public function wrapHeaders($header, $text)
- {
- $eol = $this->_eol;
- $header_text = rtrim($header) . ': ';
-
- /* Remove any existing linebreaks. */
- $text = $header_text . preg_replace("/\r?\n\s?/", ' ', rtrim($text));
-
- if (!in_array(strtolower($header), array('content-type', 'content-disposition'))) {
- /* Wrap the line. */
- $line = wordwrap($text, 75, $eol . ' ');
-
- /* Make sure there are no empty lines. */
- $line = preg_replace('/' . $eol . ' ' . $eol . ' /', '/' . $eol . ' /', $line);
-
- return substr($line, strlen($header_text));
- }
-
- /* Split the line by the RFC parameter separator ';'. */
- $params = preg_split("/\s*;\s*/", $text);
-
- $line = '';
- $eollength = strlen($eol);
- $length = 1000 - $eollength;
- $paramcount = count($params) - 1;
-
- reset($params);
- while (list($count, $val) = each($params)) {
- /* If longer than RFC allows, then simply chop off the excess. */
- $moreparams = ($count != $paramcount);
- $maxlength = $length - (!empty($line) ? 1 : 0) - (($moreparams) ? 1 : 0);
- if (strlen($val) > $maxlength) {
- $val = substr($val, 0, $maxlength);
-
- /* If we have an opening quote, add a closing quote after
- * chopping the rest of the text. */
- if (strpos($val, '"') !== false) {
- $val = substr($val, 0, -1) . '"';
- }
- }
-
- if (!empty($line)) {
- $line .= ' ';
- }
- $line .= $val . (($moreparams) ? ';' : '') . $eol;
- }
-
- return substr($line, strlen($header_text), ($eollength * -1));
- }
-
- /**
* Builds a Horde_Mime_Headers object from header text.
* This function can be called statically:
* $headers = Horde_Mime_Headers::parseHeaders().
$currtext .= ' ' . ltrim($val);
} else {
if (!is_null($currheader)) {
- $headers->addHeader($currheader, $currtext, true);
+ // TODO: RFC 2231
+ $headers->addHeader($currheader, $currtext, array('decode' => true));
}
$pos = strpos($val, ':');
$currheader = substr($val, 0, $pos);
$currtext = ltrim(substr($val, $pos + 1));
}
}
- $headers->addHeader($currheader, $currtext, true);
+ $headers->addHeader($currheader, $currtext, array('decode' => true));
return $headers;
}
protected $_mailer_driver = 'smtp';
/**
+ * The charset to use for the message.
+ *
+ * @var string
+ */
+ protected $_charset;
+
+ /**
* The Mail driver parameters.
*
* @link http://pear.php.net/Mail
}
$this->_headers = new Horde_Mime_Headers();
+ $this->_charset = $charset;
if ($subject) {
- $this->addHeader('Subject', $subject, $charset);
+ $this->addHeader('Subject', $subject);
}
if ($to) {
- $this->addHeader('To', $to, $charset);
+ $this->addHeader('To', $to);
}
if ($from) {
- $this->addHeader('From', $from, $charset);
+ $this->addHeader('From', $from);
}
if ($body) {
$this->setBody($body, $charset);
$headers = new Horde_Mime_Headers();
}
- foreach ($this->getHeaderArray() as $key => $val) {
- $headers->replaceHeader($key, $val);
- }
-
- return $headers;
- }
-
- /**
- * Get the list of MIME headers for this part in an array.
- *
- * @return array The full set of MIME headers.
- */
- public function getHeaderArray()
- {
- $headers = array();
-
- if ($this->_basepart) {
- /* Per RFC 2046 [4], this MUST appear in the message headers. */
- $headers['MIME-Version'] = '1.0';
- }
-
$ptype = $this->getPrimaryType();
$stype = $this->getSubType();
- /* Get the character set for this part. */
- $charset = $this->getCharset();
-
- /* Get the Content-Type - this is ALWAYS required. */
- $ctype = $this->getType(true);
-
- /* Manually encode Content-Type and Disposition parameters in here,
- * rather than in Horde_Mime_Headers, since it is easier to do when
- * the paramters are broken down. Encoding in the headers object will
- * ignore these headers Since they will already be in 7bit. */
- foreach ($this->getAllContentTypeParameters() as $key => $value) {
- /* Skip the charset key since that would have already been
- * added to $ctype by getType(). */
- if ($key == 'charset') {
- continue;
- }
-
- $encode_2231 = Horde_Mime::encodeParamString($key, $value, $charset);
- /* Try to work around non RFC 2231-compliant MUAs by sending both
- * a RFC 2047-like parameter name and then the correct RFC 2231
- * parameter. See:
- * http://lists.horde.org/archives/dev/Week-of-Mon-20040426/014240.html */
- if (!empty($GLOBALS['conf']['mailformat']['brokenrfc2231']) &&
- (strpos($encode_2231, '*=') !== false)) {
- $ctype .= '; ' . $key . '="' . Horde_Mime::encode($value, $charset) . '"';
- }
- $ctype .= '; ' . $encode_2231;
- }
- $headers['Content-Type'] = $ctype;
+ /* Get the Content-Type itself. */
+ $headers->replaceHeader('Content-Type', $this->getType(), array('params' => $this->getAllContentTypeParameters()));
/* Get the description, if any. */
if (($descrip = $this->getDescription())) {
- $headers['Content-Description'] = $descrip;
+ $headers->replaceHeader('Content-Description', $descrip);
}
/* RFC 2045 [4] - message/rfc822 and message/partial require the
- MIME-Version header only if they themselves claim to be MIME
- compliant. */
- if (($ptype == 'message') &&
- (($stype == 'rfc822') || ($stype == 'partial'))) {
- // TODO - Check for "MIME-Version" in message/rfc822 part.
- $headers['MIME-Version'] = '1.0';
+ * MIME-Version header only if they themselves claim to be MIME
+ * compliant.
+ * @TODO - Check for "MIME-Version" in message/rfc822 part.
+ * Per RFC 2046 [4], this MUST appear in the base message headers. */
+ if ($this->_basepart ||
+ (($ptype == 'message') &&
+ (($stype == 'rfc822') || ($stype == 'partial')))) {
+ $headers->replaceHeader('MIME-Version', '1.0');
}
/* message/* parts require no additional header information. */
there is a name parameter. */
$name = $this->getName();
if (($ptype != 'multipart') || !empty($name)) {
- $disp = $this->getDisposition();
-
- /* Add any disposition parameter information, if available. */
- if (!empty($name)) {
- $encode_2231 = Horde_Mime::encodeParamString('filename', $name, $charset);
- /* Same broken RFC 2231 workaround as above. */
- if (!empty($GLOBALS['conf']['mailformat']['brokenrfc2231']) &&
- (strpos($encode_2231, '*=') !== false)) {
- $disp .= '; filename="' . Horde_Mime::encode($name, $charset) . '"';
- }
- $disp .= '; ' . $encode_2231;
- }
-
- $headers['Content-Disposition'] = $disp;
+ $headers->replaceHeader('Content-Disposition', $this->getDisposition(), array('params' => (!empty($name) ? array('filename' => $name) : array())));
}
/* Add transfer encoding information. */
- $headers['Content-Transfer-Encoding'] = $this->getTransferEncoding();
+ $headers->replaceHeader('Content-Transfer-Encoding', $this->getTransferEncoding());
/* Add content ID information. */
if (!is_null($this->_contentid)) {
- $headers['Content-ID'] = $this->_contentid;
+ $headers->replaceHeader('Content-ID', $this->_contentid);
}
return $headers;