Aleksander Machniak
2013-10-17 197203727417a03d87053a47e5aa5175a76e3e0b
program/include/rcube_imap.php
@@ -32,7 +32,6 @@
 */
class rcube_imap
{
    public $debug_level = 1;
    public $skip_deleted = false;
    public $page_size = 10;
    public $list_page = 1;
@@ -318,6 +317,19 @@
    /**
     * Activate/deactivate debug mode
     *
     * @param boolean $dbg True if IMAP conversation should be logged
     * @access public
     */
    function set_debug($dbg = true)
    {
        $this->options['debug'] = $dbg;
        $this->conn->setDebug($dbg, array($this, 'debug_handler'));
    }
    /**
     * Set default message charset
     *
     * This will be used for message decoding if a charset specification is not available
@@ -466,7 +478,7 @@
     */
    function get_mailbox_name()
    {
        return $this->conn->connected() ? $this->mailbox : '';
        return $this->mailbox;
    }
@@ -675,8 +687,8 @@
    {
        $mode = strtoupper($mode);
        // count search set
        if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
        // count search set, assume search set is always up-to-date (don't check $force flag)
        if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS')) {
            if ($this->search_threads)
                return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
            else
@@ -1361,6 +1373,11 @@
        $this->_messagecount($mailbox, 'ALL', true);
        $result = 0;
        if (empty($old)) {
            return $result;
        }
        $new = $this->get_folder_stats($mailbox);
        // got new messages
@@ -1508,7 +1525,7 @@
                // I didn't found that SEARCH should return sorted IDs
                if (is_array($a_index))
                    sort($a_index);
            } else if ($max = $this->_messagecount($mailbox)) {
            } else if ($max = $this->_messagecount($mailbox, 'ALL', true, false)) {
                $a_index = range(1, $max);
            }
@@ -1619,6 +1636,7 @@
     * @param  string  $sort_field Header field to sort by
     * @return array   search results as list of message IDs
     * @access public
     * @todo: Search criteria should be provided in non-IMAP format, eg. array
     */
    function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL)
    {
@@ -1694,7 +1712,7 @@
        }
        if ($orig_criteria == 'ALL') {
            $max = $this->_messagecount($mailbox);
            $max = $this->_messagecount($mailbox, 'ALL', true, false);
            $a_messages = $max ? range(1, $max) : array();
        }
        else {
@@ -1704,7 +1722,7 @@
            // Error, try with US-ASCII (some servers may support only US-ASCII)
            if ($a_messages === false && $charset && $charset != 'US-ASCII')
                $a_messages = $this->conn->search($mailbox,
                    'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset));
                    $this->convert_criteria($criteria, $charset));
            // I didn't found that SEARCH should return sorted IDs
            if (is_array($a_messages) && !$this->sort_field)
@@ -1758,9 +1776,9 @@
                $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
                $string = substr($str, $string_offset - 1, $m[0]);
                $string = rcube_charset_convert($string, $charset, $dest_charset);
                if (!$string)
                if ($string === false)
                    continue;
                $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
                $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
                $last = $m[0] + $string_offset - 1;
            }
            if ($last < strlen($str))
@@ -1961,6 +1979,10 @@
        }
        $headers = $this->get_headers($uid, $mailbox);
        // message doesn't exist?
        if (empty($headers))
            return null;
        // structure might be cached
        if (!empty($headers->structure))
@@ -2320,9 +2342,14 @@
        // decode filename
        if (!empty($filename_mime)) {
            $part->filename = rcube_imap::decode_mime_string($filename_mime,
                $part->charset ? $part->charset : ($this->struct_charset ? $this->struct_charset :
                rc_detect_encoding($filename_mime, $this->default_charset)));
            if (!empty($part->charset))
                $charset = $part->charset;
            else if (!empty($this->struct_charset))
                $charset = $this->struct_charset;
            else
                $charset = rc_detect_encoding($filename_mime, $this->default_charset);
            $part->filename = rcube_imap::decode_mime_string($filename_mime, $charset);
        }
        else if (!empty($filename_encoded)) {
            // decode filename according to RFC 2231, Section 4
@@ -2330,6 +2357,7 @@
                $filename_charset = $fmatches[1];
                $filename_encoded = $fmatches[2];
            }
            $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
        }
    }
@@ -2366,24 +2394,22 @@
     */
    function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
    {
        // get part encoding if not provided
        // get part data if not provided
        if (!is_object($o_part)) {
            $structure = $this->conn->getStructure($this->mailbox, $uid, true);
            $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
            $o_part = new rcube_message_part;
            $o_part->ctype_primary = strtolower(rcube_imap_generic::getStructurePartType($structure, $part));
            $o_part->encoding      = strtolower(rcube_imap_generic::getStructurePartEncoding($structure, $part));
            $o_part->charset       = rcube_imap_generic::getStructurePartCharset($structure, $part);
            $o_part->ctype_primary = $part_data['type'];
            $o_part->encoding      = $part_data['encoding'];
            $o_part->charset       = $part_data['charset'];
            $o_part->size          = $part_data['size'];
        }
        // TODO: Add caching for message parts
        if (!$part) {
            $part = 'TEXT';
        if ($o_part && $o_part->size) {
            $body = $this->conn->handlePartBody($this->mailbox, $uid, true,
                $part ? $part : 'TEXT', $o_part->encoding, $print, $fp);
        }
        $body = $this->conn->handlePartBody($this->mailbox, $uid, true, $part,
            $o_part->encoding, $print, $fp);
        if ($fp || $print) {
            return true;
@@ -2396,7 +2422,11 @@
           if (!$skip_charset_conv) {
                if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
                    $o_part->charset = $this->default_charset;
                    // try to extract charset information from HTML meta tag (#1488125)
                    if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m))
                        $o_part->charset = strtoupper($m[1]);
                    else
                        $o_part->charset = $this->default_charset;
                }
                $body = rcube_charset_convert($body, $o_part->charset);
            }
@@ -2582,10 +2612,14 @@
        // make sure mailbox exists
        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
            if (in_array($to_mbox, $this->default_folders))
                $this->create_mailbox($to_mbox, true);
            else
            if (in_array($to_mbox, $this->default_folders)) {
                if (!$this->create_mailbox($to_mbox, true)) {
                    return false;
                }
            }
            else {
                return false;
            }
        }
        $config = rcmail::get_instance()->config;
@@ -2663,10 +2697,14 @@
        // make sure mailbox exists
        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
            if (in_array($to_mbox, $this->default_folders))
                $this->create_mailbox($to_mbox, true);
            else
            if (in_array($to_mbox, $this->default_folders)) {
                if (!$this->create_mailbox($to_mbox, true)) {
                    return false;
                }
            }
            else {
                return false;
            }
        }
        // copy messages
@@ -2967,14 +3005,14 @@
    /**
     * Private method for mailbox listing
     * Private method for mailbox listing (LSUB)
     *
     * @param   string  $root   Optional root folder
     * @param   string  $name   Optional name pattern
     * @param   mixed   $filter Optional filter
     * @param   string  $rights Optional ACL requirements
     *
     * @return  array   List of mailboxes/folders
     * @return  array   List of subscribed folders
     * @see     rcube_imap::list_mailboxes()
     * @access  private
     */
@@ -3002,9 +3040,10 @@
                    NULL, array('SUBSCRIBED'));
                // unsubscribe non-existent folders, remove from the list
                if (is_array($a_folders) && $name == '*') {
                // we can do this only when LIST response is available
                if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
                    foreach ($a_folders as $idx => $folder) {
                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
                        if (($opts = $this->conn->data['LIST'][$folder])
                            && in_array('\\NonExistent', $opts)
                        ) {
                            $this->conn->unsubscribe($folder);
@@ -3017,11 +3056,12 @@
            else {
                $a_folders = $this->conn->listSubscribed($root, $name);
                // unsubscribe non-existent folders, remove from the list
                if (is_array($a_folders) && $name == '*') {
                // unsubscribe non-existent folders, remove them from the list,
                // we can do this only when LIST response is available
                if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
                    foreach ($a_folders as $idx => $folder) {
                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
                            && in_array('\\Noselect', $opts)
                        if (!isset($this->conn->data['LIST'][$folder])
                            || in_array('\\Noselect', $this->conn->data['LIST'][$folder])
                        ) {
                            // Some servers returns \Noselect for existing folders
                            if (!$this->mailbox_exists($folder)) {
@@ -3055,7 +3095,19 @@
     */
    function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
    {
        // @TODO: caching
        $cache_key = $root.':'.$name;
        if (!empty($filter)) {
            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
        }
        $cache_key .= ':'.$rights;
        $cache_key = 'mailboxes.list.'.md5($cache_key);
        // get cached folder list
        $a_mboxes = $this->get_cache($cache_key);
        if (is_array($a_mboxes)) {
            return $a_mboxes;
        }
        // Give plugins a chance to provide a list of mailboxes
        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
@@ -3065,7 +3117,7 @@
        }
        else {
            // retrieve list of folders from IMAP server
            $a_mboxes = $this->conn->listMailboxes($root, $name);
            $a_mboxes = $this->_list_unsubscribed($root, $name);
        }
        if (!is_array($a_mboxes)) {
@@ -3075,6 +3127,11 @@
        // INBOX should always be available
        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
            array_unshift($a_mboxes, 'INBOX');
        }
        // cache folder attributes
        if ($root == '' && $name == '*' && empty($filter)) {
            $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
        }
        // filter folders list according to rights requirements
@@ -3087,7 +3144,74 @@
            $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
        }
        // write mailboxlist to cache
        $this->update_cache($cache_key, $a_mboxes);
        return $a_mboxes;
    }
    /**
     * Private method for mailbox listing (LIST)
     *
     * @param   string  $root   Optional root folder
     * @param   string  $name   Optional name pattern
     *
     * @return  array   List of folders
     * @see     rcube_imap::list_unsubscribed()
     */
    private function _list_unsubscribed($root='', $name='*')
    {
        $result = $this->conn->listMailboxes($root, $name);
        if (!is_array($result)) {
            return array();
        }
        // #1486796: some server configurations doesn't
        // return folders in all namespaces, we'll try to detect that situation
        // and ask for these namespaces separately
        if ($root == '' && $name == '*') {
            $delim     = $this->get_hierarchy_delimiter();
            $namespace = $this->get_namespace();
            $search    = array();
            // build list of namespace prefixes
            foreach ((array)$namespace as $ns) {
                if (is_array($ns)) {
                    foreach ($ns as $ns_data) {
                        if (strlen($ns_data[0])) {
                            $search[] = $ns_data[0];
                        }
                    }
                }
            }
            if (!empty($search)) {
                // go through all folders detecting namespace usage
                foreach ($result as $folder) {
                    foreach ($search as $idx => $prefix) {
                        if (strpos($folder, $prefix) === 0) {
                            unset($search[$idx]);
                        }
                    }
                    if (empty($search)) {
                        break;
                    }
                }
                // get folders in hidden namespaces and add to the result
                foreach ($search as $prefix) {
                    $list = $this->conn->listMailboxes($prefix, $name);
                    if (!empty($list)) {
                        $result = array_merge($result, $list);
                    }
                }
            }
        }
        return $result;
    }
@@ -3363,8 +3487,8 @@
        foreach ($this->namespace as $type => $namespace) {
            if (is_array($namespace)) {
                foreach ($namespace as $ns) {
                    if (strlen($ns[0])) {
                        if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
                    if ($len = strlen($ns[0])) {
                        if (($len > 1 && $mailbox == substr($ns[0], 0, -1))
                            || strpos($mailbox, $ns[0]) === 0
                        ) {
                            return $type;
@@ -3418,30 +3542,29 @@
    /**
     * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
     * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
     *
     * @param string $mailbox Folder name
     * @param bool   $force   Set to True if options should be refreshed
     *                        Options are available after LIST command only
     * @param bool   $force   Set to True if attributes should be refreshed
     *
     * @return array Options list
     */
    function mailbox_options($mailbox, $force=false)
    function mailbox_attributes($mailbox, $force=false)
    {
        if ($mailbox == 'INBOX') {
            return array();
        // get attributes directly from LIST command
        if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$mailbox])) {
            $opts = $this->conn->data['LIST'][$mailbox];
        }
        // get cached folder attributes
        else if (!$force) {
            $opts = $this->get_cache('mailboxes.attributes');
            $opts = $opts[$mailbox];
        }
        if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
            if ($force) {
                $this->conn->listMailboxes('', $mailbox);
            }
            else {
                return array();
            }
        if (!is_array($opts)) {
            $this->conn->listMailboxes('', $mailbox);
            $opts = $this->conn->data['LIST'][$mailbox];
        }
        $opts = $this->conn->data['LIST'][$mailbox];
        return is_array($opts) ? $opts : array();
    }
@@ -3524,17 +3647,17 @@
            }
        }
        $options['name']      = $mailbox;
        $options['options']   = $this->mailbox_options($mailbox, true);
        $options['namespace'] = $this->mailbox_namespace($mailbox);
        $options['rights']    = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
        $options['special']   = in_array($mailbox, $this->default_folders);
        $options['name']       = $mailbox;
        $options['attributes'] = $this->mailbox_attributes($mailbox, true);
        $options['namespace']  = $this->mailbox_namespace($mailbox);
        $options['rights']     = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
        $options['special']    = in_array($mailbox, $this->default_folders);
        // Set 'noselect' and 'norename' flags
        if (is_array($options['options'])) {
            foreach ($options['options'] as $opt) {
                $opt = strtolower($opt);
                if ($opt == '\noselect' || $opt == '\nonexistent') {
        // Set 'noselect' flag
        if (is_array($options['attributes'])) {
            foreach ($options['attributes'] as $attrib) {
                $attrib = strtolower($attrib);
                if ($attrib == '\noselect' || $attrib == '\nonexistent') {
                    $options['noselect'] = true;
                }
            }
@@ -3543,6 +3666,15 @@
            $options['noselect'] = true;
        }
        // Get folder rights (MYRIGHTS)
        if ($acl && !$options['noselect']) {
            // skip shared roots
            if (!$options['is_root'] || $options['namespace'] == 'personal') {
                $options['rights'] =  (array)$this->my_rights($mailbox);
            }
        }
        // Set 'norename' flag
        if (!empty($options['rights'])) {
            $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
@@ -4042,7 +4174,7 @@
        $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
        // encoded-word regexp
        $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
        $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
        // Find all RFC2047's encoded words
        if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {