From 1c4f23d6e58e12f93d8de2c3ae416df575e8ad85 Mon Sep 17 00:00:00 2001
From: alecpl <alec@alec.pl>
Date: Thu, 05 Jan 2012 06:25:42 -0500
Subject: [PATCH] - Exclude MIME functionality from rcube_imap class into rcube_mime class

---
 tests/maildecode.php              |   11 
 program/include/rcube_imap.php    |  445 +----------------------
 program/include/rcube_mime.php    |  484 +++++++++++++++++++++++++
 program/steps/mail/compose.inc    |   10 
 program/steps/mail/func.inc       |   23 
 program/steps/mail/viewsource.inc |    3 
 program/include/rcube_message.php |  118 -----
 program/steps/mail/sendmail.inc   |    2 
 bin/msgexport.sh                  |   12 
 program/steps/mail/addcontact.inc |    2 
 10 files changed, 558 insertions(+), 552 deletions(-)

diff --git a/bin/msgexport.sh b/bin/msgexport.sh
index 239d893..5f90619 100755
--- a/bin/msgexport.sh
+++ b/bin/msgexport.sh
@@ -51,17 +51,17 @@
 	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));
 		fwrite($out, $IMAP->conn->handlePartBody($mbox, $i));
 		fwrite($out, "\n\n\n");
-		
+
 		progress_update($i, $count);
 	}
 	vputs("\ncomplete.\n");
-	
+
 	if ($filename)
 		fclose($out);
 }
@@ -114,7 +114,7 @@
 if ($IMAP->connect($host, $args['user'], $args['pass'], $imap_port, $imap_ssl))
 {
 	vputs("IMAP login successful.\n");
-	
+
 	$filename = null;
 	$mailboxes = $args['mbox'] == '*' ? $IMAP->list_mailboxes(null) : array($args['mbox']);
 
@@ -124,7 +124,7 @@
 			$filename = preg_replace('/\.[a-z0-9]{3,4}$/i', '', $args['file']) . asciiwords($mbox) . '.mbox';
 		else if ($args['mbox'] == '*')
 			$filename = asciiwords($mbox) . '.mbox';
-			
+
 		if ($args['mbox'] == '*' && in_array(strtolower($mbox), array('junk','spam','trash')))
 			continue;
 
diff --git a/program/include/rcube_imap.php b/program/include/rcube_imap.php
index 2a2f203..badf815 100644
--- a/program/include/rcube_imap.php
+++ b/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;
     }
 
 
diff --git a/program/include/rcube_message.php b/program/include/rcube_message.php
index 633f59b..d05b57a 100644
--- a/program/include/rcube_message.php
+++ b/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->app = rcmail::get_instance();
+        $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);
-    }
-
 }
diff --git a/program/include/rcube_mime.php b/program/include/rcube_mime.php
new file mode 100644
index 0000000..4d97199
--- /dev/null
+++ b/program/include/rcube_mime.php
@@ -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);
+    }
+
+}
diff --git a/program/steps/mail/addcontact.inc b/program/steps/mail/addcontact.inc
index 0120852..ab7bfc0 100644
--- a/program/steps/mail/addcontact.inc
+++ b/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(
diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
index 855fbab..3095d24 100644
--- a/program/steps/mail/compose.inc
+++ b/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(
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index f791f70..e17b196 100644
--- a/program/steps/mail/func.inc
+++ b/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");
diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc
index 9d4d425..953c752 100644
--- a/program/steps/mail/sendmail.inc
+++ b/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");
 
diff --git a/program/steps/mail/viewsource.inc b/program/steps/mail/viewsource.inc
index d951fbe..e2b4e1b 100644
--- a/program/steps/mail/viewsource.inc
+++ b/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)
diff --git a/tests/maildecode.php b/tests/maildecode.php
index 664161c..4ac4993 100644
--- a/tests/maildecode.php
+++ b/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);

--
Gitblit v1.9.1