- Exclude MIME functionality from rcube_imap class into rcube_mime class

pull/1/head
alecpl 13 years ago
parent c21d7fa787
commit 1c4f23d6e5

@ -51,17 +51,17 @@ function export_mailbox($mbox, $filename)
for ($count = $IMAP->messagecount(), $i=1; $i <= $count; $i++) for ($count = $IMAP->messagecount(), $i=1; $i <= $count; $i++)
{ {
$headers = $IMAP->get_headers($i, null, false); $headers = $IMAP->get_headers($i, null, false);
$from = current($IMAP->decode_address_list($headers->from, 1, false)); $from = current(rcube_mime::decode_address_list($headers->from, 1, false));
fwrite($out, sprintf("From %s %s UID %d\n", $from['mailto'], $headers->date, $headers->uid)); fwrite($out, sprintf("From %s %s UID %d\n", $from['mailto'], $headers->date, $headers->uid));
fwrite($out, $IMAP->conn->fetchPartHeader($mbox, $i)); fwrite($out, $IMAP->conn->fetchPartHeader($mbox, $i));
fwrite($out, $IMAP->conn->handlePartBody($mbox, $i)); fwrite($out, $IMAP->conn->handlePartBody($mbox, $i));
fwrite($out, "\n\n\n"); fwrite($out, "\n\n\n");
progress_update($i, $count); progress_update($i, $count);
} }
vputs("\ncomplete.\n"); vputs("\ncomplete.\n");
if ($filename) if ($filename)
fclose($out); fclose($out);
} }
@ -114,7 +114,7 @@ $IMAP = new rcube_imap(null);
if ($IMAP->connect($host, $args['user'], $args['pass'], $imap_port, $imap_ssl)) if ($IMAP->connect($host, $args['user'], $args['pass'], $imap_port, $imap_ssl))
{ {
vputs("IMAP login successful.\n"); vputs("IMAP login successful.\n");
$filename = null; $filename = null;
$mailboxes = $args['mbox'] == '*' ? $IMAP->list_mailboxes(null) : array($args['mbox']); $mailboxes = $args['mbox'] == '*' ? $IMAP->list_mailboxes(null) : array($args['mbox']);
@ -124,7 +124,7 @@ if ($IMAP->connect($host, $args['user'], $args['pass'], $imap_port, $imap_ssl))
$filename = preg_replace('/\.[a-z0-9]{3,4}$/i', '', $args['file']) . asciiwords($mbox) . '.mbox'; $filename = preg_replace('/\.[a-z0-9]{3,4}$/i', '', $args['file']) . asciiwords($mbox) . '.mbox';
else if ($args['mbox'] == '*') else if ($args['mbox'] == '*')
$filename = asciiwords($mbox) . '.mbox'; $filename = asciiwords($mbox) . '.mbox';
if ($args['mbox'] == '*' && in_array(strtolower($mbox), array('junk','spam','trash'))) if ($args['mbox'] == '*' && in_array(strtolower($mbox), array('junk','spam','trash')))
continue; continue;

@ -381,28 +381,6 @@ class rcube_imap
} }
/**
* Forces selection of a mailbox
*
* @param string $mailbox Mailbox/Folder name
* @access public
*/
function select_mailbox($mailbox=null)
{
if (!strlen($mailbox)) {
$mailbox = $this->mailbox;
}
$selected = $this->conn->select($mailbox);
if ($selected && $this->mailbox != $mailbox) {
// clear messagecount cache for this mailbox
$this->_clear_messagecount($mailbox);
$this->mailbox = $mailbox;
}
}
/** /**
* Set internal list page * Set internal list page
* *
@ -428,24 +406,24 @@ class rcube_imap
/** /**
* Save a set of message ids for future message listing methods * Save a search result for future message listing methods
* *
* @param string IMAP Search query * @param array $set Search set, result from rcube_imap::get_search_set():
* @param rcube_result_index|rcube_result_thread Result set * 0 - searching criteria, string
* @param string Charset of search string * 1 - search result, rcube_result_index|rcube_result_thread
* @param string Sorting field * 2 - searching character set, string
* @param string True if set is sorted (SORT was used for searching) * 3 - sorting field, string
* 4 - true if sorted, bool
*/ */
function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $sorted=false) function set_search_set($set)
{ {
if (is_array($str) && $msgs === null) $set = (array)$set;
list($str, $msgs, $charset, $sort_field, $sorted) = $str;
$this->search_string = $set[0];
$this->search_string = $str; $this->search_set = $set[1];
$this->search_set = $msgs; $this->search_charset = $set[2];
$this->search_charset = $charset; $this->search_sort_field = $set[3];
$this->search_sort_field = $sort_field; $this->search_sorted = $set[4];
$this->search_sorted = $sorted;
$this->search_threads = is_a($this->search_set, 'rcube_result_thread'); $this->search_threads = is_a($this->search_set, 'rcube_result_thread');
} }
@ -453,8 +431,6 @@ class rcube_imap
/** /**
* Return the saved search set as hash array * Return the saved search set as hash array
* *
* @param bool $clone Clone result object
*
* @return array Search set * @return array Search set
*/ */
function get_search_set() function get_search_set()
@ -1413,8 +1389,8 @@ class rcube_imap
$results = $this->_search_index($mailbox, $str, $charset, $sort_field); $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
$this->set_search_set($str, $results, $charset, $sort_field, $this->set_search_set(array($str, $results, $charset, $sort_field,
$this->threading || $this->search_sorted ? true : false); $this->threading || $this->search_sorted ? true : false));
} }
@ -1551,22 +1527,6 @@ class rcube_imap
} }
/**
* Check if the given message UID is part of the current search set
*
* @param string $msgid Message UID
*
* @return boolean True on match or if no search request is stored
*/
function in_searchset($uid)
{
if (!empty($this->search_string)) {
return $this->search_set->exists($uid);
}
return true;
}
/** /**
* Return message headers object of a specific message * Return message headers object of a specific message
* *
@ -1662,7 +1622,7 @@ class rcube_imap
return $headers; return $headers;
} }
$struct = &$this->_structure_part($structure, 0, '', $headers); $struct = $this->_structure_part($structure, 0, '', $headers);
// don't trust given content-type // don't trust given content-type
if (empty($struct->parts) && !empty($headers->ctype)) { if (empty($struct->parts) && !empty($headers->ctype)) {
@ -1685,7 +1645,7 @@ class rcube_imap
* @param string $parent * @param string $parent
* @access private * @access private
*/ */
function &_structure_part($part, $count=0, $parent='', $mime_headers=null) private function _structure_part($part, $count=0, $parent='', $mime_headers=null)
{ {
$struct = new rcube_message_part; $struct = new rcube_message_part;
$struct->mime_id = empty($parent) ? (string)$count : "$parent.$count"; $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
@ -1844,7 +1804,7 @@ class rcube_imap
} }
if (is_string($mime_headers)) if (is_string($mime_headers))
$struct->headers = $this->_parse_headers($mime_headers) + $struct->headers; $struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers;
else if (is_object($mime_headers)) else if (is_object($mime_headers))
$struct->headers = get_object_vars($mime_headers) + $struct->headers; $struct->headers = get_object_vars($mime_headers) + $struct->headers;
@ -1988,7 +1948,7 @@ class rcube_imap
else else
$charset = rc_detect_encoding($filename_mime, $this->default_charset); $charset = rc_detect_encoding($filename_mime, $this->default_charset);
$part->filename = rcube_imap::decode_mime_string($filename_mime, $charset); $part->filename = rcube_mime::decode_mime_string($filename_mime, $charset);
} }
else if (!empty($filename_encoded)) { else if (!empty($filename_encoded)) {
// decode filename according to RFC 2231, Section 4 // decode filename according to RFC 2231, Section 4
@ -2031,7 +1991,7 @@ class rcube_imap
* *
* @return string Message/part body if not printed * @return string Message/part body if not printed
*/ */
function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false) function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
{ {
// get part data if not provided // get part data if not provided
if (!is_object($o_part)) { if (!is_object($o_part)) {
@ -2084,7 +2044,7 @@ class rcube_imap
* @return string $part Message/part body * @return string $part Message/part body
* @see rcube_imap::get_message_part() * @see rcube_imap::get_message_part()
*/ */
function &get_body($uid, $part=1) function get_body($uid, $part=1)
{ {
$headers = $this->get_headers($uid); $headers = $this->get_headers($uid);
return rcube_charset_convert($this->get_message_part($uid, $part, NULL), return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
@ -2100,7 +2060,7 @@ class rcube_imap
* *
* @return string Message source string * @return string Message source string
*/ */
function &get_raw_body($uid, $fp=null) function get_raw_body($uid, $fp=null)
{ {
return $this->conn->handlePartBody($this->mailbox, $uid, return $this->conn->handlePartBody($this->mailbox, $uid,
true, null, null, false, $fp); true, null, null, false, $fp);
@ -2113,7 +2073,7 @@ class rcube_imap
* @param int $uid Message UID * @param int $uid Message UID
* @return string Message headers string * @return string Message headers string
*/ */
function &get_raw_headers($uid) function get_raw_headers($uid)
{ {
return $this->conn->fetchPartHeader($this->mailbox, $uid, true); return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
} }
@ -3621,6 +3581,7 @@ class rcube_imap
} }
} }
/** /**
* Getter for messages cache object * Getter for messages cache object
*/ */
@ -3637,6 +3598,7 @@ class rcube_imap
return $this->mcache; return $this->mcache;
} }
/** /**
* Clears the messages cache. * Clears the messages cache.
* *
@ -3651,204 +3613,6 @@ class rcube_imap
} }
/* --------------------------------
* encoding/decoding methods
* --------------------------------*/
/**
* Split an address list into a structured array list
*
* @param string $input Input string
* @param int $max List only this number of addresses
* @param boolean $decode Decode address strings
* @return array Indexed list of addresses
*/
function decode_address_list($input, $max=null, $decode=true)
{
$a = $this->_parse_address_list($input, $decode);
$out = array();
// Special chars as defined by RFC 822 need to in quoted string (or escaped).
$special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
if (!is_array($a))
return $out;
$c = count($a);
$j = 0;
foreach ($a as $val) {
$j++;
$address = trim($val['address']);
$name = trim($val['name']);
if ($name && $address && $name != $address)
$string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
else if ($address)
$string = $address;
else if ($name)
$string = $name;
$out[$j] = array(
'name' => $name,
'mailto' => $address,
'string' => $string
);
if ($max && $j==$max)
break;
}
return $out;
}
/**
* Decode a message header value
*
* @param string $input Header value
* @param boolean $remove_quotas Remove quotes if necessary
* @return string Decoded string
*/
function decode_header($input, $remove_quotes=false)
{
$str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
if ($str[0] == '"' && $remove_quotes)
$str = str_replace('"', '', $str);
return $str;
}
/**
* Decode a mime-encoded string to internal charset
*
* @param string $input Header value
* @param string $fallback Fallback charset if none specified
*
* @return string Decoded string
* @static
*/
public static function decode_mime_string($input, $fallback=null)
{
if (!empty($fallback)) {
$default_charset = $fallback;
}
else {
$default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
}
// rfc: all line breaks or other characters not found
// in the Base64 Alphabet must be ignored by decoding software
// delete all blanks between MIME-lines, differently we can
// receive unnecessary blanks and broken utf-8 symbols
$input = preg_replace("/\?=\s+=\?/", '?==?', $input);
// encoded-word regexp
$re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
// Find all RFC2047's encoded words
if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
// Initialize variables
$tmp = array();
$out = '';
$start = 0;
foreach ($matches as $idx => $m) {
$pos = $m[0][1];
$charset = $m[1][0];
$encoding = $m[2][0];
$text = $m[3][0];
$length = strlen($m[0][0]);
// Append everything that is before the text to be decoded
if ($start != $pos) {
$substr = substr($input, $start, $pos-$start);
$out .= rcube_charset_convert($substr, $default_charset);
$start = $pos;
}
$start += $length;
// Per RFC2047, each string part "MUST represent an integral number
// of characters . A multi-octet character may not be split across
// adjacent encoded-words." However, some mailers break this, so we
// try to handle characters spanned across parts anyway by iterating
// through and aggregating sequential encoded parts with the same
// character set and encoding, then perform the decoding on the
// aggregation as a whole.
$tmp[] = $text;
if ($next_match = $matches[$idx+1]) {
if ($next_match[0][1] == $start
&& $next_match[1][0] == $charset
&& $next_match[2][0] == $encoding
) {
continue;
}
}
$count = count($tmp);
$text = '';
// Decode and join encoded-word's chunks
if ($encoding == 'B' || $encoding == 'b') {
// base64 must be decoded a segment at a time
for ($i=0; $i<$count; $i++)
$text .= base64_decode($tmp[$i]);
}
else { //if ($encoding == 'Q' || $encoding == 'q') {
// quoted printable can be combined and processed at once
for ($i=0; $i<$count; $i++)
$text .= $tmp[$i];
$text = str_replace('_', ' ', $text);
$text = quoted_printable_decode($text);
}
$out .= rcube_charset_convert($text, $charset);
$tmp = array();
}
// add the last part of the input string
if ($start != strlen($input)) {
$out .= rcube_charset_convert(substr($input, $start), $default_charset);
}
// return the results
return $out;
}
// no encoding information, use fallback
return rcube_charset_convert($input, $default_charset);
}
/**
* Decode a mime part
*
* @param string $input Input string
* @param string $encoding Part encoding
* @return string Decoded string
*/
function mime_decode($input, $encoding='7bit')
{
switch (strtolower($encoding)) {
case 'quoted-printable':
return quoted_printable_decode($input);
case 'base64':
return base64_decode($input);
case 'x-uuencode':
case 'x-uue':
case 'uue':
case 'uuencode':
return convert_uudecode($input);
case '7bit':
default:
return $input;
}
}
/* -------------------------------- /* --------------------------------
* private methods * private methods
* --------------------------------*/ * --------------------------------*/
@ -3937,7 +3701,7 @@ class rcube_imap
* *
* @return int Message UID * @return int Message UID
*/ */
function id2uid($id, $mailbox = null) private function id2uid($id, $mailbox = null)
{ {
if (!strlen($mailbox)) { if (!strlen($mailbox)) {
$mailbox = $this->mailbox; $mailbox = $this->mailbox;
@ -4028,161 +3792,6 @@ class rcube_imap
} }
/**
* Split RFC822 header string into an associative array
* @access private
*/
private function _parse_headers($headers)
{
$a_headers = array();
$headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
$lines = explode("\n", $headers);
$c = count($lines);
for ($i=0; $i<$c; $i++) {
if ($p = strpos($lines[$i], ': ')) {
$field = strtolower(substr($lines[$i], 0, $p));
$value = trim(substr($lines[$i], $p+1));
if (!empty($value))
$a_headers[$field] = $value;
}
}
return $a_headers;
}
/**
* @access private
*/
private function _parse_address_list($str, $decode=true)
{
// remove any newlines and carriage returns before
$str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
// extract list items, remove comments
$str = self::explode_header_string(',;', $str, true);
$result = array();
// simplified regexp, supporting quoted local part
$email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
foreach ($str as $key => $val) {
$name = '';
$address = '';
$val = trim($val);
if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
$address = $m[2];
$name = trim($m[1]);
}
else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
$address = $m[1];
$name = '';
}
else {
$name = $val;
}
// dequote and/or decode name
if ($name) {
if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
$name = stripslashes($name);
}
if ($decode) {
$name = $this->decode_header($name);
}
}
if (!$address && $name) {
$address = $name;
}
if ($address) {
$result[$key] = array('name' => $name, 'address' => $address);
}
}
return $result;
}
/**
* Explodes header (e.g. address-list) string into array of strings
* using specified separator characters with proper handling
* of quoted-strings and comments (RFC2822)
*
* @param string $separator String containing separator characters
* @param string $str Header string
* @param bool $remove_comments Enable to remove comments
*
* @return array Header items
*/
static function explode_header_string($separator, $str, $remove_comments=false)
{
$length = strlen($str);
$result = array();
$quoted = false;
$comment = 0;
$out = '';
for ($i=0; $i<$length; $i++) {
// we're inside a quoted string
if ($quoted) {
if ($str[$i] == '"') {
$quoted = false;
}
else if ($str[$i] == '\\') {
if ($comment <= 0) {
$out .= '\\';
}
$i++;
}
}
// we're inside a comment string
else if ($comment > 0) {
if ($str[$i] == ')') {
$comment--;
}
else if ($str[$i] == '(') {
$comment++;
}
else if ($str[$i] == '\\') {
$i++;
}
continue;
}
// separator, add to result array
else if (strpos($separator, $str[$i]) !== false) {
if ($out) {
$result[] = $out;
}
$out = '';
continue;
}
// start of quoted string
else if ($str[$i] == '"') {
$quoted = true;
}
// start of comment
else if ($remove_comments && $str[$i] == '(') {
$comment++;
}
if ($comment <= 0) {
$out .= $str[$i];
}
}
if ($out && $comment <= 0) {
$result[] = $out;
}
return $result;
}
/** /**
* This is our own debug handler for the IMAP connection * This is our own debug handler for the IMAP connection
* @access public * @access public

@ -42,6 +42,13 @@ class rcube_message
* @var rcube_imap * @var rcube_imap
*/ */
private $imap; private $imap;
/**
* Instance of mime class
*
* @var rcube_mime
*/
private $mime;
private $opt = array(); private $opt = array();
private $inline_parts = array(); private $inline_parts = array();
private $parse_alternative = false; private $parse_alternative = false;
@ -63,27 +70,24 @@ class rcube_message
* *
* @param string $uid The message UID. * @param string $uid The message UID.
* *
* @uses rcmail::get_instance()
* @uses rcube_imap::decode_mime_string()
* @uses self::set_safe()
*
* @see self::$app, self::$imap, self::$opt, self::$structure * @see self::$app, self::$imap, self::$opt, self::$structure
*/ */
function __construct($uid) function __construct($uid)
{ {
$this->app = rcmail::get_instance(); $this->uid = $uid;
$this->app = rcmail::get_instance();
$this->imap = $this->app->imap; $this->imap = $this->app->imap;
$this->imap->get_all_headers = true; $this->imap->get_all_headers = true;
$this->uid = $uid;
$this->headers = $this->imap->get_message($uid); $this->headers = $this->imap->get_message($uid);
if (!$this->headers) if (!$this->headers)
return; return;
$this->subject = rcube_imap::decode_mime_string( $this->mime = new rcube_mime($this->headers->charset);
$this->headers->subject, $this->headers->charset);
list(, $this->sender) = each($this->imap->decode_address_list($this->headers->from)); $this->subject = $this->mime->decode_mime_string($this->headers->subject);
list(, $this->sender) = each($this->mime->decode_address_list($this->headers->from, 1));
$this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$uid])); $this->set_safe((intval($_GET['_safe']) || $_SESSION['safe_messages'][$uid]));
$this->opt = array( $this->opt = array(
@ -115,12 +119,15 @@ class rcube_message
*/ */
public function get_header($name, $raw = false) public function get_header($name, $raw = false)
{ {
if (empty($this->headers))
return null;
if ($this->headers->$name) if ($this->headers->$name)
$value = $this->headers->$name; $value = $this->headers->$name;
else if ($this->headers->others[$name]) else if ($this->headers->others[$name])
$value = $this->headers->others[$name]; $value = $this->headers->others[$name];
return $raw ? $value : $this->imap->decode_header($value); return $raw ? $value : $this->mime->decode_header($value);
} }
@ -654,95 +661,4 @@ class rcube_message
return $parts; return $parts;
} }
/**
* Interpret a format=flowed message body according to RFC 2646
*
* @param string $text Raw body formatted as flowed text
* @return string Interpreted text with unwrapped lines and stuffed space removed
*/
public static function unfold_flowed($text)
{
$text = preg_split('/\r?\n/', $text);
$last = -1;
$q_level = 0;
foreach ($text as $idx => $line) {
if ($line[0] == '>' && preg_match('/^(>+\s*)/', $line, $regs)) {
$q = strlen(str_replace(' ', '', $regs[0]));
$line = substr($line, strlen($regs[0]));
if ($q == $q_level && $line
&& isset($text[$last])
&& $text[$last][strlen($text[$last])-1] == ' '
) {
$text[$last] .= $line;
unset($text[$idx]);
}
else {
$last = $idx;
}
}
else {
$q = 0;
if ($line == '-- ') {
$last = $idx;
}
else {
// remove space-stuffing
$line = preg_replace('/^\s/', '', $line);
if (isset($text[$last]) && $line
&& $text[$last] != '-- '
&& $text[$last][strlen($text[$last])-1] == ' '
) {
$text[$last] .= $line;
unset($text[$idx]);
}
else {
$text[$idx] = $line;
$last = $idx;
}
}
}
$q_level = $q;
}
return implode("\r\n", $text);
}
/**
* Wrap the given text to comply with RFC 2646
*
* @param string $text Text to wrap
* @param int $length Length
* @return string Wrapped text
*/
public static function format_flowed($text, $length = 72)
{
$text = preg_split('/\r?\n/', $text);
foreach ($text as $idx => $line) {
if ($line != '-- ') {
if ($line[0] == '>' && preg_match('/^(>+)/', $line, $regs)) {
$prefix = $regs[0];
$level = strlen($prefix);
$line = rtrim(substr($line, $level));
$line = $prefix . rc_wordwrap($line, $length - $level - 2, " \r\n$prefix ");
}
else if ($line) {
$line = rc_wordwrap(rtrim($line), $length - 2, " \r\n");
// space-stuffing
$line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);
}
$text[$idx] = $line;
}
}
return implode("\r\n", $text);
}
} }

@ -0,0 +1,484 @@
<?php
/**
+-----------------------------------------------------------------------+
| program/include/rcube_mime.php |
| |
| This file is part of the Roundcube Webmail client |
| Copyright (C) 2005-2012, The Roundcube Dev Team |
| Copyright (C) 2011-2012, Kolab Systems AG |
| Licensed under the GNU GPL |
| |
| PURPOSE: |
| MIME message parsing utilities |
| |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
$Id$
*/
/**
* Class for parsing MIME messages
*
* @package Mail
* @author Thomas Bruederli <roundcube@gmail.com>
* @author Aleksander Machniak <alec@alec.pl>
*/
class rcube_mime
{
private static $default_charset = RCMAIL_CHARSET;
/**
* Object constructor.
*/
function __construct($default_charset = null)
{
if ($default_charset) {
self::$default_charset = $default_charset;
}
else {
self::$default_charset = rcmail::get_instance()->config->get('default_charset', RCMAIL_CHARSET);
}
}
/**
* Split an address list into a structured array list
*
* @param string $input Input string
* @param int $max List only this number of addresses
* @param boolean $decode Decode address strings
* @param string $fallback Fallback charset if none specified
*
* @return array Indexed list of addresses
*/
static function decode_address_list($input, $max = null, $decode = true, $fallback = null)
{
$a = self::parse_address_list($input, $decode, $fallback);
$out = array();
$j = 0;
// Special chars as defined by RFC 822 need to in quoted string (or escaped).
$special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
if (!is_array($a))
return $out;
foreach ($a as $val) {
$j++;
$address = trim($val['address']);
$name = trim($val['name']);
if ($name && $address && $name != $address)
$string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
else if ($address)
$string = $address;
else if ($name)
$string = $name;
$out[$j] = array(
'name' => $name,
'mailto' => $address,
'string' => $string
);
if ($max && $j==$max)
break;
}
return $out;
}
/**
* Decode a message header value
*
* @param string $input Header value
* @param string $fallback Fallback charset if none specified
*
* @return string Decoded string
*/
public static function decode_header($input, $fallback = null)
{
$str = self::decode_mime_string((string)$input, $fallback);
return $str;
}
/**
* Decode a mime-encoded string to internal charset
*
* @param string $input Header value
* @param string $fallback Fallback charset if none specified
*
* @return string Decoded string
*/
public static function decode_mime_string($input, $fallback = null)
{
$default_charset = !empty($fallback) ? $fallback : self::$default_charset;
// rfc: all line breaks or other characters not found
// in the Base64 Alphabet must be ignored by decoding software
// delete all blanks between MIME-lines, differently we can
// receive unnecessary blanks and broken utf-8 symbols
$input = preg_replace("/\?=\s+=\?/", '?==?', $input);
// encoded-word regexp
$re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
// Find all RFC2047's encoded words
if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
// Initialize variables
$tmp = array();
$out = '';
$start = 0;
foreach ($matches as $idx => $m) {
$pos = $m[0][1];
$charset = $m[1][0];
$encoding = $m[2][0];
$text = $m[3][0];
$length = strlen($m[0][0]);
// Append everything that is before the text to be decoded
if ($start != $pos) {
$substr = substr($input, $start, $pos-$start);
$out .= rcube_charset_convert($substr, $default_charset);
$start = $pos;
}
$start += $length;
// Per RFC2047, each string part "MUST represent an integral number
// of characters . A multi-octet character may not be split across
// adjacent encoded-words." However, some mailers break this, so we
// try to handle characters spanned across parts anyway by iterating
// through and aggregating sequential encoded parts with the same
// character set and encoding, then perform the decoding on the
// aggregation as a whole.
$tmp[] = $text;
if ($next_match = $matches[$idx+1]) {
if ($next_match[0][1] == $start
&& $next_match[1][0] == $charset
&& $next_match[2][0] == $encoding
) {
continue;
}
}
$count = count($tmp);
$text = '';
// Decode and join encoded-word's chunks
if ($encoding == 'B' || $encoding == 'b') {
// base64 must be decoded a segment at a time
for ($i=0; $i<$count; $i++)
$text .= base64_decode($tmp[$i]);
}
else { //if ($encoding == 'Q' || $encoding == 'q') {
// quoted printable can be combined and processed at once
for ($i=0; $i<$count; $i++)
$text .= $tmp[$i];
$text = str_replace('_', ' ', $text);
$text = quoted_printable_decode($text);
}
$out .= rcube_charset_convert($text, $charset);
$tmp = array();
}
// add the last part of the input string
if ($start != strlen($input)) {
$out .= rcube_charset_convert(substr($input, $start), $default_charset);
}
// return the results
return $out;
}
// no encoding information, use fallback
return rcube_charset_convert($input, $default_charset);
}
/**
* Decode a mime part
*
* @param string $input Input string
* @param string $encoding Part encoding
* @return string Decoded string
*/
public static function decode($input, $encoding = '7bit')
{
switch (strtolower($encoding)) {
case 'quoted-printable':
return quoted_printable_decode($input);
case 'base64':
return base64_decode($input);
case 'x-uuencode':
case 'x-uue':
case 'uue':
case 'uuencode':
return convert_uudecode($input);
case '7bit':
default:
return $input;
}
}
/**
* Split RFC822 header string into an associative array
* @access private
*/
public static function parse_headers($headers)
{
$a_headers = array();
$headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
$lines = explode("\n", $headers);
$c = count($lines);
for ($i=0; $i<$c; $i++) {
if ($p = strpos($lines[$i], ': ')) {
$field = strtolower(substr($lines[$i], 0, $p));
$value = trim(substr($lines[$i], $p+1));
if (!empty($value))
$a_headers[$field] = $value;
}
}
return $a_headers;
}
/**
* @access private
*/
private static function parse_address_list($str, $decode = true, $fallback = null)
{
// remove any newlines and carriage returns before
$str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
// extract list items, remove comments
$str = self::explode_header_string(',;', $str, true);
$result = array();
// simplified regexp, supporting quoted local part
$email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
foreach ($str as $key => $val) {
$name = '';
$address = '';
$val = trim($val);
if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
$address = $m[2];
$name = trim($m[1]);
}
else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
$address = $m[1];
$name = '';
}
else {
$name = $val;
}
// dequote and/or decode name
if ($name) {
if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
$name = stripslashes($name);
}
if ($decode) {
$name = self::decode_header($name, $fallback);
}
}
if (!$address && $name) {
$address = $name;
}
if ($address) {
$result[$key] = array('name' => $name, 'address' => $address);
}
}
return $result;
}
/**
* Explodes header (e.g. address-list) string into array of strings
* using specified separator characters with proper handling
* of quoted-strings and comments (RFC2822)
*
* @param string $separator String containing separator characters
* @param string $str Header string
* @param bool $remove_comments Enable to remove comments
*
* @return array Header items
*/
public static function explode_header_string($separator, $str, $remove_comments = false)
{
$length = strlen($str);
$result = array();
$quoted = false;
$comment = 0;
$out = '';
for ($i=0; $i<$length; $i++) {
// we're inside a quoted string
if ($quoted) {
if ($str[$i] == '"') {
$quoted = false;
}
else if ($str[$i] == "\\") {
if ($comment <= 0) {
$out .= "\\";
}
$i++;
}
}
// we are inside a comment string
else if ($comment > 0) {
if ($str[$i] == ')') {
$comment--;
}
else if ($str[$i] == '(') {
$comment++;
}
else if ($str[$i] == "\\") {
$i++;
}
continue;
}
// separator, add to result array
else if (strpos($separator, $str[$i]) !== false) {
if ($out) {
$result[] = $out;
}
$out = '';
continue;
}
// start of quoted string
else if ($str[$i] == '"') {
$quoted = true;
}
// start of comment
else if ($remove_comments && $str[$i] == '(') {
$comment++;
}
if ($comment <= 0) {
$out .= $str[$i];
}
}
if ($out && $comment <= 0) {
$result[] = $out;
}
return $result;
}
/**
* Interpret a format=flowed message body according to RFC 2646
*
* @param string $text Raw body formatted as flowed text
*
* @return string Interpreted text with unwrapped lines and stuffed space removed
*/
public static function unfold_flowed($text)
{
$text = preg_split('/\r?\n/', $text);
$last = -1;
$q_level = 0;
foreach ($text as $idx => $line) {
if ($line[0] == '>' && preg_match('/^(>+\s*)/', $line, $regs)) {
$q = strlen(str_replace(' ', '', $regs[0]));
$line = substr($line, strlen($regs[0]));
if ($q == $q_level && $line
&& isset($text[$last])
&& $text[$last][strlen($text[$last])-1] == ' '
) {
$text[$last] .= $line;
unset($text[$idx]);
}
else {
$last = $idx;
}
}
else {
$q = 0;
if ($line == '-- ') {
$last = $idx;
}
else {
// remove space-stuffing
$line = preg_replace('/^\s/', '', $line);
if (isset($text[$last]) && $line
&& $text[$last] != '-- '
&& $text[$last][strlen($text[$last])-1] == ' '
) {
$text[$last] .= $line;
unset($text[$idx]);
}
else {
$text[$idx] = $line;
$last = $idx;
}
}
}
$q_level = $q;
}
return implode("\r\n", $text);
}
/**
* Wrap the given text to comply with RFC 2646
*
* @param string $text Text to wrap
* @param int $length Length
*
* @return string Wrapped text
*/
public static function format_flowed($text, $length = 72)
{
$text = preg_split('/\r?\n/', $text);
foreach ($text as $idx => $line) {
if ($line != '-- ') {
if ($line[0] == '>' && preg_match('/^(>+)/', $line, $regs)) {
$prefix = $regs[0];
$level = strlen($prefix);
$line = rtrim(substr($line, $level));
$line = $prefix . rc_wordwrap($line, $length - $level - 2, " \r\n$prefix ");
}
else if ($line) {
$line = rc_wordwrap(rtrim($line), $length - 2, " \r\n");
// space-stuffing
$line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);
}
$text[$idx] = $line;
}
}
return implode("\r\n", $text);
}
}

@ -37,7 +37,7 @@ if ($abook == null || !is_object($CONTACTS)) {
if (!empty($_POST['_address']) && is_object($CONTACTS)) if (!empty($_POST['_address']) && is_object($CONTACTS))
{ {
$contact_arr = $RCMAIL->imap->decode_address_list(get_input_value('_address', RCUBE_INPUT_POST, true), 1, false); $contact_arr = rcube_mime::decode_address_list(get_input_value('_address', RCUBE_INPUT_POST, true), 1, false);
if (!empty($contact_arr[1]['mailto'])) { if (!empty($contact_arr[1]['mailto'])) {
$contact = array( $contact = array(

@ -268,7 +268,7 @@ else if (count($MESSAGE->identities)) {
// extract all recipients of the reply-message // extract all recipients of the reply-message
if (is_object($MESSAGE->headers) && in_array($compose_mode, array(RCUBE_COMPOSE_REPLY, RCUBE_COMPOSE_FORWARD))) if (is_object($MESSAGE->headers) && in_array($compose_mode, array(RCUBE_COMPOSE_REPLY, RCUBE_COMPOSE_FORWARD)))
{ {
$a_to = $RCMAIL->imap->decode_address_list($MESSAGE->headers->to); $a_to = rcube_mime::decode_address_list($MESSAGE->headers->to, null, true, $MESSAGE->headers->charset);
foreach ($a_to as $addr) { foreach ($a_to as $addr) {
if (!empty($addr['mailto'])) { if (!empty($addr['mailto'])) {
$a_recipients[] = strtolower($addr['mailto']); $a_recipients[] = strtolower($addr['mailto']);
@ -277,7 +277,7 @@ else if (count($MESSAGE->identities)) {
} }
if (!empty($MESSAGE->headers->cc)) { if (!empty($MESSAGE->headers->cc)) {
$a_cc = $RCMAIL->imap->decode_address_list($MESSAGE->headers->cc); $a_cc = rcube_mime::decode_address_list($MESSAGE->headers->cc, null, true, $MESSAGE->headers->charset);
foreach ($a_cc as $addr) { foreach ($a_cc as $addr) {
if (!empty($addr['mailto'])) { if (!empty($addr['mailto'])) {
$a_recipients[] = strtolower($addr['mailto']); $a_recipients[] = strtolower($addr['mailto']);
@ -420,7 +420,7 @@ foreach ($parts as $header) {
// split recipients and put them back together in a unique way // split recipients and put them back together in a unique way
if (!empty($fvalue) && in_array($header, array('to', 'cc', 'bcc'))) { if (!empty($fvalue) && in_array($header, array('to', 'cc', 'bcc'))) {
$to_addresses = $RCMAIL->imap->decode_address_list($fvalue, null, $decode_header); $to_addresses = rcube_mime::decode_address_list($fvalue, null, $decode_header, $MESSAGE->headers->charset);
$fvalue = array(); $fvalue = array();
foreach ($to_addresses as $addr_part) { foreach ($to_addresses as $addr_part) {
@ -650,7 +650,7 @@ function rcmail_prepare_message_body()
if ($body && $part && $part->ctype_secondary == 'plain' if ($body && $part && $part->ctype_secondary == 'plain'
&& $part->ctype_parameters['format'] == 'flowed' && $part->ctype_parameters['format'] == 'flowed'
) { ) {
$body = rcube_message::unfold_flowed($body); $body = rcube_mime::unfold_flowed($body);
} }
} }
} }
@ -811,7 +811,7 @@ function rcmail_create_reply_body($body, $bodyIsHtml)
global $RCMAIL, $MESSAGE, $LINE_LENGTH; global $RCMAIL, $MESSAGE, $LINE_LENGTH;
// build reply prefix // build reply prefix
$from = array_pop($RCMAIL->imap->decode_address_list($MESSAGE->get_header('from'), 1, false)); $from = array_pop(rcube_mime::decode_address_list($MESSAGE->get_header('from'), 1, false, $MESSAGE->headers->charset));
$prefix = rcube_label(array( $prefix = rcube_label(array(
'name' => 'mailreplyintro', 'name' => 'mailreplyintro',
'vars' => array( 'vars' => array(

@ -266,14 +266,12 @@ function rcmail_js_message_list($a_headers, $insert_top=FALSE, $a_show_cols=null
$a_msg_cols = array(); $a_msg_cols = array();
$a_msg_flags = array(); $a_msg_flags = array();
$RCMAIL->imap->set_charset(!empty($header->charset) ? $header->charset : $CONFIG['default_charset']);
// format each col; similar as in rcmail_message_list() // format each col; similar as in rcmail_message_list()
foreach ($a_show_cols as $col) { foreach ($a_show_cols as $col) {
if (in_array($col, array('from', 'to', 'cc', 'replyto'))) if (in_array($col, array('from', 'to', 'cc', 'replyto')))
$cont = Q(rcmail_address_string($header->$col, 3), 'show'); $cont = Q(rcmail_address_string($header->$col, 3, false, null, $header->charset), 'show');
else if ($col=='subject') { else if ($col=='subject') {
$cont = trim($RCMAIL->imap->decode_header($header->$col)); $cont = trim(rcube_mime::decode_header($header->$col, $header->charset));
if (!$cont) $cont = rcube_label('nosubject'); if (!$cont) $cont = rcube_label('nosubject');
$cont = Q($cont); $cont = Q($cont);
} }
@ -914,7 +912,7 @@ function rcmail_message_headers($attrib, $headers=NULL)
} }
else if ($hkey == 'replyto') { else if ($hkey == 'replyto') {
if ($headers['replyto'] != $headers['from']) if ($headers['replyto'] != $headers['from'])
$header_value = rcmail_address_string($value, null, true, $attrib['addicon']); $header_value = rcmail_address_string($value, null, true, $attrib['addicon'], $headers['charset']);
else else
continue; continue;
} }
@ -922,19 +920,19 @@ function rcmail_message_headers($attrib, $headers=NULL)
if ($headers['mail-replyto'] != $headers['reply-to'] if ($headers['mail-replyto'] != $headers['reply-to']
&& $headers['reply-to'] != $headers['from'] && $headers['reply-to'] != $headers['from']
) )
$header_value = rcmail_address_string($value, null, true, $attrib['addicon']); $header_value = rcmail_address_string($value, null, true, $attrib['addicon'], $headers['charset']);
else else
continue; continue;
} }
else if ($hkey == 'mail-followup-to') { else if ($hkey == 'mail-followup-to') {
$header_value = rcmail_address_string($value, null, true, $attrib['addicon']); $header_value = rcmail_address_string($value, null, true, $attrib['addicon'], $headers['charset']);
} }
else if (in_array($hkey, array('from', 'to', 'cc', 'bcc'))) else if (in_array($hkey, array('from', 'to', 'cc', 'bcc')))
$header_value = rcmail_address_string($value, null, true, $attrib['addicon']); $header_value = rcmail_address_string($value, null, true, $attrib['addicon'], $headers['charset']);
else if ($hkey == 'subject' && empty($value)) else if ($hkey == 'subject' && empty($value))
$header_value = rcube_label('nosubject'); $header_value = rcube_label('nosubject');
else else
$header_value = trim($RCMAIL->imap->decode_header($value)); $header_value = trim(rcube_mime::decode_header($value, $headers['charset']));
$output_headers[$hkey] = array( $output_headers[$hkey] = array(
'title' => rcube_label(preg_replace('/(^mail-|-)/', '', $hkey)), 'title' => rcube_label(preg_replace('/(^mail-|-)/', '', $hkey)),
@ -1260,11 +1258,11 @@ function rcmail_alter_html_link($matches)
/** /**
* decode address string and re-format it as HTML links * decode address string and re-format it as HTML links
*/ */
function rcmail_address_string($input, $max=null, $linked=false, $addicon=null) function rcmail_address_string($input, $max=null, $linked=false, $addicon=null, $default_charset=null)
{ {
global $RCMAIL, $PRINT_MODE, $CONFIG; global $RCMAIL, $PRINT_MODE, $CONFIG;
$a_parts = $RCMAIL->imap->decode_address_list($input); $a_parts = rcube_mime::decode_address_list($input, null, true, $default_charset);
if (!sizeof($a_parts)) if (!sizeof($a_parts))
return $input; return $input;
@ -1483,7 +1481,8 @@ function rcmail_send_mdn($message, &$smtp_error)
{ {
$identity = $RCMAIL->user->get_identity(); $identity = $RCMAIL->user->get_identity();
$sender = format_email_recipient($identity['email'], $identity['name']); $sender = format_email_recipient($identity['email'], $identity['name']);
$recipient = array_shift($RCMAIL->imap->decode_address_list($message->headers->mdn_to)); $recipient = array_shift(rcube_mime::decode_address_list(
$message->headers->mdn_to, 1, true, $message->headers->charset));
$mailto = $recipient['mailto']; $mailto = $recipient['mailto'];
$compose = new Mail_mime("\r\n"); $compose = new Mail_mime("\r\n");

@ -520,7 +520,7 @@ else {
// compose format=flowed content if enabled // compose format=flowed content if enabled
if ($flowed = $RCMAIL->config->get('send_format_flowed', true)) if ($flowed = $RCMAIL->config->get('send_format_flowed', true))
$message_body = rcube_message::format_flowed($message_body, min($LINE_LENGTH+2, 79)); $message_body = rcube_mime::format_flowed($message_body, min($LINE_LENGTH+2, 79));
else else
$message_body = rc_wordwrap($message_body, $LINE_LENGTH, "\r\n"); $message_body = rc_wordwrap($message_body, $LINE_LENGTH, "\r\n");

@ -29,7 +29,8 @@ if ($uid = get_input_value('_uid', RCUBE_INPUT_GET))
header("Content-Type: text/plain; charset={$charset}"); header("Content-Type: text/plain; charset={$charset}");
if (!empty($_GET['_save'])) { if (!empty($_GET['_save'])) {
$filename = ($headers->subject ? $RCMAIL->imap->decode_header($headers->subject) : 'roundcube') . '.eml'; $subject = rcube_mime::decode_header($headers->subject, $headers->charset);
$filename = ($subject ? $subject : $RCMAIL->config->get('product_name', 'email')) . '.eml';
$browser = $RCMAIL->output->browser; $browser = $RCMAIL->output->browser;
if ($browser->ie && $browser->ver < 7) if ($browser->ie && $browser->ver < 7)

@ -12,14 +12,11 @@ class rcube_test_maildecode extends UnitTestCase
function __construct() function __construct()
{ {
$this->UnitTestCase('Mail headers decoding tests'); $this->UnitTestCase('Mail headers decoding tests');
$this->app = rcmail::get_instance();
$this->app->imap_init(false);
} }
/** /**
* Test decoding of single e-mail address strings * Test decoding of single e-mail address strings
* Uses rcube_imap::decode_address_list() * Uses rcube_mime::decode_address_list()
*/ */
function test_decode_single_address() function test_decode_single_address()
{ {
@ -76,7 +73,7 @@ class rcube_test_maildecode extends UnitTestCase
); );
foreach ($headers as $idx => $header) { foreach ($headers as $idx => $header) {
$res = $this->app->imap->decode_address_list($header); $res = rcube_mime::decode_address_list($header);
$this->assertEqual($results[$idx][0], count($res), "Rows number in result for header: " . $header); $this->assertEqual($results[$idx][0], count($res), "Rows number in result for header: " . $header);
$this->assertEqual($results[$idx][1], $res[1]['name'], "Name part decoding for header: " . $header); $this->assertEqual($results[$idx][1], $res[1]['name'], "Name part decoding for header: " . $header);
@ -86,7 +83,7 @@ class rcube_test_maildecode extends UnitTestCase
/** /**
* Test decoding of header values * Test decoding of header values
* Uses rcube_imap::decode_mime_string() * Uses rcube_mime::decode_mime_string()
*/ */
function test_header_decode_qp() function test_header_decode_qp()
{ {
@ -123,7 +120,7 @@ class rcube_test_maildecode extends UnitTestCase
); );
foreach ($test as $idx => $item) { foreach ($test as $idx => $item) {
$res = $this->app->imap->decode_mime_string($item['in'], 'UTF-8'); $res = rcube_mime::decode_mime_string($item['in'], 'UTF-8');
$res = quoted_printable_encode($res); $res = quoted_printable_encode($res);
$this->assertEqual($item['out'], $res, "Header decoding for: " . $idx); $this->assertEqual($item['out'], $res, "Header decoding for: " . $idx);

Loading…
Cancel
Save