Thomas
2013-10-21 4af76d20cafcd456bf3ce0fcb17b25a888c45160
program/lib/Roundcube/rcube_imap_generic.php
@@ -2,8 +2,6 @@
/**
 +-----------------------------------------------------------------------+
 | program/include/rcube_imap_generic.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                             |
@@ -19,13 +17,11 @@
 |   functionality built-in.                                             |
 |                                                                       |
 |   Based on Iloha IMAP Library. See http://ilohamail.org/ for details  |
 |                                                                       |
 +-----------------------------------------------------------------------+
 | Author: Aleksander Machniak <alec@alec.pl>                            |
 | Author: Ryo Chijiiwa <Ryo@IlohaMail.org>                              |
 +-----------------------------------------------------------------------+
*/
/**
 * PHP based wrapper class to connect to an IMAP server
@@ -706,18 +702,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    = '';
@@ -750,19 +739,23 @@
        }
        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
        $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));
            return false;
        }
        if ($this->prefs['timeout'] > 0)
        if ($this->prefs['timeout'] > 0) {
            stream_set_timeout($this->fp, $this->prefs['timeout']);
        }
        $line = trim(fgets($this->fp, 8192));
@@ -892,6 +885,31 @@
    }
    /**
     * 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';
        }
        // 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 +924,7 @@
     */
    function closeConnection()
    {
        if ($this->putLine($this->nextTag() . ' LOGOUT')) {
        if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) {
            $this->readReply();
        }
@@ -1077,7 +1095,7 @@
        }
        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;
        }
@@ -1310,6 +1328,11 @@
                // * LIST (<options>) <delimiter> <mailbox>
                if ($cmd == 'LIST' || $cmd == 'LSUB') {
                    list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3);
                    // Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879)
                    if ($delim) {
                        $mailbox = rtrim($mailbox, $delim);
                    }
                    // Add to result array
                    if (!$lstatus) {
@@ -1928,7 +1951,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;
        }
@@ -1990,7 +2013,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;
        }
@@ -2125,14 +2148,18 @@
                    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;
                    }
                }
@@ -2206,10 +2233,13 @@
                            }
                            break;
                        default:
                            if (strlen($field) > 2) {
                                $result[$id]->others[$field] = $string;
                            if (strlen($field) < 3) {
                                break;
                            }
                            break;
                            if ($result[$id]->others[$field]) {
                                $string = array_merge((array)$result[$id]->others[$field], (array)$string);
                            }
                            $result[$id]->others[$field] = $string;
                        }
                    }
                }
@@ -2217,7 +2247,6 @@
            // VANISHED response (QRESYNC RFC5162)
            // Sample: * VANISHED (EARLIER) 300:310,405,411
            else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
                $line   = substr($line, strlen($match[0]));
                $v_data = $this->tokenizeResponse($line, 1);
@@ -2230,24 +2259,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);
        }
@@ -2379,7 +2437,7 @@
        return $this->handlePartBody($mailbox, $id, $is_uid, $part);
    }
    function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL, $formatted=false)
    function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=NULL, $print=NULL, $file=NULL, $formatted=false, $max_bytes=0)
    {
        if (!$this->select($mailbox)) {
            return false;
@@ -2405,10 +2463,13 @@
        // Use BINARY extension when possible (and safe)
        $binary     = $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
        $fetch_mode = $binary ? 'BINARY' : 'BODY';
        $partial    = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
        // format request
        $key       = $this->nextTag();
        $request   = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part])";
        $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)) {
@@ -2421,116 +2482,129 @@
            $mode = -1;
        }
        // receive reply line
        do {
            $line = rtrim($this->readLine(1024));
            $a    = explode(' ', $line);
        } while (!($end = $this->startsWith($line, $key, true)) && $a[2] != 'FETCH');
            $line = trim($this->readLine(1024));
        $len    = strlen($line);
        $result = false;
        if ($a[2] != 'FETCH') {
        }
        // handle empty "* X FETCH ()" response
        else if ($line[$len-1] == ')' && $line[$len-2] != '(') {
            // one line response, get everything between first and last quotes
            if (substr($line, -4, 3) == 'NIL') {
                // NIL response
                $result = '';
            } else {
                $from = strpos($line, '"') + 1;
                $to   = strrpos($line, '"');
                $len  = $to - $from;
                $result = substr($line, $from, $len);
            if (!$line) {
                break;
            }
            if ($mode == 1) {
                $result = base64_decode($result);
            }
            else if ($mode == 2) {
                $result = quoted_printable_decode($result);
            }
            else if ($mode == 3) {
                $result = convert_uudecode($result);
            // skip irrelevant untagged responses (we have a result already)
            if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) {
                continue;
            }
        } else if ($line[$len-1] == '}') {
            // multi-line request, find sizes of content and receive that many bytes
            $from     = strpos($line, '{') + 1;
            $to       = strrpos($line, '}');
            $len      = $to - $from;
            $sizeStr  = substr($line, $from, $len);
            $bytes    = (int)$sizeStr;
            $prev     = '';
            $line = $m[2];
            while ($bytes > 0) {
                $line = $this->readLine(8192);
            // handle one line response
            if ($line[0] == '(' && substr($line, -1) == ')') {
                // tokenize content inside brackets
                $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line));
                if ($line === NULL) {
                    break;
                }
                $len = strlen($line);
                if ($len > $bytes) {
                    $line = substr($line, 0, $bytes);
                    $len = strlen($line);
                }
                $bytes -= $len;
                // BASE64
                if ($mode == 1) {
                    $line = rtrim($line, "\t\r\n\0\x0B");
                    // create chunks with proper length for base64 decoding
                    $line = $prev.$line;
                    $length = strlen($line);
                    if ($length % 4) {
                        $length = floor($length / 4) * 4;
                        $prev = substr($line, $length);
                        $line = substr($line, 0, $length);
                for ($i=0; $i<count($tokens); $i+=2) {
                    if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) {
                        $result = $tokens[$i+1];
                        $found  = true;
                        break;
                    }
                    else
                        $prev = '';
                    $line = base64_decode($line);
                // QUOTED-PRINTABLE
                } else if ($mode == 2) {
                    $line = rtrim($line, "\t\r\0\x0B");
                    $line = quoted_printable_decode($line);
                // UUENCODE
                } else if ($mode == 3) {
                    $line = rtrim($line, "\t\r\n\0\x0B");
                    if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line))
                        continue;
                    $line = convert_uudecode($line);
                // default
                } else if ($formatted) {
                    $line = rtrim($line, "\t\r\n\0\x0B") . "\n";
                }
                if ($file)
                    fwrite($file, $line);
                else if ($print)
                    echo $line;
                else
                    $result .= $line;
                if ($result !== false) {
                    if ($mode == 1) {
                        $result = base64_decode($result);
                    }
                    else if ($mode == 2) {
                        $result = quoted_printable_decode($result);
                    }
                    else if ($mode == 3) {
                        $result = convert_uudecode($result);
                    }
                }
            }
        }
            // response with string literal
            else if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
                $bytes = (int) $m[1];
                $prev  = '';
                $found = true;
        // read in anything up until last line
        if (!$end)
            do {
                $line = $this->readLine(1024);
            } while (!$this->startsWith($line, $key, true));
                // empty body
                if (!$bytes) {
                    $result = '';
                }
                else while ($bytes > 0) {
                    $line = $this->readLine(8192);
                    if ($line === NULL) {
                        break;
                    }
                    $len = strlen($line);
                    if ($len > $bytes) {
                        $line = substr($line, 0, $bytes);
                        $len  = strlen($line);
                    }
                    $bytes -= $len;
                    // BASE64
                    if ($mode == 1) {
                        $line = rtrim($line, "\t\r\n\0\x0B");
                        // create chunks with proper length for base64 decoding
                        $line = $prev.$line;
                        $length = strlen($line);
                        if ($length % 4) {
                            $length = floor($length / 4) * 4;
                            $prev = substr($line, $length);
                            $line = substr($line, 0, $length);
                        }
                        else {
                            $prev = '';
                        }
                        $line = base64_decode($line);
                    }
                    // QUOTED-PRINTABLE
                    else if ($mode == 2) {
                        $line = rtrim($line, "\t\r\0\x0B");
                        $line = quoted_printable_decode($line);
                    }
                    // UUENCODE
                    else if ($mode == 3) {
                        $line = rtrim($line, "\t\r\n\0\x0B");
                        if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) {
                            continue;
                        }
                        $line = convert_uudecode($line);
                    }
                    // default
                    else if ($formatted) {
                        $line = rtrim($line, "\t\r\n\0\x0B") . "\n";
                    }
                    if ($file) {
                        if (fwrite($file, $line) === false) {
                            break;
                        }
                    }
                    else if ($print) {
                        echo $line;
                    }
                    else {
                        $result .= $line;
                    }
                }
            }
        } while (!$this->startsWith($line, $key, true));
        if ($result !== false) {
            if ($file) {
                fwrite($file, $result);
            } else if ($print) {
                return fwrite($file, $result);
            }
            else if ($print) {
                echo $result;
            } else
                return $result;
            return true;
                return true;
            }
            return $result;
        }
        return false;
@@ -2543,10 +2617,11 @@
     * @param string $message Message content
     * @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
     */
    function append($mailbox, &$message, $flags = array(), $date = null)
    function append($mailbox, &$message, $flags = array(), $date = null, $binary = false)
    {
        unset($this->data['APPENDUID']);
@@ -2554,8 +2629,13 @@
            return false;
        }
        $message = str_replace("\r", '', $message);
        $message = str_replace("\n", "\r\n", $message);
        $binary       = $binary && $this->getCapability('BINARY');
        $literal_plus = !$binary && $this->prefs['literal+'];
        if (!$binary) {
            $message = str_replace("\r", '', $message);
            $message = str_replace("\n", "\r\n", $message);
        }
        $len = strlen($message);
        if (!$len) {
@@ -2568,12 +2648,12 @@
        if (!empty($date)) {
            $request .= ' ' . $this->escape($date);
        }
        $request .= ' {' . $len . ($this->prefs['literal+'] ? '+' : '') . '}';
        $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}';
        // send APPEND command
        if ($this->putLine($request)) {
            // Do not wait when LITERAL+ is supported
            if (!$this->prefs['literal+']) {
            if (!$literal_plus) {
                $line = $this->readReply();
                if ($line[0] != '+') {
@@ -2615,10 +2695,11 @@
     * @param string $headers Message headers
     * @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
     */
    function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null)
    function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false)
    {
        unset($this->data['APPENDUID']);
@@ -2649,18 +2730,21 @@
            $len += strlen($headers) + strlen($body_separator);
        }
        $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 .= ' {' . $len . ($this->prefs['literal+'] ? '+' : '') . '}';
        $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}';
        // send APPEND command
        if ($this->putLine($request)) {
            // Don't wait when LITERAL+ is supported
            if (!$this->prefs['literal+']) {
            if (!$literal_plus) {
                $line = $this->readReply();
                if ($line[0] != '+') {
@@ -2922,7 +3006,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);
@@ -3419,25 +3503,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]));
                }