<?php
/**
 * @package Horde_Kolab
 *
 * $Horde: framework/Kolab/Kolab/IMAP.php,v 1.12.2.12 2008/06/07 16:07:24 chuck Exp $
 */

/** We need the Kolab XML library for xml handling. */
require_once 'Horde/Kolab/XML.php';

/** We need the Horde Cache system for caching */
require_once 'Horde/Cache.php';

/** We need the Horde History System for logging */
require_once 'Horde/History.php';

/** We need the DOM library for xml handling (PHP4/5). */
require_once 'Horde/DOM.php';

/** We need the String & NLS libraries for character set conversions, etc. */
require_once 'Horde/String.php';
require_once 'Horde/NLS.php';

/** We need the Horde MIME library to deal with MIME messages, obviously
 *  :-). */
require_once 'Horde/MIME.php';
require_once 'Horde/MIME/Part.php';
require_once 'Horde/MIME/Message.php';
require_once 'Horde/MIME/Headers.php';
require_once 'Horde/MIME/Structure.php';

/**
 * The root of the Kolab annotation hierarchy, used on the various IMAP folder
 * that are used by Kolab clients.
 */
define('KOLAB_ANNOT_ROOT', '/vendor/kolab/');

/**
 * The root of the Horde annotation hierarchy on the IMAP server
 */
define('HORDE_ANNOT_ROOT', '/vendor/horde/');

/**
 * The annotation, as defined by the Kolab format spec, that is used to store
 * information about what groupware format the folder contains.
 */
define('KOLAB_ANNOT_FOLDER_TYPE', KOLAB_ANNOT_ROOT . 'folder-type');

/**
 * A Horde-specific annotation that stores the UID of the Horde share that
 * corresponds to the IMAP folder. This is used in the synchronisation process
 * of the IMAP folder list and the Horde share list.
 */
define('KOLAB_ANNOT_SHARE_UID', KOLAB_ANNOT_ROOT . 'h-share-uid');

/**
 * The X- header used to store a copy of the Kolab object type string for the
 * object type that is contained within the email message.
 */
define('KOLAB_HEADER_TYPE', 'X-Kolab-Type');

/**
 * The string used to identify the Horde/Kolab integration engine in various
 * places (e.g. in Kolab objects generated by the library).
 */
define('KOLAB_PRODUCT_ID', 'horde/kolab/1.0');

/**
 * The prefix for all Kolab groupware object mime type strings. Each object
 * type has this string in common, with a suffix of e.g. 'note', 'event', etc.
 * to indicate what the specific Kolab groupware format is.
 */
define('KOLAB_MIME_TYPE_PREFIX', 'application/x-vnd.kolab.');

/**
 * The character sequence used by a (Kolab) Cyrus IMAP server to separate lines
 * within a message.
 */
define('KOLAB_NEWLINE', "\r\n");

/**
 * The Kolab_IMAP class provides a wrapper around the Kolab IMAP
 * storage for groupware objects
 *
 * $Horde: framework/Kolab/Kolab/IMAP.php,v 1.12.2.12 2008/06/07 16:07:24 chuck Exp $
 *
 * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Stuart Binge <omicron@mighty.co.za>
 * @author  Gunnar Wrobel <wrobel@pardus.de>
 * @author  Thomas Jarosch <thomas.jarosch@intra2net.com>
 * @package Horde_Kolab
 */
class Kolab_IMAP {

    /**
     * The MIME_Message object that contains the currently loaded message. This
     * is used when updating an object, in order to preserve everything else
     * within the message that we don't know how to handle.
     *
     * @var MIME_Message
     */
    var $_message;

    /**
     * An array containing $_message's headers.
     *
     * @var array
     */
    var $_headers;

    /**
     * The IMAP message number of $this->_message.
     *
     * @var integer
     */
    var $_msg_no;

    /**
     * The full mime type string of the current Kolab object format we're
     * dealing with.
     *
     * @var string
     */
    var $_mime_type;

    /**
     * The MIME identifier of the current Kolab groupware object.
     *
     * @var string
     */
    var $_mime_id;

    /**
     * The currently selected object type
     *
     * @var string
     */
    var $_object_type;

    /**
     * An addition to the cache key in case we are operating on
     * something other than the default type
     *
     * @var string
     */
    var $_type_key;

    /**
     * A copy of the app_consts for the current app
     *
     * @var string
     */
    var $_app_consts;

    /**
     * The folder type for the current application
     *
     * @var string
     */
    var $_folder_type;

    /**
     * The the IMAP folder that corresponds to the current share.
     *
     * @var string
     */
    var $_folder;

    /**
     * The (encoded) name of the IMAP folder that corresponds to the current
     * share.
     *
     * @var string
     */
    var $_share_uid;

    /**
     * Our Net_IMAP object, used to communicate with the Cyrus server.
     *
     * @var Net_IMAP
     */
    var $_imap = null;

    /**
     * The cache of all objects in the current folder represented as
     * data hashes.
     *
     *   Internal cache structure:
     *
     *   | version
     *   |
     *   | uidvalidity
     *   |
     *   | uidnext
     *   |
     *   | objects: key is uid (GUID)
     *   | ----------- hashed object data
     *                 |----------- uid: object id (GUID)
     *   |             |----------- all fields from kolab specification
     *   |
     *   | uids   Mapping between imap uid and object uids: imap uid -> object uid
     *            Special: A value of "false" means we've seen the uid
     *                     but we deciced to ignore it in the future
     *
     * @var array
     */
    var $_cache;

    /**
     * The internal version of the cache format.
     *
     * @var int
     */
    var $_base_version = 1;

    /**
     * The version of the XML loader
     *
     * @var int
     */
    var $_loader_version = 0;

    /**
     * The version of the cache format that includes the loader version.
     *
     * @var int
     */
    var $_cache_version;

    /**
     * The current user.
     *
     * @var string
     */
    var $_owner;

    /**
     * Flag to synchronize only once per page load
     *
     * @var boolean
     */
    var $_synchronized;

    /**
     * Headers of the IMAP message currently being parsed.
     *
     * @var string
     */
    var $_raw_headers;

    /**
     * Full IMAP message currently being parsed.
     *
     * @var string
     */
    var $_raw_message;

    /**
     * MIME parsed version of the message
     *
     * @var mixed
     */
    var $_mime_message;

    /**
     * MIME parsed version of the header
     *
     * @var mixed
     */
    var $_mime_headers;

    /**
     * ID of the MIME part with the Kolab XML object
     *
     * @var int
     */
    var $_mime_part_id;

    /**
     * Do we optimize for cyrus IMAPD?
     *
     * @var boolean
     */
    var $_cache_cyrus_optimize;

    /**
     * The link to the cache
     *
     * @var boolean
     */
    var $_horde_cache;

    /**
     * Open the specified share.
     *
     * @param string $share_uid      The uid of the share
     *                               that should be opened.
     * @param int    $loader_version The version of the XML
     *                               loader
     *
     * @return mixed  True on success, a PEAR error otherwise
     */
    function open($share_uid, $app_consts, $loader_version = 0)
    {
        global $conf;

        $this->_loader_version = $loader_version;
        $this->_cache_version = ($loader_version << 8) || $this->_base_version;
        $this->_folder_type = $app_consts['folder_type'];
        $this->_app_consts = $app_consts;
        $this->_object_type = $app_consts['mime_type_suffix'];
        $this->_mime_type = KOLAB_MIME_TYPE_PREFIX . $this->_object_type;
        $this->_type_key = '';
        $this->_owner = Auth::getAuth();
        $this->_share_uid = $share_uid;
        $this->_app_consts = $app_consts;
        $folder = $this->parseFolder($share_uid);
        if (is_a($folder, 'PEAR_Error')) {
            return $folder;
        }
        $this->_folder = $folder;

        $this->_imap = &Kolab_IMAP_Connection::singleton(Kolab::getServer('imap'),
                                                         $conf['kolab']['imap']['port'], true, false);

        if (is_a($this->_imap, 'PEAR_Error')) {
            return $this->_imap;
        }
        $result = $this->_imap->connect($this->_owner,
                                        Auth::getCredential('password'));
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        // This is only necessary for the new framework.
        if ($loader_version > 0) {
            /**
             * We explicitly select the file based cache to ensure
             * that different users have access to the same cache
             * data. I am not certain this is guaranteed for the other
             * possible cache drivers.
             */
            $this->_horde_cache = &Horde_Cache::singleton('file',
                                                          array('prefix' => 'kolab_cache',
                                                                'dir' => Horde::getTempDir()));
            $this->_cache_cyrus_optimize = true;
            $this->_resetCache();
            $this->_cacheLoad();
            $result = $this->_synchronize();
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
        } else {
            // Check that the folder exists. For the new framework
            // this happens in _synchronize()
            $result = $this->_imap->select($this->_folder);
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
        }

        return true;
    }

    /**
     * Close the current share.
     */
    function close()
    {
    }

    function setObjectType($type)
    {
        if (in_array($type, $this->_app_consts['allowed_types'])) {
            $this->_object_type = $type;
            $this->_mime_type = KOLAB_MIME_TYPE_PREFIX . $this->_object_type;
            if ($type != $this->_app_consts['mime_type_suffix']) {
                $this->_type_key = '@' . $type;
            } else {
                $this->_type_key = '';
            }
            if ($this->_loader_version > 0) {
                $this->_resetCache();
                $this->_cacheLoad();
                $result = $this->_synchronize();
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }
            }
        } else {
            return PEAR::raiseError(sprintf(_("Object type %s not allowed for folder type %s!"), $type, $this->_folder_type));
        }
    }

    /**
     * Handles the horde syntax for default shares
     *
     * @param string $folder The folder name that should be parsed
     *
     * @return string The corrected IMAP folder name.
     */
    function parseFolder($folder)
    {
        // Handle default shares
        if ($folder == $this->_owner) {
            $folders = $this->listFolders();

            if ($folders) {
                foreach ($folders as $folder) {
                    if ($folder[1] == $this->_folder_type && $folder[2] == 1) {
                        return $folder[0];
                    }
                }
            }
            return PEAR::raiseError(sprintf(_("Default folder of type %s does not exist!"), $this->_folder_type));
        }
        return rawurldecode($folder);
    }

    /**
     * Returns a list of all IMAP folders (including their groupware type)
     * that the current user has acccess to.
     *
     * @return array  An array of array($foldername, $foldertype) items (empty
     *                on error).
     */
    function listFolders()
    {
        // Connect to the IMAP server
        $imap = &Kolab_IMAP_Connection::singleton(Kolab::getServer('imap'),
                                                  $GLOBALS['conf']['kolab']['imap']['port']);

        return $imap->listFolders();
    }

    /**
     * Save an object.
     *
     * @param array  $object         The array that holds the data object
     * @param string $old_object_id  The id of the object if it existed before
     *
     * @return mixed  True on success, a PEAR error otherwise
     */
    function save($object, $old_object_id = null)
    {
        $saver = &Horde_Kolab_XML::factory($this->_object_type, $this->_loader_version);
        if (is_a($saver, 'PEAR_Error')) {
            return $saver;
        }

        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $new_headers = new MIME_Headers();

        // update existing kolab object
        if ($old_object_id != null) {
            // check if object really exists
            if (!$this->objectUidExists($old_object_id)) {
                return PEAR::raiseError(sprintf(_("Old object %s does not exist."), $old_object_id));
            }

            // get IMAP uid
            $imap_uid = $this->getImapUid($old_object_id);
            if ($imap_uid === false) {
                return PEAR::raiseError(sprintf(_("Old object %s does not map to a uid."), $old_object_id));
            }

            // parse email and load XML structure
            $old_xml = $this->parseMessage($imap_uid);
            if (is_a($old_xml, 'PEAR_Error')) {
                return $old_xml;
            }

            $result = $saver->load($old_xml);
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }

            $object = array_merge($result, $object);

            // copy email header
            if (!empty($this->_mime_headers) && !$this->_mime_headers === false) {
                foreach ($this->_mime_headers as $header => $value) {
                    $new_headers->addheader($header, $value);
                }
            }
        } else {
            $this->prepareNewMessage($new_headers);
            $this->_mime_part_id = false;
        }

        $xml = $saver->save($object);
        if (is_a($xml, 'PEAR_Error')) {
            return $xml;
        }

        // Update mime part
        $part = new MIME_Part($this->_mime_type, $xml, NLS::getCharset());
        $part->setTransferEncoding("quoted-printable");
        $part->setName($this->_object_type . ".xml");

        if ($this->_mime_part_id === false) {
            $this->_mime_message->addPart($part);
        } else {
            $this->_mime_message->alterPart($this->_mime_part_id, $part);
        }

        // Update email headers
        $new_headers->addHeader('From', Auth::getAuth());
        $new_headers->addHeader('To', Auth::getAuth());
        $new_headers->addHeader('Date', date('r'));
        $new_headers->addHeader("X-Kolab-Type", $this->_mime_type);
        $new_headers->addHeader('Subject', $object["uid"]);
        $new_headers->addHeader("User-Agent", "Horde::Kolab v1.0");
        $new_headers->addMIMEHeaders($this->_mime_message);

        $msg = preg_replace("/\r\n|\n|\r/s", "\r\n",
                            $new_headers->toString() . $this->_mime_message->toString(false));

        // delete old email?
        if ($old_object_id != null) {
            $result = $this->_imap->deleteMessages($imap_uid);
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
        }

        // store new email
        $result = $this->_imap->appendMessage($msg);
        if (is_a($result, 'PEAR_Error')) {
            if ($old_object_id != null) {
                $result = $this->_imap->undeleteMessages($imap_uid);
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }
            }
            return $result;
        }

        // remove deleted object
        if ($old_object_id != null) {
            $result = $this->_imap->expunge();
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
        }
        $result = $this->_synchronize();
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }
        return true;
    }

    /**
     * Get an IMAP message and retrieve the Kolab XML object
     *
     * @param int     $imap_uid       The message to retrieve
     * @param boolean $parse_headers  Should the heades be MIME parsed?
     *
     * @return string  $text with all newlines replaced by KOLAB_NEWLINE.
     */
    function parseMessage($imap_uid, $parse_headers = true)
    {
        $error = false;

        $result = $this->_imap->getMessageHeader($imap_uid);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }
        $this->_raw_headers = $result;

        $body = $this->_imap->getMessageBody($imap_uid);
        if (is_a($body, 'PEAR_Error')) {
            return $body;
        }

        $this->_raw_message = $this->_raw_headers . $body;

        $this->_mime_message = &MIME_Structure::parseTextMIMEMessage($this->_raw_message);
        $parts = $this->_mime_message->contentTypeMap();

        // Read in a Kolab event object, if one exists
        $this->_mime_part_id = array_search($this->_mime_type, $parts);
        if ($this->_mime_part_id !== false) {
            if ($parse_headers) {
                $this->_mime_headers = MIME_Structure::parseMIMEHeaders($this->_raw_headers);
            }

            $part = $this->_mime_message->getPart($this->_mime_part_id);
            $part->transferDecodeContents();

            $xml = $part->getContents();

            return $xml;
        }

        // Nothing found -> clear MIME vars
        $this->_raw_headers = false;
        $this->_raw_message = false;
        $this->_mime_headers = false;
        $this->_mime_message = false;
        $this->_mime_part_id = false;

        return false;
    }

    /**
     * Prepares a new kolab Groupeware message.
     *
     * @return string The MIME message
     */
    function prepareNewMessage()
    {
        // New email code
        $this->_mime_message = new MIME_Message();

        $kolab_text = sprintf(_("This is a Kolab Groupware object. To view this object you will need an email client that understands the Kolab Groupware format. For a list of such email clients please visit %s"),
                                'http://www.kolab.org/kolab2-clients.html');

        $part = new MIME_Part('text/plain', String::wrap($kolab_text, 76, "\r\n", NLS::getCharset()),
                                NLS::getCharset());
        $part->setTransferEncoding('quoted-printable');
        $this->_mime_message->addPart($part);
    }

    /**
     * Move the specified message from the current folder into a new
     * folder
     *
     * @param  string $object_uid Id of the message to be deleted.
     * @param  string $new_folder Name of the target folder.
     *
     * @return mixed True is successful, false if the message does not
     * exist, a PEAR error otherwise.
     */
    function move($object_uid, $new_folder)
    {
        if (!$this->objectUidExists($object_uid)) {
            return false;
        }

        // Find imap uid
        $imap_uid = $this->getImapUid($object_uid);
        if ($imap_uid === false) {
            return false;
        }

        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        // This assumes that both folders are owned by $this->_owner.
        // This needs to be checked.
        $new_folder = $this->parseFolder($new_folder);
        if (is_a($new_folder, 'PEAR_Error')) {
            return $new_folder;
        }

        $result = $this->_imap->moveMessage($imap_uid, $new_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $this->_imap->expunge();
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        unset($this->_cache['objects'][$object_uid]);
        unset($this->_cache['uids'][$imap_uid]);

        return true;
    }

    /**
     * Delete the specified message from the current folder
     *
     * @param  string $object_uid Id of the message to be deleted.
     *
     * @return mixed True is successful, false if the message does not
     * exist, a PEAR error otherwise.
     */
    function delete($object_uid)
    {
        if (!$this->objectUidExists($object_uid)) {
            return false;
        }

        // find imap uid
        $imap_uid = $this->getImapUid($object_uid);
        if ($imap_uid === false) {
            return false;
        }

        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $this->_imap->deleteMessages($imap_uid);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $this->_imap->expunge();
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        unset($this->_cache['objects'][$object_uid]);
        unset($this->_cache['uids'][$imap_uid]);

        return true;
    }

    /**
     * Delete all messages from the current folder
     *
     * @return mixed  True is successful, a PEAR error otherwise.
     */
    function deleteAll()
    {
        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        foreach ($this->_cache["uids"] as $imap_uid => $object_uid) {
            if ($object_uid === false)
                continue;

            $result = $this->_imap->deleteMessages($imap_uid);
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }

            unset($this->_cache['objects'][$object_uid]);
            unset($this->_cache['uids'][$imap_uid]);

        }

        $result = $this->_imap->expunge();
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        return true;
    }

    /**
     * Synchronize the data cache for the current folder.
     */
    function _synchronize()
    {
        // Select the folder to update uidnext
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $status = $this->_imap->status();
        if (is_a($status, 'PEAR_Error')) {
            return $status;
        }

        $new_uids = $this->_imap->getUids();
        if (is_a($new_uids, 'PEAR_Error')) {
            return $new_uids;
        }

        $changes = $this->folderChanged($status['uidvalidity'], $status['uidnext'], array_keys($this->_cache['uids']), $new_uids);
        if ($changes) {
            $modified = array();

            $recent_uids = array_diff($new_uids, array_keys($this->_cache['uids']));

            $loader = &Horde_Kolab_XML::factory($this->_object_type, $this->_loader_version);
            if (is_a($loader, 'PEAR_Error')) {
                return $loader;
            }

            $count = 0;
            foreach ($recent_uids as $imap_uid) {
                // get message
                $xmltext = $this->parseMessage($imap_uid, false);
                if (is_a($xmltext, 'PEAR_Error')) {
                    Horde::logMessage($xmltext, __FILE__, __LINE__, PEAR_LOG_WARNING);
                    continue;
                }

                if ($xmltext) {
                    $object = $loader->load($xmltext);
                    if (is_a($object, 'PEAR_Error')) {
                        $this->_ignoreUid($imap_uid);
                        $object->addUserInfo('IMAP UID: ' . $imap_uid);
                        Horde::logMessage($object, __FILE__, __LINE__, PEAR_LOG_WARNING);
                        continue;
                    }
                } else {
                    $object = false;
                }

                if ($object !== false) {
                    $this->_storeInCache($imap_uid, $object['uid'], $object);
                    $mod_ts = time();
                    if (is_array($changes) && in_array($object['uid'], $changes)) {
                        $this->updateHistory($object['uid'], $mod_ts, 'modify');
                        $modified[] = $object['uid'];
                    } else {
                        $this->updateHistory($object['uid'], $mod_ts, 'add');
                    }
                } else {
                    $this->_ignoreUid($imap_uid);
                }

                // write out cache once in a while so if the browser times out
                // we don't have to start from the beginning.
                if ($count > 500) {
                    $count = 0;
                    $this->_cacheStore();
                }
                $count++;
            }

            $this->_cacheStore();

            if (is_array($changes)) {
                $deleted = array_diff($changes, $modified);
                foreach ($deleted as $deleted_oid) {
                    $this->updateHistory($deleted_oid, time(), 'delete');
                }
            }
        }
    }

    function updateHistory($object_uid, $mod_ts, $action)
    {
        /* Log the action on this item in the history log. */
        $history = &Horde_History::singleton();

        $history_id = $this->_app_consts['application'] . ':'
            . $this->_share_uid . ':' . $object_uid;
        $history->log($history_id, array('action' => $action, 'ts' => $mod_ts), true);
    }


    /**
     * Check if the folder has changed and the cache needs to be updated.
     *
     * @param string $uidvalidity uid validity of the IMAP folder
     * @param string $uidnext     next uid for the IMAP folder
     * @param array  $old_uids    Old list of uids in the folder
     * @param array  $new_uids    New list of uids in the folder
     *
     * @return mixed True or an array of deleted IMAP uids if the
     *               folder changed and false otherwise
     */
    function folderChanged($uidvalidity, $uidnext, &$old_uids, &$new_uids)
    {
        $changed = false;
        $reset_done = false;

        // uidvalidity changed?
        if ($uidvalidity != $this->_cache['uidvalidity']) {
            $this->_resetCache();
            $reset_done = true;
        }

        // uidnext changed?
        if ($uidnext != $this->_cache['uidnext']) {
            $changed = true;
        }

        $this->_cache['uidvalidity'] = $uidvalidity;
        $this->_cache['uidnext'] = $uidnext;

        if ($reset_done) {
            return true;
        }

        // Speed optimization: if uidnext and uidvalidity didn't change
        // and count(old_uids) == count(new_uids), the folder didn't change.
        if ($changed || count($old_uids) != count ($new_uids)) {
            // remove deleted messages from cache
            $delete_uids = array_diff($old_uids, $new_uids);
            $deleted_oids = array();
            foreach ($delete_uids as $delete_uid) {
                $object_uid = $this->_cache['uids'][$delete_uid];
                if ($object_uid !== false) {
                    unset($this->_cache['objects'][$object_uid]);
                    $deleted_oids[] = $object_uid;
                }
                unset($this->_cache['uids'][$delete_uid]);
            }
            if (!empty($deleted_oids)) {
                $changed = $deleted_oids;
            } else {
                $changed = true;
            }
        }
        return $changed;
    }

    /**
     * Initialize the cache structure.
     */
    function _resetCache()
    {
        $this->_cache = array();

        $this->_cache['version'] = $this->_cache_version;
        $this->_cache['uidvalidity'] = -1;
        $this->_cache['uidnext'] = -1;
        $this->_cache['objects'] = array();
        $this->_cache['uids'] = array();
        $this->_cache['key'] = $this->computeKey();
    }

    /**
     * Return a unique key for the this folder
     *
     * @return string A key that represents the current folder
     */
    function computeKey()
    {
        if ($this->_cache_cyrus_optimize) {
            $search_prefix = 'INBOX/';

            $pos = strpos($this->_folder, $search_prefix);
            if ($pos !== false && $pos == 0) {
                $key = 'user/' . Auth::getBareAuth() . '/'
                           . substr($this->_folder,
                                    strlen($search_prefix))
                           . $this->_type_key;
            } else {
                $key = $this->_folder;
            }
        } else {
            $key = $this->_owner . '/' . $this->_folder;
        }
        return $key;
    }

    /**
     * Retrieve the folder data from the cache
     *
     * @return boolean true on success
     */
    function _cacheLoad()
    {
        $cache = $this->_horde_cache->get($this->_cache['key'], 0);

        if (!$cache) {
            return true;
        }

        $this->_cache = unserialize($cache);

        // Delete disc cache if it's from an old version
        if ($this->_cache['version'] != $this->_cache_version) {
            $this->_horde_cache->expire($this->_cache['key']);
            $this->_resetCache();
        }

        return true;
    }

    /**
     * Store the folder data in the cache
     *
     * @param boolean $force Should storage of the cache be forced?
     *
     * @return boolean true on success
     */
    function _cacheStore()
    {
        $this->_horde_cache->set($this->_cache['key'],
                                 serialize($this->_cache));
    }

    /**
     * Store an object in the cache.
     *
     * @param int     $imap_uid       The message ID
     * @param string  $object_uid     The object ID
     * @param array   $object         The object data
     */
    function _storeInCache(&$imap_uid, &$object_uid, &$object)
    {
        $this->_cache['uids'][$imap_uid] = $object_uid;
        $this->_cache['objects'][$object_uid] = $object;
    }

    /**
     * Mark the IMAP message ID as invalid (no Kolab XML object).
     *
     * @param int     $imap_uid       The message ID
     */
    function _ignoreUid(&$imap_uid)
    {
        $this->_cache['uids'][$imap_uid] = false;
    }

    /**
     * Generate a unique object id
     *
     * @return string  The unique id
     */
    function generateUID()
    {
        do {
            $key = md5(uniqid(mt_rand(), true));
        } while($this->objectUidExists($key));

        return $key;
    }

    /**
     * Return the IMAP uid for the given object id
     *
     * @param string   $object_id      The object ID
     *
     * @return int  The IMAP id
     */
    function getImapUid($object_uid)
    {
        $imap_uid = array_search($object_uid, $this->_cache['uids']);
        if ($imap_uid === false) {
            return false;
        }

        return $imap_uid;
    }

    /**
     * Test if the IMAP uid exists
     *
     * @param int   $uid      The IMAP uid
     *
     * @return boolean  True if the uid exists
     */
    function imapUidExists($uid)
    {
        return array_key_exists($uid, $this->_cache['uids']);
    }

    /**
     * Check if the given id exists
     *
     * @param string $uid  The object id
     *
     * @return boolean  True if the id was found, false otherwise
     */
    function objectUidExists($uid)
    {
        return array_key_exists($uid, $this->_cache['objects']);
    }

    /**
     * Return the specified object
     *
     * @param string     $object_id       The object id
     *
     * @return mixed The object data as array or a PEAR error if the
     * object is missing from the cache.
     */
    function getObject($object_id)
    {
        if (!$this->objectUidExists($object_id)) {
            return PEAR::raiseError(sprintf(_("Kolab cache: Object uid %s does not exist in the cache!"), $object_id));
        }
        return $this->_cache['objects'][$object_id];
    }

    /**
     * Retrieve all object ids in the current folder
     *
     * @return array  The object ids
     */
    function getObjectIds()
    {
        return array_keys($this->_cache['objects']);
    }

    /**
     * Retrieve all objects in the current folder
     *
     * @return array  All object data arrays
     */
    function getObjects()
    {
        return array_values($this->_cache['objects']);
    }

    /**
     * Retrieve all objects in the current folder as an array
     *
     * @return array  The object data array
     */
    function getObjectArray()
    {
        return $this->_cache['objects'];
    }

    /**
     * List the objects in the current share.
     *
     * @deprecated
     *
     * @return mixed  false if there are no objects, a list of message
     *                ids or a PEAR error.
     */
    function listObjects()
    {
        if (empty($this->_imap)) {
            return false;
        }

        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $this->_imap->searchHeaders(KOLAB_HEADER_TYPE, $this->_mime_type);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }
        return $result;
    }

    /**
     * List the objects in the specified folder.
     *
     * @deprecated
     *
     * @param string $folder  The folder to search.
     *
     * @return mixed  false if there are no objects, a list of message
     *                ids otherwise.
     */
    function listObjectsInFolder($folder)
    {
        // Connect to the IMAP server
        $imap = &Kolab_IMAP_Connection::singleton(Kolab::getServer('imap'),
                                                  $GLOBALS['conf']['kolab']['imap']['port'], false);

        // Login using the current Horde credentials
        $result = $imap->connect(Auth::getAuth(), Auth::getCredential('password'));
        if (is_a($result, 'PEAR_Error')) {
            Horde::logMessage('Unable to authenticate with the Kolab IMAP server', __FILE__, __LINE__, PEAR_LOG_ERR);
            return false;
        }

        // Select mailbox to search in
        $result = $imap->select($folder);
        if (is_a($result, 'PEAR_Error')) {
            return false;
        }

        $result = $imap->searchHeaders(KOLAB_HEADER_TYPE, $this->_mime_type);
        if (!isset($result)) {
            $result = array();
        }
        return $result;
    }

    /**
     * Find the object with the given UID in the current share.
     *
     * @deprecated
     *
     * @param string $uid  The UID of the object.
     *
     * @return mixed  false if there is no such object
     */
    function findObject($uid)
    {
        if (empty($this->_imap)) {
            return false;
        }

        if (empty($uid) || $uid == "") {
            return PEAR::raiseError("Cannot search for an empty uid.");
        }

        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $this->_imap->search("SUBJECT \"$uid\"");
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        if (empty($result)) {
            return PEAR::raiseError(sprintf(_("No message corresponds to object %s"), $uid));
        }

        return $result[0];
    }

    /**
     * Load the object with the given UID into $this->_xml
     *
     * @deprecated
     *
     * @param string  $uid      The UID of the object.
     * @param boolean $is_msgno Indicate if $uid holds an
     *                          IMAP message number
     *
     * @return mixed  false if there is no such object, a PEAR error if
     *                the object could not be loaded. Otherwise the xml
     *                document will be returned
     */
    function loadObject($uid, $is_msgno = false)
    {
        if (empty($this->_imap)) {
            $object = false;
            return $object;
        }

        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        if ($is_msgno === false) {
            $uid = $this->findObject($uid);
            if (is_a($uid, 'PEAR_Error')) {
                return $uid;
            }
        }

        $header = $this->_imap->getMessageHeader($uid);
        if (is_a($header, 'PEAR_Error')) {
            return $header;
        }
        $this->_headers = MIME_Structure::parseMIMEHeaders($header);

        $message_text = $this->_imap->getMessage($uid);
        if (is_a($message_text, 'PEAR_Error')) {
            return $message_text;
        }

        if (is_array($message_text)) {
            $message_text = array_shift($message_text);
        }

        $this->_msg_no = $uid;
        $this->_message = &MIME_Structure::parseTextMIMEMessage($message_text);

        $parts = $this->_message->contentTypeMap();
        $this->_mime_id = array_search($this->_mime_type, $parts);
        if ($this->_mime_id !== false) {
            $part = $this->_message->getPart($this->_mime_id);
            $text = $part->transferDecode();
        } else {
            return PEAR::raiseError(sprintf(_("Horde/Kolab: No object of type %s found in message %s"), $this->_mime_type, $uid));
        }

        return Horde_DOM_Document::factory(array('xml' => $text));
    }

    /**
     * Create the object with UID in the current share
     *
     * @deprecated
     *
     * @param string  $uid      The UID of the object.
     *
     * @return mixed  false if there is no open share, a PEAR error if
     *                the object could not be created. Otherwise the xml
     *                document will be returned
     */
    function newObject($uid)
    {
        if (empty($this->_imap)) {
            $object = false;
            return $object;
        }

        $this->_msg_no = -1;
        $this->_message = new MIME_Message();

        $kolab_text = sprintf(_("This is a Kolab Groupware object. To view this object you will need an email client that understands the Kolab Groupware format. For a list of such email clients please visit %s"),
                              'http://www.kolab.org/kolab2-clients.html');
        $part = new MIME_Part('text/plain',
                              String::wrap($kolab_text, 76, KOLAB_NEWLINE, NLS::getCharset()),
                              NLS::getCharset());
        $part->setTransferEncoding('quoted-printable');
        $this->_message->addPart($part);

        $part = new MIME_Part($this->_mime_type, '', NLS::getCharset());
        $part->setTransferEncoding('quoted-printable');
        $this->_message->addPart($part);

        $parts = $this->_message->contentTypeMap();
        $this->_mime_id = array_search($this->_mime_type, $parts);
        if ($this->_mime_id === false) {
            return PEAR::raiseError(sprintf(_("Horde/Kolab: Unable to retrieve MIME ID for the part of type %s"), $this->_mime_type));
        }

        $headers = new MIME_Headers();
        $headers->addHeader('From', Auth::getAuth());
        $headers->addHeader('To', Auth::getAuth());
        $headers->addHeader('Subject', $uid);
        $headers->addHeader('User-Agent', KOLAB_PRODUCT_ID);
        $headers->addHeader('Reply-To', '');
        $headers->addHeader('Date', date('r'));
        $headers->addHeader(KOLAB_HEADER_TYPE, $this->_mime_type);
        $headers->addMIMEHeaders($this->_message);

        $this->_headers = $headers->toArray();

        $xml = '<?xml version="1.0" encoding="UTF-8"?>' .
            '<' . $this->_object_type . ' version="1.0">' .
            '<uid>' . $uid . '</uid>' .
            '<body></body>' .
            '<categories></categories>' .
            '<creation-date>' . Kolab::encodeDateTime() . '</creation-date>' .
            '<sensitivity>public</sensitivity>' .
            '</' . $this->_object_type . '>';

        return Horde_DOM_Document::factory(array('xml' => $xml));
    }

    /**
     * Save the current object.
     *
     * @deprecated
     *
     * @return mixed  false if there is no open share, a PEAR error if
     *                the object could not be saved. True otherwise
     */
    function saveObject($xml, $uid)
    {
        if (empty($this->_imap)) {
            return false;
        }

        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $part = new MIME_Part($this->_mime_type, $xml->dump_mem(true),
                              NLS::getCharset());
        $part->setTransferEncoding('quoted-printable');
        $this->_message->alterPart($this->_mime_id, $part);

        if ($this->_msg_no != -1) {
            $this->removeObjects($this->_msg_no, true);
        }

        $headers = new MIME_Headers();
        foreach ($this->_headers as $key => $val) {
            $headers->addHeader($key, $val);
        }

        $message = Kolab_IMAP::kolabNewlines($headers->toString() .
                                             $this->_message->toString(false));

        $result = $this->_imap->appendMessage($message);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $this->_msg_no = $this->findObject($uid);
        if (is_a($this->_msg_no, 'PEAR_Error')) {
            return $this->_msg_no;
        }

        return true;
    }

    /** Not certain that this is still a valid TODO entry. Need to check
     * -- Gunnar, 06.03.2007
     *
     * TODO: write a function that is able to look through all folders
     * of the type we're interested in, looking for an object with a
     * specific UID. This is needed for the getByGUID() functions in
     * the various application drivers.
     */

    /**
     * Move the object with the given UID from the current share into
     * the specified new share.
     *
     * @deprecated
     *
     * @param string  $uid       The UID of the object.
     * @param boolean $new_share The share the object should be moved to.
     *
     * @return mixed  false if there is no current share, a PEAR error if
     *                the object could not be moved. True otherwise.
     */
    function moveObject($uid, $new_share)
    {
        if (empty($this->_imap)) {
            return false;
        }

        // No IMAP folder select needed as findObject
        // does it for us

        $new_share = rawurldecode($new_share);
        $new_share = $this->parseFolder($new_share);
        if (is_a($new_share, 'PEAR_Error')) {
            return $new_share;
        }

        $msg_no = $this->findObject($uid);
        if (is_a($msg_no, 'PEAR_Error')) {
            return $msg_no;
        }

        $result = $this->_imap->copyMessage($msg_no, $new_share);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $this->_imap->deleteMessages($msg_no);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        return $this->_imap->expunge();
    }

    /**
     * Remove the specified objects from the current share.
     *
     * @deprecated
     *
     * @param string  $objects  The UIDs (or maessage numbers)
     *                          of the objects to be deleted.
     * @param boolean $is_msgno Indicate if $objects holds
     *                          IMAP message numbers
     *
     * @return mixed  false if there is no IMAP connection, a PEAR
     *                error if the objects could not be removed. True
     *                if the call succeeded.
     */
    function removeObjects($objects, $is_msgno = false)
    {
        if (empty($this->_imap)) {
            return false;
        }

        if (!is_array($objects)) {
            $objects = array($objects);
        }

        if ($is_msgno === false) {
            $new_objects = array();

            foreach ($objects as $object) {
                $result = $this->findObject($object);
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }

                $new_objects[] = $result;
            }

            $objects = $new_objects;
        }

        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $this->_imap->deleteMessages($objects);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $this->_imap->expunge();
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        return true;
    }

    /**
     * Remove all objects from the current share.
     *
     * @deprecated
     *
     * @return mixed  false if there is no IMAP connection, a PEAR
     *                error if the objects could not be removed. True
     *                if the call succeeded.
     */
    function removeAllObjects()
    {
        if (empty($this->_imap)) {
            return false;
        }

        // Select folder
        $result = $this->_imap->select($this->_folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $messages = $this->listObjects();

        if ($messages) {
            $result = $this->_imap->deleteMessages($messages);
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
        }

        return true;
    }

    /**
     * Returns the groupware type of the given IMAP folder.
     *
     * @deprecated
     *
     * @param object $mailbox  The mailbox of interest.
     *
     * @return mixed  A string indicating the groupware type of $mailbox or
     *                boolean "false" on error.
     */
    function getMailboxType($mailbox)
    {
        $type = false;

        // Connect to the IMAP server
        $imap = &Kolab_IMAP_Connection::singleton(Kolab::getServer('imap'),
                                                  $GLOBALS['conf']['kolab']['imap']['port']);

        // Login using the current Horde credentials
        $result = $imap->connect(Auth::getAuth(),
                               Auth::getCredential('password'));
        if (is_a($result, 'PEAR_Error')) {
            Horde::logMessage('Unable to authenticate with the Kolab IMAP server', __FILE__, __LINE__, PEAR_LOG_ERR);
            return $type;
        }

        // Obtain the groupware annotation of $mailbox
        $annotation = $imap->getAnnotation(KOLAB_ANNOT_FOLDER_TYPE,
                                           'value.shared',
                                           $mailbox);
        if (!is_a($annotation, 'PEAR_Error') && !empty($annotation)) {
            $type = explode('.', $annotation);
            if ((!empty($type[1]) && $type[1] == 'default') ||
                (empty($type[1]))) {
                $type = $type[0];
            }
        }

        return $type;
    }

    /**
     * Converts all newlines (in DOS, MAC & UNIX format) in the specified text
     * to Kolab (Cyrus) format.
     *
     * @param string $text  The text to convert.
     *
     * @return string  $text with all newlines replaced by KOLAB_NEWLINE.
     */
    function kolabNewlines($text)
    {
        return preg_replace("/\r\n|\n|\r/s", KOLAB_NEWLINE, $text);
    }

    /**
     * Find the object using the given criteria in the current share.
     *
     * @deprecated
     *
     * @param string $criteria  The search criteria.
     *
     * @return mixed  false if no object can be found
     */
    function findObjects($criteria)
    {
        if (empty($this->_imap)) {
            return false;
        }

        return $this->_imap->search($criteria);
    }

    /**
     * Return the MIME type of the message we are currently dealing with.
     *
     * @deprecated
     *
     * @return string  The MIME type of the message we are currently
     *                 dealing with.
     */
    function getMimeType()
    {
        return $this->_mime_type;
    }

}

/**
 * The Kolab_IMAP_Connection class provides a wrapper around two
 * different Kolab IMAP connection types.
 *
 * $Horde: framework/Kolab/Kolab/IMAP.php,v 1.12.2.12 2008/06/07 16:07:24 chuck Exp $
 *
 * Copyright 2007-2008 The Horde Project (http://www.horde.org/)
 *
 * See the enclosed file COPYING for license information (LGPL). If you
 * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
 *
 * @author  Gunnar Wrobel <wrobel@pardus.de>
 * @author  Thomas Jarosch <thomas.jarosch@intra2net.com>
 * @package Horde_Kolab
 */
class Kolab_IMAP_Connection {

    /**
     * IMAP server to connect to
     *
     * @var string
     */
    var $_server;

    /**
     * IMAP server port to connect to
     *
     * @var int
     */
    var $_port;

    /**
     * IMAP connection
     *
     * @var mixed
     */
    var $_imap;

    /**
     * Should the IMAP connection have its own cache?
     *
     * @var boolean
     */
    var $_cached;

    /**
     * Connection reuse detection signature
     *
     * @var string
     */
    var $_reuse_detection;

    /**
     * Constructor
     *
     * @param string     $server   Server to connect to
     * @param int        $port     Port to connect to
     */
    function Kolab_IMAP_Connection($server, $port, $cached = true)
    {
        $this->_server = $server;
        $this->_port   = $port;
        $this->_cached = $cached;
    }

    /**
     * Attempts to return a reference to a concrete Kolab_IMAP_Connection instance.
     * It will only create a new instance if no Kolab_IMAP_Connection instance
     * exists.
     *
     * @static
     *
     * @param string     $server                Server name
     * @param int        $port                  Port
     * @param boolean    $annotation_required   Do we actually need
     *                                          the annotation calls?
     * @param boolean    $cached                Should the IMAP connection
     *                                          have its own cache?
     *
     * @return Kolab_IMAP_Connection The concrete reference or false on error
     */
    function &singleton($server, $port, $annotation_required = true,
                        $cached = true)
    {
        static $instances = array();

        /**
         * There are Kolab specific PHP functions available that make
         * the IMAP access more efficient. If these are detected, or
         * if they are not required for the current operation, the
         * PHP IMAP implementation should be used.
         *
         * The c-client Kolab driver implements IMAP caching so is
         * preferable whenever possible.
         */
        if ($annotation_required) {
            if (function_exists('imap_status_current')
                && function_exists('imap_getannotation')) {
                $driver = 'cclient';
            } else {
                $driver = 'pear';
            }
        } else {
            $driver = 'cclient';
        }

        if (!empty($GLOBALS['KOLAB_TESTING'])) {
            $driver = 'test';
        }

        $signature = "$server|$port|$driver|$cached";
        if (!isset($instances[$signature])) {
            $instances[$signature] = &Kolab_IMAP_Connection::factory($server, $port, $driver, $cached);
        }

        return $instances[$signature];
    }

    /**
     * Attempts to return a concrete Kolab_IMAP_Connection instance
     * based on the available PHP functionality.
     *
     * @param string     $server                Server name
     * @param int        $port                  Port
     * @param string     $driver                Which driver should we use?
     * @param boolean    $cached                Should the IMAP connection
     *                                          have its own cache?
     *
     * @return Kolab_IMAP_Connection The newly created concrete
     * Kolab_IMAP_Connection instance
     */
    function &factory($server, $port, $driver = 'cclient',
                      $cached = true)
    {
        @include_once dirname(__FILE__) . '/IMAP/' . $driver . '.php';

        $class = 'Kolab_IMAP_Connection_' . $driver;
        if (class_exists($class)) {
            $driver = &new $class($server, $port, $cached);
        } else {
            return PEAR::raiseError(sprintf(_("Failed to load Kolab IMAP driver %s"), $driver));
        }

        return $driver;
    }

    /**
     * Returns a list of all IMAP folders (including their groupware type)
     * that the current user has acccess to.
     *
     * @return array  An array of array($foldername, $foldertype) items (empty
     *                on error).
     */
    function listFolders()
    {
        require_once 'Horde/SessionObjects.php';

        static $folders = array();

        if (empty($folders)) {
            $session = &Horde_SessionObjects::singleton();
            $folders = $session->query('horde_kolab_imaplist');
        }

        if (!empty($folders)) {
            return $folders;
        }

        // Login using the current Horde credentials
        $result = $this->connect(Auth::getAuth(),
                                 Auth::getCredential('password'));
        if (is_a($result, 'PEAR_Error')) {
            Horde::logMessage('Unable to authenticate with the Kolab IMAP server', __FILE__, __LINE__, PEAR_LOG_ERR);
            return $folders;
        }

        // Obtain a list of all folders the current user has access to
        $folder_list = $this->getMailboxes();
        if (is_a($folder_list, 'PEAR_Error')) {
            Horde::logMessage('Unable to obtain IMAP folder list', __FILE__, __LINE__, PEAR_LOG_ERR);
            return $folders;
        }

        // Iterate over all the folders obtaining their groupware types
        foreach ($folder_list as $folder) {
            $foldertype = 'mail';
            $default = false;
            $subtype = '';

            $annotation = $this->getAnnotation(KOLAB_ANNOT_FOLDER_TYPE,
                                               'value.shared',
                                               $folder);
            if (!is_a($annotation, 'PEAR_Error') && !empty($annotation)) {
                $type = explode('.', $annotation);
                if (!empty($type[0])) {
                    $foldertype = $type[0];
                    if (!empty($type[1])) {
                        $subtype = $type[1];
                        if ($type[1] == 'default') {
                            $default = true;
                        }
                    }
                }
            }

            $folders[] = array($folder, $foldertype, $default, $subtype);
        }

        $session = &Horde_SessionObjects::singleton();
        $session->overwrite('horde_kolab_imaplist', $folders, false);

        return $folders;
    }

    /**
     * Resets the list cache
     */
    function resetListCache()
    {
        require_once 'Horde/SessionObjects.php';
        $session = &Horde_SessionObjects::singleton();
        $session->overwrite('horde_kolab_imaplist', false, false);
    }

}
