thomascube
2010-12-17 db1a87cd6c506f2afbd1a37c64cb56ae11120b49
program/include/rcube_imap_generic.php
@@ -1,6 +1,6 @@
<?php
/*
/**
 +-----------------------------------------------------------------------+
 | program/include/rcube_imap_generic.php                                |
 |                                                                       |
@@ -85,7 +85,8 @@
{
    public $error;
    public $errornum;
   public $message;
    public $result;
    public $resultcode;
    public $data = array();
    public $flags = array(
        'SEEN'     => '\\Seen',
@@ -113,8 +114,9 @@
    const ERROR_NO = -1;
    const ERROR_BAD = -2;
    const ERROR_BYE = -3;
    const ERROR_COMMAND = -5;
    const ERROR_UNKNOWN = -4;
    const ERROR_COMMAND = -5;
    const ERROR_READONLY = -6;
    const COMMAND_NORESPONSE = 1;
    const COMMAND_CAPABILITY = 2;
@@ -280,7 +282,6 @@
       return $data;
    }
    // don't use it in loops, until you exactly know what you're doing
    function readReply(&$untagged=null)
    {
       do {
@@ -303,7 +304,7 @@
            $str = trim($matches[2]);
          if ($res == 'OK') {
             return $this->errornum = self::ERROR_OK;
                $this->errornum = self::ERROR_OK;
          } else if ($res == 'NO') {
                $this->errornum = self::ERROR_NO;
          } else if ($res == 'BAD') {
@@ -314,15 +315,29 @@
             $this->errornum = self::ERROR_BYE;
          }
            if ($str)
            if ($str) {
                $str = trim($str);
                // get response string and code (RFC5530)
                if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) {
                    $this->resultcode = strtoupper($m[1]);
                    $str = trim(substr($str, strlen($m[1]) + 2));
                }
                else {
                    $this->resultcode = null;
                }
                $this->result = $str;
                if ($this->errornum != self::ERROR_OK) {
                $this->error = $err_prefix ? $err_prefix.$str : $str;
                }
            }
           return $this->errornum;
       }
       return self::ERROR_UNKNOWN;
    }
    private function setError($code, $msg='')
    function setError($code, $msg='')
    {
        $this->errornum = $code;
        $this->error    = $msg;
@@ -405,7 +420,7 @@
            }
          $this->putLine($this->nextTag() . " AUTHENTICATE $type");
          $line = trim($this->readLine(1024));
            $line = trim($this->readReply());
          if ($line[0] == '+') {
             $challenge = substr($line, 2);
@@ -455,7 +470,7 @@
                // send result
                $this->putLine($reply);
                $line = $this->readLine(1024);
                $line = trim($this->readReply());
                if ($line[0] == '+') {
                 $challenge = substr($line, 2);
@@ -475,7 +490,7 @@
                $this->putLine('');
            }
            $line = $this->readLine(1024);
            $line = $this->readReply();
            $result = $this->parseResult($line);
        }
        else { // PLAIN
@@ -497,7 +512,7 @@
            }
            else {
              $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
              $line = trim($this->readLine(1024));
                $line = trim($this->readReply());
              if ($line[0] != '+') {
                 return $this->parseResult($line);
@@ -505,7 +520,7 @@
                // send result, get reply and process it
                $this->putLine($reply);
                $line = $this->readLine(1024);
                $line = $this->readReply();
                $result = $this->parseResult($line);
            }
        }
@@ -518,7 +533,7 @@
            return $this->fp;
        }
        else {
            $this->setError($result, "Unable to authenticate user ($type): $line");
            $this->setError($result, "AUTHENTICATE $type: $line");
        }
        return $result;
@@ -683,7 +698,7 @@
             $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
           $this->setError(self::ERROR_BAD, $error);
            $this->close();
            $this->closeConnection();
           return false;
       }
@@ -692,21 +707,19 @@
          $this->parseCapability($matches[1], true);
       }
       $this->message = $line;
       // 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->close();
                    $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->close();
                    $this->closeConnection();
                return false;
             }
@@ -738,7 +751,7 @@
            // Prevent from sending credentials in plain text when connection is not secure
          if ($auth_method == 'LOGIN' && $this->getCapability('LOGINDISABLED')) {
             $this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
                $this->close();
                $this->closeConnection();
             return false;
            }
            // replace AUTH with CRAM-MD5 for backward compat.
@@ -778,8 +791,7 @@
          return true;
        }
        // Close connection
        $this->close();
        $this->closeConnection();
        return false;
    }
@@ -789,7 +801,7 @@
      return ($this->fp && $this->logged) ? true : false;
    }
    function close()
    function closeConnection()
    {
       if ($this->putLine($this->nextTag() . ' LOGOUT')) {
           $this->readReply();
@@ -799,6 +811,14 @@
      $this->fp = false;
    }
    /**
     * Executes SELECT command (if mailbox is already not in selected state)
     *
     * @param string $mailbox Mailbox name
     *
     * @return boolean True on success, false on error
     * @access public
     */
    function select($mailbox)
    {
       if (!strlen($mailbox)) {
@@ -834,6 +854,8 @@
             }
            }
            $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
          $this->selected = $mailbox;
         return true;
      }
@@ -842,7 +864,7 @@
    }
    /**
     * Executes STATUS comand
     * Executes STATUS command
     *
     * @param string $mailbox Mailbox name
     * @param array  $items   Additional requested item names. By default
@@ -887,21 +909,142 @@
        return false;
    }
    function checkForRecent($mailbox)
    /**
     * Executes EXPUNGE command
     *
     * @param string $mailbox  Mailbox name
     * @param string $messages Message UIDs to expunge
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function expunge($mailbox, $messages=NULL)
    {
       if (!strlen($mailbox)) {
          $mailbox = 'INBOX';
        if (!$this->select($mailbox)) {
            return false;
       }
       $this->select($mailbox);
        if (!$this->data['READ-WRITE']) {
            $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'EXPUNGE');
            return false;
        }
       if ($this->selected == $mailbox) {
          return $this->data['RECENT'];
        // Clear internal status cache
        unset($this->data['STATUS:'.$mailbox]);
        if ($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 = ''; // state has changed, need to reselect
            return true;
       }
       return false;
    }
    /**
     * Executes CLOSE command
     *
     * @return boolean True on success, False on error
     * @access public
     * @since 0.5
     */
    function close()
    {
        $result = $this->execute('CLOSE', NULL, self::COMMAND_NORESPONSE);
        if ($result == self::ERROR_OK) {
            $this->selected = '';
            return true;
        }
        return false;
    }
    /**
     * Executes SUBSCRIBE command
     *
     * @param string $mailbox Mailbox name
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function subscribe($mailbox)
    {
        $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
            self::COMMAND_NORESPONSE);
        return ($result == self::ERROR_OK);
    }
    /**
     * Executes UNSUBSCRIBE command
     *
     * @param string $mailbox Mailbox name
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function unsubscribe($mailbox)
    {
        $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
            self::COMMAND_NORESPONSE);
        return ($result == self::ERROR_OK);
    }
    /**
     * Executes DELETE command
     *
     * @param string $mailbox Mailbox name
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function deleteFolder($mailbox)
    {
        $result = $this->execute('DELETE', array($this->escape($mailbox)),
            self::COMMAND_NORESPONSE);
        return ($result == self::ERROR_OK);
    }
    /**
     * Removes all messages in a folder
     *
     * @param string $mailbox Mailbox name
     *
     * @return boolean True on success, False on error
     * @access public
     */
    function clearFolder($mailbox)
    {
        $num_in_trash = $this->countMessages($mailbox);
        if ($num_in_trash > 0) {
            $res = $this->delete($mailbox, '1:*');
        }
        if ($res) {
            if ($this->selected == $mailbox)
                $res = $this->close();
            else
                $res = $this->expunge($mailbox);
        }
        return $res;
    }
    /**
     * Returns count of all messages in a folder
     *
     * @param string $mailbox Mailbox name
     *
     * @return int Number of messages, False on error
     * @access public
     */
    function countMessages($mailbox, $refresh = false)
    {
       if ($refresh) {
@@ -922,6 +1065,29 @@
        $counts = $this->status($mailbox);
        if (is_array($counts)) {
            return (int) $counts['MESSAGES'];
        }
        return false;
    }
    /**
     * Returns count of messages with \Recent flag in a folder
     *
     * @param string $mailbox Mailbox name
     *
     * @return int Number of messages, False on error
     * @access public
     */
    function countRecent($mailbox)
    {
        if (!strlen($mailbox)) {
            $mailbox = 'INBOX';
        }
        $this->select($mailbox);
        if ($this->selected == $mailbox) {
            return $this->data['RECENT'];
        }
        return false;
@@ -1303,14 +1469,18 @@
                $parts_count = count($a);
                if ($parts_count>=6) {
                   for ($i=0; $i<$parts_count; $i=$i+2) {
                      if ($a[$i] == 'UID')
                            if ($a[$i] == 'UID') {
                         $result[$id]->uid = intval($a[$i+1]);
                      else if ($a[$i] == 'RFC822.SIZE')
                            }
                            else if ($a[$i] == 'RFC822.SIZE') {
                         $result[$id]->size = intval($a[$i+1]);
                      else if ($a[$i] == 'INTERNALDATE')
                            }
                            else if ($a[$i] == 'INTERNALDATE') {
                         $time_str = $a[$i+1];
                      else if ($a[$i] == 'FLAGS')
                            }
                            else if ($a[$i] == 'FLAGS') {
                         $flags_str = $a[$i+1];
                            }
                   }
                   $time_str = str_replace('"', '', $time_str);
@@ -1333,7 +1503,7 @@
                }
                // the rest of the result
                preg_match('/ BODY\[HEADER.FIELDS \(.*?\)\]\s*(.*)$/s', $line, $m);
                    if (preg_match('/ BODY\[HEADER.FIELDS \(.*?\)\]\s*(.*)$/s', $line, $m)) {
                $reslines = explode("\n", trim($m[1], '"'));
                // re-parse (see below)
                foreach ($reslines as $resln) {
@@ -1341,6 +1511,7 @@
                      $lines[$ln] .= (empty($lines[$ln])?'':"\n").trim($resln);
                   } else {
                      $lines[++$ln] = trim($resln);
                            }
                   }
                }
             }
@@ -1444,12 +1615,14 @@
                      $result[$id]->messageID = $string;
                      break;
                   case 'x-priority':
                      if (preg_match('/^(\d+)/', $string, $matches))
                            if (preg_match('/^(\d+)/', $string, $matches)) {
                         $result[$id]->priority = intval($matches[1]);
                            }
                      break;
                   default:
                      if (strlen($field) > 2)
                            if (strlen($field) > 2) {
                         $result[$id]->others[$field] = $string;
                            }
                      break;
                   } // end switch ()
                } // end while ()
@@ -1565,27 +1738,6 @@
       return $result;
    }
    function expunge($mailbox, $messages=NULL)
    {
       if (!$this->select($mailbox)) {
            return false;
        }
        // Clear internal status cache
        unset($this->data['STATUS:'.$mailbox]);
      if ($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 = ''; // state has changed, need to reselect
         return true;
      }
       return false;
    }
    function modFlag($mailbox, $messages, $flag, $mod)
    {
@@ -1594,6 +1746,11 @@
       }
       if (!$this->select($mailbox)) {
            return false;
        }
        if (!$this->data['READ-WRITE']) {
            $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
           return false;
       }
@@ -1640,6 +1797,15 @@
    function move($messages, $from, $to)
    {
        if (!$this->select($from)) {
            return false;
        }
        if (!$this->data['READ-WRITE']) {
            $this->setError(self::ERROR_READONLY, "Mailbox is read-only", 'STORE');
            return false;
        }
        $r = $this->copy($messages, $from, $to);
        if ($r) {
@@ -1681,7 +1847,7 @@
             while ($n > 0) {
                $p = strpos($str, ')', $off);
                if ($p === false) {
                   error_log('Mismatched brackets parsing IMAP THREAD response:');
                        error_log("Mismatched brackets parsing IMAP THREAD response:");
                   error_log(substr($str, ($begin < 10) ? 0 : ($begin - 10), $end - $begin + 20));
                   error_log(str_repeat(' ', $off - (($begin < 10) ? 0 : ($begin - 10))));
                   return $node;
@@ -1808,14 +1974,18 @@
                if (!empty($items)) {
                    $result = array();
                    if (in_array('COUNT', $items))
                    if (in_array('COUNT', $items)) {
                        $result['COUNT'] = count($response);
                    if (in_array('MIN', $items))
                    }
                    if (in_array('MIN', $items)) {
                        $result['MIN'] = !empty($response) ? min($response) : 0;
                    if (in_array('MAX', $items))
                    }
                    if (in_array('MAX', $items)) {
                        $result['MAX'] = !empty($response) ? max($response) : 0;
                    if (in_array('ALL', $items))
                    }
                    if (in_array('ALL', $items)) {
                        $result['ALL'] = $this->compressMessageSet($response, true);
                    }
                    return $result;
                }
@@ -1959,8 +2129,9 @@
        $type   = $mime ? 'MIME' : 'HEADER';
       // format request
       foreach($parts as $part)
        foreach($parts as $part) {
          $peeks[] = "BODY.PEEK[$part.$type]";
        }
       $request = "$key FETCH $id (" . implode(' ', $peeks) . ')';
@@ -2048,12 +2219,15 @@
            $result = substr($line, $from, $len);
         }
           if ($mode == 1)
            if ($mode == 1) {
            $result = base64_decode($result);
         else if ($mode == 2)
            }
            else if ($mode == 2) {
            $result = quoted_printable_decode($result);
         else if ($mode == 3)
            }
            else if ($mode == 3) {
            $result = convert_uudecode($result);
            }
       } else if ($line[$len-1] == '}') {
           // multi-line request, find sizes of content and receive that many bytes
@@ -2067,8 +2241,9 @@
           while ($bytes > 0) {
              $line = $this->readLine(4096);
              if ($line === NULL)
                if ($line === NULL) {
                  break;
                }
               $len  = strlen($line);
@@ -2153,39 +2328,6 @@
      return ($result == self::ERROR_OK);
    }
    function deleteFolder($mailbox)
    {
        $result = $this->execute('DELETE', array($this->escape($mailbox)),
           self::COMMAND_NORESPONSE);
       return ($result == self::ERROR_OK);
    }
    function clearFolder($mailbox)
    {
       $num_in_trash = $this->countMessages($mailbox);
       if ($num_in_trash > 0) {
          $this->delete($mailbox, '1:*');
       }
       return ($this->expunge($mailbox) >= 0);
    }
    function subscribe($mailbox)
    {
       $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
           self::COMMAND_NORESPONSE);
       return ($result == self::ERROR_OK);
    }
    function unsubscribe($mailbox)
    {
       $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
           self::COMMAND_NORESPONSE);
       return ($result == self::ERROR_OK);
    }
    function append($mailbox, &$message)
    {
       if (!$mailbox) {
@@ -2207,7 +2349,7 @@
       if ($this->putLine($request)) {
            // Don't wait when LITERAL+ is supported
            if (!$this->prefs['literal+']) {
                $line = $this->readLine(512);
                $line = $this->readReply();
              if ($line[0] != '+') {
                 $this->parseResult($line, 'APPEND: ');
@@ -2271,7 +2413,7 @@
       if ($this->putLine($request)) {
            // Don't wait when LITERAL+ is supported
            if (!$this->prefs['literal+']) {
              $line = $this->readLine(512);
                $line = $this->readReply();
              if ($line[0] != '+') {
                 $this->parseResult($line, 'APPEND: ');
@@ -2372,8 +2514,9 @@
          $parts        = explode(' ', $quota_line);
          $storage_part = array_search('STORAGE', $parts);
          if (!$storage_part)
            if (!$storage_part) {
                continue;
            }
          $used  = intval($parts[$storage_part+1]);
          $total = intval($parts[$storage_part+2]);
@@ -2554,11 +2697,12 @@
        }
        foreach ($entries as $name => $value) {
            if ($value === null)
            if ($value === null) {
                $value = 'NIL';
            else
            }
            else {
                $value = sprintf("{%d}\r\n%s", strlen($value), $value);
            }
            $entries[$name] = $this->escape($name) . ' ' . $value;
        }
@@ -2583,16 +2727,18 @@
     */
    function deleteMetadata($mailbox, $entries)
    {
        if (!is_array($entries) && !empty($entries))
        if (!is_array($entries) && !empty($entries)) {
            $entries = explode(' ', $entries);
        }
        if (empty($entries)) {
            $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
            return false;
        }
        foreach ($entries as $entry)
        foreach ($entries as $entry) {
            $data[$entry] = NULL;
        }
        return $this->setMetadata($mailbox, $data);
    }
@@ -2628,13 +2774,16 @@
            $options = array_change_key_case($options, CASE_UPPER);
            $opts = array();
            if (!empty($options['MAXSIZE']))
            if (!empty($options['MAXSIZE'])) {
                $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
            if (!empty($options['DEPTH']))
            }
            if (!empty($options['DEPTH'])) {
                $opts[] = 'DEPTH '.intval($options['DEPTH']);
            }
            if ($opts)
            if ($opts) {
                $optlist = '(' . implode(' ', $opts) . ')';
            }
        }
        $optlist .= ($optlist ? ' ' : '') . $entlist;
@@ -2701,10 +2850,12 @@
            $attr  = $entry[1];
            $value = $entry[2];
            if ($value === null)
            if ($value === null) {
                $value = 'NIL';
            else
            }
            else {
                $value = sprintf("{%d}\r\n%s", strlen($value), $value);
            }
            $entries[] = sprintf('%s (%s %s)',
                $this->escape($name), $this->escape($attr), $value);
@@ -2796,10 +2947,12 @@
                        for ($x=0, $len=count($attribs); $x<$len;) {
                            $attr  = $attribs[$x++];
                            $value = $attribs[$x++];
                            if ($attr == 'value.priv')
                            if ($attr == 'value.priv') {
                                $res['/private' . $entry] = $value;
                            else if ($attr == 'value.shared')
                            }
                            else if ($attr == 'value.shared') {
                                $res['/shared' . $entry] = $value;
                            }
                        }
                    }
                    $last_entry = $entry;
@@ -2847,8 +3000,9 @@
        $noresp   = ($options & self::COMMAND_NORESPONSE);
        $response = $noresp ? null : '';
        if (!empty($arguments))
        if (!empty($arguments)) {
            $query .= ' ' . implode(' ', $arguments);
        }
        // Send command
       if (!$this->putLineC($query)) {
@@ -2859,8 +3013,9 @@
        // Parse response
       do {
          $line = $this->readLine(4096);
          if ($response !== null)
            if ($response !== null) {
              $response .= $line;
            }
       } while (!$this->startsWith($line, $tag . ' ', true, true));
       $code = $this->parseResult($line, $command . ': ');
@@ -2878,9 +3033,9 @@
          $this->parseCapability($matches[1], true);
       }
        // return last line only (without command tag and result)
        // return last line only (without command tag, result and response code)
        if ($line && ($options & self::COMMAND_LASTLINE)) {
            $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*/i", '', trim($line));
            $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
        }
       return $noresp ? $code : array($code, $response);
@@ -2979,9 +3134,11 @@
    {
       $result = '';
       $size = strlen($string);
       for ($i=0; $i<$size; $i++) {
          $result .= chr(ord($string[$i]) ^ ord($string2[$i]));
       }
       return $result;
    }
@@ -3001,7 +3158,9 @@
       while ((($ts = @strtotime($date))===false) || ($ts < 0)) {
           $d = explode(' ', $date);
          array_pop($d);
          if (!$d) break;
            if (!$d) {
                break;
            }
          $date = implode(' ', $d);
       }
@@ -3046,16 +3205,14 @@
     */
    static function escape($string)
    {
        // NIL
        if ($string === null) {
            return 'NIL';
        }
        // empty string
        else if ($string === '') {
            return '""';
        }
        // string: special chars: SP, CTL, (, ), {, %, *, ", \, ]
        else if (preg_match('/([\x00-\x20\x28-\x29\x7B\x25\x2A\x22\x5C\x5D\x7F]+)/', $string)) {
            // string: special chars: SP, CTL, (, ), {, %, *, ", \, ]
           return '"' . strtr($string, array('"'=>'\\"', '\\' => '\\\\')) . '"';
        }