2 commits - lib/init.php lib/kolab lib/kolab_sync.php lib/plugins

Aleksander Machniak machniak at kolabsys.com
Fri Nov 16 18:26:02 CET 2012


 lib/init.php                                                      |   10 
 lib/kolab/Horde_Date.php                                          | 1304 -------
 lib/kolab/Horde_Date_Recurrence.php                               | 1673 ----------
 lib/kolab/Horde_Kolab_Format_XML_configuration.php                |   76 
 lib/kolab/kolab_date_recurrence.php                               |  171 -
 lib/kolab/kolab_format.php                                        |  330 -
 lib/kolab/kolab_format_configuration.php                          |  163 
 lib/kolab/kolab_format_contact.php                                |  509 ---
 lib/kolab/kolab_format_distributionlist.php                       |  147 
 lib/kolab/kolab_format_event.php                                  |  303 -
 lib/kolab/kolab_format_journal.php                                |  112 
 lib/kolab/kolab_format_note.php                                   |  111 
 lib/kolab/kolab_format_task.php                                   |  145 
 lib/kolab/kolab_format_xcal.php                                   |  396 --
 lib/kolab/kolab_storage.php                                       |  652 ---
 lib/kolab/kolab_storage_cache.php                                 |  728 ----
 lib/kolab/kolab_storage_folder.php                                |  838 -----
 lib/kolab_sync.php                                                |    7 
 lib/plugins/kolab_auth/LICENSE                                    |  661 +++
 lib/plugins/kolab_auth/config.inc.php.dist                        |   64 
 lib/plugins/kolab_auth/kolab_auth.php                             |  531 +++
 lib/plugins/kolab_auth/localization/de_CH.inc                     |    5 
 lib/plugins/kolab_auth/localization/de_DE.inc                     |    5 
 lib/plugins/kolab_auth/localization/en_US.inc                     |    5 
 lib/plugins/kolab_auth/localization/pl_PL.inc                     |    5 
 lib/plugins/kolab_auth/package.xml                                |   59 
 lib/plugins/kolab_folders/LICENSE                                 |  661 +++
 lib/plugins/kolab_folders/config.inc.php.dist                     |   34 
 lib/plugins/kolab_folders/kolab_folders.js                        |   65 
 lib/plugins/kolab_folders/kolab_folders.php                       |  543 +++
 lib/plugins/kolab_folders/localization/de_CH.inc                  |   24 
 lib/plugins/kolab_folders/localization/de_DE.inc                  |   24 
 lib/plugins/kolab_folders/localization/en_US.inc                  |   24 
 lib/plugins/kolab_folders/localization/pl_PL.inc                  |   21 
 lib/plugins/kolab_folders/package.xml                             |   63 
 lib/plugins/libkolab/README                                       |   43 
 lib/plugins/libkolab/SQL/mysql.sql                                |   25 
 lib/plugins/libkolab/bin/Date_Recurrence_weekday.diff             |  325 +
 lib/plugins/libkolab/bin/Date_last_weekday.diff                   |   37 
 lib/plugins/libkolab/bin/get_horde_date.sh                        |   64 
 lib/plugins/libkolab/bin/modcache.sh                              |  191 +
 lib/plugins/libkolab/config.inc.php.dist                          |    9 
 lib/plugins/libkolab/lib/Horde_Date.php                           | 1304 +++++++
 lib/plugins/libkolab/lib/Horde_Date_Recurrence.php                | 1673 ++++++++++
 lib/plugins/libkolab/lib/Horde_Kolab_Format_XML_configuration.php |   76 
 lib/plugins/libkolab/lib/kolab_date_recurrence.php                |  171 +
 lib/plugins/libkolab/lib/kolab_format.php                         |  330 +
 lib/plugins/libkolab/lib/kolab_format_configuration.php           |  163 
 lib/plugins/libkolab/lib/kolab_format_contact.php                 |  509 +++
 lib/plugins/libkolab/lib/kolab_format_distributionlist.php        |  147 
 lib/plugins/libkolab/lib/kolab_format_event.php                   |  303 +
 lib/plugins/libkolab/lib/kolab_format_journal.php                 |  112 
 lib/plugins/libkolab/lib/kolab_format_note.php                    |  111 
 lib/plugins/libkolab/lib/kolab_format_task.php                    |  145 
 lib/plugins/libkolab/lib/kolab_format_xcal.php                    |  396 ++
 lib/plugins/libkolab/lib/kolab_storage.php                        |  645 +++
 lib/plugins/libkolab/lib/kolab_storage_cache.php                  |  728 ++++
 lib/plugins/libkolab/lib/kolab_storage_folder.php                 |  838 +++++
 lib/plugins/libkolab/libkolab.php                                 |   75 
 59 files changed, 11225 insertions(+), 7664 deletions(-)

New commits:
commit 71c9b0d52b8f038449bba4ccc2919acb9907924c
Author: Aleksander Machniak <alec at alec.pl>
Date:   Fri Nov 16 18:25:37 2012 +0100

    Improve Roundcube Framework classes autoloading

diff --git a/lib/init.php b/lib/init.php
index b58b7e7..9105152 100644
--- a/lib/init.php
+++ b/lib/init.php
@@ -50,7 +50,6 @@ foreach ($config as $optname => $optval) {
 // Define include path
 $include_path  = INSTALL_PATH . 'lib' . PATH_SEPARATOR;
 $include_path .= INSTALL_PATH . 'lib/ext' . PATH_SEPARATOR;
-$include_path .= INSTALL_PATH . 'lib/ext/Roundcube' . PATH_SEPARATOR;
 $include_path .= ini_get('include_path');
 set_include_path($include_path);
 
@@ -64,7 +63,7 @@ if (extension_loaded('mbstring')) {
 }
 
 // include global functions from Roundcube Framework
-require_once 'rcube_shared.inc';
+require_once 'Roundcube/rcube_shared.inc';
 
 // Register main autoloader
 spl_autoload_register('kolab_sync_autoload');
@@ -89,13 +88,15 @@ function kolab_sync_autoload($classname)
             '/Net_(.+)/',
             '/Auth_(.+)/',
             '/^html_.+/',
+            '/^rcube(.*)/',
             '/^utf8$/',
         ),
         array(
             'Mail/\\1',
             'Net/\\1',
             'Auth/\\1',
-            'html',
+            'Roundcube/html',
+            'Roundcube/rcube\\1',
             'utf8.class',
         ),
         $classname


commit 16f1cbc58b80233d09934408d753ad9550197cbb
Author: Aleksander Machniak <alec at alec.pl>
Date:   Fri Nov 16 15:36:25 2012 +0100

    Better dependent code separation, removed /lib/kolab added lib/plugins

diff --git a/lib/init.php b/lib/init.php
index 8a96b68..b58b7e7 100644
--- a/lib/init.php
+++ b/lib/init.php
@@ -29,7 +29,7 @@ define('RCMAIL_VERSION', '0.9-git');
 define('RCMAIL_CHARSET', 'UTF-8');
 define('INSTALL_PATH', realpath(dirname(__FILE__) . '/../') . '/');
 define('RCMAIL_CONFIG_DIR', INSTALL_PATH . 'config');
-define('RCMAIL_PLUGINS_DIR', INSTALL_PATH . 'plugins');
+define('RCMAIL_PLUGINS_DIR', INSTALL_PATH . 'lib/plugins');
 
 // PHP configuration
 $config = array(
@@ -50,7 +50,6 @@ foreach ($config as $optname => $optval) {
 // Define include path
 $include_path  = INSTALL_PATH . 'lib' . PATH_SEPARATOR;
 $include_path .= INSTALL_PATH . 'lib/ext' . PATH_SEPARATOR;
-$include_path .= INSTALL_PATH . 'lib/kolab' . PATH_SEPARATOR;
 $include_path .= INSTALL_PATH . 'lib/ext/Roundcube' . PATH_SEPARATOR;
 $include_path .= ini_get('include_path');
 set_include_path($include_path);
diff --git a/lib/kolab/Horde_Date.php b/lib/kolab/Horde_Date.php
deleted file mode 100644
index 9197f84..0000000
--- a/lib/kolab/Horde_Date.php
+++ /dev/null
@@ -1,1304 +0,0 @@
-<?php
-
-/**
- * This is a concatenated copy of the following files:
- *   Horde/Date/Utils.php, Horde/Date/Recurrence.php
- * Pull the latest version of these files from the PEAR channel of the Horde
- * project at http://pear.horde.org by installing the Horde_Date package.
- */
-
-
-/**
- * Horde Date wrapper/logic class, including some calculation
- * functions.
- *
- * @category Horde
- * @package  Date
- *
- * @TODO in format():
- *   http://php.net/intldateformatter
- *
- * @TODO on timezones:
- *   http://trac.agavi.org/ticket/1008
- *   http://trac.agavi.org/changeset/3659
- *
- * @TODO on switching to PHP::DateTime:
- *   The only thing ever stored in the database *IS* Unix timestamps. Doing
- *   anything other than that is unmanageable, yet some frameworks use 'server
- *   based' times in their systems, simply because they do not bother with
- *   daylight saving and only 'serve' one timezone!
- *
- *   The second you have to manage 'real' time across timezones then daylight
- *   saving becomes essential, BUT only on the display side! Since the browser
- *   only provides a time offset, this is useless and to be honest should simply
- *   be ignored ( until it is upgraded to provide the correct information ;)
- *   ). So we need a 'display' function that takes a simple numeric epoch, and a
- *   separate timezone id into which the epoch is to be 'converted'. My W3C
- *   mapping works simply because ADOdb then converts that to it's own simple
- *   offset abbreviation - in my case GMT or BST. As long as DateTime passes the
- *   full 64 bit number the date range from 100AD is also preserved ( and
- *   further back if 2 digit years are disabled ). If I want to display the
- *   'real' timezone with this 'time' then I just add it in place of ADOdb's
- *   'timezone'. I am tempted to simply adjust the ADOdb class to take a
- *   timezone in place of the simple GMT switch it currently uses.
- *
- *   The return path is just the reverse and simply needs to take the client
- *   display offset off prior to storage of the UTC epoch. SO we use
- *   DateTimeZone to get an offset value for the clients timezone and simply add
- *   or subtract this from a timezone agnostic display on the client end when
- *   entering new times.
- *
- *
- *   It's not really feasible to store dates in specific timezone, as most
- *   national/local timezones support DST - and that is a pain to support, as
- *   eg.  sorting breaks when some timestamps get repeated. That's why it's
- *   usually better to store datetimes as either UTC datetime or plain unix
- *   timestamp. I usually go with the former - using database datetime type.
- */
-
-/**
- * @category Horde
- * @package  Date
- */
-class Horde_Date
-{
-    const DATE_SUNDAY = 0;
-    const DATE_MONDAY = 1;
-    const DATE_TUESDAY = 2;
-    const DATE_WEDNESDAY = 3;
-    const DATE_THURSDAY = 4;
-    const DATE_FRIDAY = 5;
-    const DATE_SATURDAY = 6;
-
-    const MASK_SUNDAY = 1;
-    const MASK_MONDAY = 2;
-    const MASK_TUESDAY = 4;
-    const MASK_WEDNESDAY = 8;
-    const MASK_THURSDAY = 16;
-    const MASK_FRIDAY = 32;
-    const MASK_SATURDAY = 64;
-    const MASK_WEEKDAYS = 62;
-    const MASK_WEEKEND = 65;
-    const MASK_ALLDAYS = 127;
-
-    const MASK_SECOND = 1;
-    const MASK_MINUTE = 2;
-    const MASK_HOUR = 4;
-    const MASK_DAY = 8;
-    const MASK_MONTH = 16;
-    const MASK_YEAR = 32;
-    const MASK_ALLPARTS = 63;
-
-    const DATE_DEFAULT = 'Y-m-d H:i:s';
-    const DATE_JSON = 'Y-m-d\TH:i:s';
-
-    /**
-     * Year
-     *
-     * @var integer
-     */
-    protected $_year;
-
-    /**
-     * Month
-     *
-     * @var integer
-     */
-    protected $_month;
-
-    /**
-     * Day
-     *
-     * @var integer
-     */
-    protected $_mday;
-
-    /**
-     * Hour
-     *
-     * @var integer
-     */
-    protected $_hour = 0;
-
-    /**
-     * Minute
-     *
-     * @var integer
-     */
-    protected $_min = 0;
-
-    /**
-     * Second
-     *
-     * @var integer
-     */
-    protected $_sec = 0;
-
-    /**
-     * String representation of the date's timezone.
-     *
-     * @var string
-     */
-    protected $_timezone;
-
-    /**
-     * Default format for __toString()
-     *
-     * @var string
-     */
-    protected $_defaultFormat = self::DATE_DEFAULT;
-
-    /**
-     * Default specs that are always supported.
-     * @var string
-     */
-    protected static $_defaultSpecs = '%CdDeHImMnRStTyY';
-
-    /**
-     * Internally supported strftime() specifiers.
-     * @var string
-     */
-    protected static $_supportedSpecs = '';
-
-    /**
-     * Map of required correction masks.
-     *
-     * @see __set()
-     *
-     * @var array
-     */
-    protected static $_corrections = array(
-        'year'  => self::MASK_YEAR,
-        'month' => self::MASK_MONTH,
-        'mday'  => self::MASK_DAY,
-        'hour'  => self::MASK_HOUR,
-        'min'   => self::MASK_MINUTE,
-        'sec'   => self::MASK_SECOND,
-    );
-
-    protected $_formatCache = array();
-
-    /**
-     * Builds a new date object. If $date contains date parts, use them to
-     * initialize the object.
-     *
-     * Recognized formats:
-     * - arrays with keys 'year', 'month', 'mday', 'day'
-     *   'hour', 'min', 'minute', 'sec'
-     * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
-     * - yyyy-mm-dd hh:mm:ss
-     * - yyyymmddhhmmss
-     * - yyyymmddThhmmssZ
-     * - yyyymmdd (might conflict with unix timestamps between 31 Oct 1966 and
-     *   03 Mar 1973)
-     * - unix timestamps
-     * - anything parsed by strtotime()/DateTime.
-     *
-     * @throws Horde_Date_Exception
-     */
-    public function __construct($date = null, $timezone = null)
-    {
-        if (!self::$_supportedSpecs) {
-            self::$_supportedSpecs = self::$_defaultSpecs;
-            if (function_exists('nl_langinfo')) {
-                self::$_supportedSpecs .= 'bBpxX';
-            }
-        }
-
-        if (func_num_args() > 2) {
-            // Handle args in order: year month day hour min sec tz
-            $this->_initializeFromArgs(func_get_args());
-            return;
-        }
-
-        $this->_initializeTimezone($timezone);
-
-        if (is_null($date)) {
-            return;
-        }
-
-        if (is_string($date)) {
-            $date = trim($date, '"');
-        }
-
-        if (is_object($date)) {
-            $this->_initializeFromObject($date);
-        } elseif (is_array($date)) {
-            $this->_initializeFromArray($date);
-        } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $date, $parts)) {
-            $this->_year  = (int)$parts[1];
-            $this->_month = (int)$parts[2];
-            $this->_mday  = (int)$parts[3];
-            $this->_hour  = (int)$parts[4];
-            $this->_min   = (int)$parts[5];
-            $this->_sec   = (int)$parts[6];
-            if ($parts[7]) {
-                $this->_initializeTimezone('UTC');
-            }
-        } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $date, $parts) &&
-                  $parts[2] > 0 && $parts[2] <= 12 &&
-                  $parts[3] > 0 && $parts[3] <= 31) {
-            $this->_year  = (int)$parts[1];
-            $this->_month = (int)$parts[2];
-            $this->_mday  = (int)$parts[3];
-            $this->_hour = $this->_min = $this->_sec = 0;
-        } elseif ((string)(int)$date == $date) {
-            // Try as a timestamp.
-            $parts = @getdate($date);
-            if ($parts) {
-                $this->_year  = $parts['year'];
-                $this->_month = $parts['mon'];
-                $this->_mday  = $parts['mday'];
-                $this->_hour  = $parts['hours'];
-                $this->_min   = $parts['minutes'];
-                $this->_sec   = $parts['seconds'];
-            }
-        } else {
-            // Use date_create() so we can catch errors with PHP 5.2. Use
-            // "new DateTime() once we require 5.3.
-            $parsed = date_create($date);
-            if (!$parsed) {
-                throw new Horde_Date_Exception(sprintf(Horde_Date_Translation::t("Failed to parse time string (%s)"), $date));
-            }
-            $parsed->setTimezone(new DateTimeZone(date_default_timezone_get()));
-            $this->_year  = (int)$parsed->format('Y');
-            $this->_month = (int)$parsed->format('m');
-            $this->_mday  = (int)$parsed->format('d');
-            $this->_hour  = (int)$parsed->format('H');
-            $this->_min   = (int)$parsed->format('i');
-            $this->_sec   = (int)$parsed->format('s');
-            $this->_initializeTimezone(date_default_timezone_get());
-        }
-    }
-
-    /**
-     * Returns a simple string representation of the date object
-     *
-     * @return string  This object converted to a string.
-     */
-    public function __toString()
-    {
-        try {
-            return $this->format($this->_defaultFormat);
-        } catch (Exception $e) {
-            return '';
-        }
-    }
-
-    /**
-     * Returns a DateTime object representing this object.
-     *
-     * @return DateTime
-     */
-    public function toDateTime()
-    {
-        $date = new DateTime(null, new DateTimeZone($this->_timezone));
-        $date->setDate($this->_year, $this->_month, $this->_mday);
-        $date->setTime($this->_hour, $this->_min, $this->_sec);
-        return $date;
-    }
-
-    /**
-     * Converts a date in the proleptic Gregorian calendar to the no of days
-     * since 24th November, 4714 B.C.
-     *
-     * Returns the no of days since Monday, 24th November, 4714 B.C. in the
-     * proleptic Gregorian calendar (which is 24th November, -4713 using
-     * 'Astronomical' year numbering, and 1st January, 4713 B.C. in the
-     * proleptic Julian calendar).  This is also the first day of the 'Julian
-     * Period' proposed by Joseph Scaliger in 1583, and the number of days
-     * since this date is known as the 'Julian Day'.  (It is not directly
-     * to do with the Julian calendar, although this is where the name
-     * is derived from.)
-     *
-     * The algorithm is valid for all years (positive and negative), and
-     * also for years preceding 4714 B.C.
-     *
-     * Algorithm is from PEAR::Date_Calc
-     *
-     * @author Monte Ohrt <monte at ispi.net>
-     * @author Pierre-Alain Joye <pajoye at php.net>
-     * @author Daniel Convissor <danielc at php.net>
-     * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
-     *
-     * @return integer  The number of days since 24th November, 4714 B.C.
-     */
-    public function toDays()
-    {
-        if (function_exists('GregorianToJD')) {
-            return gregoriantojd($this->_month, $this->_mday, $this->_year);
-        }
-
-        $day = $this->_mday;
-        $month = $this->_month;
-        $year = $this->_year;
-
-        if ($month > 2) {
-            // March = 0, April = 1, ..., December = 9,
-            // January = 10, February = 11
-            $month -= 3;
-        } else {
-            $month += 9;
-            --$year;
-        }
-
-        $hb_negativeyear = $year < 0;
-        $century         = intval($year / 100);
-        $year            = $year % 100;
-
-        if ($hb_negativeyear) {
-            // Subtract 1 because year 0 is a leap year;
-            // And N.B. that we must treat the leap years as occurring
-            // one year earlier than they do, because for the purposes
-            // of calculation, the year starts on 1st March:
-            //
-            return intval((14609700 * $century + ($year == 0 ? 1 : 0)) / 400) +
-                   intval((1461 * $year + 1) / 4) +
-                   intval((153 * $month + 2) / 5) +
-                   $day + 1721118;
-        } else {
-            return intval(146097 * $century / 4) +
-                   intval(1461 * $year / 4) +
-                   intval((153 * $month + 2) / 5) +
-                   $day + 1721119;
-        }
-    }
-
-    /**
-     * Converts number of days since 24th November, 4714 B.C. (in the proleptic
-     * Gregorian calendar, which is year -4713 using 'Astronomical' year
-     * numbering) to Gregorian calendar date.
-     *
-     * Returned date belongs to the proleptic Gregorian calendar, using
-     * 'Astronomical' year numbering.
-     *
-     * The algorithm is valid for all years (positive and negative), and
-     * also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'),
-     * and so the only limitation is platform-dependent (for 32-bit systems
-     * the maximum year would be something like about 1,465,190 A.D.).
-     *
-     * N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'.
-     *
-     * Algorithm is from PEAR::Date_Calc
-     *
-     * @author Monte Ohrt <monte at ispi.net>
-     * @author Pierre-Alain Joye <pajoye at php.net>
-     * @author Daniel Convissor <danielc at php.net>
-     * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
-     *
-     * @param int    $days   the number of days since 24th November, 4714 B.C.
-     * @param string $format the string indicating how to format the output
-     *
-     * @return  Horde_Date  A Horde_Date object representing the date.
-     */
-    public static function fromDays($days)
-    {
-        if (function_exists('JDToGregorian')) {
-            list($month, $day, $year) = explode('/', JDToGregorian($days));
-        } else {
-            $days = intval($days);
-
-            $days   -= 1721119;
-            $century = floor((4 * $days - 1) / 146097);
-            $days    = floor(4 * $days - 1 - 146097 * $century);
-            $day     = floor($days / 4);
-
-            $year = floor((4 * $day +  3) / 1461);
-            $day  = floor(4 * $day +  3 - 1461 * $year);
-            $day  = floor(($day +  4) / 4);
-
-            $month = floor((5 * $day - 3) / 153);
-            $day   = floor(5 * $day - 3 - 153 * $month);
-            $day   = floor(($day +  5) /  5);
-
-            $year = $century * 100 + $year;
-            if ($month < 10) {
-                $month +=3;
-            } else {
-                $month -=9;
-                ++$year;
-            }
-        }
-
-        return new Horde_Date($year, $month, $day);
-    }
-
-    /**
-     * Getter for the date and time properties.
-     *
-     * @param string $name  One of 'year', 'month', 'mday', 'hour', 'min' or
-     *                      'sec'.
-     *
-     * @return integer  The property value, or null if not set.
-     */
-    public function __get($name)
-    {
-        if ($name == 'day') {
-            $name = 'mday';
-        }
-
-        return $this->{'_' . $name};
-    }
-
-    /**
-     * Setter for the date and time properties.
-     *
-     * @param string $name    One of 'year', 'month', 'mday', 'hour', 'min' or
-     *                        'sec'.
-     * @param integer $value  The property value.
-     */
-    public function __set($name, $value)
-    {
-        if ($name == 'timezone') {
-            $this->_initializeTimezone($value);
-            return;
-        }
-        if ($name == 'day') {
-            $name = 'mday';
-        }
-
-        if ($name != 'year' && $name != 'month' && $name != 'mday' &&
-            $name != 'hour' && $name != 'min' && $name != 'sec') {
-            throw new InvalidArgumentException('Undefined property ' . $name);
-        }
-
-        $down = $value < $this->{'_' . $name};
-        $this->{'_' . $name} = $value;
-        $this->_correct(self::$_corrections[$name], $down);
-        $this->_formatCache = array();
-    }
-
-    /**
-     * Returns whether a date or time property exists.
-     *
-     * @param string $name  One of 'year', 'month', 'mday', 'hour', 'min' or
-     *                      'sec'.
-     *
-     * @return boolen  True if the property exists and is set.
-     */
-    public function __isset($name)
-    {
-        if ($name == 'day') {
-            $name = 'mday';
-        }
-        return ($name == 'year' || $name == 'month' || $name == 'mday' ||
-                $name == 'hour' || $name == 'min' || $name == 'sec') &&
-            isset($this->{'_' . $name});
-    }
-
-    /**
-     * Adds a number of seconds or units to this date, returning a new Date
-     * object.
-     */
-    public function add($factor)
-    {
-        $d = clone($this);
-        if (is_array($factor) || is_object($factor)) {
-            foreach ($factor as $property => $value) {
-                $d->$property += $value;
-            }
-        } else {
-            $d->sec += $factor;
-        }
-
-        return $d;
-    }
-
-    /**
-     * Subtracts a number of seconds or units from this date, returning a new
-     * Horde_Date object.
-     */
-    public function sub($factor)
-    {
-        if (is_array($factor)) {
-            foreach ($factor as &$value) {
-                $value *= -1;
-            }
-        } else {
-            $factor *= -1;
-        }
-
-        return $this->add($factor);
-    }
-
-    /**
-     * Converts this object to a different timezone.
-     *
-     * @param string $timezone  The new timezone.
-     *
-     * @return Horde_Date  This object.
-     */
-    public function setTimezone($timezone)
-    {
-        $date = $this->toDateTime();
-        $date->setTimezone(new DateTimeZone($timezone));
-        $this->_timezone = $timezone;
-        $this->_year     = (int)$date->format('Y');
-        $this->_month    = (int)$date->format('m');
-        $this->_mday     = (int)$date->format('d');
-        $this->_hour     = (int)$date->format('H');
-        $this->_min      = (int)$date->format('i');
-        $this->_sec      = (int)$date->format('s');
-        $this->_formatCache = array();
-        return $this;
-    }
-
-    /**
-     * Sets the default date format used in __toString()
-     *
-     * @param string $format
-     */
-    public function setDefaultFormat($format)
-    {
-        $this->_defaultFormat = $format;
-    }
-
-    /**
-     * Returns the day of the week (0 = Sunday, 6 = Saturday) of this date.
-     *
-     * @return integer  The day of the week.
-     */
-    public function dayOfWeek()
-    {
-        if ($this->_month > 2) {
-            $month = $this->_month - 2;
-            $year = $this->_year;
-        } else {
-            $month = $this->_month + 10;
-            $year = $this->_year - 1;
-        }
-
-        $day = (floor((13 * $month - 1) / 5) +
-                $this->_mday + ($year % 100) +
-                floor(($year % 100) / 4) +
-                floor(($year / 100) / 4) - 2 *
-                floor($year / 100) + 77);
-
-        return (int)($day - 7 * floor($day / 7));
-    }
-
-    /**
-     * Returns the day number of the year (1 to 365/366).
-     *
-     * @return integer  The day of the year.
-     */
-    public function dayOfYear()
-    {
-        return $this->format('z') + 1;
-    }
-
-    /**
-     * Returns the week of the month.
-     *
-     * @return integer  The week number.
-     */
-    public function weekOfMonth()
-    {
-        return ceil($this->_mday / 7);
-    }
-
-    /**
-     * Returns the week of the year, first Monday is first day of first week.
-     *
-     * @return integer  The week number.
-     */
-    public function weekOfYear()
-    {
-        return $this->format('W');
-    }
-
-    /**
-     * Returns the number of weeks in the given year (52 or 53).
-     *
-     * @param integer $year  The year to count the number of weeks in.
-     *
-     * @return integer $numWeeks   The number of weeks in $year.
-     */
-    public static function weeksInYear($year)
-    {
-        // Find the last Thursday of the year.
-        $date = new Horde_Date($year . '-12-31');
-        while ($date->dayOfWeek() != self::DATE_THURSDAY) {
-            --$date->mday;
-        }
-        return $date->weekOfYear();
-    }
-
-    /**
-     * Sets the date of this object to the $nth weekday of $weekday.
-     *
-     * @param integer $weekday  The day of the week (0 = Sunday, etc).
-     * @param integer $nth      The $nth $weekday to set to (defaults to 1).
-     */
-    public function setNthWeekday($weekday, $nth = 1)
-    {
-        if ($weekday < self::DATE_SUNDAY || $weekday > self::DATE_SATURDAY) {
-            return;
-        }
-
-        if ($nth < 0) {  // last $weekday of month
-            $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
-            $last = $this->dayOfWeek();
-            $this->_mday += ($weekday - $last);
-            if ($this->_mday > $lastday)
-                $this->_mday -= 7;
-        }
-        else {
-            $this->_mday = 1;
-            $first = $this->dayOfWeek();
-            if ($weekday < $first) {
-                $this->_mday = 8 + $weekday - $first;
-            } else {
-                $this->_mday = $weekday - $first + 1;
-            }
-            $diff = 7 * $nth - 7;
-            $this->_mday += $diff;
-            $this->_correct(self::MASK_DAY, $diff < 0);
-        }
-    }
-
-    /**
-     * Is the date currently represented by this object a valid date?
-     *
-     * @return boolean  Validity, counting leap years, etc.
-     */
-    public function isValid()
-    {
-        return ($this->_year >= 0 && $this->_year <= 9999);
-    }
-
-    /**
-     * Compares this date to another date object to see which one is
-     * greater (later). Assumes that the dates are in the same
-     * timezone.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return integer  ==  0 if they are on the same date
-     *                  >=  1 if $this is greater (later)
-     *                  <= -1 if $other is greater (later)
-     */
-    public function compareDate($other)
-    {
-        if (!($other instanceof Horde_Date)) {
-            $other = new Horde_Date($other);
-        }
-
-        if ($this->_year != $other->year) {
-            return $this->_year - $other->year;
-        }
-        if ($this->_month != $other->month) {
-            return $this->_month - $other->month;
-        }
-
-        return $this->_mday - $other->mday;
-    }
-
-    /**
-     * Returns whether this date is after the other.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return boolean  True if this date is after the other.
-     */
-    public function after($other)
-    {
-        return $this->compareDate($other) > 0;
-    }
-
-    /**
-     * Returns whether this date is before the other.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return boolean  True if this date is before the other.
-     */
-    public function before($other)
-    {
-        return $this->compareDate($other) < 0;
-    }
-
-    /**
-     * Returns whether this date is the same like the other.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return boolean  True if this date is the same like the other.
-     */
-    public function equals($other)
-    {
-        return $this->compareDate($other) == 0;
-    }
-
-    /**
-     * Compares this to another date object by time, to see which one
-     * is greater (later). Assumes that the dates are in the same
-     * timezone.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return integer  ==  0 if they are at the same time
-     *                  >=  1 if $this is greater (later)
-     *                  <= -1 if $other is greater (later)
-     */
-    public function compareTime($other)
-    {
-        if (!($other instanceof Horde_Date)) {
-            $other = new Horde_Date($other);
-        }
-
-        if ($this->_hour != $other->hour) {
-            return $this->_hour - $other->hour;
-        }
-        if ($this->_min != $other->min) {
-            return $this->_min - $other->min;
-        }
-
-        return $this->_sec - $other->sec;
-    }
-
-    /**
-     * Compares this to another date object, including times, to see
-     * which one is greater (later). Assumes that the dates are in the
-     * same timezone.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return integer  ==  0 if they are equal
-     *                  >=  1 if $this is greater (later)
-     *                  <= -1 if $other is greater (later)
-     */
-    public function compareDateTime($other)
-    {
-        if (!($other instanceof Horde_Date)) {
-            $other = new Horde_Date($other);
-        }
-
-        if ($diff = $this->compareDate($other)) {
-            return $diff;
-        }
-
-        return $this->compareTime($other);
-    }
-
-    /**
-     * Returns number of days between this date and another.
-     *
-     * @param Horde_Date $other  The other day to diff with.
-     *
-     * @return integer  The absolute number of days between the two dates.
-     */
-    public function diff($other)
-    {
-        return abs($this->toDays() - $other->toDays());
-    }
-
-    /**
-     * Returns the time offset for local time zone.
-     *
-     * @param boolean $colon  Place a colon between hours and minutes?
-     *
-     * @return string  Timezone offset as a string in the format +HH:MM.
-     */
-    public function tzOffset($colon = true)
-    {
-        return $colon ? $this->format('P') : $this->format('O');
-    }
-
-    /**
-     * Returns the unix timestamp representation of this date.
-     *
-     * @return integer  A unix timestamp.
-     */
-    public function timestamp()
-    {
-        if ($this->_year >= 1970 && $this->_year < 2038) {
-            return mktime($this->_hour, $this->_min, $this->_sec,
-                          $this->_month, $this->_mday, $this->_year);
-        }
-        return $this->format('U');
-    }
-
-    /**
-     * Returns the unix timestamp representation of this date, 12:00am.
-     *
-     * @return integer  A unix timestamp.
-     */
-    public function datestamp()
-    {
-        if ($this->_year >= 1970 && $this->_year < 2038) {
-            return mktime(0, 0, 0, $this->_month, $this->_mday, $this->_year);
-        }
-        $date = new DateTime($this->format('Y-m-d'));
-        return $date->format('U');
-    }
-
-    /**
-     * Formats date and time to be passed around as a short url parameter.
-     *
-     * @return string  Date and time.
-     */
-    public function dateString()
-    {
-        return sprintf('%04d%02d%02d', $this->_year, $this->_month, $this->_mday);
-    }
-
-    /**
-     * Formats date and time to the ISO format used by JSON.
-     *
-     * @return string  Date and time.
-     */
-    public function toJson()
-    {
-        return $this->format(self::DATE_JSON);
-    }
-
-    /**
-     * Formats date and time to the RFC 2445 iCalendar DATE-TIME format.
-     *
-     * @param boolean $floating  Whether to return a floating date-time
-     *                           (without time zone information).
-     *
-     * @return string  Date and time.
-     */
-    public function toiCalendar($floating = false)
-    {
-        if ($floating) {
-            return $this->format('Ymd\THis');
-        }
-        $dateTime = $this->toDateTime();
-        $dateTime->setTimezone(new DateTimeZone('UTC'));
-        return $dateTime->format('Ymd\THis\Z');
-    }
-
-    /**
-     * Formats time using the specifiers available in date() or in the DateTime
-     * class' format() method.
-     *
-     * To format in languages other than English, use strftime() instead.
-     *
-     * @param string $format
-     *
-     * @return string  Formatted time.
-     */
-    public function format($format)
-    {
-        if (!isset($this->_formatCache[$format])) {
-            $this->_formatCache[$format] = $this->toDateTime()->format($format);
-        }
-        return $this->_formatCache[$format];
-    }
-
-    /**
-     * Formats date and time using strftime() format.
-     *
-     * @return string  strftime() formatted date and time.
-     */
-    public function strftime($format)
-    {
-        if (preg_match('/%[^' . self::$_supportedSpecs . ']/', $format)) {
-            return strftime($format, $this->timestamp());
-        } else {
-            return $this->_strftime($format);
-        }
-    }
-
-    /**
-     * Formats date and time using a limited set of the strftime() format.
-     *
-     * @return string  strftime() formatted date and time.
-     */
-    protected function _strftime($format)
-    {
-        return preg_replace(
-            array('/%b/e',
-                  '/%B/e',
-                  '/%C/e',
-                  '/%d/e',
-                  '/%D/e',
-                  '/%e/e',
-                  '/%H/e',
-                  '/%I/e',
-                  '/%m/e',
-                  '/%M/e',
-                  '/%n/',
-                  '/%p/e',
-                  '/%R/e',
-                  '/%S/e',
-                  '/%t/',
-                  '/%T/e',
-                  '/%x/e',
-                  '/%X/e',
-                  '/%y/e',
-                  '/%Y/',
-                  '/%%/'),
-            array('$this->_strftime(Horde_Nls::getLangInfo(constant(\'ABMON_\' . (int)$this->_month)))',
-                  '$this->_strftime(Horde_Nls::getLangInfo(constant(\'MON_\' . (int)$this->_month)))',
-                  '(int)($this->_year / 100)',
-                  'sprintf(\'%02d\', $this->_mday)',
-                  '$this->_strftime(\'%m/%d/%y\')',
-                  'sprintf(\'%2d\', $this->_mday)',
-                  'sprintf(\'%02d\', $this->_hour)',
-                  'sprintf(\'%02d\', $this->_hour == 0 ? 12 : ($this->_hour > 12 ? $this->_hour - 12 : $this->_hour))',
-                  'sprintf(\'%02d\', $this->_month)',
-                  'sprintf(\'%02d\', $this->_min)',
-                  "\n",
-                  '$this->_strftime(Horde_Nls::getLangInfo($this->_hour < 12 ? AM_STR : PM_STR))',
-                  '$this->_strftime(\'%H:%M\')',
-                  'sprintf(\'%02d\', $this->_sec)',
-                  "\t",
-                  '$this->_strftime(\'%H:%M:%S\')',
-                  '$this->_strftime(Horde_Nls::getLangInfo(D_FMT))',
-                  '$this->_strftime(Horde_Nls::getLangInfo(T_FMT))',
-                  'substr(sprintf(\'%04d\', $this->_year), -2)',
-                  (int)$this->_year,
-                  '%'),
-            $format);
-    }
-
-    /**
-     * Corrects any over- or underflows in any of the date's members.
-     *
-     * @param integer $mask  We may not want to correct some overflows.
-     * @param integer $down  Whether to correct the date up or down.
-     */
-    protected function _correct($mask = self::MASK_ALLPARTS, $down = false)
-    {
-        if ($mask & self::MASK_SECOND) {
-            if ($this->_sec < 0 || $this->_sec > 59) {
-                $mask |= self::MASK_MINUTE;
-
-                $this->_min += (int)($this->_sec / 60);
-                $this->_sec %= 60;
-                if ($this->_sec < 0) {
-                    $this->_min--;
-                    $this->_sec += 60;
-                }
-            }
-        }
-
-        if ($mask & self::MASK_MINUTE) {
-            if ($this->_min < 0 || $this->_min > 59) {
-                $mask |= self::MASK_HOUR;
-
-                $this->_hour += (int)($this->_min / 60);
-                $this->_min %= 60;
-                if ($this->_min < 0) {
-                    $this->_hour--;
-                    $this->_min += 60;
-                }
-            }
-        }
-
-        if ($mask & self::MASK_HOUR) {
-            if ($this->_hour < 0 || $this->_hour > 23) {
-                $mask |= self::MASK_DAY;
-
-                $this->_mday += (int)($this->_hour / 24);
-                $this->_hour %= 24;
-                if ($this->_hour < 0) {
-                    $this->_mday--;
-                    $this->_hour += 24;
-                }
-            }
-        }
-
-        if ($mask & self::MASK_MONTH) {
-            $this->_correctMonth($down);
-            /* When correcting the month, always correct the day too. Months
-             * have different numbers of days. */
-            $mask |= self::MASK_DAY;
-        }
-
-        if ($mask & self::MASK_DAY) {
-            while ($this->_mday > 28 &&
-                   $this->_mday > Horde_Date_Utils::daysInMonth($this->_month, $this->_year)) {
-                if ($down) {
-                    $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month + 1, $this->_year) - Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
-                } else {
-                    $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
-                    $this->_month++;
-                }
-                $this->_correctMonth($down);
-            }
-            while ($this->_mday < 1) {
-                --$this->_month;
-                $this->_correctMonth($down);
-                $this->_mday += Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
-            }
-        }
-    }
-
-    /**
-     * Corrects the current month.
-     *
-     * This cannot be done in _correct() because that would also trigger a
-     * correction of the day, which would result in an infinite loop.
-     *
-     * @param integer $down  Whether to correct the date up or down.
-     */
-    protected function _correctMonth($down = false)
-    {
-        $this->_year += (int)($this->_month / 12);
-        $this->_month %= 12;
-        if ($this->_month < 1) {
-            $this->_year--;
-            $this->_month += 12;
-        }
-    }
-
-    /**
-     * Handles args in order: year month day hour min sec tz
-     */
-    protected function _initializeFromArgs($args)
-    {
-        $tz = (isset($args[6])) ? array_pop($args) : null;
-        $this->_initializeTimezone($tz);
-
-        $args = array_slice($args, 0, 6);
-        $keys = array('year' => 1, 'month' => 1, 'mday' => 1, 'hour' => 0, 'min' => 0, 'sec' => 0);
-        $date = array_combine(array_slice(array_keys($keys), 0, count($args)), $args);
-        $date = array_merge($keys, $date);
-
-        $this->_initializeFromArray($date);
-    }
-
-    protected function _initializeFromArray($date)
-    {
-        if (isset($date['year']) && is_string($date['year']) && strlen($date['year']) == 2) {
-            if ($date['year'] > 70) {
-                $date['year'] += 1900;
-            } else {
-                $date['year'] += 2000;
-            }
-        }
-
-        foreach ($date as $key => $val) {
-            if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
-                $this->{'_'. $key} = (int)$val;
-            }
-        }
-
-        // If $date['day'] is present and numeric we may have been passed
-        // a Horde_Form_datetime array.
-        if (isset($date['day']) &&
-            (string)(int)$date['day'] == $date['day']) {
-            $this->_mday = (int)$date['day'];
-        }
-        // 'minute' key also from Horde_Form_datetime
-        if (isset($date['minute']) &&
-            (string)(int)$date['minute'] == $date['minute']) {
-            $this->_min = (int)$date['minute'];
-        }
-
-        $this->_correct();
-    }
-
-    protected function _initializeFromObject($date)
-    {
-        if ($date instanceof DateTime) {
-            $this->_year  = (int)$date->format('Y');
-            $this->_month = (int)$date->format('m');
-            $this->_mday  = (int)$date->format('d');
-            $this->_hour  = (int)$date->format('H');
-            $this->_min   = (int)$date->format('i');
-            $this->_sec   = (int)$date->format('s');
-            $this->_initializeTimezone($date->getTimezone()->getName());
-        } else {
-            $is_horde_date = $date instanceof Horde_Date;
-            foreach (array('year', 'month', 'mday', 'hour', 'min', 'sec') as $key) {
-                if ($is_horde_date || isset($date->$key)) {
-                    $this->{'_' . $key} = (int)$date->$key;
-                }
-            }
-            if (!$is_horde_date) {
-                $this->_correct();
-            } else {
-                $this->_initializeTimezone($date->timezone);
-            }
-        }
-    }
-
-    protected function _initializeTimezone($timezone)
-    {
-        if (empty($timezone)) {
-            $timezone = date_default_timezone_get();
-        }
-        $this->_timezone = $timezone;
-    }
-
-}
-
-/**
- * @category Horde
- * @package  Date
- */
-
-/**
- * Horde Date wrapper/logic class, including some calculation
- * functions.
- *
- * @category Horde
- * @package  Date
- */
-class Horde_Date_Utils
-{
-    /**
-     * Returns whether a year is a leap year.
-     *
-     * @param integer $year  The year.
-     *
-     * @return boolean  True if the year is a leap year.
-     */
-    public static function isLeapYear($year)
-    {
-        if (strlen($year) != 4 || preg_match('/\D/', $year)) {
-            return false;
-        }
-
-        return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
-    }
-
-    /**
-     * Returns the date of the year that corresponds to the first day of the
-     * given week.
-     *
-     * @param integer $week  The week of the year to find the first day of.
-     * @param integer $year  The year to calculate for.
-     *
-     * @return Horde_Date  The date of the first day of the given week.
-     */
-    public static function firstDayOfWeek($week, $year)
-    {
-        return new Horde_Date(sprintf('%04dW%02d', $year, $week));
-    }
-
-    /**
-     * Returns the number of days in the specified month.
-     *
-     * @param integer $month  The month
-     * @param integer $year   The year.
-     *
-     * @return integer  The number of days in the month.
-     */
-    public static function daysInMonth($month, $year)
-    {
-        static $cache = array();
-        if (!isset($cache[$year][$month])) {
-            $date = new DateTime(sprintf('%04d-%02d-01', $year, $month));
-            $cache[$year][$month] = $date->format('t');
-        }
-        return $cache[$year][$month];
-    }
-
-    /**
-     * Returns a relative, natural language representation of a timestamp
-     *
-     * @todo Wider range of values ... maybe future time as well?
-     * @todo Support minimum resolution parameter.
-     *
-     * @param mixed $time          The time. Any format accepted by Horde_Date.
-     * @param string $date_format  Format to display date if timestamp is
-     *                             more then 1 day old.
-     * @param string $time_format  Format to display time if timestamp is 1
-     *                             day old.
-     *
-     * @return string  The relative time (i.e. 2 minutes ago)
-     */
-    public static function relativeDateTime($time, $date_format = '%x',
-                                            $time_format = '%X')
-    {
-        $date = new Horde_Date($time);
-
-        $delta = time() - $date->timestamp();
-        if ($delta < 60) {
-            return sprintf(Horde_Date_Translation::ngettext("%d second ago", "%d seconds ago", $delta), $delta);
-        }
-
-        $delta = round($delta / 60);
-        if ($delta < 60) {
-            return sprintf(Horde_Date_Translation::ngettext("%d minute ago", "%d minutes ago", $delta), $delta);
-        }
-
-        $delta = round($delta / 60);
-        if ($delta < 24) {
-            return sprintf(Horde_Date_Translation::ngettext("%d hour ago", "%d hours ago", $delta), $delta);
-        }
-
-        if ($delta > 24 && $delta < 48) {
-            $date = new Horde_Date($time);
-            return sprintf(Horde_Date_Translation::t("yesterday at %s"), $date->strftime($time_format));
-        }
-
-        $delta = round($delta / 24);
-        if ($delta < 7) {
-            return sprintf(Horde_Date_Translation::t("%d days ago"), $delta);
-        }
-
-        if (round($delta / 7) < 5) {
-            $delta = round($delta / 7);
-            return sprintf(Horde_Date_Translation::ngettext("%d week ago", "%d weeks ago", $delta), $delta);
-        }
-
-        // Default to the user specified date format.
-        return $date->strftime($date_format);
-    }
-
-    /**
-     * Tries to convert strftime() formatters to date() formatters.
-     *
-     * Unsupported formatters will be removed.
-     *
-     * @param string $format  A strftime() formatting string.
-     *
-     * @return string  A date() formatting string.
-     */
-    public static function strftime2date($format)
-    {
-        $replace = array(
-            '/%a/'  => 'D',
-            '/%A/'  => 'l',
-            '/%d/'  => 'd',
-            '/%e/'  => 'j',
-            '/%j/'  => 'z',
-            '/%u/'  => 'N',
-            '/%w/'  => 'w',
-            '/%U/'  => '',
-            '/%V/'  => 'W',
-            '/%W/'  => '',
-            '/%b/'  => 'M',
-            '/%B/'  => 'F',
-            '/%h/'  => 'M',
-            '/%m/'  => 'm',
-            '/%C/'  => '',
-            '/%g/'  => '',
-            '/%G/'  => 'o',
-            '/%y/'  => 'y',
-            '/%Y/'  => 'Y',
-            '/%H/'  => 'H',
-            '/%I/'  => 'h',
-            '/%i/'  => 'g',
-            '/%M/'  => 'i',
-            '/%p/'  => 'A',
-            '/%P/'  => 'a',
-            '/%r/'  => 'h:i:s A',
-            '/%R/'  => 'H:i',
-            '/%S/'  => 's',
-            '/%T/'  => 'H:i:s',
-            '/%X/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(T_FMT))',
-            '/%z/'  => 'O',
-            '/%Z/'  => '',
-            '/%c/'  => '',
-            '/%D/'  => 'm/d/y',
-            '/%F/'  => 'Y-m-d',
-            '/%s/'  => 'U',
-            '/%x/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(D_FMT))',
-            '/%n/'  => "\n",
-            '/%t/'  => "\t",
-            '/%%/'  => '%'
-        );
-
-        return preg_replace(array_keys($replace), array_values($replace), $format);
-    }
-
-}
diff --git a/lib/kolab/Horde_Date_Recurrence.php b/lib/kolab/Horde_Date_Recurrence.php
deleted file mode 100644
index 81f0857..0000000
--- a/lib/kolab/Horde_Date_Recurrence.php
+++ /dev/null
@@ -1,1673 +0,0 @@
-<?php
-
-/**
- * This is a modified copy of Horde/Date/Recurrence.php
- * Pull the latest version of this file from the PEAR channel of the Horde
- * project at http://pear.horde.org by installing the Horde_Date package.
- */
-
-if (!class_exists('Horde_Date'))
-	require_once(dirname(__FILE__) . '/Horde_Date.php');
-
-// minimal required implementation of Horde_Date_Translation to avoid a huge dependency nightmare
-class Horde_Date_Translation
-{
-	function t($arg) { return $arg; }
-	function ngettext($sing, $plur, $num) { return ($num > 1 ? $plur : $sing); }
-}
-
-
-/**
- * This file contains the Horde_Date_Recurrence class and according constants.
- *
- * Copyright 2007-2012 Horde LLC (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.horde.org/licenses/lgpl21.
- *
- * @category Horde
- * @package  Date
- */
-
-/**
- * The Horde_Date_Recurrence class implements algorithms for calculating
- * recurrences of events, including several recurrence types, intervals,
- * exceptions, and conversion from and to vCalendar and iCalendar recurrence
- * rules.
- *
- * All methods expecting dates as parameters accept all values that the
- * Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date
- * object, an ISO time string or a hash.
- *
- * @author   Jan Schneider <jan at horde.org>
- * @category Horde
- * @package  Date
- */
-class Horde_Date_Recurrence
-{
-    /** No Recurrence **/
-    const RECUR_NONE = 0;
-
-    /** Recurs daily. */
-    const RECUR_DAILY = 1;
-
-    /** Recurs weekly. */
-    const RECUR_WEEKLY = 2;
-
-    /** Recurs monthly on the same date. */
-    const RECUR_MONTHLY_DATE = 3;
-
-    /** Recurs monthly on the same week day. */
-    const RECUR_MONTHLY_WEEKDAY = 4;
-
-    /** Recurs yearly on the same date. */
-    const RECUR_YEARLY_DATE = 5;
-
-    /** Recurs yearly on the same day of the year. */
-    const RECUR_YEARLY_DAY = 6;
-
-    /** Recurs yearly on the same week day. */
-    const RECUR_YEARLY_WEEKDAY = 7;
-
-    /**
-     * The start time of the event.
-     *
-     * @var Horde_Date
-     */
-    public $start;
-
-    /**
-     * The end date of the recurrence interval.
-     *
-     * @var Horde_Date
-     */
-    public $recurEnd = null;
-
-    /**
-     * The number of recurrences.
-     *
-     * @var integer
-     */
-    public $recurCount = null;
-
-    /**
-     * The type of recurrence this event follows. RECUR_* constant.
-     *
-     * @var integer
-     */
-    public $recurType = self::RECUR_NONE;
-
-    /**
-     * The length of time between recurrences. The time unit depends on the
-     * recurrence type.
-     *
-     * @var integer
-     */
-    public $recurInterval = 1;
-
-    /**
-     * Any additional recurrence data.
-     *
-     * @var integer
-     */
-    public $recurData = null;
-
-    /**
-     * BYDAY recurrence number
-     *
-     * @var integer
-     */
-    public $recurNthDay = null;
-
-    /**
-     * BYMONTH recurrence data
-     *
-     * @var array
-     */
-    public $recurMonths = array();
-
-    /**
-     * All the exceptions from recurrence for this event.
-     *
-     * @var array
-     */
-    public $exceptions = array();
-
-    /**
-     * All the dates this recurrence has been marked as completed.
-     *
-     * @var array
-     */
-    public $completions = array();
-
-    /**
-     * Constructor.
-     *
-     * @param Horde_Date $start  Start of the recurring event.
-     */
-    public function __construct($start)
-    {
-        $this->start = new Horde_Date($start);
-    }
-
-    /**
-     * Resets the class properties.
-     */
-    public function reset()
-    {
-        $this->recurEnd = null;
-        $this->recurCount = null;
-        $this->recurType = self::RECUR_NONE;
-        $this->recurInterval = 1;
-        $this->recurData = null;
-        $this->exceptions = array();
-        $this->completions = array();
-    }
-
-    /**
-     * Checks if this event recurs on a given day of the week.
-     *
-     * @param integer $dayMask  A mask consisting of Horde_Date::MASK_*
-     *                          constants specifying the day(s) to check.
-     *
-     * @return boolean  True if this event recurs on the given day(s).
-     */
-    public function recurOnDay($dayMask)
-    {
-        return ($this->recurData & $dayMask);
-    }
-
-    /**
-     * Specifies the days this event recurs on.
-     *
-     * @param integer $dayMask  A mask consisting of Horde_Date::MASK_*
-     *                          constants specifying the day(s) to recur on.
-     */
-    public function setRecurOnDay($dayMask)
-    {
-        $this->recurData = $dayMask;
-    }
-
-    /**
-     *
-     * @param integer $nthDay The nth weekday of month to repeat events on
-     */
-    public function setRecurNthWeekday($nth)
-    {
-        $this->recurNthDay = (int)$nth;
-    }
-
-    /**
-     *
-     * @return integer  The nth weekday of month to repeat events.
-     */
-    public function getRecurNthWeekday()
-    {
-        return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7);
-    }
-
-    /**
-     * Specifies the months for yearly (weekday) recurrence
-     *
-     * @param array $months  List of months (integers) this event recurs on.
-     */
-    function setRecurByMonth($months)
-    {
-        $this->recurMonths = (array)$months;
-    }
-
-    /**
-     * Returns a list of months this yearly event recurs on
-     *
-     * @return array List of months (integers) this event recurs on.
-     */
-    function getRecurByMonth()
-    {
-        return $this->recurMonths;
-    }
-
-    /**
-     * Returns the days this event recurs on.
-     *
-     * @return integer  A mask consisting of Horde_Date::MASK_* constants
-     *                  specifying the day(s) this event recurs on.
-     */
-    public function getRecurOnDays()
-    {
-        return $this->recurData;
-    }
-
-    /**
-     * Returns whether this event has a specific recurrence type.
-     *
-     * @param integer $recurrence  RECUR_* constant of the
-     *                             recurrence type to check for.
-     *
-     * @return boolean  True if the event has the specified recurrence type.
-     */
-    public function hasRecurType($recurrence)
-    {
-        return ($recurrence == $this->recurType);
-    }
-
-    /**
-     * Sets a recurrence type for this event.
-     *
-     * @param integer $recurrence  A RECUR_* constant.
-     */
-    public function setRecurType($recurrence)
-    {
-        $this->recurType = $recurrence;
-    }
-
-    /**
-     * Returns recurrence type of this event.
-     *
-     * @return integer  A RECUR_* constant.
-     */
-    public function getRecurType()
-    {
-        return $this->recurType;
-    }
-
-    /**
-     * Returns a description of this event's recurring type.
-     *
-     * @return string  Human readable recurring type.
-     */
-    public function getRecurName()
-    {
-        switch ($this->getRecurType()) {
-        case self::RECUR_NONE: return Horde_Date_Translation::t("No recurrence");
-        case self::RECUR_DAILY: return Horde_Date_Translation::t("Daily");
-        case self::RECUR_WEEKLY: return Horde_Date_Translation::t("Weekly");
-        case self::RECUR_MONTHLY_DATE:
-        case self::RECUR_MONTHLY_WEEKDAY: return Horde_Date_Translation::t("Monthly");
-        case self::RECUR_YEARLY_DATE:
-        case self::RECUR_YEARLY_DAY:
-        case self::RECUR_YEARLY_WEEKDAY: return Horde_Date_Translation::t("Yearly");
-        }
-    }
-
-    /**
-     * Sets the length of time between recurrences of this event.
-     *
-     * @param integer $interval  The time between recurrences.
-     */
-    public function setRecurInterval($interval)
-    {
-        if ($interval > 0) {
-            $this->recurInterval = $interval;
-        }
-    }
-
-    /**
-     * Retrieves the length of time between recurrences of this event.
-     *
-     * @return integer  The number of seconds between recurrences.
-     */
-    public function getRecurInterval()
-    {
-        return $this->recurInterval;
-    }
-
-    /**
-     * Sets the number of recurrences of this event.
-     *
-     * @param integer $count  The number of recurrences.
-     */
-    public function setRecurCount($count)
-    {
-        if ($count > 0) {
-            $this->recurCount = (int)$count;
-            // Recurrence counts and end dates are mutually exclusive.
-            $this->recurEnd = null;
-        } else {
-            $this->recurCount = null;
-        }
-    }
-
-    /**
-     * Retrieves the number of recurrences of this event.
-     *
-     * @return integer  The number recurrences.
-     */
-    public function getRecurCount()
-    {
-        return $this->recurCount;
-    }
-
-    /**
-     * Returns whether this event has a recurrence with a fixed count.
-     *
-     * @return boolean  True if this recurrence has a fixed count.
-     */
-    public function hasRecurCount()
-    {
-        return isset($this->recurCount);
-    }
-
-    /**
-     * Sets the start date of the recurrence interval.
-     *
-     * @param Horde_Date $start  The recurrence start.
-     */
-    public function setRecurStart($start)
-    {
-        $this->start = clone $start;
-    }
-
-    /**
-     * Retrieves the start date of the recurrence interval.
-     *
-     * @return Horde_Date  The recurrence start.
-     */
-    public function getRecurStart()
-    {
-        return $this->start;
-    }
-
-    /**
-     * Sets the end date of the recurrence interval.
-     *
-     * @param Horde_Date $end  The recurrence end.
-     */
-    public function setRecurEnd($end)
-    {
-        if (!empty($end)) {
-            // Recurrence counts and end dates are mutually exclusive.
-            $this->recurCount = null;
-            $this->recurEnd = clone $end;
-        } else {
-            $this->recurEnd = $end;
-        }
-    }
-
-    /**
-     * Retrieves the end date of the recurrence interval.
-     *
-     * @return Horde_Date  The recurrence end.
-     */
-    public function getRecurEnd()
-    {
-        return $this->recurEnd;
-    }
-
-    /**
-     * Returns whether this event has a recurrence end.
-     *
-     * @return boolean  True if this recurrence ends.
-     */
-    public function hasRecurEnd()
-    {
-        return isset($this->recurEnd) && isset($this->recurEnd->year) &&
-            $this->recurEnd->year != 9999;
-    }
-
-    /**
-     * Finds the next recurrence of this event that's after $afterDate.
-     *
-     * @param Horde_Date|string $after  Return events after this date.
-     *
-     * @return Horde_Date|boolean  The date of the next recurrence or false
-     *                             if the event does not recur after
-     *                             $afterDate.
-     */
-    public function nextRecurrence($after)
-    {
-        if (!($after instanceof Horde_Date)) {
-            $after = new Horde_Date($after);
-        } else {
-            $after = clone($after);
-        }
-
-        // Make sure $after and $this->start are in the same TZ
-        $after->setTimezone($this->start->timezone);
-        if ($this->start->compareDateTime($after) >= 0) {
-            return clone $this->start;
-        }
-
-        if ($this->recurInterval == 0) {
-            return false;
-        }
-
-        switch ($this->getRecurType()) {
-        case self::RECUR_DAILY:
-            $diff = $this->start->diff($after);
-            $recur = ceil($diff / $this->recurInterval);
-            if ($this->recurCount && $recur >= $this->recurCount) {
-                return false;
-            }
-
-            $recur *= $this->recurInterval;
-            $next = $this->start->add(array('day' => $recur));
-            if ((!$this->hasRecurEnd() ||
-                 $next->compareDateTime($this->recurEnd) <= 0) &&
-                $next->compareDateTime($after) >= 0) {
-                return $next;
-            }
-            break;
-
-        case self::RECUR_WEEKLY:
-            if (empty($this->recurData)) {
-                return false;
-            }
-
-            $start_week = Horde_Date_Utils::firstDayOfWeek($this->start->format('W'),
-                                                           $this->start->year);
-            $start_week->timezone = $this->start->timezone;
-            $start_week->hour = $this->start->hour;
-            $start_week->min  = $this->start->min;
-            $start_week->sec  = $this->start->sec;
-
-            // Make sure we are not at the ISO-8601 first week of year while
-            // still in month 12...OR in the ISO-8601 last week of year while
-            // in month 1 and adjust the year accordingly.
-            $week = $after->format('W');
-            if ($week == 1 && $after->month == 12) {
-                $theYear = $after->year + 1;
-            } elseif ($week >= 52 && $after->month == 1) {
-                $theYear = $after->year - 1;
-            } else {
-                $theYear = $after->year;
-            }
-
-            $after_week = Horde_Date_Utils::firstDayOfWeek($week, $theYear);
-            $after_week->timezone = $this->start->timezone;
-            $after_week_end = clone $after_week;
-            $after_week_end->mday += 7;
-
-            $diff = $start_week->diff($after_week);
-            $interval = $this->recurInterval * 7;
-            $repeats = floor($diff / $interval);
-            if ($diff % $interval < 7) {
-                $recur = $diff;
-            } else {
-                /**
-                 * If the after_week is not in the first week interval the
-                 * search needs to skip ahead a complete interval. The way it is
-                 * calculated here means that an event that occurs every second
-                 * week on Monday and Wednesday with the event actually starting
-                 * on Tuesday or Wednesday will only have one incidence in the
-                 * first week.
-                 */
-                $recur = $interval * ($repeats + 1);
-            }
-
-            if ($this->hasRecurCount()) {
-                $recurrences = 0;
-                /**
-                 * Correct the number of recurrences by the number of events
-                 * that lay between the start of the start week and the
-                 * recurrence start.
-                 */
-                $next = clone $start_week;
-                while ($next->compareDateTime($this->start) < 0) {
-                    if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
-                        $recurrences--;
-                    }
-                    ++$next->mday;
-                }
-                if ($repeats > 0) {
-                    $weekdays = $this->recurData;
-                    $total_recurrences_per_week = 0;
-                    while ($weekdays > 0) {
-                        if ($weekdays % 2) {
-                            $total_recurrences_per_week++;
-                        }
-                        $weekdays = ($weekdays - ($weekdays % 2)) / 2;
-                    }
-                    $recurrences += $total_recurrences_per_week * $repeats;
-                }
-            }
-
-            $next = clone $start_week;
-            $next->mday += $recur;
-            while ($next->compareDateTime($after) < 0 &&
-                   $next->compareDateTime($after_week_end) < 0) {
-                if ($this->hasRecurCount()
-                    && $next->compareDateTime($after) < 0
-                    && $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
-                    $recurrences++;
-                }
-                ++$next->mday;
-            }
-            if ($this->hasRecurCount() &&
-                $recurrences >= $this->recurCount) {
-                return false;
-            }
-            if (!$this->hasRecurEnd() ||
-                $next->compareDateTime($this->recurEnd) <= 0) {
-                if ($next->compareDateTime($after_week_end) >= 0) {
-                    return $this->nextRecurrence($after_week_end);
-                }
-                while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) &&
-                       $next->compareDateTime($after_week_end) < 0) {
-                    ++$next->mday;
-                }
-                if (!$this->hasRecurEnd() ||
-                    $next->compareDateTime($this->recurEnd) <= 0) {
-                    if ($next->compareDateTime($after_week_end) >= 0) {
-                        return $this->nextRecurrence($after_week_end);
-                    } else {
-                        return $next;
-                    }
-                }
-            }
-            break;
-
-        case self::RECUR_MONTHLY_DATE:
-            $start = clone $this->start;
-            if ($after->compareDateTime($start) < 0) {
-                $after = clone $start;
-            } else {
-                $after = clone $after;
-            }
-
-            // If we're starting past this month's recurrence of the event,
-            // look in the next month on the day the event recurs.
-            if ($after->mday > $start->mday) {
-                ++$after->month;
-                $after->mday = $start->mday;
-            }
-
-            // Adjust $start to be the first match.
-            $offset = ($after->month - $start->month) + ($after->year - $start->year) * 12;
-            $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
-
-            if ($this->recurCount &&
-                ($offset / $this->recurInterval) >= $this->recurCount) {
-                return false;
-            }
-            $start->month += $offset;
-            $count = $offset / $this->recurInterval;
-
-            do {
-                if ($this->recurCount &&
-                    $count++ >= $this->recurCount) {
-                    return false;
-                }
-
-                // Bail if we've gone past the end of recurrence.
-                if ($this->hasRecurEnd() &&
-                    $this->recurEnd->compareDateTime($start) < 0) {
-                    return false;
-                }
-                if ($start->isValid()) {
-                    return $start;
-                }
-
-                // If the interval is 12, and the date isn't valid, then we
-                // need to see if February 29th is an option. If not, then the
-                // event will _never_ recur, and we need to stop checking to
-                // avoid an infinite loop.
-                if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) {
-                    return false;
-                }
-
-                // Add the recurrence interval.
-                $start->month += $this->recurInterval;
-            } while (true);
-
-            break;
-
-        case self::RECUR_MONTHLY_WEEKDAY:
-            // Start with the start date of the event.
-            $estart = clone $this->start;
-
-            // What day of the week, and week of the month, do we recur on?
-            if (isset($this->recurNthDay)) {
-                $nth = $this->recurNthDay;
-                $weekday = log($this->recurData, 2);
-            } else {
-                $nth = ceil($this->start->mday / 7);
-                $weekday = $estart->dayOfWeek();
-            }
-
-            // Adjust $estart to be the first candidate.
-            $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
-            $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
-
-            // Adjust our working date until it's after $after.
-            $estart->month += $offset - $this->recurInterval;
-
-            $count = $offset / $this->recurInterval;
-            do {
-                if ($this->recurCount &&
-                    $count++ >= $this->recurCount) {
-                    return false;
-                }
-
-                $estart->month += $this->recurInterval;
-
-                $next = clone $estart;
-                $next->setNthWeekday($weekday, $nth);
-
-                if ($next->compareDateTime($after) < 0) {
-                    // We haven't made it past $after yet, try again.
-                    continue;
-                }
-                if ($this->hasRecurEnd() &&
-                    $next->compareDateTime($this->recurEnd) > 0) {
-                    // We've gone past the end of recurrence; we can give up
-                    // now.
-                    return false;
-                }
-
-                // We have a candidate to return.
-                break;
-            } while (true);
-
-            return $next;
-
-        case self::RECUR_YEARLY_DATE:
-            // Start with the start date of the event.
-            $estart = clone $this->start;
-            $after = clone $after;
-
-            if ($after->month > $estart->month ||
-                ($after->month == $estart->month && $after->mday > $estart->mday)) {
-                ++$after->year;
-                $after->month = $estart->month;
-                $after->mday = $estart->mday;
-            }
-
-            // Seperate case here for February 29th
-            if ($estart->month == 2 && $estart->mday == 29) {
-                while (!Horde_Date_Utils::isLeapYear($after->year)) {
-                    ++$after->year;
-                }
-            }
-
-            // Adjust $estart to be the first candidate.
-            $offset = $after->year - $estart->year;
-            if ($offset > 0) {
-                $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
-                $estart->year += $offset;
-            }
-
-            // We've gone past the end of recurrence; give up.
-            if ($this->recurCount &&
-                $offset >= $this->recurCount) {
-                return false;
-            }
-            if ($this->hasRecurEnd() &&
-                $this->recurEnd->compareDateTime($estart) < 0) {
-                return false;
-            }
-
-            return $estart;
-
-        case self::RECUR_YEARLY_DAY:
-            // Check count first.
-            $dayofyear = $this->start->dayOfYear();
-            $count = ($after->year - $this->start->year) / $this->recurInterval + 1;
-            if ($this->recurCount &&
-                ($count > $this->recurCount ||
-                 ($count == $this->recurCount &&
-                  $after->dayOfYear() > $dayofyear))) {
-                return false;
-            }
-
-            // Start with a rough interval.
-            $estart = clone $this->start;
-            $estart->year += floor($count - 1) * $this->recurInterval;
-
-            // Now add the difference to the required day of year.
-            $estart->mday += $dayofyear - $estart->dayOfYear();
-
-            // Add an interval if the estimation was wrong.
-            if ($estart->compareDate($after) < 0) {
-                $estart->year += $this->recurInterval;
-                $estart->mday += $dayofyear - $estart->dayOfYear();
-            }
-
-            // We've gone past the end of recurrence; give up.
-            if ($this->hasRecurEnd() &&
-                $this->recurEnd->compareDateTime($estart) < 0) {
-                return false;
-            }
-
-            return $estart;
-
-        case self::RECUR_YEARLY_WEEKDAY:
-            // Start with the start date of the event.
-            $estart = clone $this->start;
-
-            // What day of the week, and week of the month, do we recur on?
-            if (isset($this->recurNthDay)) {
-                $nth = $this->recurNthDay;
-                $weekday = log($this->recurData, 2);
-            } else {
-                $nth = ceil($this->start->mday / 7);
-                $weekday = $estart->dayOfWeek();
-            }
-
-            // Adjust $estart to be the first candidate.
-            $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
-
-            // Adjust our working date until it's after $after.
-            $estart->year += $offset - $this->recurInterval;
-
-            $count = $offset / $this->recurInterval;
-            do {
-                if ($this->recurCount &&
-                    $count++ >= $this->recurCount) {
-                    return false;
-                }
-
-                $estart->year += $this->recurInterval;
-
-                $next = clone $estart;
-                $next->setNthWeekday($weekday, $nth);
-
-                if ($next->compareDateTime($after) < 0) {
-                    // We haven't made it past $after yet, try again.
-                    continue;
-                }
-                if ($this->hasRecurEnd() &&
-                    $next->compareDateTime($this->recurEnd) > 0) {
-                    // We've gone past the end of recurrence; we can give up
-                    // now.
-                    return false;
-                }
-
-                // We have a candidate to return.
-                break;
-            } while (true);
-
-            return $next;
-        }
-
-        // We didn't find anything, the recurType was bad, or something else
-        // went wrong - return false.
-        return false;
-    }
-
-    /**
-     * Returns whether this event has any date that matches the recurrence
-     * rules and is not an exception.
-     *
-     * @return boolean  True if an active recurrence exists.
-     */
-    public function hasActiveRecurrence()
-    {
-        if (!$this->hasRecurEnd()) {
-            return true;
-        }
-
-        $next = $this->nextRecurrence(new Horde_Date($this->start));
-        while (is_object($next)) {
-            if (!$this->hasException($next->year, $next->month, $next->mday) &&
-                !$this->hasCompletion($next->year, $next->month, $next->mday)) {
-                return true;
-            }
-
-            $next = $this->nextRecurrence($next->add(array('day' => 1)));
-        }
-
-        return false;
-    }
-
-    /**
-     * Returns the next active recurrence.
-     *
-     * @param Horde_Date $afterDate  Return events after this date.
-     *
-     * @return Horde_Date|boolean The date of the next active
-     *                             recurrence or false if the event
-     *                             has no active recurrence after
-     *                             $afterDate.
-     */
-    public function nextActiveRecurrence($afterDate)
-    {
-        $next = $this->nextRecurrence($afterDate);
-        while (is_object($next)) {
-            if (!$this->hasException($next->year, $next->month, $next->mday) &&
-                !$this->hasCompletion($next->year, $next->month, $next->mday)) {
-                return $next;
-            }
-            $next->mday++;
-            $next = $this->nextRecurrence($next);
-        }
-
-        return false;
-    }
-
-    /**
-     * Adds an exception to a recurring event.
-     *
-     * @param integer $year   The year of the execption.
-     * @param integer $month  The month of the execption.
-     * @param integer $mday   The day of the month of the exception.
-     */
-    public function addException($year, $month, $mday)
-    {
-        $this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
-    }
-
-    /**
-     * Deletes an exception from a recurring event.
-     *
-     * @param integer $year   The year of the execption.
-     * @param integer $month  The month of the execption.
-     * @param integer $mday   The day of the month of the exception.
-     */
-    public function deleteException($year, $month, $mday)
-    {
-        $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions);
-        if ($key !== false) {
-            unset($this->exceptions[$key]);
-        }
-    }
-
-    /**
-     * Checks if an exception exists for a given reccurence of an event.
-     *
-     * @param integer $year   The year of the reucrance.
-     * @param integer $month  The month of the reucrance.
-     * @param integer $mday   The day of the month of the reucrance.
-     *
-     * @return boolean  True if an exception exists for the given date.
-     */
-    public function hasException($year, $month, $mday)
-    {
-        return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
-                        $this->getExceptions());
-    }
-
-    /**
-     * Retrieves all the exceptions for this event.
-     *
-     * @return array  Array containing the dates of all the exceptions in
-     *                YYYYMMDD form.
-     */
-    public function getExceptions()
-    {
-        return $this->exceptions;
-    }
-
-    /**
-     * Adds a completion to a recurring event.
-     *
-     * @param integer $year   The year of the execption.
-     * @param integer $month  The month of the execption.
-     * @param integer $mday   The day of the month of the completion.
-     */
-    public function addCompletion($year, $month, $mday)
-    {
-        $this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
-    }
-
-    /**
-     * Deletes a completion from a recurring event.
-     *
-     * @param integer $year   The year of the execption.
-     * @param integer $month  The month of the execption.
-     * @param integer $mday   The day of the month of the completion.
-     */
-    public function deleteCompletion($year, $month, $mday)
-    {
-        $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions);
-        if ($key !== false) {
-            unset($this->completions[$key]);
-        }
-    }
-
-    /**
-     * Checks if a completion exists for a given reccurence of an event.
-     *
-     * @param integer $year   The year of the reucrance.
-     * @param integer $month  The month of the recurrance.
-     * @param integer $mday   The day of the month of the recurrance.
-     *
-     * @return boolean  True if a completion exists for the given date.
-     */
-    public function hasCompletion($year, $month, $mday)
-    {
-        return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
-                        $this->getCompletions());
-    }
-
-    /**
-     * Retrieves all the completions for this event.
-     *
-     * @return array  Array containing the dates of all the completions in
-     *                YYYYMMDD form.
-     */
-    public function getCompletions()
-    {
-        return $this->completions;
-    }
-
-    /**
-     * Parses a vCalendar 1.0 recurrence rule.
-     *
-     * @link http://www.imc.org/pdi/vcal-10.txt
-     * @link http://www.shuchow.com/vCalAddendum.html
-     *
-     * @param string $rrule  A vCalendar 1.0 conform RRULE value.
-     */
-    public function fromRRule10($rrule)
-    {
-        $this->reset();
-
-        if (!$rrule) {
-            return;
-        }
-
-        if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) {
-            // No recurrence data - event does not recur.
-            $this->setRecurType(self::RECUR_NONE);
-        }
-
-        // Always default the recurInterval to 1.
-        $this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1);
-
-        $remainder = trim($matches[3]);
-
-        switch ($matches[1]) {
-        case 'D':
-            $this->setRecurType(self::RECUR_DAILY);
-            break;
-
-        case 'W':
-            $this->setRecurType(self::RECUR_WEEKLY);
-            if (!empty($remainder)) {
-                $mask = 0;
-                while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
-                    $day = trim($matches[0]);
-                    $remainder = substr($remainder, strlen($matches[0]));
-                    $mask |= $maskdays[$day];
-                }
-                $this->setRecurOnDay($mask);
-            } else {
-                // Recur on the day of the week of the original recurrence.
-                $maskdays = array(
-                    Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
-                    Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
-                    Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
-                    Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
-                    Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
-                    Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
-                    Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY,
-                );
-                $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
-            }
-            break;
-
-        case 'MP':
-            $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
-            break;
-
-        case 'MD':
-            $this->setRecurType(self::RECUR_MONTHLY_DATE);
-            break;
-
-        case 'YM':
-            $this->setRecurType(self::RECUR_YEARLY_DATE);
-            break;
-
-        case 'YD':
-            $this->setRecurType(self::RECUR_YEARLY_DAY);
-            break;
-        }
-
-        // We don't support modifiers at the moment, strip them.
-        while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) {
-               $remainder = substr($remainder, 1);
-        }
-        if (!empty($remainder)) {
-            if (strpos($remainder, '#') === 0) {
-                $this->setRecurCount(substr($remainder, 1));
-            } else {
-                list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d');
-                $this->setRecurEnd(new Horde_Date(array('year' => $year,
-                                                        'month' => $month,
-                                                        'mday' => $mday,
-                                                        'hour' => 23,
-                                                        'min' => 59,
-                                                        'sec' => 59)));
-            }
-        }
-    }
-
-    /**
-     * Creates a vCalendar 1.0 recurrence rule.
-     *
-     * @link http://www.imc.org/pdi/vcal-10.txt
-     * @link http://www.shuchow.com/vCalAddendum.html
-     *
-     * @param Horde_Icalendar $calendar  A Horde_Icalendar object instance.
-     *
-     * @return string  A vCalendar 1.0 conform RRULE value.
-     */
-    public function toRRule10($calendar)
-    {
-        switch ($this->recurType) {
-        case self::RECUR_NONE:
-            return '';
-
-        case self::RECUR_DAILY:
-            $rrule = 'D' . $this->recurInterval;
-            break;
-
-        case self::RECUR_WEEKLY:
-            $rrule = 'W' . $this->recurInterval;
-            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
-
-            for ($i = 0; $i <= 7; ++$i) {
-                if ($this->recurOnDay(pow(2, $i))) {
-                    $rrule .= ' ' . $vcaldays[$i];
-                }
-            }
-            break;
-
-        case self::RECUR_MONTHLY_DATE:
-            $rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday);
-            break;
-
-        case self::RECUR_MONTHLY_WEEKDAY:
-            $nth_weekday = (int)($this->start->mday / 7);
-            if (($this->start->mday % 7) > 0) {
-                $nth_weekday++;
-            }
-
-            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
-            $rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()];
-
-            break;
-
-        case self::RECUR_YEARLY_DATE:
-            $rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month);
-            break;
-
-        case self::RECUR_YEARLY_DAY:
-            $rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear();
-            break;
-
-        default:
-            return '';
-        }
-
-        if ($this->hasRecurEnd()) {
-            $recurEnd = clone $this->recurEnd;
-            return $rrule . ' ' . $calendar->_exportDateTime($recurEnd);
-        }
-
-        return $rrule . ' #' . (int)$this->getRecurCount();
-    }
-
-    /**
-     * Parses an iCalendar 2.0 recurrence rule.
-     *
-     * @link http://rfc.net/rfc2445.html#s4.3.10
-     * @link http://rfc.net/rfc2445.html#s4.8.5
-     * @link http://www.shuchow.com/vCalAddendum.html
-     *
-     * @param string $rrule  An iCalendar 2.0 conform RRULE value.
-     */
-    public function fromRRule20($rrule)
-    {
-        $this->reset();
-
-        // Parse the recurrence rule into keys and values.
-        $rdata = array();
-        $parts = explode(';', $rrule);
-        foreach ($parts as $part) {
-            list($key, $value) = explode('=', $part, 2);
-            $rdata[strtoupper($key)] = $value;
-        }
-
-        if (isset($rdata['FREQ'])) {
-            // Always default the recurInterval to 1.
-            $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
-
-            $maskdays = array(
-                'SU' => Horde_Date::MASK_SUNDAY,
-                'MO' => Horde_Date::MASK_MONDAY,
-                'TU' => Horde_Date::MASK_TUESDAY,
-                'WE' => Horde_Date::MASK_WEDNESDAY,
-                'TH' => Horde_Date::MASK_THURSDAY,
-                'FR' => Horde_Date::MASK_FRIDAY,
-                'SA' => Horde_Date::MASK_SATURDAY,
-            );
-
-            switch (strtoupper($rdata['FREQ'])) {
-            case 'DAILY':
-                $this->setRecurType(self::RECUR_DAILY);
-                break;
-
-            case 'WEEKLY':
-                $this->setRecurType(self::RECUR_WEEKLY);
-                if (isset($rdata['BYDAY'])) {
-                    $days = explode(',', $rdata['BYDAY']);
-                    $mask = 0;
-                    foreach ($days as $day) {
-                        $mask |= $maskdays[$day];
-                    }
-                    $this->setRecurOnDay($mask);
-                } else {
-                    // Recur on the day of the week of the original
-                    // recurrence.
-                    $maskdays = array(
-                        Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
-                        Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
-                        Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
-                        Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
-                        Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
-                        Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
-                        Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY);
-                    $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
-                }
-                break;
-
-            case 'MONTHLY':
-                if (isset($rdata['BYDAY'])) {
-                    $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
-                    if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
-                        $this->setRecurOnDay($maskdays[$m[2]]);
-                        $this->setRecurNthWeekday($m[1]);
-                    }
-                } else {
-                    $this->setRecurType(self::RECUR_MONTHLY_DATE);
-                }
-                break;
-
-            case 'YEARLY':
-                if (isset($rdata['BYYEARDAY'])) {
-                    $this->setRecurType(self::RECUR_YEARLY_DAY);
-                } elseif (isset($rdata['BYDAY'])) {
-                    $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
-                    if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
-                        $this->setRecurOnDay($maskdays[$m[2]]);
-                        $this->setRecurNthWeekday($m[1]);
-                    }
-                    if ($rdata['BYMONTH']) {
-                        $months = explode(',', $rdata['BYMONTH']);
-                        $this->setRecurByMonth($months);
-                    }
-                } else {
-                    $this->setRecurType(self::RECUR_YEARLY_DATE);
-                }
-                break;
-            }
-
-            if (isset($rdata['UNTIL'])) {
-                list($year, $month, $mday) = sscanf($rdata['UNTIL'],
-                                                    '%04d%02d%02d');
-                $this->setRecurEnd(new Horde_Date(array('year' => $year,
-                                                        'month' => $month,
-                                                        'mday' => $mday,
-                                                        'hour' => 23,
-                                                        'min' => 59,
-                                                        'sec' => 59)));
-            }
-            if (isset($rdata['COUNT'])) {
-                $this->setRecurCount($rdata['COUNT']);
-            }
-        } else {
-            // No recurrence data - event does not recur.
-            $this->setRecurType(self::RECUR_NONE);
-        }
-    }
-
-    /**
-     * Creates an iCalendar 2.0 recurrence rule.
-     *
-     * @link http://rfc.net/rfc2445.html#s4.3.10
-     * @link http://rfc.net/rfc2445.html#s4.8.5
-     * @link http://www.shuchow.com/vCalAddendum.html
-     *
-     * @param Horde_Icalendar $calendar  A Horde_Icalendar object instance.
-     *
-     * @return string  An iCalendar 2.0 conform RRULE value.
-     */
-    public function toRRule20($calendar)
-    {
-        switch ($this->recurType) {
-        case self::RECUR_NONE:
-            return '';
-
-        case self::RECUR_DAILY:
-            $rrule = 'FREQ=DAILY;INTERVAL='  . $this->recurInterval;
-            break;
-
-        case self::RECUR_WEEKLY:
-            $rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval . ';BYDAY=';
-            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
-
-            for ($i = $flag = 0; $i <= 7; ++$i) {
-                if ($this->recurOnDay(pow(2, $i))) {
-                    if ($flag) {
-                        $rrule .= ',';
-                    }
-                    $rrule .= $vcaldays[$i];
-                    $flag = true;
-                }
-            }
-            break;
-
-        case self::RECUR_MONTHLY_DATE:
-            $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval;
-            break;
-
-        case self::RECUR_MONTHLY_WEEKDAY:
-            if (isset($this->recurNthDay)) {
-                $nth_weekday = $this->recurNthDay;
-                $day_of_week = log($this->recurData, 2);
-            } else {
-                $day_of_week = $this->start->dayOfWeek();
-                $nth_weekday = (int)($this->start->mday / 7);
-                if (($this->start->mday % 7) > 0) {
-                    $nth_weekday++;
-                }
-            }
-            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
-            $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
-                . ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week];
-            break;
-
-        case self::RECUR_YEARLY_DATE:
-            $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval;
-            break;
-
-        case self::RECUR_YEARLY_DAY:
-            $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
-                . ';BYYEARDAY=' . $this->start->dayOfYear();
-            break;
-
-        case self::RECUR_YEARLY_WEEKDAY:
-            if (isset($this->recurNthDay)) {
-                $nth_weekday = $this->recurNthDay;
-                $day_of_week = log($this->recurData, 2);
-            } else {
-                $day_of_week = $this->start->dayOfWeek();
-                $nth_weekday = (int)($this->start->mday / 7);
-                if (($this->start->mday % 7) > 0) {
-                    $nth_weekday++;
-                }
-             }
-            $months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month;
-            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
-            $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
-                . ';BYDAY='
-                . $nth_weekday
-                . $vcaldays[$day_of_week]
-                . ';BYMONTH=' . $this->start->month;
-            break;
-        }
-
-        if ($this->hasRecurEnd()) {
-            $recurEnd = clone $this->recurEnd;
-            $rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd);
-        }
-        if ($count = $this->getRecurCount()) {
-            $rrule .= ';COUNT=' . $count;
-        }
-        return $rrule;
-    }
-
-    /**
-     * Parses the recurrence data from a hash.
-     *
-     * @param array $hash  The hash to convert.
-     *
-     * @return boolean  True if the hash seemed valid, false otherwise.
-     */
-    public function fromHash($hash)
-    {
-        $this->reset();
-
-        if (!isset($hash['interval']) || !isset($hash['cycle'])) {
-            $this->setRecurType(self::RECUR_NONE);
-            return false;
-        }
-
-        $this->setRecurInterval((int)$hash['interval']);
-
-        $month2number = array(
-            'january'   => 1,
-            'february'  => 2,
-            'march'     => 3,
-            'april'     => 4,
-            'may'       => 5,
-            'june'      => 6,
-            'july'      => 7,
-            'august'    => 8,
-            'september' => 9,
-            'october'   => 10,
-            'november'  => 11,
-            'december'  => 12,
-        );
-
-        $parse_day = false;
-        $set_daymask = false;
-        $update_month = false;
-        $update_daynumber = false;
-        $update_weekday = false;
-        $nth_weekday = -1;
-
-        switch ($hash['cycle']) {
-        case 'daily':
-            $this->setRecurType(self::RECUR_DAILY);
-            break;
-
-        case 'weekly':
-            $this->setRecurType(self::RECUR_WEEKLY);
-            $parse_day = true;
-            $set_daymask = true;
-            break;
-
-        case 'monthly':
-            if (!isset($hash['daynumber'])) {
-                $this->setRecurType(self::RECUR_NONE);
-                return false;
-            }
-
-            switch ($hash['type']) {
-            case 'daynumber':
-                $this->setRecurType(self::RECUR_MONTHLY_DATE);
-                $update_daynumber = true;
-                break;
-
-            case 'weekday':
-                $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
-                $this->setRecurNthWeekday($hash['daynumber']);
-                $parse_day = true;
-                $set_daymask = true;
-                break;
-            }
-            break;
-
-        case 'yearly':
-            if (!isset($hash['type'])) {
-                $this->setRecurType(self::RECUR_NONE);
-                return false;
-            }
-
-            switch ($hash['type']) {
-            case 'monthday':
-                $this->setRecurType(self::RECUR_YEARLY_DATE);
-                $update_month = true;
-                $update_daynumber = true;
-                break;
-
-            case 'yearday':
-                if (!isset($hash['month'])) {
-                    $this->setRecurType(self::RECUR_NONE);
-                    return false;
-                }
-
-                $this->setRecurType(self::RECUR_YEARLY_DAY);
-                // Start counting days in January.
-                $hash['month'] = 'january';
-                $update_month = true;
-                $update_daynumber = true;
-                break;
-
-            case 'weekday':
-                if (!isset($hash['daynumber'])) {
-                    $this->setRecurType(self::RECUR_NONE);
-                    return false;
-                }
-
-                $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
-                $this->setRecurNthWeekday($hash['daynumber']);
-                $parse_day = true;
-                $set_daymask = true;
-
-                if ($hash['month'] && isset($month2number[$hash['month']])) {
-                    $this->setRecurByMonth($month2number[$hash['month']]);
-                }
-                break;
-            }
-        }
-
-        if (isset($hash['range-type']) && isset($hash['range'])) {
-            switch ($hash['range-type']) {
-            case 'number':
-                $this->setRecurCount((int)$hash['range']);
-                break;
-
-            case 'date':
-                $recur_end = new Horde_Date($hash['range']);
-                $recur_end->hour = 23;
-                $recur_end->min = 59;
-                $recur_end->sec = 59;
-                $this->setRecurEnd($recur_end);
-                break;
-            }
-        }
-
-        // Need to parse <day>?
-        $last_found_day = -1;
-        if ($parse_day) {
-            if (!isset($hash['day'])) {
-                $this->setRecurType(self::RECUR_NONE);
-                return false;
-            }
-
-            $mask = 0;
-            $bits = array(
-                'monday' => Horde_Date::MASK_MONDAY,
-                'tuesday' => Horde_Date::MASK_TUESDAY,
-                'wednesday' => Horde_Date::MASK_WEDNESDAY,
-                'thursday' => Horde_Date::MASK_THURSDAY,
-                'friday' => Horde_Date::MASK_FRIDAY,
-                'saturday' => Horde_Date::MASK_SATURDAY,
-                'sunday' => Horde_Date::MASK_SUNDAY,
-            );
-            $days = array(
-                'monday' => Horde_Date::DATE_MONDAY,
-                'tuesday' => Horde_Date::DATE_TUESDAY,
-                'wednesday' => Horde_Date::DATE_WEDNESDAY,
-                'thursday' => Horde_Date::DATE_THURSDAY,
-                'friday' => Horde_Date::DATE_FRIDAY,
-                'saturday' => Horde_Date::DATE_SATURDAY,
-                'sunday' => Horde_Date::DATE_SUNDAY,
-            );
-
-            foreach ($hash['day'] as $day) {
-                // Validity check.
-                if (empty($day) || !isset($bits[$day])) {
-                    continue;
-                }
-
-                $mask |= $bits[$day];
-                $last_found_day = $days[$day];
-            }
-
-            if ($set_daymask) {
-                $this->setRecurOnDay($mask);
-            }
-        }
-
-        if ($update_month || $update_daynumber || $update_weekday) {
-            if ($update_month) {
-                if (isset($month2number[$hash['month']])) {
-                    $this->start->month = $month2number[$hash['month']];
-                }
-            }
-
-            if ($update_daynumber) {
-                if (!isset($hash['daynumber'])) {
-                    $this->setRecurType(self::RECUR_NONE);
-                    return false;
-                }
-
-                $this->start->mday = $hash['daynumber'];
-            }
-
-            if ($update_weekday) {
-                $this->setNthWeekday($nth_weekday);
-            }
-        }
-
-        // Exceptions.
-        if (isset($hash['exceptions'])) {
-            $this->exceptions = $hash['exceptions'];
-        }
-
-        if (isset($hash['completions'])) {
-            $this->completions = $hash['completions'];
-        }
-
-        return true;
-    }
-
-    /**
-     * Export this object into a hash.
-     *
-     * @return array  The recurrence hash.
-     */
-    public function toHash()
-    {
-        if ($this->getRecurType() == self::RECUR_NONE) {
-            return array();
-        }
-
-        $day2number = array(
-            0 => 'sunday',
-            1 => 'monday',
-            2 => 'tuesday',
-            3 => 'wednesday',
-            4 => 'thursday',
-            5 => 'friday',
-            6 => 'saturday'
-        );
-        $month2number = array(
-            1 => 'january',
-            2 => 'february',
-            3 => 'march',
-            4 => 'april',
-            5 => 'may',
-            6 => 'june',
-            7 => 'july',
-            8 => 'august',
-            9 => 'september',
-            10 => 'october',
-            11 => 'november',
-            12 => 'december'
-        );
-
-        $hash = array('interval' => $this->getRecurInterval());
-        $start = $this->getRecurStart();
-
-        switch ($this->getRecurType()) {
-        case self::RECUR_DAILY:
-            $hash['cycle'] = 'daily';
-            break;
-
-        case self::RECUR_WEEKLY:
-            $hash['cycle'] = 'weekly';
-            $bits = array(
-                'monday' => Horde_Date::MASK_MONDAY,
-                'tuesday' => Horde_Date::MASK_TUESDAY,
-                'wednesday' => Horde_Date::MASK_WEDNESDAY,
-                'thursday' => Horde_Date::MASK_THURSDAY,
-                'friday' => Horde_Date::MASK_FRIDAY,
-                'saturday' => Horde_Date::MASK_SATURDAY,
-                'sunday' => Horde_Date::MASK_SUNDAY,
-            );
-            $days = array();
-            foreach ($bits as $name => $bit) {
-                if ($this->recurOnDay($bit)) {
-                    $days[] = $name;
-                }
-            }
-            $hash['day'] = $days;
-            break;
-
-        case self::RECUR_MONTHLY_DATE:
-            $hash['cycle'] = 'monthly';
-            $hash['type'] = 'daynumber';
-            $hash['daynumber'] = $start->mday;
-            break;
-
-        case self::RECUR_MONTHLY_WEEKDAY:
-            $hash['cycle'] = 'monthly';
-            $hash['type'] = 'weekday';
-            $hash['daynumber'] = $start->weekOfMonth();
-            $hash['day'] = array ($day2number[$start->dayOfWeek()]);
-            break;
-
-        case self::RECUR_YEARLY_DATE:
-            $hash['cycle'] = 'yearly';
-            $hash['type'] = 'monthday';
-            $hash['daynumber'] = $start->mday;
-            $hash['month'] = $month2number[$start->month];
-            break;
-
-        case self::RECUR_YEARLY_DAY:
-            $hash['cycle'] = 'yearly';
-            $hash['type'] = 'yearday';
-            $hash['daynumber'] = $start->dayOfYear();
-            break;
-
-        case self::RECUR_YEARLY_WEEKDAY:
-            $hash['cycle'] = 'yearly';
-            $hash['type'] = 'weekday';
-            $hash['daynumber'] = $start->weekOfMonth();
-            $hash['day'] = array ($day2number[$start->dayOfWeek()]);
-            $hash['month'] = $month2number[$start->month];
-        }
-
-        if ($this->hasRecurCount()) {
-            $hash['range-type'] = 'number';
-            $hash['range'] = $this->getRecurCount();
-        } elseif ($this->hasRecurEnd()) {
-            $date = $this->getRecurEnd();
-            $hash['range-type'] = 'date';
-            $hash['range'] = $date->datestamp();
-        } else {
-            $hash['range-type'] = 'none';
-            $hash['range'] = '';
-        }
-
-        // Recurrence exceptions
-        $hash['exceptions'] = $this->exceptions;
-        $hash['completions'] = $this->completions;
-
-        return $hash;
-    }
-
-    /**
-     * Returns a simple object suitable for json transport representing this
-     * object.
-     *
-     * Possible properties are:
-     * - t: type
-     * - i: interval
-     * - e: end date
-     * - c: count
-     * - d: data
-     * - co: completions
-     * - ex: exceptions
-     *
-     * @return object  A simple object.
-     */
-    public function toJson()
-    {
-        $json = new stdClass;
-        $json->t = $this->recurType;
-        $json->i = $this->recurInterval;
-        if ($this->hasRecurEnd()) {
-            $json->e = $this->recurEnd->toJson();
-        }
-        if ($this->recurCount) {
-            $json->c = $this->recurCount;
-        }
-        if ($this->recurData) {
-            $json->d = $this->recurData;
-        }
-        if ($this->completions) {
-            $json->co = $this->completions;
-        }
-        if ($this->exceptions) {
-            $json->ex = $this->exceptions;
-        }
-        return $json;
-    }
-
-}
diff --git a/lib/kolab/Horde_Kolab_Format_XML_configuration.php b/lib/kolab/Horde_Kolab_Format_XML_configuration.php
deleted file mode 100644
index c80fbd3..0000000
--- a/lib/kolab/Horde_Kolab_Format_XML_configuration.php
+++ /dev/null
@@ -1,76 +0,0 @@
-<?php
-
-/**
- * Kolab XML handler for configuration (KEP:9).
- *
- * @author  Aleksander Machniak <machniak at kolabsys.com>
- *
- * Copyright (C) 2011, Kolab Systems AG <contact at 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 Horde_Kolab_Format_XML_configuration extends Horde_Kolab_Format_XML {
-    /**
-     * Specific data fields for the configuration object
-     *
-     * @var Kolab
-     */
-    var $_fields_specific;
-
-    var $_root_version = 2.1;
-
-    /**
-     * Constructor
-     */
-    function Horde_Kolab_Format_XML_configuration($params = array())
-    {
-        $this->_root_name = 'configuration';
-
-        // Specific configuration fields, in kolab format specification order
-        $this->_fields_specific = array(
-            'application' => array (
-                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
-                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
-            ),
-            'type' => array(
-                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
-                'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
-            ),
-        );
-
-        // Dictionary fields
-        if (!empty($params['subtype']) && preg_match('/^dictionary.*/', $params['subtype'])) {
-            $this->_fields_specific = array_merge($this->_fields_specific, array(
-                'language' => array (
-                    'type'    => HORDE_KOLAB_XML_TYPE_STRING,
-                    'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
-                ),
-                'e' => array(
-                    'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
-                    'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
-                    'array'   => array(
-                        'type' => HORDE_KOLAB_XML_TYPE_STRING,
-                        'value' => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
-                    ),
-                ),
-            ));
-        }
-
-        parent::Horde_Kolab_Format_XML($params);
-
-        unset($this->_fields_basic['body']);
-        unset($this->_fields_basic['categories']);
-        unset($this->_fields_basic['sensitivity']);
-    }
-}
diff --git a/lib/kolab/kolab_date_recurrence.php b/lib/kolab/kolab_date_recurrence.php
deleted file mode 100644
index 427f62a..0000000
--- a/lib/kolab/kolab_date_recurrence.php
+++ /dev/null
@@ -1,171 +0,0 @@
-<?php
-
-/**
- * Recurrence computation class for xcal-based Kolab format objects
- *
- * Uitility class to compute instances of recurring events.
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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_date_recurrence
-{
-    private $engine;
-    private $object;
-    private $next;
-    private $duration;
-    private $tz_offset = 0;
-    private $dst_start = 0;
-    private $allday = false;
-    private $hour = 0;
-
-    /**
-     * Default constructor
-     *
-     * @param array The Kolab object to operate on
-     */
-    function __construct($object)
-    {
-        $this->object = $object;
-        $this->next = new Horde_Date($object['start'], kolab_format::$timezone->getName());
-
-        if (is_object($object['start']) && is_object($object['end']))
-            $this->duration = $object['start']->diff($object['end']);
-        else
-            $this->duration = new DateInterval('PT' . ($object['end'] - $object['start']) . 'S');
-
-        // use (copied) Horde classes to compute recurring instances
-        // TODO: replace with something that has less than 6'000 lines of code
-        $this->engine = new Horde_Date_Recurrence($this->next);
-        $this->engine->fromRRule20($this->to_rrule($object['recurrence']));  // TODO: get that string directly from libkolabxml
-
-        foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
-            $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
-
-        $now = new DateTime('now', kolab_format::$timezone);
-        $this->tz_offset = $object['allday'] ? $now->getOffset() - date('Z') : 0;
-        $this->dst_start = $this->next->format('I');
-        $this->allday = $object['allday'];
-        $this->hour = $this->next->hour;
-    }
-
-    /**
-     * Get date/time of the next occurence of this event
-     *
-     * @param boolean Return a Unix timestamp instead of a DateTime object
-     * @return mixed  DateTime object/unix timestamp or False if recurrence ended
-     */
-    public function next_start($timestamp = false)
-    {
-        $time = false;
-        if ($this->next && ($next = $this->engine->nextActiveRecurrence(array('year' => $this->next->year, 'month' => $this->next->month, 'mday' => $this->next->mday + 1, 'hour' => $this->next->hour, 'min' => $this->next->min, 'sec' => $this->next->sec)))) {
-            if ($this->allday) {
-                $next->hour = $this->hour;  # fix time for all-day events
-                $next->min = 0;
-            }
-            if ($timestamp) {
-                # consider difference in daylight saving between base event and recurring instance
-                $dst_diff = ($this->dst_start - $next->format('I')) * 3600;
-                $time = $next->timestamp() - $this->tz_offset - $dst_diff;
-            }
-            else {
-                $time = $next->toDateTime();
-            }
-            $this->next = $next;
-        }
-
-        return $time;
-    }
-
-    /**
-     * Get the next recurring instance of this event
-     *
-     * @return mixed Array with event properties or False if recurrence ended
-     */
-    public function next_instance()
-    {
-        if ($next_start = $this->next_start()) {
-            $next_end = clone $next_start;
-            $next_end->add($this->duration);
-
-            $next = $this->object;
-            $next['recurrence_id'] = $next_start->format('Y-m-d');
-            $next['start'] = $next_start;
-            $next['end'] = $next_end;
-            unset($next['_formatobj']);
-
-            return $next;
-        }
-
-        return false;
-    }
-
-    /**
-     * Get the end date of the occurence of this recurrence cycle
-     *
-     * @param string Date limit (where infinite recurrences should abort)
-     * @return mixed Timestamp with end date of the last event or False if recurrence exceeds limit
-     */
-    public function end($limit = 'now +1 year')
-    {
-        if ($this->object['recurrence']['UNTIL'])
-            return $this->object['recurrence']['UNTIL']->format('U');
-
-        $limit_time = strtotime($limit);
-        while ($next_start = $this->next_start(true)) {
-            if ($next_start > $limit_time)
-                break;
-        }
-
-        if ($this->next) {
-            $next_end = $this->next->toDateTime();
-            $next_end->add($this->duration);
-            return $next_end->format('U');
-        }
-
-        return false;
-    }
-
-    /**
-     * Convert the internal structured data into a vcalendar RRULE 2.0 string
-     */
-    private function to_rrule($recurrence)
-    {
-      if (is_string($recurrence))
-          return $recurrence;
-
-        $rrule = '';
-        foreach ((array)$recurrence as $k => $val) {
-            $k = strtoupper($k);
-            switch ($k) {
-            case 'UNTIL':
-                $val = $val->format('Ymd\THis');
-                break;
-            case 'EXDATE':
-                foreach ((array)$val as $i => $ex)
-                    $val[$i] = $ex->format('Ymd\THis');
-                $val = join(',', (array)$val);
-                break;
-            }
-            $rrule .= $k . '=' . $val . ';';
-        }
-
-      return $rrule;
-    }
-
-}
diff --git a/lib/kolab/kolab_format.php b/lib/kolab/kolab_format.php
deleted file mode 100644
index 23246d3..0000000
--- a/lib/kolab/kolab_format.php
+++ /dev/null
@@ -1,330 +0,0 @@
-<?php
-
-/**
- * Kolab format model class wrapping libkolabxml bindings
- *
- * Abstract base class for different Kolab groupware objects read from/written
- * to the new Kolab 3 format using the PHP bindings of libkolabxml.
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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/>.
- */
-
-abstract class kolab_format
-{
-    public static $timezone;
-
-    public /*abstract*/ $CTYPE;
-
-    protected /*abstract*/ $read_func;
-    protected /*abstract*/ $write_func;
-
-    protected $obj;
-    protected $data;
-    protected $xmldata;
-    protected $loaded = false;
-
-    const VERSION = '3.0';
-    const KTYPE_PREFIX = 'application/x-vnd.kolab.';
-
-    /**
-     * Factory method to instantiate a kolab_format object of the given type
-     *
-     * @param string Object type to instantiate
-     * @param string Cached xml data to initialize with
-     * @return object kolab_format
-     */
-    public static function factory($type, $xmldata = null)
-    {
-        if (!isset(self::$timezone))
-            self::$timezone = new DateTimeZone('UTC');
-
-        $type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type);
-        $suffix = preg_replace('/[^a-z]+/', '', $type);
-        $classname = 'kolab_format_' . $suffix;
-        if (class_exists($classname))
-            return new $classname($xmldata);
-
-        return PEAR::raiseError("Failed to load Kolab Format wrapper for type " . $type);
-    }
-
-    /**
-     * Convert the given date/time value into a cDateTime object
-     *
-     * @param mixed         Date/Time value either as unix timestamp, date string or PHP DateTime object
-     * @param DateTimeZone  The timezone the date/time is in. Use global default if Null, local time if False
-     * @param boolean       True of the given date has no time component
-     * @return object       The libkolabxml date/time object
-     */
-    public static function get_datetime($datetime, $tz = null, $dateonly = false)
-    {
-        // use timezone information from datetime of global setting
-        if (!$tz && $tz !== false) {
-            if ($datetime instanceof DateTime)
-                $tz = $datetime->getTimezone();
-            if (!$tz)
-                $tz = self::$timezone;
-        }
-        $result = new cDateTime();
-
-        // got a unix timestamp (in UTC)
-        if (is_numeric($datetime)) {
-            $datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
-            if ($tz) $datetime->setTimezone($tz);
-        }
-        else if (is_string($datetime) && strlen($datetime))
-            $datetime = new DateTime($datetime, $tz ?: null);
-
-        if ($datetime instanceof DateTime) {
-            $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
-
-            if (!$dateonly)
-                $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
-
-            if ($tz && $tz->getName() == 'UTC')
-                $result->setUTC(true);
-            else if ($tz !== false)
-                $result->setTimezone($tz->getName());
-        }
-
-        return $result;
-    }
-
-    /**
-     * Convert the given cDateTime into a PHP DateTime object
-     *
-     * @param object cDateTime  The libkolabxml datetime object
-     * @return object DateTime  PHP datetime instance
-     */
-    public static function php_datetime($cdt)
-    {
-        if (!is_object($cdt) || !$cdt->isValid())
-            return null;
-
-        $d = new DateTime;
-        $d->setTimezone(self::$timezone);
-
-        try {
-            if ($tzs = $cdt->timezone()) {
-                $tz = new DateTimeZone($tzs);
-                $d->setTimezone($tz);
-            }
-            else if ($cdt->isUTC()) {
-                $d->setTimezone(new DateTimeZone('UTC'));
-            }
-        }
-        catch (Exception $e) { }
-
-        $d->setDate($cdt->year(), $cdt->month(), $cdt->day());
-
-        if ($cdt->isDateOnly()) {
-            $d->_dateonly = true;
-            $d->setTime(12, 0, 0);  // set time to noon to avoid timezone troubles
-        }
-        else {
-            $d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
-        }
-
-        return $d;
-    }
-
-    /**
-     * Convert a libkolabxml vector to a PHP array
-     *
-     * @param object vector Object
-     * @return array Indexed array contaning vector elements
-     */
-    public static function vector2array($vec, $max = PHP_INT_MAX)
-    {
-        $arr = array();
-        for ($i=0; $i < $vec->size() && $i < $max; $i++)
-            $arr[] = $vec->get($i);
-        return $arr;
-    }
-
-    /**
-     * Build a libkolabxml vector (string) from a PHP array
-     *
-     * @param array Array with vector elements
-     * @return object vectors
-     */
-    public static function array2vector($arr)
-    {
-        $vec = new vectors;
-        foreach ((array)$arr as $val) {
-            if (strlen($val))
-                $vec->push($val);
-        }
-        return $vec;
-    }
-
-    /**
-     * Parse the X-Kolab-Type header from MIME messages and return the object type in short form
-     *
-     * @param string X-Kolab-Type header value
-     * @return string Kolab object type (contact,event,task,note,etc.)
-     */
-    public static function mime2object_type($x_kolab_type)
-    {
-        return preg_replace('/dictionary.[a-z.]+$/', 'dictionary', substr($x_kolab_type, strlen(self::KTYPE_PREFIX)));
-    }
-
-    /**
-     * Check for format errors after calling kolabformat::write*()
-     *
-     * @return boolean True if there were errors, False if OK
-     */
-    protected function format_errors()
-    {
-        $ret = $log = false;
-        switch (kolabformat::error()) {
-            case kolabformat::NoError:
-                $ret = false;
-                break;
-            case kolabformat::Warning:
-                $ret = false;
-                $log = "Warning";
-                break;
-            default:
-                $ret = true;
-                $log = "Error";
-        }
-
-        if ($log) {
-            rcube::raise_error(array(
-                'code' => 660,
-                'type' => 'php',
-                'file' => __FILE__,
-                'line' => __LINE__,
-                'message' => "kolabformat write $log: " . kolabformat::errorMessage(),
-            ), true);
-        }
-
-        return $ret;
-    }
-
-    /**
-     * Save the last generated UID to the object properties.
-     * Should be called after kolabformat::writeXXXX();
-     */
-    protected function update_uid()
-    {
-        // get generated UID
-        if (!$this->data['uid']) {
-            $this->data['uid'] = kolabformat::getSerializedUID();
-            $this->obj->setUid($this->data['uid']);
-        }
-    }
-
-    /**
-     * Initialize libkolabxml object with cached xml data
-     */
-    protected function init()
-    {
-        if (!$this->loaded) {
-            if ($this->xmldata) {
-                $this->load($this->xmldata);
-                $this->xmldata = null;
-            }
-            $this->loaded = true;
-        }
-    }
-
-    /**
-     * Direct getter for object properties
-     */
-    public function __get($var)
-    {
-        return $this->data[$var];
-    }
-
-    /**
-     * Load Kolab object data from the given XML block
-     *
-     * @param string XML data
-     */
-    public function load($xml)
-    {
-        $this->obj = call_user_func($this->read_func, $xml, false);
-        $this->loaded = !$this->format_errors();
-    }
-
-    /**
-     * Write object data to XML format
-     *
-     * @return string XML data
-     */
-    public function write()
-    {
-        $this->init();
-        $this->xmldata = call_user_func($this->write_func, $this->obj);
-
-        if (!$this->format_errors())
-            $this->update_uid();
-        else
-            $this->xmldata = null;
-
-        return $this->xmldata;
-    }
-
-    /**
-     * Set properties to the kolabformat object
-     *
-     * @param array  Object data as hash array
-     */
-    abstract public function set(&$object);
-
-    /**
-     *
-     */
-    abstract public function is_valid();
-
-    /**
-     * Convert the Kolab object into a hash array data structure
-     *
-     * @return array  Kolab object data as hash array
-     */
-    abstract public function to_array();
-
-    /**
-     * Load object data from Kolab2 format
-     *
-     * @param array Hash array with object properties (produced by Horde Kolab_Format classes)
-     */
-    abstract public function fromkolab2($object);
-
-    /**
-     * Callback for kolab_storage_cache to get object specific tags to cache
-     *
-     * @return array List of tags to save in cache
-     */
-    public function get_tags()
-    {
-        return array();
-    }
-
-    /**
-     * Callback for kolab_storage_cache to get words to index for fulltext search
-     *
-     * @return array List of words to save in cache
-     */
-    public function get_words()
-    {
-        return array();
-    }
-}
diff --git a/lib/kolab/kolab_format_configuration.php b/lib/kolab/kolab_format_configuration.php
deleted file mode 100644
index 974fc45..0000000
--- a/lib/kolab/kolab_format_configuration.php
+++ /dev/null
@@ -1,163 +0,0 @@
-<?php
-
-/**
- * Kolab Configuration data model class
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_configuration extends kolab_format
-{
-    public $CTYPE = 'application/x-vnd.kolab.configuration';
-
-    protected $read_func = 'kolabformat::readConfiguration';
-    protected $write_func = 'kolabformat::writeConfiguration';
-
-    private $type_map = array(
-        'dictionary' => Configuration::TypeDictionary,
-        'category' => Configuration::TypeCategoryColor,
-    );
-
-
-    function __construct($xmldata = null)
-    {
-        $this->obj = new Configuration;
-        $this->xmldata = $xmldata;
-    }
-
-    /**
-     * Set properties to the kolabformat object
-     *
-     * @param array  Object data as hash array
-     */
-    public function set(&$object)
-    {
-        $this->init();
-
-        // read type-specific properties
-        switch ($object['type']) {
-        case 'dictionary':
-            $dict = new Dictionary($object['language']);
-            $dict->setEntries(self::array2vector($object['e']));
-            $this->obj = new Configuration($dict);
-            break;
-
-        case 'category':
-            // TODO: implement this
-            $categories = new vectorcategorycolor;
-            $this->obj = new Configuration($categories);
-            break;
-        default:
-            return false;
-        }
-
-        // set some automatic values if missing
-        if (!empty($object['uid']))
-            $this->obj->setUid($object['uid']);
-        if (!empty($object['created']))
-            $this->obj->setCreated(self::get_datetime($object['created']));
-
-        // adjust content-type string
-        $this->CTYPE = 'application/x-vnd.kolab.configuration.' . $object['type'];
-
-        // cache this data
-        $this->data = $object;
-        unset($this->data['_formatobj']);
-    }
-
-    /**
-     *
-     */
-    public function is_valid()
-    {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
-    }
-
-    /**
-     * Convert the Configuration object into a hash array data structure
-     *
-     * @return array  Config object data as hash array
-     */
-    public function to_array()
-    {
-        // return cached result
-        if (!empty($this->data))
-            return $this->data;
-
-        $this->init();
-        $type_map = array_flip($this->type_map);
-
-        // read object properties
-        $object = array(
-            'uid'     => $this->obj->uid(),
-            'created' => self::php_datetime($this->obj->created()),
-            'changed' => self::php_datetime($this->obj->lastModified()),
-            'type'    => $type_map[$this->obj->type()],
-        );
-
-        // read type-specific properties
-        switch ($object['type']) {
-        case 'dictionary':
-            $dict = $this->obj->dictionary();
-            $object['language'] = $dict->language();
-            $object['e'] = self::vector2array($dict->entries());
-            break;
-
-        case 'category':
-            // TODO: implement this
-            break;
-        }
-
-        // adjust content-type string
-        if ($object['type'])
-            $this->CTYPE = 'application/x-vnd.kolab.configuration.' . $object['type'];
-
-        $this->data = $object;
-        return $this->data;
-    }
-
-    /**
-     * Load data from old Kolab2 format
-     */
-    public function fromkolab2($record)
-    {
-        $object = array(
-            'uid'     => $record['uid'],
-            'changed' => $record['last-modification-date'],
-        );
-
-        $this->data = $object + $record;
-    }
-
-    /**
-     * Callback for kolab_storage_cache to get object specific tags to cache
-     *
-     * @return array List of tags to save in cache
-     */
-    public function get_tags()
-    {
-        $tags = array();
-
-        if ($this->data['type'] == 'dictionary')
-            $tags = array($this->data['language']);
-
-        return $tags;
-    }
-
-}
diff --git a/lib/kolab/kolab_format_contact.php b/lib/kolab/kolab_format_contact.php
deleted file mode 100644
index ffef059..0000000
--- a/lib/kolab/kolab_format_contact.php
+++ /dev/null
@@ -1,509 +0,0 @@
-<?php
-
-/**
- * Kolab Contact model class
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_contact extends kolab_format
-{
-    public $CTYPE = 'application/vcard+xml';
-
-    protected $read_func = 'kolabformat::readContact';
-    protected $write_func = 'kolabformat::writeContact';
-
-    public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email');
-
-    public $phonetypes = array(
-        'home'    => Telephone::Home,
-        'work'    => Telephone::Work,
-        'text'    => Telephone::Text,
-        'main'    => Telephone::Voice,
-        'homefax' => Telephone::Fax,
-        'workfax' => Telephone::Fax,
-        'mobile'  => Telephone::Cell,
-        'video'   => Telephone::Video,
-        'pager'   => Telephone::Pager,
-        'car'     => Telephone::Car,
-        'other'   => Telephone::Textphone,
-    );
-
-    public $addresstypes = array(
-        'home' => Address::Home,
-        'work' => Address::Work,
-        'office' => 0,
-    );
-
-    private $gendermap = array(
-        'female' => Contact::Female,
-        'male'   => Contact::Male,
-    );
-
-    private $relatedmap = array(
-        'manager'   => Related::Manager,
-        'assistant' => Related::Assistant,
-        'spouse'    => Related::Spouse,
-        'children'  => Related::Child,
-    );
-
-    // old Kolab 2 format field map
-    private $kolab2_fieldmap = array(
-      // kolab       => roundcube
-      'full-name'    => 'name',
-      'given-name'   => 'firstname',
-      'middle-names' => 'middlename',
-      'last-name'    => 'surname',
-      'prefix'       => 'prefix',
-      'suffix'       => 'suffix',
-      'nick-name'    => 'nickname',
-      'organization' => 'organization',
-      'department'   => 'department',
-      'job-title'    => 'jobtitle',
-      'birthday'     => 'birthday',
-      'anniversary'  => 'anniversary',
-      'phone'        => 'phone',
-      'im-address'   => 'im',
-      'web-page'     => 'website',
-      'profession'   => 'profession',
-      'manager-name' => 'manager',
-      'assistant'    => 'assistant',
-      'spouse-name'  => 'spouse',
-      'children'     => 'children',
-      'body'         => 'notes',
-      'pgp-publickey' => 'pgppublickey',
-      'free-busy-url' => 'freebusyurl',
-      'picture'       => 'photo',
-    );
-    private $kolab2_phonetypes = array(
-        'home1' => 'home',
-        'business1' => 'work',
-        'business2' => 'work',
-        'businessfax' => 'workfax',
-    );
-    private $kolab2_addresstypes = array(
-        'business' => 'work'
-    );
-    private $kolab2_gender = array(0 => 'male', 1 => 'female');
-
-
-    /**
-     * Default constructor
-     */
-    function __construct($xmldata = null)
-    {
-        $this->obj = new Contact;
-        $this->xmldata = $xmldata;
-
-        // complete phone types
-        $this->phonetypes['homefax'] |= Telephone::Home;
-        $this->phonetypes['workfax'] |= Telephone::Work;
-    }
-
-    /**
-     * Set contact properties to the kolabformat object
-     *
-     * @param array  Contact data as hash array
-     */
-    public function set(&$object)
-    {
-        $this->init();
-
-        // set some automatic values if missing
-        if (false && !$this->obj->created()) {
-            if (!empty($object['created']))
-                $object['created'] = new DateTime('now', self::$timezone);
-            $this->obj->setCreated(self::get_datetime($object['created']));
-        }
-
-        if (!empty($object['uid']))
-            $this->obj->setUid($object['uid']);
-
-        $object['changed'] = new DateTime('now', self::$timezone);
-        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
-
-        // do the hard work of setting object values
-        $nc = new NameComponents;
-        $nc->setSurnames(self::array2vector($object['surname']));
-        $nc->setGiven(self::array2vector($object['firstname']));
-        $nc->setAdditional(self::array2vector($object['middlename']));
-        $nc->setPrefixes(self::array2vector($object['prefix']));
-        $nc->setSuffixes(self::array2vector($object['suffix']));
-        $this->obj->setNameComponents($nc);
-        $this->obj->setName($object['name']);
-
-        if (isset($object['nickname']))
-            $this->obj->setNickNames(self::array2vector($object['nickname']));
-        if (isset($object['profession']))
-            $this->obj->setTitles(self::array2vector($object['profession']));
-
-        // organisation related properties (affiliation)
-        $org = new Affiliation;
-        $offices = new vectoraddress;
-        if ($object['organization'])
-            $org->setOrganisation($object['organization']);
-        if ($object['department'])
-            $org->setOrganisationalUnits(self::array2vector($object['department']));
-        if ($object['jobtitle'])
-            $org->setRoles(self::array2vector($object['jobtitle']));
-
-        $rels = new vectorrelated;
-        if ($object['manager']) {
-            foreach ((array)$object['manager'] as $manager)
-                $rels->push(new Related(Related::Text, $manager, Related::Manager));
-        }
-        if ($object['assistant']) {
-            foreach ((array)$object['assistant'] as $assistant)
-                $rels->push(new Related(Related::Text, $assistant, Related::Assistant));
-        }
-        $org->setRelateds($rels);
-
-        // email, im, url
-        $this->obj->setEmailAddresses(self::array2vector($object['email']));
-        $this->obj->setIMaddresses(self::array2vector($object['im']));
-
-        $vurls = new vectorurl;
-        foreach ((array)$object['website'] as $url) {
-            $type = $url['type'] == 'blog' ? Url::Blog : Url::NoType;
-            $vurls->push(new Url($url['url'], $type));
-        }
-        $this->obj->setUrls($vurls);
-
-        // addresses
-        $adrs = new vectoraddress;
-        foreach ((array)$object['address'] as $address) {
-            $adr = new Address;
-            $type = $this->addresstypes[$address['type']];
-            if (isset($type))
-                $adr->setTypes($type);
-            else if ($address['type'])
-                $adr->setLabel($address['type']);
-            if ($address['street'])
-                $adr->setStreet($address['street']);
-            if ($address['locality'])
-                $adr->setLocality($address['locality']);
-            if ($address['code'])
-                $adr->setCode($address['code']);
-            if ($address['region'])
-                $adr->setRegion($address['region']);
-            if ($address['country'])
-                $adr->setCountry($address['country']);
-
-            if ($address['type'] == 'office')
-                $offices->push($adr);
-            else
-                $adrs->push($adr);
-        }
-        $this->obj->setAddresses($adrs);
-        $org->setAddresses($offices);
-
-        // add org affiliation after addresses are set
-        $orgs = new vectoraffiliation;
-        $orgs->push($org);
-        $this->obj->setAffiliations($orgs);
-
-        // telephones
-        $tels = new vectortelephone;
-        foreach ((array)$object['phone'] as $phone) {
-            $tel = new Telephone;
-            if (isset($this->phonetypes[$phone['type']]))
-                $tel->setTypes($this->phonetypes[$phone['type']]);
-            $tel->setNumber($phone['number']);
-            $tels->push($tel);
-        }
-        $this->obj->setTelephones($tels);
-
-        if (isset($object['gender']))
-            $this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
-        if (isset($object['notes']))
-            $this->obj->setNote($object['notes']);
-        if (isset($object['freebusyurl']))
-            $this->obj->setFreeBusyUrl($object['freebusyurl']);
-        if (isset($object['birthday']))
-            $this->obj->setBDay(self::get_datetime($object['birthday'], false, true));
-        if (isset($object['anniversary']))
-            $this->obj->setAnniversary(self::get_datetime($object['anniversary'], false, true));
-
-        if (!empty($object['photo'])) {
-            if ($type = rcube_mime::image_content_type($object['photo']))
-                $this->obj->setPhoto($object['photo'], $type);
-        }
-        else if (isset($object['photo']))
-            $this->obj->setPhoto('','');
-        else if ($this->obj->photoMimetype())  // load saved photo for caching
-            $object['photo'] = $this->obj->photo();
-
-        // spouse and children are relateds
-        $rels = new vectorrelated;
-        if ($object['spouse']) {
-            $rels->push(new Related(Related::Text, $object['spouse'], Related::Spouse));
-        }
-        if ($object['children']) {
-            foreach ((array)$object['children'] as $child)
-                $rels->push(new Related(Related::Text, $child, Related::Child));
-        }
-        $this->obj->setRelateds($rels);
-
-        // insert/replace crypto keys
-        $pgp_index = $pkcs7_index = -1;
-        $keys = $this->obj->keys();
-        for ($i=0; $i < $keys->size(); $i++) {
-            $key = $keys->get($i);
-            if ($pgp_index < 0 && $key->type() == Key::PGP)
-                $pgp_index = $i;
-            else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
-                $pkcs7_index = $i;
-        }
-
-        $pgpkey   = $object['pgppublickey']   ? new Key($object['pgppublickey'], Key::PGP) : new Key();
-        $pkcs7key = $object['pkcs7publickey'] ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
-
-        if ($pgp_index >= 0)
-            $keys->set($pgp_index, $pgpkey);
-        else if (!empty($object['pgppublickey']))
-            $keys->push($pgpkey);
-        if ($pkcs7_index >= 0)
-            $keys->set($pkcs7_index, $pkcs7key);
-        else if (!empty($object['pkcs7publickey']))
-            $keys->push($pkcs7key);
-
-        $this->obj->setKeys($keys);
-
-        // TODO: handle language, gpslocation, etc.
-
-
-        // cache this data
-        $this->data = $object;
-        unset($this->data['_formatobj']);
-    }
-
-    /**
-     *
-     */
-    public function is_valid()
-    {
-        return $this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/);
-    }
-
-    /**
-     * Convert the Contact object into a hash array data structure
-     *
-     * @return array  Contact data as hash array
-     */
-    public function to_array()
-    {
-        // return cached result
-        if (!empty($this->data))
-            return $this->data;
-
-        $this->init();
-
-        // read object properties into local data object
-        $object = array(
-            'uid'       => $this->obj->uid(),
-            'name'      => $this->obj->name(),
-            'changed'   => self::php_datetime($this->obj->lastModified()),
-        );
-
-        $nc = $this->obj->nameComponents();
-        $object['surname']    = join(' ', self::vector2array($nc->surnames()));
-        $object['firstname']  = join(' ', self::vector2array($nc->given()));
-        $object['middlename'] = join(' ', self::vector2array($nc->additional()));
-        $object['prefix']     = join(' ', self::vector2array($nc->prefixes()));
-        $object['suffix']     = join(' ', self::vector2array($nc->suffixes()));
-        $object['nickname']   = join(' ', self::vector2array($this->obj->nickNames()));
-        $object['profession'] = join(' ', self::vector2array($this->obj->titles()));
-
-        // organisation related properties (affiliation)
-        $orgs = $this->obj->affiliations();
-        if ($orgs->size()) {
-            $org = $orgs->get(0);
-            $object['organization']   = $org->organisation();
-            $object['jobtitle']       = join(' ', self::vector2array($org->roles()));
-            $object['department']     = join(' ', self::vector2array($org->organisationalUnits()));
-            $this->read_relateds($org->relateds(), $object);
-        }
-
-        $object['email']   = self::vector2array($this->obj->emailAddresses());
-        $object['im']      = self::vector2array($this->obj->imAddresses());
-
-        $urls = $this->obj->urls();
-        for ($i=0; $i < $urls->size(); $i++) {
-            $url = $urls->get($i);
-            $subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
-            $object['website'][] = array('url' => $url->url(), 'type' => $subtype);
-        }
-
-        // addresses
-        $this->read_addresses($this->obj->addresses(), $object);
-        if ($org && ($offices = $org->addresses()))
-            $this->read_addresses($offices, $object, 'office');
-
-        // telehones
-        $tels = $this->obj->telephones();
-        $teltypes = array_flip($this->phonetypes);
-        for ($i=0; $i < $tels->size(); $i++) {
-            $tel = $tels->get($i);
-            $object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
-        }
-
-        $object['notes'] = $this->obj->note();
-        $object['freebusyurl'] = $this->obj->freeBusyUrl();
-
-        if ($bday = self::php_datetime($this->obj->bDay()))
-            $object['birthday'] = $bday->format('c');
-
-        if ($anniversary = self::php_datetime($this->obj->anniversary()))
-            $object['anniversary'] = $anniversary->format('c');
-
-        $gendermap = array_flip($this->gendermap);
-        if (($g = $this->obj->gender()) && $gendermap[$g])
-            $object['gender'] = $gendermap[$g];
-
-        if ($this->obj->photoMimetype())
-            $object['photo'] = $this->obj->photo();
-
-        // relateds -> spouse, children
-        $this->read_relateds($this->obj->relateds(), $object);
-
-        // crypto settings: currently only key values are supported
-        $keys = $this->obj->keys();
-        for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
-            $key = $keys->get($i);
-            if ($key->type() == Key::PGP)
-                $object['pgppublickey'] = $key->key();
-            else if ($key->type() == Key::PKCS7_MIME)
-                $object['pkcs7publickey'] = $key->key();
-        }
-
-        $this->data = $object;
-        return $this->data;
-    }
-
-    /**
-     * Callback for kolab_storage_cache to get words to index for fulltext search
-     *
-     * @return array List of words to save in cache
-     */
-    public function get_words()
-    {
-        $data = '';
-        foreach (self::$fulltext_cols as $col) {
-            $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
-            if (strlen($val))
-                $data .= $val . ' ';
-        }
-
-        return array_unique(rcube_utils::normalize_string($data, true));
-    }
-
-    /**
-     * Load data from old Kolab2 format
-     *
-     * @param array Hash array with object properties
-     */
-    public function fromkolab2($record)
-    {
-        $object = array(
-          'uid' => $record['uid'],
-          'email' => array(),
-          'phone' => array(),
-        );
-
-        foreach ($this->kolab2_fieldmap as $kolab => $rcube) {
-          if (is_array($record[$kolab]) || strlen($record[$kolab]))
-            $object[$rcube] = $record[$kolab];
-        }
-
-        if (isset($record['gender']))
-            $object['gender'] = $this->kolab2_gender[$record['gender']];
-
-        foreach ((array)$record['email'] as $i => $email)
-            $object['email'][] = $email['smtp-address'];
-
-        if (!$record['email'] && $record['emails'])
-            $object['email'] = preg_split('/,\s*/', $record['emails']);
-
-        if (is_array($record['address'])) {
-            foreach ($record['address'] as $i => $adr) {
-                $object['address'][] = array(
-                    'type' => $this->kolab2_addresstypes[$adr['type']] ? $this->kolab2_addresstypes[$adr['type']] : $adr['type'],
-                    'street' => $adr['street'],
-                    'locality' => $adr['locality'],
-                    'code' => $adr['postal-code'],
-                    'region' => $adr['region'],
-                    'country' => $adr['country'],
-                );
-            }
-        }
-
-        // office location goes into an address block
-        if ($record['office-location'])
-            $object['address'][] = array('type' => 'office', 'locality' => $record['office-location']);
-
-        // merge initials into nickname
-        if ($record['initials'])
-            $object['nickname'] = trim($object['nickname'] . ', ' . $record['initials'], ', ');
-
-        // remove empty fields
-        $this->data = array_filter($object);
-    }
-
-    /**
-     * Helper method to copy contents of an Address vector to the contact data object
-     */
-    private function read_addresses($addresses, &$object, $type = null)
-    {
-        $adrtypes = array_flip($this->addresstypes);
-
-        for ($i=0; $i < $addresses->size(); $i++) {
-            $adr = $addresses->get($i);
-            $object['address'][] = array(
-                'type'     => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
-                'street'   => $adr->street(),
-                'code'     => $adr->code(),
-                'locality' => $adr->locality(),
-                'region'   => $adr->region(),
-                'country'  => $adr->country()
-            );
-        }
-    }
-
-    /**
-     * Helper method to map contents of a Related vector to the contact data object
-     */
-    private function read_relateds($rels, &$object)
-    {
-        $typemap = array_flip($this->relatedmap);
-
-        for ($i=0; $i < $rels->size(); $i++) {
-            $rel = $rels->get($i);
-            if ($rel->type() != Related::Text)  // we can't handle UID relations yet
-                continue;
-
-            $types = $rel->relationTypes();
-            foreach ($typemap as $t => $field) {
-                if ($types & $t) {
-                    $object[$field][] = $rel->text();
-                    break;
-                }
-            }
-        }
-    }
-}
diff --git a/lib/kolab/kolab_format_distributionlist.php b/lib/kolab/kolab_format_distributionlist.php
deleted file mode 100644
index fcb94c1..0000000
--- a/lib/kolab/kolab_format_distributionlist.php
+++ /dev/null
@@ -1,147 +0,0 @@
-<?php
-
-/**
- * Kolab Distribution List model class
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_distributionlist extends kolab_format
-{
-    public $CTYPE = 'application/vcard+xml';
-
-    protected $read_func = 'kolabformat::readDistlist';
-    protected $write_func = 'kolabformat::writeDistlist';
-
-
-    function __construct($xmldata = null)
-    {
-        $this->obj = new DistList;
-        $this->xmldata = $xmldata;
-    }
-
-    /**
-     * Set properties to the kolabformat object
-     *
-     * @param array  Object data as hash array
-     */
-    public function set(&$object)
-    {
-        $this->init();
-
-        // set some automatic values if missing
-        if (!empty($object['uid']))
-            $this->obj->setUid($object['uid']);
-
-        $object['changed'] = new DateTime('now', self::$timezone);
-        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
-
-        $this->obj->setName($object['name']);
-
-        $seen = array();
-        $members = new vectorcontactref;
-        foreach ((array)$object['member'] as $member) {
-            if ($member['uid'])
-                $m = new ContactReference(ContactReference::UidReference, $member['uid']);
-            else if ($member['email'])
-                $m = new ContactReference(ContactReference::EmailReference, $member['email']);
-            else
-                continue;
-
-            $m->setName($member['name']);
-            $members->push($m);
-            $seen[$member['email']]++;
-        }
-
-        $this->obj->setMembers($members);
-
-        // set type property for proper caching
-        $object['_type'] = 'distribution-list';
-
-        // cache this data
-        $this->data = $object;
-        unset($this->data['_formatobj']);
-    }
-
-    public function is_valid()
-    {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
-    }
-
-    /**
-     * Load data from old Kolab2 format
-     */
-    public function fromkolab2($record)
-    {
-        $object = array(
-            'uid'     => $record['uid'],
-            'changed' => $record['last-modification-date'],
-            'name'    => $record['last-name'],
-            'member'  => array(),
-        );
-
-        foreach ((array)$record['member'] as $member) {
-            $object['member'][] = array(
-                'email' => $member['smtp-address'],
-                'name' => $member['display-name'],
-                'uid' => $member['uid'],
-            );
-        }
-
-        $this->data = $object;
-    }
-
-    /**
-     * Convert the Distlist object into a hash array data structure
-     *
-     * @return array  Distribution list data as hash array
-     */
-    public function to_array()
-    {
-        // return cached result
-        if (!empty($this->data))
-            return $this->data;
-
-        $this->init();
-
-        // read object properties
-        $object = array(
-            'uid'       => $this->obj->uid(),
-            'changed'   => self::php_datetime($this->obj->lastModified()),
-            'name'      => $this->obj->name(),
-            'member'    => array(),
-            '_type'     => 'distribution-list',
-        );
-
-        $members = $this->obj->members();
-        for ($i=0; $i < $members->size(); $i++) {
-            $member = $members->get($i);
-#            if ($member->type() == ContactReference::UidReference && ($uid = $member->uid()))
-                $object['member'][] = array(
-                    'uid' => $member->uid(),
-                    'email' => $member->email(),
-                    'name' => $member->name(),
-                );
-        }
-
-        $this->data = $object;
-        return $this->data;
-    }
-
-}
diff --git a/lib/kolab/kolab_format_event.php b/lib/kolab/kolab_format_event.php
deleted file mode 100644
index 33ed5af..0000000
--- a/lib/kolab/kolab_format_event.php
+++ /dev/null
@@ -1,303 +0,0 @@
-<?php
-
-/**
- * Kolab Event model class
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_event extends kolab_format_xcal
-{
-    protected $read_func = 'kolabformat::readEvent';
-    protected $write_func = 'kolabformat::writeEvent';
-
-    private $kolab2_rolemap = array(
-        'required' => 'REQ-PARTICIPANT',
-        'optional' => 'OPT-PARTICIPANT',
-        'resource' => 'CHAIR',
-    );
-    private $kolab2_statusmap = array(
-        'none'      => 'NEEDS-ACTION',
-        'tentative' => 'TENTATIVE',
-        'accepted'  => 'CONFIRMED',
-        'accepted'  => 'ACCEPTED',
-        'declined'  => 'DECLINED',
-    );
-    private $kolab2_monthmap = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december');
-
-
-    /**
-     * Default constructor
-     */
-    function __construct($xmldata = null)
-    {
-        $this->obj = new Event;
-        $this->xmldata = $xmldata;
-    }
-
-    /**
-     * Set event properties to the kolabformat object
-     *
-     * @param array  Event data as hash array
-     */
-    public function set(&$object)
-    {
-        $this->init();
-
-        // set common xcal properties
-        parent::set($object);
-
-        // do the hard work of setting object values
-        $this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
-        $this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
-        $this->obj->setTransparency($object['free_busy'] == 'free');
-
-        $status = kolabformat::StatusUndefined;
-        if ($object['free_busy'] == 'tentative')
-            $status = kolabformat::StatusTentative;
-        if ($object['cancelled'])
-            $status = kolabformat::StatusCancelled;
-        $this->obj->setStatus($status);
-
-        // save attachments
-        $vattach = new vectorattachment;
-        foreach ((array)$object['_attachments'] as $cid => $attr) {
-            if (empty($attr))
-                continue;
-            $attach = new Attachment;
-            $attach->setLabel((string)$attr['name']);
-            $attach->setUri('cid:' . $cid, $attr['mimetype']);
-            $vattach->push($attach);
-        }
-        $this->obj->setAttachments($vattach);
-
-        // cache this data
-        $this->data = $object;
-        unset($this->data['_formatobj']);
-    }
-
-    /**
-     *
-     */
-    public function is_valid()
-    {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid());
-    }
-
-    /**
-     * Convert the Event object into a hash array data structure
-     *
-     * @return array  Event data as hash array
-     */
-    public function to_array()
-    {
-        // return cached result
-        if (!empty($this->data))
-            return $this->data;
-
-        $this->init();
-
-        // read common xcal props
-        $object = parent::to_array();
-
-        // read object properties
-        $object += array(
-            'end'         => self::php_datetime($this->obj->end()),
-            'allday'      => $this->obj->start()->isDateOnly(),
-            'free_busy'   => $this->obj->transparency() ? 'free' : 'busy',  // TODO: transparency is only boolean
-            'attendees'   => array(),
-        );
-
-        // organizer is part of the attendees list in Roundcube
-        if ($object['organizer']) {
-            $object['organizer']['role'] = 'ORGANIZER';
-            array_unshift($object['attendees'], $object['organizer']);
-        }
-
-        // status defines different event properties...
-        $status = $this->obj->status();
-        if ($status == kolabformat::StatusTentative)
-          $object['free_busy'] = 'tentative';
-        else if ($status == kolabformat::StatusCancelled)
-          $objec['cancelled'] = true;
-
-        // handle attachments
-        $vattach = $this->obj->attachments();
-        for ($i=0; $i < $vattach->size(); $i++) {
-            $attach = $vattach->get($i);
-
-            // skip cid: attachments which are mime message parts handled by kolab_storage_folder
-            if (substr($attach->uri(), 0, 4) != 'cid') {
-                $name = $attach->label();
-                $data = $attach->data();
-                $object['_attachments'][$name] = array(
-                    'name' => $name,
-                    'mimetype' => $attach->mimetype(),
-                    'size' => strlen($data),
-                    'content' => $data,
-                );
-            }
-        }
-
-        $this->data = $object;
-        return $this->data;
-    }
-
-    /**
-     * Callback for kolab_storage_cache to get object specific tags to cache
-     *
-     * @return array List of tags to save in cache
-     */
-    public function get_tags()
-    {
-        $tags = array();
-
-        foreach ((array)$this->data['categories'] as $cat) {
-            $tags[] = rcube_utils::normalize_string($cat);
-        }
-
-        if (!empty($this->data['alarms'])) {
-            $tags[] = 'x-has-alarms';
-        }
-
-        return $tags;
-    }
-
-    /**
-     * Load data from old Kolab2 format
-     */
-    public function fromkolab2($rec)
-    {
-        if (PEAR::isError($rec))
-            return;
-
-        $start_time = date('H:i:s', $rec['start-date']);
-        $allday = $rec['_is_all_day'] || ($start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']));
-
-        // in Roundcube all-day events go from 12:00 to 13:00
-        if ($allday) {
-            $now = new DateTime('now', self::$timezone);
-            $gmt_offset = $now->getOffset();
-
-            $rec['start-date'] += 12 * 3600;
-            $rec['end-date']   -= 11 * 3600;
-            $rec['end-date']   -= $gmt_offset - date('Z', $rec['end-date']);    // shift times from server's timezone to user's timezone
-            $rec['start-date'] -= $gmt_offset - date('Z', $rec['start-date']);  // because generated with mktime() in Horde_Kolab_Format_Date::decodeDate()
-            // sanity check
-            if ($rec['end-date'] <= $rec['start-date'])
-              $rec['end-date'] += 86400;
-        }
-
-        // convert alarm time into internal format
-        if ($rec['alarm']) {
-            $alarm_value = $rec['alarm'];
-            $alarm_unit = 'M';
-            if ($rec['alarm'] % 1440 == 0) {
-                $alarm_value /= 1440;
-                $alarm_unit = 'D';
-            }
-            else if ($rec['alarm'] % 60 == 0) {
-                $alarm_value /= 60;
-                $alarm_unit = 'H';
-            }
-            $alarm_value *= -1;
-        }
-
-        // convert recurrence rules into internal pseudo-vcalendar format
-        if ($recurrence = $rec['recurrence']) {
-            $rrule = array(
-                'FREQ' => strtoupper($recurrence['cycle']),
-                'INTERVAL' => intval($recurrence['interval']),
-            );
-
-            if ($recurrence['range-type'] == 'number')
-                $rrule['COUNT'] = intval($recurrence['range']);
-            else if ($recurrence['range-type'] == 'date')
-                $rrule['UNTIL'] = date_create('@'.$recurrence['range']);
-
-            if ($recurrence['day']) {
-                $byday = array();
-                $prefix = ($rrule['FREQ'] == 'MONTHLY' || $rrule['FREQ'] == 'YEARLY') ? intval($recurrence['daynumber'] ? $recurrence['daynumber'] : 1) : '';
-                foreach ($recurrence['day'] as $day)
-                    $byday[] = $prefix . substr(strtoupper($day), 0, 2);
-                $rrule['BYDAY'] = join(',', $byday);
-            }
-            if ($recurrence['daynumber']) {
-                if ($recurrence['type'] == 'monthday' || $recurrence['type'] == 'daynumber')
-                    $rrule['BYMONTHDAY'] = $recurrence['daynumber'];
-                else if ($recurrence['type'] == 'yearday')
-                    $rrule['BYYEARDAY'] = $recurrence['daynumber'];
-            }
-            if ($recurrence['month']) {
-                $monthmap = array_flip($this->kolab2_monthmap);
-                $rrule['BYMONTH'] = strtolower($monthmap[$recurrence['month']]);
-            }
-
-            if ($recurrence['exclusion']) {
-                foreach ((array)$recurrence['exclusion'] as $excl)
-                    $rrule['EXDATE'][] = date_create($excl . date(' H:i:s', $rec['start-date']));  // use time of event start
-            }
-        }
-
-        $attendees = array();
-        if ($rec['organizer']) {
-            $attendees[] = array(
-                'role' => 'ORGANIZER',
-                'name' => $rec['organizer']['display-name'],
-                'email' => $rec['organizer']['smtp-address'],
-                'status' => 'ACCEPTED',
-            );
-            $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
-        }
-
-        foreach ((array)$rec['attendee'] as $attendee) {
-            $attendees[] = array(
-                'role' => $this->kolab2_rolemap[$attendee['role']],
-                'name' => $attendee['display-name'],
-                'email' => $attendee['smtp-address'],
-                'status' => $this->kolab2_statusmap[$attendee['status']],
-                'rsvp' => $attendee['request-response'],
-            );
-            $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
-        }
-
-        $this->data = array(
-            'uid' => $rec['uid'],
-            'title' => $rec['summary'],
-            'location' => $rec['location'],
-            'description' => $rec['body'],
-            'start' => new DateTime('@'.$rec['start-date']),
-            'end'   => new DateTime('@'.$rec['end-date']),
-            'allday' => $allday,
-            'recurrence' => $rrule,
-            'alarms' => $alarm_value . $alarm_unit,
-            'categories' => explode(',', $rec['categories']),
-            'attachments' => $attachments,
-            'attendees' => $attendees,
-            'free_busy' => $rec['show-time-as'],
-            'priority' => $rec['priority'],
-            'sensitivity' => $rec['sensitivity'],
-            'changed' => $rec['last-modification-date'],
-        );
-
-        // assign current timezone to event start/end
-        $this->data['start']->setTimezone(self::$timezone);
-        $this->data['end']->setTimezone(self::$timezone);
-    }
-}
diff --git a/lib/kolab/kolab_format_journal.php b/lib/kolab/kolab_format_journal.php
deleted file mode 100644
index 5869af0..0000000
--- a/lib/kolab/kolab_format_journal.php
+++ /dev/null
@@ -1,112 +0,0 @@
-<?php
-
-/**
- * Kolab Journal model class
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_journal extends kolab_format
-{
-    public $CTYPE = 'application/calendar+xml';
-
-    protected $read_func = 'kolabformat::readJournal';
-    protected $write_func = 'kolabformat::writeJournal';
-
-
-    function __construct($xmldata = null)
-    {
-        $this->obj = new Journal;
-        $this->xmldata = $xmldata;
-    }
-
-    /**
-     * Set properties to the kolabformat object
-     *
-     * @param array  Object data as hash array
-     */
-    public function set(&$object)
-    {
-        $this->init();
-
-        // set some automatic values if missing
-        if (!empty($object['uid']))
-            $this->obj->setUid($object['uid']);
-
-        $object['changed'] = new DateTime('now', self::$timezone);
-        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
-
-        // TODO: set object propeties
-
-        // cache this data
-        $this->data = $object;
-        unset($this->data['_formatobj']);
-    }
-
-    /**
-     *
-     */
-    public function is_valid()
-    {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
-    }
-
-    /**
-     * Load data from old Kolab2 format
-     */
-    public function fromkolab2($record)
-    {
-        $object = array(
-            'uid'     => $record['uid'],
-            'changed' => $record['last-modification-date'],
-        );
-
-        // TODO: implement this
-
-        $this->data = $object;
-    }
-
-    /**
-     * Convert the Configuration object into a hash array data structure
-     *
-     * @return array  Config object data as hash array
-     */
-    public function to_array()
-    {
-        // return cached result
-        if (!empty($this->data))
-            return $this->data;
-
-        $this->init();
-
-        // read object properties
-        $object = array(
-            'uid'     => $this->obj->uid(),
-            'created' => self::php_datetime($this->obj->created()),
-            'changed' => self::php_datetime($this->obj->lastModified()),
-        );
-
-
-        // TODO: read object properties
-
-        $this->data = $object;
-        return $this->data;
-    }
-
-}
diff --git a/lib/kolab/kolab_format_note.php b/lib/kolab/kolab_format_note.php
deleted file mode 100644
index 1c88a8b..0000000
--- a/lib/kolab/kolab_format_note.php
+++ /dev/null
@@ -1,111 +0,0 @@
-<?php
-
-/**
- * Kolab Note model class
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_note extends kolab_format
-{
-    public $CTYPE = 'application/x-vnd.kolab.note';
-
-    protected $read_func = 'kolabformat::readNote';
-    protected $write_func = 'kolabformat::writeNote';
-
-
-    function __construct($xmldata = null)
-    {
-        $this->obj = new Note;
-        $this->xmldata = $xmldata;
-    }
-
-    /**
-     * Set properties to the kolabformat object
-     *
-     * @param array  Object data as hash array
-     */
-    public function set(&$object)
-    {
-        $this->init();
-
-        // set some automatic values if missing
-        if (!empty($object['uid']))
-            $this->obj->setUid($object['uid']);
-
-        $object['changed'] = new DateTime('now', self::$timezone);
-        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
-
-        // TODO: set object propeties
-
-        // cache this data
-        $this->data = $object;
-        unset($this->data['_formatobj']);
-    }
-
-    /**
-     *
-     */
-    public function is_valid()
-    {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
-    }
-
-    /**
-     * Load data from old Kolab2 format
-     */
-    public function fromkolab2($record)
-    {
-        $object = array(
-            'uid'     => $record['uid'],
-            'changed' => $record['last-modification-date'],
-        );
-
-
-        $this->data = $object;
-    }
-
-    /**
-     * Convert the Configuration object into a hash array data structure
-     *
-     * @return array  Config object data as hash array
-     */
-    public function to_array()
-    {
-        // return cached result
-        if (!empty($this->data))
-            return $this->data;
-
-        $this->init();
-
-        // read object properties
-        $object = array(
-            'uid'       => $this->obj->uid(),
-            'created'   => self::php_datetime($this->obj->created()),
-            'changed'   => self::php_datetime($this->obj->lastModified()),
-        );
-
-
-        // TODO: read object properties
-
-        $this->data = $object;
-        return $this->data;
-    }
-
-}
diff --git a/lib/kolab/kolab_format_task.php b/lib/kolab/kolab_format_task.php
deleted file mode 100644
index 2a7a629..0000000
--- a/lib/kolab/kolab_format_task.php
+++ /dev/null
@@ -1,145 +0,0 @@
-<?php
-
-/**
- * Kolab Task (ToDo) model class
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_task extends kolab_format_xcal
-{
-    protected $read_func = 'kolabformat::readTodo';
-    protected $write_func = 'kolabformat::writeTodo';
-
-
-    function __construct($xmldata = null)
-    {
-        $this->obj = new Todo;
-        $this->xmldata = $xmldata;
-    }
-
-    /**
-     * Set properties to the kolabformat object
-     *
-     * @param array  Object data as hash array
-     */
-    public function set(&$object)
-    {
-        $this->init();
-
-        // set common xcal properties
-        parent::set($object);
-
-        $this->obj->setPercentComplete(intval($object['complete']));
-
-        if (isset($object['start']))
-            $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
-
-        $this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
-
-        $related = new vectors;
-        if (!empty($object['parent_id']))
-            $related->push($object['parent_id']);
-        $this->obj->setRelatedTo($related);
-
-        // cache this data
-        $this->data = $object;
-        unset($this->data['_formatobj']);
-    }
-
-    /**
-     *
-     */
-    public function is_valid()
-    {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
-    }
-
-    /**
-     * Convert the Configuration object into a hash array data structure
-     *
-     * @return array  Config object data as hash array
-     */
-    public function to_array()
-    {
-        // return cached result
-        if (!empty($this->data))
-            return $this->data;
-
-        $this->init();
-
-        // read common xcal props
-        $object = parent::to_array();
-
-        $object['complete'] = intval($this->obj->percentComplete());
-
-        // if due date is set
-        if ($due = $this->obj->due())
-            $object['due'] = self::php_datetime($due);
-
-        // related-to points to parent task; we only support one relation
-        $related = self::vector2array($this->obj->relatedTo());
-        if (count($related))
-            $object['parent_id'] = $related[0];
-
-        // TODO: map more properties
-
-        $this->data = $object;
-        return $this->data;
-    }
-
-    /**
-     * Load data from old Kolab2 format
-     */
-    public function fromkolab2($record)
-    {
-        $object = array(
-            'uid'     => $record['uid'],
-            'changed' => $record['last-modification-date'],
-        );
-
-        // TODO: implement this
-
-        $this->data = $object;
-    }
-
-    /**
-     * Callback for kolab_storage_cache to get object specific tags to cache
-     *
-     * @return array List of tags to save in cache
-     */
-    public function get_tags()
-    {
-        $tags = array();
-
-        if ($this->data['status'] == 'COMPLETED' || $this->data['complete'] == 100)
-            $tags[] = 'x-complete';
-
-        if ($this->data['priority'] == 1)
-            $tags[] = 'x-flagged';
-
-        if (!empty($this->data['alarms']))
-            $tags[] = 'x-has-alarms';
-
-        if ($this->data['parent_id'])
-            $tags[] = 'x-parent:' . $this->data['parent_id'];
-
-        return $tags;
-    }
-}
diff --git a/lib/kolab/kolab_format_xcal.php b/lib/kolab/kolab_format_xcal.php
deleted file mode 100644
index 1191df5..0000000
--- a/lib/kolab/kolab_format_xcal.php
+++ /dev/null
@@ -1,396 +0,0 @@
-<?php
-
-/**
- * Xcal based Kolab format class wrapping libkolabxml bindings
- *
- * Base class for xcal-based Kolab groupware objects such as event, todo, journal
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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/>.
- */
-
-abstract class kolab_format_xcal extends kolab_format
-{
-    public $CTYPE = 'application/calendar+xml';
-
-    public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
-
-    protected $sensitivity_map = array(
-        'public'       => kolabformat::ClassPublic,
-        'private'      => kolabformat::ClassPrivate,
-        'confidential' => kolabformat::ClassConfidential,
-    );
-
-    protected $role_map = array(
-        'REQ-PARTICIPANT' => kolabformat::Required,
-        'OPT-PARTICIPANT' => kolabformat::Optional,
-        'NON-PARTICIPANT' => kolabformat::NonParticipant,
-        'CHAIR' => kolabformat::Chair,
-    );
-
-    protected $rrule_type_map = array(
-        'MINUTELY' => RecurrenceRule::Minutely,
-        'HOURLY' => RecurrenceRule::Hourly,
-        'DAILY' => RecurrenceRule::Daily,
-        'WEEKLY' => RecurrenceRule::Weekly,
-        'MONTHLY' => RecurrenceRule::Monthly,
-        'YEARLY' => RecurrenceRule::Yearly,
-    );
-
-    protected $weekday_map = array(
-        'MO' => kolabformat::Monday,
-        'TU' => kolabformat::Tuesday,
-        'WE' => kolabformat::Wednesday,
-        'TH' => kolabformat::Thursday,
-        'FR' => kolabformat::Friday,
-        'SA' => kolabformat::Saturday,
-        'SU' => kolabformat::Sunday,
-    );
-
-    protected $alarm_type_map = array(
-        'DISPLAY' => Alarm::DisplayAlarm,
-        'EMAIL' => Alarm::EMailAlarm,
-        'AUDIO' => Alarm::AudioAlarm,
-    );
-
-    private $status_map = array(
-        'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
-        'IN-PROCESS'   => kolabformat::StatusInProcess,
-        'COMPLETED'    => kolabformat::StatusCompleted,
-        'CANCELLED'    => kolabformat::StatusCancelled,
-    );
-
-    protected $part_status_map = array(
-        'UNKNOWN' => kolabformat::PartNeedsAction,
-        'NEEDS-ACTION' => kolabformat::PartNeedsAction,
-        'TENTATIVE' => kolabformat::PartTentative,
-        'ACCEPTED' => kolabformat::PartAccepted,
-        'DECLINED' => kolabformat::PartDeclined,
-        'DELEGATED' => kolabformat::PartDelegated,
-      );
-
-
-    /**
-     * Convert common xcard properties into a hash array data structure
-     *
-     * @return array  Object data as hash array
-     */
-    public function to_array()
-    {
-        $status_map = array_flip($this->status_map);
-        $sensitivity_map = array_flip($this->sensitivity_map);
-
-        $object = array(
-            'uid'         => $this->obj->uid(),
-            'created'     => self::php_datetime($this->obj->created()),
-            'changed'     => self::php_datetime($this->obj->lastModified()),
-            'title'       => $this->obj->summary(),
-            'location'    => $this->obj->location(),
-            'description' => $this->obj->description(),
-            'status'      => $this->status_map[$this->obj->status()],
-            'sensitivity' => $sensitivity_map[$this->obj->classification()],
-            'priority'    => $this->obj->priority(),
-            'categories'  => self::vector2array($this->obj->categories()),
-            'start'       => self::php_datetime($this->obj->start()),
-        );
-
-        // read organizer and attendees
-        if ($organizer = $this->obj->organizer()) {
-            $object['organizer'] = array(
-                'email' => $organizer->email(),
-                'name' => $organizer->name(),
-            );
-        }
-
-        $role_map = array_flip($this->role_map);
-        $part_status_map = array_flip($this->part_status_map);
-        $attvec = $this->obj->attendees();
-        for ($i=0; $i < $attvec->size(); $i++) {
-            $attendee = $attvec->get($i);
-            $cr = $attendee->contact();
-            $object['attendees'][] = array(
-                'role' => $role_map[$attendee->role()],
-                'status' => $part_status_map[$attendee->partStat()],
-                'rsvp' => $attendee->rsvp(),
-                'email' => $cr->email(),
-                'name' => $cr->name(),
-            );
-        }
-
-        // read recurrence rule
-        if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
-            $rrule_type_map = array_flip($this->rrule_type_map);
-            $object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
-
-            if ($intvl = $rr->interval())
-                $object['recurrence']['INTERVAL'] = $intvl;
-
-            if (($count = $rr->count()) && $count > 0) {
-                $object['recurrence']['COUNT'] = $count;
-            }
-            else if ($until = self::php_datetime($rr->end())) {
-                $until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
-                $object['recurrence']['UNTIL'] = $until;
-            }
-
-            if (($byday = $rr->byday()) && $byday->size()) {
-                $weekday_map = array_flip($this->weekday_map);
-                $weekdays = array();
-                for ($i=0; $i < $byday->size(); $i++) {
-                    $daypos = $byday->get($i);
-                    $prefix = $daypos->occurence();
-                    $weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
-                }
-                $object['recurrence']['BYDAY'] = join(',', $weekdays);
-            }
-
-            if (($bymday = $rr->bymonthday()) && $bymday->size()) {
-                $object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
-            }
-
-            if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
-                $object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
-            }
-
-            if ($exceptions = $this->obj->exceptionDates()) {
-                for ($i=0; $i < $exceptions->size(); $i++) {
-                    if ($exdate = self::php_datetime($exceptions->get($i)))
-                        $object['recurrence']['EXDATE'][] = $exdate;
-                }
-            }
-        }
-
-        // read alarm
-        $valarms = $this->obj->alarms();
-        $alarm_types = array_flip($this->alarm_type_map);
-        for ($i=0; $i < $valarms->size(); $i++) {
-            $alarm = $valarms->get($i);
-            $type = $alarm_types[$alarm->type()];
-
-            if ($type == 'DISPLAY' || $type == 'EMAIL') {  // only DISPLAY and EMAIL alarms are supported
-                if ($start = self::php_datetime($alarm->start())) {
-                    $object['alarms'] = '@' . $start->format('U');
-                }
-                else if ($offset = $alarm->relativeStart()) {
-                    $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
-                    if      ($w = $offset->weeks())     $value .= $w . 'W';
-                    else if ($d = $offset->days())      $value .= $d . 'D';
-                    else if ($h = $offset->hours())     $value .= $h . 'H';
-                    else if ($m = $offset->minutes())   $value .= $m . 'M';
-                    else if ($s = $offset->seconds())   $value .= $s . 'S';
-                    else continue;
-
-                    $object['alarms'] = $value;
-                }
-                $object['alarms']  .= ':' . $type;
-                break;
-            }
-        }
-
-        return $object;
-    }
-
-
-    /**
-     * Set common xcal properties to the kolabformat object
-     *
-     * @param array  Event data as hash array
-     */
-    public function set(&$object)
-    {
-        // set some automatic values if missing
-        if (!$this->obj->created()) {
-            if (!empty($object['created']))
-                $object['created'] = new DateTime('now', self::$timezone);
-            $this->obj->setCreated(self::get_datetime($object['created']));
-        }
-
-        if (!empty($object['uid']))
-            $this->obj->setUid($object['uid']);
-
-        $object['changed'] = new DateTime('now', self::$timezone);
-        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
-
-        // increment sequence
-        $this->obj->setSequence($this->obj->sequence()+1);
-
-        $this->obj->setSummary($object['title']);
-        $this->obj->setLocation($object['location']);
-        $this->obj->setDescription($object['description']);
-        $this->obj->setPriority($object['priority']);
-        $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
-        $this->obj->setCategories(self::array2vector($object['categories']));
-
-        // process event attendees
-        $attendees = new vectorattendee;
-        foreach ((array)$object['attendees'] as $attendee) {
-            if ($attendee['role'] == 'ORGANIZER') {
-                $object['organizer'] = $attendee;
-            }
-            else {
-                $cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
-                $cr->setName($attendee['name']);
-
-                $att = new Attendee;
-                $att->setContact($cr);
-                $att->setPartStat($this->status_map[$attendee['status']]);
-                $att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
-                $att->setRSVP((bool)$attendee['rsvp']);
-
-                if ($att->isValid()) {
-                    $attendees->push($att);
-                }
-                else {
-                    rcube::raise_error(array(
-                        'code' => 600, 'type' => 'php',
-                        'file' => __FILE__, 'line' => __LINE__,
-                        'message' => "Invalid event attendee: " . json_encode($attendee),
-                    ), true);
-                }
-            }
-        }
-        $this->obj->setAttendees($attendees);
-
-        if ($object['organizer']) {
-            $organizer = new ContactReference(ContactReference::EmailReference, $object['organizer']['email']);
-            $organizer->setName($object['organizer']['name']);
-            $this->obj->setOrganizer($organizer);
-        }
-
-        // save recurrence rule
-        if ($object['recurrence']) {
-            $rr = new RecurrenceRule;
-            $rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
-
-            if ($object['recurrence']['INTERVAL'])
-                $rr->setInterval(intval($object['recurrence']['INTERVAL']));
-
-            if ($object['recurrence']['BYDAY']) {
-                $byday = new vectordaypos;
-                foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
-                    $occurrence = 0;
-                    if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
-                        $occurrence = intval($m[1]);
-                        $day = $m[2];
-                    }
-                    if (isset($this->weekday_map[$day]))
-                        $byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
-                }
-                $rr->setByday($byday);
-            }
-
-            if ($object['recurrence']['BYMONTHDAY']) {
-                $bymday = new vectori;
-                foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
-                    $bymday->push(intval($day));
-                $rr->setBymonthday($bymday);
-            }
-
-            if ($object['recurrence']['BYMONTH']) {
-                $bymonth = new vectori;
-                foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
-                    $bymonth->push(intval($month));
-                $rr->setBymonth($bymonth);
-            }
-
-            if ($object['recurrence']['COUNT'])
-                $rr->setCount(intval($object['recurrence']['COUNT']));
-            else if ($object['recurrence']['UNTIL'])
-                $rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true));
-
-            if ($rr->isValid()) {
-                $this->obj->setRecurrenceRule($rr);
-
-                // add exception dates (only if recurrence rule is valid)
-                $exdates = new vectordatetime;
-                foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
-                    $exdates->push(self::get_datetime($exdate, null, true));
-                $this->obj->setExceptionDates($exdates);
-            }
-            else {
-                rcube::raise_error(array(
-                    'code' => 600, 'type' => 'php',
-                    'file' => __FILE__, 'line' => __LINE__,
-                    'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
-                ), true);
-            }
-        }
-
-        // save alarm
-        $valarms = new vectoralarm;
-        if ($object['alarms']) {
-            list($offset, $type) = explode(":", $object['alarms']);
-
-            if ($type == 'EMAIL') {  // email alarms implicitly go to event owner
-                $recipients = new vectorcontactref;
-                $recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
-                $alarm = new Alarm($object['title'], strval($object['description']), $recipients);
-            }
-            else {  // default: display alarm
-                $alarm = new Alarm($object['title']);
-            }
-
-            if (preg_match('/^@(\d+)/', $offset, $d)) {
-                $alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
-            }
-            else if (preg_match('/^([-+]?)(\d+)([SMHDW])/', $offset, $d)) {
-                $days = $hours = $minutes = $seconds = 0;
-                switch ($d[3]) {
-                    case 'W': $days  = 7*intval($d[2]); break;
-                    case 'D': $days    = intval($d[2]); break;
-                    case 'H': $hours   = intval($d[2]); break;
-                    case 'M': $minutes = intval($d[2]); break;
-                    case 'S': $seconds = intval($d[2]); break;
-                }
-                $alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
-            }
-
-            $valarms->push($alarm);
-        }
-        $this->obj->setAlarms($valarms);
-    }
-
-    /**
-     * Callback for kolab_storage_cache to get words to index for fulltext search
-     *
-     * @return array List of words to save in cache
-     */
-    public function get_words()
-    {
-        $data = '';
-        foreach (self::$fulltext_cols as $colname) {
-            list($col, $field) = explode(':', $colname);
-
-            if ($field) {
-                $a = array();
-                foreach ((array)$this->data[$col] as $attr)
-                    $a[] = $attr[$field];
-                $val = join(' ', $a);
-            }
-            else {
-                $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
-            }
-
-            if (strlen($val))
-                $data .= $val . ' ';
-        }
-
-        return array_unique(rcube_utils::normalize_string($data, true));
-    }
-
-}
\ No newline at end of file
diff --git a/lib/kolab/kolab_storage.php b/lib/kolab/kolab_storage.php
deleted file mode 100644
index b29b416..0000000
--- a/lib/kolab/kolab_storage.php
+++ /dev/null
@@ -1,652 +0,0 @@
-<?php
-
-/**
- * Kolab storage class providing static methods to access groupware objects on a Kolab server.
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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
-{
-    const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
-    const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
-    const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
-    const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
-    const SERVERSIDE_SUBSCRIPTION = 0;
-    const CLIENTSIDE_SUBSCRIPTION = 1;
-
-    public static $last_error;
-
-    private static $ready = false;
-    private static $config;
-    private static $cache;
-    private static $imap;
-
-
-    /**
-     * Setup the environment needed by the libs
-     */
-    public static function setup()
-    {
-        if (self::$ready)
-            return true;
-
-        $rcmail = rcube::get_instance();
-        self::$config = $rcmail->config;
-        self::$imap = $rcmail->get_storage();
-        self::$ready = class_exists('kolabformat') &&
-            (self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
-
-        if (self::$ready) {
-            // set imap options
-            self::$imap->set_options(array(
-                'skip_deleted' => true,
-                'threading' => false,
-            ));
-            self::$imap->set_pagesize(9999);
-        }
-
-        return self::$ready;
-    }
-
-
-    /**
-     * Get a list of storage folders for the given data type
-     *
-     * @param string Data type to list folders for (contact,distribution-list,event,task,note)
-     *
-     * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
-     */
-    public static function get_folders($type)
-    {
-        $folders = $folderdata = array();
-
-        if (self::setup()) {
-            foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
-                $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
-            }
-        }
-
-        return $folders;
-    }
-
-
-    /**
-     * Getter for a specific storage folder
-     *
-     * @param string  IMAP folder to access (UTF7-IMAP)
-     * @return object kolab_storage_folder  The folder object
-     */
-    public static function get_folder($folder)
-    {
-        return self::setup() ? new kolab_storage_folder($folder) : null;
-    }
-
-
-    /**
-     * Getter for a single Kolab object, identified by its UID.
-     * This will search all folders storing objects of the given type.
-     *
-     * @param string Object UID
-     * @param string Object type (contact,distribution-list,event,task,note)
-     * @return array The Kolab object represented as hash array or false if not found
-     */
-    public static function get_object($uid, $type)
-    {
-        self::setup();
-        $folder = null;
-        foreach ((array)self::list_folders('', '*', $type) as $foldername) {
-            if (!$folder)
-                $folder = new kolab_storage_folder($foldername);
-            else
-                $folder->set_folder($foldername);
-
-            if ($object = $folder->get_object($uid))
-                return $object;
-        }
-
-        return false;
-    }
-
-
-    /**
-     *
-     */
-    public static function get_freebusy_server()
-    {
-        return unslashify(self::$config->get('kolab_freebusy_server', 'https://' . $_SESSION['imap_host'] . '/freebusy'));
-    }
-
-
-    /**
-     * Compose an URL to query the free/busy status for the given user
-     */
-    public static function get_freebusy_url($email)
-    {
-        return self::get_freebusy_server() . '/' . $email . '.ifb';
-    }
-
-
-    /**
-     * Creates folder ID from folder name
-     *
-     * @param string $folder Folder name (UTF7-IMAP)
-     *
-     * @return string Folder ID string
-     */
-    public static function folder_id($folder)
-    {
-        return asciiwords(strtr($folder, '/.-', '___'));
-    }
-
-
-    /**
-     * Deletes IMAP folder
-     *
-     * @param string $name Folder name (UTF7-IMAP)
-     *
-     * @return bool True on success, false on failure
-     */
-    public static function folder_delete($name)
-    {
-        // clear cached entries first
-        if ($folder = self::get_folder($name))
-            $folder->cache->purge();
-
-        $success = self::$imap->delete_folder($name);
-        self::$last_error = self::$imap->get_error_str();
-
-        return $success;
-    }
-
-    /**
-     * Creates IMAP folder
-     *
-     * @param string $name        Folder name (UTF7-IMAP)
-     * @param string $type        Folder type
-     * @param bool   $subscribed  Sets folder subscription
-     *
-     * @return bool True on success, false on failure
-     */
-    public static function folder_create($name, $type = null, $subscribed = false)
-    {
-        self::setup();
-
-        if ($saved = self::$imap->create_folder($name, $subscribed)) {
-            // set metadata for folder type
-            if ($type) {
-                $saved = self::set_folder_type($name, $type);
-
-                // revert if metadata could not be set
-                if (!$saved) {
-                    self::$imap->delete_folder($name);
-                }
-            }
-        }
-
-        if ($saved) {
-            return true;
-        }
-
-        self::$last_error = self::$imap->get_error_str();
-        return false;
-    }
-
-    /**
-     * Renames IMAP folder
-     *
-     * @param string $oldname Old folder name (UTF7-IMAP)
-     * @param string $newname New folder name (UTF7-IMAP)
-     *
-     * @return bool True on success, false on failure
-     */
-    public static function folder_rename($oldname, $newname)
-    {
-        self::setup();
-
-        $success = self::$imap->rename_folder($oldname, $newname);
-        self::$last_error = self::$imap->get_error_str();
-
-        return $success;
-    }
-
-
-    /**
-     * Rename or Create a new IMAP folder.
-     *
-     * Does additional checks for permissions and folder name restrictions
-     *
-     * @param array Hash array with folder properties and metadata
-     *  - name: Folder name
-     *  - oldname: Old folder name when changed
-     *  - parent: Parent folder to create the new one in
-     *  - type: Folder type to create
-     * @return mixed New folder name or False on failure
-     */
-    public static function folder_update(&$prop)
-    {
-        self::setup();
-
-        $folder    = rcube_charset::convert($prop['name'], RCMAIL_CHARSET, 'UTF7-IMAP');
-        $oldfolder = $prop['oldname']; // UTF7
-        $parent    = $prop['parent']; // UTF7
-        $delimiter = self::$imap->get_hierarchy_delimiter();
-
-        if (strlen($oldfolder)) {
-            $options = self::$imap->folder_info($oldfolder);
-        }
-
-        if (!empty($options) && ($options['norename'] || $options['protected'])) {
-        }
-        // sanity checks (from steps/settings/save_folder.inc)
-        else if (!strlen($folder)) {
-            self::$last_error = 'cannotbeempty';
-            return false;
-        }
-        else if (strlen($folder) > 128) {
-            self::$last_error = 'nametoolong';
-            return false;
-        }
-        else {
-            // these characters are problematic e.g. when used in LIST/LSUB
-            foreach (array($delimiter, '%', '*') as $char) {
-                if (strpos($folder, $delimiter) !== false) {
-                    self::$last_error = 'forbiddencharacter';
-                    return false;
-                }
-            }
-        }
-
-        if (!empty($options) && ($options['protected'] || $options['norename'])) {
-            $folder = $oldfolder;
-        }
-        else if (strlen($parent)) {
-            $folder = $parent . $delimiter . $folder;
-        }
-        else {
-            // add namespace prefix (when needed)
-            $folder = self::$imap->mod_folder($folder, 'in');
-        }
-
-        // Check access rights to the parent folder
-        if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
-            $parent_opts = self::$imap->folder_info($parent);
-            if ($parent_opts['namespace'] != 'personal'
-                && (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
-            ) {
-                self::$last_error = 'No permission to create folder';
-                return false;
-          }
-        }
-
-        // update the folder name
-        if (strlen($oldfolder)) {
-            if ($oldfolder != $folder) {
-                $result = self::folder_rename($oldfolder, $folder);
-          }
-          else
-              $result = true;
-        }
-        // create new folder
-        else {
-            $result = self::folder_create($folder, $prop['type'], $prop['subscribed'] === self::SERVERSIDE_SUBSCRIPTION);
-        }
-
-        // save color in METADATA
-        // TODO: also save 'showalarams' and other properties here
-
-        if ($result && $prop['color']) {
-            $meta_saved = false;
-            $ns = self::$imap->folder_namespace($folder);
-            if ($ns == 'personal')  // save in shared namespace for personal folders
-                $meta_saved = self::$imap->set_metadata($folder, array(self::COLOR_KEY_SHARED => $prop['color']));
-            if (!$meta_saved)    // try in private namespace
-                $meta_saved = self::$imap->set_metadata($folder, array(self::COLOR_KEY_PRIVATE => $prop['color']));
-            if ($meta_saved)
-                unset($prop['color']);  // unsetting will prevent fallback to local user prefs
-        }
-
-        return $result ? $folder : false;
-    }
-
-
-    /**
-     * Getter for human-readable name of Kolab object (folder)
-     * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
-     *
-     * @param string $folder    IMAP folder name (UTF7-IMAP)
-     * @param string $folder_ns Will be set to namespace name of the folder
-     *
-     * @return string Name of the folder-object
-     */
-    public static function object_name($folder, &$folder_ns=null)
-    {
-        self::setup();
-
-        $found     = false;
-        $namespace = self::$imap->get_namespace();
-
-        if (!empty($namespace['shared'])) {
-            foreach ($namespace['shared'] as $ns) {
-                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
-                    $prefix = '';
-                    $folder = substr($folder, strlen($ns[0]));
-                    $delim  = $ns[1];
-                    $found  = true;
-                    $folder_ns = 'shared';
-                    break;
-                }
-            }
-        }
-        if (!$found && !empty($namespace['other'])) {
-            foreach ($namespace['other'] as $ns) {
-                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
-                    // remove namespace prefix
-                    $folder = substr($folder, strlen($ns[0]));
-                    $delim  = $ns[1];
-                    // get username
-                    $pos    = strpos($folder, $delim);
-                    if ($pos) {
-                        $prefix = '('.substr($folder, 0, $pos).') ';
-                        $folder = substr($folder, $pos+1);
-                    }
-                    else {
-                        $prefix = '('.$folder.')';
-                        $folder = '';
-                    }
-                    $found  = true;
-                    $folder_ns = 'other';
-                    break;
-                }
-            }
-        }
-        if (!$found && !empty($namespace['personal'])) {
-            foreach ($namespace['personal'] as $ns) {
-                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
-                    // remove namespace prefix
-                    $folder = substr($folder, strlen($ns[0]));
-                    $prefix = '';
-                    $delim  = $ns[1];
-                    $found  = true;
-                    break;
-                }
-            }
-        }
-
-        if (empty($delim))
-            $delim = self::$imap->get_hierarchy_delimiter();
-
-        $folder = rcube_charset::convert($folder, 'UTF7-IMAP');
-        $folder = html::quote($folder);
-        $folder = str_replace(html::quote($delim), ' » ', $folder);
-
-        if ($prefix)
-            $folder = html::quote($prefix) . ' ' . $folder;
-
-        if (!$folder_ns)
-            $folder_ns = 'personal';
-
-        return $folder;
-    }
-
-
-    /**
-     * Helper method to generate a truncated folder name to display
-     */
-    public static function folder_displayname($origname, &$names)
-    {
-        $name = $origname;
-
-        // find folder prefix to truncate
-        for ($i = count($names)-1; $i >= 0; $i--) {
-            if (strpos($name, $names[$i] . ' » ') === 0) {
-                $length = strlen($names[$i] . ' » ');
-                $prefix = substr($name, 0, $length);
-                $count  = count(explode(' » ', $prefix));
-                $name   = str_repeat('  ', $count-1) . '» ' . substr($name, $length);
-                break;
-            }
-        }
-        $names[] = $origname;
-
-        return $name;
-    }
-
-
-    /**
-     * Creates a SELECT field with folders list
-     *
-     * @param string $type    Folder type
-     * @param array  $attrs   SELECT field attributes (e.g. name)
-     * @param string $current The name of current folder (to skip it)
-     *
-     * @return html_select SELECT object
-     */
-    public static function folder_selector($type, $attrs, $current = '')
-    {
-        // get all folders of specified type
-        $folders = self::get_folders($type);
-
-        $delim = self::$imap->get_hierarchy_delimiter();
-        $names = array();
-        $len   = strlen($current);
-
-        if ($len && ($rpos = strrpos($current, $delim))) {
-            $parent = substr($current, 0, $rpos);
-            $p_len  = strlen($parent);
-        }
-
-        // Filter folders list
-        foreach ($folders as $c_folder) {
-            $name = $c_folder->name;
-            // skip current folder and it's subfolders
-            if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
-                continue;
-            }
-
-            // always show the parent of current folder
-            if ($p_len && $name == $parent) { }
-            // skip folders where user have no rights to create subfolders
-            else if ($c_folder->get_owner() != $_SESSION['username']) {
-                $rights = $c_folder->get_myrights();
-                if (!preg_match('/[ck]/', $rights)) {
-                    continue;
-                }
-            }
-
-            $names[$name] = rcube_charset::convert($name, 'UTF7-IMAP');
-        }
-
-        // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
-        if ($p_len && !isset($names[$parent])) {
-            $names[$parent] = rcube_charset::convert($parent, 'UTF7-IMAP');
-        }
-
-        // Sort folders list
-        asort($names, SORT_LOCALE_STRING);
-
-        $folders = array_keys($names);
-        $names   = array();
-
-        // Build SELECT field of parent folder
-        $attrs['is_escaped'] = true;
-        $select = new html_select($attrs);
-        $select->add('---', '');
-
-        foreach ($folders as $name) {
-            $imap_name = $name;
-            $name      = $origname = self::object_name($name);
-
-            // find folder prefix to truncate
-            for ($i = count($names)-1; $i >= 0; $i--) {
-                if (strpos($name, $names[$i].' » ') === 0) {
-                    $length = strlen($names[$i].' » ');
-                    $prefix = substr($name, 0, $length);
-                    $count  = count(explode(' » ', $prefix));
-                    $name   = str_repeat('  ', $count-1) . '» ' . substr($name, $length);
-                    break;
-                }
-            }
-
-            $names[] = $origname;
-            $select->add($name, $imap_name);
-        }
-
-        return $select;
-    }
-
-
-    /**
-     * Returns a list of folder names
-     *
-     * @param string  Optional root folder
-     * @param string  Optional name pattern
-     * @param string  Data type to list folders for (contact,distribution-list,event,task,note,mail)
-     * @param string  Enable to return subscribed folders only
-     * @param array   Will be filled with folder-types data
-     *
-     * @return array List of folders
-     */
-    public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = false, &$folderdata = array())
-    {
-        if (!self::setup()) {
-            return null;
-        }
-
-        if (!$filter) {
-            // Get ALL folders list, standard way
-            if ($subscribed) {
-                return self::$imap->list_folders_subscribed($root, $mbox);
-            }
-            else {
-                return self::$imap->list_folders($root, $mbox);
-            }
-        }
-
-        $prefix = $root . $mbox;
-
-        // get folders types
-        $folderdata = self::$imap->get_metadata($prefix, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
-
-        if (!is_array($folderdata)) {
-            return array();
-        }
-
-        $folderdata = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
-        $regexp     = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
-
-        // In some conditions we can skip LIST command (?)
-        if ($subscribed == false && $filter != 'mail' && $prefix == '*') {
-            foreach ($folderdata as $folder => $type) {
-                if (!preg_match($regexp, $type)) {
-                    unset($folderdata[$folder]);
-                }
-            }
-            return array_keys($folderdata);
-        }
-
-        // Get folders list
-        if ($subscribed) {
-            $folders = self::$imap->list_folders_subscribed($root, $mbox);
-        }
-        else {
-            $folders = self::$imap->list_folders($root, $mbox);
-        }
-
-        // In case of an error, return empty list (?)
-        if (!is_array($folders)) {
-            return array();
-        }
-
-        // Filter folders list
-        foreach ($folders as $idx => $folder) {
-            $type = $folderdata[$folder];
-
-            if ($filter == 'mail' && empty($type)) {
-                continue;
-            }
-            if (empty($type) || !preg_match($regexp, $type)) {
-                unset($folders[$idx]);
-            }
-        }
-
-        return $folders;
-    }
-
-
-    /**
-     * Callback for array_map to select the correct annotation value
-     */
-    static function folder_select_metadata($types)
-    {
-        if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
-            return $types[self::CTYPE_KEY_PRIVATE];
-        }
-        else if (!empty($types[self::CTYPE_KEY])) {
-            list($ctype, $suffix) = explode('.', $types[self::CTYPE_KEY]);
-            return $ctype;
-        }
-        return null;
-    }
-
-
-    /**
-     * Returns type of IMAP folder
-     *
-     * @param string $folder Folder name (UTF7-IMAP)
-     *
-     * @return string Folder type
-     */
-    static function folder_type($folder)
-    {
-        self::setup();
-
-        $metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
-
-        if (!is_array($metadata)) {
-            return null;
-        }
-
-        if (!empty($metadata[$folder])) {
-            return self::folder_select_metadata($metadata[$folder]);
-        }
-
-        return 'mail';
-    }
-
-    /**
-     * Sets folder content-type.
-     *
-     * @param string $folder Folder name
-     * @param string $type   Content type
-     *
-     * @return boolean True on success
-     */
-    static function set_folder_type($folder, $type='mail')
-    {
-        list($ctype, $subtype) = explode('.', $type);
-
-        $success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
-
-        if (!$success)  // fallback: only set private annotation
-            $success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
-
-        return $success;
-    }
-}
diff --git a/lib/kolab/kolab_storage_cache.php b/lib/kolab/kolab_storage_cache.php
deleted file mode 100644
index c3e88da..0000000
--- a/lib/kolab/kolab_storage_cache.php
+++ /dev/null
@@ -1,728 +0,0 @@
-<?php
-
-/**
- * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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_cache
-{
-    private $db;
-    private $imap;
-    private $folder;
-    private $uid2msg;
-    private $objects;
-    private $index = array();
-    private $resource_uri;
-    private $enabled = true;
-    private $synched = false;
-    private $synclock = false;
-    private $ready = false;
-    private $max_sql_packet = 1046576;  // 1 MB - 2000 bytes
-    private $binary_cols = array('photo','pgppublickey','pkcs7publickey');
-
-
-    /**
-     * Default constructor
-     */
-    public function __construct(kolab_storage_folder $storage_folder = null)
-    {
-        $rcmail = rcube::get_instance();
-        $this->db = $rcmail->get_dbh();
-        $this->imap = $rcmail->get_storage();
-        $this->enabled = $rcmail->config->get('kolab_cache', false);
-
-        if ($this->enabled) {
-            // remove sync-lock on script termination
-            $rcmail->add_shutdown_function(array($this, '_sync_unlock'));
-
-            // read max_allowed_packet from mysql config
-            $this->max_sql_packet = min($this->db->get_variable('max_allowed_packet', 1048500), 4*1024*1024) - 2000;  // mysql limit or max 4 MB
-        }
-
-        if ($storage_folder)
-            $this->set_folder($storage_folder);
-    }
-
-
-    /**
-     * Connect cache with a storage folder
-     *
-     * @param kolab_storage_folder The storage folder instance to connect with
-     */
-    public function set_folder(kolab_storage_folder $storage_folder)
-    {
-        $this->folder = $storage_folder;
-
-        if (empty($this->folder->name)) {
-            $this->ready = false;
-            return;
-        }
-
-        // compose fully qualified ressource uri for this instance
-        $this->resource_uri = $this->folder->get_resource_uri();
-        $this->ready = $this->enabled;
-    }
-
-
-    /**
-     * Synchronize local cache data with remote
-     */
-    public function synchronize()
-    {
-        // only sync once per request cycle
-        if ($this->synched)
-            return;
-
-        // increase time limit
-        @set_time_limit(500);
-
-        // lock synchronization for this folder or wait if locked
-        $this->_sync_lock();
-
-        // synchronize IMAP mailbox cache
-        $this->imap->folder_sync($this->folder->name);
-
-        // compare IMAP index with object cache index
-        $imap_index = $this->imap->index($this->folder->name);
-        $this->index = $imap_index->get();
-
-        // determine objects to fetch or to invalidate
-        if ($this->ready) {
-            // read cache index
-            $sql_result = $this->db->query(
-                "SELECT msguid, uid FROM kolab_cache WHERE resource=? AND type<>?",
-                $this->resource_uri,
-                'lock'
-            );
-
-            $old_index = array();
-            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-                $old_index[] = $sql_arr['msguid'];
-                $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
-            }
-
-            // fetch new objects from imap
-            foreach (array_diff($this->index, $old_index) as $msguid) {
-                if ($object = $this->folder->read_object($msguid, '*')) {
-                    $this->_extended_insert($msguid, $object);
-                }
-            }
-            $this->_extended_insert(0, null);
-
-            // delete invalid entries from local DB
-            $del_index = array_diff($old_index, $this->index);
-            if (!empty($del_index)) {
-                $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
-                $this->db->query(
-                    "DELETE FROM kolab_cache WHERE resource=? AND msguid IN ($quoted_ids)",
-                    $this->resource_uri
-                );
-            }
-        }
-
-        // remove lock
-        $this->_sync_unlock();
-
-        $this->synched = time();
-    }
-
-
-    /**
-     * Read a single entry from cache or from IMAP directly
-     *
-     * @param string Related IMAP message UID
-     * @param string Object type to read
-     * @param string IMAP folder name the entry relates to
-     * @param array  Hash array with object properties or null if not found
-     */
-    public function get($msguid, $type = null, $foldername = null)
-    {
-        // delegate to another cache instance
-        if ($foldername && $foldername != $this->folder->name) {
-            return kolab_storage::get_folder($foldername)->cache->get($msguid, $object);
-        }
-
-        // load object if not in memory
-        if (!isset($this->objects[$msguid])) {
-            if ($this->ready) {
-                $sql_result = $this->db->query(
-                    "SELECT * FROM kolab_cache ".
-                    "WHERE resource=? AND type=? AND msguid=?",
-                    $this->resource_uri,
-                    $type ?: $this->folder->type,
-                    $msguid
-                );
-
-                if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-                    $this->objects[$msguid] = $this->_unserialize($sql_arr);
-                }
-            }
-
-            // fetch from IMAP if not present in cache
-            if (empty($this->objects[$msguid])) {
-                $result = $this->_fetch(array($msguid), $type, $foldername);
-                $this->objects[$msguid] = $result[0];
-            }
-        }
-
-        return $this->objects[$msguid];
-    }
-
-
-    /**
-     * Insert/Update a cache entry
-     *
-     * @param string Related IMAP message UID
-     * @param mixed  Hash array with object properties to save or false to delete the cache entry
-     * @param string IMAP folder name the entry relates to
-     */
-    public function set($msguid, $object, $foldername = null)
-    {
-        if (!$msguid) {
-            return;
-        }
-
-        // delegate to another cache instance
-        if ($foldername && $foldername != $this->folder->name) {
-            kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
-            return;
-        }
-
-        // remove old entry
-        if ($this->ready) {
-            $this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=? AND type<>?",
-                $this->resource_uri, $msguid, 'lock');
-        }
-
-        if ($object) {
-            // insert new object data...
-            $this->insert($msguid, $object);
-        }
-        else {
-            // ...or set in-memory cache to false
-            $this->objects[$msguid] = $object;
-        }
-    }
-
-
-    /**
-     * Insert a cache entry
-     *
-     * @param string Related IMAP message UID
-     * @param mixed  Hash array with object properties to save or false to delete the cache entry
-     */
-    public function insert($msguid, $object)
-    {
-        // write to cache
-        if ($this->ready) {
-            $sql_data = $this->_serialize($object);
-            $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
-
-            $result = $this->db->query(
-                "INSERT INTO kolab_cache ".
-                " (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words)".
-                " VALUES (?, ?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ?, ?, ?)",
-                $this->resource_uri,
-                $objtype,
-                $msguid,
-                $object['uid'],
-                $sql_data['changed'],
-                $sql_data['data'],
-                $sql_data['xml'],
-                $sql_data['dtstart'],
-                $sql_data['dtend'],
-                $sql_data['tags'],
-                $sql_data['words']
-            );
-
-            if (!$this->db->affected_rows($result)) {
-                rcube::raise_error(array(
-                    'code' => 900, 'type' => 'php',
-                    'message' => "Failed to write to kolab cache"
-                ), true);
-            }
-        }
-
-        // keep a copy in memory for fast access
-        $this->objects[$msguid] = $object;
-        $this->uid2msg[$object['uid']] = $msguid;
-    }
-
-
-    /**
-     * Move an existing cache entry to a new resource
-     *
-     * @param string Entry's IMAP message UID
-     * @param string Entry's Object UID
-     * @param string Target IMAP folder to move it to
-     */
-    public function move($msguid, $objuid, $target_folder)
-    {
-        $target = kolab_storage::get_folder($target_folder);
-
-        // resolve new message UID in target folder
-        if ($new_msguid = $target->cache->uid2msguid($objuid)) {
-            $this->db->query(
-                "UPDATE kolab_cache SET resource=?, msguid=? ".
-                "WHERE resource=? AND msguid=? AND type<>?",
-                $target->get_resource_uri(),
-                $new_msguid,
-                $this->resource_uri,
-                $msguid,
-                'lock'
-            );
-        }
-        else {
-            // just clear cache entry
-            $this->set($msguid, false);
-        }
-
-        unset($this->uid2msg[$uid]);
-    }
-
-
-    /**
-     * Remove all objects from local cache
-     */
-    public function purge($type = null)
-    {
-        $result = $this->db->query(
-            "DELETE FROM kolab_cache WHERE resource=?".
-            ($type ? ' AND type=?' : ''),
-            $this->resource_uri,
-            $type
-        );
-        return $this->db->affected_rows($result);
-    }
-
-
-    /**
-     * Select Kolab objects filtered by the given query
-     *
-     * @param array Pseudo-SQL query as list of filter parameter triplets
-     *   triplet: array('<colname>', '<comparator>', '<value>')
-     * @param boolean Set true to only return UIDs instead of complete objects
-     * @return array List of Kolab data objects (each represented as hash array) or UIDs
-     */
-    public function select($query = array(), $uids = false)
-    {
-        $result = array();
-
-        // read from local cache DB (assume it to be synchronized)
-        if ($this->ready) {
-            $sql_result = $this->db->query(
-                "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM kolab_cache ".
-                "WHERE resource=? " . $this->_sql_where($query),
-                $this->resource_uri
-            );
-
-            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-                if ($uids) {
-                    $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
-                    $result[] = $sql_arr['uid'];
-                }
-                else if ($object = $this->_unserialize($sql_arr)) {
-                    $result[] = $object;
-                }
-            }
-        }
-        else {
-            // extract object type from query parameter
-            $filter = $this->_query2assoc($query);
-
-            // use 'list' for folder's default objects
-            if ($filter['type'] == $this->type) {
-                $index = $this->index;
-            }
-            else {  // search by object type
-                $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
-                $index = $this->imap->search_once($this->folder->name, $search)->get();
-            }
-
-            // fetch all messages in $index from IMAP
-            $result = $uids ? $this->_fetch_uids($index, $filter['type']) : $this->_fetch($index, $filter['type']);
-
-            // TODO: post-filter result according to query
-        }
-
-        return $result;
-    }
-
-
-    /**
-     * Get number of objects mathing the given query
-     *
-     * @param array  $query Pseudo-SQL query as list of filter parameter triplets
-     * @return integer The number of objects of the given type
-     */
-    public function count($query = array())
-    {
-        $count = 0;
-
-        // cache is in sync, we can count records in local DB
-        if ($this->synched) {
-            $sql_result = $this->db->query(
-                "SELECT COUNT(*) AS numrows FROM kolab_cache ".
-                "WHERE resource=? " . $this->_sql_where($query),
-                $this->resource_uri
-            );
-
-            $sql_arr = $this->db->fetch_assoc($sql_result);
-            $count = intval($sql_arr['numrows']);
-        }
-        else {
-            // search IMAP by object type
-            $filter = $this->_query2assoc($query);
-            $ctype  = kolab_format::KTYPE_PREFIX . $filter['type'];
-            $index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
-            $count = $index->count();
-        }
-
-        return $count;
-    }
-
-
-    /**
-     * Helper method to compose a valid SQL query from pseudo filter triplets
-     */
-    private function _sql_where($query)
-    {
-        $sql_where = '';
-        foreach ($query as $param) {
-            if ($param[1] == '=' && is_array($param[2])) {
-                $qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
-                $param[1] = 'IN';
-            }
-            else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') {
-                $not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : '';
-                $param[1] = $not . 'LIKE';
-                $qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
-            }
-            else if ($param[0] == 'tags') {
-                $param[1] = 'LIKE';
-                $qvalue = $this->db->quote('% '.$param[2].' %');
-            }
-            else {
-                $qvalue = $this->db->quote($param[2]);
-            }
-
-            $sql_where .= sprintf(' AND %s %s %s',
-                $this->db->quote_identifier($param[0]),
-                $param[1],
-                $qvalue
-            );
-        }
-
-        return $sql_where;
-    }
-
-    /**
-     * Helper method to convert the given pseudo-query triplets into
-     * an associative filter array with 'equals' values only
-     */
-    private function _query2assoc($query)
-    {
-        // extract object type from query parameter
-        $filter = array();
-        foreach ($query as $param) {
-            if ($param[1] == '=')
-                $filter[$param[0]] = $param[2];
-        }
-        return $filter;
-    }
-
-    /**
-     * Fetch messages from IMAP
-     *
-     * @param array  List of message UIDs to fetch
-     * @param string Requested object type or * for all
-     * @param string IMAP folder to read from
-     * @return array List of parsed Kolab objects
-     */
-    private function _fetch($index, $type = null, $folder = null)
-    {
-        $results = array();
-        foreach ((array)$index as $msguid) {
-            if ($object = $this->folder->read_object($msguid, $type, $folder)) {
-                $results[] = $object;
-                $this->set($msguid, $object);
-            }
-        }
-
-        return $results;
-    }
-
-
-    /**
-     * Fetch object UIDs (aka message subjects) from IMAP
-     *
-     * @param array List of message UIDs to fetch
-     * @param string Requested object type or * for all
-     * @param string IMAP folder to read from
-     * @return array List of parsed Kolab objects
-     */
-    private function _fetch_uids($index, $type = null)
-    {
-        if (!$type)
-            $type = $this->folder->type;
-
-        $results = array();
-        foreach ((array)$this->imap->fetch_headers($this->folder->name, $index, false) as $msguid => $headers) {
-            $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
-
-            // check object type header and abort on mismatch
-            if ($type != '*' && $object_type != $type)
-                return false;
-
-            $uid = $headers->subject;
-            $this->uid2msg[$uid] = $msguid;
-            $results[] = $uid;
-        }
-
-        return $results;
-    }
-
-
-    /**
-     * Helper method to convert the given Kolab object into a dataset to be written to cache
-     */
-    private function _serialize($object)
-    {
-        $bincols = array_flip($this->binary_cols);
-        $sql_data = array('changed' => null, 'dtstart' => null, 'dtend' => null, 'xml' => '', 'tags' => '', 'words' => '');
-        $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
-
-        // set type specific values
-        if ($objtype == 'event') {
-            // database runs in server's timezone so using date() is what we want
-            $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
-            $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['end'])   ? $object['end']->format('U')   : $object['end']);
-
-            // extend date range for recurring events
-            if ($object['recurrence']) {
-                $recurrence = new kolab_date_recurrence($object);
-                $sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +1 year'));
-            }
-        }
-        else if ($objtype == 'task') {
-            if ($object['start'])
-                $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
-            if ($object['due'])
-                $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['due'])   ? $object['due']->format('U')   : $object['due']);
-        }
-
-        if ($object['changed']) {
-            $sql_data['changed'] = date('Y-m-d H:i:s', is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
-        }
-
-        if ($object['_formatobj']) {
-            $sql_data['xml'] = preg_replace('!(</?[a-z0-9:-]+>)[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write());
-            $sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' ';  // pad with spaces for strict/prefix search
-            $sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
-        }
-
-        // extract object data
-        $data = array();
-        foreach ($object as $key => $val) {
-            if ($val === "" || $val === null) {
-                // skip empty properties
-                continue;
-            }
-            if (isset($bincols[$key])) {
-                $data[$key] = base64_encode($val);
-            }
-            else if ($key[0] != '_') {
-                $data[$key] = $val;
-            }
-            else if ($key == '_attachments') {
-                foreach ($val as $k => $att) {
-                    unset($att['content'], $att['path']);
-                    if ($att['id'])
-                        $data[$key][$k] = $att;
-                }
-            }
-        }
-
-        $sql_data['data'] = serialize($data);
-        return $sql_data;
-    }
-
-    /**
-     * Helper method to turn stored cache data into a valid storage object
-     */
-    private function _unserialize($sql_arr)
-    {
-        $object = unserialize($sql_arr['data']);
-
-        // decode binary properties
-        foreach ($this->binary_cols as $key) {
-            if (!empty($object[$key]))
-                $object[$key] = base64_decode($object[$key]);
-        }
-
-        // add meta data
-        $object['_type'] = $sql_arr['type'];
-        $object['_msguid'] = $sql_arr['msguid'];
-        $object['_mailbox'] = $this->folder->name;
-        $object['_formatobj'] = kolab_format::factory($sql_arr['type'], $sql_arr['xml']);
-
-        return $object;
-    }
-
-    /**
-     * Write records into cache using extended inserts to reduce the number of queries to be executed
-     *
-     * @param int  Message UID. Set 0 to commit buffered inserts
-     * @param array Kolab object to cache
-     */
-    private function _extended_insert($msguid, $object)
-    {
-        static $buffer = '';
-
-        $line = '';
-        if ($object) {
-            $sql_data = $this->_serialize($object);
-            $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
-
-            $values = array(
-                $this->db->quote($this->resource_uri),
-                $this->db->quote($objtype),
-                $this->db->quote($msguid),
-                $this->db->quote($object['uid']),
-                $this->db->now(),
-                $this->db->quote($sql_data['changed']),
-                $this->db->quote($sql_data['data']),
-                $this->db->quote($sql_data['xml']),
-                $this->db->quote($sql_data['dtstart']),
-                $this->db->quote($sql_data['dtend']),
-                $this->db->quote($sql_data['tags']),
-                $this->db->quote($sql_data['words']),
-            );
-            $line = '(' . join(',', $values) . ')';
-        }
-
-        if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet))) {
-            $result = $this->db->query(
-                "INSERT INTO kolab_cache ".
-                " (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words)".
-                " VALUES $buffer"
-            );
-            if (!$this->db->affected_rows($result)) {
-                rcube::raise_error(array(
-                    'code' => 900, 'type' => 'php',
-                    'message' => "Failed to write to kolab cache"
-                ), true);
-            }
-
-            $buffer = '';
-        }
-
-        $buffer .= ($buffer ? ',' : '') . $line;
-    }
-
-    /**
-     * Check lock record for this folder and wait if locked or set lock
-     */
-    private function _sync_lock()
-    {
-        if (!$this->ready)
-            return;
-
-        $sql_arr = $this->db->fetch_assoc($this->db->query(
-            "SELECT msguid AS locked, ".$this->db->unixtimestamp('created')." AS created FROM kolab_cache ".
-            "WHERE resource=? AND type=?",
-            $this->resource_uri,
-            'lock'
-        ));
-
-        // abort if database is not set-up
-        if ($this->db->is_error()) {
-            $this->ready = false;
-            return;
-        }
-
-        $this->synclock = true;
-
-        // create lock record if not exists
-        if (!$sql_arr) {
-            $this->db->query(
-                "INSERT INTO kolab_cache (resource, type, msguid, created, uid, data, xml)".
-                " VALUES (?, ?, 1, ?, '', '', '')",
-                $this->resource_uri,
-                'lock',
-                date('Y-m-d H:i:s')
-            );
-        }
-        // wait if locked (expire locks after 10 minutes)
-        else if (intval($sql_arr['locked']) > 0 && (time() - $sql_arr['created']) < 600) {
-            usleep(500000);
-            return $this->_sync_lock();
-        }
-        // set lock
-        else {
-            $this->db->query(
-                "UPDATE kolab_cache SET msguid=1, created=? ".
-                "WHERE resource=? AND type=?",
-                date('Y-m-d H:i:s'),
-                $this->resource_uri,
-                'lock'
-            );
-        }
-    }
-
-    /**
-     * Remove lock for this folder
-     */
-    public function _sync_unlock()
-    {
-        if (!$this->ready || !$this->synclock)
-            return;
-
-        $this->db->query(
-            "UPDATE kolab_cache SET msguid=0 ".
-            "WHERE resource=? AND type=?",
-            $this->resource_uri,
-            'lock'
-        );
-
-        $this->synclock = false;
-    }
-
-    /**
-     * Resolve an object UID into an IMAP message UID
-     *
-     * @param string  Kolab object UID
-     * @param boolean Include deleted objects
-     * @return int The resolved IMAP message UID
-     */
-    public function uid2msguid($uid, $deleted = false)
-    {
-        if (!isset($this->uid2msg[$uid])) {
-            // use IMAP SEARCH to get the right message
-            $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid);
-            $results = $index->get();
-            $this->uid2msg[$uid] = $results[0];
-        }
-
-        return $this->uid2msg[$uid];
-    }
-
-}
diff --git a/lib/kolab/kolab_storage_folder.php b/lib/kolab/kolab_storage_folder.php
deleted file mode 100644
index 08bf669..0000000
--- a/lib/kolab/kolab_storage_folder.php
+++ /dev/null
@@ -1,838 +0,0 @@
-<?php
-
-/**
- * The kolab_storage_folder class represents an IMAP folder on the Kolab server.
- *
- * @version @package_version@
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2012, Kolab Systems AG <contact at 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
-{
-    /**
-     * The folder name.
-     * @var string
-     */
-    public $name;
-
-    /**
-     * The type of this folder.
-     * @var string
-     */
-    public $type;
-
-    /**
-     * Is this folder set to be the default for its type
-     * @var boolean
-     */
-    public $default = false;
-
-    /**
-     * Is this folder set to be default
-     * @var boolean
-     */
-    public $cache;
-
-    private $type_annotation;
-    private $imap;
-    private $info;
-    private $owner;
-    private $resource_uri;
-    private $uid2msg = array();
-
-
-    /**
-     * Default constructor
-     */
-    function __construct($name, $type = null)
-    {
-        $this->imap = rcube::get_instance()->get_storage();
-        $this->imap->set_options(array('skip_deleted' => true));
-        $this->cache = new kolab_storage_cache($this);
-        $this->set_folder($name, $type);
-    }
-
-
-    /**
-     * Set the IMAP folder this instance connects to
-     *
-     * @param string The folder name/path
-     * @param string Optional folder type if known
-     */
-    public function set_folder($name, $ftype = null)
-    {
-        $this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name);
-
-        list($this->type, $suffix) = explode('.', $this->type_annotation);
-        $this->default      = $suffix == 'default';
-        $this->name         = $name;
-        $this->resource_uri = null;
-
-        $this->imap->set_folder($this->name);
-        $this->cache->set_folder($this);
-    }
-
-
-    /**
-     *
-     */
-    private function get_folder_info()
-    {
-        if (!isset($this->info))
-            $this->info = $this->imap->folder_info($this->name);
-
-        return $this->info;
-    }
-
-
-    /**
-     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
-     *
-     * @param array List of metadata keys to read
-     * @return array Metadata entry-value hash array on success, NULL on error
-     */
-    public function get_metadata($keys)
-    {
-        $metadata = $this->imap->get_metadata($this->name, (array)$keys);
-        return $metadata[$this->name];
-    }
-
-
-    /**
-     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
-     *
-     * @param array  $entries Entry-value array (use NULL value as NIL)
-     * @return boolean True on success, False on failure
-     */
-    public function set_metadata($entries)
-    {
-        return $this->imap->set_metadata($this->name, $entries);
-    }
-
-
-    /**
-     * Returns the owner of the folder.
-     *
-     * @return string  The owner of this folder.
-     */
-    public function get_owner()
-    {
-        // return cached value
-        if (isset($this->owner))
-            return $this->owner;
-
-        $info = $this->get_folder_info();
-        $rcmail = rcube::get_instance();
-
-        switch ($info['namespace']) {
-        case 'personal':
-            $this->owner = $rcmail->get_user_name();
-            break;
-
-        case 'shared':
-            $this->owner = 'anonymous';
-            break;
-
-        default:
-            $owner = '';
-            list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
-            if (strpos($user, '@') === false) {
-                $domain = strstr($rcmail->get_user_name(), '@');
-                if (!empty($domain))
-                    $user .= $domain;
-            }
-            $this->owner = $user;
-            break;
-        }
-
-        return $this->owner;
-    }
-
-
-    /**
-     * Getter for the name of the namespace to which the IMAP folder belongs
-     *
-     * @return string Name of the namespace (personal, other, shared)
-     */
-    public function get_namespace()
-    {
-        return $this->imap->folder_namespace($this->name);
-    }
-
-
-    /**
-     * Get IMAP ACL information for this folder
-     *
-     * @return string  Permissions as string
-     */
-    public function get_myrights()
-    {
-        $rights = $this->info['rights'];
-
-        if (!is_array($rights))
-            $rights = $this->imap->my_rights($this->name);
-
-        return join('', (array)$rights);
-    }
-
-
-    /**
-     * 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);
-                $subpath = $suffix;
-            }
-        }
-        else {
-            $subpath = $this->name;
-        }
-
-        // compose fully qualified ressource uri for this instance
-        $this->resource_uri = 'imap://' . urlencode($this->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
-        return $this->resource_uri;
-    }
-
-    /**
-     * Check subscription status of this folder
-     *
-     * @param string Subscription type (kolab_storage::SERVERSIDE_SUBSCRIPTION or kolab_storage::CLIENTSIDE_SUBSCRIPTION)
-     * @return boolean True if subscribed, false if not
-     */
-    public function is_subscribed($type = 0)
-    {
-        static $subscribed;  // local cache
-
-        if ($type == kolab_storage::SERVERSIDE_SUBSCRIPTION) {
-            if (!$subscribed)
-                $subscribed = $this->imap->list_folders_subscribed();
-
-            return in_array($this->name, $subscribed);
-        }
-        else if (kolab_storage::CLIENTSIDE_SUBSCRIPTION) {
-            // TODO: implement this
-            return true;
-        }
-
-        return false;
-    }
-
-    /**
-     * Change subscription status of this folder
-     *
-     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
-     * @param string  Subscription type (kolab_storage::SERVERSIDE_SUBSCRIPTION or kolab_storage::CLIENTSIDE_SUBSCRIPTION)
-     * @return True on success, false on error
-     */
-    public function subscribe($subscribed, $type = 0)
-    {
-        if ($type == kolab_storage::SERVERSIDE_SUBSCRIPTION) {
-            return $subscribed ? $this->imap->subscribe($this->name) : $this->imap->unsubscribe($this->name);
-        }
-        else {
-          // TODO: implement this
-        }
-
-        return false;
-    }
-
-
-    /**
-     * 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($type_or_query = null)
-    {
-        if (!$type_or_query)
-            $query = array(array('type','=',$this->type));
-        else if (is_string($type_or_query))
-            $query = array(array('type','=',$type_or_query));
-        else
-            $query = $this->_prepare_query((array)$type_or_query);
-
-        // synchronize cache first
-        $this->cache->synchronize();
-
-        return $this->cache->count($query);
-    }
-
-
-    /**
-     * List all Kolab objects of the given type
-     *
-     * @param string  $type Object type (e.g. contact, event, todo, journal, note, configuration)
-     * @return array  List of Kolab data objects (each represented as hash array)
-     */
-    public function get_objects($type = null)
-    {
-        if (!$type) $type = $this->type;
-
-        // synchronize caches
-        $this->cache->synchronize();
-
-        // fetch objects from cache
-        return $this->cache->select(array(array('type','=',$type)));
-    }
-
-
-    /**
-     * Select *some* Kolab objects matching the given query
-     *
-     * @param array Pseudo-SQL query as list of filter parameter triplets
-     *   triplet: array('<colname>', '<comparator>', '<value>')
-     * @return array List of Kolab data objects (each represented as hash array)
-     */
-    public function select($query = array())
-    {
-        // check query argument
-        if (empty($query))
-            return $this->get_objects();
-
-        // synchronize caches
-        $this->cache->synchronize();
-
-        // fetch objects from cache
-        return $this->cache->select($this->_prepare_query($query));
-    }
-
-
-    /**
-     * 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())
-    {
-        // synchronize caches
-        $this->cache->synchronize();
-
-        // fetch UIDs from cache
-        return $this->cache->select($this->_prepare_query($query), true);
-    }
-
-
-    /**
-     * Helper method to sanitize query arguments
-     */
-    private function _prepare_query($query)
-    {
-        $type = null;
-        foreach ($query as $i => $param) {
-            if ($param[0] == 'type') {
-                $type = $param[2];
-            }
-            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]);
-            }
-        }
-
-        // add type selector if not in $query
-        if (!$type)
-            $query[] = array('type','=',$this->type);
-
-        return $query;
-    }
-
-
-    /**
-     * Getter for a single Kolab object, identified by its UID
-     *
-     * @param string Object UID
-     * @return array The Kolab object represented as hash array
-     */
-    public function get_object($uid)
-    {
-        // synchronize caches
-        $this->cache->synchronize();
-
-        $msguid = $this->cache->uid2msguid($uid);
-        if ($msguid && ($object = $this->cache->get($msguid)))
-            return $object;
-
-        return false;
-    }
-
-
-    /**
-     * 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
-     * @return mixed  The attachment content as binary string
-     */
-    public function get_attachment($uid, $part, $mailbox = null)
-    {
-        if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
-            $this->imap->set_folder($mailbox ? $mailbox : $this->name);
-            return $this->imap->get_message_part($msguid, $part);
-        }
-
-        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 (!$type) $type = $this->type;
-        if (!$folder) $folder = $this->name;
-
-        $this->imap->set_folder($folder);
-
-        $headers = $this->imap->get_message_headers($msguid);
-
-        // Message doesn't exist?
-        if (empty($headers)) {
-            return false;
-        }
-
-        $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
-        $content_type  = kolab_format::KTYPE_PREFIX . $object_type;
-
-        // check object type header and abort on mismatch
-        if ($type != '*' && $object_type != $type)
-            return false;
-
-        $message = new rcube_message($msguid);
-        $attachments = array();
-
-        // get XML part
-        foreach ((array)$message->attachments as $part) {
-            if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
-                $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
-            }
-            else if ($part->filename || $part->content_id) {
-                $key = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
-                $attachments[$key] = array(
-                    'id' => $part->mime_id,
-                    'name' => $part->filename,
-                    'mimetype' => $part->mimetype,
-                    'size' => $part->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;
-        }
-
-        $format = kolab_format::factory($object_type);
-
-        if (is_a($format, 'PEAR_Error'))
-            return false;
-
-        // check kolab format version
-        $mime_version = $headers->others['x-kolab-mime-version'];
-        if (empty($mime_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)
-                $mime_version = 2.0;
-            else
-                $mime_version = 3.0; // assume 3.0
-        }
-
-        if ($mime_version <= 2.0) {
-            // read Kolab 2.0 format
-            $handler = class_exists('Horde_Kolab_Format') ? Horde_Kolab_Format::factory('XML', $xmltype, array('subtype' => $subtype)) : null;
-            if (!is_object($handler) || is_a($handler, 'PEAR_Error')) {
-                return false;
-            }
-
-            // XML-to-array
-            $object = $handler->load($xml);
-            $format->fromkolab2($object);
-        }
-        else {
-            // load Kolab 3 format using libkolabxml
-            $format->load($xml);
-        }
-
-        if ($format->is_valid()) {
-            $object = $format->to_array();
-            $object['_type'] = $object_type;
-            $object['_msguid'] = $msguid;
-            $object['_mailbox'] = $this->name;
-            $object['_attachments'] = array_merge((array)$object['_attachments'], $attachments);
-            $object['_formatobj'] = $format;
-
-            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);
-        }
-
-        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 boolean          True on success, false on error
-     */
-    public function save(&$object, $type = null, $uid = null)
-    {
-        if (!$type)
-            $type = $this->type;
-
-        // copy attachments from old message
-        if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $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 ($key == 'photo.attachment' && !isset($object['photo']) && !$object['_attachments'][$key]['content'] && $att['id']) {
-                    $object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
-                    unset($object['_attachments'][$key]);
-                }
-            }
-        }
-
-        // generate unique keys (used as content-id) for attachments
-        if (is_array($object['_attachments'])) {
-            $numatt = count($object['_attachments']);
-            foreach ($object['_attachments'] as $key => $attachment) {
-                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) . $ext;
-
-                    $object['_attachments'][$cid] = $attachment;
-                    unset($object['_attachments'][$key]);
-                }
-            }
-        }
-
-        if ($raw_msg = $this->build_message($object, $type)) {
-            $result = $this->imap->save_message($this->name, $raw_msg, '', false);
-
-            // delete old message
-            if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
-                $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
-                $this->cache->set($object['_msguid'], false, $object['_mailbox']);
-            }
-            else if ($result && $uid && ($msguid = $this->cache->uid2msguid($uid))) {
-                $this->imap->delete_message($msguid, $this->name);
-                $this->cache->set($object['_msguid'], false);
-            }
-
-            // update cache with new UID
-            if ($result) {
-                $object['_msguid'] = $result;
-                $this->cache->insert($result, $object);
-            }
-        }
-        
-        return $result;
-    }
-
-
-    /**
-     * 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)
-    {
-        $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
-        $success = false;
-
-        if ($msguid && $expunge) {
-            $success = $this->imap->delete_message($msguid, $this->name);
-        }
-        else if ($msguid) {
-            $success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
-        }
-
-        if ($success) {
-            $this->cache->set($msguid, false);
-        }
-
-        return $success;
-    }
-
-
-    /**
-     *
-     */
-    public function delete_all()
-    {
-        $this->cache->purge();
-        return $this->imap->clear_folder($this->name);
-    }
-
-
-    /**
-     * Restore a previously deleted object
-     *
-     * @param string Object UID
-     * @return mixed Message UID on success, false on error
-     */
-    public function undelete($uid)
-    {
-        if ($msguid = $this->cache->uid2msguid($uid, true)) {
-            if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) {
-                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 ($msguid = $this->cache->uid2msguid($uid)) {
-            if ($success = $this->imap->move_message($msguid, $target_folder, $this->name)) {
-                $this->cache->move($msguid, $uid, $target_folder);
-                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
-     */
-    private function build_message(&$object, $type)
-    {
-        // 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);
-
-        if (PEAR::isError($format))
-            return false;
-
-        $format->set($object);
-        $xml = $format->write();
-        $object['uid'] = $format->uid;  // read UID from format
-        $object['_formatobj'] = $format;
-
-        if (!$format->is_valid() || empty($object['uid'])) {
-            return false;
-        }
-
-        $mime = new Mail_mime("\r\n");
-        $rcmail = rcube::get_instance();
-        $headers = array();
-        $part_id = 1;
-
-        if ($ident = $rcmail->user->get_identity()) {
-            $headers['From'] = $ident['email'];
-            $headers['To'] = $ident['email'];
-        }
-        $headers['Date'] = date('r');
-        $headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
-        $headers['X-Kolab-Mime-Version'] = kolab_format::VERSION;
-        $headers['Subject'] = $object['uid'];
-//        $headers['Message-ID'] = $rcmail->gen_message_id();
-        $headers['User-Agent'] = $rcmail->config->get('useragent');
-
-        $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");
-
-        $mime->addAttachment($xml,  // file
-            $format->CTYPE,         // content-type
-            'kolab.xml',            // filename
-            false,                  // is_file
-            '8bit',                 // encoding
-            'attachment',           // disposition
-            RCMAIL_CHARSET          // charset
-        );
-        $part_id++;
-
-        // save object attachments as separate parts
-        // TODO: optimize memory consumption by using tempfiles for transfer
-        foreach ((array)$object['_attachments'] as $key => $att) {
-            if (empty($att['content']) && !empty($att['id'])) {
-                $msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
-                $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox']);
-            }
-
-            $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCMAIL_CHARSET, 'quoted-printable'));
-            $name = !empty($att['name']) ? $att['name'] : $key;
-
-            if (!empty($att['content'])) {
-                $mime->addAttachment($att['content'], $att['mimetype'], $name, false, 'base64', 'attachment', '', '', '', null, null, '', RCMAIL_CHARSET, $headers);
-                $part_id++;
-            }
-            else if (!empty($att['path'])) {
-                $mime->addAttachment($att['path'], $att['mimetype'], $name, true, 'base64', 'attachment', '', '', '', null, null, '', RCMAIL_CHARSET, $headers);
-                $part_id++;
-            }
-
-            $object['_attachments'][$key]['id'] = $part_id;
-        }
-
-        return $mime->getMessage();
-    }
-
-
-    /**
-     * 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(), $owner, $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)
-    {
-        require_once('HTTP/Request2.php');
-
-        try {
-            $rcmail = rcube::get_instance();
-            $request = new HTTP_Request2($url);
-            $request->setConfig(array('ssl_verify_peer' => $rcmail->config->get('kolab_ssl_verify_peer', true)));
-
-            // 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;
-    }
-
-}
-
diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php
index 21c3f03..27ad3d9 100644
--- a/lib/kolab_sync.php
+++ b/lib/kolab_sync.php
@@ -61,6 +61,10 @@ class kolab_sync extends rcube
         return self::$instance;
     }
 
+
+    /**
+     * Initialization of class instance
+     */
     public function startup()
     {
         // Initialize Syncroton Logger
@@ -72,11 +76,12 @@ class kolab_sync extends rcube
         //          e.g. are not using output or rcmail objects or
         //          doesn't throw errors when using them
         $plugins = (array)$this->config->get('activesync_plugins', array('kolab_auth', 'kolab_folders'));
+        $required = array('libkolab', 'kolab_folders');
 
         // Initialize/load plugins
         $this->plugins = kolab_sync_plugin_api::get_instance();
         $this->plugins->init($this, $this->task);
-        $this->plugins->load_plugins($plugins);
+        $this->plugins->load_plugins($plugins, $required);
     }
 
 
diff --git a/lib/plugins/kolab_auth/LICENSE b/lib/plugins/kolab_auth/LICENSE
new file mode 100644
index 0000000..dba13ed
--- /dev/null
+++ b/lib/plugins/kolab_auth/LICENSE
@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/lib/plugins/kolab_auth/config.inc.php.dist b/lib/plugins/kolab_auth/config.inc.php.dist
new file mode 100644
index 0000000..6ddfc63
--- /dev/null
+++ b/lib/plugins/kolab_auth/config.inc.php.dist
@@ -0,0 +1,64 @@
+<?php
+
+// The id of the LDAP address book (which refers to the $rcmail_config['ldap_public'])
+// or complete addressbook definition array.
+$rcmail_config['kolab_auth_addressbook'] = '';
+
+// This will overwrite defined filter
+$rcmail_config['kolab_auth_filter'] = '(&(objectClass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)(alias=%fu)))';
+
+// Use this fields (from fieldmap configuration) to get authentication ID
+$rcmail_config['kolab_auth_login'] = 'email';
+
+// Use this fields (from fieldmap configuration) for default identity.
+// If the value array contains more than one field, first non-empty will be used
+// Note: These aren't LDAP attributes, but field names in config
+// Note: If there's more than one email address, as many identities will be created
+$rcmail_config['kolab_auth_name']  = array('name', 'cn');
+$rcmail_config['kolab_auth_email'] = array('email');
+
+// Login and password of the admin user. Enables "Login As" feature.
+$rcmail_config['kolab_auth_admin_login']    = '';
+$rcmail_config['kolab_auth_admin_password'] = '';
+
+// Enable audit logging for abuse of administrative privileges.
+$rcmail_config['kolab_auth_auditlog'] = true;
+
+// Role field (from fieldmap configuration)
+$rcmail_config['kolab_auth_role'] = 'role';
+// The required value for the role attribute to contain should the user be allowed
+// to login as another user.
+$rcmail_config['kolab_auth_role_value'] = '';
+
+// Administrative group name to which user must be assigned to
+// which adds privilege to login as another user.
+$rcmail_config['kolab_auth_group'] = '';
+
+// Enable plugins on a role-by-role basis. In this example, the 'acl' plugin
+// is enabled for people with a 'cn=professional-user,dc=mykolab,dc=ch' role.
+//
+// Note that this does NOT mean the 'acl' plugin is disabled for other people.
+$rcmail_config['kolab_auth_role_plugins'] = Array(
+        'cn=professional-user,dc=mykolab,dc=ch' => Array(
+                'acl',
+            ),
+    );
+
+// Settings on a role-by-role basis. In this example, the 'htmleditor' setting
+// is enabled(1) for people with a 'cn=professional-user,dc=mykolab,dc=ch' role,
+// and it cannot be overridden. Sample use-case: disable htmleditor for normal people,
+// do not allow the setting to be controlled through the preferences, enable the
+// html editor for professional users and allow them to override the setting in
+// the preferences.
+$rcmail_config['kolab_auth_role_settings'] = Array(
+        'cn=professional-user,dc=mykolab,dc=ch' => Array(
+                'htmleditor' => Array(
+                        'mode' => 'override',
+                        'value' => 1,
+                        'allow_override' => true
+                    ),
+            ),
+    );
+
+
+?>
diff --git a/lib/plugins/kolab_auth/kolab_auth.php b/lib/plugins/kolab_auth/kolab_auth.php
new file mode 100644
index 0000000..620def5
--- /dev/null
+++ b/lib/plugins/kolab_auth/kolab_auth.php
@@ -0,0 +1,531 @@
+<?php
+
+/**
+ * Kolab Authentication (based on ldap_authentication plugin)
+ *
+ * Authenticates on LDAP server, finds canonized authentication ID for IMAP
+ * and for new users creates identity based on LDAP information.
+ *
+ * Supports impersonate feature (login as another user). To use this feature
+ * imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
+ *
+ * @version @package_version@
+ * @author Aleksander Machniak <machniak at kolabsys.com>
+ *
+ * Copyright (C) 2011, Kolab Systems AG <contact at 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_auth extends rcube_plugin
+{
+    private $ldap;
+    private $data = array();
+
+    public function init()
+    {
+        $rcmail = rcube::get_instance();
+
+        $this->add_hook('authenticate', array($this, 'authenticate'));
+        $this->add_hook('startup', array($this, 'startup'));
+        $this->add_hook('user_create', array($this, 'user_create'));
+
+        // Hooks related to "Login As" feature
+        $this->add_hook('template_object_loginform', array($this, 'login_form'));
+        $this->add_hook('storage_connect', array($this, 'imap_connect'));
+        $this->add_hook('managesieve_connect', array($this, 'imap_connect'));
+        $this->add_hook('smtp_connect', array($this, 'smtp_connect'));
+
+        $this->add_hook('write_log', array($this, 'write_log'));
+
+        // TODO: This section does not actually seem to work
+        if ($rcmail->config->get('kolab_auth_auditlog', false)) {
+            $rcmail->config->set('debug_level', 1);
+            $rcmail->config->set('devel_mode', true);
+            $rcmail->config->set('smtp_log', true);
+            $rcmail->config->set('log_logins', true);
+            $rcmail->config->set('log_session', true);
+            $rcmail->config->set('sql_debug', true);
+            $rcmail->config->set('memcache_debug', true);
+            $rcmail->config->set('imap_debug', true);
+            $rcmail->config->set('ldap_debug', true);
+            $rcmail->config->set('smtp_debug', true);
+
+        }
+
+    }
+
+    public function startup($args) {
+        // Arguments are task / action, not interested
+        if (!empty($_SESSION['user_roledns'])) {
+            $this->load_user_role_plugins_and_settings($_SESSION['user_roledns']);
+        }
+
+        return $args;
+    }
+
+    public function load_user_role_plugins_and_settings($role_dns) {
+        $rcmail = rcube::get_instance();
+        $this->load_config();
+
+        // Check role dependent plugins to enable and settings to modify
+
+        // Example 'kolab_auth_role_plugins' =
+        //
+        //  Array(
+        //      '<role_dn>' => Array('plugin1', 'plugin2'),
+        //  );
+
+        $role_plugins = $rcmail->config->get('kolab_auth_role_plugins');
+
+        // Example $rcmail_config['kolab_auth_role_settings'] =
+        //
+        //  Array(
+        //      '<role_dn>' => Array(
+        //          '$setting' => Array(
+        //              'mode' => '(override|merge)', (default: override)
+        //              'value' => <>,
+        //              'allow_override' => (true|false) (default: false)
+        //          ),
+        //      ),
+        //  );
+
+        $role_settings = $rcmail->config->get('kolab_auth_role_settings');
+
+        foreach ($role_dns as $role_dn) {
+            if (isset($role_plugins[$role_dn]) && is_array($role_plugins[$role_dn])) {
+                foreach ($role_plugins[$role_dn] as $plugin) {
+                    $this->require_plugin($plugin);
+                }
+            }
+
+            if (isset($role_settings[$role_dn]) && is_array($role_settings[$role_dn])) {
+                foreach ($role_settings[$role_dn] as $setting_name => $setting) {
+                    if (!isset($setting['mode'])) {
+                        $setting['mode'] = 'override';
+                    }
+
+                    if ($setting['mode'] == "override") {
+                        $rcmail->config->set($setting_name, $setting['value']);
+                    } elseif ($setting['mode'] == "merge") {
+                        $orig_setting = $rcmail->config->get($setting_name);
+
+                        if (!empty($orig_setting)) {
+                            if (is_array($orig_setting)) {
+                                $rcmail->config->set($setting_name, array_merge($orig_setting, $setting['value']));
+                            }
+                        } else {
+                            $rcmail->config->set($setting_name, $setting['value']);
+                        }
+                    }
+
+                    $dont_override = (array) $rcmail->config->get('dont_override');
+
+                    if (!isset($setting['allow_override']) || !$setting['allow_override']) {
+                        $rcmail->config->set('dont_override', array_merge($dont_override, array($setting_name)));
+                    }
+                    else {
+                        if (in_array($setting_name, $dont_override)) {
+                            $_dont_override = array();
+                            foreach ($dont_override as $_setting) {
+                                if ($_setting != $setting_name) {
+                                    $_dont_override[] = $_setting;
+                                }
+                            }
+                            $rcmail->config->set('dont_override', $_dont_override);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public function write_log($args) {
+        $rcmail = rcube::get_instance();
+
+        if (!$rcmail->config->get('kolab_auth_auditlog', false)) {
+            return $args;
+        }
+
+        $args['abort'] = true;
+
+        if ($rcmail->config->get('log_driver') == 'syslog') {
+            $prio = $args['name'] == 'errors' ? LOG_ERR : LOG_INFO;
+            syslog($prio, $args['line']);
+            return $args;
+        }
+        else {
+            $line = sprintf("[%s]: %s\n", $args['date'], $args['line']);
+
+            // log_driver == 'file' is assumed here
+            $log_dir  = $rcmail->config->get('log_dir', INSTALL_PATH . 'logs');
+            $log_path = $log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']);
+
+            // Append original username + target username
+            if (!is_dir($log_path)) {
+                // Attempt to create the directory
+                if (@mkdir($log_path, 0750, true)) {
+                    $log_dir = $log_path;
+                }
+            }
+            else {
+                $log_dir = $log_path;
+            }
+
+            // try to open specific log file for writing
+            $logfile = $log_dir.'/'.$args['name'];
+
+            if ($fp = fopen($logfile, 'a')) {
+                fwrite($fp, $line);
+                fflush($fp);
+                fclose($fp);
+                return $args;
+            }
+            else {
+                trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
+            }
+        }
+
+        return $args;
+    }
+
+    /**
+     * Sets defaults for new user.
+     */
+    public function user_create($args)
+    {
+        if (!empty($this->data['user_email'])) {
+            // addresses list is supported
+            if (array_key_exists('email_list', $args)) {
+                $args['email_list'] = array_unique($this->data['user_email']);
+            }
+            else {
+                $args['user_email'] = $this->data['user_email'][0];
+            }
+        }
+
+        if (!empty($this->data['user_name'])) {
+            $args['user_name'] = $this->data['user_name'];
+        }
+
+        return $args;
+    }
+
+    /**
+     * Modifies login form adding additional "Login As" field
+     */
+    public function login_form($args)
+    {
+        $this->load_config();
+        $this->add_texts('localization/');
+
+        $rcmail      = rcube::get_instance();
+        $admin_login = $rcmail->config->get('kolab_auth_admin_login');
+        $group       = $rcmail->config->get('kolab_auth_group');
+        $role_attr   = $rcmail->config->get('kolab_auth_role');
+
+        // Show "Login As" input
+        if (empty($admin_login) || (empty($group) && empty($role_attr))) {
+            return $args;
+        }
+
+        $input = new html_inputfield(array('name' => '_loginas', 'id' => 'rcmloginas',
+            'type' => 'text', 'autocomplete' => 'off'));
+        $row = html::tag('tr', null,
+            html::tag('td', 'title', html::label('rcmloginas', Q($this->gettext('loginas'))))
+            . html::tag('td', 'input', $input->show(trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST))))
+        );
+        $args['content'] = preg_replace('/<\/tbody>/i', $row . '</tbody>', $args['content']);
+
+        return $args;
+    }
+
+    /**
+     * Find user credentials In LDAP.
+     */
+    public function authenticate($args)
+    {
+        $this->load_config();
+
+        if (!$this->init_ldap()) {
+            $args['abort'] = true;
+            return $args;
+        }
+
+        $rcmail      = rcube::get_instance();
+        $admin_login = $rcmail->config->get('kolab_auth_admin_login');
+        $admin_pass  = $rcmail->config->get('kolab_auth_admin_password');
+        $login_attr  = $rcmail->config->get('kolab_auth_login');
+        $name_attr   = $rcmail->config->get('kolab_auth_name');
+        $email_attr  = $rcmail->config->get('kolab_auth_email');
+
+        // get username and host
+        $host    = rcube_utils::parse_host($args['host']);
+        $user    = $args['user'];
+        $pass    = $args['pass'];
+        $loginas = trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST));
+
+        if (empty($user) || empty($pass)) {
+            $args['abort'] = true;
+            return $args;
+        }
+
+        // Find user record in LDAP
+        $record = $this->get_user_record($user, $host);
+
+        if (empty($record)) {
+            $args['abort'] = true;
+            return $args;
+        }
+
+        $role_attr = $rcmail->config->get('kolab_auth_role');
+
+        if (!empty($role_attr) && !empty($record[$role_attr])) {
+            $_SESSION['user_roledns'] = (array)($record[$role_attr]);
+        }
+
+        // Login As...
+        if (!empty($loginas) && $admin_login) {
+            // Authenticate to LDAP
+            $dn     = $this->ldap->dn_decode($record['ID']);
+            $result = $this->ldap->bind($dn, $pass);
+
+            if (!$result) {
+                return $args;
+            }
+
+            // check if the original user has/belongs to administrative role/group
+            $isadmin   = false;
+            $group     = $rcmail->config->get('kolab_auth_group');
+            $role_attr = $rcmail->config->get('kolab_auth_role');
+            $role_dn   = $rcmail->config->get('kolab_auth_role_value');
+
+            // check role attribute
+            if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) {
+                $role_dn = $this->parse_vars($role_dn, $user, $host);
+                foreach ((array)$record[$role_attr] as $role) {
+                    if ($role == $role_dn) {
+                        $isadmin = true;
+                        break;
+                    }
+                }
+            }
+
+            // check group
+            if (!$isadmin && !empty($group)) {
+                $groups = $this->ldap->get_record_groups($record['ID']);
+                foreach ($groups as $g) {
+                    if ($group == $this->ldap->dn_decode($g)) {
+                        $isadmin = true;
+                        break;
+                    }
+                }
+
+            }
+
+            // Save original user login for log (see below)
+            if ($login_attr) {
+                $origname = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
+            }
+            else {
+                $origname = $user;
+            }
+
+            $record = null;
+
+            // user has the privilage, get "login as" user credentials
+            if ($isadmin) {
+                $record = $this->get_user_record($loginas, $host);
+            }
+
+            if (empty($record)) {
+                $args['abort'] = true;
+                return $args;
+            }
+
+            $args['user'] = $loginas;
+
+            // Mark session to use SASL proxy for IMAP authentication
+            $_SESSION['kolab_auth_admin']    = strtolower($origname);
+            $_SESSION['kolab_auth_login']    = $rcmail->encrypt($admin_login);
+            $_SESSION['kolab_auth_password'] = $rcmail->encrypt($admin_pass);
+        }
+
+        // Store UID in session for use by other plugins
+        $_SESSION['kolab_uid'] = is_array($record['uid']) ? $record['uid'][0] : $record['uid'];
+
+        // Set user login
+        if ($login_attr) {
+            $this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
+        }
+        if ($this->data['user_login']) {
+            $args['user'] = $this->data['user_login'];
+        }
+
+        // User name for identity (first log in)
+        foreach ((array)$name_attr as $field) {
+            $name = is_array($record[$field]) ? $record[$field][0] : $record[$field];
+            if (!empty($name)) {
+                $this->data['user_name'] = $name;
+                break;
+            }
+        }
+        // User email(s) for identity (first log in)
+        foreach ((array)$email_attr as $field) {
+            $email = is_array($record[$field]) ? array_filter($record[$field]) : $record[$field];
+            if (!empty($email)) {
+                $this->data['user_email'] = array_merge((array)$this->data['user_email'], (array)$email);
+            }
+        }
+
+        // Log "Login As" usage
+        if (!empty($origname)) {
+            rcube::write_log('userlogins', sprintf('Admin login for %s by %s from %s',
+                $args['user'], $origname, rcube_utils::remote_ip()));
+        }
+
+        return $args;
+    }
+
+    /**
+     * Sets SASL Proxy login/password for IMAP and Managesieve auth
+     */
+    public function imap_connect($args)
+    {
+        if (!empty($_SESSION['kolab_auth_admin'])) {
+            $rcmail      = rcube::get_instance();
+            $admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
+            $admin_pass  = $rcmail->decrypt($_SESSION['kolab_auth_password']);
+
+            $args['auth_cid'] = $admin_login;
+            $args['auth_pw']  = $admin_pass;
+        }
+
+        return $args;
+    }
+
+    /**
+     * Sets SASL Proxy login/password for SMTP auth
+     */
+    public function smtp_connect($args)
+    {
+        if (!empty($_SESSION['kolab_auth_admin'])) {
+            $rcmail      = rcube::get_instance();
+            $admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
+            $admin_pass  = $rcmail->decrypt($_SESSION['kolab_auth_password']);
+
+            $args['options']['smtp_auth_cid'] = $admin_login;
+            $args['options']['smtp_auth_pw']  = $admin_pass;
+        }
+
+        return $args;
+    }
+
+    /**
+     * Initializes LDAP object and connects to LDAP server
+     */
+    private function init_ldap()
+    {
+        if ($this->ldap) {
+            return $this->ldap->ready;
+        }
+
+        $rcmail = rcube::get_instance();
+
+        $addressbook = $rcmail->config->get('kolab_auth_addressbook');
+
+        if (!is_array($addressbook)) {
+            $ldap_config = (array)$rcmail->config->get('ldap_public');
+            $addressbook = $ldap_config[$addressbook];
+        }
+
+        if (empty($addressbook)) {
+            return false;
+        }
+
+        $this->ldap = new kolab_auth_ldap_backend(
+            $addressbook,
+            $rcmail->config->get('ldap_debug'),
+            $rcmail->config->mail_domain($_SESSION['imap_host'])
+        );
+
+        return $this->ldap->ready;
+    }
+
+    /**
+     * Fetches user data from LDAP addressbook
+     */
+    private function get_user_record($user, $host)
+    {
+        $rcmail = rcube::get_instance();
+        $filter = $rcmail->config->get('kolab_auth_filter');
+
+        $filter = $this->parse_vars($filter, $user, $host);
+
+        // reset old result
+        $this->ldap->reset();
+
+        // get record
+        $this->ldap->set_filter($filter);
+        $results = $this->ldap->list_records();
+
+        if (count($results->records) == 1) {
+            return $results->records[0];
+        }
+    }
+
+    /**
+     * Prepares filter query for LDAP search
+     */
+    private function parse_vars($str, $user, $host)
+    {
+        $rcmail = rcube::get_instance();
+        $domain = $rcmail->config->get('username_domain');
+
+        if (!empty($domain) && strpos($user, '@') === false) {
+            if (is_array($domain) && isset($domain[$host])) {
+                $user .= '@'.rcube_utils::parse_host($domain[$host], $host);
+            }
+            else if (is_string($domain)) {
+                $user .= '@'.rcube_utils::parse_host($domain, $host);
+            }
+        }
+
+        // replace variables in filter
+        list($u, $d) = explode('@', $user);
+        $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
+        $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
+
+        return strtr($str, $replaces);
+    }
+}
+
+/**
+ * Wrapper class for rcube_ldap addressbook
+ */
+class kolab_auth_ldap_backend extends rcube_ldap
+{
+    function __construct($p, $debug=false, $mail_domain=null)
+    {
+        parent::__construct($p, $debug, $mail_domain);
+        $this->fieldmap['uid'] = 'uid';
+    }
+
+    function set_filter($filter)
+    {
+        if ($filter) {
+            $this->prop['filter'] = $filter;
+        }
+    }
+}
diff --git a/lib/plugins/kolab_auth/localization/de_CH.inc b/lib/plugins/kolab_auth/localization/de_CH.inc
new file mode 100644
index 0000000..9cdad33
--- /dev/null
+++ b/lib/plugins/kolab_auth/localization/de_CH.inc
@@ -0,0 +1,5 @@
+<?php
+
+$labels['loginas'] = 'Anmelden als';
+
+?>
diff --git a/lib/plugins/kolab_auth/localization/de_DE.inc b/lib/plugins/kolab_auth/localization/de_DE.inc
new file mode 100644
index 0000000..9cdad33
--- /dev/null
+++ b/lib/plugins/kolab_auth/localization/de_DE.inc
@@ -0,0 +1,5 @@
+<?php
+
+$labels['loginas'] = 'Anmelden als';
+
+?>
diff --git a/lib/plugins/kolab_auth/localization/en_US.inc b/lib/plugins/kolab_auth/localization/en_US.inc
new file mode 100644
index 0000000..e1adb3f
--- /dev/null
+++ b/lib/plugins/kolab_auth/localization/en_US.inc
@@ -0,0 +1,5 @@
+<?php
+
+$labels['loginas'] = 'Login As';
+
+?>
diff --git a/lib/plugins/kolab_auth/localization/pl_PL.inc b/lib/plugins/kolab_auth/localization/pl_PL.inc
new file mode 100644
index 0000000..785f200
--- /dev/null
+++ b/lib/plugins/kolab_auth/localization/pl_PL.inc
@@ -0,0 +1,5 @@
+<?php
+
+$labels['loginas'] = 'Zaloguj jako';
+
+?>
diff --git a/lib/plugins/kolab_auth/package.xml b/lib/plugins/kolab_auth/package.xml
new file mode 100644
index 0000000..6200c4c
--- /dev/null
+++ b/lib/plugins/kolab_auth/package.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+    http://pear.php.net/dtd/tasks-1.0.xsd
+    http://pear.php.net/dtd/package-2.0
+    http://pear.php.net/dtd/package-2.0.xsd">
+	<name>kolab_auth</name>
+    <uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
+	<summary>Kolab Authentication</summary>
+	<description>
+        Authenticates on LDAP server, finds canonized authentication ID for IMAP
+        and for new users creates identity based on LDAP information.
+        Supports impersonate feature (login as another user). To use this feature
+        imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
+	</description>
+	<lead>
+		<name>Aleksander Machniak</name>
+		<user>machniak</user>
+		<email>machniak at kolabsys.com</email>
+		<active>yes</active>
+	</lead>
+	<date>2012-10-08</date>
+	<version>
+		<release>0.4</release>
+		<api>0.1</api>
+	</version>
+	<stability>
+		<release>stable</release>
+		<api>stable</api>
+	</stability>
+	<license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
+	<notes>-</notes>
+	<contents>
+		<dir baseinstalldir="/" name="/">
+			<file name="kolab_auth.php" role="php">
+				<tasks:replace from="@name@" to="name" type="package-info"/>
+				<tasks:replace from="@package_version@" to="version" type="package-info"/>
+			</file>
+			<file name="config.inc.php.dist" role="data"></file>
+			<file name="LICENSE" role="data"></file>
+
+			<file name="localization/de_CH.inc" role="data"></file>
+			<file name="localization/de_DE.inc" role="data"></file>
+			<file name="localization/en_US.inc" role="data"></file>
+			<file name="localization/pl_PL.inc" role="data"></file>
+		</dir>
+		<!-- / -->
+	</contents>
+	<dependencies>
+		<required>
+			<php>
+				<min>5.2.1</min>
+			</php>
+			<pearinstaller>
+				<min>1.7.0</min>
+			</pearinstaller>
+		</required>
+	</dependencies>
+	<phprelease/>
+</package>
diff --git a/lib/plugins/kolab_folders/LICENSE b/lib/plugins/kolab_folders/LICENSE
new file mode 100644
index 0000000..dba13ed
--- /dev/null
+++ b/lib/plugins/kolab_folders/LICENSE
@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time.  Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/lib/plugins/kolab_folders/config.inc.php.dist b/lib/plugins/kolab_folders/config.inc.php.dist
new file mode 100644
index 0000000..e393684
--- /dev/null
+++ b/lib/plugins/kolab_folders/config.inc.php.dist
@@ -0,0 +1,34 @@
+<?php
+
+// Default kolab-specific folders. Set values to non-empty
+// strings to create default folders of apropriate type.
+// If there is no default folder with specified type in user mailbox,
+// it will be created.
+// Note: Mail folders will be also subscribed.
+
+// Default Configuration folder
+$rcmail_config['kolab_folders_configuration_default'] = '';
+// Default Calendar folder
+$rcmail_config['kolab_folders_event_default'] = '';
+// Default Contacts (Addressbook) folder
+$rcmail_config['kolab_folders_contact_default'] = '';
+// Default Tasks folder
+$rcmail_config['kolab_folders_task_default'] = '';
+// Default Notes folder
+$rcmail_config['kolab_folders_note_default'] = '';
+// Default Journal folder
+$rcmail_config['kolab_folders_journal_default'] = '';
+
+// INBOX folder
+$rcmail_config['kolab_folders_mail_inbox'] = '';
+// Drafts folder
+$rcmail_config['kolab_folders_mail_drafts'] = '';
+// Sent folder
+$rcmail_config['kolab_folders_mail_sentitems'] = '';
+// Trash folder
+$rcmail_config['kolab_folders_mail_wastebasket'] = '';
+// Others folders
+$rcmail_config['kolab_folders_mail_outbox'] = '';
+$rcmail_config['kolab_folders_mail_junkemail'] = '';
+
+?>
diff --git a/lib/plugins/kolab_folders/kolab_folders.js b/lib/plugins/kolab_folders/kolab_folders.js
new file mode 100644
index 0000000..7cabfdd
--- /dev/null
+++ b/lib/plugins/kolab_folders/kolab_folders.js
@@ -0,0 +1,65 @@
+/**
+ * Client script for the Kolab folder management/listing extension
+ *
+ * @version @package_version@
+ * @author Aleksander Machniak <machniak at kolabsys.com>
+ *
+ * Copyright (C) 2011, Kolab Systems AG <contact at 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/>.
+ */
+
+$(document).ready(function() {
+    // IE doesn't allow setting OPTION's display/visibility
+    // We'll need to remove SELECT's options, see below
+    if (bw.ie) {
+        rcmail.env.subtype_html = $('#_subtype').html();
+    }
+
+    // Add onchange handler for folder type SELECT, and call it on form init
+    $('#_ctype').change(function() {
+        var type = $(this).val(),
+            sub = $('#_subtype'),
+            subtype = sub.val();
+
+        // For IE we need to revert the whole SELECT to the original state
+        if (bw.ie) {
+            sub.html(rcmail.env.subtype_html).val(subtype);
+        }
+
+        // For non-mail folders we must hide mail-specific subtypes
+        $('option', sub).each(function() {
+            var opt = $(this), val = opt.val();
+            if (val == '')
+                return;
+            // there's no mail.default
+            if (val == 'default' && type != 'mail') {
+                opt.show();
+                return;
+            };
+
+            if (type == 'mail' && val != 'default')
+                opt.show();
+            else if (bw.ie)
+                opt.remove();
+            else
+                opt.hide();
+        });
+
+        // And re-set subtype
+        if (type != 'mail' && subtype != '' && subtype != 'default') {
+            sub.val('');
+        }
+    }).change();
+});
diff --git a/lib/plugins/kolab_folders/kolab_folders.php b/lib/plugins/kolab_folders/kolab_folders.php
new file mode 100644
index 0000000..ed05122
--- /dev/null
+++ b/lib/plugins/kolab_folders/kolab_folders.php
@@ -0,0 +1,543 @@
+<?php
+
+/**
+ * Type-aware folder management/listing for Kolab
+ *
+ * @version @package_version@
+ * @author Aleksander Machniak <machniak at kolabsys.com>
+ *
+ * Copyright (C) 2011, Kolab Systems AG <contact at 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_folders extends rcube_plugin
+{
+    public $task = '?(?!login).*';
+
+    public $types = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration');
+    public $mail_types = array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail');
+
+    private $rc;
+    private static $instance;
+
+
+    /**
+     * Plugin initialization.
+     */
+    function init()
+    {
+        self::$instance = $this;
+        $this->rc = rcube::get_instance();
+
+        // load required plugin
+        $this->require_plugin('libkolab');
+
+        // Folder listing hooks
+        $this->add_hook('storage_folders', array($this, 'mailboxes_list'));
+
+        // Folder manager hooks
+        $this->add_hook('folder_form', array($this, 'folder_form'));
+        $this->add_hook('folder_update', array($this, 'folder_save'));
+        $this->add_hook('folder_create', array($this, 'folder_save'));
+        $this->add_hook('folder_delete', array($this, 'folder_save'));
+        $this->add_hook('folder_rename', array($this, 'folder_save'));
+        $this->add_hook('folders_list', array($this, 'folders_list'));
+    }
+
+    /**
+     * Handler for mailboxes_list hook. Enables type-aware lists filtering.
+     */
+    function mailboxes_list($args)
+    {
+        // infinite loop prevention
+        if ($this->is_processing) {
+            return $args;
+        }
+
+        if (!$this->metadata_support()) {
+            return $args;
+        }
+
+        $this->is_processing = true;
+
+        // get folders
+        $folders = kolab_storage::list_folders($args['root'], $args['name'], $args['filter'], $args['mode'] == 'LSUB', $folderdata);
+
+        $this->is_processing = false;
+
+        if (!is_array($folders)) {
+            return $args;
+        }
+
+        // Create default folders
+        if ($args['root'] == '' && $args['name'] = '*') {
+            $this->create_default_folders($folders, $args['filter'], $folderdata);
+        }
+
+        $args['folders'] = $folders;
+
+        return $args;
+    }
+
+    /**
+     * Handler for folders_list hook. Add css classes to folder rows.
+     */
+    function folders_list($args)
+    {
+        if (!$this->metadata_support()) {
+            return $args;
+        }
+
+        $table   = $args['table'];
+        $storage = $this->rc->get_storage();
+
+        // get folders types
+        $folderdata = $storage->get_metadata('*', kolab_storage::CTYPE_KEY);
+
+        if (!is_array($folderdata)) {
+            return $args;
+        }
+
+        // Add type-based style for table rows
+        // See kolab_folders::folder_class_name()
+        for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
+            $attrib = $table->get_row_attribs($i);
+            $folder = $attrib['foldername']; // UTF7-IMAP
+            $type   = !empty($folderdata[$folder]) ? $folderdata[$folder][kolab_storage::CTYPE_KEY] : null;
+
+            if (!$type)
+                $type = 'mail';
+
+            $class_name = self::folder_class_name($type);
+
+            $attrib['class'] = trim($attrib['class'] . ' ' . $class_name);
+            $table->set_row_attribs($attrib, $i);
+        }
+
+        return $args;
+    }
+
+    /**
+     * Handler for folder info/edit form (folder_form hook).
+     * Adds folder type selector.
+     */
+    function folder_form($args)
+    {
+        if (!$this->metadata_support()) {
+            return $args;
+        }
+        // load translations
+        $this->add_texts('localization/', false);
+
+        // INBOX folder is of type mail.inbox and this cannot be changed
+        if ($args['name'] == 'INBOX') {
+            $args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
+                'label' => $this->gettext('folderctype'),
+                'value' => sprintf('%s (%s)', $this->gettext('foldertypemail'), $this->gettext('inbox')),
+            );
+
+            return $args;
+        }
+
+        if ($args['options']['is_root']) {
+            return $args;
+        }
+
+        $mbox = strlen($args['name']) ? $args['name'] : $args['parent_name'];
+
+        if (isset($_POST['_ctype'])) {
+            $new_ctype   = trim(get_input_value('_ctype', RCUBE_INPUT_POST));
+            $new_subtype = trim(get_input_value('_subtype', RCUBE_INPUT_POST));
+        }
+
+        // Get type of the folder or the parent
+        if (strlen($mbox)) {
+            list($ctype, $subtype) = $this->get_folder_type($mbox);
+            if (strlen($args['parent_name']) && $subtype == 'default')
+                $subtype = ''; // there can be only one
+        }
+
+        if (!$ctype) {
+            $ctype = 'mail';
+        }
+
+        $storage = $this->rc->get_storage();
+
+        // Don't allow changing type of shared folder, according to ACL
+        if (strlen($mbox)) {
+            $options = $storage->folder_info($mbox);
+            if ($options['namespace'] != 'personal' && !in_array('a', $options['rights'])) {
+                if (in_array($ctype, $this->types)) {
+                    $value = $this->gettext('foldertype'.$ctype);
+                }
+                else {
+                    $value = $ctype;
+                }
+                if ($subtype) {
+                    $value .= ' ('. ($subtype == 'default' ? $this->gettext('default') : $subtype) .')';
+                }
+
+                $args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
+                    'label' => $this->gettext('folderctype'),
+                    'value' => $value,
+                );
+
+                return $args;
+            }
+        }
+
+        // Add javascript script to the client
+        $this->include_script('kolab_folders.js');
+
+        // build type SELECT fields
+        $type_select = new html_select(array('name' => '_ctype', 'id' => '_ctype'));
+        $sub_select  = new html_select(array('name' => '_subtype', 'id' => '_subtype'));
+
+        foreach ($this->types as $type) {
+            $type_select->add($this->gettext('foldertype'.$type), $type);
+        }
+        // add non-supported type
+        if (!in_array($ctype, $this->types)) {
+            $type_select->add($ctype, $ctype);
+        }
+
+        $sub_select->add('', '');
+        $sub_select->add($this->gettext('default'), 'default');
+        foreach ($this->mail_types as $type) {
+            $sub_select->add($this->gettext($type), $type);
+        }
+
+        $args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
+            'label' => $this->gettext('folderctype'),
+            'value' => $type_select->show(isset($new_ctype) ? $new_ctype : $ctype)
+                . $sub_select->show(isset($new_subtype) ? $new_subtype : $subtype),
+        );
+
+        return $args;
+    }
+
+    /**
+     * Handler for folder update/create action (folder_update/folder_create hook).
+     */
+    function folder_save($args)
+    {
+        // Folder actions from folders list
+        if (empty($args['record'])) {
+            return $args;
+        }
+
+        // Folder create/update with form
+        $ctype     = trim(get_input_value('_ctype', RCUBE_INPUT_POST));
+        $subtype   = trim(get_input_value('_subtype', RCUBE_INPUT_POST));
+        $mbox      = $args['record']['name'];
+        $old_mbox  = $args['record']['oldname'];
+        $subscribe = $args['record']['subscribe'];
+
+        if (empty($ctype)) {
+            return $args;
+        }
+
+        // load translations
+        $this->add_texts('localization/', false);
+
+        // Skip folder creation/rename in core
+        // @TODO: Maybe we should provide folder_create_after and folder_update_after hooks?
+        //        Using create_mailbox/rename_mailbox here looks bad
+        $args['abort']  = true;
+
+        // There can be only one default folder of specified type
+        if ($subtype == 'default') {
+            $default = $this->get_default_folder($ctype);
+
+            if ($default !== null && $old_mbox != $default) {
+                $args['result'] = false;
+                $args['message'] = $this->gettext('defaultfolderexists');
+                return $args;
+            }
+        }
+        // Subtype sanity-checks
+        else if ($subtype && ($ctype != 'mail' || !in_array($subtype, $this->mail_types))) {
+            $subtype = '';
+        }
+
+        $ctype .= $subtype ? '.'.$subtype : '';
+
+        $storage = $this->rc->get_storage();
+
+        // Create folder
+        if (!strlen($old_mbox)) {
+            // By default don't subscribe to non-mail folders
+            if ($subscribe)
+                $subscribe = (bool) preg_match('/^mail/', $ctype);
+
+            $result = $storage->create_folder($mbox, $subscribe);
+            // Set folder type
+            if ($result) {
+                $this->set_folder_type($mbox, $ctype);
+            }
+        }
+        // Rename folder
+        else {
+            if ($old_mbox != $mbox) {
+                $result = $storage->rename_folder($old_mbox, $mbox);
+            }
+            else {
+                $result = true;
+            }
+
+            if ($result) {
+                list($oldtype, $oldsubtype) = $this->get_folder_type($mbox);
+                $oldtype .= $oldsubtype ? '.'.$oldsubtype : '';
+
+                if ($ctype != $oldtype) {
+                    $this->set_folder_type($mbox, $ctype);
+                }
+            }
+        }
+
+        $args['record']['class'] = self::folder_class_name($ctype);
+        $args['record']['subscribe'] = $subscribe;
+        $args['result'] = $result;
+
+        return $args;
+    }
+
+    /**
+     * Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
+     *
+     * @return boolean
+     */
+    function metadata_support()
+    {
+        $storage = $this->rc->get_storage();
+
+        return $storage->get_capability('METADATA') ||
+            $storage->get_capability('ANNOTATEMORE') ||
+            $storage->get_capability('ANNOTATEMORE2');
+    }
+
+    /**
+     * Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
+     *
+     * @param string $folder Folder name
+     *
+     * @return array Folder content-type
+     */
+    function get_folder_type($folder)
+    {
+        $storage    = $this->rc->get_storage();
+        $folderdata = $storage->get_metadata($folder, array(kolab_storage::CTYPE_KEY_PRIVATE, kolab_storage::CTYPE_KEY));
+
+        if (!($ctype = $folderdata[$folder][kolab_storage::CTYPE_KEY_PRIVATE])) {
+            $ctype = $folderdata[$folder][kolab_storage::CTYPE_KEY];
+        }
+
+        return explode('.', $ctype);
+    }
+
+    /**
+     * Sets folder content-type.
+     *
+     * @param string $folder Folder name
+     * @param string $type   Content type
+     *
+     * @return boolean True on success
+     */
+    function set_folder_type($folder, $type='mail')
+    {
+        return kolab_storage::set_folder_type($folder, $type);
+    }
+
+    /**
+     * Returns the name of default folder
+     *
+     * @param string $type Folder type
+     *
+     * @return string Folder name
+     */
+    function get_default_folder($type)
+    {
+        $storage    = $this->rc->get_storage();
+        $folderdata = $storage->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE, kolab_storage::CTYPE_KEY));
+
+        if (!is_array($folderdata)) {
+            return null;
+        }
+
+        $type     .= '.default';
+        $namespace = $storage->get_namespace();
+
+        // get all folders of specified type
+        $folderdata = array_map(array($this, 'folder_select_metadata'), $folderdata);
+        $folderdata = array_intersect($folderdata, array($type));
+
+        foreach ($folderdata as $folder => $data) {
+            // check if folder is in personal namespace
+            foreach (array('shared', 'other') as $nskey) {
+                if (!empty($namespace[$nskey])) {
+                    foreach ($namespace[$nskey] as $ns) {
+                        if ($ns[0] && substr($folder, 0, strlen($ns[0])) == $ns[0]) {
+                            continue 3;
+                        }
+                    }
+                }
+            }
+
+            // There can be only one default folder of specified type
+            return $folder;
+        }
+
+        return null;
+    }
+
+    /**
+     * Callback for array_map to select the correct annotation value
+     */
+    private function folder_select_metadata($types)
+    {
+        return $types[kolab_storage::CTYPE_KEY_PRIVATE] ?: $types[kolab_storage::CTYPE_KEY];
+    }
+
+    /**
+     * Returns CSS class name for specified folder type
+     *
+     * @param string $type Folder type
+     *
+     * @return string Class name
+     */
+    static function folder_class_name($type)
+    {
+        list($ctype, $subtype) = explode('.', $type);
+
+        $class[] = 'type-' . ($ctype ? $ctype : 'mail');
+
+        if ($subtype)
+            $class[] = 'subtype-' . $subtype;
+
+        return implode(' ', $class);
+    }
+
+    /**
+     * Creates default folders if they doesn't exist
+     */
+    private function create_default_folders(&$folders, $filter, $folderdata = null)
+    {
+        $storage     = $this->rc->get_storage();
+        $namespace   = $storage->get_namespace();
+        $defaults    = array();
+        $need_update = false;
+
+        if (!is_array($folderdata)) {
+            $folderdata = $storage->get_metadata('*', kolab_storage::CTYPE_KEY);
+
+            if (!is_array($folderdata)) {
+                return;
+            }
+
+            // "Flattenize" metadata array to become a name->type hash
+            $folderdata = array_map('implode', $folderdata);
+        }
+
+        // Find personal namespace prefix
+        if (is_array($namespace['personal']) && count($namespace['personal']) == 1) {
+            $prefix = $namespace['personal'][0][0];
+        }
+        else {
+            $prefix = '';
+        }
+
+        $this->load_config();
+
+        // get configured defaults
+        foreach ($this->types as $type) {
+            $subtypes = $type == 'mail' ? $this->mail_types : array('default');
+            foreach ($subtypes as $subtype) {
+                $opt_name = 'kolab_folders_' . $type . '_' . $subtype;
+                if ($folder = $this->rc->config->get($opt_name)) {
+                    // convert configuration value to UTF7-IMAP charset
+                    $folder = rcube_charset::convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP');
+                    // and namespace prefix if needed
+                    if ($prefix && strpos($folder, $prefix) === false && $folder != 'INBOX') {
+                        $folder = $prefix . $folder;
+                    }
+                    $defaults[$type . '.' . $subtype] = $folder;
+                }
+            }
+        }
+
+        // find default folders
+        foreach ($defaults as $type => $foldername) {
+            // folder exists, do nothing
+            if (!empty($folderdata[$foldername])) {
+                continue;
+            }
+
+            // special case, need to set type only
+            if ($foldername == 'INBOX' || $type == 'mail.inbox') {
+                $this->set_folder_type($foldername, 'mail.inbox');
+                continue;
+            }
+
+            // get all folders of specified type
+            $folders = array_intersect($folderdata, array($type));
+            unset($folders[0]);
+
+            // find folders in personal namespace
+            foreach ($folders as $folder) {
+                if ($folder) {
+                    foreach (array('shared', 'other') as $nskey) {
+                        if (!empty($namespace[$nskey])) {
+                            foreach ($namespace[$nskey] as $ns) {
+                                if ($ns[0] && substr($folder, 0, strlen($ns[0])) == $ns[0]) {
+                                    continue 3;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // got folder in personal namespace
+                continue 2;
+            }
+
+            list($type1, $type2) = explode('.', $type);
+
+            // create folder
+            if ($type1 != 'mail' || !$storage->folder_exists($foldername)) {
+                $storage->create_folder($foldername, $type1 == 'mail');
+            }
+
+            // set type
+            $result = $this->set_folder_type($foldername, $type);
+
+            // add new folder to the result
+            if ($result && (!$filter || $filter == $type1)) {
+                $folders[] = $foldername;
+            }
+        }
+    }
+
+
+    /**
+     * Static getter for default folder of the given type
+     *
+     * @param string $type Folder type
+     * @return string Folder name
+     */
+    public static function default_folder($type)
+    {
+        return self::$instance->get_default_folder($type);
+    }
+}
diff --git a/lib/plugins/kolab_folders/localization/de_CH.inc b/lib/plugins/kolab_folders/localization/de_CH.inc
new file mode 100644
index 0000000..f9f6e16
--- /dev/null
+++ b/lib/plugins/kolab_folders/localization/de_CH.inc
@@ -0,0 +1,24 @@
+<?php
+
+$labels = array();
+
+$labels['folderctype'] = 'Ordnerinhalt';
+$labels['foldertypemail'] = 'E-Mail';
+$labels['foldertypeevent'] = 'Kalender'; // Termine?
+$labels['foldertypejournal'] = 'Journal';
+$labels['foldertypetask'] = 'Aufgaben';
+$labels['foldertypenote'] = 'Notizen';
+$labels['foldertypecontact'] = 'Kontakte';
+$labels['foldertypeconfiguration'] = 'Konfiguration';
+
+$labels['default'] = 'Standard';
+$labels['inbox'] = 'Posteingang';
+$labels['drafts'] = 'Entwürfe';
+$labels['sentitems'] = 'Gesendet';
+$labels['outbox'] = 'Postausgang';
+$labels['wastebasket'] = 'Gelöscht';
+$labels['junkemail'] = 'Spam';
+
+$messages['defaultfolderexists'] = 'Es existiert bereits ein Standardordner für den angegebenen Typ';
+
+?>
diff --git a/lib/plugins/kolab_folders/localization/de_DE.inc b/lib/plugins/kolab_folders/localization/de_DE.inc
new file mode 100644
index 0000000..f9f6e16
--- /dev/null
+++ b/lib/plugins/kolab_folders/localization/de_DE.inc
@@ -0,0 +1,24 @@
+<?php
+
+$labels = array();
+
+$labels['folderctype'] = 'Ordnerinhalt';
+$labels['foldertypemail'] = 'E-Mail';
+$labels['foldertypeevent'] = 'Kalender'; // Termine?
+$labels['foldertypejournal'] = 'Journal';
+$labels['foldertypetask'] = 'Aufgaben';
+$labels['foldertypenote'] = 'Notizen';
+$labels['foldertypecontact'] = 'Kontakte';
+$labels['foldertypeconfiguration'] = 'Konfiguration';
+
+$labels['default'] = 'Standard';
+$labels['inbox'] = 'Posteingang';
+$labels['drafts'] = 'Entwürfe';
+$labels['sentitems'] = 'Gesendet';
+$labels['outbox'] = 'Postausgang';
+$labels['wastebasket'] = 'Gelöscht';
+$labels['junkemail'] = 'Spam';
+
+$messages['defaultfolderexists'] = 'Es existiert bereits ein Standardordner für den angegebenen Typ';
+
+?>
diff --git a/lib/plugins/kolab_folders/localization/en_US.inc b/lib/plugins/kolab_folders/localization/en_US.inc
new file mode 100644
index 0000000..70867bc
--- /dev/null
+++ b/lib/plugins/kolab_folders/localization/en_US.inc
@@ -0,0 +1,24 @@
+<?php
+
+$labels = array();
+
+$labels['folderctype'] = 'Content type';
+$labels['foldertypemail'] = 'Mail';
+$labels['foldertypeevent'] = 'Calendar'; // Events?
+$labels['foldertypejournal'] = 'Journal';
+$labels['foldertypetask'] = 'Tasks';
+$labels['foldertypenote'] = 'Notes';
+$labels['foldertypecontact'] = 'Contacts';
+$labels['foldertypeconfiguration'] = 'Configuration';
+
+$labels['default'] = 'Default';
+$labels['inbox'] = 'Inbox';
+$labels['drafts'] = 'Drafts';
+$labels['sentitems'] = 'Sent';
+$labels['outbox'] = 'Outbox';
+$labels['wastebasket'] = 'Trash';
+$labels['junkemail'] = 'Junk';
+
+$messages['defaultfolderexists'] = 'There is already default folder of specified type';
+
+?>
diff --git a/lib/plugins/kolab_folders/localization/pl_PL.inc b/lib/plugins/kolab_folders/localization/pl_PL.inc
new file mode 100644
index 0000000..95177cd
--- /dev/null
+++ b/lib/plugins/kolab_folders/localization/pl_PL.inc
@@ -0,0 +1,21 @@
+<?php
+
+$labels = array();
+$labels['folderctype'] = 'Zawartość';
+$labels['foldertypemail'] = 'Poczta';
+$labels['foldertypeevent'] = 'Kalendarz';
+$labels['foldertypejournal'] = 'Dziennik';
+$labels['foldertypetask'] = 'Zadania';
+$labels['foldertypenote'] = 'Notatki';
+$labels['foldertypecontact'] = 'Kontakty';
+$labels['foldertypeconfiguration'] = 'Konfiguracja';
+$labels['default'] = 'Domyślny';
+$labels['inbox'] = 'Odebrane';
+$labels['drafts'] = 'Szkice';
+$labels['sentitems'] = 'Wysłane';
+$labels['outbox'] = 'WychodzÄ…ce';
+$labels['wastebasket'] = 'Kosz';
+$labels['junkemail'] = 'Spam';
+$messages['defaultfolderexists'] = 'Folder domyślny dla podanego typu już istnieje';
+
+?>
diff --git a/lib/plugins/kolab_folders/package.xml b/lib/plugins/kolab_folders/package.xml
new file mode 100644
index 0000000..875d614
--- /dev/null
+++ b/lib/plugins/kolab_folders/package.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+    http://pear.php.net/dtd/tasks-1.0.xsd
+    http://pear.php.net/dtd/package-2.0
+    http://pear.php.net/dtd/package-2.0.xsd">
+	<name>kolab_folders</name>
+	<uri>http://git.kolab.org/roundcubemail-plugins-kolab/</uri>
+	<summary>Type-aware folder management/listing for Kolab</summary>
+	<description>
+	    The plugin extends folders handling with features of the Kolab Suite
+	    according to specified format (http://www.kolab.org/doc/kolabformat-2.0-html).
+        With this plugin enabled it is possible to:
+        - set/get/change folder's type,
+        - filter folders list by folder type,
+        - style folders list rows (in folder manager),
+        - create default folders with specified type.
+	</description>
+	<lead>
+		<name>Aleksander Machniak</name>
+		<user>machniak</user>
+		<email>machniak at kolabsys.com</email>
+		<active>yes</active>
+	</lead>
+	<date>2012-05-14</date>
+	<version>
+		<release>2.0</release>
+		<api>2.0</api>
+	</version>
+	<stability>
+		<release>stable</release>
+		<api>stable</api>
+	</stability>
+	<license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
+	<notes>-</notes>
+	<contents>
+		<dir baseinstalldir="/" name="/">
+			<file name="kolab_folders.php" role="php">
+				<tasks:replace from="@name@" to="name" type="package-info"/>
+				<tasks:replace from="@package_version@" to="version" type="package-info"/>
+			</file>
+			<file name="kolab_folders.js" role="data">
+				<tasks:replace from="@name@" to="name" type="package-info"/>
+				<tasks:replace from="@package_version@" to="version" type="package-info"/>
+			</file>
+			<file name="config.inc.php.dist" role="data"></file>
+			<file name="localization/en_US.inc" role="data"></file>
+			<file name="localization/pl_PL.inc" role="data"></file>
+			<file name="LICENSE" role="data"></file>
+		</dir>
+		<!-- / -->
+	</contents>
+	<dependencies>
+		<required>
+			<php>
+				<min>5.2.1</min>
+			</php>
+			<pearinstaller>
+				<min>1.7.0</min>
+			</pearinstaller>
+		</required>
+	</dependencies>
+	<phprelease />
+</package>
diff --git a/lib/plugins/libkolab/README b/lib/plugins/libkolab/README
new file mode 100644
index 0000000..0a3c0ce
--- /dev/null
+++ b/lib/plugins/libkolab/README
@@ -0,0 +1,43 @@
+libkolab plugin to access to Kolab groupware data
+=================================================
+
+The contained library classes establish a connection to the Kolab server
+and manage the access to the Kolab groupware objects stored in various
+IMAP folders. For reading and writing these objects, the PHP bindings of
+the libkolabxml library are used.
+
+
+REQUIREMENTS
+------------
+* libkolabxml PHP bindings
+  - kolabformat.so loaded into PHP
+  - kolabformat.php placed somewhere in the include_path
+* PEAR: HTTP/Request2
+* PEAR: Net/URL2
+
+* Optional for old format support:
+  Horde Kolab_Format package and all of its dependencies
+  which are at least Horde_(Browser,DOM,NLS,String,Utils)
+
+
+INSTALLATION
+------------
+To use local cache you need to create a dedicated table in Roundcube's database.
+To do so, execute the SQL commands in SQL/<yourdatabase>.sql
+
+
+CONFIGURATION
+-------------
+The following options can be configured in Roundcube's main config file
+or a local config file (config.inc.php) located in the plugin folder.
+
+// Enable caching of Kolab objects in local database
+$rcmail_config['kolab_cache'] = true;
+
+// Optional override of the URL to read and trigger Free/Busy information of Kolab users
+// Defaults to https://<imap-server->/freebusy
+$rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
+
+// Set this option to disable SSL certificate checks when triggering Free/Busy (enabled by default)
+$rcmail_config['kolab_ssl_verify_peer'] = false;
+
diff --git a/lib/plugins/libkolab/SQL/mysql.sql b/lib/plugins/libkolab/SQL/mysql.sql
new file mode 100644
index 0000000..244ab3d
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql.sql
@@ -0,0 +1,25 @@
+/**
+ * libkolab database schema
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli
+ * @licence GNU AGPL
+ **/
+
+DROP TABLE IF EXISTS `kolab_cache`;
+
+CREATE TABLE `kolab_cache` (
+  `resource` VARCHAR(255) CHARACTER SET ascii NOT NULL,
+  `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+  `msguid` BIGINT UNSIGNED NOT NULL,
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `created` DATETIME DEFAULT NULL,
+  `changed` DATETIME DEFAULT NULL,
+  `data` TEXT NOT NULL,
+  `xml` TEXT NOT NULL,
+  `dtstart` DATETIME,
+  `dtend` DATETIME,
+  `tags` VARCHAR(255) NOT NULL,
+  `words` TEXT NOT NULL,
+  PRIMARY KEY(`resource`,`type`,`msguid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
diff --git a/lib/plugins/libkolab/bin/Date_Recurrence_weekday.diff b/lib/plugins/libkolab/bin/Date_Recurrence_weekday.diff
new file mode 100644
index 0000000..e8b767d
--- /dev/null
+++ b/lib/plugins/libkolab/bin/Date_Recurrence_weekday.diff
@@ -0,0 +1,325 @@
+--- Date/Recurrence.php.orig	2012-07-10 19:54:48.000000000 +0200
++++ Date/Recurrence.php	2012-07-10 19:55:38.000000000 +0200
+@@ -95,6 +95,20 @@
+     public $recurData = null;
+ 
+     /**
++     * BYDAY recurrence number
++     *
++     * @var integer
++     */
++    public $recurNthDay = null;
++
++    /**
++     * BYMONTH recurrence data
++     *
++     * @var array
++     */
++    public $recurMonths = array();
++
++    /**
+      * All the exceptions from recurrence for this event.
+      *
+      * @var array
+@@ -157,6 +171,44 @@
+     }
+ 
+     /**
++     *
++     * @param integer $nthDay The nth weekday of month to repeat events on
++     */
++    public function setRecurNthWeekday($nth)
++    {
++        $this->recurNthDay = (int)$nth;
++    }
++
++    /**
++     *
++     * @return integer  The nth weekday of month to repeat events.
++     */
++    public function getRecurNthWeekday()
++    {
++        return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7);
++    }
++
++    /**
++     * Specifies the months for yearly (weekday) recurrence
++     *
++     * @param array $months  List of months (integers) this event recurs on.
++     */
++    function setRecurByMonth($months)
++    {
++        $this->recurMonths = (array)$months;
++    }
++
++    /**
++     * Returns a list of months this yearly event recurs on
++     *
++     * @return array List of months (integers) this event recurs on.
++     */
++    function getRecurByMonth()
++    {
++        return $this->recurMonths;
++    }
++
++    /**
+      * Returns the days this event recurs on.
+      *
+      * @return integer  A mask consisting of Horde_Date::MASK_* constants
+@@ -546,8 +598,13 @@
+             $estart = clone $this->start;
+ 
+             // What day of the week, and week of the month, do we recur on?
+-            $nth = ceil($this->start->mday / 7);
+-            $weekday = $estart->dayOfWeek();
++            if (isset($this->recurNthDay)) {
++                $nth = $this->recurNthDay;
++                $weekday = log($this->recurData, 2);
++            } else {
++                $nth = ceil($this->start->mday / 7);
++                $weekday = $estart->dayOfWeek();
++            }
+ 
+             // Adjust $estart to be the first candidate.
+             $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
+@@ -660,8 +717,13 @@
+             $estart = clone $this->start;
+ 
+             // What day of the week, and week of the month, do we recur on?
+-            $nth = ceil($this->start->mday / 7);
+-            $weekday = $estart->dayOfWeek();
++            if (isset($this->recurNthDay)) {
++                $nth = $this->recurNthDay;
++                $weekday = log($this->recurData, 2);
++            } else {
++                $nth = ceil($this->start->mday / 7);
++                $weekday = $estart->dayOfWeek();
++            }
+ 
+             // Adjust $estart to be the first candidate.
+             $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+@@ -894,15 +956,6 @@
+         case 'W':
+             $this->setRecurType(self::RECUR_WEEKLY);
+             if (!empty($remainder)) {
+-                $maskdays = array(
+-                    'SU' => Horde_Date::MASK_SUNDAY,
+-                    'MO' => Horde_Date::MASK_MONDAY,
+-                    'TU' => Horde_Date::MASK_TUESDAY,
+-                    'WE' => Horde_Date::MASK_WEDNESDAY,
+-                    'TH' => Horde_Date::MASK_THURSDAY,
+-                    'FR' => Horde_Date::MASK_FRIDAY,
+-                    'SA' => Horde_Date::MASK_SATURDAY,
+-                );
+                 $mask = 0;
+                 while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
+                     $day = trim($matches[0]);
+@@ -953,7 +1006,10 @@
+                 list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d');
+                 $this->setRecurEnd(new Horde_Date(array('year' => $year,
+                                                         'month' => $month,
+-                                                        'mday' => $mday)));
++                                                        'mday' => $mday,
++                                                        'hour' => 23,
++                                                        'min' => 59,
++                                                        'sec' => 59)));
+             }
+         }
+     }
+@@ -1049,6 +1105,16 @@
+             // Always default the recurInterval to 1.
+             $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
+ 
++            $maskdays = array(
++                'SU' => Horde_Date::MASK_SUNDAY,
++                'MO' => Horde_Date::MASK_MONDAY,
++                'TU' => Horde_Date::MASK_TUESDAY,
++                'WE' => Horde_Date::MASK_WEDNESDAY,
++                'TH' => Horde_Date::MASK_THURSDAY,
++                'FR' => Horde_Date::MASK_FRIDAY,
++                'SA' => Horde_Date::MASK_SATURDAY,
++            );
++
+             switch (Horde_String::upper($rdata['FREQ'])) {
+             case 'DAILY':
+                 $this->setRecurType(self::RECUR_DAILY);
+@@ -1057,15 +1123,6 @@
+             case 'WEEKLY':
+                 $this->setRecurType(self::RECUR_WEEKLY);
+                 if (isset($rdata['BYDAY'])) {
+-                    $maskdays = array(
+-                        'SU' => Horde_Date::MASK_SUNDAY,
+-                        'MO' => Horde_Date::MASK_MONDAY,
+-                        'TU' => Horde_Date::MASK_TUESDAY,
+-                        'WE' => Horde_Date::MASK_WEDNESDAY,
+-                        'TH' => Horde_Date::MASK_THURSDAY,
+-                        'FR' => Horde_Date::MASK_FRIDAY,
+-                        'SA' => Horde_Date::MASK_SATURDAY,
+-                    );
+                     $days = explode(',', $rdata['BYDAY']);
+                     $mask = 0;
+                     foreach ($days as $day) {
+@@ -1090,6 +1147,10 @@
+             case 'MONTHLY':
+                 if (isset($rdata['BYDAY'])) {
+                     $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
++                    if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
++                        $this->setRecurOnDay($maskdays[$m[2]]);
++                        $this->setRecurNthWeekday($m[1]);
++                    }
+                 } else {
+                     $this->setRecurType(self::RECUR_MONTHLY_DATE);
+                 }
+@@ -1100,6 +1161,14 @@
+                     $this->setRecurType(self::RECUR_YEARLY_DAY);
+                 } elseif (isset($rdata['BYDAY'])) {
+                     $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
++                    if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
++                        $this->setRecurOnDay($maskdays[$m[2]]);
++                        $this->setRecurNthWeekday($m[1]);
++                    }
++                    if ($rdata['BYMONTH']) {
++                        $months = explode(',', $rdata['BYMONTH']);
++                        $this->setRecurByMonth($months);
++                    }
+                 } else {
+                     $this->setRecurType(self::RECUR_YEARLY_DATE);
+                 }
+@@ -1163,13 +1232,19 @@
+             break;
+ 
+         case self::RECUR_MONTHLY_WEEKDAY:
+-            $nth_weekday = (int)($this->start->mday / 7);
+-            if (($this->start->mday % 7) > 0) {
+-                $nth_weekday++;
++            if (isset($this->recurNthDay)) {
++                $nth_weekday = $this->recurNthDay;
++                $day_of_week = log($this->recurData, 2);
++            } else {
++                $day_of_week = $this->start->dayOfWeek();
++                $nth_weekday = (int)($this->start->mday / 7);
++                if (($this->start->mday % 7) > 0) {
++                    $nth_weekday++;
++                }
+             }
+             $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+             $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
+-                . ';BYDAY=' . $nth_weekday . $vcaldays[$this->start->dayOfWeek()];
++                . ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week];
+             break;
+ 
+         case self::RECUR_YEARLY_DATE:
+@@ -1182,15 +1257,22 @@
+             break;
+ 
+         case self::RECUR_YEARLY_WEEKDAY:
+-            $nth_weekday = (int)($this->start->mday / 7);
+-            if (($this->start->mday % 7) > 0) {
+-                $nth_weekday++;
+-            }
++            if (isset($this->recurNthDay)) {
++                $nth_weekday = $this->recurNthDay;
++                $day_of_week = log($this->recurData, 2);
++            } else {
++                $day_of_week = $this->start->dayOfWeek();
++                $nth_weekday = (int)($this->start->mday / 7);
++                if (($this->start->mday % 7) > 0) {
++                    $nth_weekday++;
++                }
++             }
++            $months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month;
+             $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+             $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
+                 . ';BYDAY='
+                 . $nth_weekday
+-                . $vcaldays[$this->start->dayOfWeek()]
++                . $vcaldays[$day_of_week]
+                 . ';BYMONTH=' . $this->start->month;
+             break;
+         }
+@@ -1223,6 +1305,21 @@
+ 
+         $this->setRecurInterval((int)$hash['interval']);
+ 
++        $month2number = array(
++            'january'   => 1,
++            'february'  => 2,
++            'march'     => 3,
++            'april'     => 4,
++            'may'       => 5,
++            'june'      => 6,
++            'july'      => 7,
++            'august'    => 8,
++            'september' => 9,
++            'october'   => 10,
++            'november'  => 11,
++            'december'  => 12,
++        );
++
+         $parse_day = false;
+         $set_daymask = false;
+         $update_month = false;
+@@ -1255,11 +1352,9 @@
+ 
+             case 'weekday':
+                 $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+-                $nth_weekday = (int)$hash['daynumber'];
+-                $hash['daynumber'] = 1;
++                $this->setRecurNthWeekday($hash['daynumber']);
+                 $parse_day = true;
+-                $update_daynumber = true;
+-                $update_weekday = true;
++                $set_daymask = true;
+                 break;
+             }
+             break;
+@@ -1297,12 +1392,13 @@
+                 }
+ 
+                 $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
+-                $nth_weekday = (int)$hash['daynumber'];
+-                $hash['daynumber'] = 1;
++                $this->setRecurNthWeekday($hash['daynumber']);
+                 $parse_day = true;
+-                $update_month = true;
+-                $update_daynumber = true;
+-                $update_weekday = true;
++                $set_daymask = true;
++
++                if ($hash['month'] && isset($month2number[$hash['month']])) {
++                    $this->setRecurByMonth($month2number[$hash['month']]);
++                }
+                 break;
+             }
+         }
+@@ -1368,21 +1464,6 @@
+ 
+         if ($update_month || $update_daynumber || $update_weekday) {
+             if ($update_month) {
+-                $month2number = array(
+-                    'january'   => 1,
+-                    'february'  => 2,
+-                    'march'     => 3,
+-                    'april'     => 4,
+-                    'may'       => 5,
+-                    'june'      => 6,
+-                    'july'      => 7,
+-                    'august'    => 8,
+-                    'september' => 9,
+-                    'october'   => 10,
+-                    'november'  => 11,
+-                    'december'  => 12,
+-                );
+-
+                 if (isset($month2number[$hash['month']])) {
+                     $this->start->month = $month2number[$hash['month']];
+                 }
+@@ -1398,7 +1479,7 @@
+             }
+ 
+             if ($update_weekday) {
+-                $this->start->setNthWeekday($last_found_day, $nth_weekday);
++                $this->setNthWeekday($nth_weekday);
+             }
+         }
+ 
diff --git a/lib/plugins/libkolab/bin/Date_last_weekday.diff b/lib/plugins/libkolab/bin/Date_last_weekday.diff
new file mode 100644
index 0000000..d260360
--- /dev/null
+++ b/lib/plugins/libkolab/bin/Date_last_weekday.diff
@@ -0,0 +1,37 @@
+--- Date.php.orig	2012-07-10 19:14:26.000000000 +0200
++++ Date.php	2012-07-10 19:16:22.000000000 +0200
+@@ -627,16 +627,25 @@
+             return;
+         }
+ 
+-        $this->_mday = 1;
+-        $first = $this->dayOfWeek();
+-        if ($weekday < $first) {
+-            $this->_mday = 8 + $weekday - $first;
+-        } else {
+-            $this->_mday = $weekday - $first + 1;
++        if ($nth < 0) {  // last $weekday of month
++            $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
++            $last = $this->dayOfWeek();
++            $this->_mday += ($weekday - $last);
++            if ($this->_mday > $lastday)
++                $this->_mday -= 7;
++        }
++        else {
++            $this->_mday = 1;
++            $first = $this->dayOfWeek();
++            if ($weekday < $first) {
++                $this->_mday = 8 + $weekday - $first;
++            } else {
++                $this->_mday = $weekday - $first + 1;
++            }
++            $diff = 7 * $nth - 7;
++            $this->_mday += $diff;
++            $this->_correct(self::MASK_DAY, $diff < 0);
+         }
+-        $diff = 7 * $nth - 7;
+-        $this->_mday += $diff;
+-        $this->_correct(self::MASK_DAY, $diff < 0);
+     }
+ 
+     /**
diff --git a/lib/plugins/libkolab/bin/get_horde_date.sh b/lib/plugins/libkolab/bin/get_horde_date.sh
new file mode 100755
index 0000000..b8e663d
--- /dev/null
+++ b/lib/plugins/libkolab/bin/get_horde_date.sh
@@ -0,0 +1,64 @@
+#!/bin/sh
+
+# Copy Horde_Date_Recurrence classes and dependencies to the given target directory.
+# This will create a standalone copy of the classes requried for date recurrence computation.
+
+SRCDIR=$1
+DESTDIR=$2
+BINDIR=`dirname $0`
+
+if [ ! -d "$SRCDIR" -o ! -d "$DESTDIR" ]; then
+  echo "Usage: get_horde_date.sh SRCDIR DESTDIR"
+  echo "Please enter valid source and destination directories for the Horde libs"
+  exit 1
+fi
+
+
+# concat Date.php and Date/Utils.php
+HORDE_DATE="$DESTDIR/Horde_Date.php"
+
+echo "<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ *   Horde/Date.php, Horde/Date/Utils.php
+ * Pull the latest version of these files from the PEAR channel of the Horde
+ * project at http://pear.horde.org by installing the Horde_Date package.
+ */
+" > $HORDE_DATE
+
+patch $SRCDIR/Date.php $BINDIR/Date_last_weekday.diff --output=$HORDE_DATE.patched
+sed 's/<?php//; s/?>//' $HORDE_DATE.patched >> $HORDE_DATE
+sed 's/<?php//; s/?>//' $SRCDIR/Date/Utils.php >> $HORDE_DATE
+
+# copy and patch Date/Recurrence.php
+HORDE_DATE_RECURRENCE="$DESTDIR/Horde_Date_Recurrence.php"
+
+echo "<?php
+
+/**
+ * This is a modified copy of Horde/Date/Recurrence.php
+ * Pull the latest version of this file from the PEAR channel of the Horde
+ * project at http://pear.horde.org by installing the Horde_Date package.
+ */
+
+if (!class_exists('Horde_Date'))
+	require_once(dirname(__FILE__) . '/Horde_Date.php');
+
+// minimal required implementation of Horde_Date_Translation to avoid a huge dependency nightmare
+class Horde_Date_Translation
+{
+	function t(\$arg) { return \$arg; }
+	function ngettext(\$sing, \$plur, \$num) { return (\$num > 1 ? \$plur : \$sing); }
+}
+" > $HORDE_DATE_RECURRENCE
+
+patch $SRCDIR/Date/Recurrence.php $BINDIR/Date_Recurrence_weekday.diff --output=$HORDE_DATE_RECURRENCE.patched
+sed 's/<?php//; s/?>//' $HORDE_DATE_RECURRENCE.patched >> $HORDE_DATE_RECURRENCE
+
+# remove dependency to Horde_String
+sed -i '' "s/Horde_String::/strto/" $HORDE_DATE_RECURRENCE
+
+rm $DESTDIR/Horde_Date*.patched
+
+
diff --git a/lib/plugins/libkolab/bin/modcache.sh b/lib/plugins/libkolab/bin/modcache.sh
new file mode 100755
index 0000000..04d36a5
--- /dev/null
+++ b/lib/plugins/libkolab/bin/modcache.sh
@@ -0,0 +1,191 @@
+#!/usr/bin/env php -d enable_dl=On
+<?php
+
+/**
+ * Kolab storage cache modification script
+ *
+ * @version 3.0
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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/>.
+ */
+
+define('INSTALL_PATH', realpath('.') . '/' );
+ini_set('display_errors', 1);
+
+if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
+    die("Execute this from the Roundcube installation dir!\n\n");
+
+require_once INSTALL_PATH . 'program/include/clisetup.php';
+
+function print_usage()
+{
+	print "Usage:  modcache.sh [OPTIONS] ACTION [USERNAME ARGS ...]\n";
+	print "Possible actions are: expunge, clear, prewarm\n";
+	print "-a, --all      Clear/expunge all caches\n";
+	print "-h, --host     IMAP host name\n";
+	print "-u, --user     IMAP user name to authenticate\n";
+	print "-t, --type     Object types to clear/expunge cache\n";
+	print "-l, --limit    Limit the number of records to be expunged\n";
+}
+
+// read arguments
+$opts = get_opt(array(
+    'a' => 'all',
+    'h' => 'host',
+    'u' => 'user',
+    'p' => 'password',
+    't' => 'type',
+    'l' => 'limit',
+    'v' => 'verbose',
+));
+
+$opts['username'] = !empty($opts[1]) ? $opts[1] : $opts['user'];
+$action = $opts[0];
+
+$rcmail = rcube::get_instance();
+
+
+/*
+ * Script controller
+ */
+switch (strtolower($action)) {
+
+/*
+ * Clear/expunge all cache records
+ */
+case 'expunge':
+    $expire = strtotime(!empty($opts[2]) ? $opts[2] : 'now - 10 days');
+    $sql_add = " AND created <= '" . date('Y-m-d 00:00:00', $expire) . "'";
+    if ($opts['limit']) {
+        $sql_add .= ' LIMIT ' . intval($opts['limit']);
+    }
+
+case 'clear':
+    // connect to database
+    $db = $rcmail->get_dbh();
+    $db->db_connect('w');
+    if (!$db->is_connected() || $db->is_error())
+        die("No DB connection\n");
+
+    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','distribution-list','event','task','configuration');
+    $folder_types_db = array_map(array($db, 'quote'), $folder_types);
+
+    if ($opts['all']) {
+        $sql_query = "DELETE FROM kolab_cache WHERE type IN (" . join(',', $folder_types_db) . ")";
+    }
+    else if ($opts['username']) {
+        $sql_query = "DELETE FROM kolab_cache WHERE type IN (" . join(',', $folder_types_db) . ") AND resource LIKE ?";
+    }
+
+    if ($sql_query) {
+        $db->query($sql_query . $sql_add, resource_prefix($opts).'%');
+        echo $db->affected_rows() . " records deleted from 'kolab_cache'\n";
+    }
+    break;
+
+
+/*
+ * Prewarm cache by synchronizing objects for the given user
+ */
+case 'prewarm':
+    // make sure libkolab classes are loaded
+    $rcmail->plugins->load_plugin('libkolab');
+
+    if (authenticate($opts)) {
+        $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','event','task','configuration');
+        foreach ($folder_types as $type) {
+            // sync every folder of the given type
+            foreach (kolab_storage::get_folders($type) as $folder) {
+                echo "Synching " . $folder->name . " ($type) ... ";
+                echo $folder->count($type) . "\n";
+
+                // also sync distribution lists in contact folders
+                if ($type == 'contact') {
+                    echo "Synching " . $folder->name . " (distribution-list) ... ";
+                    echo $folder->count('distribution-list') . "\n";
+                }
+            }
+        }
+    }
+    else
+        die("Authentication failed for " . $opts['user']);
+    break;
+
+
+/*
+ * Unknown action => show usage
+ */
+default:
+    print_usage();
+    exit;
+}
+
+
+/**
+ * Compose cache resource URI prefix for the given user credentials
+ */
+function resource_prefix($opts)
+{
+    return 'imap://' . urlencode($opts['username']) . '@' . $opts['host'] . '/';
+}
+
+
+/**
+ * Authenticate to the IMAP server with the given user credentials
+ */
+function authenticate(&$opts)
+{
+    global $rcmail;
+
+    // prompt for password
+    if (empty($opts['password']) && ($opts['username'] || $opts['user'])) {
+        $opts['password'] = prompt_silent("Password: ");
+    }
+
+    // simulate "login as" feature
+    if ($opts['user'] && $opts['user'] != $opts['username'])
+        $_POST['_loginas'] = $opts['username'];
+    else if (empty($opts['user']))
+        $opts['user'] = $opts['username'];
+
+    // let the kolab_auth plugin do its magic
+    $auth = $rcmail->plugins->exec_hook('authenticate', array(
+        'host' => trim($opts['host']),
+        'user' => trim($opts['user']),
+        'pass' => $opts['password'],
+        'cookiecheck' => false,
+        'valid' => !empty($opts['user']) && !empty($opts['host']),
+    ));
+
+    if ($auth['valid']) {
+        $storage = $rcmail->get_storage();
+        if ($storage->connect($auth['host'], $auth['user'], $auth['pass'], 143, false)) {
+            if ($opts['verbose'])
+                echo "IMAP login succeeded.\n";
+            if (($user = rcube_user::query($opts['username'], $auth['host'])) && $user->ID)
+                $rcmail->set_user($user);
+        }
+        else
+            die("Login to IMAP server failed!\n");
+    }
+    else {
+        die("Invalid login credentials!\n");
+    }
+
+    return $auth['valid'];
+}
+
diff --git a/lib/plugins/libkolab/config.inc.php.dist b/lib/plugins/libkolab/config.inc.php.dist
new file mode 100644
index 0000000..fedf793
--- /dev/null
+++ b/lib/plugins/libkolab/config.inc.php.dist
@@ -0,0 +1,9 @@
+<?php
+    /* Configuration for libkolab */
+
+    $rcmail_config['kolab_cache'] = true;
+
+    $rcmail_config['kolab_freebusy_server'] = 'https://' . $_SESSION['imap_host'] . '/freebusy';
+    $rcmail_config['kolab_ssl_verify_peer'] = true;
+
+?>
diff --git a/lib/plugins/libkolab/lib/Horde_Date.php b/lib/plugins/libkolab/lib/Horde_Date.php
new file mode 100644
index 0000000..9197f84
--- /dev/null
+++ b/lib/plugins/libkolab/lib/Horde_Date.php
@@ -0,0 +1,1304 @@
+<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ *   Horde/Date/Utils.php, Horde/Date/Recurrence.php
+ * Pull the latest version of these files from the PEAR channel of the Horde
+ * project at http://pear.horde.org by installing the Horde_Date package.
+ */
+
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * @category Horde
+ * @package  Date
+ *
+ * @TODO in format():
+ *   http://php.net/intldateformatter
+ *
+ * @TODO on timezones:
+ *   http://trac.agavi.org/ticket/1008
+ *   http://trac.agavi.org/changeset/3659
+ *
+ * @TODO on switching to PHP::DateTime:
+ *   The only thing ever stored in the database *IS* Unix timestamps. Doing
+ *   anything other than that is unmanageable, yet some frameworks use 'server
+ *   based' times in their systems, simply because they do not bother with
+ *   daylight saving and only 'serve' one timezone!
+ *
+ *   The second you have to manage 'real' time across timezones then daylight
+ *   saving becomes essential, BUT only on the display side! Since the browser
+ *   only provides a time offset, this is useless and to be honest should simply
+ *   be ignored ( until it is upgraded to provide the correct information ;)
+ *   ). So we need a 'display' function that takes a simple numeric epoch, and a
+ *   separate timezone id into which the epoch is to be 'converted'. My W3C
+ *   mapping works simply because ADOdb then converts that to it's own simple
+ *   offset abbreviation - in my case GMT or BST. As long as DateTime passes the
+ *   full 64 bit number the date range from 100AD is also preserved ( and
+ *   further back if 2 digit years are disabled ). If I want to display the
+ *   'real' timezone with this 'time' then I just add it in place of ADOdb's
+ *   'timezone'. I am tempted to simply adjust the ADOdb class to take a
+ *   timezone in place of the simple GMT switch it currently uses.
+ *
+ *   The return path is just the reverse and simply needs to take the client
+ *   display offset off prior to storage of the UTC epoch. SO we use
+ *   DateTimeZone to get an offset value for the clients timezone and simply add
+ *   or subtract this from a timezone agnostic display on the client end when
+ *   entering new times.
+ *
+ *
+ *   It's not really feasible to store dates in specific timezone, as most
+ *   national/local timezones support DST - and that is a pain to support, as
+ *   eg.  sorting breaks when some timestamps get repeated. That's why it's
+ *   usually better to store datetimes as either UTC datetime or plain unix
+ *   timestamp. I usually go with the former - using database datetime type.
+ */
+
+/**
+ * @category Horde
+ * @package  Date
+ */
+class Horde_Date
+{
+    const DATE_SUNDAY = 0;
+    const DATE_MONDAY = 1;
+    const DATE_TUESDAY = 2;
+    const DATE_WEDNESDAY = 3;
+    const DATE_THURSDAY = 4;
+    const DATE_FRIDAY = 5;
+    const DATE_SATURDAY = 6;
+
+    const MASK_SUNDAY = 1;
+    const MASK_MONDAY = 2;
+    const MASK_TUESDAY = 4;
+    const MASK_WEDNESDAY = 8;
+    const MASK_THURSDAY = 16;
+    const MASK_FRIDAY = 32;
+    const MASK_SATURDAY = 64;
+    const MASK_WEEKDAYS = 62;
+    const MASK_WEEKEND = 65;
+    const MASK_ALLDAYS = 127;
+
+    const MASK_SECOND = 1;
+    const MASK_MINUTE = 2;
+    const MASK_HOUR = 4;
+    const MASK_DAY = 8;
+    const MASK_MONTH = 16;
+    const MASK_YEAR = 32;
+    const MASK_ALLPARTS = 63;
+
+    const DATE_DEFAULT = 'Y-m-d H:i:s';
+    const DATE_JSON = 'Y-m-d\TH:i:s';
+
+    /**
+     * Year
+     *
+     * @var integer
+     */
+    protected $_year;
+
+    /**
+     * Month
+     *
+     * @var integer
+     */
+    protected $_month;
+
+    /**
+     * Day
+     *
+     * @var integer
+     */
+    protected $_mday;
+
+    /**
+     * Hour
+     *
+     * @var integer
+     */
+    protected $_hour = 0;
+
+    /**
+     * Minute
+     *
+     * @var integer
+     */
+    protected $_min = 0;
+
+    /**
+     * Second
+     *
+     * @var integer
+     */
+    protected $_sec = 0;
+
+    /**
+     * String representation of the date's timezone.
+     *
+     * @var string
+     */
+    protected $_timezone;
+
+    /**
+     * Default format for __toString()
+     *
+     * @var string
+     */
+    protected $_defaultFormat = self::DATE_DEFAULT;
+
+    /**
+     * Default specs that are always supported.
+     * @var string
+     */
+    protected static $_defaultSpecs = '%CdDeHImMnRStTyY';
+
+    /**
+     * Internally supported strftime() specifiers.
+     * @var string
+     */
+    protected static $_supportedSpecs = '';
+
+    /**
+     * Map of required correction masks.
+     *
+     * @see __set()
+     *
+     * @var array
+     */
+    protected static $_corrections = array(
+        'year'  => self::MASK_YEAR,
+        'month' => self::MASK_MONTH,
+        'mday'  => self::MASK_DAY,
+        'hour'  => self::MASK_HOUR,
+        'min'   => self::MASK_MINUTE,
+        'sec'   => self::MASK_SECOND,
+    );
+
+    protected $_formatCache = array();
+
+    /**
+     * Builds a new date object. If $date contains date parts, use them to
+     * initialize the object.
+     *
+     * Recognized formats:
+     * - arrays with keys 'year', 'month', 'mday', 'day'
+     *   'hour', 'min', 'minute', 'sec'
+     * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
+     * - yyyy-mm-dd hh:mm:ss
+     * - yyyymmddhhmmss
+     * - yyyymmddThhmmssZ
+     * - yyyymmdd (might conflict with unix timestamps between 31 Oct 1966 and
+     *   03 Mar 1973)
+     * - unix timestamps
+     * - anything parsed by strtotime()/DateTime.
+     *
+     * @throws Horde_Date_Exception
+     */
+    public function __construct($date = null, $timezone = null)
+    {
+        if (!self::$_supportedSpecs) {
+            self::$_supportedSpecs = self::$_defaultSpecs;
+            if (function_exists('nl_langinfo')) {
+                self::$_supportedSpecs .= 'bBpxX';
+            }
+        }
+
+        if (func_num_args() > 2) {
+            // Handle args in order: year month day hour min sec tz
+            $this->_initializeFromArgs(func_get_args());
+            return;
+        }
+
+        $this->_initializeTimezone($timezone);
+
+        if (is_null($date)) {
+            return;
+        }
+
+        if (is_string($date)) {
+            $date = trim($date, '"');
+        }
+
+        if (is_object($date)) {
+            $this->_initializeFromObject($date);
+        } elseif (is_array($date)) {
+            $this->_initializeFromArray($date);
+        } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $date, $parts)) {
+            $this->_year  = (int)$parts[1];
+            $this->_month = (int)$parts[2];
+            $this->_mday  = (int)$parts[3];
+            $this->_hour  = (int)$parts[4];
+            $this->_min   = (int)$parts[5];
+            $this->_sec   = (int)$parts[6];
+            if ($parts[7]) {
+                $this->_initializeTimezone('UTC');
+            }
+        } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $date, $parts) &&
+                  $parts[2] > 0 && $parts[2] <= 12 &&
+                  $parts[3] > 0 && $parts[3] <= 31) {
+            $this->_year  = (int)$parts[1];
+            $this->_month = (int)$parts[2];
+            $this->_mday  = (int)$parts[3];
+            $this->_hour = $this->_min = $this->_sec = 0;
+        } elseif ((string)(int)$date == $date) {
+            // Try as a timestamp.
+            $parts = @getdate($date);
+            if ($parts) {
+                $this->_year  = $parts['year'];
+                $this->_month = $parts['mon'];
+                $this->_mday  = $parts['mday'];
+                $this->_hour  = $parts['hours'];
+                $this->_min   = $parts['minutes'];
+                $this->_sec   = $parts['seconds'];
+            }
+        } else {
+            // Use date_create() so we can catch errors with PHP 5.2. Use
+            // "new DateTime() once we require 5.3.
+            $parsed = date_create($date);
+            if (!$parsed) {
+                throw new Horde_Date_Exception(sprintf(Horde_Date_Translation::t("Failed to parse time string (%s)"), $date));
+            }
+            $parsed->setTimezone(new DateTimeZone(date_default_timezone_get()));
+            $this->_year  = (int)$parsed->format('Y');
+            $this->_month = (int)$parsed->format('m');
+            $this->_mday  = (int)$parsed->format('d');
+            $this->_hour  = (int)$parsed->format('H');
+            $this->_min   = (int)$parsed->format('i');
+            $this->_sec   = (int)$parsed->format('s');
+            $this->_initializeTimezone(date_default_timezone_get());
+        }
+    }
+
+    /**
+     * Returns a simple string representation of the date object
+     *
+     * @return string  This object converted to a string.
+     */
+    public function __toString()
+    {
+        try {
+            return $this->format($this->_defaultFormat);
+        } catch (Exception $e) {
+            return '';
+        }
+    }
+
+    /**
+     * Returns a DateTime object representing this object.
+     *
+     * @return DateTime
+     */
+    public function toDateTime()
+    {
+        $date = new DateTime(null, new DateTimeZone($this->_timezone));
+        $date->setDate($this->_year, $this->_month, $this->_mday);
+        $date->setTime($this->_hour, $this->_min, $this->_sec);
+        return $date;
+    }
+
+    /**
+     * Converts a date in the proleptic Gregorian calendar to the no of days
+     * since 24th November, 4714 B.C.
+     *
+     * Returns the no of days since Monday, 24th November, 4714 B.C. in the
+     * proleptic Gregorian calendar (which is 24th November, -4713 using
+     * 'Astronomical' year numbering, and 1st January, 4713 B.C. in the
+     * proleptic Julian calendar).  This is also the first day of the 'Julian
+     * Period' proposed by Joseph Scaliger in 1583, and the number of days
+     * since this date is known as the 'Julian Day'.  (It is not directly
+     * to do with the Julian calendar, although this is where the name
+     * is derived from.)
+     *
+     * The algorithm is valid for all years (positive and negative), and
+     * also for years preceding 4714 B.C.
+     *
+     * Algorithm is from PEAR::Date_Calc
+     *
+     * @author Monte Ohrt <monte at ispi.net>
+     * @author Pierre-Alain Joye <pajoye at php.net>
+     * @author Daniel Convissor <danielc at php.net>
+     * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
+     *
+     * @return integer  The number of days since 24th November, 4714 B.C.
+     */
+    public function toDays()
+    {
+        if (function_exists('GregorianToJD')) {
+            return gregoriantojd($this->_month, $this->_mday, $this->_year);
+        }
+
+        $day = $this->_mday;
+        $month = $this->_month;
+        $year = $this->_year;
+
+        if ($month > 2) {
+            // March = 0, April = 1, ..., December = 9,
+            // January = 10, February = 11
+            $month -= 3;
+        } else {
+            $month += 9;
+            --$year;
+        }
+
+        $hb_negativeyear = $year < 0;
+        $century         = intval($year / 100);
+        $year            = $year % 100;
+
+        if ($hb_negativeyear) {
+            // Subtract 1 because year 0 is a leap year;
+            // And N.B. that we must treat the leap years as occurring
+            // one year earlier than they do, because for the purposes
+            // of calculation, the year starts on 1st March:
+            //
+            return intval((14609700 * $century + ($year == 0 ? 1 : 0)) / 400) +
+                   intval((1461 * $year + 1) / 4) +
+                   intval((153 * $month + 2) / 5) +
+                   $day + 1721118;
+        } else {
+            return intval(146097 * $century / 4) +
+                   intval(1461 * $year / 4) +
+                   intval((153 * $month + 2) / 5) +
+                   $day + 1721119;
+        }
+    }
+
+    /**
+     * Converts number of days since 24th November, 4714 B.C. (in the proleptic
+     * Gregorian calendar, which is year -4713 using 'Astronomical' year
+     * numbering) to Gregorian calendar date.
+     *
+     * Returned date belongs to the proleptic Gregorian calendar, using
+     * 'Astronomical' year numbering.
+     *
+     * The algorithm is valid for all years (positive and negative), and
+     * also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'),
+     * and so the only limitation is platform-dependent (for 32-bit systems
+     * the maximum year would be something like about 1,465,190 A.D.).
+     *
+     * N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'.
+     *
+     * Algorithm is from PEAR::Date_Calc
+     *
+     * @author Monte Ohrt <monte at ispi.net>
+     * @author Pierre-Alain Joye <pajoye at php.net>
+     * @author Daniel Convissor <danielc at php.net>
+     * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
+     *
+     * @param int    $days   the number of days since 24th November, 4714 B.C.
+     * @param string $format the string indicating how to format the output
+     *
+     * @return  Horde_Date  A Horde_Date object representing the date.
+     */
+    public static function fromDays($days)
+    {
+        if (function_exists('JDToGregorian')) {
+            list($month, $day, $year) = explode('/', JDToGregorian($days));
+        } else {
+            $days = intval($days);
+
+            $days   -= 1721119;
+            $century = floor((4 * $days - 1) / 146097);
+            $days    = floor(4 * $days - 1 - 146097 * $century);
+            $day     = floor($days / 4);
+
+            $year = floor((4 * $day +  3) / 1461);
+            $day  = floor(4 * $day +  3 - 1461 * $year);
+            $day  = floor(($day +  4) / 4);
+
+            $month = floor((5 * $day - 3) / 153);
+            $day   = floor(5 * $day - 3 - 153 * $month);
+            $day   = floor(($day +  5) /  5);
+
+            $year = $century * 100 + $year;
+            if ($month < 10) {
+                $month +=3;
+            } else {
+                $month -=9;
+                ++$year;
+            }
+        }
+
+        return new Horde_Date($year, $month, $day);
+    }
+
+    /**
+     * Getter for the date and time properties.
+     *
+     * @param string $name  One of 'year', 'month', 'mday', 'hour', 'min' or
+     *                      'sec'.
+     *
+     * @return integer  The property value, or null if not set.
+     */
+    public function __get($name)
+    {
+        if ($name == 'day') {
+            $name = 'mday';
+        }
+
+        return $this->{'_' . $name};
+    }
+
+    /**
+     * Setter for the date and time properties.
+     *
+     * @param string $name    One of 'year', 'month', 'mday', 'hour', 'min' or
+     *                        'sec'.
+     * @param integer $value  The property value.
+     */
+    public function __set($name, $value)
+    {
+        if ($name == 'timezone') {
+            $this->_initializeTimezone($value);
+            return;
+        }
+        if ($name == 'day') {
+            $name = 'mday';
+        }
+
+        if ($name != 'year' && $name != 'month' && $name != 'mday' &&
+            $name != 'hour' && $name != 'min' && $name != 'sec') {
+            throw new InvalidArgumentException('Undefined property ' . $name);
+        }
+
+        $down = $value < $this->{'_' . $name};
+        $this->{'_' . $name} = $value;
+        $this->_correct(self::$_corrections[$name], $down);
+        $this->_formatCache = array();
+    }
+
+    /**
+     * Returns whether a date or time property exists.
+     *
+     * @param string $name  One of 'year', 'month', 'mday', 'hour', 'min' or
+     *                      'sec'.
+     *
+     * @return boolen  True if the property exists and is set.
+     */
+    public function __isset($name)
+    {
+        if ($name == 'day') {
+            $name = 'mday';
+        }
+        return ($name == 'year' || $name == 'month' || $name == 'mday' ||
+                $name == 'hour' || $name == 'min' || $name == 'sec') &&
+            isset($this->{'_' . $name});
+    }
+
+    /**
+     * Adds a number of seconds or units to this date, returning a new Date
+     * object.
+     */
+    public function add($factor)
+    {
+        $d = clone($this);
+        if (is_array($factor) || is_object($factor)) {
+            foreach ($factor as $property => $value) {
+                $d->$property += $value;
+            }
+        } else {
+            $d->sec += $factor;
+        }
+
+        return $d;
+    }
+
+    /**
+     * Subtracts a number of seconds or units from this date, returning a new
+     * Horde_Date object.
+     */
+    public function sub($factor)
+    {
+        if (is_array($factor)) {
+            foreach ($factor as &$value) {
+                $value *= -1;
+            }
+        } else {
+            $factor *= -1;
+        }
+
+        return $this->add($factor);
+    }
+
+    /**
+     * Converts this object to a different timezone.
+     *
+     * @param string $timezone  The new timezone.
+     *
+     * @return Horde_Date  This object.
+     */
+    public function setTimezone($timezone)
+    {
+        $date = $this->toDateTime();
+        $date->setTimezone(new DateTimeZone($timezone));
+        $this->_timezone = $timezone;
+        $this->_year     = (int)$date->format('Y');
+        $this->_month    = (int)$date->format('m');
+        $this->_mday     = (int)$date->format('d');
+        $this->_hour     = (int)$date->format('H');
+        $this->_min      = (int)$date->format('i');
+        $this->_sec      = (int)$date->format('s');
+        $this->_formatCache = array();
+        return $this;
+    }
+
+    /**
+     * Sets the default date format used in __toString()
+     *
+     * @param string $format
+     */
+    public function setDefaultFormat($format)
+    {
+        $this->_defaultFormat = $format;
+    }
+
+    /**
+     * Returns the day of the week (0 = Sunday, 6 = Saturday) of this date.
+     *
+     * @return integer  The day of the week.
+     */
+    public function dayOfWeek()
+    {
+        if ($this->_month > 2) {
+            $month = $this->_month - 2;
+            $year = $this->_year;
+        } else {
+            $month = $this->_month + 10;
+            $year = $this->_year - 1;
+        }
+
+        $day = (floor((13 * $month - 1) / 5) +
+                $this->_mday + ($year % 100) +
+                floor(($year % 100) / 4) +
+                floor(($year / 100) / 4) - 2 *
+                floor($year / 100) + 77);
+
+        return (int)($day - 7 * floor($day / 7));
+    }
+
+    /**
+     * Returns the day number of the year (1 to 365/366).
+     *
+     * @return integer  The day of the year.
+     */
+    public function dayOfYear()
+    {
+        return $this->format('z') + 1;
+    }
+
+    /**
+     * Returns the week of the month.
+     *
+     * @return integer  The week number.
+     */
+    public function weekOfMonth()
+    {
+        return ceil($this->_mday / 7);
+    }
+
+    /**
+     * Returns the week of the year, first Monday is first day of first week.
+     *
+     * @return integer  The week number.
+     */
+    public function weekOfYear()
+    {
+        return $this->format('W');
+    }
+
+    /**
+     * Returns the number of weeks in the given year (52 or 53).
+     *
+     * @param integer $year  The year to count the number of weeks in.
+     *
+     * @return integer $numWeeks   The number of weeks in $year.
+     */
+    public static function weeksInYear($year)
+    {
+        // Find the last Thursday of the year.
+        $date = new Horde_Date($year . '-12-31');
+        while ($date->dayOfWeek() != self::DATE_THURSDAY) {
+            --$date->mday;
+        }
+        return $date->weekOfYear();
+    }
+
+    /**
+     * Sets the date of this object to the $nth weekday of $weekday.
+     *
+     * @param integer $weekday  The day of the week (0 = Sunday, etc).
+     * @param integer $nth      The $nth $weekday to set to (defaults to 1).
+     */
+    public function setNthWeekday($weekday, $nth = 1)
+    {
+        if ($weekday < self::DATE_SUNDAY || $weekday > self::DATE_SATURDAY) {
+            return;
+        }
+
+        if ($nth < 0) {  // last $weekday of month
+            $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+            $last = $this->dayOfWeek();
+            $this->_mday += ($weekday - $last);
+            if ($this->_mday > $lastday)
+                $this->_mday -= 7;
+        }
+        else {
+            $this->_mday = 1;
+            $first = $this->dayOfWeek();
+            if ($weekday < $first) {
+                $this->_mday = 8 + $weekday - $first;
+            } else {
+                $this->_mday = $weekday - $first + 1;
+            }
+            $diff = 7 * $nth - 7;
+            $this->_mday += $diff;
+            $this->_correct(self::MASK_DAY, $diff < 0);
+        }
+    }
+
+    /**
+     * Is the date currently represented by this object a valid date?
+     *
+     * @return boolean  Validity, counting leap years, etc.
+     */
+    public function isValid()
+    {
+        return ($this->_year >= 0 && $this->_year <= 9999);
+    }
+
+    /**
+     * Compares this date to another date object to see which one is
+     * greater (later). Assumes that the dates are in the same
+     * timezone.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return integer  ==  0 if they are on the same date
+     *                  >=  1 if $this is greater (later)
+     *                  <= -1 if $other is greater (later)
+     */
+    public function compareDate($other)
+    {
+        if (!($other instanceof Horde_Date)) {
+            $other = new Horde_Date($other);
+        }
+
+        if ($this->_year != $other->year) {
+            return $this->_year - $other->year;
+        }
+        if ($this->_month != $other->month) {
+            return $this->_month - $other->month;
+        }
+
+        return $this->_mday - $other->mday;
+    }
+
+    /**
+     * Returns whether this date is after the other.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return boolean  True if this date is after the other.
+     */
+    public function after($other)
+    {
+        return $this->compareDate($other) > 0;
+    }
+
+    /**
+     * Returns whether this date is before the other.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return boolean  True if this date is before the other.
+     */
+    public function before($other)
+    {
+        return $this->compareDate($other) < 0;
+    }
+
+    /**
+     * Returns whether this date is the same like the other.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return boolean  True if this date is the same like the other.
+     */
+    public function equals($other)
+    {
+        return $this->compareDate($other) == 0;
+    }
+
+    /**
+     * Compares this to another date object by time, to see which one
+     * is greater (later). Assumes that the dates are in the same
+     * timezone.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return integer  ==  0 if they are at the same time
+     *                  >=  1 if $this is greater (later)
+     *                  <= -1 if $other is greater (later)
+     */
+    public function compareTime($other)
+    {
+        if (!($other instanceof Horde_Date)) {
+            $other = new Horde_Date($other);
+        }
+
+        if ($this->_hour != $other->hour) {
+            return $this->_hour - $other->hour;
+        }
+        if ($this->_min != $other->min) {
+            return $this->_min - $other->min;
+        }
+
+        return $this->_sec - $other->sec;
+    }
+
+    /**
+     * Compares this to another date object, including times, to see
+     * which one is greater (later). Assumes that the dates are in the
+     * same timezone.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return integer  ==  0 if they are equal
+     *                  >=  1 if $this is greater (later)
+     *                  <= -1 if $other is greater (later)
+     */
+    public function compareDateTime($other)
+    {
+        if (!($other instanceof Horde_Date)) {
+            $other = new Horde_Date($other);
+        }
+
+        if ($diff = $this->compareDate($other)) {
+            return $diff;
+        }
+
+        return $this->compareTime($other);
+    }
+
+    /**
+     * Returns number of days between this date and another.
+     *
+     * @param Horde_Date $other  The other day to diff with.
+     *
+     * @return integer  The absolute number of days between the two dates.
+     */
+    public function diff($other)
+    {
+        return abs($this->toDays() - $other->toDays());
+    }
+
+    /**
+     * Returns the time offset for local time zone.
+     *
+     * @param boolean $colon  Place a colon between hours and minutes?
+     *
+     * @return string  Timezone offset as a string in the format +HH:MM.
+     */
+    public function tzOffset($colon = true)
+    {
+        return $colon ? $this->format('P') : $this->format('O');
+    }
+
+    /**
+     * Returns the unix timestamp representation of this date.
+     *
+     * @return integer  A unix timestamp.
+     */
+    public function timestamp()
+    {
+        if ($this->_year >= 1970 && $this->_year < 2038) {
+            return mktime($this->_hour, $this->_min, $this->_sec,
+                          $this->_month, $this->_mday, $this->_year);
+        }
+        return $this->format('U');
+    }
+
+    /**
+     * Returns the unix timestamp representation of this date, 12:00am.
+     *
+     * @return integer  A unix timestamp.
+     */
+    public function datestamp()
+    {
+        if ($this->_year >= 1970 && $this->_year < 2038) {
+            return mktime(0, 0, 0, $this->_month, $this->_mday, $this->_year);
+        }
+        $date = new DateTime($this->format('Y-m-d'));
+        return $date->format('U');
+    }
+
+    /**
+     * Formats date and time to be passed around as a short url parameter.
+     *
+     * @return string  Date and time.
+     */
+    public function dateString()
+    {
+        return sprintf('%04d%02d%02d', $this->_year, $this->_month, $this->_mday);
+    }
+
+    /**
+     * Formats date and time to the ISO format used by JSON.
+     *
+     * @return string  Date and time.
+     */
+    public function toJson()
+    {
+        return $this->format(self::DATE_JSON);
+    }
+
+    /**
+     * Formats date and time to the RFC 2445 iCalendar DATE-TIME format.
+     *
+     * @param boolean $floating  Whether to return a floating date-time
+     *                           (without time zone information).
+     *
+     * @return string  Date and time.
+     */
+    public function toiCalendar($floating = false)
+    {
+        if ($floating) {
+            return $this->format('Ymd\THis');
+        }
+        $dateTime = $this->toDateTime();
+        $dateTime->setTimezone(new DateTimeZone('UTC'));
+        return $dateTime->format('Ymd\THis\Z');
+    }
+
+    /**
+     * Formats time using the specifiers available in date() or in the DateTime
+     * class' format() method.
+     *
+     * To format in languages other than English, use strftime() instead.
+     *
+     * @param string $format
+     *
+     * @return string  Formatted time.
+     */
+    public function format($format)
+    {
+        if (!isset($this->_formatCache[$format])) {
+            $this->_formatCache[$format] = $this->toDateTime()->format($format);
+        }
+        return $this->_formatCache[$format];
+    }
+
+    /**
+     * Formats date and time using strftime() format.
+     *
+     * @return string  strftime() formatted date and time.
+     */
+    public function strftime($format)
+    {
+        if (preg_match('/%[^' . self::$_supportedSpecs . ']/', $format)) {
+            return strftime($format, $this->timestamp());
+        } else {
+            return $this->_strftime($format);
+        }
+    }
+
+    /**
+     * Formats date and time using a limited set of the strftime() format.
+     *
+     * @return string  strftime() formatted date and time.
+     */
+    protected function _strftime($format)
+    {
+        return preg_replace(
+            array('/%b/e',
+                  '/%B/e',
+                  '/%C/e',
+                  '/%d/e',
+                  '/%D/e',
+                  '/%e/e',
+                  '/%H/e',
+                  '/%I/e',
+                  '/%m/e',
+                  '/%M/e',
+                  '/%n/',
+                  '/%p/e',
+                  '/%R/e',
+                  '/%S/e',
+                  '/%t/',
+                  '/%T/e',
+                  '/%x/e',
+                  '/%X/e',
+                  '/%y/e',
+                  '/%Y/',
+                  '/%%/'),
+            array('$this->_strftime(Horde_Nls::getLangInfo(constant(\'ABMON_\' . (int)$this->_month)))',
+                  '$this->_strftime(Horde_Nls::getLangInfo(constant(\'MON_\' . (int)$this->_month)))',
+                  '(int)($this->_year / 100)',
+                  'sprintf(\'%02d\', $this->_mday)',
+                  '$this->_strftime(\'%m/%d/%y\')',
+                  'sprintf(\'%2d\', $this->_mday)',
+                  'sprintf(\'%02d\', $this->_hour)',
+                  'sprintf(\'%02d\', $this->_hour == 0 ? 12 : ($this->_hour > 12 ? $this->_hour - 12 : $this->_hour))',
+                  'sprintf(\'%02d\', $this->_month)',
+                  'sprintf(\'%02d\', $this->_min)',
+                  "\n",
+                  '$this->_strftime(Horde_Nls::getLangInfo($this->_hour < 12 ? AM_STR : PM_STR))',
+                  '$this->_strftime(\'%H:%M\')',
+                  'sprintf(\'%02d\', $this->_sec)',
+                  "\t",
+                  '$this->_strftime(\'%H:%M:%S\')',
+                  '$this->_strftime(Horde_Nls::getLangInfo(D_FMT))',
+                  '$this->_strftime(Horde_Nls::getLangInfo(T_FMT))',
+                  'substr(sprintf(\'%04d\', $this->_year), -2)',
+                  (int)$this->_year,
+                  '%'),
+            $format);
+    }
+
+    /**
+     * Corrects any over- or underflows in any of the date's members.
+     *
+     * @param integer $mask  We may not want to correct some overflows.
+     * @param integer $down  Whether to correct the date up or down.
+     */
+    protected function _correct($mask = self::MASK_ALLPARTS, $down = false)
+    {
+        if ($mask & self::MASK_SECOND) {
+            if ($this->_sec < 0 || $this->_sec > 59) {
+                $mask |= self::MASK_MINUTE;
+
+                $this->_min += (int)($this->_sec / 60);
+                $this->_sec %= 60;
+                if ($this->_sec < 0) {
+                    $this->_min--;
+                    $this->_sec += 60;
+                }
+            }
+        }
+
+        if ($mask & self::MASK_MINUTE) {
+            if ($this->_min < 0 || $this->_min > 59) {
+                $mask |= self::MASK_HOUR;
+
+                $this->_hour += (int)($this->_min / 60);
+                $this->_min %= 60;
+                if ($this->_min < 0) {
+                    $this->_hour--;
+                    $this->_min += 60;
+                }
+            }
+        }
+
+        if ($mask & self::MASK_HOUR) {
+            if ($this->_hour < 0 || $this->_hour > 23) {
+                $mask |= self::MASK_DAY;
+
+                $this->_mday += (int)($this->_hour / 24);
+                $this->_hour %= 24;
+                if ($this->_hour < 0) {
+                    $this->_mday--;
+                    $this->_hour += 24;
+                }
+            }
+        }
+
+        if ($mask & self::MASK_MONTH) {
+            $this->_correctMonth($down);
+            /* When correcting the month, always correct the day too. Months
+             * have different numbers of days. */
+            $mask |= self::MASK_DAY;
+        }
+
+        if ($mask & self::MASK_DAY) {
+            while ($this->_mday > 28 &&
+                   $this->_mday > Horde_Date_Utils::daysInMonth($this->_month, $this->_year)) {
+                if ($down) {
+                    $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month + 1, $this->_year) - Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+                } else {
+                    $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+                    $this->_month++;
+                }
+                $this->_correctMonth($down);
+            }
+            while ($this->_mday < 1) {
+                --$this->_month;
+                $this->_correctMonth($down);
+                $this->_mday += Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+            }
+        }
+    }
+
+    /**
+     * Corrects the current month.
+     *
+     * This cannot be done in _correct() because that would also trigger a
+     * correction of the day, which would result in an infinite loop.
+     *
+     * @param integer $down  Whether to correct the date up or down.
+     */
+    protected function _correctMonth($down = false)
+    {
+        $this->_year += (int)($this->_month / 12);
+        $this->_month %= 12;
+        if ($this->_month < 1) {
+            $this->_year--;
+            $this->_month += 12;
+        }
+    }
+
+    /**
+     * Handles args in order: year month day hour min sec tz
+     */
+    protected function _initializeFromArgs($args)
+    {
+        $tz = (isset($args[6])) ? array_pop($args) : null;
+        $this->_initializeTimezone($tz);
+
+        $args = array_slice($args, 0, 6);
+        $keys = array('year' => 1, 'month' => 1, 'mday' => 1, 'hour' => 0, 'min' => 0, 'sec' => 0);
+        $date = array_combine(array_slice(array_keys($keys), 0, count($args)), $args);
+        $date = array_merge($keys, $date);
+
+        $this->_initializeFromArray($date);
+    }
+
+    protected function _initializeFromArray($date)
+    {
+        if (isset($date['year']) && is_string($date['year']) && strlen($date['year']) == 2) {
+            if ($date['year'] > 70) {
+                $date['year'] += 1900;
+            } else {
+                $date['year'] += 2000;
+            }
+        }
+
+        foreach ($date as $key => $val) {
+            if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
+                $this->{'_'. $key} = (int)$val;
+            }
+        }
+
+        // If $date['day'] is present and numeric we may have been passed
+        // a Horde_Form_datetime array.
+        if (isset($date['day']) &&
+            (string)(int)$date['day'] == $date['day']) {
+            $this->_mday = (int)$date['day'];
+        }
+        // 'minute' key also from Horde_Form_datetime
+        if (isset($date['minute']) &&
+            (string)(int)$date['minute'] == $date['minute']) {
+            $this->_min = (int)$date['minute'];
+        }
+
+        $this->_correct();
+    }
+
+    protected function _initializeFromObject($date)
+    {
+        if ($date instanceof DateTime) {
+            $this->_year  = (int)$date->format('Y');
+            $this->_month = (int)$date->format('m');
+            $this->_mday  = (int)$date->format('d');
+            $this->_hour  = (int)$date->format('H');
+            $this->_min   = (int)$date->format('i');
+            $this->_sec   = (int)$date->format('s');
+            $this->_initializeTimezone($date->getTimezone()->getName());
+        } else {
+            $is_horde_date = $date instanceof Horde_Date;
+            foreach (array('year', 'month', 'mday', 'hour', 'min', 'sec') as $key) {
+                if ($is_horde_date || isset($date->$key)) {
+                    $this->{'_' . $key} = (int)$date->$key;
+                }
+            }
+            if (!$is_horde_date) {
+                $this->_correct();
+            } else {
+                $this->_initializeTimezone($date->timezone);
+            }
+        }
+    }
+
+    protected function _initializeTimezone($timezone)
+    {
+        if (empty($timezone)) {
+            $timezone = date_default_timezone_get();
+        }
+        $this->_timezone = $timezone;
+    }
+
+}
+
+/**
+ * @category Horde
+ * @package  Date
+ */
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * @category Horde
+ * @package  Date
+ */
+class Horde_Date_Utils
+{
+    /**
+     * Returns whether a year is a leap year.
+     *
+     * @param integer $year  The year.
+     *
+     * @return boolean  True if the year is a leap year.
+     */
+    public static function isLeapYear($year)
+    {
+        if (strlen($year) != 4 || preg_match('/\D/', $year)) {
+            return false;
+        }
+
+        return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
+    }
+
+    /**
+     * Returns the date of the year that corresponds to the first day of the
+     * given week.
+     *
+     * @param integer $week  The week of the year to find the first day of.
+     * @param integer $year  The year to calculate for.
+     *
+     * @return Horde_Date  The date of the first day of the given week.
+     */
+    public static function firstDayOfWeek($week, $year)
+    {
+        return new Horde_Date(sprintf('%04dW%02d', $year, $week));
+    }
+
+    /**
+     * Returns the number of days in the specified month.
+     *
+     * @param integer $month  The month
+     * @param integer $year   The year.
+     *
+     * @return integer  The number of days in the month.
+     */
+    public static function daysInMonth($month, $year)
+    {
+        static $cache = array();
+        if (!isset($cache[$year][$month])) {
+            $date = new DateTime(sprintf('%04d-%02d-01', $year, $month));
+            $cache[$year][$month] = $date->format('t');
+        }
+        return $cache[$year][$month];
+    }
+
+    /**
+     * Returns a relative, natural language representation of a timestamp
+     *
+     * @todo Wider range of values ... maybe future time as well?
+     * @todo Support minimum resolution parameter.
+     *
+     * @param mixed $time          The time. Any format accepted by Horde_Date.
+     * @param string $date_format  Format to display date if timestamp is
+     *                             more then 1 day old.
+     * @param string $time_format  Format to display time if timestamp is 1
+     *                             day old.
+     *
+     * @return string  The relative time (i.e. 2 minutes ago)
+     */
+    public static function relativeDateTime($time, $date_format = '%x',
+                                            $time_format = '%X')
+    {
+        $date = new Horde_Date($time);
+
+        $delta = time() - $date->timestamp();
+        if ($delta < 60) {
+            return sprintf(Horde_Date_Translation::ngettext("%d second ago", "%d seconds ago", $delta), $delta);
+        }
+
+        $delta = round($delta / 60);
+        if ($delta < 60) {
+            return sprintf(Horde_Date_Translation::ngettext("%d minute ago", "%d minutes ago", $delta), $delta);
+        }
+
+        $delta = round($delta / 60);
+        if ($delta < 24) {
+            return sprintf(Horde_Date_Translation::ngettext("%d hour ago", "%d hours ago", $delta), $delta);
+        }
+
+        if ($delta > 24 && $delta < 48) {
+            $date = new Horde_Date($time);
+            return sprintf(Horde_Date_Translation::t("yesterday at %s"), $date->strftime($time_format));
+        }
+
+        $delta = round($delta / 24);
+        if ($delta < 7) {
+            return sprintf(Horde_Date_Translation::t("%d days ago"), $delta);
+        }
+
+        if (round($delta / 7) < 5) {
+            $delta = round($delta / 7);
+            return sprintf(Horde_Date_Translation::ngettext("%d week ago", "%d weeks ago", $delta), $delta);
+        }
+
+        // Default to the user specified date format.
+        return $date->strftime($date_format);
+    }
+
+    /**
+     * Tries to convert strftime() formatters to date() formatters.
+     *
+     * Unsupported formatters will be removed.
+     *
+     * @param string $format  A strftime() formatting string.
+     *
+     * @return string  A date() formatting string.
+     */
+    public static function strftime2date($format)
+    {
+        $replace = array(
+            '/%a/'  => 'D',
+            '/%A/'  => 'l',
+            '/%d/'  => 'd',
+            '/%e/'  => 'j',
+            '/%j/'  => 'z',
+            '/%u/'  => 'N',
+            '/%w/'  => 'w',
+            '/%U/'  => '',
+            '/%V/'  => 'W',
+            '/%W/'  => '',
+            '/%b/'  => 'M',
+            '/%B/'  => 'F',
+            '/%h/'  => 'M',
+            '/%m/'  => 'm',
+            '/%C/'  => '',
+            '/%g/'  => '',
+            '/%G/'  => 'o',
+            '/%y/'  => 'y',
+            '/%Y/'  => 'Y',
+            '/%H/'  => 'H',
+            '/%I/'  => 'h',
+            '/%i/'  => 'g',
+            '/%M/'  => 'i',
+            '/%p/'  => 'A',
+            '/%P/'  => 'a',
+            '/%r/'  => 'h:i:s A',
+            '/%R/'  => 'H:i',
+            '/%S/'  => 's',
+            '/%T/'  => 'H:i:s',
+            '/%X/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(T_FMT))',
+            '/%z/'  => 'O',
+            '/%Z/'  => '',
+            '/%c/'  => '',
+            '/%D/'  => 'm/d/y',
+            '/%F/'  => 'Y-m-d',
+            '/%s/'  => 'U',
+            '/%x/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(D_FMT))',
+            '/%n/'  => "\n",
+            '/%t/'  => "\t",
+            '/%%/'  => '%'
+        );
+
+        return preg_replace(array_keys($replace), array_values($replace), $format);
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/Horde_Date_Recurrence.php b/lib/plugins/libkolab/lib/Horde_Date_Recurrence.php
new file mode 100644
index 0000000..81f0857
--- /dev/null
+++ b/lib/plugins/libkolab/lib/Horde_Date_Recurrence.php
@@ -0,0 +1,1673 @@
+<?php
+
+/**
+ * This is a modified copy of Horde/Date/Recurrence.php
+ * Pull the latest version of this file from the PEAR channel of the Horde
+ * project at http://pear.horde.org by installing the Horde_Date package.
+ */
+
+if (!class_exists('Horde_Date'))
+	require_once(dirname(__FILE__) . '/Horde_Date.php');
+
+// minimal required implementation of Horde_Date_Translation to avoid a huge dependency nightmare
+class Horde_Date_Translation
+{
+	function t($arg) { return $arg; }
+	function ngettext($sing, $plur, $num) { return ($num > 1 ? $plur : $sing); }
+}
+
+
+/**
+ * This file contains the Horde_Date_Recurrence class and according constants.
+ *
+ * Copyright 2007-2012 Horde LLC (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.horde.org/licenses/lgpl21.
+ *
+ * @category Horde
+ * @package  Date
+ */
+
+/**
+ * The Horde_Date_Recurrence class implements algorithms for calculating
+ * recurrences of events, including several recurrence types, intervals,
+ * exceptions, and conversion from and to vCalendar and iCalendar recurrence
+ * rules.
+ *
+ * All methods expecting dates as parameters accept all values that the
+ * Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date
+ * object, an ISO time string or a hash.
+ *
+ * @author   Jan Schneider <jan at horde.org>
+ * @category Horde
+ * @package  Date
+ */
+class Horde_Date_Recurrence
+{
+    /** No Recurrence **/
+    const RECUR_NONE = 0;
+
+    /** Recurs daily. */
+    const RECUR_DAILY = 1;
+
+    /** Recurs weekly. */
+    const RECUR_WEEKLY = 2;
+
+    /** Recurs monthly on the same date. */
+    const RECUR_MONTHLY_DATE = 3;
+
+    /** Recurs monthly on the same week day. */
+    const RECUR_MONTHLY_WEEKDAY = 4;
+
+    /** Recurs yearly on the same date. */
+    const RECUR_YEARLY_DATE = 5;
+
+    /** Recurs yearly on the same day of the year. */
+    const RECUR_YEARLY_DAY = 6;
+
+    /** Recurs yearly on the same week day. */
+    const RECUR_YEARLY_WEEKDAY = 7;
+
+    /**
+     * The start time of the event.
+     *
+     * @var Horde_Date
+     */
+    public $start;
+
+    /**
+     * The end date of the recurrence interval.
+     *
+     * @var Horde_Date
+     */
+    public $recurEnd = null;
+
+    /**
+     * The number of recurrences.
+     *
+     * @var integer
+     */
+    public $recurCount = null;
+
+    /**
+     * The type of recurrence this event follows. RECUR_* constant.
+     *
+     * @var integer
+     */
+    public $recurType = self::RECUR_NONE;
+
+    /**
+     * The length of time between recurrences. The time unit depends on the
+     * recurrence type.
+     *
+     * @var integer
+     */
+    public $recurInterval = 1;
+
+    /**
+     * Any additional recurrence data.
+     *
+     * @var integer
+     */
+    public $recurData = null;
+
+    /**
+     * BYDAY recurrence number
+     *
+     * @var integer
+     */
+    public $recurNthDay = null;
+
+    /**
+     * BYMONTH recurrence data
+     *
+     * @var array
+     */
+    public $recurMonths = array();
+
+    /**
+     * All the exceptions from recurrence for this event.
+     *
+     * @var array
+     */
+    public $exceptions = array();
+
+    /**
+     * All the dates this recurrence has been marked as completed.
+     *
+     * @var array
+     */
+    public $completions = array();
+
+    /**
+     * Constructor.
+     *
+     * @param Horde_Date $start  Start of the recurring event.
+     */
+    public function __construct($start)
+    {
+        $this->start = new Horde_Date($start);
+    }
+
+    /**
+     * Resets the class properties.
+     */
+    public function reset()
+    {
+        $this->recurEnd = null;
+        $this->recurCount = null;
+        $this->recurType = self::RECUR_NONE;
+        $this->recurInterval = 1;
+        $this->recurData = null;
+        $this->exceptions = array();
+        $this->completions = array();
+    }
+
+    /**
+     * Checks if this event recurs on a given day of the week.
+     *
+     * @param integer $dayMask  A mask consisting of Horde_Date::MASK_*
+     *                          constants specifying the day(s) to check.
+     *
+     * @return boolean  True if this event recurs on the given day(s).
+     */
+    public function recurOnDay($dayMask)
+    {
+        return ($this->recurData & $dayMask);
+    }
+
+    /**
+     * Specifies the days this event recurs on.
+     *
+     * @param integer $dayMask  A mask consisting of Horde_Date::MASK_*
+     *                          constants specifying the day(s) to recur on.
+     */
+    public function setRecurOnDay($dayMask)
+    {
+        $this->recurData = $dayMask;
+    }
+
+    /**
+     *
+     * @param integer $nthDay The nth weekday of month to repeat events on
+     */
+    public function setRecurNthWeekday($nth)
+    {
+        $this->recurNthDay = (int)$nth;
+    }
+
+    /**
+     *
+     * @return integer  The nth weekday of month to repeat events.
+     */
+    public function getRecurNthWeekday()
+    {
+        return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7);
+    }
+
+    /**
+     * Specifies the months for yearly (weekday) recurrence
+     *
+     * @param array $months  List of months (integers) this event recurs on.
+     */
+    function setRecurByMonth($months)
+    {
+        $this->recurMonths = (array)$months;
+    }
+
+    /**
+     * Returns a list of months this yearly event recurs on
+     *
+     * @return array List of months (integers) this event recurs on.
+     */
+    function getRecurByMonth()
+    {
+        return $this->recurMonths;
+    }
+
+    /**
+     * Returns the days this event recurs on.
+     *
+     * @return integer  A mask consisting of Horde_Date::MASK_* constants
+     *                  specifying the day(s) this event recurs on.
+     */
+    public function getRecurOnDays()
+    {
+        return $this->recurData;
+    }
+
+    /**
+     * Returns whether this event has a specific recurrence type.
+     *
+     * @param integer $recurrence  RECUR_* constant of the
+     *                             recurrence type to check for.
+     *
+     * @return boolean  True if the event has the specified recurrence type.
+     */
+    public function hasRecurType($recurrence)
+    {
+        return ($recurrence == $this->recurType);
+    }
+
+    /**
+     * Sets a recurrence type for this event.
+     *
+     * @param integer $recurrence  A RECUR_* constant.
+     */
+    public function setRecurType($recurrence)
+    {
+        $this->recurType = $recurrence;
+    }
+
+    /**
+     * Returns recurrence type of this event.
+     *
+     * @return integer  A RECUR_* constant.
+     */
+    public function getRecurType()
+    {
+        return $this->recurType;
+    }
+
+    /**
+     * Returns a description of this event's recurring type.
+     *
+     * @return string  Human readable recurring type.
+     */
+    public function getRecurName()
+    {
+        switch ($this->getRecurType()) {
+        case self::RECUR_NONE: return Horde_Date_Translation::t("No recurrence");
+        case self::RECUR_DAILY: return Horde_Date_Translation::t("Daily");
+        case self::RECUR_WEEKLY: return Horde_Date_Translation::t("Weekly");
+        case self::RECUR_MONTHLY_DATE:
+        case self::RECUR_MONTHLY_WEEKDAY: return Horde_Date_Translation::t("Monthly");
+        case self::RECUR_YEARLY_DATE:
+        case self::RECUR_YEARLY_DAY:
+        case self::RECUR_YEARLY_WEEKDAY: return Horde_Date_Translation::t("Yearly");
+        }
+    }
+
+    /**
+     * Sets the length of time between recurrences of this event.
+     *
+     * @param integer $interval  The time between recurrences.
+     */
+    public function setRecurInterval($interval)
+    {
+        if ($interval > 0) {
+            $this->recurInterval = $interval;
+        }
+    }
+
+    /**
+     * Retrieves the length of time between recurrences of this event.
+     *
+     * @return integer  The number of seconds between recurrences.
+     */
+    public function getRecurInterval()
+    {
+        return $this->recurInterval;
+    }
+
+    /**
+     * Sets the number of recurrences of this event.
+     *
+     * @param integer $count  The number of recurrences.
+     */
+    public function setRecurCount($count)
+    {
+        if ($count > 0) {
+            $this->recurCount = (int)$count;
+            // Recurrence counts and end dates are mutually exclusive.
+            $this->recurEnd = null;
+        } else {
+            $this->recurCount = null;
+        }
+    }
+
+    /**
+     * Retrieves the number of recurrences of this event.
+     *
+     * @return integer  The number recurrences.
+     */
+    public function getRecurCount()
+    {
+        return $this->recurCount;
+    }
+
+    /**
+     * Returns whether this event has a recurrence with a fixed count.
+     *
+     * @return boolean  True if this recurrence has a fixed count.
+     */
+    public function hasRecurCount()
+    {
+        return isset($this->recurCount);
+    }
+
+    /**
+     * Sets the start date of the recurrence interval.
+     *
+     * @param Horde_Date $start  The recurrence start.
+     */
+    public function setRecurStart($start)
+    {
+        $this->start = clone $start;
+    }
+
+    /**
+     * Retrieves the start date of the recurrence interval.
+     *
+     * @return Horde_Date  The recurrence start.
+     */
+    public function getRecurStart()
+    {
+        return $this->start;
+    }
+
+    /**
+     * Sets the end date of the recurrence interval.
+     *
+     * @param Horde_Date $end  The recurrence end.
+     */
+    public function setRecurEnd($end)
+    {
+        if (!empty($end)) {
+            // Recurrence counts and end dates are mutually exclusive.
+            $this->recurCount = null;
+            $this->recurEnd = clone $end;
+        } else {
+            $this->recurEnd = $end;
+        }
+    }
+
+    /**
+     * Retrieves the end date of the recurrence interval.
+     *
+     * @return Horde_Date  The recurrence end.
+     */
+    public function getRecurEnd()
+    {
+        return $this->recurEnd;
+    }
+
+    /**
+     * Returns whether this event has a recurrence end.
+     *
+     * @return boolean  True if this recurrence ends.
+     */
+    public function hasRecurEnd()
+    {
+        return isset($this->recurEnd) && isset($this->recurEnd->year) &&
+            $this->recurEnd->year != 9999;
+    }
+
+    /**
+     * Finds the next recurrence of this event that's after $afterDate.
+     *
+     * @param Horde_Date|string $after  Return events after this date.
+     *
+     * @return Horde_Date|boolean  The date of the next recurrence or false
+     *                             if the event does not recur after
+     *                             $afterDate.
+     */
+    public function nextRecurrence($after)
+    {
+        if (!($after instanceof Horde_Date)) {
+            $after = new Horde_Date($after);
+        } else {
+            $after = clone($after);
+        }
+
+        // Make sure $after and $this->start are in the same TZ
+        $after->setTimezone($this->start->timezone);
+        if ($this->start->compareDateTime($after) >= 0) {
+            return clone $this->start;
+        }
+
+        if ($this->recurInterval == 0) {
+            return false;
+        }
+
+        switch ($this->getRecurType()) {
+        case self::RECUR_DAILY:
+            $diff = $this->start->diff($after);
+            $recur = ceil($diff / $this->recurInterval);
+            if ($this->recurCount && $recur >= $this->recurCount) {
+                return false;
+            }
+
+            $recur *= $this->recurInterval;
+            $next = $this->start->add(array('day' => $recur));
+            if ((!$this->hasRecurEnd() ||
+                 $next->compareDateTime($this->recurEnd) <= 0) &&
+                $next->compareDateTime($after) >= 0) {
+                return $next;
+            }
+            break;
+
+        case self::RECUR_WEEKLY:
+            if (empty($this->recurData)) {
+                return false;
+            }
+
+            $start_week = Horde_Date_Utils::firstDayOfWeek($this->start->format('W'),
+                                                           $this->start->year);
+            $start_week->timezone = $this->start->timezone;
+            $start_week->hour = $this->start->hour;
+            $start_week->min  = $this->start->min;
+            $start_week->sec  = $this->start->sec;
+
+            // Make sure we are not at the ISO-8601 first week of year while
+            // still in month 12...OR in the ISO-8601 last week of year while
+            // in month 1 and adjust the year accordingly.
+            $week = $after->format('W');
+            if ($week == 1 && $after->month == 12) {
+                $theYear = $after->year + 1;
+            } elseif ($week >= 52 && $after->month == 1) {
+                $theYear = $after->year - 1;
+            } else {
+                $theYear = $after->year;
+            }
+
+            $after_week = Horde_Date_Utils::firstDayOfWeek($week, $theYear);
+            $after_week->timezone = $this->start->timezone;
+            $after_week_end = clone $after_week;
+            $after_week_end->mday += 7;
+
+            $diff = $start_week->diff($after_week);
+            $interval = $this->recurInterval * 7;
+            $repeats = floor($diff / $interval);
+            if ($diff % $interval < 7) {
+                $recur = $diff;
+            } else {
+                /**
+                 * If the after_week is not in the first week interval the
+                 * search needs to skip ahead a complete interval. The way it is
+                 * calculated here means that an event that occurs every second
+                 * week on Monday and Wednesday with the event actually starting
+                 * on Tuesday or Wednesday will only have one incidence in the
+                 * first week.
+                 */
+                $recur = $interval * ($repeats + 1);
+            }
+
+            if ($this->hasRecurCount()) {
+                $recurrences = 0;
+                /**
+                 * Correct the number of recurrences by the number of events
+                 * that lay between the start of the start week and the
+                 * recurrence start.
+                 */
+                $next = clone $start_week;
+                while ($next->compareDateTime($this->start) < 0) {
+                    if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
+                        $recurrences--;
+                    }
+                    ++$next->mday;
+                }
+                if ($repeats > 0) {
+                    $weekdays = $this->recurData;
+                    $total_recurrences_per_week = 0;
+                    while ($weekdays > 0) {
+                        if ($weekdays % 2) {
+                            $total_recurrences_per_week++;
+                        }
+                        $weekdays = ($weekdays - ($weekdays % 2)) / 2;
+                    }
+                    $recurrences += $total_recurrences_per_week * $repeats;
+                }
+            }
+
+            $next = clone $start_week;
+            $next->mday += $recur;
+            while ($next->compareDateTime($after) < 0 &&
+                   $next->compareDateTime($after_week_end) < 0) {
+                if ($this->hasRecurCount()
+                    && $next->compareDateTime($after) < 0
+                    && $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
+                    $recurrences++;
+                }
+                ++$next->mday;
+            }
+            if ($this->hasRecurCount() &&
+                $recurrences >= $this->recurCount) {
+                return false;
+            }
+            if (!$this->hasRecurEnd() ||
+                $next->compareDateTime($this->recurEnd) <= 0) {
+                if ($next->compareDateTime($after_week_end) >= 0) {
+                    return $this->nextRecurrence($after_week_end);
+                }
+                while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) &&
+                       $next->compareDateTime($after_week_end) < 0) {
+                    ++$next->mday;
+                }
+                if (!$this->hasRecurEnd() ||
+                    $next->compareDateTime($this->recurEnd) <= 0) {
+                    if ($next->compareDateTime($after_week_end) >= 0) {
+                        return $this->nextRecurrence($after_week_end);
+                    } else {
+                        return $next;
+                    }
+                }
+            }
+            break;
+
+        case self::RECUR_MONTHLY_DATE:
+            $start = clone $this->start;
+            if ($after->compareDateTime($start) < 0) {
+                $after = clone $start;
+            } else {
+                $after = clone $after;
+            }
+
+            // If we're starting past this month's recurrence of the event,
+            // look in the next month on the day the event recurs.
+            if ($after->mday > $start->mday) {
+                ++$after->month;
+                $after->mday = $start->mday;
+            }
+
+            // Adjust $start to be the first match.
+            $offset = ($after->month - $start->month) + ($after->year - $start->year) * 12;
+            $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+            if ($this->recurCount &&
+                ($offset / $this->recurInterval) >= $this->recurCount) {
+                return false;
+            }
+            $start->month += $offset;
+            $count = $offset / $this->recurInterval;
+
+            do {
+                if ($this->recurCount &&
+                    $count++ >= $this->recurCount) {
+                    return false;
+                }
+
+                // Bail if we've gone past the end of recurrence.
+                if ($this->hasRecurEnd() &&
+                    $this->recurEnd->compareDateTime($start) < 0) {
+                    return false;
+                }
+                if ($start->isValid()) {
+                    return $start;
+                }
+
+                // If the interval is 12, and the date isn't valid, then we
+                // need to see if February 29th is an option. If not, then the
+                // event will _never_ recur, and we need to stop checking to
+                // avoid an infinite loop.
+                if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) {
+                    return false;
+                }
+
+                // Add the recurrence interval.
+                $start->month += $this->recurInterval;
+            } while (true);
+
+            break;
+
+        case self::RECUR_MONTHLY_WEEKDAY:
+            // Start with the start date of the event.
+            $estart = clone $this->start;
+
+            // What day of the week, and week of the month, do we recur on?
+            if (isset($this->recurNthDay)) {
+                $nth = $this->recurNthDay;
+                $weekday = log($this->recurData, 2);
+            } else {
+                $nth = ceil($this->start->mday / 7);
+                $weekday = $estart->dayOfWeek();
+            }
+
+            // Adjust $estart to be the first candidate.
+            $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
+            $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+            // Adjust our working date until it's after $after.
+            $estart->month += $offset - $this->recurInterval;
+
+            $count = $offset / $this->recurInterval;
+            do {
+                if ($this->recurCount &&
+                    $count++ >= $this->recurCount) {
+                    return false;
+                }
+
+                $estart->month += $this->recurInterval;
+
+                $next = clone $estart;
+                $next->setNthWeekday($weekday, $nth);
+
+                if ($next->compareDateTime($after) < 0) {
+                    // We haven't made it past $after yet, try again.
+                    continue;
+                }
+                if ($this->hasRecurEnd() &&
+                    $next->compareDateTime($this->recurEnd) > 0) {
+                    // We've gone past the end of recurrence; we can give up
+                    // now.
+                    return false;
+                }
+
+                // We have a candidate to return.
+                break;
+            } while (true);
+
+            return $next;
+
+        case self::RECUR_YEARLY_DATE:
+            // Start with the start date of the event.
+            $estart = clone $this->start;
+            $after = clone $after;
+
+            if ($after->month > $estart->month ||
+                ($after->month == $estart->month && $after->mday > $estart->mday)) {
+                ++$after->year;
+                $after->month = $estart->month;
+                $after->mday = $estart->mday;
+            }
+
+            // Seperate case here for February 29th
+            if ($estart->month == 2 && $estart->mday == 29) {
+                while (!Horde_Date_Utils::isLeapYear($after->year)) {
+                    ++$after->year;
+                }
+            }
+
+            // Adjust $estart to be the first candidate.
+            $offset = $after->year - $estart->year;
+            if ($offset > 0) {
+                $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+                $estart->year += $offset;
+            }
+
+            // We've gone past the end of recurrence; give up.
+            if ($this->recurCount &&
+                $offset >= $this->recurCount) {
+                return false;
+            }
+            if ($this->hasRecurEnd() &&
+                $this->recurEnd->compareDateTime($estart) < 0) {
+                return false;
+            }
+
+            return $estart;
+
+        case self::RECUR_YEARLY_DAY:
+            // Check count first.
+            $dayofyear = $this->start->dayOfYear();
+            $count = ($after->year - $this->start->year) / $this->recurInterval + 1;
+            if ($this->recurCount &&
+                ($count > $this->recurCount ||
+                 ($count == $this->recurCount &&
+                  $after->dayOfYear() > $dayofyear))) {
+                return false;
+            }
+
+            // Start with a rough interval.
+            $estart = clone $this->start;
+            $estart->year += floor($count - 1) * $this->recurInterval;
+
+            // Now add the difference to the required day of year.
+            $estart->mday += $dayofyear - $estart->dayOfYear();
+
+            // Add an interval if the estimation was wrong.
+            if ($estart->compareDate($after) < 0) {
+                $estart->year += $this->recurInterval;
+                $estart->mday += $dayofyear - $estart->dayOfYear();
+            }
+
+            // We've gone past the end of recurrence; give up.
+            if ($this->hasRecurEnd() &&
+                $this->recurEnd->compareDateTime($estart) < 0) {
+                return false;
+            }
+
+            return $estart;
+
+        case self::RECUR_YEARLY_WEEKDAY:
+            // Start with the start date of the event.
+            $estart = clone $this->start;
+
+            // What day of the week, and week of the month, do we recur on?
+            if (isset($this->recurNthDay)) {
+                $nth = $this->recurNthDay;
+                $weekday = log($this->recurData, 2);
+            } else {
+                $nth = ceil($this->start->mday / 7);
+                $weekday = $estart->dayOfWeek();
+            }
+
+            // Adjust $estart to be the first candidate.
+            $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+            // Adjust our working date until it's after $after.
+            $estart->year += $offset - $this->recurInterval;
+
+            $count = $offset / $this->recurInterval;
+            do {
+                if ($this->recurCount &&
+                    $count++ >= $this->recurCount) {
+                    return false;
+                }
+
+                $estart->year += $this->recurInterval;
+
+                $next = clone $estart;
+                $next->setNthWeekday($weekday, $nth);
+
+                if ($next->compareDateTime($after) < 0) {
+                    // We haven't made it past $after yet, try again.
+                    continue;
+                }
+                if ($this->hasRecurEnd() &&
+                    $next->compareDateTime($this->recurEnd) > 0) {
+                    // We've gone past the end of recurrence; we can give up
+                    // now.
+                    return false;
+                }
+
+                // We have a candidate to return.
+                break;
+            } while (true);
+
+            return $next;
+        }
+
+        // We didn't find anything, the recurType was bad, or something else
+        // went wrong - return false.
+        return false;
+    }
+
+    /**
+     * Returns whether this event has any date that matches the recurrence
+     * rules and is not an exception.
+     *
+     * @return boolean  True if an active recurrence exists.
+     */
+    public function hasActiveRecurrence()
+    {
+        if (!$this->hasRecurEnd()) {
+            return true;
+        }
+
+        $next = $this->nextRecurrence(new Horde_Date($this->start));
+        while (is_object($next)) {
+            if (!$this->hasException($next->year, $next->month, $next->mday) &&
+                !$this->hasCompletion($next->year, $next->month, $next->mday)) {
+                return true;
+            }
+
+            $next = $this->nextRecurrence($next->add(array('day' => 1)));
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the next active recurrence.
+     *
+     * @param Horde_Date $afterDate  Return events after this date.
+     *
+     * @return Horde_Date|boolean The date of the next active
+     *                             recurrence or false if the event
+     *                             has no active recurrence after
+     *                             $afterDate.
+     */
+    public function nextActiveRecurrence($afterDate)
+    {
+        $next = $this->nextRecurrence($afterDate);
+        while (is_object($next)) {
+            if (!$this->hasException($next->year, $next->month, $next->mday) &&
+                !$this->hasCompletion($next->year, $next->month, $next->mday)) {
+                return $next;
+            }
+            $next->mday++;
+            $next = $this->nextRecurrence($next);
+        }
+
+        return false;
+    }
+
+    /**
+     * Adds an exception to a recurring event.
+     *
+     * @param integer $year   The year of the execption.
+     * @param integer $month  The month of the execption.
+     * @param integer $mday   The day of the month of the exception.
+     */
+    public function addException($year, $month, $mday)
+    {
+        $this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+    }
+
+    /**
+     * Deletes an exception from a recurring event.
+     *
+     * @param integer $year   The year of the execption.
+     * @param integer $month  The month of the execption.
+     * @param integer $mday   The day of the month of the exception.
+     */
+    public function deleteException($year, $month, $mday)
+    {
+        $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions);
+        if ($key !== false) {
+            unset($this->exceptions[$key]);
+        }
+    }
+
+    /**
+     * Checks if an exception exists for a given reccurence of an event.
+     *
+     * @param integer $year   The year of the reucrance.
+     * @param integer $month  The month of the reucrance.
+     * @param integer $mday   The day of the month of the reucrance.
+     *
+     * @return boolean  True if an exception exists for the given date.
+     */
+    public function hasException($year, $month, $mday)
+    {
+        return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
+                        $this->getExceptions());
+    }
+
+    /**
+     * Retrieves all the exceptions for this event.
+     *
+     * @return array  Array containing the dates of all the exceptions in
+     *                YYYYMMDD form.
+     */
+    public function getExceptions()
+    {
+        return $this->exceptions;
+    }
+
+    /**
+     * Adds a completion to a recurring event.
+     *
+     * @param integer $year   The year of the execption.
+     * @param integer $month  The month of the execption.
+     * @param integer $mday   The day of the month of the completion.
+     */
+    public function addCompletion($year, $month, $mday)
+    {
+        $this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+    }
+
+    /**
+     * Deletes a completion from a recurring event.
+     *
+     * @param integer $year   The year of the execption.
+     * @param integer $month  The month of the execption.
+     * @param integer $mday   The day of the month of the completion.
+     */
+    public function deleteCompletion($year, $month, $mday)
+    {
+        $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions);
+        if ($key !== false) {
+            unset($this->completions[$key]);
+        }
+    }
+
+    /**
+     * Checks if a completion exists for a given reccurence of an event.
+     *
+     * @param integer $year   The year of the reucrance.
+     * @param integer $month  The month of the recurrance.
+     * @param integer $mday   The day of the month of the recurrance.
+     *
+     * @return boolean  True if a completion exists for the given date.
+     */
+    public function hasCompletion($year, $month, $mday)
+    {
+        return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
+                        $this->getCompletions());
+    }
+
+    /**
+     * Retrieves all the completions for this event.
+     *
+     * @return array  Array containing the dates of all the completions in
+     *                YYYYMMDD form.
+     */
+    public function getCompletions()
+    {
+        return $this->completions;
+    }
+
+    /**
+     * Parses a vCalendar 1.0 recurrence rule.
+     *
+     * @link http://www.imc.org/pdi/vcal-10.txt
+     * @link http://www.shuchow.com/vCalAddendum.html
+     *
+     * @param string $rrule  A vCalendar 1.0 conform RRULE value.
+     */
+    public function fromRRule10($rrule)
+    {
+        $this->reset();
+
+        if (!$rrule) {
+            return;
+        }
+
+        if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) {
+            // No recurrence data - event does not recur.
+            $this->setRecurType(self::RECUR_NONE);
+        }
+
+        // Always default the recurInterval to 1.
+        $this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1);
+
+        $remainder = trim($matches[3]);
+
+        switch ($matches[1]) {
+        case 'D':
+            $this->setRecurType(self::RECUR_DAILY);
+            break;
+
+        case 'W':
+            $this->setRecurType(self::RECUR_WEEKLY);
+            if (!empty($remainder)) {
+                $mask = 0;
+                while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
+                    $day = trim($matches[0]);
+                    $remainder = substr($remainder, strlen($matches[0]));
+                    $mask |= $maskdays[$day];
+                }
+                $this->setRecurOnDay($mask);
+            } else {
+                // Recur on the day of the week of the original recurrence.
+                $maskdays = array(
+                    Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
+                    Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
+                    Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
+                    Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
+                    Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
+                    Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
+                    Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY,
+                );
+                $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
+            }
+            break;
+
+        case 'MP':
+            $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+            break;
+
+        case 'MD':
+            $this->setRecurType(self::RECUR_MONTHLY_DATE);
+            break;
+
+        case 'YM':
+            $this->setRecurType(self::RECUR_YEARLY_DATE);
+            break;
+
+        case 'YD':
+            $this->setRecurType(self::RECUR_YEARLY_DAY);
+            break;
+        }
+
+        // We don't support modifiers at the moment, strip them.
+        while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) {
+               $remainder = substr($remainder, 1);
+        }
+        if (!empty($remainder)) {
+            if (strpos($remainder, '#') === 0) {
+                $this->setRecurCount(substr($remainder, 1));
+            } else {
+                list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d');
+                $this->setRecurEnd(new Horde_Date(array('year' => $year,
+                                                        'month' => $month,
+                                                        'mday' => $mday,
+                                                        'hour' => 23,
+                                                        'min' => 59,
+                                                        'sec' => 59)));
+            }
+        }
+    }
+
+    /**
+     * Creates a vCalendar 1.0 recurrence rule.
+     *
+     * @link http://www.imc.org/pdi/vcal-10.txt
+     * @link http://www.shuchow.com/vCalAddendum.html
+     *
+     * @param Horde_Icalendar $calendar  A Horde_Icalendar object instance.
+     *
+     * @return string  A vCalendar 1.0 conform RRULE value.
+     */
+    public function toRRule10($calendar)
+    {
+        switch ($this->recurType) {
+        case self::RECUR_NONE:
+            return '';
+
+        case self::RECUR_DAILY:
+            $rrule = 'D' . $this->recurInterval;
+            break;
+
+        case self::RECUR_WEEKLY:
+            $rrule = 'W' . $this->recurInterval;
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+
+            for ($i = 0; $i <= 7; ++$i) {
+                if ($this->recurOnDay(pow(2, $i))) {
+                    $rrule .= ' ' . $vcaldays[$i];
+                }
+            }
+            break;
+
+        case self::RECUR_MONTHLY_DATE:
+            $rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday);
+            break;
+
+        case self::RECUR_MONTHLY_WEEKDAY:
+            $nth_weekday = (int)($this->start->mday / 7);
+            if (($this->start->mday % 7) > 0) {
+                $nth_weekday++;
+            }
+
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+            $rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()];
+
+            break;
+
+        case self::RECUR_YEARLY_DATE:
+            $rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month);
+            break;
+
+        case self::RECUR_YEARLY_DAY:
+            $rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear();
+            break;
+
+        default:
+            return '';
+        }
+
+        if ($this->hasRecurEnd()) {
+            $recurEnd = clone $this->recurEnd;
+            return $rrule . ' ' . $calendar->_exportDateTime($recurEnd);
+        }
+
+        return $rrule . ' #' . (int)$this->getRecurCount();
+    }
+
+    /**
+     * Parses an iCalendar 2.0 recurrence rule.
+     *
+     * @link http://rfc.net/rfc2445.html#s4.3.10
+     * @link http://rfc.net/rfc2445.html#s4.8.5
+     * @link http://www.shuchow.com/vCalAddendum.html
+     *
+     * @param string $rrule  An iCalendar 2.0 conform RRULE value.
+     */
+    public function fromRRule20($rrule)
+    {
+        $this->reset();
+
+        // Parse the recurrence rule into keys and values.
+        $rdata = array();
+        $parts = explode(';', $rrule);
+        foreach ($parts as $part) {
+            list($key, $value) = explode('=', $part, 2);
+            $rdata[strtoupper($key)] = $value;
+        }
+
+        if (isset($rdata['FREQ'])) {
+            // Always default the recurInterval to 1.
+            $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
+
+            $maskdays = array(
+                'SU' => Horde_Date::MASK_SUNDAY,
+                'MO' => Horde_Date::MASK_MONDAY,
+                'TU' => Horde_Date::MASK_TUESDAY,
+                'WE' => Horde_Date::MASK_WEDNESDAY,
+                'TH' => Horde_Date::MASK_THURSDAY,
+                'FR' => Horde_Date::MASK_FRIDAY,
+                'SA' => Horde_Date::MASK_SATURDAY,
+            );
+
+            switch (strtoupper($rdata['FREQ'])) {
+            case 'DAILY':
+                $this->setRecurType(self::RECUR_DAILY);
+                break;
+
+            case 'WEEKLY':
+                $this->setRecurType(self::RECUR_WEEKLY);
+                if (isset($rdata['BYDAY'])) {
+                    $days = explode(',', $rdata['BYDAY']);
+                    $mask = 0;
+                    foreach ($days as $day) {
+                        $mask |= $maskdays[$day];
+                    }
+                    $this->setRecurOnDay($mask);
+                } else {
+                    // Recur on the day of the week of the original
+                    // recurrence.
+                    $maskdays = array(
+                        Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
+                        Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
+                        Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
+                        Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
+                        Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
+                        Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
+                        Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY);
+                    $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
+                }
+                break;
+
+            case 'MONTHLY':
+                if (isset($rdata['BYDAY'])) {
+                    $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+                    if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
+                        $this->setRecurOnDay($maskdays[$m[2]]);
+                        $this->setRecurNthWeekday($m[1]);
+                    }
+                } else {
+                    $this->setRecurType(self::RECUR_MONTHLY_DATE);
+                }
+                break;
+
+            case 'YEARLY':
+                if (isset($rdata['BYYEARDAY'])) {
+                    $this->setRecurType(self::RECUR_YEARLY_DAY);
+                } elseif (isset($rdata['BYDAY'])) {
+                    $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
+                    if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
+                        $this->setRecurOnDay($maskdays[$m[2]]);
+                        $this->setRecurNthWeekday($m[1]);
+                    }
+                    if ($rdata['BYMONTH']) {
+                        $months = explode(',', $rdata['BYMONTH']);
+                        $this->setRecurByMonth($months);
+                    }
+                } else {
+                    $this->setRecurType(self::RECUR_YEARLY_DATE);
+                }
+                break;
+            }
+
+            if (isset($rdata['UNTIL'])) {
+                list($year, $month, $mday) = sscanf($rdata['UNTIL'],
+                                                    '%04d%02d%02d');
+                $this->setRecurEnd(new Horde_Date(array('year' => $year,
+                                                        'month' => $month,
+                                                        'mday' => $mday,
+                                                        'hour' => 23,
+                                                        'min' => 59,
+                                                        'sec' => 59)));
+            }
+            if (isset($rdata['COUNT'])) {
+                $this->setRecurCount($rdata['COUNT']);
+            }
+        } else {
+            // No recurrence data - event does not recur.
+            $this->setRecurType(self::RECUR_NONE);
+        }
+    }
+
+    /**
+     * Creates an iCalendar 2.0 recurrence rule.
+     *
+     * @link http://rfc.net/rfc2445.html#s4.3.10
+     * @link http://rfc.net/rfc2445.html#s4.8.5
+     * @link http://www.shuchow.com/vCalAddendum.html
+     *
+     * @param Horde_Icalendar $calendar  A Horde_Icalendar object instance.
+     *
+     * @return string  An iCalendar 2.0 conform RRULE value.
+     */
+    public function toRRule20($calendar)
+    {
+        switch ($this->recurType) {
+        case self::RECUR_NONE:
+            return '';
+
+        case self::RECUR_DAILY:
+            $rrule = 'FREQ=DAILY;INTERVAL='  . $this->recurInterval;
+            break;
+
+        case self::RECUR_WEEKLY:
+            $rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval . ';BYDAY=';
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+
+            for ($i = $flag = 0; $i <= 7; ++$i) {
+                if ($this->recurOnDay(pow(2, $i))) {
+                    if ($flag) {
+                        $rrule .= ',';
+                    }
+                    $rrule .= $vcaldays[$i];
+                    $flag = true;
+                }
+            }
+            break;
+
+        case self::RECUR_MONTHLY_DATE:
+            $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval;
+            break;
+
+        case self::RECUR_MONTHLY_WEEKDAY:
+            if (isset($this->recurNthDay)) {
+                $nth_weekday = $this->recurNthDay;
+                $day_of_week = log($this->recurData, 2);
+            } else {
+                $day_of_week = $this->start->dayOfWeek();
+                $nth_weekday = (int)($this->start->mday / 7);
+                if (($this->start->mday % 7) > 0) {
+                    $nth_weekday++;
+                }
+            }
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+            $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
+                . ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week];
+            break;
+
+        case self::RECUR_YEARLY_DATE:
+            $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval;
+            break;
+
+        case self::RECUR_YEARLY_DAY:
+            $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
+                . ';BYYEARDAY=' . $this->start->dayOfYear();
+            break;
+
+        case self::RECUR_YEARLY_WEEKDAY:
+            if (isset($this->recurNthDay)) {
+                $nth_weekday = $this->recurNthDay;
+                $day_of_week = log($this->recurData, 2);
+            } else {
+                $day_of_week = $this->start->dayOfWeek();
+                $nth_weekday = (int)($this->start->mday / 7);
+                if (($this->start->mday % 7) > 0) {
+                    $nth_weekday++;
+                }
+             }
+            $months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month;
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+            $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
+                . ';BYDAY='
+                . $nth_weekday
+                . $vcaldays[$day_of_week]
+                . ';BYMONTH=' . $this->start->month;
+            break;
+        }
+
+        if ($this->hasRecurEnd()) {
+            $recurEnd = clone $this->recurEnd;
+            $rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd);
+        }
+        if ($count = $this->getRecurCount()) {
+            $rrule .= ';COUNT=' . $count;
+        }
+        return $rrule;
+    }
+
+    /**
+     * Parses the recurrence data from a hash.
+     *
+     * @param array $hash  The hash to convert.
+     *
+     * @return boolean  True if the hash seemed valid, false otherwise.
+     */
+    public function fromHash($hash)
+    {
+        $this->reset();
+
+        if (!isset($hash['interval']) || !isset($hash['cycle'])) {
+            $this->setRecurType(self::RECUR_NONE);
+            return false;
+        }
+
+        $this->setRecurInterval((int)$hash['interval']);
+
+        $month2number = array(
+            'january'   => 1,
+            'february'  => 2,
+            'march'     => 3,
+            'april'     => 4,
+            'may'       => 5,
+            'june'      => 6,
+            'july'      => 7,
+            'august'    => 8,
+            'september' => 9,
+            'october'   => 10,
+            'november'  => 11,
+            'december'  => 12,
+        );
+
+        $parse_day = false;
+        $set_daymask = false;
+        $update_month = false;
+        $update_daynumber = false;
+        $update_weekday = false;
+        $nth_weekday = -1;
+
+        switch ($hash['cycle']) {
+        case 'daily':
+            $this->setRecurType(self::RECUR_DAILY);
+            break;
+
+        case 'weekly':
+            $this->setRecurType(self::RECUR_WEEKLY);
+            $parse_day = true;
+            $set_daymask = true;
+            break;
+
+        case 'monthly':
+            if (!isset($hash['daynumber'])) {
+                $this->setRecurType(self::RECUR_NONE);
+                return false;
+            }
+
+            switch ($hash['type']) {
+            case 'daynumber':
+                $this->setRecurType(self::RECUR_MONTHLY_DATE);
+                $update_daynumber = true;
+                break;
+
+            case 'weekday':
+                $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+                $this->setRecurNthWeekday($hash['daynumber']);
+                $parse_day = true;
+                $set_daymask = true;
+                break;
+            }
+            break;
+
+        case 'yearly':
+            if (!isset($hash['type'])) {
+                $this->setRecurType(self::RECUR_NONE);
+                return false;
+            }
+
+            switch ($hash['type']) {
+            case 'monthday':
+                $this->setRecurType(self::RECUR_YEARLY_DATE);
+                $update_month = true;
+                $update_daynumber = true;
+                break;
+
+            case 'yearday':
+                if (!isset($hash['month'])) {
+                    $this->setRecurType(self::RECUR_NONE);
+                    return false;
+                }
+
+                $this->setRecurType(self::RECUR_YEARLY_DAY);
+                // Start counting days in January.
+                $hash['month'] = 'january';
+                $update_month = true;
+                $update_daynumber = true;
+                break;
+
+            case 'weekday':
+                if (!isset($hash['daynumber'])) {
+                    $this->setRecurType(self::RECUR_NONE);
+                    return false;
+                }
+
+                $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
+                $this->setRecurNthWeekday($hash['daynumber']);
+                $parse_day = true;
+                $set_daymask = true;
+
+                if ($hash['month'] && isset($month2number[$hash['month']])) {
+                    $this->setRecurByMonth($month2number[$hash['month']]);
+                }
+                break;
+            }
+        }
+
+        if (isset($hash['range-type']) && isset($hash['range'])) {
+            switch ($hash['range-type']) {
+            case 'number':
+                $this->setRecurCount((int)$hash['range']);
+                break;
+
+            case 'date':
+                $recur_end = new Horde_Date($hash['range']);
+                $recur_end->hour = 23;
+                $recur_end->min = 59;
+                $recur_end->sec = 59;
+                $this->setRecurEnd($recur_end);
+                break;
+            }
+        }
+
+        // Need to parse <day>?
+        $last_found_day = -1;
+        if ($parse_day) {
+            if (!isset($hash['day'])) {
+                $this->setRecurType(self::RECUR_NONE);
+                return false;
+            }
+
+            $mask = 0;
+            $bits = array(
+                'monday' => Horde_Date::MASK_MONDAY,
+                'tuesday' => Horde_Date::MASK_TUESDAY,
+                'wednesday' => Horde_Date::MASK_WEDNESDAY,
+                'thursday' => Horde_Date::MASK_THURSDAY,
+                'friday' => Horde_Date::MASK_FRIDAY,
+                'saturday' => Horde_Date::MASK_SATURDAY,
+                'sunday' => Horde_Date::MASK_SUNDAY,
+            );
+            $days = array(
+                'monday' => Horde_Date::DATE_MONDAY,
+                'tuesday' => Horde_Date::DATE_TUESDAY,
+                'wednesday' => Horde_Date::DATE_WEDNESDAY,
+                'thursday' => Horde_Date::DATE_THURSDAY,
+                'friday' => Horde_Date::DATE_FRIDAY,
+                'saturday' => Horde_Date::DATE_SATURDAY,
+                'sunday' => Horde_Date::DATE_SUNDAY,
+            );
+
+            foreach ($hash['day'] as $day) {
+                // Validity check.
+                if (empty($day) || !isset($bits[$day])) {
+                    continue;
+                }
+
+                $mask |= $bits[$day];
+                $last_found_day = $days[$day];
+            }
+
+            if ($set_daymask) {
+                $this->setRecurOnDay($mask);
+            }
+        }
+
+        if ($update_month || $update_daynumber || $update_weekday) {
+            if ($update_month) {
+                if (isset($month2number[$hash['month']])) {
+                    $this->start->month = $month2number[$hash['month']];
+                }
+            }
+
+            if ($update_daynumber) {
+                if (!isset($hash['daynumber'])) {
+                    $this->setRecurType(self::RECUR_NONE);
+                    return false;
+                }
+
+                $this->start->mday = $hash['daynumber'];
+            }
+
+            if ($update_weekday) {
+                $this->setNthWeekday($nth_weekday);
+            }
+        }
+
+        // Exceptions.
+        if (isset($hash['exceptions'])) {
+            $this->exceptions = $hash['exceptions'];
+        }
+
+        if (isset($hash['completions'])) {
+            $this->completions = $hash['completions'];
+        }
+
+        return true;
+    }
+
+    /**
+     * Export this object into a hash.
+     *
+     * @return array  The recurrence hash.
+     */
+    public function toHash()
+    {
+        if ($this->getRecurType() == self::RECUR_NONE) {
+            return array();
+        }
+
+        $day2number = array(
+            0 => 'sunday',
+            1 => 'monday',
+            2 => 'tuesday',
+            3 => 'wednesday',
+            4 => 'thursday',
+            5 => 'friday',
+            6 => 'saturday'
+        );
+        $month2number = array(
+            1 => 'january',
+            2 => 'february',
+            3 => 'march',
+            4 => 'april',
+            5 => 'may',
+            6 => 'june',
+            7 => 'july',
+            8 => 'august',
+            9 => 'september',
+            10 => 'october',
+            11 => 'november',
+            12 => 'december'
+        );
+
+        $hash = array('interval' => $this->getRecurInterval());
+        $start = $this->getRecurStart();
+
+        switch ($this->getRecurType()) {
+        case self::RECUR_DAILY:
+            $hash['cycle'] = 'daily';
+            break;
+
+        case self::RECUR_WEEKLY:
+            $hash['cycle'] = 'weekly';
+            $bits = array(
+                'monday' => Horde_Date::MASK_MONDAY,
+                'tuesday' => Horde_Date::MASK_TUESDAY,
+                'wednesday' => Horde_Date::MASK_WEDNESDAY,
+                'thursday' => Horde_Date::MASK_THURSDAY,
+                'friday' => Horde_Date::MASK_FRIDAY,
+                'saturday' => Horde_Date::MASK_SATURDAY,
+                'sunday' => Horde_Date::MASK_SUNDAY,
+            );
+            $days = array();
+            foreach ($bits as $name => $bit) {
+                if ($this->recurOnDay($bit)) {
+                    $days[] = $name;
+                }
+            }
+            $hash['day'] = $days;
+            break;
+
+        case self::RECUR_MONTHLY_DATE:
+            $hash['cycle'] = 'monthly';
+            $hash['type'] = 'daynumber';
+            $hash['daynumber'] = $start->mday;
+            break;
+
+        case self::RECUR_MONTHLY_WEEKDAY:
+            $hash['cycle'] = 'monthly';
+            $hash['type'] = 'weekday';
+            $hash['daynumber'] = $start->weekOfMonth();
+            $hash['day'] = array ($day2number[$start->dayOfWeek()]);
+            break;
+
+        case self::RECUR_YEARLY_DATE:
+            $hash['cycle'] = 'yearly';
+            $hash['type'] = 'monthday';
+            $hash['daynumber'] = $start->mday;
+            $hash['month'] = $month2number[$start->month];
+            break;
+
+        case self::RECUR_YEARLY_DAY:
+            $hash['cycle'] = 'yearly';
+            $hash['type'] = 'yearday';
+            $hash['daynumber'] = $start->dayOfYear();
+            break;
+
+        case self::RECUR_YEARLY_WEEKDAY:
+            $hash['cycle'] = 'yearly';
+            $hash['type'] = 'weekday';
+            $hash['daynumber'] = $start->weekOfMonth();
+            $hash['day'] = array ($day2number[$start->dayOfWeek()]);
+            $hash['month'] = $month2number[$start->month];
+        }
+
+        if ($this->hasRecurCount()) {
+            $hash['range-type'] = 'number';
+            $hash['range'] = $this->getRecurCount();
+        } elseif ($this->hasRecurEnd()) {
+            $date = $this->getRecurEnd();
+            $hash['range-type'] = 'date';
+            $hash['range'] = $date->datestamp();
+        } else {
+            $hash['range-type'] = 'none';
+            $hash['range'] = '';
+        }
+
+        // Recurrence exceptions
+        $hash['exceptions'] = $this->exceptions;
+        $hash['completions'] = $this->completions;
+
+        return $hash;
+    }
+
+    /**
+     * Returns a simple object suitable for json transport representing this
+     * object.
+     *
+     * Possible properties are:
+     * - t: type
+     * - i: interval
+     * - e: end date
+     * - c: count
+     * - d: data
+     * - co: completions
+     * - ex: exceptions
+     *
+     * @return object  A simple object.
+     */
+    public function toJson()
+    {
+        $json = new stdClass;
+        $json->t = $this->recurType;
+        $json->i = $this->recurInterval;
+        if ($this->hasRecurEnd()) {
+            $json->e = $this->recurEnd->toJson();
+        }
+        if ($this->recurCount) {
+            $json->c = $this->recurCount;
+        }
+        if ($this->recurData) {
+            $json->d = $this->recurData;
+        }
+        if ($this->completions) {
+            $json->co = $this->completions;
+        }
+        if ($this->exceptions) {
+            $json->ex = $this->exceptions;
+        }
+        return $json;
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/Horde_Kolab_Format_XML_configuration.php b/lib/plugins/libkolab/lib/Horde_Kolab_Format_XML_configuration.php
new file mode 100644
index 0000000..c80fbd3
--- /dev/null
+++ b/lib/plugins/libkolab/lib/Horde_Kolab_Format_XML_configuration.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * Kolab XML handler for configuration (KEP:9).
+ *
+ * @author  Aleksander Machniak <machniak at kolabsys.com>
+ *
+ * Copyright (C) 2011, Kolab Systems AG <contact at 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 Horde_Kolab_Format_XML_configuration extends Horde_Kolab_Format_XML {
+    /**
+     * Specific data fields for the configuration object
+     *
+     * @var Kolab
+     */
+    var $_fields_specific;
+
+    var $_root_version = 2.1;
+
+    /**
+     * Constructor
+     */
+    function Horde_Kolab_Format_XML_configuration($params = array())
+    {
+        $this->_root_name = 'configuration';
+
+        // Specific configuration fields, in kolab format specification order
+        $this->_fields_specific = array(
+            'application' => array (
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_MAYBE_MISSING,
+            ),
+            'type' => array(
+                'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
+            ),
+        );
+
+        // Dictionary fields
+        if (!empty($params['subtype']) && preg_match('/^dictionary.*/', $params['subtype'])) {
+            $this->_fields_specific = array_merge($this->_fields_specific, array(
+                'language' => array (
+                    'type'    => HORDE_KOLAB_XML_TYPE_STRING,
+                    'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
+                ),
+                'e' => array(
+                    'type'    => HORDE_KOLAB_XML_TYPE_MULTIPLE,
+                    'value'   => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
+                    'array'   => array(
+                        'type' => HORDE_KOLAB_XML_TYPE_STRING,
+                        'value' => HORDE_KOLAB_XML_VALUE_NOT_EMPTY,
+                    ),
+                ),
+            ));
+        }
+
+        parent::Horde_Kolab_Format_XML($params);
+
+        unset($this->_fields_basic['body']);
+        unset($this->_fields_basic['categories']);
+        unset($this->_fields_basic['sensitivity']);
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_date_recurrence.php b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
new file mode 100644
index 0000000..427f62a
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * Recurrence computation class for xcal-based Kolab format objects
+ *
+ * Uitility class to compute instances of recurring events.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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_date_recurrence
+{
+    private $engine;
+    private $object;
+    private $next;
+    private $duration;
+    private $tz_offset = 0;
+    private $dst_start = 0;
+    private $allday = false;
+    private $hour = 0;
+
+    /**
+     * Default constructor
+     *
+     * @param array The Kolab object to operate on
+     */
+    function __construct($object)
+    {
+        $this->object = $object;
+        $this->next = new Horde_Date($object['start'], kolab_format::$timezone->getName());
+
+        if (is_object($object['start']) && is_object($object['end']))
+            $this->duration = $object['start']->diff($object['end']);
+        else
+            $this->duration = new DateInterval('PT' . ($object['end'] - $object['start']) . 'S');
+
+        // use (copied) Horde classes to compute recurring instances
+        // TODO: replace with something that has less than 6'000 lines of code
+        $this->engine = new Horde_Date_Recurrence($this->next);
+        $this->engine->fromRRule20($this->to_rrule($object['recurrence']));  // TODO: get that string directly from libkolabxml
+
+        foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
+            $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
+
+        $now = new DateTime('now', kolab_format::$timezone);
+        $this->tz_offset = $object['allday'] ? $now->getOffset() - date('Z') : 0;
+        $this->dst_start = $this->next->format('I');
+        $this->allday = $object['allday'];
+        $this->hour = $this->next->hour;
+    }
+
+    /**
+     * Get date/time of the next occurence of this event
+     *
+     * @param boolean Return a Unix timestamp instead of a DateTime object
+     * @return mixed  DateTime object/unix timestamp or False if recurrence ended
+     */
+    public function next_start($timestamp = false)
+    {
+        $time = false;
+        if ($this->next && ($next = $this->engine->nextActiveRecurrence(array('year' => $this->next->year, 'month' => $this->next->month, 'mday' => $this->next->mday + 1, 'hour' => $this->next->hour, 'min' => $this->next->min, 'sec' => $this->next->sec)))) {
+            if ($this->allday) {
+                $next->hour = $this->hour;  # fix time for all-day events
+                $next->min = 0;
+            }
+            if ($timestamp) {
+                # consider difference in daylight saving between base event and recurring instance
+                $dst_diff = ($this->dst_start - $next->format('I')) * 3600;
+                $time = $next->timestamp() - $this->tz_offset - $dst_diff;
+            }
+            else {
+                $time = $next->toDateTime();
+            }
+            $this->next = $next;
+        }
+
+        return $time;
+    }
+
+    /**
+     * Get the next recurring instance of this event
+     *
+     * @return mixed Array with event properties or False if recurrence ended
+     */
+    public function next_instance()
+    {
+        if ($next_start = $this->next_start()) {
+            $next_end = clone $next_start;
+            $next_end->add($this->duration);
+
+            $next = $this->object;
+            $next['recurrence_id'] = $next_start->format('Y-m-d');
+            $next['start'] = $next_start;
+            $next['end'] = $next_end;
+            unset($next['_formatobj']);
+
+            return $next;
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the end date of the occurence of this recurrence cycle
+     *
+     * @param string Date limit (where infinite recurrences should abort)
+     * @return mixed Timestamp with end date of the last event or False if recurrence exceeds limit
+     */
+    public function end($limit = 'now +1 year')
+    {
+        if ($this->object['recurrence']['UNTIL'])
+            return $this->object['recurrence']['UNTIL']->format('U');
+
+        $limit_time = strtotime($limit);
+        while ($next_start = $this->next_start(true)) {
+            if ($next_start > $limit_time)
+                break;
+        }
+
+        if ($this->next) {
+            $next_end = $this->next->toDateTime();
+            $next_end->add($this->duration);
+            return $next_end->format('U');
+        }
+
+        return false;
+    }
+
+    /**
+     * Convert the internal structured data into a vcalendar RRULE 2.0 string
+     */
+    private function to_rrule($recurrence)
+    {
+      if (is_string($recurrence))
+          return $recurrence;
+
+        $rrule = '';
+        foreach ((array)$recurrence as $k => $val) {
+            $k = strtoupper($k);
+            switch ($k) {
+            case 'UNTIL':
+                $val = $val->format('Ymd\THis');
+                break;
+            case 'EXDATE':
+                foreach ((array)$val as $i => $ex)
+                    $val[$i] = $ex->format('Ymd\THis');
+                $val = join(',', (array)$val);
+                break;
+            }
+            $rrule .= $k . '=' . $val . ';';
+        }
+
+      return $rrule;
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_format.php b/lib/plugins/libkolab/lib/kolab_format.php
new file mode 100644
index 0000000..23246d3
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_format.php
@@ -0,0 +1,330 @@
+<?php
+
+/**
+ * Kolab format model class wrapping libkolabxml bindings
+ *
+ * Abstract base class for different Kolab groupware objects read from/written
+ * to the new Kolab 3 format using the PHP bindings of libkolabxml.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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/>.
+ */
+
+abstract class kolab_format
+{
+    public static $timezone;
+
+    public /*abstract*/ $CTYPE;
+
+    protected /*abstract*/ $read_func;
+    protected /*abstract*/ $write_func;
+
+    protected $obj;
+    protected $data;
+    protected $xmldata;
+    protected $loaded = false;
+
+    const VERSION = '3.0';
+    const KTYPE_PREFIX = 'application/x-vnd.kolab.';
+
+    /**
+     * Factory method to instantiate a kolab_format object of the given type
+     *
+     * @param string Object type to instantiate
+     * @param string Cached xml data to initialize with
+     * @return object kolab_format
+     */
+    public static function factory($type, $xmldata = null)
+    {
+        if (!isset(self::$timezone))
+            self::$timezone = new DateTimeZone('UTC');
+
+        $type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type);
+        $suffix = preg_replace('/[^a-z]+/', '', $type);
+        $classname = 'kolab_format_' . $suffix;
+        if (class_exists($classname))
+            return new $classname($xmldata);
+
+        return PEAR::raiseError("Failed to load Kolab Format wrapper for type " . $type);
+    }
+
+    /**
+     * Convert the given date/time value into a cDateTime object
+     *
+     * @param mixed         Date/Time value either as unix timestamp, date string or PHP DateTime object
+     * @param DateTimeZone  The timezone the date/time is in. Use global default if Null, local time if False
+     * @param boolean       True of the given date has no time component
+     * @return object       The libkolabxml date/time object
+     */
+    public static function get_datetime($datetime, $tz = null, $dateonly = false)
+    {
+        // use timezone information from datetime of global setting
+        if (!$tz && $tz !== false) {
+            if ($datetime instanceof DateTime)
+                $tz = $datetime->getTimezone();
+            if (!$tz)
+                $tz = self::$timezone;
+        }
+        $result = new cDateTime();
+
+        // got a unix timestamp (in UTC)
+        if (is_numeric($datetime)) {
+            $datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
+            if ($tz) $datetime->setTimezone($tz);
+        }
+        else if (is_string($datetime) && strlen($datetime))
+            $datetime = new DateTime($datetime, $tz ?: null);
+
+        if ($datetime instanceof DateTime) {
+            $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
+
+            if (!$dateonly)
+                $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
+
+            if ($tz && $tz->getName() == 'UTC')
+                $result->setUTC(true);
+            else if ($tz !== false)
+                $result->setTimezone($tz->getName());
+        }
+
+        return $result;
+    }
+
+    /**
+     * Convert the given cDateTime into a PHP DateTime object
+     *
+     * @param object cDateTime  The libkolabxml datetime object
+     * @return object DateTime  PHP datetime instance
+     */
+    public static function php_datetime($cdt)
+    {
+        if (!is_object($cdt) || !$cdt->isValid())
+            return null;
+
+        $d = new DateTime;
+        $d->setTimezone(self::$timezone);
+
+        try {
+            if ($tzs = $cdt->timezone()) {
+                $tz = new DateTimeZone($tzs);
+                $d->setTimezone($tz);
+            }
+            else if ($cdt->isUTC()) {
+                $d->setTimezone(new DateTimeZone('UTC'));
+            }
+        }
+        catch (Exception $e) { }
+
+        $d->setDate($cdt->year(), $cdt->month(), $cdt->day());
+
+        if ($cdt->isDateOnly()) {
+            $d->_dateonly = true;
+            $d->setTime(12, 0, 0);  // set time to noon to avoid timezone troubles
+        }
+        else {
+            $d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
+        }
+
+        return $d;
+    }
+
+    /**
+     * Convert a libkolabxml vector to a PHP array
+     *
+     * @param object vector Object
+     * @return array Indexed array contaning vector elements
+     */
+    public static function vector2array($vec, $max = PHP_INT_MAX)
+    {
+        $arr = array();
+        for ($i=0; $i < $vec->size() && $i < $max; $i++)
+            $arr[] = $vec->get($i);
+        return $arr;
+    }
+
+    /**
+     * Build a libkolabxml vector (string) from a PHP array
+     *
+     * @param array Array with vector elements
+     * @return object vectors
+     */
+    public static function array2vector($arr)
+    {
+        $vec = new vectors;
+        foreach ((array)$arr as $val) {
+            if (strlen($val))
+                $vec->push($val);
+        }
+        return $vec;
+    }
+
+    /**
+     * Parse the X-Kolab-Type header from MIME messages and return the object type in short form
+     *
+     * @param string X-Kolab-Type header value
+     * @return string Kolab object type (contact,event,task,note,etc.)
+     */
+    public static function mime2object_type($x_kolab_type)
+    {
+        return preg_replace('/dictionary.[a-z.]+$/', 'dictionary', substr($x_kolab_type, strlen(self::KTYPE_PREFIX)));
+    }
+
+    /**
+     * Check for format errors after calling kolabformat::write*()
+     *
+     * @return boolean True if there were errors, False if OK
+     */
+    protected function format_errors()
+    {
+        $ret = $log = false;
+        switch (kolabformat::error()) {
+            case kolabformat::NoError:
+                $ret = false;
+                break;
+            case kolabformat::Warning:
+                $ret = false;
+                $log = "Warning";
+                break;
+            default:
+                $ret = true;
+                $log = "Error";
+        }
+
+        if ($log) {
+            rcube::raise_error(array(
+                'code' => 660,
+                'type' => 'php',
+                'file' => __FILE__,
+                'line' => __LINE__,
+                'message' => "kolabformat write $log: " . kolabformat::errorMessage(),
+            ), true);
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Save the last generated UID to the object properties.
+     * Should be called after kolabformat::writeXXXX();
+     */
+    protected function update_uid()
+    {
+        // get generated UID
+        if (!$this->data['uid']) {
+            $this->data['uid'] = kolabformat::getSerializedUID();
+            $this->obj->setUid($this->data['uid']);
+        }
+    }
+
+    /**
+     * Initialize libkolabxml object with cached xml data
+     */
+    protected function init()
+    {
+        if (!$this->loaded) {
+            if ($this->xmldata) {
+                $this->load($this->xmldata);
+                $this->xmldata = null;
+            }
+            $this->loaded = true;
+        }
+    }
+
+    /**
+     * Direct getter for object properties
+     */
+    public function __get($var)
+    {
+        return $this->data[$var];
+    }
+
+    /**
+     * Load Kolab object data from the given XML block
+     *
+     * @param string XML data
+     */
+    public function load($xml)
+    {
+        $this->obj = call_user_func($this->read_func, $xml, false);
+        $this->loaded = !$this->format_errors();
+    }
+
+    /**
+     * Write object data to XML format
+     *
+     * @return string XML data
+     */
+    public function write()
+    {
+        $this->init();
+        $this->xmldata = call_user_func($this->write_func, $this->obj);
+
+        if (!$this->format_errors())
+            $this->update_uid();
+        else
+            $this->xmldata = null;
+
+        return $this->xmldata;
+    }
+
+    /**
+     * Set properties to the kolabformat object
+     *
+     * @param array  Object data as hash array
+     */
+    abstract public function set(&$object);
+
+    /**
+     *
+     */
+    abstract public function is_valid();
+
+    /**
+     * Convert the Kolab object into a hash array data structure
+     *
+     * @return array  Kolab object data as hash array
+     */
+    abstract public function to_array();
+
+    /**
+     * Load object data from Kolab2 format
+     *
+     * @param array Hash array with object properties (produced by Horde Kolab_Format classes)
+     */
+    abstract public function fromkolab2($object);
+
+    /**
+     * Callback for kolab_storage_cache to get object specific tags to cache
+     *
+     * @return array List of tags to save in cache
+     */
+    public function get_tags()
+    {
+        return array();
+    }
+
+    /**
+     * Callback for kolab_storage_cache to get words to index for fulltext search
+     *
+     * @return array List of words to save in cache
+     */
+    public function get_words()
+    {
+        return array();
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_format_configuration.php b/lib/plugins/libkolab/lib/kolab_format_configuration.php
new file mode 100644
index 0000000..974fc45
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_format_configuration.php
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * Kolab Configuration data model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_configuration extends kolab_format
+{
+    public $CTYPE = 'application/x-vnd.kolab.configuration';
+
+    protected $read_func = 'kolabformat::readConfiguration';
+    protected $write_func = 'kolabformat::writeConfiguration';
+
+    private $type_map = array(
+        'dictionary' => Configuration::TypeDictionary,
+        'category' => Configuration::TypeCategoryColor,
+    );
+
+
+    function __construct($xmldata = null)
+    {
+        $this->obj = new Configuration;
+        $this->xmldata = $xmldata;
+    }
+
+    /**
+     * Set properties to the kolabformat object
+     *
+     * @param array  Object data as hash array
+     */
+    public function set(&$object)
+    {
+        $this->init();
+
+        // read type-specific properties
+        switch ($object['type']) {
+        case 'dictionary':
+            $dict = new Dictionary($object['language']);
+            $dict->setEntries(self::array2vector($object['e']));
+            $this->obj = new Configuration($dict);
+            break;
+
+        case 'category':
+            // TODO: implement this
+            $categories = new vectorcategorycolor;
+            $this->obj = new Configuration($categories);
+            break;
+        default:
+            return false;
+        }
+
+        // set some automatic values if missing
+        if (!empty($object['uid']))
+            $this->obj->setUid($object['uid']);
+        if (!empty($object['created']))
+            $this->obj->setCreated(self::get_datetime($object['created']));
+
+        // adjust content-type string
+        $this->CTYPE = 'application/x-vnd.kolab.configuration.' . $object['type'];
+
+        // cache this data
+        $this->data = $object;
+        unset($this->data['_formatobj']);
+    }
+
+    /**
+     *
+     */
+    public function is_valid()
+    {
+        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+    }
+
+    /**
+     * Convert the Configuration object into a hash array data structure
+     *
+     * @return array  Config object data as hash array
+     */
+    public function to_array()
+    {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        $this->init();
+        $type_map = array_flip($this->type_map);
+
+        // read object properties
+        $object = array(
+            'uid'     => $this->obj->uid(),
+            'created' => self::php_datetime($this->obj->created()),
+            'changed' => self::php_datetime($this->obj->lastModified()),
+            'type'    => $type_map[$this->obj->type()],
+        );
+
+        // read type-specific properties
+        switch ($object['type']) {
+        case 'dictionary':
+            $dict = $this->obj->dictionary();
+            $object['language'] = $dict->language();
+            $object['e'] = self::vector2array($dict->entries());
+            break;
+
+        case 'category':
+            // TODO: implement this
+            break;
+        }
+
+        // adjust content-type string
+        if ($object['type'])
+            $this->CTYPE = 'application/x-vnd.kolab.configuration.' . $object['type'];
+
+        $this->data = $object;
+        return $this->data;
+    }
+
+    /**
+     * Load data from old Kolab2 format
+     */
+    public function fromkolab2($record)
+    {
+        $object = array(
+            'uid'     => $record['uid'],
+            'changed' => $record['last-modification-date'],
+        );
+
+        $this->data = $object + $record;
+    }
+
+    /**
+     * Callback for kolab_storage_cache to get object specific tags to cache
+     *
+     * @return array List of tags to save in cache
+     */
+    public function get_tags()
+    {
+        $tags = array();
+
+        if ($this->data['type'] == 'dictionary')
+            $tags = array($this->data['language']);
+
+        return $tags;
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
new file mode 100644
index 0000000..ffef059
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_format_contact.php
@@ -0,0 +1,509 @@
+<?php
+
+/**
+ * Kolab Contact model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_contact extends kolab_format
+{
+    public $CTYPE = 'application/vcard+xml';
+
+    protected $read_func = 'kolabformat::readContact';
+    protected $write_func = 'kolabformat::writeContact';
+
+    public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email');
+
+    public $phonetypes = array(
+        'home'    => Telephone::Home,
+        'work'    => Telephone::Work,
+        'text'    => Telephone::Text,
+        'main'    => Telephone::Voice,
+        'homefax' => Telephone::Fax,
+        'workfax' => Telephone::Fax,
+        'mobile'  => Telephone::Cell,
+        'video'   => Telephone::Video,
+        'pager'   => Telephone::Pager,
+        'car'     => Telephone::Car,
+        'other'   => Telephone::Textphone,
+    );
+
+    public $addresstypes = array(
+        'home' => Address::Home,
+        'work' => Address::Work,
+        'office' => 0,
+    );
+
+    private $gendermap = array(
+        'female' => Contact::Female,
+        'male'   => Contact::Male,
+    );
+
+    private $relatedmap = array(
+        'manager'   => Related::Manager,
+        'assistant' => Related::Assistant,
+        'spouse'    => Related::Spouse,
+        'children'  => Related::Child,
+    );
+
+    // old Kolab 2 format field map
+    private $kolab2_fieldmap = array(
+      // kolab       => roundcube
+      'full-name'    => 'name',
+      'given-name'   => 'firstname',
+      'middle-names' => 'middlename',
+      'last-name'    => 'surname',
+      'prefix'       => 'prefix',
+      'suffix'       => 'suffix',
+      'nick-name'    => 'nickname',
+      'organization' => 'organization',
+      'department'   => 'department',
+      'job-title'    => 'jobtitle',
+      'birthday'     => 'birthday',
+      'anniversary'  => 'anniversary',
+      'phone'        => 'phone',
+      'im-address'   => 'im',
+      'web-page'     => 'website',
+      'profession'   => 'profession',
+      'manager-name' => 'manager',
+      'assistant'    => 'assistant',
+      'spouse-name'  => 'spouse',
+      'children'     => 'children',
+      'body'         => 'notes',
+      'pgp-publickey' => 'pgppublickey',
+      'free-busy-url' => 'freebusyurl',
+      'picture'       => 'photo',
+    );
+    private $kolab2_phonetypes = array(
+        'home1' => 'home',
+        'business1' => 'work',
+        'business2' => 'work',
+        'businessfax' => 'workfax',
+    );
+    private $kolab2_addresstypes = array(
+        'business' => 'work'
+    );
+    private $kolab2_gender = array(0 => 'male', 1 => 'female');
+
+
+    /**
+     * Default constructor
+     */
+    function __construct($xmldata = null)
+    {
+        $this->obj = new Contact;
+        $this->xmldata = $xmldata;
+
+        // complete phone types
+        $this->phonetypes['homefax'] |= Telephone::Home;
+        $this->phonetypes['workfax'] |= Telephone::Work;
+    }
+
+    /**
+     * Set contact properties to the kolabformat object
+     *
+     * @param array  Contact data as hash array
+     */
+    public function set(&$object)
+    {
+        $this->init();
+
+        // set some automatic values if missing
+        if (false && !$this->obj->created()) {
+            if (!empty($object['created']))
+                $object['created'] = new DateTime('now', self::$timezone);
+            $this->obj->setCreated(self::get_datetime($object['created']));
+        }
+
+        if (!empty($object['uid']))
+            $this->obj->setUid($object['uid']);
+
+        $object['changed'] = new DateTime('now', self::$timezone);
+        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
+
+        // do the hard work of setting object values
+        $nc = new NameComponents;
+        $nc->setSurnames(self::array2vector($object['surname']));
+        $nc->setGiven(self::array2vector($object['firstname']));
+        $nc->setAdditional(self::array2vector($object['middlename']));
+        $nc->setPrefixes(self::array2vector($object['prefix']));
+        $nc->setSuffixes(self::array2vector($object['suffix']));
+        $this->obj->setNameComponents($nc);
+        $this->obj->setName($object['name']);
+
+        if (isset($object['nickname']))
+            $this->obj->setNickNames(self::array2vector($object['nickname']));
+        if (isset($object['profession']))
+            $this->obj->setTitles(self::array2vector($object['profession']));
+
+        // organisation related properties (affiliation)
+        $org = new Affiliation;
+        $offices = new vectoraddress;
+        if ($object['organization'])
+            $org->setOrganisation($object['organization']);
+        if ($object['department'])
+            $org->setOrganisationalUnits(self::array2vector($object['department']));
+        if ($object['jobtitle'])
+            $org->setRoles(self::array2vector($object['jobtitle']));
+
+        $rels = new vectorrelated;
+        if ($object['manager']) {
+            foreach ((array)$object['manager'] as $manager)
+                $rels->push(new Related(Related::Text, $manager, Related::Manager));
+        }
+        if ($object['assistant']) {
+            foreach ((array)$object['assistant'] as $assistant)
+                $rels->push(new Related(Related::Text, $assistant, Related::Assistant));
+        }
+        $org->setRelateds($rels);
+
+        // email, im, url
+        $this->obj->setEmailAddresses(self::array2vector($object['email']));
+        $this->obj->setIMaddresses(self::array2vector($object['im']));
+
+        $vurls = new vectorurl;
+        foreach ((array)$object['website'] as $url) {
+            $type = $url['type'] == 'blog' ? Url::Blog : Url::NoType;
+            $vurls->push(new Url($url['url'], $type));
+        }
+        $this->obj->setUrls($vurls);
+
+        // addresses
+        $adrs = new vectoraddress;
+        foreach ((array)$object['address'] as $address) {
+            $adr = new Address;
+            $type = $this->addresstypes[$address['type']];
+            if (isset($type))
+                $adr->setTypes($type);
+            else if ($address['type'])
+                $adr->setLabel($address['type']);
+            if ($address['street'])
+                $adr->setStreet($address['street']);
+            if ($address['locality'])
+                $adr->setLocality($address['locality']);
+            if ($address['code'])
+                $adr->setCode($address['code']);
+            if ($address['region'])
+                $adr->setRegion($address['region']);
+            if ($address['country'])
+                $adr->setCountry($address['country']);
+
+            if ($address['type'] == 'office')
+                $offices->push($adr);
+            else
+                $adrs->push($adr);
+        }
+        $this->obj->setAddresses($adrs);
+        $org->setAddresses($offices);
+
+        // add org affiliation after addresses are set
+        $orgs = new vectoraffiliation;
+        $orgs->push($org);
+        $this->obj->setAffiliations($orgs);
+
+        // telephones
+        $tels = new vectortelephone;
+        foreach ((array)$object['phone'] as $phone) {
+            $tel = new Telephone;
+            if (isset($this->phonetypes[$phone['type']]))
+                $tel->setTypes($this->phonetypes[$phone['type']]);
+            $tel->setNumber($phone['number']);
+            $tels->push($tel);
+        }
+        $this->obj->setTelephones($tels);
+
+        if (isset($object['gender']))
+            $this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
+        if (isset($object['notes']))
+            $this->obj->setNote($object['notes']);
+        if (isset($object['freebusyurl']))
+            $this->obj->setFreeBusyUrl($object['freebusyurl']);
+        if (isset($object['birthday']))
+            $this->obj->setBDay(self::get_datetime($object['birthday'], false, true));
+        if (isset($object['anniversary']))
+            $this->obj->setAnniversary(self::get_datetime($object['anniversary'], false, true));
+
+        if (!empty($object['photo'])) {
+            if ($type = rcube_mime::image_content_type($object['photo']))
+                $this->obj->setPhoto($object['photo'], $type);
+        }
+        else if (isset($object['photo']))
+            $this->obj->setPhoto('','');
+        else if ($this->obj->photoMimetype())  // load saved photo for caching
+            $object['photo'] = $this->obj->photo();
+
+        // spouse and children are relateds
+        $rels = new vectorrelated;
+        if ($object['spouse']) {
+            $rels->push(new Related(Related::Text, $object['spouse'], Related::Spouse));
+        }
+        if ($object['children']) {
+            foreach ((array)$object['children'] as $child)
+                $rels->push(new Related(Related::Text, $child, Related::Child));
+        }
+        $this->obj->setRelateds($rels);
+
+        // insert/replace crypto keys
+        $pgp_index = $pkcs7_index = -1;
+        $keys = $this->obj->keys();
+        for ($i=0; $i < $keys->size(); $i++) {
+            $key = $keys->get($i);
+            if ($pgp_index < 0 && $key->type() == Key::PGP)
+                $pgp_index = $i;
+            else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
+                $pkcs7_index = $i;
+        }
+
+        $pgpkey   = $object['pgppublickey']   ? new Key($object['pgppublickey'], Key::PGP) : new Key();
+        $pkcs7key = $object['pkcs7publickey'] ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
+
+        if ($pgp_index >= 0)
+            $keys->set($pgp_index, $pgpkey);
+        else if (!empty($object['pgppublickey']))
+            $keys->push($pgpkey);
+        if ($pkcs7_index >= 0)
+            $keys->set($pkcs7_index, $pkcs7key);
+        else if (!empty($object['pkcs7publickey']))
+            $keys->push($pkcs7key);
+
+        $this->obj->setKeys($keys);
+
+        // TODO: handle language, gpslocation, etc.
+
+
+        // cache this data
+        $this->data = $object;
+        unset($this->data['_formatobj']);
+    }
+
+    /**
+     *
+     */
+    public function is_valid()
+    {
+        return $this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/);
+    }
+
+    /**
+     * Convert the Contact object into a hash array data structure
+     *
+     * @return array  Contact data as hash array
+     */
+    public function to_array()
+    {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        $this->init();
+
+        // read object properties into local data object
+        $object = array(
+            'uid'       => $this->obj->uid(),
+            'name'      => $this->obj->name(),
+            'changed'   => self::php_datetime($this->obj->lastModified()),
+        );
+
+        $nc = $this->obj->nameComponents();
+        $object['surname']    = join(' ', self::vector2array($nc->surnames()));
+        $object['firstname']  = join(' ', self::vector2array($nc->given()));
+        $object['middlename'] = join(' ', self::vector2array($nc->additional()));
+        $object['prefix']     = join(' ', self::vector2array($nc->prefixes()));
+        $object['suffix']     = join(' ', self::vector2array($nc->suffixes()));
+        $object['nickname']   = join(' ', self::vector2array($this->obj->nickNames()));
+        $object['profession'] = join(' ', self::vector2array($this->obj->titles()));
+
+        // organisation related properties (affiliation)
+        $orgs = $this->obj->affiliations();
+        if ($orgs->size()) {
+            $org = $orgs->get(0);
+            $object['organization']   = $org->organisation();
+            $object['jobtitle']       = join(' ', self::vector2array($org->roles()));
+            $object['department']     = join(' ', self::vector2array($org->organisationalUnits()));
+            $this->read_relateds($org->relateds(), $object);
+        }
+
+        $object['email']   = self::vector2array($this->obj->emailAddresses());
+        $object['im']      = self::vector2array($this->obj->imAddresses());
+
+        $urls = $this->obj->urls();
+        for ($i=0; $i < $urls->size(); $i++) {
+            $url = $urls->get($i);
+            $subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
+            $object['website'][] = array('url' => $url->url(), 'type' => $subtype);
+        }
+
+        // addresses
+        $this->read_addresses($this->obj->addresses(), $object);
+        if ($org && ($offices = $org->addresses()))
+            $this->read_addresses($offices, $object, 'office');
+
+        // telehones
+        $tels = $this->obj->telephones();
+        $teltypes = array_flip($this->phonetypes);
+        for ($i=0; $i < $tels->size(); $i++) {
+            $tel = $tels->get($i);
+            $object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
+        }
+
+        $object['notes'] = $this->obj->note();
+        $object['freebusyurl'] = $this->obj->freeBusyUrl();
+
+        if ($bday = self::php_datetime($this->obj->bDay()))
+            $object['birthday'] = $bday->format('c');
+
+        if ($anniversary = self::php_datetime($this->obj->anniversary()))
+            $object['anniversary'] = $anniversary->format('c');
+
+        $gendermap = array_flip($this->gendermap);
+        if (($g = $this->obj->gender()) && $gendermap[$g])
+            $object['gender'] = $gendermap[$g];
+
+        if ($this->obj->photoMimetype())
+            $object['photo'] = $this->obj->photo();
+
+        // relateds -> spouse, children
+        $this->read_relateds($this->obj->relateds(), $object);
+
+        // crypto settings: currently only key values are supported
+        $keys = $this->obj->keys();
+        for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
+            $key = $keys->get($i);
+            if ($key->type() == Key::PGP)
+                $object['pgppublickey'] = $key->key();
+            else if ($key->type() == Key::PKCS7_MIME)
+                $object['pkcs7publickey'] = $key->key();
+        }
+
+        $this->data = $object;
+        return $this->data;
+    }
+
+    /**
+     * Callback for kolab_storage_cache to get words to index for fulltext search
+     *
+     * @return array List of words to save in cache
+     */
+    public function get_words()
+    {
+        $data = '';
+        foreach (self::$fulltext_cols as $col) {
+            $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
+            if (strlen($val))
+                $data .= $val . ' ';
+        }
+
+        return array_unique(rcube_utils::normalize_string($data, true));
+    }
+
+    /**
+     * Load data from old Kolab2 format
+     *
+     * @param array Hash array with object properties
+     */
+    public function fromkolab2($record)
+    {
+        $object = array(
+          'uid' => $record['uid'],
+          'email' => array(),
+          'phone' => array(),
+        );
+
+        foreach ($this->kolab2_fieldmap as $kolab => $rcube) {
+          if (is_array($record[$kolab]) || strlen($record[$kolab]))
+            $object[$rcube] = $record[$kolab];
+        }
+
+        if (isset($record['gender']))
+            $object['gender'] = $this->kolab2_gender[$record['gender']];
+
+        foreach ((array)$record['email'] as $i => $email)
+            $object['email'][] = $email['smtp-address'];
+
+        if (!$record['email'] && $record['emails'])
+            $object['email'] = preg_split('/,\s*/', $record['emails']);
+
+        if (is_array($record['address'])) {
+            foreach ($record['address'] as $i => $adr) {
+                $object['address'][] = array(
+                    'type' => $this->kolab2_addresstypes[$adr['type']] ? $this->kolab2_addresstypes[$adr['type']] : $adr['type'],
+                    'street' => $adr['street'],
+                    'locality' => $adr['locality'],
+                    'code' => $adr['postal-code'],
+                    'region' => $adr['region'],
+                    'country' => $adr['country'],
+                );
+            }
+        }
+
+        // office location goes into an address block
+        if ($record['office-location'])
+            $object['address'][] = array('type' => 'office', 'locality' => $record['office-location']);
+
+        // merge initials into nickname
+        if ($record['initials'])
+            $object['nickname'] = trim($object['nickname'] . ', ' . $record['initials'], ', ');
+
+        // remove empty fields
+        $this->data = array_filter($object);
+    }
+
+    /**
+     * Helper method to copy contents of an Address vector to the contact data object
+     */
+    private function read_addresses($addresses, &$object, $type = null)
+    {
+        $adrtypes = array_flip($this->addresstypes);
+
+        for ($i=0; $i < $addresses->size(); $i++) {
+            $adr = $addresses->get($i);
+            $object['address'][] = array(
+                'type'     => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
+                'street'   => $adr->street(),
+                'code'     => $adr->code(),
+                'locality' => $adr->locality(),
+                'region'   => $adr->region(),
+                'country'  => $adr->country()
+            );
+        }
+    }
+
+    /**
+     * Helper method to map contents of a Related vector to the contact data object
+     */
+    private function read_relateds($rels, &$object)
+    {
+        $typemap = array_flip($this->relatedmap);
+
+        for ($i=0; $i < $rels->size(); $i++) {
+            $rel = $rels->get($i);
+            if ($rel->type() != Related::Text)  // we can't handle UID relations yet
+                continue;
+
+            $types = $rel->relationTypes();
+            foreach ($typemap as $t => $field) {
+                if ($types & $t) {
+                    $object[$field][] = $rel->text();
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
new file mode 100644
index 0000000..fcb94c1
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -0,0 +1,147 @@
+<?php
+
+/**
+ * Kolab Distribution List model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_distributionlist extends kolab_format
+{
+    public $CTYPE = 'application/vcard+xml';
+
+    protected $read_func = 'kolabformat::readDistlist';
+    protected $write_func = 'kolabformat::writeDistlist';
+
+
+    function __construct($xmldata = null)
+    {
+        $this->obj = new DistList;
+        $this->xmldata = $xmldata;
+    }
+
+    /**
+     * Set properties to the kolabformat object
+     *
+     * @param array  Object data as hash array
+     */
+    public function set(&$object)
+    {
+        $this->init();
+
+        // set some automatic values if missing
+        if (!empty($object['uid']))
+            $this->obj->setUid($object['uid']);
+
+        $object['changed'] = new DateTime('now', self::$timezone);
+        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
+
+        $this->obj->setName($object['name']);
+
+        $seen = array();
+        $members = new vectorcontactref;
+        foreach ((array)$object['member'] as $member) {
+            if ($member['uid'])
+                $m = new ContactReference(ContactReference::UidReference, $member['uid']);
+            else if ($member['email'])
+                $m = new ContactReference(ContactReference::EmailReference, $member['email']);
+            else
+                continue;
+
+            $m->setName($member['name']);
+            $members->push($m);
+            $seen[$member['email']]++;
+        }
+
+        $this->obj->setMembers($members);
+
+        // set type property for proper caching
+        $object['_type'] = 'distribution-list';
+
+        // cache this data
+        $this->data = $object;
+        unset($this->data['_formatobj']);
+    }
+
+    public function is_valid()
+    {
+        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+    }
+
+    /**
+     * Load data from old Kolab2 format
+     */
+    public function fromkolab2($record)
+    {
+        $object = array(
+            'uid'     => $record['uid'],
+            'changed' => $record['last-modification-date'],
+            'name'    => $record['last-name'],
+            'member'  => array(),
+        );
+
+        foreach ((array)$record['member'] as $member) {
+            $object['member'][] = array(
+                'email' => $member['smtp-address'],
+                'name' => $member['display-name'],
+                'uid' => $member['uid'],
+            );
+        }
+
+        $this->data = $object;
+    }
+
+    /**
+     * Convert the Distlist object into a hash array data structure
+     *
+     * @return array  Distribution list data as hash array
+     */
+    public function to_array()
+    {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        $this->init();
+
+        // read object properties
+        $object = array(
+            'uid'       => $this->obj->uid(),
+            'changed'   => self::php_datetime($this->obj->lastModified()),
+            'name'      => $this->obj->name(),
+            'member'    => array(),
+            '_type'     => 'distribution-list',
+        );
+
+        $members = $this->obj->members();
+        for ($i=0; $i < $members->size(); $i++) {
+            $member = $members->get($i);
+#            if ($member->type() == ContactReference::UidReference && ($uid = $member->uid()))
+                $object['member'][] = array(
+                    'uid' => $member->uid(),
+                    'email' => $member->email(),
+                    'name' => $member->name(),
+                );
+        }
+
+        $this->data = $object;
+        return $this->data;
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_format_event.php b/lib/plugins/libkolab/lib/kolab_format_event.php
new file mode 100644
index 0000000..33ed5af
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_format_event.php
@@ -0,0 +1,303 @@
+<?php
+
+/**
+ * Kolab Event model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_event extends kolab_format_xcal
+{
+    protected $read_func = 'kolabformat::readEvent';
+    protected $write_func = 'kolabformat::writeEvent';
+
+    private $kolab2_rolemap = array(
+        'required' => 'REQ-PARTICIPANT',
+        'optional' => 'OPT-PARTICIPANT',
+        'resource' => 'CHAIR',
+    );
+    private $kolab2_statusmap = array(
+        'none'      => 'NEEDS-ACTION',
+        'tentative' => 'TENTATIVE',
+        'accepted'  => 'CONFIRMED',
+        'accepted'  => 'ACCEPTED',
+        'declined'  => 'DECLINED',
+    );
+    private $kolab2_monthmap = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december');
+
+
+    /**
+     * Default constructor
+     */
+    function __construct($xmldata = null)
+    {
+        $this->obj = new Event;
+        $this->xmldata = $xmldata;
+    }
+
+    /**
+     * Set event properties to the kolabformat object
+     *
+     * @param array  Event data as hash array
+     */
+    public function set(&$object)
+    {
+        $this->init();
+
+        // set common xcal properties
+        parent::set($object);
+
+        // do the hard work of setting object values
+        $this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
+        $this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
+        $this->obj->setTransparency($object['free_busy'] == 'free');
+
+        $status = kolabformat::StatusUndefined;
+        if ($object['free_busy'] == 'tentative')
+            $status = kolabformat::StatusTentative;
+        if ($object['cancelled'])
+            $status = kolabformat::StatusCancelled;
+        $this->obj->setStatus($status);
+
+        // save attachments
+        $vattach = new vectorattachment;
+        foreach ((array)$object['_attachments'] as $cid => $attr) {
+            if (empty($attr))
+                continue;
+            $attach = new Attachment;
+            $attach->setLabel((string)$attr['name']);
+            $attach->setUri('cid:' . $cid, $attr['mimetype']);
+            $vattach->push($attach);
+        }
+        $this->obj->setAttachments($vattach);
+
+        // cache this data
+        $this->data = $object;
+        unset($this->data['_formatobj']);
+    }
+
+    /**
+     *
+     */
+    public function is_valid()
+    {
+        return $this->data || (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid());
+    }
+
+    /**
+     * Convert the Event object into a hash array data structure
+     *
+     * @return array  Event data as hash array
+     */
+    public function to_array()
+    {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        $this->init();
+
+        // read common xcal props
+        $object = parent::to_array();
+
+        // read object properties
+        $object += array(
+            'end'         => self::php_datetime($this->obj->end()),
+            'allday'      => $this->obj->start()->isDateOnly(),
+            'free_busy'   => $this->obj->transparency() ? 'free' : 'busy',  // TODO: transparency is only boolean
+            'attendees'   => array(),
+        );
+
+        // organizer is part of the attendees list in Roundcube
+        if ($object['organizer']) {
+            $object['organizer']['role'] = 'ORGANIZER';
+            array_unshift($object['attendees'], $object['organizer']);
+        }
+
+        // status defines different event properties...
+        $status = $this->obj->status();
+        if ($status == kolabformat::StatusTentative)
+          $object['free_busy'] = 'tentative';
+        else if ($status == kolabformat::StatusCancelled)
+          $objec['cancelled'] = true;
+
+        // handle attachments
+        $vattach = $this->obj->attachments();
+        for ($i=0; $i < $vattach->size(); $i++) {
+            $attach = $vattach->get($i);
+
+            // skip cid: attachments which are mime message parts handled by kolab_storage_folder
+            if (substr($attach->uri(), 0, 4) != 'cid') {
+                $name = $attach->label();
+                $data = $attach->data();
+                $object['_attachments'][$name] = array(
+                    'name' => $name,
+                    'mimetype' => $attach->mimetype(),
+                    'size' => strlen($data),
+                    'content' => $data,
+                );
+            }
+        }
+
+        $this->data = $object;
+        return $this->data;
+    }
+
+    /**
+     * Callback for kolab_storage_cache to get object specific tags to cache
+     *
+     * @return array List of tags to save in cache
+     */
+    public function get_tags()
+    {
+        $tags = array();
+
+        foreach ((array)$this->data['categories'] as $cat) {
+            $tags[] = rcube_utils::normalize_string($cat);
+        }
+
+        if (!empty($this->data['alarms'])) {
+            $tags[] = 'x-has-alarms';
+        }
+
+        return $tags;
+    }
+
+    /**
+     * Load data from old Kolab2 format
+     */
+    public function fromkolab2($rec)
+    {
+        if (PEAR::isError($rec))
+            return;
+
+        $start_time = date('H:i:s', $rec['start-date']);
+        $allday = $rec['_is_all_day'] || ($start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']));
+
+        // in Roundcube all-day events go from 12:00 to 13:00
+        if ($allday) {
+            $now = new DateTime('now', self::$timezone);
+            $gmt_offset = $now->getOffset();
+
+            $rec['start-date'] += 12 * 3600;
+            $rec['end-date']   -= 11 * 3600;
+            $rec['end-date']   -= $gmt_offset - date('Z', $rec['end-date']);    // shift times from server's timezone to user's timezone
+            $rec['start-date'] -= $gmt_offset - date('Z', $rec['start-date']);  // because generated with mktime() in Horde_Kolab_Format_Date::decodeDate()
+            // sanity check
+            if ($rec['end-date'] <= $rec['start-date'])
+              $rec['end-date'] += 86400;
+        }
+
+        // convert alarm time into internal format
+        if ($rec['alarm']) {
+            $alarm_value = $rec['alarm'];
+            $alarm_unit = 'M';
+            if ($rec['alarm'] % 1440 == 0) {
+                $alarm_value /= 1440;
+                $alarm_unit = 'D';
+            }
+            else if ($rec['alarm'] % 60 == 0) {
+                $alarm_value /= 60;
+                $alarm_unit = 'H';
+            }
+            $alarm_value *= -1;
+        }
+
+        // convert recurrence rules into internal pseudo-vcalendar format
+        if ($recurrence = $rec['recurrence']) {
+            $rrule = array(
+                'FREQ' => strtoupper($recurrence['cycle']),
+                'INTERVAL' => intval($recurrence['interval']),
+            );
+
+            if ($recurrence['range-type'] == 'number')
+                $rrule['COUNT'] = intval($recurrence['range']);
+            else if ($recurrence['range-type'] == 'date')
+                $rrule['UNTIL'] = date_create('@'.$recurrence['range']);
+
+            if ($recurrence['day']) {
+                $byday = array();
+                $prefix = ($rrule['FREQ'] == 'MONTHLY' || $rrule['FREQ'] == 'YEARLY') ? intval($recurrence['daynumber'] ? $recurrence['daynumber'] : 1) : '';
+                foreach ($recurrence['day'] as $day)
+                    $byday[] = $prefix . substr(strtoupper($day), 0, 2);
+                $rrule['BYDAY'] = join(',', $byday);
+            }
+            if ($recurrence['daynumber']) {
+                if ($recurrence['type'] == 'monthday' || $recurrence['type'] == 'daynumber')
+                    $rrule['BYMONTHDAY'] = $recurrence['daynumber'];
+                else if ($recurrence['type'] == 'yearday')
+                    $rrule['BYYEARDAY'] = $recurrence['daynumber'];
+            }
+            if ($recurrence['month']) {
+                $monthmap = array_flip($this->kolab2_monthmap);
+                $rrule['BYMONTH'] = strtolower($monthmap[$recurrence['month']]);
+            }
+
+            if ($recurrence['exclusion']) {
+                foreach ((array)$recurrence['exclusion'] as $excl)
+                    $rrule['EXDATE'][] = date_create($excl . date(' H:i:s', $rec['start-date']));  // use time of event start
+            }
+        }
+
+        $attendees = array();
+        if ($rec['organizer']) {
+            $attendees[] = array(
+                'role' => 'ORGANIZER',
+                'name' => $rec['organizer']['display-name'],
+                'email' => $rec['organizer']['smtp-address'],
+                'status' => 'ACCEPTED',
+            );
+            $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
+        }
+
+        foreach ((array)$rec['attendee'] as $attendee) {
+            $attendees[] = array(
+                'role' => $this->kolab2_rolemap[$attendee['role']],
+                'name' => $attendee['display-name'],
+                'email' => $attendee['smtp-address'],
+                'status' => $this->kolab2_statusmap[$attendee['status']],
+                'rsvp' => $attendee['request-response'],
+            );
+            $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
+        }
+
+        $this->data = array(
+            'uid' => $rec['uid'],
+            'title' => $rec['summary'],
+            'location' => $rec['location'],
+            'description' => $rec['body'],
+            'start' => new DateTime('@'.$rec['start-date']),
+            'end'   => new DateTime('@'.$rec['end-date']),
+            'allday' => $allday,
+            'recurrence' => $rrule,
+            'alarms' => $alarm_value . $alarm_unit,
+            'categories' => explode(',', $rec['categories']),
+            'attachments' => $attachments,
+            'attendees' => $attendees,
+            'free_busy' => $rec['show-time-as'],
+            'priority' => $rec['priority'],
+            'sensitivity' => $rec['sensitivity'],
+            'changed' => $rec['last-modification-date'],
+        );
+
+        // assign current timezone to event start/end
+        $this->data['start']->setTimezone(self::$timezone);
+        $this->data['end']->setTimezone(self::$timezone);
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_format_journal.php b/lib/plugins/libkolab/lib/kolab_format_journal.php
new file mode 100644
index 0000000..5869af0
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_format_journal.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * Kolab Journal model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_journal extends kolab_format
+{
+    public $CTYPE = 'application/calendar+xml';
+
+    protected $read_func = 'kolabformat::readJournal';
+    protected $write_func = 'kolabformat::writeJournal';
+
+
+    function __construct($xmldata = null)
+    {
+        $this->obj = new Journal;
+        $this->xmldata = $xmldata;
+    }
+
+    /**
+     * Set properties to the kolabformat object
+     *
+     * @param array  Object data as hash array
+     */
+    public function set(&$object)
+    {
+        $this->init();
+
+        // set some automatic values if missing
+        if (!empty($object['uid']))
+            $this->obj->setUid($object['uid']);
+
+        $object['changed'] = new DateTime('now', self::$timezone);
+        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
+
+        // TODO: set object propeties
+
+        // cache this data
+        $this->data = $object;
+        unset($this->data['_formatobj']);
+    }
+
+    /**
+     *
+     */
+    public function is_valid()
+    {
+        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+    }
+
+    /**
+     * Load data from old Kolab2 format
+     */
+    public function fromkolab2($record)
+    {
+        $object = array(
+            'uid'     => $record['uid'],
+            'changed' => $record['last-modification-date'],
+        );
+
+        // TODO: implement this
+
+        $this->data = $object;
+    }
+
+    /**
+     * Convert the Configuration object into a hash array data structure
+     *
+     * @return array  Config object data as hash array
+     */
+    public function to_array()
+    {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        $this->init();
+
+        // read object properties
+        $object = array(
+            'uid'     => $this->obj->uid(),
+            'created' => self::php_datetime($this->obj->created()),
+            'changed' => self::php_datetime($this->obj->lastModified()),
+        );
+
+
+        // TODO: read object properties
+
+        $this->data = $object;
+        return $this->data;
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_format_note.php b/lib/plugins/libkolab/lib/kolab_format_note.php
new file mode 100644
index 0000000..1c88a8b
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_format_note.php
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * Kolab Note model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_note extends kolab_format
+{
+    public $CTYPE = 'application/x-vnd.kolab.note';
+
+    protected $read_func = 'kolabformat::readNote';
+    protected $write_func = 'kolabformat::writeNote';
+
+
+    function __construct($xmldata = null)
+    {
+        $this->obj = new Note;
+        $this->xmldata = $xmldata;
+    }
+
+    /**
+     * Set properties to the kolabformat object
+     *
+     * @param array  Object data as hash array
+     */
+    public function set(&$object)
+    {
+        $this->init();
+
+        // set some automatic values if missing
+        if (!empty($object['uid']))
+            $this->obj->setUid($object['uid']);
+
+        $object['changed'] = new DateTime('now', self::$timezone);
+        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
+
+        // TODO: set object propeties
+
+        // cache this data
+        $this->data = $object;
+        unset($this->data['_formatobj']);
+    }
+
+    /**
+     *
+     */
+    public function is_valid()
+    {
+        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+    }
+
+    /**
+     * Load data from old Kolab2 format
+     */
+    public function fromkolab2($record)
+    {
+        $object = array(
+            'uid'     => $record['uid'],
+            'changed' => $record['last-modification-date'],
+        );
+
+
+        $this->data = $object;
+    }
+
+    /**
+     * Convert the Configuration object into a hash array data structure
+     *
+     * @return array  Config object data as hash array
+     */
+    public function to_array()
+    {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        $this->init();
+
+        // read object properties
+        $object = array(
+            'uid'       => $this->obj->uid(),
+            'created'   => self::php_datetime($this->obj->created()),
+            'changed'   => self::php_datetime($this->obj->lastModified()),
+        );
+
+
+        // TODO: read object properties
+
+        $this->data = $object;
+        return $this->data;
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_format_task.php b/lib/plugins/libkolab/lib/kolab_format_task.php
new file mode 100644
index 0000000..2a7a629
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_format_task.php
@@ -0,0 +1,145 @@
+<?php
+
+/**
+ * Kolab Task (ToDo) model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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_format_task extends kolab_format_xcal
+{
+    protected $read_func = 'kolabformat::readTodo';
+    protected $write_func = 'kolabformat::writeTodo';
+
+
+    function __construct($xmldata = null)
+    {
+        $this->obj = new Todo;
+        $this->xmldata = $xmldata;
+    }
+
+    /**
+     * Set properties to the kolabformat object
+     *
+     * @param array  Object data as hash array
+     */
+    public function set(&$object)
+    {
+        $this->init();
+
+        // set common xcal properties
+        parent::set($object);
+
+        $this->obj->setPercentComplete(intval($object['complete']));
+
+        if (isset($object['start']))
+            $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
+
+        $this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
+
+        $related = new vectors;
+        if (!empty($object['parent_id']))
+            $related->push($object['parent_id']);
+        $this->obj->setRelatedTo($related);
+
+        // cache this data
+        $this->data = $object;
+        unset($this->data['_formatobj']);
+    }
+
+    /**
+     *
+     */
+    public function is_valid()
+    {
+        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+    }
+
+    /**
+     * Convert the Configuration object into a hash array data structure
+     *
+     * @return array  Config object data as hash array
+     */
+    public function to_array()
+    {
+        // return cached result
+        if (!empty($this->data))
+            return $this->data;
+
+        $this->init();
+
+        // read common xcal props
+        $object = parent::to_array();
+
+        $object['complete'] = intval($this->obj->percentComplete());
+
+        // if due date is set
+        if ($due = $this->obj->due())
+            $object['due'] = self::php_datetime($due);
+
+        // related-to points to parent task; we only support one relation
+        $related = self::vector2array($this->obj->relatedTo());
+        if (count($related))
+            $object['parent_id'] = $related[0];
+
+        // TODO: map more properties
+
+        $this->data = $object;
+        return $this->data;
+    }
+
+    /**
+     * Load data from old Kolab2 format
+     */
+    public function fromkolab2($record)
+    {
+        $object = array(
+            'uid'     => $record['uid'],
+            'changed' => $record['last-modification-date'],
+        );
+
+        // TODO: implement this
+
+        $this->data = $object;
+    }
+
+    /**
+     * Callback for kolab_storage_cache to get object specific tags to cache
+     *
+     * @return array List of tags to save in cache
+     */
+    public function get_tags()
+    {
+        $tags = array();
+
+        if ($this->data['status'] == 'COMPLETED' || $this->data['complete'] == 100)
+            $tags[] = 'x-complete';
+
+        if ($this->data['priority'] == 1)
+            $tags[] = 'x-flagged';
+
+        if (!empty($this->data['alarms']))
+            $tags[] = 'x-has-alarms';
+
+        if ($this->data['parent_id'])
+            $tags[] = 'x-parent:' . $this->data['parent_id'];
+
+        return $tags;
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_format_xcal.php b/lib/plugins/libkolab/lib/kolab_format_xcal.php
new file mode 100644
index 0000000..1191df5
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -0,0 +1,396 @@
+<?php
+
+/**
+ * Xcal based Kolab format class wrapping libkolabxml bindings
+ *
+ * Base class for xcal-based Kolab groupware objects such as event, todo, journal
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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/>.
+ */
+
+abstract class kolab_format_xcal extends kolab_format
+{
+    public $CTYPE = 'application/calendar+xml';
+
+    public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
+
+    protected $sensitivity_map = array(
+        'public'       => kolabformat::ClassPublic,
+        'private'      => kolabformat::ClassPrivate,
+        'confidential' => kolabformat::ClassConfidential,
+    );
+
+    protected $role_map = array(
+        'REQ-PARTICIPANT' => kolabformat::Required,
+        'OPT-PARTICIPANT' => kolabformat::Optional,
+        'NON-PARTICIPANT' => kolabformat::NonParticipant,
+        'CHAIR' => kolabformat::Chair,
+    );
+
+    protected $rrule_type_map = array(
+        'MINUTELY' => RecurrenceRule::Minutely,
+        'HOURLY' => RecurrenceRule::Hourly,
+        'DAILY' => RecurrenceRule::Daily,
+        'WEEKLY' => RecurrenceRule::Weekly,
+        'MONTHLY' => RecurrenceRule::Monthly,
+        'YEARLY' => RecurrenceRule::Yearly,
+    );
+
+    protected $weekday_map = array(
+        'MO' => kolabformat::Monday,
+        'TU' => kolabformat::Tuesday,
+        'WE' => kolabformat::Wednesday,
+        'TH' => kolabformat::Thursday,
+        'FR' => kolabformat::Friday,
+        'SA' => kolabformat::Saturday,
+        'SU' => kolabformat::Sunday,
+    );
+
+    protected $alarm_type_map = array(
+        'DISPLAY' => Alarm::DisplayAlarm,
+        'EMAIL' => Alarm::EMailAlarm,
+        'AUDIO' => Alarm::AudioAlarm,
+    );
+
+    private $status_map = array(
+        'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
+        'IN-PROCESS'   => kolabformat::StatusInProcess,
+        'COMPLETED'    => kolabformat::StatusCompleted,
+        'CANCELLED'    => kolabformat::StatusCancelled,
+    );
+
+    protected $part_status_map = array(
+        'UNKNOWN' => kolabformat::PartNeedsAction,
+        'NEEDS-ACTION' => kolabformat::PartNeedsAction,
+        'TENTATIVE' => kolabformat::PartTentative,
+        'ACCEPTED' => kolabformat::PartAccepted,
+        'DECLINED' => kolabformat::PartDeclined,
+        'DELEGATED' => kolabformat::PartDelegated,
+      );
+
+
+    /**
+     * Convert common xcard properties into a hash array data structure
+     *
+     * @return array  Object data as hash array
+     */
+    public function to_array()
+    {
+        $status_map = array_flip($this->status_map);
+        $sensitivity_map = array_flip($this->sensitivity_map);
+
+        $object = array(
+            'uid'         => $this->obj->uid(),
+            'created'     => self::php_datetime($this->obj->created()),
+            'changed'     => self::php_datetime($this->obj->lastModified()),
+            'title'       => $this->obj->summary(),
+            'location'    => $this->obj->location(),
+            'description' => $this->obj->description(),
+            'status'      => $this->status_map[$this->obj->status()],
+            'sensitivity' => $sensitivity_map[$this->obj->classification()],
+            'priority'    => $this->obj->priority(),
+            'categories'  => self::vector2array($this->obj->categories()),
+            'start'       => self::php_datetime($this->obj->start()),
+        );
+
+        // read organizer and attendees
+        if ($organizer = $this->obj->organizer()) {
+            $object['organizer'] = array(
+                'email' => $organizer->email(),
+                'name' => $organizer->name(),
+            );
+        }
+
+        $role_map = array_flip($this->role_map);
+        $part_status_map = array_flip($this->part_status_map);
+        $attvec = $this->obj->attendees();
+        for ($i=0; $i < $attvec->size(); $i++) {
+            $attendee = $attvec->get($i);
+            $cr = $attendee->contact();
+            $object['attendees'][] = array(
+                'role' => $role_map[$attendee->role()],
+                'status' => $part_status_map[$attendee->partStat()],
+                'rsvp' => $attendee->rsvp(),
+                'email' => $cr->email(),
+                'name' => $cr->name(),
+            );
+        }
+
+        // read recurrence rule
+        if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
+            $rrule_type_map = array_flip($this->rrule_type_map);
+            $object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
+
+            if ($intvl = $rr->interval())
+                $object['recurrence']['INTERVAL'] = $intvl;
+
+            if (($count = $rr->count()) && $count > 0) {
+                $object['recurrence']['COUNT'] = $count;
+            }
+            else if ($until = self::php_datetime($rr->end())) {
+                $until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
+                $object['recurrence']['UNTIL'] = $until;
+            }
+
+            if (($byday = $rr->byday()) && $byday->size()) {
+                $weekday_map = array_flip($this->weekday_map);
+                $weekdays = array();
+                for ($i=0; $i < $byday->size(); $i++) {
+                    $daypos = $byday->get($i);
+                    $prefix = $daypos->occurence();
+                    $weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
+                }
+                $object['recurrence']['BYDAY'] = join(',', $weekdays);
+            }
+
+            if (($bymday = $rr->bymonthday()) && $bymday->size()) {
+                $object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
+            }
+
+            if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
+                $object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
+            }
+
+            if ($exceptions = $this->obj->exceptionDates()) {
+                for ($i=0; $i < $exceptions->size(); $i++) {
+                    if ($exdate = self::php_datetime($exceptions->get($i)))
+                        $object['recurrence']['EXDATE'][] = $exdate;
+                }
+            }
+        }
+
+        // read alarm
+        $valarms = $this->obj->alarms();
+        $alarm_types = array_flip($this->alarm_type_map);
+        for ($i=0; $i < $valarms->size(); $i++) {
+            $alarm = $valarms->get($i);
+            $type = $alarm_types[$alarm->type()];
+
+            if ($type == 'DISPLAY' || $type == 'EMAIL') {  // only DISPLAY and EMAIL alarms are supported
+                if ($start = self::php_datetime($alarm->start())) {
+                    $object['alarms'] = '@' . $start->format('U');
+                }
+                else if ($offset = $alarm->relativeStart()) {
+                    $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+                    if      ($w = $offset->weeks())     $value .= $w . 'W';
+                    else if ($d = $offset->days())      $value .= $d . 'D';
+                    else if ($h = $offset->hours())     $value .= $h . 'H';
+                    else if ($m = $offset->minutes())   $value .= $m . 'M';
+                    else if ($s = $offset->seconds())   $value .= $s . 'S';
+                    else continue;
+
+                    $object['alarms'] = $value;
+                }
+                $object['alarms']  .= ':' . $type;
+                break;
+            }
+        }
+
+        return $object;
+    }
+
+
+    /**
+     * Set common xcal properties to the kolabformat object
+     *
+     * @param array  Event data as hash array
+     */
+    public function set(&$object)
+    {
+        // set some automatic values if missing
+        if (!$this->obj->created()) {
+            if (!empty($object['created']))
+                $object['created'] = new DateTime('now', self::$timezone);
+            $this->obj->setCreated(self::get_datetime($object['created']));
+        }
+
+        if (!empty($object['uid']))
+            $this->obj->setUid($object['uid']);
+
+        $object['changed'] = new DateTime('now', self::$timezone);
+        $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
+
+        // increment sequence
+        $this->obj->setSequence($this->obj->sequence()+1);
+
+        $this->obj->setSummary($object['title']);
+        $this->obj->setLocation($object['location']);
+        $this->obj->setDescription($object['description']);
+        $this->obj->setPriority($object['priority']);
+        $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
+        $this->obj->setCategories(self::array2vector($object['categories']));
+
+        // process event attendees
+        $attendees = new vectorattendee;
+        foreach ((array)$object['attendees'] as $attendee) {
+            if ($attendee['role'] == 'ORGANIZER') {
+                $object['organizer'] = $attendee;
+            }
+            else {
+                $cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
+                $cr->setName($attendee['name']);
+
+                $att = new Attendee;
+                $att->setContact($cr);
+                $att->setPartStat($this->status_map[$attendee['status']]);
+                $att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
+                $att->setRSVP((bool)$attendee['rsvp']);
+
+                if ($att->isValid()) {
+                    $attendees->push($att);
+                }
+                else {
+                    rcube::raise_error(array(
+                        'code' => 600, 'type' => 'php',
+                        'file' => __FILE__, 'line' => __LINE__,
+                        'message' => "Invalid event attendee: " . json_encode($attendee),
+                    ), true);
+                }
+            }
+        }
+        $this->obj->setAttendees($attendees);
+
+        if ($object['organizer']) {
+            $organizer = new ContactReference(ContactReference::EmailReference, $object['organizer']['email']);
+            $organizer->setName($object['organizer']['name']);
+            $this->obj->setOrganizer($organizer);
+        }
+
+        // save recurrence rule
+        if ($object['recurrence']) {
+            $rr = new RecurrenceRule;
+            $rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
+
+            if ($object['recurrence']['INTERVAL'])
+                $rr->setInterval(intval($object['recurrence']['INTERVAL']));
+
+            if ($object['recurrence']['BYDAY']) {
+                $byday = new vectordaypos;
+                foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
+                    $occurrence = 0;
+                    if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
+                        $occurrence = intval($m[1]);
+                        $day = $m[2];
+                    }
+                    if (isset($this->weekday_map[$day]))
+                        $byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
+                }
+                $rr->setByday($byday);
+            }
+
+            if ($object['recurrence']['BYMONTHDAY']) {
+                $bymday = new vectori;
+                foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
+                    $bymday->push(intval($day));
+                $rr->setBymonthday($bymday);
+            }
+
+            if ($object['recurrence']['BYMONTH']) {
+                $bymonth = new vectori;
+                foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
+                    $bymonth->push(intval($month));
+                $rr->setBymonth($bymonth);
+            }
+
+            if ($object['recurrence']['COUNT'])
+                $rr->setCount(intval($object['recurrence']['COUNT']));
+            else if ($object['recurrence']['UNTIL'])
+                $rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true));
+
+            if ($rr->isValid()) {
+                $this->obj->setRecurrenceRule($rr);
+
+                // add exception dates (only if recurrence rule is valid)
+                $exdates = new vectordatetime;
+                foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
+                    $exdates->push(self::get_datetime($exdate, null, true));
+                $this->obj->setExceptionDates($exdates);
+            }
+            else {
+                rcube::raise_error(array(
+                    'code' => 600, 'type' => 'php',
+                    'file' => __FILE__, 'line' => __LINE__,
+                    'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
+                ), true);
+            }
+        }
+
+        // save alarm
+        $valarms = new vectoralarm;
+        if ($object['alarms']) {
+            list($offset, $type) = explode(":", $object['alarms']);
+
+            if ($type == 'EMAIL') {  // email alarms implicitly go to event owner
+                $recipients = new vectorcontactref;
+                $recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
+                $alarm = new Alarm($object['title'], strval($object['description']), $recipients);
+            }
+            else {  // default: display alarm
+                $alarm = new Alarm($object['title']);
+            }
+
+            if (preg_match('/^@(\d+)/', $offset, $d)) {
+                $alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
+            }
+            else if (preg_match('/^([-+]?)(\d+)([SMHDW])/', $offset, $d)) {
+                $days = $hours = $minutes = $seconds = 0;
+                switch ($d[3]) {
+                    case 'W': $days  = 7*intval($d[2]); break;
+                    case 'D': $days    = intval($d[2]); break;
+                    case 'H': $hours   = intval($d[2]); break;
+                    case 'M': $minutes = intval($d[2]); break;
+                    case 'S': $seconds = intval($d[2]); break;
+                }
+                $alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
+            }
+
+            $valarms->push($alarm);
+        }
+        $this->obj->setAlarms($valarms);
+    }
+
+    /**
+     * Callback for kolab_storage_cache to get words to index for fulltext search
+     *
+     * @return array List of words to save in cache
+     */
+    public function get_words()
+    {
+        $data = '';
+        foreach (self::$fulltext_cols as $colname) {
+            list($col, $field) = explode(':', $colname);
+
+            if ($field) {
+                $a = array();
+                foreach ((array)$this->data[$col] as $attr)
+                    $a[] = $attr[$field];
+                $val = join(' ', $a);
+            }
+            else {
+                $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
+            }
+
+            if (strlen($val))
+                $data .= $val . ' ';
+        }
+
+        return array_unique(rcube_utils::normalize_string($data, true));
+    }
+
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage.php b/lib/plugins/libkolab/lib/kolab_storage.php
new file mode 100644
index 0000000..57e5491
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage.php
@@ -0,0 +1,645 @@
+<?php
+
+/**
+ * Kolab storage class providing static methods to access groupware objects on a Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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
+{
+    const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
+    const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
+    const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
+    const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
+    const SERVERSIDE_SUBSCRIPTION = 0;
+    const CLIENTSIDE_SUBSCRIPTION = 1;
+
+    public static $last_error;
+
+    private static $ready = false;
+    private static $config;
+    private static $cache;
+    private static $imap;
+
+
+    /**
+     * Setup the environment needed by the libs
+     */
+    public static function setup()
+    {
+        if (self::$ready)
+            return true;
+
+        $rcmail = rcube::get_instance();
+        self::$config = $rcmail->config;
+        self::$imap = $rcmail->get_storage();
+        self::$ready = class_exists('kolabformat') &&
+            (self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
+
+        if (self::$ready) {
+            // set imap options
+            self::$imap->set_options(array(
+                'skip_deleted' => true,
+                'threading' => false,
+            ));
+            self::$imap->set_pagesize(9999);
+        }
+
+        return self::$ready;
+    }
+
+
+    /**
+     * Get a list of storage folders for the given data type
+     *
+     * @param string Data type to list folders for (contact,distribution-list,event,task,note)
+     *
+     * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
+     */
+    public static function get_folders($type)
+    {
+        $folders = $folderdata = array();
+
+        if (self::setup()) {
+            foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
+                $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+            }
+        }
+
+        return $folders;
+    }
+
+
+    /**
+     * Getter for a specific storage folder
+     *
+     * @param string  IMAP folder to access (UTF7-IMAP)
+     * @return object kolab_storage_folder  The folder object
+     */
+    public static function get_folder($folder)
+    {
+        return self::setup() ? new kolab_storage_folder($folder) : null;
+    }
+
+
+    /**
+     * Getter for a single Kolab object, identified by its UID.
+     * This will search all folders storing objects of the given type.
+     *
+     * @param string Object UID
+     * @param string Object type (contact,distribution-list,event,task,note)
+     * @return array The Kolab object represented as hash array or false if not found
+     */
+    public static function get_object($uid, $type)
+    {
+        self::setup();
+        $folder = null;
+        foreach ((array)self::list_folders('', '*', $type) as $foldername) {
+            if (!$folder)
+                $folder = new kolab_storage_folder($foldername);
+            else
+                $folder->set_folder($foldername);
+
+            if ($object = $folder->get_object($uid))
+                return $object;
+        }
+
+        return false;
+    }
+
+
+    /**
+     *
+     */
+    public static function get_freebusy_server()
+    {
+        return unslashify(self::$config->get('kolab_freebusy_server', 'https://' . $_SESSION['imap_host'] . '/freebusy'));
+    }
+
+
+    /**
+     * Compose an URL to query the free/busy status for the given user
+     */
+    public static function get_freebusy_url($email)
+    {
+        return self::get_freebusy_server() . '/' . $email . '.ifb';
+    }
+
+
+    /**
+     * Creates folder ID from folder name
+     *
+     * @param string $folder Folder name (UTF7-IMAP)
+     *
+     * @return string Folder ID string
+     */
+    public static function folder_id($folder)
+    {
+        return asciiwords(strtr($folder, '/.-', '___'));
+    }
+
+
+    /**
+     * Deletes IMAP folder
+     *
+     * @param string $name Folder name (UTF7-IMAP)
+     *
+     * @return bool True on success, false on failure
+     */
+    public static function folder_delete($name)
+    {
+        // clear cached entries first
+        if ($folder = self::get_folder($name))
+            $folder->cache->purge();
+
+        $success = self::$imap->delete_folder($name);
+        self::$last_error = self::$imap->get_error_str();
+
+        return $success;
+    }
+
+    /**
+     * Creates IMAP folder
+     *
+     * @param string $name        Folder name (UTF7-IMAP)
+     * @param string $type        Folder type
+     * @param bool   $subscribed  Sets folder subscription
+     *
+     * @return bool True on success, false on failure
+     */
+    public static function folder_create($name, $type = null, $subscribed = false)
+    {
+        self::setup();
+
+        if ($saved = self::$imap->create_folder($name, $subscribed)) {
+            // set metadata for folder type
+            if ($type) {
+                $saved = self::set_folder_type($name, $type);
+
+                // revert if metadata could not be set
+                if (!$saved) {
+                    self::$imap->delete_folder($name);
+                }
+            }
+        }
+
+        if ($saved) {
+            return true;
+        }
+
+        self::$last_error = self::$imap->get_error_str();
+        return false;
+    }
+
+    /**
+     * Renames IMAP folder
+     *
+     * @param string $oldname Old folder name (UTF7-IMAP)
+     * @param string $newname New folder name (UTF7-IMAP)
+     *
+     * @return bool True on success, false on failure
+     */
+    public static function folder_rename($oldname, $newname)
+    {
+        self::setup();
+
+        $success = self::$imap->rename_folder($oldname, $newname);
+        self::$last_error = self::$imap->get_error_str();
+
+        return $success;
+    }
+
+
+    /**
+     * Rename or Create a new IMAP folder.
+     *
+     * Does additional checks for permissions and folder name restrictions
+     *
+     * @param array Hash array with folder properties and metadata
+     *  - name: Folder name
+     *  - oldname: Old folder name when changed
+     *  - parent: Parent folder to create the new one in
+     *  - type: Folder type to create
+     * @return mixed New folder name or False on failure
+     */
+    public static function folder_update(&$prop)
+    {
+        self::setup();
+
+        $folder    = rcube_charset::convert($prop['name'], RCMAIL_CHARSET, 'UTF7-IMAP');
+        $oldfolder = $prop['oldname']; // UTF7
+        $parent    = $prop['parent']; // UTF7
+        $delimiter = self::$imap->get_hierarchy_delimiter();
+
+        if (strlen($oldfolder)) {
+            $options = self::$imap->folder_info($oldfolder);
+        }
+
+        if (!empty($options) && ($options['norename'] || $options['protected'])) {
+        }
+        // sanity checks (from steps/settings/save_folder.inc)
+        else if (!strlen($folder)) {
+            self::$last_error = 'cannotbeempty';
+            return false;
+        }
+        else if (strlen($folder) > 128) {
+            self::$last_error = 'nametoolong';
+            return false;
+        }
+        else {
+            // these characters are problematic e.g. when used in LIST/LSUB
+            foreach (array($delimiter, '%', '*') as $char) {
+                if (strpos($folder, $delimiter) !== false) {
+                    self::$last_error = 'forbiddencharacter';
+                    return false;
+                }
+            }
+        }
+
+        if (!empty($options) && ($options['protected'] || $options['norename'])) {
+            $folder = $oldfolder;
+        }
+        else if (strlen($parent)) {
+            $folder = $parent . $delimiter . $folder;
+        }
+        else {
+            // add namespace prefix (when needed)
+            $folder = self::$imap->mod_folder($folder, 'in');
+        }
+
+        // Check access rights to the parent folder
+        if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
+            $parent_opts = self::$imap->folder_info($parent);
+            if ($parent_opts['namespace'] != 'personal'
+                && (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
+            ) {
+                self::$last_error = 'No permission to create folder';
+                return false;
+          }
+        }
+
+        // update the folder name
+        if (strlen($oldfolder)) {
+            if ($oldfolder != $folder) {
+                $result = self::folder_rename($oldfolder, $folder);
+          }
+          else
+              $result = true;
+        }
+        // create new folder
+        else {
+            $result = self::folder_create($folder, $prop['type'], $prop['subscribed'] === self::SERVERSIDE_SUBSCRIPTION);
+        }
+
+        // save color in METADATA
+        // TODO: also save 'showalarams' and other properties here
+
+        if ($result && $prop['color']) {
+            $meta_saved = false;
+            $ns = self::$imap->folder_namespace($folder);
+            if ($ns == 'personal')  // save in shared namespace for personal folders
+                $meta_saved = self::$imap->set_metadata($folder, array(self::COLOR_KEY_SHARED => $prop['color']));
+            if (!$meta_saved)    // try in private namespace
+                $meta_saved = self::$imap->set_metadata($folder, array(self::COLOR_KEY_PRIVATE => $prop['color']));
+            if ($meta_saved)
+                unset($prop['color']);  // unsetting will prevent fallback to local user prefs
+        }
+
+        return $result ? $folder : false;
+    }
+
+
+    /**
+     * Getter for human-readable name of Kolab object (folder)
+     * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
+     *
+     * @param string $folder    IMAP folder name (UTF7-IMAP)
+     * @param string $folder_ns Will be set to namespace name of the folder
+     *
+     * @return string Name of the folder-object
+     */
+    public static function object_name($folder, &$folder_ns=null)
+    {
+        self::setup();
+
+        $found     = false;
+        $namespace = self::$imap->get_namespace();
+
+        if (!empty($namespace['shared'])) {
+            foreach ($namespace['shared'] as $ns) {
+                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+                    $prefix = '';
+                    $folder = substr($folder, strlen($ns[0]));
+                    $delim  = $ns[1];
+                    $found  = true;
+                    $folder_ns = 'shared';
+                    break;
+                }
+            }
+        }
+        if (!$found && !empty($namespace['other'])) {
+            foreach ($namespace['other'] as $ns) {
+                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+                    // remove namespace prefix
+                    $folder = substr($folder, strlen($ns[0]));
+                    $delim  = $ns[1];
+                    // get username
+                    $pos    = strpos($folder, $delim);
+                    if ($pos) {
+                        $prefix = '('.substr($folder, 0, $pos).') ';
+                        $folder = substr($folder, $pos+1);
+                    }
+                    else {
+                        $prefix = '('.$folder.')';
+                        $folder = '';
+                    }
+                    $found  = true;
+                    $folder_ns = 'other';
+                    break;
+                }
+            }
+        }
+        if (!$found && !empty($namespace['personal'])) {
+            foreach ($namespace['personal'] as $ns) {
+                if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
+                    // remove namespace prefix
+                    $folder = substr($folder, strlen($ns[0]));
+                    $prefix = '';
+                    $delim  = $ns[1];
+                    $found  = true;
+                    break;
+                }
+            }
+        }
+
+        if (empty($delim))
+            $delim = self::$imap->get_hierarchy_delimiter();
+
+        $folder = rcube_charset::convert($folder, 'UTF7-IMAP');
+        $folder = html::quote($folder);
+        $folder = str_replace(html::quote($delim), ' » ', $folder);
+
+        if ($prefix)
+            $folder = html::quote($prefix) . ' ' . $folder;
+
+        if (!$folder_ns)
+            $folder_ns = 'personal';
+
+        return $folder;
+    }
+
+
+    /**
+     * Helper method to generate a truncated folder name to display
+     */
+    public static function folder_displayname($origname, &$names)
+    {
+        $name = $origname;
+
+        // find folder prefix to truncate
+        for ($i = count($names)-1; $i >= 0; $i--) {
+            if (strpos($name, $names[$i] . ' » ') === 0) {
+                $length = strlen($names[$i] . ' » ');
+                $prefix = substr($name, 0, $length);
+                $count  = count(explode(' » ', $prefix));
+                $name   = str_repeat('  ', $count-1) . '» ' . substr($name, $length);
+                break;
+            }
+        }
+        $names[] = $origname;
+
+        return $name;
+    }
+
+
+    /**
+     * Creates a SELECT field with folders list
+     *
+     * @param string $type    Folder type
+     * @param array  $attrs   SELECT field attributes (e.g. name)
+     * @param string $current The name of current folder (to skip it)
+     *
+     * @return html_select SELECT object
+     */
+    public static function folder_selector($type, $attrs, $current = '')
+    {
+        // get all folders of specified type
+        $folders = self::get_folders($type);
+
+        $delim = self::$imap->get_hierarchy_delimiter();
+        $names = array();
+        $len   = strlen($current);
+
+        if ($len && ($rpos = strrpos($current, $delim))) {
+            $parent = substr($current, 0, $rpos);
+            $p_len  = strlen($parent);
+        }
+
+        // Filter folders list
+        foreach ($folders as $c_folder) {
+            $name = $c_folder->name;
+            // skip current folder and it's subfolders
+            if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
+                continue;
+            }
+
+            // always show the parent of current folder
+            if ($p_len && $name == $parent) { }
+            // skip folders where user have no rights to create subfolders
+            else if ($c_folder->get_owner() != $_SESSION['username']) {
+                $rights = $c_folder->get_myrights();
+                if (!preg_match('/[ck]/', $rights)) {
+                    continue;
+                }
+            }
+
+            $names[$name] = rcube_charset::convert($name, 'UTF7-IMAP');
+        }
+
+        // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
+        if ($p_len && !isset($names[$parent])) {
+            $names[$parent] = rcube_charset::convert($parent, 'UTF7-IMAP');
+        }
+
+        // Sort folders list
+        asort($names, SORT_LOCALE_STRING);
+
+        $folders = array_keys($names);
+        $names   = array();
+
+        // Build SELECT field of parent folder
+        $attrs['is_escaped'] = true;
+        $select = new html_select($attrs);
+        $select->add('---', '');
+
+        foreach ($folders as $name) {
+            $imap_name = $name;
+            $name      = $origname = self::object_name($name);
+
+            // find folder prefix to truncate
+            for ($i = count($names)-1; $i >= 0; $i--) {
+                if (strpos($name, $names[$i].' » ') === 0) {
+                    $length = strlen($names[$i].' » ');
+                    $prefix = substr($name, 0, $length);
+                    $count  = count(explode(' » ', $prefix));
+                    $name   = str_repeat('  ', $count-1) . '» ' . substr($name, $length);
+                    break;
+                }
+            }
+
+            $names[] = $origname;
+            $select->add($name, $imap_name);
+        }
+
+        return $select;
+    }
+
+
+    /**
+     * Returns a list of folder names
+     *
+     * @param string  Optional root folder
+     * @param string  Optional name pattern
+     * @param string  Data type to list folders for (contact,distribution-list,event,task,note,mail)
+     * @param string  Enable to return subscribed folders only
+     * @param array   Will be filled with folder-types data
+     *
+     * @return array List of folders
+     */
+    public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = false, &$folderdata = array())
+    {
+        if (!self::setup()) {
+            return null;
+        }
+
+        if (!$filter) {
+            // Get ALL folders list, standard way
+            if ($subscribed) {
+                return self::$imap->list_folders_subscribed($root, $mbox);
+            }
+            else {
+                return self::$imap->list_folders($root, $mbox);
+            }
+        }
+
+        $prefix = $root . $mbox;
+
+        // get folders types
+        $folderdata = self::$imap->get_metadata($prefix, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
+
+        if (!is_array($folderdata)) {
+            return array();
+        }
+
+        $folderdata = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+        $regexp     = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
+
+        // In some conditions we can skip LIST command (?)
+        if ($subscribed == false && $filter != 'mail' && $prefix == '*') {
+            foreach ($folderdata as $folder => $type) {
+                if (!preg_match($regexp, $type)) {
+                    unset($folderdata[$folder]);
+                }
+            }
+            return array_keys($folderdata);
+        }
+
+        // Get folders list
+        if ($subscribed) {
+            $folders = self::$imap->list_folders_subscribed($root, $mbox);
+        }
+        else {
+            $folders = self::$imap->list_folders($root, $mbox);
+        }
+
+        // In case of an error, return empty list (?)
+        if (!is_array($folders)) {
+            return array();
+        }
+
+        // Filter folders list
+        foreach ($folders as $idx => $folder) {
+            $type = $folderdata[$folder];
+
+            if ($filter == 'mail' && empty($type)) {
+                continue;
+            }
+            if (empty($type) || !preg_match($regexp, $type)) {
+                unset($folders[$idx]);
+            }
+        }
+
+        return $folders;
+    }
+
+
+    /**
+     * Callback for array_map to select the correct annotation value
+     */
+    static function folder_select_metadata($types)
+    {
+        return $types[self::CTYPE_KEY_PRIVATE] ?: $types[self::CTYPE_KEY];
+    }
+
+
+    /**
+     * Returns type of IMAP folder
+     *
+     * @param string $folder Folder name (UTF7-IMAP)
+     *
+     * @return string Folder type
+     */
+    static function folder_type($folder)
+    {
+        self::setup();
+
+        $metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
+
+        if (!is_array($metadata)) {
+            return null;
+        }
+
+        if (!empty($metadata[$folder])) {
+            return self::folder_select_metadata($metadata[$folder]);
+        }
+
+        return 'mail';
+    }
+
+    /**
+     * Sets folder content-type.
+     *
+     * @param string $folder Folder name
+     * @param string $type   Content type
+     *
+     * @return boolean True on success
+     */
+    static function set_folder_type($folder, $type='mail')
+    {
+        list($ctype, $subtype) = explode('.', $type);
+
+        $success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
+
+        if (!$success)  // fallback: only set private annotation
+            $success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
+
+        return $success;
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
new file mode 100644
index 0000000..c3e88da
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -0,0 +1,728 @@
+<?php
+
+/**
+ * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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_cache
+{
+    private $db;
+    private $imap;
+    private $folder;
+    private $uid2msg;
+    private $objects;
+    private $index = array();
+    private $resource_uri;
+    private $enabled = true;
+    private $synched = false;
+    private $synclock = false;
+    private $ready = false;
+    private $max_sql_packet = 1046576;  // 1 MB - 2000 bytes
+    private $binary_cols = array('photo','pgppublickey','pkcs7publickey');
+
+
+    /**
+     * Default constructor
+     */
+    public function __construct(kolab_storage_folder $storage_folder = null)
+    {
+        $rcmail = rcube::get_instance();
+        $this->db = $rcmail->get_dbh();
+        $this->imap = $rcmail->get_storage();
+        $this->enabled = $rcmail->config->get('kolab_cache', false);
+
+        if ($this->enabled) {
+            // remove sync-lock on script termination
+            $rcmail->add_shutdown_function(array($this, '_sync_unlock'));
+
+            // read max_allowed_packet from mysql config
+            $this->max_sql_packet = min($this->db->get_variable('max_allowed_packet', 1048500), 4*1024*1024) - 2000;  // mysql limit or max 4 MB
+        }
+
+        if ($storage_folder)
+            $this->set_folder($storage_folder);
+    }
+
+
+    /**
+     * Connect cache with a storage folder
+     *
+     * @param kolab_storage_folder The storage folder instance to connect with
+     */
+    public function set_folder(kolab_storage_folder $storage_folder)
+    {
+        $this->folder = $storage_folder;
+
+        if (empty($this->folder->name)) {
+            $this->ready = false;
+            return;
+        }
+
+        // compose fully qualified ressource uri for this instance
+        $this->resource_uri = $this->folder->get_resource_uri();
+        $this->ready = $this->enabled;
+    }
+
+
+    /**
+     * Synchronize local cache data with remote
+     */
+    public function synchronize()
+    {
+        // only sync once per request cycle
+        if ($this->synched)
+            return;
+
+        // increase time limit
+        @set_time_limit(500);
+
+        // lock synchronization for this folder or wait if locked
+        $this->_sync_lock();
+
+        // synchronize IMAP mailbox cache
+        $this->imap->folder_sync($this->folder->name);
+
+        // compare IMAP index with object cache index
+        $imap_index = $this->imap->index($this->folder->name);
+        $this->index = $imap_index->get();
+
+        // determine objects to fetch or to invalidate
+        if ($this->ready) {
+            // read cache index
+            $sql_result = $this->db->query(
+                "SELECT msguid, uid FROM kolab_cache WHERE resource=? AND type<>?",
+                $this->resource_uri,
+                'lock'
+            );
+
+            $old_index = array();
+            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                $old_index[] = $sql_arr['msguid'];
+                $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+            }
+
+            // fetch new objects from imap
+            foreach (array_diff($this->index, $old_index) as $msguid) {
+                if ($object = $this->folder->read_object($msguid, '*')) {
+                    $this->_extended_insert($msguid, $object);
+                }
+            }
+            $this->_extended_insert(0, null);
+
+            // delete invalid entries from local DB
+            $del_index = array_diff($old_index, $this->index);
+            if (!empty($del_index)) {
+                $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
+                $this->db->query(
+                    "DELETE FROM kolab_cache WHERE resource=? AND msguid IN ($quoted_ids)",
+                    $this->resource_uri
+                );
+            }
+        }
+
+        // remove lock
+        $this->_sync_unlock();
+
+        $this->synched = time();
+    }
+
+
+    /**
+     * Read a single entry from cache or from IMAP directly
+     *
+     * @param string Related IMAP message UID
+     * @param string Object type to read
+     * @param string IMAP folder name the entry relates to
+     * @param array  Hash array with object properties or null if not found
+     */
+    public function get($msguid, $type = null, $foldername = null)
+    {
+        // delegate to another cache instance
+        if ($foldername && $foldername != $this->folder->name) {
+            return kolab_storage::get_folder($foldername)->cache->get($msguid, $object);
+        }
+
+        // load object if not in memory
+        if (!isset($this->objects[$msguid])) {
+            if ($this->ready) {
+                $sql_result = $this->db->query(
+                    "SELECT * FROM kolab_cache ".
+                    "WHERE resource=? AND type=? AND msguid=?",
+                    $this->resource_uri,
+                    $type ?: $this->folder->type,
+                    $msguid
+                );
+
+                if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                    $this->objects[$msguid] = $this->_unserialize($sql_arr);
+                }
+            }
+
+            // fetch from IMAP if not present in cache
+            if (empty($this->objects[$msguid])) {
+                $result = $this->_fetch(array($msguid), $type, $foldername);
+                $this->objects[$msguid] = $result[0];
+            }
+        }
+
+        return $this->objects[$msguid];
+    }
+
+
+    /**
+     * Insert/Update a cache entry
+     *
+     * @param string Related IMAP message UID
+     * @param mixed  Hash array with object properties to save or false to delete the cache entry
+     * @param string IMAP folder name the entry relates to
+     */
+    public function set($msguid, $object, $foldername = null)
+    {
+        if (!$msguid) {
+            return;
+        }
+
+        // delegate to another cache instance
+        if ($foldername && $foldername != $this->folder->name) {
+            kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
+            return;
+        }
+
+        // remove old entry
+        if ($this->ready) {
+            $this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=? AND type<>?",
+                $this->resource_uri, $msguid, 'lock');
+        }
+
+        if ($object) {
+            // insert new object data...
+            $this->insert($msguid, $object);
+        }
+        else {
+            // ...or set in-memory cache to false
+            $this->objects[$msguid] = $object;
+        }
+    }
+
+
+    /**
+     * Insert a cache entry
+     *
+     * @param string Related IMAP message UID
+     * @param mixed  Hash array with object properties to save or false to delete the cache entry
+     */
+    public function insert($msguid, $object)
+    {
+        // write to cache
+        if ($this->ready) {
+            $sql_data = $this->_serialize($object);
+            $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
+
+            $result = $this->db->query(
+                "INSERT INTO kolab_cache ".
+                " (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words)".
+                " VALUES (?, ?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ?, ?, ?)",
+                $this->resource_uri,
+                $objtype,
+                $msguid,
+                $object['uid'],
+                $sql_data['changed'],
+                $sql_data['data'],
+                $sql_data['xml'],
+                $sql_data['dtstart'],
+                $sql_data['dtend'],
+                $sql_data['tags'],
+                $sql_data['words']
+            );
+
+            if (!$this->db->affected_rows($result)) {
+                rcube::raise_error(array(
+                    'code' => 900, 'type' => 'php',
+                    'message' => "Failed to write to kolab cache"
+                ), true);
+            }
+        }
+
+        // keep a copy in memory for fast access
+        $this->objects[$msguid] = $object;
+        $this->uid2msg[$object['uid']] = $msguid;
+    }
+
+
+    /**
+     * Move an existing cache entry to a new resource
+     *
+     * @param string Entry's IMAP message UID
+     * @param string Entry's Object UID
+     * @param string Target IMAP folder to move it to
+     */
+    public function move($msguid, $objuid, $target_folder)
+    {
+        $target = kolab_storage::get_folder($target_folder);
+
+        // resolve new message UID in target folder
+        if ($new_msguid = $target->cache->uid2msguid($objuid)) {
+            $this->db->query(
+                "UPDATE kolab_cache SET resource=?, msguid=? ".
+                "WHERE resource=? AND msguid=? AND type<>?",
+                $target->get_resource_uri(),
+                $new_msguid,
+                $this->resource_uri,
+                $msguid,
+                'lock'
+            );
+        }
+        else {
+            // just clear cache entry
+            $this->set($msguid, false);
+        }
+
+        unset($this->uid2msg[$uid]);
+    }
+
+
+    /**
+     * Remove all objects from local cache
+     */
+    public function purge($type = null)
+    {
+        $result = $this->db->query(
+            "DELETE FROM kolab_cache WHERE resource=?".
+            ($type ? ' AND type=?' : ''),
+            $this->resource_uri,
+            $type
+        );
+        return $this->db->affected_rows($result);
+    }
+
+
+    /**
+     * Select Kolab objects filtered by the given query
+     *
+     * @param array Pseudo-SQL query as list of filter parameter triplets
+     *   triplet: array('<colname>', '<comparator>', '<value>')
+     * @param boolean Set true to only return UIDs instead of complete objects
+     * @return array List of Kolab data objects (each represented as hash array) or UIDs
+     */
+    public function select($query = array(), $uids = false)
+    {
+        $result = array();
+
+        // read from local cache DB (assume it to be synchronized)
+        if ($this->ready) {
+            $sql_result = $this->db->query(
+                "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM kolab_cache ".
+                "WHERE resource=? " . $this->_sql_where($query),
+                $this->resource_uri
+            );
+
+            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                if ($uids) {
+                    $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+                    $result[] = $sql_arr['uid'];
+                }
+                else if ($object = $this->_unserialize($sql_arr)) {
+                    $result[] = $object;
+                }
+            }
+        }
+        else {
+            // extract object type from query parameter
+            $filter = $this->_query2assoc($query);
+
+            // use 'list' for folder's default objects
+            if ($filter['type'] == $this->type) {
+                $index = $this->index;
+            }
+            else {  // search by object type
+                $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
+                $index = $this->imap->search_once($this->folder->name, $search)->get();
+            }
+
+            // fetch all messages in $index from IMAP
+            $result = $uids ? $this->_fetch_uids($index, $filter['type']) : $this->_fetch($index, $filter['type']);
+
+            // TODO: post-filter result according to query
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Get number of objects mathing the given query
+     *
+     * @param array  $query Pseudo-SQL query as list of filter parameter triplets
+     * @return integer The number of objects of the given type
+     */
+    public function count($query = array())
+    {
+        $count = 0;
+
+        // cache is in sync, we can count records in local DB
+        if ($this->synched) {
+            $sql_result = $this->db->query(
+                "SELECT COUNT(*) AS numrows FROM kolab_cache ".
+                "WHERE resource=? " . $this->_sql_where($query),
+                $this->resource_uri
+            );
+
+            $sql_arr = $this->db->fetch_assoc($sql_result);
+            $count = intval($sql_arr['numrows']);
+        }
+        else {
+            // search IMAP by object type
+            $filter = $this->_query2assoc($query);
+            $ctype  = kolab_format::KTYPE_PREFIX . $filter['type'];
+            $index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
+            $count = $index->count();
+        }
+
+        return $count;
+    }
+
+
+    /**
+     * Helper method to compose a valid SQL query from pseudo filter triplets
+     */
+    private function _sql_where($query)
+    {
+        $sql_where = '';
+        foreach ($query as $param) {
+            if ($param[1] == '=' && is_array($param[2])) {
+                $qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
+                $param[1] = 'IN';
+            }
+            else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') {
+                $not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : '';
+                $param[1] = $not . 'LIKE';
+                $qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
+            }
+            else if ($param[0] == 'tags') {
+                $param[1] = 'LIKE';
+                $qvalue = $this->db->quote('% '.$param[2].' %');
+            }
+            else {
+                $qvalue = $this->db->quote($param[2]);
+            }
+
+            $sql_where .= sprintf(' AND %s %s %s',
+                $this->db->quote_identifier($param[0]),
+                $param[1],
+                $qvalue
+            );
+        }
+
+        return $sql_where;
+    }
+
+    /**
+     * Helper method to convert the given pseudo-query triplets into
+     * an associative filter array with 'equals' values only
+     */
+    private function _query2assoc($query)
+    {
+        // extract object type from query parameter
+        $filter = array();
+        foreach ($query as $param) {
+            if ($param[1] == '=')
+                $filter[$param[0]] = $param[2];
+        }
+        return $filter;
+    }
+
+    /**
+     * Fetch messages from IMAP
+     *
+     * @param array  List of message UIDs to fetch
+     * @param string Requested object type or * for all
+     * @param string IMAP folder to read from
+     * @return array List of parsed Kolab objects
+     */
+    private function _fetch($index, $type = null, $folder = null)
+    {
+        $results = array();
+        foreach ((array)$index as $msguid) {
+            if ($object = $this->folder->read_object($msguid, $type, $folder)) {
+                $results[] = $object;
+                $this->set($msguid, $object);
+            }
+        }
+
+        return $results;
+    }
+
+
+    /**
+     * Fetch object UIDs (aka message subjects) from IMAP
+     *
+     * @param array List of message UIDs to fetch
+     * @param string Requested object type or * for all
+     * @param string IMAP folder to read from
+     * @return array List of parsed Kolab objects
+     */
+    private function _fetch_uids($index, $type = null)
+    {
+        if (!$type)
+            $type = $this->folder->type;
+
+        $results = array();
+        foreach ((array)$this->imap->fetch_headers($this->folder->name, $index, false) as $msguid => $headers) {
+            $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
+
+            // check object type header and abort on mismatch
+            if ($type != '*' && $object_type != $type)
+                return false;
+
+            $uid = $headers->subject;
+            $this->uid2msg[$uid] = $msguid;
+            $results[] = $uid;
+        }
+
+        return $results;
+    }
+
+
+    /**
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
+     */
+    private function _serialize($object)
+    {
+        $bincols = array_flip($this->binary_cols);
+        $sql_data = array('changed' => null, 'dtstart' => null, 'dtend' => null, 'xml' => '', 'tags' => '', 'words' => '');
+        $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
+
+        // set type specific values
+        if ($objtype == 'event') {
+            // database runs in server's timezone so using date() is what we want
+            $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
+            $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['end'])   ? $object['end']->format('U')   : $object['end']);
+
+            // extend date range for recurring events
+            if ($object['recurrence']) {
+                $recurrence = new kolab_date_recurrence($object);
+                $sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +1 year'));
+            }
+        }
+        else if ($objtype == 'task') {
+            if ($object['start'])
+                $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
+            if ($object['due'])
+                $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['due'])   ? $object['due']->format('U')   : $object['due']);
+        }
+
+        if ($object['changed']) {
+            $sql_data['changed'] = date('Y-m-d H:i:s', is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
+        }
+
+        if ($object['_formatobj']) {
+            $sql_data['xml'] = preg_replace('!(</?[a-z0-9:-]+>)[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write());
+            $sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' ';  // pad with spaces for strict/prefix search
+            $sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
+        }
+
+        // extract object data
+        $data = array();
+        foreach ($object as $key => $val) {
+            if ($val === "" || $val === null) {
+                // skip empty properties
+                continue;
+            }
+            if (isset($bincols[$key])) {
+                $data[$key] = base64_encode($val);
+            }
+            else if ($key[0] != '_') {
+                $data[$key] = $val;
+            }
+            else if ($key == '_attachments') {
+                foreach ($val as $k => $att) {
+                    unset($att['content'], $att['path']);
+                    if ($att['id'])
+                        $data[$key][$k] = $att;
+                }
+            }
+        }
+
+        $sql_data['data'] = serialize($data);
+        return $sql_data;
+    }
+
+    /**
+     * Helper method to turn stored cache data into a valid storage object
+     */
+    private function _unserialize($sql_arr)
+    {
+        $object = unserialize($sql_arr['data']);
+
+        // decode binary properties
+        foreach ($this->binary_cols as $key) {
+            if (!empty($object[$key]))
+                $object[$key] = base64_decode($object[$key]);
+        }
+
+        // add meta data
+        $object['_type'] = $sql_arr['type'];
+        $object['_msguid'] = $sql_arr['msguid'];
+        $object['_mailbox'] = $this->folder->name;
+        $object['_formatobj'] = kolab_format::factory($sql_arr['type'], $sql_arr['xml']);
+
+        return $object;
+    }
+
+    /**
+     * Write records into cache using extended inserts to reduce the number of queries to be executed
+     *
+     * @param int  Message UID. Set 0 to commit buffered inserts
+     * @param array Kolab object to cache
+     */
+    private function _extended_insert($msguid, $object)
+    {
+        static $buffer = '';
+
+        $line = '';
+        if ($object) {
+            $sql_data = $this->_serialize($object);
+            $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
+
+            $values = array(
+                $this->db->quote($this->resource_uri),
+                $this->db->quote($objtype),
+                $this->db->quote($msguid),
+                $this->db->quote($object['uid']),
+                $this->db->now(),
+                $this->db->quote($sql_data['changed']),
+                $this->db->quote($sql_data['data']),
+                $this->db->quote($sql_data['xml']),
+                $this->db->quote($sql_data['dtstart']),
+                $this->db->quote($sql_data['dtend']),
+                $this->db->quote($sql_data['tags']),
+                $this->db->quote($sql_data['words']),
+            );
+            $line = '(' . join(',', $values) . ')';
+        }
+
+        if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet))) {
+            $result = $this->db->query(
+                "INSERT INTO kolab_cache ".
+                " (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words)".
+                " VALUES $buffer"
+            );
+            if (!$this->db->affected_rows($result)) {
+                rcube::raise_error(array(
+                    'code' => 900, 'type' => 'php',
+                    'message' => "Failed to write to kolab cache"
+                ), true);
+            }
+
+            $buffer = '';
+        }
+
+        $buffer .= ($buffer ? ',' : '') . $line;
+    }
+
+    /**
+     * Check lock record for this folder and wait if locked or set lock
+     */
+    private function _sync_lock()
+    {
+        if (!$this->ready)
+            return;
+
+        $sql_arr = $this->db->fetch_assoc($this->db->query(
+            "SELECT msguid AS locked, ".$this->db->unixtimestamp('created')." AS created FROM kolab_cache ".
+            "WHERE resource=? AND type=?",
+            $this->resource_uri,
+            'lock'
+        ));
+
+        // abort if database is not set-up
+        if ($this->db->is_error()) {
+            $this->ready = false;
+            return;
+        }
+
+        $this->synclock = true;
+
+        // create lock record if not exists
+        if (!$sql_arr) {
+            $this->db->query(
+                "INSERT INTO kolab_cache (resource, type, msguid, created, uid, data, xml)".
+                " VALUES (?, ?, 1, ?, '', '', '')",
+                $this->resource_uri,
+                'lock',
+                date('Y-m-d H:i:s')
+            );
+        }
+        // wait if locked (expire locks after 10 minutes)
+        else if (intval($sql_arr['locked']) > 0 && (time() - $sql_arr['created']) < 600) {
+            usleep(500000);
+            return $this->_sync_lock();
+        }
+        // set lock
+        else {
+            $this->db->query(
+                "UPDATE kolab_cache SET msguid=1, created=? ".
+                "WHERE resource=? AND type=?",
+                date('Y-m-d H:i:s'),
+                $this->resource_uri,
+                'lock'
+            );
+        }
+    }
+
+    /**
+     * Remove lock for this folder
+     */
+    public function _sync_unlock()
+    {
+        if (!$this->ready || !$this->synclock)
+            return;
+
+        $this->db->query(
+            "UPDATE kolab_cache SET msguid=0 ".
+            "WHERE resource=? AND type=?",
+            $this->resource_uri,
+            'lock'
+        );
+
+        $this->synclock = false;
+    }
+
+    /**
+     * Resolve an object UID into an IMAP message UID
+     *
+     * @param string  Kolab object UID
+     * @param boolean Include deleted objects
+     * @return int The resolved IMAP message UID
+     */
+    public function uid2msguid($uid, $deleted = false)
+    {
+        if (!isset($this->uid2msg[$uid])) {
+            // use IMAP SEARCH to get the right message
+            $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid);
+            $results = $index->get();
+            $this->uid2msg[$uid] = $results[0];
+        }
+
+        return $this->uid2msg[$uid];
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder.php b/lib/plugins/libkolab/lib/kolab_storage_folder.php
new file mode 100644
index 0000000..08bf669
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.php
@@ -0,0 +1,838 @@
+<?php
+
+/**
+ * The kolab_storage_folder class represents an IMAP folder on the Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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
+{
+    /**
+     * The folder name.
+     * @var string
+     */
+    public $name;
+
+    /**
+     * The type of this folder.
+     * @var string
+     */
+    public $type;
+
+    /**
+     * Is this folder set to be the default for its type
+     * @var boolean
+     */
+    public $default = false;
+
+    /**
+     * Is this folder set to be default
+     * @var boolean
+     */
+    public $cache;
+
+    private $type_annotation;
+    private $imap;
+    private $info;
+    private $owner;
+    private $resource_uri;
+    private $uid2msg = array();
+
+
+    /**
+     * Default constructor
+     */
+    function __construct($name, $type = null)
+    {
+        $this->imap = rcube::get_instance()->get_storage();
+        $this->imap->set_options(array('skip_deleted' => true));
+        $this->cache = new kolab_storage_cache($this);
+        $this->set_folder($name, $type);
+    }
+
+
+    /**
+     * Set the IMAP folder this instance connects to
+     *
+     * @param string The folder name/path
+     * @param string Optional folder type if known
+     */
+    public function set_folder($name, $ftype = null)
+    {
+        $this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name);
+
+        list($this->type, $suffix) = explode('.', $this->type_annotation);
+        $this->default      = $suffix == 'default';
+        $this->name         = $name;
+        $this->resource_uri = null;
+
+        $this->imap->set_folder($this->name);
+        $this->cache->set_folder($this);
+    }
+
+
+    /**
+     *
+     */
+    private function get_folder_info()
+    {
+        if (!isset($this->info))
+            $this->info = $this->imap->folder_info($this->name);
+
+        return $this->info;
+    }
+
+
+    /**
+     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
+     *
+     * @param array List of metadata keys to read
+     * @return array Metadata entry-value hash array on success, NULL on error
+     */
+    public function get_metadata($keys)
+    {
+        $metadata = $this->imap->get_metadata($this->name, (array)$keys);
+        return $metadata[$this->name];
+    }
+
+
+    /**
+     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+     *
+     * @param array  $entries Entry-value array (use NULL value as NIL)
+     * @return boolean True on success, False on failure
+     */
+    public function set_metadata($entries)
+    {
+        return $this->imap->set_metadata($this->name, $entries);
+    }
+
+
+    /**
+     * Returns the owner of the folder.
+     *
+     * @return string  The owner of this folder.
+     */
+    public function get_owner()
+    {
+        // return cached value
+        if (isset($this->owner))
+            return $this->owner;
+
+        $info = $this->get_folder_info();
+        $rcmail = rcube::get_instance();
+
+        switch ($info['namespace']) {
+        case 'personal':
+            $this->owner = $rcmail->get_user_name();
+            break;
+
+        case 'shared':
+            $this->owner = 'anonymous';
+            break;
+
+        default:
+            $owner = '';
+            list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+            if (strpos($user, '@') === false) {
+                $domain = strstr($rcmail->get_user_name(), '@');
+                if (!empty($domain))
+                    $user .= $domain;
+            }
+            $this->owner = $user;
+            break;
+        }
+
+        return $this->owner;
+    }
+
+
+    /**
+     * Getter for the name of the namespace to which the IMAP folder belongs
+     *
+     * @return string Name of the namespace (personal, other, shared)
+     */
+    public function get_namespace()
+    {
+        return $this->imap->folder_namespace($this->name);
+    }
+
+
+    /**
+     * Get IMAP ACL information for this folder
+     *
+     * @return string  Permissions as string
+     */
+    public function get_myrights()
+    {
+        $rights = $this->info['rights'];
+
+        if (!is_array($rights))
+            $rights = $this->imap->my_rights($this->name);
+
+        return join('', (array)$rights);
+    }
+
+
+    /**
+     * 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);
+                $subpath = $suffix;
+            }
+        }
+        else {
+            $subpath = $this->name;
+        }
+
+        // compose fully qualified ressource uri for this instance
+        $this->resource_uri = 'imap://' . urlencode($this->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
+        return $this->resource_uri;
+    }
+
+    /**
+     * Check subscription status of this folder
+     *
+     * @param string Subscription type (kolab_storage::SERVERSIDE_SUBSCRIPTION or kolab_storage::CLIENTSIDE_SUBSCRIPTION)
+     * @return boolean True if subscribed, false if not
+     */
+    public function is_subscribed($type = 0)
+    {
+        static $subscribed;  // local cache
+
+        if ($type == kolab_storage::SERVERSIDE_SUBSCRIPTION) {
+            if (!$subscribed)
+                $subscribed = $this->imap->list_folders_subscribed();
+
+            return in_array($this->name, $subscribed);
+        }
+        else if (kolab_storage::CLIENTSIDE_SUBSCRIPTION) {
+            // TODO: implement this
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Change subscription status of this folder
+     *
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+     * @param string  Subscription type (kolab_storage::SERVERSIDE_SUBSCRIPTION or kolab_storage::CLIENTSIDE_SUBSCRIPTION)
+     * @return True on success, false on error
+     */
+    public function subscribe($subscribed, $type = 0)
+    {
+        if ($type == kolab_storage::SERVERSIDE_SUBSCRIPTION) {
+            return $subscribed ? $this->imap->subscribe($this->name) : $this->imap->unsubscribe($this->name);
+        }
+        else {
+          // TODO: implement this
+        }
+
+        return false;
+    }
+
+
+    /**
+     * 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($type_or_query = null)
+    {
+        if (!$type_or_query)
+            $query = array(array('type','=',$this->type));
+        else if (is_string($type_or_query))
+            $query = array(array('type','=',$type_or_query));
+        else
+            $query = $this->_prepare_query((array)$type_or_query);
+
+        // synchronize cache first
+        $this->cache->synchronize();
+
+        return $this->cache->count($query);
+    }
+
+
+    /**
+     * List all Kolab objects of the given type
+     *
+     * @param string  $type Object type (e.g. contact, event, todo, journal, note, configuration)
+     * @return array  List of Kolab data objects (each represented as hash array)
+     */
+    public function get_objects($type = null)
+    {
+        if (!$type) $type = $this->type;
+
+        // synchronize caches
+        $this->cache->synchronize();
+
+        // fetch objects from cache
+        return $this->cache->select(array(array('type','=',$type)));
+    }
+
+
+    /**
+     * Select *some* Kolab objects matching the given query
+     *
+     * @param array Pseudo-SQL query as list of filter parameter triplets
+     *   triplet: array('<colname>', '<comparator>', '<value>')
+     * @return array List of Kolab data objects (each represented as hash array)
+     */
+    public function select($query = array())
+    {
+        // check query argument
+        if (empty($query))
+            return $this->get_objects();
+
+        // synchronize caches
+        $this->cache->synchronize();
+
+        // fetch objects from cache
+        return $this->cache->select($this->_prepare_query($query));
+    }
+
+
+    /**
+     * 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())
+    {
+        // synchronize caches
+        $this->cache->synchronize();
+
+        // fetch UIDs from cache
+        return $this->cache->select($this->_prepare_query($query), true);
+    }
+
+
+    /**
+     * Helper method to sanitize query arguments
+     */
+    private function _prepare_query($query)
+    {
+        $type = null;
+        foreach ($query as $i => $param) {
+            if ($param[0] == 'type') {
+                $type = $param[2];
+            }
+            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]);
+            }
+        }
+
+        // add type selector if not in $query
+        if (!$type)
+            $query[] = array('type','=',$this->type);
+
+        return $query;
+    }
+
+
+    /**
+     * Getter for a single Kolab object, identified by its UID
+     *
+     * @param string Object UID
+     * @return array The Kolab object represented as hash array
+     */
+    public function get_object($uid)
+    {
+        // synchronize caches
+        $this->cache->synchronize();
+
+        $msguid = $this->cache->uid2msguid($uid);
+        if ($msguid && ($object = $this->cache->get($msguid)))
+            return $object;
+
+        return false;
+    }
+
+
+    /**
+     * 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
+     * @return mixed  The attachment content as binary string
+     */
+    public function get_attachment($uid, $part, $mailbox = null)
+    {
+        if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
+            $this->imap->set_folder($mailbox ? $mailbox : $this->name);
+            return $this->imap->get_message_part($msguid, $part);
+        }
+
+        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 (!$type) $type = $this->type;
+        if (!$folder) $folder = $this->name;
+
+        $this->imap->set_folder($folder);
+
+        $headers = $this->imap->get_message_headers($msguid);
+
+        // Message doesn't exist?
+        if (empty($headers)) {
+            return false;
+        }
+
+        $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
+        $content_type  = kolab_format::KTYPE_PREFIX . $object_type;
+
+        // check object type header and abort on mismatch
+        if ($type != '*' && $object_type != $type)
+            return false;
+
+        $message = new rcube_message($msguid);
+        $attachments = array();
+
+        // get XML part
+        foreach ((array)$message->attachments as $part) {
+            if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
+                $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
+            }
+            else if ($part->filename || $part->content_id) {
+                $key = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
+                $attachments[$key] = array(
+                    'id' => $part->mime_id,
+                    'name' => $part->filename,
+                    'mimetype' => $part->mimetype,
+                    'size' => $part->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;
+        }
+
+        $format = kolab_format::factory($object_type);
+
+        if (is_a($format, 'PEAR_Error'))
+            return false;
+
+        // check kolab format version
+        $mime_version = $headers->others['x-kolab-mime-version'];
+        if (empty($mime_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)
+                $mime_version = 2.0;
+            else
+                $mime_version = 3.0; // assume 3.0
+        }
+
+        if ($mime_version <= 2.0) {
+            // read Kolab 2.0 format
+            $handler = class_exists('Horde_Kolab_Format') ? Horde_Kolab_Format::factory('XML', $xmltype, array('subtype' => $subtype)) : null;
+            if (!is_object($handler) || is_a($handler, 'PEAR_Error')) {
+                return false;
+            }
+
+            // XML-to-array
+            $object = $handler->load($xml);
+            $format->fromkolab2($object);
+        }
+        else {
+            // load Kolab 3 format using libkolabxml
+            $format->load($xml);
+        }
+
+        if ($format->is_valid()) {
+            $object = $format->to_array();
+            $object['_type'] = $object_type;
+            $object['_msguid'] = $msguid;
+            $object['_mailbox'] = $this->name;
+            $object['_attachments'] = array_merge((array)$object['_attachments'], $attachments);
+            $object['_formatobj'] = $format;
+
+            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);
+        }
+
+        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 boolean          True on success, false on error
+     */
+    public function save(&$object, $type = null, $uid = null)
+    {
+        if (!$type)
+            $type = $this->type;
+
+        // copy attachments from old message
+        if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $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 ($key == 'photo.attachment' && !isset($object['photo']) && !$object['_attachments'][$key]['content'] && $att['id']) {
+                    $object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
+                    unset($object['_attachments'][$key]);
+                }
+            }
+        }
+
+        // generate unique keys (used as content-id) for attachments
+        if (is_array($object['_attachments'])) {
+            $numatt = count($object['_attachments']);
+            foreach ($object['_attachments'] as $key => $attachment) {
+                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) . $ext;
+
+                    $object['_attachments'][$cid] = $attachment;
+                    unset($object['_attachments'][$key]);
+                }
+            }
+        }
+
+        if ($raw_msg = $this->build_message($object, $type)) {
+            $result = $this->imap->save_message($this->name, $raw_msg, '', false);
+
+            // delete old message
+            if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
+                $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
+                $this->cache->set($object['_msguid'], false, $object['_mailbox']);
+            }
+            else if ($result && $uid && ($msguid = $this->cache->uid2msguid($uid))) {
+                $this->imap->delete_message($msguid, $this->name);
+                $this->cache->set($object['_msguid'], false);
+            }
+
+            // update cache with new UID
+            if ($result) {
+                $object['_msguid'] = $result;
+                $this->cache->insert($result, $object);
+            }
+        }
+        
+        return $result;
+    }
+
+
+    /**
+     * 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)
+    {
+        $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
+        $success = false;
+
+        if ($msguid && $expunge) {
+            $success = $this->imap->delete_message($msguid, $this->name);
+        }
+        else if ($msguid) {
+            $success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
+        }
+
+        if ($success) {
+            $this->cache->set($msguid, false);
+        }
+
+        return $success;
+    }
+
+
+    /**
+     *
+     */
+    public function delete_all()
+    {
+        $this->cache->purge();
+        return $this->imap->clear_folder($this->name);
+    }
+
+
+    /**
+     * Restore a previously deleted object
+     *
+     * @param string Object UID
+     * @return mixed Message UID on success, false on error
+     */
+    public function undelete($uid)
+    {
+        if ($msguid = $this->cache->uid2msguid($uid, true)) {
+            if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) {
+                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 ($msguid = $this->cache->uid2msguid($uid)) {
+            if ($success = $this->imap->move_message($msguid, $target_folder, $this->name)) {
+                $this->cache->move($msguid, $uid, $target_folder);
+                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
+     */
+    private function build_message(&$object, $type)
+    {
+        // 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);
+
+        if (PEAR::isError($format))
+            return false;
+
+        $format->set($object);
+        $xml = $format->write();
+        $object['uid'] = $format->uid;  // read UID from format
+        $object['_formatobj'] = $format;
+
+        if (!$format->is_valid() || empty($object['uid'])) {
+            return false;
+        }
+
+        $mime = new Mail_mime("\r\n");
+        $rcmail = rcube::get_instance();
+        $headers = array();
+        $part_id = 1;
+
+        if ($ident = $rcmail->user->get_identity()) {
+            $headers['From'] = $ident['email'];
+            $headers['To'] = $ident['email'];
+        }
+        $headers['Date'] = date('r');
+        $headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
+        $headers['X-Kolab-Mime-Version'] = kolab_format::VERSION;
+        $headers['Subject'] = $object['uid'];
+//        $headers['Message-ID'] = $rcmail->gen_message_id();
+        $headers['User-Agent'] = $rcmail->config->get('useragent');
+
+        $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");
+
+        $mime->addAttachment($xml,  // file
+            $format->CTYPE,         // content-type
+            'kolab.xml',            // filename
+            false,                  // is_file
+            '8bit',                 // encoding
+            'attachment',           // disposition
+            RCMAIL_CHARSET          // charset
+        );
+        $part_id++;
+
+        // save object attachments as separate parts
+        // TODO: optimize memory consumption by using tempfiles for transfer
+        foreach ((array)$object['_attachments'] as $key => $att) {
+            if (empty($att['content']) && !empty($att['id'])) {
+                $msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
+                $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox']);
+            }
+
+            $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCMAIL_CHARSET, 'quoted-printable'));
+            $name = !empty($att['name']) ? $att['name'] : $key;
+
+            if (!empty($att['content'])) {
+                $mime->addAttachment($att['content'], $att['mimetype'], $name, false, 'base64', 'attachment', '', '', '', null, null, '', RCMAIL_CHARSET, $headers);
+                $part_id++;
+            }
+            else if (!empty($att['path'])) {
+                $mime->addAttachment($att['path'], $att['mimetype'], $name, true, 'base64', 'attachment', '', '', '', null, null, '', RCMAIL_CHARSET, $headers);
+                $part_id++;
+            }
+
+            $object['_attachments'][$key]['id'] = $part_id;
+        }
+
+        return $mime->getMessage();
+    }
+
+
+    /**
+     * 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(), $owner, $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)
+    {
+        require_once('HTTP/Request2.php');
+
+        try {
+            $rcmail = rcube::get_instance();
+            $request = new HTTP_Request2($url);
+            $request->setConfig(array('ssl_verify_peer' => $rcmail->config->get('kolab_ssl_verify_peer', true)));
+
+            // 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;
+    }
+
+}
+
diff --git a/lib/plugins/libkolab/libkolab.php b/lib/plugins/libkolab/libkolab.php
new file mode 100644
index 0000000..3709ee0
--- /dev/null
+++ b/lib/plugins/libkolab/libkolab.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * Kolab core library
+ *
+ * Plugin to setup a basic environment for the interaction with a Kolab server.
+ * Other Kolab-related plugins will depend on it and can use the library classes
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at 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 libkolab extends rcube_plugin
+{
+    /**
+     * Required startup method of a Roundcube plugin
+     */
+    public function init()
+    {
+        // load local config
+        $this->load_config();
+
+        $this->add_hook('storage_init', array($this, 'storage_init'));
+
+        // extend include path to load bundled lib classes
+        $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
+        set_include_path($include_path);
+
+        $rcmail = rcube::get_instance();
+        try {
+            kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
+        }
+        catch (Exception $e) {
+            raise_error($e, true);
+            kolab_format::$timezone = new DateTimeZone('GMT');
+        }
+
+        // load (old) dependencies if available
+        if (@include_once('Horde/Util.php')) {
+            include_once 'Horde/Kolab/Format.php';
+            include_once 'Horde/Kolab/Format/XML.php';
+            include_once 'Horde/Kolab/Format/XML/contact.php';
+            include_once 'Horde/Kolab/Format/XML/event.php';
+            include_once 'Horde_Kolab_Format_XML_configuration.php';
+
+            String::setDefaultCharset('UTF-8');
+        }
+    }
+
+    /**
+     * Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
+     */
+    function storage_init($p)
+    {
+        $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION');
+        return $p;
+    }
+
+
+}





More information about the commits mailing list