Location: PHPKode > projects > phlyMail Lite > phlymail/shared/lib/imap.inc.php
<?php
/**
 * IMAP client connector class
 *
 * @package phlyMail Nahariya 4.0+
 * @subpackage Email handler
 * @subpackage Network client connectivity
 * @author Matthias Sommerfeld <hide@address.com>
 * @copyright 2005-2010 phlyLabs, Berlin (http://phlylabs.de)
 * @version 4.3.9 2010-05-11
 * @todo Implement ACLs, Quotas
 * @todo Allow usage of transparent IMAP proxy, which caches connections
 */

class imap {
    public $append_errors = true;
    public $timestamp_errors = false;

    protected $error = false;
    protected $fp = false;
    protected $has_tls = false;
    protected $may_tls = true;
    protected $is_tls = false;
    protected $is_ssl = false;
    protected $_diag_session = false;
    protected $scount = 0;
    protected $currFolder = false;
    protected $folderInfo = false;
    protected $server_capa = false;
    // List of SASL mechanisms we support
    protected $SASL = array('cram_sha256', 'cram_sha1', 'cram_md5', 'login', 'plain');

    public function __construct($server, $port = 143, $recon_slp = 0, $allowtls = true)
    {
        if ($this->_diag_session) $this->diag = fopen(dirname(__FILE__).'/imap_diag.txt', 'a');
        $this->may_tls = $allowtls;
        if (!$port) $port = 143;
        if ($this->connect($server, $port)) {
            $this->server = $server;
            $this->port = $port;
            $this->connected = true;
        } else $this->connected = false;
        return true;
    }

    /**
     * Sole aim is, to know, whether we are connected or not
     * since we cannot return something useful on construction
     * of the object
     */
    public function check_connected() { return $this->connected; }

    public function get_last_error()
    {
        $return = ($this->error) ? $this->error : '';
        unset ($this->error);
        return $return;
    }

    /**
     * Log in to IMAP server
     * @param string  $user  Username
     * @param string  $pass  Password
     * @param string  $mailbox  Folder to connect to
     * @param bool  $ro  Whether to connect in read-only mode or not
     */
    public function login($user = '', $pass = '', $mailbox = 'INBOX', $ro = false)
    {
        $return = array('type' => false, 'login' => false);
        // Try to find out about IMAP AUTH and use it on success
        $this->server_capa = $capa = $this->capa(false);
        if ($this->has_tls && false !== $capa && isset($capa['stls']) && $capa['stls']) {
            $this->talk('STARTTLS');
            $res = stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
            if (!$res) {
                $this->close();
                $this->set_error('Cannot enable TLS, although server advertises it');
            } else {
                $this->is_tls = true;
            }
            // Now check, whether the connections has not been closed
            if (!$this->alive()) {
                $this->set_error($response);
                $this->close();
                sleep($this->reconnect_sleep);
                $this->connect($this->server, $this->port);
                if (!$this->alive()) return $return;
                if (!$this->is_ssl) $this->noop();
            }
        }
        // Server supports IMAP AUTH... try the supported mechanisms to authenticate
        if (/*!$this->is_ssl && !$this->is_tls && */false !== $capa && $capa['sasl'] != false) {
            // Find the mechanisms supported on both sides
            $SASL = array_intersect($this->SASL, $capa['sasl_internal']);
            $this->is_auth = false;
            foreach ($SASL as $v) {
                $function_name = '_auth_'.$v;
                if ($this->{$function_name}($user, $pass)) {
                    $return['login'] = 1;
                    break;
                }
            }
            // Now check, whether the connections has not been closed
            if (!$this->alive()) {
                $this->set_error($response);
                $this->close();
                sleep($this->reconnect_sleep);
                $this->connect($this->server, $this->port);
                if (!$this->alive()) return $return;
                if (!$this->is_ssl) $this->noop();
            }
        }
        // Login; only if NOT disabled
        if (1 != $return['login'] && (false === $capa || !$capa['logindisabled'])) {
            $response = $this->talk('LOGIN', $this->escapeString($user, $pass), true);
            if ($response) {
                $return['login'] = 1;
            } else {
                $this->set_error($response);
                if (!$this->alive()) return $return;
            }
        }
        $info = $this->selectFolder($mailbox);
        if (false !== $info) {
            $this->folderInfo = $info;
            $return = array_merge($return, $info);
        }
        $return['type'] = ($this->is_ssl || $this->is_tls) ? 'secure' : 'normal';
        return $return;
    }

    /**
     * Query capability information from the server.
     * @param bool Set to true to receive the unparsed response, Default: false,
     *     which will give you a nice little array of recognized capabilities
     * @return mixed  String on $raw = true, array of recognized features otherwise
     * @since 3.6.0
     */
    public function capa($raw = false)
    {
        $return = array('acl' => false, 'binary' => false, 'children' => false, 'catenate' => false, 'stls' => false, 'condstore' => false
                ,'convert' => false, 'enable' => false, 'esearch' => false, 'id' => false, 'idle' => false, 'literal+' => false
                ,'login-referrals' => false, 'logindisabled' => false, 'mailbox-referrals' => false, 'namespace' => false
                ,'qresync' => false, 'quota' => false, 'sasl' => false, 'sasl-ir' => false, 'searchres' => false, 'sort' => false
                ,'thread' => false, 'uidplus' => false, 'unselect' => false, 'urlauth' => false);
        if (false !== strpos($this->greeting, '[CAPABILITY')) {
            preg_match('!\[(CAPABILITY .+)\]!', $this->greeting, $found);
            $response = explode(' ', $found[1]);
            $this->greeting = preg_replace('!\[(CAPABILITY .+)\]!', '', $this->greeting);
        } else {
            $response = $this->talk('CAPABILITY');
            $response = $response[0];
        }
        if ($raw) return $response;
        array_shift($response); // Drop Command echo from response
        while ($capa = array_shift($response)) {
            $capa = strtolower($capa);
            if ($capa == 'starttls') {
                $return['stls'] = true;
                continue;
            }
            if (substr($capa, 0, 5) == 'auth=') {
                $return['sasl'] = true;
                $return['sasl_internal'][] = str_replace('-', '_', substr($capa, 5));
                continue;
            }
            if (substr($capa, 0, 9) == 'compress=') {
                $return['compress'][] = str_replace('-', '_', substr($capa, 9));
                continue;
            }
            if (substr($capa, 0, 7) == 'thread=') {
                $return['thread'][] = str_replace('-', '_', substr($capa, 7));
                continue;
            }
            if (isset($return[$capa])) $return[$capa] = true;
        }
        return $return;
    }

    // Return LIST, if mail given of this one, else complete
    public function get_list($mail = false, $listonly = false)
    {
        $LBLs = array(1 => '$label1', 2 => '$label2', 3 => '$label3', 4 => '$label4', 5 => '$label5'
                ,6 => '$label6', 7 => '$label7', 8 => '$label8', 9 => '$label9', 10 => '$label10'
                ,11 => '$label11', 12 => '$label12', 13 => '$label13', 14 => '$label14');
        if ($mail) {
            $info = $this->fetch(array('FLAGS', 'RFC822.SIZE', 'UID'), $mail);
            $flags = array();
            foreach ($info['FLAGS'] as $k => $v) {
                $flags[strtolower($v)] = $k;
            }
            $return = array
                    ('size' => $info['RFC822.SIZE']
                    ,'rawflags' => $info['FLAGS']
                    ,'uidl' => $info['UID']
                    ,'recent' => (isset($flags['\recent']))
                    ,'flagged' => (isset($flags['\flagged']))
                    ,'answered' => (isset($flags['\answered']))
                    ,'seen' => (isset($flags['\seen']))
                    ,'draft' => (isset($flags['\draft']))
                    ,'forwarded' => (isset($flags['\forwarded']) || isset($flags['$forwarded']))
                    ,'bounced' => (isset($flags['\bounced']))
                    ,'label' => 0
                    );
            foreach ($LBLs as $off => $lbl) {
                if (isset($flags[$lbl])) {
                    $return['label'] = $off;
                    break;
                }
            }
            return $return;
        } else {
            $return = array();
            // Mailbox is empty?
            if ($this->countMessages() == 0) return $return;
            // Only UIDs to be listed
            if ($listonly) return $this->fetch('UID', 1, INF);
            // Full info requested            
            foreach ($this->fetch(array('FLAGS', 'RFC822.SIZE', 'UID'), 1, INF) as $msgno => $info) {
                $flags = array();
                foreach ($info['FLAGS'] as $k => $v) {
                    $flags[strtolower($v)] = $k;
                }
                if (isset($flags['\deleted'])) continue;
                $return[$msgno] = array
                        ('size' => $info['RFC822.SIZE']
                        ,'rawflags' => $info['FLAGS']
                        ,'uidl' => $info['UID']
                        ,'recent' => (isset($flags['\recent']))
                        ,'flagged' => (isset($flags['\flagged']))
                        ,'answered' => (isset($flags['\answered']))
                        ,'seen' => (isset($flags['\seen']))
                        ,'draft' => (isset($flags['\draft']))
                        ,'forwarded' => (isset($flags['\forwarded']) || isset($flags['$forwarded']))
                        ,'bounced' => (isset($flags['\bounced']))
                        ,'label' => 0
                        );
                foreach ($LBLs as $off => $lbl) {
                    if (isset($flags[$lbl])) {
                        $return[$msgno]['label'] = $off;
                        break;
                    }
                }
            }
            return $return;
        }
    }

    // Get the header lines of a mail
    // DEPRECATED
    public function top($mail = false)
    {
        if (!$mail) {
            $this->set_error('No mail given');
            return false;
        }
        return $this->getRawHeader($mail);
    }

    // Get the Unique ID of a mail
    // DEPRECATED
    public function uidl($mail = false)
    {
        if (!$mail) {
            $this->set_error('No mail given');
            return false;
        }
        return $this->getUniqueId($mail);
    }

    // DEPRECATED
    public function msgno($uidl = false)
    {
        if (!$uidl) {
            $this->set_error('No UID given');
            return false;
        }
        return $this->getNumberByUniqueId($uidl);
    }

    // Get stats of a IMAP box
    // WARNING: The returned data is incompatible to that of the POP3 driver
    public function stat()
    {
        $result = $this->selectFolder($this->currFolder);
        if (false !== $result) {
            return array('mails' => $result['exists'], 'size' => 0);
        } else {
            return false;
        }
    }

    // For compliance with the POP3 class only
    public function reset() { return true; }

    /**
     * Retrieve a mail from server and put into given file
     * @param int  $mail  The msgno of the mail to retrieve
     * @param string  $path  Where to store the file (full path including file name)
     * @return string $path  The path to the file
     */
    public function retrieve_to_file($mail = false, $path = false)
    {
        $old_umask = umask(0);
        if (!$mail || !$path) {
            $this->set_error('Usage: retrieve_to_file(integer mail, string path)');
            return false;
        }
        if (!file_exists(dirname($path)) || !is_dir(dirname($path))) {
            $this->set_error('Non existent directory '.dirname($path));
            return false;
        }
        $fh = fopen($path, 'w');
        if (!is_resource($fh)) {
            $this->set_error('Could not open file handle');
            return false;
        }
        $bytes = $this->getRawContent($mail);
        while (true) {
            $line = $this->talk_ml();
            if (false === $line) break; // probably end of file
            $bytes -= strlen($line);
            fputs($fh, $line);
            if ($bytes <= 0) break;
        }
        fclose($fh);
        while (false !== $this->talk_ml()) { /* void */ }
        chmod($path, $GLOBALS['_PM_']['core']['file_umask']);
        umask($old_umask);
        return $path;
    }

    /**
     * Append a new message to given folder. Use fputs / fwrite for the message
     * body subsequently and $this->finishAppend() to get the result.
     *
     * @param string $folder  name of target folder
     * @param int $length  lenth of the message
     * @param array  $flags   flags for new message
     * @param string $date    date for new message
     * @return resource $
     */
    public function appendMessage($folder, $length, $flags = null, $date = null)
    {
        if ($folder === null) $folder = $this->currFolder;
        // if ($flags === null) $flags = array('\Seen'); # Inspired by user report
        $tokens = array();
        $tokens[] = $this->escapeString($folder);
        if ($flags !== null) $tokens[] = $this->escapeList($flags);
        if ($date !== null) $tokens[] = $this->escapeString($date);
        $tokens[] = '{' . intval($length) . '}';
        $this->sendRequest('APPEND', $tokens);
        if ($this->_assumedNextLine('+ OK', '+')) {
            return $this->fp;
        }
        return false;
    }

    /**
     * copy message(s) from current folder to other folder
     *
     * @param string   $folder destination folder
     * @param int|null $to     if null only one message ($from) is fetched, else it's the
     *                         last message, INF means last message avaible
     * @return bool success
     */
    public function copyMessage($folder, $from, $to = null)
    {
        $set = (int)$from;
        if ($to != null) {
            $set .= ':' . ($to === INF ? '*' : (int)$to);
        }
        return $this->talk('COPY', array($set, $this->escapeString($folder)), true);
    }

    /**
     * set flags
     *
     * @param  array       $flags  flags to set, add or remove - see $mode
     * @param  int         $from   message for items or start message if $to !== null
     * @param  int|null    $to     if null only one message ($from) is fetched, else it's the
     *                             last message, INF means last message avaible; passing an array makes
     *                              up a comma separated message list consisting of $from,$to[0], $to[1], ...
     * @param  string|null $mode   '+' to add flags, '-' to remove flags, everything else sets the flags as given
     * @param  bool        $silent if false the return values are the new flags for the wanted messages
     * @return bool|array new flags if $silent is false, else true or false depending on success
     */
    public function setFlags(array $flags, $from, $to = null, $mode = null, $silent = true)
    {
        $item = 'FLAGS';
        if ($mode == '+' || $mode == '-') $item = $mode . $item;
        if ($silent) $item .= '.SILENT';
        $flags = $this->escapeList($flags);
        $set = (int) $from;
        if ($to != null) {
            if (is_array($to) && !empty($to)) {
                $set .= ','.implode(',', $to);
            } else {
                $set .= ':'.($to === INF ? '*' : (int) $to);
            }
        }
        $result = $this->talk('STORE', array($set, $item, $flags), $silent);
        if ($silent) return $result ? true : false;
        $tokens = $result;
        $result = array();
        foreach ($tokens as $token) {
            if ($token[1] != 'FETCH' || $token[2][0] != 'FLAGS') continue;
            $result[$token[0]] = $token[2][1];
        }
        return $result;
    }

    /**
     * Count all messages in current box
     *
     * @return int number of messages
     */
    public function countMessages()
    {
        if (!$this->currFolder) return false;
        // we're reselecting the current mailbox, because STATUS is slow and shouldn't be used on the current mailbox
        $result = $this->selectFolder($this->currFolder);
        return $result['exists'];
    }

    /**
     * get a list of messages with number and size
     *
     * @param int $id number of message
     * @return int|array size of given message of list with all messages as array(num => size)
     */
    public function getSize($id = 0)
    {
        if ($id) return $this->fetch('RFC822.SIZE', $id);
        return $this->fetch('RFC822.SIZE', 1, INF);
    }

    /**
     * Fetch a message
     *
     * @param int $id number of message
     * @return array
     */
    public function getMessage($id)
    {
        $data = $this->fetch(array('FLAGS', 'RFC822.HEADER'), $id);
        $header = $data['RFC822.HEADER'];
        $flags = array();
        foreach ($data['FLAGS'] as $flag) { $flags[] = $flag; }
        return array('id' => $id, 'headers' => $header, 'flags' => $flags);
    }

    /*
     * Get raw header of message or part
     *
     * @param int  $id  number of message
     * @param null|string  $part  path to part or null for messsage header
     * @return string  raw header
     */
    public function getRawHeader($id, $part = null)
    {
        if ($part !== null) return $this->fetch('BODY.PEEK[' . $part . '.HEADER]', $id);
        return $this->fetch('RFC822.HEADER', $id);
    }

    /*
     * Get raw content of message or part
     *
     * @param int  $id  number of message
     * @param null|string  $part  path to part or null for messsage content
     * @return int  Number of bytes to read subsequently with $this->talk_ml()
     */
    public function getRawContent($id, $part = null)
    {
        $bytes = 0;
        $this->sendRequest('FETCH', array($id, 'BODY.PEEK['.($part !== null ? $part : '').']'), $tag);
        // Read ahead out of band responses
        while ($line = $this->talk_ml()) {
            if (false === $line) return 0; // The command failed due to a protocol error;
            if (0 === strpos($line, '* '.$id.' FETCH')) {
                preg_match('!\{(\d+)\}$!', trim($line), $found);
                $bytes = $found[1];
                break;
            }
        }
        return $bytes;
    }

    /**
     * Remove a message / a message set from server. If you're doing that from a web enviroment
     * you should be careful and use a uniqueid as parameter if possible to
     * identify the message.
     *
     * @param  int         $from   message for items or start message if $to !== null
     * @param  int|null    $to     if null only one message ($from) is fetched, else it's the
     *                             last message, INF means last message avaible; passing an array makes
     *                             up a comma separated message list consisting of $from,$to[0], $to[1], ...
     * @return null
     */
    public function removeMessage($id, $to = null)
    {
        if (!$this->setFlags(array('\Deleted'), $id, $to, '+')) return false;
        return true;
    }

    /**
     * get unique id for one or all messages
     *
     * if storage does not support unique ids it's the same as the message number
     *
     * @param int|null $id message number
     * @return array|string message number for given message or all messages as array
     */
    public function getUniqueId($id = null)
    {
        if ($id) return $this->fetch('UID', $id);
        return $this->fetch('UID', 1, INF);
    }

    /**
     * get a message number from a unique id
     *
     * I.e. if you have a webmailer that supports deleting messages you should use unique ids
     * as parameter and use this method to translate it to message number right before calling removeMessage()
     *
     * @param string $id unique id
     * @return int message number
     */
    public function getNumberByUniqueId($id)
    {
        $result = $this->talk('SEARCH', array('UID', $id));
        if (!empty($result) && isset($result[0][1])) return $result[0][1];
        return false;
    }

    /**
     * Search messages matching given parameters.
     * The parameter array follows the complicated yet flexible IMAP syntax.
     *
     * @param array $param Search criteria and search string
     * @return array message numbers found
     */
    public function searchMessages(array $params)
    {
        $search = '';
        while (count($params)) $search .= trim(array_shift($params)).' ';
        $result = $this->talk('UID SEARCH', array((string) $search));
        if (empty($result) || !isset($result[0][1])) return array();
        array_shift($result[0]);
        return $result[0];
    }

    /**
     * Examine and select have the same response. The common code for both
     * is in this method
     *
     * @param  string $command can be 'EXAMINE' or 'SELECT' and this is used as command
     * @param  string $box which folder to change to or examine
     * @return bool|array false if error, array with returned information otherwise
     *             (flags, exists, recent, uidvalidity, permanentflags, customflags)
     */
    public function examineOrSelectFolder($command = 'EXAMINE', $box = 'INBOX')
    {
        $this->sendRequest($command, array($this->escapeString($box)), $tag);
        $result = array('customflags' => 0);
        while (!$this->readLine($tokens, $tag)) {
            if ($tokens[0] == 'FLAGS') {
                array_shift($tokens);
                $result['flags'] = $tokens;
                continue;
            }
            switch ($tokens[1]) {
            case 'EXISTS':
            case 'RECENT':
                $result[strtolower($tokens[1])] = $tokens[0];
                break;
            case '[UIDVALIDITY':
                $result['uidvalidity'] = (int) $tokens[2];
                break;
            case '[PERMANENTFLAGS':
                $result['permanentflags'] = implode(' ', $tokens[2]);
                $result['permanentflags'] = substr($result['permanentflags'], 0, strpos($result['permanentflags'], ']')-1);
                $result['customflags'] = (strpos($result['permanentflags'], '\*') !== false) ? 1 : 0;
                $result['permanentflags'] = explode(' ', $result['permanentflags']);
                break;
            }
        }
        if ($tokens[0] != 'OK') return false;
        return $result;
    }

    /**
     * change folder
     *
     * @param  string $box change to this folder
     * @return bool|array see examineOrselect()
     */
    public function selectFolder($box = 'INBOX')
    {
        $this->currFolder = $box;
        return $this->examineOrSelectFolder('SELECT', $box);
    }

    /**
     * examine folder
     *
     * @param  string $box examine this folder
     * @return bool|array see examineOrselect()
     */
    public function examineFolder($box = 'INBOX')
    {
        return $this->examineOrSelectFolder('EXAMINE', $box);
    }

    /**
     * fetch one or more items of one or more messages
     *
     * @param  string|array $items items to fetch from message(s) as string (if only one item)
     *         or array of strings
     * @param  int          $from  message for items or start message if $to !== null
     *[@param  int|null     $to    if null only one message ($from) is fetched, else it's the
     *         last message, INF means last message avaible]
     * @return string|array if only one item of one message is fetched it's returned as string
     *                      if items of one message are fetched it's returned as (name => value)
     *                      if one items of messages are fetched it's returned as (msgno => value)
     *                      if items of messages are fetchted it's returned as (msgno => (name => value))
     * @throws Exception
     */
    public function fetch($items, $from, $to = null)
    {
        if (is_array($from)) {
            $set = implode(',', $from);
        } elseif ($to === null) {
            $set = (int)$from;
        } elseif ($to === INF) {
            $set = (int)$from . ':*';
        } else {
            $set = (int)$from . ':' . (int)$to;
        }
        $items = (array)$items;
        $itemList = $this->escapeList($items);
        $this->sendRequest('FETCH', array($set, $itemList), $tag);
        // If we specify the PEEK param, the server won't return it....
        foreach ($items as $k => $v) { if (preg_match('!\.PEEK!', $v)) { $items[$k] = str_replace('.PEEK', '', $v); } }
        $result = array();
        while (!$this->readLine($tokens, $tag)) {
            // ignore other responses
            if ($tokens[1] != 'FETCH') continue;
            // ignore other messages
            if ($to === null && !is_array($from) && $tokens[0] != $from) continue;
            // if we only want one item we return that one directly
            if (count($items) == 1) {
                if ($tokens[2][0] == $items[0]) {
                    $data = $tokens[2][1];
                } else {
                    // maybe the server sent another field we didn't want
                    $count = count($tokens[2]);
                    // we start with 2, because 0 was already checked
                    for ($i = 2; $i < $count; $i += 2) {
                        if ($tokens[2][$i] != $items[0]) continue;
                        $data = $tokens[2][$i + 1];
                        break;
                    }
                }
            } else {
                $data = array();
                while (key($tokens[2]) !== null) {
                    $data[current($tokens[2])] = next($tokens[2]);
                    next($tokens[2]);
                }
            }
            // if we want only one message we can ignore everything else and just return
            if ($to === null && !is_array($from) && $tokens[0] == $from) {
                // we still need to read all lines
                while (!$this->readLine($tokens, $tag));
                return $data;
            }
            $result[$tokens[0]] = $data;
        }
        if ($to === null && !is_array($from)) {
            throw new Exception('the single id was not found in response');
        }
        return $result;
    }

    /**
     * get mailbox list
     *
     * this method can't be named after the IMAP command 'LIST', as list is a reserved keyword
     *
     * @param  string $reference mailbox reference for list
     * @param  string $mailbox   mailbox name match with wildcards
     *[@param  bool  $lsub  Set to true to get the subcribed folders only]
     * @return array mailboxes that matched $mailbox as array(globalName => array('delim' => .., 'flags' => ..))
     */
    public function listMailbox($reference = '', $mailbox = '*', $lsub = false)
    {
        $result = array();
        $command = ($lsub !== false ? 'LSUB' : 'LIST');
        $list = $this->talk($command, $this->escapeString($reference, $mailbox));
        if (!$list) return $result;
        foreach ($list as $item) {
            if (count($item) != 4 || $item[0] != $command) continue;
            $result[$item[3]] = array('delim' => $item[2], 'flags' => $item[1]);
        }
        return $result;
    }


    /**
     * create a new folder
     *
     * This method also creates parent folders if necessary. Some mail storages may restrict, which folder
     * may be used as parent or which chars may be used in the folder name
     *
     * @param string  $name  global name of folder, local name if $parentFolder is set
     * @param string  $parentFolder  parent folder for new folder, else root folder is parent
     * @return bool
     * @todo  Find out correct delimiter first, make it public property or third parameter
     */
    public function createFolder($name, $parentFolder = null)
    {
        if ($parentFolder != null) {
            $folder = $parentFolder.'/'.$name;
        } else {
            $folder = $name;
        }
        return $this->talk('CREATE', array($this->escapeString($folder)), true);
    }


    /**
     * remove a folder
     *
     * @param string  $folder  name or instance of folder
     * @return bool
     */
    public function removeFolder($folder)
    {
        return $this->talk('DELETE', array($this->escapeString($folder)), true);
    }

    /**
     * rename and/or move folder
     *
     * The new name has the same restrictions as in createFolder()
     *
     * @param string  $old  name of folder
     * @param string  $new  new global name of folder
     * @return bool
     */
    public function renameFolder($old, $new)
    {
        return $this->talk('RENAME', $this->escapeString($old, $new), true);
    }

    /**
     * subscribe to a folder
     *
     * @param string  $folder  name of folder
     * @return bool
     */
    public function subscribeFolder($folder)
    {
        return $this->talk('SUBSCRIBE', array($this->escapeString($folder)), true);
    }

    /**
     * unsubscribe from a specific mailbox
     *
     * @param string $folder folder name
     * @return bool success
     */
    public function unsubscribeFolder($folder)
    {
        return $this->talk('UNSUBSCRIBE', array($this->escapeString($folder)), true);
    }

    /**
     * permanently remove messages
     *[@param bool  Pass TRUE to silently drop \Deleted messages without caring for repsonse codes; Default: FALSE]
     * @return bool success
     * @todo  Parse response!
     */
    public function expunge($short = false)
    {
        if ($short) {
            return $this->talk('CLOSE');
        }
        return $this->talk('EXPUNGE');
    }

    /**
     * send noop
     * @return bool success
     * @todo  Parse response!
     */
    public function noop()
    {
        return $this->talk('NOOP');
    }

    /**
     * Not yet supported
     *
     * @param string $root  Where to start fetching the quota, usually INBOX
     * @return mixed $quota Array (see php.net/imap_get_quotaroot) on success or FALSE on failure
     * @since 0.2.3
     */
    public function get_quota($root = 'INBOX')
    {
        /* $quota = @imap_get_quotaroot($this->mbox, $root);
        return (is_array($quota) ? $quota : false); */
    }

    /**
     * @return string current folder
     */
    public function getCurrentFolder()
    {
        return $this->currFolder;
    }

    // Close IMAP connection
    public function close()
    {
        if ($this->connected) {
            $this->talk_auth('LOGOUT', true); // Might prevent a hanging connection
            fclose($this->fp);
        }
        $this->fp = false;
        return true;
    }

    //
    // Private / protected methods
    //

    // Do the actual connect to the chosen server
    protected function connect($host = '', $port = 143)
    {
        // Should speed up IMAP operations
        if (isset($_SESSION['phM_IPcache']) && isset($_SESSION['phM_IPcache'][$host])) {
            $host = $_SESSION['phM_IPcache'][$host];
        } elseif (function_exists('gethostbyname') && !preg_match('!^\d+\.\d+\.\d+\.\d+$!', $host)) {
            $IP = @gethostbyname($host);
            if (false !== $IP) {
                $_SESSION['phM_IPcache'][$host] = $IP;
                $host = $IP;
            }
        }
        $ERRNO = $ERRSTR = false;
        $ssl_capable = function_exists('extension_loaded') && extension_loaded('openssl');
        $this->has_tls = $this->may_tls ? (function_exists('stream_socket_enable_crypto')) : false;
        if ($port == 993) {
            if (!$ssl_capable) {
                $port = 143;
            } else {
                $host = (substr($host, 0, 6) == 'ssl://') ? $host : 'ssl://'.$host;
            }
        } elseif (substr($host, 0, 6) == 'ssl://') {
            if (!$ssl_capable) $host = str_replace('ssl://', '', $host);
        }
        $this->is_ssl = (substr($host, 0, 6) == 'ssl://') ? true : false;
        $fp = @fsockopen($host, $port, $ERRNO, $ERRSTR, 5);
        if (!$fp) {
            if ($this->_diag_session) {
                fputs($this->diag, 'Connection failed: '.$ERRSTR.' ('.$ERRNO.')'.LF);
            }
            $this->set_error('Connection failed: '.$ERRSTR.' ('.$ERRNO.')');
            return false;
        }
        $this->fp = $fp;
        $this->greeting = trim(fgets($fp, 1024));
        if (strtolower(substr($this->greeting, 0, 4)) != '* ok') {
            if ($this->_diag_session) {
                fputs($this->diag, ($this->greeting ? 'IMAP server response: '.$this->greeting : 'Bogus IMAP server behaviour!').LF);
            }
            $this->set_error($this->greeting ? 'IMAP server response: '.$this->greeting : 'Bogus IMAP server behaviour!');
            return false;
        }
        return true;
    }


    // Add or set an (timestamped error), that can be requested via get_last_error()
    protected function set_error($error)
    {
        $vorn = ($this->timestamp_errors) ? time().' ' : '';
        if ($this->append_errors) {
            $this->error .= $vorn.$error.LF;
        } else {
            $this->error  = $vorn.$error;
        }
    }

    // Try to find out, whether the connection is still alive
    protected function alive()
    {
        return $this->noop();
    }

    /**
     * Implementation of SASL mechanism CRAM-MD5
     *
     * @param string  Username
     * @param string  Password
     * @return boolean  TRUE on successful authentication, FALSE otherwise
     * @access private
     */
    protected function _auth_cram_md5($user = '', $pass = '')
    {
        // See RFC2104 (HMAC, also known as Keyed-MD5)
        $response = $this->talk_auth('AUTHENTICATE CRAM-MD5', true, $tag);
        if (strtoupper(substr($response, 0, 2)) == '+ ') {
            // Get the challenge from the server
            $challenge = base64_decode(substr(trim($response), 2));
            // Secret to use
            $secret = $pass;
            // Rightpad with NUL bytes to have 64 chars
            if (strlen($secret) < 64) $secret = $secret.str_repeat(chr(0x00), 64 - strlen($secret));
            // In case, the secret is longer than 64 chars, md5() it
            if (strlen($secret) > 64) $secret = md5($secret);
            $ipad = str_repeat(chr(0x36), 64);
            $opad = str_repeat(chr(0x5c), 64);
            $shared = bin2hex(pack('H*', md5(($secret ^ $opad).pack('H*', md5(($secret ^ $ipad).$challenge)))));
            $response = $this->talk_auth(base64_encode($user.' '.$shared));
            if (strtoupper(substr($response, 0, 3+strlen($tag))) != strtoupper($tag).' OK') {
                $this->error .= 'AUTH CRAM-MD5 failed: '.trim($response).LF;
                return false;
            }
            return true;
        } else {
            $this->error .= 'AUTH CRAM-MD5 rejected: '.trim($response).LF;
            return false;
        }
    }

    /**
     * Implementation of SASL mechanism CRAM-SHA1
     *
     * @param  string  Username
     * @param  string  Password
     * @return  boolean  TRUE on successful authentication, FALSE otherwise
     * @access  private
     */
    protected function _auth_cram_sha1($user = '', $pass = '')
    {
        $response = $this->talk_auth('AUTHENTICATE CRAM-SHA1', true, $tag);
        if (strtoupper(substr($response, 0, 2)) == '+ ') {
            // Get the challenge from the server
            $challenge = base64_decode(substr(trim($response), 2));
            // Secret to use
            $secret = $pass;
            // Rightpad with NUL bytes to have 64 chars
            if (strlen($secret) < 64) $secret = $secret.str_repeat(chr(0x00), 64 - strlen($secret));
            // In case, the secret is longer than 64 chars, md5() it
            if (strlen($secret) > 64) $secret = sha1($secret);
            $ipad = str_repeat(chr(0x36), 64);
            $opad = str_repeat(chr(0x5c), 64);
            $shared = bin2hex(pack('H*', sha1(($secret ^ $opad).pack('H*', sha1(($secret ^ $ipad).$challenge)))));
            $response = $this->talk_auth(base64_encode($user.' '.$shared));
            if (strtoupper(substr($response, 0, 3+strlen($tag))) != strtoupper($tag).' OK') {
                $this->error .= 'AUTH CRAM-SHA1 failed: '.trim($response).LF;
                return false;
            }
            return true;
        } else {
            $this->error .= 'AUTH CRAM-SHA1 rejected: '.trim($response).LF;
            return false;
        }
    }

    /**
     * Implementation of SASL mechanism CRAM-SHA256
     *
     * @param  string  Username
     * @param  string  Password
     * @return  boolean  TRUE on successful authentication, FALSE otherwise
     * @access  private
     */
    protected function _auth_cram_sha256($user = '', $pass = '')
    {
        $response = $this->talk_auth('AUTHENTICATE CRAM-SHA256', true, $tag);
        if (strtoupper(substr($response, 0, 2)) == '+ ') {
            // Get the challenge from the server
            $challenge = base64_decode(substr(trim($response), 2));
            // Secret to use
            $secret = $pass;
            // Rightpad with NUL bytes to have 64 chars
            if (strlen($secret) < 64) $secret = $secret.str_repeat(chr(0x00), 64 - strlen($secret));
            // In case, the secret is longer than 64 chars, md5() it
            if (strlen($secret) > 64) $secret = sha256($secret);
            $ipad = str_repeat(chr(0x36), 64);
            $opad = str_repeat(chr(0x5c), 64);
            $shared = bin2hex(pack('H*', sha256(($secret ^ $opad).pack('H*', sha256(($secret ^ $ipad).$challenge)))));
            $response = $this->talk_auth(base64_encode($user.' '.$shared));
            if (strtoupper(substr($response, 0, 3+strlen($tag))) != strtoupper($tag).' OK') {
                $this->error .= 'AUTH CRAM-SHA256 failed: '.trim($response).LF;
                return false;
            }
            return true;
        } else {
            $this->error .= 'AUTH CRAM-SHA256 rejected: '.trim($response).LF;
            return false;
        }
    }

    /**
     * Implementation of SASL mechanism LOGIN
     *
     * @param  string  Username
     * @param  string  Password
     * @return  boolean  TRUE on successful authentication, FALSE otherwise
     * @access  private
     */
    protected function _auth_login($user = '', $pass = '')
    {
        $response = $this->talk_auth('AUTHENTICATE LOGIN', true, $tag);
        if (substr($response, 0, 2) == '+ ') {
            $response = $this->talk_auth(base64_encode($user));
            if (substr($response, 0, 1) != '+') {
                $this->error .= 'AUTH LOGIN failed, wrong username? Aborting authentication.'.LF;
                return false;
            }
            $response = $this->talk_auth(base64_encode($pass));
            if (strtoupper(substr($response, 0, 3+strlen($tag))) != strtoupper($tag).' OK') {
                $this->error .= 'AUTH LOGIN failed, wrong password? Aborting authentication.'.LF;
                return false;
            }
            return true;
        } else {
            $this->error .= 'AUTH LOGIN rejected: '.trim($response).LF;
            return false;
        }
    }

    /**
     * Implementation of SASL mechanism PLAIN
     *
     * @param  string  Username
     * @param  string  Password
     * @return  boolean  TRUE on successful authentication, FALSE otherwise
     * @access  private
     */
    protected function _auth_plain($user = '', $pass = '')
    {
        $response = $this->talk_auth('AUTHENTICATE PLAIN '.base64_encode(chr(0).$user.chr(0).$pass), true, $tag);
        if (strtoupper(substr($response, 0, 3+strlen($tag))) != strtoupper($tag).' OK') {
            $this->error .= 'AUTH PLAIN failed: '.$response.LF;
            return false;
        }
        return true;
    }

    //
    // Communication layer methods - used to talk to the server, read its responses and encode/decode data accordingly
    //

    /**
     * get the next line from socket
     * @return string next line
     */
    protected function _nextLine()
    {
        if (!is_resource($this->fp)) return false;
        $line = @fgets($this->fp, 4096);
        if ($this->_diag_session) fputs($this->diag, 'S: '.$line);
        return $line;
    }

    /**
     * get next line and assume it starts with $start. some requests give a simple
     * feedback so we can quickly check if we can go on.
     *
     * @param  string $start  the first bytes we assume to be in the next line
     *[@param  string $optstart  Optional "second guess", a string the line could start with, too]
     * @return bool line starts with $start
     */
    protected function _assumedNextLine($start, $optstart = null)
    {
        $line = $this->_nextLine();
        if (strpos($line, $start) === 0) return true;
        if (!is_null($optstart) && strpos($line, $optstart) === 0) return true;
        return false;
    }

    /**
     * get next line and split the tag. that's the normal case for a response line
     *
     * @param  string $tag tag of line is returned by reference
     * @return string next line
     */
    protected function _nextTaggedLine(&$tag)
    {
        $line = $this->_nextLine();
        $line = explode(' ', $line, 2); // seperate tag from line
        $tag = $line[0];
        return isset($line[1]) ? $line[1] : false;
    }

    /**
     * split a given line in tokens. a token is literal of any form or a list
     * @param  string $line line to decode
     * @return array tokens, literals are returned as string, lists as array
     */
    protected function _decodeLine($line)
    {
        $tokens = array();
        $stack = array();
        /* We start to decode the response here. The understood tokens are:
                literal
                "literal" or also "lit\\er\"al"
                {bytes}<NL>literal
                (literals*)
            All tokens are returned in an array. Literals in braces (the last understood
            token in the list) are returned as an array of tokens. I.e. the following response:
                "foo" baz {3}<NL>bar ("f\\\"oo" bar)
            would be returned as:
                array('foo', 'baz', 'bar', array('f\\\"oo', 'bar')); */
        // replace any trailling <NL> including spaces with a single space
        $line = rtrim($line).' ';
        while (($pos = strpos($line, ' ')) !== false) {
            $token = substr($line, 0, $pos);
            if (!strlen($token)) $token = 0x0;
            while ($token[0] == '(') {
                array_push($stack, $tokens);
                $tokens = array();
                $token = substr($token, 1);
            }
            if ($token[0] == '"') {
                if (preg_match('%^"((.|\\\\|\\")*?)"%', $line, $matches)) {
                    $tokens[] = $matches[1];
                    $line = substr($line, strlen($matches[0]) + 1);
                    continue;
                }
            }
            if ($token[0] == '{') {
                $endPos = strpos($token, '}');
                $chars = substr($token, 1, $endPos - 1);
                if (is_numeric($chars)) {
                    $token = '';
                    while (strlen($token) < $chars) {
                        $token .= $this->_nextLine();
                    }
                    $line = '';
                    if (strlen($token) > $chars) {
                        $line = substr($token, $chars);
                        $token = substr($token, 0, $chars);
                    } else {
                        $line .= $this->_nextLine();
                    }
                    $tokens[] = $token;
                    $line = trim($line) . ' ';
                    continue;
                }
            }
            if ($stack && $token[strlen($token) - 1] == ')') {
                // closing braces are not seperated by spaces, so we need to count them
                $braces = strlen($token);
                $token = rtrim($token, ')');
                // only count braces if more than one
                $braces -= strlen($token) + 1;
                // only add if token had more than just closing braces
                if ($token) $tokens[] = $token;
                $token = $tokens;
                $tokens = array_pop($stack);
                // special handling if more than one closing brace
                while ($braces-- > 0) {
                    $tokens[] = $token;
                    $token = $tokens;
                    $tokens = array_pop($stack);
                }
            }
            $tokens[] = $token;
            $line = substr($line, $pos + 1);
        }
        // maybe the server forgot to send some closing braces
        while ($stack) {
            $child = $tokens;
            $tokens = array_pop($stack);
            $tokens[] = $child;
        }
        return $tokens;
    }

    /**
     * read a response "line" (could also be more than one real line if response has {..}<NL>)
     * and do a simple decode
     *
     * @param  array|string  $tokens    decoded tokens are returned by reference, if $dontParse
     *                                  is true the unparsed line is returned here
     * @param  string        $wantedTag check for this tag for response code. Default '*' is
     *                                  continuation tag.
     * @param  bool          $dontParse if true only the unparsed line is returned $tokens
     * @return bool if returned tag matches wanted tag
     */
    public function readLine(&$tokens = array(), $wantedTag = '*', $dontParse = false)
    {
        $line = $this->_nextTaggedLine($tag);
        $tokens = (!$dontParse) ? $this->_decodeLine($line) : $line;
        // if tag is wanted tag we might be at the end of a multiline response
        return $tag == $wantedTag;
    }

    /**
     * read all lines of response until given tag is found (last line of response)
     *
     * @param  string       $tag       the tag of your request
     * @param  bool         $dontParse if true every line is returned unparsed instead of
     *                                 the decoded tokens
     * @return null|bool|array tokens if success, false if error, null if bad request
     */
    public function readResponse($tag, $dontParse = false)
    {
        $lines = array();
        while (!$this->readLine($tokens, $tag, $dontParse)) {
            $lines[] = $tokens;
        }
        if ($dontParse) {
            // last two chars are still needed for response code
            $tokens = array(substr($tokens, 0, 2));
        }
        // last line has response code
        if ($tokens[0] == 'OK') return $lines ? $lines : true;
        if ($tokens[0] == 'NO') return false;
        return null;
    }

    /**
     * send a request
     *
     * @param  string $command your request command
     * @param  array  $tokens  additional parameters to command, use escapeString() to prepare
     * @param  string $tag     provide a tag otherwise an autogenerated is returned
     * @return null
     */
    public function sendRequest($command, $tokens = array(), &$tag = null)
    {
        if (null === $tokens) $tokens = array();
        if (!$tag) {
            $this->stag = $tag = sprintf('p%03d', $this->scount++);
        }
        $line = $tag.' '.$command;
        foreach ($tokens as $token) {
            if (is_array($token)) {
                @fputs($this->fp, $line.' '.$token[0].CRLF);
                if ($this->_diag_session) fputs($this->diag, 'C: '.$line.' '.$token[0].CRLF);
                if (!$this->_assumedNextLine('+ OK', '+')) {
                    throw new Exception('cannot send literal string');
                }
                $line = $token[1];
            } else {
                $line .= ' '.$token;
            }
        }
        @fputs($this->fp, $line.CRLF);
        if ($this->_diag_session) fputs($this->diag, 'C: '.$line.CRLF);
    }

    /**
     * send a request and get response at once
     *
     * @param  string $command   command as in sendRequest()
     * @param  array  $tokens    parameters as in sendRequest()
     * @param  bool   $dontParse if true unparsed lines are returned instead of tokens
     * @return mixed response as in readResponse()
     */
    public function talk($command, $tokens = array(), $dontParse = false)
    {
        $this->sendRequest($command, $tokens, $tag);
        return $this->readResponse($tag, $dontParse);
    }

    public function talk_auth($command, $tagMe = false, &$tag = '')
    {
        if (!is_resource($this->fp)) return false;
        if ($tagMe) {
            $tag = sprintf('p%03d', $this->scount++);
            $command = $tag.' '.$command;
        }
        if ($this->_diag_session) fputs($this->diag, 'C: '.$command.CRLF);
        @fputs($this->fp, $command.CRLF);
        return $this->_nextLine();
    }

    /**
     * Used for streaming calls, where the first initialization just sent a command and maybe
     * checked for a positive response from the server, subsequent lines get requested from
     * here.
     *
     * @return mixed  Returns the line on success, false on end of transmission.
     */
    public function talk_ml()
    {
        if (!is_resource($this->fp)) return false;
        $line = fgets($this->fp, 4096);
        if ($this->_diag_session) fputs($this->diag, 'S: '.$line);
        if (!$line || substr($line, 0, strlen($this->stag)) == $this->stag) {
            return false;
        }
        return $line;
    }

    public function append_ml($line)
    {
        if (!is_resource($this->fp)) return false;
        fwrite($this->fp, $line);
        if ($this->_diag_session) fputs($this->diag, 'C: '.$line);
    }

    /**
     * This method should be used to finalise an APPEND run and to check the
     * success of the APPEND operation.
     *
     * @return bool  TRUE on success, FALSE otherwise
     */
    public function finishAppend()
    {
        $this->append_ml(CRLF.CRLF);
        if ($this->readResponse($this->stag, true)) return true;
        return false;
    }

    /**
     * escape one or more literals i.e. for sendRequest
     *
     * @param  string|array $string the literal(s)
     * @return string|array escape literals, literals with newline are returned
     *                      as array('{size}', 'string');
     */
    public function escapeString($string)
    {
        if (func_num_args() < 2) {
            if (strpos($string, LF) !== false) {
                return array('{'.strlen($string).'}', $string);
            } else {
                return '"'.str_replace(array('\\', '"'), array('\\\\', '\\"'), $string).'"';
            }
        }
        $result = array();
        foreach (func_get_args() as $string) {
            $result[] = $this->escapeString($string);
        }
        return $result;
    }

    /**
     * escape a list with literals or lists
     *
     * @param  array $list list with literals or lists as PHP array
     * @return string escaped list for imap
     */
    public function escapeList($list)
    {
        $result = array();
        foreach ($list as $k => $v) {
            if (!is_array($v)) {
                $result[] = $v;
                continue;
            }
            $result[] = $this->escapeList($v);
        }
        return '('.implode(' ', $result).')';
    }
}
?>
Return current item: phlyMail Lite