alecpl
2012-01-05 1c4f23d6e58e12f93d8de2c3ae416df575e8ad85
- Exclude MIME functionality from rcube_imap class into rcube_mime class


1 files added
9 files modified
1098 ■■■■ changed files
bin/msgexport.sh 2 ●●● patch | view | raw | blame | history
program/include/rcube_imap.php 445 ●●●●● patch | view | raw | blame | history
program/include/rcube_message.php 116 ●●●● patch | view | raw | blame | history
program/include/rcube_mime.php 484 ●●●●● patch | view | raw | blame | history
program/steps/mail/addcontact.inc 2 ●●● patch | view | raw | blame | history
program/steps/mail/compose.inc 10 ●●●● patch | view | raw | blame | history
program/steps/mail/func.inc 23 ●●●● patch | view | raw | blame | history
program/steps/mail/sendmail.inc 2 ●●● patch | view | raw | blame | history
program/steps/mail/viewsource.inc 3 ●●●● patch | view | raw | blame | history
tests/maildecode.php 11 ●●●●● patch | view | raw | blame | history
bin/msgexport.sh
@@ -51,7 +51,7 @@
    for ($count = $IMAP->messagecount(), $i=1; $i <= $count; $i++)
    {
        $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, $IMAP->conn->fetchPartHeader($mbox, $i));
program/include/rcube_imap.php
@@ -382,28 +382,6 @@
    /**
     * 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
     *
     * @param  number $page Page number to list
@@ -428,32 +406,30 @@
    /**
     * 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  rcube_result_index|rcube_result_thread  Result set
     * @param  string  Charset of search string
     * @param  string  Sorting field
     * @param  string  True if set is sorted (SORT was used for searching)
     * @param  array  $set  Search set, result from rcube_imap::get_search_set():
     *                      0 - searching criteria, string
     *                      1 - search result, rcube_result_index|rcube_result_thread
     *                      2 - searching character set, string
     *                      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)
            list($str, $msgs, $charset, $sort_field, $sorted) = $str;
        $set = (array)$set;
        $this->search_string     = $str;
        $this->search_set        = $msgs;
        $this->search_charset    = $charset;
        $this->search_sort_field = $sort_field;
        $this->search_sorted     = $sorted;
        $this->search_string     = $set[0];
        $this->search_set        = $set[1];
        $this->search_charset    = $set[2];
        $this->search_sort_field = $set[3];
        $this->search_sorted     = $set[4];
        $this->search_threads    = is_a($this->search_set, 'rcube_result_thread');
    }
    /**
     * Return the saved search set as hash array
     *
     * @param bool $clone Clone result object
     *
     * @return array Search set
     */
@@ -1413,8 +1389,8 @@
        $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
        $this->set_search_set($str, $results, $charset, $sort_field,
            $this->threading || $this->search_sorted ? true : false);
        $this->set_search_set(array($str, $results, $charset, $sort_field,
            $this->threading || $this->search_sorted ? true : false));
    }
@@ -1552,22 +1528,6 @@
    /**
     * 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
     *
     * @param int     $id       Message sequence ID or UID
@@ -1662,7 +1622,7 @@
                return $headers;
        }
        $struct = &$this->_structure_part($structure, 0, '', $headers);
        $struct = $this->_structure_part($structure, 0, '', $headers);
        // don't trust given content-type
        if (empty($struct->parts) && !empty($headers->ctype)) {
@@ -1685,7 +1645,7 @@
     * @param string $parent
     * @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->mime_id = empty($parent) ? (string)$count : "$parent.$count";
@@ -1844,7 +1804,7 @@
            }
            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))
                $struct->headers = get_object_vars($mime_headers) + $struct->headers;
@@ -1988,7 +1948,7 @@
            else
                $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)) {
            // decode filename according to RFC 2231, Section 4
@@ -2031,7 +1991,7 @@
     *
     * @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
        if (!is_object($o_part)) {
@@ -2084,7 +2044,7 @@
     * @return string $part Message/part body
     * @see    rcube_imap::get_message_part()
     */
    function &get_body($uid, $part=1)
    function get_body($uid, $part=1)
    {
        $headers = $this->get_headers($uid);
        return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
@@ -2100,7 +2060,7 @@
     *
     * @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,
            true, null, null, false, $fp);
@@ -2113,7 +2073,7 @@
     * @param int $uid  Message UID
     * @return string Message headers string
     */
    function &get_raw_headers($uid)
    function get_raw_headers($uid)
    {
        return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
    }
@@ -3621,6 +3581,7 @@
        }
    }
    /**
     * Getter for messages cache object
     */
@@ -3637,6 +3598,7 @@
        return $this->mcache;
    }
    /**
     * Clears the messages cache.
     *
@@ -3647,204 +3609,6 @@
    {
        if ($mcache = $this->get_mcache_engine()) {
            $mcache->clear($mailbox, $uids);
        }
    }
    /* --------------------------------
     *   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;
        }
    }
@@ -3937,7 +3701,7 @@
     *
     * @return int Message UID
     */
    function id2uid($id, $mailbox = null)
    private function id2uid($id, $mailbox = null)
    {
        if (!strlen($mailbox)) {
            $mailbox = $this->mailbox;
@@ -4025,161 +3789,6 @@
            }
            $this->update_cache('messagecount', $a_mailbox_cache);
        }
    }
    /**
     * 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;
    }
program/include/rcube_message.php
@@ -42,6 +42,13 @@
     * @var rcube_imap
     */
    private $imap;
    /**
     * Instance of mime class
     *
     * @var rcube_mime
     */
    private $mime;
    private $opt = array();
    private $inline_parts = array();
    private $parse_alternative = false;
@@ -63,27 +70,24 @@
     *
     * @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
     */
    function __construct($uid)
    {
        $this->uid  = $uid;
        $this->app = rcmail::get_instance();
        $this->imap = $this->app->imap;
        $this->imap->get_all_headers = true;
        $this->uid = $uid;
        $this->headers = $this->imap->get_message($uid);
        if (!$this->headers)
            return;
        $this->subject = rcube_imap::decode_mime_string(
            $this->headers->subject, $this->headers->charset);
        list(, $this->sender) = each($this->imap->decode_address_list($this->headers->from));
        $this->mime = new rcube_mime($this->headers->charset);
        $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->opt = array(
@@ -115,12 +119,15 @@
     */
    public function get_header($name, $raw = false)
    {
        if (empty($this->headers))
            return null;
        if ($this->headers->$name)
            $value = $this->headers->$name;
        else if ($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 @@
        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);
    }
}
program/include/rcube_mime.php
New file
@@ -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);
    }
}
program/steps/mail/addcontact.inc
@@ -37,7 +37,7 @@
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'])) {
    $contact = array(
program/steps/mail/compose.inc
@@ -268,7 +268,7 @@
  // extract all recipients of the reply-message
  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) {
      if (!empty($addr['mailto'])) {
        $a_recipients[] = strtolower($addr['mailto']);
@@ -277,7 +277,7 @@
    }
    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) {
        if (!empty($addr['mailto'])) {
          $a_recipients[] = strtolower($addr['mailto']);
@@ -420,7 +420,7 @@
  // split recipients and put them back together in a unique way
  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();
    foreach ($to_addresses as $addr_part) {
@@ -650,7 +650,7 @@
        if ($body && $part && $part->ctype_secondary == 'plain'
            && $part->ctype_parameters['format'] == 'flowed'
        ) {
          $body = rcube_message::unfold_flowed($body);
          $body = rcube_mime::unfold_flowed($body);
        }
      }
    }
@@ -811,7 +811,7 @@
  global $RCMAIL, $MESSAGE, $LINE_LENGTH;
  // 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(
    'name' => 'mailreplyintro',
    'vars' => array(
program/steps/mail/func.inc
@@ -266,14 +266,12 @@
    $a_msg_cols = 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()
    foreach ($a_show_cols as $col) {
      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') {
        $cont = trim($RCMAIL->imap->decode_header($header->$col));
        $cont = trim(rcube_mime::decode_header($header->$col, $header->charset));
        if (!$cont) $cont = rcube_label('nosubject');
        $cont = Q($cont);
      }
@@ -914,7 +912,7 @@
    }
    else if ($hkey == 'replyto') {
      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
        continue;
    }
@@ -922,19 +920,19 @@
      if ($headers['mail-replyto'] != $headers['reply-to']
        && $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
        continue;
    }
    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')))
      $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))
      $header_value = rcube_label('nosubject');
    else
      $header_value = trim($RCMAIL->imap->decode_header($value));
      $header_value = trim(rcube_mime::decode_header($value, $headers['charset']));
    $output_headers[$hkey] = array(
        'title' => rcube_label(preg_replace('/(^mail-|-)/', '', $hkey)),
@@ -1260,11 +1258,11 @@
/**
 * 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;
  $a_parts = $RCMAIL->imap->decode_address_list($input);
  $a_parts = rcube_mime::decode_address_list($input, null, true, $default_charset);
  if (!sizeof($a_parts))
    return $input;
@@ -1483,7 +1481,8 @@
  {
    $identity = $RCMAIL->user->get_identity();
    $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'];
    $compose = new Mail_mime("\r\n");
program/steps/mail/sendmail.inc
@@ -520,7 +520,7 @@
  // compose format=flowed content if enabled
  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
    $message_body = rc_wordwrap($message_body, $LINE_LENGTH, "\r\n");
program/steps/mail/viewsource.inc
@@ -29,7 +29,8 @@
  header("Content-Type: text/plain; charset={$charset}");
  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;
    if ($browser->ie && $browser->ver < 7)
tests/maildecode.php
@@ -12,14 +12,11 @@
  function __construct()
  {
    $this->UnitTestCase('Mail headers decoding tests');
    $this->app = rcmail::get_instance();
    $this->app->imap_init(false);
  }
  /**
   * 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()
  {
@@ -76,7 +73,7 @@
    );
    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][1], $res[1]['name'], "Name part decoding for header: " . $header);
@@ -86,7 +83,7 @@
  /**
   * Test decoding of header values
   * Uses rcube_imap::decode_mime_string()
   * Uses rcube_mime::decode_mime_string()
   */
  function test_header_decode_qp()
  {
@@ -123,7 +120,7 @@
    );
    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);
      $this->assertEqual($item['out'], $res, "Header decoding for: " . $idx);