3 commits - plugins/calendar plugins/libcalendaring

Thomas Brüderli bruederli at kolabsys.com
Tue Jul 23 17:27:53 CEST 2013


 plugins/calendar/drivers/database/database_driver.php                   |    1 
 plugins/calendar/lib/Horde_Date.php                                     | 1305 +++
 plugins/libcalendaring/README                                           |   10 
 plugins/libcalendaring/lib/Horde_Date.php                               | 1304 ---
 plugins/libcalendaring/lib/Horde_iCalendar.php                          | 3300 ----------
 plugins/libcalendaring/lib/Horde_iCalendar_timezone.diff                |   23 
 plugins/libcalendaring/lib/Sabre/VObject/Component.php                  |  405 +
 plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php           |  108 
 plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php        |  244 
 plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php            |  107 
 plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php           |   70 
 plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php        |   68 
 plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php         |   46 
 plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php            |   68 
 plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php             |  181 
 plugins/libcalendaring/lib/Sabre/VObject/Document.php                   |  109 
 plugins/libcalendaring/lib/Sabre/VObject/ElementList.php                |  172 
 plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php          |  322 
 plugins/libcalendaring/lib/Sabre/VObject/Node.php                       |  187 
 plugins/libcalendaring/lib/Sabre/VObject/Parameter.php                  |  100 
 plugins/libcalendaring/lib/Sabre/VObject/ParseException.php             |   12 
 plugins/libcalendaring/lib/Sabre/VObject/Property.php                   |  442 +
 plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php          |  125 
 plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php          |  245 
 plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php     |  180 
 plugins/libcalendaring/lib/Sabre/VObject/Reader.php                     |  223 
 plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php         | 1112 +++
 plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php         |  111 
 plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php |   39 
 plugins/libcalendaring/lib/Sabre/VObject/Splitter/VCard.php             |   76 
 plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php                 |   61 
 plugins/libcalendaring/lib/Sabre/VObject/TimeZoneUtil.php               |  482 +
 plugins/libcalendaring/lib/Sabre/VObject/Version.php                    |   24 
 plugins/libcalendaring/lib/Sabre/VObject/includes.php                   |   41 
 plugins/libcalendaring/lib/get_horde_icalendar.sh                       |   31 
 plugins/libcalendaring/lib/get_sabre_vobject.sh                         |   10 
 plugins/libcalendaring/lib/sabre-vobject-2.1.0.tar.gz                   |binary
 plugins/libcalendaring/libvcalendar.php                                 | 1151 +--
 plugins/libcalendaring/tests/libvcalendar.php                           |   19 
 39 files changed, 7340 insertions(+), 5174 deletions(-)

New commits:
commit 3780f04f7143a1f7553821d7e1c7dc3429131632
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jul 23 17:27:25 2013 +0200

    Convert created date to DateTime object to make iCal export work properly

diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 0038218..e20b9f0 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -777,6 +777,7 @@ class database_driver extends calendar_driver
     $event['start'] = new DateTime($event['start']);
     $event['end'] = new DateTime($event['end']);
     $event['allday'] = intval($event['all_day']);
+    $event['created'] = new DateTime($event['created']);
     $event['changed'] = new DateTime($event['changed']);
     $event['free_busy'] = $free_busy_map[$event['free_busy']];
     $event['sensitivity'] = $sensitivity_map[$event['sensitivity']];


commit cc01865c95078382ba65ee33beff1a87e0a1fded
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jul 23 17:26:09 2013 +0200

    Move Horde_Date from libcalendaring to help with recurrence computation

diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
deleted file mode 120000
index 5cd5ce2..0000000
--- a/plugins/calendar/lib/Horde_Date.php
+++ /dev/null
@@ -1 +0,0 @@
-../../libcalendaring/lib/Horde_Date.php
\ No newline at end of file
diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
new file mode 100644
index 0000000..9197f84
--- /dev/null
+++ b/plugins/calendar/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);
+    }
+
+}


commit 489be2379ed97499973ed575b7fb804fe21d6522
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue Jul 23 17:14:11 2013 +0200

    Replace Horde_iCalendar with the SabreTooth VObject library

diff --git a/plugins/libcalendaring/README b/plugins/libcalendaring/README
index c49a82f..86e784d 100644
--- a/plugins/libcalendaring/README
+++ b/plugins/libcalendaring/README
@@ -7,9 +7,9 @@ Provides utility functions for calendar-related modules such as
 * attachment handling
 * iCal parsing and exporting
 
-iCal parsing is done with the help of the Horde_iCalendar class. A copy
-of that class with all its dependencies is part of this package. In order
-to update it, execute lib/get_horde_icalendar.sh > lib/Horde_iCalendar.php
-Finally apply the patch lib/Horde_iCalendar_timezone.diff to fix timezone
-handling when parsing iCal files.
+iCal parsing and exporting is done with the help of the Sabretooth VObject
+library [1]. A copy of that library with all its dependencies is part of this
+package. In order to update it, execute ./get_sabre_vobject.sh within the
+lib/ directory.
 
+[1]: https://github.com/fruux/sabre-vobject
diff --git a/plugins/libcalendaring/lib/Horde_Date.php b/plugins/libcalendaring/lib/Horde_Date.php
deleted file mode 100644
index 9197f84..0000000
--- a/plugins/libcalendaring/lib/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/plugins/libcalendaring/lib/Horde_iCalendar.php b/plugins/libcalendaring/lib/Horde_iCalendar.php
deleted file mode 100644
index 6d75d27..0000000
--- a/plugins/libcalendaring/lib/Horde_iCalendar.php
+++ /dev/null
@@ -1,3300 +0,0 @@
-<?php
-
-/**
- * This is a concatenated copy of the following files:
- *   Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
- */
-
-if (!class_exists('Horde_Date'))
-    require_once(dirname(__FILE__) . '/Horde_Date.php');
-
-
-$GLOBALS['_HORDE_STRING_CHARSET'] = 'iso-8859-1';
-
-/**
- * The String:: class provides static methods for charset and locale safe
- * string manipulation.
- *
- * $Horde: framework/Util/String.php,v 1.43.6.38 2009-09-15 16:36:14 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Jan Schneider <jan at horde.org>
- * @since   Horde 3.0
- * @package Horde_Util
- */
-class String {
-
-    /**
-     * Caches the result of extension_loaded() calls.
-     *
-     * @param string $ext  The extension name.
-     *
-     * @return boolean  Is the extension loaded?
-     *
-     * @see Util::extensionExists()
-     */
-    function extensionExists($ext)
-    {
-        static $cache = array();
-
-        if (!isset($cache[$ext])) {
-            $cache[$ext] = extension_loaded($ext);
-        }
-
-        return $cache[$ext];
-    }
-
-    /**
-     * Sets a default charset that the String:: methods will use if none is
-     * explicitly specified.
-     *
-     * @param string $charset  The charset to use as the default one.
-     */
-    function setDefaultCharset($charset)
-    {
-        $GLOBALS['_HORDE_STRING_CHARSET'] = $charset;
-        if (String::extensionExists('mbstring') &&
-            function_exists('mb_regex_encoding')) {
-            $old_error = error_reporting(0);
-            mb_regex_encoding(String::_mbstringCharset($charset));
-            error_reporting($old_error);
-        }
-    }
-
-    /**
-     * Converts a string from one charset to another.
-     *
-     * Works only if either the iconv or the mbstring extension
-     * are present and best if both are available.
-     * The original string is returned if conversion failed or none
-     * of the extensions were available.
-     *
-     * @param mixed $input  The data to be converted. If $input is an an array,
-     *                      the array's values get converted recursively.
-     * @param string $from  The string's current charset.
-     * @param string $to    The charset to convert the string to. If not
-     *                      specified, the global variable
-     *                      $_HORDE_STRING_CHARSET will be used.
-     *
-     * @return mixed  The converted input data.
-     */
-    function convertCharset($input, $from, $to = null)
-    {
-        /* Don't bother converting numbers. */
-        if (is_numeric($input)) {
-            return $input;
-        }
-
-        /* Get the user's default character set if none passed in. */
-        if (is_null($to)) {
-            $to = $GLOBALS['_HORDE_STRING_CHARSET'];
-        }
-
-        /* If the from and to character sets are identical, return now. */
-        if ($from == $to) {
-            return $input;
-        }
-        $from = String::lower($from);
-        $to = String::lower($to);
-        if ($from == $to) {
-            return $input;
-        }
-
-        if (is_array($input)) {
-            $tmp = array();
-            reset($input);
-            while (list($key, $val) = each($input)) {
-                $tmp[String::_convertCharset($key, $from, $to)] = String::convertCharset($val, $from, $to);
-            }
-            return $tmp;
-        }
-        if (is_object($input)) {
-            // PEAR_Error objects are almost guaranteed to contain recursion,
-            // which will cause a segfault in PHP.  We should never reach
-            // this line, but add a check and a log message to help the devs
-            // track down and fix this issue.
-            if (is_a($input, 'PEAR_Error')) {
-                Horde::logMessage('Called convertCharset() on a PEAR_Error object. ' . print_r($input, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
-                return '';
-            }
-            $vars = get_object_vars($input);
-            while (list($key, $val) = each($vars)) {
-                $input->$key = String::convertCharset($val, $from, $to);
-            }
-            return $input;
-        }
-
-        if (!is_string($input)) {
-            return $input;
-        }
-
-        return String::_convertCharset($input, $from, $to);
-    }
-
-    /**
-     * Internal function used to do charset conversion.
-     *
-     * @access private
-     *
-     * @param string $input  See String::convertCharset().
-     * @param string $from   See String::convertCharset().
-     * @param string $to     See String::convertCharset().
-     *
-     * @return string  The converted string.
-     */
-    function _convertCharset($input, $from, $to)
-    {
-        $output = '';
-        $from_check = (($from == 'iso-8859-1') || ($from == 'us-ascii'));
-        $to_check = (($to == 'iso-8859-1') || ($to == 'us-ascii'));
-
-        /* Use utf8_[en|de]code() if possible and if the string isn't too
-         * large (less than 16 MB = 16 * 1024 * 1024 = 16777216 bytes) - these
-         * functions use more memory. */
-        if (strlen($input) < 16777216 || !(String::extensionExists('iconv') || String::extensionExists('mbstring'))) {
-            if ($from_check && ($to == 'utf-8')) {
-                return utf8_encode($input);
-            }
-
-            if (($from == 'utf-8') && $to_check) {
-                return utf8_decode($input);
-            }
-        }
-
-        /* First try iconv with transliteration. */
-        if (($from != 'utf7-imap') &&
-            ($to != 'utf7-imap') &&
-            String::extensionExists('iconv')) {
-            /* We need to tack an extra character temporarily because of a bug
-             * in iconv() if the last character is not a 7 bit ASCII
-             * character. */
-            $oldTrackErrors = ini_set('track_errors', 1);
-            unset($php_errormsg);
-            $output = @iconv($from, $to . '//TRANSLIT', $input . 'x');
-            $output = (isset($php_errormsg)) ? false : String::substr($output, 0, -1, $to);
-            ini_set('track_errors', $oldTrackErrors);
-        }
-
-        /* Next try mbstring. */
-        if (!$output && String::extensionExists('mbstring')) {
-            $old_error = error_reporting(0);
-            $output = mb_convert_encoding($input, $to, String::_mbstringCharset($from));
-            error_reporting($old_error);
-        }
-
-        /* At last try imap_utf7_[en|de]code if appropriate. */
-        if (!$output && String::extensionExists('imap')) {
-            if ($from_check && ($to == 'utf7-imap')) {
-                return @imap_utf7_encode($input);
-            }
-            if (($from == 'utf7-imap') && $to_check) {
-                return @imap_utf7_decode($input);
-            }
-        }
-
-        return (!$output) ? $input : $output;
-    }
-
-    /**
-     * Makes a string lowercase.
-     *
-     * @param string  $string   The string to be converted.
-     * @param boolean $locale   If true the string will be converted based on a
-     *                          given charset, locale independent else.
-     * @param string  $charset  If $locale is true, the charset to use when
-     *                          converting. If not provided the current charset.
-     *
-     * @return string  The string with lowercase characters
-     */
-    function lower($string, $locale = false, $charset = null)
-    {
-        static $lowers;
-
-        if ($locale) {
-            /* The existence of mb_strtolower() depends on the platform. */
-            if (String::extensionExists('mbstring') &&
-                function_exists('mb_strtolower')) {
-                if (is_null($charset)) {
-                    $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-                }
-                $old_error = error_reporting(0);
-                $ret = mb_strtolower($string, String::_mbstringCharset($charset));
-                error_reporting($old_error);
-                if (!empty($ret)) {
-                    return $ret;
-                }
-            }
-            return strtolower($string);
-        }
-
-        if (!isset($lowers)) {
-            $lowers = array();
-        }
-        if (!isset($lowers[$string])) {
-            $language = setlocale(LC_CTYPE, 0);
-            setlocale(LC_CTYPE, 'C');
-            $lowers[$string] = strtolower($string);
-            setlocale(LC_CTYPE, $language);
-        }
-
-        return $lowers[$string];
-    }
-
-    /**
-     * Makes a string uppercase.
-     *
-     * @param string  $string   The string to be converted.
-     * @param boolean $locale   If true the string will be converted based on a
-     *                          given charset, locale independent else.
-     * @param string  $charset  If $locale is true, the charset to use when
-     *                          converting. If not provided the current charset.
-     *
-     * @return string  The string with uppercase characters
-     */
-    function upper($string, $locale = false, $charset = null)
-    {
-        static $uppers;
-
-        if ($locale) {
-            /* The existence of mb_strtoupper() depends on the
-             * platform. */
-            if (function_exists('mb_strtoupper')) {
-                if (is_null($charset)) {
-                    $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-                }
-                $old_error = error_reporting(0);
-                $ret = mb_strtoupper($string, String::_mbstringCharset($charset));
-                error_reporting($old_error);
-                if (!empty($ret)) {
-                    return $ret;
-                }
-            }
-            return strtoupper($string);
-        }
-
-        if (!isset($uppers)) {
-            $uppers = array();
-        }
-        if (!isset($uppers[$string])) {
-            $language = setlocale(LC_CTYPE, 0);
-            setlocale(LC_CTYPE, 'C');
-            $uppers[$string] = strtoupper($string);
-            setlocale(LC_CTYPE, $language);
-        }
-
-        return $uppers[$string];
-    }
-
-    /**
-     * Returns a string with the first letter capitalized if it is
-     * alphabetic.
-     *
-     * @param string  $string   The string to be capitalized.
-     * @param boolean $locale   If true the string will be converted based on a
-     *                          given charset, locale independent else.
-     * @param string  $charset  The charset to use, defaults to current charset.
-     *
-     * @return string  The capitalized string.
-     */
-    function ucfirst($string, $locale = false, $charset = null)
-    {
-        if ($locale) {
-            $first = String::substr($string, 0, 1, $charset);
-            if (String::isAlpha($first, $charset)) {
-                $string = String::upper($first, true, $charset) . String::substr($string, 1, null, $charset);
-            }
-        } else {
-            $string = String::upper(substr($string, 0, 1), false) . substr($string, 1);
-        }
-        return $string;
-    }
-
-    /**
-     * Returns part of a string.
-     *
-     * @param string $string   The string to be converted.
-     * @param integer $start   The part's start position, zero based.
-     * @param integer $length  The part's length.
-     * @param string $charset  The charset to use when calculating the part's
-     *                         position and length, defaults to current
-     *                         charset.
-     *
-     * @return string  The string's part.
-     */
-    function substr($string, $start, $length = null, $charset = null)
-    {
-        if (is_null($length)) {
-            $length = String::length($string, $charset) - $start;
-        }
-
-        if ($length == 0) {
-            return '';
-        }
-
-        /* Try iconv. */
-        if (function_exists('iconv_substr')) {
-            if (is_null($charset)) {
-                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-            }
-
-            $old_error = error_reporting(0);
-            $ret = iconv_substr($string, $start, $length, $charset);
-            error_reporting($old_error);
-            /* iconv_substr() returns false on failure. */
-            if ($ret !== false) {
-                return $ret;
-            }
-        }
-
-        /* Try mbstring. */
-        if (String::extensionExists('mbstring')) {
-            if (is_null($charset)) {
-                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-            }
-            $old_error = error_reporting(0);
-            $ret = mb_substr($string, $start, $length, String::_mbstringCharset($charset));
-            error_reporting($old_error);
-            /* mb_substr() returns empty string on failure. */
-            if (strlen($ret)) {
-                return $ret;
-            }
-        }
-
-        return substr($string, $start, $length);
-    }
-
-    /**
-     * Returns the character (not byte) length of a string.
-     *
-     * @param string $string  The string to return the length of.
-     * @param string $charset The charset to use when calculating the string's
-     *                        length.
-     *
-     * @return string  The string's part.
-     */
-    function length($string, $charset = null)
-    {
-        if (is_null($charset)) {
-            $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-        }
-        $charset = String::lower($charset);
-        if ($charset == 'utf-8' || $charset == 'utf8') {
-            return strlen(utf8_decode($string));
-        }
-        if (String::extensionExists('mbstring')) {
-            $old_error = error_reporting(0);
-            $ret = mb_strlen($string, String::_mbstringCharset($charset));
-            error_reporting($old_error);
-            if (!empty($ret)) {
-                return $ret;
-            }
-        }
-        return strlen($string);
-    }
-
-    /**
-     * Returns the numeric position of the first occurrence of $needle
-     * in the $haystack string.
-     *
-     * @param string $haystack  The string to search through.
-     * @param string $needle    The string to search for.
-     * @param integer $offset   Allows to specify which character in haystack
-     *                          to start searching.
-     * @param string $charset   The charset to use when searching for the
-     *                          $needle string.
-     *
-     * @return integer  The position of first occurrence.
-     */
-    function pos($haystack, $needle, $offset = 0, $charset = null)
-    {
-        if (String::extensionExists('mbstring')) {
-            if (is_null($charset)) {
-                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-            }
-            $track_errors = ini_set('track_errors', 1);
-            $old_error = error_reporting(0);
-            $ret = mb_strpos($haystack, $needle, $offset, String::_mbstringCharset($charset));
-            error_reporting($old_error);
-            ini_set('track_errors', $track_errors);
-            if (!isset($php_errormsg)) {
-                return $ret;
-            }
-        }
-        return strpos($haystack, $needle, $offset);
-    }
-
-    /**
-     * Returns a string padded to a certain length with another string.
-     *
-     * This method behaves exactly like str_pad but is multibyte safe.
-     *
-     * @param string $input    The string to be padded.
-     * @param integer $length  The length of the resulting string.
-     * @param string $pad      The string to pad the input string with. Must
-     *                         be in the same charset like the input string.
-     * @param const $type      The padding type. One of STR_PAD_LEFT,
-     *                         STR_PAD_RIGHT, or STR_PAD_BOTH.
-     * @param string $charset  The charset of the input and the padding
-     *                         strings.
-     *
-     * @return string  The padded string.
-     */
-    function pad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT,
-                 $charset = null)
-    {
-        $mb_length = String::length($input, $charset);
-        $sb_length = strlen($input);
-        $pad_length = String::length($pad, $charset);
-
-        /* Return if we already have the length. */
-        if ($mb_length >= $length) {
-            return $input;
-        }
-
-        /* Shortcut for single byte strings. */
-        if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
-            return str_pad($input, $length, $pad, $type);
-        }
-
-        switch ($type) {
-        case STR_PAD_LEFT:
-            $left = $length - $mb_length;
-            $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) . $input;
-            break;
-        case STR_PAD_BOTH:
-            $left = floor(($length - $mb_length) / 2);
-            $right = ceil(($length - $mb_length) / 2);
-            $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) .
-                $input .
-                String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
-            break;
-        case STR_PAD_RIGHT:
-            $right = $length - $mb_length;
-            $output = $input . String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
-            break;
-        }
-
-        return $output;
-    }
-
-    /**
-     * Wraps the text of a message.
-     *
-     * @since Horde 3.2
-     *
-     * @param string $string         String containing the text to wrap.
-     * @param integer $width         Wrap the string at this number of
-     *                               characters.
-     * @param string $break          Character(s) to use when breaking lines.
-     * @param boolean $cut           Whether to cut inside words if a line
-     *                               can't be wrapped.
-     * @param string $charset        Character set to use when breaking lines.
-     * @param boolean $line_folding  Whether to apply line folding rules per
-     *                               RFC 822 or similar. The correct break
-     *                               characters including leading whitespace
-     *                               have to be specified too.
-     *
-     * @return string  String containing the wrapped text.
-     */
-    function wordwrap($string, $width = 75, $break = "\n", $cut = false,
-                      $charset = null, $line_folding = false)
-    {
-        /* Get the user's default character set if none passed in. */
-        if (is_null($charset)) {
-            $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-        }
-        $charset = String::_mbstringCharset($charset);
-        $string = String::convertCharset($string, $charset, 'utf-8');
-        $wrapped = '';
-
-        while (String::length($string, 'utf-8') > $width) {
-            $line = String::substr($string, 0, $width, 'utf-8');
-            $string = String::substr($string, String::length($line, 'utf-8'), null, 'utf-8');
-            // Make sure didn't cut a word, unless we want hard breaks anyway.
-            if (!$cut && preg_match('/^(.+?)((\s|\r?\n).*)/us', $string, $match)) {
-                $line .= $match[1];
-                $string = $match[2];
-            }
-            // Wrap at existing line breaks.
-            if (preg_match('/^(.*?)(\r?\n)(.*)$/u', $line, $match)) {
-                $wrapped .= $match[1] . $match[2];
-                $string = $match[3] . $string;
-                continue;
-            }
-            // Wrap at the last colon or semicolon followed by a whitespace if
-            // doing line folding.
-            if ($line_folding &&
-                preg_match('/^(.*?)(;|:)(\s+.*)$/u', $line, $match)) {
-                $wrapped .= $match[1] . $match[2] . $break;
-                $string = $match[3] . $string;
-                continue;
-            }
-            // Wrap at the last whitespace of $line.
-            if ($line_folding) {
-                $sub = '(.+[^\s])';
-            } else {
-                $sub = '(.*)';
-            }
-            if (preg_match('/^' . $sub . '(\s+)(.*)$/u', $line, $match)) {
-                $wrapped .= $match[1] . $break;
-                $string = ($line_folding ? $match[2] : '') . $match[3] . $string;
-                continue;
-            }
-            // Hard wrap if necessary.
-            if ($cut) {
-                $wrapped .= $line . $break;
-                continue;
-            }
-            $wrapped .= $line;
-        }
-
-        return String::convertCharset($wrapped . $string, 'utf-8', $charset);
-    }
-
-    /**
-     * Wraps the text of a message.
-     *
-     * @param string $text        String containing the text to wrap.
-     * @param integer $length     Wrap $text at this number of characters.
-     * @param string $break_char  Character(s) to use when breaking lines.
-     * @param string $charset     Character set to use when breaking lines.
-     * @param boolean $quote      Ignore lines that are wrapped with the '>'
-     *                            character (RFC 2646)? If true, we don't
-     *                            remove any padding whitespace at the end of
-     *                            the string.
-     *
-     * @return string  String containing the wrapped text.
-     */
-    function wrap($text, $length = 80, $break_char = "\n", $charset = null,
-                  $quote = false)
-    {
-        $paragraphs = array();
-
-        foreach (preg_split('/\r?\n/', $text) as $input) {
-            if ($quote && (strpos($input, '>') === 0)) {
-                $line = $input;
-            } else {
-                /* We need to handle the Usenet-style signature line
-                 * separately; since the space after the two dashes is
-                 * REQUIRED, we don't want to trim the line. */
-                if ($input != '-- ') {
-                    $input = rtrim($input);
-                }
-                $line = String::wordwrap($input, $length, $break_char, false, $charset);
-            }
-
-            $paragraphs[] = $line;
-        }
-
-        return implode($break_char, $paragraphs);
-    }
-
-    /**
-     * Returns true if the every character in the parameter is an alphabetic
-     * character.
-     *
-     * @param $string   The string to test.
-     * @param $charset  The charset to use when testing the string.
-     *
-     * @return boolean  True if the parameter was alphabetic only.
-     */
-    function isAlpha($string, $charset = null)
-    {
-        if (!String::extensionExists('mbstring')) {
-            return ctype_alpha($string);
-        }
-
-        $charset = String::_mbstringCharset($charset);
-        $old_charset = mb_regex_encoding();
-        $old_error = error_reporting(0);
-
-        if ($charset != $old_charset) {
-            mb_regex_encoding($charset);
-        }
-        $alpha = !mb_ereg_match('[^[:alpha:]]', $string);
-        if ($charset != $old_charset) {
-            mb_regex_encoding($old_charset);
-        }
-
-        error_reporting($old_error);
-
-        return $alpha;
-    }
-
-    /**
-     * Returns true if ever character in the parameter is a lowercase letter in
-     * the current locale.
-     *
-     * @param $string   The string to test.
-     * @param $charset  The charset to use when testing the string.
-     *
-     * @return boolean  True if the parameter was lowercase.
-     */
-    function isLower($string, $charset = null)
-    {
-        return ((String::lower($string, true, $charset) === $string) &&
-                String::isAlpha($string, $charset));
-    }
-
-    /**
-     * Returns true if every character in the parameter is an uppercase letter
-     * in the current locale.
-     *
-     * @param string $string   The string to test.
-     * @param string $charset  The charset to use when testing the string.
-     *
-     * @return boolean  True if the parameter was uppercase.
-     */
-    function isUpper($string, $charset = null)
-    {
-        return ((String::upper($string, true, $charset) === $string) &&
-                String::isAlpha($string, $charset));
-    }
-
-    /**
-     * Performs a multibyte safe regex match search on the text provided.
-     *
-     * @since Horde 3.1
-     *
-     * @param string $text     The text to search.
-     * @param array $regex     The regular expressions to use, without perl
-     *                         regex delimiters (e.g. '/' or '|').
-     * @param string $charset  The character set of the text.
-     *
-     * @return array  The matches array from the first regex that matches.
-     */
-    function regexMatch($text, $regex, $charset = null)
-    {
-        if (!empty($charset)) {
-            $regex = String::convertCharset($regex, $charset, 'utf-8');
-            $text = String::convertCharset($text, $charset, 'utf-8');
-        }
-
-        $matches = array();
-        foreach ($regex as $val) {
-            if (preg_match('/' . $val . '/u', $text, $matches)) {
-                break;
-            }
-        }
-
-        if (!empty($charset)) {
-            $matches = String::convertCharset($matches, 'utf-8', $charset);
-        }
-
-        return $matches;
-    }
-
-    /**
-     * Workaround charsets that don't work with mbstring functions.
-     *
-     * @access private
-     *
-     * @param string $charset  The original charset.
-     *
-     * @return string  The charset to use with mbstring functions.
-     */
-    function _mbstringCharset($charset)
-    {
-        /* mbstring functions do not handle the 'ks_c_5601-1987' &
-         * 'ks_c_5601-1989' charsets. However, these charsets are used, for
-         * example, by various versions of Outlook to send Korean characters.
-         * Use UHC (CP949) encoding instead. See, e.g.,
-         * http://lists.w3.org/Archives/Public/ietf-charsets/2001AprJun/0030.html */
-        if (in_array(String::lower($charset), array('ks_c_5601-1987', 'ks_c_5601-1989'))) {
-            $charset = 'UHC';
-        }
-
-        return $charset;
-    }
-
-}
-
-
-
-/**
- * @package Horde_iCalendar
- */
-
-/**
- * String package
- */
-
-
-
-/**
- * Class representing iCalendar files.
- *
- * $Horde: framework/iCalendar/iCalendar.php,v 1.57.4.81 2010-11-10 14:34:25 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar {
-
-    /**
-     * The parent (containing) iCalendar object.
-     *
-     * @var Horde_iCalendar
-     */
-    var $_container = false;
-
-    /**
-     * The name/value pairs of attributes for this object (UID,
-     * DTSTART, etc.). Which are present depends on the object and on
-     * what kind of component it is.
-     *
-     * @var array
-     */
-    var $_attributes = array();
-
-    /**
-     * Any children (contained) iCalendar components of this object.
-     *
-     * @var array
-     */
-    var $_components = array();
-
-    /**
-     * According to RFC 2425, we should always use CRLF-terminated lines.
-     *
-     * @var string
-     */
-    var $_newline = "\r\n";
-
-    /**
-     * iCalendar format version (different behavior for 1.0 and 2.0
-     * especially with recurring events).
-     *
-     * @var string
-     */
-    var $_version;
-
-    function Horde_iCalendar($version = '2.0')
-    {
-        $this->_version = $version;
-        $this->setAttribute('VERSION', $version);
-    }
-
-    /**
-     * Return a reference to a new component.
-     *
-     * @param string          $type       The type of component to return
-     * @param Horde_iCalendar $container  A container that this component
-     *                                    will be associated with.
-     *
-     * @return object  Reference to a Horde_iCalendar_* object as specified.
-     *
-     * @static
-     */
-    function &newComponent($type, &$container)
-    {
-        $type = String::lower($type);
-        $class = 'Horde_iCalendar_' . $type;
-        if (!class_exists($class)) {
-            include 'Horde/iCalendar/' . $type . '.php';
-        }
-        if (class_exists($class)) {
-            $component = new $class();
-            if ($container !== false) {
-                $component->_container = &$container;
-                // Use version of container, not default set by component
-                // constructor.
-                $component->_version = $container->_version;
-            }
-        } else {
-            // Should return an dummy x-unknown type class here.
-            $component = false;
-        }
-
-        return $component;
-    }
-
-    /**
-     * Sets the value of an attribute.
-     *
-     * @param string $name     The name of the attribute.
-     * @param string $value    The value of the attribute.
-     * @param array $params    Array containing any addition parameters for
-     *                         this attribute.
-     * @param boolean $append  True to append the attribute, False to replace
-     *                         the first matching attribute found.
-     * @param array $values    Array representation of $value.  For
-     *                         comma/semicolon seperated lists of values.  If
-     *                         not set use $value as single array element.
-     */
-    function setAttribute($name, $value, $params = array(), $append = true,
-                          $values = false)
-    {
-        // Make sure we update the internal format version if
-        // setAttribute('VERSION', ...) is called.
-        if ($name == 'VERSION') {
-            $this->_version = $value;
-            if ($this->_container !== false) {
-                $this->_container->_version = $value;
-            }
-        }
-
-        if (!$values) {
-            $values = array($value);
-        }
-        $found = false;
-        if (!$append) {
-            foreach (array_keys($this->_attributes) as $key) {
-                if ($this->_attributes[$key]['name'] == String::upper($name)) {
-                    $this->_attributes[$key]['params'] = $params;
-                    $this->_attributes[$key]['value'] = $value;
-                    $this->_attributes[$key]['values'] = $values;
-                    $found = true;
-                    break;
-                }
-            }
-        }
-
-        if ($append || !$found) {
-            $this->_attributes[] = array(
-                'name'      => String::upper($name),
-                'params'    => $params,
-                'value'     => $value,
-                'values'    => $values
-            );
-        }
-    }
-
-    /**
-     * Sets parameter(s) for an (already existing) attribute.  The
-     * parameter set is merged into the existing set.
-     *
-     * @param string $name   The name of the attribute.
-     * @param array $params  Array containing any additional parameters for
-     *                       this attribute.
-     * @return boolean  True on success, false if no attribute $name exists.
-     */
-    function setParameter($name, $params = array())
-    {
-        $keys = array_keys($this->_attributes);
-        foreach ($keys as $key) {
-            if ($this->_attributes[$key]['name'] == $name) {
-                $this->_attributes[$key]['params'] =
-                    array_merge($this->_attributes[$key]['params'], $params);
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Get the value of an attribute.
-     *
-     * @param string $name     The name of the attribute.
-     * @param boolean $params  Return the parameters for this attribute instead
-     *                         of its value.
-     *
-     * @return mixed (object)  PEAR_Error if the attribute does not exist.
-     *               (string)  The value of the attribute.
-     *               (array)   The parameters for the attribute or
-     *                         multiple values for an attribute.
-     */
-    function getAttribute($name, $params = false)
-    {
-        $result = array();
-        foreach ($this->_attributes as $attribute) {
-            if ($attribute['name'] == $name) {
-                if ($params) {
-                    $result[] = $attribute['params'];
-                } else {
-                    $result[] = $attribute['value'];
-                }
-            }
-        }
-        if (!count($result)) {
-            require_once 'PEAR.php';
-            return PEAR::raiseError('Attribute "' . $name . '" Not Found');
-        } if (count($result) == 1 && !$params) {
-            return $result[0];
-        } else {
-            return $result;
-        }
-    }
-
-    /**
-     * Gets the values of an attribute as an array.  Multiple values
-     * are possible due to:
-     *
-     *  a) multiplce occurences of 'name'
-     *  b) (unsecapd) comma seperated lists.
-     *
-     * So for a vcard like "KEY:a,b\nKEY:c" getAttributesValues('KEY')
-     * will return array('a', 'b', 'c').
-     *
-     * @param string  $name    The name of the attribute.
-     * @return mixed (object)  PEAR_Error if the attribute does not exist.
-     *               (array)   Multiple values for an attribute.
-     */
-    function getAttributeValues($name)
-    {
-        $result = array();
-        foreach ($this->_attributes as $attribute) {
-            if ($attribute['name'] == $name) {
-                $result = array_merge($attribute['values'], $result);
-            }
-        }
-        if (!count($result)) {
-            return PEAR::raiseError('Attribute "' . $name . '" Not Found');
-        }
-        return $result;
-    }
-
-    /**
-     * Returns the value of an attribute, or a specified default value
-     * if the attribute does not exist.
-     *
-     * @param string $name    The name of the attribute.
-     * @param mixed $default  What to return if the attribute specified by
-     *                        $name does not exist.
-     *
-     * @return mixed (string) The value of $name.
-     *               (mixed)  $default if $name does not exist.
-     */
-    function getAttributeDefault($name, $default = '')
-    {
-        $value = $this->getAttribute($name);
-        return is_a($value, 'PEAR_Error') ? $default : $value;
-    }
-
-    /**
-     * Remove all occurences of an attribute.
-     *
-     * @param string $name  The name of the attribute.
-     */
-    function removeAttribute($name)
-    {
-        $keys = array_keys($this->_attributes);
-        foreach ($keys as $key) {
-            if ($this->_attributes[$key]['name'] == $name) {
-                unset($this->_attributes[$key]);
-            }
-        }
-    }
-
-    /**
-     * Get attributes for all tags or for a given tag.
-     *
-     * @param string $tag  Return attributes for this tag, or all attributes if
-     *                     not given.
-     *
-     * @return array  An array containing all the attributes and their types.
-     */
-    function getAllAttributes($tag = false)
-    {
-        if ($tag === false) {
-            return $this->_attributes;
-        }
-        $result = array();
-        foreach ($this->_attributes as $attribute) {
-            if ($attribute['name'] == $tag) {
-                $result[] = $attribute;
-            }
-        }
-        return $result;
-    }
-
-    /**
-     * Add a vCalendar component (eg vEvent, vTimezone, etc.).
-     *
-     * @param Horde_iCalendar $component  Component (subclass) to add.
-     */
-    function addComponent($component)
-    {
-        if (is_a($component, 'Horde_iCalendar')) {
-            $component->_container = &$this;
-            $this->_components[] = &$component;
-        }
-    }
-
-    /**
-     * Retrieve all the components.
-     *
-     * @return array  Array of Horde_iCalendar objects.
-     */
-    function getComponents()
-    {
-        return $this->_components;
-    }
-
-    function getType()
-    {
-        return 'vcalendar';
-    }
-
-    /**
-     * Return the classes (entry types) we have.
-     *
-     * @return array  Hash with class names Horde_iCalendar_xxx as keys
-     *                and number of components of this class as value.
-     */
-    function getComponentClasses()
-    {
-        $r = array();
-        foreach ($this->_components as $c) {
-            $cn = strtolower(get_class($c));
-            if (empty($r[$cn])) {
-                $r[$cn] = 1;
-            } else {
-                $r[$cn]++;
-            }
-        }
-
-        return $r;
-    }
-
-    /**
-     * Number of components in this container.
-     *
-     * @return integer  Number of components in this container.
-     */
-    function getComponentCount()
-    {
-        return count($this->_components);
-    }
-
-    /**
-     * Retrieve a specific component.
-     *
-     * @param integer $idx  The index of the object to retrieve.
-     *
-     * @return mixed    (boolean) False if the index does not exist.
-     *                  (Horde_iCalendar_*) The requested component.
-     */
-    function getComponent($idx)
-    {
-        if (isset($this->_components[$idx])) {
-            return $this->_components[$idx];
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Locates the first child component of the specified class, and returns a
-     * reference to it.
-     *
-     * @param string $type  The type of component to find.
-     *
-     * @return boolean|Horde_iCalendar_*  False if no subcomponent of the
-     *                                    specified class exists or a reference
-     *                                    to the requested component.
-     */
-    function &findComponent($childclass)
-    {
-        $childclass = 'Horde_iCalendar_' . String::lower($childclass);
-        $keys = array_keys($this->_components);
-        foreach ($keys as $key) {
-            if (is_a($this->_components[$key], $childclass)) {
-                return $this->_components[$key];
-            }
-        }
-
-        $component = false;
-        return $component;
-    }
-
-    /**
-     * Locates the first matching child component of the specified class, and
-     * returns a reference to it.
-     *
-     * @param string $childclass  The type of component to find.
-     * @param string $attribute   This attribute must be set in the component
-     *                            for it to match.
-     * @param string $value       Optional value that $attribute must match.
-     *
-     * @return boolean|Horde_iCalendar_*  False if no matching subcomponent of
-     *                                    the specified class exists, or a
-     *                                    reference to the requested component.
-     */
-    function &findComponentByAttribute($childclass, $attribute, $value = null)
-    {
-        $childclass = 'Horde_iCalendar_' . String::lower($childclass);
-        $keys = array_keys($this->_components);
-        foreach ($keys as $key) {
-            if (is_a($this->_components[$key], $childclass)) {
-                $attr = $this->_components[$key]->getAttribute($attribute);
-                if (is_a($attr, 'PEAR_Error')) {
-                    continue;
-                }
-                if ($value !== null && $value != $attr) {
-                    continue;
-                }
-                return $this->_components[$key];
-            }
-        }
-
-        $component = false;
-        return $component;
-    }
-
-    /**
-     * Clears the iCalendar object (resets the components and attributes
-     * arrays).
-     */
-    function clear()
-    {
-        $this->_components = array();
-        $this->_attributes = array();
-    }
-
-    /**
-     * Checks if entry is vcalendar 1.0, vcard 2.1 or vnote 1.1.
-     *
-     * These 'old' formats are defined by www.imc.org. The 'new' (non-old)
-     * formats icalendar 2.0 and vcard 3.0 are defined in rfc2426 and rfc2445
-     * respectively.
-     *
-     * @since Horde 3.1.2
-     */
-    function isOldFormat()
-    {
-        if ($this->getType() == 'vcard') {
-            return ($this->_version < 3);
-        }
-        if ($this->getType() == 'vNote') {
-            return ($this->_version < 2);
-        }
-        if ($this->_version >= 2) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Export as vCalendar format.
-     */
-    function exportvCalendar()
-    {
-        // Default values.
-        $requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
-        $requiredAttributes['METHOD'] = 'PUBLISH';
-
-        foreach ($requiredAttributes as $name => $default_value) {
-            if (is_a($this->getattribute($name), 'PEAR_Error')) {
-                $this->setAttribute($name, $default_value);
-            }
-        }
-
-        return $this->_exportvData('VCALENDAR');
-    }
-
-    /**
-     * Export this entry as a hash array with tag names as keys.
-     *
-     * @param boolean $paramsInKeys
-     *                If false, the operation can be quite lossy as the
-     *                parameters are ignored when building the array keys.
-     *                So if you export a vcard with
-     *                LABEL;TYPE=WORK:foo
-     *                LABEL;TYPE=HOME:bar
-     *                the resulting hash contains only one label field!
-     *                If set to true, array keys look like 'LABEL;TYPE=WORK'
-     * @return array  A hash array with tag names as keys.
-     */
-    function toHash($paramsInKeys = false)
-    {
-        $hash = array();
-        foreach ($this->_attributes as $a)  {
-            $k = $a['name'];
-            if ($paramsInKeys && is_array($a['params'])) {
-                foreach ($a['params'] as $p => $v) {
-                    $k .= ";$p=$v";
-                }
-            }
-            $hash[$k] = $a['value'];
-        }
-
-        return $hash;
-    }
-
-    /**
-     * Parses a string containing vCalendar data.
-     *
-     * @todo This method doesn't work well at all, if $base is VCARD.
-     *
-     * @param string $text     The data to parse.
-     * @param string $base     The type of the base object.
-     * @param string $charset  The encoding charset for $text. Defaults to
-     *                         utf-8 for new format, iso-8859-1 for old format.
-     * @param boolean $clear   If true clears the iCal object before parsing.
-     *
-     * @return boolean  True on successful import, false otherwise.
-     */
-    function parsevCalendar($text, $base = 'VCALENDAR', $charset = null,
-                            $clear = true)
-    {
-        if ($clear) {
-            $this->clear();
-        }
-        if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) {
-            $container = true;
-            $vCal = $matches[1];
-        } else {
-            // Text isn't enclosed in BEGIN:VCALENDAR
-            // .. END:VCALENDAR. We'll try to parse it anyway.
-            $container = false;
-            $vCal = $text;
-        }
-        $vCal = trim($vCal);
-
-        // Extract all subcomponents.
-        $matches = $components = null;
-        if (preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $components)) {
-            foreach ($components[0] as $key => $data) {
-                // Remove from the vCalendar data.
-                $vCal = str_replace($data, '', $vCal);
-            }
-        } elseif (!$container) {
-            return false;
-        }
-
-        // Unfold "quoted printable" folded lines like:
-        //  BODY;ENCODING=QUOTED-PRINTABLE:=
-        //  another=20line=
-        //  last=20line
-        while (preg_match_all('/^([^:]+;\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\r?\n)+(.*[^=])?\r?\n)/mU', $vCal, $matches)) {
-            foreach ($matches[1] as $s) {
-                $r = preg_replace('/=\r?\n/', '', $s);
-                $vCal = str_replace($s, $r, $vCal);
-            }
-        }
-
-        // Unfold any folded lines.
-        if ($this->isOldFormat()) {
-            $vCal = preg_replace('/[\r\n]+([ \t])/', '$1', $vCal);
-        } else {
-            $vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal);
-        }
-
-        // Parse the remaining attributes.
-        if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\r\n]*)\r?$/m', $vCal, $matches)) {
-            foreach ($matches[0] as $attribute) {
-                preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts);
-                $tag = trim(String::upper($parts[1]));
-                $value = $parts[4];
-                $params = array();
-
-                // Parse parameters.
-                if (!empty($parts[2])) {
-                    preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts);
-                    foreach ($param_parts[2] as $key => $paramName) {
-                        $paramName = String::upper($paramName);
-                        $paramValue = $param_parts[4][$key];
-                        if ($paramName == 'TYPE') {
-                            $paramValue = preg_split('/(?<!\\\\),/', $paramValue);
-                            if (count($paramValue) == 1) {
-                                $paramValue = $paramValue[0];
-                            }
-                        }
-                        if (is_string($paramValue)) {
-                            if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
-                                $paramValue = $parts[1];
-                            }
-                        } else {
-                            foreach ($paramValue as $k => $tmp) {
-                                if (preg_match('/"([^"]*)"/', $tmp, $parts)) {
-                                    $paramValue[$k] = $parts[1];
-                                }
-                            }
-                        }
-                        $params[$paramName] = $paramValue;
-                    }
-                }
-
-                // Charset and encoding handling.
-                if ((isset($params['ENCODING']) &&
-                     String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') ||
-                    isset($params['QUOTED-PRINTABLE'])) {
-
-                    $value = quoted_printable_decode($value);
-                    if (isset($params['CHARSET'])) {
-                        $value = String::convertCharset($value, $params['CHARSET']);
-                    } else {
-                        $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
-                    }
-                } elseif (isset($params['CHARSET'])) {
-                    $value = String::convertCharset($value, $params['CHARSET']);
-                } else {
-                    // As per RFC 2279, assume UTF8 if we don't have an
-                    // explicit charset parameter.
-                    $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
-                }
-
-                // Get timezone info for date fields from $params.
-                $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false;
-
-                switch ($tag) {
-                // Date fields.
-                case 'COMPLETED':
-                case 'CREATED':
-                case 'LAST-MODIFIED':
-                case 'X-MOZ-LASTACK': 
-                case 'X-MOZ-SNOOZE-TIME': 
-                    $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
-                    break;
-
-                case 'BDAY':
-                case 'X-SYNCJE-ANNIVERSARY':
-                case 'X-ANNIVERSARY':
-                    $this->setAttribute($tag, $this->_parseDate($value), $params);
-                    break;
-
-                case 'DTEND':
-                case 'DTSTART':
-                case 'DTSTAMP':
-                case 'DUE':
-                case 'AALARM':
-                case 'RECURRENCE-ID':
-                    // types like AALARM may contain additional data after a ;
-                    // ignore these.
-                    $ts = explode(';', $value);
-                    if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') {
-                        $this->setAttribute($tag, $this->_parseDate($ts[0]), $params);
-                    } else {
-                        $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params);
-                    }
-                    break;
-
-                case 'TRIGGER':
-                    if (isset($params['VALUE']) &&
-                        $params['VALUE'] == 'DATE-TIME') {
-                            $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
-                    } else {
-                        $this->setAttribute($tag, $this->_parseDuration($value), $params);
-                    }
-                    break;
-
-                // Comma seperated dates.
-                case 'EXDATE':
-                case 'RDATE':
-                    if (!strlen($value)) {
-                        break;
-                    }
-                    $dates = array();
-                    $separator = $this->isOldFormat() ? ';' : ',';
-                    preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values);
-
-                    foreach ($values[1] as $value) {
-                        $dates[] = $this->_parseDate($value);
-                    }
-                    $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates);
-                    break;
-
-                // Duration fields.
-                case 'DURATION':
-                    $this->setAttribute($tag, $this->_parseDuration($value), $params);
-                    break;
-
-                // Period of time fields.
-                case 'FREEBUSY':
-                    $periods = array();
-                    preg_match_all('/,([^,]*)/', ',' . $value, $values);
-                    foreach ($values[1] as $value) {
-                        $periods[] = $this->_parsePeriod($value);
-                    }
-
-                    $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods);
-                    break;
-
-                // UTC offset fields.
-                case 'TZOFFSETFROM':
-                case 'TZOFFSETTO':
-                    $this->setAttribute($tag, $this->_parseUtcOffset($value), $params);
-                    break;
-
-                // Integer fields.
-                case 'PERCENT-COMPLETE':
-                case 'PRIORITY':
-                case 'REPEAT':
-                case 'SEQUENCE':
-                    $this->setAttribute($tag, intval($value), $params);
-                    break;
-
-                // Geo fields.
-                case 'GEO':
-                    if ($this->isOldFormat()) {
-                        $floats = explode(',', $value);
-                        $value = array('latitude' => floatval($floats[1]),
-                                       'longitude' => floatval($floats[0]));
-                    } else {
-                        $floats = explode(';', $value);
-                        $value = array('latitude' => floatval($floats[0]),
-                                       'longitude' => floatval($floats[1]));
-                    }
-                    $this->setAttribute($tag, $value, $params);
-                    break;
-
-                // Recursion fields.
-                case 'EXRULE':
-                case 'RRULE':
-                    $this->setAttribute($tag, trim($value), $params);
-                    break;
-
-                // ADR, ORG and N are lists seperated by unescaped semicolons
-                // with a specific number of slots.
-                case 'ADR':
-                case 'N':
-                case 'ORG':
-                    $value = trim($value);
-                    // As of rfc 2426 2.4.2 semicolon, comma, and colon must
-                    // be escaped (comma is unescaped after splitting below).
-                    $value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
-                                         array($this->_newline, $this->_newline, ';', ':'),
-                                         $value);
-
-                    // Split by unescaped semicolons:
-                    $values = preg_split('/(?<!\\\\);/', $value);
-                    $value = str_replace('\\;', ';', $value);
-                    $values = str_replace('\\;', ';', $values);
-                    $this->setAttribute($tag, trim($value), $params, true, $values);
-                    break;
-
-                // String fields.
-                default:
-                    if ($this->isOldFormat()) {
-                        // vCalendar 1.0 and vCard 2.1 only escape semicolons
-                        // and use unescaped semicolons to create lists.
-                        $value = trim($value);
-                        // Split by unescaped semicolons:
-                        $values = preg_split('/(?<!\\\\);/', $value);
-                        $value = str_replace('\\;', ';', $value);
-                        $values = str_replace('\\;', ';', $values);
-                        $this->setAttribute($tag, trim($value), $params, true, $values);
-                    } else {
-                        $value = trim($value);
-                        // As of rfc 2426 2.4.2 semicolon, comma, and colon
-                        // must be escaped (comma is unescaped after splitting
-                        // below).
-                        $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'),
-                                             array($this->_newline, $this->_newline, ';', ':', '\\'),
-                                             $value);
-
-                        // Split by unescaped commas.
-                        $values = preg_split('/(?<!\\\\),/', $value);
-                        $value = str_replace('\\,', ',', $value);
-                        $values = str_replace('\\,', ',', $values);
-
-                        $this->setAttribute($tag, trim($value), $params, true, $values);
-                    }
-                    break;
-                }
-            }
-        }
-
-        // Process all components.
-        if ($components) {
-            // vTimezone components are processed first. They are
-            // needed to process vEvents that may use a TZID.
-            foreach ($components[0] as $key => $data) {
-                $type = trim($components[1][$key]);
-                if ($type != 'VTIMEZONE') {
-                    continue;
-                }
-                $component = &Horde_iCalendar::newComponent($type, $this);
-                if ($component === false) {
-                    return PEAR::raiseError("Unable to create object for type $type");
-                }
-                $component->parsevCalendar($data, $type, $charset);
-
-                $this->addComponent($component);
-            }
-
-            // Now process the non-vTimezone components.
-            foreach ($components[0] as $key => $data) {
-                $type = trim($components[1][$key]);
-                if ($type == 'VTIMEZONE') {
-                    continue;
-                }
-                $component = &Horde_iCalendar::newComponent($type, $this);
-                if ($component === false) {
-                    return PEAR::raiseError("Unable to create object for type $type");
-                }
-                $component->parsevCalendar($data, $type, $charset);
-
-                $this->addComponent($component);
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Export this component in vCal format.
-     *
-     * @param string $base  The type of the base object.
-     *
-     * @return string  vCal format data.
-     */
-    function _exportvData($base = 'VCALENDAR')
-    {
-        $result = 'BEGIN:' . String::upper($base) . $this->_newline;
-
-        // VERSION is not allowed for entries enclosed in VCALENDAR/ICALENDAR,
-        // as it is part of the enclosing VCALENDAR/ICALENDAR. See rfc2445
-        if ($base !== 'VEVENT' && $base !== 'VTODO' && $base !== 'VALARM' &&
-            $base !== 'VJOURNAL' && $base !== 'VFREEBUSY') {
-            // Ensure that version is the first attribute.
-            $result .= 'VERSION:' . $this->_version . $this->_newline;
-        }
-        foreach ($this->_attributes as $attribute) {
-            $name = $attribute['name'];
-            if ($name == 'VERSION') {
-                // Already done.
-                continue;
-            }
-
-            $params_str = '';
-            $params = $attribute['params'];
-            if ($params) {
-                foreach ($params as $param_name => $param_value) {
-                    /* Skip CHARSET for iCalendar 2.0 data, not allowed. */
-                    if ($param_name == 'CHARSET' && !$this->isOldFormat()) {
-                        continue;
-                    }
-                    /* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */
-                    if ($this->isOldFormat() &&
-                        $param_name == 'VALUE' && $param_value == 'DATE') {
-                        continue;
-                    }
-
-                    if ($param_value === null) {
-                        $params_str .= ";$param_name";
-                    } else {
-                        $len = strlen($param_value);
-                        $safe_value = '';
-                        $quote = false;
-                        for ($i = 0; $i < $len; ++$i) {
-                            $ord = ord($param_value[$i]);
-                            // Accept only valid characters.
-                            if ($ord == 9 || $ord == 32 || $ord == 33 ||
-                                ($ord >= 35 && $ord <= 126) ||
-                                $ord >= 128) {
-                                $safe_value .= $param_value[$i];
-                                // Characters above 128 do not need to be
-                                // quoted as per RFC2445 but Outlook requires
-                                // this.
-                                if ($ord == 44 || $ord == 58 || $ord == 59 ||
-                                    $ord >= 128) {
-                                    $quote = true;
-                                }
-                            }
-                        }
-                        if ($quote) {
-                            $safe_value = '"' . $safe_value . '"';
-                        }
-                        $params_str .= ";$param_name=$safe_value";
-                    }
-                }
-            }
-
-            $value = $attribute['value'];
-            switch ($name) {
-            // Date fields.
-            case 'COMPLETED':
-            case 'CREATED':
-            case 'DCREATED':
-            case 'LAST-MODIFIED':
-            case 'X-MOZ-LASTACK':
-            case 'X-MOZ-SNOOZE-TIME':
-                $value = $this->_exportDateTime($value);
-                break;
-
-            case 'DTEND':
-            case 'DTSTART':
-            case 'DTSTAMP':
-            case 'DUE':
-            case 'AALARM':
-            case 'RECURRENCE-ID':
-                if (isset($params['VALUE'])) {
-                    if ($params['VALUE'] == 'DATE') {
-                        // VCALENDAR 1.0 uses T000000 - T235959 for all day events:
-                        if ($this->isOldFormat() && $name == 'DTEND') {
-                            $d = new Horde_Date($value);
-                            $value = new Horde_Date(array(
-                                'year' => $d->year,
-                                'month' => $d->month,
-                                'mday' => $d->mday - 1));
-                            $value->correct();
-                            $value = $this->_exportDate($value, '235959');
-                        } else {
-                            $value = $this->_exportDate($value, '000000');
-                        }
-                    } else {
-                        $value = $this->_exportDateTime($value);
-                    }
-                } else {
-                    $value = $this->_exportDateTime($value);
-                }
-                break;
-
-            // Comma seperated dates.
-            case 'EXDATE':
-            case 'RDATE':
-                $dates = array();
-                foreach ($value as $date) {
-                    if (isset($params['VALUE'])) {
-                        if ($params['VALUE'] == 'DATE') {
-                            $dates[] = $this->_exportDate($date, '000000');
-                        } elseif ($params['VALUE'] == 'PERIOD') {
-                            $dates[] = $this->_exportPeriod($date);
-                        } else {
-                            $dates[] = $this->_exportDateTime($date);
-                        }
-                    } else {
-                        $dates[] = $this->_exportDateTime($date);
-                    }
-                }
-                $value = implode($this->isOldFormat() ? ';' : ',', $dates);
-                break;
-
-            case 'TRIGGER':
-                if (isset($params['VALUE'])) {
-                    if ($params['VALUE'] == 'DATE-TIME') {
-                        $value = $this->_exportDateTime($value);
-                    } elseif ($params['VALUE'] == 'DURATION') {
-                        $value = $this->_exportDuration($value);
-                    }
-                } else {
-                    $value = $this->_exportDuration($value);
-                }
-                break;
-
-            // Duration fields.
-            case 'DURATION':
-                $value = $this->_exportDuration($value);
-                break;
-
-            // Period of time fields.
-            case 'FREEBUSY':
-                $value_str = '';
-                foreach ($value as $period) {
-                    $value_str .= empty($value_str) ? '' : ',';
-                    $value_str .= $this->_exportPeriod($period);
-                }
-                $value = $value_str;
-                break;
-
-            // UTC offset fields.
-            case 'TZOFFSETFROM':
-            case 'TZOFFSETTO':
-                $value = $this->_exportUtcOffset($value);
-                break;
-
-            // Integer fields.
-            case 'PERCENT-COMPLETE':
-            case 'PRIORITY':
-            case 'REPEAT':
-            case 'SEQUENCE':
-                $value = "$value";
-                break;
-
-            // Geo fields.
-            case 'GEO':
-                if ($this->isOldFormat()) {
-                    $value = $value['longitude'] . ',' . $value['latitude'];
-                } else {
-                    $value = $value['latitude'] . ';' . $value['longitude'];
-                }
-                break;
-
-            // Recurrence fields.
-            case 'EXRULE':
-            case 'RRULE':
-                break;
-
-            default:
-                if ($this->isOldFormat()) {
-                    if (is_array($attribute['values']) &&
-                        count($attribute['values']) > 1) {
-                        $values = $attribute['values'];
-                        if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
-                            $glue = ';';
-                        } else {
-                            $glue = ',';
-                        }
-                        $values = str_replace(';', '\\;', $values);
-                        $value = implode($glue, $values);
-                    } else {
-                        /* vcard 2.1 and vcalendar 1.0 escape only
-                         * semicolons */
-                        $value = str_replace(';', '\\;', $value);
-                    }
-                    // Text containing newlines or ASCII >= 127 must be BASE64
-                    // or QUOTED-PRINTABLE encoded. Currently we use
-                    // QUOTED-PRINTABLE as default.
-                    if (preg_match("/[^\x20-\x7F]/", $value) &&
-                        empty($params['ENCODING']))  {
-                        $params['ENCODING'] = 'QUOTED-PRINTABLE';
-                        $params_str .= ';ENCODING=QUOTED-PRINTABLE';
-                        // Add CHARSET as well. At least the synthesis client
-                        // gets confused otherwise
-                        if (empty($params['CHARSET'])) {
-                            $params['CHARSET'] = 'UTF-8';
-                            $params_str .= ';CHARSET=' . $params['CHARSET'];
-                        }
-                    }
-                } else {
-                    if (is_array($attribute['values']) &&
-                        count($attribute['values'])) {
-                        $values = $attribute['values'];
-                        if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
-                            $glue = ';';
-                        } else {
-                            $glue = ',';
-                        }
-                        // As of rfc 2426 2.5 semicolon and comma must be
-                        // escaped.
-                        $values = str_replace(array('\\', ';', ','),
-                                              array('\\\\', '\\;', '\\,'),
-                                              $values);
-                        $value = implode($glue, $values);
-                    } else {
-                        // As of rfc 2426 2.5 semicolon and comma must be
-                        // escaped.
-                        $value = str_replace(array('\\', ';', ','),
-                                             array('\\\\', '\\;', '\\,'),
-                                             $value);
-                    }
-                    $value = preg_replace('/\r?\n/', '\n', $value);
-                }
-                break;
-            }
-
-            $value = str_replace("\r", '', $value);
-            if (!empty($params['ENCODING']) &&
-                $params['ENCODING'] == 'QUOTED-PRINTABLE' &&
-                strlen(trim($value))) {
-                $result .= $name . $params_str . ':'
-                    . str_replace('=0A', '=0D=0A',
-                                  $this->_quotedPrintableEncode($value))
-                    . $this->_newline;
-            } else {
-                $attr_string = $name . $params_str . ':' . $value;
-                if (!$this->isOldFormat()) {
-                    $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ',
-                                                    true, 'utf-8', true);
-                }
-                $result .= $attr_string . $this->_newline;
-            }
-        }
-
-        foreach ($this->_components as $component) {
-            $result .= $component->exportvCalendar();
-        }
-
-        return $result . 'END:' . $base . $this->_newline;
-    }
-
-    /**
-     * Parse a UTC Offset field.
-     */
-    function _parseUtcOffset($text)
-    {
-        $offset = array();
-        if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $timeParts)) {
-            $offset['ahead']  = (bool)($timeParts[1] == '+');
-            $offset['hour']   = intval($timeParts[2]);
-            $offset['minute'] = intval($timeParts[3]);
-            if (isset($timeParts[4])) {
-                $offset['second'] = intval($timeParts[4]);
-            }
-            return $offset;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Export a UTC Offset field.
-     */
-    function _exportUtcOffset($value)
-    {
-        $offset = $value['ahead'] ? '+' : '-';
-        $offset .= sprintf('%02d%02d',
-                           $value['hour'], $value['minute']);
-        if (isset($value['second'])) {
-            $offset .= sprintf('%02d', $value['second']);
-        }
-
-        return $offset;
-    }
-
-    /**
-     * Parse a Time Period field.
-     */
-    function _parsePeriod($text)
-    {
-        $periodParts = explode('/', $text);
-
-        $start = $this->_parseDateTime($periodParts[0]);
-
-        if ($duration = $this->_parseDuration($periodParts[1])) {
-            return array('start' => $start, 'duration' => $duration);
-        } elseif ($end = $this->_parseDateTime($periodParts[1])) {
-            return array('start' => $start, 'end' => $end);
-        }
-    }
-
-    /**
-     * Export a Time Period field.
-     */
-    function _exportPeriod($value)
-    {
-        $period = $this->_exportDateTime($value['start']);
-        $period .= '/';
-        if (isset($value['duration'])) {
-            $period .= $this->_exportDuration($value['duration']);
-        } else {
-            $period .= $this->_exportDateTime($value['end']);
-        }
-        return $period;
-    }
-
-    /**
-     * Grok the TZID and return an offset in seconds from UTC for this
-     * date and time.
-     */
-    function _parseTZID($date, $time, $tzid)
-    {
-        $vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid);
-        if (!$vtimezone) {
-            // use PHP's standard timezone db to determine tzoffset
-            try {
-                $tz = new DateTimeZone($tzid);
-                $dt = new DateTime('now', $tz);
-                $dt->setDate($date['year'], $date['month'], $date['mday']);
-                $dt->setTime($time['hour'], $time['minute'], $date['recond']);
-                return $tz->getOffset($dt);
-            }
-            catch (Exception $e) {
-                return false;
-            }
-        }
-
-        $change_times = array();
-        foreach ($vtimezone->getComponents() as $o) {
-            $t = $vtimezone->parseChild($o, $date['year']);
-            if ($t !== false) {
-                $change_times[] = $t;
-            }
-        }
-
-        if (!$change_times) {
-            return false;
-        }
-
-        sort($change_times);
-
-        // Time is arbitrarily based on UTC for comparison.
-        $t = @gmmktime($time['hour'], $time['minute'], $time['second'],
-                       $date['month'], $date['mday'], $date['year']);
-
-        if ($t < $change_times[0]['time']) {
-            return $change_times[0]['from'];
-        }
-
-        for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) {
-            if (($t >= $change_times[$i]['time']) &&
-                ($t < $change_times[$i + 1]['time'])) {
-                return $change_times[$i]['to'];
-            }
-        }
-
-        if ($t >= $change_times[$n - 1]['time']) {
-            return $change_times[$n - 1]['to'];
-        }
-
-        return false;
-    }
-
-    /**
-     * Parses a DateTime field and returns a unix timestamp. If the
-     * field cannot be parsed then the original text is returned
-     * unmodified.
-     *
-     * @todo This function should be moved to Horde_Date and made public.
-     */
-    function _parseDateTime($text, $tzid = false)
-    {
-        $dateParts = explode('T', $text);
-        if (count($dateParts) != 2 && !empty($text)) {
-            // Not a datetime field but may be just a date field.
-            if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
-                // Or not
-                return $text;
-            }
-            $newtext = $text.'T000000';
-            $dateParts = explode('T', $newtext);
-        }
-
-        if (!$date = Horde_iCalendar::_parseDate($dateParts[0])) {
-            return $text;
-        }
-        if (!$time = Horde_iCalendar::_parseTime($dateParts[1])) {
-            return $text;
-        }
-
-        // Get timezone info for date fields from $tzid and container.
-        $tzoffset = ($time['zone'] == 'Local' && $tzid && is_a($this->_container, 'Horde_iCalendar'))
-            ? $this->_parseTZID($date, $time, $tzid) : false;
-        if ($time['zone'] == 'UTC' || $tzoffset !== false) {
-            $result = @gmmktime($time['hour'], $time['minute'], $time['second'],
-                                $date['month'], $date['mday'], $date['year']);
-            if ($tzoffset) {
-                $result -= $tzoffset;
-            }
-        } else {
-            // We don't know the timezone so assume local timezone.
-            // FIXME: shouldn't this be based on the user's timezone
-            // preference rather than the server's timezone?
-            $result = @mktime($time['hour'], $time['minute'], $time['second'],
-                              $date['month'], $date['mday'], $date['year']);
-        }
-
-        return ($result !== false) ? $result : $text;
-    }
-
-    /**
-     * Export a DateTime field.
-     */
-    function _exportDateTime($value)
-    {
-        $temp = array();
-        if (!is_object($value) && !is_array($value)) {
-            $tz = date('O', $value);
-            $TZOffset = (3600 * substr($tz, 0, 3)) + (60 * substr($tz, 3, 2));
-            $value -= $TZOffset;
-
-            $temp['zone']   = 'UTC';
-            list($temp['year'], $temp['month'], $temp['mday'], $temp['hour'], $temp['minute'], $temp['second']) = explode('-', date('Y-n-j-G-i-s', $value));
-        } else {
-            $dateOb = new Horde_Date($value);
-            return Horde_iCalendar::_exportDateTime($dateOb->timestamp());
-        }
-
-        return Horde_iCalendar::_exportDate($temp) . 'T' . Horde_iCalendar::_exportTime($temp);
-    }
-
-    /**
-     * Parses a Time field.
-     *
-     * @static
-     */
-    function _parseTime($text)
-    {
-        if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) {
-            $time['hour'] = intval($timeParts[1]);
-            $time['minute'] = intval($timeParts[2]);
-            $time['second'] = intval($timeParts[3]);
-            if (isset($timeParts[4])) {
-                $time['zone'] = 'UTC';
-            } else {
-                $time['zone'] = 'Local';
-            }
-            return $time;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Exports a Time field.
-     */
-    function _exportTime($value)
-    {
-        $time = sprintf('%02d%02d%02d',
-                        $value['hour'], $value['minute'], $value['second']);
-        if ($value['zone'] == 'UTC') {
-            $time .= 'Z';
-        }
-        return $time;
-    }
-
-    /**
-     * Parses a Date field.
-     *
-     * @static
-     */
-    function _parseDate($text)
-    {
-        $parts = explode('T', $text);
-        if (count($parts) == 2) {
-            $text = $parts[0];
-        }
-
-        if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
-            return false;
-        }
-
-        return array('year' => $match[1],
-                     'month' => $match[2],
-                     'mday' => $match[3]);
-    }
-
-    /**
-     * Exports a date field.
-     *
-     * @param object|array $value  Date object or hash.
-     * @param string $autoconvert  If set, use this as time part to export the
-     *                             date as datetime when exporting to Vcalendar
-     *                             1.0. Examples: '000000' or '235959'
-     */
-    function _exportDate($value, $autoconvert = false)
-    {
-        if (is_object($value)) {
-            $value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday);
-        }
-        if ($autoconvert !== false && $this->isOldFormat()) {
-            return sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert);
-        } else {
-            return sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']);
-        }
-    }
-
-    /**
-     * Parse a Duration Value field.
-     */
-    function _parseDuration($text)
-    {
-        if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $durvalue)) {
-            // Weeks.
-            $duration = 7 * 86400 * intval($durvalue[3]);
-
-            if (count($durvalue) > 4) {
-                // Days.
-                $duration += 86400 * intval($durvalue[4]);
-            }
-            if (count($durvalue) > 5) {
-                // Hours.
-                $duration += 3600 * intval($durvalue[7]);
-
-                // Mins.
-                if (isset($durvalue[8])) {
-                    $duration += 60 * intval($durvalue[8]);
-                }
-
-                // Secs.
-                if (isset($durvalue[9])) {
-                    $duration += intval($durvalue[9]);
-                }
-            }
-
-            // Sign.
-            if ($durvalue[1] == "-") {
-                $duration *= -1;
-            }
-
-            return $duration;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Export a duration value.
-     */
-    function _exportDuration($value)
-    {
-        $duration = '';
-        if ($value < 0) {
-            $value *= -1;
-            $duration .= '-';
-        }
-        $duration .= 'P';
-
-        $weeks = floor($value / (7 * 86400));
-        $value = $value % (7 * 86400);
-        if ($weeks) {
-            $duration .= $weeks . 'W';
-        }
-
-        $days = floor($value / (86400));
-        $value = $value % (86400);
-        if ($days) {
-            $duration .= $days . 'D';
-        }
-
-        if ($value) {
-            $duration .= 'T';
-
-            $hours = floor($value / 3600);
-            $value = $value % 3600;
-            if ($hours) {
-                $duration .= $hours . 'H';
-            }
-
-            $mins = floor($value / 60);
-            $value = $value % 60;
-            if ($mins) {
-                $duration .= $mins . 'M';
-            }
-
-            if ($value) {
-                $duration .= $value . 'S';
-            }
-        }
-
-        return $duration;
-    }
-
-    /**
-     * Converts an 8bit string to a quoted-printable string according to RFC
-     * 2045, section 6.7.
-     *
-     * imap_8bit() does not apply all necessary rules.
-     *
-     * @param string $input  The string to be encoded.
-     *
-     * @return string  The quoted-printable encoded string.
-     */
-    function _quotedPrintableEncode($input = '')
-    {
-        $output = $line = '';
-        $len = strlen($input);
-
-        for ($i = 0; $i < $len; ++$i) {
-            $ord = ord($input[$i]);
-            // Encode non-printable characters (rule 2).
-            if ($ord == 9 ||
-                ($ord >= 32 && $ord <= 60) ||
-                ($ord >= 62 && $ord <= 126)) {
-                $chunk = $input[$i];
-            } else {
-                // Quoted printable encoding (rule 1).
-                $chunk = '=' . String::upper(sprintf('%02X', $ord));
-            }
-            $line .= $chunk;
-            // Wrap long lines (rule 5)
-            if (strlen($line) + 1 > 76) {
-                $line = String::wordwrap($line, 75, "=\r\n", true, 'us-ascii', true);
-                $newline = strrchr($line, "\r\n");
-                if ($newline !== false) {
-                    $output .= substr($line, 0, -strlen($newline) + 2);
-                    $line = substr($newline, 2);
-                } else {
-                    $output .= $line;
-                }
-                continue;
-            }
-            // Wrap at line breaks for better readability (rule 4).
-            if (substr($line, -3) == '=0A') {
-                $output .= $line . "=\r\n";
-                $line = '';
-            }
-        }
-        $output .= $line;
-
-        // Trailing whitespace must be encoded (rule 3).
-        $lastpos = strlen($output) - 1;
-        if ($output[$lastpos] == chr(9) ||
-            $output[$lastpos] == chr(32)) {
-            $output[$lastpos] = '=';
-            $output .= String::upper(sprintf('%02X', ord($output[$lastpos])));
-        }
-
-        return $output;
-    }
-
-}
-
-
-
-/**
- * Class representing vAlarms.
- *
- * $Horde: framework/iCalendar/iCalendar/valarm.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_valarm extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vAlarm';
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('VALARM');
-    }
-
-}
-
-/**
- * Class representing vEvents.
- *
- * $Horde: framework/iCalendar/iCalendar/vevent.php,v 1.31.10.16 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vevent extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vEvent';
-    }
-
-    function exportvCalendar()
-    {
-        // Default values.
-        $requiredAttributes = array();
-        $requiredAttributes['DTSTAMP'] = time();
-        $requiredAttributes['UID'] = $this->_exportDateTime(time())
-            . substr(str_pad(base_convert(microtime(), 10, 36), 16, uniqid(mt_rand()), STR_PAD_LEFT), -16)
-            . '@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost');
-
-        $method = !empty($this->_container) ?
-            $this->_container->getAttribute('METHOD') : 'PUBLISH';
-
-        switch ($method) {
-        case 'PUBLISH':
-            $requiredAttributes['DTSTART'] = time();
-            $requiredAttributes['SUMMARY'] = '';
-            break;
-
-        case 'REQUEST':
-            $requiredAttributes['ATTENDEE'] = '';
-            $requiredAttributes['DTSTART'] = time();
-            $requiredAttributes['SUMMARY'] = '';
-            break;
-
-        case 'REPLY':
-            $requiredAttributes['ATTENDEE'] = '';
-            break;
-
-        case 'ADD':
-            $requiredAttributes['DTSTART'] = time();
-            $requiredAttributes['SEQUENCE'] = 1;
-            $requiredAttributes['SUMMARY'] = '';
-            break;
-
-        case 'CANCEL':
-            $requiredAttributes['ATTENDEE'] = '';
-            $requiredAttributes['SEQUENCE'] = 1;
-            break;
-
-        case 'REFRESH':
-            $requiredAttributes['ATTENDEE'] = '';
-            break;
-        }
-
-        foreach ($requiredAttributes as $name => $default_value) {
-            if (is_a($this->getAttribute($name), 'PEAR_Error')) {
-                $this->setAttribute($name, $default_value);
-            }
-        }
-
-        return parent::_exportvData('VEVENT');
-    }
-
-    /**
-     * Update the status of an attendee of an event.
-     *
-     * @param $email    The email address of the attendee.
-     * @param $status   The participant status to set.
-     * @param $fullname The full name of the participant to set.
-     */
-    function updateAttendee($email, $status, $fullname = '')
-    {
-        foreach ($this->_attributes as $key => $attribute) {
-            if ($attribute['name'] == 'ATTENDEE' &&
-                $attribute['value'] == 'mailto:' . $email) {
-                $this->_attributes[$key]['params']['PARTSTAT'] = $status;
-                if (!empty($fullname)) {
-                    $this->_attributes[$key]['params']['CN'] = $fullname;
-                }
-                unset($this->_attributes[$key]['params']['RSVP']);
-                return;
-            }
-        }
-        $params = array('PARTSTAT' => $status);
-        if (!empty($fullname)) {
-            $params['CN'] = $fullname;
-        }
-        $this->setAttribute('ATTENDEE', 'mailto:' . $email, $params);
-    }
-
-    /**
-     * Return the organizer display name or email.
-     *
-     * @return string  The organizer name to display for this event.
-     */
-    function organizerName()
-    {
-        $organizer = $this->getAttribute('ORGANIZER', true);
-        if (is_a($organizer, 'PEAR_Error')) {
-            return _("An unknown person");
-        }
-
-        if (isset($organizer[0]['CN'])) {
-            return $organizer[0]['CN'];
-        }
-
-        $organizer = parse_url($this->getAttribute('ORGANIZER'));
-
-        return $organizer['path'];
-    }
-
-    /**
-     * Update this event with details from another event.
-     *
-     * @param Horde_iCalendar_vEvent $vevent  The vEvent with latest details.
-     */
-    function updateFromvEvent($vevent)
-    {
-        $newAttributes = $vevent->getAllAttributes();
-        foreach ($newAttributes as $newAttribute) {
-            $currentValue = $this->getAttribute($newAttribute['name']);
-            if (is_a($currentValue, 'PEAR_error')) {
-                // Already exists so just add it.
-                $this->setAttribute($newAttribute['name'],
-                                    $newAttribute['value'],
-                                    $newAttribute['params']);
-            } else {
-                // Already exists so locate and modify.
-                $found = false;
-
-                // Try matching the attribte name and value incase
-                // only the params changed (eg attendee updating
-                // status).
-                foreach ($this->_attributes as $id => $attr) {
-                    if ($attr['name'] == $newAttribute['name'] &&
-                        $attr['value'] == $newAttribute['value']) {
-                        // merge the params
-                        foreach ($newAttribute['params'] as $param_id => $param_name) {
-                            $this->_attributes[$id]['params'][$param_id] = $param_name;
-                        }
-                        $found = true;
-                        break;
-                    }
-                }
-                if (!$found) {
-                    // Else match the first attribute with the same
-                    // name (eg changing start time).
-                    foreach ($this->_attributes as $id => $attr) {
-                        if ($attr['name'] == $newAttribute['name']) {
-                            $this->_attributes[$id]['value'] = $newAttribute['value'];
-                            // Merge the params.
-                            foreach ($newAttribute['params'] as $param_id => $param_name) {
-                                $this->_attributes[$id]['params'][$param_id] = $param_name;
-                            }
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Update just the attendess of event with details from another
-     * event.
-     *
-     * @param Horde_iCalendar_vEvent $vevent  The vEvent with latest details
-     */
-    function updateAttendeesFromvEvent($vevent)
-    {
-        $newAttributes = $vevent->getAllAttributes();
-        foreach ($newAttributes as $newAttribute) {
-            if ($newAttribute['name'] != 'ATTENDEE') {
-                continue;
-            }
-            $currentValue = $this->getAttribute($newAttribute['name']);
-            if (is_a($currentValue, 'PEAR_error')) {
-                // Already exists so just add it.
-                $this->setAttribute($newAttribute['name'],
-                                    $newAttribute['value'],
-                                    $newAttribute['params']);
-            } else {
-                // Already exists so locate and modify.
-                $found = false;
-                // Try matching the attribte name and value incase
-                // only the params changed (eg attendee updating
-                // status).
-                foreach ($this->_attributes as $id => $attr) {
-                    if ($attr['name'] == $newAttribute['name'] &&
-                        $attr['value'] == $newAttribute['value']) {
-                        // Merge the params.
-                        foreach ($newAttribute['params'] as $param_id => $param_name) {
-                            $this->_attributes[$id]['params'][$param_id] = $param_name;
-                        }
-                        $found = true;
-                        break;
-                    }
-                }
-
-                if (!$found) {
-                    // Else match the first attribute with the same
-                    // name (eg changing start time).
-                    foreach ($this->_attributes as $id => $attr) {
-                        if ($attr['name'] == $newAttribute['name']) {
-                            $this->_attributes[$id]['value'] = $newAttribute['value'];
-                            // Merge the params.
-                            foreach ($newAttribute['params'] as $param_id => $param_name) {
-                                $this->_attributes[$id]['params'][$param_id] = $param_name;
-                            }
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-}
-
-/**
- * Class representing vFreebusy components.
- *
- * $Horde: framework/iCalendar/iCalendar/vfreebusy.php,v 1.16.10.18 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @todo Don't use timestamps
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vfreebusy extends Horde_iCalendar {
-
-    var $_busyPeriods = array();
-    var $_extraParams = array();
-
-    /**
-     * Returns the type of this calendar component.
-     *
-     * @return string  The type of this component.
-     */
-    function getType()
-    {
-        return 'vFreebusy';
-    }
-
-    /**
-     * Parses a string containing vFreebusy data.
-     *
-     * @param string $data     The data to parse.
-     */
-    function parsevCalendar($data, $type = null, $charset = null)
-    {
-        parent::parsevCalendar($data, 'VFREEBUSY', $charset);
-
-        // Do something with all the busy periods.
-        foreach ($this->_attributes as $key => $attribute) {
-            if ($attribute['name'] != 'FREEBUSY') {
-                continue;
-            }
-            foreach ($attribute['values'] as $value) {
-                $params = isset($attribute['params'])
-                    ? $attribute['params']
-                    : array();
-                if (isset($value['duration'])) {
-                    $this->addBusyPeriod('BUSY', $value['start'], null,
-                                         $value['duration'], $params);
-                } else {
-                    $this->addBusyPeriod('BUSY', $value['start'],
-                                         $value['end'], null, $params);
-                }
-            }
-            unset($this->_attributes[$key]);
-        }
-    }
-
-    /**
-     * Returns the component exported as string.
-     *
-     * @return string  The exported vFreeBusy information according to the
-     *                 iCalender format specification.
-     */
-    function exportvCalendar()
-    {
-        foreach ($this->_busyPeriods as $start => $end) {
-            $periods = array(array('start' => $start, 'end' => $end));
-            $this->setAttribute('FREEBUSY', $periods,
-                                isset($this->_extraParams[$start])
-                                ? $this->_extraParams[$start] : array());
-        }
-
-        $res = parent::_exportvData('VFREEBUSY');
-
-        foreach ($this->_attributes as $key => $attribute) {
-            if ($attribute['name'] == 'FREEBUSY') {
-                unset($this->_attributes[$key]);
-            }
-        }
-
-        return $res;
-    }
-
-    /**
-     * Returns a display name for this object.
-     *
-     * @return string  A clear text name for displaying this object.
-     */
-    function getName()
-    {
-        $name = '';
-        $method = !empty($this->_container) ?
-            $this->_container->getAttribute('METHOD') : 'PUBLISH';
-
-        if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
-            $attr = 'ORGANIZER';
-        } elseif ($method == 'REPLY') {
-            $attr = 'ATTENDEE';
-        }
-
-        $name = $this->getAttribute($attr, true);
-        if (!is_a($name, 'PEAR_Error') && isset($name[0]['CN'])) {
-            return $name[0]['CN'];
-        }
-
-        $name = $this->getAttribute($attr);
-        if (is_a($name, 'PEAR_Error')) {
-            return '';
-        } else {
-            $name = parse_url($name);
-            return $name['path'];
-        }
-    }
-
-    /**
-     * Returns the email address for this object.
-     *
-     * @return string  The email address of this object's owner.
-     */
-    function getEmail()
-    {
-        $name = '';
-        $method = !empty($this->_container)
-                  ? $this->_container->getAttribute('METHOD') : 'PUBLISH';
-
-        if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
-            $attr = 'ORGANIZER';
-        } elseif ($method == 'REPLY') {
-            $attr = 'ATTENDEE';
-        }
-
-        $name = $this->getAttribute($attr);
-        if (is_a($name, 'PEAR_Error')) {
-            return '';
-        } else {
-            $name = parse_url($name);
-            return $name['path'];
-        }
-    }
-
-    /**
-     * Returns the busy periods.
-     *
-     * @return array  All busy periods.
-     */
-    function getBusyPeriods()
-    {
-        return $this->_busyPeriods;
-    }
-
-    /**
-     * Returns any additional freebusy parameters.
-     *
-     * @return array  Additional parameters of the freebusy periods.
-     */
-    function getExtraParams()
-    {
-        return $this->_extraParams;
-    }
-
-    /**
-     * Returns all the free periods of time in a given period.
-     *
-     * @param integer $startStamp  The start timestamp.
-     * @param integer $endStamp    The end timestamp.
-     *
-     * @return array  A hash with free time periods, the start times as the
-     *                keys and the end times as the values.
-     */
-    function getFreePeriods($startStamp, $endStamp)
-    {
-        $this->simplify();
-        $periods = array();
-
-        // Check that we have data for some part of this period.
-        if ($this->getEnd() < $startStamp || $this->getStart() > $endStamp) {
-            return $periods;
-        }
-
-        // Locate the first time in the requested period we have data for.
-        $nextstart = max($startStamp, $this->getStart());
-
-        // Check each busy period and add free periods in between.
-        foreach ($this->_busyPeriods as $start => $end) {
-            if ($start <= $endStamp && $end >= $nextstart) {
-                if ($nextstart <= $start) {
-                    $periods[$nextstart] = min($start, $endStamp);
-                }
-                $nextstart = min($end, $endStamp);
-            }
-        }
-
-        // If we didn't read the end of the requested period but still have
-        // data then mark as free to the end of the period or available data.
-        if ($nextstart < $endStamp && $nextstart < $this->getEnd()) {
-            $periods[$nextstart] = min($this->getEnd(), $endStamp);
-        }
-
-        return $periods;
-    }
-
-    /**
-     * Adds a busy period to the info.
-     *
-     * This function may throw away data in case you add a period with a start
-     * date that already exists. The longer of the two periods will be chosen
-     * (and all information associated with the shorter one will be removed).
-     *
-     * @param string $type       The type of the period. Either 'FREE' or
-     *                           'BUSY'; only 'BUSY' supported at the moment.
-     * @param integer $start     The start timestamp of the period.
-     * @param integer $end       The end timestamp of the period.
-     * @param integer $duration  The duration of the period. If specified, the
-     *                           $end parameter will be ignored.
-     * @param array   $extra     Additional parameters for this busy period.
-     */
-    function addBusyPeriod($type, $start, $end = null, $duration = null,
-                           $extra = array())
-    {
-        if ($type == 'FREE') {
-            // Make sure this period is not marked as busy.
-            return false;
-        }
-
-        // Calculate the end time if duration was specified.
-        $tempEnd = is_null($duration) ? $end : $start + $duration;
-
-        // Make sure the period length is always positive.
-        $end = max($start, $tempEnd);
-        $start = min($start, $tempEnd);
-
-        if (isset($this->_busyPeriods[$start])) {
-            // Already a period starting at this time. Change the current
-            // period only if the new one is longer. This might be a problem
-            // if the callee assumes that there is no simplification going
-            // on. But since the periods are stored using the start time of
-            // the busy periods we have to throw away data here.
-            if ($end > $this->_busyPeriods[$start]) {
-                $this->_busyPeriods[$start] = $end;
-                $this->_extraParams[$start] = $extra;
-            }
-        } else {
-            // Add a new busy period.
-            $this->_busyPeriods[$start] = $end;
-            $this->_extraParams[$start] = $extra;
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns the timestamp of the start of the time period this free busy
-     * information covers.
-     *
-     * @return integer  A timestamp.
-     */
-    function getStart()
-    {
-        if (!is_a($this->getAttribute('DTSTART'), 'PEAR_Error')) {
-            return $this->getAttribute('DTSTART');
-        } elseif (count($this->_busyPeriods)) {
-            return min(array_keys($this->_busyPeriods));
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Returns the timestamp of the end of the time period this free busy
-     * information covers.
-     *
-     * @return integer  A timestamp.
-     */
-    function getEnd()
-    {
-        if (!is_a($this->getAttribute('DTEND'), 'PEAR_Error')) {
-            return $this->getAttribute('DTEND');
-        } elseif (count($this->_busyPeriods)) {
-            return max(array_values($this->_busyPeriods));
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Merges the busy periods of another Horde_iCalendar_vfreebusy object
-     * into this one.
-     *
-     * This might lead to simplification no matter what you specify for the
-     * "simplify" flag since periods with the same start date will lead to the
-     * shorter period being removed (see addBusyPeriod).
-     *
-     * @param Horde_iCalendar_vfreebusy $freebusy  A freebusy object.
-     * @param boolean $simplify                    If true, simplify() will
-     *                                             called after the merge.
-     */
-    function merge($freebusy, $simplify = true)
-    {
-        if (!is_a($freebusy, 'Horde_iCalendar_vfreebusy')) {
-            return false;
-        }
-
-        $extra = $freebusy->getExtraParams();
-        foreach ($freebusy->getBusyPeriods() as $start => $end) {
-            // This might simplify the busy periods without taking the
-            // "simplify" flag into account.
-            $this->addBusyPeriod('BUSY', $start, $end, null,
-                                 isset($extra[$start])
-                                 ? $extra[$start] : array());
-        }
-
-        $thisattr = $this->getAttribute('DTSTART');
-        $thatattr = $freebusy->getAttribute('DTSTART');
-        if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
-            $this->setAttribute('DTSTART', $thatattr, array(), false);
-        } elseif (!is_a($thatattr, 'PEAR_Error')) {
-            if ($thatattr < $thisattr) {
-                $this->setAttribute('DTSTART', $thatattr, array(), false);
-            }
-        }
-
-        $thisattr = $this->getAttribute('DTEND');
-        $thatattr = $freebusy->getAttribute('DTEND');
-        if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
-            $this->setAttribute('DTEND', $thatattr, array(), false);
-        } elseif (!is_a($thatattr, 'PEAR_Error')) {
-            if ($thatattr > $thisattr) {
-                $this->setAttribute('DTEND', $thatattr, array(), false);
-            }
-        }
-
-        if ($simplify) {
-            $this->simplify();
-        }
-
-        return true;
-    }
-
-    /**
-     * Removes all overlaps and simplifies the busy periods array as much as
-     * possible.
-     */
-    function simplify()
-    {
-        $clean = false;
-        $busy  = array($this->_busyPeriods, $this->_extraParams);
-        while (!$clean) {
-            $result = $this->_simplify($busy[0], $busy[1]);
-            $clean = $result === $busy;
-            $busy = $result;
-        }
-
-        ksort($result[1], SORT_NUMERIC);
-        $this->_extraParams = $result[1];
-
-        ksort($result[0], SORT_NUMERIC);
-        $this->_busyPeriods = $result[0];
-    }
-
-    function _simplify($busyPeriods, $extraParams = array())
-    {
-        $checked = array();
-        $checkedExtra = array();
-        $checkedEmpty = true;
-
-        foreach ($busyPeriods as $start => $end) {
-            if ($checkedEmpty) {
-                $checked[$start] = $end;
-                $checkedExtra[$start] = isset($extraParams[$start])
-                    ? $extraParams[$start] : array();
-                $checkedEmpty = false;
-            } else {
-                $added = false;
-                foreach ($checked as $testStart => $testEnd) {
-                    // Replace old period if the new period lies around the
-                    // old period.
-                    if ($start <= $testStart && $end >= $testEnd) {
-                        // Remove old period entry.
-                        unset($checked[$testStart]);
-                        unset($checkedExtra[$testStart]);
-                        // Add replacing entry.
-                        $checked[$start] = $end;
-                        $checkedExtra[$start] = isset($extraParams[$start])
-                            ? $extraParams[$start] : array();
-                        $added = true;
-                    } elseif ($start >= $testStart && $end <= $testEnd) {
-                        // The new period lies fully within the old
-                        // period. Just forget about it.
-                        $added = true;
-                    } elseif (($end <= $testEnd && $end >= $testStart) ||
-                              ($start >= $testStart && $start <= $testEnd)) {
-                        // Now we are in trouble: Overlapping time periods. If
-                        // we allow for additional parameters we cannot simply
-                        // choose one of the two parameter sets. It's better
-                        // to leave two separated time periods.
-                        $extra = isset($extraParams[$start])
-                            ? $extraParams[$start] : array();
-                        $testExtra = isset($checkedExtra[$testStart])
-                            ? $checkedExtra[$testStart] : array();
-                        // Remove old period entry.
-                        unset($checked[$testStart]);
-                        unset($checkedExtra[$testStart]);
-                        // We have two periods overlapping. Are their
-                        // additional parameters the same or different?
-                        $newStart = min($start, $testStart);
-                        $newEnd = max($end, $testEnd);
-                        if ($extra === $testExtra) {
-                            // Both periods have the same information. So we
-                            // can just merge.
-                            $checked[$newStart] = $newEnd;
-                            $checkedExtra[$newStart] = $extra;
-                        } else {
-                            // Extra parameters are different. Create one
-                            // period at the beginning with the params of the
-                            // first period and create a trailing period with
-                            // the params of the second period. The break
-                            // point will be the end of the first period.
-                            $break = min($end, $testEnd);
-                            $checked[$newStart] = $break;
-                            $checkedExtra[$newStart] =
-                                isset($extraParams[$newStart])
-                                ? $extraParams[$newStart] : array();
-                            $checked[$break] = $newEnd;
-                            $highStart = max($start, $testStart);
-                            $checkedExtra[$break] =
-                                isset($extraParams[$highStart])
-                                ? $extraParams[$highStart] : array();
-
-                            // Ensure we also have the extra data in the
-                            // extraParams.
-                            $extraParams[$break] =
-                                isset($extraParams[$highStart])
-                                ? $extraParams[$highStart] : array();
-                        }
-                        $added = true;
-                    }
-
-                    if ($added) {
-                        break;
-                    }
-                }
-
-                if (!$added) {
-                    $checked[$start] = $end;
-                    $checkedExtra[$start] = isset($extraParams[$start])
-                        ? $extraParams[$start] : array();
-                }
-            }
-        }
-
-        return array($checked, $checkedExtra);
-    }
-
-}
-
-/**
- * Class representing vJournals.
- *
- * $Horde: framework/iCalendar/iCalendar/vjournal.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vjournal extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vJournal';
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('VJOURNAL');
-    }
-
-}
-
-
-
-
-/**
- * Class representing vNotes.
- *
- * $Horde: framework/iCalendar/iCalendar/vnote.php,v 1.3.10.10 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @author  Karsten Fourmont <fourmont at gmx.de>
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vnote extends Horde_iCalendar {
-
-    function Horde_iCalendar_vnote($version = '1.1')
-    {
-        return parent::Horde_iCalendar($version);
-    }
-
-    function getType()
-    {
-        return 'vNote';
-    }
-
-    /**
-     * Unlike vevent and vtodo, a vnote is normally not enclosed in an
-     * iCalendar container. (BEGIN..END)
-     */
-    function exportvCalendar()
-    {
-        $requiredAttributes['BODY'] = '';
-        $requiredAttributes['VERSION'] = '1.1';
-
-        foreach ($requiredAttributes as $name => $default_value) {
-            if (is_a($this->getattribute($name), 'PEAR_Error')) {
-                $this->setAttribute($name, $default_value);
-            }
-        }
-
-        return $this->_exportvData('VNOTE');
-    }
-
-}
-
-/**
- * Class representing vTimezones.
- *
- * $Horde: framework/iCalendar/iCalendar/vtimezone.php,v 1.8.10.10 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vtimezone extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vTimeZone';
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('VTIMEZONE');
-    }
-
-    /**
-     * Parse child components of the vTimezone component. Returns an
-     * array with the exact time of the time change as well as the
-     * 'from' and 'to' offsets around the change. Time is arbitrarily
-     * based on UTC for comparison.
-     */
-    function parseChild(&$child, $year)
-    {
-        // Make sure 'time' key is first for sort().
-        $result['time'] = 0;
-
-        $t = $child->getAttribute('TZOFFSETFROM');
-        if (is_a($t, 'PEAR_Error')) {
-            return false;
-        }
-        $result['from'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
-
-        $t = $child->getAttribute('TZOFFSETTO');
-        if (is_a($t, 'PEAR_Error')) {
-            return false;
-        }
-        $result['to'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
-
-        $switch_time = $child->getAttribute('DTSTART');
-        if (is_a($switch_time, 'PEAR_Error')) {
-            return false;
-        }
-
-        $rrules = $child->getAttribute('RRULE');
-        if (is_a($rrules, 'PEAR_Error')) {
-            if (!is_int($switch_time)) {
-                return false;
-            }
-            // Convert this timestamp from local time to UTC for
-            // comparison (All dates are compared as if they are UTC).
-            $t = getdate($switch_time);
-            $result['time'] = @gmmktime($t['hours'], $t['minutes'], $t['seconds'],
-                                        $t['mon'], $t['mday'], $t['year']);
-            return $result;
-        }
-
-        $rrules = explode(';', $rrules);
-        foreach ($rrules as $rrule) {
-            $t = explode('=', $rrule);
-            switch ($t[0]) {
-            case 'FREQ':
-                if ($t[1] != 'YEARLY') {
-                    return false;
-                }
-                break;
-
-            case 'INTERVAL':
-                if ($t[1] != '1') {
-                    return false;
-                }
-                break;
-
-            case 'BYMONTH':
-                $month = intval($t[1]);
-                break;
-
-            case 'BYDAY':
-                $len = strspn($t[1], '1234567890-+');
-                if ($len == 0) {
-                    return false;
-                }
-                $weekday = substr($t[1], $len);
-                $weekdays = array(
-                    'SU' => 0,
-                    'MO' => 1,
-                    'TU' => 2,
-                    'WE' => 3,
-                    'TH' => 4,
-                    'FR' => 5,
-                    'SA' => 6
-                );
-                $weekday = $weekdays[$weekday];
-                $which = intval(substr($t[1], 0, $len));
-                break;
-
-            case 'UNTIL':
-                if (intval($year) > intval(substr($t[1], 0, 4))) {
-                    return false;
-                }
-                break;
-            }
-        }
-
-        if (empty($month) || !isset($weekday)) {
-            return false;
-        }
-
-        if (is_int($switch_time)) {
-            // Was stored as localtime.
-            $switch_time = strftime('%H:%M:%S', $switch_time);
-            $switch_time = explode(':', $switch_time);
-        } else {
-            $switch_time = explode('T', $switch_time);
-            if (count($switch_time) != 2) {
-                return false;
-            }
-            $switch_time[0] = substr($switch_time[1], 0, 2);
-            $switch_time[2] = substr($switch_time[1], 4, 2);
-            $switch_time[1] = substr($switch_time[1], 2, 2);
-        }
-
-        // Get the timestamp for the first day of $month.
-        $when = gmmktime($switch_time[0], $switch_time[1], $switch_time[2],
-                         $month, 1, $year);
-        // Get the day of the week for the first day of $month.
-        $first_of_month_weekday = intval(gmstrftime('%w', $when));
-
-        // Go to the first $weekday before first day of $month.
-        if ($weekday >= $first_of_month_weekday) {
-            $weekday -= 7;
-        }
-        $when -= ($first_of_month_weekday - $weekday) * 60 * 60 * 24;
-
-        // If going backwards go to the first $weekday after last day
-        // of $month.
-        if ($which < 0) {
-            do {
-                $when += 60*60*24*7;
-            } while (intval(gmstrftime('%m', $when)) == $month);
-        }
-
-        // Calculate $weekday number $which.
-        $when += $which * 60 * 60 * 24 * 7;
-
-        $result['time'] = $when;
-
-        return $result;
-    }
-
-}
-
-/**
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_standard extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'standard';
-    }
-
-    function parsevCalendar($data)
-    {
-        parent::parsevCalendar($data, 'STANDARD');
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('STANDARD');
-    }
-
-}
-
-/**
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_daylight extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'daylight';
-    }
-
-    function parsevCalendar($data)
-    {
-        parent::parsevCalendar($data, 'DAYLIGHT');
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('DAYLIGHT');
-    }
-
-}
-
-/**
- * Class representing vTodos.
- *
- * $Horde: framework/iCalendar/iCalendar/vtodo.php,v 1.13.10.9 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vtodo extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vTodo';
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('VTODO');
-    }
-
-    /**
-     * Convert this todo to an array of attributes.
-     *
-     * @return array  Array containing the details of the todo in a hash
-     *                as used by Horde applications.
-     */
-    function toArray()
-    {
-        $todo = array();
-
-        $name = $this->getAttribute('SUMMARY');
-        if (!is_array($name) && !is_a($name, 'PEAR_Error')) {
-            $todo['name'] = $name;
-        }
-        $desc = $this->getAttribute('DESCRIPTION');
-        if (!is_array($desc) && !is_a($desc, 'PEAR_Error')) {
-            $todo['desc'] = $desc;
-        }
-
-        $priority = $this->getAttribute('PRIORITY');
-        if (!is_array($priority) && !is_a($priority, 'PEAR_Error')) {
-            $todo['priority'] = $priority;
-        }
-
-        $due = $this->getAttribute('DTSTAMP');
-        if (!is_array($due) && !is_a($due, 'PEAR_Error')) {
-            $todo['due'] = $due;
-        }
-
-        return $todo;
-    }
-
-    /**
-     * Set the attributes for this todo item from an array.
-     *
-     * @param array $todo  Array containing the details of the todo in
-     *                     the same format that toArray() exports.
-     */
-    function fromArray($todo)
-    {
-        if (isset($todo['name'])) {
-            $this->setAttribute('SUMMARY', $todo['name']);
-        }
-        if (isset($todo['desc'])) {
-            $this->setAttribute('DESCRIPTION', $todo['desc']);
-        }
-
-        if (isset($todo['priority'])) {
-            $this->setAttribute('PRIORITY', $todo['priority']);
-        }
-
-        if (isset($todo['due'])) {
-            $this->setAttribute('DTSTAMP', $todo['due']);
-        }
-    }
-
-}
diff --git a/plugins/libcalendaring/lib/Horde_iCalendar_timezone.diff b/plugins/libcalendaring/lib/Horde_iCalendar_timezone.diff
deleted file mode 100644
index edd5587..0000000
--- a/plugins/libcalendaring/lib/Horde_iCalendar_timezone.diff
+++ /dev/null
@@ -1,23 +0,0 @@
-diff --git a/plugins/calendar/lib/Horde_iCalendar.php b/plugins/calendar/lib/Horde_iCalendar.php
-index a3ff79d..6d75d27 100644
---- a/lib/Horde_iCalendar.php
-+++ b/lib/Horde_iCalendar.php
-@@ -1874,7 +1874,17 @@ class Horde_iCalendar {
-     {
-         $vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid);
-         if (!$vtimezone) {
--            return false;
-+            // use PHP's standard timezone db to determine tzoffset
-+            try {
-+                $tz = new DateTimeZone($tzid);
-+                $dt = new DateTime('now', $tz);
-+                $dt->setDate($date['year'], $date['month'], $date['mday']);
-+                $dt->setTime($time['hour'], $time['minute'], $date['recond']);
-+                return $tz->getOffset($dt);
-+            }
-+            catch (Exception $e) {
-+                return false;
-+            }
-         }
- 
-         $change_times = array();
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component.php b/plugins/libcalendaring/lib/Sabre/VObject/Component.php
new file mode 100644
index 0000000..1c1d924
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component.php
@@ -0,0 +1,405 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * VObject Component
+ *
+ * This class represents a VCALENDAR/VCARD component. A component is for example
+ * VEVENT, VTODO and also VCALENDAR. It starts with BEGIN:COMPONENTNAME and
+ * ends with END:COMPONENTNAME
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Component extends Node {
+
+    /**
+     * Name, for example VEVENT
+     *
+     * @var string
+     */
+    public $name;
+
+    /**
+     * Children properties and components
+     *
+     * @var array
+     */
+    public $children = array();
+
+    /**
+     * If components are added to this map, they will be automatically mapped
+     * to their respective classes, if parsed by the reader or constructed with
+     * the 'create' method.
+     *
+     * @var array
+     */
+    static public $classMap = array(
+        'VALARM'        => 'Sabre\\VObject\\Component\\VAlarm',
+        'VCALENDAR'     => 'Sabre\\VObject\\Component\\VCalendar',
+        'VCARD'         => 'Sabre\\VObject\\Component\\VCard',
+        'VEVENT'        => 'Sabre\\VObject\\Component\\VEvent',
+        'VJOURNAL'      => 'Sabre\\VObject\\Component\\VJournal',
+        'VTODO'         => 'Sabre\\VObject\\Component\\VTodo',
+        'VFREEBUSY'     => 'Sabre\\VObject\\Component\\VFreeBusy',
+    );
+
+    /**
+     * Creates the new component by name, but in addition will also see if
+     * there's a class mapped to the property name.
+     *
+     * @param string $name
+     * @param string $value
+     * @return Component
+     */
+    static public function create($name, $value = null) {
+
+        $name = strtoupper($name);
+
+        if (isset(self::$classMap[$name])) {
+            return new self::$classMap[$name]($name, $value);
+        } else {
+            return new self($name, $value);
+        }
+
+    }
+
+    /**
+     * Creates a new component.
+     *
+     * By default this object will iterate over its own children, but this can
+     * be overridden with the iterator argument
+     *
+     * @param string $name
+     * @param ElementList $iterator
+     */
+    public function __construct($name, ElementList $iterator = null) {
+
+        $this->name = strtoupper($name);
+        if (!is_null($iterator)) $this->iterator = $iterator;
+
+    }
+
+    /**
+     * Turns the object back into a serialized blob.
+     *
+     * @return string
+     */
+    public function serialize() {
+
+        $str = "BEGIN:" . $this->name . "\r\n";
+
+        /**
+         * Gives a component a 'score' for sorting purposes.
+         *
+         * This is solely used by the childrenSort method.
+         *
+         * A higher score means the item will be lower in the list.
+         * To avoid score collisions, each "score category" has a reasonable
+         * space to accomodate elements. The $key is added to the $score to
+         * preserve the original relative order of elements.
+         *
+         * @param int $key
+         * @param array $array
+         * @return int
+         */
+        $sortScore = function($key, $array) {
+
+            if ($array[$key] instanceof Component) {
+
+                // We want to encode VTIMEZONE first, this is a personal
+                // preference.
+                if ($array[$key]->name === 'VTIMEZONE') {
+                    $score=300000000;
+                    return $score+$key;
+                } else {
+                    $score=400000000;
+                    return $score+$key;
+                }
+            } else {
+                // Properties get encoded first
+                // VCARD version 4.0 wants the VERSION property to appear first
+                if ($array[$key] instanceof Property) {
+                    if ($array[$key]->name === 'VERSION') {
+                        $score=100000000;
+                        return $score+$key;
+                    } else {
+                        // All other properties
+                        $score=200000000;
+                        return $score+$key;
+                    }
+                }
+            }
+
+        };
+
+        $tmp = $this->children;
+        uksort($this->children, function($a, $b) use ($sortScore, $tmp) {
+
+            $sA = $sortScore($a, $tmp);
+            $sB = $sortScore($b, $tmp);
+
+            if ($sA === $sB) return 0;
+
+            return ($sA < $sB) ? -1 : 1;
+
+        });
+
+        foreach($this->children as $child) $str.=$child->serialize();
+        $str.= "END:" . $this->name . "\r\n";
+
+        return $str;
+
+    }
+
+    /**
+     * Adds a new component or element
+     *
+     * You can call this method with the following syntaxes:
+     *
+     * add(Node $node)
+     * add(string $name, $value, array $parameters = array())
+     *
+     * The first version adds an Element
+     * The second adds a property as a string.
+     *
+     * @param mixed $item
+     * @param mixed $itemValue
+     * @return void
+     */
+    public function add($item, $itemValue = null, array $parameters = array()) {
+
+        if ($item instanceof Node) {
+            if (!is_null($itemValue)) {
+                throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node');
+            }
+            $item->parent = $this;
+            $this->children[] = $item;
+        } elseif(is_string($item)) {
+
+            $item = Property::create($item,$itemValue, $parameters);
+            $item->parent = $this;
+            $this->children[] = $item;
+
+        } else {
+
+            throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string');
+
+        }
+
+    }
+
+    /**
+     * Returns an iterable list of children
+     *
+     * @return ElementList
+     */
+    public function children() {
+
+        return new ElementList($this->children);
+
+    }
+
+    /**
+     * Returns an array with elements that match the specified name.
+     *
+     * This function is also aware of MIME-Directory groups (as they appear in
+     * vcards). This means that if a property is grouped as "HOME.EMAIL", it
+     * will also be returned when searching for just "EMAIL". If you want to
+     * search for a property in a specific group, you can select on the entire
+     * string ("HOME.EMAIL"). If you want to search on a specific property that
+     * has not been assigned a group, specify ".EMAIL".
+     *
+     * Keys are retained from the 'children' array, which may be confusing in
+     * certain cases.
+     *
+     * @param string $name
+     * @return array
+     */
+    public function select($name) {
+
+        $group = null;
+        $name = strtoupper($name);
+        if (strpos($name,'.')!==false) {
+            list($group,$name) = explode('.', $name, 2);
+        }
+
+        $result = array();
+        foreach($this->children as $key=>$child) {
+
+            if (
+                strtoupper($child->name) === $name &&
+                (is_null($group) || ( $child instanceof Property && strtoupper($child->group) === $group))
+            ) {
+
+                $result[$key] = $child;
+
+            }
+        }
+
+        reset($result);
+        return $result;
+
+    }
+
+    /**
+     * This method only returns a list of sub-components. Properties are
+     * ignored.
+     *
+     * @return array
+     */
+    public function getComponents() {
+
+        $result = array();
+        foreach($this->children as $child) {
+            if ($child instanceof Component) {
+                $result[] = $child;
+            }
+        }
+
+        return $result;
+
+    }
+
+    /**
+     * Validates the node for correctness.
+     *
+     * The following options are supported:
+     *   - Node::REPAIR - If something is broken, and automatic repair may
+     *                    be attempted.
+     *
+     * An array is returned with warnings.
+     *
+     * Every item in the array has the following properties:
+     *    * level - (number between 1 and 3 with severity information)
+     *    * message - (human readable message)
+     *    * node - (reference to the offending node)
+     *
+     * @param int $options
+     * @return array
+     */
+    public function validate($options = 0) {
+
+        $result = array();
+        foreach($this->children as $child) {
+            $result = array_merge($result, $child->validate($options));
+        }
+        return $result;
+
+    }
+
+    /* Magic property accessors {{{ */
+
+    /**
+     * Using 'get' you will either get a property or component,
+     *
+     * If there were no child-elements found with the specified name,
+     * null is returned.
+     *
+     * @param string $name
+     * @return Property
+     */
+    public function __get($name) {
+
+        $matches = $this->select($name);
+        if (count($matches)===0) {
+            return null;
+        } else {
+            $firstMatch = current($matches);
+            /** @var $firstMatch Property */
+            $firstMatch->setIterator(new ElementList(array_values($matches)));
+            return $firstMatch;
+        }
+
+    }
+
+    /**
+     * This method checks if a sub-element with the specified name exists.
+     *
+     * @param string $name
+     * @return bool
+     */
+    public function __isset($name) {
+
+        $matches = $this->select($name);
+        return count($matches)>0;
+
+    }
+
+    /**
+     * Using the setter method you can add properties or subcomponents
+     *
+     * You can either pass a Component, Property
+     * object, or a string to automatically create a Property.
+     *
+     * If the item already exists, it will be removed. If you want to add
+     * a new item with the same name, always use the add() method.
+     *
+     * @param string $name
+     * @param mixed $value
+     * @return void
+     */
+    public function __set($name, $value) {
+
+        $matches = $this->select($name);
+        $overWrite = count($matches)?key($matches):null;
+
+        if ($value instanceof Component || $value instanceof Property) {
+            $value->parent = $this;
+            if (!is_null($overWrite)) {
+                $this->children[$overWrite] = $value;
+            } else {
+                $this->children[] = $value;
+            }
+        } elseif (is_scalar($value)) {
+            $property = Property::create($name,$value);
+            $property->parent = $this;
+            if (!is_null($overWrite)) {
+                $this->children[$overWrite] = $property;
+            } else {
+                $this->children[] = $property;
+            }
+        } else {
+            throw new \InvalidArgumentException('You must pass a \\Sabre\\VObject\\Component, \\Sabre\\VObject\\Property or scalar type');
+        }
+
+    }
+
+    /**
+     * Removes all properties and components within this component.
+     *
+     * @param string $name
+     * @return void
+     */
+    public function __unset($name) {
+
+        $matches = $this->select($name);
+        foreach($matches as $k=>$child) {
+
+            unset($this->children[$k]);
+            $child->parent = null;
+
+        }
+
+    }
+
+    /* }}} */
+
+    /**
+     * This method is automatically called when the object is cloned.
+     * Specifically, this will ensure all child elements are also cloned.
+     *
+     * @return void
+     */
+    public function __clone() {
+
+        foreach($this->children as $key=>$child) {
+            $this->children[$key] = clone $child;
+            $this->children[$key]->parent = $this;
+        }
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php
new file mode 100644
index 0000000..2f86c44
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php
@@ -0,0 +1,108 @@
+<?php
+
+namespace Sabre\VObject\Component;
+use Sabre\VObject;
+
+/**
+ * VAlarm component
+ *
+ * This component contains some additional functionality specific for VALARMs.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VAlarm extends VObject\Component {
+
+    /**
+     * Returns a DateTime object when this alarm is going to trigger.
+     *
+     * This ignores repeated alarm, only the first trigger is returned.
+     *
+     * @return DateTime
+     */
+    public function getEffectiveTriggerTime() {
+
+        $trigger = $this->TRIGGER;
+        if(!isset($trigger['VALUE']) || strtoupper($trigger['VALUE']) === 'DURATION') {
+            $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER);
+            $related = (isset($trigger['RELATED']) && strtoupper($trigger['RELATED']) == 'END') ? 'END' : 'START';
+
+            $parentComponent = $this->parent;
+            if ($related === 'START') {
+
+                if ($parentComponent->name === 'VTODO') {
+                    $propName = 'DUE';
+                } else {
+                    $propName = 'DTSTART';
+                }
+
+                $effectiveTrigger = clone $parentComponent->$propName->getDateTime();
+                $effectiveTrigger->add($triggerDuration);
+            } else {
+                if ($parentComponent->name === 'VTODO') {
+                    $endProp = 'DUE';
+                } elseif ($parentComponent->name === 'VEVENT') {
+                    $endProp = 'DTEND';
+                } else {
+                    throw new \LogicException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT');
+                }
+
+                if (isset($parentComponent->$endProp)) {
+                    $effectiveTrigger = clone $parentComponent->$endProp->getDateTime();
+                    $effectiveTrigger->add($triggerDuration);
+                } elseif (isset($parentComponent->DURATION)) {
+                    $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
+                    $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION);
+                    $effectiveTrigger->add($duration);
+                    $effectiveTrigger->add($triggerDuration);
+                } else {
+                    $effectiveTrigger = clone $parentComponent->DTSTART->getDateTime();
+                    $effectiveTrigger->add($triggerDuration);
+                }
+            }
+        } else {
+            $effectiveTrigger = $trigger->getDateTime();
+        }
+        return $effectiveTrigger;
+
+    }
+
+    /**
+     * Returns true or false depending on if the event falls in the specified
+     * time-range. This is used for filtering purposes.
+     *
+     * The rules used to determine if an event falls within the specified
+     * time-range is based on the CalDAV specification.
+     *
+     * @param \DateTime $start
+     * @param \DateTime $end
+     * @return bool
+     */
+    public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+        $effectiveTrigger = $this->getEffectiveTriggerTime();
+
+        if (isset($this->DURATION)) {
+            $duration = VObject\DateTimeParser::parseDuration($this->DURATION);
+            $repeat = (string)$this->repeat;
+            if (!$repeat) {
+                $repeat = 1;
+            }
+
+            $period = new \DatePeriod($effectiveTrigger, $duration, (int)$repeat);
+
+            foreach($period as $occurrence) {
+
+                if ($start <= $occurrence && $end > $occurrence) {
+                    return true;
+                }
+            }
+            return false;
+        } else {
+            return ($start <= $effectiveTrigger && $end > $effectiveTrigger);
+        }
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php
new file mode 100644
index 0000000..9de6798
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php
@@ -0,0 +1,244 @@
+<?php
+
+namespace Sabre\VObject\Component;
+
+use Sabre\VObject;
+
+/**
+ * The VCalendar component
+ *
+ * This component adds functionality to a component, specific for a VCALENDAR.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VCalendar extends VObject\Document {
+
+    static $defaultName = 'VCALENDAR';
+
+    /**
+     * Returns a list of all 'base components'. For instance, if an Event has
+     * a recurrence rule, and one instance is overridden, the overridden event
+     * will have the same UID, but will be excluded from this list.
+     *
+     * VTIMEZONE components will always be excluded.
+     *
+     * @param string $componentName filter by component name
+     * @return array
+     */
+    public function getBaseComponents($componentName = null) {
+
+        $components = array();
+        foreach($this->children as $component) {
+
+            if (!$component instanceof VObject\Component)
+                continue;
+
+            if (isset($component->{'RECURRENCE-ID'}))
+                continue;
+
+            if ($componentName && $component->name !== strtoupper($componentName))
+                continue;
+
+            if ($component->name === 'VTIMEZONE')
+                continue;
+
+            $components[] = $component;
+
+        }
+
+        return $components;
+
+    }
+
+    /**
+     * If this calendar object, has events with recurrence rules, this method
+     * can be used to expand the event into multiple sub-events.
+     *
+     * Each event will be stripped from it's recurrence information, and only
+     * the instances of the event in the specified timerange will be left
+     * alone.
+     *
+     * In addition, this method will cause timezone information to be stripped,
+     * and normalized to UTC.
+     *
+     * This method will alter the VCalendar. This cannot be reversed.
+     *
+     * This functionality is specifically used by the CalDAV standard. It is
+     * possible for clients to request expand events, if they are rather simple
+     * clients and do not have the possibility to calculate recurrences.
+     *
+     * @param DateTime $start
+     * @param DateTime $end
+     * @return void
+     */
+    public function expand(\DateTime $start, \DateTime $end) {
+
+        $newEvents = array();
+
+        foreach($this->select('VEVENT') as $key=>$vevent) {
+
+            if (isset($vevent->{'RECURRENCE-ID'})) {
+                unset($this->children[$key]);
+                continue;
+            }
+
+
+            if (!$vevent->rrule) {
+                unset($this->children[$key]);
+                if ($vevent->isInTimeRange($start, $end)) {
+                    $newEvents[] = $vevent;
+                }
+                continue;
+            }
+
+            $uid = (string)$vevent->uid;
+            if (!$uid) {
+                throw new \LogicException('Event did not have a UID!');
+            }
+
+            $it = new VObject\RecurrenceIterator($this, $vevent->uid);
+            $it->fastForward($start);
+
+            while($it->valid() && $it->getDTStart() < $end) {
+
+                if ($it->getDTEnd() > $start) {
+
+                    $newEvents[] = $it->getEventObject();
+
+                }
+                $it->next();
+
+            }
+            unset($this->children[$key]);
+
+        }
+
+        foreach($newEvents as $newEvent) {
+
+            foreach($newEvent->children as $child) {
+                if ($child instanceof VObject\Property\DateTime &&
+                    $child->getDateType() == VObject\Property\DateTime::LOCALTZ) {
+                        $child->setDateTime($child->getDateTime(),VObject\Property\DateTime::UTC);
+                    }
+            }
+
+            $this->add($newEvent);
+
+        }
+
+        // Removing all VTIMEZONE components
+        unset($this->VTIMEZONE);
+
+    }
+
+    /**
+     * Validates the node for correctness.
+     * An array is returned with warnings.
+     *
+     * Every item in the array has the following properties:
+     *    * level - (number between 1 and 3 with severity information)
+     *    * message - (human readable message)
+     *    * node - (reference to the offending node)
+     *
+     * @return array
+     */
+    /*
+    public function validate() {
+
+        $warnings = array();
+
+        $version = $this->select('VERSION');
+        if (count($version)!==1) {
+            $warnings[] = array(
+                'level' => 1,
+                'message' => 'The VERSION property must appear in the VCALENDAR component exactly 1 time',
+                'node' => $this,
+            );
+        } else {
+            if ((string)$this->VERSION !== '2.0') {
+                $warnings[] = array(
+                    'level' => 1,
+                    'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.',
+                    'node' => $this,
+                );
+            }
+        }
+        $version = $this->select('PRODID');
+        if (count($version)!==1) {
+            $warnings[] = array(
+                'level' => 2,
+                'message' => 'The PRODID property must appear in the VCALENDAR component exactly 1 time',
+                'node' => $this,
+            );
+        }
+        if (count($this->CALSCALE) > 1) {
+            $warnings[] = array(
+                'level' => 2,
+                'message' => 'The CALSCALE property must not be specified more than once.',
+                'node' => $this,
+            );
+        }
+        if (count($this->METHOD) > 1) {
+            $warnings[] = array(
+                'level' => 2,
+                'message' => 'The METHOD property must not be specified more than once.',
+                'node' => $this,
+            );
+        }
+
+        $allowedComponents = array(
+            'VEVENT',
+            'VTODO',
+            'VJOURNAL',
+            'VFREEBUSY',
+            'VTIMEZONE',
+        );
+        $allowedProperties = array(
+            'PRODID',
+            'VERSION',
+            'CALSCALE',
+            'METHOD',
+        );
+        $componentsFound = 0;
+        foreach($this->children as $child) {
+            if($child instanceof Component) {
+                $componentsFound++;
+                if (!in_array($child->name, $allowedComponents)) {
+                    $warnings[] = array(
+                        'level' => 1,
+                        'message' => 'The ' . $child->name . " component is not allowed in the VCALENDAR component",
+                        'node' => $this,
+                    );
+                }
+            }
+            if ($child instanceof Property) {
+                if (!in_array($child->name, $allowedProperties)) {
+                    $warnings[] = array(
+                        'level' => 2,
+                        'message' => 'The ' . $child->name . " property is not allowed in the VCALENDAR component",
+                        'node' => $this,
+                    );
+                }
+            }
+        }
+
+        if ($componentsFound===0) {
+            $warnings[] = array(
+                'level' => 1,
+                'message' => 'An iCalendar object must have at least 1 component.',
+                'node' => $this,
+            );
+        }
+
+        return array_merge(
+            $warnings,
+            parent::validate()
+        );
+
+    }
+     */
+
+}
+
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php
new file mode 100644
index 0000000..0fc8b70
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php
@@ -0,0 +1,107 @@
+<?php
+
+namespace Sabre\VObject\Component;
+
+use Sabre\VObject;
+
+/**
+ * The VCard component
+ *
+ * This component represents the BEGIN:VCARD and END:VCARD found in every
+ * vcard.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VCard extends VObject\Component {
+
+    static $defaultName = 'VCARD';
+
+    /**
+     * VCards with version 2.1, 3.0 and 4.0 are found.
+     *
+     * If the VCARD doesn't know its version, 4.0 is assumed.
+     */
+    const DEFAULT_VERSION = '4.0';
+
+    /**
+     * Validates the node for correctness.
+     *
+     * The following options are supported:
+     *   - Node::REPAIR - If something is broken, and automatic repair may
+     *                    be attempted.
+     *
+     * An array is returned with warnings.
+     *
+     * Every item in the array has the following properties:
+     *    * level - (number between 1 and 3 with severity information)
+     *    * message - (human readable message)
+     *    * node - (reference to the offending node)
+     *
+     * @param int $options
+     * @return array
+     */
+    public function validate($options = 0) {
+
+        $warnings = array();
+
+        $version = $this->select('VERSION');
+        if (count($version)!==1) {
+            $warnings[] = array(
+                'level' => 1,
+                'message' => 'The VERSION property must appear in the VCARD component exactly 1 time',
+                'node' => $this,
+            );
+            if ($options & self::REPAIR) {
+                $this->VERSION = self::DEFAULT_VERSION;
+            }
+        } else {
+            $version = (string)$this->VERSION;
+            if ($version!=='2.1' && $version!=='3.0' && $version!=='4.0') {
+                $warnings[] = array(
+                    'level' => 1,
+                    'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
+                    'node' => $this,
+                );
+                if ($options & self::REPAIR) {
+                    $this->VERSION = '4.0';
+                }
+            }
+
+        }
+        $fn = $this->select('FN');
+        if (count($fn)!==1) {
+            $warnings[] = array(
+                'level' => 1,
+                'message' => 'The FN property must appear in the VCARD component exactly 1 time',
+                'node' => $this,
+            );
+            if (($options & self::REPAIR) && count($fn) === 0) {
+                // We're going to try to see if we can use the contents of the
+                // N property.
+                if (isset($this->N)) {
+                    $value = explode(';', (string)$this->N);
+                    if (isset($value[1]) && $value[1]) {
+                        $this->FN = $value[1] . ' ' . $value[0];
+                    } else {
+                        $this->FN = $value[0];
+                    }
+
+                // Otherwise, the ORG property may work
+                } elseif (isset($this->ORG)) {
+                    $this->FN = (string)$this->ORG;
+                }
+
+            }
+        }
+
+        return array_merge(
+            parent::validate($options),
+            $warnings
+        );
+
+    }
+
+}
+
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php
new file mode 100644
index 0000000..2375c53
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php
@@ -0,0 +1,70 @@
+<?php
+
+namespace Sabre\VObject\Component;
+use Sabre\VObject;
+
+/**
+ * VEvent component
+ *
+ * This component contains some additional functionality specific for VEVENT's.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VEvent extends VObject\Component {
+
+    /**
+     * Returns true or false depending on if the event falls in the specified
+     * time-range. This is used for filtering purposes.
+     *
+     * The rules used to determine if an event falls within the specified
+     * time-range is based on the CalDAV specification.
+     *
+     * @param \DateTime $start
+     * @param \DateTime $end
+     * @return bool
+     */
+    public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+        if ($this->RRULE) {
+            $it = new VObject\RecurrenceIterator($this);
+            $it->fastForward($start);
+
+            // We fast-forwarded to a spot where the end-time of the
+            // recurrence instance exceeded the start of the requested
+            // time-range.
+            //
+            // If the starttime of the recurrence did not exceed the
+            // end of the time range as well, we have a match.
+            return ($it->getDTStart() < $end && $it->getDTEnd() > $start);
+
+        }
+
+        $effectiveStart = $this->DTSTART->getDateTime();
+        if (isset($this->DTEND)) {
+
+            // The DTEND property is considered non inclusive. So for a 3 day
+            // event in july, dtstart and dtend would have to be July 1st and
+            // July 4th respectively.
+            //
+            // See:
+            // http://tools.ietf.org/html/rfc5545#page-54
+            $effectiveEnd = $this->DTEND->getDateTime();
+
+        } elseif (isset($this->DURATION)) {
+            $effectiveEnd = clone $effectiveStart;
+            $effectiveEnd->add( VObject\DateTimeParser::parseDuration($this->DURATION) );
+        } elseif ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
+            $effectiveEnd = clone $effectiveStart;
+            $effectiveEnd->modify('+1 day');
+        } else {
+            $effectiveEnd = clone $effectiveStart;
+        }
+        return (
+            ($start <= $effectiveEnd) && ($end > $effectiveStart)
+        );
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php
new file mode 100644
index 0000000..7afe9fd
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Sabre\VObject\Component;
+
+use Sabre\VObject;
+
+/**
+ * The VFreeBusy component
+ *
+ * This component adds functionality to a component, specific for VFREEBUSY
+ * components.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VFreeBusy extends VObject\Component {
+
+    /**
+     * Checks based on the contained FREEBUSY information, if a timeslot is
+     * available.
+     *
+     * @param DateTime $start
+     * @param Datetime $end
+     * @return bool
+     */
+    public function isFree(\DateTime $start, \Datetime $end) {
+
+        foreach($this->select('FREEBUSY') as $freebusy) {
+
+            // We are only interested in FBTYPE=BUSY (the default),
+            // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE.
+            if (isset($freebusy['FBTYPE']) && strtoupper(substr((string)$freebusy['FBTYPE'],0,4))!=='BUSY') {
+                continue;
+            }
+
+            // The freebusy component can hold more than 1 value, separated by
+            // commas.
+            $periods = explode(',', (string)$freebusy);
+
+            foreach($periods as $period) {
+                // Every period is formatted as [start]/[end]. The start is an
+                // absolute UTC time, the end may be an absolute UTC time, or
+                // duration (relative) value.
+                list($busyStart, $busyEnd) = explode('/', $period);
+
+                $busyStart = VObject\DateTimeParser::parse($busyStart);
+                $busyEnd = VObject\DateTimeParser::parse($busyEnd);
+                if ($busyEnd instanceof \DateInterval) {
+                    $tmp = clone $busyStart;
+                    $tmp->add($busyEnd);
+                    $busyEnd = $tmp;
+                }
+
+                if($start < $busyEnd && $end > $busyStart) {
+                    return false;
+                }
+
+            }
+
+        }
+
+        return true;
+
+    }
+
+}
+
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php
new file mode 100644
index 0000000..2328878
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Sabre\VObject\Component;
+
+use Sabre\VObject;
+
+/**
+ * VJournal component
+ *
+ * This component contains some additional functionality specific for VJOURNALs.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VJournal extends VObject\Component {
+
+    /**
+     * Returns true or false depending on if the event falls in the specified
+     * time-range. This is used for filtering purposes.
+     *
+     * The rules used to determine if an event falls within the specified
+     * time-range is based on the CalDAV specification.
+     *
+     * @param DateTime $start
+     * @param DateTime $end
+     * @return bool
+     */
+    public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+        $dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
+        if ($dtstart) {
+            $effectiveEnd = clone $dtstart;
+            if ($this->DTSTART->getDateType() == VObject\Property\DateTime::DATE) {
+                $effectiveEnd->modify('+1 day');
+            }
+
+            return ($start <= $effectiveEnd && $end > $dtstart);
+
+        }
+        return false;
+
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php b/plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php
new file mode 100644
index 0000000..b1579cf
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace Sabre\VObject\Component;
+
+use Sabre\VObject;
+
+/**
+ * VTodo component
+ *
+ * This component contains some additional functionality specific for VTODOs.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VTodo extends VObject\Component {
+
+    /**
+     * Returns true or false depending on if the event falls in the specified
+     * time-range. This is used for filtering purposes.
+     *
+     * The rules used to determine if an event falls within the specified
+     * time-range is based on the CalDAV specification.
+     *
+     * @param DateTime $start
+     * @param DateTime $end
+     * @return bool
+     */
+    public function isInTimeRange(\DateTime $start, \DateTime $end) {
+
+        $dtstart = isset($this->DTSTART)?$this->DTSTART->getDateTime():null;
+        $duration = isset($this->DURATION)?VObject\DateTimeParser::parseDuration($this->DURATION):null;
+        $due = isset($this->DUE)?$this->DUE->getDateTime():null;
+        $completed = isset($this->COMPLETED)?$this->COMPLETED->getDateTime():null;
+        $created = isset($this->CREATED)?$this->CREATED->getDateTime():null;
+
+        if ($dtstart) {
+            if ($duration) {
+                $effectiveEnd = clone $dtstart;
+                $effectiveEnd->add($duration);
+                return $start <= $effectiveEnd && $end > $dtstart;
+            } elseif ($due) {
+                return
+                    ($start < $due || $start <= $dtstart) &&
+                    ($end > $dtstart || $end >= $due);
+            } else {
+                return $start <= $dtstart && $end > $dtstart;
+            }
+        }
+        if ($due) {
+            return ($start < $due && $end >= $due);
+        }
+        if ($completed && $created) {
+            return
+                ($start <= $created || $start <= $completed) &&
+                ($end >= $created || $end >= $completed);
+        }
+        if ($completed) {
+            return ($start <= $completed && $end >= $completed);
+        }
+        if ($created) {
+            return ($end > $created);
+        }
+        return true;
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php b/plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php
new file mode 100644
index 0000000..0360050
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php
@@ -0,0 +1,181 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * DateTimeParser
+ *
+ * This class is responsible for parsing the several different date and time
+ * formats iCalendar and vCards have.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class DateTimeParser {
+
+    /**
+     * Parses an iCalendar (rfc5545) formatted datetime and returns a DateTime object
+     *
+     * Specifying a reference timezone is optional. It will only be used
+     * if the non-UTC format is used. The argument is used as a reference, the
+     * returned DateTime object will still be in the UTC timezone.
+     *
+     * @param string $dt
+     * @param DateTimeZone $tz
+     * @return DateTime
+     */
+    static public function parseDateTime($dt,\DateTimeZone $tz = null) {
+
+        // Format is YYYYMMDD + "T" + hhmmss
+        $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/',$dt,$matches);
+
+        if (!$result) {
+            throw new \LogicException('The supplied iCalendar datetime value is incorrect: ' . $dt);
+        }
+
+        if ($matches[7]==='Z' || is_null($tz)) {
+            $tz = new \DateTimeZone('UTC');
+        }
+        $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3] . ' ' . $matches[4] . ':' . $matches[5] .':' . $matches[6], $tz);
+
+        // Still resetting the timezone, to normalize everything to UTC
+        $date->setTimeZone(new \DateTimeZone('UTC'));
+        return $date;
+
+    }
+
+    /**
+     * Parses an iCalendar (rfc5545) formatted date and returns a DateTime object
+     *
+     * @param string $date
+     * @return DateTime
+     */
+    static public function parseDate($date) {
+
+        // Format is YYYYMMDD
+        $result = preg_match('/^([1-4][0-9]{3})([0-1][0-9])([0-3][0-9])$/',$date,$matches);
+
+        if (!$result) {
+            throw new \LogicException('The supplied iCalendar date value is incorrect: ' . $date);
+        }
+
+        $date = new \DateTime($matches[1] . '-' . $matches[2] . '-' . $matches[3], new \DateTimeZone('UTC'));
+        return $date;
+
+    }
+
+    /**
+     * Parses an iCalendar (RFC5545) formatted duration value.
+     *
+     * This method will either return a DateTimeInterval object, or a string
+     * suitable for strtotime or DateTime::modify.
+     *
+     * @param string $duration
+     * @param bool $asString
+     * @return DateInterval|string
+     */
+    static public function parseDuration($duration, $asString = false) {
+
+        $result = preg_match('/^(?P<plusminus>\+|-)?P((?P<week>\d+)W)?((?P<day>\d+)D)?(T((?P<hour>\d+)H)?((?P<minute>\d+)M)?((?P<second>\d+)S)?)?$/', $duration, $matches);
+        if (!$result) {
+            throw new \LogicException('The supplied iCalendar duration value is incorrect: ' . $duration);
+        }
+
+        if (!$asString) {
+            $invert = false;
+            if ($matches['plusminus']==='-') {
+                $invert = true;
+            }
+
+
+            $parts = array(
+                'week',
+                'day',
+                'hour',
+                'minute',
+                'second',
+            );
+            foreach($parts as $part) {
+                $matches[$part] = isset($matches[$part])&&$matches[$part]?(int)$matches[$part]:0;
+            }
+
+
+            // We need to re-construct the $duration string, because weeks and
+            // days are not supported by DateInterval in the same string.
+            $duration = 'P';
+            $days = $matches['day'];
+            if ($matches['week']) {
+                $days+=$matches['week']*7;
+            }
+            if ($days)
+                $duration.=$days . 'D';
+
+            if ($matches['minute'] || $matches['second'] || $matches['hour']) {
+                $duration.='T';
+
+                if ($matches['hour'])
+                    $duration.=$matches['hour'].'H';
+
+                if ($matches['minute'])
+                    $duration.=$matches['minute'].'M';
+
+                if ($matches['second'])
+                    $duration.=$matches['second'].'S';
+
+            }
+
+            if ($duration==='P') {
+                $duration = 'PT0S';
+            }
+            $iv = new \DateInterval($duration);
+            if ($invert) $iv->invert = true;
+
+            return $iv;
+
+        }
+
+
+
+        $parts = array(
+            'week',
+            'day',
+            'hour',
+            'minute',
+            'second',
+        );
+
+        $newDur = '';
+        foreach($parts as $part) {
+            if (isset($matches[$part]) && $matches[$part]) {
+                $newDur.=' '.$matches[$part] . ' ' . $part . 's';
+            }
+        }
+
+        $newDur = ($matches['plusminus']==='-'?'-':'+') . trim($newDur);
+        if ($newDur === '+') { $newDur = '+0 seconds'; };
+        return $newDur;
+
+    }
+
+    /**
+     * Parses either a Date or DateTime, or Duration value.
+     *
+     * @param string $date
+     * @param DateTimeZone|string $referenceTZ
+     * @return DateTime|DateInterval
+     */
+    static public function parse($date, $referenceTZ = null) {
+
+        if ($date[0]==='P' || ($date[0]==='-' && $date[1]==='P')) {
+            return self::parseDuration($date);
+        } elseif (strlen($date)===8) {
+            return self::parseDate($date);
+        } else {
+            return self::parseDateTime($date, $referenceTZ);
+        }
+
+    }
+
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Document.php b/plugins/libcalendaring/lib/Sabre/VObject/Document.php
new file mode 100644
index 0000000..50a662e
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Document.php
@@ -0,0 +1,109 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * Document
+ *
+ * A document is just like a component, except that it's also the top level
+ * element.
+ *
+ * Both a VCALENDAR and a VCARD are considered documents.
+ *
+ * This class also provides a registry for document types.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH. All rights reserved.
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+abstract class Document extends Component {
+
+    /**
+     * The default name for this component.
+     *
+     * This should be 'VCALENDAR' or 'VCARD'.
+     *
+     * @var string
+     */
+    static $defaultName;
+
+    /**
+     * Creates a new document.
+     *
+     * We're changing the default behavior slightly here. First, we don't want
+     * to have to specify a name (we already know it), and we want to allow
+     * children to be specified in the first argument.
+     *
+     * But, the default behavior also works.
+     *
+     * So the two sigs:
+     *
+     * new Document(array $children = array());
+     * new Document(string $name, array $children = array())
+     *
+     * @return void
+     */
+    public function __construct() {
+
+        $args = func_get_args();
+        if (count($args)===0 || is_array($args[0])) {
+            array_unshift($args, static::$defaultName);
+            call_user_func_array(array('parent', '__construct'), $args);
+        } else {
+            call_user_func_array(array('parent', '__construct'), $args);
+        }
+
+    }
+
+    /**
+     * Creates a new component
+     *
+     * This method automatically searches for the correct component class, based
+     * on its name.
+     *
+     * You can specify the children either in key=>value syntax, in which case
+     * properties will automatically be created, or you can just pass a list of
+     * Component and Property object.
+     *
+     * @param string $name
+     * @param array $children
+     * @return Component
+     */
+    public function createComponent($name, array $children = array()) {
+
+        $component = Component::create($name);
+        foreach($children as $k=>$v) {
+
+            if ($v instanceof Node) {
+                $component->add($v);
+            } else {
+                $component->add($k, $v);
+            }
+
+        }
+        return $component;
+
+    }
+
+    /**
+     * Factory method for creating new properties
+     *
+     * This method automatically searches for the correct property class, based
+     * on its name.
+     *
+     * You can specify the parameters either in key=>value syntax, in which case
+     * parameters will automatically be created, or you can just pass a list of
+     * Parameter objects.
+     *
+     * @param string $name
+     * @param mixed $value
+     * @param array $parameters
+     * @return Property
+     */
+    public function createProperty($name, $value = null, array $parameters = array()) {
+
+        return Property::create($name, $value, $parameters);
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/ElementList.php b/plugins/libcalendaring/lib/Sabre/VObject/ElementList.php
new file mode 100644
index 0000000..1c20370
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/ElementList.php
@@ -0,0 +1,172 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * VObject ElementList
+ *
+ * This class represents a list of elements. Lists are the result of queries,
+ * such as doing $vcalendar->vevent where there's multiple VEVENT objects.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class ElementList implements \Iterator, \Countable, \ArrayAccess {
+
+    /**
+     * Inner elements
+     *
+     * @var array
+     */
+    protected $elements = array();
+
+    /**
+     * Creates the element list.
+     *
+     * @param array $elements
+     */
+    public function __construct(array $elements) {
+
+        $this->elements = $elements;
+
+    }
+
+    /* {{{ Iterator interface */
+
+    /**
+     * Current position
+     *
+     * @var int
+     */
+    private $key = 0;
+
+    /**
+     * Returns current item in iteration
+     *
+     * @return Element
+     */
+    public function current() {
+
+        return $this->elements[$this->key];
+
+    }
+
+    /**
+     * To the next item in the iterator
+     *
+     * @return void
+     */
+    public function next() {
+
+        $this->key++;
+
+    }
+
+    /**
+     * Returns the current iterator key
+     *
+     * @return int
+     */
+    public function key() {
+
+        return $this->key;
+
+    }
+
+    /**
+     * Returns true if the current position in the iterator is a valid one
+     *
+     * @return bool
+     */
+    public function valid() {
+
+        return isset($this->elements[$this->key]);
+
+    }
+
+    /**
+     * Rewinds the iterator
+     *
+     * @return void
+     */
+    public function rewind() {
+
+        $this->key = 0;
+
+    }
+
+    /* }}} */
+
+    /* {{{ Countable interface */
+
+    /**
+     * Returns the number of elements
+     *
+     * @return int
+     */
+    public function count() {
+
+        return count($this->elements);
+
+    }
+
+    /* }}} */
+
+    /* {{{ ArrayAccess Interface */
+
+
+    /**
+     * Checks if an item exists through ArrayAccess.
+     *
+     * @param int $offset
+     * @return bool
+     */
+    public function offsetExists($offset) {
+
+        return isset($this->elements[$offset]);
+
+    }
+
+    /**
+     * Gets an item through ArrayAccess.
+     *
+     * @param int $offset
+     * @return mixed
+     */
+    public function offsetGet($offset) {
+
+        return $this->elements[$offset];
+
+    }
+
+    /**
+     * Sets an item through ArrayAccess.
+     *
+     * @param int $offset
+     * @param mixed $value
+     * @return void
+     */
+    public function offsetSet($offset,$value) {
+
+        throw new \LogicException('You can not add new objects to an ElementList');
+
+    }
+
+    /**
+     * Sets an item through ArrayAccess.
+     *
+     * This method just forwards the request to the inner iterator
+     *
+     * @param int $offset
+     * @return void
+     */
+    public function offsetUnset($offset) {
+
+        throw new \LogicException('You can not remove objects from an ElementList');
+
+    }
+
+    /* }}} */
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php b/plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php
new file mode 100644
index 0000000..96d0be5
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php
@@ -0,0 +1,322 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * This class helps with generating FREEBUSY reports based on existing sets of
+ * objects.
+ *
+ * It only looks at VEVENT and VFREEBUSY objects from the sourcedata, and
+ * generates a single VFREEBUSY object.
+ *
+ * VFREEBUSY components are described in RFC5545, The rules for what should
+ * go in a single freebusy report is taken from RFC4791, section 7.10.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class FreeBusyGenerator {
+
+    /**
+     * Input objects
+     *
+     * @var array
+     */
+    protected $objects;
+
+    /**
+     * Start of range
+     *
+     * @var DateTime|null
+     */
+    protected $start;
+
+    /**
+     * End of range
+     *
+     * @var DateTime|null
+     */
+    protected $end;
+
+    /**
+     * VCALENDAR object
+     *
+     * @var Component
+     */
+    protected $baseObject;
+
+    /**
+     * Creates the generator.
+     *
+     * Check the setTimeRange and setObjects methods for details about the
+     * arguments.
+     *
+     * @param DateTime $start
+     * @param DateTime $end
+     * @param mixed $objects
+     * @return void
+     */
+    public function __construct(\DateTime $start = null, \DateTime $end = null, $objects = null) {
+
+        if ($start && $end) {
+            $this->setTimeRange($start, $end);
+        }
+
+        if ($objects) {
+            $this->setObjects($objects);
+        }
+
+    }
+
+    /**
+     * Sets the VCALENDAR object.
+     *
+     * If this is set, it will not be generated for you. You are responsible
+     * for setting things like the METHOD, CALSCALE, VERSION, etc..
+     *
+     * The VFREEBUSY object will be automatically added though.
+     *
+     * @param Component $vcalendar
+     * @return void
+     */
+    public function setBaseObject(Component $vcalendar) {
+
+        $this->baseObject = $vcalendar;
+
+    }
+
+    /**
+     * Sets the input objects
+     *
+     * You must either specify a valendar object as a strong, or as the parse
+     * Component.
+     * It's also possible to specify multiple objects as an array.
+     *
+     * @param mixed $objects
+     * @return void
+     */
+    public function setObjects($objects) {
+
+        if (!is_array($objects)) {
+            $objects = array($objects);
+        }
+
+        $this->objects = array();
+        foreach($objects as $object) {
+
+            if (is_string($object)) {
+                $this->objects[] = Reader::read($object);
+            } elseif ($object instanceof Component) {
+                $this->objects[] = $object;
+            } else {
+                throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects');
+            }
+
+        }
+
+    }
+
+    /**
+     * Sets the time range
+     *
+     * Any freebusy object falling outside of this time range will be ignored.
+     *
+     * @param DateTime $start
+     * @param DateTime $end
+     * @return void
+     */
+    public function setTimeRange(\DateTime $start = null, \DateTime $end = null) {
+
+        $this->start = $start;
+        $this->end = $end;
+
+    }
+
+    /**
+     * Parses the input data and returns a correct VFREEBUSY object, wrapped in
+     * a VCALENDAR.
+     *
+     * @return Component
+     */
+    public function getResult() {
+
+        $busyTimes = array();
+
+        foreach($this->objects as $object) {
+
+            foreach($object->getBaseComponents() as $component) {
+
+                switch($component->name) {
+
+                    case 'VEVENT' :
+
+                        $FBTYPE = 'BUSY';
+                        if (isset($component->TRANSP) && (strtoupper($component->TRANSP) === 'TRANSPARENT')) {
+                            break;
+                        }
+                        if (isset($component->STATUS)) {
+                            $status = strtoupper($component->STATUS);
+                            if ($status==='CANCELLED') {
+                                break;
+                            }
+                            if ($status==='TENTATIVE') {
+                                $FBTYPE = 'BUSY-TENTATIVE';
+                            }
+                        }
+
+                        $times = array();
+
+                        if ($component->RRULE) {
+
+                            $iterator = new RecurrenceIterator($object, (string)$component->uid);
+                            if ($this->start) {
+                                $iterator->fastForward($this->start);
+                            }
+
+                            $maxRecurrences = 200;
+
+                            while($iterator->valid() && --$maxRecurrences) {
+
+                                $startTime = $iterator->getDTStart();
+                                if ($this->end && $startTime > $this->end) {
+                                    break;
+                                }
+                                $times[] = array(
+                                    $iterator->getDTStart(),
+                                    $iterator->getDTEnd(),
+                                );
+
+                                $iterator->next();
+
+                            }
+
+                        } else {
+
+                            $startTime = $component->DTSTART->getDateTime();
+                            if ($this->end && $startTime > $this->end) {
+                                break;
+                            }
+                            $endTime = null;
+                            if (isset($component->DTEND)) {
+                                $endTime = $component->DTEND->getDateTime();
+                            } elseif (isset($component->DURATION)) {
+                                $duration = DateTimeParser::parseDuration((string)$component->DURATION);
+                                $endTime = clone $startTime;
+                                $endTime->add($duration);
+                            } elseif ($component->DTSTART->getDateType() === Property\DateTime::DATE) {
+                                $endTime = clone $startTime;
+                                $endTime->modify('+1 day');
+                            } else {
+                                // The event had no duration (0 seconds)
+                                break;
+                            }
+
+                            $times[] = array($startTime, $endTime);
+
+                        }
+
+                        foreach($times as $time) {
+
+                            if ($this->end && $time[0] > $this->end) break;
+                            if ($this->start && $time[1] < $this->start) break;
+
+                            $busyTimes[] = array(
+                                $time[0],
+                                $time[1],
+                                $FBTYPE,
+                            );
+                        }
+                        break;
+
+                    case 'VFREEBUSY' :
+                        foreach($component->FREEBUSY as $freebusy) {
+
+                            $fbType = isset($freebusy['FBTYPE'])?strtoupper($freebusy['FBTYPE']):'BUSY';
+
+                            // Skipping intervals marked as 'free'
+                            if ($fbType==='FREE')
+                                continue;
+
+                            $values = explode(',', $freebusy);
+                            foreach($values as $value) {
+                                list($startTime, $endTime) = explode('/', $value);
+                                $startTime = DateTimeParser::parseDateTime($startTime);
+
+                                if (substr($endTime,0,1)==='P' || substr($endTime,0,2)==='-P') {
+                                    $duration = DateTimeParser::parseDuration($endTime);
+                                    $endTime = clone $startTime;
+                                    $endTime->add($duration);
+                                } else {
+                                    $endTime = DateTimeParser::parseDateTime($endTime);
+                                }
+
+                                if($this->start && $this->start > $endTime) continue;
+                                if($this->end && $this->end < $startTime) continue;
+                                $busyTimes[] = array(
+                                    $startTime,
+                                    $endTime,
+                                    $fbType
+                                );
+
+                            }
+
+
+                        }
+                        break;
+
+
+
+                }
+
+
+            }
+
+        }
+
+        if ($this->baseObject) {
+            $calendar = $this->baseObject;
+        } else {
+            $calendar = Component::create('VCALENDAR');
+            $calendar->version = '2.0';
+            $calendar->prodid = '-//Sabre//Sabre VObject ' . Version::VERSION . '//EN';
+            $calendar->calscale = 'GREGORIAN';
+        }
+
+        $vfreebusy = Component::create('VFREEBUSY');
+        $calendar->add($vfreebusy);
+
+        if ($this->start) {
+            $dtstart = Property::create('DTSTART');
+            $dtstart->setDateTime($this->start,Property\DateTime::UTC);
+            $vfreebusy->add($dtstart);
+        }
+        if ($this->end) {
+            $dtend = Property::create('DTEND');
+            $dtend->setDateTime($this->end,Property\DateTime::UTC);
+            $vfreebusy->add($dtend);
+        }
+        $dtstamp = Property::create('DTSTAMP');
+        $dtstamp->setDateTime(new \DateTime('now'), Property\DateTime::UTC);
+        $vfreebusy->add($dtstamp);
+
+        foreach($busyTimes as $busyTime) {
+
+            $busyTime[0]->setTimeZone(new \DateTimeZone('UTC'));
+            $busyTime[1]->setTimeZone(new \DateTimeZone('UTC'));
+
+            $prop = Property::create(
+                'FREEBUSY',
+                $busyTime[0]->format('Ymd\\THis\\Z') . '/' . $busyTime[1]->format('Ymd\\THis\\Z')
+            );
+            $prop['FBTYPE'] = $busyTime[2];
+            $vfreebusy->add($prop);
+
+        }
+
+        return $calendar;
+
+    }
+
+}
+
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Node.php b/plugins/libcalendaring/lib/Sabre/VObject/Node.php
new file mode 100644
index 0000000..bee68ec
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Node.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * Base class for all nodes
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+abstract class Node implements \IteratorAggregate, \ArrayAccess, \Countable {
+
+    /**
+     * The following constants are used by the validate() method.
+     */
+    const REPAIR = 1;
+
+    /**
+     * Turns the object back into a serialized blob.
+     *
+     * @return string
+     */
+    abstract function serialize();
+
+    /**
+     * Iterator override
+     *
+     * @var ElementList
+     */
+    protected $iterator = null;
+
+    /**
+     * A link to the parent node
+     *
+     * @var Node
+     */
+    public $parent = null;
+
+    /**
+     * Validates the node for correctness.
+     *
+     * The following options are supported:
+     *   - Node::REPAIR - If something is broken, and automatic repair may
+     *                    be attempted.
+     *
+     * An array is returned with warnings.
+     *
+     * Every item in the array has the following properties:
+     *    * level - (number between 1 and 3 with severity information)
+     *    * message - (human readable message)
+     *    * node - (reference to the offending node)
+     *
+     * @param int $options
+     * @return array
+     */
+    public function validate($options = 0) {
+
+        return array();
+
+    }
+
+    /* {{{ IteratorAggregator interface */
+
+    /**
+     * Returns the iterator for this object
+     *
+     * @return ElementList
+     */
+    public function getIterator() {
+
+        if (!is_null($this->iterator))
+            return $this->iterator;
+
+        return new ElementList(array($this));
+
+    }
+
+    /**
+     * Sets the overridden iterator
+     *
+     * Note that this is not actually part of the iterator interface
+     *
+     * @param ElementList $iterator
+     * @return void
+     */
+    public function setIterator(ElementList $iterator) {
+
+        $this->iterator = $iterator;
+
+    }
+
+    /* }}} */
+
+    /* {{{ Countable interface */
+
+    /**
+     * Returns the number of elements
+     *
+     * @return int
+     */
+    public function count() {
+
+        $it = $this->getIterator();
+        return $it->count();
+
+    }
+
+    /* }}} */
+
+    /* {{{ ArrayAccess Interface */
+
+
+    /**
+     * Checks if an item exists through ArrayAccess.
+     *
+     * This method just forwards the request to the inner iterator
+     *
+     * @param int $offset
+     * @return bool
+     */
+    public function offsetExists($offset) {
+
+        $iterator = $this->getIterator();
+        return $iterator->offsetExists($offset);
+
+    }
+
+    /**
+     * Gets an item through ArrayAccess.
+     *
+     * This method just forwards the request to the inner iterator
+     *
+     * @param int $offset
+     * @return mixed
+     */
+    public function offsetGet($offset) {
+
+        $iterator = $this->getIterator();
+        return $iterator->offsetGet($offset);
+
+    }
+
+    /**
+     * Sets an item through ArrayAccess.
+     *
+     * This method just forwards the request to the inner iterator
+     *
+     * @param int $offset
+     * @param mixed $value
+     * @return void
+     */
+    public function offsetSet($offset,$value) {
+
+        $iterator = $this->getIterator();
+        $iterator->offsetSet($offset,$value);
+
+    // @codeCoverageIgnoreStart
+    //
+    // This method always throws an exception, so we ignore the closing
+    // brace
+    }
+    // @codeCoverageIgnoreEnd
+
+    /**
+     * Sets an item through ArrayAccess.
+     *
+     * This method just forwards the request to the inner iterator
+     *
+     * @param int $offset
+     * @return void
+     */
+    public function offsetUnset($offset) {
+
+        $iterator = $this->getIterator();
+        $iterator->offsetUnset($offset);
+
+    // @codeCoverageIgnoreStart
+    //
+    // This method always throws an exception, so we ignore the closing
+    // brace
+    }
+    // @codeCoverageIgnoreEnd
+
+    /* }}} */
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Parameter.php b/plugins/libcalendaring/lib/Sabre/VObject/Parameter.php
new file mode 100644
index 0000000..37c9fd0
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Parameter.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * VObject Parameter
+ *
+ * This class represents a parameter. A parameter is always tied to a property.
+ * In the case of:
+ *   DTSTART;VALUE=DATE:20101108
+ * VALUE=DATE would be the parameter name and value.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Parameter extends Node {
+
+    /**
+     * Parameter name
+     *
+     * @var string
+     */
+    public $name;
+
+    /**
+     * Parameter value
+     *
+     * @var string
+     */
+    public $value;
+
+    /**
+     * Sets up the object
+     *
+     * @param string $name
+     * @param string $value
+     */
+    public function __construct($name, $value = null) {
+
+        if (!is_scalar($value) && !is_null($value)) {
+            throw new \InvalidArgumentException('The value argument must be a scalar value or null');
+        }
+
+        $this->name = strtoupper($name);
+        $this->value = $value;
+
+    }
+
+    /**
+     * Returns the parameter's internal value.
+     *
+     * @return string
+     */
+    public function getValue() {
+
+        return $this->value;
+
+    }
+
+
+    /**
+     * Turns the object back into a serialized blob.
+     *
+     * @return string
+     */
+    public function serialize() {
+
+        if (is_null($this->value)) {
+            return $this->name;
+        }
+        $src = array(
+            '\\',
+            "\n",
+            ';',
+            ',',
+        );
+        $out = array(
+            '\\\\',
+            '\n',
+            '\;',
+            '\,',
+        );
+
+        return $this->name . '=' . str_replace($src, $out, $this->value);
+
+    }
+
+    /**
+     * Called when this object is being cast to a string
+     *
+     * @return string
+     */
+    public function __toString() {
+
+        return $this->value;
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/ParseException.php b/plugins/libcalendaring/lib/Sabre/VObject/ParseException.php
new file mode 100644
index 0000000..66b49c6
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/ParseException.php
@@ -0,0 +1,12 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * Exception thrown by Reader if an invalid object was attempted to be parsed.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class ParseException extends \Exception { }
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Property.php b/plugins/libcalendaring/lib/Sabre/VObject/Property.php
new file mode 100644
index 0000000..ad54146
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Property.php
@@ -0,0 +1,442 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * VObject Property
+ *
+ * A property in VObject is usually in the form PARAMNAME:paramValue.
+ * An example is : SUMMARY:Weekly meeting
+ *
+ * Properties can also have parameters:
+ * SUMMARY;LANG=en:Weekly meeting.
+ *
+ * Parameters can be accessed using the ArrayAccess interface.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Property extends Node {
+
+    /**
+     * Propertyname
+     *
+     * @var string
+     */
+    public $name;
+
+    /**
+     * Group name
+     *
+     * This may be something like 'HOME' for vcards.
+     *
+     * @var string
+     */
+    public $group;
+
+    /**
+     * Property parameters
+     *
+     * @var array
+     */
+    public $parameters = array();
+
+    /**
+     * Property value
+     *
+     * @var string
+     */
+    public $value;
+
+    /**
+     * If properties are added to this map, they will be automatically mapped
+     * to their respective classes, if parsed by the reader or constructed with
+     * the 'create' method.
+     *
+     * @var array
+     */
+    static public $classMap = array(
+        'COMPLETED'     => 'Sabre\\VObject\\Property\\DateTime',
+        'CREATED'       => 'Sabre\\VObject\\Property\\DateTime',
+        'DTEND'         => 'Sabre\\VObject\\Property\\DateTime',
+        'DTSTAMP'       => 'Sabre\\VObject\\Property\\DateTime',
+        'DTSTART'       => 'Sabre\\VObject\\Property\\DateTime',
+        'DUE'           => 'Sabre\\VObject\\Property\\DateTime',
+        'EXDATE'        => 'Sabre\\VObject\\Property\\MultiDateTime',
+        'LAST-MODIFIED' => 'Sabre\\VObject\\Property\\DateTime',
+        'RECURRENCE-ID' => 'Sabre\\VObject\\Property\\DateTime',
+        'TRIGGER'       => 'Sabre\\VObject\\Property\\DateTime',
+        'N'             => 'Sabre\\VObject\\Property\\Compound',
+        'ORG'           => 'Sabre\\VObject\\Property\\Compound',
+        'ADR'           => 'Sabre\\VObject\\Property\\Compound',
+        'CATEGORIES'    => 'Sabre\\VObject\\Property\\Compound',
+    );
+
+    /**
+     * Creates the new property by name, but in addition will also see if
+     * there's a class mapped to the property name.
+     *
+     * Parameters can be specified with the optional third argument. Parameters
+     * must be a key->value map of the parameter name, and value. If the value
+     * is specified as an array, it is assumed that multiple parameters with
+     * the same name should be added.
+     *
+     * @param string $name
+     * @param string $value
+     * @param array $parameters
+     * @return Property
+     */
+    static public function create($name, $value = null, array $parameters = array()) {
+
+        $name = strtoupper($name);
+        $shortName = $name;
+        $group = null;
+        if (strpos($shortName,'.')!==false) {
+            list($group, $shortName) = explode('.', $shortName);
+        }
+
+        if (isset(self::$classMap[$shortName])) {
+            return new self::$classMap[$shortName]($name, $value, $parameters);
+        } else {
+            return new self($name, $value, $parameters);
+        }
+
+    }
+
+    /**
+     * Creates a new property object
+     *
+     * Parameters can be specified with the optional third argument. Parameters
+     * must be a key->value map of the parameter name, and value. If the value
+     * is specified as an array, it is assumed that multiple parameters with
+     * the same name should be added.
+     *
+     * @param string $name
+     * @param string $value
+     * @param array $parameters
+     */
+    public function __construct($name, $value = null, array $parameters = array()) {
+
+        if (!is_scalar($value) && !is_null($value)) {
+            throw new \InvalidArgumentException('The value argument must be scalar or null');
+        }
+
+        $name = strtoupper($name);
+        $group = null;
+        if (strpos($name,'.')!==false) {
+            list($group, $name) = explode('.', $name);
+        }
+        $this->name = $name;
+        $this->group = $group;
+        $this->setValue($value);
+
+        foreach($parameters as $paramName => $paramValues) {
+
+            if (!is_array($paramValues)) {
+                $paramValues = array($paramValues);
+            }
+
+            foreach($paramValues as $paramValue) {
+                $this->add($paramName, $paramValue);
+            }
+
+        }
+
+    }
+
+    /**
+     * Updates the internal value
+     *
+     * @param string $value
+     * @return void
+     */
+    public function setValue($value) {
+
+        $this->value = $value;
+
+    }
+
+    /**
+     * Returns the internal value
+     *
+     * @param string $value
+     * @return string
+     */
+    public function getValue() {
+
+        return $this->value;
+
+    }
+
+    /**
+     * Turns the object back into a serialized blob.
+     *
+     * @return string
+     */
+    public function serialize() {
+
+        $str = $this->name;
+        if ($this->group) $str = $this->group . '.' . $this->name;
+
+        foreach($this->parameters as $param) {
+
+            $str.=';' . $param->serialize();
+
+        }
+
+        $src = array(
+            '\\',
+            "\n",
+        );
+        $out = array(
+            '\\\\',
+            '\n',
+        );
+        $str.=':' . str_replace($src, $out, $this->value);
+
+        $out = '';
+        while(strlen($str)>0) {
+            if (strlen($str)>75) {
+                $out.= mb_strcut($str,0,75,'utf-8') . "\r\n";
+                $str = ' ' . mb_strcut($str,75,strlen($str),'utf-8');
+            } else {
+                $out.=$str . "\r\n";
+                $str='';
+                break;
+            }
+        }
+
+        return $out;
+
+    }
+
+    /**
+     * Adds a new componenten or element
+     *
+     * You can call this method with the following syntaxes:
+     *
+     * add(Parameter $element)
+     * add(string $name, $value)
+     *
+     * The first version adds an Parameter
+     * The second adds a property as a string.
+     *
+     * @param mixed $item
+     * @param mixed $itemValue
+     * @return void
+     */
+    public function add($item, $itemValue = null) {
+
+        if ($item instanceof Parameter) {
+            if (!is_null($itemValue)) {
+                throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject');
+            }
+            $item->parent = $this;
+            $this->parameters[] = $item;
+        } elseif(is_string($item)) {
+
+            $parameter = new Parameter($item,$itemValue);
+            $parameter->parent = $this;
+            $this->parameters[] = $parameter;
+
+        } else {
+
+            throw new \InvalidArgumentException('The first argument must either be a Node a string');
+
+        }
+
+    }
+
+    /* ArrayAccess interface {{{ */
+
+    /**
+     * Checks if an array element exists
+     *
+     * @param mixed $name
+     * @return bool
+     */
+    public function offsetExists($name) {
+
+        if (is_int($name)) return parent::offsetExists($name);
+
+        $name = strtoupper($name);
+
+        foreach($this->parameters as $parameter) {
+            if ($parameter->name == $name) return true;
+        }
+        return false;
+
+    }
+
+    /**
+     * Returns a parameter, or parameter list.
+     *
+     * @param string $name
+     * @return Node
+     */
+    public function offsetGet($name) {
+
+        if (is_int($name)) return parent::offsetGet($name);
+        $name = strtoupper($name);
+
+        $result = array();
+        foreach($this->parameters as $parameter) {
+            if ($parameter->name == $name)
+                $result[] = $parameter;
+        }
+
+        if (count($result)===0) {
+            return null;
+        } elseif (count($result)===1) {
+            return $result[0];
+        } else {
+            $result[0]->setIterator(new ElementList($result));
+            return $result[0];
+        }
+
+    }
+
+    /**
+     * Creates a new parameter
+     *
+     * @param string $name
+     * @param mixed $value
+     * @return void
+     */
+    public function offsetSet($name, $value) {
+
+        if (is_int($name)) parent::offsetSet($name, $value);
+
+        if (is_scalar($value)) {
+            if (!is_string($name))
+                throw new \InvalidArgumentException('A parameter name must be specified. This means you cannot use the $array[]="string" to add parameters.');
+
+            $this->offsetUnset($name);
+            $parameter = new Parameter($name, $value);
+            $parameter->parent = $this;
+            $this->parameters[] = $parameter;
+
+        } elseif ($value instanceof Parameter) {
+            if (!is_null($name))
+                throw new \InvalidArgumentException('Don\'t specify a parameter name if you\'re passing a \\Sabre\\VObject\\Parameter. Add using $array[]=$parameterObject.');
+
+            $value->parent = $this;
+            $this->parameters[] = $value;
+        } else {
+            throw new \InvalidArgumentException('You can only add parameters to the property object');
+        }
+
+    }
+
+    /**
+     * Removes one or more parameters with the specified name
+     *
+     * @param string $name
+     * @return void
+     */
+    public function offsetUnset($name) {
+
+        if (is_int($name)) parent::offsetUnset($name);
+        $name = strtoupper($name);
+
+        foreach($this->parameters as $key=>$parameter) {
+            if ($parameter->name == $name) {
+                $parameter->parent = null;
+                unset($this->parameters[$key]);
+            }
+
+        }
+
+    }
+
+    /* }}} */
+
+    /**
+     * Called when this object is being cast to a string
+     *
+     * @return string
+     */
+    public function __toString() {
+
+        return (string)$this->value;
+
+    }
+
+    /**
+     * This method is automatically called when the object is cloned.
+     * Specifically, this will ensure all child elements are also cloned.
+     *
+     * @return void
+     */
+    public function __clone() {
+
+        foreach($this->parameters as $key=>$child) {
+            $this->parameters[$key] = clone $child;
+            $this->parameters[$key]->parent = $this;
+        }
+
+    }
+
+    /**
+     * Validates the node for correctness.
+     *
+     * The following options are supported:
+     *   - Node::REPAIR - If something is broken, and automatic repair may
+     *                    be attempted.
+     *
+     * An array is returned with warnings.
+     *
+     * Every item in the array has the following properties:
+     *    * level - (number between 1 and 3 with severity information)
+     *    * message - (human readable message)
+     *    * node - (reference to the offending node)
+     *
+     * @param int $options
+     * @return array
+     */
+    public function validate($options = 0) {
+
+        $warnings = array();
+
+        // Checking if our value is UTF-8
+        if (!StringUtil::isUTF8($this->value)) {
+            $warnings[] = array(
+                'level' => 1,
+                'message' => 'Property is not valid UTF-8!',
+                'node' => $this,
+            );
+            if ($options & self::REPAIR) {
+                $this->value = StringUtil::convertToUTF8($this->value);
+            }
+        }
+
+        // Checking if the propertyname does not contain any invalid bytes.
+        if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) {
+            $warnings[] = array(
+                'level' => 1,
+                'message' => 'The propertyname: ' . $this->name . ' contains invalid characters. Only A-Z, 0-9 and - are allowed',
+                'node' => $this,
+            );
+            if ($options & self::REPAIR) {
+                // Uppercasing and converting underscores to dashes.
+                $this->name = strtoupper(
+                    str_replace('_', '-', $this->name)
+                );
+                // Removing every other invalid character
+                $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name);
+
+            }
+
+        }
+
+        // Validating inner parameters
+        foreach($this->parameters as $param) {
+            $warnings = array_merge($warnings, $param->validate($options));
+        }
+
+        return $warnings;
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php b/plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php
new file mode 100644
index 0000000..26f0900
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php
@@ -0,0 +1,125 @@
+<?php
+
+namespace Sabre\VObject\Property;
+
+use Sabre\VObject;
+
+/**
+ * Compound property.
+ *
+ * This class adds (de)serialization of compound properties to/from arrays.
+ *
+ * Currently the following properties from RFC 6350 are mapped to use this
+ * class:
+ *
+ *  N:          Section 6.2.2
+ *  ADR:        Section 6.3.1
+ *  ORG:        Section 6.6.4
+ *  CATEGORIES: Section 6.7.1
+ *
+ * In order to use this correctly, you must call setParts and getParts to
+ * retrieve and modify dates respectively.
+ *
+ * @author Thomas Tanghus (http://tanghus.net/)
+ * @author Lars Kneschke
+ * @author Evert Pot (http://evertpot.com/)
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Compound extends VObject\Property {
+
+    /**
+     * If property names are added to this map, they will be (de)serialised as arrays
+     * using the getParts() and setParts() methods.
+     * The keys are the property names, values are delimiter chars.
+     *
+     * @var array
+     */
+    static public $delimiterMap = array(
+        'N'           =>    ';',
+        'ADR'         =>    ';',
+        'ORG'         =>    ';',
+        'CATEGORIES'  =>    ',',
+    );
+
+    /**
+     * The currently used delimiter.
+     *
+     * @var string
+     */
+    protected $delimiter = null;
+
+    /**
+     * Get a compound value as an array.
+     *
+     * @param $name string
+     * @return array
+     */
+    public function getParts() {
+
+        if (is_null($this->value)) {
+            return array();
+        }
+
+        $delimiter = $this->getDelimiter();
+
+        // split by any $delimiter which is NOT prefixed by a slash.
+        // Note that this is not a a perfect solution. If a value is prefixed
+        // by two slashes, it should actually be split anyway.
+        //
+        // Hopefully we can fix this better in a future version, where we can
+        // break compatibility a bit.
+        $compoundValues = preg_split("/(?<!\\\)$delimiter/", $this->value);
+
+        // remove slashes from any semicolon and comma left escaped in the single values
+        $compoundValues = array_map(
+            function($val) {
+                return strtr($val, array('\,' => ',', '\;' => ';'));
+        }, $compoundValues);
+
+        return $compoundValues;
+
+    }
+
+    /**
+     * Returns the delimiter for this property.
+     *
+     * @return string
+     */
+    public function getDelimiter() {
+
+        if (!$this->delimiter) {
+            if (isset(self::$delimiterMap[$this->name])) {
+                $this->delimiter = self::$delimiterMap[$this->name];
+            } else {
+                // To be a bit future proof, we are going to default the
+                // delimiter to ;
+                $this->delimiter = ';';
+            }
+        }
+        return $this->delimiter;
+
+    }
+
+    /**
+     * Set a compound value as an array.
+     *
+     *
+     * @param $name string
+     * @return array
+     */
+    public function setParts(array $values) {
+
+        // add slashes to all semicolons and commas in the single values
+        $values = array_map(
+            function($val) {
+                return strtr($val, array(',' => '\,', ';' => '\;'));
+            }, $values);
+
+        $this->setValue(
+            implode($this->getDelimiter(), $values)
+        );
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php b/plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php
new file mode 100644
index 0000000..95e9b02
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php
@@ -0,0 +1,245 @@
+<?php
+
+namespace Sabre\VObject\Property;
+
+use Sabre\VObject;
+
+/**
+ * DateTime property
+ *
+ * This element is used for iCalendar properties such as the DTSTART property.
+ * It basically provides a few helper functions that make it easier to deal
+ * with these. It supports both DATE-TIME and DATE values.
+ *
+ * In order to use this correctly, you must call setDateTime and getDateTime to
+ * retrieve and modify dates respectively.
+ *
+ * If you use the 'value' or properties directly, this object does not keep
+ * reference and results might appear incorrectly.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class DateTime extends VObject\Property {
+
+    /**
+     * Local 'floating' time
+     */
+    const LOCAL = 1;
+
+    /**
+     * UTC-based time
+     */
+    const UTC = 2;
+
+    /**
+     * Local time plus timezone
+     */
+    const LOCALTZ = 3;
+
+    /**
+     * Only a date, time is ignored
+     */
+    const DATE = 4;
+
+    /**
+     * DateTime representation
+     *
+     * @var \DateTime
+     */
+    protected $dateTime;
+
+    /**
+     * dateType
+     *
+     * @var int
+     */
+    protected $dateType;
+
+    /**
+     * Updates the Date and Time.
+     *
+     * @param \DateTime $dt
+     * @param int $dateType
+     * @return void
+     */
+    public function setDateTime(\DateTime $dt, $dateType = self::LOCALTZ) {
+
+        switch($dateType) {
+
+            case self::LOCAL :
+                $this->setValue($dt->format('Ymd\\THis'));
+                $this->offsetUnset('VALUE');
+                $this->offsetUnset('TZID');
+                $this->offsetSet('VALUE','DATE-TIME');
+                break;
+            case self::UTC :
+                $dt->setTimeZone(new \DateTimeZone('UTC'));
+                $this->setValue($dt->format('Ymd\\THis\\Z'));
+                $this->offsetUnset('VALUE');
+                $this->offsetUnset('TZID');
+                $this->offsetSet('VALUE','DATE-TIME');
+                break;
+            case self::LOCALTZ :
+                $this->setValue($dt->format('Ymd\\THis'));
+                $this->offsetUnset('VALUE');
+                $this->offsetUnset('TZID');
+                $this->offsetSet('VALUE','DATE-TIME');
+                $this->offsetSet('TZID', $dt->getTimeZone()->getName());
+                break;
+            case self::DATE :
+                $this->setValue($dt->format('Ymd'));
+                $this->offsetUnset('VALUE');
+                $this->offsetUnset('TZID');
+                $this->offsetSet('VALUE','DATE');
+                break;
+            default :
+                throw new \InvalidArgumentException('You must pass a valid dateType constant');
+
+        }
+        $this->dateTime = $dt;
+        $this->dateType = $dateType;
+
+    }
+
+    /**
+     * Returns the current DateTime value.
+     *
+     * If no value was set, this method returns null.
+     *
+     * @return \DateTime|null
+     */
+    public function getDateTime() {
+
+        if ($this->dateTime)
+            return $this->dateTime;
+
+        list(
+            $this->dateType,
+            $this->dateTime
+        ) = self::parseData($this->value, $this);
+        return $this->dateTime;
+
+    }
+
+    /**
+     * Returns the type of Date format.
+     *
+     * This method returns one of the format constants. If no date was set,
+     * this method will return null.
+     *
+     * @return int|null
+     */
+    public function getDateType() {
+
+        if ($this->dateType)
+            return $this->dateType;
+
+        list(
+            $this->dateType,
+            $this->dateTime,
+        ) = self::parseData($this->value, $this);
+        return $this->dateType;
+
+    }
+
+    /**
+     * This method will return true, if the property had a date and a time, as
+     * opposed to only a date.
+     *
+     * @return bool
+     */
+    public function hasTime() {
+
+        return $this->getDateType()!==self::DATE;
+
+    }
+
+    /**
+     * Parses the internal data structure to figure out what the current date
+     * and time is.
+     *
+     * The returned array contains two elements:
+     *   1. A 'DateType' constant (as defined on this class), or null.
+     *   2. A DateTime object (or null)
+     *
+     * @param string|null $propertyValue The string to parse (yymmdd or
+     *                                   ymmddThhmmss, etc..)
+     * @param \Sabre\VObject\Property|null $property The instance of the
+     *                                              property we're parsing.
+     * @return array
+     */
+    static public function parseData($propertyValue, VObject\Property $property = null) {
+
+        if (is_null($propertyValue)) {
+            return array(null, null);
+        }
+
+        $date = '(?P<year>[1-2][0-9]{3})(?P<month>[0-1][0-9])(?P<date>[0-3][0-9])';
+        $time = '(?P<hour>[0-2][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9])';
+        $regex = "/^$date(T$time(?P<isutc>Z)?)?$/";
+
+        if (!preg_match($regex, $propertyValue, $matches)) {
+            throw new \InvalidArgumentException($propertyValue . ' is not a valid \DateTime or Date string');
+        }
+
+        if (!isset($matches['hour'])) {
+            // Date-only
+            return array(
+                self::DATE,
+                new \DateTime($matches['year'] . '-' . $matches['month'] . '-' . $matches['date'] . ' 00:00:00', new \DateTimeZone('UTC')),
+            );
+        }
+
+        $dateStr =
+            $matches['year'] .'-' .
+            $matches['month'] . '-' .
+            $matches['date'] . ' ' .
+            $matches['hour'] . ':' .
+            $matches['minute'] . ':' .
+            $matches['second'];
+
+        if (isset($matches['isutc'])) {
+            $dt = new \DateTime($dateStr,new \DateTimeZone('UTC'));
+            $dt->setTimeZone(new \DateTimeZone('UTC'));
+            return array(
+                self::UTC,
+                $dt
+            );
+        }
+
+        // Finding the timezone.
+        $tzid = $property['TZID'];
+        if (!$tzid) {
+            // This was a floating time string. This implies we use the
+            // timezone from date_default_timezone_set / date.timezone ini
+            // setting.
+            return array(
+                self::LOCAL,
+                new \DateTime($dateStr)
+            );
+        }
+
+        // To look up the timezone, we must first find the VCALENDAR component.
+        $root = $property;
+        while($root->parent) {
+            $root = $root->parent;
+        }
+        if ($root->name === 'VCALENDAR') {
+            $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid, $root);
+        } else {
+            $tz = VObject\TimeZoneUtil::getTimeZone((string)$tzid);
+        }
+
+        $dt = new \DateTime($dateStr, $tz);
+        $dt->setTimeZone($tz);
+
+        return array(
+            self::LOCALTZ,
+            $dt
+        );
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php b/plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php
new file mode 100644
index 0000000..f01491b
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php
@@ -0,0 +1,180 @@
+<?php
+
+namespace Sabre\VObject\Property;
+
+use Sabre\VObject;
+
+/**
+ * Multi-DateTime property
+ *
+ * This element is used for iCalendar properties such as the EXDATE property.
+ * It basically provides a few helper functions that make it easier to deal
+ * with these. It supports both DATE-TIME and DATE values.
+ *
+ * In order to use this correctly, you must call setDateTimes and getDateTimes
+ * to retrieve and modify dates respectively.
+ *
+ * If you use the 'value' or properties directly, this object does not keep
+ * reference and results might appear incorrectly.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class MultiDateTime extends VObject\Property {
+
+    /**
+     * DateTime representation
+     *
+     * @var DateTime[]
+     */
+    protected $dateTimes;
+
+    /**
+     * dateType
+     *
+     * This is one of the Sabre\VObject\Property\DateTime constants.
+     *
+     * @var int
+     */
+    protected $dateType;
+
+    /**
+     * Updates the value
+     *
+     * @param array $dt Must be an array of DateTime objects.
+     * @param int $dateType
+     * @return void
+     */
+    public function setDateTimes(array $dt, $dateType = VObject\Property\DateTime::LOCALTZ) {
+
+        foreach($dt as $i)
+            if (!$i instanceof \DateTime)
+                throw new \InvalidArgumentException('You must pass an array of DateTime objects');
+
+        $this->offsetUnset('VALUE');
+        $this->offsetUnset('TZID');
+        switch($dateType) {
+
+            case DateTime::LOCAL :
+                $val = array();
+                foreach($dt as $i) {
+                    $val[] = $i->format('Ymd\\THis');
+                }
+                $this->setValue(implode(',',$val));
+                $this->offsetSet('VALUE','DATE-TIME');
+                break;
+            case DateTime::UTC :
+                $val = array();
+                foreach($dt as $i) {
+                    $i->setTimeZone(new \DateTimeZone('UTC'));
+                    $val[] = $i->format('Ymd\\THis\\Z');
+                }
+                $this->setValue(implode(',',$val));
+                $this->offsetSet('VALUE','DATE-TIME');
+                break;
+            case DateTime::LOCALTZ :
+                $val = array();
+                foreach($dt as $i) {
+                    $val[] = $i->format('Ymd\\THis');
+                }
+                $this->setValue(implode(',',$val));
+                $this->offsetSet('VALUE','DATE-TIME');
+                $this->offsetSet('TZID', $dt[0]->getTimeZone()->getName());
+                break;
+            case DateTime::DATE :
+                $val = array();
+                foreach($dt as $i) {
+                    $val[] = $i->format('Ymd');
+                }
+                $this->setValue(implode(',',$val));
+                $this->offsetSet('VALUE','DATE');
+                break;
+            default :
+                throw new \InvalidArgumentException('You must pass a valid dateType constant');
+
+        }
+        $this->dateTimes = $dt;
+        $this->dateType = $dateType;
+
+    }
+
+    /**
+     * Returns the current DateTime value.
+     *
+     * If no value was set, this method returns null.
+     *
+     * @return array|null
+     */
+    public function getDateTimes() {
+
+        if ($this->dateTimes)
+            return $this->dateTimes;
+
+        $dts = array();
+
+        if (!$this->value) {
+            $this->dateTimes = null;
+            $this->dateType = null;
+            return null;
+        }
+
+        foreach(explode(',',$this->value) as $val) {
+            list(
+                $type,
+                $dt
+            ) = DateTime::parseData($val, $this);
+            $dts[] = $dt;
+            $this->dateType = $type;
+        }
+        $this->dateTimes = $dts;
+        return $this->dateTimes;
+
+    }
+
+    /**
+     * Returns the type of Date format.
+     *
+     * This method returns one of the format constants. If no date was set,
+     * this method will return null.
+     *
+     * @return int|null
+     */
+    public function getDateType() {
+
+        if ($this->dateType)
+            return $this->dateType;
+
+        if (!$this->value) {
+            $this->dateTimes = null;
+            $this->dateType = null;
+            return null;
+        }
+
+        $dts = array();
+        foreach(explode(',',$this->value) as $val) {
+            list(
+                $type,
+                $dt
+            ) = DateTime::parseData($val, $this);
+            $dts[] = $dt;
+            $this->dateType = $type;
+        }
+        $this->dateTimes = $dts;
+        return $this->dateType;
+
+    }
+
+    /**
+     * This method will return true, if the property had a date and a time, as
+     * opposed to only a date.
+     *
+     * @return bool
+     */
+    public function hasTime() {
+
+        return $this->getDateType()!==DateTime::DATE;
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Reader.php b/plugins/libcalendaring/lib/Sabre/VObject/Reader.php
new file mode 100644
index 0000000..a001b2b
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Reader.php
@@ -0,0 +1,223 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * VCALENDAR/VCARD reader
+ *
+ * This class reads the vobject file, and returns a full element tree.
+ *
+ * TODO: this class currently completely works 'statically'. This is pointless,
+ * and defeats OOP principals. Needs refactoring in a future version.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Reader {
+
+    /**
+     * If this option is passed to the reader, it will be less strict about the
+     * validity of the lines.
+     *
+     * Currently using this option just means, that it will accept underscores
+     * in property names.
+     */
+    const OPTION_FORGIVING = 1;
+
+    /**
+     * If this option is turned on, any lines we cannot parse will be ignored
+     * by the reader.
+     */
+    const OPTION_IGNORE_INVALID_LINES = 2;
+
+    /**
+     * Parses the file and returns the top component
+     *
+     * The options argument is a bitfield. Pass any of the OPTIONS constant to
+     * alter the parsers' behaviour.
+     *
+     * @param string $data
+     * @param int $options
+     * @return Node
+     */
+    static function read($data, $options = 0) {
+
+        // Normalizing newlines
+        $data = str_replace(array("\r","\n\n"), array("\n","\n"), $data);
+
+        $lines = explode("\n", $data);
+
+        // Unfolding lines
+        $lines2 = array();
+        foreach($lines as $line) {
+
+            // Skipping empty lines
+            if (!$line) continue;
+
+            if ($line[0]===" " || $line[0]==="\t") {
+                $lines2[count($lines2)-1].=substr($line,1);
+            } else {
+                $lines2[] = $line;
+            }
+
+        }
+
+        unset($lines);
+
+        reset($lines2);
+
+        return self::readLine($lines2, $options);
+
+    }
+
+    /**
+     * Reads and parses a single line.
+     *
+     * This method receives the full array of lines. The array pointer is used
+     * to traverse.
+     *
+     * This method returns null if an invalid line was encountered, and the
+     * IGNORE_INVALID_LINES option was turned on.
+     *
+     * @param array $lines
+     * @param int $options See the OPTIONS constants.
+     * @return Node
+     */
+    static private function readLine(&$lines, $options = 0) {
+
+        $line = current($lines);
+        $lineNr = key($lines);
+        next($lines);
+
+        // Components
+        if (strtoupper(substr($line,0,6)) === "BEGIN:") {
+
+            $componentName = strtoupper(substr($line,6));
+            $obj = Component::create($componentName);
+
+            $nextLine = current($lines);
+
+            while(strtoupper(substr($nextLine,0,4))!=="END:") {
+
+                $parsedLine = self::readLine($lines, $options);
+                $nextLine = current($lines);
+
+                if (is_null($parsedLine)) {
+                    continue;
+                }
+                $obj->add($parsedLine);
+
+                if ($nextLine===false)
+                    throw new ParseException('Invalid VObject. Document ended prematurely.');
+
+            }
+
+            // Checking component name of the 'END:' line.
+            if (substr($nextLine,4)!==$obj->name) {
+                throw new ParseException('Invalid VObject, expected: "END:' . $obj->name . '" got: "' . $nextLine . '"');
+            }
+            next($lines);
+
+            return $obj;
+
+        }
+
+        // Properties
+        //$result = preg_match('/(?P<name>[A-Z0-9-]+)(?:;(?P<parameters>^(?<!:):))(.*)$/',$line,$matches);
+
+        if ($options & self::OPTION_FORGIVING) {
+            $token = '[A-Z0-9-\._]+';
+        } else {
+            $token = '[A-Z0-9-\.]+';
+        }
+        $parameters = "(?:;(?P<parameters>([^:^\"]|\"([^\"]*)\")*))?";
+        $regex = "/^(?P<name>$token)$parameters:(?P<value>.*)$/i";
+
+        $result = preg_match($regex,$line,$matches);
+
+        if (!$result) {
+            if ($options & self::OPTION_IGNORE_INVALID_LINES) {
+                return null;
+            } else {
+                throw new ParseException('Invalid VObject, line ' . ($lineNr+1) . ' did not follow the icalendar/vcard format');
+            }
+        }
+
+        $propertyName = strtoupper($matches['name']);
+        $propertyValue = preg_replace_callback('#(\\\\(\\\\|N|n))#',function($matches) {
+            if ($matches[2]==='n' || $matches[2]==='N') {
+                return "\n";
+            } else {
+                return $matches[2];
+            }
+        }, $matches['value']);
+
+        $obj = Property::create($propertyName, $propertyValue);
+
+        if ($matches['parameters']) {
+
+            foreach(self::readParameters($matches['parameters']) as $param) {
+                $obj->add($param);
+            }
+
+        }
+
+        return $obj;
+
+
+    }
+
+    /**
+     * Reads a parameter list from a property
+     *
+     * This method returns an array of Parameter
+     *
+     * @param string $parameters
+     * @return array
+     */
+    static private function readParameters($parameters) {
+
+        $token = '[A-Z0-9-]+';
+
+        $paramValue = '(?P<paramValue>[^\"^;]*|"[^"]*")';
+
+        $regex = "/(?<=^|;)(?P<paramName>$token)(=$paramValue(?=$|;))?/i";
+        preg_match_all($regex, $parameters, $matches,  PREG_SET_ORDER);
+
+        $params = array();
+        foreach($matches as $match) {
+
+            if (!isset($match['paramValue'])) {
+
+                $value = null;
+
+            } else {
+
+                $value = $match['paramValue'];
+
+                if (isset($value[0]) && $value[0]==='"') {
+                    // Stripping quotes, if needed
+                    $value = substr($value,1,strlen($value)-2);
+                }
+
+                $value = preg_replace_callback('#(\\\\(\\\\|N|n|;|,))#',function($matches) {
+                    if ($matches[2]==='n' || $matches[2]==='N') {
+                        return "\n";
+                    } else {
+                        return $matches[2];
+                    }
+                }, $value);
+
+            }
+
+            $params[] = new Parameter($match['paramName'], $value);
+
+        }
+
+        return $params;
+
+    }
+
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php b/plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php
new file mode 100644
index 0000000..0d5997e
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php
@@ -0,0 +1,1112 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * This class is used to determine new for a recurring event, when the next
+ * events occur.
+ *
+ * This iterator may loop infinitely in the future, therefore it is important
+ * that if you use this class, you set hard limits for the amount of iterations
+ * you want to handle.
+ *
+ * Note that currently there is not full support for the entire iCalendar
+ * specification, as it's very complex and contains a lot of permutations
+ * that's not yet used very often in software.
+ *
+ * For the focus has been on features as they actually appear in Calendaring
+ * software, but this may well get expanded as needed / on demand
+ *
+ * The following RRULE properties are supported
+ *   * UNTIL
+ *   * INTERVAL
+ *   * COUNT
+ *   * FREQ=DAILY
+ *     * BYDAY
+ *     * BYHOUR
+ *   * FREQ=WEEKLY
+ *     * BYDAY
+ *     * BYHOUR
+ *     * WKST
+ *   * FREQ=MONTHLY
+ *     * BYMONTHDAY
+ *     * BYDAY
+ *     * BYSETPOS
+ *   * FREQ=YEARLY
+ *     * BYMONTH
+ *     * BYMONTHDAY (only if BYMONTH is also set)
+ *     * BYDAY (only if BYMONTH is also set)
+ *
+ * Anything beyond this is 'undefined', which means that it may get ignored, or
+ * you may get unexpected results. The effect is that in some applications the
+ * specified recurrence may look incorrect, or is missing.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class RecurrenceIterator implements \Iterator {
+
+    /**
+     * The initial event date
+     *
+     * @var DateTime
+     */
+    public $startDate;
+
+    /**
+     * The end-date of the initial event
+     *
+     * @var DateTime
+     */
+    public $endDate;
+
+    /**
+     * The 'current' recurrence.
+     *
+     * This will be increased for every iteration.
+     *
+     * @var DateTime
+     */
+    public $currentDate;
+
+
+    /**
+     * List of dates that are excluded from the rules.
+     *
+     * This list contains the items that have been overriden by the EXDATE
+     * property.
+     *
+     * @var array
+     */
+    public $exceptionDates = array();
+
+    /**
+     * Base event
+     *
+     * @var Component\VEvent
+     */
+    public $baseEvent;
+
+    /**
+     * List of dates that are overridden by other events.
+     * Similar to $overriddenEvents, but this just contains the original dates.
+     *
+     * @var array
+     */
+    public $overriddenDates = array();
+
+    /**
+     * list of events that are 'overridden'.
+     *
+     * This is an array of Component\VEvent objects.
+     *
+     * @var array
+     */
+    public $overriddenEvents = array();
+
+
+    /**
+     * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
+     * yearly.
+     *
+     * @var string
+     */
+    public $frequency;
+
+    /**
+     * The last instance of this recurrence, inclusively
+     *
+     * @var DateTime|null
+     */
+    public $until;
+
+    /**
+     * The number of recurrences, or 'null' if infinitely recurring.
+     *
+     * @var int
+     */
+    public $count;
+
+    /**
+     * The interval.
+     *
+     * If for example frequency is set to daily, interval = 2 would mean every
+     * 2 days.
+     *
+     * @var int
+     */
+    public $interval = 1;
+
+    /**
+     * Which seconds to recur.
+     *
+     * This is an array of integers (between 0 and 60)
+     *
+     * @var array
+     */
+    public $bySecond;
+
+    /**
+     * Which minutes to recur
+     *
+     * This is an array of integers (between 0 and 59)
+     *
+     * @var array
+     */
+    public $byMinute;
+
+    /**
+     * Which hours to recur
+     *
+     * This is an array of integers (between 0 and 23)
+     *
+     * @var array
+     */
+    public $byHour;
+
+    /**
+     * Which weekdays to recur.
+     *
+     * This is an array of weekdays
+     *
+     * This may also be preceeded by a positive or negative integer. If present,
+     * this indicates the nth occurrence of a specific day within the monthly or
+     * yearly rrule. For instance, -2TU indicates the second-last tuesday of
+     * the month, or year.
+     *
+     * @var array
+     */
+    public $byDay;
+
+    /**
+     * Which days of the month to recur
+     *
+     * This is an array of days of the months (1-31). The value can also be
+     * negative. -5 for instance means the 5th last day of the month.
+     *
+     * @var array
+     */
+    public $byMonthDay;
+
+    /**
+     * Which days of the year to recur.
+     *
+     * This is an array with days of the year (1 to 366). The values can also
+     * be negative. For instance, -1 will always represent the last day of the
+     * year. (December 31st).
+     *
+     * @var array
+     */
+    public $byYearDay;
+
+    /**
+     * Which week numbers to recur.
+     *
+     * This is an array of integers from 1 to 53. The values can also be
+     * negative. -1 will always refer to the last week of the year.
+     *
+     * @var array
+     */
+    public $byWeekNo;
+
+    /**
+     * Which months to recur
+     *
+     * This is an array of integers from 1 to 12.
+     *
+     * @var array
+     */
+    public $byMonth;
+
+    /**
+     * Which items in an existing st to recur.
+     *
+     * These numbers work together with an existing by* rule. It specifies
+     * exactly which items of the existing by-rule to filter.
+     *
+     * Valid values are 1 to 366 and -1 to -366. As an example, this can be
+     * used to recur the last workday of the month.
+     *
+     * This would be done by setting frequency to 'monthly', byDay to
+     * 'MO,TU,WE,TH,FR' and bySetPos to -1.
+     *
+     * @var array
+     */
+    public $bySetPos;
+
+    /**
+     * When a week starts
+     *
+     * @var string
+     */
+    public $weekStart = 'MO';
+
+    /**
+     * The current item in the list
+     *
+     * @var int
+     */
+    public $counter = 0;
+
+    /**
+     * Simple mapping from iCalendar day names to day numbers
+     *
+     * @var array
+     */
+    private $dayMap = array(
+        'SU' => 0,
+        'MO' => 1,
+        'TU' => 2,
+        'WE' => 3,
+        'TH' => 4,
+        'FR' => 5,
+        'SA' => 6,
+    );
+
+    /**
+     * Mappings between the day number and english day name.
+     *
+     * @var array
+     */
+    private $dayNames = array(
+        0 => 'Sunday',
+        1 => 'Monday',
+        2 => 'Tuesday',
+        3 => 'Wednesday',
+        4 => 'Thursday',
+        5 => 'Friday',
+        6 => 'Saturday',
+    );
+
+    /**
+     * If the current iteration of the event is an overriden event, this
+     * property will hold the VObject
+     *
+     * @var Component
+     */
+    private $currentOverriddenEvent;
+
+    /**
+     * This property may contain the date of the next not-overridden event.
+     * This date is calculated sometimes a bit early, before overridden events
+     * are evaluated.
+     *
+     * @var DateTime
+     */
+    private $nextDate;
+
+    /**
+     * Creates the iterator
+     *
+     * You should pass a VCALENDAR component, as well as the UID of the event
+     * we're going to traverse.
+     *
+     * @param Component $vcal
+     * @param string|null $uid
+     */
+    public function __construct(Component $vcal, $uid=null) {
+
+        if (is_null($uid)) {
+            if ($vcal->name === 'VCALENDAR') {
+                throw new \InvalidArgumentException('If you pass a VCALENDAR object, you must pass a uid argument as well');
+            }
+            $components = array($vcal);
+            $uid = (string)$vcal->uid;
+        } else {
+            $components = $vcal->select('VEVENT');
+        }
+        foreach($components as $component) {
+            if ((string)$component->uid == $uid) {
+                if (isset($component->{'RECURRENCE-ID'})) {
+                    $this->overriddenEvents[$component->DTSTART->getDateTime()->getTimeStamp()] = $component;
+                    $this->overriddenDates[] = $component->{'RECURRENCE-ID'}->getDateTime();
+                } else {
+                    $this->baseEvent = $component;
+                }
+            }
+        }
+        if (!$this->baseEvent) {
+            throw new \InvalidArgumentException('Could not find a base event with uid: ' . $uid);
+        }
+
+        $this->startDate = clone $this->baseEvent->DTSTART->getDateTime();
+
+        $this->endDate = null;
+        if (isset($this->baseEvent->DTEND)) {
+            $this->endDate = clone $this->baseEvent->DTEND->getDateTime();
+        } else {
+            $this->endDate = clone $this->startDate;
+            if (isset($this->baseEvent->DURATION)) {
+                $this->endDate->add(DateTimeParser::parse($this->baseEvent->DURATION->value));
+            } elseif ($this->baseEvent->DTSTART->getDateType()===Property\DateTime::DATE) {
+                $this->endDate->modify('+1 day');
+            }
+        }
+        $this->currentDate = clone $this->startDate;
+
+        $rrule = (string)$this->baseEvent->RRULE;
+
+        $parts = explode(';', $rrule);
+
+        // If no rrule was specified, we create a default setting
+        if (!$rrule) {
+            $this->frequency = 'daily';
+            $this->count = 1;
+        } else foreach($parts as $part) {
+
+            list($key, $value) = explode('=', $part, 2);
+
+            switch(strtoupper($key)) {
+
+                case 'FREQ' :
+                    if (!in_array(
+                        strtolower($value),
+                        array('secondly','minutely','hourly','daily','weekly','monthly','yearly')
+                    )) {
+                        throw new \InvalidArgumentException('Unknown value for FREQ=' . strtoupper($value));
+
+                    }
+                    $this->frequency = strtolower($value);
+                    break;
+
+                case 'UNTIL' :
+                    $this->until = DateTimeParser::parse($value);
+
+                    // In some cases events are generated with an UNTIL=
+                    // parameter before the actual start of the event.
+                    //
+                    // Not sure why this is happening. We assume that the
+                    // intention was that the event only recurs once.
+                    //
+                    // So we are modifying the parameter so our code doesn't
+                    // break.
+                    if($this->until < $this->baseEvent->DTSTART->getDateTime()) {
+                        $this->until = $this->baseEvent->DTSTART->getDateTime();
+                    }
+                    break;
+
+                case 'COUNT' :
+                    $this->count = (int)$value;
+                    break;
+
+                case 'INTERVAL' :
+                    $this->interval = (int)$value;
+                    if ($this->interval < 1) {
+                        throw new \InvalidArgumentException('INTERVAL in RRULE must be a positive integer!');
+                    }
+                    break;
+
+                case 'BYSECOND' :
+                    $this->bySecond = explode(',', $value);
+                    break;
+
+                case 'BYMINUTE' :
+                    $this->byMinute = explode(',', $value);
+                    break;
+
+                case 'BYHOUR' :
+                    $this->byHour = explode(',', $value);
+                    break;
+
+                case 'BYDAY' :
+                    $this->byDay = explode(',', strtoupper($value));
+                    break;
+
+                case 'BYMONTHDAY' :
+                    $this->byMonthDay = explode(',', $value);
+                    break;
+
+                case 'BYYEARDAY' :
+                    $this->byYearDay = explode(',', $value);
+                    break;
+
+                case 'BYWEEKNO' :
+                    $this->byWeekNo = explode(',', $value);
+                    break;
+
+                case 'BYMONTH' :
+                    $this->byMonth = explode(',', $value);
+                    break;
+
+                case 'BYSETPOS' :
+                    $this->bySetPos = explode(',', $value);
+                    break;
+
+                case 'WKST' :
+                    $this->weekStart = strtoupper($value);
+                    break;
+
+            }
+
+        }
+
+        // Parsing exception dates
+        if (isset($this->baseEvent->EXDATE)) {
+            foreach($this->baseEvent->EXDATE as $exDate) {
+
+                foreach(explode(',', (string)$exDate) as $exceptionDate) {
+
+                    $this->exceptionDates[] =
+                        DateTimeParser::parse($exceptionDate, $this->startDate->getTimeZone());
+
+                }
+
+            }
+
+        }
+
+    }
+
+    /**
+     * Returns the current item in the list
+     *
+     * @return DateTime
+     */
+    public function current() {
+
+        if (!$this->valid()) return null;
+        return clone $this->currentDate;
+
+    }
+
+    /**
+     * This method returns the startdate for the current iteration of the
+     * event.
+     *
+     * @return DateTime
+     */
+    public function getDtStart() {
+
+        if (!$this->valid()) return null;
+        return clone $this->currentDate;
+
+    }
+
+    /**
+     * This method returns the enddate for the current iteration of the
+     * event.
+     *
+     * @return DateTime
+     */
+    public function getDtEnd() {
+
+        if (!$this->valid()) return null;
+        $dtEnd = clone $this->currentDate;
+        $dtEnd->add( $this->startDate->diff( $this->endDate ) );
+        return clone $dtEnd;
+
+    }
+
+    /**
+     * Returns a VEVENT object with the updated start and end date.
+     *
+     * Any recurrence information is removed, and this function may return an
+     * 'overridden' event instead.
+     *
+     * This method always returns a cloned instance.
+     *
+     * @return Component\VEvent
+     */
+    public function getEventObject() {
+
+        if ($this->currentOverriddenEvent) {
+            return clone $this->currentOverriddenEvent;
+        }
+        $event = clone $this->baseEvent;
+        unset($event->RRULE);
+        unset($event->EXDATE);
+        unset($event->RDATE);
+        unset($event->EXRULE);
+
+        $event->DTSTART->setDateTime($this->getDTStart(), $event->DTSTART->getDateType());
+        if (isset($event->DTEND)) {
+            $event->DTEND->setDateTime($this->getDtEnd(), $event->DTSTART->getDateType());
+        }
+        if ($this->counter > 0) {
+            $event->{'RECURRENCE-ID'} = (string)$event->DTSTART;
+        }
+
+        return $event;
+
+    }
+
+    /**
+     * Returns the current item number
+     *
+     * @return int
+     */
+    public function key() {
+
+        return $this->counter;
+
+    }
+
+    /**
+     * Whether or not there is a 'next item'
+     *
+     * @return bool
+     */
+    public function valid() {
+
+        if (!is_null($this->count)) {
+            return $this->counter < $this->count;
+        }
+        if (!is_null($this->until)) {
+            return $this->currentDate <= $this->until;
+        }
+        return true;
+
+    }
+
+    /**
+     * Resets the iterator
+     *
+     * @return void
+     */
+    public function rewind() {
+
+        $this->currentDate = clone $this->startDate;
+        $this->counter = 0;
+
+    }
+
+    /**
+     * This method allows you to quickly go to the next occurrence after the
+     * specified date.
+     *
+     * Note that this checks the current 'endDate', not the 'stardDate'. This
+     * means that if you forward to January 1st, the iterator will stop at the
+     * first event that ends *after* January 1st.
+     *
+     * @param DateTime $dt
+     * @return void
+     */
+    public function fastForward(\DateTime $dt) {
+
+        while($this->valid() && $this->getDTEnd() <= $dt) {
+            $this->next();
+        }
+
+    }
+
+    /**
+     * Returns true if this recurring event never ends.
+     *
+     * @return bool
+     */
+    public function isInfinite() {
+
+        return !$this->count && !$this->until;
+
+    }
+
+    /**
+     * Goes on to the next iteration
+     *
+     * @return void
+     */
+    public function next() {
+
+        /*
+        if (!is_null($this->count) && $this->counter >= $this->count) {
+            $this->currentDate = null;
+        }*/
+
+
+        $previousStamp = $this->currentDate->getTimeStamp();
+
+        while(true) {
+
+            $this->currentOverriddenEvent = null;
+
+            // If we have a next date 'stored', we use that
+            if ($this->nextDate) {
+                $this->currentDate = $this->nextDate;
+                $currentStamp = $this->currentDate->getTimeStamp();
+                $this->nextDate = null;
+            } else {
+
+                // Otherwise, we calculate it
+                switch($this->frequency) {
+
+                    case 'hourly' :
+                        $this->nextHourly();
+                        break;
+
+                    case 'daily' :
+                        $this->nextDaily();
+                        break;
+
+                    case 'weekly' :
+                        $this->nextWeekly();
+                        break;
+
+                    case 'monthly' :
+                        $this->nextMonthly();
+                        break;
+
+                    case 'yearly' :
+                        $this->nextYearly();
+                        break;
+
+                }
+                $currentStamp = $this->currentDate->getTimeStamp();
+
+                // Checking exception dates
+                foreach($this->exceptionDates as $exceptionDate) {
+                    if ($this->currentDate == $exceptionDate) {
+                        $this->counter++;
+                        continue 2;
+                    }
+                }
+                foreach($this->overriddenDates as $overriddenDate) {
+                    if ($this->currentDate == $overriddenDate) {
+                        continue 2;
+                    }
+                }
+
+            }
+
+            // Checking overridden events
+            foreach($this->overriddenEvents as $index=>$event) {
+                if ($index > $previousStamp && $index <= $currentStamp) {
+
+                    // We're moving the 'next date' aside, for later use.
+                    $this->nextDate = clone $this->currentDate;
+
+                    $this->currentDate = $event->DTSTART->getDateTime();
+                    $this->currentOverriddenEvent = $event;
+
+                    break;
+                }
+            }
+
+            break;
+
+        }
+
+        /*
+        if (!is_null($this->until)) {
+            if($this->currentDate > $this->until) {
+                $this->currentDate = null;
+            }
+        }*/
+
+        $this->counter++;
+
+    }
+
+    /**
+     * Does the processing for advancing the iterator for hourly frequency.
+     *
+     * @return void
+     */
+    protected function nextHourly() {
+
+        if (!$this->byHour) {
+            $this->currentDate->modify('+' . $this->interval . ' hours');
+            return;
+        }
+    }
+
+    /**
+     * Does the processing for advancing the iterator for daily frequency.
+     *
+     * @return void
+     */
+    protected function nextDaily() {
+
+        if (!$this->byHour && !$this->byDay) {
+            $this->currentDate->modify('+' . $this->interval . ' days');
+            return;
+        }
+
+        if (isset($this->byHour)) {
+            $recurrenceHours = $this->getHours();
+        }
+
+        if (isset($this->byDay)) {
+            $recurrenceDays = $this->getDays();
+        }
+
+        do {
+
+            if ($this->byHour) {
+                if ($this->currentDate->format('G') == '23') {
+                    // to obey the interval rule
+                    $this->currentDate->modify('+' . $this->interval-1 . ' days');
+                }
+
+                $this->currentDate->modify('+1 hours');
+
+            } else {
+                $this->currentDate->modify('+' . $this->interval . ' days');
+
+            }
+
+            // Current day of the week
+            $currentDay = $this->currentDate->format('w');
+
+            // Current hour of the day
+            $currentHour = $this->currentDate->format('G');
+
+        } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+
+    }
+
+    /**
+     * Does the processing for advancing the iterator for weekly frequency.
+     *
+     * @return void
+     */
+    protected function nextWeekly() {
+
+        if (!$this->byHour && !$this->byDay) {
+            $this->currentDate->modify('+' . $this->interval . ' weeks');
+            return;
+        }
+
+        if ($this->byHour) {
+            $recurrenceHours = $this->getHours();
+        }
+
+        if ($this->byDay) {
+            $recurrenceDays = $this->getDays();
+        }
+
+        // First day of the week:
+        $firstDay = $this->dayMap[$this->weekStart];
+
+        do {
+
+            if ($this->byHour) {
+                $this->currentDate->modify('+1 hours');
+            } else {
+                $this->currentDate->modify('+1 days');
+            }
+
+            // Current day of the week
+            $currentDay = (int) $this->currentDate->format('w');
+
+            // Current hour of the day
+            $currentHour = (int) $this->currentDate->format('G');
+
+            // We need to roll over to the next week
+            if ($currentDay === $firstDay && (!$this->byHour || $currentHour == '0')) {
+                $this->currentDate->modify('+' . $this->interval-1 . ' weeks');
+
+                // We need to go to the first day of this week, but only if we
+                // are not already on this first day of this week.
+                if($this->currentDate->format('w') != $firstDay) {
+                    $this->currentDate->modify('last ' . $this->dayNames[$this->dayMap[$this->weekStart]]);
+                }
+            }
+
+            // We have a match
+        } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours)));
+    }
+
+    /**
+     * Does the processing for advancing the iterator for monthly frequency.
+     *
+     * @return void
+     */
+    protected function nextMonthly() {
+
+        $currentDayOfMonth = $this->currentDate->format('j');
+        if (!$this->byMonthDay && !$this->byDay) {
+
+            // If the current day is higher than the 28th, rollover can
+            // occur to the next month. We Must skip these invalid
+            // entries.
+            if ($currentDayOfMonth < 29) {
+                $this->currentDate->modify('+' . $this->interval . ' months');
+            } else {
+                $increase = 0;
+                do {
+                    $increase++;
+                    $tempDate = clone $this->currentDate;
+                    $tempDate->modify('+ ' . ($this->interval*$increase) . ' months');
+                } while ($tempDate->format('j') != $currentDayOfMonth);
+                $this->currentDate = $tempDate;
+            }
+            return;
+        }
+
+        while(true) {
+
+            $occurrences = $this->getMonthlyOccurrences();
+
+            foreach($occurrences as $occurrence) {
+
+                // The first occurrence thats higher than the current
+                // day of the month wins.
+                if ($occurrence > $currentDayOfMonth) {
+                    break 2;
+                }
+
+            }
+
+            // If we made it all the way here, it means there were no
+            // valid occurrences, and we need to advance to the next
+            // month.
+            $this->currentDate->modify('first day of this month');
+            $this->currentDate->modify('+ ' . $this->interval . ' months');
+
+            // This goes to 0 because we need to start counting at hte
+            // beginning.
+            $currentDayOfMonth = 0;
+
+        }
+
+        $this->currentDate->setDate($this->currentDate->format('Y'), $this->currentDate->format('n'), $occurrence);
+
+    }
+
+    /**
+     * Does the processing for advancing the iterator for yearly frequency.
+     *
+     * @return void
+     */
+    protected function nextYearly() {
+
+        $currentMonth = $this->currentDate->format('n');
+        $currentYear = $this->currentDate->format('Y');
+        $currentDayOfMonth = $this->currentDate->format('j');
+
+        // No sub-rules, so we just advance by year
+        if (!$this->byMonth) {
+
+            // Unless it was a leap day!
+            if ($currentMonth==2 && $currentDayOfMonth==29) {
+
+                $counter = 0;
+                do {
+                    $counter++;
+                    // Here we increase the year count by the interval, until
+                    // we hit a date that's also in a leap year.
+                    //
+                    // We could just find the next interval that's dividable by
+                    // 4, but that would ignore the rule that there's no leap
+                    // year every year that's dividable by a 100, but not by
+                    // 400. (1800, 1900, 2100). So we just rely on the datetime
+                    // functions instead.
+                    $nextDate = clone $this->currentDate;
+                    $nextDate->modify('+ ' . ($this->interval*$counter) . ' years');
+                } while ($nextDate->format('n')!=2);
+                $this->currentDate = $nextDate;
+
+                return;
+
+            }
+
+            // The easiest form
+            $this->currentDate->modify('+' . $this->interval . ' years');
+            return;
+
+        }
+
+        $currentMonth = $this->currentDate->format('n');
+        $currentYear = $this->currentDate->format('Y');
+        $currentDayOfMonth = $this->currentDate->format('j');
+
+        $advancedToNewMonth = false;
+
+        // If we got a byDay or getMonthDay filter, we must first expand
+        // further.
+        if ($this->byDay || $this->byMonthDay) {
+
+            while(true) {
+
+                $occurrences = $this->getMonthlyOccurrences();
+
+                foreach($occurrences as $occurrence) {
+
+                    // The first occurrence that's higher than the current
+                    // day of the month wins.
+                    // If we advanced to the next month or year, the first
+                    // occurrence is always correct.
+                    if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) {
+                        break 2;
+                    }
+
+                }
+
+                // If we made it here, it means we need to advance to
+                // the next month or year.
+                $currentDayOfMonth = 1;
+                $advancedToNewMonth = true;
+                do {
+
+                    $currentMonth++;
+                    if ($currentMonth>12) {
+                        $currentYear+=$this->interval;
+                        $currentMonth = 1;
+                    }
+                } while (!in_array($currentMonth, $this->byMonth));
+
+                $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+            }
+
+            // If we made it here, it means we got a valid occurrence
+            $this->currentDate->setDate($currentYear, $currentMonth, $occurrence);
+            return;
+
+        } else {
+
+            // These are the 'byMonth' rules, if there are no byDay or
+            // byMonthDay sub-rules.
+            do {
+
+                $currentMonth++;
+                if ($currentMonth>12) {
+                    $currentYear+=$this->interval;
+                    $currentMonth = 1;
+                }
+            } while (!in_array($currentMonth, $this->byMonth));
+            $this->currentDate->setDate($currentYear, $currentMonth, $currentDayOfMonth);
+
+            return;
+
+        }
+
+    }
+
+    /**
+     * Returns all the occurrences for a monthly frequency with a 'byDay' or
+     * 'byMonthDay' expansion for the current month.
+     *
+     * The returned list is an array of integers with the day of month (1-31).
+     *
+     * @return array
+     */
+    protected function getMonthlyOccurrences() {
+
+        $startDate = clone $this->currentDate;
+
+        $byDayResults = array();
+
+        // Our strategy is to simply go through the byDays, advance the date to
+        // that point and add it to the results.
+        if ($this->byDay) foreach($this->byDay as $day) {
+
+            $dayName = $this->dayNames[$this->dayMap[substr($day,-2)]];
+
+            // Dayname will be something like 'wednesday'. Now we need to find
+            // all wednesdays in this month.
+            $dayHits = array();
+
+            $checkDate = clone $startDate;
+            $checkDate->modify('first day of this month');
+            $checkDate->modify($dayName);
+
+            do {
+                $dayHits[] = $checkDate->format('j');
+                $checkDate->modify('next ' . $dayName);
+            } while ($checkDate->format('n') === $startDate->format('n'));
+
+            // So now we have 'all wednesdays' for month. It is however
+            // possible that the user only really wanted the 1st, 2nd or last
+            // wednesday.
+            if (strlen($day)>2) {
+                $offset = (int)substr($day,0,-2);
+
+                if ($offset>0) {
+                    // It is possible that the day does not exist, such as a
+                    // 5th or 6th wednesday of the month.
+                    if (isset($dayHits[$offset-1])) {
+                        $byDayResults[] = $dayHits[$offset-1];
+                    }
+                } else {
+
+                    // if it was negative we count from the end of the array
+                    $byDayResults[] = $dayHits[count($dayHits) + $offset];
+                }
+            } else {
+                // There was no counter (first, second, last wednesdays), so we
+                // just need to add the all to the list).
+                $byDayResults = array_merge($byDayResults, $dayHits);
+
+            }
+
+        }
+
+        $byMonthDayResults = array();
+        if ($this->byMonthDay) foreach($this->byMonthDay as $monthDay) {
+
+            // Removing values that are out of range for this month
+            if ($monthDay > $startDate->format('t') ||
+                $monthDay < 0-$startDate->format('t')) {
+                    continue;
+            }
+            if ($monthDay>0) {
+                $byMonthDayResults[] = $monthDay;
+            } else {
+                // Negative values
+                $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay;
+            }
+        }
+
+        // If there was just byDay or just byMonthDay, they just specify our
+        // (almost) final list. If both were provided, then byDay limits the
+        // list.
+        if ($this->byMonthDay && $this->byDay) {
+            $result = array_intersect($byMonthDayResults, $byDayResults);
+        } elseif ($this->byMonthDay) {
+            $result = $byMonthDayResults;
+        } else {
+            $result = $byDayResults;
+        }
+        $result = array_unique($result);
+        sort($result, SORT_NUMERIC);
+
+        // The last thing that needs checking is the BYSETPOS. If it's set, it
+        // means only certain items in the set survive the filter.
+        if (!$this->bySetPos) {
+            return $result;
+        }
+
+        $filteredResult = array();
+        foreach($this->bySetPos as $setPos) {
+
+            if ($setPos<0) {
+                $setPos = count($result)-($setPos+1);
+            }
+            if (isset($result[$setPos-1])) {
+                $filteredResult[] = $result[$setPos-1];
+            }
+        }
+
+        sort($filteredResult, SORT_NUMERIC);
+        return $filteredResult;
+
+    }
+
+    protected function getHours()
+    {
+        $recurrenceHours = array();
+        foreach($this->byHour as $byHour) {
+            $recurrenceHours[] = $byHour;
+        }
+
+        return $recurrenceHours;
+    }
+
+    protected function getDays()
+    {
+        $recurrenceDays = array();
+        foreach($this->byDay as $byDay) {
+
+            // The day may be preceeded with a positive (+n) or
+            // negative (-n) integer. However, this does not make
+            // sense in 'weekly' so we ignore it here.
+            $recurrenceDays[] = $this->dayMap[substr($byDay,-2)];
+
+        }
+
+        return $recurrenceDays;
+    }
+}
+
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php
new file mode 100644
index 0000000..657cfb8
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Sabre\VObject\Splitter;
+
+use Sabre\VObject;
+
+/**
+ * Splitter
+ *
+ * This class is responsible for splitting up iCalendar objects.
+ *
+ * This class expects a single VCALENDAR object with one or more
+ * calendar-objects inside. Objects with identical UID's will be combined into
+ * a single object.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @author Armin Hackmann
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class ICalendar implements SplitterInterface {
+
+    /**
+     * Timezones
+     *
+     * @var array
+     */
+    protected $vtimezones = array();
+
+    /**
+     * iCalendar objects
+     *
+     * @var array
+     */
+    protected $objects = array();
+
+    /**
+     * Constructor
+     *
+     * The splitter should receive an readable file stream as it's input.
+     *
+     * @param resource $input
+     */
+    public function __construct($input) {
+
+        $data = VObject\Reader::read(stream_get_contents($input));
+        $vtimezones = array();
+        $components = array();
+
+        foreach($data->children as $component) {
+            if (!$component instanceof VObject\Component) {
+                continue;
+            }
+
+            // Get all timezones
+            if ($component->name === 'VTIMEZONE') {
+                $this->vtimezones[(string)$component->TZID] = $component;
+                continue;
+            }
+
+            // Get component UID for recurring Events search
+            if($component->UID) {
+                $uid = (string)$component->UID;
+            } else {
+                // Generating a random UID
+                $uid = sha1(microtime()) . '-vobjectimport';
+            }
+
+            // Take care of recurring events
+            if (!array_key_exists($uid, $this->objects)) {
+                $this->objects[$uid] = VObject\Component::create('VCALENDAR');
+            }
+
+            $this->objects[$uid]->add(clone $component);
+        }
+
+    }
+
+    /**
+     * Every time getNext() is called, a new object will be parsed, until we
+     * hit the end of the stream.
+     *
+     * When the end is reached, null will be returned.
+     *
+     * @return Sabre\VObject\Component|null
+     */
+    public function getNext() {
+
+        if($object=array_shift($this->objects)) {
+
+            // create our baseobject
+            $object->version = '2.0';
+            $object->prodid = '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
+            $object->calscale = 'GREGORIAN';
+
+            // add vtimezone information to obj (if we have it)
+            foreach ($this->vtimezones as $vtimezone) {
+                $object->add($vtimezone);
+            }
+
+            return $object;
+
+        } else {
+
+            return null;
+
+        }
+
+   }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php
new file mode 100644
index 0000000..c012688
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Sabre\VObject\Splitter;
+
+/**
+ * VObject splitter
+ *
+ * The splitter is responsible for reading a large vCard or iCalendar object,
+ * and splitting it into multiple objects.
+ *
+ * This is for example for Card and CalDAV, which require every event and vcard
+ * to exist in their own objects, instead of one large one.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+interface SplitterInterface {
+
+    /**
+     * Constructor
+     *
+     * The splitter should receive an readable file stream as it's input.
+     *
+     * @param resource $input
+     */
+    function __construct($input);
+
+    /**
+     * Every time getNext() is called, a new object will be parsed, until we
+     * hit the end of the stream.
+     *
+     * When the end is reached, null will be returned.
+     *
+     * @return Sabre\VObject\Component|null
+     */
+    function getNext();
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Splitter/VCard.php b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/VCard.php
new file mode 100644
index 0000000..7a8718c
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/VCard.php
@@ -0,0 +1,76 @@
+<?php
+
+namespace Sabre\VObject\Splitter;
+
+use Sabre\VObject;
+
+/**
+ * Splitter
+ *
+ * This class is responsible for splitting up VCard objects.
+ *
+ * It is assumed that the input stream contains 1 or more VCARD objects. This
+ * class checks for BEGIN:VCARD and END:VCARD and parses each encountered
+ * component individually.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Dominik Tobschall
+ * @author Armin Hackmann
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class VCard implements SplitterInterface {
+
+    /**
+     * File handle
+     *
+     * @var resource
+     */
+    protected $input;
+
+    /**
+     * Constructor
+     *
+     * The splitter should receive an readable file stream as it's input.
+     *
+     * @param resource $input
+     */
+    public function __construct($input) {
+
+        $this->input = $input;
+
+    }
+
+    /**
+     * Every time getNext() is called, a new object will be parsed, until we
+     * hit the end of the stream.
+     *
+     * When the end is reached, null will be returned.
+     *
+     * @return Sabre\VObject\Component|null
+     */
+    public function getNext() {
+
+        $vcard = '';
+
+        do {
+
+            if (feof($this->input)) {
+                return false;
+            }
+
+            $line = fgets($this->input);
+            $vcard .= $line;
+
+        } while(strtoupper(substr($line,0,4))!=="END:");
+
+        $object = VObject\Reader::read($vcard);
+
+        if($object->name !== 'VCARD') {
+            throw new \InvalidArgumentException("Thats no vCard!", 1);
+        }
+
+        return $object;
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php b/plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php
new file mode 100644
index 0000000..ea88e1e
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * Useful utilities for working with various strings.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class StringUtil {
+
+    /**
+     * Returns true or false depending on if a string is valid UTF-8
+     *
+     * @param string $str
+     * @return bool
+     */
+    static function isUTF8($str) {
+
+        // First check.. mb_check_encoding
+        if (!mb_check_encoding($str, 'UTF-8')) {
+            return false;
+        }
+
+        // Control characters
+        if (preg_match('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', $str)) {
+            return false;
+        }
+
+        return true;
+
+    }
+
+    /**
+     * This method tries its best to convert the input string to UTF-8.
+     *
+     * Currently only ISO-5991-1 input and UTF-8 input is supported, but this
+     * may be expanded upon if we receive other examples.
+     *
+     * @param string $str
+     * @return string
+     */
+    static function convertToUTF8($str) {
+
+        $encoding = mb_detect_encoding($str , array('UTF-8','ISO-8859-1'), true);
+
+        if ($encoding === 'ISO-8859-1') {
+            $newStr = utf8_encode($str);
+        } else {
+            $newStr = $str;
+        }
+
+        // Removing any control characters
+        return (preg_replace('%(?:[\x00-\x08\x0B-\x0C\x0E\x0F])%', '', $newStr));
+
+    }
+
+}
+
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/TimeZoneUtil.php b/plugins/libcalendaring/lib/Sabre/VObject/TimeZoneUtil.php
new file mode 100644
index 0000000..6f5b69f
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/TimeZoneUtil.php
@@ -0,0 +1,482 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * Time zone name translation
+ *
+ * This file translates well-known time zone names into "Olson database" time zone names.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Frank Edelhaeuser (fedel at users.sourceforge.net)
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class TimeZoneUtil {
+
+    public static $map = array(
+
+        // from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/zone_tzid.html
+        // snapshot taken on 2012/01/16
+
+        // windows
+        'AUS Central Standard Time'=>'Australia/Darwin',
+        'AUS Eastern Standard Time'=>'Australia/Sydney',
+        'Afghanistan Standard Time'=>'Asia/Kabul',
+        'Alaskan Standard Time'=>'America/Anchorage',
+        'Arab Standard Time'=>'Asia/Riyadh',
+        'Arabian Standard Time'=>'Asia/Dubai',
+        'Arabic Standard Time'=>'Asia/Baghdad',
+        'Argentina Standard Time'=>'America/Buenos_Aires',
+        'Armenian Standard Time'=>'Asia/Yerevan',
+        'Atlantic Standard Time'=>'America/Halifax',
+        'Azerbaijan Standard Time'=>'Asia/Baku',
+        'Azores Standard Time'=>'Atlantic/Azores',
+        'Bangladesh Standard Time'=>'Asia/Dhaka',
+        'Canada Central Standard Time'=>'America/Regina',
+        'Cape Verde Standard Time'=>'Atlantic/Cape_Verde',
+        'Caucasus Standard Time'=>'Asia/Yerevan',
+        'Cen. Australia Standard Time'=>'Australia/Adelaide',
+        'Central America Standard Time'=>'America/Guatemala',
+        'Central Asia Standard Time'=>'Asia/Almaty',
+        'Central Brazilian Standard Time'=>'America/Cuiaba',
+        'Central Europe Standard Time'=>'Europe/Budapest',
+        'Central European Standard Time'=>'Europe/Warsaw',
+        'Central Pacific Standard Time'=>'Pacific/Guadalcanal',
+        'Central Standard Time'=>'America/Chicago',
+        'Central Standard Time (Mexico)'=>'America/Mexico_City',
+        'China Standard Time'=>'Asia/Shanghai',
+        'Dateline Standard Time'=>'Etc/GMT+12',
+        'E. Africa Standard Time'=>'Africa/Nairobi',
+        'E. Australia Standard Time'=>'Australia/Brisbane',
+        'E. Europe Standard Time'=>'Europe/Minsk',
+        'E. South America Standard Time'=>'America/Sao_Paulo',
+        'Eastern Standard Time'=>'America/New_York',
+        'Egypt Standard Time'=>'Africa/Cairo',
+        'Ekaterinburg Standard Time'=>'Asia/Yekaterinburg',
+        'FLE Standard Time'=>'Europe/Kiev',
+        'Fiji Standard Time'=>'Pacific/Fiji',
+        'GMT Standard Time'=>'Europe/London',
+        'GTB Standard Time'=>'Europe/Istanbul',
+        'Georgian Standard Time'=>'Asia/Tbilisi',
+        'Greenland Standard Time'=>'America/Godthab',
+        'Greenwich Standard Time'=>'Atlantic/Reykjavik',
+        'Hawaiian Standard Time'=>'Pacific/Honolulu',
+        'India Standard Time'=>'Asia/Calcutta',
+        'Iran Standard Time'=>'Asia/Tehran',
+        'Israel Standard Time'=>'Asia/Jerusalem',
+        'Jordan Standard Time'=>'Asia/Amman',
+        'Kamchatka Standard Time'=>'Asia/Kamchatka',
+        'Korea Standard Time'=>'Asia/Seoul',
+        'Magadan Standard Time'=>'Asia/Magadan',
+        'Mauritius Standard Time'=>'Indian/Mauritius',
+        'Mexico Standard Time'=>'America/Mexico_City',
+        'Mexico Standard Time 2'=>'America/Chihuahua',
+        'Mid-Atlantic Standard Time'=>'Etc/GMT-2',
+        'Middle East Standard Time'=>'Asia/Beirut',
+        'Montevideo Standard Time'=>'America/Montevideo',
+        'Morocco Standard Time'=>'Africa/Casablanca',
+        'Mountain Standard Time'=>'America/Denver',
+        'Mountain Standard Time (Mexico)'=>'America/Chihuahua',
+        'Myanmar Standard Time'=>'Asia/Rangoon',
+        'N. Central Asia Standard Time'=>'Asia/Novosibirsk',
+        'Namibia Standard Time'=>'Africa/Windhoek',
+        'Nepal Standard Time'=>'Asia/Katmandu',
+        'New Zealand Standard Time'=>'Pacific/Auckland',
+        'Newfoundland Standard Time'=>'America/St_Johns',
+        'North Asia East Standard Time'=>'Asia/Irkutsk',
+        'North Asia Standard Time'=>'Asia/Krasnoyarsk',
+        'Pacific SA Standard Time'=>'America/Santiago',
+        'Pacific Standard Time'=>'America/Los_Angeles',
+        'Pacific Standard Time (Mexico)'=>'America/Santa_Isabel',
+        'Pakistan Standard Time'=>'Asia/Karachi',
+        'Paraguay Standard Time'=>'America/Asuncion',
+        'Romance Standard Time'=>'Europe/Paris',
+        'Russian Standard Time'=>'Europe/Moscow',
+        'SA Eastern Standard Time'=>'America/Cayenne',
+        'SA Pacific Standard Time'=>'America/Bogota',
+        'SA Western Standard Time'=>'America/La_Paz',
+        'SE Asia Standard Time'=>'Asia/Bangkok',
+        'Samoa Standard Time'=>'Pacific/Apia',
+        'Singapore Standard Time'=>'Asia/Singapore',
+        'South Africa Standard Time'=>'Africa/Johannesburg',
+        'Sri Lanka Standard Time'=>'Asia/Colombo',
+        'Syria Standard Time'=>'Asia/Damascus',
+        'Taipei Standard Time'=>'Asia/Taipei',
+        'Tasmania Standard Time'=>'Australia/Hobart',
+        'Tokyo Standard Time'=>'Asia/Tokyo',
+        'Tonga Standard Time'=>'Pacific/Tongatapu',
+        'US Eastern Standard Time'=>'America/Indianapolis',
+        'US Mountain Standard Time'=>'America/Phoenix',
+        'UTC+12'=>'Etc/GMT-12',
+        'UTC-02'=>'Etc/GMT+2',
+        'UTC-11'=>'Etc/GMT+11',
+        'Ulaanbaatar Standard Time'=>'Asia/Ulaanbaatar',
+        'Venezuela Standard Time'=>'America/Caracas',
+        'Vladivostok Standard Time'=>'Asia/Vladivostok',
+        'W. Australia Standard Time'=>'Australia/Perth',
+        'W. Central Africa Standard Time'=>'Africa/Lagos',
+        'W. Europe Standard Time'=>'Europe/Berlin',
+        'West Asia Standard Time'=>'Asia/Tashkent',
+        'West Pacific Standard Time'=>'Pacific/Port_Moresby',
+        'Yakutsk Standard Time'=>'Asia/Yakutsk',
+
+        // Microsoft exchange timezones
+        // Source:
+        // http://msdn.microsoft.com/en-us/library/ms988620%28v=exchg.65%29.aspx
+        //
+        // Correct timezones deduced with help from:
+        // http://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+        'Universal Coordinated Time' => 'UTC',
+        'Casablanca, Monrovia' => 'Africa/Casablanca',
+        'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon',
+        'Greenwich Mean Time; Dublin, Edinburgh, London' =>  'Europe/London',
+        'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin',
+        'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague',
+        'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris',
+        'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris',
+        'Prague, Central Europe' => 'Europe/Prague',
+        'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo',
+        'West Central Africa' => 'Africa/Luanda', // This was a best guess
+        'Athens, Istanbul, Minsk' => 'Europe/Athens',
+        'Bucharest' => 'Europe/Bucharest',
+        'Cairo' => 'Africa/Cairo',
+        'Harare, Pretoria' => 'Africa/Harare',
+        'Helsinki, Riga, Tallinn' => 'Europe/Helsinki',
+        'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem',
+        'Baghdad' => 'Asia/Baghdad',
+        'Arab, Kuwait, Riyadh' => 'Asia/Kuwait',
+        'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow',
+        'East Africa, Nairobi' => 'Africa/Nairobi',
+        'Tehran' => 'Asia/Tehran',
+        'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess
+        'Baku, Tbilisi, Yerevan' => 'Asia/Baku',
+        'Kabul' => 'Asia/Kabul',
+        'Ekaterinburg' => 'Asia/Yekaterinburg',
+        'Islamabad, Karachi, Tashkent' => 'Asia/Karachi',
+        'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta',
+        'Kathmandu, Nepal' => 'Asia/Kathmandu',
+        'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty',
+        'Astana, Dhaka' => 'Asia/Dhaka',
+        'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo',
+        'Rangoon' => 'Asia/Rangoon',
+        'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok',
+        'Krasnoyarsk' => 'Asia/Krasnoyarsk',
+        'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai',
+        'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk',
+        'Kuala Lumpur, Singapore' => 'Asia/Singapore',
+        'Perth, Western Australia' => 'Australia/Perth',
+        'Taipei' => 'Asia/Taipei',
+        'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo',
+        'Seoul, Korea Standard time' => 'Asia/Seoul',
+        'Yakutsk' => 'Asia/Yakutsk',
+        'Adelaide, Central Australia' => 'Australia/Adelaide',
+        'Darwin' => 'Australia/Darwin',
+        'Brisbane, East Australia' => 'Australia/Brisbane',
+        'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney',
+        'Guam, Port Moresby' => 'Pacific/Guam',
+        'Hobart, Tasmania' => 'Australia/Hobart',
+        'Vladivostok' => 'Asia/Vladivostok',
+        'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan',
+        'Auckland, Wellington' => 'Pacific/Auckland',
+        'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji',
+        'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu',
+        'Azores' => 'Atlantic/Azores',
+        'Cape Verde Is.' => 'Atlantic/Cape_Verde',
+        'Mid-Atlantic' => 'America/Noronha',
+        'Brasilia' => 'America/Sao_Paulo', // Best guess
+        'Buenos Aires' => 'America/Argentina/Buenos_Aires',
+        'Greenland' => 'America/Godthab',
+        'Newfoundland' => 'America/St_Johns',
+        'Atlantic Time (Canada)' => 'America/Halifax',
+        'Caracas, La Paz' => 'America/Caracas',
+        'Santiago' => 'America/Santiago',
+        'Bogota, Lima, Quito' => 'America/Bogota',
+        'Eastern Time (US & Canada)' => 'America/New_York',
+        'Indiana (East)' => 'America/Indiana/Indianapolis',
+        'Central America' => 'America/Guatemala',
+        'Central Time (US & Canada)' => 'America/Chicago',
+        'Mexico City, Tegucigalpa' => 'America/Mexico_City',
+        'Saskatchewan' => 'America/Edmonton',
+        'Arizona' => 'America/Phoenix',
+        'Mountain Time (US & Canada)' => 'America/Denver', // Best guess
+        'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess
+        'Alaska' => 'America/Anchorage',
+        'Hawaii' => 'Pacific/Honolulu',
+        'Midway Island, Samoa' => 'Pacific/Midway',
+        'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein',
+
+        // The following list are timezone names that could be generated by
+        // Lotus / Domino
+        'Dateline'               => 'Etc/GMT-12',
+        'Samoa'                  => 'Pacific/Apia',
+        'Hawaiian'               => 'Pacific/Honolulu',
+        'Alaskan'                => 'America/Anchorage',
+        'Pacific'                => 'America/Los_Angeles',
+        'Pacific Standard Time'  => 'America/Los_Angeles',
+        'Mexico Standard Time 2' => 'America/Chihuahua',
+        'Mountain'               => 'America/Denver',
+        'Mountain Standard Time' => 'America/Chihuahua',
+        'US Mountain'            => 'America/Phoenix',
+        'Canada Central'         => 'America/Edmonton',
+        'Central America'        => 'America/Guatemala',
+        'Central'                => 'America/Chicago',
+        'Central Standard Time'  => 'America/Mexico_City',
+        'Mexico'                 => 'America/Mexico_City',
+        'Eastern'                => 'America/New_York',
+        'SA Pacific'             => 'America/Bogota',
+        'US Eastern'             => 'America/Indiana/Indianapolis',
+        'Venezuela'              => 'America/Caracas',
+        'Atlantic'               => 'America/Halifax',
+        'Central Brazilian'      => 'America/Manaus',
+        'Pacific SA'             => 'America/Santiago',
+        'SA Western'             => 'America/La_Paz',
+        'Newfoundland'           => 'America/St_Johns',
+        'Argentina'              => 'America/Argentina/Buenos_Aires',
+        'E. South America'       => 'America/Belem',
+        'Greenland'              => 'America/Godthab',
+        'Montevideo'             => 'America/Montevideo',
+        'SA Eastern'             => 'America/Belem',
+        'Mid-Atlantic'           => 'Etc/GMT-2',
+        'Azores'                 => 'Atlantic/Azores',
+        'Cape Verde'             => 'Atlantic/Cape_Verde',
+        'Greenwich'              => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT.
+        'Morocco'                => 'Africa/Casablanca',
+        'Central Europe'         => 'Europe/Prague',
+        'Central European'       => 'Europe/Sarajevo',
+        'Romance'                => 'Europe/Paris',
+        'W. Central Africa'      => 'Africa/Lagos', // Best guess
+        'W. Europe'              => 'Europe/Amsterdam',
+        'E. Europe'              => 'Europe/Minsk',
+        'Egypt'                  => 'Africa/Cairo',
+        'FLE'                    => 'Europe/Helsinki',
+        'GTB'                    => 'Europe/Athens',
+        'Israel'                 => 'Asia/Jerusalem',
+        'Jordan'                 => 'Asia/Amman',
+        'Middle East'            => 'Asia/Beirut',
+        'Namibia'                => 'Africa/Windhoek',
+        'South Africa'           => 'Africa/Harare',
+        'Arab'                   => 'Asia/Kuwait',
+        'Arabic'                 => 'Asia/Baghdad',
+        'E. Africa'              => 'Africa/Nairobi',
+        'Georgian'               => 'Asia/Tbilisi',
+        'Russian'                => 'Europe/Moscow',
+        'Iran'                   => 'Asia/Tehran',
+        'Arabian'                => 'Asia/Muscat',
+        'Armenian'               => 'Asia/Yerevan',
+        'Azerbijan'              => 'Asia/Baku',
+        'Caucasus'               => 'Asia/Yerevan',
+        'Mauritius'              => 'Indian/Mauritius',
+        'Afghanistan'            => 'Asia/Kabul',
+        'Ekaterinburg'           => 'Asia/Yekaterinburg',
+        'Pakistan'               => 'Asia/Karachi',
+        'West Asia'              => 'Asia/Tashkent',
+        'India'                  => 'Asia/Calcutta',
+        'Sri Lanka'              => 'Asia/Colombo',
+        'Nepal'                  => 'Asia/Kathmandu',
+        'Central Asia'           => 'Asia/Dhaka',
+        'N. Central Asia'        => 'Asia/Almaty',
+        'Myanmar'                => 'Asia/Rangoon',
+        'North Asia'             => 'Asia/Krasnoyarsk',
+        'SE Asia'                => 'Asia/Bangkok',
+        'China'                  => 'Asia/Shanghai',
+        'North Asia East'        => 'Asia/Irkutsk',
+        'Singapore'              => 'Asia/Singapore',
+        'Taipei'                 => 'Asia/Taipei',
+        'W. Australia'           => 'Australia/Perth',
+        'Korea'                  => 'Asia/Seoul',
+        'Tokyo'                  => 'Asia/Tokyo',
+        'Yakutsk'                => 'Asia/Yakutsk',
+        'AUS Central'            => 'Australia/Darwin',
+        'Cen. Australia'         => 'Australia/Adelaide',
+        'AUS Eastern'            => 'Australia/Sydney',
+        'E. Australia'           => 'Australia/Brisbane',
+        'Tasmania'               => 'Australia/Hobart',
+        'Vladivostok'            => 'Asia/Vladivostok',
+        'West Pacific'           => 'Pacific/Guam',
+        'Central Pacific'        => 'Asia/Magadan',
+        'Fiji'                   => 'Pacific/Fiji',
+        'New Zealand'            => 'Pacific/Auckland',
+        'Tonga'                  => 'Pacific/Tongatapu',
+    );
+
+    /**
+     * List of microsoft exchange timezone ids.
+     *
+     * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx
+     */
+    public static $microsoftExchangeMap = array(
+        0  => 'UTC',
+        31 => 'Africa/Casablanca',
+
+        // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo.
+        // I'm not even kidding.. We handle this special case in the
+        // getTimeZone method.
+        2  => 'Europe/Lisbon',
+        1  => 'Europe/London',
+        4  => 'Europe/Berlin',
+        6  => 'Europe/Prague',
+        3  => 'Europe/Paris',
+        69 => 'Africa/Luanda', // This was a best guess
+        7  => 'Europe/Athens',
+        5  => 'Europe/Bucharest',
+        49 => 'Africa/Cairo',
+        50 => 'Africa/Harare',
+        59 => 'Europe/Helsinki',
+        27 => 'Asia/Jerusalem',
+        26 => 'Asia/Baghdad',
+        74 => 'Asia/Kuwait',
+        51 => 'Europe/Moscow',
+        56 => 'Africa/Nairobi',
+        25 => 'Asia/Tehran',
+        24 => 'Asia/Muscat', // Best guess
+        54 => 'Asia/Baku',
+        48 => 'Asia/Kabul',
+        58 => 'Asia/Yekaterinburg',
+        47 => 'Asia/Karachi',
+        23 => 'Asia/Calcutta',
+        62 => 'Asia/Kathmandu',
+        46 => 'Asia/Almaty',
+        71 => 'Asia/Dhaka',
+        66 => 'Asia/Colombo',
+        61 => 'Asia/Rangoon',
+        22 => 'Asia/Bangkok',
+        64 => 'Asia/Krasnoyarsk',
+        45 => 'Asia/Shanghai',
+        63 => 'Asia/Irkutsk',
+        21 => 'Asia/Singapore',
+        73 => 'Australia/Perth',
+        75 => 'Asia/Taipei',
+        20 => 'Asia/Tokyo',
+        72 => 'Asia/Seoul',
+        70 => 'Asia/Yakutsk',
+        19 => 'Australia/Adelaide',
+        44 => 'Australia/Darwin',
+        18 => 'Australia/Brisbane',
+        76 => 'Australia/Sydney',
+        43 => 'Pacific/Guam',
+        42 => 'Australia/Hobart',
+        68 => 'Asia/Vladivostok',
+        41 => 'Asia/Magadan',
+        17 => 'Pacific/Auckland',
+        40 => 'Pacific/Fiji',
+        67 => 'Pacific/Tongatapu',
+        29 => 'Atlantic/Azores',
+        53 => 'Atlantic/Cape_Verde',
+        30 => 'America/Noronha',
+         8 => 'America/Sao_Paulo', // Best guess
+        32 => 'America/Argentina/Buenos_Aires',
+        60 => 'America/Godthab',
+        28 => 'America/St_Johns',
+         9 => 'America/Halifax',
+        33 => 'America/Caracas',
+        65 => 'America/Santiago',
+        35 => 'America/Bogota',
+        10 => 'America/New_York',
+        34 => 'America/Indiana/Indianapolis',
+        55 => 'America/Guatemala',
+        11 => 'America/Chicago',
+        37 => 'America/Mexico_City',
+        36 => 'America/Edmonton',
+        38 => 'America/Phoenix',
+        12 => 'America/Denver', // Best guess
+        13 => 'America/Los_Angeles', // Best guess
+        14 => 'America/Anchorage',
+        15 => 'Pacific/Honolulu',
+        16 => 'Pacific/Midway',
+        39 => 'Pacific/Kwajalein',
+    );
+
+    /**
+     * This method will try to find out the correct timezone for an iCalendar
+     * date-time value.
+     *
+     * You must pass the contents of the TZID parameter, as well as the full
+     * calendar.
+     *
+     * If the lookup fails, this method will return the default PHP timezone
+     * (as configured using date_default_timezone_set, or the date.timezone ini
+     * setting).
+     *
+     * Alternatively, if $failIfUncertain is set to true, it will throw an
+     * exception if we cannot accurately determine the timezone.
+     *
+     * @param string $tzid
+     * @param Sabre\VObject\Component $vcalendar
+     * @return DateTimeZone
+     */
+    static public function getTimeZone($tzid, Component $vcalendar = null, $failIfUncertain = false) {
+
+        // First we will just see if the tzid is a support timezone identifier.
+        try {
+            return new \DateTimeZone($tzid);
+        } catch (\Exception $e) {
+        }
+
+        // Next, we check if the tzid is somewhere in our tzid map.
+        if (isset(self::$map[$tzid])) {
+            return new \DateTimeZone(self::$map[$tzid]);
+        }
+
+        // Maybe the author was hyper-lazy and just included an offset. We
+        // support it, but we aren't happy about it.
+        if (preg_match('/^GMT(\+|-)([0-9]{4})$/', $tzid, $matches)) {
+            return new \DateTimeZone('Etc/GMT' . $matches[1] . ltrim(substr($matches[2],0,2),'0'));
+        }
+
+        if ($vcalendar) {
+
+            // If that didn't work, we will scan VTIMEZONE objects
+            foreach($vcalendar->select('VTIMEZONE') as $vtimezone) {
+
+                if ((string)$vtimezone->TZID === $tzid) {
+
+                    // Some clients add 'X-LIC-LOCATION' with the olson name.
+                    if (isset($vtimezone->{'X-LIC-LOCATION'})) {
+
+                        $lic = (string)$vtimezone->{'X-LIC-LOCATION'};
+
+                        // Libical generators may specify strings like
+                        // "SystemV/EST5EDT". For those we must remove the
+                        // SystemV part.
+                        if (substr($lic,0,8)==='SystemV/') {
+                            $lic = substr($lic,8);
+                        }
+
+                        try {
+                            return new \DateTimeZone($lic);
+                        } catch (\Exception $e) {
+                        }
+
+                    }
+                    // Microsoft may add a magic number, which we also have an
+                    // answer for.
+                    if (isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) {
+                        $cdoId = (int)$vtimezone->{'X-MICROSOFT-CDO-TZID'}->value;
+
+                        // 2 can mean both Europe/Lisbon and Europe/Sarajevo.
+                        if ($cdoId===2 && strpos((string)$vtimezone->TZID, 'Sarajevo')!==false) {
+                            return new \DateTimeZone('Europe/Sarajevo');
+                        }
+
+                        if (isset(self::$microsoftExchangeMap[$cdoId])) {
+                            return new \DateTimeZone(self::$microsoftExchangeMap[$cdoId]);
+                        }
+                    }
+
+                }
+
+            }
+
+        }
+
+        if ($failIfUncertain) {
+            throw new \InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: ' . $tzid);
+        }
+
+        // If we got all the way here, we default to UTC.
+        return new \DateTimeZone(date_default_timezone_get());
+
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/Version.php b/plugins/libcalendaring/lib/Sabre/VObject/Version.php
new file mode 100644
index 0000000..373980e
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Version.php
@@ -0,0 +1,24 @@
+<?php
+
+namespace Sabre\VObject;
+
+/**
+ * This class contains the version number for the VObject package
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+class Version {
+
+    /**
+     * Full version number
+     */
+    const VERSION = '2.1.0';
+
+    /**
+     * Stability : alpha, beta, stable
+     */
+    const STABILITY = 'stable';
+
+}
diff --git a/plugins/libcalendaring/lib/Sabre/VObject/includes.php b/plugins/libcalendaring/lib/Sabre/VObject/includes.php
new file mode 100644
index 0000000..d15329a
--- /dev/null
+++ b/plugins/libcalendaring/lib/Sabre/VObject/includes.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * Includes file
+ *
+ * This file includes the entire VObject library in one go.
+ * The benefit is that an autoloader is not needed, which is often faster.
+ *
+ * @copyright Copyright (C) 2007-2013 fruux GmbH (https://fruux.com/).
+ * @author Evert Pot (http://evertpot.com/)
+ * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
+ */
+
+// Begin includes
+include __DIR__ . '/DateTimeParser.php';
+include __DIR__ . '/ElementList.php';
+include __DIR__ . '/FreeBusyGenerator.php';
+include __DIR__ . '/Node.php';
+include __DIR__ . '/Parameter.php';
+include __DIR__ . '/ParseException.php';
+include __DIR__ . '/Property.php';
+include __DIR__ . '/Reader.php';
+include __DIR__ . '/RecurrenceIterator.php';
+include __DIR__ . '/Splitter/SplitterInterface.php';
+include __DIR__ . '/StringUtil.php';
+include __DIR__ . '/TimeZoneUtil.php';
+include __DIR__ . '/Version.php';
+include __DIR__ . '/Splitter/VCard.php';
+include __DIR__ . '/Component.php';
+include __DIR__ . '/Document.php';
+include __DIR__ . '/Property/Compound.php';
+include __DIR__ . '/Property/DateTime.php';
+include __DIR__ . '/Property/MultiDateTime.php';
+include __DIR__ . '/Splitter/ICalendar.php';
+include __DIR__ . '/Component/VAlarm.php';
+include __DIR__ . '/Component/VCalendar.php';
+include __DIR__ . '/Component/VEvent.php';
+include __DIR__ . '/Component/VFreeBusy.php';
+include __DIR__ . '/Component/VJournal.php';
+include __DIR__ . '/Component/VTodo.php';
+// End includes
diff --git a/plugins/libcalendaring/lib/get_horde_icalendar.sh b/plugins/libcalendaring/lib/get_horde_icalendar.sh
deleted file mode 100755
index 1992bf2..0000000
--- a/plugins/libcalendaring/lib/get_horde_icalendar.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/sh
-
-# Copy Horde_iCalendar classes and dependencies to stdout.
-# This will create a standalone copy of the classes requried for iCal parsing.
-
-SRCDIR=$1
-
-if [ ! -d "$SRCDIR" ]; then
-  echo "Usage: get_horde_icalendar.sh SRCDIR"
-  echo "Please enter a valid source directory of the Horde lib"
-  exit 1
-fi
-
-echo "<?php
-
-/**
- * This is a concatenated copy of the following files:
- *   Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
- * Pull the latest version of these file from the PEAR channel of the Horde project at http://pear.horde.org
- */
-
-require_once(dirname(__FILE__) . '/Horde_Date.php');"
-
-sed 's/<?php//; s/?>//' $SRCDIR/String.php
-echo "\n"
-sed 's/<?php//; s/?>//' $SRCDIR/iCalendar.php | sed -E "s/include_once.+//; s/NLS::getCharset\(\)/'UTF-8'/"
-echo "\n"
-
-for fn in `ls $SRCDIR/iCalendar/*.php | grep -v 'vcard.php'`; do
-	sed 's/<?php//; s/?>//' $fn | sed -E "s/(include|require)_once.+//"
-done;
diff --git a/plugins/libcalendaring/lib/get_sabre_vobject.sh b/plugins/libcalendaring/lib/get_sabre_vobject.sh
new file mode 100755
index 0000000..6cff8d2
--- /dev/null
+++ b/plugins/libcalendaring/lib/get_sabre_vobject.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# Download and install the Sabre\Vobject library for this plugin
+
+wget 'https://github.com/fruux/sabre-vobject/archive/2.1.0.tar.gz' -O sabre-vobject-2.1.0.tar.gz
+tar xf sabre-vobject-2.1.0.tar.gz
+
+mv sabre-vobject-2.1.0/lib/* .
+rm -rf sabre-vobject-2.1.0
+
diff --git a/plugins/libcalendaring/lib/sabre-vobject-2.1.0.tar.gz b/plugins/libcalendaring/lib/sabre-vobject-2.1.0.tar.gz
new file mode 100644
index 0000000..339a992
Binary files /dev/null and b/plugins/libcalendaring/lib/sabre-vobject-2.1.0.tar.gz differ
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index fef2c24..e421b60 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -1,14 +1,10 @@
 <?php
 
 /**
- * iCalendar functions for the Calendar plugin
+ * iCalendar functions for the libcalendaring plugin
  *
- * @version @package_version@
- * @author Lazlo Westerhof <hello at lazlo.me>
  * @author Thomas Bruederli <bruederli at kolabsys.com>
- * @author Bogomil "Bogo" Shopov <shopov at kolabsys.com>
  *
- * Copyright (C) 2010, Lazlo Westerhof <hello at lazlo.me>
  * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -25,550 +21,703 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
+use \Sabre\VObject;
 
 /**
  * Class to parse and build vCalendar (iCalendar) files
  *
- * Uses the Horde:iCalendar class for parsing. To install:
- * > pear channel-discover pear.horde.org
- * > pear install horde/Horde_Icalendar
+ * Uses the SabreTooth VObject library, version 2.1.
+ *
+ * Download from https://github.com/fruux/sabre-vobject/archive/2.1.0.zip
+ * and place the lib files in this plugin's lib directory
  *
  */
 class libvcalendar
 {
-  const EOL = "\r\n";
-  
-  private $timezone;
-
-  public $method;
-  public $events = array();
-
-  function __construct($timezone = null)
-  {
-    $this->timezone = $timezone ? $timezone : new DateTimezone('UTC');
-  }
-
-  /**
-   * Import events from iCalendar format
-   *
-   * @param  string vCalendar input
-   * @param  string Input charset (from envelope)
-   * @return array List of events extracted from the input
-   */
-  public function import($vcal, $charset = RCMAIL_CHARSET)
-  {
-    $parser = $this->get_parser();
-    $parser->parsevCalendar($vcal, 'VCALENDAR', $charset);
-    $this->method = $parser->getAttributeDefault('METHOD', '');
-    $this->events = $seen = array();
-    if ($data = $parser->getComponents()) {
-      foreach ($data as $comp) {
-        if ($comp->getType() == 'vEvent') {
-          $event = $this->_to_rcube_format($comp);
-          if (!$seen[$event['uid']]++)
-            $this->events[] = $event;
-        }
-      }
+    private $timezone;
+    private $attach_uri = null;
+    private $prodid = '-//Roundcube//Roundcube libcalendaring//Sabre//Sabre VObject//EN';
+    private $type_component_map = array('event' => 'VEVENT', 'task' => 'VTODO');
+    private $attendee_keymap = array('name' => 'CN', 'status' => 'PARTSTAT', 'role' => 'ROLE', 'cutype' => 'CUTYPE', 'rsvp' => 'RSVP');
+
+    public $method;
+    public $objects = array();
+
+    /**
+     * Default constructor
+     */
+    function __construct($tz = null)
+    {
+        // load Sabre\VObject classes
+        if (!class_exists('\Sabre\VObject\Reader')) {
+            require_once(__DIR__ . '/lib/Sabre/VObject/includes.php');
+        }
+
+        $this->timezone = $tz ? $tz : new DateTimezone('UTC');
+        $this->prodid = '-//Roundcube//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
     }
-    
-    return $this->events;
-  }
-
-  /**
-   * Read iCalendar events from a file
-   *
-   * @param string File path to read from
-   * @return array List of events extracted from the file
-   */
-  public function import_from_file($filepath)
-  {
-    $this->events = $seen = array();
-    $fp = fopen($filepath, 'r');
-
-    // check file content first
-    $begin = fread($fp, 1024);
-    if (!preg_match('/BEGIN:VCALENDAR/i', $begin))
-      return $this->events;
-
-    $parser = $this->get_parser();
-    $buffer = '';
-
-    fseek($fp, 0);
-    while (($line = fgets($fp, 2048)) !== false) {
-      $buffer .= $line;
-      if (preg_match('/END:VEVENT/i', $line)) {
-        if (preg_match('/BEGIN:VCALENDAR/i', $buffer))
-          $buffer .= self::EOL ."END:VCALENDAR";
-        $parser->parsevCalendar($buffer, 'VCALENDAR', RCMAIL_CHARSET, false);
-        $buffer = '';
-      }
+
+    /**
+     * Setter for timezone information
+     */
+    public function set_timezone($tz)
+    {
+        $this->timezone = $tz;
     }
-    fclose($fp);
 
-    if ($data = $parser->getComponents()) {
-      foreach ($data as $comp) {
-        if ($comp->getType() == 'vEvent') {
-          $event = $this->_to_rcube_format($comp);
-          if (!$seen[$event['uid']]++)
-            $this->events[] = $event;
+    /**
+     * Setter for URI template for attachment links
+     */
+    public function set_attach_uri($uri)
+    {
+        $this->attach_uri = $uri;
+    }
+
+    /**
+     * Setter for a custom PRODID attribute
+     */
+    public function set_prodid($prodid)
+    {
+        $this->prodid = $prodid;
+    }
+
+    /**
+     * Free resources by clearing member vars
+     */
+    public function reset()
+    {
+        $this->method = '';
+        $this->objects = array();
+    }
+
+    /**
+    * Import events from iCalendar format
+    *
+    * @param  string vCalendar input
+    * @param  string Input charset (from envelope)
+    * @return array List of events extracted from the input
+    */
+    public function import($vcal, $charset = 'UTF-8')
+    {
+        // TODO: convert charset to UTF-8 if other
+
+        try {
+            $vobject = VObject\Reader::read($vcal, VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES);
+            if ($vobject)
+                return $this->import_from_vobject($vobject);
+        }
+        catch (Exception $e) {
+            throw $e;
+            rcube::raise_error(array(
+                'code' => 600, 'type' => 'php',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "iCal data parse error: " . $e->getMessage()),
+                true, false);
         }
-      }
+
+        return array();
     }
 
-    $this->method = $parser->getAttributeDefault('METHOD', '');
-
-    return $this->events;
-  }
-
-  /**
-   * Load iCal parser from the Horde lib
-   */
-  public function get_parser()
-  {
-    if (!class_exists('Horde_iCalendar'))
-      require_once(__DIR__ . '/lib/Horde_iCalendar.php');
-
-    // set target charset for parsed events
-    $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;
-
-    return new Horde_iCalendar;
-  }
-
-  /**
-   * Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format
-   */
-  private function _to_rcube_format($ve)
-  {
-    $event = array(
-      'uid' => $ve->getAttributeDefault('UID'),
-      'changed' => $this->_date2time($ve->getAttributeDefault('DTSTAMP', 0)),
-      'title' => $ve->getAttributeDefault('SUMMARY'),
-      'start' => $this->_date2time($ve->getAttribute('DTSTART')),
-      'end' => $this->_date2time($ve->getAttribute('DTEND')),
-      // set defaults
-      'free_busy' => 'busy',
-      'priority' => 0,
-      'attendees' => array(),
-    );
-
-    // check for all-day dates
-    if (is_array($ve->getAttribute('DTSTART')))
-      $event['allday'] = true;
-
-    if ($event['allday'])
-      $event['end']->sub(new DateInterval('PT23H'));
-
-    // assign current timezone to event start/end
-    if (is_a($event['start'], 'DateTime'))
-        $event['start']->setTimezone($this->timezone);
-    else
-        unset($event['start']);
-
-    if (is_a($event['end'], 'DateTime'))
-        $event['end']->setTimezone($this->timezone);
-    else
-        unset($event['end']);
-
-    // map other attributes to internal fields
-    $_attendees = array();
-    foreach ($ve->getAllAttributes() as $attr) {
-      switch ($attr['name']) {
-        case 'ORGANIZER':
-          $organizer = array(
-            'name' => $attr['params']['CN'],
-            'email' => preg_replace('/^mailto:/i', '', $attr['value']),
-            'role' => 'ORGANIZER',
-            'status' => 'ACCEPTED',
-          );
-          if (isset($_attendees[$organizer['email']])) {
-            $i = $_attendees[$organizer['email']];
-            $event['attendees'][$i]['role'] = $organizer['role'];
-          }
-          break;
-        
-        case 'ATTENDEE':
-          $attendee = array(
-            'name' => $attr['params']['CN'],
-            'email' => preg_replace('/^mailto:/i', '', $attr['value']),
-            'role' => $attr['params']['ROLE'] ? $attr['params']['ROLE'] : 'REQ-PARTICIPANT',
-            'status' => $attr['params']['PARTSTAT'],
-            'rsvp' => $attr['params']['RSVP'] == 'TRUE',
-          );
-          if ($organizer && $organizer['email'] == $attendee['email'])
-            $attendee['role'] = 'ORGANIZER';
-          
-          $event['attendees'][] = $attendee;
-          $_attendees[$attendee['email']] = count($event['attendees']) - 1;
-          break;
-          
-        case 'TRANSP':
-          $event['free_busy'] = $attr['value'] == 'TRANSPARENT' ? 'free' : 'busy';
-          break;
-        
-        case 'STATUS':
-          if ($attr['value'] == 'TENTATIVE')
-            $event['free_busy'] = 'tentative';
-          else if ($attr['value'] == 'CANCELLED')
-            $event['cancelled'] = true;
-          break;
-        
-        case 'PRIORITY':
-          if (is_numeric($attr['value'])) {
-            $event['priority'] = $attr['value'];
-          }
-          break;
-        
-        case 'RRULE':
-          // parse recurrence rule attributes
-          foreach (explode(';', $attr['value']) as $par) {
-            list($k, $v) = explode('=', $par);
-            $params[$k] = $v;
-          }
-          if ($params['UNTIL'])
-            $params['UNTIL'] = date_create($params['UNTIL']);
-          if (!$params['INTERVAL'])
-            $params['INTERVAL'] = 1;
-          
-          $event['recurrence'] = $params;
-          break;
-        
-        case 'EXDATE':
-          $event['recurrence']['EXDATE'][] = $this->_date2time($attr['value']);
-          break;
-        
-        case 'RECURRENCE-ID':
-          $event['recurrence_id'] = $this->_date2time($attr['value']);
-          break;
-        
-        case 'SEQUENCE':
-          $event['sequence'] = intval($attr['value']);
-          break;
-        
-        case 'CATEGORIES':
-        case 'DESCRIPTION':
-        case 'LOCATION':
-        case 'URL':
-          $event[strtolower($attr['name'])] = $attr['value'];
-          break;
-        
-        case 'CLASS':
-        case 'X-CALENDARSERVER-ACCESS':
-          $event['sensitivity'] = strtolower($attr['value']);
-          break;
-
-        case 'X-MICROSOFT-CDO-BUSYSTATUS':
-          if ($attr['value'] == 'OOF')
-            $event['free_busy'] == 'outofoffice';
-          else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE')))
-            $event['free_busy'] = strtolower($attr['value']);
-          break;
-
-        case 'ATTACH':
-          // decode inline attachment
-          if (strtoupper($attr['params']['VALUE']) == 'BINARY' && !empty($attr['value'])) {
-            $data = !strcasecmp($attr['params']['ENCODING'], 'BASE64') ? base64_decode($attr['value']) : $attr['value'];
-            $mimetype = $attr['params']['FMTTYPE'] ? $attr['params']['FMTTYPE'] : rcube_mime::file_content_type($data, $attr['params']['X-LABEL'], 'application/octet-stream', true);
-            $extensions = rcube_mime::get_mime_extensions($mimetype);
-            $filename = $attr['params']['X-LABEL'] ? $attr['params']['X-LABEL'] : 'attachment' . count($event['attachments']) . '.' . $extensions[0];
-            $event['attachments'][] = array(
-              'mimetype' => $mimetype,
-              'name' => $filename,
-              'data' => $data,
-              'size' => strlen($data),
-            );
-          }
-          else if (!empty($attr['value']) && preg_match('!^[hftps]+://!', $attr['value'])) {
-            // TODO: add support for displaying/managing link attachments in UI
-            $event['links'][] = $attr['value'];
-          }
-          break;
-
-        default:
-          if (substr($attr['name'], 0, 2) == 'X-')
-            $event['x-custom'][] = array($attr['name'], $attr['value']);
-      }
+    /**
+    * Read iCalendar events from a file
+    *
+    * @param string File path to read from
+    * @return array List of events extracted from the file
+    */
+    public function import_from_file($filepath)
+    {
+        $this->objects = array();
+        $fp = fopen($filepath, 'r');
+
+        // check file content first
+        $begin = fread($fp, 1024);
+        if (!preg_match('/BEGIN:VCALENDAR/i', $begin)) {
+            return $this->objects;
+        }
+        fclose($fp);
+
+        return $this->import(file_get_contents($filepath));
     }
 
-    // find alarms
-    if ($valarm = $ve->findComponent('valarm')) {
-      $action = 'DISPLAY';
-      $trigger = null;
-      
-      foreach ($valarm->getAllAttributes() as $attr) {
-        switch ($attr['name']) {
-          case 'TRIGGER':
-            if ($attr['params']['VALUE'] == 'DATE-TIME') {
-              $trigger = '@' . $attr['value'];
+    /**
+     * Import objects from an already parsed Sabre\VObject\Component object
+     *
+     * @param object Sabre\VObject\Component to read from
+     * @return array List of events extracted from the file
+     */
+    public function import_from_vobject($vobject)
+    {
+        $this->objects = $seen = array();
+
+        if ($vobject->name == 'VCALENDAR') {
+            $this->method = strval($vobject->METHOD);
+
+            foreach ($vobject->getBaseComponents() as $ve) {
+                if ($ve->name == 'VEVENT' || $ve->name == 'VTODO') {
+                    // convert to hash array representation
+                    $object = $this->_to_array($ve);
+
+                    if (!$seen[$object['uid']]++) {
+                        // parse recurrence exceptions
+                        if ($object['recurrence']) {
+                            foreach ($vobject->children as $i => $component) {
+                                if ($component->name == 'VEVENT' && isset($component->{'RECURRENCE-ID'})) {
+                                    $object['recurrence']['EXCEPTIONS'][] = $this->_to_array($component);
+                                }
+                            }
+                        }
+
+                        $this->objects[] = $object;
+                    }
+                }
             }
-            else {
-              $trigger = $attr['value'];
-              $offset = abs($trigger);
-              $unit = 'S';
-              if ($offset % 86400 == 0) {
-                $unit = 'D';
-                $trigger = intval($trigger / 86400);
-              }
-              else if ($offset % 3600 == 0) {
-                $unit = 'H';
-                $trigger = intval($trigger / 3600);
-              }
-              else if ($offset % 60 == 0) {
-                $unit = 'M';
-                $trigger = intval($trigger / 60);
-              }
+        }
+
+        return $this->objects;
+    }
+
+    /**
+     * Convert the given VEvent object to a libkolab compatible array representation
+     *
+     * @param object Vevent object to convert
+     * @return array Hash array with object properties
+     */
+    private function _to_array($ve)
+    {
+        $event = array(
+            'uid'     => strval($ve->UID),
+            'title'   => strval($ve->SUMMARY),
+            'created' => $ve->CREATED ? $ve->CREATED->getDateTime() : null,
+            'changed' => null,
+            '_type'   => $ve->name == 'VTODO' ? 'task' : 'event',
+            // set defaults
+            'free_busy' => 'busy',
+            'priority' => 0,
+            'attendees' => array(),
+        );
+
+        if ($ve->{'LAST-MODIFIED'}) {
+            $event['changed'] = $ve->{'LAST-MODIFIED'}->getDateTime();
+        }
+        else if ($ve->DTSTAMP) {
+            $event['changed'] = $ve->DTSTAMP->getDateTime();
+        }
+
+        // map other attributes to internal fields
+        $_attendees = array();
+        foreach ($ve->children as $prop) {
+            if (!($prop instanceof VObject\Property))
+                continue;
+
+            switch ($prop->name) {
+            case 'DTSTART':
+            case 'DTEND':
+            case 'DUE':
+                $propmap = array('DTSTART' => 'start', 'DTEND' => 'end', 'DUE' => 'due');
+                $event[$propmap[$prop->name]] =  self::convert_datetime($prop);
+                break;
+
+            case 'TRANSP':
+                $event['free_busy'] = $prop->value == 'TRANSPARENT' ? 'free' : 'busy';
+                break;
+
+            case 'STATUS':
+                if ($prop->value == 'TENTATIVE')
+                    $event['free_busy'] = 'tentative';
+                else if ($prop->value == 'CANCELLED')
+                    $event['cancelled'] = true;
+                else if ($prop->value == 'COMPLETED')
+                    $event['complete'] = 100;
+                break;
+
+            case 'PRIORITY':
+                if (is_numeric($prop->value))
+                    $event['priority'] = $prop->value;
+                break;
+
+            case 'RRULE':
+                $params = array();
+                // parse recurrence rule attributes
+                foreach (explode(';', $prop->value) as $par) {
+                    list($k, $v) = explode('=', $par);
+                    $params[$k] = $v;
+                }
+                if ($params['UNTIL'])
+                    $params['UNTIL'] = date_create($params['UNTIL']);
+                if (!$params['INTERVAL'])
+                    $params['INTERVAL'] = 1;
+
+                $event['recurrence'] = $params;
+                break;
+
+            case 'EXDATE':
+                $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], (array)self::convert_datetime($prop));
+                break;
+
+            case 'RECURRENCE-ID':
+                // $event['recurrence_id'] = self::convert_datetime($prop);
+                break;
+
+            case 'RELATED-TO':
+                if ($prop->offsetGet('RELTYPE') == 'PARENT') {
+                    $event['parent_id'] = $prop->value;
+                }
+                break;
+
+            case 'SEQUENCE':
+                $event['sequence'] = intval($prop->value);
+                break;
+
+            case 'PERCENT-COMPLETE':
+                $event['complete'] = intval($prop->value);
+                break;
+
+            case 'DESCRIPTION':
+            case 'LOCATION':
+            case 'URL':
+                $event[strtolower($prop->name)] = $prop->value;
+                break;
+
+            case 'CATEGORY':
+            case 'CATEGORIES':
+                $event['categories'] = $prop->getParts();
+                break;
+
+            case 'CLASS':
+            case 'X-CALENDARSERVER-ACCESS':
+                $event['sensitivity'] = strtolower($prop->value);
+                break;
+
+            case 'X-MICROSOFT-CDO-BUSYSTATUS':
+                if ($prop->value == 'OOF')
+                    $event['free_busy'] == 'outofoffice';
+                else if (in_array($prop->value, array('FREE', 'BUSY', 'TENTATIVE')))
+                    $event['free_busy'] = strtolower($prop->value);
+                break;
+
+            case 'ATTENDEE':
+            case 'ORGANIZER':
+                $params = array();
+                foreach ($prop->parameters as $param) {
+                    switch ($param->name) {
+                        case 'RSVP': $params[$param->name] = strtolower($param->value) == 'true'; break;
+                        default:     $params[$param->name] = $param->value; break;
+                    }
+                }
+                $attendee = self::map_keys($params, array_flip($this->attendee_keymap));
+                $attendee['email'] = preg_replace('/^mailto:/i', '', $prop->value);
+
+                if ($prop->name == 'ORGANIZER') {
+                    $attendee['role'] = 'ORGANIZER';
+                    $attendee['status'] = 'ACCEPTED';
+                    $event['organizer'] = $attendee;
+                }
+                else if ($attendee['email'] != $event['organizer']['email']) {
+                    $event['attendees'][] = $attendee;
+                }
+                break;
+
+            case 'ATTACH':
+                $params = self::parameters_array($prop);
+                if (substr($prop->value, 0, 4) == 'http' && !strpos($prop->value, ':attachment:')) {
+                    $event['links'][] = $prop->value;
+                }
+                else if (strlen($prop->value) && strtoupper($params['VALUE']) == 'BINARY') {
+                    $attachment = self::map_keys($params, array('FMTTYPE' => 'mimetype', 'X-LABEL' => 'name'));
+                    $attachment['data'] = base64_decode($prop->value);
+                    $attachment['size'] = strlen($attachment['content']);
+                    $event['attachments'][] = $attachment;
+                }
+                break;
+
+            default:
+                if (substr($prop->name, 0, 2) == 'X-')
+                    $event['x-custom'][] = array($prop->name, strval($prop->value));
+                break;
             }
-            break;
+        }
 
-          case 'ACTION':
-            $action = $attr['value'];
-            break;
+        // check DURATION property if no end date is set
+        if (empty($event['end']) && $ve->DURATION) {
+            try {
+                $duration = new DateInterval(strval($ve->DURATION));
+                $end = clone $event['start'];
+                $end->add($duration);
+                $event['end'] = $end;
+            }
+            catch (\Exception $e) {
+                trigger_error(strval($e), E_USER_WARNING);
+            }
         }
-      }
-      if ($trigger)
-        $event['alarms'] = $trigger . $unit . ':' . $action;
-    }
 
-    // add organizer to attendees list if not already present
-    if ($organizer && !isset($_attendees[$organizer['email']]))
-      array_unshift($event['attendees'], $organizer);
-
-    // make sure the event has an UID
-#    if (!$event['uid'])
-#      $event['uid'] = $this->cal->generate_uid();
-    
-    return $event;
-  }
-  
-  /**
-   * Helper method to correctly interpret an all-day date value
-   */
-  private function _date2time($prop)
-  {
-    // create timestamp at 12:00 in user's timezone
-    if (is_array($prop))
-      return date_create(sprintf('%04d%02d%02dT120000', $prop['year'], $prop['month'], $prop['mday']), $this->timezone);
-    else if (is_numeric($prop))
-      return date_create('@'.$prop);
-    
-    return $prop;
-  }
-
-
-  /**
-   * Free resources by clearing member vars
-   */
-  public function reset()
-  {
-    $this->method = '';
-    $this->events = array();
-  }
-
-  /**
-   * Export events to iCalendar format
-   *
-   * @param  array   Events as array
-   * @param  string  VCalendar method to advertise
-   * @param  boolean Directly send data to stdout instead of returning
-   * @param  callable Callback function to fetch attachment contents, false if no attachment export
-   * @return string  Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
-   */
-  public function export($events, $method = null, $write = false, $get_attachment = false, $recurrence_id = null)
-  {
-      $memory_limit = parse_bytes(ini_get('memory_limit'));
-
-      if (!$recurrence_id) {
-        $ical = "BEGIN:VCALENDAR" . self::EOL;
-        $ical .= "VERSION:2.0" . self::EOL;
-        $ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL;
-        $ical .= "CALSCALE:GREGORIAN" . self::EOL;
-
-        if ($method)
-          $ical .= "METHOD:" . strtoupper($method) . self::EOL;
+        // check for all-day dates
+        if ($event['start']->_dateonly) {
+            $event['allday'] = true;
+        }
 
-        if ($write) {
-          echo $ical;
-          $ical = '';
+        // shift end-date by one day (except Thunderbird)
+        if ($event['allday'] && is_object($event['end'])) {
+            $event['end']->sub(new \DateInterval('PT23H'));
         }
-      }
 
-      foreach ($events as $event) {
-        $vevent = "BEGIN:VEVENT" . self::EOL;
-        $vevent .= "UID:" . self::escape($event['uid']) . self::EOL;
-        $vevent .= $this->format_datetime("DTSTAMP", $event['changed'] ?: new DateTime(), false, true) . self::EOL;
-        if ($event['sequence'])
-          $vevent .= "SEQUENCE:" . intval($event['sequence']) . self::EOL;
-        if ($recurrence_id)
-          $vevent .= $recurrence_id . self::EOL;
+        // sanity-check and fix end date
+        if (empty($event['end'])) {
+            $event['end'] = clone $event['start'];
+        }
+        else if ($event['end'] < $event['start']) {
+            $event['end'] = clone $event['start'];
+        }
+
+        // make organizer part of the attendees list for compatibility reasons
+        if (!empty($event['organizer']) && is_array($event['attendees'])) {
+            array_unshift($event['attendees'], $event['organizer']);
+        }
+
+        // find alarms
+        if ($valarms = $ve->select('VALARM')) {
+            $action = 'DISPLAY';
+            $trigger = null;
+
+            $valarm = reset($valarms);
+            foreach ($valarm->children as $prop) {
+                switch ($prop->name) {
+                case 'TRIGGER':
+                    foreach ($prop->parameters as $param) {
+                        if ($param->name == 'VALUE' && $param->value == 'DATE-TIME') {
+                            $trigger = '@' . $prop->getDateTime()->format('U');
+                        }
+                    }
+                    if (!$trigger) {
+                        $trigger = preg_replace('/PT/', '', $prop->value);
+                    }
+                    break;
+
+                case 'ACTION':
+                    $action = $prop->value;
+                    break;
+                }
+            }
+
+            if ($trigger)
+                $event['alarms'] = $trigger . ':' . $action;
+        }
         
-        // correctly set all-day dates
-        if ($event['allday']) {
-          $event['end'] = clone $event['end'];
-          $event['end']->add(new DateInterval('P1D'));  // ends the next day
-          $vevent .= $this->format_datetime("DTSTART", $event['start'], true) . self::EOL;
-          $vevent .= $this->format_datetime("DTEND",   $event['end'], true) . self::EOL;
+        // assign current timezone to event start/end
+        if ($event['start'] instanceof DateTime) {
+            $event['start']->setTimezone($this->timezone);
         }
         else {
-          $vevent .= $this->format_datetime("DTSTART", $event['start'], false) . self::EOL;
-          $vevent .= $this->format_datetime("DTEND",   $event['end'], false) . self::EOL;
+            unset($event['start']);
         }
-        $vevent .= "SUMMARY:" . self::escape($event['title']) . self::EOL;
-        $vevent .= "DESCRIPTION:" . self::escape($event['description']) . self::EOL;
-        
-        if (!empty($event['attendees'])){
-          $vevent .= $this->_get_attendees($event['attendees']);
+
+        if ($event['end'] instanceof DateTime) {
+            $event['end']->setTimezone($this->timezone);
+        }
+        else {
+            unset($event['end']);
         }
 
-        if (!empty($event['location'])) {
-          $vevent .= "LOCATION:" . self::escape($event['location']) . self::EOL;
+        // minimal validation
+        if (empty($event['uid']) || empty($event['start']) != empty($event['end'])) {
+            throw new VObject\ParseException('Object validation failed: missing mandatory object properties');
         }
-        if (!empty($event['url'])) {
-          $vevent .= "URL:" . self::escape($event['url']) . self::EOL;
+
+        return $event;
+    }
+
+    /**
+     * Helper method to correctly interpret an all-day date value
+     */
+    public static function convert_datetime($prop)
+    {
+        if (empty($prop)) {
+            return null;
         }
-        if ($event['recurrence'] && !$recurrence_id) {
-          $vevent .= "RRULE:" . libcalendaring::to_rrule($event['recurrence'], self::EOL) . self::EOL;
-          
-          foreach ((array)$event['recurrence']['EXDATE'] as $ex) {
-             $vevent .= $this->format_datetime("EXDATE", $ex, false, true) . self::EOL;
-          }
+        else if ($prop instanceof VObject\Property\MultiDateTime) {
+            $dt = array();
+            $dateonly = ($prop->getDateType() & VObject\Property\DateTime::DATE);
+            foreach ($prop->getDateTimes() as $item) {
+                $item->_dateonly = $dateonly;
+                $dt[] = $item;
+            }
         }
-        if (!empty($event['categories'])) {
-          $vevent .= "CATEGORIES:" . self::escape($event['categories']) . self::EOL;
+        else if ($prop instanceof VObject\Property\DateTime) {
+            $dt = $prop->getDateTime();
+            if ($prop->getDateType() & VObject\Property\DateTime::DATE) {
+                $dt->_dateonly = true;
+            }
         }
-        if (!empty($event['sensitivity']) && $event['sensitivity'] != 'public') {
-          $vevent .= "CLASS:" . strtoupper($event['sensitivity']) . self::EOL;
+        else if ($prop instanceof DateTime) {
+            $dt = $prop;
         }
-        if ($event['alarms']) {
-          list($trigger, $action) = explode(':', $event['alarms']);
-          $val = libcalendaring::parse_alaram_value($trigger);
-          
-          $vevent .= "BEGIN:VALARM\n";
-          if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL;
-          else         $vevent .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL;
-          if ($action) $vevent .= "ACTION:" . self::escape(strtoupper($action)) . self::EOL;
-          $vevent .= "END:VALARM\n";
+
+        return $dt;
+    }
+
+
+    /**
+     * Create a Sabre\VObject\Property instance from a PHP DateTime object
+     *
+     * @param string Property name
+     * @param object DateTime
+     */
+    public static function datetime_prop($name, $dt, $utc = false)
+    {
+        $vdt = new VObject\Property\DateTime($name);
+        $vdt->setDateTime($dt, $dt->_dateonly ? VObject\Property\DateTime::DATE :
+            ($utc ? VObject\Property\DateTime::UTC : VObject\Property\DateTime::LOCALTZ));
+        return $vdt;
+    }
+
+    /**
+     * Copy values from one hash array to another using a key-map
+     */
+    public static function map_keys($values, $map)
+    {
+        $out = array();
+        foreach ($map as $from => $to) {
+            if (isset($values[$from]))
+                $out[$to] = $values[$from];
         }
-        
-        $vevent .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL;
-        
-        if ($event['priority']) {
-          $vevent .= "PRIORITY:" . $event['priority'] . self::EOL;
+        return $out;
+    }
+
+    /**
+     *
+     */
+    private static function parameters_array($prop)
+    {
+        $params = array();
+        foreach ($prop->parameters as $param) {
+            $params[strtoupper($param->name)] = $param->value;
         }
-        
+        return $params;
+    }
+
+
+    /**
+     * Export events to iCalendar format
+     *
+     * @param  array   Events as array
+     * @param  string  VCalendar method to advertise
+     * @param  boolean Directly send data to stdout instead of returning
+     * @param  callable Callback function to fetch attachment contents, false if no attachment export
+     * @return string  Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
+     */
+    public function export($objects, $method = null, $write = false, $get_attachment = false, $recurrence_id = null)
+    {
+        $memory_limit = parse_bytes(ini_get('memory_limit'));
+
+        // encapsulate in VCALENDAR container
+        $vcal = VObject\Component::create('VCALENDAR');
+        $vcal->version = '2.0';
+        $vcal->prodid = $this->prodid;
+        $vcal->calscale = 'GREGORIAN';
+
+        if (!empty($method)) {
+            $vcal->METHOD = $method;
+        }
+
+        // TODO: include timezone information
+
+        // write vcalendar header
+        if ($write) {
+            echo preg_replace('/END:VCALENDAR[\r\n]*$/m', '', $vcal->serialize());
+        }
+
+        foreach ($objects as $object) {
+            $this->_to_ical($object, !$write?$vcal:false, $get_attachment);
+        }
+
+        if ($write) {
+            echo "END:VCALENDAR\r\n";
+            return true;
+        }
+        else {
+            return $vcal->serialize();
+        }
+    }
+
+    /**
+     * Build a valid iCal format block from the given event
+     *
+     * @param  array    Hash array with event/task properties from libkolab
+     * @param  object   VCalendar object to append event to or false for directly sending data to stdout
+     * @param  callable Callback function to fetch attachment contents, false if no attachment export
+     * @param  object   RECURRENCE-ID property when serializing a recurrence exception
+     */
+    private function _to_ical($event, $vcal, $get_attachment, $recurrence_id = null)
+    {
+        $type = $event['_type'] ?: 'event';
+        $ve = VObject\Component::create($this->type_component_map[$type]);
+        $ve->add('UID', $event['uid']);
+
+        // all-day events end the next day
+        if ($event['allday'] && !empty($event['end'])) {
+            $event['end'] = clone $event['end'];
+            $event['end']->add(new \DateInterval('P1D'));
+            $event['end']->_dateonly = true;
+        }
+        if (!empty($event['created']))
+            $ve->add(self::datetime_prop('CREATED', $event['created'], true));
+        if (!empty($event['changed']))
+            $ve->add(self::datetime_prop('DTSTAMP', $event['changed'], true));
+        if (!empty($event['start']))
+            $ve->add(self::datetime_prop('DTSTART', $event['start'], false));
+        if (!empty($event['end']))
+            $ve->add(self::datetime_prop('DTEND',   $event['end'], false));
+        if (!empty($event['due']))
+            $ve->add(self::datetime_prop('DUE',   $event['due'], false));
+
+        if ($recurrence_id)
+            $ve->add($recurrence_id);
+
+        $ve->add('SUMMARY', $event['title']);
+
+        if ($event['location'])
+            $ve->add('LOCATION', $event['location']);
+        if ($event['description'])
+            $ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); // normalize line endings
+
+        if ($event['sequence'])
+            $ve->add('SEQUENCE', $event['sequence']);
+
+        if ($event['recurrence'] && !$recurrence_id) {
+            if ($exdates = $event['recurrence']['EXDATE']) {
+                unset($event['recurrence']['EXDATE']);  // don't serialize EXDATEs into RRULE value
+            }
+
+            $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence']));
+
+            // add EXDATEs each one per line (for Thunderbird Lightning)
+            if ($exdates) {
+                foreach ($exdates as $ex) {
+                    if ($ex instanceof \DateTime) {
+                        $exd = clone $event['start'];
+                        $exd->setDate($ex->format('Y'), $ex->format('n'), $ex->format('j'));
+                        $exd->setTimeZone(new \DateTimeZone('UTC'));
+                        $ve->add(new VObject\Property('EXDATE', $exd->format('Ymd\\THis\\Z')));
+                    }
+                }
+            }
+        }
+
+        if ($event['categories']) {
+            $cat = VObject\Property::create('CATEGORIES');
+            $cat->setParts((array)$event['categories']);
+            $ve->add($cat);
+        }
+
+        $ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
+
+        if ($event['priority'])
+          $ve->add('PRIORITY', $event['priority']);
+
         if ($event['cancelled'])
-          $vevent .= "STATUS:CANCELLED" . self::EOL;
+            $ve->add('STATUS', 'CANCELLED');
         else if ($event['free_busy'] == 'tentative')
-          $vevent .= "STATUS:TENTATIVE" . self::EOL;
-        
-        foreach ((array)$event['x-custom'] as $prop)
-          $vevent .= $prop[0] . ':' . self::escape($prop[1]) . self::EOL;
-        
-        // export attachments using the given callback function
-        if (is_callable($get_attachment) && !empty($event['attachments'])) {
-          foreach ((array)$event['attachments'] as $attach) {
-            // check available memory and skip attachment export if we can't buffer it
-            if ($memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024)
-                  && $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) {
+            $ve->add('STATUS', 'TENTATIVE');
+        else if ($event['complete'] == 100)
+            $ve->add('STATUS', 'COMPLETED');
+
+        if (!empty($event['sensitivity']))
+            $ve->add('CLASS', strtoupper($event['sensitivity']));
+
+        if (isset($event['complete'])) {
+            $ve->add('PERCENT-COMPLETE', intval($event['complete']));
+            // Apple iCal required the COMPLETED date to be set in order to consider a task complete
+            if ($event['complete'] == 100)
+                $ve->add(self::datetime_prop('COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true));
+        }
+
+        if ($event['alarms']) {
+            $va = VObject\Component::create('VALARM');
+            list($trigger, $va->action) = explode(':', $event['alarms']);
+            $val = libcalendaring::parse_alaram_value($trigger);
+            if ($val[1]) $va->add('TRIGGER', preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger));
+            else         $va->add('TRIGGER', gmdate('Ymd\THis\Z', $val[0]), array('VALUE' => 'DATE-TIME'));
+            $ve->add($va);
+        }
+
+        foreach ((array)$event['attendees'] as $attendee) {
+            if ($attendee['role'] == 'ORGANIZER') {
+                if (empty($event['organizer']))
+                    $event['organizer'] = $attendee;
                 continue;
             }
-            // TODO: let the callback print the data directly to stdout (with b64 encoding)
-            if ($data = call_user_func($get_attachment, $attach['id'], $event)) {
-              $vevent .= sprintf('ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=%s;X-LABEL=%s:',
-                self::escape($attach['mimetype']), self::escape($attach['name']));
-              $vevent .= base64_encode($data) . self::EOL;
-            }
-            unset($data);  // attempt to free memory
-          }
+            $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
+            $ve->add('ATTENDEE', 'mailto:' . $attendee['email'], self::map_keys($attendee, $this->attendee_keymap));
         }
-        
-        $vevent .= "END:VEVENT" . self::EOL;
 
-        // append recurrence exceptions
-        if ($event['recurrence']['EXCEPTIONS'] && !$recurrence_id) {
-          foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
-            $exdate = clone $event['start'];
-            $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
-            $vevent .= $this->export(array($ex), null, false, $get_attachment,
-              $this->format_datetime('RECURRENCE-ID', $exdate, $event['allday']));
-          }
-        }
-
-        if ($write)
-          echo rcube_vcard::rfc2425_fold($vevent);
-        else
-          $ical .= $vevent;
-      }
-      
-      if (!$recurrence_id) {
-        $ical .= "END:VCALENDAR" . self::EOL;
-      
-        if ($write) {
-          echo $ical;
-          return true;
+        if ($event['organizer']) {
+            $ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], self::map_keys($event['organizer'], array('name' => 'CN')));
         }
-      }
 
-      // fold lines to 75 chars
-      return rcube_vcard::rfc2425_fold($ical);
-  }
+        foreach ((array)$event['url'] as $url) {
+            $ve->add('URL', $url);
+        }
 
-  private function format_datetime($attr, $dt, $dateonly = false, $utc = false)
-  {
-    if (is_numeric($dt))
-        $dt = new DateTime('@'.$dt);
+        if (!empty($event['parent_id'])) {
+            $ve->add('RELATED-TO', $event['parent_id'], array('RELTYPE' => 'PARENT'));
+        }
 
-    if ($utc)
-      $dt->setTimezone(new DateTimeZone('UTC'));
+        // export attachments
+        if (!empty($event['attachments'])) {
+            foreach ((array)$event['attachments'] as $attach) {
+                // check available memory and skip attachment export if we can't buffer it
+                if (is_callable($get_attachment) && $memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024)
+                    && $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) {
+                    continue;
+                }
+                // embed attachments using the given callback function
+                if (is_callable($get_attachment) && ($data = call_user_func($get_attachment, $attach['id'], $event))) {
+                    // embed attachments for iCal
+                    $ve->add('ATTACH',
+                        base64_encode($data),
+                        array('VALUE' => 'BINARY', 'ENCODING' => 'BASE64', 'FMTTYPE' => $attach['mimetype'], 'X-LABEL' => $attach['name']));
+                    unset($data);  // attempt to free memory
+                }
+                // list attachments as absolute URIs
+                else if (!empty($this->attach_uri)) {
+                    $ve->add('ATTACH',
+                        strtr($this->attach_uri, array(
+                            '{{id}}'       => urlencode($attach['id']),
+                            '{{name}}'     => urlencode($attach['name']),
+                            '{{mimetype}}' => urlencode($attach['mimetype']),
+                        )),
+                        array('FMTTYPE' => $attach['mimetype'], 'VALUE' => 'URI'));
+                }
+            }
+        }
 
-    if ($dateonly) {
-      return $attr . ';VALUE=DATE:' . $dt->format('Ymd');
-    }
-    else {
-      // <ATTR>;TZID=Europe/Zurich:20120706T210000
-      $tz = $dt->getTimezone();
-      $tzname = $tz ? $tz->getName() : null;
-      $tzid = $tzname && $tzname != 'UTC' && $tzname != '+00:00' ? ';TZID=' . self::escape($tzname) : '';
-      return $attr . $tzid . ':' . $dt->format('Ymd\THis' . ($tzid ? '' : '\Z'));
-    }
-  }
-
-  /**
-   * Escape values according to RFC 2445 4.3.11
-   */
-  private function escape($str)
-  {
-    return strtr($str, array('\\' => '\\\\', "\n" => '\n', ';' => '\;', ',' => '\,'));
-  }
-
-  /**
-  * Construct the orginizer of the event.
-  * @param Array Attendees and roles
-  *
-  */
-  private function _get_attendees($ats)
-  {
-    $organizer = "";
-    $attendees = "";
-    foreach ($ats as $at) {
-      if ($at['role'] == "ORGANIZER") {
-        if ($at['email']) {
-          $organizer .= "ORGANIZER;";
-          if (!empty($at['name']))
-            $organizer .= 'CN="' . $at['name'] . '"';
-          $organizer .= ":mailto:". $at['email'] . self::EOL;
-        }
-      }
-      else if ($at['email']) {
-        //I am an attendee 
-        $attendees .= "ATTENDEE;ROLE=" . $at['role'] . ";PARTSTAT=" . $at['status'];
-        if ($at['rsvp'])
-          $attendees .= ";RSVP=TRUE";
-        if (!empty($at['name']))
-          $attendees .= ';CN="' . $at['name'] . '"';
-        $attendees .= ":mailto:" . $at['email'] . self::EOL;
-      }
-    }
+        foreach ((array)$event['links'] as $uri) {
+            $ve->add('ATTACH', $uri);
+        }
+
+        // add custom properties
+        foreach ((array)$event['x-custom'] as $prop) {
+            $ve->add($prop[0], $prop[1]);
+        }
+
+        // append to vcalendar container
+        if ($vcal) {
+            $vcal->add($ve);
+        }
+        else {   // serialize and send to stdout
+            echo $ve->serialize();
+        }
 
-    return $organizer . $attendees;
-  }
+        // append recurrence exceptions
+        if ($event['recurrence']['EXCEPTIONS']) {
+            foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
+                $exdate = clone $event['start'];
+                $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
+                $recurrence_id = self::datetime_prop('RECURRENCE-ID', $exdate, true);
+                // if ($ex['thisandfuture'])  // not supported by any client :-(
+                //    $recurrence_id->add('RANGE', 'THISANDFUTURE');
+                $this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id);
+            }
+        }
+    }
 
 }
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
index b28c7ac..3a560b2 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -123,14 +123,14 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
         $this->assertEquals('-H', $alarm[1], "Alarm unit");
 
         // categories, class
-        $this->assertEquals('libcalendaring tests', $event['categories'], "Event categories");
-        $this->assertEquals(2, $event['sensitivity'], "Class/sensitivity = confidential");
+        $this->assertEquals('libcalendaring tests', join(',', (array)$event['categories']), "Event categories");
+        $this->assertEquals('confidential', $event['sensitivity'], "Class/sensitivity = confidential");
     }
 
     /**
      * Test for iCal export from internal hash array representation
      *
-     * @depend test_extended
+     * @depends test_extended
      */
     function test_export()
     {
@@ -156,7 +156,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
         $this->assertContains('SEQUENCE:' . $event['sequence'],           $ics, "Export Sequence number");
         $this->assertContains('CLASS:CONFIDENTIAL',                       $ics, "Sensitivity => Class");
         $this->assertContains('DESCRIPTION:*Exported by',                 $ics, "Export Description");
-        $this->assertContains('ORGANIZER;CN="Rolf Test":mailto:rolf@',    $ics, "Export organizer");
+        $this->assertContains('ORGANIZER;CN=Rolf Test:mailto:rolf@',    $ics, "Export organizer");
         $this->assertRegExp('/ATTENDEE.*;ROLE=REQ-PARTICIPANT/',          $ics, "Export Attendee ROLE");
         $this->assertRegExp('/ATTENDEE.*;PARTSTAT=NEEDS-ACTION/',         $ics, "Export Attendee Status");
         $this->assertRegExp('/ATTENDEE.*;RSVP=TRUE/',                     $ics, "Export Attendee RSVP");
@@ -182,7 +182,8 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @depend test_export
+     * @depends test_extended
+     * @depends test_export
      */
     function test_export_multiple()
     {
@@ -202,7 +203,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
     }
 
     /**
-     * @depend test_export
+     * @depends test_export
      */
     function test_export_recurrence_exceptions()
     {
@@ -233,13 +234,13 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
         $this->assertEquals($num, substr_count($ics, 'UID:'.$event['uid']), "Recurrence Exceptions with same UID");
         $this->assertEquals($num, substr_count($ics, 'END:VEVENT'),         "VEVENT encapsulation END");
 
-        $this->assertContains('RECURRENCE-ID:20130814', $ics, "Recurrence-ID (1) being the exception date");
-        $this->assertContains('RECURRENCE-ID:20131113', $ics, "Recurrence-ID (2) being the exception date");
+        $this->assertContains('RECURRENCE-ID;VALUE=DATE-TIME:20130814', $ics, "Recurrence-ID (1) being the exception date");
+        $this->assertContains('RECURRENCE-ID;VALUE=DATE-TIME:20131113', $ics, "Recurrence-ID (2) being the exception date");
         $this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title");
     }
     
     /**
-     * @depend test_export
+     * @depends test_export
      */
     function test_export_direct()
     {




More information about the commits mailing list