From: Michael M Slusarz Date: Tue, 2 Dec 2008 08:06:50 +0000 (-0700) Subject: RFC 2231 fixes X-Git-Url: https://git.internetallee.de/?a=commitdiff_plain;h=e6df0c6894fd8efeb747827a60e5dcfe78718fc2;p=horde.git RFC 2231 fixes Do a better job of encoding/wrapping MIME parameter values correctly - in short, we need to keep these parameters separate from the "base" value of the header so that we can (potentially) encode them differently than the base value. --- diff --git a/framework/Mime/lib/Horde/Mime.php b/framework/Mime/lib/Horde/Mime.php index d0a7ea28e..55fd4f984 100644 --- a/framework/Mime/lib/Horde/Mime.php +++ b/framework/Mime/lib/Horde/Mime.php @@ -1,7 +1,7 @@ 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; } @@ -354,27 +367,26 @@ class Horde_Mime $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: + *
+     * 
*/ static public function decodeParamString($string, $to_charset = null) { @@ -382,11 +394,6 @@ class Horde_Mime return false; } - if (!isset($to_charset)) { - require_once 'Horde/NLS.php'; - $to_charset = NLS::getCharset(); - } - $attribute = substr($string, 0, $pos); $charset = $lang = null; $output = ''; diff --git a/framework/Mime/lib/Horde/Mime/Address.php b/framework/Mime/lib/Horde/Mime/Address.php index fe9a98361..fcebaca23 100644 --- a/framework/Mime/lib/Horde/Mime/Address.php +++ b/framework/Mime/lib/Horde/Mime/Address.php @@ -364,7 +364,7 @@ class Horde_Mime_Address * 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. */ diff --git a/framework/Mime/lib/Horde/Mime/Headers.php b/framework/Mime/lib/Horde/Mime/Headers.php index 686927680..ceb521adb 100644 --- a/framework/Mime/lib/Horde/Mime/Headers.php +++ b/framework/Mime/lib/Horde/Mime/Headers.php @@ -45,8 +45,8 @@ class Horde_Mime_Headers *
      * '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.
      * 
@@ -55,30 +55,43 @@ class Horde_Mime_Headers */ 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; @@ -97,8 +110,8 @@ class Horde_Mime_Headers *
      * '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.
      * 
@@ -225,11 +238,16 @@ class Horde_Mime_Headers /** * 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: + *
+     * 'decode' - (boolean) MIME decode the value?
+     * 'params' - (array) MIME parameters for Content-Type or
+     *            Content-Disposition
+     * 
*/ - public function addHeader($header, $value, $decode = false) + public function addHeader($header, $value, $options = array()) { require_once 'Horde/String.php'; @@ -242,7 +260,7 @@ class Horde_Mime_Headers } $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); @@ -259,6 +277,10 @@ class Horde_Mime_Headers } else { $ptr['value'] = $value; } + + if (!empty($options['params'])) { + $ptr['params'] = $options['params']; + } } /** @@ -275,26 +297,36 @@ class Horde_Mime_Headers /** * 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: + *
+     * 'decode' - (boolean) MIME decode the value?
+     * 'params' - (array) MIME parameters for Content-Type or
+     *            Content-Disposition
+     * 
*/ - 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: + *
+     * 'decode' - (boolean) MIME decode the value?
+     * 'params' - (array) MIME parameters for Content-Type or
+     *            Content-Disposition
+     * 
* * @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'; @@ -454,66 +486,6 @@ class Horde_Mime_Headers } /** - * 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(). @@ -537,14 +509,15 @@ class Horde_Mime_Headers $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; } diff --git a/framework/Mime/lib/Horde/Mime/Mail.php b/framework/Mime/lib/Horde/Mime/Mail.php index 7d8ae007c..0991e198c 100644 --- a/framework/Mime/lib/Horde/Mime/Mail.php +++ b/framework/Mime/lib/Horde/Mime/Mail.php @@ -60,6 +60,13 @@ class Horde_Mime_Mail 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 @@ -85,15 +92,16 @@ class Horde_Mime_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); diff --git a/framework/Mime/lib/Horde/Mime/Part.php b/framework/Mime/lib/Horde/Mime/Part.php index 29676b2b2..9a22b94e9 100644 --- a/framework/Mime/lib/Horde/Mime/Part.php +++ b/framework/Mime/lib/Horde/Mime/Part.php @@ -740,72 +740,26 @@ class Horde_Mime_Part $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. */ @@ -817,28 +771,15 @@ class Horde_Mime_Part 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;