<?php

/** We require the base Horde library. */
require_once 'Horde.php';
require_once 'Horde/Share.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 Horde_Kolab library requires version >= 1.0.3 of Net_IMAP (i.e. a version
 * that includes support for the ANNOTATEMORE IMAP extension). The latest
 * version of Net_IMAP can be obtained from http://pear.php.net/get/Net_IMAP
 */
require_once 'Net/IMAP.php';

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

/**
 * The character used to delimit subfolders within a (Kolab) Cyrus folder
 * hierarchy.
 */
define('KOLAB_FOLDER_DELIM', '/');

/**
 * 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 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');

/**
 * An index into the $_SESSION array which is used to cache what application
 * was last synchronised; this prevents us from continuously re-synchronising
 * on every page load within a single application.
 */
define('KOLAB_LAST_SYNCHED_APP', 'kolab_last_synched_app');

/**
 * An index into the $_SESSION array which is used to cache a mapping of share
 * UIDs to IMAP folders; this is used by the various application drivers when
 * opening a specific share, in order to know what the corresponding IMAP
 * folder that must be opened is.
 */
define('KOLAB_SHARE_MAP', 'kolab_share_map');

/**
 * 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 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 Horde_Kolab library is both an object used by application drivers to
 * communicate with a Kolab server, as well as a utility library providing
 * several functions to help in the IMAP folder <-> Horde Share synchronisation
 * process.
 *
 * $Horde: framework/Kolab/Kolab.php,v 1.26.10.7 2005/09/21 12:05:12 jan Exp $
 *
 * Copyright 2004-2005 Horde Project (http://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>
 * @package Horde_Kolab
 */
class Kolab {

// Class interface

    /**
     * The current application that this Kolab object instance is catering to.
     *
     * @var string
     */
    var $_app;

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

    /**
     * A cached version of Kolab::getAppConsts($this->_app) that is used in
     * several of the Kolab:: functions.
     *
     * @var array
     */
    var $_app_consts;

    /**
     * A shortcut to $this->_app_consts['mime_type_suffix']
     *
     * @var string
     */
    var $_object_type;

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

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

    /**
     * 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 MIME identifier of the Kolab groupware object within $this->_message.
     *
     * @var string
     */
    var $_mime_id;

    /**
     * The (Kolab) UID of $this->_message.
     *
     * @var string
     */
    var $_uid;

    /**
     * The DomDocument object that contains the XML DOM tree of the currently
     * loaded groupware object. We cache this here to ensure preservation of
     * unknown fields when re-saving the object.
     *
     * @var DomDocument
     */
    var $_xml;

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

    function getUID()
    {
        return $this->_uid;
    }

    function getMimeType()
    {
        return $this->_mime_type;
    }

    function Kolab()
    {
        global $registry;

        $this->_app = $registry->getApp();
    }

    function open($share_uid)
    {
        global $conf;

        if (!Util::extensionExists('domxml')) {
            return PEAR::raiseError(_("Horde/Kolab: The integration engine requires the 'domxml' PHP extension"));
        }

        $this->_app_consts = Kolab::getAppConsts($this->_app);
        if (is_a($this->_app_consts, 'PEAR_Error')) {
            return $this->_app_consts;
        }

        $this->_object_type = $this->_app_consts['mime_type_suffix'];
        $this->_mime_type = KOLAB_MIME_TYPE_PREFIX . $this->_object_type;

        $share_map = Kolab::getShareMap();
        if (empty($share_map[$share_uid])) {
            return false;
        }

        $this->_folder = $share_map[$share_uid];

        $this->_imap = &new Net_IMAP($conf['kolab']['imap']['server'], $conf['kolab']['imap']['port']);
        if (is_a($this->_imap, 'PEAR_Error')) {
            return $this->_imap;
        }

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

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

        return true;
    }

    function close()
    {
        $this->_imap->disconnect();
        $this->_imap = null;
    }

    function findObject($uid)
    {
        if (empty($this->_imap)) {
            return false;
        }

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

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

        return $result[0];
    }

    function listObjects()
    {
        if (empty($this->_imap)) {
            return false;
        }

        return $this->_imap->search('HEADER "' . KOLAB_HEADER_TYPE . '" "' . $this->_mime_type . '"');
    }

    function findObjects($criteria)
    {
        if (empty($this->_imap)) {
            return false;
        }

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

    // 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.

    function moveObject($uid, $new_share)
    {
        if (empty($this->_imap)) {
            return false;
        }

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

        $share_map = Kolab::getShareMap();
        if (empty($share_map[$new_share])) {
            return PEAR::raiseError(sprintf(_("Horde/Kolab: Unknown share \"%s\""), $new_share));
        }

        $new_folder = $share_map[$new_share];

        return $this->_imap->copyMessages($new_folder, $msg_no);
    }

    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;
        }

        $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;
    }

    function removeAllObjects()
    {
        if (empty($this->_imap)) {
            return false;
        }

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

        return true;
    }

    function &loadObject($uid, $is_msgno = false)
    {
        if (empty($this->_imap)) {
            return false;
        }

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

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

        $message_text = $this->_imap->getMessages($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();
        if (($this->_mime_id = array_search($this->_mime_type, $parts)) !== 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));
        }

        $this->_xml = @domxml_open_mem($text, DOMXML_LOAD_PARSING |
            DOMXML_LOAD_COMPLETE_ATTRS | DOMXML_LOAD_SUBSTITUTE_ENTITIES |
            DOMXML_LOAD_DONT_KEEP_BLANKS, $error);
        if (!empty($error)) {
            return PEAR::raiseError(sprintf(_("Horde/Kolab: Invalid %s XML data encountered in message %s"), $this->_object_type, $uid));
        }

        $this->_uid = $this->getVal('uid');
        return $this->_xml->document_element();
    }

    function &newObject($uid)
    {
        if (empty($this->_imap)) {
            return false;
        }

        $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();
        if (($this->_mime_id = array_search($this->_mime_type, $parts)) === 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();

        $this->_xml = domxml_open_mem(
            '<?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 . '>',
            DOMXML_LOAD_PARSING | DOMXML_LOAD_COMPLETE_ATTRS |
            DOMXML_LOAD_SUBSTITUTE_ENTITIES | DOMXML_LOAD_DONT_KEEP_BLANKS,
            $error
        );
        if (!empty($error)) {
            return PEAR::raiseError(_("Horde/Kolab: Unable to create new XML tree for object %s", $uid));
        }

        $this->_uid = $uid;

        return $this->_xml->document_element();
    }

    function saveObject()
    {
        if (empty($this->_imap)) {
            return false;
        }

        $this->setVal('last-modification-date', Kolab::encodeDateTime());
        $this->setVal('product-id', KOLAB_PRODUCT_ID);

        $part = &new MIME_Part($this->_mime_type, $this->_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::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($this->_uid);
        if (is_a($this->_msg_no, 'PEAR_Error')) {
            return $this->_msg_no;
        }

        return true;
    }

    function &getCurrentObject()
    {
        return $this->_xml->document_element();
    }

    function &getElem($name, &$parent)
    {
        $elements = &$this->getAllElems($name, $parent);

        if (empty($elements)) {
            return false;
        }

        return $elements[0];
    }

    function &getAllElems($name, &$parent)
    {
        return $parent->get_elements_by_tagname($name);
    }

    function &getRootElem($name)
    {
        return $this->getElem($name, $this->getCurrentObject());
    }

    function &getAllRootElems($name)
    {
        return $this->getAllElems($name, $this->getCurrentObject());
    }

    function delElem($name, &$parent)
    {
        $element = &$this->getElem($name, $parent);
        if ($element === false) {
            return;
        }

        return $parent->remove_child($element);
    }

    function delAllElems($name, &$parent)
    {
        $elements = &$this->getAllElems($name, $parent);
        for ($i = 0, $j = count($elements); $i < $j; $i++) {
            $parent->remove_child($elements[$i]);
        }
        return true;
    }

    function delAllRootElems($name)
    {
        return $this->delAllElems($name, $this->getCurrentObject());
    }

    function delRootElem(&$element)
    {
        if ($element === false) {
            return;
        }

        $root = &$this->getCurrentObject();
        return $root->remove_child($element);
    }

    function getElemVal(&$parent, $name, $default = 0)
    {
        if ($parent === false) {
            return $default;
        }

        $element = &$this->getElem($name, $parent);
        if ($element === false) {
            return $default;
        }

        return $element->get_content();
    }

    function getElemStr(&$parent, $name, $default = '')
    {
        if ($parent === false) {
            return $default;
        }

        $element = &$this->getElem($name, $parent);
        if ($element === false) {
            return $default;
        }

        return String::convertCharset($element->get_content(), 'utf-8');
    }

    function getVal($name, $default = 0)
    {
        return $this->getElemVal($this->getCurrentObject(), $name, $default);
    }

    function getStr($name, $default = '')
    {
        return $this->getElemStr($this->getCurrentObject(), $name, $default);
    }

    function &initElem($name, &$parent)
    {
        if ($parent === false) {
            $parent = $this->getCurrentObject();
        }

        $element = $this->getElem($name, $parent);

        if ($element === false) {
            $element = $parent->append_child($this->_xml->create_element($name));
        }

        $children = $element->child_nodes();
        foreach ($children as $child) {
            if ($child->node_type() == XML_TEXT_NODE) {
                $element->remove_child($child);
            }
        }

        return $element;
    }

    function &initRootElem($name)
    {
        return $this->initElem($name, $this->getCurrentObject());
    }

    function &appendElem($name, &$parent)
    {
        return $parent->append_child($this->_xml->create_element($name));
    }

    function &appendRootElem($name)
    {
        return $this->appendElem($name, $this->getCurrentObject());
    }

    function &setElemVal(&$parent, $name, $value = '')
    {
        if (empty($value)) {
            $this->delElem($name, $parent);
            return false;
        }

        $element = &$this->initElem($name, $parent);
        $element->set_content($value);

        return $element;
    }

    function &setElemStr(&$parent, $name, $value = '')
    {
        return $this->setElemVal($parent, $name, String::convertCharset($value, NLS::getCharset(), 'utf-8'));
    }

    function &setVal($name, $value = '')
    {
        return $this->setElemVal($this->getCurrentObject(), $name, $value);
    }

    function &setStr($name, $value = '')
    {
        return $this->setElemStr($this->getCurrentObject(), $name, $value);
    }

// Utility library interface

    /**
     * Returns a string containing the current UTC date in the format
     * prescribed by the Kolab Format Specification.
     *
     * @return string  The current UTC date in the format 'YYYY-MM-DD'.
     */
    function encodeDate($date = false)
    {
        if ($date === 0) {
            return 0;
        }
        return gmstrftime('%Y-%m-%d', $date === false ? time() : $date);
    }

    /**
     * Returns a UNIX timestamp corresponding the given date string which is in
     * the format prescribed by the Kolab Format Specification.
     *
     * @param string $date  The string representation of the date.
     *
     * @return integer  The unix timestamp corresponding to $date.
     */
    function decodeDate($date)
    {
        if (empty($date)) {
            return 0;
        }

        list($year, $month, $day) = explode('-', $date);
        return gmmktime(0, 0, 0, $month, $day, $year);
    }

    /**
     * Returns a string containing the current UTC date and time in the format
     * prescribed by the Kolab Format Specification.
     *
     * @return string    The current UTC date and time in the format
     *                   'YYYY-MM-DDThh:mm:ssZ', where the T and Z are literal
     *                   characters.
     */
    function encodeDateTime($datetime = false)
    {
        if ($datetime === 0) {
            return 0;
        }
        return gmstrftime('%Y-%m-%dT%H:%M:%SZ', $datetime === false ? time() : $datetime);
    }

    /**
     * Returns a UNIX timestamp corresponding the given date-time string which
     * is in the format prescribed by the Kolab Format Specification.
     *
     * @param string $datetime  The string representation of the date & time.
     *
     * @return integer  The unix timestamp corresponding to $datetime.
     */
    function decodeDateTime($datetime)
    {
        if (empty($datetime)) {
            return 0;
        }

        list($year, $month, $day, $hour, $minute, $second)
            = sscanf($datetime, '%d-%d-%dT%d:%d:%dZ');
        return gmmktime($hour, $minute, $second, $month, $day, $year);
    }

    /**
     * Returns a UNIX timestamp corresponding the given date or date-time
     * string which is in either format prescribed by the Kolab Format
     * Specification.
     *
     * @param string $date  The string representation of the date (& time).
     *
     * @return integer  The unix timestamp corresponding to $date.
     */
    function decodeDateOrDateTime($date)
    {
        if (empty($date)) {
            return 0;
        }

        return (strlen($date) == 10 ? Kolab::decodeDate($date) : Kolab::decodeDateTime($date));
    }

    function percentageToBoolean($percentage)
    {
        return $percentage == 100 ? '1' : '0';
    }

    function booleanToPercentage($boolean)
    {
        return $boolean ? '100' : '0';
    }

    /**
     * Converts a string in the current character set to an IMAP UTF-7 string,
     * suitable for use as the name of an IMAP folder.
     *
     * @param string $name  The text in the current character set to convert.
     *
     * @return string    $name encoded in the IMAP variation of UTF-7.
     */
    function encodeImapFolderName($name)
    {
        return String::convertCharset($name, NLS::getCharset(), 'UTF7-IMAP');
    }

    /**
     * Converts a string in the IMAP variation of UTF-7 into a string in the
     * current character set.
     *
     * @param string $name  The text in IMAP UTF-7 to convert.
     *
     * @return string    $name encoded in the current character set.
     */
    function decodeImapFolderName($name)
    {
        return String::convertCharset($name, 'UTF7-IMAP');
    }

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

    /**
     * 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);
    }

    /**
     * Returns the unfolded representation of the given text.
     *
     * @param string $text  The text to unfold.
     *
     * @return string  The unfolded representation of $text.
     */
    function unfoldText($text)
    {
        return preg_replace("/\r\n[ \t]+/", "", $text);
    }

    /**
     * Returns an array of application-specific constants, that are used in
     * a generic manner throughout the library.
     *
     * @param string $app  The application whose constants to query.
     *
     * @return mixed  An array of application-specific constants if $app is a
     *                supported application, or a PEAR_Error object if $app is
     *                not supported.
     */
    function getAppConsts($app)
    {
        switch ($app) {
        case 'mnemo':
            return array(
                'mime_type_suffix'      => 'note',
                'default_folder_name'   => "Notes",
                'share_name'            => _("notepad"),
                'default_share_name'    => _("%s's Notepad"),
            );

        case 'kronolith':
            return array(
                'mime_type_suffix'      => 'event',
                'default_folder_name'   => "Calendar",
                'share_name'            => _("calendar"),
                'default_share_name'    => _("%s's Calendar"),
            );

        case 'turba':
            return array(
                'mime_type_suffix'      => 'contact',
                'default_folder_name'   => "Contacts",
                'share_name'            => _("address book"),
                'default_share_name'    => _("%s's Address Book"),
            );

        case 'nag':
            return array(
                'mime_type_suffix'      => 'task',
                'default_folder_name'   => "Tasks",
                'share_name'            => _("task list"),
                'default_share_name'    => _("%s's Tasklist"),
            );

        default:
            return PEAR::raiseError(sprintf(_("The Horde/Kolab integration engine does not support \"%s\""), $app));
        }
    }

    /**
     * Returns an IMAP ACL string corresponding to the given Horde permissions
     * value.
     *
     * @param integer $perms  The Horde permissions value.
     * @param boolean $owner  Will the ACL string be used for the folder owner?
     *                        This includes the 'a' rights in the ACL string.
     *
     * @return string  The IMAP ACL string corresponding to $perms and $owner.
     */
    function permsToACL($perms, $owner = true)
    {
        $result = $owner ? 'a' : '';
        if ($perms & PERMS_SHOW) {
            $result .= 'l';
        }
        if ($perms & PERMS_READ) {
            $result .= 'r';
        }
        if ($perms & PERMS_EDIT) {
            $result .= 'iswc';
        }
        if ($perms & PERMS_DELETE) {
            $result .= 'd';
        }

        return $result;
    }

    /**
     * Returns a Horde permissions value corresponding to the given IMAP ACL
     * string.
     *
     * @param string $acl_string  The IMAP ACL string.
     *
     * @return integer  The Horde permissions value corresponding to
     *                  $acl_string.
     */
    function aclToPerms($acl_string)
    {
        $result = 0;

        for ($i = 0, $j = strlen($acl_string); $i < $j; $i++) {
            switch ($acl_string[$i]) {
            case 'l':
                $result |= PERMS_SHOW;
                break;

            case 'r':
                $result |= PERMS_READ;
                break;

            case 'i':
                $result |= PERMS_EDIT;
                break;

            case 'd':
                $result |= PERMS_DELETE;
                break;
            }
        }

        return $result;
    }

    /**
     * Creates an associative array out of an IMAP ACL list as returned by the
     * Net_IMAP object.
     *
     * @param array $acl  The IMAP ACL array.
     *
     * @return array  An associative array, where the keys are individual users
     *                and the values the ACL string for the specific user.
     */
    function aclToArray($acl)
    {
        if (is_a($acl, 'PEAR_Error')) {
            return $acl;
        }

        $result = array();
        foreach ($acl as $user) {
            $result[$user['USER']] = $user['RIGHTS'];
        }

        return $result;
    }

    function getShareMap()
    {
        return empty($_SESSION[KOLAB_SHARE_MAP])
          ? array()
          : unserialize($_SESSION[KOLAB_SHARE_MAP]);
    }

    function setShareMap($share_map)
    {
        $_SESSION[KOLAB_SHARE_MAP] = serialize($share_map);
    }

    function updateMappedShare(&$share_map, $share_uid, $imap_folder)
    {
        $share_map[$share_uid] = $imap_folder;
    }

    function getShareMapping(&$share_map, $share_uid, $dne_result = '')
    {
        return empty($share_map[$share_uid]) ? $dne_result : $share_map[$share_uid];
    }

    function removeMappedShare(&$share_map, $share_uid)
    {
        if (isset($share_map[$share_uid])) {
            unset($share_map[$share_uid]);
        }
    }

    /**
     * Synchronises the Perms of a Horde share to the ACL of an IMAP folder,
     * creating the folder if necessary. It also sets the necessary annotations
     * indicating what groupware format type is contained within the folder,
     * and what Horde share it corresponds to.
     *
     * @param object $imap   A reference to a Net_IMAP object that is already
     *                       connected and authenticated to the correct server.
     * @param object $share  A reference to the Horde share object that we are
     *                       interested in.
     * @param string $app_consts  A reference to the result of a getAppConsts()
     *                            call for the current Horde application.
     *                       Horde/Kolab integration-supported app.
     *
     * @return mixed  True on success, a PEAR_Error object on failure.
     */
    function synchroniseShare(&$imap, &$share, &$app_consts)
    {
        if (empty($share)) {
            return PEAR::raiseError(_("Invalid share object"));
        }

        $share_uid = $share->getName();
        $default = $share_uid == Auth::getAuth();

        $folder_type = $app_consts['mime_type_suffix'] . ($default ? '.default' : '');
        $folder_name = 'INBOX/' . ($default ? $app_consts['default_folder_name'] : $share->get('name'));
        //Kolab::encodeImapFolderName($default ? $app_consts['default_folder_name'] : $share->get('name'));
        if (is_a($folder_name, 'PEAR_Error')) {
            return $folder_name;
        }

        $share_map = Kolab::getShareMap();

        $old_folder = Kolab::getShareMapping($share_map, $share_uid);

        $result = $imap->mailboxExist($folder_name);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        if ($folder_name != $old_folder) {
            if ($result && $old_folder) {
                return PEAR::raiseError(sprintf(_("Unable to rename %s to %s: destination folder already exists"), $folder_name, $old_folder));
            }

            if ($old_folder) {
                $result = $imap->renameMailbox($old_folder, $folder_name);
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }
            } elseif ($result === false) {
                $result = $imap->createMailbox($folder_name);
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }
            }
        }

        $acl = Kolab::aclToArray($imap->getACL($folder_name));
        if (is_a($acl, 'PEAR_Error')) {
            return $acl;
        }

        $perm = &$share->getPermission();
        $perm_list = $perm->getUserPermissions();
        $owner = $share->get('owner');

        foreach ($perm_list as $user => $user_perms) {
            $result = $imap->setACL($folder_name, $user, Kolab::permsToAcl($user_perms, $user == $owner));
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }

            unset($acl[$user]);
        }

        // Make sure we don't delete the current user from the folder's ACL, or
        // else we'll be locked out
        unset($acl[$owner]);

        foreach ($acl as $user => $user_acl) {
            $result = $imap->deleteACL($folder_name, $user);
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
        }

        $result = $imap->setAnnotation(KOLAB_ANNOT_FOLDER_TYPE, array('value.shared' => $folder_type), $folder_name);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $imap->setAnnotation(KOLAB_ANNOT_SHARE_UID, array('value.shared' => $share_uid), $folder_name);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        Kolab::updateMappedShare($share_map, $share_uid, $folder_name);
        Kolab::setShareMap($share_map);

        return true;
    }

    function updateShare(&$share)
    {
        global $registry, $notification;

        $app = $registry->getApp();

        // We only update shares if Kolab integration is enabled, and if we've
        // previously synchronised the shares list (i.e. we know whether there
        // is an IMAP folder that corresponds to the given share).
        if (empty($GLOBALS['conf']['kolab']['enabled']) ||
            (!empty($_SESSION[KOLAB_LAST_SYNCHED_APP]) &&
             $_SESSION[KOLAB_LAST_SYNCHED_APP] != $app)) {
            return false;
        }

        $app_consts = Kolab::getAppConsts($registry->getApp());
        if (is_a($app_consts, 'PEAR_Error')) {
            return false;
        }

        $result = Kolab::_updateShare($share, $app_consts);
        $id = $share->get('name');
        if (is_a($result, 'PEAR_Error')) {
            $notification->push(sprintf(_("Horde/Kolab: Unable to synchronise %s \"%s\": %s"),
                $app_consts['share_name'], $id, $result->getMessage()), 'horde.error');
        } elseif ($result === true) {
            $notification->push(sprintf(_("Horde/Kolab: Successfully synchronised %s \"%s\""),
                $app_consts['share_name'], $id), 'horde.success');
        }

        return true;
    }

    function _updateShare(&$share, &$app_consts)
    {
        global $conf;

        $imap = &new Net_IMAP($conf['kolab']['imap']['server'], $conf['kolab']['imap']['port']);
        if (is_a($imap, 'PEAR_Error')) {
            return $imap;
        }

        $result = $imap->login(Auth::getAuth(), Auth::getCredential('password'), false, false);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = Kolab::synchroniseShare($imap, $share, $app_consts);

        $imap->disconnect();

        return $result;
    }

    function removeShare(&$share)
    {
        global $registry, $notification;

        $app = $registry->getApp();

        // We only remove shares if Kolab integration is enabled, and if we've
        // previously synchronised the shares list (i.e. we know what IMAP
        // folder corresponds to the given share).
        if (empty($GLOBALS['conf']['kolab']['enabled']) ||
            (!empty($_SESSION[KOLAB_LAST_SYNCHED_APP]) &&
             $_SESSION[KOLAB_LAST_SYNCHED_APP] != $app)) {
            return false;
        }

        $app_consts = Kolab::getAppConsts($app);
        if (is_a($app_consts, 'PEAR_Error')) {
            return false;
        }

        $result = Kolab::_removeShare($share);
        $id = $share->get('name');
        if (is_a($result, 'PEAR_Error')) {
            $notification->push(sprintf(_("Horde/Kolab: Unable to remove %s \"%s\": %s"),
                $app_consts['share_name'], $id, $result->getMessage()), 'horde.error');
        } elseif ($result === true) {
            $notification->push(sprintf(_("Horde/Kolab: Successfully removed %s \"%s\""),
                $app_consts['share_name'], $id), 'horde.success');
        }

        return true;
    }

    function _removeShare(&$share)
    {
        global $conf;

        if (empty($share)) {
            return PEAR::raiseError(_("Invalid share object"));
        }

        $share_uid = $share->getName();

        $share_map = Kolab::getShareMap();

        $folder = Kolab::getShareMapping($share_map, $share_uid);
        if (empty($folder)) {
            return true;
        }

        $imap = &new Net_IMAP($conf['kolab']['imap']['server'], $conf['kolab']['imap']['port']);
        if (is_a($imap, 'PEAR_Error')) {
            return $imap;
        }

        $result = $imap->login(Auth::getAuth(), Auth::getCredential('password'), false, false);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $result = $imap->deleteMailbox($folder);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        $imap->disconnect();

        Kolab::removeMappedShare($share_map, $share_uid);

        Kolab::setShareMap($share_map);

        return true;
    }

    function synchroniseShares(&$shares, $app)
    {
        if (empty($GLOBALS['conf']['kolab']['enabled']) ||
            array_search($app, array('mnemo', 'nag', 'turba', 'kronolith')) === false) {
            return;
        }

        // Turba doesn't use shares, so we just create a dummy share list here
        // to make sure the following code works.
        if (empty($shares) && $app == 'turba') {
            $shares = &Horde_Share::singleton('turba');
        }

        // Check if we've already synchronised the IMAP folder list; if we have
        // then exit early. Folder synchronisation is done on application
        // startup - this includes switching between applications as well as
        // upon initial login.
        if (!empty($_SESSION[KOLAB_LAST_SYNCHED_APP]) &&
            $_SESSION[KOLAB_LAST_SYNCHED_APP] == $app) {
            return;
        }

        global $notification;

        $result = Kolab::_synchShares($shares);
        if (is_a($result, 'PEAR_Error')) {
            $notification->push(sprintf(_("Horde/Kolab: Unable to synchronise shares: %s"), $result->getMessage()), 'horde.error');
        } /*elseif ($result === true) {
            $notification->push(_("Horde/Kolab: Successfully synchronised shares"), 'horde.success');
        }*/
    }

    /**
     * Synchronises the IMAP folder list and the Horde share list.
     *
     * @return mixed  True on a successful synchronisation, false when a
     *                synchronisation is not required (i.e. multiple page loads
     *                in a single application), or a PEAR_Error object on
     *                failure.
     */
    function _synchShares(&$shares)
    {
        global $registry, $conf;

        $app = $registry->getApp();

        // Otherwise we need to map the current IMAP folder list to the
        // applications share list.

        // Make sure this is being called from a known application
        $app_consts = Kolab::getAppConsts($app);
        if (is_a($app_consts, 'PEAR_Error')) {
            return $app_consts;
        }

        // Connect to the IMAP server
        $imap = &new Net_IMAP($conf['kolab']['imap']['server'], $conf['kolab']['imap']['port']);
        if (is_a($imap, 'PEAR_Error')) {
            return $imap;
        }

        // Login using the current Horde credentials
        $current_user = Auth::getAuth();
        $domain = strstr($current_user, '@');
        $result = $imap->login($current_user, Auth::getCredential('password'), false, false);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        // Obtain a list of all folders the current user has access to
        $folder_list = $imap->getMailboxes();
        if (is_a($folder_list, 'PEAR_Error')) {
            return $folder_list;
        }

        // We're only interested in the folders of the specific resource type
        // for the current application, so filter out the rest
        $local_folders = array();
        $remote_folders = array();
        foreach ($folder_list as $folder) {
            $annotation = $imap->getAnnotation(KOLAB_ANNOT_FOLDER_TYPE, 'value.shared', $folder);
            if (is_a($annotation, 'PEAR_Error')) {
                return $annotation;
            }

            // If there is no annotation then we treat it as a standard mail
            // folder (i.e. we ignore it).
            if (empty($annotation)) {
                continue;
            }

            // If the folder is of the correct type then we add it to our list
            // of folders that should correspond to a share. Here we get any
            // other additional information we may need, such as whether the
            // folder was previously mapped to a share, its ACL, its owner (if
            // it is a shared folder) and its display name.
            $type = explode('.', $annotation);
            if ($type[0] == $app_consts['mime_type_suffix']) {
                $acl = Kolab::aclToArray($imap->getACL($folder));
                if (is_a($acl, 'PEAR_Error')) {
                    return $acl;
                }

                $share_uid = $imap->getAnnotation(KOLAB_ANNOT_SHARE_UID, 'value.shared', $folder);
                if (is_a($share_uid, 'PEAR_Error')) {
                    return $share_uid;
                }

                preg_match(";(INBOX|user/([^/]+))/([^@]+)(@.*)?;", $folder, $matches);

                if ($matches[1] == 'INBOX') {
                    $local_folders[$folder] = array(
                        'share_uid'     => $share_uid,
                        'acl'           => $acl,
                        'name'          => Kolab::decodeImapFolderName($matches[3]),
                        'default'       => (!empty($type[1]) && $type[1] == 'default'),
                    );
                } else {
                    $user_domain = isset($matches[4]) ? $matches[4] : $domain;
                    $folder = isset($matches[4]) ? $folder : $folder . $user_domain;
                    $remote_folders[$folder] = array(
                        'share_uid'     => $share_uid,
                        'acl'           => $acl,
                        'name'          => Kolab::decodeImapFolderName($matches[3]),
                        'owner'         => $matches[2] . $user_domain,
                    );
                }
            }
        }

        // We now map the remaining folder list to the current applications
        // share list, to synch Horde with other Kolab clients that may have
        // previously modified the groupware folders


        // TODO: see if there is a more accurate way of listing all shares that
        // are accessible, regardless of what permissions the user has (i.e. if
        // the user has at least one permission level to the share, the share
        // should be in one of these lists).
        $local_shares = $shares->listShares($current_user, PERMS_SHOW, $current_user);
        $remote_shares = array_diff($shares->listShares($current_user, PERMS_SHOW), $local_shares);

        Kolab::setShareMap(array());
        $share_map = array();

        // Now synchronise the folder list with the share list
        foreach ($local_folders as $folder => $attrs) {
            if (empty($attrs['share_uid'])) {
                $uid = md5(microtime());
                $share = &$shares->newShare($uid);
                $share->set('name', $attrs['name']);
                $shares->addShare($share);

                $result = $imap->setAnnotation(KOLAB_ANNOT_SHARE_UID, array('value.shared' => $uid), $folder);
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }

                Kolab::updateMappedShare($share_map, $uid, $folder);
            } else {
                if (!$shares->exists($attrs['share_uid'])) {
                    $share = &$shares->newShare($attrs['share_uid']);
                    $share->set('name', $attrs['name']);
                    $shares->addShare($share);
                } else {
                    $share = &$local_shares[$attrs['share_uid']];
                    if ($share->get('name') != $attrs['name']) {
                        $share->set('name', $attrs['name']);
                    }
                }

                Kolab::updateMappedShare($share_map, $attrs['share_uid'], $folder);
            }

            $current_users = $share->listUsers();

            foreach ($attrs['acl'] as $user => $acl) {
                $share->addUserPermission($user, Kolab::aclToPerms($acl));
                unset($current_users[$user]);
            }

            foreach ($current_users as $user) {
                $share->removeUserPermission($user, PERMS_ALL);
            }

            $share->save();

            if (!empty($attrs['share_uid'])) {
                unset($local_shares[$attrs['share_uid']]);
            }
        }

        unset($local_shares[$current_user]);

        // Re-authenticate as the admin user in order to set annotations on
        // folders that the current user doesn't own
        $imap->disconnect();
        $imap = &new Net_IMAP($conf['kolab']['imap']['server'], $conf['kolab']['imap']['port']);
        if (is_a($imap, 'PEAR_Error')) {
            return $imap;
        }

        $result = $imap->login($conf['kolab']['imap']['adminuser'], $conf['kolab']['imap']['adminpw'], false, false);
        if (is_a($result, 'PEAR_Error')) {
            return $result;
        }

        foreach ($remote_folders as $folder => $attrs) {
            if (empty($attrs['share_uid'])) {
                $uid = md5(microtime());
                $share = &$shares->newShare($uid);
                $share->set('owner', $attrs['owner']);
                $share->set('name', $attrs['name']);
                $shares->addShare($share);

                $result = $imap->setAnnotation(KOLAB_ANNOT_SHARE_UID, array('value.shared' => $uid), $folder);
                if (is_a($result, 'PEAR_Error')) {
                    return $result;
                }

                Kolab::updateMappedShare($share_map, $uid, $folder);
            } else {
                if (!$shares->exists($attrs['share_uid'])) {
                    $share = &$shares->newShare($attrs['share_uid']);
                    $share->set('owner', $attrs['owner']);
                    $share->set('name', $attrs['name']);
                    $shares->addShare($share);
                } else {
                    $share = &$remote_shares[$attrs['share_uid']];
                    if ($share->get('name') != $attrs['name']) {
                        $share->set('name', $attrs['name']);
                    }
                }

                Kolab::updateMappedShare($share_map, $attrs['share_uid'], $folder);
            }

            $current_users = $share->listUsers();

            foreach ($attrs['acl'] as $user => $acl) {
                $share->addUserPermission($user, Kolab::aclToPerms($acl));
                unset($current_users[$user]);
            }

            foreach ($current_users as $user) {
                $share->removeUserPermission($user, PERMS_ALL);
            }

            $share->save();

            if (!empty($attrs['share_uid'])) {
                unset($remote_shares[$attrs['share_uid']]);
            }
        }

        // Finally, remove any extra shares we may have (these were once folders
        // that were subsequently deleted by another Kolab client). This is only
        // done for shares that the current user owns, i.e. shares that were
        // subfolders of the current users' INBOX.
        foreach ($local_shares as $share) {
            $result = $shares->removeShare($share);
            if (is_a($result, 'PEAR_Error')) {
                return $result;
            }
        }

        // For remote shares, we simply remove the current user from the shares
        // permissions list.
        for ($i = 0, $j = count($remote_shares); $i < $j; $i++) {
            $remote_shares[$i]->removeUserPermission($current_user, PERMS_ALL);
            $remote_shares[$i]->save();
        }

        // And we're done! Disconnect from IMAP, set the last synch'ed app
        // flag to the current application, and update the share map.
        $imap->disconnect();

        $_SESSION[KOLAB_LAST_SYNCHED_APP] = $app;
        Kolab::setShareMap($share_map);

        return true;
    }

}
