Thomas Bruederli
2014-08-18 3ebac0167bf20104fb7a2a55934765117760264c
program/lib/Roundcube/rcube_imap_generic.php
@@ -48,17 +48,19 @@
        '*'        => '\\*',
    );
    private $fp;
    private $host;
    private $logged = false;
    private $capability = array();
    private $capability_readed = false;
    private $prefs;
    private $cmd_tag;
    private $cmd_num = 0;
    private $resourceid;
    private $_debug = false;
    private $_debug_handler = false;
    public static $mupdate;
    protected $fp;
    protected $host;
    protected $logged = false;
    protected $capability = array();
    protected $capability_readed = false;
    protected $prefs;
    protected $cmd_tag;
    protected $cmd_num = 0;
    protected $resourceid;
    protected $_debug = false;
    protected $_debug_handler = false;
    const ERROR_OK = 0;
    const ERROR_NO = -1;
@@ -71,6 +73,7 @@
    const COMMAND_NORESPONSE = 1;
    const COMMAND_CAPABILITY = 2;
    const COMMAND_LASTLINE   = 4;
    const COMMAND_ANONYMIZED = 8;
    const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
@@ -86,16 +89,28 @@
     *
     * @param string $string Command string
     * @param bool   $endln  True if CRLF need to be added at the end of command
     * @param bool   $anonymized Don't write the given data to log but a placeholder
     *
     * @param int Number of bytes sent, False on error
     */
    function putLine($string, $endln=true)
    function putLine($string, $endln=true, $anonymized=false)
    {
        if (!$this->fp)
            return false;
        if ($this->_debug) {
            $this->debug('C: '. rtrim($string));
            // anonymize the sent command for logging
            $cut = $endln ? 2 : 0;
            if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) {
                $log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut);
            }
            else if ($anonymized) {
                $log = sprintf('****** [%d]', strlen($string) - $cut);
            }
            else {
                $log = rtrim($string);
            }
            $this->debug('C: ' . $log);
        }
        $res = fwrite($this->fp, $string . ($endln ? "\r\n" : ''));
@@ -114,10 +129,11 @@
     *
     * @param string $string Command string
     * @param bool   $endln  True if CRLF need to be added at the end of command
     * @param bool   $anonymized Don't write the given data to log but a placeholder
     *
     * @return int|bool Number of bytes sent, False on error
     */
    function putLineC($string, $endln=true)
    function putLineC($string, $endln=true, $anonymized=false)
    {
        if (!$this->fp) {
            return false;
@@ -136,7 +152,7 @@
                        $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
                    }
                    $bytes = $this->putLine($parts[$i].$parts[$i+1], false);
                    $bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized);
                    if ($bytes === false)
                        return false;
                    $res += $bytes;
@@ -151,7 +167,7 @@
                    $i++;
                }
                else {
                    $bytes = $this->putLine($parts[$i], false);
                    $bytes = $this->putLine($parts[$i], false, $anonymized);
                    if ($bytes === false)
                        return false;
                    $res += $bytes;
@@ -336,7 +352,7 @@
     *
     * @return bool True if connection is closed
     */
    private function eof()
    protected function eof()
    {
        if (!is_resource($this->fp)) {
            return true;
@@ -359,7 +375,7 @@
    /**
     * Closes connection stream.
     */
    private function closeSocket()
    protected function closeSocket()
    {
        @fclose($this->fp);
        $this->fp = null;
@@ -405,7 +421,7 @@
        return false;
    }
    private function hasCapability($name)
    protected function hasCapability($name)
    {
        if (empty($this->capability) || $name == '') {
            return false;
@@ -517,7 +533,7 @@
                $reply = base64_encode($user . ' ' . $hash);
                // send result
                $this->putLine($reply);
                $this->putLine($reply, true, true);
            }
            else {
                // RFC2831: DIGEST-MD5
@@ -535,7 +551,7 @@
                    base64_decode($challenge), $this->host, 'imap', $user));
                // send result
                $this->putLine($reply);
                $this->putLine($reply, true, true);
                $line = trim($this->readReply());
                if ($line[0] == '+') {
@@ -575,7 +591,7 @@
            // RFC 4959 (SASL-IR): save one round trip
            if ($this->getCapability('SASL-IR')) {
                list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
                    self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY);
                    self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
            }
            else {
                $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
@@ -586,7 +602,7 @@
                }
                // send result, get reply and process it
                $this->putLine($reply);
                $this->putLine($reply, true, true);
                $line = $this->readReply();
                $result = $this->parseResult($line);
            }
@@ -617,7 +633,7 @@
    function login($user, $password)
    {
        list($code, $response) = $this->execute('LOGIN', array(
            $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY);
            $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
        // re-set capabilities list if untagged CAPABILITY response provided
        if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
@@ -704,119 +720,33 @@
     */
    function connect($host, $user, $password, $options=null)
    {
        // set options
        if (is_array($options)) {
            $this->prefs = $options;
        }
        // set auth method
        if (!empty($this->prefs['auth_type'])) {
            $auth_method = strtoupper($this->prefs['auth_type']);
        } else {
            $auth_method = 'CHECK';
        }
        // configure
        $this->set_prefs($options);
        if (!empty($this->prefs['disabled_caps'])) {
            $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
        }
        $result = false;
        // initialize connection
        $this->error    = '';
        $this->errornum = self::ERROR_OK;
        $this->selected = null;
        $this->user     = $user;
        $this->host     = $host;
        $this->user     = $user;
        $this->logged   = false;
        $this->selected = null;
        // check input
        if (empty($host)) {
            $this->setError(self::ERROR_BAD, "Empty host");
            return false;
        }
        if (empty($user)) {
            $this->setError(self::ERROR_NO, "Empty user");
            return false;
        }
        if (empty($password)) {
            $this->setError(self::ERROR_NO, "Empty password");
            return false;
        }
        if (!$this->prefs['port']) {
            $this->prefs['port'] = 143;
        }
        // check for SSL
        if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
            $host = $this->prefs['ssl_mode'] . '://' . $host;
        }
        if ($this->prefs['timeout'] <= 0) {
            $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
        }
        // Connect
        $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
        if (!$this->fp) {
            if (!$errstr) {
                $errstr = "Unknown reason (fsockopen() function disabled?)";
            }
            $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr));
        if (!$this->_connect($host)) {
            return false;
        }
        if ($this->prefs['timeout'] > 0) {
            stream_set_timeout($this->fp, $this->prefs['timeout']);
        }
        $line = trim(fgets($this->fp, 8192));
        if ($this->_debug) {
            // set connection identifier for debug output
            preg_match('/#([0-9]+)/', (string)$this->fp, $m);
            $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
            if ($line)
                $this->debug('S: '. $line);
        }
        // Connected to wrong port or connection error?
        if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
            if ($line)
                $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
            else
                $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
            $this->setError(self::ERROR_BAD, $error);
            $this->closeConnection();
            return false;
        }
        // RFC3501 [7.1] optional CAPABILITY response
        if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
            $this->parseCapability($matches[1], true);
        }
        // TLS connection
        if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
            if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
                $res = $this->execute('STARTTLS');
                if ($res[0] != self::ERROR_OK) {
                    $this->closeConnection();
                    return false;
                }
                if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
                    $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
                    $this->closeConnection();
                    return false;
                }
                // Now we're secure, capabilities need to be reread
                $this->clearCapability();
            }
        }
        // Send ID info
@@ -824,6 +754,7 @@
            $this->id($this->prefs['ident']);
        }
        $auth_method  = $this->prefs['auth_type'];
        $auth_methods = array();
        $result       = null;
@@ -895,6 +826,133 @@
        $this->closeConnection();
        return false;
    }
    /**
     * Connects to IMAP server.
     *
     * @param string $host Server hostname or IP
     *
     * @return bool True on success, False on failure
     */
    protected function _connect($host)
    {
        // initialize connection
        $this->error    = '';
        $this->errornum = self::ERROR_OK;
        if (!$this->prefs['port']) {
            $this->prefs['port'] = 143;
        }
        // check for SSL
        if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
            $host = $this->prefs['ssl_mode'] . '://' . $host;
        }
        if ($this->prefs['timeout'] <= 0) {
            $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
        }
        if (!empty($this->prefs['socket_options'])) {
            $context  = stream_context_create($this->prefs['socket_options']);
            $this->fp = stream_socket_client($host . ':' . $this->prefs['port'], $errno, $errstr,
                $this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context);
        }
        else {
            $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
        }
        if (!$this->fp) {
            $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s",
                $host, $this->prefs['port'], $errstr ?: "Unknown reason"));
            return false;
        }
        if ($this->prefs['timeout'] > 0) {
            stream_set_timeout($this->fp, $this->prefs['timeout']);
        }
        $line = trim(fgets($this->fp, 8192));
        if ($this->_debug) {
            // set connection identifier for debug output
            preg_match('/#([0-9]+)/', (string) $this->fp, $m);
            $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
            if ($line) {
                $this->debug('S: '. $line);
            }
        }
        // Connected to wrong port or connection error?
        if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
            if ($line)
                $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
            else
                $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
            $this->setError(self::ERROR_BAD, $error);
            $this->closeConnection();
            return false;
        }
        // RFC3501 [7.1] optional CAPABILITY response
        if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
            $this->parseCapability($matches[1], true);
        }
        // TLS connection
        if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
            $res = $this->execute('STARTTLS');
            if ($res[0] != self::ERROR_OK) {
                $this->closeConnection();
                return false;
            }
            if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
                $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
                $this->closeConnection();
                return false;
            }
            // Now we're secure, capabilities need to be reread
            $this->clearCapability();
        }
        return true;
    }
    /**
     * Initializes environment
     */
    protected function set_prefs($prefs)
    {
        // set preferences
        if (is_array($prefs)) {
            $this->prefs = $prefs;
        }
        // set auth method
        if (!empty($this->prefs['auth_type'])) {
            $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']);
        }
        else {
            $this->prefs['auth_type'] = 'CHECK';
        }
        // disabled capabilities
        if (!empty($this->prefs['disabled_caps'])) {
            $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
        }
        // additional message flags
        if (!empty($this->prefs['message_flags'])) {
            $this->flags = array_merge($this->flags, $this->prefs['message_flags']);
            unset($this->prefs['message_flags']);
        }
    }
    /**
@@ -1158,13 +1216,20 @@
     * Folder creation (CREATE)
     *
     * @param string $mailbox Mailbox name
     * @param array  $types    Optional folder types (RFC 6154)
     *
     * @return bool True on success, False on error
     */
    function createFolder($mailbox)
    function createFolder($mailbox, $types = null)
    {
        $result = $this->execute('CREATE', array($this->escape($mailbox)),
            self::COMMAND_NORESPONSE);
        $args = array($this->escape($mailbox));
        // RFC 6154: CREATE-SPECIAL-USE
        if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) {
            $args[] = '(USE (' . implode(' ', $types) . '))';
        }
        $result = $this->execute('CREATE', $args, self::COMMAND_NORESPONSE);
        return ($result == self::ERROR_OK);
    }
@@ -1228,15 +1293,15 @@
     *
     * @param string $ref         Reference name
     * @param string $mailbox     Mailbox name
     * @param array  $status_opts (see self::_listMailboxes)
     * @param array  $return_opts (see self::_listMailboxes)
     * @param array  $select_opts (see self::_listMailboxes)
     *
     * @return array List of mailboxes or hash of options if $status_opts argument
     *               is non-empty.
     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
     *                    is requested, False on error.
     */
    function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
    function listMailboxes($ref, $mailbox, $return_opts=array(), $select_opts=array())
    {
        return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts);
        return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts);
    }
    /**
@@ -1244,14 +1309,14 @@
     *
     * @param string $ref         Reference name
     * @param string $mailbox     Mailbox name
     * @param array  $status_opts (see self::_listMailboxes)
     * @param array  $return_opts (see self::_listMailboxes)
     *
     * @return array List of mailboxes or hash of options if $status_opts argument
     *               is non-empty.
     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
     *                    is requested, False on error.
     */
    function listSubscribed($ref, $mailbox, $status_opts=array())
    function listSubscribed($ref, $mailbox, $return_opts=array())
    {
        return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL);
        return $this->_listMailboxes($ref, $mailbox, true, $return_opts, NULL);
    }
    /**
@@ -1260,22 +1325,25 @@
     * @param string $ref         Reference name
     * @param string $mailbox     Mailbox name
     * @param bool   $subscribed  Enables returning subscribed mailboxes only
     * @param array  $status_opts List of STATUS options (RFC5819: LIST-STATUS)
     *                            Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
     * @param array  $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED)
     *                            Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN,
     *                                      MYRIGHTS, SUBSCRIBED, CHILDREN
     * @param array  $select_opts List of selection options (RFC5258: LIST-EXTENDED)
     *                            Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
     *                            Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
     *                                      SPECIAL-USE (RFC6154)
     *
     * @return array List of mailboxes or hash of options if $status_ops argument
     *               is non-empty.
     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
     *                    is requested, False on error.
     */
    private function _listMailboxes($ref, $mailbox, $subscribed=false,
        $status_opts=array(), $select_opts=array())
    protected function _listMailboxes($ref, $mailbox, $subscribed=false,
        $return_opts=array(), $select_opts=array())
    {
        if (!strlen($mailbox)) {
            $mailbox = '*';
        }
        $args = array();
        $rets = array();
        if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
            $select_opts = (array) $select_opts;
@@ -1286,11 +1354,29 @@
        $args[] = $this->escape($ref);
        $args[] = $this->escape($mailbox);
        if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
            $status_opts = (array) $status_opts;
            $lstatus = true;
        if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) {
            $ext_opts    = array('SUBSCRIBED', 'CHILDREN');
            $rets        = array_intersect($return_opts, $ext_opts);
            $return_opts = array_diff($return_opts, $rets);
        }
            $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
        if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) {
            $lstatus     = true;
            $status_opts = array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN');
            $opts        = array_diff($return_opts, $status_opts);
            $status_opts = array_diff($return_opts, $opts);
            if (!empty($status_opts)) {
                $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')';
            }
            if (!empty($opts)) {
                $rets = array_merge($rets, $opts);
            }
        }
        if (!empty($rets)) {
            $args[] = 'RETURN (' . implode(' ', $rets) . ')';
        }
        list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
@@ -1310,9 +1396,10 @@
                $line = substr($response, $last, $pos - $last);
                $last = $pos + 2;
                if (!preg_match('/^\* (LIST|LSUB|STATUS) /i', $line, $m)) {
                if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) {
                    continue;
                }
                $cmd  = strtoupper($m[1]);
                $line = substr($line, strlen($m[0]));
@@ -1333,9 +1420,8 @@
                        $folders[$mailbox] = array();
                    }
                    // store LSUB options only if not empty, this way
                    // we can detect a situation when LIST doesn't return specified folder
                    if (!empty($opts) || $cmd == 'LIST') {
                    // store folder options
                    if ($cmd == 'LIST') {
                        // Add to options array
                        if (empty($this->data['LIST'][$mailbox]))
                            $this->data['LIST'][$mailbox] = $opts;
@@ -1344,13 +1430,20 @@
                                $this->data['LIST'][$mailbox], $opts));
                    }
                }
                // * STATUS <mailbox> (<result>)
                else if ($cmd == 'STATUS') {
                    list($mailbox, $status) = $this->tokenizeResponse($line, 2);
                else if ($lstatus) {
                    // * STATUS <mailbox> (<result>)
                    if ($cmd == 'STATUS') {
                        list($mailbox, $status) = $this->tokenizeResponse($line, 2);
                    for ($i=0, $len=count($status); $i<$len; $i += 2) {
                        list($name, $value) = $this->tokenizeResponse($status, 2);
                        $folders[$mailbox][$name] = $value;
                        for ($i=0, $len=count($status); $i<$len; $i += 2) {
                            list($name, $value) = $this->tokenizeResponse($status, 2);
                            $folders[$mailbox][$name] = $value;
                        }
                    }
                    // * MYRIGHTS <mailbox> <acl>
                    else if ($cmd == 'MYRIGHTS') {
                        list($mailbox, $acl)  = $this->tokenizeResponse($line, 2);
                        $folders[$mailbox]['MYRIGHTS'] = $acl;
                    }
                }
            }
@@ -1537,23 +1630,23 @@
     *
     * @param string $mailbox    Mailbox name
     * @param string $field      Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
     * @param string $add        Searching criteria
     * @param string $criteria   Searching criteria
     * @param bool   $return_uid Enables UID SORT usage
     * @param string $encoding   Character set
     *
     * @return rcube_result_index Response data
     */
    function sort($mailbox, $field, $add='', $return_uid=false, $encoding = 'US-ASCII')
    function sort($mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
    {
        $field = strtoupper($field);
        $old_sel   = $this->selected;
        $supported = array('ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO');
        $field     = strtoupper($field);
        if ($field == 'INTERNALDATE') {
            $field = 'ARRIVAL';
        }
        $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
            'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
        if (!$fields[$field]) {
        if (!in_array($field, $supported)) {
            return new rcube_result_index($mailbox);
        }
@@ -1561,18 +1654,21 @@
            return new rcube_result_index($mailbox);
        }
        // return empty result when folder is empty and we're just after SELECT
        if ($old_sel != $mailbox && !$this->data['EXISTS']) {
            return new rcube_result_index($mailbox, '* SORT');
        }
        // RFC 5957: SORT=DISPLAY
        if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) {
            $field = 'DISPLAY' . $field;
        }
        // message IDs
        if (!empty($add)) {
            $add = $this->compressMessageSet($add);
        }
        $encoding = $encoding ? trim($encoding) : 'US-ASCII';
        $criteria = $criteria ? 'ALL ' . trim($criteria) : 'ALL';
        list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
            array("($field)", $encoding, !empty($add) ? $add : 'ALL'));
            array("($field)", $encoding, $criteria));
        if ($code != self::ERROR_OK) {
            $response = null;
@@ -1602,7 +1698,7 @@
        // return empty result when folder is empty and we're just after SELECT
        if ($old_sel != $mailbox && !$this->data['EXISTS']) {
            return new rcube_result_thread($mailbox);
            return new rcube_result_thread($mailbox, '* THREAD');
        }
        $encoding  = $encoding ? trim($encoding) : 'US-ASCII';
@@ -1822,8 +1918,8 @@
                        $result[$id] = '';
                    }
                } else if ($mode == 2) {
                    if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
                        $result[$id] = trim($matches[2]);
                    if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) {
                        $result[$id] = trim($matches[1]);
                    } else {
                        $result[$id] = 0;
                    }
@@ -1930,12 +2026,8 @@
     *
     * @return bool True on success, False on failure
     */
    private function modFlag($mailbox, $messages, $flag, $mod = '+')
    protected function modFlag($mailbox, $messages, $flag, $mod = '+')
    {
        if ($mod != '+' && $mod != '-') {
            $mod = '+';
        }
        if (!$this->select($mailbox)) {
            return false;
        }
@@ -1945,12 +2037,25 @@
            return false;
        }
        if ($this->flags[strtoupper($flag)]) {
            $flag = $this->flags[strtoupper($flag)];
        }
        if (!$flag || !in_array($flag, (array) $this->data['PERMANENTFLAGS'])
            || !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])
        ) {
            return false;
        }
        // Clear internal status cache
        if ($flag == 'SEEN') {
            unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
        }
        $flag   = $this->flags[strtoupper($flag)];
        if ($mod != '+' && $mod != '-') {
            $mod = '+';
        }
        $result = $this->execute('UID STORE', array(
            $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
            self::COMMAND_NORESPONSE);
@@ -2493,7 +2598,7 @@
        }
        if ($binary) {
            // WARNING: Use $formatting argument with care, this may break binary data stream
            // WARNING: Use $formatted argument with care, this may break binary data stream
            $mode = -1;
        }
@@ -2543,7 +2648,11 @@
                $prev  = '';
                $found = true;
                while ($bytes > 0) {
                // empty body
                if (!$bytes) {
                    $result = '';
                }
                else while ($bytes > 0) {
                    $line = $this->readLine(8192);
                    if ($line === NULL) {
@@ -2775,57 +2884,64 @@
    /**
     * Returns QUOTA information
     *
     * @param string $mailbox Mailbox name
     *
     * @return array Quota information
     */
    function getQuota()
    function getQuota($mailbox = null)
    {
        /*
         * GETQUOTAROOT "INBOX"
         * QUOTAROOT INBOX user/rchijiiwa1
         * QUOTA user/rchijiiwa1 (STORAGE 654 9765)
         * OK Completed
         */
        $result      = false;
        $quota_lines = array();
        $key         = $this->nextTag();
        $command     = $key . ' GETQUOTAROOT INBOX';
        // get line(s) containing quota info
        if ($this->putLine($command)) {
            do {
                $line = rtrim($this->readLine(5000));
                if (preg_match('/^\* QUOTA /', $line)) {
                    $quota_lines[] = $line;
                }
            } while (!$this->startsWith($line, $key, true, true));
        }
        else {
            $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
        if ($mailbox === null || $mailbox === '') {
            $mailbox = 'INBOX';
        }
        // return false if not found, parse if found
        // a0001 GETQUOTAROOT INBOX
        // * QUOTAROOT INBOX user/sample
        // * QUOTA user/sample (STORAGE 654 9765)
        // a0001 OK Completed
        list($code, $response) = $this->execute('GETQUOTAROOT', array($this->escape($mailbox)));
        $result   = false;
        $min_free = PHP_INT_MAX;
        foreach ($quota_lines as $key => $quota_line) {
            $quota_line   = str_replace(array('(', ')'), '', $quota_line);
            $parts        = explode(' ', $quota_line);
            $storage_part = array_search('STORAGE', $parts);
        $all      = array();
            if (!$storage_part) {
                continue;
        if ($code == self::ERROR_OK) {
            foreach (explode("\n", $response) as $line) {
                if (preg_match('/^\* QUOTA /', $line)) {
                    list(, , $quota_root) = $this->tokenizeResponse($line, 3);
                    while ($line) {
                        list($type, $used, $total) = $this->tokenizeResponse($line, 1);
                        $type = strtolower($type);
                        if ($type && $total) {
                            $all[$quota_root][$type]['used']  = intval($used);
                            $all[$quota_root][$type]['total'] = intval($total);
                        }
                    }
                    if (empty($all[$quota_root]['storage'])) {
                        continue;
                    }
                    $used  = $all[$quota_root]['storage']['used'];
                    $total = $all[$quota_root]['storage']['total'];
                    $free  = $total - $used;
                    // calculate lowest available space from all storage quotas
                    if ($free < $min_free) {
                        $min_free          = $free;
                        $result['used']    = $used;
                        $result['total']   = $total;
                        $result['percent'] = min(100, round(($used/max(1,$total))*100));
                        $result['free']    = 100 - $result['percent'];
                    }
                }
            }
        }
            $used  = intval($parts[$storage_part+1]);
            $total = intval($parts[$storage_part+2]);
            $free  = $total - $used;
            // return lowest available space from all quotas
            if ($free < $min_free) {
                $min_free          = $free;
                $result['used']    = $used;
                $result['total']   = $total;
                $result['percent'] = min(100, round(($used/max(1,$total))*100));
                $result['free']    = 100 - $result['percent'];
            }
        if (!empty($result)) {
            $result['all'] = $all;
        }
        return $result;
@@ -2987,7 +3103,7 @@
        }
        foreach ($entries as $name => $value) {
            $entries[$name] = $this->escape($name) . ' ' . $this->escape($value);
            $entries[$name] = $this->escape($name) . ' ' . $this->escape($value, true);
        }
        $entries = implode(' ', $entries);
@@ -3084,7 +3200,8 @@
                    if (isset($mbox) && is_array($data[$i])) {
                        $size_sub = count($data[$i]);
                        for ($x=0; $x<$size_sub; $x++) {
                            $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
                            if ($data[$i][$x+1] !== null)
                                $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
                        }
                        unset($data[$i]);
                    }
@@ -3102,7 +3219,8 @@
                        }
                    }
                    else if (isset($mbox)) {
                        $result[$mbox][$data[$i]] = $data[++$i];
                        if ($data[$i+1] !== null)
                            $result[$mbox][$data[$i]] = $data[++$i];
                        unset($data[$i]);
                        unset($data[$i-1]);
                    }
@@ -3136,6 +3254,11 @@
        }
        foreach ($data as $entry) {
            // Workaround cyrus-murder bug, the entry[2] string needs to be escaped
            if (self::$mupdate) {
                $entry[2] = addcslashes($entry[2], '\\"');
            }
            // ANNOTATEMORE drafts before version 08 require quoted parameters
            $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true),
                $this->escape($entry[1], true), $this->escape($entry[2], true));
@@ -3242,10 +3365,10 @@
                        for ($x=0, $len=count($attribs); $x<$len;) {
                            $attr  = $attribs[$x++];
                            $value = $attribs[$x++];
                            if ($attr == 'value.priv') {
                            if ($attr == 'value.priv' && $value !== null) {
                                $result[$mbox]['/private' . $entry] = $value;
                            }
                            else if ($attr == 'value.shared') {
                            else if ($attr == 'value.shared' && $value !== null) {
                                $result[$mbox]['/shared' . $entry] = $value;
                            }
                        }
@@ -3392,7 +3515,7 @@
        }
        // Send command
        if (!$this->putLineC($query)) {
        if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) {
            $this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
            return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
        }
@@ -3615,7 +3738,7 @@
        return $result;
    }
    private function _xor($string, $string2)
    protected function _xor($string, $string2)
    {
        $result = '';
        $size   = strlen($string);
@@ -3634,7 +3757,7 @@
     *
     * @return string Space-separated list of flags
     */
    private function flagsToStr($flags)
    protected function flagsToStr($flags)
    {
        foreach ((array)$flags as $idx => $flag) {
            if ($flag = $this->flags[strtoupper($flag)]) {
@@ -3686,7 +3809,7 @@
    /**
     * CAPABILITY response parser
     */
    private function parseCapability($str, $trusted=false)
    protected function parseCapability($str, $trusted=false)
    {
        $str = preg_replace('/^\* CAPABILITY /i', '', $str);
@@ -3698,6 +3821,10 @@
        if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
            $this->prefs['literal+'] = true;
        }
        if (preg_match('/(\[| )MUPDATE=.*/', $str)) {
            self::$mupdate = true;
        }
        if ($trusted) {
@@ -3759,7 +3886,7 @@
     *
     * @since 0.5-stable
     */
    private function debug($message)
    protected function debug($message)
    {
        if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
            $diff    = $len - self::DEBUG_LINE_LENGTH;