HOME


Mini Shell 1.0
DIR:/usr/local/cwpsrv/var/services/roundcube/plugins/libkolab/lib/
Upload File :
Current File : //usr/local/cwpsrv/var/services/roundcube/plugins/libkolab/lib/kolab_storage_folder.php
<?php

/**
 * The kolab_storage_folder class represents an IMAP folder on the Kolab server.
 *
 * @version @package_version@
 * @author Thomas Bruederli <bruederli@kolabsys.com>
 * @author Aleksander Machniak <machniak@kolabsys.com>
 *
 * Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
class kolab_storage_folder extends kolab_storage_folder_api
{
    /**
     * The kolab_storage_cache instance for caching operations
     * @var object
     */
    public $cache;

    /**
     * Indicate validity status
     * @var boolean
     */
    public $valid = false;

    protected $error = 0;
    protected $resource_uri;


    /**
     * Default constructor
     *
     * @param string The folder name/path
     * @param string Expected folder type
     */
    function __construct($name, $type = null, $type_annotation = null)
    {
        parent::__construct($name);
        $this->set_folder($name, $type, $type_annotation);
    }

    /**
     * Set the IMAP folder this instance connects to
     *
     * @param string The folder name/path
     * @param string Expected folder type
     * @param string Optional folder type if known
     */
    public function set_folder($name, $type = null, $type_annotation = null)
    {
        $this->name = $name;

        if (empty($type_annotation)) {
            $type_annotation = $this->get_type();
        }

        $oldtype = $this->type;
        list($this->type, $suffix) = explode('.', $type_annotation);
        $this->default      = $suffix == 'default';
        $this->subtype      = $this->default ? '' : $suffix;
        $this->id           = kolab_storage::folder_id($name);
        $this->valid        = !empty($this->type) && $this->type != 'mail' && (!$type || $this->type == $type);

        if (!$this->valid) {
            $this->error = $this->imap->get_error_code() < 0 ? kolab_storage::ERROR_IMAP_CONN : kolab_storage::ERROR_INVALID_FOLDER;
        }

        // reset cached object properties
        $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;

        // get a new cache instance if folder type changed
        if (!$this->cache || $this->type != $oldtype)
            $this->cache = kolab_storage_cache::factory($this);
        else
            $this->cache->set_folder($this);

        $this->imap->set_folder($this->name);
    }

    /**
     * Returns code of last error
     *
     * @return int Error code
     */
    public function get_error()
    {
        return $this->error ?: $this->cache->get_error();
    }

    /**
     * Check IMAP connection error state
     */
    public function check_error()
    {
        if (($err_code = $this->imap->get_error_code()) < 0) {
            $this->error = kolab_storage::ERROR_IMAP_CONN;
            if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
                $this->error = kolab_storage::ERROR_NO_PERMISSION;
            }
        }

        return $this->error;
    }

    /**
     * Compose a unique resource URI for this IMAP folder
     */
    public function get_resource_uri()
    {
        if (!empty($this->resource_uri)) {
            return $this->resource_uri;
        }

        // strip namespace prefix from folder name
        $ns     = $this->get_namespace();
        $nsdata = $this->imap->get_namespace($ns);

        if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
            $subpath = substr($this->name, strlen($nsdata[0][0]));
            if ($ns == 'other') {
                list($user, $suffix) = explode($nsdata[0][1], $subpath, 2);
                $subpath = $suffix;
            }
        }
        else {
            $subpath = $this->name;
        }

        // compose fully qualified ressource uri for this instance
        $this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath;
        return $this->resource_uri;
    }

    /**
     * Helper method to extract folder UID metadata
     *
     * @return string Folder's UID
     */
    public function get_uid()
    {
        // UID is defined in folder METADATA
        $metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_CYRUS);
        $metadata = $this->get_metadata();

        if ($metadata !== null) {
            foreach ($metakeys as $key) {
                if ($uid = $metadata[$key]) {
                    return $uid;
                }
            }

            // generate a folder UID and set it to IMAP
            $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-');
            if ($this->set_uid($uid)) {
                return $uid;
            }
        }

        $this->check_error();

        // create hash from folder name if we can't write the UID metadata
        return md5($this->name . $this->get_owner());
    }

    /**
     * Helper method to set an UID value to the given IMAP folder instance
     *
     * @param string Folder's UID
     * @return boolean True on succes, False on failure
     */
    public function set_uid($uid)
    {
        $success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid));

        $this->check_error();
        return $success;
    }

    /**
     * Compose a folder Etag identifier
     */
    public function get_ctag()
    {
        $fdata = $this->get_imap_data();
        $this->check_error();
        return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']);
    }

    /**
     * Check activation status of this folder
     *
     * @return boolean True if enabled, false if not
     */
    public function is_active()
    {
        return kolab_storage::folder_is_active($this->name);
    }

    /**
     * Change activation status of this folder
     *
     * @param boolean The desired subscription status: true = active, false = not active
     *
     * @return True on success, false on error
     */
    public function activate($active)
    {
        return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
    }

    /**
     * Check subscription status of this folder
     *
     * @return boolean True if subscribed, false if not
     */
    public function is_subscribed()
    {
        return kolab_storage::folder_is_subscribed($this->name);
    }

    /**
     * Change subscription status of this folder
     *
     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
     *
     * @return True on success, false on error
     */
    public function subscribe($subscribed)
    {
        return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
    }

    /**
     * Get number of objects stored in this folder
     *
     * @param mixed Pseudo-SQL query as list of filter parameter triplets
     *    or string with object type (e.g. contact, event, todo, journal, note, configuration)
     *
     * @return integer The number of objects of the given type
     * @see self::select()
     */
    public function count($query = null)
    {
        if (!$this->valid) {
            return 0;
        }

        // synchronize cache first
        $this->cache->synchronize();

        return $this->cache->count($this->_prepare_query($query));
    }

    /**
     * List Kolab objects matching the given query
     *
     * @param mixed Pseudo-SQL query as list of filter parameter triplets
     *    or string with object type (e.g. contact, event, todo, journal, note, configuration)
     *
     * @return array List of Kolab data objects (each represented as hash array)
     * @deprecated Use select()
     */
    public function get_objects($query = array())
    {
        return $this->select($query);
    }

    /**
     * Select Kolab objects matching the given query
     *
     * @param mixed   Pseudo-SQL query as list of filter parameter triplets
     *                or string with object type (e.g. contact, event, todo, journal, note, configuration)
     * @param boolean Use fast mode to fetch only minimal set of information
     *                (no xml fetching and parsing, etc.)
     *
     * @return array List of Kolab data objects (each represented as hash array)
     */
    public function select($query = array(), $fast = false)
    {
        if (!$this->valid) {
            return array();
        }

        // synchronize caches
        $this->cache->synchronize();

        // fetch objects from cache
        return $this->cache->select($this->_prepare_query($query), false, $fast);
    }

    /**
     * Getter for object UIDs only
     *
     * @param array Pseudo-SQL query as list of filter parameter triplets
     * @return array List of Kolab object UIDs
     */
    public function get_uids($query = array())
    {
        if (!$this->valid) {
            return array();
        }

        // synchronize caches
        $this->cache->synchronize();

        // fetch UIDs from cache
        return $this->cache->select($this->_prepare_query($query), true);
    }

    /**
     * Setter for ORDER BY and LIMIT parameters for cache queries
     *
     * @param array   List of columns to order by
     * @param integer Limit result set to this length
     * @param integer Offset row
     */
    public function set_order_and_limit($sortcols, $length = null, $offset = 0)
    {
        $this->cache->set_order_by($sortcols);

        if ($length !== null) {
            $this->cache->set_limit($length, $offset);
        }
    }

    /**
     * Helper method to sanitize query arguments
     */
    private function _prepare_query($query)
    {
        // string equals type query
        // FIXME: should not be called this way!
        if (is_string($query)) {
            return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array();
        }

        foreach ((array)$query as $i => $param) {
            if ($param[0] == 'type' && !$this->cache->has_type_col()) {
                unset($query[$i]);
            }
            else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) {
                if (is_object($param[2]) && is_a($param[2], 'DateTime'))
                    $param[2] = $param[2]->format('U');
                if (is_numeric($param[2]))
                    $query[$i][2] = date('Y-m-d H:i:s', $param[2]);
            }
        }

        return $query;
    }

    /**
     * Getter for a single Kolab object identified by its UID
     *
     * @param string $uid Object UID
     *
     * @return array The Kolab object represented as hash array
     */
    public function get_object($uid)
    {
        if (!$this->valid || !$uid) {
            return false;
        }

        // synchronize caches
        $this->cache->synchronize();

        return $this->cache->get_by_uid($uid);
    }

    /**
     * Fetch a Kolab object attachment which is stored in a separate part
     * of the mail MIME message that represents the Kolab record.
     *
     * @param string   Object's UID
     * @param string   The attachment's mime number
     * @param string   IMAP folder where message is stored;
     *                 If set, that also implies that the given UID is an IMAP UID
     * @param bool     True to print the part content
     * @param resource File pointer to save the message part
     * @param boolean  Disables charset conversion
     *
     * @return mixed  The attachment content as binary string
     */
    public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
    {
        if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) {
            $this->imap->set_folder($mailbox ? $mailbox : $this->name);

            if (substr($part, 0, 2) == 'i:') {
                // attachment data is stored in XML
                if ($object = $this->cache->get($msguid)) {
                    // load data from XML (attachment content is not stored in cache)
                    if ($object['_formatobj'] && isset($object['_size'])) {
                        $object['_attachments'] = array();
                        $object['_formatobj']->get_attachments($object);
                    }

                    foreach ($object['_attachments'] as $attach) {
                        if ($attach['id'] == $part) {
                            if ($print)   echo $attach['content'];
                            else if ($fp) fwrite($fp, $attach['content']);
                            else          return $attach['content'];
                            return true;
                        }
                    }
                }
            }
            else {
                // return message part from IMAP directly
                // TODO: We could improve performance if we cache part's encoding
                //       without 3rd argument get_message_part() will request BODYSTRUCTURE from IMAP
                return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
            }
        }

        return null;
    }

    /**
     * Fetch the mime message from the storage server and extract
     * the Kolab groupware object from it
     *
     * @param string The IMAP message UID to fetch
     * @param string The object type expected (use wildcard '*' to accept all types)
     * @param string The folder name where the message is stored
     *
     * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
     */
    public function read_object($msguid, $type = null, $folder = null)
    {
        if (!$this->valid) {
            return false;
        }

        if (!$type) $type = $this->type;
        if (!$folder) $folder = $this->name;

        $this->imap->set_folder($folder);

        $this->cache->imap_mode(true);
        $message = new rcube_message($msguid);
        $this->cache->imap_mode(false);

        // Message doesn't exist?
        if (empty($message->headers)) {
            return false;
        }

        // extract the X-Kolab-Type header from the XML attachment part if missing
        if (empty($message->headers->others['x-kolab-type'])) {
            foreach ((array)$message->attachments as $part) {
                if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
                    $message->headers->others['x-kolab-type'] = $part->mimetype;
                    break;
                }
            }
        }
        // fix buggy messages stating the X-Kolab-Type header twice
        else if (is_array($message->headers->others['x-kolab-type'])) {
            $message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']);
        }

        // no object type header found: abort
        if (empty($message->headers->others['x-kolab-type'])) {
            rcube::raise_error(array(
                'code' => 600,
                'type' => 'php',
                'file' => __FILE__,
                'line' => __LINE__,
                'message' => "No X-Kolab-Type information found in message $msguid ($this->name).",
            ), true);
            return false;
        }

        $object_type  = kolab_format::mime2object_type($message->headers->others['x-kolab-type']);
        $content_type = kolab_format::KTYPE_PREFIX . $object_type;

        // check object type header and abort on mismatch
        if ($type != '*' && strpos($object_type, $type) !== 0 && !($object_type == 'distribution-list' && $type == 'contact')) {
            return false;
        }

        $attachments = array();

        // get XML part
        foreach ((array)$message->attachments as $part) {
            if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!i', $part->mimetype))) {
                $xml = $message->get_part_body($part->mime_id, true);
            }
            else if ($part->filename || $part->content_id) {
                $key  = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
                $size = null;

                // Use Content-Disposition 'size' as for the Kolab Format spec.
                if (isset($part->d_parameters['size'])) {
                    $size = $part->d_parameters['size'];
                }
                // we can trust part size only if it's not encoded
                else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') {
                    $size = $part->size;
                }

                $attachments[$key] = array(
                    'id'       => $part->mime_id,
                    'name'     => $part->filename,
                    'mimetype' => $part->mimetype,
                    'size'     => $size,
                );
            }
        }

        if (!$xml) {
            rcube::raise_error(array(
                'code' => 600,
                'type' => 'php',
                'file' => __FILE__,
                'line' => __LINE__,
                'message' => "Could not find Kolab data part in message $msguid ($this->name).",
            ), true);
            return false;
        }

        // check kolab format version
        $format_version = $message->headers->others['x-kolab-mime-version'];
        if (empty($format_version)) {
            list($xmltype, $subtype) = explode('.', $object_type);
            $xmlhead = substr($xml, 0, 512);

            // detect old Kolab 2.0 format
            if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false)
                $format_version = '2.0';
            else
                $format_version = '3.0'; // assume 3.0
        }

        // get Kolab format handler for the given type
        $format = kolab_format::factory($object_type, $format_version);

        if (is_a($format, 'PEAR_Error'))
            return false;

        // load Kolab object from XML part
        $format->load($xml);

        if ($format->is_valid()) {
            $object = $format->to_array(array('_attachments' => $attachments));
            $object['_type']      = $object_type;
            $object['_msguid']    = $msguid;
            $object['_mailbox']   = $this->name;
            $object['_formatobj'] = $format;
            $object['_size']      = strlen($xml);

            return $object;
        }
        else {
            // try to extract object UID from XML block
            if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
                $msgadd = " UID = " . trim(strip_tags($m[1]));

            rcube::raise_error(array(
                'code' => 600,
                'type' => 'php',
                'file' => __FILE__,
                'line' => __LINE__,
                'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd,
            ), true);

            self::save_user_xml("$msguid.xml", $xml);
        }

        return false;
    }

    /**
     * Save an object in this folder.
     *
     * @param array  $object The array that holds the data of the object.
     * @param string $type   The type of the kolab object.
     * @param string $uid    The UID of the old object if it existed before
     *
     * @return mixed False on error or IMAP message UID on success
     */
    public function save(&$object, $type = null, $uid = null)
    {
        if (!$this->valid && empty($object)) {
            return false;
        }

        if (!$type)
            $type = $this->type;

        // copy attachments from old message
        $copyfrom = $object['_copyfrom'] ?: $object['_msguid'];
        if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) {
            foreach ((array)$old['_attachments'] as $key => $att) {
                if (!isset($object['_attachments'][$key])) {
                    $object['_attachments'][$key] = $old['_attachments'][$key];
                }
                // unset deleted attachment entries
                if ($object['_attachments'][$key] == false) {
                    unset($object['_attachments'][$key]);
                }
                // load photo.attachment from old Kolab2 format to be directly embedded in xcard block
                else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
                    if (!isset($object['photo']))
                        $object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']);
                    unset($object['_attachments'][$key]);
                }
            }
        }

        // save contact photo to attachment for Kolab2 format
        if (kolab_storage::$version == '2.0' && $object['photo']) {
            $attkey = 'kolab-picture.png';  // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp
            $object['_attachments'][$attkey] = array(
                'mimetype'=> rcube_mime::image_content_type($object['photo']),
                'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']),
            );
        }

        // process attachments
        if (is_array($object['_attachments'])) {
            $numatt = count($object['_attachments']);
            foreach ($object['_attachments'] as $key => $attachment) {
                // FIXME: kolab_storage and Roundcube attachment hooks use different fields!
                if (empty($attachment['content']) && !empty($attachment['data'])) {
                    $attachment['content'] = $attachment['data'];
                    unset($attachment['data'], $object['_attachments'][$key]['data']);
                }

                // make sure size is set, so object saved in cache contains this info
                if (!isset($attachment['size'])) {
                    if (!empty($attachment['content'])) {
                        if (is_resource($attachment['content'])) {
                            // this need to be a seekable resource, otherwise
                            // fstat() failes and we're unable to determine size
                            // here nor in rcube_imap_generic before IMAP APPEND
                            $stat = fstat($attachment['content']);
                            $attachment['size'] = $stat ? $stat['size'] : 0;
                        }
                        else {
                            $attachment['size'] = strlen($attachment['content']);
                        }
                    }
                    else if (!empty($attachment['path'])) {
                        $attachment['size'] = filesize($attachment['path']);
                    }
                    $object['_attachments'][$key] = $attachment;
                }

                // generate unique keys (used as content-id) for attachments
                if (is_numeric($key) && $key < $numatt) {
                    // derrive content-id from attachment file name
                    $ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null;
                    $basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext));  // to 7bit ascii
                    if (!$basename) $basename = 'noname';
                    $cid = $basename . '.' . microtime(true) . $key . $ext;

                    $object['_attachments'][$cid] = $attachment;
                    unset($object['_attachments'][$key]);
                }
            }
        }

        // save recurrence exceptions as individual objects due to lack of support in Kolab v2 format
        if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) {
            $this->save_recurrence_exceptions($object, $type);
        }

        // check IMAP BINARY extension support for 'file' objects
        // allow configuration to workaround bug in Cyrus < 2.4.17
        $rcmail = rcube::get_instance();
        $binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY');

        // generate and save object message
        if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) {
            // resolve old msguid before saving
            if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) {
                $object['_msguid'] = $msguid;
                $object['_mailbox'] = $this->name;
            }

            $result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);

            // update cache with new UID
            if ($result) {
                $old_uid = $object['_msguid'];

                $object['_msguid'] = $result;
                $object['_mailbox'] = $this->name;

                if ($old_uid) {
                    // delete old message
                    $this->cache->imap_mode(true);
                    $this->imap->delete_message($old_uid, $object['_mailbox']);
                    $this->cache->imap_mode(false);
                }

                // insert/update message in cache
                $this->cache->save($result, $object, $old_uid);
            }

            // remove temp file
            if ($body_file) {
                @unlink($body_file);
            }
        }

        return $result;
    }

    /**
     * Save recurrence exceptions as individual objects.
     * The Kolab v2 format doesn't allow us to save fully embedded exception objects.
     *
     * @param array Hash array with event properties
     * @param string Object type
     */
    private function save_recurrence_exceptions(&$object, $type = null)
    {
        if ($object['recurrence']['EXCEPTIONS']) {
            $exdates = array();
            foreach ((array)$object['recurrence']['EXDATE'] as $exdate) {
                $key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate);
                $exdates[$key] = 1;
            }

            // save every exception as individual object
            foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
                $exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd'));
                $exception['sequence'] = $object['sequence'] + 1;

                if ($exception['thisandfuture']) {
                    $exception['recurrence'] = $object['recurrence'];

                    // adjust the recurrence duration of the exception
                    if ($object['recurrence']['COUNT']) {
                        $recurrence = new kolab_date_recurrence($object['_formatobj']);
                        if ($end = $recurrence->end()) {
                            unset($exception['recurrence']['COUNT']);
                            $exception['recurrence']['UNTIL'] = $end;
                        }
                    }

                    // set UNTIL date if we have a thisandfuture exception
                    $untildate = clone $exception['start'];
                    $untildate->sub(new DateInterval('P1D'));
                    $object['recurrence']['UNTIL'] = $untildate;
                    unset($object['recurrence']['COUNT']);
                }
                else {
                    if (!$exdates[$exception['start']->format('Y-m-d')])
                        $object['recurrence']['EXDATE'][] = clone $exception['start'];
                    unset($exception['recurrence']);
                }

                unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']);
                $this->save($exception, $type, $exception['uid']);
            }

            unset($object['recurrence']['EXCEPTIONS']);
        }
    }

    /**
     * Generate an object UID with the given recurrence-ID in a way that it is
     * unique (the original UID is not a substring) but still recoverable.
     */
    private static function recurrence_exception_uid($uid, $recurrence_id)
    {
        $offset = -2;
        return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset);
    }

    /**
     * Delete the specified object from this folder.
     *
     * @param  mixed   $object  The Kolab object to delete or object UID
     * @param  boolean $expunge Should the folder be expunged?
     *
     * @return boolean True if successful, false on error
     */
    public function delete($object, $expunge = true)
    {
        if (!$this->valid) {
            return false;
        }

        $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
        $success = false;

        $this->cache->imap_mode(true);

        if ($msguid && $expunge) {
            $success = $this->imap->delete_message($msguid, $this->name);
        }
        else if ($msguid) {
            $success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
        }

        $this->cache->imap_mode(false);

        if ($success) {
            $this->cache->set($msguid, false);
        }

        return $success;
    }

    /**
     *
     */
    public function delete_all()
    {
        if (!$this->valid) {
            return false;
        }

        $this->cache->purge();
        $this->cache->imap_mode(true);
        $result = $this->imap->clear_folder($this->name);
        $this->cache->imap_mode(false);

        return $result;
    }

    /**
     * Restore a previously deleted object
     *
     * @param string Object UID
     * @return mixed Message UID on success, false on error
     */
    public function undelete($uid)
    {
        if (!$this->valid) {
            return false;
        }

        if ($msguid = $this->cache->uid2msguid($uid, true)) {
            $this->cache->imap_mode(true);
            $result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
            $this->cache->imap_mode(false);

            if ($result) {
                return $msguid;
            }
        }

        return false;
    }

    /**
     * Move a Kolab object message to another IMAP folder
     *
     * @param string Object UID
     * @param string IMAP folder to move object to
     * @return boolean True on success, false on failure
     */
    public function move($uid, $target_folder)
    {
        if (!$this->valid) {
            return false;
        }

        if (is_string($target_folder))
            $target_folder = kolab_storage::get_folder($target_folder);

        if ($msguid = $this->cache->uid2msguid($uid)) {
            $this->cache->imap_mode(true);
            $result = $this->imap->move_message($msguid, $target_folder->name, $this->name);
            $this->cache->imap_mode(false);

            if ($result) {
                $new_uid = ($copyuid = $this->imap->conn->data['COPYUID']) ? $copyuid[1] : null;
                $this->cache->move($msguid, $uid, $target_folder, $new_uid);
                return true;
            }
            else {
                rcube::raise_error(array(
                    'code' => 600, 'type' => 'php',
                    'file' => __FILE__, 'line' => __LINE__,
                    'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(),
                ), true);
            }
        }

        return false;
    }

    /**
     * Creates source of the configuration object message
     *
     * @param array  $object    The array that holds the data of the object.
     * @param string $type      The type of the kolab object.
     * @param bool   $binary    Enables use of binary encoding of attachment(s)
     * @param string $body_file Reference to filename of message body
     *
     * @return mixed Message as string or array with two elements
     *               (one for message file path, second for message headers)
     */
    private function build_message(&$object, $type, $binary, &$body_file)
    {
        // load old object to preserve data we don't understand/process
        if (is_object($object['_formatobj']))
            $format = $object['_formatobj'];
        else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'])))
            $format = $old['_formatobj'];

        // create new kolab_format instance
        if (!$format)
            $format = kolab_format::factory($type, kolab_storage::$version);

        if (PEAR::isError($format))
            return false;

        $format->set($object);
        $xml = $format->write(kolab_storage::$version);
        $object['uid'] = $format->uid;  // read UID from format
        $object['_formatobj'] = $format;

        if (empty($xml) || !$format->is_valid() || empty($object['uid'])) {
            return false;
        }

        $mime     = new Mail_mime("\r\n");
        $rcmail   = rcube::get_instance();
        $headers  = array();
        $files    = array();
        $part_id  = 1;
        $encoding = $binary ? 'binary' : 'base64';

        if ($user_email = $rcmail->get_user_email()) {
            $headers['From'] = $user_email;
            $headers['To'] = $user_email;
        }
        $headers['Date'] = date('r');
        $headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
        $headers['X-Kolab-Mime-Version'] = kolab_storage::$version;
        $headers['Subject'] = $object['uid'];
//        $headers['Message-ID'] = $rcmail->gen_message_id();
        $headers['User-Agent'] = $rcmail->config->get('useragent');

        // Check if we have enough memory to handle the message in it
        // It's faster than using files, so we'll do this if we only can
        if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) {
            $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB

            foreach ($object['_attachments'] as $attachment) {
                $memory += $attachment['size'];
            }

            // 1.33 is for base64, we need at least 4x more memory than the message size
            if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) {
                $marker   = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%';
                $is_file  = true;
                $temp_dir = unslashify($rcmail->config->get('temp_dir'));
                $mime->setParam('delay_file_io', true);
            }
        }

        $mime->headers($headers);
        $mime->setTXTBody("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 http://www.kolab.org/\n\n");

        $ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE;
        // Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines"
        // when APPENDing from temp file
        $xml = preg_replace('/\r?\n/', "\r\n", $xml);

        $mime->addAttachment($xml,  // file
            $ctype,                 // content-type
            'kolab.xml',            // filename
            false,                  // is_file
            '8bit',                 // encoding
            'attachment',           // disposition
            RCUBE_CHARSET           // charset
        );
        $part_id++;

        // save object attachments as separate parts
        foreach ((array)$object['_attachments'] as $key => $att) {
            if (empty($att['content']) && !empty($att['id'])) {
                // @TODO: use IMAP CATENATE to skip attachment fetch+push operation
                $msguid = $object['_copyfrom'] ?: ($object['_msguid'] ?: $object['uid']);
                if ($is_file) {
                    $att['path'] = tempnam($temp_dir, 'rcmAttmnt');
                    if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {
                        fclose($fp);
                    }
                    else {
                        return false;
                    }
                }
                else {
                    $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true);
                }
            }

            $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable'));
            $name = !empty($att['name']) ? $att['name'] : $key;

            // To store binary files we can use faster method
            // without writting full message content to a temporary file but
            // directly to IMAP, see rcube_imap_generic::append().
            // I.e. use file handles where possible
            if (!empty($att['path'])) {
                if ($is_file && $binary) {
                    $files[] = fopen($att['path'], 'r');
                    $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
                }
                else {
                    $mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
                }
            }
            else {
                if (is_resource($att['content']) && $is_file && $binary) {
                    $files[] = $att['content'];
                    $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
                }
                else {
                    if (is_resource($att['content'])) {
                        @rewind($att['content']);
                        $att['content'] = stream_get_contents($att['content']);
                    }
                    $mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
                }
            }

            $object['_attachments'][$key]['id'] = ++$part_id;
        }

        if (!$is_file || !empty($files)) {
            $message = $mime->getMessage();
        }

        // parse message and build message array with
        // attachment file pointers in place of file markers
        if (!empty($files)) {
            $message = explode($marker, $message);
            $tmp     = array();

            foreach ($message as $msg_part) {
                $tmp[] = $msg_part;
                if ($file = array_shift($files)) {
                    $tmp[] = $file;
                }
            }
            $message = $tmp;
        }
        // write complete message body into temp file
        else if ($is_file) {
            // use common temp dir
            $body_file = tempnam($temp_dir, 'rcmMsg');

            if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) {
                rcube::raise_error(array('code' => 650, 'type' => 'php',
                    'file' => __FILE__, 'line' => __LINE__,
                    'message' => "Could not create message: ".$mime_result->getMessage()),
                    true, false);
                return false;
            }

            $message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r'));
        }

        return $message;
    }

    /**
     * Triggers any required updates after changes within the
     * folder. This is currently only required for handling free/busy
     * information with Kolab.
     *
     * @return boolean|PEAR_Error True if successfull.
     */
    public function trigger()
    {
        $owner = $this->get_owner();
        $result = false;

        switch($this->type) {
        case 'event':
            if ($this->get_namespace() == 'personal') {
                $result = $this->trigger_url(
                    sprintf('%s/trigger/%s/%s.pfb',
                        kolab_storage::get_freebusy_server(),
                        urlencode($owner),
                        urlencode($this->imap->mod_folder($this->name))
                    ),
                    $this->imap->options['user'],
                    $this->imap->options['password']
                );
            }
            break;

        default:
            return true;
        }

        if ($result && is_object($result) && is_a($result, 'PEAR_Error')) {
            return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s",
                                            $this->name, $result->getMessage()));
        }

        return $result;
    }

    /**
     * Triggers a URL.
     *
     * @param string $url          The URL to be triggered.
     * @param string $auth_user    Username to authenticate with
     * @param string $auth_passwd  Password for basic auth
     * @return boolean|PEAR_Error  True if successfull.
     */
    private function trigger_url($url, $auth_user = null, $auth_passwd = null)
    {
        try {
            $request = libkolab::http_request($url);

            // set authentication credentials
            if ($auth_user && $auth_passwd)
                $request->setAuth($auth_user, $auth_passwd);

            $result = $request->send();
            // rcube::write_log('trigger', $result->getBody());
        }
        catch (Exception $e) {
            return PEAR::raiseError($e->getMessage());
        }

        return true;
    }

    /**
     * Log content to a file in per_user_loggin dir if configured
     */
    private static function save_user_xml($filename, $content)
    {
        $rcmail = rcube::get_instance();

        if ($rcmail->config->get('kolab_format_error_log')) {
            $log_dir   = $rcmail->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
            $user_name = $rcmail->get_user_name();
            $log_dir   = $log_dir . '/' . $user_name;

            if (!empty($user_name) && is_writable($log_dir)) {
                file_put_contents("$log_dir/$filename", $content);
            }
        }
    }
}