Aleksander Machniak
2014-04-15 a2e09c950d8716346b151f7980df1b51ebfbdd78
program/lib/Roundcube/rcube_imap_generic.php
@@ -48,6 +48,8 @@
        '*'        => '\\*',
    );
    public static $mupdate;
    private $fp;
    private $host;
    private $logged = false;
@@ -71,6 +73,9 @@
    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
    /**
     * Object constructor
@@ -84,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" : ''));
@@ -112,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;
@@ -134,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;
@@ -149,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;
@@ -515,7 +533,7 @@
                $reply = base64_encode($user . ' ' . $hash);
                // send result
                $this->putLine($reply);
                $this->putLine($reply, true, true);
            }
            else {
                // RFC2831: DIGEST-MD5
@@ -533,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] == '+') {
@@ -573,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");
@@ -584,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);
            }
@@ -615,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)) {
@@ -702,18 +720,11 @@
     */
    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);
        $result = false;
        $auth_method = $this->prefs['auth_type'];
        $result      = false;
        // initialize connection
        $this->error    = '';
@@ -746,7 +757,7 @@
        }
        if ($this->prefs['timeout'] <= 0) {
            $this->prefs['timeout'] = ini_get('default_socket_timeout');
            $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
        }
        // Connect
@@ -794,23 +805,21 @@
        // TLS connection
        if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
            if (version_compare(PHP_VERSION, '5.1.0', '>=')) {
                $res = $this->execute('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();
            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
@@ -892,6 +901,36 @@
    }
    /**
     * 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']);
        }
    }
    /**
     * Checks connection status
     *
     * @return bool True if connection is active and user is logged in, False otherwise.
@@ -906,7 +945,7 @@
     */
    function closeConnection()
    {
        if ($this->putLine($this->nextTag() . ' LOGOUT')) {
        if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) {
            $this->readReply();
        }
@@ -1065,8 +1104,8 @@
    /**
     * Executes EXPUNGE command
     *
     * @param string $mailbox  Mailbox name
     * @param string $messages Message UIDs to expunge
     * @param string       $mailbox  Mailbox name
     * @param string|array $messages Message UIDs to expunge
     *
     * @return boolean True on success, False on error
     */
@@ -1077,17 +1116,20 @@
        }
        if (!$this->data['READ-WRITE']) {
            $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE');
            $this->setError(self::ERROR_READONLY, "Mailbox is read-only");
            return false;
        }
        // Clear internal status cache
        unset($this->data['STATUS:'.$mailbox]);
        if ($messages)
            $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
        else
        if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) {
            $messages = self::compressMessageSet($messages);
            $result   = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
        }
        else {
            $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
        }
        if ($result == self::ERROR_OK) {
            $this->selected = null; // state has changed, need to reselect
@@ -1149,13 +1191,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);
    }
@@ -1251,10 +1300,12 @@
     * @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  $status_opts List of STATUS options
     *                            (RFC5819: LIST-STATUS:  MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN)
     *                            or RETURN options (RFC5258: LIST_EXTENDED: 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.
@@ -1267,6 +1318,7 @@
        }
        $args = array();
        $rets = array();
        if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
            $select_opts = (array) $select_opts;
@@ -1277,11 +1329,21 @@
        $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($status_opts) && $this->getCapability('LIST-EXTENDED')) {
            $rets = array_intersect($status_opts, array('SUBSCRIBED', 'CHILDREN'));
        }
            $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
        if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
            $status_opts = array_intersect($status_opts, array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN'));
            if (!empty($status_opts)) {
                $lstatus = true;
                $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')';
            }
        }
        if (!empty($rets)) {
            $args[] = 'RETURN (' . implode(' ', $rets) . ')';
        }
        list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
@@ -1324,9 +1386,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;
@@ -1528,23 +1589,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);
        }
@@ -1552,17 +1613,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, 'ALL' . (!empty($add) ? ' '.$add : '')));
            array("($field)", $encoding, $criteria));
        if ($code != self::ERROR_OK) {
            $response = null;
@@ -1592,7 +1657,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';
@@ -1649,7 +1714,6 @@
        }
        if (!empty($criteria)) {
            $modseq = stripos($criteria, 'MODSEQ') !== false;
            $params .= ($params ? ' ' : '') . $criteria;
        }
        else {
@@ -1788,7 +1852,6 @@
                if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
                    $flags = explode(' ', strtoupper($matches[1]));
                    if (in_array('\\DELETED', $flags)) {
                        $deleted[$id] = $id;
                        continue;
                    }
                }
@@ -1933,7 +1996,7 @@
        }
        if (!$this->data['READ-WRITE']) {
            $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
            $this->setError(self::ERROR_READONLY, "Mailbox is read-only");
            return false;
        }
@@ -1980,7 +2043,6 @@
    /**
     * Moves message(s) from one folder to another.
     * Original message(s) will be marked as deleted.
     *
     * @param string|array  $messages  Message UID(s)
     * @param string        $from      Mailbox name
@@ -1995,19 +2057,45 @@
        }
        if (!$this->data['READ-WRITE']) {
            $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
            $this->setError(self::ERROR_READONLY, "Mailbox is read-only");
            return false;
        }
        $r = $this->copy($messages, $from, $to);
        // use MOVE command (RFC 6851)
        if ($this->hasCapability('MOVE')) {
            // Clear last COPYUID data
            unset($this->data['COPYUID']);
        if ($r) {
            // Clear internal status cache
            unset($this->data['STATUS:'.$to]);
            unset($this->data['STATUS:'.$from]);
            $result = $this->execute('UID MOVE', array(
                $this->compressMessageSet($messages), $this->escape($to)),
                self::COMMAND_NORESPONSE);
            return ($result == self::ERROR_OK);
        }
        // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE
        $result = $this->copy($messages, $from, $to);
        if ($result) {
            // Clear internal status cache
            unset($this->data['STATUS:'.$from]);
            return $this->flag($from, $messages, 'DELETED');
            $result = $this->flag($from, $messages, 'DELETED');
            if ($messages == '*') {
                // CLOSE+SELECT should be faster than EXPUNGE
                $this->close();
            }
            else {
                $this->expunge($from, $messages);
            }
        }
        return $r;
        return $result;
    }
    /**
@@ -2130,21 +2218,25 @@
                    else if ($name == 'RFC822') {
                        $result[$id]->body = $value;
                    }
                    else if ($name == 'BODY') {
                        $body = $this->tokenizeResponse($line, 1);
                        if ($value[0] == 'HEADER.FIELDS')
                            $headers = $body;
                        else if (!empty($value))
                            $result[$id]->bodypart[$value[0]] = $body;
                    else if (stripos($name, 'BODY[') === 0) {
                        $name = str_replace(']', '', substr($name, 5));
                        if ($name == 'HEADER.FIELDS') {
                            // skip ']' after headers list
                            $this->tokenizeResponse($line, 1);
                            $headers = $this->tokenizeResponse($line, 1);
                        }
                        else if (strlen($name))
                            $result[$id]->bodypart[$name] = $value;
                        else
                            $result[$id]->body = $body;
                            $result[$id]->body = $value;
                    }
                }
                // create array with header field:data
                if (!empty($headers)) {
                    $headers = explode("\n", trim($headers));
                    foreach ($headers as $hid => $resln) {
                    foreach ($headers as $resln) {
                        if (ord($resln[0]) <= 32) {
                            $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
                        } else {
@@ -2152,7 +2244,7 @@
                        }
                    }
                    while (list($lines_key, $str) = each($lines)) {
                    foreach ($lines as $str) {
                        list($field, $string) = explode(':', $str, 2);
                        $field  = strtolower($field);
@@ -2237,24 +2329,53 @@
        return $result;
    }
    function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add = '')
    /**
     * Returns message(s) data (flags, headers, etc.)
     *
     * @param string $mailbox     Mailbox name
     * @param mixed  $message_set Message(s) sequence identifier(s) or UID(s)
     * @param bool   $is_uid      True if $message_set contains UIDs
     * @param bool   $bodystr     Enable to add BODYSTRUCTURE data to the result
     * @param array  $add_headers List of additional headers
     *
     * @return bool|array List of rcube_message_header elements, False on error
     */
    function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = array())
    {
        $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE');
        if ($bodystr)
        $headers     = array('DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO',
            'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY');
        if (!empty($add_headers)) {
            $add_headers = array_map('strtoupper', $add_headers);
            $headers     = array_unique(array_merge($headers, $add_headers));
        }
        if ($bodystr) {
            $query_items[] = 'BODYSTRUCTURE';
        $query_items[] = 'BODY.PEEK[HEADER.FIELDS ('
            . 'DATE FROM TO SUBJECT CONTENT-TYPE CC REPLY-TO LIST-POST DISPOSITION-NOTIFICATION-TO X-PRIORITY'
            . ($add ? ' ' . trim($add) : '')
            . ')]';
        }
        $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . implode(' ', $headers) . ')]';
        $result = $this->fetch($mailbox, $message_set, $is_uid, $query_items);
        return $result;
    }
    function fetchHeader($mailbox, $id, $uidfetch=false, $bodystr=false, $add='')
    /**
     * Returns message data (flags, headers, etc.)
     *
     * @param string $mailbox     Mailbox name
     * @param int    $id          Message sequence identifier or UID
     * @param bool   $is_uid      True if $id is an UID
     * @param bool   $bodystr     Enable to add BODYSTRUCTURE data to the result
     * @param array  $add_headers List of additional headers
     *
     * @return bool|rcube_message_header Message data, False on error
     */
    function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = array())
    {
        $a = $this->fetchHeaders($mailbox, $id, $uidfetch, $bodystr, $add);
        $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers);
        if (is_array($a)) {
            return array_shift($a);
        }
@@ -2418,6 +2539,7 @@
        $key     = $this->nextTag();
        $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)";
        $result  = false;
        $found   = false;
        // send request
        if (!$this->putLine($request)) {
@@ -2426,7 +2548,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;
        }
@@ -2437,18 +2559,26 @@
                break;
            }
            if (!preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) {
            // skip irrelevant untagged responses (we have a result already)
            if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) {
                continue;
            }
            $line = $m[2];
            $last = substr($line, -1);
            // handle one line response
            if ($line[0] == '(' && $last == ')') {
            if ($line[0] == '(' && substr($line, -1) == ')') {
                // tokenize content inside brackets
                $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\$)/', '', $line));
                $result = count($tokens) == 1 ? $tokens[0] : false;
                // the content can be e.g.: (UID 9844 BODY[2.4] NIL)
                $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line));
                for ($i=0; $i<count($tokens); $i+=2) {
                    if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) {
                        $result = $tokens[$i+1];
                        $found  = true;
                        break;
                    }
                }
                if ($result !== false) {
                    if ($mode == 1) {
@@ -2466,8 +2596,13 @@
            else if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
                $bytes = (int) $m[1];
                $prev  = '';
                $found = true;
                while ($bytes > 0) {
                // empty body
                if (!$bytes) {
                    $result = '';
                }
                else while ($bytes > 0) {
                    $line = $this->readLine(8192);
                    if ($line === NULL) {
@@ -2549,11 +2684,11 @@
    /**
     * Handler for IMAP APPEND command
     *
     * @param string $mailbox Mailbox name
     * @param string $message Message content
     * @param array  $flags   Message flags
     * @param string $date    Message internal date
     * @param bool   $binary  Enable BINARY append (RFC3516)
     * @param string       $mailbox Mailbox name
     * @param string|array $message The message source string or array (of strings and file pointers)
     * @param array        $flags   Message flags
     * @param string       $date    Message internal date
     * @param bool         $binary  Enable BINARY append (RFC3516)
     *
     * @return string|bool On success APPENDUID response (if available) or True, False on failure
     */
@@ -2567,13 +2702,28 @@
        $binary       = $binary && $this->getCapability('BINARY');
        $literal_plus = !$binary && $this->prefs['literal+'];
        $len          = 0;
        $msg          = is_array($message) ? $message : array(&$message);
        $chunk_size   = 512000;
        if (!$binary) {
            $message = str_replace("\r", '', $message);
            $message = str_replace("\n", "\r\n", $message);
        for ($i=0, $cnt=count($msg); $i<$cnt; $i++) {
            if (is_resource($msg[$i])) {
                $stat = fstat($msg[$i]);
                if ($stat === false) {
                    return false;
                }
                $len += $stat['size'];
            }
            else {
                if (!$binary) {
                    $msg[$i] = str_replace("\r", '', $msg[$i]);
                    $msg[$i] = str_replace("\n", "\r\n", $msg[$i]);
                }
                $len += strlen($msg[$i]);
            }
        }
        $len = strlen($message);
        if (!$len) {
            return false;
        }
@@ -2598,7 +2748,32 @@
                }
            }
            if (!$this->putLine($message)) {
            foreach ($msg as $msg_part) {
                // file pointer
                if (is_resource($msg_part)) {
                    rewind($msg_part);
                    while (!feof($msg_part) && $this->fp) {
                        $buffer = fread($msg_part, $chunk_size);
                        $this->putLine($buffer, false);
                    }
                    fclose($msg_part);
                }
                // string
                else {
                    $size = strlen($msg_part);
                    // Break up the data by sending one chunk (up to 512k) at a time.
                    // This approach reduces our peak memory usage
                    for ($offset = 0; $offset < $size; $offset += $chunk_size) {
                        $chunk = substr($msg_part, $offset, $chunk_size);
                        if (!$this->putLine($chunk, false)) {
                            return false;
                        }
                    }
                }
            }
            if (!$this->putLine('')) { // \r\n
                return false;
            }
@@ -2637,94 +2812,23 @@
     */
    function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false)
    {
        unset($this->data['APPENDUID']);
        if ($mailbox === null || $mailbox === '') {
            return false;
        }
        // open message file
        $in_fp = false;
        if (file_exists(realpath($path))) {
            $in_fp = fopen($path, 'r');
            $fp = fopen($path, 'r');
        }
        if (!$in_fp) {
        if (!$fp) {
            $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
            return false;
        }
        $body_separator = "\r\n\r\n";
        $len = filesize($path);
        if (!$len) {
            return false;
        }
        $message = array();
        if ($headers) {
            $headers = preg_replace('/[\r\n]+$/', '', $headers);
            $len += strlen($headers) + strlen($body_separator);
            $message[] = trim($headers, "\r\n") . "\r\n\r\n";
        }
        $message[] = $fp;
        $binary       = $binary && $this->getCapability('BINARY');
        $literal_plus = !$binary && $this->prefs['literal+'];
        // build APPEND command
        $key = $this->nextTag();
        $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')';
        if (!empty($date)) {
            $request .= ' ' . $this->escape($date);
        }
        $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}';
        // send APPEND command
        if ($this->putLine($request)) {
            // Don't wait when LITERAL+ is supported
            if (!$literal_plus) {
                $line = $this->readReply();
                if ($line[0] != '+') {
                    $this->parseResult($line, 'APPEND: ');
                    return false;
                }
            }
            // send headers with body separator
            if ($headers) {
                $this->putLine($headers . $body_separator, false);
            }
            // send file
            while (!feof($in_fp) && $this->fp) {
                $buffer = fgets($in_fp, 4096);
                $this->putLine($buffer, false);
            }
            fclose($in_fp);
            if (!$this->putLine('')) { // \r\n
                return false;
            }
            // read response
            do {
                $line = $this->readLine();
            } while (!$this->startsWith($line, $key, true, true));
            // Clear internal status cache
            unset($this->data['STATUS:'.$mailbox]);
            if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK)
                return false;
            else if (!empty($this->data['APPENDUID']))
                return $this->data['APPENDUID'];
            else
                return true;
        }
        else {
            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
        }
        return false;
        return $this->append($mailbox, $message, $flags, $date, $binary);
    }
    /**
@@ -2942,7 +3046,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);
@@ -3091,6 +3195,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));
@@ -3347,7 +3456,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, '');
        }
@@ -3439,25 +3548,24 @@
            // Parenthesized list
            case '(':
            case '[':
                $str = substr($str, 1);
                $result[] = self::tokenizeResponse($str);
                break;
            case ')':
            case ']':
                $str = substr($str, 1);
                return $result;
                break;
            // String atom, number, NIL, *, %
            // String atom, number, astring, NIL, *, %
            default:
                // empty string
                if ($str === '' || $str === null) {
                    break 2;
                }
                // excluded chars: SP, CTL, ), [, ]
                if (preg_match('/^([^\x00-\x20\x29\x5B\x5D\x7F]+)/', $str, $m)) {
                // excluded chars: SP, CTL, ), DEL
                // we do not exclude [ and ] (#1489223)
                if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) {
                    $result[] = $m[1] == 'NIL' ? NULL : $m[1];
                    $str = substr($str, strlen($m[1]));
                }
@@ -3474,7 +3582,7 @@
        if (is_array($element)) {
            reset($element);
            while (list($key, $value) = each($element)) {
            foreach ($element as $value) {
                $string .= ' ' . self::r_implode($value);
            }
        }
@@ -3502,7 +3610,7 @@
            // if less than 255 bytes long, let's not bother
            if (!$force && strlen($messages)<255) {
                return $messages;
           }
            }
            // see if it's already been compressed
            if (strpos($messages, ':') !== false) {
@@ -3610,8 +3718,20 @@
     */
    static function strToTime($date)
    {
        // support non-standard "GMTXXXX" literal
        $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
        // Clean malformed data
        $date = preg_replace(
            array(
                '/GMT\s*([+-][0-9]+)/',                     // support non-standard "GMTXXXX" literal
                '/[^a-z0-9\x20\x09:+-]/i',                  // remove any invalid characters
                '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i',   // remove weekday names
            ),
            array(
                '\\1',
                '',
                '',
            ), $date);
        $date = trim($date);
        // if date parsing fails, we have a date in non-rfc format
        // remove token from the end and try again
@@ -3636,8 +3756,16 @@
        $this->capability = explode(' ', strtoupper($str));
        if (!empty($this->prefs['disabled_caps'])) {
            $this->capability = array_diff($this->capability, $this->prefs['disabled_caps']);
        }
        if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
            $this->prefs['literal+'] = true;
        }
        if (preg_match('/(\[| )MUPDATE=.*/', $str)) {
            self::$mupdate = true;
        }
        if ($trusted) {
@@ -3681,9 +3809,10 @@
    /**
     * Set the value of the debugging flag.
     *
     * @param   boolean $debug      New value for the debugging flag.
     * @param boolean  $debug   New value for the debugging flag.
     * @param callback $handler Logging handler function
     *
     * @since   0.5-stable
     * @since 0.5-stable
     */
    function setDebug($debug, $handler = null)
    {
@@ -3694,12 +3823,18 @@
    /**
     * Write the given debug text to the current debug output handler.
     *
     * @param   string  $message    Debug mesage text.
     * @param string $message Debug mesage text.
     *
     * @since   0.5-stable
     * @since 0.5-stable
     */
    private function debug($message)
    {
        if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
            $diff    = $len - self::DEBUG_LINE_LENGTH;
            $message = substr($message, 0, self::DEBUG_LINE_LENGTH)
                . "... [truncated $diff bytes]";
        }
        if ($this->resourceid) {
            $message = sprintf('[%s] %s', $this->resourceid, $message);
        }