changeset 4:888e774ee983

libcalendar plugin as distributed
author Charlie Root
date Sat, 13 Jan 2018 08:57:56 -0500
parents f6fe4b6ae66a
children b31e49dc4392
files plugins/libcalendaring/README plugins/libcalendaring/alarm.mp3 plugins/libcalendaring/alarm.wav plugins/libcalendaring/lib/Horde_Date.php plugins/libcalendaring/lib/Horde_Date_Recurrence.php plugins/libcalendaring/lib/Sabre/VObject/Component.php plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php plugins/libcalendaring/lib/Sabre/VObject/Document.php plugins/libcalendaring/lib/Sabre/VObject/ElementList.php plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php plugins/libcalendaring/lib/Sabre/VObject/Node.php plugins/libcalendaring/lib/Sabre/VObject/Parameter.php plugins/libcalendaring/lib/Sabre/VObject/ParseException.php plugins/libcalendaring/lib/Sabre/VObject/Property.php plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php plugins/libcalendaring/lib/Sabre/VObject/Reader.php plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php plugins/libcalendaring/lib/Sabre/VObject/Splitter/VCard.php plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php plugins/libcalendaring/lib/Sabre/VObject/TimeZoneUtil.php plugins/libcalendaring/lib/Sabre/VObject/Version.php plugins/libcalendaring/lib/Sabre/VObject/includes.php plugins/libcalendaring/lib/get_sabre_vobject.sh plugins/libcalendaring/lib/libcalendaring_itip.php plugins/libcalendaring/lib/libcalendaring_recurrence.php plugins/libcalendaring/lib/sabre-vobject-2.1.0.tar.gz plugins/libcalendaring/libcalendaring.js plugins/libcalendaring/libcalendaring.php plugins/libcalendaring/libvcalendar.php plugins/libcalendaring/localization/bg_BG.inc plugins/libcalendaring/localization/ca_ES.inc plugins/libcalendaring/localization/cs_CZ.inc plugins/libcalendaring/localization/da_DK.inc plugins/libcalendaring/localization/de_CH.inc plugins/libcalendaring/localization/de_DE.inc plugins/libcalendaring/localization/en_US.inc plugins/libcalendaring/localization/es_AR.inc plugins/libcalendaring/localization/es_ES.inc plugins/libcalendaring/localization/et_EE.inc plugins/libcalendaring/localization/fi_FI.inc plugins/libcalendaring/localization/fr_FR.inc plugins/libcalendaring/localization/he.inc plugins/libcalendaring/localization/hr.inc plugins/libcalendaring/localization/hu_HU.inc plugins/libcalendaring/localization/it_IT.inc plugins/libcalendaring/localization/ja_JP.inc plugins/libcalendaring/localization/ko_KR.inc plugins/libcalendaring/localization/ku_IQ.inc plugins/libcalendaring/localization/nl_NL.inc plugins/libcalendaring/localization/pl_PL.inc plugins/libcalendaring/localization/pt_BR.inc plugins/libcalendaring/localization/pt_PT.inc plugins/libcalendaring/localization/ro.inc plugins/libcalendaring/localization/ru_RU.inc plugins/libcalendaring/localization/sk_SK.inc plugins/libcalendaring/localization/sl_SI.inc plugins/libcalendaring/localization/sv.inc plugins/libcalendaring/localization/sv_SE.inc plugins/libcalendaring/localization/th_TH.inc plugins/libcalendaring/localization/tr_TR.inc plugins/libcalendaring/localization/uk_UA.inc plugins/libcalendaring/localization/vi.inc plugins/libcalendaring/localization/vi_VN.inc plugins/libcalendaring/localization/zh_CN.inc plugins/libcalendaring/localization/zh_TW.inc plugins/libcalendaring/skins/larry/libcal.css plugins/libcalendaring/tests/libcalendaring.php plugins/libcalendaring/tests/libvcalendar.php plugins/libcalendaring/tests/resources/alarms.ics plugins/libcalendaring/tests/resources/apple-alarms.ics plugins/libcalendaring/tests/resources/attachment.ics plugins/libcalendaring/tests/resources/dummy.ifb plugins/libcalendaring/tests/resources/escaped.ics plugins/libcalendaring/tests/resources/freebusy.ifb plugins/libcalendaring/tests/resources/invalid-dates.ics plugins/libcalendaring/tests/resources/invalid-event.ics plugins/libcalendaring/tests/resources/invalid.txt plugins/libcalendaring/tests/resources/itip.ics plugins/libcalendaring/tests/resources/multiple-rdate.ics plugins/libcalendaring/tests/resources/multiple.ics plugins/libcalendaring/tests/resources/recurrence-id.ics plugins/libcalendaring/tests/resources/recurring.ics plugins/libcalendaring/tests/resources/snd.ics plugins/libcalendaring/tests/resources/vtodo.ics
diffstat 95 files changed, 18647 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/README	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,16 @@
+Library providing common functions for calendar-based plugins
+-------------------------------------------------------------
+
+Provides utility functions for calendar-related modules such as
+
+* alarms display and dismissal
+* attachment handling
+* iCal parsing and exporting
+* iTip invitations handling
+
+iCal parsing and exporting is done with the help of the Sabre VObject
+library [1]. It needs to be installed with Roundcube using composer:
+
+  $ composer require "sabre/vobject" "~3.3.3"
+
+[1]: http://sabre.io/vobject/
Binary file plugins/libcalendaring/alarm.mp3 has changed
Binary file plugins/libcalendaring/alarm.wav has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Horde_Date.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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@ispi.net>
+     * @author Pierre-Alain Joye <pajoye@php.net>
+     * @author Daniel Convissor <danielc@php.net>
+     * @author C.A. Woodcock <c01234@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@ispi.net>
+     * @author Pierre-Alain Joye <pajoye@php.net>
+     * @author Daniel Convissor <danielc@php.net>
+     * @author C.A. Woodcock <c01234@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);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Horde_Date_Recurrence.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,1744 @@
+<?php
+
+/**
+ * This is a modified copy of Horde/Date/Recurrence.php (2015-01-05)
+ * Pull the latest version of this file from the PEAR channel of the Horde
+ * project at http://pear.horde.org by installing the Horde_Date package.
+ */
+
+if (!class_exists('Horde_Date')) {
+    require_once(__DIR__ . '/Horde_Date.php');
+}
+
+// minimal required implementation of Horde_Date_Translation to avoid a huge dependency nightmare
+class Horde_Date_Translation
+{
+    function t($arg) { return $arg; }
+    function ngettext($sing, $plur, $num) { return ($num > 1 ? $plur : $sing); }
+}
+
+
+/**
+ * This file contains the Horde_Date_Recurrence class and according constants.
+ *
+ * Copyright 2007-2015 Horde LLC (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.horde.org/licenses/lgpl21.
+ *
+ * @category Horde
+ * @package  Date
+ */
+
+/**
+ * The Horde_Date_Recurrence class implements algorithms for calculating
+ * recurrences of events, including several recurrence types, intervals,
+ * exceptions, and conversion from and to vCalendar and iCalendar recurrence
+ * rules.
+ *
+ * All methods expecting dates as parameters accept all values that the
+ * Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date
+ * object, an ISO time string or a hash.
+ *
+ * @author   Jan Schneider <jan@horde.org>
+ * @category Horde
+ * @package  Date
+ */
+class Horde_Date_Recurrence
+{
+    /** No Recurrence **/
+    const RECUR_NONE = 0;
+
+    /** Recurs daily. */
+    const RECUR_DAILY = 1;
+
+    /** Recurs weekly. */
+    const RECUR_WEEKLY = 2;
+
+    /** Recurs monthly on the same date. */
+    const RECUR_MONTHLY_DATE = 3;
+
+    /** Recurs monthly on the same week day. */
+    const RECUR_MONTHLY_WEEKDAY = 4;
+
+    /** Recurs yearly on the same date. */
+    const RECUR_YEARLY_DATE = 5;
+
+    /** Recurs yearly on the same day of the year. */
+    const RECUR_YEARLY_DAY = 6;
+
+    /** Recurs yearly on the same week day. */
+    const RECUR_YEARLY_WEEKDAY = 7;
+
+    /**
+     * The start time of the event.
+     *
+     * @var Horde_Date
+     */
+    public $start;
+
+    /**
+     * The end date of the recurrence interval.
+     *
+     * @var Horde_Date
+     */
+    public $recurEnd = null;
+
+    /**
+     * The number of recurrences.
+     *
+     * @var integer
+     */
+    public $recurCount = null;
+
+    /**
+     * The type of recurrence this event follows. RECUR_* constant.
+     *
+     * @var integer
+     */
+    public $recurType = self::RECUR_NONE;
+
+    /**
+     * The length of time between recurrences. The time unit depends on the
+     * recurrence type.
+     *
+     * @var integer
+     */
+    public $recurInterval = 1;
+
+    /**
+     * Any additional recurrence data.
+     *
+     * @var integer
+     */
+    public $recurData = null;
+
+    /**
+     * BYDAY recurrence number
+     *
+     * @var integer
+     */
+    public $recurNthDay = null;
+
+    /**
+     * BYMONTH recurrence data
+     *
+     * @var array
+     */
+    public $recurMonths = array();
+
+    /**
+     * RDATE recurrence values
+     *
+     * @var array
+     */
+    public $rdates = array();
+
+    /**
+     * All the exceptions from recurrence for this event.
+     *
+     * @var array
+     */
+    public $exceptions = array();
+
+    /**
+     * All the dates this recurrence has been marked as completed.
+     *
+     * @var array
+     */
+    public $completions = array();
+
+    /**
+     * Constructor.
+     *
+     * @param Horde_Date $start  Start of the recurring event.
+     */
+    public function __construct($start)
+    {
+        $this->start = new Horde_Date($start);
+    }
+
+    /**
+     * Resets the class properties.
+     */
+    public function reset()
+    {
+        $this->recurEnd = null;
+        $this->recurCount = null;
+        $this->recurType = self::RECUR_NONE;
+        $this->recurInterval = 1;
+        $this->recurData = null;
+        $this->exceptions = array();
+        $this->completions = array();
+    }
+
+    /**
+     * Checks if this event recurs on a given day of the week.
+     *
+     * @param integer $dayMask  A mask consisting of Horde_Date::MASK_*
+     *                          constants specifying the day(s) to check.
+     *
+     * @return boolean  True if this event recurs on the given day(s).
+     */
+    public function recurOnDay($dayMask)
+    {
+        return ($this->recurData & $dayMask);
+    }
+
+    /**
+     * Specifies the days this event recurs on.
+     *
+     * @param integer $dayMask  A mask consisting of Horde_Date::MASK_*
+     *                          constants specifying the day(s) to recur on.
+     */
+    public function setRecurOnDay($dayMask)
+    {
+        $this->recurData = $dayMask;
+    }
+
+    /**
+     *
+     * @param integer $nthDay The nth weekday of month to repeat events on
+     */
+    public function setRecurNthWeekday($nth)
+    {
+        $this->recurNthDay = (int)$nth;
+    }
+
+    /**
+     *
+     * @return integer  The nth weekday of month to repeat events.
+     */
+    public function getRecurNthWeekday()
+    {
+        return isset($this->recurNthDay) ? $this->recurNthDay : ceil($this->start->mday / 7);
+    }
+
+    /**
+     * Specifies the months for yearly (weekday) recurrence
+     *
+     * @param array $months  List of months (integers) this event recurs on.
+     */
+    function setRecurByMonth($months)
+    {
+        $this->recurMonths = (array)$months;
+    }
+
+    /**
+     * Returns a list of months this yearly event recurs on
+     *
+     * @return array List of months (integers) this event recurs on.
+     */
+    function getRecurByMonth()
+    {
+        return $this->recurMonths;
+    }
+
+    /**
+     * Returns the days this event recurs on.
+     *
+     * @return integer  A mask consisting of Horde_Date::MASK_* constants
+     *                  specifying the day(s) this event recurs on.
+     */
+    public function getRecurOnDays()
+    {
+        return $this->recurData;
+    }
+
+    /**
+     * Returns whether this event has a specific recurrence type.
+     *
+     * @param integer $recurrence  RECUR_* constant of the
+     *                             recurrence type to check for.
+     *
+     * @return boolean  True if the event has the specified recurrence type.
+     */
+    public function hasRecurType($recurrence)
+    {
+        return ($recurrence == $this->recurType);
+    }
+
+    /**
+     * Sets a recurrence type for this event.
+     *
+     * @param integer $recurrence  A RECUR_* constant.
+     */
+    public function setRecurType($recurrence)
+    {
+        $this->recurType = $recurrence;
+    }
+
+    /**
+     * Returns recurrence type of this event.
+     *
+     * @return integer  A RECUR_* constant.
+     */
+    public function getRecurType()
+    {
+        return $this->recurType;
+    }
+
+    /**
+     * Returns a description of this event's recurring type.
+     *
+     * @return string  Human readable recurring type.
+     */
+    public function getRecurName()
+    {
+        switch ($this->getRecurType()) {
+        case self::RECUR_NONE:
+            return Horde_Date_Translation::t("No recurrence");
+        case self::RECUR_DAILY:
+            return Horde_Date_Translation::t("Daily");
+        case self::RECUR_WEEKLY:
+            return Horde_Date_Translation::t("Weekly");
+        case self::RECUR_MONTHLY_DATE:
+        case self::RECUR_MONTHLY_WEEKDAY:
+            return Horde_Date_Translation::t("Monthly");
+        case self::RECUR_YEARLY_DATE:
+        case self::RECUR_YEARLY_DAY:
+        case self::RECUR_YEARLY_WEEKDAY:
+            return Horde_Date_Translation::t("Yearly");
+        }
+    }
+
+    /**
+     * Sets the length of time between recurrences of this event.
+     *
+     * @param integer $interval  The time between recurrences.
+     */
+    public function setRecurInterval($interval)
+    {
+        if ($interval > 0) {
+            $this->recurInterval = $interval;
+        }
+    }
+
+    /**
+     * Retrieves the length of time between recurrences of this event.
+     *
+     * @return integer  The number of seconds between recurrences.
+     */
+    public function getRecurInterval()
+    {
+        return $this->recurInterval;
+    }
+
+    /**
+     * Sets the number of recurrences of this event.
+     *
+     * @param integer $count  The number of recurrences.
+     */
+    public function setRecurCount($count)
+    {
+        if ($count > 0) {
+            $this->recurCount = (int)$count;
+            // Recurrence counts and end dates are mutually exclusive.
+            $this->recurEnd = null;
+        } else {
+            $this->recurCount = null;
+        }
+    }
+
+    /**
+     * Retrieves the number of recurrences of this event.
+     *
+     * @return integer  The number recurrences.
+     */
+    public function getRecurCount()
+    {
+        return $this->recurCount;
+    }
+
+    /**
+     * Returns whether this event has a recurrence with a fixed count.
+     *
+     * @return boolean  True if this recurrence has a fixed count.
+     */
+    public function hasRecurCount()
+    {
+        return isset($this->recurCount);
+    }
+
+    /**
+     * Sets the start date of the recurrence interval.
+     *
+     * @param Horde_Date $start  The recurrence start.
+     */
+    public function setRecurStart($start)
+    {
+        $this->start = clone $start;
+    }
+
+    /**
+     * Retrieves the start date of the recurrence interval.
+     *
+     * @return Horde_Date  The recurrence start.
+     */
+    public function getRecurStart()
+    {
+        return $this->start;
+    }
+
+    /**
+     * Sets the end date of the recurrence interval.
+     *
+     * @param Horde_Date $end  The recurrence end.
+     */
+    public function setRecurEnd($end)
+    {
+        if (!empty($end)) {
+            // Recurrence counts and end dates are mutually exclusive.
+            $this->recurCount = null;
+            $this->recurEnd = clone $end;
+        } else {
+            $this->recurEnd = $end;
+        }
+    }
+
+    /**
+     * Retrieves the end date of the recurrence interval.
+     *
+     * @return Horde_Date  The recurrence end.
+     */
+    public function getRecurEnd()
+    {
+        return $this->recurEnd;
+    }
+
+    /**
+     * Returns whether this event has a recurrence end.
+     *
+     * @return boolean  True if this recurrence ends.
+     */
+    public function hasRecurEnd()
+    {
+        return isset($this->recurEnd) && isset($this->recurEnd->year) &&
+            $this->recurEnd->year != 9999;
+    }
+
+    /**
+     * Finds the next recurrence of this event that's after $afterDate.
+     *
+     * @param Horde_Date|string $after  Return events after this date.
+     *
+     * @return Horde_Date|boolean  The date of the next recurrence or false
+     *                             if the event does not recur after
+     *                             $afterDate.
+     */
+    public function nextRecurrence($after)
+    {
+        if (!($after instanceof Horde_Date)) {
+            $after = new Horde_Date($after);
+        } else {
+            $after = clone($after);
+        }
+
+        // Make sure $after and $this->start are in the same TZ
+        $after->setTimezone($this->start->timezone);
+        if ($this->start->compareDateTime($after) >= 0) {
+            return clone $this->start;
+        }
+
+        if ($this->recurInterval == 0 && empty($this->rdates)) {
+            return false;
+        }
+
+        switch ($this->getRecurType()) {
+        case self::RECUR_DAILY:
+            $diff = $this->start->diff($after);
+            $recur = ceil($diff / $this->recurInterval);
+            if ($this->recurCount && $recur >= $this->recurCount) {
+                return false;
+            }
+
+            $recur *= $this->recurInterval;
+            $next = $this->start->add(array('day' => $recur));
+            if ((!$this->hasRecurEnd() ||
+                 $next->compareDateTime($this->recurEnd) <= 0) &&
+                $next->compareDateTime($after) >= 0) {
+                return $next;
+            }
+
+            break;
+
+        case self::RECUR_WEEKLY:
+            if (empty($this->recurData)) {
+                return false;
+            }
+
+            $start_week = Horde_Date_Utils::firstDayOfWeek($this->start->format('W'),
+                                                           $this->start->year);
+            $start_week->timezone = $this->start->timezone;
+            $start_week->hour = $this->start->hour;
+            $start_week->min  = $this->start->min;
+            $start_week->sec  = $this->start->sec;
+
+            // Make sure we are not at the ISO-8601 first week of year while
+            // still in month 12...OR in the ISO-8601 last week of year while
+            // in month 1 and adjust the year accordingly.
+            $week = $after->format('W');
+            if ($week == 1 && $after->month == 12) {
+                $theYear = $after->year + 1;
+            } elseif ($week >= 52 && $after->month == 1) {
+                $theYear = $after->year - 1;
+            } else {
+                $theYear = $after->year;
+            }
+
+            $after_week = Horde_Date_Utils::firstDayOfWeek($week, $theYear);
+            $after_week->timezone = $this->start->timezone;
+            $after_week_end = clone $after_week;
+            $after_week_end->mday += 7;
+
+            $diff = $start_week->diff($after_week);
+            $interval = $this->recurInterval * 7;
+            $repeats = floor($diff / $interval);
+            if ($diff % $interval < 7) {
+                $recur = $diff;
+            } else {
+                /**
+                 * If the after_week is not in the first week interval the
+                 * search needs to skip ahead a complete interval. The way it is
+                 * calculated here means that an event that occurs every second
+                 * week on Monday and Wednesday with the event actually starting
+                 * on Tuesday or Wednesday will only have one incidence in the
+                 * first week.
+                 */
+                $recur = $interval * ($repeats + 1);
+            }
+
+            if ($this->hasRecurCount()) {
+                $recurrences = 0;
+                /**
+                 * Correct the number of recurrences by the number of events
+                 * that lay between the start of the start week and the
+                 * recurrence start.
+                 */
+                $next = clone $start_week;
+                while ($next->compareDateTime($this->start) < 0) {
+                    if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
+                        $recurrences--;
+                    }
+                    ++$next->mday;
+                }
+                if ($repeats > 0) {
+                    $weekdays = $this->recurData;
+                    $total_recurrences_per_week = 0;
+                    while ($weekdays > 0) {
+                        if ($weekdays % 2) {
+                            $total_recurrences_per_week++;
+                        }
+                        $weekdays = ($weekdays - ($weekdays % 2)) / 2;
+                    }
+                    $recurrences += $total_recurrences_per_week * $repeats;
+                }
+            }
+
+            $next = clone $start_week;
+            $next->mday += $recur;
+            while ($next->compareDateTime($after) < 0 &&
+                   $next->compareDateTime($after_week_end) < 0) {
+                if ($this->hasRecurCount()
+                    && $next->compareDateTime($after) < 0
+                    && $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
+                    $recurrences++;
+                }
+                ++$next->mday;
+            }
+            if ($this->hasRecurCount() &&
+                $recurrences >= $this->recurCount) {
+                return false;
+            }
+            if (!$this->hasRecurEnd() ||
+                $next->compareDateTime($this->recurEnd) <= 0) {
+                if ($next->compareDateTime($after_week_end) >= 0) {
+                    return $this->nextRecurrence($after_week_end);
+                }
+                while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) &&
+                       $next->compareDateTime($after_week_end) < 0) {
+                    ++$next->mday;
+                }
+                if (!$this->hasRecurEnd() ||
+                    $next->compareDateTime($this->recurEnd) <= 0) {
+                    if ($next->compareDateTime($after_week_end) >= 0) {
+                        return $this->nextRecurrence($after_week_end);
+                    } else {
+                        return $next;
+                    }
+                }
+            }
+            break;
+
+        case self::RECUR_MONTHLY_DATE:
+            $start = clone $this->start;
+            if ($after->compareDateTime($start) < 0) {
+                $after = clone $start;
+            } else {
+                $after = clone $after;
+            }
+
+            // If we're starting past this month's recurrence of the event,
+            // look in the next month on the day the event recurs.
+            if ($after->mday > $start->mday) {
+                ++$after->month;
+                $after->mday = $start->mday;
+            }
+
+            // Adjust $start to be the first match.
+            $offset = ($after->month - $start->month) + ($after->year - $start->year) * 12;
+            $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+            if ($this->recurCount &&
+                ($offset / $this->recurInterval) >= $this->recurCount) {
+                return false;
+            }
+            $start->month += $offset;
+            $count = $offset / $this->recurInterval;
+
+            do {
+                if ($this->recurCount &&
+                    $count++ >= $this->recurCount) {
+                    return false;
+                }
+
+                // Bail if we've gone past the end of recurrence.
+                if ($this->hasRecurEnd() &&
+                    $this->recurEnd->compareDateTime($start) < 0) {
+                    return false;
+                }
+                if ($start->isValid()) {
+                    return $start;
+                }
+
+                // If the interval is 12, and the date isn't valid, then we
+                // need to see if February 29th is an option. If not, then the
+                // event will _never_ recur, and we need to stop checking to
+                // avoid an infinite loop.
+                if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) {
+                    return false;
+                }
+
+                // Add the recurrence interval.
+                $start->month += $this->recurInterval;
+            } while (true);
+
+            break;
+
+        case self::RECUR_MONTHLY_WEEKDAY:
+            // Start with the start date of the event.
+            $estart = clone $this->start;
+
+            // What day of the week, and week of the month, do we recur on?
+            if (isset($this->recurNthDay)) {
+                $nth = $this->recurNthDay;
+                $weekday = log($this->recurData, 2);
+            } else {
+                $nth = ceil($this->start->mday / 7);
+                $weekday = $estart->dayOfWeek();
+            }
+
+            // Adjust $estart to be the first candidate.
+            $offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
+            $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+            // Adjust our working date until it's after $after.
+            $estart->month += $offset - $this->recurInterval;
+
+            $count = $offset / $this->recurInterval;
+            do {
+                if ($this->recurCount &&
+                    $count++ >= $this->recurCount) {
+                    return false;
+                }
+
+                $estart->month += $this->recurInterval;
+
+                $next = clone $estart;
+                $next->setNthWeekday($weekday, $nth);
+
+                if ($next->month != $estart->month) {
+                    // We're already in the next month.
+                    continue;
+                }
+                if ($next->compareDateTime($after) < 0) {
+                    // We haven't made it past $after yet, try again.
+                    continue;
+                }
+                if ($this->hasRecurEnd() &&
+                    $next->compareDateTime($this->recurEnd) > 0) {
+                    // We've gone past the end of recurrence; we can give up
+                    // now.
+                    return false;
+                }
+
+                // We have a candidate to return.
+                break;
+            } while (true);
+
+            return $next;
+
+        case self::RECUR_YEARLY_DATE:
+            // Start with the start date of the event.
+            $estart = clone $this->start;
+            $after = clone $after;
+
+            if ($after->month > $estart->month ||
+                ($after->month == $estart->month && $after->mday > $estart->mday)) {
+                ++$after->year;
+                $after->month = $estart->month;
+                $after->mday = $estart->mday;
+            }
+
+            // Seperate case here for February 29th
+            if ($estart->month == 2 && $estart->mday == 29) {
+                while (!Horde_Date_Utils::isLeapYear($after->year)) {
+                    ++$after->year;
+                }
+            }
+
+            // Adjust $estart to be the first candidate.
+            $offset = $after->year - $estart->year;
+            if ($offset > 0) {
+                $offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+                $estart->year += $offset;
+            }
+
+            // We've gone past the end of recurrence; give up.
+            if ($this->recurCount &&
+                $offset >= $this->recurCount) {
+                return false;
+            }
+            if ($this->hasRecurEnd() &&
+                $this->recurEnd->compareDateTime($estart) < 0) {
+                return false;
+            }
+
+            return $estart;
+
+        case self::RECUR_YEARLY_DAY:
+            // Check count first.
+            $dayofyear = $this->start->dayOfYear();
+            $count = ($after->year - $this->start->year) / $this->recurInterval + 1;
+            if ($this->recurCount &&
+                ($count > $this->recurCount ||
+                 ($count == $this->recurCount &&
+                  $after->dayOfYear() > $dayofyear))) {
+                return false;
+            }
+
+            // Start with a rough interval.
+            $estart = clone $this->start;
+            $estart->year += floor($count - 1) * $this->recurInterval;
+
+            // Now add the difference to the required day of year.
+            $estart->mday += $dayofyear - $estart->dayOfYear();
+
+            // Add an interval if the estimation was wrong.
+            if ($estart->compareDate($after) < 0) {
+                $estart->year += $this->recurInterval;
+                $estart->mday += $dayofyear - $estart->dayOfYear();
+            }
+
+            // We've gone past the end of recurrence; give up.
+            if ($this->hasRecurEnd() &&
+                $this->recurEnd->compareDateTime($estart) < 0) {
+                return false;
+            }
+
+            return $estart;
+
+        case self::RECUR_YEARLY_WEEKDAY:
+            // Start with the start date of the event.
+            $estart = clone $this->start;
+
+            // What day of the week, and week of the month, do we recur on?
+            if (isset($this->recurNthDay)) {
+                $nth = $this->recurNthDay;
+                $weekday = log($this->recurData, 2);
+            } else {
+                $nth = ceil($this->start->mday / 7);
+                $weekday = $estart->dayOfWeek();
+            }
+
+            // Adjust $estart to be the first candidate.
+            $offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
+
+            // Adjust our working date until it's after $after.
+            $estart->year += $offset - $this->recurInterval;
+
+            $count = $offset / $this->recurInterval;
+            do {
+                if ($this->recurCount &&
+                    $count++ >= $this->recurCount) {
+                    return false;
+                }
+
+                $estart->year += $this->recurInterval;
+
+                $next = clone $estart;
+                $next->setNthWeekday($weekday, $nth);
+
+                if ($next->compareDateTime($after) < 0) {
+                    // We haven't made it past $after yet, try again.
+                    continue;
+                }
+                if ($this->hasRecurEnd() &&
+                    $next->compareDateTime($this->recurEnd) > 0) {
+                    // We've gone past the end of recurrence; we can give up
+                    // now.
+                    return false;
+                }
+
+                // We have a candidate to return.
+                break;
+            } while (true);
+
+            return $next;
+        }
+
+        // fall-back to RDATE properties
+        if (!empty($this->rdates)) {
+            $next = clone $this->start;
+            foreach ($this->rdates as $rdate) {
+                $next->year  = $rdate->year;
+                $next->month = $rdate->month;
+                $next->mday  = $rdate->mday;
+                if ($next->compareDateTime($after) >= 0) {
+                    return $next;
+                }
+            }
+        }
+
+        // We didn't find anything, the recurType was bad, or something else
+        // went wrong - return false.
+        return false;
+    }
+
+    /**
+     * Returns whether this event has any date that matches the recurrence
+     * rules and is not an exception.
+     *
+     * @return boolean  True if an active recurrence exists.
+     */
+    public function hasActiveRecurrence()
+    {
+        if (!$this->hasRecurEnd()) {
+            return true;
+        }
+
+        $next = $this->nextRecurrence(new Horde_Date($this->start));
+        while (is_object($next)) {
+            if (!$this->hasException($next->year, $next->month, $next->mday) &&
+                !$this->hasCompletion($next->year, $next->month, $next->mday)) {
+                return true;
+            }
+
+            $next = $this->nextRecurrence($next->add(array('day' => 1)));
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the next active recurrence.
+     *
+     * @param Horde_Date $afterDate  Return events after this date.
+     *
+     * @return Horde_Date|boolean The date of the next active
+     *                             recurrence or false if the event
+     *                             has no active recurrence after
+     *                             $afterDate.
+     */
+    public function nextActiveRecurrence($afterDate)
+    {
+        $next = $this->nextRecurrence($afterDate);
+        while (is_object($next)) {
+            if (!$this->hasException($next->year, $next->month, $next->mday) &&
+                !$this->hasCompletion($next->year, $next->month, $next->mday)) {
+                return $next;
+            }
+            $next->mday++;
+            $next = $this->nextRecurrence($next);
+        }
+
+        return false;
+    }
+
+    /**
+     * Adds an absolute recurrence date.
+     *
+     * @param integer $year   The year of the instance.
+     * @param integer $month  The month of the instance.
+     * @param integer $mday   The day of the month of the instance.
+     */
+    public function addRDate($year, $month, $mday)
+    {
+        $this->rdates[] = new Horde_Date($year, $month, $mday);
+    }
+
+    /**
+     * Adds an exception to a recurring event.
+     *
+     * @param integer $year   The year of the execption.
+     * @param integer $month  The month of the execption.
+     * @param integer $mday   The day of the month of the exception.
+     */
+    public function addException($year, $month, $mday)
+    {
+        $key = sprintf('%04d%02d%02d', $year, $month, $mday);
+        if (array_search($key, $this->exceptions) === false) {
+            $this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+        }
+    }
+
+    /**
+     * Deletes an exception from a recurring event.
+     *
+     * @param integer $year   The year of the execption.
+     * @param integer $month  The month of the execption.
+     * @param integer $mday   The day of the month of the exception.
+     */
+    public function deleteException($year, $month, $mday)
+    {
+        $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions);
+        if ($key !== false) {
+            unset($this->exceptions[$key]);
+        }
+    }
+
+    /**
+     * Checks if an exception exists for a given reccurence of an event.
+     *
+     * @param integer $year   The year of the reucrance.
+     * @param integer $month  The month of the reucrance.
+     * @param integer $mday   The day of the month of the reucrance.
+     *
+     * @return boolean  True if an exception exists for the given date.
+     */
+    public function hasException($year, $month, $mday)
+    {
+        return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
+                        $this->getExceptions());
+    }
+
+    /**
+     * Retrieves all the exceptions for this event.
+     *
+     * @return array  Array containing the dates of all the exceptions in
+     *                YYYYMMDD form.
+     */
+    public function getExceptions()
+    {
+        return $this->exceptions;
+    }
+
+    /**
+     * Adds a completion to a recurring event.
+     *
+     * @param integer $year   The year of the execption.
+     * @param integer $month  The month of the execption.
+     * @param integer $mday   The day of the month of the completion.
+     */
+    public function addCompletion($year, $month, $mday)
+    {
+        $this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
+    }
+
+    /**
+     * Deletes a completion from a recurring event.
+     *
+     * @param integer $year   The year of the execption.
+     * @param integer $month  The month of the execption.
+     * @param integer $mday   The day of the month of the completion.
+     */
+    public function deleteCompletion($year, $month, $mday)
+    {
+        $key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions);
+        if ($key !== false) {
+            unset($this->completions[$key]);
+        }
+    }
+
+    /**
+     * Checks if a completion exists for a given reccurence of an event.
+     *
+     * @param integer $year   The year of the reucrance.
+     * @param integer $month  The month of the recurrance.
+     * @param integer $mday   The day of the month of the recurrance.
+     *
+     * @return boolean  True if a completion exists for the given date.
+     */
+    public function hasCompletion($year, $month, $mday)
+    {
+        return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
+                        $this->getCompletions());
+    }
+
+    /**
+     * Retrieves all the completions for this event.
+     *
+     * @return array  Array containing the dates of all the completions in
+     *                YYYYMMDD form.
+     */
+    public function getCompletions()
+    {
+        return $this->completions;
+    }
+
+    /**
+     * Parses a vCalendar 1.0 recurrence rule.
+     *
+     * @link http://www.imc.org/pdi/vcal-10.txt
+     * @link http://www.shuchow.com/vCalAddendum.html
+     *
+     * @param string $rrule  A vCalendar 1.0 conform RRULE value.
+     */
+    public function fromRRule10($rrule)
+    {
+        $this->reset();
+
+        if (!$rrule) {
+            return;
+        }
+
+        if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) {
+            // No recurrence data - event does not recur.
+            $this->setRecurType(self::RECUR_NONE);
+        }
+
+        // Always default the recurInterval to 1.
+        $this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1);
+
+        $remainder = trim($matches[3]);
+
+        switch ($matches[1]) {
+        case 'D':
+            $this->setRecurType(self::RECUR_DAILY);
+            break;
+
+        case 'W':
+            $this->setRecurType(self::RECUR_WEEKLY);
+            if (!empty($remainder)) {
+                $mask = 0;
+                while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
+                    $day = trim($matches[0]);
+                    $remainder = substr($remainder, strlen($matches[0]));
+                    $mask |= $maskdays[$day];
+                }
+                $this->setRecurOnDay($mask);
+            } else {
+                // Recur on the day of the week of the original recurrence.
+                $maskdays = array(
+                    Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
+                    Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
+                    Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
+                    Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
+                    Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
+                    Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
+                    Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY,
+                );
+                $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
+            }
+            break;
+
+        case 'MP':
+            $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+            break;
+
+        case 'MD':
+            $this->setRecurType(self::RECUR_MONTHLY_DATE);
+            break;
+
+        case 'YM':
+            $this->setRecurType(self::RECUR_YEARLY_DATE);
+            break;
+
+        case 'YD':
+            $this->setRecurType(self::RECUR_YEARLY_DAY);
+            break;
+        }
+
+        // We don't support modifiers at the moment, strip them.
+        while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) {
+               $remainder = substr($remainder, 1);
+        }
+        if (!empty($remainder)) {
+            if (strpos($remainder, '#') === 0) {
+                $this->setRecurCount(substr($remainder, 1));
+            } else {
+                list($year, $month, $mday, $hour, $min, $sec, $tz) =
+                    sscanf($remainder, '%04d%02d%02dT%02d%02d%02d%s');
+                $this->setRecurEnd(new Horde_Date(array('year' => $year,
+                                                        'month' => $month,
+                                                        'mday' => $mday,
+                                                        'hour' => $hour,
+                                                        'min' => $min,
+                                                        'sec' => $sec),
+                                                  $tz == 'Z' ? 'UTC' : $this->start->timezone));
+            }
+        }
+    }
+
+    /**
+     * Creates a vCalendar 1.0 recurrence rule.
+     *
+     * @link http://www.imc.org/pdi/vcal-10.txt
+     * @link http://www.shuchow.com/vCalAddendum.html
+     *
+     * @param Horde_Icalendar $calendar  A Horde_Icalendar object instance.
+     *
+     * @return string  A vCalendar 1.0 conform RRULE value.
+     */
+    public function toRRule10($calendar)
+    {
+        switch ($this->recurType) {
+        case self::RECUR_NONE:
+            return '';
+
+        case self::RECUR_DAILY:
+            $rrule = 'D' . $this->recurInterval;
+            break;
+
+        case self::RECUR_WEEKLY:
+            $rrule = 'W' . $this->recurInterval;
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+
+            for ($i = 0; $i <= 7; ++$i) {
+                if ($this->recurOnDay(pow(2, $i))) {
+                    $rrule .= ' ' . $vcaldays[$i];
+                }
+            }
+            break;
+
+        case self::RECUR_MONTHLY_DATE:
+            $rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday);
+            break;
+
+        case self::RECUR_MONTHLY_WEEKDAY:
+            $nth_weekday = (int)($this->start->mday / 7);
+            if (($this->start->mday % 7) > 0) {
+                $nth_weekday++;
+            }
+
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+            $rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()];
+
+            break;
+
+        case self::RECUR_YEARLY_DATE:
+            $rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month);
+            break;
+
+        case self::RECUR_YEARLY_DAY:
+            $rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear();
+            break;
+
+        default:
+            return '';
+        }
+
+        if ($this->hasRecurEnd()) {
+            $recurEnd = clone $this->recurEnd;
+            return $rrule . ' ' . $calendar->_exportDateTime($recurEnd);
+        }
+
+        return $rrule . ' #' . (int)$this->getRecurCount();
+    }
+
+    /**
+     * Parses an iCalendar 2.0 recurrence rule.
+     *
+     * @link http://rfc.net/rfc2445.html#s4.3.10
+     * @link http://rfc.net/rfc2445.html#s4.8.5
+     * @link http://www.shuchow.com/vCalAddendum.html
+     *
+     * @param string $rrule  An iCalendar 2.0 conform RRULE value.
+     */
+    public function fromRRule20($rrule)
+    {
+        $this->reset();
+
+        // Parse the recurrence rule into keys and values.
+        $rdata = array();
+        $parts = explode(';', $rrule);
+        foreach ($parts as $part) {
+            list($key, $value) = explode('=', $part, 2);
+            $rdata[strtoupper($key)] = $value;
+        }
+
+        if (isset($rdata['FREQ'])) {
+            // Always default the recurInterval to 1.
+            $this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
+
+            $maskdays = array(
+                'SU' => Horde_Date::MASK_SUNDAY,
+                'MO' => Horde_Date::MASK_MONDAY,
+                'TU' => Horde_Date::MASK_TUESDAY,
+                'WE' => Horde_Date::MASK_WEDNESDAY,
+                'TH' => Horde_Date::MASK_THURSDAY,
+                'FR' => Horde_Date::MASK_FRIDAY,
+                'SA' => Horde_Date::MASK_SATURDAY,
+            );
+
+            switch (strtoupper($rdata['FREQ'])) {
+            case 'DAILY':
+                $this->setRecurType(self::RECUR_DAILY);
+                break;
+
+            case 'WEEKLY':
+                $this->setRecurType(self::RECUR_WEEKLY);
+                if (isset($rdata['BYDAY'])) {
+                    $days = explode(',', $rdata['BYDAY']);
+                    $mask = 0;
+                    foreach ($days as $day) {
+                        $mask |= $maskdays[$day];
+                    }
+                    $this->setRecurOnDay($mask);
+                } else {
+                    // Recur on the day of the week of the original
+                    // recurrence.
+                    $maskdays = array(
+                        Horde_Date::DATE_SUNDAY => Horde_Date::MASK_SUNDAY,
+                        Horde_Date::DATE_MONDAY => Horde_Date::MASK_MONDAY,
+                        Horde_Date::DATE_TUESDAY => Horde_Date::MASK_TUESDAY,
+                        Horde_Date::DATE_WEDNESDAY => Horde_Date::MASK_WEDNESDAY,
+                        Horde_Date::DATE_THURSDAY => Horde_Date::MASK_THURSDAY,
+                        Horde_Date::DATE_FRIDAY => Horde_Date::MASK_FRIDAY,
+                        Horde_Date::DATE_SATURDAY => Horde_Date::MASK_SATURDAY);
+                    $this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
+                }
+                break;
+
+            case 'MONTHLY':
+                if (isset($rdata['BYDAY'])) {
+                    $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+                    if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
+                        $this->setRecurOnDay($maskdays[$m[2]]);
+                        $this->setRecurNthWeekday($m[1]);
+                    }
+                } else {
+                    $this->setRecurType(self::RECUR_MONTHLY_DATE);
+                }
+                break;
+
+            case 'YEARLY':
+                if (isset($rdata['BYYEARDAY'])) {
+                    $this->setRecurType(self::RECUR_YEARLY_DAY);
+                } elseif (isset($rdata['BYDAY'])) {
+                    $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
+                    if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
+                        $this->setRecurOnDay($maskdays[$m[2]]);
+                        $this->setRecurNthWeekday($m[1]);
+                    }
+                    if ($rdata['BYMONTH']) {
+                        $months = explode(',', $rdata['BYMONTH']);
+                        $this->setRecurByMonth($months);
+                    }
+                } else {
+                    $this->setRecurType(self::RECUR_YEARLY_DATE);
+                }
+                break;
+            }
+
+            // MUST take into account the time portion if it is present.
+            // See Bug: 12869 and Bug: 2813
+            if (isset($rdata['UNTIL'])) {
+                if (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $rdata['UNTIL'], $parts)) {
+                    $until = new Horde_Date($rdata['UNTIL'], 'UTC');
+                    $until->setTimezone($this->start->timezone);
+                } else {
+                    list($year, $month, $mday) = sscanf($rdata['UNTIL'],
+                                                        '%04d%02d%02d');
+                    $until = new Horde_Date(
+                        array('year' => $year,
+                              'month' => $month,
+                              'mday' => $mday + 1),
+                        $this->start->timezone
+                    );
+                }
+                $this->setRecurEnd($until);
+            }
+            if (isset($rdata['COUNT'])) {
+                $this->setRecurCount($rdata['COUNT']);
+            }
+        } else {
+            // No recurrence data - event does not recur.
+            $this->setRecurType(self::RECUR_NONE);
+        }
+    }
+
+    /**
+     * Creates an iCalendar 2.0 recurrence rule.
+     *
+     * @link http://rfc.net/rfc2445.html#s4.3.10
+     * @link http://rfc.net/rfc2445.html#s4.8.5
+     * @link http://www.shuchow.com/vCalAddendum.html
+     *
+     * @param Horde_Icalendar $calendar  A Horde_Icalendar object instance.
+     *
+     * @return string  An iCalendar 2.0 conform RRULE value.
+     */
+    public function toRRule20($calendar)
+    {
+        switch ($this->recurType) {
+        case self::RECUR_NONE:
+            return '';
+
+        case self::RECUR_DAILY:
+            $rrule = 'FREQ=DAILY;INTERVAL='  . $this->recurInterval;
+            break;
+
+        case self::RECUR_WEEKLY:
+            $rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval;
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+
+            for ($i = $flag = 0; $i <= 7; ++$i) {
+                if ($this->recurOnDay(pow(2, $i))) {
+                    if ($flag == 0) {
+                        $rrule .= ';BYDAY=';
+                        $flag = 1;
+                    } else {
+                        $rrule .= ',';
+                    }
+                    $rrule .= $vcaldays[$i];
+                }
+            }
+            break;
+
+        case self::RECUR_MONTHLY_DATE:
+            $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval;
+            break;
+
+        case self::RECUR_MONTHLY_WEEKDAY:
+            if (isset($this->recurNthDay)) {
+                $nth_weekday = $this->recurNthDay;
+                $day_of_week = log($this->recurData, 2);
+            } else {
+                $day_of_week = $this->start->dayOfWeek();
+                $nth_weekday = (int)($this->start->mday / 7);
+                if (($this->start->mday % 7) > 0) {
+                    $nth_weekday++;
+                }
+            }
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+            $rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
+                . ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week];
+            break;
+
+        case self::RECUR_YEARLY_DATE:
+            $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval;
+            break;
+
+        case self::RECUR_YEARLY_DAY:
+            $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
+                . ';BYYEARDAY=' . $this->start->dayOfYear();
+            break;
+
+        case self::RECUR_YEARLY_WEEKDAY:
+            if (isset($this->recurNthDay)) {
+                $nth_weekday = $this->recurNthDay;
+                $day_of_week = log($this->recurData, 2);
+            } else {
+                $day_of_week = $this->start->dayOfWeek();
+                $nth_weekday = (int)($this->start->mday / 7);
+                if (($this->start->mday % 7) > 0) {
+                    $nth_weekday++;
+                }
+             }
+            $months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month;
+            $vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
+            $rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
+                . ';BYDAY='
+                . $nth_weekday
+                . $vcaldays[$day_of_week]
+                . ';BYMONTH=' . $this->start->month;
+            break;
+        }
+
+        if ($this->hasRecurEnd()) {
+            $recurEnd = clone $this->recurEnd;
+            $rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd);
+        }
+        if ($count = $this->getRecurCount()) {
+            $rrule .= ';COUNT=' . $count;
+        }
+        return $rrule;
+    }
+
+    /**
+     * Parses the recurrence data from a Kolab hash.
+     *
+     * @param array $hash  The hash to convert.
+     *
+     * @return boolean  True if the hash seemed valid, false otherwise.
+     */
+    public function fromKolab($hash)
+    {
+        $this->reset();
+
+        if (!isset($hash['interval']) || !isset($hash['cycle'])) {
+            $this->setRecurType(self::RECUR_NONE);
+            return false;
+        }
+
+        $this->setRecurInterval((int)$hash['interval']);
+
+        $month2number = array(
+            'january'   => 1,
+            'february'  => 2,
+            'march'     => 3,
+            'april'     => 4,
+            'may'       => 5,
+            'june'      => 6,
+            'july'      => 7,
+            'august'    => 8,
+            'september' => 9,
+            'october'   => 10,
+            'november'  => 11,
+            'december'  => 12,
+        );
+
+        $parse_day = false;
+        $set_daymask = false;
+        $update_month = false;
+        $update_daynumber = false;
+        $update_weekday = false;
+        $nth_weekday = -1;
+
+        switch ($hash['cycle']) {
+        case 'daily':
+            $this->setRecurType(self::RECUR_DAILY);
+            break;
+
+        case 'weekly':
+            $this->setRecurType(self::RECUR_WEEKLY);
+            $parse_day = true;
+            $set_daymask = true;
+            break;
+
+        case 'monthly':
+            if (!isset($hash['daynumber'])) {
+                $this->setRecurType(self::RECUR_NONE);
+                return false;
+            }
+
+            switch ($hash['type']) {
+            case 'daynumber':
+                $this->setRecurType(self::RECUR_MONTHLY_DATE);
+                $update_daynumber = true;
+                break;
+
+            case 'weekday':
+                $this->setRecurType(self::RECUR_MONTHLY_WEEKDAY);
+                $this->setRecurNthWeekday($hash['daynumber']);
+                $parse_day = true;
+                $set_daymask = true;
+                break;
+            }
+            break;
+
+        case 'yearly':
+            if (!isset($hash['type'])) {
+                $this->setRecurType(self::RECUR_NONE);
+                return false;
+            }
+
+            switch ($hash['type']) {
+            case 'monthday':
+                $this->setRecurType(self::RECUR_YEARLY_DATE);
+                $update_month = true;
+                $update_daynumber = true;
+                break;
+
+            case 'yearday':
+                if (!isset($hash['daynumber'])) {
+                    $this->setRecurType(self::RECUR_NONE);
+                    return false;
+                }
+
+                $this->setRecurType(self::RECUR_YEARLY_DAY);
+                // Start counting days in January.
+                $hash['month'] = 'january';
+                $update_month = true;
+                $update_daynumber = true;
+                break;
+
+            case 'weekday':
+                if (!isset($hash['daynumber'])) {
+                    $this->setRecurType(self::RECUR_NONE);
+                    return false;
+                }
+
+                $this->setRecurType(self::RECUR_YEARLY_WEEKDAY);
+                $this->setRecurNthWeekday($hash['daynumber']);
+                $parse_day = true;
+                $set_daymask = true;
+
+                if ($hash['month'] && isset($month2number[$hash['month']])) {
+                    $this->setRecurByMonth($month2number[$hash['month']]);
+                }
+                break;
+            }
+        }
+
+        if (isset($hash['range-type']) && isset($hash['range'])) {
+            switch ($hash['range-type']) {
+            case 'number':
+                $this->setRecurCount((int)$hash['range']);
+                break;
+
+            case 'date':
+                $recur_end = new Horde_Date($hash['range']);
+                $recur_end->hour = 23;
+                $recur_end->min = 59;
+                $recur_end->sec = 59;
+                $this->setRecurEnd($recur_end);
+                break;
+            }
+        }
+
+        // Need to parse <day>?
+        $last_found_day = -1;
+        if ($parse_day) {
+            if (!isset($hash['day'])) {
+                $this->setRecurType(self::RECUR_NONE);
+                return false;
+            }
+
+            $mask = 0;
+            $bits = array(
+                'monday' => Horde_Date::MASK_MONDAY,
+                'tuesday' => Horde_Date::MASK_TUESDAY,
+                'wednesday' => Horde_Date::MASK_WEDNESDAY,
+                'thursday' => Horde_Date::MASK_THURSDAY,
+                'friday' => Horde_Date::MASK_FRIDAY,
+                'saturday' => Horde_Date::MASK_SATURDAY,
+                'sunday' => Horde_Date::MASK_SUNDAY,
+            );
+            $days = array(
+                'monday' => Horde_Date::DATE_MONDAY,
+                'tuesday' => Horde_Date::DATE_TUESDAY,
+                'wednesday' => Horde_Date::DATE_WEDNESDAY,
+                'thursday' => Horde_Date::DATE_THURSDAY,
+                'friday' => Horde_Date::DATE_FRIDAY,
+                'saturday' => Horde_Date::DATE_SATURDAY,
+                'sunday' => Horde_Date::DATE_SUNDAY,
+            );
+
+            foreach ($hash['day'] as $day) {
+                // Validity check.
+                if (empty($day) || !isset($bits[$day])) {
+                    continue;
+                }
+
+                $mask |= $bits[$day];
+                $last_found_day = $days[$day];
+            }
+
+            if ($set_daymask) {
+                $this->setRecurOnDay($mask);
+            }
+        }
+
+        if ($update_month || $update_daynumber || $update_weekday) {
+            if ($update_month) {
+                if (isset($month2number[$hash['month']])) {
+                    $this->start->month = $month2number[$hash['month']];
+                }
+            }
+
+            if ($update_daynumber) {
+                if (!isset($hash['daynumber'])) {
+                    $this->setRecurType(self::RECUR_NONE);
+                    return false;
+                }
+
+                $this->start->mday = $hash['daynumber'];
+            }
+
+            if ($update_weekday) {
+                $this->setNthWeekday($nth_weekday);
+            }
+        }
+
+        // Exceptions.
+        if (isset($hash['exclusion'])) {
+            foreach ($hash['exclusion'] as $exception) {
+                if ($exception instanceof DateTime) {
+                    $this->exceptions[] = $exception->format('Ymd');
+                }
+            }
+        }
+
+        if (isset($hash['complete'])) {
+            foreach ($hash['complete'] as $completion) {
+                if ($exception instanceof DateTime) {
+                    $this->completions[] = $completion->format('Ymd');
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Export this object into a Kolab hash.
+     *
+     * @return array  The recurrence hash.
+     */
+    public function toKolab()
+    {
+        if ($this->getRecurType() == self::RECUR_NONE) {
+            return array();
+        }
+
+        $day2number = array(
+            0 => 'sunday',
+            1 => 'monday',
+            2 => 'tuesday',
+            3 => 'wednesday',
+            4 => 'thursday',
+            5 => 'friday',
+            6 => 'saturday'
+        );
+        $month2number = array(
+            1 => 'january',
+            2 => 'february',
+            3 => 'march',
+            4 => 'april',
+            5 => 'may',
+            6 => 'june',
+            7 => 'july',
+            8 => 'august',
+            9 => 'september',
+            10 => 'october',
+            11 => 'november',
+            12 => 'december'
+        );
+
+        $hash = array('interval' => $this->getRecurInterval());
+        $start = $this->getRecurStart();
+
+        switch ($this->getRecurType()) {
+        case self::RECUR_DAILY:
+            $hash['cycle'] = 'daily';
+            break;
+
+        case self::RECUR_WEEKLY:
+            $hash['cycle'] = 'weekly';
+            $bits = array(
+                'monday' => Horde_Date::MASK_MONDAY,
+                'tuesday' => Horde_Date::MASK_TUESDAY,
+                'wednesday' => Horde_Date::MASK_WEDNESDAY,
+                'thursday' => Horde_Date::MASK_THURSDAY,
+                'friday' => Horde_Date::MASK_FRIDAY,
+                'saturday' => Horde_Date::MASK_SATURDAY,
+                'sunday' => Horde_Date::MASK_SUNDAY,
+            );
+            $days = array();
+            foreach ($bits as $name => $bit) {
+                if ($this->recurOnDay($bit)) {
+                    $days[] = $name;
+                }
+            }
+            $hash['day'] = $days;
+            break;
+
+        case self::RECUR_MONTHLY_DATE:
+            $hash['cycle'] = 'monthly';
+            $hash['type'] = 'daynumber';
+            $hash['daynumber'] = $start->mday;
+            break;
+
+        case self::RECUR_MONTHLY_WEEKDAY:
+            $hash['cycle'] = 'monthly';
+            $hash['type'] = 'weekday';
+            $hash['daynumber'] = $start->weekOfMonth();
+            $hash['day'] = array ($day2number[$start->dayOfWeek()]);
+            break;
+
+        case self::RECUR_YEARLY_DATE:
+            $hash['cycle'] = 'yearly';
+            $hash['type'] = 'monthday';
+            $hash['daynumber'] = $start->mday;
+            $hash['month'] = $month2number[$start->month];
+            break;
+
+        case self::RECUR_YEARLY_DAY:
+            $hash['cycle'] = 'yearly';
+            $hash['type'] = 'yearday';
+            $hash['daynumber'] = $start->dayOfYear();
+            break;
+
+        case self::RECUR_YEARLY_WEEKDAY:
+            $hash['cycle'] = 'yearly';
+            $hash['type'] = 'weekday';
+            $hash['daynumber'] = $start->weekOfMonth();
+            $hash['day'] = array ($day2number[$start->dayOfWeek()]);
+            $hash['month'] = $month2number[$start->month];
+        }
+
+        if ($this->hasRecurCount()) {
+            $hash['range-type'] = 'number';
+            $hash['range'] = $this->getRecurCount();
+        } elseif ($this->hasRecurEnd()) {
+            $date = $this->getRecurEnd();
+            $hash['range-type'] = 'date';
+            $hash['range'] = $date->toDateTime();
+        } else {
+            $hash['range-type'] = 'none';
+            $hash['range'] = '';
+        }
+
+        // Recurrence exceptions
+        $hash['exclusion'] = $hash['complete'] = array();
+        foreach ($this->exceptions as $exception) {
+            $hash['exclusion'][] = new DateTime($exception);
+        }
+        foreach ($this->completions as $completionexception) {
+            $hash['complete'][] = new DateTime($completionexception);
+        }
+
+        return $hash;
+    }
+
+    /**
+     * Returns a simple object suitable for json transport representing this
+     * object.
+     *
+     * Possible properties are:
+     * - t: type
+     * - i: interval
+     * - e: end date
+     * - c: count
+     * - d: data
+     * - co: completions
+     * - ex: exceptions
+     *
+     * @return object  A simple object.
+     */
+    public function toJson()
+    {
+        $json = new stdClass;
+        $json->t = $this->recurType;
+        $json->i = $this->recurInterval;
+        if ($this->hasRecurEnd()) {
+            $json->e = $this->recurEnd->toJson();
+        }
+        if ($this->recurCount) {
+            $json->c = $this->recurCount;
+        }
+        if ($this->recurData) {
+            $json->d = $this->recurData;
+        }
+        if ($this->completions) {
+            $json->co = $this->completions;
+        }
+        if ($this->exceptions) {
+            $json->ex = $this->exceptions;
+        }
+        return $json;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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;
+        }
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VAlarm.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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);
+        }
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCalendar.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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()
+        );
+
+    }
+     */
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VCard.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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
+        );
+
+    }
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VEvent.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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)
+        );
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VFreeBusy.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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;
+
+    }
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VJournal.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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;
+
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Component/VTodo.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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;
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/DateTimeParser.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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);
+        }
+
+    }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Document.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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);
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/ElementList.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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');
+
+    }
+
+    /* }}} */
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/FreeBusyGenerator.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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;
+
+    }
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Node.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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
+
+    /* }}} */
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Parameter.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,102 @@
+<?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',
+        );
+
+        // quote parameters according to RFC 5545, Section 3.2
+        $quotes = '';
+        if (preg_match('/[:;,]/', $this->value)) {
+            $quotes = '"';
+        }
+
+        return $this->name . '=' . $quotes . str_replace($src, $out, $this->value) . $quotes;
+
+    }
+
+    /**
+     * Called when this object is being cast to a string
+     *
+     * @return string
+     */
+    public function __toString() {
+
+        return $this->value;
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/ParseException.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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 { }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Property.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,453 @@
+<?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",
+            "\r",
+        );
+        $out = array(
+            '\\\\',
+            '\n',
+            '',
+        );
+
+        // avoid double-escaping of \, and \; from Compound properties
+        if (method_exists($this, 'setParts')) {
+            $src[] = '\\\\,';
+            $out[] = '\\,';
+            $src[] = '\\\\;';
+            $out[] = '\\;';
+        }
+
+        $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;
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Property/Compound.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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)
+        );
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Property/DateTime.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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
+        );
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Property/MultiDateTime.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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;
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Reader.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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;
+
+    }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/RecurrenceIterator.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,1144 @@
+<?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;
+
+    /**
+     * This counts the number of overridden events we've handled so far
+     *
+     * @var int
+     */
+    private $handledOverridden = 0;
+
+    /**
+     * 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;
+                }
+            }
+        }
+
+        ksort($this->overriddenEvents);
+
+        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) && $this->currentDate > $this->until) {
+
+            // Need to make sure there's no overridden events past the
+            // until date.
+            foreach($this->overriddenEvents as $overriddenEvent) {
+
+                if ($overriddenEvent->DTSTART->getDateTime() >= $this->currentDate) {
+
+                    return true;
+                }
+            }
+            return false;
+        }
+        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() {
+
+        $previousStamp = $this->currentDate->getTimeStamp();
+
+        // Finding the next overridden event in line, and storing that for
+        // later use.
+        $overriddenEvent = null;
+        $overriddenDate = null;
+        $this->currentOverriddenEvent = null;
+
+        foreach($this->overriddenEvents as $index=>$event) {
+            if ($index > $previousStamp) {
+                $overriddenEvent = $event;
+                $overriddenDate = clone $event->DTSTART->getDateTime();
+                break;
+            }
+        }
+
+        // If we have a stored 'next date', we will use that.
+        if ($this->nextDate) {
+            if (!$overriddenDate || $this->nextDate < $overriddenDate) {
+                $this->currentDate = $this->nextDate;
+                $currentStamp = $this->currentDate->getTimeStamp();
+                $this->nextDate = null;
+            } else {
+                $this->currentDate = clone $overriddenDate;
+                $this->currentOverriddenEvent = $overriddenEvent;
+            }
+            $this->counter++;
+            return;
+        }
+
+        while(true) {
+
+            // Otherwise, we find the next event in the normal RRULE
+            // sequence.
+            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 $check) {
+                if ($this->currentDate == $check) {
+                    continue 2;
+                }
+            }
+            break;
+
+        }
+
+
+
+        // Is the date we have actually higher than the next overiddenEvent?
+        if ($overriddenDate && $this->currentDate > $overriddenDate) {
+            $this->nextDate = clone $this->currentDate;
+            $this->currentDate = clone $overriddenDate;
+            $this->currentOverriddenEvent = $overriddenEvent;
+            $this->handledOverridden++;
+        }
+        $this->counter++;
+
+
+        /*
+         * If we have overridden events left in the queue, but our counter is
+         * running out, we should grab one of those.
+         */
+        if (!is_null($overriddenEvent) && !is_null($this->count) && count($this->overriddenEvents) - $this->handledOverridden >= ($this->count - $this->counter)) {
+
+            $this->currentOverriddenEvent = $overriddenEvent;
+            $this->currentDate = clone $overriddenDate;
+            $this->handledOverridden++;
+
+        }
+
+    }
+
+    /**
+     * 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;
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/ICalendar.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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;
+
+        }
+
+   }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/SplitterInterface.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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();
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Splitter/VCard.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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;
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/StringUtil.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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));
+
+    }
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/TimeZoneUtil.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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@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());
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/Version.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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.3';
+
+    /**
+     * Stability : alpha, beta, stable
+     */
+    const STABILITY = 'stable';
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/Sabre/VObject/includes.php	Sat Jan 13 08:57:56 2018 -0500
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/get_sabre_vobject.sh	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,12 @@
+#!/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
+
+cd lib/Sabre/VObject && wget --no-check-certificate -O Property.php https://raw2.github.com/thomascube/sabre-vobject/84b64c65f9a94f7ec5a5e327bab3cc1335dd613c/lib/Sabre/VObject/Property.php
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,867 @@
+<?php
+
+/**
+ * iTIP functions for the calendar-based Roudncube plugins
+ *
+ * Class providing functionality to manage iTIP invitations
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2011-2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class libcalendaring_itip
+{
+    protected $rc;
+    protected $lib;
+    protected $plugin;
+    protected $sender;
+    protected $domain;
+    protected $itip_send = false;
+    protected $rsvp_actions = array('accepted','tentative','declined','delegated');
+    protected $rsvp_status  = array('accepted','tentative','declined','delegated');
+
+    function __construct($plugin, $domain = 'libcalendaring')
+    {
+        $this->plugin = $plugin;
+        $this->rc = rcube::get_instance();
+        $this->lib = libcalendaring::get_instance();
+        $this->domain = $domain;
+
+        $hook = $this->rc->plugins->exec_hook('calendar_load_itip',
+            array('identity' => $this->rc->user->list_emails(true)));
+        $this->sender = $hook['identity'];
+
+        $this->plugin->add_hook('message_before_send', array($this, 'before_send_hook'));
+        $this->plugin->add_hook('smtp_connect', array($this, 'smtp_connect_hook'));
+    }
+
+    public function set_sender_email($email)
+    {
+        if (!empty($email))
+            $this->sender['email'] = $email;
+    }
+
+    public function set_rsvp_actions($actions)
+    {
+        $this->rsvp_actions = (array)$actions;
+        $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
+    }
+
+    public function set_rsvp_status($status)
+    {
+        $this->rsvp_status = $status;
+    }
+
+    /**
+     * Wrapper for rcube_plugin::gettext()
+     * Checking for a label in different domains
+     *
+     * @see rcube::gettext()
+     */
+    public function gettext($p)
+    {
+        $label = is_array($p) ? $p['name'] : $p;
+        $domain = $this->domain;
+        if (!$this->rc->text_exists($label, $domain)) {
+            $domain = 'libcalendaring';
+        }
+        return $this->rc->gettext($p, $domain);
+    }
+
+    /**
+     * Send an iTip mail message
+     *
+     * @param array   Event object to send
+     * @param string  iTip method (REQUEST|REPLY|CANCEL)
+     * @param array   Hash array with recipient data (name, email)
+     * @param string  Mail subject
+     * @param string  Mail body text label
+     * @param object  Mail_mime object with message data
+     * @param boolean Request RSVP
+     * @return boolean True on success, false on failure
+     */
+    public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true)
+    {
+        if (!$this->sender['name'])
+            $this->sender['name'] = $this->sender['email'];
+
+        if (!$message) {
+            libcalendaring::identify_recurrence_instance($event);
+            $message = $this->compose_itip_message($event, $method, $rsvp);
+        }
+
+        $mailto = rcube_utils::idn_to_ascii($recipient['email']);
+
+        $headers = $message->headers();
+        $headers['To'] = format_email_recipient($mailto, $recipient['name']);
+        $headers['Subject'] = $this->gettext(array(
+            'name' => $subject,
+            'vars' => array(
+                'title' => $event['title'],
+                'name' => $this->sender['name']
+            )
+        ));
+
+        // compose a list of all event attendees
+        $attendees_list = array();
+        foreach ((array)$event['attendees'] as $attendee) {
+            $attendees_list[] = ($attendee['name'] && $attendee['email']) ?
+                $attendee['name'] . ' <' . $attendee['email'] . '>' :
+                ($attendee['name'] ? $attendee['name'] : $attendee['email']);
+        }
+
+        $recurrence_info = '';
+        if (!empty($event['recurrence_id'])) {
+            $recurrence_info = "\n\n** " . $this->gettext($event['thisandfuture'] ? 'itipmessagefutureoccurrence' : 'itipmessagesingleoccurrence') . ' **';
+        }
+        else if (!empty($event['recurrence'])) {
+            $recurrence_info = sprintf("\n%s: %s", $this->gettext('recurring'), $this->lib->recurrence_text($event['recurrence']));
+        }
+
+        $mailbody = $this->gettext(array(
+            'name' => $bodytext,
+            'vars' => array(
+                'title' => $event['title'],
+                'date' => $this->lib->event_date_text($event, true) . $recurrence_info,
+                'attendees' => join(",\n ", $attendees_list),
+                'sender' => $this->sender['name'],
+                'organizer' => $this->sender['name'],
+            )
+        ));
+
+        // if (!empty($event['comment'])) {
+        //     $mailbody .= "\n\n" . $this->gettext('itipsendercomment') . $event['comment'];
+        // }
+
+        // append links for direct invitation replies
+        if ($method == 'REQUEST' && $rsvp && ($token = $this->store_invitation($event, $recipient['email']))) {
+            $mailbody .= "\n\n" . $this->gettext(array(
+                'name' => 'invitationattendlinks',
+                'vars' => array('url' => $this->plugin->get_url(array('action' => 'attend', 't' => $token))),
+            ));
+        }
+        else if ($method == 'CANCEL' && $event['cancelled']) {
+            $this->cancel_itip_invitation($event);
+        }
+
+        $message->headers($headers, true);
+        $message->setTXTBody(rcube_mime::format_flowed($mailbody, 79));
+
+        if ($this->rc->config->get('libcalendaring_itip_debug', false)) {
+            rcube::console('iTip ' . $method, $message->txtHeaders() . "\r\n" . $message->get());
+        }
+
+        // finally send the message
+        $this->itip_send = true;
+        $sent = $this->rc->deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
+        $this->itip_send = false;
+
+        return $sent;
+    }
+
+    /**
+     * Plugin hook triggered by rcube::deliver_message() before delivering a message.
+     * Here we can set the 'smtp_server' config option to '' in order to use
+     * PHP's mail() function for unauthenticated email sending.
+     */
+    public function before_send_hook($p)
+    {
+        if ($this->itip_send && !$this->rc->user->ID && $this->rc->config->get('calendar_itip_smtp_server', null) === '') {
+            $this->rc->config->set('smtp_server', '');
+        }
+
+        return $p;
+    }
+
+    /**
+     * Plugin hook to alter SMTP authentication.
+     * This is used if iTip messages are to be sent from an unauthenticated session
+     */
+    public function smtp_connect_hook($p)
+    {
+        // replace smtp auth settings if we're not in an authenticated session
+        if ($this->itip_send && !$this->rc->user->ID) {
+            foreach (array('smtp_server', 'smtp_user', 'smtp_pass') as $prop) {
+                $p[$prop] = $this->rc->config->get("calendar_itip_$prop", $p[$prop]);
+            }
+        }
+
+      return $p;
+    }
+
+    /**
+     * Helper function to build a Mail_mime object to send an iTip message
+     *
+     * @param array   Event object to send
+     * @param string  iTip method (REQUEST|REPLY|CANCEL)
+     * @param boolean Request RSVP
+     * @return object Mail_mime object with message data
+     */
+    public function compose_itip_message($event, $method, $rsvp = true)
+    {
+        $from     = rcube_utils::idn_to_ascii($this->sender['email']);
+        $from_utf = rcube_utils::idn_to_utf8($from);
+        $sender   = format_email_recipient($from, $this->sender['name']);
+
+        // truncate list attendees down to the recipient of the iTip Reply.
+        // constraints for a METHOD:REPLY according to RFC 5546
+        if ($method == 'REPLY') {
+            $replying_attendee = null;
+            $reply_attendees = array();
+            foreach ($event['attendees'] as $attendee) {
+                if ($attendee['role'] == 'ORGANIZER') {
+                    $reply_attendees[] = $attendee;
+                }
+                else if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) {
+                    $replying_attendee = $attendee;
+                    if ($attendee['status'] != 'DELEGATED') {
+                        unset($replying_attendee['rsvp']);  // unset the RSVP attribute
+                    }
+                }
+                // include attendees relevant for delegation (RFC 5546, Section 4.2.5)
+                else if ((!empty($attendee['delegated-to']) &&
+                            (strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0)) ||
+                         (!empty($attendee['delegated-from']) &&
+                            (strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0))) {
+                    $reply_attendees[] = $attendee;
+                }
+            }
+            if ($replying_attendee) {
+                array_unshift($reply_attendees, $replying_attendee);
+                $event['attendees'] = $reply_attendees;
+            }
+            if ($event['recurrence']) {
+                unset($event['recurrence']['EXCEPTIONS']);
+            }
+        }
+        // set RSVP for every attendee
+        else if ($method == 'REQUEST') {
+            foreach ($event['attendees'] as $i => $attendee) {
+                if (($rsvp || !isset($attendee['rsvp'])) && ($attendee['status'] != 'DELEGATED' && $attendee['role'] != 'NON-PARTICIPANT')) {
+                    $event['attendees'][$i]['rsvp']= (bool)$rsvp;
+                }
+            }
+        }
+        else if ($method == 'CANCEL') {
+            if ($event['recurrence']) {
+                unset($event['recurrence']['EXCEPTIONS']);
+            }
+        }
+
+        // Set SENT-BY property if the sender is not the organizer
+        if ($method == 'CANCEL' || $method == 'REQUEST') {
+            foreach ((array)$event['attendees'] as $idx => $attendee) {
+                if ($attendee['role'] == 'ORGANIZER'
+                    && $attendee['email']
+                    && strcasecmp($attendee['email'], $from) != 0
+                    && strcasecmp($attendee['email'], $from_utf) != 0
+                ) {
+                    $attendee['sent-by'] = 'mailto:' . $from_utf;
+                    $event['organizer'] = $event['attendees'][$idx] = $attendee;
+                    break;
+                }
+            }
+        }
+
+        // compose multipart message using PEAR:Mail_Mime
+        $message = new Mail_mime("\r\n");
+        $message->setParam('text_encoding', 'quoted-printable');
+        $message->setParam('head_encoding', 'quoted-printable');
+        $message->setParam('head_charset', RCUBE_CHARSET);
+        $message->setParam('text_charset', RCUBE_CHARSET . ";\r\n format=flowed");
+        $message->setContentType('multipart/alternative');
+
+        // compose common headers array
+        $headers = array(
+            'From' => $sender,
+            'Date' => $this->rc->user_date(),
+            'Message-ID' => $this->rc->gen_message_id(),
+            'X-Sender' => $from,
+        );
+        if ($agent = $this->rc->config->get('useragent')) {
+            $headers['User-Agent'] = $agent;
+        }
+
+        $message->headers($headers);
+
+        // attach ics file for this event
+        $ical = libcalendaring::get_ical();
+        $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
+        $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
+        $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCUBE_CHARSET . "; method=" . $method);
+
+        return $message;
+    }
+
+    /**
+     * Forward the given iTip event as delegation to another person
+     *
+     * @param array Event object to delegate
+     * @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
+     * @param boolean The delegator's RSVP flag
+     * @param array List with indexes of new/updated attendees
+     * @return boolean True on success, False on failure
+     */
+    public function delegate_to(&$event, $delegate, $rsvp = false, &$attendees = array())
+    {
+        if (is_string($delegate)) {
+            $delegates = rcube_mime::decode_address_list($delegate, 1, false);
+            if (count($delegates) > 0) {
+                $delegate = reset($delegates);
+            }
+        }
+
+        $emails = $this->lib->get_user_emails();
+        $me     = $this->rc->user->list_emails(true);
+
+        // find/create the delegate attendee
+        $delegate_attendee = array(
+            'email' => $delegate['mailto'],
+            'name'  => $delegate['name'],
+            'role'  => 'REQ-PARTICIPANT',
+        );
+        $delegate_index = count($event['attendees']);
+
+        foreach ($event['attendees'] as $i => $attendee) {
+          // set myself the DELEGATED-TO parameter
+          if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+              $event['attendees'][$i]['delegated-to'] = $delegate['mailto'];
+              $event['attendees'][$i]['status'] = 'DELEGATED';
+              $event['attendees'][$i]['role'] = 'NON-PARTICIPANT';
+              $event['attendees'][$i]['rsvp'] = $rsvp;
+
+              $me['email'] = $attendee['email'];
+              $delegate_attendee['role'] = $attendee['role'];
+          }
+          // the disired delegatee is already listed as an attendee
+          else if (stripos($delegate['mailto'], $attendee['email']) !== false && $attendee['role'] != 'ORGANIZER') {
+              $delegate_attendee = $attendee;
+              $delegate_index = $i;
+              break;
+          }
+          // TODO: remove previous delegatee (i.e. attendee that has DELEGATED-FROM == $me)
+        }
+
+        // set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter
+        $delegate_attendee['rsvp'] = true;
+        $delegate_attendee['status'] = 'NEEDS-ACTION';
+        $delegate_attendee['delegated-from'] = $me['email'];
+        $event['attendees'][$delegate_index] = $delegate_attendee;
+
+        $attendees[] = $delegate_index;
+
+        $this->set_sender_email($me['email']);
+        return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto');
+    }
+
+    /**
+     * Handler for calendar/itip-status requests
+     */
+    public function get_itip_status($event, $existing = null)
+    {
+      $action = $event['rsvp'] ? 'rsvp' : '';
+      $status = $event['fallback'];
+      $latest = $resheduled = false;
+      $html   = '';
+
+      if (is_numeric($event['changed']))
+        $event['changed'] = new DateTime('@'.$event['changed']);
+
+      // check if the given itip object matches the last state
+      if ($existing) {
+        $latest = (isset($event['sequence']) && intval($existing['sequence']) == intval($event['sequence'])) ||
+                  (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] >= $event['changed']);
+      }
+
+      // determine action for REQUEST
+      if ($event['method'] == 'REQUEST') {
+        $html = html::div('rsvp-status', $this->gettext('acceptinvitation'));
+
+        if ($existing) {
+          $rsvp = $event['rsvp'];
+          $emails = $this->lib->get_user_emails();
+          foreach ($existing['attendees'] as $attendee) {
+            if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+              $status = strtoupper($attendee['status']);
+              break;
+            }
+          }
+
+          // Detect re-sheduling
+          if (!$latest) {
+            // FIXME: This is probably to simplistic, or maybe we should just check
+            //        attendee's RSVP flag in the new event?
+            $resheduled = $existing['start'] != $event['start'] || $existing['end'] > $event['end'];
+          }
+        }
+        else {
+          $rsvp = $event['rsvp'] && $this->rc->config->get('calendar_allow_itip_uninvited', true);
+        }
+
+        $status_lc = strtolower($status);
+
+        if ($status_lc == 'unknown' && !$this->rc->config->get('calendar_allow_itip_uninvited', true)) {
+          $html = html::div('rsvp-status', $this->gettext('notanattendee'));
+          $action = 'import';
+        }
+        else if (in_array($status_lc, $this->rsvp_status)) {
+          $status_text = $this->gettext(($latest ? 'youhave' : 'youhavepreviously') . $status_lc);
+
+          if ($existing && ($existing['sequence'] > $event['sequence'] || (!isset($event['sequence']) && $existing['changed'] && $existing['changed'] > $event['changed']))) {
+            $action = '';  // nothing to do here, outdated invitation
+            if ($status_lc == 'needs-action')
+              $status_text = $this->gettext('outdatedinvitation');
+          }
+          else if (!$existing && !$rsvp) {
+            $action = 'import';
+          }
+          else if ($resheduled) {
+            $action = 'rsvp';
+          }
+          else if ($status_lc != 'needs-action') {
+            $action = !$latest ? 'update' : '';
+          }
+
+          $html = html::div('rsvp-status ' . $status_lc, $status_text);
+        }
+      }
+      // determine action for REPLY
+      else if ($event['method'] == 'REPLY') {
+        // check whether the sender already is an attendee
+        if ($existing) {
+          $action = $this->rc->config->get('calendar_allow_itip_uninvited', true) ? 'accept' : '';
+          $listed = false;
+          foreach ($existing['attendees'] as $attendee) {
+            if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
+              $status_lc = strtolower($status);
+              if (in_array($status_lc, $this->rsvp_status)) {
+                $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
+                    'name' => 'attendee' . $status_lc,
+                    'vars' => array(
+                        'delegatedto' => rcube::Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')),
+                    )
+                )));
+              }
+              $action = $attendee['status'] == $status || !$latest ? '' : 'update';
+              $listed = true;
+              break;
+            }
+          }
+
+          if (!$listed) {
+            $html = html::div('rsvp-status', $this->gettext('itipnewattendee'));
+          }
+        }
+        else {
+          $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
+          $action = '';
+        }
+      }
+      else if ($event['method'] == 'CANCEL') {
+        if (!$existing) {
+          $html = html::div('rsvp-status hint', $this->gettext('itipobjectnotfound'));
+          $action = '';
+        }
+      }
+
+      return array(
+          'uid'        => $event['uid'],
+          'id'         => asciiwords($event['uid'], true),
+          'existing'   => $existing ? true : false,
+          'saved'      => $existing ? true : false,
+          'latest'     => $latest,
+          'status'     => $status,
+          'action'     => $action,
+          'resheduled' => $resheduled,
+          'html'       => $html,
+      );
+    }
+
+    /**
+     * Build inline UI elements for iTip messages
+     */
+    public function mail_itip_inline_ui($event, $method, $mime_id, $task, $message_date = null, $preview_url = null)
+    {
+        $buttons = array();
+        $dom_id = asciiwords($event['uid'], true);
+        $rsvp_status = 'unknown';
+
+        // pass some metadata about the event and trigger the asynchronous status check
+        $changed = is_object($event['changed']) ? $event['changed'] : $message_date;
+        $metadata = array(
+            'uid'      => $event['uid'],
+            '_instance' => $event['_instance'],
+            'changed'  => $changed ? $changed->format('U') : 0,
+            'sequence' => intval($event['sequence']),
+            'method'   => $method,
+            'task'     => $task,
+        );
+
+        // create buttons to be activated from async request checking existence of this event in local calendars
+        $buttons[] = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
+
+        // on iTip REPLY we have two options:
+        if ($method == 'REPLY') {
+            $title = $this->gettext('itipreply');
+
+            foreach ($event['attendees'] as $attendee) {
+                if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER') {
+                    if (empty($event['_sender']) || self::compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
+                        $metadata['attendee'] = $attendee['email'];
+                        $rsvp_status = strtoupper($attendee['status']);
+                        if ($attendee['delegated-to']) {
+                            $metadata['delegated-to'] = $attendee['delegated-to'];
+                        }
+                        break;
+                    }
+                }
+            }
+
+            // 1. update the attendee status on our copy
+            $update_button = html::tag('input', array(
+                'type' => 'button',
+                'class' => 'button',
+                'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
+                'value' => $this->gettext('updateattendeestatus'),
+            ));
+
+            // 2. accept or decline a new or delegate attendee
+            $accept_buttons = html::tag('input', array(
+                'type' => 'button',
+                'class' => "button accept",
+                'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
+                'value' => $this->gettext('acceptattendee'),
+            ));
+            $accept_buttons .= html::tag('input', array(
+                'type' => 'button',
+                'class' => "button decline",
+                'onclick' => "rcube_libcalendaring.decline_attendee_reply('" . rcube::JQ($mime_id) . "', '$task')",
+                'value' => $this->gettext('declineattendee'),
+            ));
+
+            $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
+            $buttons[] = html::div(array('id' => 'accept-'.$dom_id, 'style' => 'display:none'), $accept_buttons);
+        }
+        // when receiving iTip REQUEST messages:
+        else if ($method == 'REQUEST') {
+            $emails = $this->lib->get_user_emails();
+            $title = $event['sequence'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
+            $metadata['rsvp'] = true;
+            $metadata['sensitivity'] = $event['sensitivity'];
+
+            if (is_object($event['start'])) {
+                $metadata['date'] = $event['start']->format('U');
+            }
+
+            // check for X-KOLAB-INVITATIONTYPE property and only show accept/decline buttons
+            if (self::get_custom_property($event, 'X-KOLAB-INVITATIONTYPE') == 'CONFIRMATION') {
+                $this->rsvp_actions = array('accepted','declined');
+                $metadata['nosave'] = true;
+            }
+
+            // 1. display RSVP buttons (if the user was invited)
+            foreach ($this->rsvp_actions as $method) {
+                $rsvp_buttons .= html::tag('input', array(
+                    'type' => 'button',
+                    'class' => "button $method",
+                    'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task', '$method', '$dom_id')",
+                    'value' => $this->gettext('itip' . $method),
+                ));
+            }
+
+            // add button to open calendar/preview
+            if (!empty($preview_url)) {
+              $msgref = $this->lib->ical_message->folder . '/' . $this->lib->ical_message->uid . '#' . $mime_id;
+              $rsvp_buttons .= html::tag('input', array(
+                  'type' => 'button',
+                  'class' => "button preview",
+                  'onclick' => "rcube_libcalendaring.open_itip_preview('" . rcube::JQ($preview_url) . "', '" . rcube::JQ($msgref) . "')",
+                  'value' => $this->gettext('openpreview'),
+              ));
+            }
+
+            // 2. update the local copy with minor changes
+            $update_button = html::tag('input', array(
+                'type' => 'button',
+                'class' => 'button',
+                'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
+                'value' => $this->gettext('updatemycopy'),
+            ));
+
+            // 3. Simply import the event without replying
+            $import_button = html::tag('input', array(
+                'type' => 'button',
+                'class' => 'button',
+                'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
+                'value' => $this->gettext('importtocalendar'),
+            ));
+
+            // check my status
+            foreach ($event['attendees'] as $attendee) {
+                if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+                    $metadata['attendee'] = $attendee['email'];
+                    $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
+                    $rsvp_status = !empty($attendee['status']) ? strtoupper($attendee['status']) : 'NEEDS-ACTION';
+                    break;
+                }
+            }
+
+            // add itip reply message controls
+            $rsvp_buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($dom_id, $metadata['nosave']));
+
+            $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons);
+            $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
+
+            // prepare autocompletion for delegation dialog
+            if (in_array('delegated', $this->rsvp_actions)) {
+                $this->rc->autocomplete_init();
+            }
+        }
+        // for CANCEL messages, we can:
+        else if ($method == 'CANCEL') {
+            $title = $this->gettext('itipcancellation');
+            $event_prop = array_filter(array(
+              'uid' => $event['uid'],
+              '_instance' => $event['_instance'],
+              '_savemode' => $event['_savemode'],
+            ));
+
+            // 1. remove the event from our calendar
+            $button_remove = html::tag('input', array(
+                'type' => 'button',
+                'class' => 'button',
+                'onclick' => "rcube_libcalendaring.remove_from_itip(" . rcube_output::json_serialize($event_prop) . ", '$task', '" . rcube::JQ($event['title']) . "')",
+                'value' => $this->gettext('removefromcalendar'),
+            ));
+
+            // 2. update our copy with status=cancelled
+            $button_update = html::tag('input', array(
+              'type' => 'button',
+              'class' => 'button',
+              'onclick' => "rcube_libcalendaring.add_from_itip_mail('" . rcube::JQ($mime_id) . "', '$task')",
+              'value' => $this->gettext('updatemycopy'),
+            ));
+
+            $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove . $button_update);
+
+            $rsvp_status = 'CANCELLED';
+            $metadata['rsvp'] = true;
+        }
+
+        // append generic import button
+        if ($import_button) {
+            $buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
+        }
+
+        // pass some metadata about the event and trigger the asynchronous status check
+        $metadata['fallback'] = $rsvp_status;
+        $metadata['rsvp'] = intval($metadata['rsvp']);
+
+        $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . rcube_output::json_serialize($metadata) . ")", 'docready');
+
+        // get localized texts from the right domain
+        foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee',
+            'cancel','itipdelegated','declineattendeeconfirm','itipcomment','delegateinvitation',
+            'delegateto','delegatersvpme','delegateinvalidaddress') as $label) {
+          $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
+        }
+
+        // show event details with buttons
+        return $this->itip_object_details_table($event, $title) .
+            html::div(array('class' => 'itip-buttons', 'id' => 'itip-buttons-' . asciiwords($metadata['uid'], true)), join('', $buttons));
+    }
+
+    /**
+     * Render an RSVP UI widget with buttons to respond on iTip invitations
+     */
+    function itip_rsvp_buttons($attrib = array(), $actions = null)
+    {
+        $attrib += array('type' => 'button');
+
+        if (!$actions)
+            $actions = $this->rsvp_actions;
+
+        foreach ($actions as $method) {
+            $buttons .= html::tag('input', array(
+                'type'  => $attrib['type'],
+                'name'  => $attrib['iname'],
+                'class' => 'button',
+                'rel'   => $method,
+                'value' => $this->gettext('itip' . $method),
+            ));
+        }
+
+        // add localized texts for the delegation dialog
+        if (in_array('delegated', $actions)) {
+            foreach (array('itipdelegated','itipcomment','delegateinvitation',
+                  'delegateto','delegatersvpme','delegateinvalidaddress','cancel') as $label) {
+                $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
+            }
+        }
+
+        foreach (array('all','current','future') as $mode) {
+            $this->rc->output->command('add_label', "rsvpmode$mode", $this->gettext("rsvpmode$mode"));
+        }
+
+        $savemode_radio = new html_radiobutton(array('name' => '_rsvpmode', 'class' => 'rsvp-replymode'));
+
+        return html::div($attrib,
+            html::div('label', $this->gettext('acceptinvitation')) .
+            html::div('rsvp-buttons',
+                $buttons .
+                html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']))
+            )
+        );
+    }
+
+    /**
+     * Render UI elements to control iTip reply message sending
+     */
+    public function itip_rsvp_options_ui($dom_id, $disable = false)
+    {
+        $itip_sending = $this->rc->config->get('calendar_itip_send_option', 3);
+
+        // itip sending is entirely disabled
+        if ($itip_sending === 0) {
+            return '';
+        }
+        // add checkbox to suppress itip reply message
+        else if ($itip_sending >= 2) {
+            $rsvp_additions = html::label(array('class' => 'noreply-toggle'),
+                html::tag('input', array('type' => 'checkbox', 'id' => 'noreply-'.$dom_id, 'value' => 1, 'disabled' => $disable, 'checked' => ($itip_sending & 1) == 0))
+                . ' ' . $this->gettext('itipsuppressreply')
+            );
+        }
+
+        // add input field for reply comment
+        $toggle_attrib = array(
+            'href'    => '#toggle',
+            'class'   => 'reply-comment-toggle',
+            'onclick' => '$(this).hide().parent().find(\'textarea\').show().focus()'
+        );
+        $textarea_attrib = array(
+            'id'    => 'reply-comment-' . $dom_id,
+            'name'  => '_comment',
+            'cols'  => 40,
+            'rows'  => 6,
+            'style' => 'display:none',
+            'placeholder' => $this->gettext('itipcomment')
+        );
+
+        $rsvp_additions .= html::a($toggle_attrib, $this->gettext('itipeditresponse'))
+            . html::div('itip-reply-comment', html::tag('textarea', $textarea_attrib, ''));
+
+        return $rsvp_additions;
+    }
+
+    /**
+     * Render event/task details in a table
+     */
+    function itip_object_details_table($event, $title)
+    {
+        $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
+        $table->add('ititle', $title);
+        $table->add('title', rcube::Q($event['title']));
+        if ($event['start'] && $event['end']) {
+            $table->add('label', $this->gettext('date'));
+            $table->add('date', rcube::Q($this->lib->event_date_text($event)));
+        }
+        else if ($event['due'] && $event['_type'] == 'task') {
+            $table->add('label', $this->gettext('date'));
+            $table->add('date', rcube::Q($this->lib->event_date_text($event)));
+        }
+        if (!empty($event['recurrence_date'])) {
+            $table->add('label', '');
+            $table->add('recurrence-id', $this->gettext($event['thisandfuture'] ? 'itipfutureoccurrence' : 'itipsingleoccurrence'));
+        }
+        else if (!empty($event['recurrence'])) {
+            $table->add('label', $this->gettext('recurring'));
+            $table->add('recurrence', $this->lib->recurrence_text($event['recurrence']));
+        }
+        if ($event['location']) {
+            $table->add('label', $this->gettext('location'));
+            $table->add('location', rcube::Q($event['location']));
+        }
+        if ($event['sensitivity'] && $event['sensitivity'] != 'public') {
+            $table->add('label', $this->gettext('sensitivity'));
+            $table->add('sensitivity', ucfirst($this->gettext($event['sensitivity'])) . '!');
+        }
+        if ($event['status'] == 'COMPLETED' || $event['status'] == 'CANCELLED') {
+            $table->add('label', $this->gettext('status'));
+            $table->add('status', $this->gettext('status-' . strtolower($event['status'])));
+        }
+        if ($event['comment']) {
+            $table->add('label', $this->gettext('comment'));
+            $table->add('location', rcube::Q($event['comment']));
+        }
+
+        return $table->show();
+    }
+
+
+    /**
+     * Create iTIP invitation token for later replies via URL
+     *
+     * @param array Hash array with event properties
+     * @param string Attendee email address
+     * @return string Invitation token
+     */
+    public function store_invitation($event, $attendee)
+    {
+        // empty stub
+        return false;
+    }
+
+    /**
+     * Mark invitations for the given event as cancelled
+     *
+     * @param array Hash array with event properties
+     */
+    public function cancel_itip_invitation($event)
+    {
+        // empty stub
+        return false;
+    }
+
+    /**
+     * Utility function to get the value of a custom property
+     */
+    public static function get_custom_property($event, $name)
+    {
+      $ret = false;
+
+      if (is_array($event['x-custom'])) {
+          array_walk($event['x-custom'], function($prop, $i) use ($name, &$ret) {
+              if (strcasecmp($prop[0], $name) === 0) {
+                  $ret = $prop[1];
+              }
+          });
+      }
+
+      return $ret;
+    }
+
+    /**
+     * Compare email address
+     */
+    public static function compare_email($value, $email, $email_utf = null)
+    {
+        $v1 = !empty($email) && strcasecmp($value, $email) === 0;
+        $v2 = !empty($email_utf) && strcasecmp($value, $email_utf) === 0;
+
+        return $v1 || $v2;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/lib/libcalendaring_recurrence.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * Recurrence computation class for shared use
+ *
+ * Uitility class to compute reccurrence dates from the given rules
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class libcalendaring_recurrence
+{
+    protected $lib;
+    protected $start;
+    protected $next;
+    protected $engine;
+    protected $recurrence;
+    protected $dateonly = false;
+    protected $hour = 0;
+
+    /**
+     * Default constructor
+     *
+     * @param object calendar The calendar plugin instance
+     */
+    function __construct($lib)
+    {
+      // use Horde classes to compute recurring instances
+      // TODO: replace with something that has less than 6'000 lines of code
+      require_once(__DIR__ . '/Horde_Date_Recurrence.php');
+
+      $this->lib = $lib;
+    }
+
+    /**
+     * Initialize recurrence engine
+     *
+     * @param array  The recurrence properties
+     * @param object DateTime The recurrence start date
+     */
+    public function init($recurrence, $start = null)
+    {
+        $this->recurrence = $recurrence;
+
+        $this->engine = new Horde_Date_Recurrence($start);
+        $this->engine->fromRRule20(libcalendaring::to_rrule($recurrence));
+
+        $this->set_start($start);
+
+        if (is_array($recurrence['EXDATE'])) {
+            foreach ($recurrence['EXDATE'] as $exdate) {
+                if (is_a($exdate, 'DateTime')) {
+                    $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
+                }
+            }
+        }
+        if (is_array($recurrence['RDATE'])) {
+            foreach ($recurrence['RDATE'] as $rdate) {
+                if (is_a($rdate, 'DateTime')) {
+                    $this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
+                }
+            }
+        }
+    }
+
+    /**
+     * Setter for (new) recurrence start date
+     *
+     * @param object DateTime The recurrence start date
+     */
+    public function set_start($start)
+    {
+        $this->start = $start;
+        $this->dateonly = $start->_dateonly;
+        $this->next = new Horde_Date($start, $this->lib->timezone->getName());
+        $this->hour = $this->next->hour;
+        $this->engine->setRecurStart($this->next);
+    }
+
+    /**
+     * Get date/time of the next occurence of this event
+     *
+     * @return mixed DateTime object or False if recurrence ended
+     */
+    public function next()
+    {
+        $time = false;
+        $after = clone $this->next;
+        $after->mday = $after->mday + 1;
+        if ($this->next && ($next = $this->engine->nextActiveRecurrence($after))) {
+            // avoid endless loops if recurrence computation fails
+            if (!$next->after($this->next)) {
+                return false;
+            }
+            // fix time for all-day events
+            if ($this->dateonly) {
+                $next->hour = $this->hour;
+                $next->min = 0;
+            }
+
+            $time = $next->toDateTime();
+            $this->next = $next;
+        }
+
+        return $time;
+    }
+
+    /**
+     * Get the end date of the occurence of this recurrence cycle
+     *
+     * @return DateTime|bool End datetime of the last occurence or False if recurrence exceeds limit
+     */
+    public function end()
+    {
+        // recurrence end date is given
+        if ($this->recurrence['UNTIL'] instanceof DateTime) {
+            return $this->recurrence['UNTIL'];
+        }
+
+        // take the last RDATE entry if set
+        if (is_array($this->recurrence['RDATE']) && !empty($this->recurrence['RDATE'])) {
+            $last = end($this->recurrence['RDATE']);
+            if ($last instanceof DateTime) {
+              return $last;
+            }
+        }
+
+        // run through all items till we reach the end
+        if ($this->recurrence['COUNT']) {
+            $last = $this->start;
+            $this->next = new Horde_Date($this->start, $this->lib->timezone->getName());
+            while (($next = $this->next()) && $c < 1000) {
+                $last = $next;
+                $c++;
+            }
+        }
+
+        return $last;
+    }
+
+}
Binary file plugins/libcalendaring/lib/sabre-vobject-2.1.0.tar.gz has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/libcalendaring.js	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,1464 @@
+/**
+ * Basic Javascript utilities for calendar-related plugins
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * @licstart  The following is the entire license notice for the
+ * JavaScript code in this page.
+ *
+ * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @licend  The above is the entire license notice
+ * for the JavaScript code in this page.
+ */
+
+function rcube_libcalendaring(settings)
+{
+    // member vars
+    this.settings = settings || {};
+    this.alarm_ids = [];
+    this.alarm_dialog = null;
+    this.snooze_popup = null;
+    this.dismiss_link = null;
+    this.group2expand = {};
+
+    // abort if env isn't set
+    if (!settings || !settings.date_format)
+      return;
+
+    // private vars
+    var me = this;
+    var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0);
+    var client_timezone = new Date().getTimezoneOffset();
+
+    // general datepicker settings
+    var datepicker_settings = {
+        // translate from fullcalendar format to datepicker format
+        dateFormat: settings.date_format.replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
+        firstDay : settings.first_day,
+        dayNamesMin: settings.days_short,
+        monthNames: settings.months,
+        monthNamesShort: settings.months,
+        changeMonth: false,
+        showOtherMonths: true,
+        selectOtherMonths: true
+    };
+
+
+    /**
+     * Quote html entities
+     */
+    var Q = this.quote_html = function(str)
+    {
+      return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
+    };
+
+    /**
+     * Create a nice human-readable string for the date/time range
+     */
+    this.event_date_text = function(event, voice)
+    {
+      if (!event.start)
+        return '';
+      if (!event.end)
+        event.end = event.start;
+
+      var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000,
+        until = voice ? ' ' + rcmail.gettext('until','libcalendaring') + ' ' : ' — ';
+      if (event.allDay) {
+        fromto = this.format_datetime(event.start, 1, voice)
+          + (duration > 86400 || event.start.getDay() != event.end.getDay() ? until + this.format_datetime(event.end, 1, voice) : '');
+      }
+      else if (duration < 86400 && event.start.getDay() == event.end.getDay()) {
+        fromto = this.format_datetime(event.start, 0, voice)
+          + (duration > 0 ? until + this.format_datetime(event.end, 2, voice) : '');
+      }
+      else {
+        fromto = this.format_datetime(event.start, 0, voice)
+          + (duration > 0 ? until + this.format_datetime(event.end, 0, voice) : '');
+      }
+
+      return fromto;
+    };
+
+    /**
+     * Checks if the event/task has 'real' attendees, excluding the current user
+     */
+    this.has_attendees = function(event)
+    {
+        return !!(event.attendees && event.attendees.length && (event.attendees.length > 1 || String(event.attendees[0].email).toLowerCase() != settings.identity.email));
+    };
+
+    /**
+     * Check if the current user is an attendee of this event/task
+     */
+    this.is_attendee = function(event, role, email)
+    {
+        var i, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
+
+        for (i=0; event.attendees && i < event.attendees.length; i++) {
+            if ((!role || event.attendees[i].role == role) && event.attendees[i].email && emails.indexOf(';'+event.attendees[i].email.toLowerCase()) >= 0) {
+                return event.attendees[i];
+            }
+        }
+
+        return false;
+    };
+
+    /**
+     * Checks if the current user is the organizer of the event/task
+     */
+    this.is_organizer = function(event, email)
+    {
+        return this.is_attendee(event, 'ORGANIZER', email) || !event.id;
+    };
+
+    /**
+     * Check permissions on the given folder object
+     */
+    this.has_permission = function(folder, perm)
+    {
+        // multiple chars means "either of"
+        if (String(perm).length > 1) {
+            for (var i=0; i < perm.length; i++) {
+                if (this.has_permission(folder, perm[i])) {
+                    return true;
+                }
+            }
+        }
+
+        if (folder.rights && String(folder.rights).indexOf(perm) >= 0) {
+            return true;
+        }
+
+        return (perm == 'i' && folder.editable) || (perm == 'v' && folder.editable);
+    };
+
+
+    /**
+     * From time and date strings to a real date object
+     */
+    this.parse_datetime = function(time, date)
+    {
+        // we use the utility function from datepicker to parse dates
+        var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date();
+
+        var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/);
+        if (!isNaN(time_arr[0])) {
+            date.setHours(time_arr[0]);
+        if (time.match(/p[.m]*/i) && date.getHours() < 12)
+            date.setHours(parseInt(time_arr[0]) + 12);
+        else if (time.match(/a[.m]*/i) && date.getHours() == 12)
+            date.setHours(0);
+      }
+      if (!isNaN(time_arr[1]))
+            date.setMinutes(time_arr[1]);
+
+      return date;
+    }
+
+    /**
+     * Convert an ISO 8601 formatted date string from the server into a Date object.
+     * Timezone information will be ignored, the server already provides dates in user's timezone.
+     */
+    this.parseISO8601 = function(s)
+    {
+        // already a Date object?
+        if (s && s.getMonth) {
+            return s;
+        }
+
+        // force d to be on check's YMD, for daylight savings purposes
+        var fixDate = function(d, check) {
+            if (+d) { // prevent infinite looping on invalid dates
+                while (d.getDate() != check.getDate()) {
+                    d.setTime(+d + (d < check ? 1 : -1) * 3600000);
+                }
+            }
+        }
+
+        // derived from http://delete.me.uk/2005/03/iso8601.html
+        var m = s && s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
+        if (!m) {
+            return null;
+        }
+
+        var date = new Date(m[1], 0, 2),
+            check = new Date(m[1], 0, 2, 9, 0);
+        if (m[3]) {
+            date.setMonth(m[3] - 1);
+            check.setMonth(m[3] - 1);
+        }
+        if (m[5]) {
+            date.setDate(m[5]);
+            check.setDate(m[5]);
+        }
+        fixDate(date, check);
+        if (m[7]) {
+            date.setHours(m[7]);
+        }
+        if (m[8]) {
+            date.setMinutes(m[8]);
+        }
+        if (m[10]) {
+            date.setSeconds(m[10]);
+        }
+        if (m[12]) {
+            date.setMilliseconds(Number("0." + m[12]) * 1000);
+        }
+        fixDate(date, check);
+
+        return date;
+    }
+
+    /**
+     * Turn the given date into an ISO 8601 date string understandable by PHPs strtotime()
+     */
+    this.date2ISO8601 = function(date)
+    {
+        var zeropad = function(num) { return (num < 10 ? '0' : '') + num; };
+
+        return date.getFullYear() + '-' + zeropad(date.getMonth()+1) + '-' + zeropad(date.getDate())
+            + 'T' + zeropad(date.getHours()) + ':' + zeropad(date.getMinutes()) + ':' + zeropad(date.getSeconds());
+    };
+
+    /**
+     * Format the given date object according to user's prefs
+     */
+    this.format_datetime = function(date, mode, voice)
+    {
+        var res = '';
+        if (!mode || mode == 1) {
+          res += $.datepicker.formatDate(voice ? 'MM d yy' : datepicker_settings.dateFormat, date, datepicker_settings);
+        }
+        if (!mode) {
+            res += voice ? ' ' + rcmail.gettext('at','libcalendaring') + ' ' : ' ';
+        }
+        if (!mode || mode == 2) {
+            res += this.format_time(date, voice);
+        }
+
+        return res;
+    }
+
+    /**
+     * Clone from fullcalendar.js
+     */
+    this.format_time = function(date, voice)
+    {
+        var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; }
+        var formatters = {
+            s   : function(d) { return d.getSeconds() },
+            ss  : function(d) { return zeroPad(d.getSeconds()) },
+            m   : function(d) { return d.getMinutes() },
+            mm  : function(d) { return zeroPad(d.getMinutes()) },
+            h   : function(d) { return d.getHours() % 12 || 12 },
+            hh  : function(d) { return zeroPad(d.getHours() % 12 || 12) },
+            H   : function(d) { return d.getHours() },
+            HH  : function(d) { return zeroPad(d.getHours()) },
+            t   : function(d) { return d.getHours() < 12 ? 'a' : 'p' },
+            tt  : function(d) { return d.getHours() < 12 ? 'am' : 'pm' },
+            T   : function(d) { return d.getHours() < 12 ? 'A' : 'P' },
+            TT  : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }
+        };
+
+        var i, i2, c, formatter, res = '',
+          format = voice ? settings['time_format'].replace(':',' ').replace('HH','H').replace('hh','h').replace('mm','m').replace('ss','s') : settings['time_format'];
+        for (i=0; i < format.length; i++) {
+            c = format.charAt(i);
+            for (i2=Math.min(i+2, format.length); i2 > i; i2--) {
+                if (formatter = formatters[format.substring(i, i2)]) {
+                    res += formatter(date);
+                    i = i2 - 1;
+                    break;
+                }
+            }
+            if (i2 == i) {
+                res += c;
+            }
+        }
+
+        return res;
+    }
+
+    /**
+     * Convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
+     */
+    this.date2unixtime = function(date)
+    {
+        var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;  // adjust DST offset
+        return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset);
+    }
+
+    /**
+     * Turn a unix timestamp value into a Date object
+     */
+    this.fromunixtime = function(ts)
+    {
+        ts -= gmt_offset * 3600;
+        var date = new Date(ts * 1000),
+            dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;
+        if (dst_offset)  // adjust DST offset
+            date.setTime((ts + 3600) * 1000);
+        return date;
+    }
+
+    /**
+     * Simple plaintext to HTML converter, makig URLs clickable
+     */
+    this.text2html = function(str, maxlen, maxlines)
+    {
+        var html = Q(String(str));
+
+        // limit visible text length
+        if (maxlen) {
+            var morelink = '<span>... <a href="#more" onclick="$(this).parent().hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','libcalendaring')+'</a></span><span style="display:none">',
+                lines = html.split(/\r?\n/),
+                words, out = '', len = 0;
+
+            for (var i=0; i < lines.length; i++) {
+                len += lines[i].length;
+                if (maxlines && i == maxlines - 1) {
+                    out += lines[i] + '\n' + morelink;
+                    maxlen = html.length * 2;
+                }
+                else if (len > maxlen) {
+                    len = out.length;
+                    words = lines[i].split(' ');
+                    for (var j=0; j < words.length; j++) {
+                        len += words[j].length + 1;
+                        out += words[j] + ' ';
+                        if (len > maxlen) {
+                            out += morelink;
+                            maxlen = html.length * 2;
+                            maxlines = 0;
+                        }
+                    }
+                    out += '\n';
+                }
+                else
+                    out += lines[i] + '\n';
+            }
+
+            if (maxlen > str.length)
+                out += '</span>';
+
+            html = out;
+        }
+
+        // simple link parser (similar to rcube_string_replacer class in PHP)
+        var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
+        var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
+        var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)', 'ig');
+        var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
+        var link_replace = function(matches, p1, p2) {
+          var title = '', text = p2;
+          if (p2 && p2.length > 55) {
+            text = p2.substr(0, 45) + '...' + p2.substr(-8);
+            title = p1 + p2;
+          }
+          return '<a href="'+p1+p2+'" class="extlink" target="_blank" title="'+title+'">'+p1+text+'</a>'
+        };
+
+        return html
+            .replace(link_pattern, link_replace)
+            .replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
+            .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
+            .replace(/\n/g, "<br/>");
+    };
+
+    this.init_alarms_edit = function(prefix, index)
+    {
+        var edit_type = $(prefix+' select.edit-alarm-type'),
+          dom_id = edit_type.attr('id');
+
+        // register events on alarm fields
+        edit_type.change(function(){
+            $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
+        });
+        $(prefix+' select.edit-alarm-offset').change(function(){
+            var val = $(this).val(), parent = $(this).parent();
+            parent.find('.edit-alarm-date, .edit-alarm-time')[val == '@' ? 'show' : 'hide']();
+            parent.find('.edit-alarm-value').prop('disabled', val === '@' || val === '0');
+            parent.find('.edit-alarm-related')[val == '@' ? 'hide' : 'show']();
+        });
+
+        $(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings);
+
+        $(prefix).on('click', 'a.delete-alarm', function(e){
+            if ($(this).closest('.edit-alarm-item').siblings().length > 0) {
+                $(this).closest('.edit-alarm-item').remove();
+            }
+            return false;
+        });
+
+        // set a unique id attribute and set label reference accordingly
+        if ((index || 0) > 0 && dom_id) {
+            dom_id += ':' + (new Date().getTime());
+            edit_type.attr('id', dom_id);
+            $(prefix+' label:first').attr('for', dom_id);
+        }
+
+        $(prefix).on('click', 'a.add-alarm', function(e){
+            var i = $(this).closest('.edit-alarm-item').siblings().length + 1;
+            var item = $(this).closest('.edit-alarm-item').clone(false)
+              .removeClass('first')
+              .appendTo(prefix);
+
+              me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
+              $('select.edit-alarm-type, select.edit-alarm-offset', item).change();
+              return false;
+        });
+    }
+
+    this.set_alarms_edit = function(prefix, valarms)
+    {
+        $(prefix + ' .edit-alarm-item:gt(0)').remove();
+
+        var i, alarm, domnode, val, offset;
+        for (i=0; i < valarms.length; i++) {
+          alarm = valarms[i];
+          if (!alarm.action)
+              alarm.action = 'DISPLAY';
+
+          if (i == 0) {
+              domnode = $(prefix + ' .edit-alarm-item').eq(0);
+          }
+          else {
+              domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix);
+              this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')', i);
+          }
+
+          $('select.edit-alarm-type', domnode).val(alarm.action);
+          $('select.edit-alarm-related', domnode).val(/END/i.test(alarm.related) ? 'end' : 'start');
+
+          if (String(alarm.trigger).match(/@(\d+)/)) {
+              var ondate = this.fromunixtime(parseInt(RegExp.$1));
+              $('select.edit-alarm-offset', domnode).val('@');
+              $('input.edit-alarm-value', domnode).val('');
+              $('input.edit-alarm-date', domnode).val(this.format_datetime(ondate, 1));
+              $('input.edit-alarm-time', domnode).val(this.format_datetime(ondate, 2));
+          }
+          else if (String(alarm.trigger).match(/^[-+]*0[MHDS]$/)) {
+              $('input.edit-alarm-value', domnode).val('0');
+              $('select.edit-alarm-offset', domnode).val('0');
+          }
+          else if (String(alarm.trigger).match(/([-+])(\d+)([MHDS])/)) {
+              val = RegExp.$2; offset = ''+RegExp.$1+RegExp.$3;
+              $('input.edit-alarm-value', domnode).val(val);
+              $('select.edit-alarm-offset', domnode).val(offset);
+          }
+        }
+
+        // set correct visibility by triggering onchange handlers
+        $(prefix + ' select.edit-alarm-type, ' + prefix + ' select.edit-alarm-offset').change();
+    };
+
+    this.serialize_alarms = function(prefix)
+    {
+        var valarms = [];
+
+        $(prefix + ' .edit-alarm-item').each(function(i, elem) {
+            var val, offset, alarm = {
+                    action: $('select.edit-alarm-type', elem).val(),
+                    related: $('select.edit-alarm-related', elem).val()
+                };
+
+            if (alarm.action) {
+                offset = $('select.edit-alarm-offset', elem).val();
+                if (offset == '@') {
+                    alarm.trigger = '@' + me.date2unixtime(me.parse_datetime($('input.edit-alarm-time', elem).val(), $('input.edit-alarm-date', elem).val()));
+                }
+                else if (offset === '0') {
+                    alarm.trigger = '0S';
+                }
+                else if (!isNaN((val = parseInt($('input.edit-alarm-value', elem).val()))) && val >= 0) {
+                    alarm.trigger = offset[0] + val + offset[1];
+                }
+
+                valarms.push(alarm);
+            }
+        });
+
+        return valarms;
+    };
+
+    // format time string
+    var time_autocomplete_format = function(hour, minutes, start) {
+        var time, diff, unit, duration = '', d = new Date();
+
+        d.setHours(hour);
+        d.setMinutes(minutes);
+        time = me.format_time(d);
+
+        if (start) {
+            diff = Math.floor((d.getTime() - start.getTime()) / 60000);
+            if (diff > 0) {
+                unit = 'm';
+                if (diff >= 60) {
+                    unit = 'h';
+                    diff = Math.round(diff / 3) / 20;
+                }
+                duration = ' (' + diff + unit + ')';
+            }
+        }
+
+        return [time, duration];
+    };
+
+    var time_autocomplete_list = function(p, callback) {
+        // Time completions
+        var st, h, step = 15, result = [], now = new Date(),
+            id = String(this.element.attr('id')),
+            m = id.match(/^(.*)-(starttime|endtime)$/),
+            start = (m && m[2] == 'endtime'
+                && (st = $('#' + m[1] + '-starttime').val())
+                && $('#' + m[1] + '-startdate').val() == $('#' + m[1] + '-enddate').val())
+                ? me.parse_datetime(st, '') : null,
+            full = p.term - 1 > 0 || p.term.length > 1,
+            hours = start ? start.getHours() : (full ? me.parse_datetime(p.term, '') : now).getHours(),
+            minutes = hours * 60 + (full ? 0 : now.getMinutes()),
+            min = Math.ceil(minutes / step) * step % 60,
+            hour = Math.floor(Math.ceil(minutes / step) * step / 60);
+
+        // list hours from 0:00 till now
+        for (h = start ? start.getHours() : 0; h < hours; h++)
+            result.push(time_autocomplete_format(h, 0, start));
+
+        // list 15min steps for the next two hours
+        for (; h < hour + 2 && h < 24; h++) {
+            while (min < 60) {
+                result.push(time_autocomplete_format(h, min, start));
+                min += step;
+            }
+            min = 0;
+        }
+
+        // list the remaining hours till 23:00
+        while (h < 24)
+            result.push(time_autocomplete_format((h++), 0, start));
+
+        return callback(result);
+    };
+
+    var time_autocomplete_open = function(event, ui) {
+        // scroll to current time
+        var $this = $(this),
+            widget = $this.autocomplete('widget')
+            menu = $this.data('ui-autocomplete').menu,
+            amregex = /^(.+)(a[.m]*)/i,
+            pmregex = /^(.+)(a[.m]*)/i,
+            val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1');
+
+        widget.css('width', '10em');
+
+        if (val === '')
+            menu._scrollIntoView(widget.children('li:first'));
+        else
+            widget.children().each(function() {
+                var li = $(this),
+                    html = li.children().first().html()
+                        .replace(/\s+\(.+\)$/, '')
+                        .replace(amregex, '0:$1')
+                        .replace(pmregex, '1:$1');
+
+                if (html.indexOf(val) == 0)
+                    menu._scrollIntoView(li);
+            });
+    };
+
+    /**
+     * Initializes time autocompletion
+     */
+    this.init_time_autocomplete = function(elem, props)
+    {
+        var default_props = {
+                delay: 100,
+                minLength: 1,
+                appendTo: props.container,
+                source: time_autocomplete_list,
+                open: time_autocomplete_open,
+                // change: time_autocomplete_change,
+                select: function(event, ui) {
+                    $(this).val(ui.item[0]).change();
+                    return false;
+                }
+            };
+
+        $(elem).attr('autocomplete', "off")
+            .autocomplete($.extend(default_props, props))
+            .click(function() {  // show drop-down upon clicks
+                $(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " ");
+            });
+
+        $(elem).data('ui-autocomplete')._renderItem = function(ul, item) {
+            return $('<li>')
+                .data('ui-autocomplete-item', item)
+                .append('<a>' + item[0] + item[1] + '</a>')
+                .appendTo(ul);
+        };
+    };
+
+    /*****  Alarms handling  *****/
+
+    /**
+     * Display a notification for the given pending alarms
+     */
+    this.display_alarms = function(alarms)
+    {
+        // clear old alert first
+        if (this.alarm_dialog)
+            this.alarm_dialog.dialog('destroy').remove();
+
+        var i, actions, adismiss, asnooze, alarm, html,
+            audio_alarms = [], records = [], event_ids = [], buttons = {};
+
+        for (i=0; i < alarms.length; i++) {
+            alarm = alarms[i];
+            alarm.start = this.parseISO8601(alarm.start);
+            alarm.end = this.parseISO8601(alarm.end);
+
+            if (alarm.action == 'AUDIO') {
+                audio_alarms.push(alarm);
+                continue;
+            }
+
+            event_ids.push(alarm.id);
+
+            html = '<h3 class="event-title">' + Q(alarm.title) + '</h3>';
+            html += '<div class="event-section">' + Q(alarm.location || '') + '</div>';
+            html += '<div class="event-section">' + Q(this.event_date_text(alarm)) + '</div>';
+
+            adismiss = $('<a href="#" class="alarm-action-dismiss"></a>').html(rcmail.gettext('dismiss','libcalendaring')).click(function(e){
+                me.dismiss_link = $(this);
+                me.dismiss_alarm(me.dismiss_link.data('id'), 0, e);
+            });
+            asnooze = $('<a href="#" class="alarm-action-snooze"></a>').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){
+                me.snooze_dropdown($(this), e);
+                e.stopPropagation();
+                return false;
+            });
+            actions = $('<div>').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id));
+
+            records.push($('<div>').addClass('alarm-item').html(html).append(actions));
+        }
+
+        if (audio_alarms.length)
+            this.audio_alarms(audio_alarms);
+
+        if (!records.length)
+            return;
+
+        this.alarm_dialog = $('<div>').attr('id', 'alarm-display').append(records);
+
+        buttons[rcmail.gettext('close')] = function() {
+            $(this).dialog('close');
+        };
+
+        buttons[rcmail.gettext('dismissall','libcalendaring')] = function(e) {
+            // submit dismissed event_ids to server
+            me.dismiss_alarm(me.alarm_ids.join(','), 0, e);
+            $(this).dialog('close');
+        };
+
+        this.alarm_dialog.appendTo(document.body).dialog({
+            modal: false,
+            resizable: true,
+            closeOnEscape: false,
+            dialogClass: 'alarms',
+            title: rcmail.gettext('alarmtitle','libcalendaring'),
+            buttons: buttons,
+            open: function() {
+              setTimeout(function() {
+                me.alarm_dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+              }, 5);
+            },
+            close: function() {
+              $('#alarm-snooze-dropdown').hide();
+              $(this).dialog('destroy').remove();
+              me.alarm_dialog = null;
+              me.alarm_ids = null;
+            },
+            drag: function(event, ui) {
+              $('#alarm-snooze-dropdown').hide();
+            }
+        });
+
+        this.alarm_dialog.closest('div[role=dialog]').attr('role', 'alertdialog');
+
+        this.alarm_ids = event_ids;
+    };
+
+    /**
+     * Display a notification and play a sound for a set of alarms
+     */
+    this.audio_alarms = function(alarms)
+    {
+        var elem, txt = [],
+            src = rcmail.assets_path('plugins/libcalendaring/alarm'),
+            plugin = navigator.mimeTypes ? navigator.mimeTypes['audio/mp3'] : {};
+
+        // first generate and display notification text
+        $.each(alarms, function() { txt.push(this.title); });
+
+        rcmail.display_message(rcmail.gettext('alarmtitle','libcalendaring') + ': ' + Q(txt.join(', ')), 'notice', 10000);
+
+        // Internet Explorer does not support wav files,
+        // support in other browsers depends on enabled plugins,
+        // so we use wav as a fallback
+        src += bw.ie || (plugin && plugin.enabledPlugin) ? '.mp3' : '.wav';
+
+        // HTML5
+        try {
+            elem = $('<audio>').attr('src', src);
+            elem.get(0).play();
+        }
+        // old method
+        catch (e) {
+            elem = $('<embed id="libcalsound" src="' + src + '" hidden=true autostart=true loop=false />');
+            elem.appendTo($('body'));
+            window.setTimeout("$('#libcalsound').remove()", 10000);
+        }
+    };
+
+    /**
+     * Show a drop-down menu with a selection of snooze times
+     */
+    this.snooze_dropdown = function(link, event)
+    {
+        if (!this.snooze_popup) {
+            this.snooze_popup = $('#alarm-snooze-dropdown');
+            // create popup if not found
+            if (!this.snooze_popup.length) {
+                this.snooze_popup = $('<div>').attr('id', 'alarm-snooze-dropdown').addClass('popupmenu').appendTo(document.body);
+                this.snooze_popup.html(rcmail.env.snooze_select)
+            }
+            $('#alarm-snooze-dropdown a').click(function(e){
+                var time = String(this.href).replace(/.+#/, '');
+                me.dismiss_alarm($('#alarm-snooze-dropdown').data('id'), time, e);
+                return false;
+            });
+        }
+
+        // hide visible popup
+        if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) {
+            rcmail.command('menu-close', 'alarm-snooze-dropdown', link.get(0), event);
+            this.dismiss_link = null;
+        }
+        else {  // open popup below the clicked link
+            rcmail.command('menu-open', 'alarm-snooze-dropdown', link.get(0), event);
+            this.snooze_popup.data('id', link.data('id'));
+            this.dismiss_link = link;
+        }
+    };
+
+    /**
+     * Dismiss or snooze alarms for the given event
+     */
+    this.dismiss_alarm = function(id, snooze, event)
+    {
+        rcmail.command('menu-close', 'alarm-snooze-dropdown', null, event);
+        rcmail.http_post('utils/plugin.alarms', { action:'dismiss', data:{ id:id, snooze:snooze } });
+
+        // remove dismissed alarm from list
+        if (this.dismiss_link) {
+            this.dismiss_link.closest('div.alarm-item').hide();
+            var new_ids = jQuery.grep(this.alarm_ids, function(v){ return v != id; });
+            if (new_ids.length)
+                this.alarm_ids = new_ids;
+            else
+                this.alarm_dialog.dialog('close');
+        }
+
+        this.dismiss_link = null;
+    };
+
+
+    /*****  Recurrence form handling  *****/
+
+    /**
+     * Install event handlers on recurrence form elements
+     */
+    this.init_recurrence_edit = function(prefix)
+    {
+        // toggle recurrence frequency forms
+        $('#edit-recurrence-frequency').change(function(e){
+            var freq = $(this).val().toLowerCase();
+            $('.recurrence-form').hide();
+            if (freq) {
+              $('#recurrence-form-'+freq).show();
+              if (freq != 'rdate')
+                $('#recurrence-form-until').show();
+            }
+        });
+        $('#recurrence-form-rdate input.button.add').click(function(e){
+            var dt, dv = $('#edit-recurrence-rdate-input').val();
+            if (dv && (dt = me.parse_datetime('12:00', dv))) {
+                me.add_rdate(dt);
+                me.sort_rdates();
+                $('#edit-recurrence-rdate-input').val('')
+            }
+            else {
+                $('#edit-recurrence-rdate-input').select();
+            }
+        });
+        $('#edit-recurrence-rdates').on('click', 'a.delete', function(e){
+            $(this).closest('li').remove();
+            return false;
+        });
+
+        $('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) });
+        $('#edit-recurrence-repeat-times').change(function(e){ $('#edit-recurrence-repeat-count').prop('checked', true); });
+        $('#edit-recurrence-rdate-input').datepicker(datepicker_settings);
+    };
+
+    /**
+     * Set recurrence form according to the given event/task record
+     */
+    this.set_recurrence_edit = function(rec)
+    {
+        var recurrence = $('#edit-recurrence-frequency').val(rec.recurrence ? rec.recurrence.FREQ || (rec.recurrence.RDATE ? 'RDATE' : '') : '').change(),
+            interval = $('.recurrence-form select.edit-recurrence-interval').val(rec.recurrence ? rec.recurrence.INTERVAL || 1 : 1),
+            rrtimes = $('#edit-recurrence-repeat-times').val(rec.recurrence ? rec.recurrence.COUNT || 1 : 1),
+            rrenddate = $('#edit-recurrence-enddate').val(rec.recurrence && rec.recurrence.UNTIL ? this.format_datetime(this.parseISO8601(rec.recurrence.UNTIL), 1) : '');
+        $('.recurrence-form input.edit-recurrence-until:checked').prop('checked', false);
+        $('#edit-recurrence-rdates').html('');
+
+        var weekdays = ['SU','MO','TU','WE','TH','FR','SA'],
+            rrepeat_id = '#edit-recurrence-repeat-forever';
+        if      (rec.recurrence && rec.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count';
+        else if (rec.recurrence && rec.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until';
+        $(rrepeat_id).prop('checked', true);
+
+        if (rec.recurrence && rec.recurrence.BYDAY && rec.recurrence.FREQ == 'WEEKLY') {
+            var wdays = rec.recurrence.BYDAY.split(',');
+            $('input.edit-recurrence-weekly-byday').val(wdays);
+        }
+        if (rec.recurrence && rec.recurrence.BYMONTHDAY) {
+            $('input.edit-recurrence-monthly-bymonthday').val(String(rec.recurrence.BYMONTHDAY).split(','));
+            $('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']);
+        }
+        if (rec.recurrence && rec.recurrence.BYDAY && (rec.recurrence.FREQ == 'MONTHLY' || rec.recurrence.FREQ == 'YEARLY')) {
+            var byday, section = rec.recurrence.FREQ.toLowerCase();
+            if ((byday = String(rec.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) {
+                $('#edit-recurrence-'+section+'-prefix').val(byday[1]);
+                $('#edit-recurrence-'+section+'-byday').val(byday[2]);
+            }
+            $('input.edit-recurrence-'+section+'-mode').val(['BYDAY']);
+        }
+        else if (rec.start) {
+            $('#edit-recurrence-monthly-byday').val(weekdays[rec.start.getDay()]);
+        }
+        if (rec.recurrence && rec.recurrence.BYMONTH) {
+            $('input.edit-recurrence-yearly-bymonth').val(String(rec.recurrence.BYMONTH).split(','));
+        }
+        else if (rec.start) {
+            $('input.edit-recurrence-yearly-bymonth').val([String(rec.start.getMonth()+1)]);
+        }
+        if (rec.recurrence && rec.recurrence.RDATE) {
+            $.each(rec.recurrence.RDATE, function(i,rdate){
+                me.add_rdate(me.parseISO8601(rdate));
+            });
+        }
+    };
+
+    /**
+     * Gather recurrence settings from form
+     */
+    this.serialize_recurrence = function(timestr)
+    {
+        var recurrence = '',
+            freq = $('#edit-recurrence-frequency').val();
+
+        if (freq != '') {
+            recurrence = {
+                FREQ: freq,
+                INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val()
+            };
+
+            var until = $('input.edit-recurrence-until:checked').val();
+            if (until == 'count')
+                recurrence.COUNT = $('#edit-recurrence-repeat-times').val();
+            else if (until == 'until')
+                recurrence.UNTIL = me.date2ISO8601(me.parse_datetime(timestr || '00:00', $('#edit-recurrence-enddate').val()));
+
+            if (freq == 'WEEKLY') {
+                var byday = [];
+                $('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); });
+                if (byday.length)
+                    recurrence.BYDAY = byday.join(',');
+            }
+            else if (freq == 'MONTHLY') {
+                var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = [];
+                if (mode == 'BYMONTHDAY') {
+                    $('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); });
+                    if (bymonday.length)
+                        recurrence.BYMONTHDAY = bymonday.join(',');
+                }
+                else
+                    recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val();
+            }
+            else if (freq == 'YEARLY') {
+                var byday, bymonth = [];
+                $('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); });
+                if (bymonth.length)
+                    recurrence.BYMONTH = bymonth.join(',');
+                if ((byday = $('#edit-recurrence-yearly-byday').val()))
+                    recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday;
+            }
+            else if (freq == 'RDATE') {
+                recurrence = { RDATE:[] };
+                // take selected but not yet added date into account
+                if ($('#edit-recurrence-rdate-input').val() != '') {
+                    $('#recurrence-form-rdate input.button.add').click();
+                }
+                $('#edit-recurrence-rdates li').each(function(i, li){
+                    recurrence.RDATE.push($(li).attr('data-value'));
+                });
+            }
+        }
+
+        return recurrence;
+    };
+
+    // add the given date to the RDATE list
+    this.add_rdate = function(date)
+    {
+        var li = $('<li>')
+            .attr('data-value', this.date2ISO8601(date))
+            .html('<span>' + Q(this.format_datetime(date, 1)) + '</span>')
+            .appendTo('#edit-recurrence-rdates');
+
+        $('<a>').attr('href', '#del')
+            .addClass('iconbutton delete')
+            .html(rcmail.get_label('delete', 'libcalendaring'))
+            .attr('title', rcmail.get_label('delete', 'libcalendaring'))
+            .appendTo(li);
+    };
+
+    // re-sort the list items by their 'data-value' attribute
+    this.sort_rdates = function()
+    {
+        var mylist = $('#edit-recurrence-rdates'),
+            listitems = mylist.children('li').get();
+        listitems.sort(function(a, b) {
+            var compA = $(a).attr('data-value');
+            var compB = $(b).attr('data-value');
+            return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
+        })
+        $.each(listitems, function(idx, item) { mylist.append(item); });
+    };
+
+
+    /*****  Attendee form handling  *****/
+
+    // expand the given contact group into individual event/task attendees
+    this.expand_attendee_group = function(e, add, remove)
+    {
+        var id = (e.data ? e.data.email : null) || $(e.target).attr('data-email'),
+            role_select = $(e.target).closest('tr').find('select.edit-attendee-role option:selected');
+
+        this.group2expand[id] = { link: e.target, data: $.extend({}, e.data || {}), adder: add, remover: remove }
+
+        // copy group role from the according form element
+        if (role_select.length) {
+            this.group2expand[id].data.role = role_select.val();
+        }
+
+        // register callback handler
+        if (!this._expand_attendee_listener) {
+            this._expand_attendee_listener = this.expand_attendee_callback;
+            rcmail.addEventListener('plugin.expand_attendee_callback', function(result) {
+                me._expand_attendee_listener(result);
+            });
+        }
+
+        rcmail.http_post('libcal/plugin.expand_attendee_group', { id: id, data: e.data || {} }, rcmail.set_busy(true, 'loading'));
+    };
+
+    // callback from server to expand an attendee group
+    this.expand_attendee_callback = function(result)
+    {
+        var attendee, id = result.id,
+            data = this.group2expand[id],
+            row = $(data.link).closest('tr');
+
+        // replace group entry with all members returned by the server
+        if (data && data.adder && result.members && result.members.length) {
+            for (var i=0; i < result.members.length; i++) {
+                attendee = result.members[i];
+                attendee.role = data.data.role;
+                attendee.cutype = 'INDIVIDUAL';
+                attendee.status = 'NEEDS-ACTION';
+                data.adder(attendee, null, row);
+            }
+
+            if (data.remover) {
+                data.remover(data.link, id)
+            }
+            else {
+                row.remove();
+            }
+
+            delete this.group2expand[id];
+        }
+        else {
+            rcmail.display_message(result.error || rcmail.gettext('expandattendeegroupnodata','libcalendaring'), 'error');
+        }
+    };
+
+
+    // Render message reference links to the given container
+    this.render_message_links = function(links, container, edit, plugin)
+    {
+        var ul = $('<ul>').addClass('attachmentslist');
+
+        $.each(links, function(i, link) {
+            if (!link.mailurl)
+                return true;  // continue
+
+            var li = $('<li>').addClass('link')
+                .addClass('message eml')
+                .append($('<a>')
+                    .attr('href', link.mailurl)
+                    .addClass('messagelink')
+                    .text(link.subject || link.uri)
+                )
+                .appendTo(ul);
+
+            // add icon to remove the link
+            if (edit) {
+                $('<a>')
+                    .attr('href', '#delete')
+                    .attr('title', rcmail.gettext('removelink', plugin))
+                    .attr('data-uri', link.uri)
+                    .addClass('delete')
+                    .text(rcmail.gettext('delete'))
+                    .appendTo(li);
+            }
+        });
+
+        container.empty().append(ul);
+    }
+
+    // resize and reposition (center) the dialog window
+    this.dialog_resize = function(id, height, width)
+    {
+        var win = $(window), w = win.width(), h = win.height();
+
+        $(id).dialog('option', {
+            height: Math.min(h-20, height+130),
+            width: Math.min(w-20, width+50)
+        });
+    };
+}
+
+//////  static methods
+
+// render HTML code for displaying an attendee record
+rcube_libcalendaring.attendee_html = function(data)
+{
+    var name, tooltip = '', context = 'libcalendaring',
+        dispname = data.name || data.email,
+        status = data.role == 'ORGANIZER' ? 'ORGANIZER' : data.status;
+
+    if (status)
+        status = status.toLowerCase();
+
+    if (data.email) {
+        tooltip = data.email;
+        name = $('<a>').attr({href: 'mailto:' + data.email, 'class': 'mailtolink', 'data-cutype': data.cutype})
+
+        if (status)
+            tooltip += ' (' + rcmail.gettext('status' + status, context) + ')';
+    }
+    else {
+        name = $('<span>');
+    }
+
+    if (data['delegated-to'])
+        tooltip = rcmail.gettext('delegatedto', context) + ' ' + data['delegated-to'];
+    else if (data['delegated-from'])
+        tooltip = rcmail.gettext('delegatedfrom', context) + ' ' + data['delegated-from'];
+
+    return $('<span>').append(
+            $('<span>').attr({'class': 'attendee ' + status, title: tooltip}).append(name.text(dispname))
+        ).html();
+};
+
+/**
+ *
+ */
+rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id)
+{
+    // ask user to delete the declined event from the local calendar (#1670)
+    var del = false;
+    if (rcmail.env.rsvp_saved && status == 'declined') {
+        del = confirm(rcmail.gettext('itip.declinedeleteconfirm'));
+    }
+
+    // open dialog for iTip delegation
+    if (status == 'delegated') {
+        rcube_libcalendaring.itip_delegate_dialog(function(data) {
+            rcmail.http_post(task + '/itip-delegate', {
+                _uid: rcmail.env.uid,
+                _mbox: rcmail.env.mailbox,
+                _part: mime_id,
+                _to: data.to,
+                _rsvp: data.rsvp ? 1 : 0,
+                _comment: data.comment,
+                _folder: data.target
+            }, rcmail.set_busy(true, 'itip.savingdata'));
+        }, $('#rsvp-'+dom_id+' .folder-select'));
+        return false;
+    }
+
+    var noreply = 0, comment = '';
+    if (dom_id) {
+      noreply = $('#noreply-'+dom_id+':checked').length ? 1 : 0;
+      if (!noreply)
+        comment = $('#reply-comment-'+dom_id).val();
+    }
+
+    rcmail.http_post(task + '/mailimportitip', {
+        _uid: rcmail.env.uid,
+        _mbox: rcmail.env.mailbox,
+        _part: mime_id,
+        _folder: $('#itip-saveto').val(),
+        _status: status,
+        _del: del?1:0,
+        _noreply: noreply,
+        _comment: comment
+      }, rcmail.set_busy(true, 'itip.savingdata'));
+
+    return false;
+};
+
+/**
+ * Helper function to render the iTip delegation dialog
+ * and trigger a callback function when submitted.
+ */
+rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
+{
+    // show dialog for entering the delegatee address and comment
+    var html = '<form class="itip-dialog-form" action="javascript:void()">' +
+        '<div class="form-section">' +
+            '<label for="itip-delegate-to">' + rcmail.gettext('itip.delegateto') + '</label><br/>' +
+            '<input type="text" id="itip-delegate-to" class="text" size="40" value="" />' +
+        '</div>' +
+        '<div class="form-section">' +
+            '<label for="itip-delegate-rsvp">' +
+                '<input type="checkbox" id="itip-delegate-rsvp" class="checkbox" size="40" value="" />' +
+                rcmail.gettext('itip.delegatersvpme') +
+            '</label>' +
+        '</div>' +
+        '<div class="form-section">' +
+            '<textarea id="itip-delegate-comment" class="itip-comment" cols="40" rows="8" placeholder="' +
+                rcmail.gettext('itip.itipcomment') + '"></textarea>' + 
+        '</div>' +
+        '<div class="form-section">' +
+            (selector && selector.length ? selector.html() : '') +
+        '</div>' +
+    '</form>';
+
+    var dialog, buttons = [];
+    buttons.push({
+        text: rcmail.gettext('itipdelegated', 'itip'),
+        click: function() {
+            var doc = window.parent.document,
+                delegatee = String($('#itip-delegate-to', doc).val()).replace(/(^\s+)|(\s+$)/, '');
+
+            if (delegatee != '' && rcube_check_email(delegatee, true)) {
+                callback({
+                    to: delegatee,
+                    rsvp: $('#itip-delegate-rsvp', doc).prop('checked'),
+                    comment: $('#itip-delegate-comment', doc).val(),
+                    target: $('#itip-saveto', doc).val()
+                });
+
+                setTimeout(function() { dialog.dialog("close"); }, 500);
+            }
+            else {
+                alert(rcmail.gettext('itip.delegateinvalidaddress'));
+                $('#itip-delegate-to', doc).focus();
+            }
+        }
+    });
+
+    buttons.push({
+        text: rcmail.gettext('cancel', 'itip'),
+        click: function() {
+            dialog.dialog('close');
+        }
+    });
+
+    dialog = rcmail.show_popup_dialog(html, rcmail.gettext('delegateinvitation', 'itip'), buttons, {
+        width: 460,
+        open: function(event, ui) {
+            $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
+            $(this).find('#itip-saveto').val('');
+
+            // initialize autocompletion
+            var ac_props, rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
+            if (rcmail.env.autocomplete_threads > 0) {
+                ac_props = {
+                    threads: rcmail.env.autocomplete_threads,
+                    sources: rcmail.env.autocomplete_sources
+                };
+            }
+            rcm.init_address_input_events($(this).find('#itip-delegate-to').focus(), ac_props);
+            rcm.env.recipients_delimiter = '';
+        },
+        close: function(event, ui) {
+            rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
+            rcm.ksearch_blur();
+            $(this).remove();
+        }
+    });
+
+    return dialog;
+};
+
+/**
+ * Show a menu for selecting the RSVP reply mode
+ */
+rcube_libcalendaring.itip_rsvp_recurring = function(btn, callback)
+{
+    var mnu = $('<ul></ul>').addClass('popupmenu libcal-rsvp-replymode');
+
+    $.each(['all','current'/*,'future'*/], function(i, mode) {
+        $('<li><a>' + rcmail.get_label('rsvpmode'+mode, 'libcalendaring') + '</a>')
+        .addClass('ui-menu-item')
+        .attr('rel', mode)
+        .appendTo(mnu);
+    });
+
+    var action = btn.attr('rel');
+
+    // open the mennu
+    mnu.menu({
+        select: function(event, ui) {
+            callback(action, ui.item.attr('rel'));
+        }
+    })
+    .appendTo(document.body)
+    .position({ my: 'left top', at: 'left bottom+2', of: btn })
+    .data('action', action);
+
+    setTimeout(function() {
+        $(document).one('click', function() {
+            mnu.menu('destroy');
+            mnu.remove();
+        });
+    }, 100);
+};
+
+/**
+ *
+ */
+rcube_libcalendaring.remove_from_itip = function(event, task, title)
+{
+    if (confirm(rcmail.gettext('itip.deleteobjectconfirm').replace('$title', title))) {
+        rcmail.http_post(task + '/itip-remove',
+            event,
+            rcmail.set_busy(true, 'itip.savingdata')
+        );
+    }
+};
+
+/**
+ *
+ */
+rcube_libcalendaring.decline_attendee_reply = function(mime_id, task)
+{
+    // show dialog for entering a comment and send to server
+    var html = '<div class="itip-dialog-confirm-text">' + rcmail.gettext('itip.declineattendeeconfirm') + '</div>' +
+        '<textarea id="itip-decline-comment" class="itip-comment" cols="40" rows="8"></textarea>';
+
+    var dialog, buttons = [];
+    buttons.push({
+        text: rcmail.gettext('declineattendee', 'itip'),
+        click: function() {
+            rcmail.http_post(task + '/itip-decline-reply', {
+                _uid: rcmail.env.uid,
+                _mbox: rcmail.env.mailbox,
+                _part: mime_id,
+                _comment: $('#itip-decline-comment', window.parent.document).val()
+            }, rcmail.set_busy(true, 'itip.savingdata'));
+          dialog.dialog("close");
+        }
+    });
+
+    buttons.push({
+        text: rcmail.gettext('cancel', 'itip'),
+        click: function() {
+          dialog.dialog('close');
+        }
+    });
+
+    dialog = rcmail.show_popup_dialog(html, rcmail.gettext('declineattendee', 'itip'), buttons, {
+        width: 460,
+        open: function() {
+            $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
+            $('#itip-decline-comment').focus();
+        }
+    });
+
+    return false;
+};
+
+/**
+ *
+ */
+rcube_libcalendaring.fetch_itip_object_status = function(p)
+{
+  rcmail.http_post(p.task + '/itip-status', { data: p });
+};
+
+/**
+ *
+ */
+rcube_libcalendaring.update_itip_object_status = function(p)
+{
+  rcmail.env.rsvp_saved = p.saved;
+  rcmail.env.itip_existing = p.existing;
+
+  // hide all elements first
+  $('#itip-buttons-'+p.id+' > div').hide();
+  $('#rsvp-'+p.id+' .folder-select').remove();
+
+  if (p.html) {
+    // append/replace rsvp status display
+    $('#loading-'+p.id).next('.rsvp-status').remove();
+    $('#loading-'+p.id).hide().after(p.html);
+  }
+
+  // enable/disable rsvp buttons
+  if (p.action == 'rsvp') {
+    $('#rsvp-'+p.id+' input.button').prop('disabled', false)
+      .filter('.'+String(p.status||'unknown').toLowerCase()).prop('disabled', p.latest);
+  }
+
+  // show rsvp/import buttons (with calendar selector)
+  $('#'+p.action+'-'+p.id).show().find('input.button').last().after(p.select);
+
+  // highlight date if date change detected
+  if (p.resheduled)
+    $('.calendar-eventdetails td.date').addClass('modified');
+
+  // show itip box appendix after replacing the given placeholders
+  if (p.append && p.append.selector) {
+    var elem = $(p.append.selector);
+    if (p.append.replacements) {
+      $.each(p.append.replacements, function(k, html) {
+        elem.html(elem.html().replace(k, html));
+      });
+    }
+    else if (p.append.html) {
+      elem.html(p.append.html)
+    }
+    elem.show();
+  }
+};
+
+/**
+ * Callback from server after an iTip message has been processed
+ */
+rcube_libcalendaring.itip_message_processed = function(metadata)
+{
+  if (metadata.after_action) {
+    setTimeout(function(){ rcube_libcalendaring.itip_after_action(metadata.after_action); }, 1200);
+  }
+  else {
+    rcube_libcalendaring.fetch_itip_object_status(metadata);
+  }
+};
+
+/**
+ * After-action on iTip request message. Action types:
+ *     0 - no action
+ *     1 - move to Trash
+ *     2 - delete the message
+ *     3 - flag as deleted
+ *     folder_name - move the message to the specified folder
+ */
+rcube_libcalendaring.itip_after_action = function(action)
+{
+  if (!action) {
+    return;
+  }
+
+  var rc = rcmail.is_framed() ? parent.rcmail : rcmail;
+
+  if (action === 2) {
+    rc.permanently_remove_messages();
+  }
+  else if (action === 3) {
+    rc.mark_message('delete');
+  }
+  else {
+    rc.move_messages(action === 1 ? rc.env.trash_mailbox : action);
+  }
+};
+
+/**
+ * Open the calendar preview for the current iTip event
+ */
+rcube_libcalendaring.open_itip_preview = function(url, msgref)
+{
+  if (!rcmail.env.itip_existing)
+    url += '&itip=' + escape(msgref);
+
+  var win = rcmail.open_window(url);
+};
+
+
+// extend jQuery
+(function($){
+  $.fn.serializeJSON = function(){
+    var json = {};
+    jQuery.map($(this).serializeArray(), function(n, i) {
+      json[n['name']] = n['value'];
+    });
+    return json;
+  };
+})(jQuery);
+
+
+/* libcalendaring plugin initialization */
+window.rcmail && rcmail.addEventListener('init', function(evt) {
+  if (rcmail.env.libcal_settings) {
+    var libcal = new rcube_libcalendaring(rcmail.env.libcal_settings);
+    rcmail.addEventListener('plugin.display_alarms', function(alarms){ libcal.display_alarms(alarms); });
+  }
+
+  rcmail.addEventListener('plugin.update_itip_object_status', rcube_libcalendaring.update_itip_object_status)
+    .addEventListener('plugin.fetch_itip_object_status', rcube_libcalendaring.fetch_itip_object_status)
+    .addEventListener('plugin.itip_message_processed', rcube_libcalendaring.itip_message_processed);
+
+  if (rcmail.env.action == 'get-attachment' && rcmail.gui_objects['attachmentframe']) {
+    rcmail.register_command('print-attachment', function() {
+      var frame = rcmail.get_frame_window(rcmail.gui_objects['attachmentframe'].id);
+      if (frame) frame.print();
+    }, true);
+  }
+
+  if (rcmail.env.action == 'get-attachment' && rcmail.env.attachment_download_url) {
+    rcmail.register_command('download-attachment', function() {
+      rcmail.location_href(rcmail.env.attachment_download_url, window);
+    }, true);
+  }
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/libcalendaring.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,1762 @@
+<?php
+
+/**
+ * Library providing common functions for calendaring plugins
+ *
+ * Provides utility functions for calendar-related modules such as
+ * - alarms display and dismissal
+ * - attachment handling
+ * - recurrence computation and UI elements
+ * - ical parsing and exporting
+ * - itip scheduling protocol
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class libcalendaring extends rcube_plugin
+{
+    public $rc;
+    public $timezone;
+    public $gmt_offset;
+    public $dst_active;
+    public $timezone_offset;
+    public $ical_parts = array();
+    public $ical_message;
+
+    public $defaults = array(
+      'calendar_date_format'  => "yyyy-MM-dd",
+      'calendar_date_short'   => "M-d",
+      'calendar_date_long'    => "MMM d yyyy",
+      'calendar_date_agenda'  => "ddd MM-dd",
+      'calendar_time_format'  => "HH:mm",
+      'calendar_first_day'    => 1,
+      'calendar_first_hour'   => 6,
+      'calendar_date_format_sets' => array(
+        'yyyy-MM-dd' => array('MMM d yyyy',   'M-d',  'ddd MM-dd'),
+        'dd-MM-yyyy' => array('d MMM yyyy',   'd-M',  'ddd dd-MM'),
+        'yyyy/MM/dd' => array('MMM d yyyy',   'M/d',  'ddd MM/dd'),
+        'MM/dd/yyyy' => array('MMM d yyyy',   'M/d',  'ddd MM/dd'),
+        'dd/MM/yyyy' => array('d MMM yyyy',   'd/M',  'ddd dd/MM'),
+        'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M',  'ddd dd.MM.'),
+        'd.M.yyyy'   => array('d. MMM yyyy',  'd.M',  'ddd d.MM.'),
+      ),
+    );
+
+    private static $instance;
+
+    private $mail_ical_parser;
+
+    /**
+     * Singleton getter to allow direct access from other plugins
+     */
+    public static function get_instance()
+    {
+        if (!self::$instance) {
+            self::$instance = new libcalendaring(rcube::get_instance()->plugins);
+            self::$instance->init_instance();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Initializes class properties
+     */
+    public function init_instance()
+    {
+        $this->rc = rcube::get_instance();
+
+        // set user's timezone
+        try {
+            $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
+        }
+        catch (Exception $e) {
+            $this->timezone = new DateTimeZone('GMT');
+        }
+
+        $now = new DateTime('now', $this->timezone);
+
+        $this->gmt_offset      = $now->getOffset();
+        $this->dst_active      = $now->format('I');
+        $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
+
+        $this->add_texts('localization/', false);
+    }
+
+    /**
+     * Required plugin startup method
+     */
+    public function init()
+    {
+        self::$instance = $this;
+
+        $this->rc = rcube::get_instance();
+        $this->init_instance();
+
+        // include client scripts and styles
+        if ($this->rc->output) {
+            // add hook to display alarms
+            $this->add_hook('refresh', array($this, 'refresh'));
+            $this->register_action('plugin.alarms', array($this, 'alarms_action'));
+            $this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group'));
+        }
+
+        // proceed initialization in startup hook
+        $this->add_hook('startup', array($this, 'startup'));
+    }
+
+    /**
+     * Startup hook
+     */
+    public function startup($args)
+    {
+        if ($this->rc->output && $this->rc->output->type == 'html') {
+            $this->rc->output->set_env('libcal_settings', $this->load_settings());
+            $this->include_script('libcalendaring.js');
+            $this->include_stylesheet($this->local_skin_path() . '/libcal.css');
+
+            $this->add_label(
+                'itipaccepted', 'itiptentative', 'itipdeclined',
+                'itipdelegated', 'expandattendeegroup', 'expandattendeegroupnodata',
+                'statusorganizer', 'statusaccepted', 'statusdeclined',
+                'statusdelegated', 'statusunknown', 'statusneeds-action',
+                'statustentative', 'statuscompleted', 'statusin-process',
+                'delegatedto', 'delegatedfrom'
+            );
+        }
+
+        if ($args['task'] == 'mail') {
+            if ($args['action'] == 'show' || $args['action'] == 'preview') {
+                $this->add_hook('message_load', array($this, 'mail_message_load'));
+            }
+        }
+    }
+
+    /**
+     * Load iCalendar functions
+     */
+    public static function get_ical()
+    {
+        $self = self::get_instance();
+        require_once __DIR__ . '/libvcalendar.php';
+        return new libvcalendar();
+    }
+
+    /**
+     * Load iTip functions
+     */
+    public static function get_itip($domain = 'libcalendaring')
+    {
+        $self = self::get_instance();
+        require_once __DIR__ . '/lib/libcalendaring_itip.php';
+        return new libcalendaring_itip($self, $domain);
+    }
+
+    /**
+     * Load recurrence computation engine
+     */
+    public static function get_recurrence()
+    {
+        $self = self::get_instance();
+        require_once __DIR__ . '/lib/libcalendaring_recurrence.php';
+        return new libcalendaring_recurrence($self);
+    }
+
+    /**
+     * Shift dates into user's current timezone
+     *
+     * @param mixed Any kind of a date representation (DateTime object, string or unix timestamp)
+     * @return object DateTime object in user's timezone
+     */
+    public function adjust_timezone($dt, $dateonly = false)
+    {
+        if (is_numeric($dt))
+            $dt = new DateTime('@'.$dt);
+        else if (is_string($dt))
+            $dt = rcube_utils::anytodatetime($dt);
+
+        if ($dt instanceof DateTime && !($dt->_dateonly || $dateonly)) {
+            $dt->setTimezone($this->timezone);
+        }
+
+        return $dt;
+    }
+
+
+    /**
+     *
+     */
+    public function load_settings()
+    {
+        $this->date_format_defaults();
+
+        $settings = array();
+        $keys     = array('date_format', 'time_format', 'date_short', 'date_long');
+
+        foreach ($keys as $key) {
+            $settings[$key] = (string)$this->rc->config->get('calendar_' . $key, $this->defaults['calendar_' . $key]);
+            $settings[$key] = str_replace('Y', 'y', $settings[$key]);
+        }
+
+        $settings['dates_long']  = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '&mdash;' " . $settings['date_long'] . '}';
+        $settings['first_day']   = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
+        $settings['timezone']    = $this->timezone_offset;
+        $settings['dst']         = $this->dst_active;
+
+        // localization
+        $settings['days'] = array(
+            $this->rc->gettext('sunday'),   $this->rc->gettext('monday'),
+            $this->rc->gettext('tuesday'),  $this->rc->gettext('wednesday'),
+            $this->rc->gettext('thursday'), $this->rc->gettext('friday'),
+            $this->rc->gettext('saturday')
+        );
+        $settings['days_short'] = array(
+            $this->rc->gettext('sun'), $this->rc->gettext('mon'),
+            $this->rc->gettext('tue'), $this->rc->gettext('wed'),
+            $this->rc->gettext('thu'), $this->rc->gettext('fri'),
+            $this->rc->gettext('sat')
+        );
+        $settings['months'] = array(
+            $this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
+            $this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
+            $this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
+            $this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
+            $this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
+            $this->rc->gettext('longnov'), $this->rc->gettext('longdec')
+        );
+        $settings['months_short'] = array(
+            $this->rc->gettext('jan'), $this->rc->gettext('feb'),
+            $this->rc->gettext('mar'), $this->rc->gettext('apr'),
+            $this->rc->gettext('may'), $this->rc->gettext('jun'),
+            $this->rc->gettext('jul'), $this->rc->gettext('aug'),
+            $this->rc->gettext('sep'), $this->rc->gettext('oct'),
+            $this->rc->gettext('nov'), $this->rc->gettext('dec')
+        );
+        $settings['today'] = $this->rc->gettext('today');
+
+        // define list of file types which can be displayed inline
+        // same as in program/steps/mail/show.inc
+        $settings['mimetypes'] = (array)$this->rc->config->get('client_mimetypes');
+
+        return $settings;
+    }
+
+
+    /**
+     * Helper function to set date/time format according to config and user preferences
+     */
+    private function date_format_defaults()
+    {
+        static $defaults = array();
+
+        // nothing to be done
+        if (isset($defaults['date_format']))
+          return;
+
+        $defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format')));
+        $defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format')));
+
+        // override defaults
+        if ($defaults['date_format'])
+            $this->defaults['calendar_date_format'] = $defaults['date_format'];
+        if ($defaults['time_format'])
+            $this->defaults['calendar_time_format'] = $defaults['time_format'];
+
+        // derive format variants from basic date format
+        $format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
+        if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
+            $this->defaults['calendar_date_long'] = $format_set[0];
+            $this->defaults['calendar_date_short'] = $format_set[1];
+            $this->defaults['calendar_date_agenda'] = $format_set[2];
+        }
+    }
+
+    /**
+     * Compose a date string for the given event
+     */
+    public function event_date_text($event, $tzinfo = false)
+    {
+        $fromto = '--';
+
+        // handle task objects
+        if ($event['_type'] == 'task' && is_object($event['due'])) {
+            $date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
+            $fromto = $this->rc->format_date($event['due'], $date_format, false);
+
+            // add timezone information
+            if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) {
+                $fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
+            }
+
+            return $fromto;
+        }
+
+        // abort if no valid event dates are given
+        if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
+            return $fromto;
+        }
+
+        $duration = $event['start']->diff($event['end'])->format('s');
+
+        $this->date_format_defaults();
+        $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
+        $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
+
+        if ($event['allday']) {
+            $fromto = $this->rc->format_date($event['start'], $date_format);
+            if (($todate = $this->rc->format_date($event['end'], $date_format)) != $fromto)
+                $fromto .= ' - ' . $todate;
+        }
+        else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) {
+            $fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) .
+                ' - ' . $this->rc->format_date($event['end'], $time_format);
+        }
+        else {
+            $fromto = $this->rc->format_date($event['start'], $date_format) . ' ' . $this->rc->format_date($event['start'], $time_format) .
+                ' - ' . $this->rc->format_date($event['end'], $date_format) . ' ' . $this->rc->format_date($event['end'], $time_format);
+        }
+
+        // add timezone information
+        if ($tzinfo && ($tzname = $this->timezone->getName())) {
+            $fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
+        }
+
+        return $fromto;
+    }
+
+
+    /**
+     * Render HTML form for alarm configuration
+     */
+    public function alarm_select($attrib, $alarm_types, $absolute_time = true)
+    {
+        unset($attrib['name']);
+
+        $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
+        $input_date  = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
+        $input_time  = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
+        $select_type    = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type', 'id' => $attrib['id']));
+        $select_offset  = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
+        $select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related'));
+        $object_type    = $attrib['_type'] ?: 'event';
+
+        $select_type->add($this->gettext('none'), '');
+        foreach ($alarm_types as $type)
+            $select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
+
+        foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
+            $select_offset->add($this->gettext('trigger' . $trigger), $trigger);
+
+        $select_offset->add($this->gettext('trigger0'), '0');
+        if ($absolute_time)
+            $select_offset->add($this->gettext('trigger@'), '@');
+
+        $select_related->add($this->gettext('relatedstart'), 'start');
+        $select_related->add($this->gettext('relatedend' . $object_type), 'end');
+
+        // pre-set with default values from user settings
+        $preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
+        $hidden = array('style' => 'display:none');
+        $html = html::span('edit-alarm-set',
+            $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
+            html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
+                $input_value->show($preset[0]) . ' ' .
+                $select_offset->show($preset[1]) . ' ' .
+                $select_related->show() . ' ' .
+                $input_date->show('', $hidden) . ' ' .
+                $input_time->show('', $hidden)
+            )
+        );
+
+        // TODO: support adding more alarms
+        #$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->gettext('addalarm')),
+        #  $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
+
+        return $html;
+    }
+
+    /**
+     * Get a list of email addresses of the given user (from login and identities)
+     *
+     * @param string User Email (default to current user)
+     * @return array Email addresses related to the user
+     */
+    public function get_user_emails($user = null)
+    {
+        static $_emails = array();
+
+        if (empty($user)) {
+            $user = $this->rc->user->get_username();
+        }
+
+        // return cached result
+        if (is_array($_emails[$user])) {
+            return $_emails[$user];
+        }
+
+        $emails = array($user);
+        $plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
+        $emails = array_map('strtolower', $plugin['emails']);
+
+        // add all emails from the current user's identities
+        if (!$plugin['abort'] && ($user == $this->rc->user->get_username())) {
+            foreach ($this->rc->user->list_emails() as $identity) {
+                $emails[] = strtolower($identity['email']);
+            }
+        }
+
+        $_emails[$user] = array_unique($emails);
+        return $_emails[$user];
+    }
+
+    /**
+     * Set the given participant status to the attendee matching the current user's identities
+     *
+     * @param array   Hash array with event struct
+     * @param string  The PARTSTAT value to set
+     * @return mixed  Email address of the updated attendee or False if none matching found
+     */
+    public function set_partstat(&$event, $status, $recursive = true)
+    {
+        $success = false;
+        $emails = $this->get_user_emails();
+        foreach ((array)$event['attendees'] as $i => $attendee) {
+            if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+                $event['attendees'][$i]['status'] = strtoupper($status);
+                $success = $attendee['email'];
+            }
+        }
+
+        // apply partstat update to each existing exception
+        if ($event['recurrence'] && is_array($event['recurrence']['EXCEPTIONS'])) {
+            foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
+                $this->set_partstat($event['recurrence']['EXCEPTIONS'][$i], $status, false);
+            }
+
+            // set link to top-level exceptions
+            $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
+        }
+
+        return $success;
+    }
+
+
+    /*********  Alarms handling  *********/
+
+    /**
+     * Helper function to convert alarm trigger strings
+     * into two-field values (e.g. "-45M" => 45, "-M")
+     */
+    public static function parse_alarm_value($val)
+    {
+        if ($val[0] == '@') {
+            return array(new DateTime($val));
+        }
+        else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) {
+            if ($m[1] == '')
+                $m[1] = '+';
+            foreach ($m2 as $seg) {
+                $prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT';
+                if ($seg[1] > 0) {  // ignore zero values
+                    // convert seconds to minutes
+                    if ($seg[2] == 'S') {
+                        $seg[2] = 'M';
+                        $seg[1] = max(1, round($seg[1]/60));
+                    }
+
+                    return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
+                }
+            }
+
+            // return zero value nevertheless
+            return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
+        }
+
+        return false;
+    }
+
+    /**
+     * Convert the alarms list items to be processed on the client
+     */
+    public static function to_client_alarms($valarms)
+    {
+        return array_map(function($alarm){
+            if ($alarm['trigger'] instanceof DateTime) {
+                $alarm['trigger'] = '@' . $alarm['trigger']->format('U');
+            }
+            else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
+                $alarm['trigger'] = $trigger[2];
+            }
+            return $alarm;
+        }, (array)$valarms);
+    }
+
+    /**
+     * Process the alarms values submitted by the client
+     */
+    public static function from_client_alarms($valarms)
+    {
+        return array_map(function($alarm){
+            if ($alarm['trigger'][0] == '@') {
+                try {
+                    $alarm['trigger'] = new DateTime($alarm['trigger']);
+                    $alarm['trigger']->setTimezone(new DateTimeZone('UTC'));
+                }
+                catch (Exception $e) { /* handle this ? */ }
+            }
+            else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
+                $alarm['trigger'] = $trigger[3];
+            }
+            return $alarm;
+        }, (array)$valarms);
+    }
+
+    /**
+     * Render localized text for alarm settings
+     */
+    public static function alarms_text($alarms)
+    {
+        if (is_array($alarms) && is_array($alarms[0])) {
+            $texts = array();
+            foreach ($alarms as $alarm) {
+                if ($text = self::alarm_text($alarm))
+                    $texts[] = $text;
+            }
+
+            return join(', ', $texts);
+        }
+        else {
+            return self::alarm_text($alarms);
+        }
+    }
+
+    /**
+     * Render localized text for a single alarm property
+     */
+    public static function alarm_text($alarm)
+    {
+        if (is_string($alarm)) {
+            list($trigger, $action) = explode(':', $alarm);
+        }
+        else {
+            $trigger = $alarm['trigger'];
+            $action  = $alarm['action'];
+            $related = $alarm['related'];
+        }
+
+        $text  = '';
+        $rcube = rcube::get_instance();
+
+        switch ($action) {
+        case 'EMAIL':
+            $text = $rcube->gettext('libcalendaring.alarmemail');
+            break;
+        case 'DISPLAY':
+            $text = $rcube->gettext('libcalendaring.alarmdisplay');
+            break;
+        case 'AUDIO':
+            $text = $rcube->gettext('libcalendaring.alarmaudio');
+            break;
+        }
+
+        if ($trigger instanceof DateTime) {
+            $text .= ' ' . $rcube->gettext(array(
+                'name' => 'libcalendaring.alarmat',
+                'vars' => array('datetime' => $rcube->format_date($trigger))
+            ));
+        }
+        else if (preg_match('/@(\d+)/', $trigger, $m)) {
+            $text .= ' ' . $rcube->gettext(array(
+                'name' => 'libcalendaring.alarmat',
+                'vars' => array('datetime' => $rcube->format_date($m[1]))
+            ));
+        }
+        else if ($val = self::parse_alarm_value($trigger)) {
+            $r = strtoupper($related ?: 'start') == 'END' ? 'end' : '';
+            // TODO: for all-day events say 'on date of event at XX' ?
+            if ($val[0] == 0) {
+                $text .= ' ' . $rcube->gettext('libcalendaring.triggerattime' . $r);
+            }
+            else {
+                $label = 'libcalendaring.trigger' . $r . $val[1];
+                $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext($label);
+            }
+        }
+        else {
+            return false;
+        }
+
+        return $text;
+    }
+
+    /**
+     * Get the next alarm (time & action) for the given event
+     *
+     * @param array Record data
+     * @return array Hash array with alarm time/type or null if no alarms are configured
+     */
+    public static function get_next_alarm($rec, $type = 'event')
+    {
+        if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED')
+            return null;
+
+        if ($type == 'task') {
+            $timezone = self::get_instance()->timezone;
+            if ($rec['startdate'])
+                $rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone);
+            if ($rec['date'])
+                $rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone);
+        }
+
+        if (!$rec['end'])
+            $rec['end'] = $rec['start'];
+
+        // support legacy format
+        if (!$rec['valarms']) {
+            list($trigger, $action) = explode(':', $rec['alarms'], 2);
+            if ($alarm = self::parse_alarm_value($trigger)) {
+                $rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0]));
+            }
+        }
+
+        $expires  = new DateTime('now - 12 hours');
+        $alarm_id = $rec['id'];  // alarm ID eq. record ID by default to keep backwards compatibility
+
+        // handle multiple alarms
+        $notify_at = null;
+        foreach ($rec['valarms'] as $alarm) {
+            $notify_time = null;
+
+            if ($alarm['trigger'] instanceof DateTime) {
+                $notify_time = $alarm['trigger'];
+            }
+            else if (is_string($alarm['trigger'])) {
+                $refdate = $alarm['related'] == 'END' ? $rec['end'] : $rec['start'];
+
+                // abort if no reference date is available to compute notification time
+                if (!is_a($refdate, 'DateTime'))
+                    continue;
+
+                // TODO: for all-day events, take start @ 00:00 as reference date ?
+
+                try {
+                    $interval = new DateInterval(trim($alarm['trigger'], '+-'));
+                    $interval->invert = $alarm['trigger'][0] == '-';
+                    $notify_time = clone $refdate;
+                    $notify_time->add($interval);
+                }
+                catch (Exception $e) {
+                    rcube::raise_error($e, true);
+                    continue;
+                }
+            }
+
+            if ($notify_time && (!$notify_at || ($notify_time > $notify_at && $notify_time > $expires))) {
+                $notify_at  = $notify_time;
+                $action     = $alarm['action'];
+                $alarm_prop = $alarm;
+
+                // generate a unique alarm ID if multiple alarms are set
+                if (count($rec['valarms']) > 1) {
+                    $alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis');
+                }
+            }
+        }
+
+        return !$notify_at ? null : array(
+            'time'   => $notify_at->format('U'),
+            'action' => $action ? strtoupper($action) : 'DISPLAY',
+            'id'     => $alarm_id,
+            'prop'   => $alarm_prop,
+        );
+    }
+
+    /**
+     * Handler for keep-alive requests
+     * This will check for pending notifications and pass them to the client
+     */
+    public function refresh($attr)
+    {
+        // collect pending alarms from all providers (e.g. calendar, tasks)
+        $plugin = $this->rc->plugins->exec_hook('pending_alarms', array(
+            'time' => time(),
+            'alarms' => array(),
+        ));
+
+        if (!$plugin['abort'] && !empty($plugin['alarms'])) {
+            // make sure texts and env vars are available on client
+            $this->add_texts('localization/', true);
+            $this->rc->output->add_label('close');
+            $this->rc->output->set_env('snooze_select', $this->snooze_select());
+            $this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms']));
+        }
+    }
+
+    /**
+     * Handler for alarm dismiss/snooze requests
+     */
+    public function alarms_action()
+    {
+//        $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
+        $data  = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
+
+        $data['ids'] = explode(',', $data['id']);
+        $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data);
+
+        if ($plugin['success'])
+            $this->rc->output->show_message('successfullysaved', 'confirmation');
+        else
+            $this->rc->output->show_message('calendar.errorsaving', 'error');
+    }
+
+    /**
+     * Generate reduced and streamlined output for pending alarms
+     */
+    private function _alarms_output($alarms)
+    {
+        $out = array();
+        foreach ($alarms as $alarm) {
+            $out[] = array(
+                'id'       => $alarm['id'],
+                'start'    => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '',
+                'end'      => $alarm['end']   ? $this->adjust_timezone($alarm['end'])->format('c') : '',
+                'allDay'   => $alarm['allday'] == 1,
+                'action'   => $alarm['action'],
+                'title'    => $alarm['title'],
+                'location' => $alarm['location'],
+                'calendar' => $alarm['calendar'],
+            );
+        }
+
+        return $out;
+    }
+
+    /**
+     * Render a dropdown menu to choose snooze time
+     */
+    private function snooze_select($attrib = array())
+    {
+        $steps = array(
+             5 => 'repeatinmin',
+            10 => 'repeatinmin',
+            15 => 'repeatinmin',
+            20 => 'repeatinmin',
+            30 => 'repeatinmin',
+            60 => 'repeatinhr',
+            120 => 'repeatinhrs',
+            1440 => 'repeattomorrow',
+            10080 => 'repeatinweek',
+        );
+
+        $items = array();
+        foreach ($steps as $n => $label) {
+            $items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
+                $this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
+        }
+
+        return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib);
+    }
+
+
+    /*********  Recurrence rules handling ********/
+
+    /**
+     * Render localized text describing the recurrence rule of an event
+     */
+    public function recurrence_text($rrule)
+    {
+        $limit     = 10;
+        $exdates   = array();
+        $format    = $this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
+        $format    = self::to_php_date_format($format);
+        $format_fn = function($dt) use ($format) {
+            return rcmail::get_instance()->format_date($dt, $format);
+        };
+
+        if (is_array($rrule['EXDATE']) && !empty($rrule['EXDATE'])) {
+            $exdates = array_map($format_fn, $rrule['EXDATE']);
+        }
+
+        if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
+            $rdates = array_map($format_fn, $rrule['RDATE']);
+
+            if (!empty($exdates)) {
+                $rdates = array_diff($rdates, $exdates);
+            }
+
+            if (count($rdates) > $limit) {
+                $rdates = array_slice($rdates, 0, $limit);
+                $more   = true;
+            }
+
+            return $this->gettext('ondate') . ' ' . join(', ', $rdates)
+                . ($more ? '...' : '');
+        }
+
+        $output  = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL'] ?: 1);
+
+        switch ($rrule['FREQ']) {
+        case 'DAILY':
+            $output .= $this->gettext('days');
+            break;
+        case 'WEEKLY':
+            $output .= $this->gettext('weeks');
+            break;
+        case 'MONTHLY':
+            $output .= $this->gettext('months');
+            break;
+        case 'YEARLY':
+            $output .= $this->gettext('years');
+            break;
+        }
+
+        if ($rrule['COUNT']) {
+            $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
+        }
+        else if ($rrule['UNTIL']) {
+            $until = $this->gettext('recurrencend') . ' ' . $this->rc->format_date($rrule['UNTIL'], $format);
+        }
+        else {
+            $until = $this->gettext('forever');
+        }
+
+        $output .= ', ' . $until;
+
+        if (!empty($exdates)) {
+            if (count($exdates) > $limit) {
+                $exdates = array_slice($exdates, 0, $limit);
+                $more    = true;
+            }
+
+            $output  = '; ' . $this->gettext('except') . ' ' . join(', ', $exdates)
+                . ($more ? '...' : '');
+        }
+
+        return $output;
+    }
+
+    /**
+     * Generate the form for recurrence settings
+     */
+    public function recurrence_form($attrib = array())
+    {
+        switch ($attrib['part']) {
+            // frequency selector
+            case 'frequency':
+                $select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency'));
+                $select->add($this->gettext('never'),   '');
+                $select->add($this->gettext('daily'),   'DAILY');
+                $select->add($this->gettext('weekly'),  'WEEKLY');
+                $select->add($this->gettext('monthly'), 'MONTHLY');
+                $select->add($this->gettext('yearly'),  'YEARLY');
+                $select->add($this->gettext('rdate'),   'RDATE');
+                $html = html::label('edit-recurrence-frequency', $this->gettext('frequency')) . $select->show('');
+                break;
+
+            // daily recurrence
+            case 'daily':
+                $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily'));
+                $html = html::div($attrib, html::label('edit-recurrence-interval-daily', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('days')));
+                break;
+
+            // weekly recurrence form
+            case 'weekly':
+                $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly'));
+                $html = html::div($attrib, html::label('edit-recurrence-interval-weekly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('weeks')));
+                // weekday selection
+                $daymap = array('sun','mon','tue','wed','thu','fri','sat');
+                $checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
+                $first = $this->rc->config->get('calendar_first_day', 1);
+                for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
+                    $d = $j % 7;
+                    $weekdays .= html::label(array('class' => 'weekday'),
+                        $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) .
+                        $this->gettext($daymap[$d])
+                    ) . ' ';
+                }
+                $html .= html::div($attrib, html::label(null, $this->gettext('bydays')) . $weekdays);
+                break;
+
+            // monthly recurrence form
+            case 'monthly':
+                $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly'));
+                $html = html::div($attrib, html::label('edit-recurrence-interval-monthly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('months')));
+
+                $checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
+                for ($monthdays = '', $d = 1; $d <= 31; $d++) {
+                    $monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
+                    $monthdays .= $d % 7 ? ' ' : html::br();
+                }
+
+                // rule selectors
+                $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
+                $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
+                $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->gettext('each')));
+                $table->add(null, $monthdays);
+                $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->gettext('onevery')));
+                $table->add(null, $this->rrule_selectors($attrib['part']));
+
+                $html .= html::div($attrib, $table->show());
+                break;
+
+            // annually recurrence form
+            case 'yearly':
+                $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly'));
+                $html = html::div($attrib, html::label('edit-recurrence-interval-yearly', $this->gettext('every')) . $select->show(1) . html::span('label-after', $this->gettext('years')));
+                // month selector
+                $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
+                $checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
+                for ($months = '', $m = 1; $m <= 12; $m++) {
+                    $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->gettext($monthmap[$m]));
+                    $months .= $m % 4 ? ' ' : html::br();
+                }
+                $html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months);
+
+                // day rule selection
+                $html .= html::div($attrib, html::label(null, $this->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---'));
+                break;
+
+            // end of recurrence form
+            case 'until':
+                $radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
+                $select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times'));
+                $input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10"));
+
+                $html = html::div('line first',
+                    html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' .
+                        $this->gettext('forever'))
+                );
+
+                $forntimes = $this->gettext(array(
+                    'name' => 'forntimes',
+                    'vars' => array('nr' => '%s'))
+                );
+                $html .= html::div('line',
+                    $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count', 'aria-label' => sprintf($forntimes, 'N'))) . ' ' .
+                        sprintf($forntimes, $select->show(1))
+                );
+
+                $html .= html::div('line',
+                    $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate'))) . ' ' .
+                        $this->gettext('untildate') . ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate')))
+                );
+
+                $html = html::div($attrib, html::label(null, ucfirst($this->gettext('recurrencend'))) . $html);
+                break;
+
+            case 'rdate':
+                $ul = html::tag('ul', array('id' => 'edit-recurrence-rdates'), '');
+                $input = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10"));
+                $button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->gettext('addrdate')));
+                $html .= html::div($attrib, $ul . html::div('inputform', $input->show() . $button->show()));
+                break;
+        }
+
+        return $html;
+    }
+
+    /**
+     * Input field for interval selection
+     */
+    private function interval_selector($attrib)
+    {
+        $select = new html_select($attrib);
+        $select->add(range(1,30), range(1,30));
+        return $select;
+    }
+
+    /**
+     * Drop-down menus for recurrence rules like "each last sunday of"
+     */
+    private function rrule_selectors($part, $noselect = null)
+    {
+        // rule selectors
+        $select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix"));
+        if ($noselect) $select_prefix->add($noselect, '');
+        $select_prefix->add(array(
+                $this->gettext('first'),
+                $this->gettext('second'),
+                $this->gettext('third'),
+                $this->gettext('fourth'),
+                $this->gettext('last')
+            ),
+            array(1, 2, 3, 4, -1));
+
+        $select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday"));
+        if ($noselect) $select_wday->add($noselect, '');
+
+        $daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
+        $first = $this->rc->config->get('calendar_first_day', 1);
+        for ($j = $first; $j <= $first+6; $j++) {
+            $d = $j % 7;
+            $select_wday->add($this->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
+        }
+
+        return $select_prefix->show() . '&nbsp;' . $select_wday->show();
+    }
+
+    /**
+     * Convert the recurrence settings to be processed on the client
+     */
+    public function to_client_recurrence($recurrence, $allday = false)
+    {
+        if ($recurrence['UNTIL'])
+            $recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c');
+
+        // format RDATE values
+        if (is_array($recurrence['RDATE'])) {
+            $libcal = $this;
+            $recurrence['RDATE'] = array_map(function($rdate) use ($libcal) {
+                return $libcal->adjust_timezone($rdate, true)->format('c');
+            }, $recurrence['RDATE']);
+        }
+
+        unset($recurrence['EXCEPTIONS']);
+
+        return $recurrence;
+    }
+
+    /**
+     * Process the alarms values submitted by the client
+     */
+    public function from_client_recurrence($recurrence, $start = null)
+    {
+        if (is_array($recurrence) && !empty($recurrence['UNTIL'])) {
+            $recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone);
+        }
+
+        if (is_array($recurrence) && is_array($recurrence['RDATE'])) {
+            $tz = $this->timezone;
+            $recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) {
+                try {
+                    $dt = new DateTime($rdate, $tz);
+                    if (is_a($start, 'DateTime'))
+                        $dt->setTime($start->format('G'), $start->format('i'));
+                    return $dt;
+                }
+                catch (Exception $e) {
+                    return null;
+                }
+            }, $recurrence['RDATE']);
+        }
+
+        return $recurrence;
+    }
+
+
+    /*********  Attachments handling  *********/
+
+    /**
+     * Handler for attachment uploads
+     */
+    public function attachment_upload($session_key, $id_prefix = '')
+    {
+        // Upload progress update
+        if (!empty($_GET['_progress'])) {
+            $this->rc->upload_progress();
+        }
+
+        $recid = $id_prefix . rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
+        $uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC);
+
+        if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) {
+            $_SESSION[$session_key] = array();
+            $_SESSION[$session_key]['id'] = $recid;
+            $_SESSION[$session_key]['attachments'] = array();
+        }
+
+        // clear all stored output properties (like scripts and env vars)
+        $this->rc->output->reset();
+
+        if (is_array($_FILES['_attachments']['tmp_name'])) {
+            foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
+              // Process uploaded attachment if there is no error
+              $err = $_FILES['_attachments']['error'][$i];
+
+              if (!$err) {
+                $attachment = array(
+                    'path' => $filepath,
+                    'size' => $_FILES['_attachments']['size'][$i],
+                    'name' => $_FILES['_attachments']['name'][$i],
+                    'mimetype' => rcube_mime::file_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
+                    'group' => $recid,
+                );
+
+                $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
+              }
+
+              if (!$err && $attachment['status'] && !$attachment['abort']) {
+                  $id = $attachment['id'];
+
+                  // store new attachment in session
+                  unset($attachment['status'], $attachment['abort']);
+                  $_SESSION[$session_key]['attachments'][$id] = $attachment;
+
+                  if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) {
+                      $button = html::img(array(
+                          'src' => $icon,
+                          'alt' => $this->rc->gettext('delete')
+                      ));
+                  }
+                  else {
+                      $button = rcube::Q($this->rc->gettext('delete'));
+                  }
+
+                  $content = html::a(array(
+                      'href' => "#delete",
+                      'class' => 'delete',
+                      'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", rcmail_output::JS_OBJECT_NAME, $id),
+                      'title' => $this->rc->gettext('delete'),
+                      'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
+                  ), $button);
+
+                  $content .= rcube::Q($attachment['name']);
+
+                  $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
+                      'html' => $content,
+                      'name' => $attachment['name'],
+                      'mimetype' => $attachment['mimetype'],
+                      'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
+                      'complete' => true), $uploadid);
+              }
+              else {  // upload failed
+                  if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
+                    $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
+                        'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
+                  }
+                  else if ($attachment['error']) {
+                      $msg = $attachment['error'];
+                  }
+                  else {
+                      $msg = $this->rc->gettext('fileuploaderror');
+                  }
+
+                  $this->rc->output->command('display_message', $msg, 'error');
+                  $this->rc->output->command('remove_from_attachment_list', $uploadid);
+                }
+            }
+        }
+        else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+            // if filesize exceeds post_max_size then $_FILES array is empty,
+            // show filesizeerror instead of fileuploaderror
+            if ($maxsize = ini_get('post_max_size'))
+                $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
+                    'size' => $this->rc->show_bytes(parse_bytes($maxsize)))));
+            else
+                $msg = $this->rc->gettext('fileuploaderror');
+
+            $this->rc->output->command('display_message', $msg, 'error');
+            $this->rc->output->command('remove_from_attachment_list', $uploadid);
+        }
+
+        $this->rc->output->send('iframe');
+    }
+
+
+    /**
+     * Deliver an event/task attachment to the client
+     * (similar as in Roundcube core program/steps/mail/get.inc)
+     */
+    public function attachment_get($attachment)
+    {
+        ob_end_clean();
+
+        if ($attachment && $attachment['body']) {
+            // allow post-processing of the attachment body
+            $part = new rcube_message_part;
+            $part->filename  = $attachment['name'];
+            $part->size      = $attachment['size'];
+            $part->mimetype  = $attachment['mimetype'];
+
+            $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
+                'body'     => $attachment['body'],
+                'mimetype' => strtolower($attachment['mimetype']),
+                'download' => !empty($_GET['_download']),
+                'part'     => $part,
+            ));
+
+            if ($plugin['abort'])
+                exit;
+
+            $mimetype = $plugin['mimetype'];
+            list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+            $browser = $this->rc->output->browser;
+
+            // send download headers
+            if ($plugin['download']) {
+                header("Content-Type: application/octet-stream");
+                if ($browser->ie)
+                    header("Content-Type: application/force-download");
+            }
+            else if ($ctype_primary == 'text') {
+                header("Content-Type: text/$ctype_secondary");
+            }
+            else {
+                header("Content-Type: $mimetype");
+                header("Content-Transfer-Encoding: binary");
+            }
+
+            // display page, @TODO: support text/plain (and maybe some other text formats)
+            if ($mimetype == 'text/html' && empty($_GET['_download'])) {
+                $OUTPUT = new rcmail_html_page();
+                // @TODO: use washtml on $body
+                $OUTPUT->write($plugin['body']);
+            }
+            else {
+                // don't kill the connection if download takes more than 30 sec.
+                @set_time_limit(0);
+
+                $filename = $attachment['name'];
+                $filename = preg_replace('[\r\n]', '', $filename);
+
+                if ($browser->ie && $browser->ver < 7)
+                    $filename = rawurlencode(abbreviate_string($filename, 55));
+                else if ($browser->ie)
+                    $filename = rawurlencode($filename);
+                else
+                    $filename = addcslashes($filename, '"');
+
+                $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
+                header("Content-Disposition: $disposition; filename=\"$filename\"");
+
+                echo $plugin['body'];
+            }
+
+            exit;
+        }
+
+        // if we arrive here, the requested part was not found
+        header('HTTP/1.1 404 Not Found');
+        exit;
+    }
+
+    /**
+     * Show "loading..." page in attachment iframe
+     */
+    public function attachment_loading_page()
+    {
+        $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
+        $message = $this->rc->gettext('loadingdata');
+
+        header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
+        print "<html>\n<head>\n"
+            . '<meta http-equiv="refresh" content="0; url='.rcube::Q($url).'">' . "\n"
+            . '<meta http-equiv="content-type" content="text/html; charset='.RCUBE_CHARSET.'">' . "\n"
+            . "</head>\n<body>\n$message\n</body>\n</html>";
+        exit;
+    }
+
+    /**
+     * Template object for attachment display frame
+     */
+    public function attachment_frame($attrib = array())
+    {
+        $mimetype = strtolower($this->attachment['mimetype']);
+        list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+        $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
+
+        $this->rc->output->add_gui_object('attachmentframe', $attrib['id']);
+
+        return html::iframe($attrib);
+    }
+
+    /**
+     *
+     */
+    public function attachment_header($attrib = array())
+    {
+        $rcmail = rcmail::get_instance();
+        $dl_link = strtolower($attrib['downloadlink']) == 'true';
+        $dl_url = $this->rc->url(array('_frame' => null, '_download' => 1) + $_GET);
+
+        $table = new html_table(array('cols' => $dl_link ? 3 : 2));
+
+        if (!empty($this->attachment['name'])) {
+            $table->add('title', rcube::Q($this->rc->gettext('filename')));
+            $table->add('header', rcube::Q($this->attachment['name']));
+            if ($dl_link) {
+                $table->add('download-link', html::a($dl_url, rcube::Q($this->rc->gettext('download'))));
+            }
+        }
+
+        if (!empty($this->attachment['mimetype'])) {
+            $table->add('title', rcube::Q($this->rc->gettext('type')));
+            $table->add('header', rcube::Q($this->attachment['mimetype']));
+        }
+
+        if (!empty($this->attachment['size'])) {
+            $table->add('title', rcube::Q($this->rc->gettext('filesize')));
+            $table->add('header', rcube::Q($this->rc->show_bytes($this->attachment['size'])));
+        }
+
+        $this->rc->output->set_env('attachment_download_url', $dl_url);
+
+        return $table->show($attrib);
+    }
+
+
+    /*********  iTip message detection  *********/
+
+    /**
+     * Check mail message structure of there are .ics files attached
+     */
+    public function mail_message_load($p)
+    {
+        $this->ical_message = $p['object'];
+        $itip_part          = null;
+
+        // check all message parts for .ics files
+        foreach ((array)$this->ical_message->mime_parts as $part) {
+            if (self::part_is_vcalendar($part, $this->ical_message)) {
+                if ($part->ctype_parameters['method'])
+                    $itip_part = $part->mime_id;
+                else
+                    $this->ical_parts[] = $part->mime_id;
+            }
+        }
+
+        // priorize part with method parameter
+        if ($itip_part) {
+            $this->ical_parts = array($itip_part);
+        }
+    }
+
+    /**
+     * Getter for the parsed iCal objects attached to the current email message
+     *
+     * @return object libvcalendar parser instance with the parsed objects
+     */
+    public function get_mail_ical_objects()
+    {
+        // create parser and load ical objects
+        if (!$this->mail_ical_parser) {
+            $this->mail_ical_parser = $this->get_ical();
+
+            foreach ($this->ical_parts as $mime_id) {
+                $part    = $this->ical_message->mime_parts[$mime_id];
+                $charset = $part->ctype_parameters['charset'] ?: RCUBE_CHARSET;
+                $this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset);
+
+                // check if the parsed object is an instance of a recurring event/task
+                array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance');
+
+                // stop on the part that has an iTip method specified
+                if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
+                    $this->mail_ical_parser->message_date = $this->ical_message->headers->date;
+                    $this->mail_ical_parser->mime_id = $mime_id;
+
+                    // store the message's sender address for comparisons
+                    $from = rcube_mime::decode_address_list($this->ical_message->headers->from, 1, true, null, true);
+                    $this->mail_ical_parser->sender = !empty($from) ? $from[1] : '';
+
+                    if (!empty($this->mail_ical_parser->sender)) {
+                        foreach ($this->mail_ical_parser->objects as $i => $object) {
+                            $this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender;
+                            $this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender);
+                        }
+                    }
+
+                    break;
+                }
+            }
+        }
+
+        return $this->mail_ical_parser;
+    }
+
+    /**
+     * Read the given mime message from IMAP and parse ical data
+     *
+     * @param string Mailbox name
+     * @param string Message UID
+     * @param string Message part ID and object index (e.g. '1.2:0')
+     * @param string Object type filter (optional)
+     *
+     * @return array Hash array with the parsed iCal 
+     */
+    public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null)
+    {
+        $charset = RCUBE_CHARSET;
+
+        // establish imap connection
+        $imap = $this->rc->get_storage();
+        $imap->set_folder($mbox);
+
+        if ($uid && $mime_id) {
+            list($mime_id, $index) = explode(':', $mime_id);
+
+            $part    = $imap->get_message_part($uid, $mime_id);
+            $headers = $imap->get_message_headers($uid);
+            $parser  = $this->get_ical();
+
+            if ($part->ctype_parameters['charset']) {
+                $charset = $part->ctype_parameters['charset'];
+            }
+
+            if ($part) {
+                $objects = $parser->import($part, $charset);
+            }
+        }
+
+        // successfully parsed events/tasks?
+        if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) {
+            if ($parser->method)
+                $object['_method'] = $parser->method;
+
+            // store the message's sender address for comparisons
+            $from = rcube_mime::decode_address_list($headers->from, 1, true, null, true);
+            $object['_sender'] = !empty($from) ? $from[1] : '';
+            $object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
+
+            // check if this is an instance of a recurring event/task
+            self::identify_recurrence_instance($object);
+
+            return $object;
+        }
+
+        return null;
+    }
+
+    /**
+     * Checks if specified message part is a vcalendar data
+     *
+     * @param rcube_message_part Part object
+     * @param rcube_message      Message object
+     *
+     * @return boolean True if part is of type vcard
+     */
+    public static function part_is_vcalendar($part, $message = null)
+    {
+        // First check if the message is "valid" (i.e. not multipart/report)
+        if ($message) {
+            $level = explode('.', $part->mime_id);
+
+            while (array_pop($level) !== null) {
+                $parent = $message->mime_parts[join('.', $level) ?: 0];
+                if ($parent->mimetype == 'multipart/report') {
+                    return false;
+                }
+            }
+        }
+
+        return (
+            in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
+            // Apple sends files as application/x-any (!?)
+            ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
+        );
+    }
+
+    /**
+     * Single occourrences of recurring events are identified by their RECURRENCE-ID property
+     * in iCal which is represented as 'recurrence_date' in our internal data structure.
+     *
+     * Check if such a property exists and derive the '_instance' identifier and '_savemode'
+     * attributes which are used in the storage backend to identify the nested exception item.
+     */
+    public static function identify_recurrence_instance(&$object)
+    {
+        // for savemode=all, remove recurrence instance identifiers
+        if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && $object['recurrence']) {
+            unset($object['_instance'], $object['recurrence_date']);
+        }
+        // set instance and 'savemode' according to recurrence-id
+        else if (!empty($object['recurrence_date']) && is_a($object['recurrence_date'], 'DateTime')) {
+            $object['_instance'] = self::recurrence_instance_identifier($object);
+            $object['_savemode'] = $object['thisandfuture'] ? 'future' : 'current';
+        }
+        else if (!empty($object['recurrence_id']) && !empty($object['_instance'])) {
+            if (strlen($object['_instance']) > 4) {
+                $object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone());
+            }
+            else {
+                $object['recurrence_date'] = clone $object['start'];
+            }
+        }
+    }
+
+    /**
+     * Return a date() format string to render identifiers for recurrence instances
+     *
+     * @param array Hash array with event properties
+     * @return string Format string
+     */
+    public static function recurrence_id_format($event)
+    {
+        return $event['allday'] ? 'Ymd' : 'Ymd\THis';
+    }
+
+    /**
+     * Return the identifer for the given instance of a recurring event
+     *
+     * @param array Hash array with event properties
+     * @param bool  All-day flag from the main event
+     *
+     * @return mixed Format string or null if identifier cannot be generated
+     */
+    public static function recurrence_instance_identifier($event, $allday = null)
+    {
+        $instance_date = $event['recurrence_date'] ?: $event['start'];
+
+        if ($instance_date && is_a($instance_date, 'DateTime')) {
+            // According to RFC5545 (3.8.4.4) RECURRENCE-ID format should
+            // be date/date-time depending on the main event type, not the exception
+            if ($allday === null) {
+                $allday = $event['allday'];
+            }
+
+            return $instance_date->format($allday ? 'Ymd' : 'Ymd\THis');
+        }
+    }
+
+
+    /*********  Attendee handling functions  *********/
+
+    /**
+     * Handler for attendee group expansion requests
+     */
+    public function expand_attendee_group()
+    {
+        $id     = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
+        $data   = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
+        $result = array('id' => $id, 'members' => array());
+        $maxnum = 500;
+
+        // iterate over all autocomplete address books (we don't know the source of the group)
+        foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) {
+            if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) {
+                foreach ($abook->list_groups($data['name'], 1) as $group) {
+                    // this is the matching group to expand
+                    if (in_array($data['email'], (array)$group['email'])) {
+                        $abook->set_pagesize($maxnum);
+                        $abook->set_group($group['ID']);
+
+                        // get all members
+                        $res = $abook->list_records($this->rc->config->get('contactlist_fields'));
+
+                        // handle errors (e.g. sizelimit, timelimit)
+                        if ($abook->get_error()) {
+                            $result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring');
+                            $res = false;
+                        }
+                        // check for maximum number of members (we don't wanna bloat the UI too much)
+                        else if ($res->count > $maxnum) {
+                            $result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring');
+                            $res = false;
+                        }
+
+                        while ($res && ($member = $res->iterate())) {
+                            $emails = (array)$abook->get_col_values('email', $member, true);
+                            if (!empty($emails) && ($email = array_shift($emails))) {
+                                $result['members'][] = array(
+                                    'email' => $email,
+                                    'name' => rcube_addressbook::compose_list_name($member),
+                                );
+                            }
+                        }
+
+                        break 2;
+                    }
+                }
+            }
+        }
+
+        $this->rc->output->command('plugin.expand_attendee_callback', $result);
+    }
+
+    /**
+     * Merge attendees of the old and new event version
+     * with keeping current user and his delegatees status
+     *
+     * @param array &$new   New object data
+     * @param array $old    Old object data
+     * @param bool  $status New status of the current user
+     */
+    public function merge_attendees(&$new, $old, $status = null)
+    {
+        if (empty($status)) {
+            $emails    = $this->get_user_emails();
+            $delegates = array();
+            $attendees = array();
+
+            // keep attendee status of the current user
+            foreach ((array) $new['attendees'] as $i => $attendee) {
+                if (empty($attendee['email'])) {
+                    continue;
+                }
+
+                $attendees[] = $email = strtolower($attendee['email']);
+
+                if (in_array($email, $emails)) {
+                    foreach ($old['attendees'] as $_attendee) {
+                        if ($attendee['email'] == $_attendee['email']) {
+                            $new['attendees'][$i] = $_attendee;
+                            if ($_attendee['status'] == 'DELEGATED' && ($email = $_attendee['delegated-to'])) {
+                                $delegates[] = strtolower($email);
+                            }
+
+                            break;
+                        }
+                    }
+                }
+            }
+
+            // make sure delegated attendee is not lost
+            foreach ($delegates as $delegatee) {
+                if (!in_array($delegatee, $attendees)) {
+                    foreach ((array) $old['attendees'] as $attendee) {
+                        if ($attendee['email'] && ($email = strtolower($attendee['email'])) && $email == $delegatee) {
+                            $new['attendees'][] = $attendee;
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        // We also make sure that status of any attendee
+        // is not overriden by NEEDS-ACTION if it was already set
+        // which could happen if you work with shared events
+        foreach ((array) $new['attendees'] as $i => $attendee) {
+            if ($attendee['email'] && $attendee['status'] == 'NEEDS-ACTION') {
+                foreach ($old['attendees'] as $_attendee) {
+                    if ($attendee['email'] == $_attendee['email']) {
+                        $new['attendees'][$i]['status'] = $_attendee['status'];
+                        unset($new['attendees'][$i]['rsvp']);
+                        break;
+                    }
+                }
+            }
+        }
+    }
+
+
+    /*********  Static utility functions  *********/
+
+    /**
+     * Convert the internal structured data into a vcalendar rrule 2.0 string
+     */
+    public static function to_rrule($recurrence, $allday = false)
+    {
+        if (is_string($recurrence))
+            return $recurrence;
+
+        $rrule = '';
+        foreach ((array)$recurrence as $k => $val) {
+            $k = strtoupper($k);
+            switch ($k) {
+            case 'UNTIL':
+                // convert to UTC according to RFC 5545
+                if (is_a($val, 'DateTime')) {
+                    if (!$allday && !$val->_dateonly) {
+                        $until = clone $val;
+                        $until->setTimezone(new DateTimeZone('UTC'));
+                        $val = $until->format('Ymd\THis\Z');
+                    }
+                    else {
+                        $val = $val->format('Ymd');
+                    }
+                }
+                break;
+            case 'RDATE':
+            case 'EXDATE':
+                foreach ((array)$val as $i => $ex) {
+                    if (is_a($ex, 'DateTime'))
+                        $val[$i] = $ex->format('Ymd\THis');
+                }
+                $val = join(',', (array)$val);
+                break;
+            case 'EXCEPTIONS':
+                continue 2;
+            }
+
+            if (strlen($val))
+                $rrule .= $k . '=' . $val . ';';
+        }
+
+        return rtrim($rrule, ';');
+    }
+
+    /**
+     * Convert from fullcalendar date format to PHP date() format string
+     */
+    public static function to_php_date_format($from)
+    {
+        // "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
+        return strtr(strtr($from, array(
+            'YYYY' => 'Y',
+            'YY'   => 'y',
+            'yyyy' => 'Y',
+            'yy'   => 'y',
+            'MMMM' => 'F',
+            'MMM'  => 'M',
+            'MM'   => 'm',
+            'M'    => 'n',
+            'dddd' => 'l',
+            'ddd'  => 'D',
+            'dd'   => 'd',
+            'd'    => 'j',
+            'HH'   => '**',
+            'hh'   => '%%',
+            'H'    => 'G',
+            'h'    => 'g',
+            'mm'   => 'i',
+            'ss'   => 's',
+            'TT'   => 'A',
+            'tt'   => 'a',
+            'T'    => 'A',
+            't'    => 'a',
+            'u'    => 'c',
+        )), array(
+            '**'   => 'H',
+            '%%'   => 'h',
+        ));
+    }
+
+    /**
+     * Convert from PHP date() format to fullcalendar format string
+     */
+    public static function from_php_date_format($from)
+    {
+        // "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
+        return strtr($from, array(
+            'y' => 'yy',
+            'Y' => 'yyyy',
+            'M' => 'MMM',
+            'F' => 'MMMM',
+            'm' => 'MM',
+            'n' => 'M',
+            'j' => 'd',
+            'd' => 'dd',
+            'D' => 'ddd',
+            'l' => 'dddd',
+            'H' => 'HH',
+            'h' => 'hh',
+            'G' => 'H',
+            'g' => 'h',
+            'i' => 'mm',
+            's' => 'ss',
+            'A' => 'TT',
+            'a' => 'tt',
+            'c' => 'u',
+        ));
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/libvcalendar.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,1439 @@
+<?php
+
+/**
+ * iCalendar functions for the libcalendaring plugin
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2013-2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+use \Sabre\VObject;
+use \Sabre\VObject\DateTimeParser;
+
+/**
+ * Class to parse and build vCalendar (iCalendar) files
+ *
+ * Uses the Sabre VObject library, version 3.x.
+ *
+ */
+class libvcalendar implements Iterator
+{
+    private $timezone;
+    private $attach_uri = null;
+    private $prodid = '-//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',
+        'delegated-from'  => 'DELEGATED-FROM',
+        'delegated-to'    => 'DELEGATED-TO',
+        'schedule-status' => 'SCHEDULE-STATUS',
+        'schedule-agent'  => 'SCHEDULE-AGENT',
+        'sent-by'         => 'SENT-BY',
+    );
+    private $organizer_keymap = array(
+        'name'            => 'CN',
+        'schedule-status' => 'SCHEDULE-STATUS',
+        'schedule-agent'  => 'SCHEDULE-AGENT',
+        'sent-by'         => 'SENT-BY',
+    );
+    private $iteratorkey = 0;
+    private $charset;
+    private $forward_exceptions;
+    private $vhead;
+    private $fp;
+    private $vtimezones = array();
+
+    public $method;
+    public $agent = '';
+    public $objects = array();
+    public $freebusy = array();
+
+
+    /**
+     * Default constructor
+     */
+    function __construct($tz = null)
+    {
+        $this->timezone = $tz;
+        $this->prodid = '-//Roundcube libcalendaring ' . RCUBE_VERSION . '//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN';
+    }
+
+    /**
+     * Setter for timezone information
+     */
+    public function set_timezone($tz)
+    {
+        $this->timezone = $tz;
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Setter for a user-agent string to tweak input/output accordingly
+     */
+    public function set_agent($agent)
+    {
+        $this->agent = $agent;
+    }
+
+    /**
+     * Free resources by clearing member vars
+     */
+    public function reset()
+    {
+        $this->vhead = '';
+        $this->method = '';
+        $this->objects = array();
+        $this->freebusy = array();
+        $this->vtimezones = array();
+        $this->iteratorkey = 0;
+
+        if ($this->fp) {
+            fclose($this->fp);
+            $this->fp = null;
+        }
+    }
+
+    /**
+    * Import events from iCalendar format
+    *
+    * @param  string vCalendar input
+    * @param  string Input charset (from envelope)
+    * @param  boolean True if parsing exceptions should be forwarded to the caller
+    * @return array List of events extracted from the input
+    */
+    public function import($vcal, $charset = 'UTF-8', $forward_exceptions = false, $memcheck = true)
+    {
+        // TODO: convert charset to UTF-8 if other
+
+        try {
+            // estimate the memory usage and try to avoid fatal errors when allowed memory gets exhausted
+            if ($memcheck) {
+                $count = substr_count($vcal, 'BEGIN:VEVENT') + substr_count($vcal, 'BEGIN:VTODO');
+                $expected_memory = $count * 70*1024;  // assume ~ 70K per event (empirically determined)
+
+                if (!rcube_utils::mem_check($expected_memory)) {
+                    throw new Exception("iCal file too big");
+                }
+            }
+
+            $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) {
+            if ($forward_exceptions) {
+                throw $e;
+            }
+            else {
+                rcube::raise_error(array(
+                    'code' => 600, 'type' => 'php',
+                    'file' => __FILE__, 'line' => __LINE__,
+                    'message' => "iCal data parse error: " . $e->getMessage()),
+                    true, false);
+            }
+        }
+
+        return array();
+    }
+
+    /**
+    * Read iCalendar events from a file
+    *
+    * @param  string File path to read from
+    * @param  string Input charset (from envelope)
+    * @param  boolean True if parsing exceptions should be forwarded to the caller
+    * @return array List of events extracted from the file
+    */
+    public function import_from_file($filepath, $charset = 'UTF-8', $forward_exceptions = false)
+    {
+        if ($this->fopen($filepath, $charset, $forward_exceptions)) {
+            while ($this->_parse_next(false)) {
+                // nop
+            }
+
+            fclose($this->fp);
+            $this->fp = null;
+        }
+
+        return $this->objects;
+    }
+
+    /**
+     * Open a file to read iCalendar events sequentially
+     *
+     * @param  string File path to read from
+     * @param  string Input charset (from envelope)
+     * @param  boolean True if parsing exceptions should be forwarded to the caller
+     * @return boolean True if file contents are considered valid
+     */
+    public function fopen($filepath, $charset = 'UTF-8', $forward_exceptions = false)
+    {
+        $this->reset();
+
+        // just to be sure...
+        @ini_set('auto_detect_line_endings', true);
+
+        $this->charset = $charset;
+        $this->forward_exceptions = $forward_exceptions;
+        $this->fp = fopen($filepath, 'r');
+
+        // check file content first
+        $begin = fread($this->fp, 1024);
+        if (!preg_match('/BEGIN:VCALENDAR/i', $begin)) {
+            return false;
+        }
+
+        fseek($this->fp, 0);
+        return $this->_parse_next();
+    }
+
+    /**
+     * Parse the next event/todo/freebusy object from the input file
+     */
+    private function _parse_next($reset = true)
+    {
+        if ($reset) {
+            $this->iteratorkey = 0;
+            $this->objects = array();
+            $this->freebusy = array();
+        }
+
+        $next = $this->_next_component();
+        $buffer = $next;
+
+        // load the next component(s) too, as they could contain recurrence exceptions
+        while (preg_match('/(RRULE|RECURRENCE-ID)[:;]/i', $next)) {
+            $next = $this->_next_component();
+            $buffer .= $next;
+        }
+
+        // parse the vevent block surrounded with the vcalendar heading
+        if (strlen($buffer) && preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $buffer)) {
+            try {
+                $this->import($this->vhead . $buffer . "END:VCALENDAR", $this->charset, true, false);
+            }
+            catch (Exception $e) {
+                if ($this->forward_exceptions) {
+                    throw new VObject\ParseException($e->getMessage() . " in\n" . $buffer);
+                }
+                else {
+                    // write the failing section to error log
+                    rcube::raise_error(array(
+                        'code' => 600, 'type' => 'php',
+                        'file' => __FILE__, 'line' => __LINE__,
+                        'message' => $e->getMessage() . " in\n" . $buffer),
+                        true, false);
+                }
+
+                // advance to next
+                return $this->_parse_next($reset);
+            }
+
+            return count($this->objects) > 0;
+        }
+
+        return false;
+    }
+
+    /**
+     * Helper method to read the next calendar component from the file
+     */
+    private function _next_component()
+    {
+        $buffer = '';
+        $vcalendar_head = false;
+        while (($line = fgets($this->fp, 1024)) !== false) {
+            // ignore END:VCALENDAR lines
+            if (preg_match('/END:VCALENDAR/i', $line)) {
+                continue;
+            }
+            // read vcalendar header (with timezone defintion)
+            if (preg_match('/BEGIN:VCALENDAR/i', $line)) {
+                $this->vhead = '';
+                $vcalendar_head = true;
+            }
+
+            // end of VCALENDAR header part
+            if ($vcalendar_head && preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $line)) {
+                $vcalendar_head = false;
+            }
+
+            if ($vcalendar_head) {
+                $this->vhead .= $line;
+            }
+            else {
+                $buffer .= $line;
+                if (preg_match('/END:(VEVENT|VTODO|VFREEBUSY)/i', $line)) {
+                    break;
+                }
+            }
+        }
+
+        return $buffer;
+    }
+
+    /**
+     * 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)
+    {
+        $seen = array();
+        $exceptions = array();
+
+        if ($vobject->name == 'VCALENDAR') {
+            $this->method = strval($vobject->METHOD);
+            $this->agent  = strval($vobject->PRODID);
+
+            foreach ($vobject->getComponents() as $ve) {
+                if ($ve->name == 'VEVENT' || $ve->name == 'VTODO') {
+                    // convert to hash array representation
+                    $object = $this->_to_array($ve);
+
+                    // temporarily store this as exception
+                    if ($object['recurrence_date']) {
+                        $exceptions[] = $object;
+                    }
+                    else if (!$seen[$object['uid']]++) {
+                        $this->objects[] = $object;
+                    }
+                }
+                else if ($ve->name == 'VFREEBUSY') {
+                    $this->objects[] = $this->_parse_freebusy($ve);
+                }
+            }
+
+            // add exceptions to the according master events
+            foreach ($exceptions as $exception) {
+                $uid = $exception['uid'];
+
+                // make this exception the master
+                if (!$seen[$uid]++) {
+                    $this->objects[] = $exception;
+                }
+                else {
+                    foreach ($this->objects as $i => $object) {
+                        // add as exception to existing entry with a matching UID
+                        if ($object['uid'] == $uid) {
+                            $this->objects[$i]['exceptions'][] = $exception;
+
+                            if (!empty($object['recurrence'])) {
+                                $this->objects[$i]['recurrence']['EXCEPTIONS'] = &$this->objects[$i]['exceptions'];
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        return $this->objects;
+    }
+
+    /**
+     * Getter for free-busy periods
+     */
+    public function get_busy_periods()
+    {
+        $out = array();
+        foreach ((array)$this->freebusy['periods'] as $period) {
+            if ($period[2] != 'FREE') {
+                $out[] = $period;
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * Helper method to determine whether the connected client is an Apple device
+     */
+    private function is_apple()
+    {
+        return stripos($this->agent, 'Apple') !== false
+            || stripos($this->agent, 'Mac OS X') !== false
+            || stripos($this->agent, 'iOS/') !== false;
+    }
+
+    /**
+     * 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'     => self::convert_string($ve->UID),
+            'title'   => self::convert_string($ve->SUMMARY),
+            '_type'   => $ve->name == 'VTODO' ? 'task' : 'event',
+            // set defaults
+            'priority' => 0,
+            'attendees' => array(),
+            'x-custom' => array(),
+        );
+
+        // Catch possible exceptions when date is invalid (Bug #2144)
+        // We can skip these fields, they aren't critical
+        foreach (array('CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed') as $attr => $field) {
+            try {
+                if (!$event[$field] && $ve->{$attr}) {
+                    $event[$field] = $ve->{$attr}->getDateTime();
+                }
+            } catch (Exception $e) {}
+        }
+
+        // map other attributes to internal fields
+        foreach ($ve->children as $prop) {
+            if (!($prop instanceof VObject\Property))
+                continue;
+
+            $value = strval($prop);
+
+            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'] = strval($prop) == 'TRANSPARENT' ? 'free' : 'busy';
+                break;
+
+            case 'STATUS':
+                if ($value == 'TENTATIVE')
+                    $event['free_busy'] = 'tentative';
+                else if ($value == 'CANCELLED')
+                    $event['cancelled'] = true;
+                else if ($value == 'COMPLETED')
+                    $event['complete'] = 100;
+
+                $event['status'] = $value;
+                break;
+
+            case 'COMPLETED':
+                if (self::convert_datetime($prop)) {
+                    $event['status'] = 'COMPLETED';
+                    $event['complete'] = 100;
+                }
+                break;
+
+            case 'PRIORITY':
+                if (is_numeric($value))
+                    $event['priority'] = $value;
+                break;
+
+            case 'RRULE':
+                $params = is_array($event['recurrence']) ? $event['recurrence'] : array();
+                // parse recurrence rule attributes
+                foreach ($prop->getParts() as $k => $v) {
+                    $params[strtoupper($k)] = is_array($v) ? implode(',', $v) : $v;
+                }
+                if ($params['UNTIL'])
+                    $params['UNTIL'] = date_create($params['UNTIL']);
+                if (!$params['INTERVAL'])
+                    $params['INTERVAL'] = 1;
+
+                $event['recurrence'] = array_filter($params);
+                break;
+
+            case 'EXDATE':
+                if (!empty($value)) {
+                    $exdates = array_map(function($_) { return is_array($_) ? $_[0] : $_; }, self::convert_datetime($prop, true));
+                    $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], $exdates);
+                }
+                break;
+
+            case 'RDATE':
+                if (!empty($value)) {
+                    $rdates = array_map(function($_) { return is_array($_) ? $_[0] : $_; }, self::convert_datetime($prop, true));
+                    $event['recurrence']['RDATE'] = array_merge((array)$event['recurrence']['RDATE'], $rdates);
+                }
+                break;
+
+            case 'RECURRENCE-ID':
+                $event['recurrence_date'] = self::convert_datetime($prop);
+                if ($prop->offsetGet('RANGE') == 'THISANDFUTURE' || $prop->offsetGet('THISANDFUTURE') !== null) {
+                    $event['thisandfuture'] = true;
+                }
+                break;
+
+            case 'RELATED-TO':
+                $reltype = $prop->offsetGet('RELTYPE');
+                if ($reltype == 'PARENT' || $reltype === null) {
+                    $event['parent_id'] = $value;
+                }
+                break;
+
+            case 'SEQUENCE':
+                $event['sequence'] = intval($value);
+                break;
+
+            case 'PERCENT-COMPLETE':
+                $event['complete'] = intval($value);
+                break;
+
+            case 'LOCATION':
+            case 'DESCRIPTION':
+            case 'URL':
+            case 'COMMENT':
+                $event[strtolower($prop->name)] = self::convert_string($prop);
+                break;
+
+            case 'CATEGORY':
+            case 'CATEGORIES':
+                $event['categories'] = array_merge((array)$event['categories'], $prop->getParts());
+                break;
+
+            case 'CLASS':
+            case 'X-CALENDARSERVER-ACCESS':
+                $event['sensitivity'] = strtolower($value);
+                break;
+
+            case 'X-MICROSOFT-CDO-BUSYSTATUS':
+                if ($value == 'OOF')
+                    $event['free_busy'] = 'outofoffice';
+                else if (in_array($value, array('FREE', 'BUSY', 'TENTATIVE')))
+                    $event['free_busy'] = strtolower($value);
+                break;
+
+            case 'ATTENDEE':
+            case 'ORGANIZER':
+                $params = array('RSVP' => false);
+                foreach ($prop->parameters() as $pname => $pvalue) {
+                    switch ($pname) {
+                        case 'RSVP': $params[$pname] = strtolower($pvalue) == 'true'; break;
+                        case 'CN':   $params[$pname] = self::unescape($pvalue); break;
+                        default:     $params[$pname] = strval($pvalue); break;
+                    }
+                }
+                $attendee = self::map_keys($params, array_flip($this->attendee_keymap));
+                $attendee['email'] = preg_replace('!^mailto:!i', '', $value);
+
+                if ($prop->name == 'ORGANIZER') {
+                    $attendee['role'] = 'ORGANIZER';
+                    $attendee['status'] = 'ACCEPTED';
+                    $event['organizer'] = $attendee;
+
+                    if (array_key_exists('schedule-agent', $attendee)) {
+                        $schedule_agent = $attendee['schedule-agent'];
+                    }
+                }
+                else if ($attendee['email'] != $event['organizer']['email']) {
+                    $event['attendees'][] = $attendee;
+                }
+                break;
+
+            case 'ATTACH':
+                $params = self::parameters_array($prop);
+                if (substr($value, 0, 4) == 'http' && !strpos($value, ':attachment:')) {
+                    $event['links'][] = $value;
+                }
+                else if (strlen($value) && strtoupper($params['VALUE']) == 'BINARY') {
+                    $attachment = self::map_keys($params, array('FMTTYPE' => 'mimetype', 'X-LABEL' => 'name', 'X-APPLE-FILENAME' => 'name'));
+                    $attachment['data'] = $value;
+                    $attachment['size'] = strlen($value);
+                    $event['attachments'][] = $attachment;
+                }
+                break;
+
+            default:
+                if (substr($prop->name, 0, 2) == 'X-')
+                    $event['x-custom'][] = array($prop->name, strval($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);
+            }
+        }
+
+        // validate event dates
+        if ($event['_type'] == 'event') {
+            $event['allday'] = false;
+
+            // check for all-day dates
+            if ($event['start']->_dateonly) {
+                $event['allday'] = true;
+            }
+
+            // events may lack the DTEND property, set it to DTSTART (RFC5545 3.6.1)
+            if (empty($event['end'])) {
+                $event['end'] = clone $event['start'];
+            }
+            // shift end-date by one day (except Thunderbird)
+            else if ($event['allday'] && is_object($event['end'])) {
+                $event['end']->sub(new \DateInterval('PT23H'));
+            }
+
+            // sanity-check and fix end date
+            if (!empty($event['end']) && $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']) && $event['_type'] == 'event') {
+            array_unshift($event['attendees'], $event['organizer']);
+        }
+
+        // find alarms
+        foreach ($ve->select('VALARM') as $valarm) {
+            $action  = 'DISPLAY';
+            $trigger = null;
+            $alarm   = array();
+
+            foreach ($valarm->children as $prop) {
+                $value = strval($prop);
+
+                switch ($prop->name) {
+                case 'TRIGGER':
+                    foreach ($prop->parameters as $param) {
+                        if ($param->name == 'VALUE' && $param->getValue() == 'DATE-TIME') {
+                            $trigger = '@' . $prop->getDateTime()->format('U');
+                            $alarm['trigger'] = $prop->getDateTime();
+                        }
+                        else if ($param->name == 'RELATED') {
+                            $alarm['related'] = $param->getValue();
+                        }
+                    }
+                    if (!$trigger && ($values = libcalendaring::parse_alarm_value($value))) {
+                        $trigger = $values[2];
+                    }
+
+                    if (!$alarm['trigger']) {
+                        $alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $value), 'T');
+                        // if all 0-values have been stripped, assume 'at time'
+                        if ($alarm['trigger'] == 'P')
+                            $alarm['trigger'] = 'PT0S';
+                    }
+                    break;
+
+                case 'ACTION':
+                    $action = $alarm['action'] = strtoupper($value);
+                    break;
+
+                case 'SUMMARY':
+                case 'DESCRIPTION':
+                case 'DURATION':
+                    $alarm[strtolower($prop->name)] = self::convert_string($prop);
+                    break;
+
+                case 'REPEAT':
+                    $alarm['repeat'] = intval($value);
+                    break;
+
+                case 'ATTENDEE':
+                    $alarm['attendees'][] = preg_replace('!^mailto:!i', '', $value);
+                    break;
+
+                case 'ATTACH':
+                    $params = self::parameters_array($prop);
+                    if (strlen($value) && (preg_match('/^[a-z]+:/', $value) || strtoupper($params['VALUE']) == 'URI')) {
+                        // we only support URI-type of attachments here
+                        $alarm['uri'] = $value;
+                    }
+                    break;
+                }
+            }
+
+            if ($action != 'NONE') {
+                if ($trigger && !$event['alarms']) // store first alarm in legacy property
+                    $event['alarms'] = $trigger . ':' . $action;
+
+                if ($alarm['trigger'])
+                    $event['valarms'][] = $alarm;
+            }
+        }
+
+        // assign current timezone to event start/end
+        if ($event['start'] instanceof DateTime) {
+            if ($this->timezone)
+                $event['start']->setTimezone($this->timezone);
+        }
+        else {
+            unset($event['start']);
+        }
+
+        if ($event['end'] instanceof DateTime) {
+            if ($this->timezone)
+                $event['end']->setTimezone($this->timezone);
+        }
+        else {
+            unset($event['end']);
+        }
+
+        // some iTip CANCEL messages only contain the start date
+        if (!$event['end'] && $event['start'] && $this->method == 'CANCEL') {
+            $event['end'] = clone $event['start'];
+        }
+
+        // T2531: Remember SCHEDULE-AGENT in custom property to properly
+        // support event updates via CalDAV when SCHEDULE-AGENT=CLIENT is used
+        if (isset($schedule_agent)) {
+            $event['x-custom'][] = array('SCHEDULE-AGENT', $schedule_agent);
+        }
+
+        // minimal validation
+        if (empty($event['uid']) || ($event['_type'] == 'event' && empty($event['start']) != empty($event['end']))) {
+            throw new VObject\ParseException('Object validation failed: missing mandatory object properties');
+        }
+
+        return $event;
+    }
+
+    /**
+     * Parse the given vfreebusy component into an array representation
+     */
+    private function _parse_freebusy($ve)
+    {
+        $this->freebusy = array('_type' => 'freebusy', 'periods' => array());
+        $seen = array();
+
+        foreach ($ve->children as $prop) {
+            if (!($prop instanceof VObject\Property))
+                continue;
+
+            $value = strval($prop);
+
+            switch ($prop->name) {
+            case 'CREATED':
+            case 'LAST-MODIFIED':
+            case 'DTSTAMP':
+            case 'DTSTART':
+            case 'DTEND':
+                $propmap = array('DTSTART' => 'start', 'DTEND' => 'end', 'CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed');
+                $this->freebusy[$propmap[$prop->name]] = self::convert_datetime($prop);
+                break;
+
+            case 'ORGANIZER':
+                $this->freebusy['organizer'] = preg_replace('!^mailto:!i', '', $value);
+                break;
+
+            case 'FREEBUSY':
+                // The freebusy component can hold more than 1 value, separated by commas.
+                $periods = explode(',', $value);
+                $fbtype = strval($prop['FBTYPE']) ?: 'BUSY';
+
+                // skip dupes
+                if ($seen[$value.':'.$fbtype]++)
+                    continue;
+
+                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 = DateTimeParser::parse($busyStart);
+                    $busyEnd = DateTimeParser::parse($busyEnd);
+                    if ($busyEnd instanceof \DateInterval) {
+                        $tmp = clone $busyStart;
+                        $tmp->add($busyEnd);
+                        $busyEnd = $tmp;
+                    }
+
+                    if ($busyEnd && $busyEnd > $busyStart)
+                        $this->freebusy['periods'][] = array($busyStart, $busyEnd, $fbtype);
+                }
+                break;
+
+            case 'COMMENT':
+                $this->freebusy['comment'] = $value;
+            }
+        }
+
+        return $this->freebusy;
+    }
+
+    /**
+     *
+     */
+    public static function convert_string($prop)
+    {
+        return strval($prop);
+    }
+
+    /**
+     *
+     */
+    public static function unescape($prop)
+    {
+        return str_replace('\,', ',', strval($prop));
+    }
+
+    /**
+     * Helper method to correctly interpret an all-day date value
+     */
+    public static function convert_datetime($prop, $as_array = false)
+    {
+        if (empty($prop)) {
+            return $as_array ? array() : null;
+        }
+
+        else if ($prop instanceof VObject\Property\iCalendar\DateTime) {
+            if (count($prop->getDateTimes()) > 1) {
+                $dt = array();
+                $dateonly = !$prop->hasTime();
+                foreach ($prop->getDateTimes() as $item) {
+                    $item->_dateonly = $dateonly;
+                    $dt[] = $item;
+                }
+            }
+            else {
+                $dt = $prop->getDateTime();
+                if (!$prop->hasTime()) {
+                    $dt->_dateonly = true;
+                }
+            }
+        }
+        else if ($prop instanceof VObject\Property\iCalendar\Period) {
+            $dt = array();
+            foreach ($prop->getParts() as $val) {
+                try {
+                    list($start, $end) = explode('/', $val);
+                    $start = DateTimeParser::parseDateTime($start);
+
+                    // This is a duration value.
+                    if ($end[0] === 'P') {
+                        $dur = DateTimeParser::parseDuration($end);
+                        $end = clone $start;
+                        $end->add($dur);
+                    }
+                    else {
+                        $end = DateTimeParser::parseDateTime($end);
+                    }
+                    $dt[] = array($start, $end);
+                }
+                catch (Exception $e) {
+                    // ignore single date parse errors
+                }
+            }
+        }
+        else if ($prop instanceof \DateTime) {
+            $dt = $prop;
+        }
+
+        // force return value to array if requested
+        if ($as_array && !is_array($dt)) {
+            $dt = empty($dt) ? array() : array($dt);
+        }
+
+        return $dt;
+    }
+
+
+    /**
+     * Create a Sabre\VObject\Property instance from a PHP DateTime object
+     *
+     * @param object  VObject\Document parent node to create property for
+     * @param string  Property name
+     * @param object  DateTime
+     * @param boolean Set as UTC date
+     * @param boolean Set as VALUE=DATE property
+     */
+    public function datetime_prop($cal, $name, $dt, $utc = false, $dateonly = null, $set_type = false)
+    {
+        if ($utc) {
+            $dt->setTimeZone(new \DateTimeZone('UTC'));
+            $is_utc = true;
+        }
+        else {
+            $is_utc = ($tz = $dt->getTimezone()) && in_array($tz->getName(), array('UTC','GMT','Z'));
+        }
+        $is_dateonly = $dateonly === null ? (bool)$dt->_dateonly : (bool)$dateonly;
+        $vdt = $cal->createProperty($name, $dt, null, $is_dateonly ? 'DATE' : 'DATE-TIME');
+
+        if ($is_dateonly) {
+            $vdt['VALUE'] = 'DATE';
+        }
+        else if ($set_type) {
+            $vdt['VALUE'] = 'DATE-TIME';
+        }
+
+        // register timezone for VTIMEZONE block
+        if (!$is_utc && !$dateonly && $tz && ($tzname = $tz->getName())) {
+            $ts = $dt->format('U');
+            if (is_array($this->vtimezones[$tzname])) {
+                $this->vtimezones[$tzname][0] = min($this->vtimezones[$tzname][0], $ts);
+                $this->vtimezones[$tzname][1] = max($this->vtimezones[$tzname][1], $ts);
+            }
+            else {
+                $this->vtimezones[$tzname] = array($ts, $ts);
+            }
+        }
+
+        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] = is_array($values[$from]) ? join(',', $values[$from]) : $values[$from];
+        }
+        return $out;
+    }
+
+    /**
+     *
+     */
+    private static function parameters_array($prop)
+    {
+        $params = array();
+        foreach ($prop->parameters() as $name => $value) {
+            $params[strtoupper($name)] = strval($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
+     * @param  boolean Add VTIMEZONE block with timezone definitions for the included events
+     * @return string  Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
+     */
+    public function export($objects, $method = null, $write = false, $get_attachment = false, $with_timezones = true)
+    {
+        $this->method = $method;
+
+        // encapsulate in VCALENDAR container
+        $vcal = new VObject\Component\VCalendar();
+        $vcal->VERSION = '2.0';
+        $vcal->PRODID = $this->prodid;
+        $vcal->CALSCALE = 'GREGORIAN';
+
+        if (!empty($method)) {
+            $vcal->METHOD = $method;
+        }
+
+        // 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);
+        }
+
+        // include timezone information
+        if ($with_timezones || !empty($method)) {
+            foreach ($this->vtimezones as $tzid => $range) {
+                $vt = self::get_vtimezone($tzid, $range[0], $range[1], $vcal);
+                if (empty($vt)) {
+                    continue;  // no timezone information found
+                }
+
+                if ($write) {
+                    echo $vt->serialize();
+                }
+                else {
+                    $vcal->add($vt);
+                }
+            }
+        }
+
+        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';
+
+        $cal = $vcal ?: new VObject\Component\VCalendar();
+        $ve = $cal->create($this->type_component_map[$type]);
+        $ve->UID = $event['uid'];
+
+        // set DTSTAMP according to RFC 5545, 3.8.7.2.
+        $dtstamp = !empty($event['changed']) && empty($this->method) ? $event['changed'] : new DateTime('now', new \DateTimeZone('UTC'));
+        $ve->DTSTAMP = $this->datetime_prop($cal, 'DTSTAMP', $dtstamp, true);
+
+        // 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($this->datetime_prop($cal, 'CREATED', $event['created'], true));
+        if (!empty($event['changed']))
+            $ve->add($this->datetime_prop($cal, 'LAST-MODIFIED', $event['changed'], true));
+        if (!empty($event['start']))
+            $ve->add($this->datetime_prop($cal, 'DTSTART', $event['start'], false, (bool)$event['allday']));
+        if (!empty($event['end']))
+            $ve->add($this->datetime_prop($cal, 'DTEND',   $event['end'], false, (bool)$event['allday']));
+        if (!empty($event['due']))
+            $ve->add($this->datetime_prop($cal, 'DUE',   $event['due'], false));
+
+        // we're exporting a recurrence instance only
+        if (!$recurrence_id && $event['recurrence_date'] && $event['recurrence_date'] instanceof DateTime) {
+            $recurrence_id = $this->datetime_prop($cal, 'RECURRENCE-ID', $event['recurrence_date'], false, (bool)$event['allday']);
+            if ($event['thisandfuture'])
+                $recurrence_id->add('RANGE', 'THISANDFUTURE');
+        }
+
+        if ($recurrence_id) {
+            $ve->add($recurrence_id);
+        }
+
+        $ve->add('SUMMARY', $event['title']);
+
+        if ($event['location'])
+            $ve->add($this->is_apple() ? new vobject_location_property($cal, 'LOCATION', $event['location']) : $cal->create('LOCATION', $event['location']));
+        if ($event['description'])
+            $ve->add('DESCRIPTION', strtr($event['description'], array("\r\n" => "\n", "\r" => "\n"))); // normalize line endings
+
+        if (isset($event['sequence']))
+            $ve->add('SEQUENCE', $event['sequence']);
+
+        if ($event['recurrence'] && !$recurrence_id) {
+            $exdates = $rdates = null;
+            if (isset($event['recurrence']['EXDATE'])) {
+                $exdates = $event['recurrence']['EXDATE'];
+                unset($event['recurrence']['EXDATE']);  // don't serialize EXDATEs into RRULE value
+            }
+            if (isset($event['recurrence']['RDATE'])) {
+                $rdates = $event['recurrence']['RDATE'];
+                unset($event['recurrence']['RDATE']);  // don't serialize RDATEs into RRULE value
+            }
+
+            if ($event['recurrence']['FREQ']) {
+                $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence'], (bool)$event['allday']));
+            }
+
+            // add EXDATEs each one per line (for Thunderbird Lightning)
+            if (is_array($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($this->datetime_prop($cal, 'EXDATE', $exd, true));
+                    }
+                }
+            }
+            // add RDATEs
+            if (!empty($rdates)) {
+                foreach ((array)$rdates as $rdate) {
+                    $ve->add($this->datetime_prop($cal, 'RDATE', $rdate));
+                }
+            }
+        }
+
+        if ($event['categories']) {
+            $cat = $cal->create('CATEGORIES');
+            $cat->setParts((array)$event['categories']);
+            $ve->add($cat);
+        }
+
+        if (!empty($event['free_busy'])) {
+            $ve->add('TRANSP', $event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
+
+            // for Outlook clients we provide the X-MICROSOFT-CDO-BUSYSTATUS property
+            if (stripos($this->agent, 'outlook') !== false) {
+                $ve->add('X-MICROSOFT-CDO-BUSYSTATUS', $event['free_busy'] == 'outofoffice' ? 'OOF' : strtoupper($event['free_busy']));
+            }
+        }
+
+        if ($event['priority'])
+          $ve->add('PRIORITY', $event['priority']);
+
+        if ($event['cancelled'])
+            $ve->add('STATUS', 'CANCELLED');
+        else if ($event['free_busy'] == 'tentative')
+            $ve->add('STATUS', 'TENTATIVE');
+        else if ($event['complete'] == 100)
+            $ve->add('STATUS', 'COMPLETED');
+        else if (!empty($event['status']))
+            $ve->add('STATUS', $event['status']);
+
+        if (!empty($event['sensitivity']))
+            $ve->add('CLASS', strtoupper($event['sensitivity']));
+
+        if (!empty($event['complete'])) {
+            $ve->add('PERCENT-COMPLETE', intval($event['complete']));
+        }
+
+        // Apple iCal and BusyCal required the COMPLETED date to be set in order to consider a task complete
+        if ($event['status'] == 'COMPLETED' || $event['complete'] == 100) {
+            $ve->add($this->datetime_prop($cal, 'COMPLETED', $event['changed'] ?: new DateTime('now - 1 hour'), true));
+        }
+
+        if ($event['valarms']) {
+            foreach ($event['valarms'] as $alarm) {
+                $va = $cal->createComponent('VALARM');
+                $va->action = $alarm['action'];
+                if ($alarm['trigger'] instanceof DateTime) {
+                    $va->add($this->datetime_prop($cal, 'TRIGGER', $alarm['trigger'], true, null, true));
+                }
+                else {
+                    $alarm_props = array();
+                    if (strtoupper($alarm['related']) == 'END') {
+                        $alarm_props['RELATED'] = 'END';
+                    }
+                    $va->add('TRIGGER', $alarm['trigger'], $alarm_props);
+                }
+
+                if ($alarm['action'] == 'EMAIL') {
+                    foreach ((array)$alarm['attendees'] as $attendee) {
+                        $va->add('ATTENDEE', 'mailto:' . $attendee);
+                    }
+                }
+                if ($alarm['description']) {
+                    $va->add('DESCRIPTION', $alarm['description'] ?: $event['title']);
+                }
+                if ($alarm['summary']) {
+                    $va->add('SUMMARY', $alarm['summary']);
+                }
+                if ($alarm['duration']) {
+                    $va->add('DURATION', $alarm['duration']);
+                    $va->add('REPEAT', intval($alarm['repeat']));
+                }
+                if ($alarm['uri']) {
+                    $va->add('ATTACH', $alarm['uri'], array('VALUE' => 'URI'));
+                }
+                $ve->add($va);
+            }
+        }
+        // legacy support
+        else if ($event['alarms']) {
+            $va = $cal->createComponent('VALARM');
+            list($trigger, $va->action) = explode(':', $event['alarms']);
+            $val = libcalendaring::parse_alarm_value($trigger);
+            if ($val[3])
+                $va->add('TRIGGER', $val[3]);
+            else if ($val[0] instanceof DateTime)
+                $va->add($this->datetime_prop($cal, 'TRIGGER', $val[0], true, null, true));
+            $ve->add($va);
+        }
+
+        // Find SCHEDULE-AGENT
+        foreach ((array)$event['x-custom'] as $prop) {
+            if ($prop[0] === 'SCHEDULE-AGENT') {
+                $schedule_agent = $prop[1];
+            }
+        }
+
+        foreach ((array)$event['attendees'] as $attendee) {
+            if ($attendee['role'] == 'ORGANIZER') {
+                if (empty($event['organizer']))
+                    $event['organizer'] = $attendee;
+            }
+            else if (!empty($attendee['email'])) {
+                if (isset($attendee['rsvp']))
+                    $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
+
+                $mailto   = $attendee['email'];
+                $attendee = array_filter(self::map_keys($attendee, $this->attendee_keymap));
+
+                if ($schedule_agent !== null && !isset($attendee['SCHEDULE-AGENT'])) {
+                    $attendee['SCHEDULE-AGENT'] = $schedule_agent;
+                }
+
+                $ve->add('ATTENDEE', 'mailto:' . $mailto, $attendee);
+            }
+        }
+
+        if ($event['organizer']) {
+            $organizer = array_filter(self::map_keys($event['organizer'], $this->organizer_keymap));
+
+            if ($schedule_agent !== null && !isset($organizer['SCHEDULE-AGENT'])) {
+                $organizer['SCHEDULE-AGENT'] = $schedule_agent;
+            }
+
+            $ve->add('ORGANIZER', 'mailto:' . $event['organizer']['email'], $organizer);
+        }
+
+        foreach ((array)$event['url'] as $url) {
+            if (!empty($url)) {
+                $ve->add('URL', $url);
+            }
+        }
+
+        if (!empty($event['parent_id'])) {
+            $ve->add('RELATED-TO', $event['parent_id'], array('RELTYPE' => 'PARENT'));
+        }
+
+        if ($event['comment'])
+            $ve->add('COMMENT', $event['comment']);
+
+        $memory_limit = parse_bytes(ini_get('memory_limit'));
+
+        // 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
+                // @todo: use rcube_utils::mem_check()
+                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',
+                        $data,
+                        array_filter(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'));
+                }
+            }
+        }
+
+        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();
+        }
+
+        // append recurrence exceptions
+        if (is_array($event['recurrence']) && $event['recurrence']['EXCEPTIONS']) {
+            foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
+                $exdate = $ex['recurrence_date'] ?: $ex['start'];
+                $recurrence_id = $this->datetime_prop($cal, 'RECURRENCE-ID', $exdate, false, (bool)$event['allday']);
+                if ($ex['thisandfuture'])
+                    $recurrence_id->add('RANGE', 'THISANDFUTURE');
+                $this->_to_ical($ex, $vcal, $get_attachment, $recurrence_id);
+            }
+        }
+    }
+
+    /**
+     * Returns a VTIMEZONE component for a Olson timezone identifier
+     * with daylight transitions covering the given date range.
+     *
+     * @param string Timezone ID as used in PHP's Date functions
+     * @param integer Unix timestamp with first date/time in this timezone
+     * @param integer Unix timestap with last date/time in this timezone
+     *
+     * @return mixed A Sabre\VObject\Component object representing a VTIMEZONE definition
+     *               or false if no timezone information is available
+     */
+    public static function get_vtimezone($tzid, $from = 0, $to = 0, $cal = null)
+    {
+        if (!$from) $from = time();
+        if (!$to)   $to = $from;
+        if (!$cal)  $cal = new VObject\Component\VCalendar();
+
+        if (is_string($tzid)) {
+            try {
+                $tz = new \DateTimeZone($tzid);
+            }
+            catch (\Exception $e) {
+                return false;
+            }
+        }
+        else if (is_a($tzid, '\\DateTimeZone')) {
+            $tz = $tzid;
+        }
+
+        if (!is_a($tz, '\\DateTimeZone')) {
+            return false;
+        }
+
+        $year = 86400 * 360;
+        $transitions = $tz->getTransitions($from - $year, $to + $year);
+
+        $vt = $cal->createComponent('VTIMEZONE');
+        $vt->TZID = $tz->getName();
+
+        $std = null; $dst = null;
+        foreach ($transitions as $i => $trans) {
+            $cmp = null;
+
+            if ($i == 0) {
+                $tzfrom = $trans['offset'] / 3600;
+                continue;
+            }
+
+            if ($trans['isdst']) {
+                $t_dst = $trans['ts'];
+                $dst = $cal->createComponent('DAYLIGHT');
+                $cmp = $dst;
+            }
+            else {
+                $t_std = $trans['ts'];
+                $std = $cal->createComponent('STANDARD');
+                $cmp = $std;
+            }
+
+            if ($cmp) {
+                $dt = new DateTime($trans['time']);
+                $offset = $trans['offset'] / 3600;
+
+                $cmp->DTSTART = $dt->format('Ymd\THis');
+                $cmp->TZOFFSETFROM = sprintf('%+03d%02d', floor($tzfrom), ($tzfrom - floor($tzfrom)) * 60);
+                $cmp->TZOFFSETTO   = sprintf('%+03d%02d', floor($offset), ($offset - floor($offset)) * 60);
+
+                if (!empty($trans['abbr'])) {
+                    $cmp->TZNAME = $trans['abbr'];
+                }
+
+                $tzfrom = $offset;
+                $vt->add($cmp);
+            }
+
+            // we covered the entire date range
+            if ($std && $dst && min($t_std, $t_dst) < $from && max($t_std, $t_dst) > $to) {
+                break;
+            }
+        }
+
+        // add X-MICROSOFT-CDO-TZID if available
+        $microsoftExchangeMap = array_flip(VObject\TimeZoneUtil::$microsoftExchangeMap);
+        if (array_key_exists($tz->getName(), $microsoftExchangeMap)) {
+            $vt->add('X-MICROSOFT-CDO-TZID', $microsoftExchangeMap[$tz->getName()]);
+        }
+
+        return $vt;
+    }
+
+
+    /*** Implement PHP 5 Iterator interface to make foreach work ***/
+
+    function current()
+    {
+        return $this->objects[$this->iteratorkey];
+    }
+
+    function key()
+    {
+        return $this->iteratorkey;
+    }
+
+    function next()
+    {
+        $this->iteratorkey++;
+
+        // read next chunk if we're reading from a file
+        if (!$this->objects[$this->iteratorkey] && $this->fp) {
+            $this->_parse_next(true);
+        }
+
+        return $this->valid();
+    }
+
+    function rewind()
+    {
+        $this->iteratorkey = 0;
+    }
+
+    function valid()
+    {
+        return !empty($this->objects[$this->iteratorkey]);
+    }
+
+}
+
+
+/**
+ * Override Sabre\VObject\Property\Text that quotes commas in the location property
+ * because Apple clients treat that property as list.
+ */
+class vobject_location_property extends VObject\Property\Text
+{
+    /**
+     * List of properties that are considered 'structured'.
+     *
+     * @var array
+     */
+    protected $structuredValues = array(
+        // vCard
+        'N',
+        'ADR',
+        'ORG',
+        'GENDER',
+        'LOCATION',
+        // iCalendar
+        'REQUEST-STATUS',
+    );
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/bg_BG.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'ĐŽĐŸ';
+$labels['alarmemailoption'] = 'ЕлДĐșŃ‚Ń€ĐŸĐœĐœĐ° ĐżĐŸŃ‰Đ°';
+$labels['frequency'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€Đž';
+$labels['statusorganizer'] = 'ĐžŃ€ĐłĐ°ĐœĐžĐ·Đ°Ń‚ĐŸŃ€';
+$labels['statustentative'] = 'ĐŸŃ€Đ”ĐŽĐČĐ°Ń€ĐžŃ‚Đ”Đ»ĐœĐŸ';
+$labels['statusneeds-action'] = 'ĐŃƒĐ¶ĐœĐŸ Đ” ĐŽĐ”ĐčстĐČОД';
+$labels['statusunknown'] = 'ĐŃĐŒĐ° ĐžĐœŃ„ĐŸŃ€ĐŒĐ°Ń†ĐžŃ';
+$labels['statuscompleted'] = 'ЗаĐČŃŠŃ€ŃˆĐ”Đœ';
+$labels['statusin-process'] = 'В ĐżŃ€ĐŸŃ†Đ”Ń';
+$labels['itipcancellation'] = 'ОтĐșĐ°Đ·Đ°ĐœĐŸ:';
+$labels['itipreply'] = 'ĐžŃ‚ĐłĐŸĐČĐŸŃ€ ĐœĐ°';
+$labels['itipaccepted'] = 'ĐŸŃ€ĐžĐ”ĐŒĐ°ĐœĐ”';
+$labels['itiptentative'] = 'ĐœĐŸĐ¶Đ” бО';
+$labels['itipdeclined'] = 'ОтхĐČŃŠŃ€Đ»ŃĐœĐ”';
+$labels['itipsubjectaccepted'] = '"$title" бДшД ĐżŃ€ĐžĐ”Ń‚ĐŸ ĐŸŃ‚ $name';
+$labels['itipsubjectdeclined'] = '"$title" бДшД ĐŸŃ‚Ń…ĐČŃŠŃ€Đ»Đ”ĐœĐŸ ĐŸŃ‚ $name';
+$labels['updateattendeestatus'] = 'ĐŸĐŸĐŽĐœĐŸĐČяĐČĐ°ĐœĐ” ĐœĐ° статусът ĐœĐ° ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐ°';
+$labels['acceptinvitation'] = 'ĐŸŃ€ĐžĐ”ĐŒĐ°Ń‚Đ” лО Ń‚Đ°Đ·Đž ĐżĐŸĐșĐ°ĐœĐ°?';
+$labels['youhaveaccepted'] = 'ВОД стД прОДлО Ń‚Đ°Đ·Đž ĐżĐŸĐșĐ°ĐœĐ°';
+$labels['importtocalendar'] = 'ЗапазĐČĐ°ĐœĐ” ĐČ ĐŒĐŸŃ ĐșĐ°Đ»Đ”ĐœĐŽĐ°Ń€';
+$labels['removefromcalendar'] = 'ĐŸŃ€Đ”ĐŒĐ°Ń…ĐČĐ°ĐœĐ” ĐŸŃ‚ ĐŒĐŸŃ ĐșĐ°Đ»Đ”ĐœĐŽĐ°Ń€';
+$labels['savingdata'] = 'ЗапазĐČĐ°ĐœĐ” ĐœĐ° ĐŽĐ°ĐœĐœĐž...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/ca_ES.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,20 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'fins';
+$labels['at'] = 'a';
+$labels['alarmemailoption'] = 'Email';
+$labels['frequency'] = 'Repeteix';
+$labels['recurrencend'] = 'fins';
+$labels['statusorganizer'] = 'Organitzador';
+$labels['statustentative'] = 'Provisional';
+$labels['statusneeds-action'] = 'Necessita una acciĂł';
+$labels['statusunknown'] = 'Desconegut';
+$labels['statuscompleted'] = 'Completat';
+$labels['statusin-process'] = 'En procés';
+$labels['savingdata'] = 'S\'estan desant les dades...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/cs_CZ.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'do';
+$labels['at'] = 'v';
+$labels['alarmemail'] = 'Poslat e-mail';
+$labels['alarmdisplay'] = 'Zobrazit zprĂĄvu';
+$labels['alarmaudio'] = 'Pƙehrát zvuk';
+$labels['alarmdisplayoption'] = 'ZprĂĄva';
+$labels['alarmemailoption'] = 'E-mail';
+$labels['alarmaudiooption'] = 'Zvuk';
+$labels['alarmat'] = '$datetime';
+$labels['trigger@'] = 'dne';
+$labels['trigger-M'] = 'minut pƙed';
+$labels['trigger-H'] = 'hodin pƙed';
+$labels['trigger-D'] = 'dnƯ pƙed';
+$labels['trigger+M'] = 'minut po';
+$labels['trigger+H'] = 'hodin po';
+$labels['trigger+D'] = 'dnĆŻ po';
+$labels['addalarm'] = 'Pƙidat upozornění';
+$labels['removealarm'] = 'Odstranit upozornění';
+$labels['alarmtitle'] = 'BlĂ­ĆŸĂ­cĂ­ se udĂĄlosti';
+$labels['dismissall'] = 'ZruĆĄit vĆĄe';
+$labels['dismiss'] = 'ZruĆĄit';
+$labels['snooze'] = 'OdloĆŸit';
+$labels['repeatinmin'] = 'Zopakovat za $min minut';
+$labels['repeatinhr'] = 'Zopakovat za 1 hodinu';
+$labels['repeatinhrs'] = 'Zopakovat za $hrs hodin';
+$labels['repeattomorrow'] = 'Zopakovat zĂ­tra';
+$labels['repeatinweek'] = 'Zopakovat za tĂœden';
+$labels['showmore'] = 'UkĂĄzat vĂ­c...';
+$labels['frequency'] = 'Opakovat';
+$labels['never'] = 'nikdy';
+$labels['daily'] = 'denně';
+$labels['weekly'] = 'tĂœdně';
+$labels['monthly'] = 'měsíčně';
+$labels['yearly'] = 'ročně';
+$labels['rdate'] = 've dnech';
+$labels['every'] = 'KaĆŸdĂœ';
+$labels['days'] = 'den (dny)';
+$labels['weeks'] = 'tĂœden (tĂœdny)';
+$labels['months'] = 'měsíc(e/Ư)';
+$labels['years'] = 'rok(y/ĆŻ) v:';
+$labels['bydays'] = 'On';
+$labels['untildate'] = 'do';
+$labels['each'] = 'KaĆŸdĂœ';
+$labels['onevery'] = 'VĆŸdy v';
+$labels['onsamedate'] = 'Ve stejné datum';
+$labels['forever'] = 'trvale';
+$labels['recurrencend'] = 'do';
+$labels['untilenddate'] = 'aĆŸ do';
+$labels['forntimes'] = 'jen $nrkrĂĄt';
+$labels['first'] = 'prvnĂ­';
+$labels['second'] = 'druhĂœ';
+$labels['third'] = 'tƙetí';
+$labels['fourth'] = 'čtvrtĂœ';
+$labels['last'] = 'poslednĂ­';
+$labels['dayofmonth'] = 'Den v měsíci';
+$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'vyjma';
+$labels['statusorganizer'] = 'Poƙadatel';
+$labels['statusaccepted'] = 'Pƙijato';
+$labels['statustentative'] = 'Nezávazně';
+$labels['statusdeclined'] = 'OdmĂ­tnuto';
+$labels['statusdelegated'] = 'Svěƙeno';
+$labels['statusneeds-action'] = 'Potƙebuje činnost';
+$labels['statusunknown'] = 'NeznĂĄmĂœ';
+$labels['statuscompleted'] = 'Hotovo';
+$labels['statusin-process'] = 'RozpracovĂĄno';
+$labels['itipinvitation'] = 'PozvĂĄnĂ­ na udĂĄlost';
+$labels['itipupdate'] = 'Aktualizace udĂĄlosti';
+$labels['itipcancellation'] = 'ZruĆĄeno:';
+$labels['itipreply'] = 'Odpověď na';
+$labels['itipaccepted'] = 'Potvrdit';
+$labels['itiptentative'] = 'MoĆŸnĂĄ';
+$labels['itipdeclined'] = 'OdmĂ­tnout';
+$labels['itipdelegated'] = 'ZĂĄstupce';
+$labels['itipneeds-action'] = 'OdloĆŸit';
+$labels['itipcomment'] = 'Vaơe odpověď';
+$labels['itipeditresponse'] = 'Zadejte text odpovědi';
+$labels['itipsendercomment'] = 'PoznĂĄmka odesĂ­latele:';
+$labels['itipsuppressreply'] = 'Neposílat odpověď';
+$labels['itipobjectnotfound'] = 'Pƙedmět, na kterĂœ tato zprĂĄva odkazuje nebyl nalezen ve vaĆĄem Ășčtu.';
+$labels['itipsubjectaccepted'] = '$name potvrdil(a) Ășčas na udĂĄlosti "$title"';
+$labels['itipsubjecttentative'] = '$name nezĂĄvazně potvrdil(a) Ășčast na udĂĄlosti "$title"';
+$labels['itipsubjectdeclined'] = '$name odmĂ­tl(a) Ășčast na udĂĄlosti "$title"';
+$labels['itipsubjectin-process'] = '"$title" je zpracovĂĄvĂĄn $name';
+$labels['itipsubjectcompleted'] = '"$title" byl dokončen $name';
+$labels['itipsubjectcancel'] = 'VaĆĄe Ășčast v "$title" byla zruĆĄena';
+$labels['itipsubjectdelegated'] = '"$title" byl svěƙen $name';
+$labels['itipsubjectdelegatedto'] = '"$title" vám byl svěƙen k vyƙízení $name';
+$labels['itipnewattendee'] = 'Toto je odpověď od novĂ©ho ĂșčastnĂ­ka';
+$labels['updateattendeestatus'] = 'Aktualizovat stav ĂșčastnĂ­ka';
+$labels['acceptinvitation'] = 'Chcete pƙijmout toto pozvĂĄnĂ­ (potvrdit Ășčast)?';
+$labels['acceptattendee'] = 'Pƙijmout ĂșčastnĂ­ka';
+$labels['declineattendee'] = 'OdmĂ­tnout ĂșčastnĂ­ka';
+$labels['declineattendeeconfirm'] = 'Zadejte zprĂĄvu pro odmĂ­tnutĂ©ho ĂșčastnĂ­ka (nepovinnĂ©)';
+$labels['youhaveaccepted'] = 'Pƙijal(a) jste toto pozvání';
+$labels['youhavetentative'] = 'Nezávazně jste pƙijal(a) toto pozvání';
+$labels['youhavedeclined'] = 'OdmĂ­tl(a) jste toto pozvĂĄnĂ­';
+$labels['youhavedelegated'] = 'VyƙízenĂ­m tohoto pozvĂĄnĂ­ jste pověƙil někoho jinĂ©ho';
+$labels['youhavein-process'] = 'Pracujete na tĂ©to pƙidělenĂ© prĂĄci';
+$labels['youhavecompleted'] = 'Dokončil jste tuto pƙidělenou práci';
+$labels['youhaveneeds-action'] = 'Vaơe odpověď na toto pozvání stále čeká na vyƙízení';
+$labels['youhavepreviouslyaccepted'] = 'Pƙedtím jste toto pozvání pƙijal';
+$labels['youhavepreviouslytentative'] = 'PƙedtĂ­m jste toto pozvĂĄnĂ­ pƙedbÄ›ĆŸně pƙijal';
+$labels['youhavepreviouslydeclined'] = 'Pƙedtím jste toto pozvání odmítl';
+$labels['youhavepreviouslydelegated'] = 'Pƙedtím jste vyƙízením tohoto pozvání někoho pověƙil';
+$labels['youhavepreviouslyin-process'] = 'PƙedtĂ­m jste nahlĂĄsil, ĆŸe se na tomto Ășkolu pracuje';
+$labels['youhavepreviouslycompleted'] = 'PƙedtĂ­m jste tento Ășkol vyƙídil';
+$labels['youhavepreviouslyneeds-action'] = 'Vaơe odpověď na toto pozvání stále čeká na vyƙízení';
+$labels['attendeeaccepted'] = 'Účastník pƙijal';
+$labels['attendeetentative'] = 'ÚčastnĂ­k pƙedbÄ›ĆŸně pƙijal';
+$labels['attendeedeclined'] = 'Účastník odmítl';
+$labels['attendeedelegated'] = 'Účastník pověƙil vyƙízením $delegatedto';
+$labels['attendeein-process'] = 'ÚčastnĂ­k na tomto Ășkolu pracuje';
+$labels['attendeecompleted'] = 'ÚčastnĂ­k Ășkol vyƙídil';
+$labels['notanattendee'] = 'Nejste na seznamu ĂșčastnĂ­kĆŻ tĂ©to udĂĄlosti';
+$labels['outdatedinvitation'] = 'Toto pozvání bylo nahrazeno novějơí verzí';
+$labels['importtocalendar'] = 'UloĆŸit do kalendáƙe';
+$labels['removefromcalendar'] = 'Odstranit z kalendáƙe';
+$labels['updatemycopy'] = 'Aktualizovat moji kopii';
+$labels['openpreview'] = 'Otevƙít náhled';
+$labels['deleteobjectconfirm'] = 'Opravdu chcete smazat tento pƙedmět?';
+$labels['declinedeleteconfirm'] = 'TakĂ© chcete tento odmĂ­tnutĂœ pƙedmět smazat ze svĂ©ho Ășčtu?';
+$labels['delegateinvitation'] = 'Pověƙit vyƙízením pozvání';
+$labels['delegateto'] = 'Pověƙit';
+$labels['delegatersvpme'] = 'Informovat o aktualizacích tohoto pƙípadu';
+$labels['delegateinvalidaddress'] = 'Zadejte, prosĂ­m, platnou e-mailovou adresu tohoto zĂĄstupce.';
+$labels['savingdata'] = 'UklĂĄdĂĄm data...';
+$labels['expandattendeegroup'] = 'Nahradit členy skupiny';
+$labels['expandattendeegroupnodata'] = 'Tuto skupinu se nepodaƙilo nahradit. Nenalezeni ĆŸĂĄdnĂ­ členovĂ©.';
+$labels['expandattendeegrouperror'] = 'Tuto skupinu se nepodaƙilo nahradit. MoĆŸnĂĄ mĂĄ pƙíliĆĄ mnoho členĆŻ.';
+$labels['expandattendeegroupsizelimit'] = 'Tato skupina má pƙíliơ mnoho členƯ, a proto se ji nepodaƙilo nahradit.';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/da_DK.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'indtil';
+$labels['at'] = 'ved';
+$labels['alarmemail'] = 'Afsend e-post';
+$labels['alarmdisplay'] = 'Vis besked';
+$labels['alarmaudio'] = 'Afspil lyd';
+$labels['alarmdisplayoption'] = 'Besked';
+$labels['alarmemailoption'] = 'E-post';
+$labels['alarmaudiooption'] = 'Lyd';
+$labels['alarmat'] = 'per $datetime';
+$labels['trigger@'] = 'per dato';
+$labels['trigger-M'] = 'minutter fĂžr';
+$labels['trigger-H'] = 'timer fĂžr';
+$labels['trigger-D'] = 'dage fĂžr';
+$labels['trigger+M'] = 'minutter efter';
+$labels['trigger+H'] = 'timer efter';
+$labels['trigger+D'] = 'dage efter';
+$labels['addalarm'] = 'TilfĂžj alarm';
+$labels['removealarm'] = 'Fjern alarm';
+$labels['alarmtitle'] = 'Kommende arrangementer';
+$labels['dismissall'] = 'Afvis alle';
+$labels['dismiss'] = 'Afvis';
+$labels['snooze'] = 'Slumre';
+$labels['repeatinmin'] = 'Gentag om $min minutter';
+$labels['repeatinhr'] = 'Gentag om 1 time';
+$labels['repeatinhrs'] = 'Gentag om $hrs timer';
+$labels['repeattomorrow'] = 'Gentag i morgen';
+$labels['repeatinweek'] = 'Gentag om en uge';
+$labels['showmore'] = 'Vis mere ...';
+$labels['recurring'] = 'Gentages';
+$labels['frequency'] = 'Gentag';
+$labels['never'] = 'aldrig';
+$labels['daily'] = 'dagligt';
+$labels['weekly'] = 'ugentligt';
+$labels['monthly'] = 'mÄnedligt';
+$labels['yearly'] = 'Ă„rligt';
+$labels['rdate'] = 'pÄ datoer';
+$labels['every'] = 'Hver';
+$labels['days'] = 'dag(e)';
+$labels['weeks'] = 'uge(r)';
+$labels['months'] = 'mÄned(er)';
+$labels['years'] = 'Ă„r i:';
+$labels['bydays'] = 'Per';
+$labels['untildate'] = 'den';
+$labels['each'] = 'Hver';
+$labels['onevery'] = 'PĂ„ hver';
+$labels['onsamedate'] = 'PĂ„ samme dato';
+$labels['forever'] = 'for altid';
+$labels['recurrencend'] = 'indtil';
+$labels['untilenddate'] = 'indtil datoen';
+$labels['forntimes'] = 'for $nr gang(e)';
+$labels['first'] = 'fĂžrste';
+$labels['second'] = 'anden';
+$labels['third'] = 'tredje';
+$labels['fourth'] = 'fjerde';
+$labels['last'] = 'sidste';
+$labels['dayofmonth'] = 'Dag pÄ mÄneden';
+$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'undtagen';
+$labels['statusorganizer'] = 'Organisator';
+$labels['statusaccepted'] = 'Accepteret';
+$labels['statustentative'] = 'ForsĂžgsvis';
+$labels['statusdeclined'] = 'Afvist';
+$labels['statusneeds-action'] = 'Needs action';
+$labels['statusunknown'] = 'Ukendt';
+$labels['statuscompleted'] = 'Completed';
+$labels['statusin-process'] = 'In process';
+$labels['itipinvitation'] = 'Invitation til';
+$labels['itipupdate'] = 'Opdatering per';
+$labels['itipcancellation'] = 'Aflyst:';
+$labels['itipreply'] = 'Svar til';
+$labels['itipaccepted'] = 'Acceptér';
+$labels['itiptentative'] = 'MĂ„ske';
+$labels['itipdeclined'] = 'Afvis';
+$labels['itipdelegated'] = 'Delegér';
+$labels['itipneeds-action'] = 'UdsĂŠt';
+$labels['itipcomment'] = 'Dit svar';
+$labels['itipeditresponse'] = 'Angiv en svartekst';
+$labels['itipsendercomment'] = 'Afsenders kommentar:';
+$labels['itipsuppressreply'] = 'Undlad at sende et svar';
+$labels['itipobjectnotfound'] = 'Objektet som denne besked refererer til, blev ikke fundet i din konto.';
+$labels['itipsubjectaccepted'] = '"$title" er blevet accepteret af $name';
+$labels['itipsubjecttentative'] = '"$title" er blevet forsĂžgsvist accepteret af $name';
+$labels['itipsubjectdeclined'] = '"$title" af blevet afvist af $name';
+$labels['itipsubjectin-process'] = '"$title" er en igangvĂŠrende proces hos $name';
+$labels['itipsubjectcompleted'] = '"$title" blev fuldfĂžrt af $name';
+$labels['itipsubjectcancel'] = 'Din deltagelse i "$title" er blevet annulleret';
+$labels['itipsubjectdelegated'] = '"$title" er blevet delegeret af $name';
+$labels['itipsubjectdelegatedto'] = '"$title" er blevet delegeret til dig af $name';
+$labels['itipnewattendee'] = 'Dette er et svar fra en ny deltager';
+$labels['updateattendeestatus'] = 'Opdatér status for deltagere';
+$labels['acceptinvitation'] = 'Vil du acceptere denne invitation?';
+$labels['acceptattendee'] = 'Acceptér deltager';
+$labels['declineattendee'] = 'Afvis deltager';
+$labels['declineattendeeconfirm'] = 'Indtast en besked til den afviste deltager (valgfrit):';
+$labels['rsvpmodeall'] = 'Hele serien';
+$labels['rsvpmodecurrent'] = 'Kun denne forekomst';
+$labels['rsvpmodefuture'] = 'Denne og fremtidige forekomster';
+$labels['itipsingleoccurrence'] = 'Dette er en <em>enkelt forekomst</em> ud af en serie af flere begivenheder';
+$labels['itipfutureoccurrence'] = 'Refererer til <em>denne og alle fremtidige forekomster</em> af en serie af begivenheder';
+$labels['itipmessagesingleoccurrence'] = 'Denne besked refererer kun til denne enkelte forekomst';
+$labels['itipmessagefutureoccurrence'] = 'Denne besked refererer til denne og alle fremtidige forekomster';
+$labels['youhaveaccepted'] = 'Du har accepteret denne invitation';
+$labels['youhavetentative'] = 'Du har forsĂžgsvist accepteret denne invitation';
+$labels['youhavedeclined'] = 'Du har afvist denne invitation';
+$labels['youhavedelegated'] = 'Du har delegeret denne invitation';
+$labels['youhavein-process'] = 'Du arbejder pÄ denne tildelte opgave';
+$labels['youhavecompleted'] = 'Du har afsluttet denne tildelte opgave';
+$labels['youhaveneeds-action'] = 'Dit svar til denne invitation afventes stadig';
+$labels['youhavepreviouslyaccepted'] = 'Du har tidligere accepteret denne invitation';
+$labels['youhavepreviouslytentative'] = 'Du har tidligere accepteret denne invitation tentativt';
+$labels['youhavepreviouslydeclined'] = 'Du har tidligere afvist denne invitation';
+$labels['youhavepreviouslydelegated'] = 'Du har tidligere delegeret denne invitation';
+$labels['youhavepreviouslyin-process'] = 'Du har tidligere rapporteret, at du vil arbejdet pÄ denne tildelte opgave';
+$labels['youhavepreviouslycompleted'] = 'Du har tidligere afsluttet denne tildelte opgave';
+$labels['youhavepreviouslyneeds-action'] = 'Du svar til denne invitation afventes stadig';
+$labels['attendeeaccepted'] = 'Deltageren er blevet accepteret';
+$labels['attendeetentative'] = 'Deltageren er tentativt blevet accepteret';
+$labels['attendeedeclined'] = 'Deltageren er blevet afvist';
+$labels['attendeedelegated'] = 'Deltageren har delegeret til $delegatedto';
+$labels['attendeein-process'] = 'Deltager i under behandling';
+$labels['attendeecompleted'] = 'Deltageren er afsluttet';
+$labels['notanattendee'] = 'Du har ikke opfĂžrt en deltager for dette objekt';
+$labels['outdatedinvitation'] = 'Denne invitation er blevet erstattet af en nyere version';
+$labels['importtocalendar'] = 'Gem i min kalender';
+$labels['removefromcalendar'] = 'Fjern fra min kalender';
+$labels['updatemycopy'] = 'Opdatér min kopi';
+$labels['openpreview'] = 'Åbn forhĂ„ndsvisning';
+$labels['deleteobjectconfirm'] = 'Sikker pÄ at du vil slette dette objekt?';
+$labels['declinedeleteconfirm'] = 'Vil du ogsÄ slette dette afviste objekt fra din konto?';
+$labels['delegateinvitation'] = 'Delegér invitation';
+$labels['delegateto'] = 'Delegér til';
+$labels['delegatersvpme'] = 'Hold mig orienteret om opdateringer vedrĂžrende denne hĂŠndelse';
+$labels['delegateinvalidaddress'] = 'Angiv venligst en gyldig e-mailadresse for den delegerede';
+$labels['savingdata'] = 'Gemmer data...';
+$labels['expandattendeegroup'] = 'Substituér med gruppemedlemmer';
+$labels['expandattendeegroupnodata'] = 'Kan ikke substituere denne gruppe. Fandt ikke medlemmer.';
+$labels['expandattendeegrouperror'] = 'Kan ikke substituere denne gruppe. Den kan indeholde for mange medlemmer.';
+$labels['expandattendeegroupsizelimit'] = 'Denne gruppe indeholder for mange medlemmer til substituering.';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/de_CH.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'bis';
+$labels['at'] = 'um';
+$labels['alarmemail'] = 'E-Mail senden';
+$labels['alarmdisplay'] = 'Nachricht anzeigen';
+$labels['alarmaudio'] = 'Audio abspielen';
+$labels['alarmdisplayoption'] = 'Nachricht';
+$labels['alarmemailoption'] = 'E-Mail';
+$labels['alarmaudiooption'] = 'Audio';
+$labels['alarmat'] = 'um $datetime';
+$labels['trigger@'] = 'genau am';
+$labels['trigger-M'] = 'Minuten davor';
+$labels['trigger-H'] = 'Stunden davor';
+$labels['trigger-D'] = 'Tage davor';
+$labels['trigger+M'] = 'Minuten danach';
+$labels['trigger+H'] = 'Stunden danach';
+$labels['trigger+D'] = 'Tage danach';
+$labels['triggerend-M'] = 'Minuten vor dem Ende';
+$labels['triggerend-H'] = 'Stunden vor dem Ende';
+$labels['triggerend-D'] = 'Tage vor dem Ende';
+$labels['triggerend+M'] = 'Minuten nach dem Ende';
+$labels['triggerend+H'] = 'Stunden nach dem Ende';
+$labels['triggerend+D'] = 'Tage nach dem Ende';
+$labels['trigger0'] = 'um exakt';
+$labels['triggerattime'] = 'zur Startzeit';
+$labels['triggerattimeend'] = 'zur Endzeit';
+$labels['relatedstart'] = 'Beginn';
+$labels['relatedendevent'] = 'Ende';
+$labels['relatedendtask'] = 'Frist';
+$labels['addalarm'] = 'Erinnerung hinzufĂŒgen';
+$labels['removealarm'] = 'Erinnerung entfernen';
+$labels['alarmtitle'] = 'Anstehende Termine';
+$labels['dismissall'] = 'Alle ignorieren';
+$labels['dismiss'] = 'Ignorieren';
+$labels['snooze'] = 'SpÀter erinnern';
+$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
+$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
+$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
+$labels['repeattomorrow'] = 'Wiederholung morgen';
+$labels['repeatinweek'] = 'Wiederholung in einer Woche';
+$labels['showmore'] = 'Mehr anzeigen...';
+$labels['recurring'] = 'Wiederholungen';
+$labels['frequency'] = 'Wiederholung';
+$labels['never'] = 'nie';
+$labels['daily'] = 'tÀglich';
+$labels['weekly'] = 'wöchentlich';
+$labels['monthly'] = 'monatlich';
+$labels['yearly'] = 'jÀhrlich';
+$labels['rdate'] = 'genau um';
+$labels['every'] = 'Alle';
+$labels['days'] = 'Tag(e)';
+$labels['weeks'] = 'Woche(n)';
+$labels['months'] = 'Monat(e)';
+$labels['years'] = 'Jahre(e) im:';
+$labels['bydays'] = 'Am';
+$labels['untildate'] = 'am';
+$labels['each'] = 'Jeden';
+$labels['onevery'] = 'An jedem';
+$labels['onsamedate'] = 'Am gleichen Tag';
+$labels['forever'] = 'unendlich';
+$labels['recurrencend'] = 'bis';
+$labels['untilenddate'] = 'bis am';
+$labels['forntimes'] = '$nr Wiederholungen';
+$labels['first'] = 'erster';
+$labels['second'] = 'zweiter';
+$labels['third'] = 'dritter';
+$labels['fourth'] = 'vierter';
+$labels['last'] = 'letzter';
+$labels['dayofmonth'] = 'Tag des Montats';
+$labels['addrdate'] = 'Datum hinzufĂŒgen';
+$labels['except'] = 'ausser';
+$labels['statusorganizer'] = 'Organisator';
+$labels['statusaccepted'] = 'Akzeptiert';
+$labels['statustentative'] = 'Mit Vorbehalt';
+$labels['statusdeclined'] = 'Abgelehnt';
+$labels['statusdelegated'] = 'Delegiert';
+$labels['statusneeds-action'] = 'Braucht Aktion';
+$labels['statusunknown'] = 'Unbekannt';
+$labels['statuscompleted'] = 'Abgeschlossen';
+$labels['statusin-process'] = 'In Bearbeitung';
+$labels['itipinvitation'] = 'Einladung zu';
+$labels['itipupdate'] = 'Aktialisiert:';
+$labels['itipcancellation'] = 'Abgesagt:';
+$labels['itipreply'] = 'Antwort zu';
+$labels['itipaccepted'] = 'Akzeptieren';
+$labels['itiptentative'] = 'Mit Vorbehalt';
+$labels['itipdeclined'] = 'Ablehnen';
+$labels['itipdelegated'] = 'Vertreter';
+$labels['itipneeds-action'] = 'Aufschieben';
+$labels['itipcomment'] = 'Ihre Antwort';
+$labels['itipeditresponse'] = 'Antwort eingeben';
+$labels['itipsendercomment'] = 'Kommentar des Absenders:';
+$labels['itipsuppressreply'] = 'Keine Antwort senden';
+$labels['itipobjectnotfound'] = 'Das Objekt auf welches sich diese Nachricht bezieht, wurde in Ihrem Konto nicht gefunden.';
+$labels['itipsubjectaccepted'] = 'Einladung zu "$title" wurde von $name angenommen';
+$labels['itipsubjecttentative'] = 'Einladung zu "$title" wurde von $name mit Vorbehalt angenommen';
+$labels['itipsubjectdeclined'] = 'Einladung zu "$title" wurde von $name abgelehnt';
+$labels['itipsubjectin-process'] = '"$title" ist nun in Bearbeitung von $name';
+$labels['itipsubjectcompleted'] = '"$title" wurde vom $name fertiggestellt';
+$labels['itipsubjectcancel'] = 'Ihre Teilnahme in "$title" wurde aufgehoben';
+$labels['itipsubjectdelegated'] = '"$title" wurde durch $name delegiert';
+$labels['itipsubjectdelegatedto'] = '"$title" wurde durch $name an Sie delegiert';
+$labels['itipnewattendee'] = 'Dies ist eine Antwort von einem neuen Teilnehmer';
+$labels['updateattendeestatus'] = 'Teilnehmerstatus aktualisieren';
+$labels['acceptinvitation'] = 'Möchten Sie die Einladung zu diesem Termin annehmen?';
+$labels['acceptattendee'] = 'Teilnehmer akzeptieren';
+$labels['declineattendee'] = 'Teilnehmer ablehnen';
+$labels['declineattendeeconfirm'] = 'Nachricht an den abgelehnten Teilnehmer verfassen (optional):';
+$labels['rsvpmodeall'] = 'Die gesamte Reihe';
+$labels['rsvpmodecurrent'] = 'Nur dieses Ereignis';
+$labels['rsvpmodefuture'] = 'Dieses und zukĂŒnftige Ereignisse';
+$labels['itipsingleoccurrence'] = 'Dieses ist eine <em>einzelne Wiederholung</emaußerhalb einer Serie von Ereignissen';
+$labels['itipfutureoccurrence'] = 'Bezieht sich auf <em>diese and alle zukĂŒnftigen Wiederholungen</emeiner Serie von Ereignissen';
+$labels['itipmessagesingleoccurrence'] = 'Diese Nachricht bezieht sich nur auf eine einzelne Wiederholung';
+$labels['itipmessagefutureoccurrence'] = 'Diese Nachricht bezieht sich auf diese und alle zukĂŒnftigen Wiederholungen';
+$labels['youhaveaccepted'] = 'Sie haben die Einladung angenommen';
+$labels['youhavetentative'] = 'Sie haben die Einladung mit Vorbehalt angenommen';
+$labels['youhavedeclined'] = 'Sie haben die Einladung abgelehnt';
+$labels['youhavedelegated'] = 'Sie haben diese Einladung abgelehnt';
+$labels['youhavein-process'] = 'Sie arbeiten an dieser Aufgabe';
+$labels['youhavecompleted'] = 'Sie haben diese Aufgabe erledigt';
+$labels['youhaveneeds-action'] = 'Ihre Antwort auf diese Einladung ist noch ausstehend';
+$labels['youhavepreviouslyaccepted'] = 'Sie haben diese Einladung zuvor angenommen';
+$labels['youhavepreviouslytentative'] = 'Sie haben diese Einladung zuvor mit Vorbehalt angenommen';
+$labels['youhavepreviouslydeclined'] = 'Sie haben diese Einladung zuvor abgelehnt';
+$labels['youhavepreviouslydelegated'] = 'Sie haben diese Einladung zuvor delegiert';
+$labels['youhavepreviouslyin-process'] = 'Sie haben diese Aufgabe zuvor als In Bearbeitung gemeldet';
+$labels['youhavepreviouslycompleted'] = 'Sie haben diese Einladung zuvor erledigt';
+$labels['youhavepreviouslyneeds-action'] = 'Ihre Antwort auf diese Einladung ist noch ausstehend';
+$labels['attendeeaccepted'] = 'Teilnehmer hat akzeptiert';
+$labels['attendeetentative'] = 'Teilnehmer hat mit Vorbehalt akzeptiert';
+$labels['attendeedeclined'] = 'Teilnehmer hat abgelehnt';
+$labels['attendeedelegated'] = 'Teilnehmer hat an $delegatedto delegiert';
+$labels['attendeein-process'] = 'Teilnehmer arbeitet an dieser Aufgabe';
+$labels['attendeecompleted'] = 'Teilnehmer hat die Aufgabe erledigt';
+$labels['notanattendee'] = 'Sie sind nicht in der Liste der Teilnehmer aufgefĂŒhrt';
+$labels['outdatedinvitation'] = 'Diese Einladung wurde durch eine neuere Version ersetzt';
+$labels['importtocalendar'] = 'In Kalender ĂŒbernehmen';
+$labels['removefromcalendar'] = 'Aus meinem Kalender löschen';
+$labels['updatemycopy'] = 'Meine Kopie aktualisieren';
+$labels['openpreview'] = 'Vorschau öffnen';
+$labels['deleteobjectconfirm'] = 'Möchten Sie dieses Objekt wirklich löschen?';
+$labels['declinedeleteconfirm'] = 'Soll das abgelehnte Objekt ebenfalls aus Ihrem Konto gelöscht werden?';
+$labels['delegateinvitation'] = 'Einladung delegieren';
+$labels['delegateto'] = 'Delegieren an';
+$labels['delegatersvpme'] = 'Informiere mich ĂŒber Aktualisierungen dieses Termins';
+$labels['delegateinvalidaddress'] = 'Geben Sie eine gĂŒltige E-Mail-Adresse fĂŒr den Delegierten ein';
+$labels['savingdata'] = 'Speichere...';
+$labels['expandattendeegroup'] = 'Mit Gruppenmitgliedern ersetzen';
+$labels['expandattendeegroupnodata'] = 'Diese Gruppe konnte nicht ersetzt werden. Keine Gruppenmitglieder gefunden.';
+$labels['expandattendeegrouperror'] = 'Diese Gruppe konnte nicht ersetzt werden. Sie hat möglicherweise zuviele Mitglieder.';
+$labels['expandattendeegroupsizelimit'] = 'Die Gruppe hat zuviele Mitglieder und konnte deshalb nicht ersetzt werden.';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/de_DE.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'bis';
+$labels['at'] = 'um';
+$labels['alarmemail'] = 'E-Mail senden';
+$labels['alarmdisplay'] = 'Nachricht anzeigen';
+$labels['alarmaudio'] = 'Audio abspielen';
+$labels['alarmdisplayoption'] = 'Nachricht';
+$labels['alarmemailoption'] = 'E-Mail';
+$labels['alarmaudiooption'] = 'Audio';
+$labels['alarmat'] = 'um $datetime';
+$labels['trigger@'] = 'genau am';
+$labels['trigger-M'] = 'Minuten davor';
+$labels['trigger-H'] = 'Stunden davor';
+$labels['trigger-D'] = 'Tage davor';
+$labels['trigger+M'] = 'Minuten danach';
+$labels['trigger+H'] = 'Stunden danach';
+$labels['trigger+D'] = 'Tage danach';
+$labels['triggerend-M'] = 'Minuten vor dem Ende';
+$labels['triggerend-H'] = 'Stunden vor dem Ende';
+$labels['triggerend-D'] = 'Tage vor dem Ende';
+$labels['triggerend+M'] = 'Minuten nach dem Ende';
+$labels['triggerend+H'] = 'Stunden nach dem Ende';
+$labels['triggerend+D'] = 'Tage nach dem Ende';
+$labels['trigger0'] = 'um exakt';
+$labels['triggerattime'] = 'zur Startzeit';
+$labels['triggerattimeend'] = 'zur Endzeit';
+$labels['relatedstart'] = 'Beginn';
+$labels['relatedendevent'] = 'Ende';
+$labels['relatedendtask'] = 'FĂ€llig';
+$labels['addalarm'] = 'Erinnerung hinzufĂŒgen';
+$labels['removealarm'] = 'Erinnerung entfernen';
+$labels['alarmtitle'] = 'Anstehende Termine';
+$labels['dismissall'] = 'Alle ignorieren';
+$labels['dismiss'] = 'Ignorieren';
+$labels['snooze'] = 'SpÀter erinnern';
+$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
+$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
+$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
+$labels['repeattomorrow'] = 'Wiederholung morgen';
+$labels['repeatinweek'] = 'Wiederholung in einer Woche';
+$labels['showmore'] = 'Mehr anzeigen...';
+$labels['recurring'] = 'Wiederholungen';
+$labels['frequency'] = 'Wiederholung';
+$labels['never'] = 'nie';
+$labels['daily'] = 'tÀglich';
+$labels['weekly'] = 'wöchentlich';
+$labels['monthly'] = 'monatlich';
+$labels['yearly'] = 'jÀhrlich';
+$labels['rdate'] = 'genau am';
+$labels['every'] = 'Alle';
+$labels['days'] = 'Tag(e)';
+$labels['weeks'] = 'Woche(n)';
+$labels['months'] = 'Monat(e)';
+$labels['years'] = 'Jahre(e) im:';
+$labels['bydays'] = 'Am';
+$labels['untildate'] = 'am';
+$labels['each'] = 'Jeden';
+$labels['onevery'] = 'An jedem';
+$labels['onsamedate'] = 'Am gleichen Tag';
+$labels['forever'] = 'unendlich';
+$labels['recurrencend'] = 'bis';
+$labels['untilenddate'] = 'bis am';
+$labels['forntimes'] = '$nr Wiederholungen';
+$labels['first'] = 'erster';
+$labels['second'] = 'zweiter';
+$labels['third'] = 'dritter';
+$labels['fourth'] = 'vierter';
+$labels['last'] = 'letzter';
+$labels['dayofmonth'] = 'Tag des Montats';
+$labels['addrdate'] = 'Datum hinzufĂŒgen';
+$labels['except'] = 'ausser';
+$labels['statusorganizer'] = 'Organisator';
+$labels['statusaccepted'] = 'Akzeptiert';
+$labels['statustentative'] = 'Mit Vorbehalt';
+$labels['statusdeclined'] = 'Abgelehnt';
+$labels['statusdelegated'] = 'Delegiert';
+$labels['statusneeds-action'] = 'Braucht Aktion';
+$labels['statusunknown'] = 'Unbekannt';
+$labels['statuscompleted'] = 'Abgeschlossen';
+$labels['statusin-process'] = 'In Bearbeitung';
+$labels['itipinvitation'] = 'Einladung zu';
+$labels['itipupdate'] = 'Aktialisiert:';
+$labels['itipcancellation'] = 'Abgesagt:';
+$labels['itipreply'] = 'Antwort zu';
+$labels['itipaccepted'] = 'Akzeptieren';
+$labels['itiptentative'] = 'Mit Vorbehalt';
+$labels['itipdeclined'] = 'Ablehnen';
+$labels['itipdelegated'] = 'Delegieren';
+$labels['itipneeds-action'] = 'Aufschieben';
+$labels['itipcomment'] = 'Ihre Antwort';
+$labels['itipeditresponse'] = 'Antwort eingeben';
+$labels['itipsendercomment'] = 'Kommentar des Absenders:';
+$labels['itipsuppressreply'] = 'Keine Antwort senden';
+$labels['itipobjectnotfound'] = 'Das Objekt auf welches sich diese Nachricht bezieht, wurde in Ihrem Konto nicht gefunden.';
+$labels['itipsubjectaccepted'] = 'Einladung zu "$title" wurde von $name angenommen';
+$labels['itipsubjecttentative'] = 'Einladung zu "$title" wurde von $name mit Vorbehalt angenommen';
+$labels['itipsubjectdeclined'] = 'Einladung zu "$title" wurde von $name abgelehnt';
+$labels['itipsubjectin-process'] = '"$title" ist nun in Bearbeitung von $name';
+$labels['itipsubjectcompleted'] = '"$title" wurde vom $name fertiggestellt';
+$labels['itipsubjectcancel'] = 'Ihre Teilnahme in "$title" wurde aufgehoben';
+$labels['itipsubjectdelegated'] = '"$title" wurde durch $name delegiert';
+$labels['itipsubjectdelegatedto'] = '"$title" wurde durch $name an Sie delegiert';
+$labels['itipnewattendee'] = 'Dies ist eine Antwort von einem neuen Teilnehmer';
+$labels['updateattendeestatus'] = 'Teilnehmerstatus aktualisieren';
+$labels['acceptinvitation'] = 'Möchten Sie die Einladung zu diesem Termin annehmen?';
+$labels['acceptattendee'] = 'Teilnehmer akzeptieren';
+$labels['declineattendee'] = 'Teilnehmer ablehnen';
+$labels['declineattendeeconfirm'] = 'Nachricht an den abgelehnten Teilnehmer verfassen (optional):';
+$labels['rsvpmodeall'] = 'Die gesamte Reihe';
+$labels['rsvpmodecurrent'] = 'Nur dieses Ereignis';
+$labels['rsvpmodefuture'] = 'Dieses und zukĂŒnftige Ereignisse';
+$labels['itipsingleoccurrence'] = 'Dieses ist eine <em>einzelne Wiederholung</em> einer Serie von Ereignissen';
+$labels['itipfutureoccurrence'] = 'Bezieht sich auf <em>diese and alle zukĂŒnftigen Wiederholungen</em> einer Serie von Ereignissen';
+$labels['itipmessagesingleoccurrence'] = 'Diese Nachricht bezieht sich nur auf eine einzelne Wiederholung';
+$labels['itipmessagefutureoccurrence'] = 'Diese Nachricht bezieht sich auf diese und alle zukĂŒnftigen Wiederholungen';
+$labels['youhaveaccepted'] = 'Sie haben die Einladung angenommen';
+$labels['youhavetentative'] = 'Sie haben die Einladung mit Vorbehalt angenommen';
+$labels['youhavedeclined'] = 'Sie haben die Einladung abgelehnt';
+$labels['youhavedelegated'] = 'Sie haben diese Einladung abgelehnt';
+$labels['youhavein-process'] = 'Sie arbeiten an dieser Aufgabe';
+$labels['youhavecompleted'] = 'Sie haben diese Aufgabe erledigt';
+$labels['youhaveneeds-action'] = 'Ihre Antwort auf diese Einladung ist noch ausstehend';
+$labels['youhavepreviouslyaccepted'] = 'Sie haben diese Einladung zuvor angenommen';
+$labels['youhavepreviouslytentative'] = 'Sie haben diese Einladung zuvor mit Vorbehalt angenommen';
+$labels['youhavepreviouslydeclined'] = 'Sie haben diese Einladung zuvor abgelehnt';
+$labels['youhavepreviouslydelegated'] = 'Sie haben diese Einladung zuvor delegiert';
+$labels['youhavepreviouslyin-process'] = 'Sie haben diese Aufgabe zuvor als In Bearbeitung gemeldet';
+$labels['youhavepreviouslycompleted'] = 'Sie haben diese Einladung zuvor erledigt';
+$labels['youhavepreviouslyneeds-action'] = 'Ihre Antwort auf diese Einladung ist noch ausstehend';
+$labels['attendeeaccepted'] = 'Teilnehmer hat akzeptiert';
+$labels['attendeetentative'] = 'Teilnehmer hat mit Vorbehalt akzeptiert';
+$labels['attendeedeclined'] = 'Teilnehmer hat abgelehnt';
+$labels['attendeedelegated'] = 'Teilnehmer hat an $delegatedto delegiert';
+$labels['attendeein-process'] = 'Teilnehmer arbeitet an dieser Aufgabe';
+$labels['attendeecompleted'] = 'Teilnehmer hat die Aufgabe erledigt';
+$labels['notanattendee'] = 'Sie sind nicht in der Liste der Teilnehmer aufgefĂŒhrt';
+$labels['outdatedinvitation'] = 'Diese Einladung wurde durch eine neuere Version ersetzt';
+$labels['importtocalendar'] = 'In Kalender ĂŒbernehmen';
+$labels['removefromcalendar'] = 'Aus meinem Kalender löschen';
+$labels['updatemycopy'] = 'Meine Kopie aktualisieren';
+$labels['openpreview'] = 'Vorschau öffnen';
+$labels['deleteobjectconfirm'] = 'Möchten Sie dieses Objekt wirklich löschen?';
+$labels['declinedeleteconfirm'] = 'Soll das abgelehnte Objekt ebenfalls aus Ihrem Konto gelöscht werden?';
+$labels['delegateinvitation'] = 'Einladung delegieren';
+$labels['delegateto'] = 'Delegieren an';
+$labels['delegatersvpme'] = 'Informiere mich ĂŒber Aktualisierungen dieses Termins';
+$labels['delegateinvalidaddress'] = 'Geben Sie eine gĂŒltige E-Mail-Adresse fĂŒr den Delegierten ein';
+$labels['savingdata'] = 'Speichere...';
+$labels['expandattendeegroup'] = 'Mit Gruppenmitgliedern ersetzen';
+$labels['expandattendeegroupnodata'] = 'Diese Gruppe konnte nicht ersetzt werden. Keine Gruppenmitglieder gefunden.';
+$labels['expandattendeegrouperror'] = 'Diese Gruppe konnte nicht ersetzt werden. Sie hat möglicherweise zuviele Mitglieder.';
+$labels['expandattendeegroupsizelimit'] = 'Die Gruppe hat zuviele Mitglieder und konnte deshalb nicht ersetzt werden.';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/en_US.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,186 @@
+<?php
+
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+
+$labels = array();
+
+// words for spoken dates
+$labels['until'] = 'until';
+$labels['at'] = 'at';
+
+// alarms related labels
+$labels['alarmemail'] = 'Send Email';
+$labels['alarmdisplay'] = 'Show message';
+$labels['alarmaudio'] = 'Play sound';
+$labels['alarmdisplayoption'] = 'Message';
+$labels['alarmemailoption'] = 'Email';
+$labels['alarmaudiooption'] = 'Sound';
+$labels['alarmat'] = 'at $datetime';
+$labels['trigger@'] = 'on date';
+$labels['trigger-M'] = 'minutes before';
+$labels['trigger-H'] = 'hours before';
+$labels['trigger-D'] = 'days before';
+$labels['trigger+M'] = 'minutes after';
+$labels['trigger+H'] = 'hours after';
+$labels['trigger+D'] = 'days after';
+$labels['triggerend-M'] = 'minutes before end';
+$labels['triggerend-H'] = 'hours before end';
+$labels['triggerend-D'] = 'days before end';
+$labels['triggerend+M'] = 'minutes after end';
+$labels['triggerend+H'] = 'hours after end';
+$labels['triggerend+D'] = 'days after end';
+$labels['trigger0'] = 'on time';
+$labels['triggerattime'] = 'at start time';
+$labels['triggerattimeend'] = 'at end time';
+$labels['relatedstart'] = 'start';
+$labels['relatedendevent'] = 'end';
+$labels['relatedendtask'] = 'due time';
+$labels['addalarm'] = 'Add alarm';
+$labels['removealarm'] = 'Remove alarm';
+
+$labels['alarmtitle'] = 'Upcoming events';
+$labels['dismissall'] = 'Dismiss all';
+$labels['dismiss'] = 'Dismiss';
+$labels['snooze'] = 'Snooze';
+$labels['repeatinmin'] = 'Repeat in $min minutes';
+$labels['repeatinhr'] = 'Repeat in 1 hour';
+$labels['repeatinhrs'] = 'Repeat in $hrs hours';
+$labels['repeattomorrow'] = 'Repeat tomorrow';
+$labels['repeatinweek'] = 'Repeat in a week';
+
+$labels['showmore'] = 'Show more...';
+
+// recurrence related labels
+$labels['recurring'] = 'Repeats';
+$labels['frequency'] = 'Repeat';
+$labels['never'] = 'never';
+$labels['daily'] = 'daily';
+$labels['weekly'] = 'weekly';
+$labels['monthly'] = 'monthly';
+$labels['yearly'] = 'annually';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'Every';
+$labels['days'] = 'day(s)';
+$labels['weeks'] = 'week(s)';
+$labels['months'] = 'month(s)';
+$labels['years'] = 'year(s)';
+$labels['bydays'] = 'On';
+$labels['untildate'] = 'the';
+$labels['each'] = 'Each';
+$labels['onevery'] = 'On every';
+$labels['onsamedate'] = 'On the same date';
+$labels['forever'] = 'forever';
+$labels['recurrencend'] = 'until';
+$labels['untilenddate'] = 'until date';
+$labels['forntimes'] = 'for $nr time(s)';
+$labels['first'] = 'first';
+$labels['second'] = 'second';
+$labels['third'] = 'third';
+$labels['fourth'] = 'fourth';
+$labels['last'] = 'last';
+$labels['dayofmonth'] = 'Day of month';
+$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'except';
+
+$labels['statusorganizer'] = 'Organizer';
+$labels['statusaccepted'] = 'Accepted';
+$labels['statustentative'] = 'Tentative';
+$labels['statusdeclined'] = 'Declined';
+$labels['statusdelegated'] = 'Delegated';
+$labels['statusneeds-action'] = 'Needs action';
+$labels['statusunknown'] = 'Unknown';
+$labels['statuscompleted'] = 'Completed';
+$labels['statusin-process'] = 'In process';
+
+// itip related labels
+$labels['itipinvitation'] = 'Invitation to';
+$labels['itipupdate'] = 'Update of';
+$labels['itipcancellation'] = 'Cancelled:';
+$labels['itipreply'] = 'Reply to';
+$labels['itipaccepted'] = 'Accept';
+$labels['itiptentative'] = 'Maybe';
+$labels['itipdeclined'] = 'Decline';
+$labels['itipdelegated'] = 'Delegate';
+$labels['itipneeds-action'] = 'Postpone';
+$labels['itipcomment'] = 'Your response';
+$labels['itipeditresponse'] = 'Enter a response text';
+$labels['itipsendercomment'] = 'Sender\'s comment: ';
+$labels['itipsuppressreply'] = 'Do not send a response';
+
+$labels['itipobjectnotfound'] = 'The object referred by this message was not found in your account.';
+$labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
+$labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
+$labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
+$labels['itipsubjectin-process'] = '"$title" is in-process by $name';
+$labels['itipsubjectcompleted'] = '"$title" was completed by $name';
+$labels['itipsubjectcancel'] = 'Your participation in "$title" has been cancelled';
+$labels['itipsubjectdelegated'] = '"$title" has been delegated by $name';
+$labels['itipsubjectdelegatedto'] = '"$title" has been delegated to you by $name';
+
+$labels['itipnewattendee'] = 'This is a reply from a new participant';
+$labels['updateattendeestatus'] = 'Update the participant\'s status';
+$labels['acceptinvitation'] = 'Do you accept this invitation?';
+$labels['acceptattendee'] = 'Accept participant';
+$labels['declineattendee'] = 'Decline participant';
+$labels['declineattendeeconfirm'] = 'Enter a message to the declined participant (optional):';
+$labels['rsvpmodeall'] = 'The entire series';
+$labels['rsvpmodecurrent'] = 'This occurrence only';
+$labels['rsvpmodefuture'] = 'This and future occurrences';
+
+$labels['itipsingleoccurrence'] = 'This is a <em>single occurrence</em> out of a series of events';
+$labels['itipfutureoccurrence'] = 'Refers to <em>this and all future occurrences</em> of a series of events';
+$labels['itipmessagesingleoccurrence'] = 'The message only refers to this single occurrence';
+$labels['itipmessagefutureoccurrence'] = 'The message refers to this and all future occurrences';
+
+$labels['youhaveaccepted'] = 'You have accepted this invitation';
+$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
+$labels['youhavedeclined'] = 'You have declined this invitation';
+$labels['youhavedelegated'] = 'You have delegated this invitation';
+$labels['youhavein-process'] = 'You are working on this assignment';
+$labels['youhavecompleted'] = 'You have completed this assignment';
+$labels['youhaveneeds-action'] = 'Your response to this invitation is still pending';
+
+$labels['youhavepreviouslyaccepted'] = 'You have previously accepted this invitation';
+$labels['youhavepreviouslytentative'] = 'You have previously accepted this invitation tentatively';
+$labels['youhavepreviouslydeclined'] = 'You have previously declined this invitation';
+$labels['youhavepreviouslydelegated'] = 'You have previously delegated this invitation';
+$labels['youhavepreviouslyin-process'] = 'You have previously reported to work on this assignment';
+$labels['youhavepreviouslycompleted'] = 'You have previously completed this assignment';
+$labels['youhavepreviouslyneeds-action'] = 'Your response to this invitation is still pending';
+
+$labels['attendeeaccepted'] = 'Participant has accepted';
+$labels['attendeetentative'] = 'Participant has tentatively accepted';
+$labels['attendeedeclined'] = 'Participant has declined';
+$labels['attendeedelegated'] = 'Participant has delegated to $delegatedto';
+$labels['attendeein-process'] = 'Participant is in-process';
+$labels['attendeecompleted'] = 'Participant has completed';
+$labels['notanattendee'] = 'You\'re not listed as an attendee of this object';
+$labels['outdatedinvitation'] = 'This invitation has been replaced by a newer version';
+
+$labels['importtocalendar'] = 'Save to my calendar';
+$labels['removefromcalendar'] = 'Remove from my calendar';
+$labels['updatemycopy'] = 'Update my copy';
+$labels['openpreview'] = 'Open Preview';
+
+$labels['deleteobjectconfirm'] = 'Do you really want to delete this object?';
+$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined object from your account?';
+
+$labels['delegateinvitation'] = 'Delegate Invitation';
+$labels['delegateto'] = 'Delegate to';
+$labels['delegatersvpme'] = 'Keep me informed about updates of this incidence';
+$labels['delegateinvalidaddress'] = 'Please enter a valid email address for the delegate';
+
+$labels['savingdata'] = 'Saving data...';
+
+// attendees labels
+$labels['expandattendeegroup'] = 'Substitute with group members';
+$labels['expandattendeegroupnodata'] = 'Unable to substitute this group. No members found.';
+$labels['expandattendeegrouperror'] = 'Unable to substitute this group. It might contain too many members.';
+$labels['expandattendeegroupsizelimit'] = 'This group contains too many members for substituting.';
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/es_AR.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'hasta';
+$labels['at'] = 'en';
+$labels['alarmemail'] = 'Enviar Correo ElectrĂłnico';
+$labels['alarmdisplay'] = 'Mostrar mensaje';
+$labels['alarmaudio'] = 'Reproducir sonido';
+$labels['alarmdisplayoption'] = 'Mensaje';
+$labels['alarmemailoption'] = 'Correo ElectrĂłnico';
+$labels['alarmaudiooption'] = 'Sonido';
+$labels['alarmat'] = 'en $datetime';
+$labels['trigger@'] = 'en fecha';
+$labels['trigger-M'] = 'minutos antes';
+$labels['trigger-H'] = 'horas antes';
+$labels['trigger-D'] = 'dĂ­as antes';
+$labels['trigger+M'] = 'minutos después';
+$labels['trigger+H'] = 'horas después';
+$labels['trigger+D'] = 'días después';
+$labels['triggerend-M'] = 'minutos antes de finalizar';
+$labels['triggerend-H'] = 'horas antes de finalizar';
+$labels['triggerend-D'] = 'dĂ­as antes de finalizar';
+$labels['triggerend+M'] = 'minutos después de finalizar';
+$labels['triggerend+H'] = 'horas después de finalizar';
+$labels['triggerend+D'] = 'dĂ­as despues de finalizar';
+$labels['trigger0'] = 'en hora';
+$labels['triggerattime'] = 'en hora de inicio';
+$labels['triggerattimeend'] = 'en hora de finalizaciĂłn';
+$labels['relatedstart'] = 'inicio';
+$labels['relatedendevent'] = 'fin';
+$labels['relatedendtask'] = 'fecha de vencimiento';
+$labels['addalarm'] = 'Agregar alarma';
+$labels['removealarm'] = 'Eliminar alarma';
+$labels['alarmtitle'] = 'Eventos prĂłximos';
+$labels['dismissall'] = 'Descartar todo';
+$labels['dismiss'] = 'Descartar';
+$labels['snooze'] = 'Dormitar';
+$labels['repeatinmin'] = 'Repetir en $min minutos';
+$labels['repeatinhr'] = 'Repetir en 1 hora';
+$labels['repeatinhrs'] = 'Repetir en $hrs horas';
+$labels['repeattomorrow'] = 'Repetir mañana';
+$labels['repeatinweek'] = 'Repetir en una semana';
+$labels['showmore'] = 'Mostrar mĂĄs...';
+$labels['recurring'] = 'Repite';
+$labels['frequency'] = 'Repetir';
+$labels['never'] = 'nunca';
+$labels['daily'] = 'diariamente';
+$labels['weekly'] = 'semanalmente';
+$labels['monthly'] = 'mensualmente';
+$labels['yearly'] = 'anualmente';
+$labels['rdate'] = 'en fechas';
+$labels['every'] = 'Cada';
+$labels['days'] = 'dĂ­a(s)';
+$labels['weeks'] = 'semana(s)';
+$labels['months'] = 'mes(es)';
+$labels['years'] = 'año(s)';
+$labels['bydays'] = 'En';
+$labels['untildate'] = 'el';
+$labels['each'] = 'Cada';
+$labels['onevery'] = 'En cada';
+$labels['onsamedate'] = 'En la misma fecha';
+$labels['forever'] = 'para siempre';
+$labels['recurrencend'] = 'hasta';
+$labels['untilenddate'] = 'hasta la fecha';
+$labels['forntimes'] = 'por $nr veces';
+$labels['first'] = 'primero';
+$labels['second'] = 'segundo';
+$labels['third'] = 'tercero';
+$labels['fourth'] = 'cuarto';
+$labels['last'] = 'Ășltimo';
+$labels['dayofmonth'] = 'DĂ­a del mes';
+$labels['addrdate'] = 'Agregar fecha de repeticiĂłn';
+$labels['except'] = 'excepto';
+$labels['statusorganizer'] = 'Organizador';
+$labels['statusaccepted'] = 'Aceptado';
+$labels['statustentative'] = 'Tentativo';
+$labels['statusdeclined'] = 'Rechazado';
+$labels['statusdelegated'] = 'Delegado';
+$labels['statusneeds-action'] = 'Necesita acciĂłn';
+$labels['statusunknown'] = 'Desconocido';
+$labels['statuscompleted'] = 'Completo';
+$labels['statusin-process'] = 'En proceso';
+$labels['itipinvitation'] = 'InvitaciĂłn a';
+$labels['itipupdate'] = 'Actualizar de';
+$labels['itipcancellation'] = 'Cancelado:';
+$labels['itipreply'] = 'Responder a';
+$labels['itipaccepted'] = 'Aceptar';
+$labels['itiptentative'] = 'QuizĂĄ';
+$labels['itipdeclined'] = 'Rechazar';
+$labels['itipdelegated'] = 'Delegado';
+$labels['itipneeds-action'] = 'Posponer';
+$labels['itipcomment'] = 'Su respuesta';
+$labels['itipeditresponse'] = 'Ingresar un texto de respuesta';
+$labels['itipsendercomment'] = 'Comentario del remitente:';
+$labels['itipsuppressreply'] = 'No enviar una respuesta';
+$labels['itipobjectnotfound'] = 'El objeto referido por este mensaje no fue encontrado en su cuenta.';
+$labels['itipsubjectaccepted'] = '"$title" ha sido aceptado por $name';
+$labels['itipsubjecttentative'] = '"$title" ha sido aceptado tentativamente por $name';
+$labels['itipsubjectdeclined'] = '"$title" ha sido rechazado por $name';
+$labels['itipsubjectin-process'] = '"$title" estĂĄ en proceso por $name';
+$labels['itipsubjectcompleted'] = '"$title" fue completado por $name';
+$labels['itipsubjectcancel'] = 'Su participaciĂłn en "$title" ha sido cancelada';
+$labels['itipsubjectdelegated'] = '"$title" ha sido delegado por $name';
+$labels['itipsubjectdelegatedto'] = '"$title" ha sido delegado a usted por $name';
+$labels['itipnewattendee'] = 'Esta es una respuesta de un nuevo participante';
+$labels['updateattendeestatus'] = 'Actualizar el estado del participante';
+$labels['acceptinvitation'] = 'ÂżAcepta esta invitaciĂłn?';
+$labels['acceptattendee'] = 'Aceptar participante';
+$labels['declineattendee'] = 'Rechazar participante';
+$labels['declineattendeeconfirm'] = 'Ingresar un mensaje para el participante rechazado (opcional):';
+$labels['rsvpmodeall'] = 'La serie completa';
+$labels['rsvpmodecurrent'] = 'Esta ocurrencia sola';
+$labels['rsvpmodefuture'] = 'Esta y futuras ocurrencias';
+$labels['itipsingleoccurrence'] = 'Esta es una <em>ocurrencia individual</em> de una serie de eventos';
+$labels['itipfutureoccurrence'] = 'Refiere a <em>esta y todas las futuras ocurrencias</em> de una serie de eventos';
+$labels['itipmessagesingleoccurrence'] = 'El mensaje solamente refiere a esta ocurrencia individual';
+$labels['itipmessagefutureoccurrence'] = 'El mensaje refiere a esta y todas las futuras ocurrencias';
+$labels['youhaveaccepted'] = 'Ha aceptado esta invitaciĂłn';
+$labels['youhavetentative'] = 'Ha aceptado tentativamente esta invitaciĂłn';
+$labels['youhavedeclined'] = 'Ha rechazado esta invitaciĂłn';
+$labels['youhavedelegated'] = 'Ha delegado esta invitaciĂłn';
+$labels['youhavein-process'] = 'Usted estĂĄ trabajando en esta asignaciĂłn';
+$labels['youhavecompleted'] = 'Ha completado esta asignaciĂłn';
+$labels['youhaveneeds-action'] = 'Su respuesta a esta invitaciĂłn estĂĄ pendiente';
+$labels['youhavepreviouslyaccepted'] = 'Ha aceptado previamente esta invitaciĂłn';
+$labels['youhavepreviouslytentative'] = 'Ha aceptado previamente esta invitacion tentativamente';
+$labels['youhavepreviouslydeclined'] = 'Ha rechazado previamente esta invitaciĂłn';
+$labels['youhavepreviouslydelegated'] = 'Ha delegado previamente esta invitaciĂłn';
+$labels['youhavepreviouslyin-process'] = 'Ha reportado previamente que trabaja en esta asignaciĂłn';
+$labels['youhavepreviouslycompleted'] = 'Ha completado previamente esta asignaciĂłn';
+$labels['youhavepreviouslyneeds-action'] = 'Su respuesta a esta invitaciĂłn estĂĄ pendiente';
+$labels['attendeeaccepted'] = 'El participante ha aceptado';
+$labels['attendeetentative'] = 'El participante ha aceptado tentativamente';
+$labels['attendeedeclined'] = 'El participante ha rechazado';
+$labels['attendeedelegated'] = 'El participante ha delegado a $delegatedto';
+$labels['attendeein-process'] = 'El participante estĂĄ en proceso';
+$labels['attendeecompleted'] = 'El participante ha completado';
+$labels['notanattendee'] = 'No esta incluĂ­do en la lista de invitados a este objeto';
+$labels['outdatedinvitation'] = 'Esta invitaciĂłn ha sido reemplazada por una nueva versiĂłn';
+$labels['importtocalendar'] = 'Guardar en mi calendario';
+$labels['removefromcalendar'] = 'Eliminar de mi calendario';
+$labels['updatemycopy'] = 'Actualizar mi copia';
+$labels['openpreview'] = 'Abrir Vista Preliminar';
+$labels['deleteobjectconfirm'] = 'Confirme que desea eliminar este objeto';
+$labels['declinedeleteconfirm'] = '¿Quiere también eliminar este objeto rechazado de su cuenta?';
+$labels['delegateinvitation'] = 'Delegar InvitaciĂłn';
+$labels['delegateto'] = 'Delegar a';
+$labels['delegatersvpme'] = 'Mantenerme informado acerca de las actualizaciones de esta incidencia';
+$labels['delegateinvalidaddress'] = 'Por favor, ingrese una direcciĂłn de correo electrĂłnico vĂĄlida para la delegaciĂłn';
+$labels['savingdata'] = 'Guardando...';
+$labels['expandattendeegroup'] = 'Sustituir con miembros del grupo';
+$labels['expandattendeegroupnodata'] = 'No se puede sustituir este grupo. No se encontraron miembros.';
+$labels['expandattendeegrouperror'] = 'No se puede sustituir este grupo. Puede contener demasiados miembros.';
+$labels['expandattendeegroupsizelimit'] = 'Este grupo contiene demasiados miembros para sustituir.';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/es_ES.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'until';
+$labels['at'] = 'at';
+$labels['alarmemail'] = 'Send Email';
+$labels['alarmdisplay'] = 'Show message';
+$labels['alarmaudio'] = 'Reproducir sonido';
+$labels['alarmdisplayoption'] = 'Message';
+$labels['alarmemailoption'] = 'Correo electrĂłnico';
+$labels['alarmaudiooption'] = 'Sonido';
+$labels['alarmat'] = 'at $datetime';
+$labels['trigger@'] = 'on date';
+$labels['trigger-M'] = 'minutes before';
+$labels['trigger-H'] = 'hours before';
+$labels['trigger-D'] = 'days before';
+$labels['trigger+M'] = 'minutes after';
+$labels['trigger+H'] = 'hours after';
+$labels['trigger+D'] = 'days after';
+$labels['triggerend-M'] = 'minutos para finalizar';
+$labels['triggerend-H'] = 'horas para finalizar';
+$labels['triggerend-D'] = 'dĂ­as para finalizar';
+$labels['triggerend+M'] = 'minutos tras finalizar';
+$labels['triggerend+H'] = 'horas tras finalizar';
+$labels['triggerend+D'] = 'dĂ­as tras finalizar';
+$labels['trigger0'] = 'a tiempo';
+$labels['triggerattime'] = 'al iniciar';
+$labels['triggerattimeend'] = 'al finalizar';
+$labels['relatedstart'] = 'iniciar';
+$labels['relatedendevent'] = 'fin';
+$labels['relatedendtask'] = 'Plazo estipulado';
+$labels['addalarm'] = 'Añadir alarma';
+$labels['removealarm'] = 'Eliminar alarma';
+$labels['alarmtitle'] = 'Upcoming events';
+$labels['dismissall'] = 'Dismiss all';
+$labels['dismiss'] = 'Dismiss';
+$labels['snooze'] = 'Snooze';
+$labels['repeatinmin'] = 'Repeat in $min minutes';
+$labels['repeatinhr'] = 'Repeat in 1 hour';
+$labels['repeatinhrs'] = 'Repeat in $hrs hours';
+$labels['repeattomorrow'] = 'Repeat tomorrow';
+$labels['repeatinweek'] = 'Repeat in a week';
+$labels['showmore'] = 'Show more...';
+$labels['recurring'] = 'Repeticiones';
+$labels['frequency'] = 'Repeat';
+$labels['never'] = 'never';
+$labels['daily'] = 'daily';
+$labels['weekly'] = 'weekly';
+$labels['monthly'] = 'monthly';
+$labels['yearly'] = 'annually';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'Every';
+$labels['days'] = 'day(s)';
+$labels['weeks'] = 'week(s)';
+$labels['months'] = 'month(s)';
+$labels['years'] = 'año(s)';
+$labels['bydays'] = 'On';
+$labels['untildate'] = 'the';
+$labels['each'] = 'Each';
+$labels['onevery'] = 'On every';
+$labels['onsamedate'] = 'On the same date';
+$labels['forever'] = 'forever';
+$labels['recurrencend'] = 'until';
+$labels['untilenddate'] = 'hasta la fecha';
+$labels['forntimes'] = 'for $nr time(s)';
+$labels['first'] = 'first';
+$labels['second'] = 'second';
+$labels['third'] = 'third';
+$labels['fourth'] = 'fourth';
+$labels['last'] = 'last';
+$labels['dayofmonth'] = 'Day of month';
+$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'excepto';
+$labels['statusorganizer'] = 'Organizador';
+$labels['statusaccepted'] = 'Aceptada';
+$labels['statustentative'] = 'Provisional';
+$labels['statusdeclined'] = 'Rechazado';
+$labels['statusdelegated'] = 'Delgado';
+$labels['statusneeds-action'] = 'Necesita acciĂłn';
+$labels['statusunknown'] = 'Desconocido';
+$labels['statuscompleted'] = 'Realizado';
+$labels['statusin-process'] = 'En proceso';
+$labels['itipinvitation'] = 'InvitaciĂłn para';
+$labels['itipupdate'] = 'ActualizaciĂłn de';
+$labels['itipcancellation'] = 'Cancelado:';
+$labels['itipreply'] = 'Responder a';
+$labels['itipaccepted'] = 'Aceptar';
+$labels['itiptentative'] = 'Tal vez';
+$labels['itipdeclined'] = 'Declinar';
+$labels['itipdelegated'] = 'Delegado';
+$labels['itipneeds-action'] = 'Posponer';
+$labels['itipcomment'] = 'Su respuesta';
+$labels['itipeditresponse'] = 'Introducir un texto de respuesta';
+$labels['itipsendercomment'] = 'Comentario del remitente:';
+$labels['itipsuppressreply'] = 'No enviar una respuesta';
+$labels['itipobjectnotfound'] = 'El objeto referido en este mensaje no se encontrĂł en su cuenta.';
+$labels['itipsubjectaccepted'] = '$name ha aceptado el "$title"';
+$labels['itipsubjecttentative'] = '$name aceptado provisionalmente el "$title"';
+$labels['itipsubjectdeclined'] = '$name ha declinado el "$title"';
+$labels['itipsubjectin-process'] = '$name estĂĄ procesando el "$title"';
+$labels['itipsubjectcompleted'] = '$name ha completado el "$title"';
+$labels['itipsubjectcancel'] = 'Se ha cancelado su participaciĂłn en "$title';
+$labels['itipsubjectdelegated'] = '$name ha delegado el "$title"';
+$labels['itipsubjectdelegatedto'] = '$name ha delegado en ti el "$title"';
+$labels['itipnewattendee'] = 'Esta es la respuesta de un nuevo participante';
+$labels['updateattendeestatus'] = 'Actualizar el estado del participante';
+$labels['acceptinvitation'] = 'ÂżAcepta esta inviitaciĂłn?';
+$labels['acceptattendee'] = 'Aceptar participante';
+$labels['declineattendee'] = 'Declinar participante';
+$labels['declineattendeeconfirm'] = 'Introduzca un mensaje para el participante rechazado (opcional):';
+$labels['rsvpmodeall'] = 'Las series completas';
+$labels['rsvpmodecurrent'] = 'Este caso sĂłlo';
+$labels['rsvpmodefuture'] = 'Este y los futuros casos';
+$labels['itipsingleoccurrence'] = 'Se trata de un <em>caso aislado</ em> en una serie de eventos';
+$labels['itipfutureoccurrence'] = 'Se refiere a <em>este y todos los casos futuros</ em> en una serie de eventos';
+$labels['itipmessagesingleoccurrence'] = 'El mensaje solo hace referencia a este caso aislado';
+$labels['itipmessagefutureoccurrence'] = 'El mensaje hace referencia a este y todos los casos futuros';
+$labels['youhaveaccepted'] = 'Ha aceptado esta invitaciĂłn';
+$labels['youhavetentative'] = 'Ha aceptado esta invitaciĂłn provisionalmente';
+$labels['youhavedeclined'] = 'Ha declinado esta invitaciĂłn';
+$labels['youhavedelegated'] = 'Ha delegado esta invitaciĂłn';
+$labels['youhavein-process'] = 'EstĂĄ trabajando en esta asignaciĂłn';
+$labels['youhavecompleted'] = 'Ha completado esta asignaciĂłn';
+$labels['youhaveneeds-action'] = 'Sigue pendiente su respuesta a esta convitaciĂłn';
+$labels['youhavepreviouslyaccepted'] = 'Ha aceptado esta invitaciĂłn anteriormente';
+$labels['youhavepreviouslytentative'] = 'Ya ha aceptado esta invitaciĂłn provisionalmente';
+$labels['youhavepreviouslydeclined'] = 'Ha declinado esta invitaciĂłn anteriormente';
+$labels['youhavepreviouslydelegated'] = 'Ha adelegado esta invitaciĂłn anteriormente';
+$labels['youhavepreviouslyin-process'] = 'Ya ha informado anteriormente para trabajar en esta asignaciĂłn';
+$labels['youhavepreviouslycompleted'] = 'Ya ha completado esta asignaciĂłn';
+$labels['youhavepreviouslyneeds-action'] = 'Sigue pendiente su respuesta a esta invitaciĂłn';
+$labels['attendeeaccepted'] = 'El participante ha aceptado';
+$labels['attendeetentative'] = 'El participante ha aceptado provisionalmente';
+$labels['attendeedeclined'] = 'El participante ha declinado';
+$labels['attendeedelegated'] = 'El participante ha delegado en $delegatedto';
+$labels['attendeein-process'] = 'El participante estĂĄ en proceso';
+$labels['attendeecompleted'] = 'El participante ha completado';
+$labels['notanattendee'] = 'Usted no estĂĄ en la lista como asistente de este proyecto';
+$labels['outdatedinvitation'] = 'Esta invitaciĂłn se ha sustituido por una nueva versiĂłn';
+$labels['importtocalendar'] = 'Guardar en mi calendario';
+$labels['removefromcalendar'] = 'Eliminar de mi calendario';
+$labels['updatemycopy'] = 'Actualizar mi copia';
+$labels['openpreview'] = 'Abrir vista previa';
+$labels['deleteobjectconfirm'] = 'ÂżEsta seguro de eliminar  este objeto?';
+$labels['declinedeleteconfirm'] = '¿También desea eliminar este objeto declinado del calendario?';
+$labels['delegateinvitation'] = 'Delegar invitaciĂłn';
+$labels['delegateto'] = 'Delegar en';
+$labels['delegatersvpme'] = 'Mantenerme informado de las actualizaciones de esta incidencia';
+$labels['delegateinvalidaddress'] = 'Introduzca una direcciĂłn de correo electrĂłnico vĂĄlida para delegar';
+$labels['savingdata'] = 'Guardando datos...';
+$labels['expandattendeegroup'] = 'Sustituir con los miembros del grupo';
+$labels['expandattendeegroupnodata'] = 'No puede sustituirse este grupo. No se han encontrado miembros.';
+$labels['expandattendeegrouperror'] = 'No puede sustituirse este grupo. Tal vez tenga demasiados miembros.';
+$labels['expandattendeegroupsizelimit'] = 'Este grupo contiene demasiados miembros que sustituir.';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/et_EE.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['alarmemailoption'] = 'E-post';
+$labels['statusorganizer'] = 'Organizer';
+$labels['statustentative'] = 'Tentative';
+$labels['statusneeds-action'] = 'Needs action';
+$labels['statusunknown'] = 'Unknown';
+$labels['statuscompleted'] = 'Completed';
+$labels['statusin-process'] = 'In process';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/fi_FI.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,143 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'kunnes';
+$labels['alarmemail'] = 'LÀhetÀ sÀhköposti';
+$labels['alarmdisplay'] = 'NÀytÀ viesti';
+$labels['alarmaudio'] = 'Toista ÀÀni';
+$labels['alarmdisplayoption'] = 'Viesti';
+$labels['alarmemailoption'] = 'SÀhköposti';
+$labels['alarmaudiooption'] = 'Ă„Ă€ni';
+$labels['alarmat'] = '$datetime';
+$labels['trigger@'] = 'pÀivÀnÀ';
+$labels['trigger-M'] = 'minuuttia ennen';
+$labels['trigger-H'] = 'tuntia ennen';
+$labels['trigger-D'] = 'pÀivÀÀ ennen';
+$labels['trigger+M'] = 'minuuttia jÀlkeen';
+$labels['trigger+H'] = 'tuntia jÀlkeen';
+$labels['trigger+D'] = 'pÀivÀÀ jÀlkeen';
+$labels['addalarm'] = 'LisÀÀ herÀte';
+$labels['removealarm'] = 'Poista herÀte';
+$labels['alarmtitle'] = 'Tulevat tapahtumat';
+$labels['dismissall'] = 'HylkÀÀ kaikki';
+$labels['dismiss'] = 'HylkÀÀ';
+$labels['snooze'] = 'Torkuta';
+$labels['repeatinmin'] = 'Toista $min minuutin pÀÀstÀ';
+$labels['repeatinhr'] = 'Toista 1 tunnin pÀÀstÀ';
+$labels['repeatinhrs'] = 'Toista $hrs tunnin pÀÀstÀ';
+$labels['repeattomorrow'] = 'Toista huomenna';
+$labels['repeatinweek'] = 'Toista viikon pÀÀstÀ';
+$labels['showmore'] = 'NÀytÀ lisÀÀ...';
+$labels['recurring'] = 'Toistuu';
+$labels['frequency'] = 'Toista';
+$labels['never'] = 'ei koskaan';
+$labels['daily'] = 'pÀivittÀin';
+$labels['weekly'] = 'viikottain';
+$labels['monthly'] = 'kuukausittain';
+$labels['yearly'] = 'vuosittain';
+$labels['rdate'] = 'pÀivinÀ';
+$labels['every'] = 'Joka';
+$labels['days'] = 'pÀivÀ(À)';
+$labels['weeks'] = 'viikko(a)';
+$labels['months'] = 'kuukausi(-tta)';
+$labels['years'] = 'vuosi(-tta)';
+$labels['each'] = 'Joka';
+$labels['onevery'] = 'Jokaisena';
+$labels['onsamedate'] = 'Samana pÀivÀnÀ';
+$labels['forever'] = 'ikuisesti';
+$labels['recurrencend'] = 'kunnes';
+$labels['untilenddate'] = 'pvm saakka';
+$labels['forntimes'] = '$nr kerta(a)';
+$labels['first'] = 'ensimmÀinen';
+$labels['second'] = 'toinen';
+$labels['third'] = 'kolmas';
+$labels['fourth'] = 'neljÀs';
+$labels['last'] = 'viimeinen';
+$labels['dayofmonth'] = 'KuukaudenpÀivÀ';
+$labels['addrdate'] = 'LisÀÀ toiston pvm';
+$labels['except'] = 'paitsi';
+$labels['statusorganizer'] = 'JÀrjestÀjÀ';
+$labels['statusaccepted'] = 'HyvÀksytty';
+$labels['statustentative'] = 'Alustava';
+$labels['statusdeclined'] = 'KieltÀydytty';
+$labels['statusneeds-action'] = 'Vaatii toimenpiteitÀ';
+$labels['statusunknown'] = 'Tuntematon';
+$labels['statuscompleted'] = 'Valmis';
+$labels['statusin-process'] = 'Prosessissa';
+$labels['itipinvitation'] = 'Kutsu';
+$labels['itipupdate'] = 'PĂ€ivitys';
+$labels['itipcancellation'] = 'Peruttu:';
+$labels['itipreply'] = 'Vastaus';
+$labels['itipaccepted'] = 'HyvÀksy';
+$labels['itiptentative'] = 'EhkÀ';
+$labels['itipdeclined'] = 'KieltÀydy';
+$labels['itipdelegated'] = 'Edustaja';
+$labels['itipneeds-action'] = 'ViivÀstetty';
+$labels['itipcomment'] = 'Oma vastaus';
+$labels['itipeditresponse'] = 'Kirjoita vastausteksti';
+$labels['itipsendercomment'] = 'LÀhettÀjÀn kommentti:';
+$labels['itipsuppressreply'] = 'ÄlĂ€ lĂ€hetĂ€ vastausta';
+$labels['itipobjectnotfound'] = 'TÀssÀ viestissÀ viittattua kohdetta ei löydy tunnuksestasi';
+$labels['itipsubjectaccepted'] = '$name on hyvÀksynyt tapahtuman "$title"';
+$labels['itipsubjecttentative'] = '$name on alustavasti hyvÀkstynyt tapahtuman "$title"';
+$labels['itipsubjectdeclined'] = '$name on kieltÀytynyt osallistumasta tapahtumaan "$title"';
+$labels['itipsubjectin-process'] = '"$title" on prosessissa $name kautta';
+$labels['itipsubjectcompleted'] = '$title on valmis $name kautta';
+$labels['itipsubjectcancel'] = 'Osallistumisesi "$title" on peruttu';
+$labels['itipsubjectdelegated'] = '"$title" on delegoitu $name kautta';
+$labels['itipsubjectdelegatedto'] = '"$title" on delegoitu sinulle $name kautta';
+$labels['itipnewattendee'] = 'TÀmÀ on vastaus uudelta osallistujalta';
+$labels['updateattendeestatus'] = 'PÀivitÀ osallistujien status';
+$labels['acceptinvitation'] = 'HyvÀksytkö tÀmÀn kutsun?';
+$labels['acceptattendee'] = 'HyvÀksy osallistuja';
+$labels['declineattendee'] = 'EstÀ osallistuja';
+$labels['declineattendeeconfirm'] = 'Anna viesti estetylle osallistujalle (vaihtoehtoinen):';
+$labels['rsvpmodeall'] = 'Koko sarja';
+$labels['rsvpmodecurrent'] = 'Vain tÀmÀ esiintymÀ';
+$labels['rsvpmodefuture'] = 'TÀmÀ ja tulevat esiintymÀt';
+$labels['itipsingleoccurrence'] = 'TÀmÀ on <em>yksittÀinen esiintymÀ</em> tapahtumasarjassa';
+$labels['itipfutureoccurrence'] = 'Viittaa <em>tÀhÀn ja kaikkiin tuleviin esiintymiin</em> tapahtumasarjassa';
+$labels['itipmessagesingleoccurrence'] = 'TÀmÀ viesti viittaa vain yhteen esiintymÀÀn';
+$labels['itipmessagefutureoccurrence'] = 'TÀmÀ viesti viittaa tÀhÀn ja kaikkiin tuleviin esiintymiin';
+$labels['youhaveaccepted'] = 'Olet hyvÀksynyt tÀmÀn kutsun';
+$labels['youhavetentative'] = 'Olet hyvÀksynyt tÀmÀn kutsun alustavasti';
+$labels['youhavedeclined'] = 'Olet kieltÀytynyt tÀstÀ kutsusta';
+$labels['youhavedelegated'] = 'Olet delegoitu tÀhÀn kutsuun';
+$labels['youhavein-process'] = 'Työskentelet tÀmÀn tehtÀvÀn parissa';
+$labels['youhavecompleted'] = 'Olet suorittanut tÀmÀn tehtÀvÀn';
+$labels['youhaveneeds-action'] = 'Vastauksesi tÀhÀn kutsuun on yhÀ jonossa';
+$labels['youhavepreviouslyaccepted'] = 'Olet aiemmin hyvÀksynyt tÀmÀn kutsun';
+$labels['youhavepreviouslytentative'] = 'Olet aiemmin hyvÀksynyt tÀmÀn kutsun varauksella';
+$labels['youhavepreviouslydeclined'] = 'Sinut on aiemmin estetty tÀstÀ kutsusta';
+$labels['youhavepreviouslydelegated'] = 'Sinut on aiemmin delegoitu tÀhÀn kutsuun';
+$labels['youhavepreviouslyin-process'] = 'Sinut on aiemmin raportoitu työskentelevÀn tÀmÀn  tehtÀvÀn parissa';
+$labels['youhavepreviouslycompleted'] = 'Olet aiemmin suorittanut tÀmÀn tehtÀvÀn';
+$labels['youhavepreviouslyneeds-action'] = 'Vastauksesi tÀhÀn kutsuun on yhÀ jonossa';
+$labels['attendeeaccepted'] = 'Osallistuja on hyvÀksynyt';
+$labels['attendeetentative'] = 'Osallistuja on hyvÀksynyt varauksella';
+$labels['attendeedeclined'] = 'Osallistuja on hylÀnnyt';
+$labels['attendeedelegated'] = 'Osallistuja on delegoitu $delegatedto';
+$labels['attendeein-process'] = 'Osallistuja on prosessissa';
+$labels['attendeecompleted'] = 'Osallistuja on suorittanut';
+$labels['notanattendee'] = 'Sinua ei ole listattu osallistujaksi tÀhÀn kohteeseen';
+$labels['outdatedinvitation'] = 'Kutsu on korvattu uudemmalla versiolla';
+$labels['importtocalendar'] = 'Tallenna omaan kalenteriin';
+$labels['removefromcalendar'] = 'Poista omasta kalenterista';
+$labels['updatemycopy'] = 'PÀivitÀ kopioni';
+$labels['openpreview'] = 'Avaa esikatselu';
+$labels['deleteobjectconfirm'] = 'Haluatko todella poistaa tÀmÀn kohteen?';
+$labels['declinedeleteconfirm'] = 'Haluatko poistaa tÀmÀn hylÀtyn kohteen tunnukseltasi?';
+$labels['delegateinvitation'] = 'Delegoi kutsu';
+$labels['delegateto'] = 'Delegoi henkilölle';
+$labels['delegatersvpme'] = 'PidÀ minut ajan tasalla tÀmÀn tapauksen pÀivityksistÀ';
+$labels['delegateinvalidaddress'] = 'Anna valiidi sÀhköpostiosoite delegoiaksesi';
+$labels['savingdata'] = 'Tallennetaan tietoja...';
+$labels['expandattendeegroup'] = 'Korvaa ryhmÀn jÀsenillÀ';
+$labels['expandattendeegroupnodata'] = 'Korvaaminen ryhmÀn jÀsenillÀ ei onnistu. YhtÀÀn jÀsentÀ ei löydy.';
+$labels['expandattendeegrouperror'] = 'Korvaaminen ryhmÀn jÀsenillÀ ei onnistu. TÀmÀ saattaa sisÀltÀÀ liian monta jÀsentÀ.';
+$labels['expandattendeegroupsizelimit'] = 'TÀmÀ ryhmÀ sisÀltÀÀ liian monta jÀsentÀ korvaamiseen';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/fr_FR.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'jusqu\'Ă ';
+$labels['at'] = 'Ă ';
+$labels['alarmemail'] = 'Envoyer un e-mail';
+$labels['alarmdisplay'] = 'Voir le message';
+$labels['alarmaudio'] = 'Alerte sonore';
+$labels['alarmdisplayoption'] = 'Message';
+$labels['alarmemailoption'] = 'E-mail';
+$labels['alarmaudiooption'] = 'Son';
+$labels['alarmat'] = 'Ă  $datetime';
+$labels['trigger@'] = 'Ă  la date';
+$labels['trigger-M'] = 'minutes avant';
+$labels['trigger-H'] = 'heures avant';
+$labels['trigger-D'] = 'jours avant';
+$labels['trigger+M'] = 'minutes aprĂšs';
+$labels['trigger+H'] = 'heures aprĂšs';
+$labels['trigger+D'] = 'jours aprĂšs';
+$labels['triggerend-M'] = 'minutes avant la fin';
+$labels['triggerend-H'] = 'heures avant la fin';
+$labels['triggerend-D'] = 'jours avant la fin';
+$labels['triggerend+M'] = 'minutes aprĂšs la fin';
+$labels['triggerend+H'] = 'heures aprĂšs la fin';
+$labels['triggerend+D'] = 'jours aprĂšs la fin';
+$labels['trigger0'] = 'dans les temps';
+$labels['triggerattime'] = 'au début';
+$labels['triggerattimeend'] = 'Ă  la fin';
+$labels['relatedstart'] = 'début';
+$labels['relatedendevent'] = 'fin';
+$labels['relatedendtask'] = 'date de fin';
+$labels['addalarm'] = 'Ajouter une alarme';
+$labels['removealarm'] = 'Supprimer l\'alarme';
+$labels['alarmtitle'] = 'Evénements à venir';
+$labels['dismissall'] = 'Tout masquer';
+$labels['dismiss'] = 'Masquer';
+$labels['snooze'] = 'En pause';
+$labels['repeatinmin'] = 'Répéter dans $min minutes';
+$labels['repeatinhr'] = 'Répéter dans 1 heure';
+$labels['repeatinhrs'] = 'Répéter dans $hrs heures';
+$labels['repeattomorrow'] = 'Répéter demain';
+$labels['repeatinweek'] = 'Répéter dans une semaine';
+$labels['showmore'] = 'Afficher plus...';
+$labels['recurring'] = 'Répétitions';
+$labels['frequency'] = 'Répéter';
+$labels['never'] = 'jamais';
+$labels['daily'] = 'quotidienne';
+$labels['weekly'] = 'hebdomadaire';
+$labels['monthly'] = 'mensuelle';
+$labels['yearly'] = 'annuelle';
+$labels['rdate'] = 'Ă  certaines dates';
+$labels['every'] = 'Tous les';
+$labels['days'] = 'jour(s)';
+$labels['weeks'] = 'semaine(s)';
+$labels['months'] = 'mois';
+$labels['years'] = 'année(s) en :';
+$labels['bydays'] = 'Le';
+$labels['untildate'] = 'le';
+$labels['each'] = 'Chaque';
+$labels['onevery'] = 'Tous les';
+$labels['onsamedate'] = 'À la mĂȘme date';
+$labels['forever'] = 'toujours';
+$labels['recurrencend'] = 'jusqu\'Ă ';
+$labels['untilenddate'] = 'jusqu\'Ă  la date';
+$labels['forntimes'] = '$nr fois';
+$labels['first'] = 'premier';
+$labels['second'] = 'deuxiĂšme';
+$labels['third'] = 'troisiĂšme';
+$labels['fourth'] = 'quatriĂšme';
+$labels['last'] = 'dernier';
+$labels['dayofmonth'] = 'Jour du mois';
+$labels['addrdate'] = 'Ajoutez une date répétée';
+$labels['except'] = 'sauf';
+$labels['statusorganizer'] = 'Organisateur';
+$labels['statusaccepted'] = 'Accepté';
+$labels['statustentative'] = 'Provisoire';
+$labels['statusdeclined'] = 'Refusé';
+$labels['statusdelegated'] = 'Délégué';
+$labels['statusneeds-action'] = 'Action exigée';
+$labels['statusunknown'] = 'Inconnu';
+$labels['statuscompleted'] = 'Terminée';
+$labels['statusin-process'] = 'En cours';
+$labels['itipinvitation'] = 'Invitation Ă ';
+$labels['itipupdate'] = 'Mise Ă  jour de';
+$labels['itipcancellation'] = 'Annulation :';
+$labels['itipreply'] = 'RĂ©pondre Ă ';
+$labels['itipaccepted'] = 'Accepter';
+$labels['itiptentative'] = 'Peut-ĂȘtre';
+$labels['itipdeclined'] = 'Refuser';
+$labels['itipdelegated'] = 'Déléguer';
+$labels['itipneeds-action'] = 'Reporter';
+$labels['itipcomment'] = 'Votre réponse';
+$labels['itipeditresponse'] = 'Saisissez votre réponse';
+$labels['itipsendercomment'] = 'Commentaire des expéditeurs';
+$labels['itipsuppressreply'] = 'Ne pas envoyer de réponse';
+$labels['itipobjectnotfound'] = 'L\'objet auquel il est fait référence dans ce message n\'a pas été trouvé dans votre compte.';
+$labels['itipsubjectaccepted'] = '"$title" a été accepté par $name';
+$labels['itipsubjecttentative'] = '"$title" a provisoirement été accepté par $name';
+$labels['itipsubjectdeclined'] = '"$title" a été refusé par $name';
+$labels['itipsubjectin-process'] = '"$title" est en cours de traitement par $name';
+$labels['itipsubjectcompleted'] = '"$title" a été rempli par $name';
+$labels['itipsubjectcancel'] = 'Votre affectation à "$title" a été annulée';
+$labels['itipsubjectdelegated'] = '"$title$ a été délégué par $name';
+$labels['itipsubjectdelegatedto'] = '"$title" vous a été délégué par $name';
+$labels['itipnewattendee'] = 'Voici la réponse d\'un nouveau participant';
+$labels['updateattendeestatus'] = 'Modifier le statut du participant';
+$labels['acceptinvitation'] = 'Acceptez-vous cette invitation ?';
+$labels['acceptattendee'] = 'Accepter l\'utilisateur';
+$labels['declineattendee'] = 'Refuser l\'utilisateur';
+$labels['declineattendeeconfirm'] = 'Saisissez un message pour l\'utilisateur refusé (optionnel) :';
+$labels['rsvpmodeall'] = 'Les séries entiÚres';
+$labels['rsvpmodecurrent'] = 'Cette occurrence seulement';
+$labels['rsvpmodefuture'] = 'Celle-ci et les occurrences futures';
+$labels['itipsingleoccurrence'] = 'C\'est une <em>seule occurrence</em> parmi une série d\'événements';
+$labels['itipfutureoccurrence'] = 'Se réfÚre à <em>celle-ci et à toutes les occurrences futures</em> parmi une série d\'événements';
+$labels['itipmessagesingleoccurrence'] = 'Le message se réfÚre seulement à cette occurrence';
+$labels['itipmessagefutureoccurrence'] = 'Le message se réfÚre à celle-ci et à toutes les occurrences futures';
+$labels['youhaveaccepted'] = 'Vous avez accepté cette invitation';
+$labels['youhavetentative'] = 'Vous avez provisoirement accepté cette invitation';
+$labels['youhavedeclined'] = 'Vous avez refusé cette invitation';
+$labels['youhavedelegated'] = 'Vous avez délégué cette invitation';
+$labels['youhavein-process'] = 'Vous traitez cette affectation';
+$labels['youhavecompleted'] = 'Vous avez traité cette affectation';
+$labels['youhaveneeds-action'] = 'Vous ĂȘtes en attente d\'une rĂ©ponse Ă  cette invitation';
+$labels['youhavepreviouslyaccepted'] = 'Vous avez déjà accepté cette invitation';
+$labels['youhavepreviouslytentative'] = 'Vous avez déjà tenté d\'accepter cette invitation';
+$labels['youhavepreviouslydeclined'] = 'Vous avez déjà décliné cette invitation';
+$labels['youhavepreviouslydelegated'] = 'Vous avez déjà délégué cette invitation';
+$labels['youhavepreviouslyin-process'] = 'Vous avez déjà signalé que vous traitiez cette affectation';
+$labels['youhavepreviouslycompleted'] = 'Vous avez déjà fini le traitement de cette affectation';
+$labels['youhavepreviouslyneeds-action'] = 'Vous ĂȘtes en attente de rĂ©ponse pour cette invitation';
+$labels['attendeeaccepted'] = 'Cet utilisateur a accepté l\'invitation';
+$labels['attendeetentative'] = 'Cette utilisateur tente d\'accepter l\'invitation';
+$labels['attendeedeclined'] = 'Cet utilisateur a décliné l\'invitation';
+$labels['attendeedelegated'] = 'Cet utilisateur a délégué à $delegatedto';
+$labels['attendeein-process'] = 'Cet utilisateur traite la tĂąche';
+$labels['attendeecompleted'] = 'Cet utilisateur a fini la tĂąche';
+$labels['notanattendee'] = 'Vous n\'ĂȘtes pas dans la liste des participants Ă  cet Ă©vĂ©nement';
+$labels['outdatedinvitation'] = 'Cette invitation a été remplacée par une nouvelle version';
+$labels['importtocalendar'] = 'Enregistrer dans mon calendrier';
+$labels['removefromcalendar'] = 'Supprimer de mon calendrier';
+$labels['updatemycopy'] = 'Mise Ă  jour de ma copie';
+$labels['openpreview'] = 'Ouvrir la prévisualisation';
+$labels['deleteobjectconfirm'] = 'Voulez-vous vraiment supprimer cet objet ?';
+$labels['declinedeleteconfirm'] = 'Voulez-vous supprimer cet objet décliné de votre compte ?';
+$labels['delegateinvitation'] = 'Invitation à déléguer';
+$labels['delegateto'] = 'Déléguer à ';
+$labels['delegatersvpme'] = 'Me tenir informé des mises à jour de cet effet';
+$labels['delegateinvalidaddress'] = 'Entrer une adresse électronique valide pour le délégué';
+$labels['savingdata'] = 'Enregistrer...';
+$labels['expandattendeegroup'] = 'Remplacement par les membres du groupe';
+$labels['expandattendeegroupnodata'] = 'Impossible de substituer ce groupe, il ne contient aucun membre.';
+$labels['expandattendeegrouperror'] = 'Impossible de substituer ce groupe, il contient trop de membres.';
+$labels['expandattendeegroupsizelimit'] = 'Ce groupe contient trop de membres pour ĂȘtre substituĂ©.';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/he.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'until';
+$labels['frequency'] = 'Repeat';
+$labels['recurrencend'] = 'until';
+$labels['savingdata'] = 'Saving data...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/hr.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'until';
+$labels['frequency'] = 'Repeat';
+$labels['recurrencend'] = 'until';
+$labels['savingdata'] = 'Saving data...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/hu_HU.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,102 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'Eddig';
+$labels['at'] = 'idƑpont';
+$labels['alarmemail'] = 'E-mail kĂŒldĂ©se';
+$labels['alarmdisplay'] = 'Üzenet megjelenĂ­tĂ©se';
+$labels['alarmdisplayoption'] = 'Üzenet';
+$labels['alarmemailoption'] = 'E-mail';
+$labels['alarmat'] = 'ekkor $datetime';
+$labels['trigger@'] = 'adott idƑpontban';
+$labels['trigger-M'] = 'perccel elƑtte';
+$labels['trigger-H'] = 'órával elƑtte';
+$labels['trigger-D'] = 'nappal elƑtte';
+$labels['trigger+M'] = 'perccel utĂĄna';
+$labels['trigger+H'] = 'ĂłrĂĄval utĂĄna';
+$labels['trigger+D'] = 'nappal utĂĄna';
+$labels['alarmtitle'] = 'KövetkezƑ esemĂ©nyek';
+$labels['dismissall'] = 'Az összes emlĂ©keztetƑ mellƑzĂ©se';
+$labels['dismiss'] = 'Nem emlĂ©kettet Ășjra';
+$labels['snooze'] = 'Újra emlĂ©keztet';
+$labels['repeatinmin'] = 'EmlĂ©keztessen $min perc mĂșlva';
+$labels['repeatinhr'] = 'EmlĂ©keztessen Ășjra 1 Ăłra mĂșlva';
+$labels['repeatinhrs'] = 'EmlĂ©keztessen Ășjra $hrs Ăłra mĂșlva';
+$labels['repeattomorrow'] = 'EmlĂ©keztessen Ășjra holnap';
+$labels['repeatinweek'] = 'EmlĂ©keztessen Ășjra egy hĂ©t mĂșlva';
+$labels['showmore'] = 'TovĂĄbb...';
+$labels['frequency'] = 'IsmĂ©tlƑdik';
+$labels['never'] = 'soha';
+$labels['daily'] = 'naponta';
+$labels['weekly'] = 'hetente';
+$labels['monthly'] = 'havonta';
+$labels['yearly'] = 'Ă©vente';
+$labels['rdate'] = 'adott idƑpontokban';
+$labels['every'] = 'Minden';
+$labels['days'] = 'napon';
+$labels['weeks'] = 'héten';
+$labels['months'] = 'hĂłnapban';
+$labels['years'] = 'Ă©vben ekkor:';
+$labels['bydays'] = 'napon';
+$labels['untildate'] = 'dĂĄtumig';
+$labels['each'] = 'napon';
+$labels['onevery'] = 'minden';
+$labels['onsamedate'] = 'Ugyanazon a dĂĄtumon';
+$labels['forever'] = 'örökké';
+$labels['recurrencend'] = 'Eddig';
+$labels['forntimes'] = '$nr alkalommal';
+$labels['first'] = 'elsƑ';
+$labels['second'] = 'mĂĄsodik';
+$labels['third'] = 'harmadik';
+$labels['fourth'] = 'negyedik';
+$labels['last'] = 'utolsĂł';
+$labels['dayofmonth'] = 'hĂłnap napja';
+$labels['addrdate'] = 'Ismétlési dåtum hozzåadåsa';
+$labels['statusorganizer'] = 'SzervezƑ';
+$labels['statustentative'] = 'Feltételes';
+$labels['statusneeds-action'] = 'Needs action';
+$labels['statusunknown'] = 'Ismeretlen foglaltsĂĄg';
+$labels['statuscompleted'] = 'Completed';
+$labels['statusin-process'] = 'In process';
+$labels['itipinvitation'] = 'Új esemĂ©ny:';
+$labels['itipupdate'] = 'MĂłdosĂ­tva:';
+$labels['itipcancellation'] = 'Lemondva:';
+$labels['itipreply'] = 'VĂĄlasz';
+$labels['itipaccepted'] = 'ElfogadĂĄs';
+$labels['itiptentative'] = 'Feltételes';
+$labels['itipdeclined'] = 'ElutasĂ­tĂĄs';
+$labels['itipdelegated'] = 'Meghatalmazott:';
+$labels['itipcomment'] = 'Megjegyzés';
+$labels['itipeditresponse'] = 'KĂ©rem, adjon meg egy vĂĄlaszt';
+$labels['itipsendercomment'] = 'A feladó megjegyzése: ';
+$labels['itipobjectnotfound'] = 'A hivatkozott elem nem található az Ön fiókjában.';
+$labels['itipsubjectaccepted'] = '$title - elfogadva';
+$labels['itipsubjecttentative'] = '$title - feltételesen elfogadva';
+$labels['itipsubjectdeclined'] = '$title - elutasĂ­tva';
+$labels['itipsubjectcancel'] = '$title - lemondva';
+$labels['itipnewattendee'] = 'VĂĄlasz Ășj rĂ©sztvevƑtƑl';
+$labels['updateattendeestatus'] = 'A rĂ©sztvevƑ stĂĄtuszĂĄnak frissĂ­tĂ©se';
+$labels['acceptinvitation'] = 'Elfogadja ezt a meghĂ­vĂĄst?';
+$labels['acceptattendee'] = 'Részvétel elfogadåsa';
+$labels['declineattendee'] = 'Részvétel elutasítåsa';
+$labels['declineattendeeconfirm'] = 'MegjegyzĂ©s az elutasĂ­tott rĂ©szvevƑnek (opcionĂĄlis):';
+$labels['youhaveaccepted'] = 'Ön elfogadta ezt a meghívást';
+$labels['youhavetentative'] = 'Ön feltĂ©telesen elfogadta ezt a meghĂ­vĂĄst';
+$labels['youhavedeclined'] = 'Ön elutasította ezt a meghívást';
+$labels['youhavedelegated'] = 'Ön meghatalmazással továbbadta ezt a meghívást';
+$labels['attendeeaccepted'] = 'A rĂ©sztvevƑ elfogadta';
+$labels['attendeetentative'] = 'A rĂ©sztvevƑ feltĂ©telesen elfogadta';
+$labels['attendeedeclined'] = 'A rĂ©sztvevƑ elutasĂ­totta';
+$labels['attendeedelegated'] = 'A meghívott meghatalmazta részvételre: $delegatedto';
+$labels['notanattendee'] = 'Ön nem szerepel az esemĂ©ny meghĂ­vottai között';
+$labels['importtocalendar'] = 'Mentés naptårba';
+$labels['removefromcalendar'] = 'Törlés a naptårból';
+$labels['updatemycopy'] = 'Naptårbejegyzés frissítése';
+$labels['deleteobjectconfirm'] = 'Biztos benne, hogy törölni szeretné ezt az elemet?';
+$labels['declinedeleteconfirm'] = 'Biztos benne, hogy törölni szeretné ezt az elutasított elemet a fiókjåból?';
+$labels['savingdata'] = 'Adatok mentése...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/it_IT.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'fino al';
+$labels['at'] = 'at';
+$labels['alarmemail'] = 'Spedisci email';
+$labels['alarmdisplay'] = 'Mostra messaggio';
+$labels['alarmdisplayoption'] = 'Messaggio';
+$labels['alarmemailoption'] = 'Email';
+$labels['trigger@'] = 'in data';
+$labels['trigger-M'] = 'miniti prima';
+$labels['trigger-H'] = 'ore prima';
+$labels['trigger-D'] = 'giorni prima';
+$labels['trigger+M'] = 'minuti dopo';
+$labels['trigger+H'] = 'ore dopo';
+$labels['trigger+D'] = 'giorni dopo';
+$labels['alarmtitle'] = 'Prossimi eventi';
+$labels['dismissall'] = 'Scarta tutti';
+$labels['dismiss'] = 'Scarta';
+$labels['snooze'] = 'Sospendi';
+$labels['repeatinmin'] = 'Ripeti tra $min minuti';
+$labels['repeatinhr'] = 'Ripeti tra 1 ora';
+$labels['repeatinhrs'] = 'Ripeti tra $hrs ore';
+$labels['repeattomorrow'] = 'Ripeti domani';
+$labels['repeatinweek'] = 'Ripeti tra una settimana';
+$labels['showmore'] = 'Mostra altro...';
+$labels['frequency'] = 'Frequenza';
+$labels['never'] = 'una volta';
+$labels['daily'] = 'quotidiana';
+$labels['weekly'] = 'settimanale';
+$labels['monthly'] = 'mensile';
+$labels['yearly'] = 'annuale';
+$labels['every'] = 'Ogni';
+$labels['days'] = 'giorno/i';
+$labels['weeks'] = 'settimana/e';
+$labels['months'] = 'mese/i';
+$labels['years'] = 'anno/i in:';
+$labels['bydays'] = 'Di';
+$labels['untildate'] = 'il';
+$labels['each'] = 'Nei giorni';
+$labels['onevery'] = 'Ogni';
+$labels['onsamedate'] = 'Alla stessa data';
+$labels['forever'] = 'per sempre';
+$labels['recurrencend'] = 'fino al';
+$labels['forntimes'] = 'per $nr volte';
+$labels['first'] = 'primo';
+$labels['second'] = 'secondo';
+$labels['third'] = 'terzo';
+$labels['fourth'] = 'quarto';
+$labels['last'] = 'ultimo';
+$labels['dayofmonth'] = 'Giorno del mese';
+$labels['statusorganizer'] = 'Organizzatore';
+$labels['statustentative'] = 'Provvisorio';
+$labels['statusneeds-action'] = 'Necessaria azione';
+$labels['statusunknown'] = 'Sconosciuto';
+$labels['statuscompleted'] = 'Completato';
+$labels['statusin-process'] = 'In processo';
+$labels['itipinvitation'] = 'Invito a';
+$labels['itipupdate'] = 'Aggiornamento di';
+$labels['itipcancellation'] = 'Annullato:';
+$labels['itipreply'] = 'Rispondi a';
+$labels['itipaccepted'] = 'Accetta';
+$labels['itiptentative'] = 'Forse';
+$labels['itipdeclined'] = 'Rifiuta';
+$labels['itipdelegated'] = 'Delegato';
+$labels['itipobjectnotfound'] = 'L\'oggetto a cui si riferisce questo messaggio non Ăš stato trovato nel tuo account.';
+$labels['itipsubjectaccepted'] = '"$title" Ăš stato accettato da $name';
+$labels['itipsubjecttentative'] = '"$title" Ăš stato accettato con riserva da $name';
+$labels['itipsubjectdeclined'] = '"$title" Ăš stato rifiutato da $name';
+$labels['itipnewattendee'] = 'Questa Ăš una risposta da un nuovo partecipante';
+$labels['updateattendeestatus'] = 'Aggiorna lo stato dei partecipanti';
+$labels['acceptinvitation'] = 'Accetti questo invito?';
+$labels['youhaveaccepted'] = 'Hai accettato questo invito';
+$labels['youhavetentative'] = 'Hai accettato con riserva questo invito';
+$labels['youhavedeclined'] = 'Hai rifiutato questo invito';
+$labels['youhavedelegated'] = 'Hai delegato questo invito';
+$labels['attendeeaccepted'] = 'Il partecipante ha accettato';
+$labels['attendeetentative'] = 'Il partecipante ha accettato con riserva';
+$labels['attendeedeclined'] = 'Il partecipante ha rifiutato';
+$labels['notanattendee'] = 'Non sei nell\'elenco dei partecipanti per questo oggetto';
+$labels['importtocalendar'] = 'Salva nel mio calendario';
+$labels['removefromcalendar'] = 'Rimuovi dal mio calendario';
+$labels['updatemycopy'] = 'Aggiorna la mia copia';
+$labels['deleteobjectconfirm'] = 'Vuoi davvero eliminare questo oggetto?';
+$labels['declinedeleteconfirm'] = 'Vuoi che l\'oggetto rifiutato venga eliminato anche dal tuo account?';
+$labels['savingdata'] = 'Salvataggio dati...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/ja_JP.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,147 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'ăŸă§';
+$labels['at'] = 'た';
+$labels['alarmemail'] = 'ăƒĄăƒŒăƒ«é€äżĄ';
+$labels['alarmdisplay'] = 'ăƒĄăƒƒă‚»ăƒŒă‚žèĄšç€ș';
+$labels['alarmaudio'] = 'ă‚”ă‚Šăƒłăƒ‰ć†ç”Ÿ';
+$labels['alarmdisplayoption'] = 'ăƒĄăƒƒă‚»ăƒŒă‚ž';
+$labels['alarmemailoption'] = 'EăƒĄăƒŒăƒ«';
+$labels['alarmaudiooption'] = '音棰';
+$labels['alarmat'] = '$datetime に';
+$labels['trigger@'] = 'æ—„ä»˜ă«';
+$labels['trigger-M'] = '戆才';
+$labels['trigger-H'] = 'æ™‚é–“ć‰';
+$labels['trigger-D'] = 'ćˆ†ćŸŒ';
+$labels['trigger+M'] = 'ćˆ†ćŸŒ';
+$labels['trigger+H'] = 'æ™‚é–“ćŸŒ';
+$labels['trigger+D'] = 'æ—„ćŸŒ';
+$labels['addalarm'] = 'ă‚ąăƒ©ăƒŒăƒ èżœćŠ ';
+$labels['removealarm'] = 'ă‚ąăƒ©ăƒŒăƒ ć‰Šé™€';
+$labels['alarmtitle'] = 'ä»ŠćŸŒăźă‚€ăƒ™ăƒłăƒˆ';
+$labels['dismissall'] = '慚お扊陀';
+$labels['dismiss'] = '扊陀';
+$labels['snooze'] = 'ă‚čăƒŽăƒŒă‚ș';
+$labels['repeatinmin'] = '$min 仄憅でçč°èż”し';
+$labels['repeatinhr'] = '1æ™‚é–“ă§çč°èż”し';
+$labels['repeatinhrs'] = '$hrs でçč°èż”し';
+$labels['repeattomorrow'] = '明旄çč°èż”し';
+$labels['repeatinweek'] = '1週間でçč°èż”し';
+$labels['showmore'] = 'ă•ă‚‰ă«èĄšç€ș
';
+$labels['recurring'] = 'çč°èż”し';
+$labels['frequency'] = 'çč°èż”し';
+$labels['never'] = 'çč°èż”さăȘい';
+$labels['daily'] = 'æŻŽæ—„';
+$labels['weekly'] = 'æŻŽé€±';
+$labels['monthly'] = 'æŻŽæœˆ';
+$labels['yearly'] = 'æŻŽćčŽ';
+$labels['rdate'] = 'ăăźæ—„ă«';
+$labels['every'] = 'い぀でも';
+$labels['days'] = 'æ—„(s)';
+$labels['weeks'] = '週(s)';
+$labels['months'] = '月(s)';
+$labels['years'] = 'ćčŽ(s):';
+$labels['bydays'] = '侊';
+$labels['untildate'] = 'そぼ';
+$labels['each'] = 'いずれも';
+$labels['onevery'] = '搄';
+$labels['onsamedate'] = 'い぀か';
+$labels['forever'] = 'ずっべ';
+$labels['recurrencend'] = 'ăŸă§';
+$labels['untilenddate'] = 'æ—„ăŸă§';
+$labels['forntimes'] = '$nr ăŸă§(s)';
+$labels['first'] = '珏1週';
+$labels['second'] = '珏2週';
+$labels['third'] = '珏3週';
+$labels['fourth'] = '珏4週';
+$labels['last'] = '最甂週';
+$labels['dayofmonth'] = 'æ—„';
+$labels['addrdate'] = 'çč°èż”ă—æ—„èżœćŠ ';
+$labels['except'] = 'äŸ‹ć€–';
+$labels['statusorganizer'] = '線成者';
+$labels['statusaccepted'] = 'æ‰żè«Ÿă•ă‚ŒăŸă—ăŸ';
+$labels['statustentative'] = '仟';
+$labels['statusdeclined'] = 'èŸžé€€ă•ă‚ŒăŸă—ăŸ';
+$labels['statusdelegated'] = 'äŸé Œă•ă‚ŒăŸă—ăŸ';
+$labels['statusneeds-action'] = 'やるăčきäș‹';
+$labels['statusunknown'] = '䞍明';
+$labels['statuscompleted'] = '漌äș†';
+$labels['statusin-process'] = 'é€ČèĄŒäž­';
+$labels['itipinvitation'] = 'æ‹›ćŸ…ă™ă‚‹';
+$labels['itipupdate'] = '曎新';
+$labels['itipcancellation'] = 'ă‚­ăƒŁăƒłă‚»ăƒ«';
+$labels['itipreply'] = 'èż”äżĄ';
+$labels['itipaccepted'] = 'æ‰żè«Ÿ';
+$labels['itiptentative'] = 'ăŸă¶ă‚“';
+$labels['itipdeclined'] = '蟞退';
+$labels['itipdelegated'] = '代理äșș';
+$labels['itipneeds-action'] = 'ć»¶æœŸ';
+$labels['itipcomment'] = 'あăȘたぼ濜答';
+$labels['itipeditresponse'] = 'ćżœç­”æ–‡ăźć…„ćŠ›';
+$labels['itipsendercomment'] = 'é€äżĄè€…ăźă‚łăƒĄăƒłăƒˆ:';
+$labels['itipsuppressreply'] = '濜答を送らăȘい';
+$labels['itipobjectnotfound'] = 'ă“ăźăƒĄăƒƒă‚»ăƒŒă‚žă‹ă‚‰ć‚ç…§ă•ă‚Œă‚‹ă‚Șăƒ–ă‚žă‚§ă‚ŻăƒˆăŻă‚ąă‚«ă‚Šăƒłăƒˆă«ăŻèŠ‹ă€ă‹ă‚ŠăŸă›ă‚“ă€‚';
+$labels['itipsubjectaccepted'] = '$name が "$title" ă‚’æ‰żè«Ÿă—ăŸă—ăŸ';
+$labels['itipsubjecttentative'] = '$name が "$title" ă‚’ä»źæ‰żè«Ÿă—ăŸă—ăŸ';
+$labels['itipsubjectdeclined'] = '$name が "$title" ă‚’èŸžé€€ă—ăŸă—ăŸ';
+$labels['itipsubjectin-process'] = '"$title" は $name ă«ă‚ˆăŁăŠé€ČèĄŒäž­ă§ă™';
+$labels['itipsubjectcompleted'] = '"$title" は $name ă«ă‚ˆăŁăŠćźŒäș†ă•ă‚ŒăŸă—ăŸ';
+$labels['itipsubjectcancel'] = '"$title" まぼあăȘăŸăźć‚ćŠ ăŻă‚­ăƒŁăƒłă‚»ăƒ«ă•ă‚ŒăŸă—ăŸă€‚';
+$labels['itipsubjectdelegated'] = '"$title" は $name ă«ă‚ˆăŁăŠä»Łç†ă•ă‚ŒăŸă—ăŸ';
+$labels['itipsubjectdelegatedto'] = '"$title" は $name ă«ă‚ˆăŁăŠă‚ăȘăŸăžä»Łç†ă•ă‚ŒăŸă—ăŸ';
+$labels['itipnewattendee'] = 'ă“ă‚ŒăŻæ–°ă—ă„ć‚ćŠ è€…ă‹ă‚‰ăźćżœç­”ă§ă™';
+$labels['updateattendeestatus'] = 'ć‚ćŠ è€…ăźçŠ¶æłæ›Žæ–°';
+$labels['acceptinvitation'] = 'ă“ăźæ‹›ćŸ…ă‚’æ‰żè«Ÿă—ăŸă™ă‹?';
+$labels['acceptattendee'] = 'ć‚ćŠ è€…ă‚’æ‰żè«Ÿ';
+$labels['declineattendee'] = 'ć‚ćŠ è€…ă‚’æ–­ă‚‹';
+$labels['declineattendeeconfirm'] = 'æ–­ăŁăŸć‚ćŠ è€…ăžăźăƒĄăƒƒă‚»ăƒŒă‚žă‚’ć…„ćŠ›(ă‚Șăƒ—ă‚·ăƒ§ăƒł):';
+$labels['rsvpmodeall'] = 'ć…šă‚·ăƒȘăƒŒă‚ș';
+$labels['rsvpmodecurrent'] = 'ć‡ș杄äș‹ăźăż';
+$labels['rsvpmodefuture'] = 'こぼć‡ș杄äș‹ăšć°†æ„たć‡ș杄äș‹';
+$labels['itipsingleoccurrence'] = 'ă“ă‚ŒăŻäž€é€Łăźă‚€ăƒ™ăƒłăƒˆć€–ăź <em>ć˜äœ“ăźć‡ș杄äș‹</em> です';
+$labels['itipfutureoccurrence'] = 'äž€é€Łăźă‚€ăƒ™ăƒłăƒˆăź <em>こぼć‡ș杄äș‹ăšć°†æ„たć‡ș杄äș‹</em> を揂照する';
+$labels['itipmessagesingleoccurrence'] = 'ă“ăźăƒĄăƒƒă‚»ăƒŒă‚žăŻă“ăźć˜äœ“ăźć‡ș杄äș‹ăźăżă‚’ć‚ç…§ă—ăŸă™';
+$labels['itipmessagefutureoccurrence'] = 'ă“ăźăƒĄăƒƒă‚»ăƒŒă‚žăŻă“ăźć‡ș杄äș‹ăšă™ăčăŠăźć°†æ„ăźć‡ș杄äș‹ă‚’ć‚ç…§ă—ăŸă™';
+$labels['youhaveaccepted'] = 'あăȘăŸăŻă“ăźæ‹›ćŸ…ă‚’æ‰żè«Ÿă—ăŸă—ăŸ';
+$labels['youhavetentative'] = 'あăȘăŸăŻă“ăźæ‹›ćŸ…ă‚’ä»źæ‰żè«Ÿă—ăŸă—ăŸă€‚';
+$labels['youhavedeclined'] = 'あăȘăŸăŻă“ăźæ‹›ćŸ…ă‚’èŸžé€€ă—ăŸă—ăŸă€‚';
+$labels['youhavedelegated'] = 'あăȘăŸăŻă“ăźæ‹›ćŸ…ă‚’ä»ŁèĄŒă•ă›ăŸă—ăŸ';
+$labels['youhavein-process'] = 'あăȘたはこぼć‰Čćœ“ă‚’ćźŸèĄŒäž­ă§ă™';
+$labels['youhavecompleted'] = 'あăȘたはこぼć‰Čćœ“ă‚’ćźŒäș†ă—ăŸă—ăŸ';
+$labels['youhaveneeds-action'] = 'ă“ăźæ‹›ćŸ…ăžăźă‚ăȘăŸăźćżœç­”ăŻăŸă äżç•™äž­ă§ă™';
+$labels['youhavepreviouslyaccepted'] = 'あăȘăŸăŻă“ăźæ‹›ćŸ…ă‚’ć‰ă‚‚ăŁăŠæ‰żè«Ÿă—ăŸă—ăŸ';
+$labels['youhavepreviouslytentative'] = 'あăȘăŸăŻă“ăźæ‹›ćŸ…ă‚’ć‰ă‚‚ăŁăŠä»źæ‰żè«Ÿă—ăŸă—ăŸ';
+$labels['youhavepreviouslydeclined'] = 'あăȘăŸăŻă“ăźæ‹›ćŸ…ă‚’ć‰ă‚‚ăŁăŠæ–­ă‚ŠăŸă—ăŸ';
+$labels['youhavepreviouslydelegated'] = 'ă“ăźæ‹›ćŸ…ă‚’ć‰ă‚‚ăŁăŠä»ŁèĄŒă•ă›ăŸă—ăŸ';
+$labels['youhavepreviouslyin-process'] = 'あăȘたは才もっどこぼć‰Čćœ“ă‚’ćźŸèĄŒă™ă‚‹äș‹ă‚’ăƒŹăƒăƒŒăƒˆă—ăŸă—ăŸ';
+$labels['youhavepreviouslycompleted'] = 'あăȘたは才もっどこぼć‰Čćœ“ă‚’ćźŒäș†ă—ăŸă—ăŸ';
+$labels['youhavepreviouslyneeds-action'] = 'ă“ăźæ‹›ćŸ…ăžăźă‚ăȘăŸăźćżœç­”ăŻăŸă äżç•™äž­ă§ă™';
+$labels['attendeeaccepted'] = 'ć‚ćŠ è€…ăŻæ‰żè«Ÿă—ăŸă—ăŸ';
+$labels['attendeetentative'] = 'ć‚ćŠ è€…ăŻä»źæ‰żè«Ÿă—ăŸă—ăŸ';
+$labels['attendeedeclined'] = 'ć‚ćŠ è€…ăŻæ–­ă‚ŠăŸă—ăŸ';
+$labels['attendeedelegated'] = 'ć‚ćŠ è€…ăŻ $delegatedto ăžä»ŁèĄŒă•ă›ăŸă—ăŸ';
+$labels['attendeein-process'] = 'ć‚ćŠ è€…ăŻé€ČèĄŒäž­ă§ă™';
+$labels['attendeecompleted'] = 'ć‚ćŠ è€…ăŻćźŒäș†ă—ăŸă—ăŸ';
+$labels['notanattendee'] = 'ă“ăźă‚€ăƒ™ăƒłăƒˆăźć‡șćž­è€…ăšă—ăŠäž€èŠ§ă«ă‚ă‚ŠăŸă›ă‚“';
+$labels['outdatedinvitation'] = 'ă“ăźæ‹›ćŸ…ăŻæ–°ă—ă„ăƒăƒŒă‚žăƒ§ăƒłăžçœźăæ›ăˆă‚‰ă‚ŒăŸă—ăŸ';
+$labels['importtocalendar'] = 'ă‚«ăƒŹăƒłăƒ€ăƒŒă«äżć­˜';
+$labels['removefromcalendar'] = 'ă‚«ăƒŹăƒłăƒ€ăƒŒă‹ă‚‰ć‰Šé™€';
+$labels['updatemycopy'] = 'è€‡èŁœă‚’æ›Žæ–°';
+$labels['openpreview'] = 'ăƒ—ăƒŹăƒ“ăƒ„ăƒŒă‚’é–‹ă';
+$labels['deleteobjectconfirm'] = 'æœŹćœ“ă«ă“ăźă‚Șăƒ–ă‚žă‚§ă‚Żăƒˆă‚’ć‰Šé™€ă—ăŸă™ă‹?';
+$labels['declinedeleteconfirm'] = 'ă‚ąă‚«ă‚Šăƒłăƒˆă‹ă‚‰ă“ăźæ–­ăŁăŸă‚Șăƒ–ă‚žă‚§ă‚Żăƒˆă‚‚ć‰Šé™€ă—ăŸă™ă‹?';
+$labels['delegateinvitation'] = 'æ‹›ćŸ…ă‚’ä»ŁèĄŒă•ă›ă‚‹';
+$labels['delegateto'] = 'ä»ŁèĄŒć…ˆ';
+$labels['delegatersvpme'] = 'こぼć‡ș杄äș‹ăźæ›Žæ–°ă«é–ąă™ă‚‹é€šçŸ„を続ける';
+$labels['delegateinvalidaddress'] = 'ä»ŁèĄŒă•ă›ă‚‹ăźă«æœ‰ćŠčăȘEăƒĄăƒŒăƒ«ă‚ąăƒ‰ăƒŹă‚čă‚’ć…„ćŠ›ă—ăŠăă ă•ă„';
+$labels['savingdata'] = 'ăƒ‡ăƒŒă‚żă‚’äżć­˜äž­â€Š';
+$labels['expandattendeegroup'] = 'ă‚°ăƒ«ăƒŒăƒ—ăƒĄăƒłăƒăƒŒăźä»Łç†äșș';
+$labels['expandattendeegroupnodata'] = 'ă“ăźă‚°ăƒ«ăƒŒăƒ—ăźä»Łç†ăŒă§ăăŸă›ă‚“ă€‚ăƒĄăƒłăƒăƒŒăŒèŠ‹ă€ă‹ă‚ŠăŸă›ă‚“ă€‚';
+$labels['expandattendeegrouperror'] = 'ă“ăźă‚°ăƒ«ăƒŒăƒ—ăźä»Łç†ăŒă§ăăŸă›ă‚“ă€‚ăŠăă‚‰ăăƒĄăƒłăƒăƒŒăŒć€šă™ăŽăŸă™ă€‚';
+$labels['expandattendeegroupsizelimit'] = 'ă“ăźă‚°ăƒ«ăƒŒăƒ—ăŻä»Łç†ă™ă‚‹ă«ăŻăƒĄăƒłăƒăƒŒăŒć€šă™ăŽăŸă™ă€‚';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/ko_KR.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'êčŒì§€';
+$labels['frequency'] = '반볔';
+$labels['recurrencend'] = 'êčŒì§€';
+$labels['statusorganizer'] = 'ìŁŒì”œìž';
+$labels['statustentative'] = '임시';
+$labels['statusunknown'] = '알 수 없는';
+$labels['savingdata'] = 'ìžëŁŒ 저임쀑...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/ku_IQ.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,8 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/nl_NL.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'tot';
+$labels['at'] = 'om';
+$labels['alarmemail'] = 'E-mail versturen';
+$labels['alarmdisplay'] = 'Bericht weergeven';
+$labels['alarmaudio'] = 'Geluid afspelen';
+$labels['alarmdisplayoption'] = 'Bericht';
+$labels['alarmemailoption'] = 'E-mail';
+$labels['alarmaudiooption'] = 'Geluid';
+$labels['alarmat'] = 'op $datetime';
+$labels['trigger@'] = 'op datum';
+$labels['trigger-M'] = 'minuten voor';
+$labels['trigger-H'] = 'uren voor';
+$labels['trigger-D'] = 'dagen voor';
+$labels['trigger+M'] = 'minuten na';
+$labels['trigger+H'] = 'uren na';
+$labels['trigger+D'] = 'dagen na';
+$labels['triggerend-M'] = 'minuten vóór einde';
+$labels['triggerend-H'] = 'uren vóór einde';
+$labels['triggerend-D'] = 'dagen vóór einde';
+$labels['triggerend+M'] = 'minuten na einde';
+$labels['triggerend+H'] = 'uren na einde';
+$labels['triggerend+D'] = 'dagen na einde';
+$labels['trigger0'] = 'op tijd';
+$labels['triggerattime'] = 'op begintijd';
+$labels['triggerattimeend'] = 'op eindtijd';
+$labels['relatedstart'] = 'begin';
+$labels['relatedendevent'] = 'einde';
+$labels['relatedendtask'] = 'vervaltijd';
+$labels['addalarm'] = 'Herinnering toevoegen';
+$labels['removealarm'] = 'Herinnering verwijderen';
+$labels['alarmtitle'] = 'Geplande activiteiten';
+$labels['dismissall'] = 'Alles negeren';
+$labels['dismiss'] = 'Negeren';
+$labels['snooze'] = 'Uitstellen';
+$labels['repeatinmin'] = 'Binnen $min minuten herhalen';
+$labels['repeatinhr'] = 'Binnen 1 uur herhalen';
+$labels['repeatinhrs'] = 'Binnen $hrs uren herhalen';
+$labels['repeattomorrow'] = 'Morgen herhalen';
+$labels['repeatinweek'] = 'Binnen een week herhalen';
+$labels['showmore'] = 'Meer tonen...';
+$labels['recurring'] = 'Herhalingen';
+$labels['frequency'] = 'Herhalen';
+$labels['never'] = 'nooit';
+$labels['daily'] = 'elke dag';
+$labels['weekly'] = 'elke week';
+$labels['monthly'] = 'elke maand';
+$labels['yearly'] = 'elk jaar';
+$labels['rdate'] = 'op datums';
+$labels['every'] = 'Elke';
+$labels['days'] = 'dag(en)';
+$labels['weeks'] = 'week / weken';
+$labels['months'] = 'maand(en)';
+$labels['years'] = 'jaar (jaren)';
+$labels['bydays'] = 'Op';
+$labels['untildate'] = 'de';
+$labels['each'] = 'Elke';
+$labels['onevery'] = 'Op elke';
+$labels['onsamedate'] = 'Op dezelfde datum';
+$labels['forever'] = 'voor altijd';
+$labels['recurrencend'] = 'tot';
+$labels['untilenddate'] = 'tot datum';
+$labels['forntimes'] = 'voor $nr keer';
+$labels['first'] = 'eerste';
+$labels['second'] = 'tweede';
+$labels['third'] = 'derde';
+$labels['fourth'] = 'vierde';
+$labels['last'] = 'laatste';
+$labels['dayofmonth'] = 'Dag van de maand';
+$labels['addrdate'] = 'Herhaaldatum toevoegen';
+$labels['except'] = 'behalve';
+$labels['statusorganizer'] = 'Organisator';
+$labels['statusaccepted'] = 'Geaccepteerd';
+$labels['statustentative'] = 'Misschien';
+$labels['statusdeclined'] = 'Afgeslagen';
+$labels['statusdelegated'] = 'Gedelegeerd';
+$labels['statusneeds-action'] = 'Actie vereist';
+$labels['statusunknown'] = 'Onbekend';
+$labels['statuscompleted'] = 'Voltooid';
+$labels['statusin-process'] = 'In behandeling';
+$labels['itipinvitation'] = 'Uitnodiging voor';
+$labels['itipupdate'] = 'Update van';
+$labels['itipcancellation'] = 'Geannuleerd:';
+$labels['itipreply'] = 'Antwoord aan';
+$labels['itipaccepted'] = 'Accepteren';
+$labels['itiptentative'] = 'Misschien';
+$labels['itipdeclined'] = 'Afslaan';
+$labels['itipdelegated'] = 'Gedelegeerde';
+$labels['itipneeds-action'] = 'Uitstellen';
+$labels['itipcomment'] = 'Uw antwoord';
+$labels['itipeditresponse'] = 'Typ het antwoord';
+$labels['itipsendercomment'] = 'Opmerking van afzender:';
+$labels['itipsuppressreply'] = 'Geen antwoord sturen';
+$labels['itipobjectnotfound'] = 'Het object uit dit bericht is niet in uw account gevonden.';
+$labels['itipsubjectaccepted'] = '"$title" is geaccepteerd door $name';
+$labels['itipsubjecttentative'] = '"$title" is onder voorbehoud geaccepteerd door $name';
+$labels['itipsubjectdeclined'] = '"$title" is afgeslagen door $name';
+$labels['itipsubjectin-process'] = '"$title" wordt behandeld door $name';
+$labels['itipsubjectcompleted'] = '"$title" is voltooid door $name';
+$labels['itipsubjectcancel'] = 'Uw deelname aan "$title" is geannuleerd';
+$labels['itipsubjectdelegated'] = '"$title" is gedelegeerd door $name';
+$labels['itipsubjectdelegatedto'] = '"$title" is door $name gedelegeerd aan u';
+$labels['itipnewattendee'] = 'Dit is een antwoord van een nieuwe deelnemer';
+$labels['updateattendeestatus'] = 'Status van deelnemer bijwerken';
+$labels['acceptinvitation'] = 'Accepteert u deze uitnodiging?';
+$labels['acceptattendee'] = 'Deelnemer accepteren';
+$labels['declineattendee'] = 'Deelnemer afwijzen';
+$labels['declineattendeeconfirm'] = 'Voeg een bericht toe voor de afgewezen deelnemer (optioneel):';
+$labels['rsvpmodeall'] = 'De gehele reeks';
+$labels['rsvpmodecurrent'] = 'Alleen deze activiteit';
+$labels['rsvpmodefuture'] = 'Deze en toekomstige activiteiten';
+$labels['itipsingleoccurrence'] = 'Dit is <em>een enkele activiteit</em> uit een reeks activiteiten';
+$labels['itipfutureoccurrence'] = 'Verwijst naar <em>deze en alle toekomstige activiteiten</em> uit een reeks activiteiten';
+$labels['itipmessagesingleoccurrence'] = 'Het bericht verwijst alleen naar deze activiteit';
+$labels['itipmessagefutureoccurrence'] = 'Het bericht verwijst naar deze en alle toekomstige activiteiten';
+$labels['youhaveaccepted'] = 'U hebt deze uitnodiging geaccepteerd';
+$labels['youhavetentative'] = 'U hebt deze uitnodiging onder voorbehoud geaccepteerd';
+$labels['youhavedeclined'] = 'U hebt deze uitnodiging afgeslagen';
+$labels['youhavedelegated'] = 'U hebt deze uitnodiging gedelegeerd';
+$labels['youhavein-process'] = 'U werkt momenteel aan deze opdracht';
+$labels['youhavecompleted'] = 'U hebt deze opdracht voltooid';
+$labels['youhaveneeds-action'] = 'U hebt deze uitnodiging nog niet beantwoord';
+$labels['youhavepreviouslyaccepted'] = 'U hebt deze uitnodiging eerder al geaccepteerd';
+$labels['youhavepreviouslytentative'] = 'U hebt deze uitnodiging eerder al onder voorbehoud geaccepteerd';
+$labels['youhavepreviouslydeclined'] = 'U hebt deze uitnodiging eerder al afgeslagen';
+$labels['youhavepreviouslydelegated'] = 'U hebt deze uitnodiging eerder al gedelegeerd';
+$labels['youhavepreviouslyin-process'] = 'U hebt eerder al gemeld dat u aan deze opdracht werkt';
+$labels['youhavepreviouslycompleted'] = 'U hebt deze opdracht eerder al voltooid';
+$labels['youhavepreviouslyneeds-action'] = 'U hebt deze uitnodiging nog niet beantwoord';
+$labels['attendeeaccepted'] = 'Deelnemer heeft geaccepteerd';
+$labels['attendeetentative'] = 'Deelnemer heeft onder voorbehoud geaccepteerd';
+$labels['attendeedeclined'] = 'Deelnemer heeft afgeslagen';
+$labels['attendeedelegated'] = 'Deelnemer heeft gedelegeerd aan $delegatedto';
+$labels['attendeein-process'] = 'De deelnemer heeft de status gewijzigd in ‘In behandeling’';
+$labels['attendeecompleted'] = 'De deelnemer heeft de status gewijzigd in ‘Voltooid’';
+$labels['notanattendee'] = 'U staat niet op de lijst met deelnemers aan deze activiteit';
+$labels['outdatedinvitation'] = 'Deze uitnodiging is vervangen door een nieuwere versie';
+$labels['importtocalendar'] = 'Opslaan in mijn agenda';
+$labels['removefromcalendar'] = 'Verwijderen uit mijn agenda';
+$labels['updatemycopy'] = 'Mijn exemplaar bijwerken';
+$labels['openpreview'] = 'Voorbeeld openen';
+$labels['deleteobjectconfirm'] = 'Weet u zeker dat u dit object wilt verwijderen?';
+$labels['declinedeleteconfirm'] = 'Wilt u dit afgewezen object ook verwijderen uit uw account?';
+$labels['delegateinvitation'] = 'Uitnodiging delegeren';
+$labels['delegateto'] = 'Delegeren aan';
+$labels['delegatersvpme'] = 'Houd me op de hoogte over updates voor deze incidentie';
+$labels['delegateinvalidaddress'] = 'Voer een geldig e-mailadres voor de gedelegeerde in';
+$labels['savingdata'] = 'Gegevens opslaan...';
+$labels['expandattendeegroup'] = 'Vervangen door groepsleden';
+$labels['expandattendeegroupnodata'] = 'Deze groep kan niet worden vervangen. Geen leden gevonden.';
+$labels['expandattendeegrouperror'] = 'Deze groep kan niet worden vervangen. De groep bevat mogelijk te veel leden.';
+$labels['expandattendeegroupsizelimit'] = 'Deze groep bevat te veel leden om te vervangen.';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/pl_PL.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'dopĂłki';
+$labels['frequency'] = 'PowtĂłrz';
+$labels['recurrencend'] = 'dopĂłki';
+$labels['statusorganizer'] = 'Organizator';
+$labels['statustentative'] = 'Niepewny';
+$labels['statusunknown'] = 'Nieznany';
+$labels['savingdata'] = 'Zapisuję dane...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/pt_BR.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'até';
+$labels['at'] = 'no';
+$labels['alarmemail'] = 'Enviar Email';
+$labels['alarmdisplay'] = 'Mostrar mensagem';
+$labels['alarmdisplayoption'] = 'Mensagem';
+$labels['alarmemailoption'] = 'Email';
+$labels['alarmat'] = 'como $datetime';
+$labels['trigger@'] = 'na data';
+$labels['trigger-M'] = 'minutos antes';
+$labels['trigger-H'] = 'horas antes';
+$labels['trigger-D'] = 'dias antes';
+$labels['trigger+M'] = 'minutos depois';
+$labels['trigger+H'] = 'horas depois';
+$labels['trigger+D'] = 'dias depois';
+$labels['alarmtitle'] = 'PrĂłximos eventos';
+$labels['dismissall'] = 'Dispensar tudo';
+$labels['dismiss'] = 'Dispensar';
+$labels['snooze'] = 'Tirar uma soneca';
+$labels['repeatinmin'] = 'Repetir em $min minutos';
+$labels['repeatinhr'] = 'Repetir em 1 hora';
+$labels['repeatinhrs'] = 'Repetir em $hrs horas';
+$labels['repeattomorrow'] = 'Repetir amanhĂŁ';
+$labels['repeatinweek'] = 'Repetir em uma semana';
+$labels['showmore'] = 'Mostrar mais...';
+$labels['frequency'] = 'Repetir';
+$labels['never'] = 'nunca';
+$labels['daily'] = 'diariamente';
+$labels['weekly'] = 'semanalmente';
+$labels['monthly'] = 'mensalmente';
+$labels['yearly'] = 'anualmente';
+$labels['every'] = 'À cada';
+$labels['days'] = 'dia(s)';
+$labels['weeks'] = 'semana(s)';
+$labels['months'] = 'mĂȘs(es)';
+$labels['years'] = 'ano(s) em:';
+$labels['bydays'] = 'No';
+$labels['untildate'] = 'em';
+$labels['each'] = 'Cada';
+$labels['onevery'] = 'Em cada';
+$labels['onsamedate'] = 'Na mesma data';
+$labels['forever'] = 'nunca termina';
+$labels['recurrencend'] = 'até';
+$labels['forntimes'] = 'por $nr vez(es)';
+$labels['first'] = 'primeira';
+$labels['second'] = 'segunda';
+$labels['third'] = 'terceira';
+$labels['fourth'] = 'quarta';
+$labels['last'] = 'Ășltima';
+$labels['dayofmonth'] = 'Dia do mĂȘs';
+$labels['statusorganizer'] = 'Organizador';
+$labels['statustentative'] = 'Tentativa';
+$labels['statusneeds-action'] = 'Needs action';
+$labels['statusunknown'] = 'Desconhecido';
+$labels['statuscompleted'] = 'Completed';
+$labels['statusin-process'] = 'In process';
+$labels['itipinvitation'] = 'Convite para';
+$labels['itipupdate'] = 'Atualização de';
+$labels['itipcancellation'] = 'Cancelado:';
+$labels['itipreply'] = 'Responder para';
+$labels['itipaccepted'] = 'Aceitar';
+$labels['itiptentative'] = 'Talvez';
+$labels['itipdeclined'] = 'Rejeitar';
+$labels['itipdelegated'] = 'Delegado';
+$labels['itipobjectnotfound'] = 'O objeto referenciado por esta mensagem nĂŁo foi encontrado em sua conta.';
+$labels['itipsubjectaccepted'] = '"$title" foi aceito por $name';
+$labels['itipsubjecttentative'] = '"$title" foi aceito como tentativa por $name';
+$labels['itipsubjectdeclined'] = '"$title" foi recusado por $name';
+$labels['itipnewattendee'] = 'Esta Ă© a resposta de um novo participante';
+$labels['updateattendeestatus'] = 'Atualizar o estado dos participantes';
+$labels['acceptinvitation'] = 'VocĂȘ aceita este convite?';
+$labels['acceptattendee'] = 'Aceitar participante';
+$labels['declineattendee'] = 'Recusar participante';
+$labels['youhaveaccepted'] = 'VocĂȘ aceitou este convite';
+$labels['youhavetentative'] = 'VocĂȘ aceitou como tentativa este convite';
+$labels['youhavedeclined'] = 'VocĂȘ recusou este convite';
+$labels['youhavedelegated'] = 'VocĂȘ delegou este convite';
+$labels['attendeeaccepted'] = 'O participante aceitou';
+$labels['attendeetentative'] = 'O participante aceitou temporariamente';
+$labels['attendeedeclined'] = 'O participante recusou';
+$labels['attendeedelegated'] = 'O participante delegou para $delegatedto';
+$labels['importtocalendar'] = 'Salvar em meu calendĂĄrio';
+$labels['removefromcalendar'] = 'Remover do meu calendĂĄrio';
+$labels['savingdata'] = 'Salvando dados...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/pt_PT.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,16 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'até';
+$labels['alarmemailoption'] = 'Email';
+$labels['frequency'] = 'Repetir';
+$labels['recurrencend'] = 'até';
+$labels['statusorganizer'] = 'Organizador';
+$labels['statustentative'] = 'Tentativa';
+$labels['statusunknown'] = 'Desconhecido';
+$labels['savingdata'] = 'Salvando dados...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/ro.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,8 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/ru_RU.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,159 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€ŃŃ‚ŃŒ';
+$labels['at'] = 'ĐČ';
+$labels['alarmemail'] = 'ĐŸĐŸŃĐ»Đ°Ń‚ŃŒ e-mail';
+$labels['alarmdisplay'] = 'ĐŸĐŸĐșĐ°Đ·Đ°Ń‚ŃŒ ŃĐŸĐŸĐ±Ń‰Đ”ĐœĐžĐ”';
+$labels['alarmaudio'] = 'ĐŸŃ€ĐŸĐžĐłŃ€Đ°Ń‚ŃŒ ĐŒĐ”Đ»ĐŸĐŽĐžŃŽ';
+$labels['alarmdisplayoption'] = 'ĐĄĐŸĐŸĐ±Ń‰Đ”ĐœĐžĐ”';
+$labels['alarmemailoption'] = 'Email';
+$labels['alarmaudiooption'] = 'ĐœĐ”Đ»ĐŸĐŽĐžŃ';
+$labels['alarmat'] = 'ĐœĐ° $datetime';
+$labels['trigger@'] = 'ĐœĐ° Юату';
+$labels['trigger-M'] = 'ĐŒĐžĐœŃƒŃ‚ ĐŽĐŸ';
+$labels['trigger-H'] = 'Ń‡Đ°ŃĐŸĐČ ĐŽĐŸ';
+$labels['trigger-D'] = 'ĐŽĐœĐ”Đč ĐŽĐŸ';
+$labels['trigger+M'] = 'ĐŒĐžĐœŃƒŃ‚ ĐżĐŸŃĐ»Đ”';
+$labels['trigger+H'] = 'Ń‡Đ°ŃĐŸĐČ ĐżĐŸŃĐ»Đ”';
+$labels['trigger+D'] = 'ĐŽĐœĐ”Đč ĐżĐŸŃĐ»Đ”';
+$labels['triggerend-M'] = 'ĐŒĐžĐœŃƒŃ‚ ĐŽĐŸ ĐŸĐșĐŸĐœŃ‡Đ°ĐœĐžŃ';
+$labels['triggerend-H'] = 'Ń‡Đ°ŃĐŸĐČ ĐŽĐŸ ĐŸĐșĐŸĐœŃ‡Đ°ĐœĐžŃ';
+$labels['triggerend-D'] = 'ĐŽĐœĐ”Đč ĐŽĐŸ ĐŸĐșĐŸĐœŃ‡Đ°ĐœĐžŃ';
+$labels['triggerend+M'] = 'ĐŒĐžĐœŃƒŃ‚ ĐżĐŸŃĐ»Đ” ĐŸĐșĐŸĐœŃ‡Đ°ĐœĐžŃ';
+$labels['triggerend+H'] = 'Ń‡Đ°ŃĐŸĐČ ĐżĐŸŃĐ»Đ” ĐŸĐșĐŸĐœŃ‡Đ°ĐœĐžŃ';
+$labels['triggerend+D'] = 'ĐŽĐœĐ”Đč ĐżĐŸŃĐ»Đ” ĐŸĐșĐŸĐœŃ‡Đ°ĐœĐžŃ';
+$labels['trigger0'] = 'ĐČĐŸ ĐČŃ€Đ”ĐŒŃ';
+$labels['triggerattime'] = 'ĐČ ĐŒĐŸĐŒĐ”ĐœŃ‚ ĐœĐ°Ń‡Đ°Đ»Đ°';
+$labels['triggerattimeend'] = 'ĐČ ĐŒĐŸĐŒĐ”ĐœŃ‚ ĐŸĐșĐŸĐœŃ‡Đ°ĐœĐžŃ';
+$labels['relatedstart'] = 'ĐœĐ°Ń‡Đ°Đ»ĐŸ';
+$labels['relatedendevent'] = 'ĐŸĐșĐŸĐœŃ‡Đ°ĐœĐžĐ”';
+$labels['relatedendtask'] = 'ŃŃ€ĐŸĐș';
+$labels['addalarm'] = 'Đ”ĐŸĐ±Đ°ĐČоть ĐœĐ°ĐżĐŸĐŒĐžĐœĐ°ĐœĐžĐ”';
+$labels['removealarm'] = 'ĐŁĐŽĐ°Đ»ĐžŃ‚ŃŒ уĐČĐ”ĐŽĐŸĐŒĐ»Đ”ĐœĐžĐ”';
+$labels['alarmtitle'] = 'ĐŸŃ€Đ”ĐŽŃŃ‚ĐŸŃŃ‰ĐžĐ” ŃĐŸĐ±Ń‹Ń‚ĐžŃ';
+$labels['dismissall'] = 'ĐžŃ‚ĐŒĐ”ĐœĐžŃ‚ŃŒ ĐČсД';
+$labels['dismiss'] = 'ĐžŃ‚ĐŒĐ”ĐœĐžŃ‚ŃŒ';
+$labels['snooze'] = 'ĐžŃ‚Đ»ĐŸĐ¶ĐžŃ‚ŃŒ';
+$labels['repeatinmin'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€ĐžŃ‚ŃŒ чДрДз $min minutes';
+$labels['repeatinhr'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€ĐžŃ‚ŃŒ чДрДз 1 час';
+$labels['repeatinhrs'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€ĐžŃ‚ŃŒ чДрДз $hrs Ń‡Đ°ŃĐŸĐČ';
+$labels['repeattomorrow'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€ĐžŃ‚ŃŒ Đ·Đ°ĐČтра';
+$labels['repeatinweek'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€ĐžŃ‚ŃŒ чДрДз ĐœĐ”ĐŽĐ”Đ»ŃŽ';
+$labels['showmore'] = 'ĐŸĐŸĐșĐ°Đ·Đ°Ń‚ŃŒ Đ±ĐŸĐ»ŃŒŃˆĐ”...';
+$labels['recurring'] = 'ĐĄĐŸĐČĐżĐ°ĐŽĐ”ĐœĐžŃ';
+$labels['frequency'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€ĐžŃ‚ŃŒ';
+$labels['never'] = 'ĐœĐžĐșĐŸĐłĐŽĐ°';
+$labels['daily'] = 'Đ”Đ¶Đ”ĐŽĐœĐ”ĐČĐœĐŸ';
+$labels['weekly'] = 'Đ”Đ¶Đ”ĐœĐ”ĐŽĐ”Đ»ŃŒĐœĐŸ';
+$labels['monthly'] = 'Đ”Đ¶Đ”ĐŒĐ”ŃŃŃ‡ĐœĐŸ';
+$labels['yearly'] = 'Đ”Đ¶Đ”ĐłĐŸĐŽĐœĐŸ';
+$labels['rdate'] = 'on dates';
+$labels['every'] = 'КажЎыĐč(ую)';
+$labels['days'] = 'ĐŽĐ”ĐœŃŒ';
+$labels['weeks'] = 'ĐœĐ”ĐŽĐ”Đ»ŃŽ';
+$labels['months'] = 'ĐŒĐ”ŃŃŃ†';
+$labels['years'] = 'ĐłĐŸĐŽ ĐČ:';
+$labels['bydays'] = 'В';
+$labels['untildate'] = 'ĐŽĐŸ';
+$labels['each'] = 'ĐšĐ°Đ¶ĐŽĐŸĐłĐŸ';
+$labels['onevery'] = 'В';
+$labels['onsamedate'] = 'В ту жД ŃĐ°ĐŒŃƒŃŽ Юату';
+$labels['forever'] = 'ĐČсДгЎа';
+$labels['recurrencend'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€ŃŃ‚ŃŒ';
+$labels['untilenddate'] = 'ĐŽĐŸ';
+$labels['forntimes'] = '$nr раз(Đ°)';
+$labels['first'] = 'пДрĐČыĐč(ую)';
+$labels['second'] = 'ĐČŃ‚ĐŸŃ€Ń‹Đč(ую)';
+$labels['third'] = 'трДтОĐč(ую)';
+$labels['fourth'] = 'чДтĐČДртыĐč(ую)';
+$labels['last'] = 'ĐżĐŸŃĐ»Đ”ĐŽĐœĐžĐč(ую)';
+$labels['dayofmonth'] = 'Đ”Đ”ĐœŃŒ ĐŒĐ”ŃŃŃ†Đ°';
+$labels['addrdate'] = 'Add repeat date';
+$labels['except'] = 'ОсĐșĐ»ŃŽŃ‡Đ°Ń';
+$labels['statusorganizer'] = 'ĐžŃ€ĐłĐ°ĐœĐžĐ·Đ°Ń‚ĐŸŃ€';
+$labels['statusaccepted'] = 'ĐŸŃ€ĐžĐœŃŃ‚ĐŸ';
+$labels['statustentative'] = 'ĐĐ”ĐŸĐżŃ€Đ”ĐŽĐ”Đ»Ń‘ĐœĐœĐŸ';
+$labels['statusdeclined'] = 'ОтĐșĐ»ĐŸĐœĐ”ĐœĐŸ';
+$labels['statusdelegated'] = 'ĐŸĐŸŃ€ŃƒŃ‡Đ”ĐœĐŸ';
+$labels['statusneeds-action'] = 'ĐąŃ€Đ”Đ±ŃƒĐ”Ń‚ ĐŽĐ”ĐčстĐČоя';
+$labels['statusunknown'] = 'ĐĐ”ĐžĐ·ĐČĐ”ŃŃ‚ĐœĐŸ';
+$labels['statuscompleted'] = 'ЗаĐČĐ”Ń€ŃˆĐ”ĐœĐœŃ‹Đ”';
+$labels['statusin-process'] = 'В ĐżŃ€ĐŸŃ†Đ”ŃŃĐ”';
+$labels['itipinvitation'] = 'ĐŸŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ” ĐœĐ°';
+$labels['itipupdate'] = 'ĐžĐ±ĐœĐŸĐČĐ»Đ”ĐœĐžĐ”';
+$labels['itipcancellation'] = 'ĐžŃ‚ĐŒĐ”ĐœŃ‘ĐœĐœŃ‹Đč:';
+$labels['itipreply'] = 'ОтĐČĐ”Ń‚ĐžŃ‚ŃŒ';
+$labels['itipaccepted'] = 'ĐŸŃ€ĐžĐœŃŃ‚ŃŒ';
+$labels['itiptentative'] = 'ĐœĐŸĐ¶Đ”Ń‚ Đ±Ń‹Ń‚ŃŒ';
+$labels['itipdeclined'] = 'ОтĐșĐ»ĐŸĐœĐžŃ‚ŃŒ';
+$labels['itipdelegated'] = 'ĐŸŃ€Đ”ĐŽŃŃ‚Đ°ĐČĐžŃ‚Đ”Đ»ŃŒ';
+$labels['itipneeds-action'] = 'ĐžŃ‚Đ»ĐŸĐ¶ĐžŃ‚ŃŒ';
+$labels['itipcomment'] = 'Ваш ĐŸŃ‚ĐČДт';
+$labels['itipeditresponse'] = 'ВĐČДЎОтД Ń‚Đ”Đșст ĐŸŃ‚ĐČДта';
+$labels['itipsendercomment'] = 'ĐšĐŸĐŒĐŒĐ”ĐœŃ‚Đ°Ń€ĐžĐč ĐŸŃ‚ĐżŃ€Đ°ĐČĐžŃ‚Đ”Đ»Ń:';
+$labels['itipsuppressreply'] = 'ĐĐ” ĐŸŃ‚ĐżŃ€Đ°ĐČĐ»ŃŃ‚ŃŒ ĐŸŃ‚ĐČДт';
+$labels['itipobjectnotfound'] = 'ОбъДĐșт, уĐșĐ°Đ·Đ°ĐœĐœŃ‹Đč ĐČ ĐŽĐ°ĐœĐœĐŸĐŒ ŃĐŸĐŸĐ±Ń‰Đ”ĐœĐžĐž ĐœĐ” был ĐœĐ°ĐčĐŽĐ”Đœ ĐČ ĐČĐ°ŃˆĐ”ĐŒ Đ°ĐșĐșĐ°ŃƒĐœŃ‚Đ”.';
+$labels['itipsubjectaccepted'] = '"$title" ĐżŃ€ĐžĐœŃŃ‚ĐŸ $name';
+$labels['itipsubjecttentative'] = '"$title" прДЎĐČĐ°Ń€ĐžŃ‚Đ”Đ»ŃŒĐœĐŸ ĐżŃ€ĐžĐœŃŃ‚ĐŸ $name';
+$labels['itipsubjectdeclined'] = '"$title" ĐŸŃ‚ĐșĐ»ĐŸĐœĐ”ĐœĐŸ $name';
+$labels['itipsubjectin-process'] = '"$title" ĐŸĐ±Ń€Đ°Đ±Đ°Ń‚Ń‹ĐČĐ°Đ”Ń‚ŃŃ $name';
+$labels['itipsubjectcompleted'] = '"$title" Đ·Đ°ĐČĐ”Ń€ŃˆĐ”ĐœĐ° $name';
+$labels['itipsubjectcancel'] = 'Đ’Đ°ŃˆĐ” ŃƒŃ‡Đ°ŃŃ‚ĐžĐ” ĐČ "$title" Đ±Ń‹Đ»ĐŸ ĐŸŃ‚ĐŒĐ”ĐœĐ”ĐœĐŸ';
+$labels['itipsubjectdelegated'] = '"$title" Đ±Ń‹Đ»ĐŸ ĐżĐŸŃ€ŃƒŃ‡Đ”ĐœĐŸ $name';
+$labels['itipsubjectdelegatedto'] = '"$title" Đ±Ń‹Đ»ĐŸ ĐżĐŸŃ€ŃƒŃ‡Đ”ĐœĐŸ Đ’Đ°ĐŒ $name';
+$labels['itipnewattendee'] = 'Đ­Ń‚ĐŸ ĐŸŃ‚ĐČДт ĐŸŃ‚ ĐœĐŸĐČĐŸĐłĐŸ ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐ°';
+$labels['updateattendeestatus'] = 'ĐžĐ±ĐœĐŸĐČоть статус ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐ°';
+$labels['acceptinvitation'] = 'Вы ĐżŃ€ĐžĐœĐžĐŒĐ°Đ”Ń‚Đ” ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ”?';
+$labels['acceptattendee'] = 'Đ”ĐŸĐ±Đ°ĐČоть ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐ°';
+$labels['declineattendee'] = 'ĐŁĐŽĐ°Đ»ĐžŃ‚ŃŒ ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐ°';
+$labels['declineattendeeconfirm'] = 'ВĐČДЎОтД ŃĐŸĐŸĐ±Ń‰Đ”ĐœĐžĐ” ĐŽĐ»Ń ĐŸŃ‚ĐșĐ»ĐŸĐœŃ‘ĐœĐœĐŸĐłĐŸ ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐ° (ĐœĐ” ĐŸĐ±ŃĐ·Đ°Ń‚Đ”Đ»ŃŒĐœĐŸ):';
+$labels['rsvpmodeall'] = 'Вся ŃĐ”Ń€ĐžŃ';
+$labels['rsvpmodecurrent'] = 'ĐąĐŸĐ»ŃŒĐșĐŸ ŃŃ‚ĐŸ ŃĐŸĐ±Ń‹Ń‚ĐžĐ”';
+$labels['rsvpmodefuture'] = 'Đ­Ń‚ĐŸ Đž ĐżĐŸŃĐ»Đ”ĐŽŃƒŃŽŃ‰ĐžĐ” ĐżĐŸĐČŃ‚ĐŸŃ€Đ”ĐœĐžŃ';
+$labels['itipsingleoccurrence'] = 'Đ­Ń‚ĐŸ <em>Đ”ĐŽĐžĐœĐžŃ‡ĐœĐŸĐ” ŃĐŸĐ±Ń‹Ń‚ĐžĐ”</em> Оз сДрОО ŃĐŸĐ±Ń‹Ń‚ĐžĐč';
+$labels['itipfutureoccurrence'] = 'ĐžŃ‚ĐœĐŸŃĐžŃ‚ŃŃ Đș <em>ŃŃ‚ĐŸĐŒŃƒ Đž ĐČŃĐ”ĐŒ ĐżĐŸŃĐ»Đ”ĐŽŃƒŃŽŃ‰ĐžĐŒ ĐżĐŸĐČŃ‚ĐŸŃ€Đ”ĐœĐžŃĐŒĐž</em> сДрОО ŃĐŸĐ±Ń‹Ń‚ĐžĐč';
+$labels['itipmessagesingleoccurrence'] = 'Đ­Ń‚ĐŸ ŃĐŸĐŸĐ±Ń‰Đ”ĐœĐžĐ” ĐŸŃ‚ĐœĐŸŃĐžŃ‚ŃŃ Ń‚ĐŸĐ»ŃŒĐșĐŸ Đș ŃŃ‚ĐŸĐŒŃƒ Đ”ĐŽĐžĐœĐžŃ‡ĐœĐŸĐŒŃƒ ŃĐŸĐ±Ń‹Ń‚ĐžŃŽ';
+$labels['itipmessagefutureoccurrence'] = 'Đ­Ń‚ĐŸ ŃĐŸĐŸĐ±Ń‰Đ”ĐœĐžĐ” ĐŸŃ‚ĐœĐŸŃĐžŃ‚ŃŃ Đș ŃŃ‚ĐŸĐŒŃƒ Đž ĐČŃĐ”ĐŒ ĐżĐŸŃĐ»Đ”ĐŽŃƒŃŽŃ‰ĐžĐŒ ĐżĐŸĐČŃ‚ĐŸŃ€Đ”ĐœĐžŃĐŒ';
+$labels['youhaveaccepted'] = 'Вы ĐżŃ€ĐžĐœŃĐ»Đž ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ”';
+$labels['youhavetentative'] = 'Вы прДЎĐČĐ°Ń€ĐžŃ‚Đ”Đ»ŃŒĐœĐŸ ĐżŃ€ĐžĐœŃĐ»Đž ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ”';
+$labels['youhavedeclined'] = 'Вы ĐŸŃ‚ĐșĐ»ĐŸĐœĐžĐ»Đž ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ”';
+$labels['youhavedelegated'] = 'Вы ĐŽĐ”Đ»Đ”ĐłĐžŃ€ĐŸĐČалО ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ”';
+$labels['youhavein-process'] = 'Вы Ń€Đ°Đ±ĐŸŃ‚Đ°Đ”Ń‚Đ” ĐœĐ°ĐŽ ŃŃ‚ĐžĐŒ Đ·Đ°ĐŽĐ°ĐœĐžĐ”ĐŒ';
+$labels['youhavecompleted'] = 'Вы Đ·Đ°ĐČĐ”Ń€ŃˆĐžĐ»Đž ŃŃ‚ĐŸ Đ·Đ°ĐŽĐ°ĐœĐžĐ”';
+$labels['youhaveneeds-action'] = 'Ваш ĐŸŃ‚ĐČДт ĐœĐ° ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ” ĐČсё Дщё ĐŸĐ¶ĐžĐŽĐ°Đ”Ń‚ŃŃ';
+$labels['youhavepreviouslyaccepted'] = 'Вы ужД ĐżŃ€ĐžĐœŃĐ»Đž ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ”';
+$labels['youhavepreviouslytentative'] = 'Вы ужД прДЎĐČĐ°Ń€ĐžŃ‚Đ”Đ»ŃŒĐœĐŸ ĐżŃ€ĐžĐœŃĐ»Đž ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ”';
+$labels['youhavepreviouslydeclined'] = 'Вы ужД ĐŸŃ‚ĐșĐ°Đ·Đ°Đ»ĐžŃŃŒ ĐŸŃ‚ ŃŃ‚ĐŸĐłĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžŃ';
+$labels['youhavepreviouslydelegated'] = 'Вы ужД ĐŽĐ”Đ»Đ”ĐłĐžŃ€ĐŸĐČалО ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ”';
+$labels['youhavepreviouslyin-process'] = 'Вы ужД ĐżŃ€Đ”ĐŽĐŸŃŃ‚Đ°ĐČОлО ĐŸŃ‚Ń‡Đ”Ń‚ ĐŸ ĐżŃ€ĐŸĐŽĐ”Đ»Đ°ĐœĐœĐŸĐč Ń€Đ°Đ±ĐŸŃ‚Đ” ĐœĐ° ŃŃ‚ĐŸ Đ·Đ°ĐŽĐ°ĐœĐžĐ”';
+$labels['youhavepreviouslycompleted'] = 'Вы ужД Đ·Đ°ĐČĐ”Ń€ŃˆĐžĐ»Đž ŃŃ‚ĐŸ Đ·Đ°ĐŽĐ°ĐœĐžĐ”';
+$labels['youhavepreviouslyneeds-action'] = 'Ваш ĐŸŃ‚ĐČДт ĐœĐ° ŃŃ‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ” ĐČсё Дщё ĐŸĐ¶ĐžĐŽĐ°Đ”Ń‚ŃŃ';
+$labels['attendeeaccepted'] = 'ĐŁŃ‡Đ°ŃŃ‚ĐœĐžĐș ĐżŃ€ĐžĐœŃĐ»';
+$labels['attendeetentative'] = 'ĐŁŃ‡Đ°ŃŃ‚ĐœĐžĐș прДЎĐČĐ°Ń€ĐžŃ‚Đ”Đ»ŃŒĐœĐŸ ĐżŃ€ĐžĐœŃĐ» ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ”';
+$labels['attendeedeclined'] = 'ĐŁŃ‡Đ°ŃŃ‚ĐœĐžĐș ĐŸŃ‚ĐșĐ°Đ·Đ°Đ»ŃŃ ĐŸŃ‚ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžŃ';
+$labels['attendeedelegated'] = 'ĐŁŃ‡Đ°ŃŃ‚ĐœĐžĐș ĐŽĐ”Đ»Đ”ĐłĐžŃ€ĐŸĐČĐ°Đ» ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ” $delegatedto';
+$labels['attendeein-process'] = 'ĐŁŃ‡Đ°ŃŃ‚ĐœĐžĐș ĐČ ĐżŃ€ĐŸŃ†Đ”ŃŃĐ”';
+$labels['attendeecompleted'] = 'ĐŁŃ‡Đ°ŃŃ‚ĐœĐžĐș Đ·Đ°ĐČĐ”Ń€ŃˆĐžĐ»';
+$labels['notanattendee'] = 'Вы ĐœĐ” уĐșĐ°Đ·Đ°ĐœŃ‹ ĐșĐ°Đș ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐș ŃŃ‚ĐŸĐłĐŸ ĐŸĐ±ŃŠĐ”Đșта';
+$labels['outdatedinvitation'] = 'Đ­Ń‚ĐŸ ĐżŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ” Đ±Ń‹Đ»ĐŸ Đ·Đ°ĐŒĐ”ĐœĐ”ĐœĐŸ ĐœĐŸĐČĐŸĐč ĐČДрсОДĐč';
+$labels['importtocalendar'] = 'ĐĄĐŸŃ…Ń€Đ°ĐœĐžŃ‚ŃŒ ĐČ ĐŒĐŸĐč ĐșĐ°Đ»Đ”ĐœĐŽĐ°Ń€ŃŒ';
+$labels['removefromcalendar'] = 'ĐŁĐŽĐ°Đ»ĐžŃ‚ŃŒ Оз ĐŒĐŸĐ”ĐłĐŸ ĐșĐ°Đ»Đ”ĐœĐŽĐ°Ń€Ń';
+$labels['updatemycopy'] = 'ĐžĐ±ĐœĐŸĐČоть ĐŒĐŸŃŽ ĐșĐŸĐżĐžŃŽ';
+$labels['openpreview'] = 'ОтĐșрыть ĐżŃ€Đ”ĐŽĐżŃ€ĐŸŃĐŒĐŸŃ‚Ń€';
+$labels['deleteobjectconfirm'] = 'Вы ĐŽĐ”ĐčстĐČĐžŃ‚Đ”Đ»ŃŒĐœĐŸ Ń…ĐŸŃ‚ĐžŃ‚Đ” ŃƒĐŽĐ°Đ»ĐžŃ‚ŃŒ ŃŃ‚ĐŸŃ‚ ĐŸĐ±ŃŠĐ”Đșт?';
+$labels['declinedeleteconfirm'] = 'Вы Ń…ĐŸŃ‚ĐžŃ‚Đ” таĐș жД ŃƒĐŽĐ°Đ»ĐžŃ‚ŃŒ ŃŃ‚ĐŸŃ‚ ĐŸĐ±ŃŠĐ”Đșт ŃĐŸ сĐČĐŸĐ”ĐłĐŸ Đ°ĐșĐșĐ°ŃƒĐœŃ‚Đ°?';
+$labels['delegateinvitation'] = 'ĐŸŃ€ĐžĐłĐ»Đ°ŃˆĐ”ĐœĐžĐ” прДЎстаĐČОтДлДĐč';
+$labels['delegateto'] = 'ĐŸĐŸŃ€ŃƒŃ‡ĐžŃ‚ŃŒ';
+$labels['delegatersvpme'] = 'ĐĄĐŸĐŸĐ±Ń‰Đ°Ń‚ŃŒ ĐŒĐœĐ” ĐŸĐ± ĐžĐ·ĐŒĐ”ĐœĐ”ĐœĐžŃŃ… ĐČ ŃŃ‚ĐŸĐŒ ŃĐŸĐ±Ń‹Ń‚ĐžĐž';
+$labels['delegateinvalidaddress'] = 'ĐŸĐŸĐ¶Đ°Đ»ŃƒĐčста, ĐČĐČДЎОтД праĐČĐžĐ»ŃŒĐœŃ‹Đč email аЎрДс прДЎстаĐČĐžŃ‚Đ”Đ»Ń';
+$labels['savingdata'] = 'ĐĄĐŸŃ…Ń€Đ°ĐœĐ”ĐœĐžĐ” ĐŽĐ°ĐœĐœŃ‹Ń…...';
+$labels['expandattendeegroup'] = 'Đ—Đ°ĐŒĐ”ĐœĐžŃ‚ŃŒ ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐ°ĐŒĐž группы';
+$labels['expandattendeegroupnodata'] = 'ĐĐ” уЮаётся Đ·Đ°ĐŒĐ”ĐœĐžŃ‚ŃŒ ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐ°ĐŒĐž группы. Но ĐŸĐŽĐœĐŸĐłĐŸ ĐœĐ” ĐœĐ°ĐčĐŽĐ”ĐœĐŸ.';
+$labels['expandattendeegrouperror'] = 'ĐĐ” уЮаётся Đ·Đ°ĐŒĐ”ĐœĐžŃ‚ŃŒ ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐ°ĐŒĐž группы. Đ’ĐŸĐ·ĐŒĐŸĐ¶ĐœĐŸ, ĐČ ĐœĐ”Đč слОшĐșĐŸĐŒ ĐŒĐœĐŸĐłĐŸ ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐŸĐČ.';
+$labels['expandattendeegroupsizelimit'] = 'Группа ŃĐŸĐŽĐ”Ń€Đ¶ĐžŃ‚ слОшĐșĐŸĐŒ ĐŒĐœĐŸĐłĐŸ ŃƒŃ‡Đ°ŃŃ‚ĐœĐžĐșĐŸĐČ ĐŽĐ»Ń Đ·Đ°ĐŒĐ”ĐœŃ‹.';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/sk_SK.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,10 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['frequency'] = 'OpakovaƄ';
+$labels['statustentative'] = 'NezåvÀzne';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/sl_SI.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'do';
+$labels['at'] = 'ob';
+$labels['alarmemailoption'] = 'Email';
+$labels['frequency'] = 'Ponovi';
+$labels['recurrencend'] = 'do';
+$labels['statusorganizer'] = 'Organizator';
+$labels['statustentative'] = 'Pogojno';
+$labels['statusneeds-action'] = 'Potrebuje pozornost';
+$labels['statusunknown'] = 'Neznano';
+$labels['statuscompleted'] = 'Opravljeno';
+$labels['statusin-process'] = 'V procesu';
+$labels['itipdelegated'] = 'Sodelujoč';
+$labels['savingdata'] = 'Shranjujem...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/sv.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,8 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/sv_SE.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'tills';
+$labels['frequency'] = 'Upprepa';
+$labels['recurrencend'] = 'tills';
+$labels['statusorganizer'] = 'Organisatör';
+$labels['statustentative'] = 'PreliminÀrt';
+$labels['statusunknown'] = 'OkÀnd';
+$labels['savingdata'] = 'Sparar data ...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/th_TH.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,85 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'àžˆàž™àž–àž¶àž‡';
+$labels['alarmemail'] = 'àžȘàčˆàž‡ àž­àž”àč€àžĄàž„àčŒ';
+$labels['alarmdisplay'] = 'àčàžȘàž”àž‡àž‚àč‰àž­àž„àž§àžČàžĄ';
+$labels['alarmaudio'] = 'àč€àž„àčˆàž™àč€àžȘàž”àžąàž‡';
+$labels['alarmdisplayoption'] = 'àž‚àč‰àž­àž„àž§àžČàžĄ';
+$labels['alarmemailoption'] = 'àž­àž”àč€àžĄàž„àčŒ';
+$labels['alarmaudiooption'] = 'àč€àžȘàž”àžąàž‡';
+$labels['alarmat'] = 'àč€àžĄàž·àčˆàž­ $datetime';
+$labels['trigger@'] = 'àž“ àž§àž±àž™àž—àž”àčˆ';
+$labels['trigger-M'] = 'àž™àžČàž—àž” àžàčˆàž­àž™';
+$labels['trigger-H'] = 'àžŠàž±àčˆàž§àč‚àžĄàž‡ àžàčˆàž­àž™';
+$labels['trigger-D'] = 'àž§àž±àž™ àžàčˆàž­àž™';
+$labels['trigger+M'] = 'àž™àžČàž—àž” àž«àž„àž±àž‡';
+$labels['trigger+H'] = 'àžŠàž±àčˆàž§àč‚àžĄàž‡ àž«àž„àž±àž‡';
+$labels['trigger+D'] = 'àž§àž±àž™ àž«àž„àž±àž‡';
+$labels['addalarm'] = 'àč€àžžàžŽàčˆàžĄàžàžČàžŁàč€àž•àž·àž­àž™';
+$labels['removealarm'] = 'àž„àžšàžàžČàžŁàč€àž•àž·àž­àž™';
+$labels['alarmtitle'] = 'àč€àž«àž•àžžàžàžČàžŁàž“àčŒàž—àž”àčˆàžàžłàž„àž±àž‡àžˆàž°àž–àž¶àž‡';
+$labels['dismissall'] = 'àžąàžàč€àž„àžŽàžàž—àž±àč‰àž‡àž«àžĄàž”';
+$labels['dismiss'] = 'àžąàžàč€àž„àžŽàž';
+$labels['snooze'] = 'àž›àžŽàž”àžŠàž±àčˆàž§àž„àžŁàžČàž§';
+$labels['repeatinmin'] = 'àž—àžłàž‹àč‰àžłàčƒàž™ $min àž™àžČàž—àž”';
+$labels['repeatinhr'] = 'àž—àžłàž‹àč‰àžłàčƒàž™ 1 àžŠàž±àčˆàž§àč‚àžĄàž‡';
+$labels['repeatinhrs'] = 'àž—àžłàž‹àč‰àžłàčƒàž™ $hrs àžŠàž±àčˆàž§àč‚àžĄàž‡';
+$labels['repeattomorrow'] = 'àž—àžłàž‹àč‰àžłàžžàžŁàžžàčˆàž‡àž™àž”àč‰';
+$labels['repeatinweek'] = 'àž—àžłàž‹àč‰àžłàčƒàž™ 1 àžȘàž±àž›àž”àžČàž«àčŒ';
+$labels['showmore'] = 'àčàžȘàž”àž‡ àžĄàžČàžàžàž§àčˆàžČàž™àž”àč‰...';
+$labels['recurring'] = 'àž—àžłàž‹àč‰àžł';
+$labels['frequency'] = 'àž—àžłàž‹àč‰àžł';
+$labels['never'] = 'àč„àžĄàčˆàžĄàž”àž—àžČàž‡';
+$labels['daily'] = 'àž—àžžàžàž§àž±àž™';
+$labels['weekly'] = 'àž—àžžàžàžȘàž±àž›àž”àžČàž«àčŒ';
+$labels['monthly'] = 'àž—àžžàžàč€àž”àž·àž­àž™';
+$labels['yearly'] = 'àž—àžžàžàž›àž”';
+$labels['rdate'] = 'àž“ àž§àž±àž™àž—àž”àčˆ';
+$labels['every'] = 'àž—àžžàž àč†';
+$labels['days'] = 'àž§àž±àž™';
+$labels['weeks'] = 'àžȘàž±àž›àž”àžČàž«àčŒ';
+$labels['months'] = 'àč€àž”àž·àž­àž™';
+$labels['years'] = 'àž›àž”';
+$labels['each'] = 'àčàž•àčˆàž„àž°';
+$labels['forever'] = 'àž•àž„àž­àž”àč„àž›';
+$labels['recurrencend'] = 'àžˆàž™àž–àž¶àž‡';
+$labels['untilenddate'] = 'àžˆàž™àž–àž¶àž‡àž§àž±àž™àž—àž”àčˆ';
+$labels['forntimes'] = 'àžˆàžłàž™àž§àž™ $nr àž„àžŁàž±àč‰àž‡';
+$labels['first'] = 'àž„àžŁàž±àč‰àž‡àž—àž”àčˆ 1';
+$labels['second'] = 'àž„àžŁàž±àč‰àž‡àž—àž”àčˆ 2';
+$labels['third'] = 'àž„àžŁàž±àč‰àž‡àž—àž”àčˆ 3';
+$labels['fourth'] = 'àž„àžŁàž±àč‰àž‡àž—àž”àčˆ 4';
+$labels['last'] = 'àž„àžŁàž±àč‰àž‡àžȘàžžàž”àž—àč‰àžČàžą';
+$labels['addrdate'] = 'àč€àžžàžŽàčˆàžĄàž§àž±àž™àž—àž”àčˆàž—àžłàž‹àč‰àžł';
+$labels['except'] = 'àžąàžàč€àž§àč‰àž™';
+$labels['statusorganizer'] = 'àžœàžčàč‰àžˆàž±àž”àž‡àžČàž™';
+$labels['statustentative'] = 'àčàž™àž§àč‚àž™àč‰àžĄ';
+$labels['statusneeds-action'] = 'àž•àč‰àž­àž‡àž—àžłàž­àž°àč„àžŁàžȘàž±àžàž­àžąàčˆàžČàž‡';
+$labels['statusunknown'] = 'àč„àžĄàčˆàž—àžŁàžČàžš';
+$labels['statuscompleted'] = 'àč€àžȘàžŁàč‡àžˆàč€àžŁàž”àžąàžšàžŁàč‰àž­àžą';
+$labels['statusin-process'] = 'àž­àžąàžžàčˆàžŁàž°àž«àž§àčˆàžČàž‡àž”àžłàč€àž™àžŽàž™àžàžČàžŁ';
+$labels['itipinvitation'] = 'àž„àžłàč€àžŠàžŽàžàž–àž¶àž‡';
+$labels['itipcancellation'] = 'àžąàžàč€àž„àžŽàž:';
+$labels['itipreply'] = 'àž•àž­àžšàžàž„àž±àžšàč„àž›àžąàž±àž‡';
+$labels['itipaccepted'] = 'àžąàž­àžĄàžŁàž±àžš';
+$labels['itiptentative'] = 'àž­àžČàžˆàžˆàž°';
+$labels['itipdeclined'] = 'àž›àžŽàžŽàč€àžȘàž˜';
+$labels['itipneeds-action'] = 'àč€àž„àž·àčˆàž­àž™';
+$labels['itipcomment'] = 'àž‚àč‰àž­àž„àž§àžČàžĄàž•àž­àžšàžàž„àž±àžšàž‚àž­àž‡àž„àžžàž“';
+$labels['itipeditresponse'] = 'àž›àč‰àž­àž™àž‚àč‰àž­àž„àž§àžČàžĄàž•àž­àžšàžàž„àž±àžš';
+$labels['itipsendercomment'] = 'àž„àž§àžČàžĄàž„àžŽàž”àč€àž«àč‡àž™àž‚àž­àž‡àžœàžčàč‰àžȘàčˆàž‡:';
+$labels['itipsuppressreply'] = 'àč„àžĄàčˆàžȘàčˆàž‡àž‚àč‰àž­àž„àž§àžČàžĄàž•àž­àžšàžàž„àž±àžš';
+$labels['itipobjectnotfound'] = 'àč„àžĄàčˆàžžàžšàž§àž±àž•àž–àžžàž—àž”àčˆàž­àč‰àžČàž‡àž–àž¶àž‡àč‚àž”àžąàž‚àč‰àž­àž„àž§àžČàžĄàž™àž”àč‰àčƒàž™àžšàž±àžàžŠàž”àž‚àž­àž‡àž„àžžàž“';
+$labels['itipsubjectaccepted'] = '"$title" àž–àžčàžàž•àž­àžšàžŁàž±àžšàč‚àž”àžą $name';
+$labels['itipsubjecttentative'] = '"$title" àžĄàž”àčàž™àž§àč‚àž™àč‰àžĄàž—àž”àčˆàžˆàž°àč„àž”àč‰àžŁàž±àžšàžàžČàžŁàž•àž­àžšàžŁàž±àžšàč‚àž”àžą $name';
+$labels['itipsubjectdeclined'] = '"$title" àž–àžčàžàž›àžŽàžŽàč€àžȘàž˜àč‚àž”àžą $name';
+$labels['itipsubjectin-process'] = '"$title" àž­àžąàžčàčˆàžŁàž°àž«àž§àčˆàžČàž‡àžàžČàžŁàžˆàž±àž”àč€àž•àžŁàž”àžąàžĄàč‚àž”àžą $name';
+$labels['itipsubjectcompleted'] = '"$title" àž–àžčàžàž—àžłàčƒàž«àč‰àžȘàžĄàžšàžčàžŁàž“àčŒàč‚àž”àžą $name';
+$labels['itipsubjectcancel'] = 'àžàžČàžŁàč€àž‚àč‰àžČàžŁàčˆàž§àžĄàž‚àž­àž‡àž„àžžàž“àčƒàž™ "$title" àž–àžčàžàžąàžàč€àž„àžŽàž';
+$labels['savingdata'] = 'àžšàž±àž™àž—àž¶àžàž‚àč‰àž­àžĄàžčàž„';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/tr_TR.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,8 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/uk_UA.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,17 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'ĐŽĐŸ';
+$labels['alarmemailoption'] = 'ĐŸĐŸŃˆŃ‚Đ°';
+$labels['frequency'] = 'ĐŸĐŸĐČŃ‚ĐŸŃ€ĐžŃ‚Đž';
+$labels['recurrencend'] = 'ĐŽĐŸ';
+$labels['statusorganizer'] = 'ĐžŃ€ĐłĐ°ĐœŃ–Đ·Đ°Ń‚ĐŸŃ€';
+$labels['statustentative'] = 'ĐĐ”ĐČĐžĐ·ĐœĐ°Ń‡Đ”ĐœĐžĐč';
+$labels['statusunknown'] = 'ĐĐ”ĐČŃ–ĐŽĐŸĐŒĐŸ';
+$labels['itipdelegated'] = 'ĐŸŃ€Đ”ĐŽŃŃ‚Đ°ĐČĐœĐžĐș';
+$labels['savingdata'] = 'Đ—Đ±Đ”Ń€Đ”Đ¶Đ”ĐœĐœŃ ĐŽĐ°ĐœĐžŃ…...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/vi.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'until';
+$labels['frequency'] = 'Repeat';
+$labels['recurrencend'] = 'until';
+$labels['savingdata'] = 'Saving data...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/vi_VN.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,11 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['statusorganizer'] = 'Organizer';
+$labels['statustentative'] = 'Tentative';
+$labels['statusunknown'] = 'Unknown';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/zh_CN.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = '目戰';
+$labels['at'] = '朹';
+$labels['alarmemail'] = 'ć‘é€é‚źä»¶';
+$labels['alarmdisplay'] = '星ç€șæ¶ˆæŻ';
+$labels['alarmaudio'] = 'æ’­æ”ŸćŁ°éŸł';
+$labels['alarmdisplayoption'] = 'æ¶ˆæŻ';
+$labels['alarmemailoption'] = '邟件';
+$labels['alarmaudiooption'] = '棰音';
+$labels['alarmat'] = '朹 $datetime';
+$labels['trigger-M'] = 'ćˆ†é’Ÿä»„ć‰';
+$labels['trigger-H'] = 'ć°æ—¶ä»„ć‰';
+$labels['trigger-D'] = 'ć€©ä»„ć‰';
+$labels['trigger+M'] = 'ćˆ†é’Ÿä»„ćŽ';
+$labels['trigger+H'] = 'ć°æ—¶ä»„ćŽ';
+$labels['trigger+D'] = '怩仄搎';
+$labels['triggerend-M'] = 'èż˜æœ‰xxxćˆ†é’Ÿç»“æŸ # xxx was supposed to be the number before "minutes before end"';
+$labels['frequency'] = 'ćŸȘ环';
+$labels['recurrencend'] = '目戰';
+$labels['statusorganizer'] = '组织者';
+$labels['statustentative'] = '䞎时';
+$labels['statusunknown'] = 'æœȘ矄';
+$labels['savingdata'] = 'äżć­˜æ•°æź...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/localization/zh_TW.inc	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Localizations for the Kolab calendaring utilities plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/libcalendaring/
+ */
+$labels['until'] = 'until';
+$labels['frequency'] = 'Repeat';
+$labels['recurrencend'] = 'until';
+$labels['statusorganizer'] = 'Organizer';
+$labels['statustentative'] = 'Tentative';
+$labels['statusunknown'] = 'Unknown';
+$labels['savingdata'] = 'Saving data...';
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/skins/larry/libcal.css	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,166 @@
+/**
+ * Roundcube libcalendaring plugin styles for skin "Larry"
+ *
+ * Copyright (c) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * The contents are subject to the Creative Commons Attribution-ShareAlike
+ * License. It is allowed to copy, distribute, transmit and to adapt the work
+ * by keeping credits to the original autors in the README file.
+ * See http://creativecommons.org/licenses/by-sa/3.0/ for details.
+ */
+
+.alarm-item {
+	margin: 0.4em 0 1em 0;
+}
+
+.alarm-item .event-title {
+	font-size: 14px;
+	margin: 0.1em 0 0.3em 0;
+}
+
+.alarm-item div.event-section {
+	margin-top: 0.1em;
+	margin-bottom: 0.3em;
+}
+
+.alarm-item .alarm-actions {
+	margin-top: 0.4em;
+}
+
+.alarm-item div.alarm-actions a {
+	margin-right: 0.8em;
+	text-decoration: none;
+}
+
+a.alarm-action-snooze:after {
+	content: ' ▌';
+	font-size: 10px;
+	color: #666;
+}
+
+#alarm-snooze-dropdown {
+	z-index: 5000;
+}
+
+span.edit-alarm-set {
+	white-space: nowrap;
+}
+
+.ui-dialog.alarms .ui-dialog-title {
+	background-image: url(../../../../skins/larry/images/messages.png);
+	background-repeat: no-repeat;
+	background-position: 0 -91px;
+	padding-left: 24px;
+}
+
+.itip-reply-comment {
+	padding-left: 2px;
+}
+
+a.reply-comment-toggle {
+	display: inline-block;
+	color: #666;
+}
+
+label.noreply-toggle + a.reply-comment-toggle {
+	margin-left: 1em;
+}
+
+.itip-reply-comment textarea {
+	display: block;
+	width: 90%;
+	margin-top: 0.5em;
+}
+
+.itip-dialog-confirm-text {
+	margin-bottom: 1em;
+}
+
+.popup textarea.itip-comment {
+	width: 98%;
+}
+
+.edit-alarm-item {
+	position: relative;
+	padding-right: 30px;
+	margin-bottom: 0.2em;
+}
+
+.edit-alarm-buttons {
+	position: absolute;
+	top: 1px;
+	right: 0;
+}
+
+.edit-alarm-buttons a.iconbutton {
+	display: none;
+}
+
+.edit-alarm-item .edit-alarm-buttons a.delete-alarm,
+.edit-alarm-item.first .edit-alarm-buttons a.add-alarm {
+	display: inline-block;
+}
+
+.edit-alarm-item.first .edit-alarm-buttons a.delete-alarm {
+	display: none;
+}
+
+.recurrence-form {
+	display: none;
+}
+
+.recurrence-form label.weekday,
+.recurrence-form label.monthday {
+	min-width: 3em;
+}
+
+.recurrence-form label.month {
+	min-width: 5em;
+}
+
+#edit-recurrence-yearly-bymonthblock {
+	margin-left: 7.5em;
+}
+
+#edit-recurrence-rdates {
+	display: block;
+	list-style: none;
+	margin: 0 0 0.8em 0;
+	padding: 0;
+	max-height: 300px;
+	overflow: auto;
+}
+
+#edit-recurrence-rdates li {
+	display: block;
+	position: relative;
+	width: 12em;
+	padding: 4px 0 4px 0;
+}
+
+#edit-recurrence-rdates li a.delete {
+	position: absolute;
+	top: 2px;
+	right: 0;
+	width: 20px;
+	height: 18px;
+	background-position: -7px -337px;
+}
+
+#recurrence-form-until div.line {
+	margin-left: 7.5em;
+	margin-bottom: 0.3em;
+}
+
+#recurrence-form-until div.line.first {
+	margin-top: -1.4em;
+}
+
+.itip-dialog-form input.text {
+	width: 98%;
+}
+
+.itip-dialog-form label > input.checkbox {
+	margin-left: 0;
+	margin-right: 10px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/libcalendaring.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * libcalendaring plugin's utility functions tests
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2015, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class libcalendaring_test extends PHPUnit_Framework_TestCase
+{
+    function setUp()
+    {
+        require_once __DIR__ . '/../libcalendaring.php';
+    }
+
+    /**
+     * libcalendaring::parse_alarm_value()
+     */
+    function test_parse_alarm_value()
+    {
+        $alarm = libcalendaring::parse_alarm_value('-15M');
+        $this->assertEquals('15', $alarm[0]);
+        $this->assertEquals('-M', $alarm[1]);
+        $this->assertEquals('-PT15M', $alarm[3]);
+
+        $alarm = libcalendaring::parse_alarm_value('-PT5H');
+        $this->assertEquals('5',  $alarm[0]);
+        $this->assertEquals('-H', $alarm[1]);
+
+        $alarm = libcalendaring::parse_alarm_value('P0DT1H0M0S');
+        $this->assertEquals('1',  $alarm[0]);
+        $this->assertEquals('+H', $alarm[1]);
+
+        // FIXME: this should return something like (1140 + 120 + 30)M
+        $alarm = libcalendaring::parse_alarm_value('-P1DT2H30M');
+        // $this->assertEquals('1590', $alarm[0]);
+        // $this->assertEquals('-M',   $alarm[1]);
+
+        $alarm = libcalendaring::parse_alarm_value('@1420722000');
+        $this->assertInstanceOf('DateTime', $alarm[0]);
+    }
+
+    /**
+     * libcalendaring::get_next_alarm()
+     */
+    function test_get_next_alarm()
+    {
+        // alarm 10 minutes before event
+        $date = date('Ymd', strtotime('today + 2 days'));
+        $event = array(
+            'start' => new DateTime($date . 'T160000Z'),
+            'end'   => new DateTime($date . 'T200000Z'),
+            'valarms' => array(
+                array(
+                    'trigger' => '-PT10M',
+                    'action'  => 'DISPLAY',
+                ),
+            ),
+        );
+        $alarm = libcalendaring::get_next_alarm($event);
+        $this->assertEquals($event['valarms'][0]['action'], $alarm['action']);
+        $this->assertEquals(strtotime($date . 'T155000Z'), $alarm['time']);
+
+        // alarm 1 hour after event start
+        $event['valarms'] = array(
+            array(
+                'trigger' => '+PT1H',
+            ),
+        );
+        $alarm = libcalendaring::get_next_alarm($event);
+        $this->assertEquals('DISPLAY', $alarm['action']);
+        $this->assertEquals(strtotime($date . 'T170000Z'), $alarm['time']);
+
+        // alarm 1 hour before event end
+        $event['valarms'] = array(
+            array(
+                'trigger' => '-PT1H',
+                'related' => 'END',
+            ),
+        );
+        $alarm = libcalendaring::get_next_alarm($event);
+        $this->assertEquals('DISPLAY', $alarm['action']);
+        $this->assertEquals(strtotime($date . 'T190000Z'), $alarm['time']);
+
+        // alarm 1 hour after event end
+        $event['valarms'] = array(
+            array(
+                'trigger' => 'PT1H',
+                'related' => 'END',
+            ),
+        );
+        $alarm = libcalendaring::get_next_alarm($event);
+        $this->assertEquals('DISPLAY', $alarm['action']);
+        $this->assertEquals(strtotime($date . 'T210000Z'), $alarm['time']);
+
+        // ignore past alarms
+        $event['start'] = new DateTime('today 22:00:00');
+        $event['end']   = new DateTime('today 23:00:00');
+        $event['valarms'] = array(
+            array(
+                'trigger' => '-P2D',
+                'action'  => 'EMAIL',
+            ),
+            array(
+                'trigger' => '-PT30M',
+                'action'  => 'DISPLAY',
+            ),
+        );
+        $alarm = libcalendaring::get_next_alarm($event);
+        $this->assertEquals('DISPLAY', $alarm['action']);
+        $this->assertEquals(strtotime('today 21:30:00'), $alarm['time']);
+
+        // absolute alarm date/time
+        $event['valarms'] = array(
+            array('trigger' => new DateTime('today 20:00:00'))
+        );
+        $alarm = libcalendaring::get_next_alarm($event);
+        $this->assertEquals($event['valarms'][0]['trigger']->format('U'), $alarm['time']);
+
+        // no alarms for cancelled events
+        $event['status'] = 'CANCELLED';
+        $alarm = libcalendaring::get_next_alarm($event);
+        $this->assertEquals(null, $alarm);
+    }
+
+    /**
+     * libcalendaring::part_is_vcalendar()
+     */
+    function test_part_is_vcalendar()
+    {
+        $part = new StdClass;
+        $part->mimetype = 'text/plain';
+        $part->filename = 'event.ics';
+
+        $this->assertFalse(libcalendaring::part_is_vcalendar($part));
+
+        $part->mimetype = 'text/calendar';
+        $this->assertTrue(libcalendaring::part_is_vcalendar($part));
+
+        $part->mimetype = 'text/x-vcalendar';
+        $this->assertTrue(libcalendaring::part_is_vcalendar($part));
+
+        $part->mimetype = 'application/ics';
+        $this->assertTrue(libcalendaring::part_is_vcalendar($part));
+
+        $part->mimetype = 'application/x-any';
+        $this->assertTrue(libcalendaring::part_is_vcalendar($part));
+    }
+
+    /**
+     * libcalendaring::to_rrule()
+     */
+    function test_to_rrule()
+    {
+        $rrule = array(
+            'FREQ' => 'MONTHLY',
+            'BYDAY' => '2WE',
+            'INTERVAL' => 2,
+            'UNTIL' => new DateTime('2025-05-01 18:00:00 CEST'),
+        );
+
+        $s = libcalendaring::to_rrule($rrule);
+
+        $this->assertRegExp('/FREQ='.$rrule['FREQ'].'/',          $s, "Recurrence Frequence");
+        $this->assertRegExp('/INTERVAL='.$rrule['INTERVAL'].'/',  $s, "Recurrence Interval");
+        $this->assertRegExp('/BYDAY='.$rrule['BYDAY'].'/',        $s, "Recurrence BYDAY");
+        $this->assertRegExp('/UNTIL=20250501T160000Z/',           $s, "Recurrence End date (in UTC)");
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/libvcalendar.php	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,599 @@
+<?php
+
+/**
+ * libcalendaring plugin's iCalendar functions tests
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class libvcalendar_test extends PHPUnit_Framework_TestCase
+{
+    function setUp()
+    {
+        require_once __DIR__ . '/../libvcalendar.php';
+        require_once __DIR__ . '/../libcalendaring.php';
+    }
+
+    /**
+     * Simple iCal parsing test
+     */
+    function test_import()
+    {
+        $ical = new libvcalendar();
+        $ics = file_get_contents(__DIR__ . '/resources/snd.ics');
+        $events = $ical->import($ics, 'UTF-8');
+
+        $this->assertEquals(1, count($events));
+        $event = $events[0];
+
+        $this->assertInstanceOf('DateTime', $event['created'], "'created' property is DateTime object");
+        $this->assertInstanceOf('DateTime', $event['changed'], "'changed' property is DateTime object");
+        $this->assertEquals('UTC', $event['created']->getTimezone()->getName(), "'created' date is in UTC");
+
+        $this->assertInstanceOf('DateTime', $event['start'], "'start' property is DateTime object");
+        $this->assertInstanceOf('DateTime', $event['end'], "'end' property is DateTime object");
+        $this->assertEquals('08-01', $event['start']->format('m-d'), "Start date is August 1st");
+        $this->assertTrue($event['allday'], "All-day event flag");
+
+        $this->assertEquals('B968B885-08FB-40E5-B89E-6DA05F26AA79', $event['uid'], "Event UID");
+        $this->assertEquals('Swiss National Day', $event['title'], "Event title");
+        $this->assertEquals('http://en.wikipedia.org/wiki/Swiss_National_Day', $event['url'], "URL property");
+        $this->assertEquals(2, $event['sequence'], "Sequence number");
+
+        $desclines = explode("\n", $event['description']);
+        $this->assertEquals(4, count($desclines), "Multiline description");
+        $this->assertEquals("French: FĂȘte nationale Suisse", rtrim($desclines[1]), "UTF-8 encoding");
+    }
+
+    /**
+     * Test parsing from files
+     */
+    function test_import_from_file()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
+        $this->assertEquals(2, count($events));
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/invalid.txt', 'UTF-8');
+        $this->assertEmpty($events);
+    }
+
+    /**
+     * Test parsing from files with multiple VCALENDAR blocks (#2884)
+     */
+    function test_import_from_file_multiple()
+    {
+        $ical = new libvcalendar();
+        $ical->fopen(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
+        $events = array();
+        foreach ($ical as $event) {
+            $events[] = $event;
+        }
+
+        $this->assertEquals(2, count($events));
+        $this->assertEquals("AAAA6A8C3CCE4EE2C1257B5C00FFFFFF-Lotus_Notes_Generated", $events[0]['uid']);
+        $this->assertEquals("AAAA1C572093EC3FC125799C004AFFFF-Lotus_Notes_Generated", $events[1]['uid']);
+    }
+
+    function test_invalid_dates()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/invalid-dates.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals(1, count($events), "Import event data");
+        $this->assertInstanceOf('DateTime', $event['created'], "Created date field");
+        $this->assertFalse(array_key_exists('changed', $event), "No changed date field");
+    }
+
+    function test_invalid_vevent()
+    {
+        $this->setExpectedException('\Sabre\VObject\ParseException');
+
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/invalid-event.ics', 'UTF-8', true);
+    }
+
+    /**
+     * Test some extended ical properties such as attendees, recurrence rules, alarms and attachments
+     */
+    function test_extended()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
+        $event = $events[0];
+        $this->assertEquals('REQUEST', $ical->method, "iTip method");
+
+        // attendees
+        $this->assertEquals(3, count($event['attendees']), "Attendees list (including organizer)");
+        $organizer = $event['attendees'][0];
+        $this->assertEquals('ORGANIZER', $organizer['role'], 'Organizer ROLE');
+        $this->assertEquals('Rolf Test', $organizer['name'], 'Organizer name');
+
+        $attendee = $event['attendees'][1];
+        $this->assertEquals('REQ-PARTICIPANT', $attendee['role'], 'Attendee ROLE');
+        $this->assertEquals('NEEDS-ACTION', $attendee['status'], 'Attendee STATUS');
+        $this->assertEquals('rolf2@mykolab.com', $attendee['email'], 'Attendee mailto:');
+        $this->assertEquals('carl@mykolab.com', $attendee['delegated-from'], 'Attendee delegated-from');
+        $this->assertTrue($attendee['rsvp'], 'Attendee RSVP');
+
+        $delegator = $event['attendees'][2];
+        $this->assertEquals('NON-PARTICIPANT',   $delegator['role'], 'Delegator ROLE');
+        $this->assertEquals('DELEGATED',         $delegator['status'], 'Delegator STATUS');
+        $this->assertEquals('INDIVIDUAL',        $delegator['cutype'], 'Delegator CUTYPE');
+        $this->assertEquals('carl@mykolab.com',  $delegator['email'], 'Delegator mailto:');
+        $this->assertEquals('rolf2@mykolab.com', $delegator['delegated-to'], 'Delegator delegated-to');
+        $this->assertFalse($delegator['rsvp'],   'Delegator RSVP');
+
+        // attachments
+        $this->assertEquals(1, count($event['attachments']), "Embedded attachments");
+        $attachment = $event['attachments'][0];
+        $this->assertEquals('text/html',                 $attachment['mimetype'], "Attachment mimetype attribute");
+        $this->assertEquals('calendar.html',             $attachment['name'],     "Attachment filename (X-LABEL) attribute");
+        $this->assertContains('<title>Kalender</title>', $attachment['data'],     "Attachment content (decoded)");
+
+        // recurrence rules
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertTrue(is_array($event['recurrence']), 'Recurrences rule as hash array');
+        $rrule = $event['recurrence'];
+        $this->assertEquals('MONTHLY',      $rrule['FREQ'],     "Recurrence frequency");
+        $this->assertEquals('1',            $rrule['INTERVAL'], "Recurrence interval");
+        $this->assertEquals('3WE',          $rrule['BYDAY'],    "Recurrence frequency");
+        $this->assertInstanceOf('DateTime', $rrule['UNTIL'],    "Recurrence end date");
+
+        $this->assertEquals(2, count($rrule['EXDATE']),          "Recurrence EXDATEs");
+        $this->assertInstanceOf('DateTime', $rrule['EXDATE'][0], "Recurrence EXDATE as DateTime");
+
+        $this->assertTrue(is_array($rrule['EXCEPTIONS']));
+        $this->assertEquals(1, count($rrule['EXCEPTIONS']), "Recurrence Exceptions");
+
+        $exception = $rrule['EXCEPTIONS'][0];
+        $this->assertEquals($event['uid'],  $event['uid'], "Exception UID");
+        $this->assertEquals('Recurring Test (Exception)',  $exception['title'], "Exception title");
+        $this->assertInstanceOf('DateTime', $exception['start'], "Exception start");
+
+        // categories, class
+        $this->assertEquals('libcalendaring tests', join(',', (array)$event['categories']), "Event categories");
+        $this->assertEquals('confidential', $event['sensitivity'], "Class/sensitivity = confidential");
+
+        // parse a recurrence chain instance
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurrence-id.ics', 'UTF-8');
+        $this->assertEquals(1, count($events), "Fall back to Component::getComponents() when getBaseComponents() is empty");
+        $this->assertInstanceOf('DateTime', $events[0]['recurrence_date'], "Recurrence-ID as date");
+        $this->assertTrue($events[0]['thisandfuture'], "Range=THISANDFUTURE");
+
+        $this->assertEquals(count($events[0]['exceptions']), 1, "Second VEVENT as exception");
+        $this->assertEquals($events[0]['exceptions'][0]['uid'], $events[0]['uid'], "Exception UID match");
+        $this->assertEquals($events[0]['exceptions'][0]['sequence'], '2', "Exception sequence");
+    }
+
+    /**
+     * 
+     */
+    function test_alarms()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals('-12H:DISPLAY', $event['alarms'], "Serialized alarms string");
+        $alarm = libcalendaring::parse_alarm_value($event['alarms']);
+        $this->assertEquals('12', $alarm[0], "Alarm value");
+        $this->assertEquals('-H', $alarm[1], "Alarm unit");
+
+        $this->assertEquals('DISPLAY', $event['valarms'][0]['action'],  "Full alarm item (action)");
+        $this->assertEquals('-PT12H',  $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
+        $this->assertEquals('END',  $event['valarms'][0]['related'], "Full alarm item (related)");
+
+        // alarm trigger with 0 values
+        $events = $ical->import_from_file(__DIR__ . '/resources/alarms.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals('-30M:DISPLAY', $event['alarms'], "Stripped alarm string");
+        $alarm = libcalendaring::parse_alarm_value($event['alarms']);
+        $this->assertEquals('30', $alarm[0], "Alarm value");
+        $this->assertEquals('-M', $alarm[1], "Alarm unit");
+        $this->assertEquals('-30M', $alarm[2], "Alarm string");
+        $this->assertEquals('-PT30M', $alarm[3], "Unified alarm string (stripped zero-values)");
+
+        $this->assertEquals('DISPLAY', $event['valarms'][0]['action'],  "First alarm action");
+        $this->assertEquals('', $event['valarms'][0]['related'],  "First alarm related property");
+        $this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'],  "First alarm text");
+
+        $this->assertEquals(3, count($event['valarms']), "List all VALARM blocks");
+
+        $valarm = $event['valarms'][1];
+        $this->assertEquals(1, count($valarm['attendees']), "Email alarm attendees");
+        $this->assertEquals('EMAIL', $valarm['action'],  "Second alarm item (action)");
+        $this->assertEquals('-P1D',  $valarm['trigger'], "Second alarm item (trigger)");
+        $this->assertEquals('This is the reminder message',  $valarm['summary'], "Email alarm text");
+        $this->assertInstanceOf('DateTime', $event['valarms'][2]['trigger'], "Absolute trigger date/time");
+
+        // test alarms export
+        $ics = $ical->export(array($event));
+        $this->assertContains('ACTION:DISPLAY',   $ics, "Display alarm block");
+        $this->assertContains('ACTION:EMAIL',     $ics, "Email alarm block");
+        $this->assertContains('DESCRIPTION:This is the first event reminder',    $ics, "Alarm description");
+        $this->assertContains('SUMMARY:This is the reminder message',            $ics, "Email alarm summary");
+        $this->assertContains('ATTENDEE:mailto:reminder-recipient@example.org',  $ics, "Email alarm recipient");
+        $this->assertContains('TRIGGER;VALUE=DATE-TIME:20130812',  $ics, "Date-Time trigger");
+    }
+
+    /**
+     * @depends test_import_from_file
+     */
+    function test_attachment()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/attachment.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals(2, count($events));
+        $this->assertEquals(1, count($event['attachments']));
+        $this->assertEquals('image/png', $event['attachments'][0]['mimetype']);
+        $this->assertEquals('500px-Opensource.svg.png', $event['attachments'][0]['name']);
+    }
+
+    /**
+     * @depends test_import
+     */
+    function test_apple_alarms()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/apple-alarms.ics', 'UTF-8');
+        $event = $events[0];
+
+        // alarms
+        $this->assertEquals('-45M:AUDIO', $event['alarms'], "Relative alarm string");
+        $alarm = libcalendaring::parse_alarm_value($event['alarms']);
+        $this->assertEquals('45', $alarm[0], "Alarm value");
+        $this->assertEquals('-M', $alarm[1], "Alarm unit");
+
+        $this->assertEquals(1, count($event['valarms']), "Ignore invalid alarm blocks");
+        $this->assertEquals('AUDIO', $event['valarms'][0]['action'],   "Full alarm item (action)");
+        $this->assertEquals('-PT45M', $event['valarms'][0]['trigger'], "Full alarm item (trigger)");
+        $this->assertEquals('Basso',  $event['valarms'][0]['uri'],     "Full alarm item (attachment)");
+    }
+
+    /**
+     * 
+     */
+    function test_escaped_values()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/escaped.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals("House, Street, Zip Place", $event['location'], "Decode escaped commas in location value");
+        $this->assertEquals("Me, meets Them\nThem, meet Me", $event['description'], "Decode description value");
+        $this->assertEquals("Kolab, Thomas", $event['attendees'][3]['name'], "Unescaped");
+
+        $ics = $ical->export($events);
+        $this->assertContains('ATTENDEE;CN="Kolab, Thomas";PARTSTAT=', $ics, "Quoted attendee parameters");
+    }
+
+    /**
+     * Parse RDATE properties (#2885)
+     */
+    function test_rdate()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals(9, count($event['recurrence']['RDATE']));
+        $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][0]);
+        $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][1]);
+    }
+
+    /**
+     * @depends test_import
+     */
+    function test_freebusy()
+    {
+        $ical = new libvcalendar();
+        $ical->import_from_file(__DIR__ . '/resources/freebusy.ifb', 'UTF-8');
+        $freebusy = $ical->freebusy;
+
+        $this->assertInstanceOf('DateTime', $freebusy['start'], "'start' property is DateTime object");
+        $this->assertInstanceOf('DateTime', $freebusy['end'], "'end' property is DateTime object");
+        $this->assertEquals(11, count($freebusy['periods']), "Number of freebusy periods defined");
+        $periods = $ical->get_busy_periods();
+        $this->assertEquals(9, count($periods), "Number of busy periods found");
+        $this->assertEquals('BUSY-TENTATIVE', $periods[8][2], "FBTYPE=BUSY-TENTATIVE");
+    }
+
+    /**
+     * @depends test_import
+     */
+    function test_freebusy_dummy()
+    {
+        $ical = new libvcalendar();
+        $ical->import_from_file(__DIR__ . '/resources/dummy.ifb', 'UTF-8');
+        $freebusy = $ical->freebusy;
+
+        $this->assertEquals(0, count($freebusy['periods']), "Ignore 0-length freebudy periods");
+        $this->assertContains('dummy', $freebusy['comment'], "Parse comment");
+    }
+
+    function test_vtodo()
+    {
+        $ical = new libvcalendar();
+        $tasks = $ical->import_from_file(__DIR__ . '/resources/vtodo.ics', 'UTF-8', true);
+        $task = $tasks[0];
+
+        $this->assertInstanceOf('DateTime', $task['start'],   "'start' property is DateTime object");
+        $this->assertInstanceOf('DateTime', $task['due'],     "'due' property is DateTime object");
+        $this->assertEquals('-1D:DISPLAY',  $task['alarms'],  "Taks alarm value");
+        $this->assertEquals('IN-PROCESS',   $task['status'],  "Task status property");
+        $this->assertEquals(1, count($task['x-custom']),      "Custom properties");
+        $this->assertEquals(4, count($task['categories']));
+        $this->assertEquals('1234567890-12345678-PARENT', $task['parent_id'], "Parent Relation");
+
+        $completed = $tasks[1];
+        $this->assertEquals('COMPLETED', $completed['status'], "Task status=completed when COMPLETED property is present");
+        $this->assertEquals(100, $completed['complete'], "Task percent complete value");
+
+        $ics = $ical->export(array($completed));
+        $this->assertRegExp('/COMPLETED(;VALUE=DATE-TIME)?:[0-9TZ]+/', $ics, "Export COMPLETED property");
+    }
+
+    /**
+     * Test for iCal export from internal hash array representation
+     *
+     * 
+     */
+    function test_export()
+    {
+        $ical = new libvcalendar();
+
+        $events = $ical->import_from_file(__DIR__ . '/resources/itip.ics', 'UTF-8');
+        $event = $events[0];
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+        $event += $events[0];
+
+        $this->attachment_data = $event['attachments'][0]['data'];
+        unset($event['attachments'][0]['data']);
+        $event['attachments'][0]['id'] = '1';
+        $event['description'] = '*Exported by libvcalendar*';
+
+        $event['start']->setTimezone(new DateTimezone('America/Montreal'));
+        $event['end']->setTimezone(new DateTimezone('Europe/Berlin'));
+
+        $ics = $ical->export(array($event), 'REQUEST', false, array($this, 'get_attachment_data'), true);
+
+        $this->assertContains('BEGIN:VCALENDAR',    $ics, "VCALENDAR encapsulation BEGIN");
+
+        $this->assertContains('BEGIN:VTIMEZONE', $ics, "VTIMEZONE encapsulation BEGIN");
+        $this->assertContains('TZID:Europe/Berlin', $ics, "Timezone ID");
+        $this->assertContains('TZOFFSETFROM:+0100', $ics, "Timzone transition FROM");
+        $this->assertContains('TZOFFSETTO:+0200', $ics, "Timzone transition TO");
+        $this->assertContains('TZOFFSETFROM:-0400', $ics, "TZOFFSETFROM with negative offset (Bug T428)");
+        $this->assertContains('TZOFFSETTO:-0500', $ics, "TZOFFSETTO with negative offset (Bug T428)");
+        $this->assertContains('END:VTIMEZONE', $ics, "VTIMEZONE encapsulation END");
+
+        $this->assertContains('BEGIN:VEVENT',       $ics, "VEVENT encapsulation BEGIN");
+        $this->assertSame(2, substr_count($ics, 'DTSTAMP'), "Duplicate DTSTAMP (T1148)");
+        $this->assertContains('UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0', $ics, "Event UID");
+        $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->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");
+        $this->assertRegExp('/:mailto:rolf2@/',                           $ics, "Export Attendee mailto:");
+
+        $rrule = $event['recurrence'];
+        $this->assertRegExp('/RRULE:.*FREQ='.$rrule['FREQ'].'/',          $ics, "Export Recurrence Frequence");
+        $this->assertRegExp('/RRULE:.*INTERVAL='.$rrule['INTERVAL'].'/',  $ics, "Export Recurrence Interval");
+        $this->assertRegExp('/RRULE:.*UNTIL=20140718T215959Z/',           $ics, "Export Recurrence End date");
+        $this->assertRegExp('/RRULE:.*BYDAY='.$rrule['BYDAY'].'/',        $ics, "Export Recurrence BYDAY");
+        $this->assertRegExp('/EXDATE.*:20131218/',     $ics, "Export Recurrence EXDATE");
+
+        $this->assertContains('BEGIN:VALARM',   $ics, "Export VALARM");
+        $this->assertContains('TRIGGER;RELATED=END:-PT12H', $ics, "Export Alarm trigger");
+
+        $this->assertRegExp('/ATTACH.*;VALUE=BINARY/',                    $ics, "Embed attachment");
+        $this->assertRegExp('/ATTACH.*;ENCODING=BASE64/',                 $ics, "Attachment B64 encoding");
+        $this->assertRegExp('!ATTACH.*;FMTTYPE=text/html!',               $ics, "Attachment mimetype");
+        $this->assertRegExp('!ATTACH.*;X-LABEL=calendar.html!',           $ics, "Attachment filename with X-LABEL");
+
+        $this->assertContains('END:VEVENT',     $ics, "VEVENT encapsulation END");
+        $this->assertContains('END:VCALENDAR',  $ics, "VCALENDAR encapsulation END");
+    }
+
+    /**
+     * @depends test_extended
+     * @depends test_export
+     */
+    function test_export_multiple()
+    {
+        $ical = new libvcalendar();
+        $events = array_merge(
+            $ical->import_from_file(__DIR__ . '/resources/snd.ics', 'UTF-8'),
+            $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8')
+        );
+
+        $num = count($events);
+        $ics = $ical->export($events, null, false);
+
+        $this->assertContains('BEGIN:VCALENDAR', $ics, "VCALENDAR encapsulation BEGIN");
+        $this->assertContains('END:VCALENDAR',   $ics, "VCALENDAR encapsulation END");
+        $this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
+        $this->assertEquals($num, substr_count($ics, 'END:VEVENT'),   "VEVENT encapsulation END");
+    }
+
+    /**
+     * @depends test_export
+     */
+    function test_export_recurrence_exceptions()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/recurring.ics', 'UTF-8');
+
+        // add exceptions
+        $event = $events[0];
+        unset($event['recurrence']['EXCEPTIONS']);
+
+        $exception1 = $event;
+        $exception1['start'] = clone $event['start'];
+        $exception1['start']->setDate(2013, 8, 14);
+        $exception1['end'] = clone $event['end'];
+        $exception1['end']->setDate(2013, 8, 14);
+
+        $exception2 = $event;
+        $exception2['start'] = clone $event['start'];
+        $exception2['start']->setDate(2013, 11, 13);
+        $exception2['end'] = clone $event['end'];
+        $exception2['end']->setDate(2013, 11, 13);
+        $exception2['title'] = 'Recurring Exception';
+
+        $events[0]['recurrence']['EXCEPTIONS'] = array($exception1, $exception2);
+
+        $ics = $ical->export($events, null, false);
+
+        $num = count($events[0]['recurrence']['EXCEPTIONS']) + 1;
+        $this->assertEquals($num, substr_count($ics, 'BEGIN:VEVENT'),       "VEVENT encapsulation BEGIN");
+        $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;TZID=Europe/Zurich:20130814', $ics, "Recurrence-ID (1) being the exception date");
+        $this->assertContains('RECURRENCE-ID;TZID=Europe/Zurich:20131113', $ics, "Recurrence-ID (2) being the exception date");
+        $this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title");
+    }
+
+    function test_export_valid_rrules()
+    {
+        $event = array(
+            'uid' => '1234567890',
+            'start' => new DateTime('now'),
+            'end' => new DateTime('now + 30min'),
+            'title' => 'test_export_valid_rrules',
+            'recurrence' => array(
+                'FREQ' => 'DAILY',
+                'COUNT' => 5,
+                'EXDATE' => array(),
+                'RDATE' => array(),
+            ),
+        );
+        $ical = new libvcalendar();
+        $ics = $ical->export(array($event), null, false, null, false);
+
+        $this->assertNotContains('EXDATE=', $ics);
+        $this->assertNotContains('RDATE=', $ics);
+    }
+
+    /**
+     *
+     */
+    function test_export_rdate()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
+        $ics = $ical->export($events, null, false);
+
+        $this->assertContains('RDATE:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values");
+    }
+
+    /**
+     * @depends test_export
+     */
+    function test_export_direct()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple.ics', 'UTF-8');
+        $num = count($events);
+
+        ob_start();
+        $return = $ical->export($events, null, true);
+        $output = ob_get_contents();
+        ob_end_clean();
+
+        $this->assertTrue($return, "Return true on successful writing");
+        $this->assertContains('BEGIN:VCALENDAR', $output, "VCALENDAR encapsulation BEGIN");
+        $this->assertContains('END:VCALENDAR',   $output, "VCALENDAR encapsulation END");
+        $this->assertEquals($num, substr_count($output, 'BEGIN:VEVENT'), "VEVENT encapsulation BEGIN");
+        $this->assertEquals($num, substr_count($output, 'END:VEVENT'),   "VEVENT encapsulation END");
+    }
+
+    function test_datetime()
+    {
+        $ical = new libvcalendar();
+        $cal  = new \Sabre\VObject\Component\VCalendar();
+        $localtime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')));
+        $localdate = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01', new DateTimeZone('Europe/Berlin')), false, true);
+        $utctime   = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('UTC')));
+        $asutctime = $ical->datetime_prop($cal, 'DTSTART', new DateTime('2013-09-01 12:00:00', new DateTimeZone('Europe/Berlin')), true);
+
+        $this->assertContains('TZID=Europe/Berlin', $localtime->serialize());
+        $this->assertContains('VALUE=DATE', $localdate->serialize());
+        $this->assertContains('20130901T120000Z', $utctime->serialize());
+        $this->assertContains('20130901T100000Z', $asutctime->serialize());
+    }
+
+    function test_get_vtimezone()
+    {
+        $vtz = libvcalendar::get_vtimezone('Europe/Berlin', strtotime('2014-08-22T15:00:00+02:00'));
+        $this->assertInstanceOf('\Sabre\VObject\Component', $vtz, "VTIMEZONE is a Component object");
+        $this->assertEquals('Europe/Berlin', $vtz->TZID);
+        $this->assertEquals('4', $vtz->{'X-MICROSOFT-CDO-TZID'});
+
+        // check for transition to daylight saving time which is BEFORE the given date
+        $dst = reset($vtz->select('DAYLIGHT'));
+        $this->assertEquals('DAYLIGHT', $dst->name);
+        $this->assertEquals('20140330T010000', $dst->DTSTART);
+        $this->assertEquals('+0100', $dst->TZOFFSETFROM);
+        $this->assertEquals('+0200', $dst->TZOFFSETTO);
+        $this->assertEquals('CEST', $dst->TZNAME);
+
+        // check (last) transition to standard time which is AFTER the given date
+        $std = end($vtz->select('STANDARD'));
+        $this->assertEquals('STANDARD', $std->name);
+        $this->assertEquals('20141026T010000', $std->DTSTART);
+        $this->assertEquals('+0200', $std->TZOFFSETFROM);
+        $this->assertEquals('+0100', $std->TZOFFSETTO);
+        $this->assertEquals('CET', $std->TZNAME);
+
+        // unknown timezone
+        $vtz = libvcalendar::get_vtimezone('America/Foo Bar');
+        $this->assertEquals(false, $vtz);
+
+        // invalid input data
+        $vtz = libvcalendar::get_vtimezone(new DateTime());
+        $this->assertEquals(false, $vtz);
+
+        // DateTimezone as input data
+        $vtz = libvcalendar::get_vtimezone(new DateTimezone('Pacific/Chatham'));
+        $this->assertInstanceOf('\Sabre\VObject\Component', $vtz);
+        $this->assertContains('TZOFFSETFROM:+1245', $vtz->serialize());
+        $this->assertContains('TZOFFSETTO:+1345', $vtz->serialize());
+    }
+
+    function get_attachment_data($id, $event)
+    {
+        return $this->attachment_data;
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/alarms.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,56 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 5.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:Europe/Zurich
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:CEST
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:CET
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+
+BEGIN:VEVENT
+UID:1dq52u617gkfqrr4uo1i2uh70
+CREATED:20130924T221822Z
+DESCRIPTION:
+DTSTART:20130818T230000Z
+DTEND:20130819T010000Z
+DTSTAMP:20130824T235608Z
+LAST-MODIFIED:20130924T222118Z
+LOCATION:
+SEQUENCE:2
+STATUS:CONFIRMED
+SUMMARY:Alarms test
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:This is the first event reminder
+TRIGGER:-P0DT0H30M0S
+END:VALARM
+BEGIN:VALARM
+ACTION:EMAIL
+DESCRIPTION:This is an event reminder
+TRIGGER:-P1D
+ATTENDEE:mailto:reminder-recipient@example.org
+SUMMARY:This is the reminder message
+DESCRIPTION:This is the second event reminder
+END:VALARM
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:An absolute reminder
+TRIGGER;VALUE=DATE-TIME:20130812T160000Z
+END:VALARM
+END:VEVENT
+
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/apple-alarms.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,50 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.9//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:Europe/Zurich
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:MESZ
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:MEZ
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+TRANSP:OPAQUE
+DTEND;TZID=Europe/Zurich:20131106T100000
+UID:EF185A2A-55FA-4FF3-9B02-56B0914FC79A
+DTSTAMP:20131029T123927Z
+LOCATION:
+DESCRIPTION:With alarm
+STATUS:CONFIRMED
+SEQUENCE:4
+X-APPLE-TRAVEL-DURATION:PT30M
+SUMMARY:Testing Bug 2415
+LAST-MODIFIED:20131029T123819Z
+DTSTART;TZID=Europe/Zurich:20131106T090000
+CREATED:20131029T123819Z
+BEGIN:VALARM
+X-WR-ALARMUID:C4A26F1A-A433-4102-82D5-A3347FC126D4
+UID:C4A26F1A-A433-4102-82D5-A3347FC126D4
+TRIGGER;VALUE=DATE-TIME:19760401T005545Z
+ACTION:NONE
+END:VALARM
+BEGIN:VALARM
+X-WR-ALARMUID:DEF5F23D-98FC-4510-BC99-F877CD9A9F8B
+UID:DEF5F23D-98FC-4510-BC99-F877CD9A9F8B
+TRIGGER;X-APPLE-RELATED-TRAVEL=-PT15M:-PT45M
+ATTACH;VALUE=URI:Basso
+ACTION:AUDIO
+END:VALARM
+END:VEVENT
+END:VCALENDAR
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/attachment.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,344 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.0-git//Sabre//Sabre VObject
+  2.1.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:93B331265CF47061F888BC0A3F0FBD7F-FCBB6C4091F28CA0
+DTSTAMP;VALUE=DATE-TIME:20131017T084408Z
+CREATED;VALUE=DATE-TIME:20131017T083610Z
+LAST-MODIFIED;VALUE=DATE-TIME:20131017T083610Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/Zurich:20131017T110000
+DTEND;VALUE=DATE-TIME;TZID=Europe/Zurich:20131017T120000
+SUMMARY:Event with attachment
+LOCATION:Test lab
+TRANSP:OPAQUE
+CLASS:PUBLIC
+ORGANIZER;CN=Bruederli:mailto:thomas.bruederli@example.org
+ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png;X-LABEL=500px-Opensou
+ rce.svg.png:iVBORw0KGgoAAAANSUhEUgAAATwAAAE8CAYAAABdBQ0GAAAACXBIWXMAAAsTAA
+ ALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iA
+ lEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIis
+ r74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz
+ /SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAY
+ CdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgw
+ ABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEO
+ cqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9
+ eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZr
+ IPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFH
+ BPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t
+ 8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmS
+ cQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQi
+ iGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4
+ IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDua
+ g3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7Ei
+ rAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqE
+ u0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBoj
+ k8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt
+ 2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX
+ 6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlS
+ aVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z
+ /YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1
+ ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aed
+ pr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6
+ feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8
+ o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+
+ eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZn
+ w7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt
+ 6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRK
+ dPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7
+ XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/
+ 0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc
+ 5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+
+ qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpx
+ apLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkk
+ xTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty
+ 8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2
+ pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6w
+ tVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm
+ 6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+l
+ Q27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHt
+ xwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTrado
+ x7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fy
+ z4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdO
+ o8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9
+ zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDY
+ brnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bC
+ xh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0
+ hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAOB5JREFUeNrsnXl0HFeV
+ /7+3etGulmRJlm15keUt9jgGR1VyrLBDMoRlfmyBDBMYfjBsA2HffmEbtgFmhgkTwjJs2YAk7B
+ kCwSEkECSsKtmJ7XiRJVmWtViLtbWWVlcv7/eH2x4SYtWr7uru6u77OUcn58Svuqvve/Wte9+7
+ 7z4SQoBhGKYQUNgEDMOw4DEMw7DgMQzDsOAxDMOw4DEMw7DgMQzDsOAxDMOw4DEMw7DgMQzDsO
+ AxDMOw4DEMw4LHMAzDgscwDJPbeJO5qK6uji3HSKNpWoCIFI/H4zFNs/JJb1xFMYloAQC8Xu9C
+ e3u7yRZjZJiYmLB9DSVTHooFr3Bpa2vzR6PRdQA2xOPxDUS0SgixAsBf/tUm/iqT/BoTwORT/s
+ 4BOEdE5wCcEUKcjsVipw8ePDjJvcKCx4LHpAKpqrqBiHYB2CmE2AygCcAGAKvhrqmQOQCnAZwC
+ cIqIjgM4FA6Hnzh06NAidyULHgsec5Fdu3aV+v3+XQCeQUS7hBA7AfxNCt6ZW4gD6ANwCMARIj
+ qsKMqB/fv3D3Kvs+Cx4BUILS0taxRFeTaAKwHsFULsQpJzuTnKKIA/E9Gj8Xi8o6am5uC+ffsi
+ PDJY8Fjw8oDW1tZNQohrhBBtANoArGOrPIkQgC4AjwL4g8/n+2NHR8cSm4UFjwUvR0LUoqKi5w
+ ohXgzgbwFsYqvYYgHA74joASJ6oLOz8zSbhAWPre0i9uzZszIWi70KwN8BeDaAYraKY5wgol8L
+ IX5mGEYHAD71igWPyTR79+6tjUQirwJwHYDnAPCwVdLOEIAfCyHu6erq0ln8WPCYNNLW1lYeiU
+ SuE0JcB+AFcNlig6Io8Hq98Hq98Pl8UBQFinI+g8XjOa/HRHTx/wkhEI/HAQDxeBwXxmIsFkMs
+ FkMkEkE0GkU0GnVjd5wmonsB3KXr+hEenSx4jENomqYB+CchxOsAlGfjHnw+H4qKiuD3+1FUVH
+ Txz+fzXRS5C6LmNEIIRKNRRCIRxGIxhMNhmKaJpaUlmKaJcDiMcDiMLJ6x3AngO36//+729vZ5
+ HrEseIx9kQsAuEEI8U8ALs+ksJWVlaG4uBilpaUoLS1FcXFx2sTMSVE0TROhUAiLi4tP+m8GhX
+ AewA8BfNswjC4exSx4jAWqqm4novcLIf4eQEm6Q9Dy8nKUl5ejoqICpaWl8Pv9eWXPeDyOUCiE
+ +fl5zM3NYX5+HuFwOBNf/RgRfaWqquoezvNjwWP+2qN7HoAPCCGuBUDp+A6Px4PKykpUVlaivL
+ wcZWVlIKKCs3U4HL4ogMFgEKFQKJ1fN0hEt8ZisW8dOHBghkc6C17BcvXVV/ump6evI6L3CSGu
+ SMd3lJaWIhAIIBAIoKKi4uKCAfNkAZydnb34F4vF0vE1c0T0XSHEzYZhDLDVWfAKhra2Nq9pmj
+ cA+ATOb8p3DCJCIBBAdXU1AoEAioqK2OA2Q+C5uTnMzMxgeno6HeFvlIhuF0J8loWPBS/fhU4x
+ TfN1AD4FYIuTIldZWYmamhpUV1fD5/OxsR1ACIH5+XlMTk5ienoapuloqT6TiL4Tj8e/0NXVNc
+ zWZsHLJ0jTtFcJIT4NYIdTIldRUYGamhrU1NSwyGVA/Obm5jA1NYXJyUkncwNDRPQtRVG+uH//
+ /jG2NAteTqNp2lVCiK8C2O3E5/n9fqxYsQJ1dXUoKSlhA2cp7J2amsLExASCwaBTHzsP4ItCiK
+ 90dXWF2MoseLkmdOuEEF8E8DqkuOp6IWStr69HVVUVLzy4iFAohImJCUxOTjoV8p4hoo/qun43
+ eOsaC57bSWz/+qgQ4v1IMY/O4/Ggrq4ODQ0NvPiQA17f9PQ0RkdHMT/vyGaLR4UQ7+nq6nqMrc
+ uC51av7johxL8hxZpzfr8fDQ0NqKurg9frZcPmGMFgEKOjo5iZmUl1l0eciL4fjUY/wmd3sOC5
+ SegahRBfB/CyVD6nrKwMq1atQk1NTUEmBOcbi4uLGBsbw7lz5y4WSUiScQDvMQzjbrYqC142w1
+ fFNM23A/hXpHAGRFlZGRobG1FVVcVGzUNM08TIyAgmJiZSEj4iuh/A23VdH2KrsuBl2qvbKoT4
+ DoCrkv2MkpISNDY2orq6mj26AiAcDl8UvhRC3Tki+pjP5/tGe3t7vJDtyYKXGa/Oa5rmBwF8Gk
+ BSKwklJSVYs2YNh64FytLSEkZGRnDu3LlUhO9RAG82DKOHBY8FLy2oqtoM4C4Ae5K53uv1Yu3a
+ tairq2OhY7C4uIjBwUHMzMwk/REAPmgYxjdRgCksLHjpFbs3AfgqgAq71yqKgpUrV2L16tW86s
+ r8FbOzsxgYGEilast9iqK8pbOzc6KQ7MaClwZaW1urhRDfEkK8Jpnrq6ursW7dOhQX87k6zKWJ
+ x+MYGxvDyMhIstvWRgG8yTCMB1jwWPCSQtO05wkhbgew1u61xcXF2LBhAwKBAD/NjDTRaBSDg4
+ PJLmwIAP/l8/k+Wghn67LgOQepqnoTgH8BYGsfFxFh1apVWLNmDW8BY5Jmbm4Op06dwtJSUrr1
+ GIBXGYbRz4LHgmfl1QWEEHciiSTisrIyNDU1oaysjJ9YxpEwd3h4GGfPnk3G25skotfruv5bFj
+ wWvKdlz549fxOLxX4GYLOd6xRFQWNjIxoaGnj1lXGcxcVFnDp1CgsLC3YvjQH4lGEYX0AeruKy
+ 4KWAqqqvBfAd2DwGsbS0FBs3bmSvjkm7tzc0NITR0dFkvL37fD7fDR0dHcF8sgkLXnKQpmmfF0
+ J8zNZFRKivr8e6det4ro7JGLOzs+jv70+m9PwJAC81DKOPBa9ABW/37t1FXq/3tsQB19L4fD5s
+ 3LiR974yWSESiWBgYACTk7YLqIwT0d/pur6fBa/ABK+lpaWGiH4Jm3thA4EAmpubuaQ6k3XGxs
+ Zw5swZuwUJQkR0g67rPy1EwSvIWExV1WYi2m9X7FatWoWtW7ey2DGuYOXKlbjsssvsFoctEULc
+ q6rqBwrRZgXn4bW0tLQS0a8A1Mpe4/F40NTUhBUrVvBTxrgyxO3t7U3mfI1b/X7/jbladYU9PG
+ vP7rlE9Ds7YldcXIzt27ez2DGuxefzYdu2bWhoaLB76T9HIpE7tm7d6ikUWxWM4Kmq+mIA98NG
+ 2klVVRV27NiB0tJSfqoYd4dqRFi/fj2am5ttZQ0IIV5fWVn547a2Nj8LXv6I3SsA/AKAtHLV19
+ djy5YtXN2EySlqa2uxbds2u/PMrzBN8xe7du3K+zd73gueqqqvB3AvAKk3GBFh7dq1aGpq4l0T
+ TE5SUVGB7du3263Q82K/33+/pmkV+WybvBY8TdPeCOB2AFJumqIoaG5uxurVq/mpYXKaC3PPFR
+ W29Ou5QogH2trayvPVLnkreKqqvkYI8V0AUhOyFyZ+eXGCyRcujOmamho7l+01TfOX+Rre5qXg
+ qar6EpwvxS4ldn6/H9u2bbP7NmQY9z/gioJNmzbZTSV7flFR0b35uJCRd4KnqurzAfwYknN2F8
+ SOV2KZfIWI0NTUZCttRQjxEtM0f5BvKSt5JXiqqu4F8D8ASmTaFxUVYfv27SgpKeGngsl70Vu/
+ fr3d+elXV1ZWfhdA3qze5Y3gaZq2E8CvIZl6UlJSgu3bt9vdlsMwOc3atWuxdq2tEwveqGnaF1
+ nw3OXZ1Qshfg1A6gCJkpISbNu2DX6/n58ApuBYvXo11q9fbye8/bCqqu9gwXMBidWk+wA0yrQv
+ Li5msWMKnoaGBqxZs8bOJbdomvZyFrwssnXrVo/f7/8RgFaZ9hcWKFjsGAZobGzEqlWrZJt7hB
+ A/am1tVVnwskRlZeUtAKTeOn6/P5lSOgyT16xduxb19fWyzUvj8fjPNE1rzNXfm7OCp6rqOwFI
+ zStcSMDkw7AZ5skQETZs2IDaWukCQo1CiPtzNTE5JwUvkX7yn1J+uMeDLVu2cOoJwywjehs3bk
+ R1dbXsJZf7/f7vsOBlgD179qyEZDEAIkJzczPKy8t5VDOMxLNiY7fR9aqqvpcFL420tbV5Y7HY
+ vQCklpfWr19v563FMAWNx+PBpk2b7Mxzf1lV1Wex4KUJ0zS/DODZMm0bGhqwcuVKHsUMYwO/32
+ +nDqQPwL2apuVMeaGcETxVVV8D4H0ybaurq7Fu3ToevQyTBKWlpdi0aZNs5eQGAPe2tbXlRKXc
+ nBA8VVXXA/hvmbZlZWVobm7m4p0MkwKBQAAbNmyQaiuEaDNN8xMseA6QqNZwO4AqS//a58PmzZ
+ vh8Xh4xDJMitTV1dnJ0bspkT3BgpcKlZWVHwLwHKt2F1aZOLGYYZxj/fr1KCsrk2nqAfCDvXv3
+ VrLgJUliG8tnZNo2NjYiEAjwCGUYJwVCUbBlyxbZQ4E2RCKRr7HgJYGqqmXxePwunF8JWpbq6m
+ o7ewIZhrGB3++3My9+g6Zp17Pg2YSIvgxgi1W74uJibNy4kRcpGCaNBAIBNDbKbaEVQtzq1lQV
+ VwqeqqrPF0JY7pMlImzatInPjmWYDLBq1SrZaaNqIcS3WfAkaGlpKcH5FBRLl62xsVF2QpVhmN
+ SjLmzcuFF2Pu9aN4a2iguN+i8Amq3aVVZW8rwdw2QYv9+PpqYm2dD25t27d7vq3FNXCZ6mabsh
+ sZvC6/XyvB3DZInq6mrZ/Lx6r9f7FRa8p6Gtrc0rhPgOAMsJufXr13O+HcNkkXXr1knVlxRCvE
+ HTtBex4D2FSCTyAQDPlHm72ChWyDBMGvB4PGhqapKKsoQQ31RV1RWT7a4QPE3T1gkhPiljZDun
+ LTEMkz4qKytRV1cn03QjgI+z4P3vG+DfIHGe7Nq1azmUZRgXsXbtWtlDsd6nqmpTtu8364KXKC
+ D4Gqt2FRUVdjYyMwyTAbxer2zUVQTgPwpa8Nra2hQAX4FFzh0RSc8XMAyTWWpqamQri79C07Tn
+ FazgRSKRfwTQYtWuoaGBD+FhGBezbt06qYKhQoibEyXfCkvw9u7dWymE+LxVO5/Ph9WrV/OIYh
+ gXU1xcjIaGBpmmlwcCgX8qOMGLRqPvx/ny0MuyZs0a3ivLMDnA6tWrpRYwhBCfzlaaSlYEr6Wl
+ pUYIYbmjorS0lBcqGCZH8Hg8shVVVhLRuwpG8IjoQwAsK6OuX7+eFyoYJoeora2VOgdaCPGhbF
+ RHzrjgqapaD+DdVu2qq6tRWVnJI4hhcggikj0xcEUkErkx7wWPiD4MoMzKaGvWrOHRwzA5SEVF
+ BaqqqmSafqC1tbU6k/eWUcHTNG21EOKdVu1qamq4zh3D5DCSDkuVEOL9eSt4iYWKEvbuGCa/KS
+ 8vl0pGFkK854orrqjK1H1lTPASP+ptloH9ihWcZMwweeLlSSw6Vng8nrfkneAlflQFe3cMUxiU
+ lZWhpqZGxst7d1tbW0aSbTMieIninpYrs3V1dVJFBRmGyQ1Wr14t4+WtM03z7/NG8EzTvA7AOi
+ vvbuXKlTxCGCaPKC0tlT3p7L35FNJ+0KpBIBBAaWkpjxCGyTMk99g+U1XV5+e84Gma9mxIlG6X
+ NArDMDlGIBCQSjMjorSnqGTCw3urVYOysjJZt5dhmBxEZrpKCPG3mqY1pvM+0ip4ra2t1UKIVz
+ phDIZhcpcVK1bIHM/gEUK8OWcFTwhxAywSjf1+P1asWMEjgmHyGEVRZKet3pSohJ4W0pr7IoSw
+ TCisq6uTqpTK5A6maWJpaeniXzgcRiwWQzQahRACsVgM8XgcHo8HXq8XiqJAURT4fD4UFxc/6Y
+ /HRv5QW1uLwcFBxOPx5ZqtN03zagAP5JTgqap6JYCdy7UhItlj3hiXEovFsLCwgNnZWczPz2Nx
+ cRHRaFTq2kgkAqvxUVxcjLKyMlRWVqKiooLzNHMYr9eL6upqTE5OWvX7W3JO8IjozUKIZdsEAg
+ E+djEHCYVCmJqaQjAYxPz8vNUbO5UIAaFQCKFQCOfOnQMAFBUVobKyElVVVaiqqmIPMMeor6+3
+ FDwhxMv27Nmzcv/+/WM5IXi7d+8uEkK8WiacZXInTJ2ensa5c+cwPz+ftfsIh8OYmJjAxMQEvF
+ 4vampqLhad5GKx7ueCl760tLRcM388Hn81gFtzQvC8Xu81Qohl80z8fr9szSwmiywuLmJkZART
+ U1Ow8tgzTTQaxfj4OMbHx1FcXIxVq1ahtraWvT4Xc2Eaa3Bw0MrLe206BC9dI+M6qwYrVqzgge
+ li5ufncfLkSTzxxBOYnJx0ndg9laWlJfT39+Pw4cMYHR1FLBbjTnQptbW1Mt54W0tLi+OVRBxX
+ nL179xYLIV4u86MZdwpHd3c3jh49iunpadcL3dOFvAMDAzh06BDGx8dz7v4LAb/fL7PRQCGi17
+ he8CKRyLWwKANVUlLC+2ZdRjwex9DQEI4cOYKZmZmc/z2RSAT9/f04fvw4FhYWuINdhkzZKACO
+ C57jc3hE9Bqrtyp7d+5iZmYGp0+fRjgcTufXBAEsAFggolkAEEIU4fz5JlWJl6Tj43Fubg5Hjx
+ 5FfX091q5dC4/Hwx3uAqqrq6EoitUK/5Wapq3Tdf2MKwUvsTr7Upkfy2SfWCyGgYEBTExMOPmx
+ xwD8CcBxACeEECfn5uYGuru7LSfVrrjiiiqPx7NFCLENwFYAuwFcBaA8lRsSQmBsbAwzMzPYuH
+ Ejn4bnArxeLyorK62iCRJCvALAV10peF6v99lCiGUHZ2lpKZdwdwELCwvo7e21Sg+QYZaIfgbg
+ QSHEw4ZhjCb7QQcOHJgBoCf+AABXX321b3p6WiOi5ycG/zOT/fxwOIwTJ05gzZo1soUpmTSyYs
+ UKmemTa50UPEpmUvdS+XOapv27EOIDy127du1arF69mns7i4yPj2NgYCCVhOEYgN8CuFMI8cuu
+ rq5Qpu5d07SdiT3arweQ9ECqrKxEc3Mz/H4/D4gsRhgHDx60GodLQoiapxtjyUQmjnp4QogXW7
+ WRnKxk0kA8Hkd/f//FXQtJenO3CCFuTcWTSwVd148A+PDWrVs/FggEXiKE+AiAvXY/JxgM4ujR
+ o9i8eTPKy8t5cGQBj8eDqqoqTE1NLdesmIieD+B+V4W0mqatE0JsX65NSUkJ74XM4tu0t7c32R
+ XYCSK62ev1fq2joyPoht+TmBO8D8B9mqY9D8BNQogX2PkM0zRx4sQJbNq0iZPgs0QgELASPBDR
+ NU4JnpNpKdfK/Dgm80QiERw/fjwZsQsR0SdN09yg6/oX3CJ2T+P1Pazr+gsBtAF4zO6L4OTJk6
+ l4vUyKgicROV7r1Pc5JnhCiGus2vBbNPOEw2EcO3YsmVy0XwHYoev6Zw8dOrSYC7/VMIyOYDCo
+ ArgRwIyNsYtTp05hdHSUB0yGKSoqksnJbVZVtdk1gpco2PecZb9IUVBRUcE9nEGi0ShOnjxpdy
+ U2COB1hmG8zDCM/lz7zd3d3THDMG4hoh0AHrYjemfOnGFPz6VeXmIezx2CFw6HLwOwbHJdZWUl
+ 753NIBdCtcVFW87Z4wBaDMO4J9d/v67rI36//4VE9EmcX1WWEr3+/n7Mzs7yAMogMpGfEGKvE9
+ /liAIR0VVOqDjj2PQC+vr6MDc3Z6cPv+vz+a40DKMnX+zQ3t4e13X9s0T0IgBTMtfE43H09PRk
+ tQRWoVFeXi6zA+YqJ77LKZfrShY89zA0NITp6Wk7l3xW1/W3dHR0LOWjPXRdfxjAswAMynrHPT
+ 09ME2TB1MGUBRFZvfLJlVV690ieG3L/eOFswqY9DM7O4uzZ8/KNo8DuNEwjE/mu10Mwzjm8Xja
+ cH7rmyWmaaK/v5+rrWTQy0tVZzIieHv27FkJYNNybcrKyngbTwYwTRN9fX2yD2kMwOsNw7ilUO
+ yzf//+wVgs9mwAXTLtZ2Zm7Lw8mBSQWdCUmTpLu+DFYjHLyUTerJ1+LszbWR2Mc6E5Eb3VMIy7
+ C81OBw8enFQU5VoA3bLTA3bmQpnkKCsrs1zUdGLhwomQtsUhd5VJgdHRUQSD0nnB/0/X9e8Vqq
+ 06OzsniOiFAIZkXiSnTp3iCsppRlEUmXy8Z7S1taW0O8wJwdtt9UPKysq4R9NIOBzG8PCwVFsi
+ utkwjC8Wus10XR8iomsBWOagLC0tSduXSWtYWxyJRC7LtuBdvtw/lpaWcv5dmjl9+rSsB/IbXd
+ ffzxa7KHpHiOi1OL94Y+lB28xpZGwiGQk+I2uC19raWgeLEj1cyj29zMzMyO6RHfb5fG8AwMuO
+ Txa93wKw9HiFEBgYGOBV2zQioxVCiJ1ZE7x4PL7TiR/BJEcsFsPp06elmgK4vqOjg/dNPQ3BYP
+ CTAB6VaGd5iDSTPEVFRTIJyNkTPJkvZ8FLH2NjY1LnUBDRlw3DeJQt9vR0d3fHiOgfAFhurxga
+ GkqlcCqz/DiVqYaePcEjol1Wbbice3qIx+MYGxuTaToQDoc/xxazDG3PEJGlncLhMHt52Q1r17
+ S0tCRdRThVD+9yKxfV6/VyL6aBiYkJ2a1P78mV8k7Zpqqq6is4f/jQsoyMjPBcXvYED0SUtJeX
+ kuAJIS5j7y473p3kDoAHDcP4JVtMjn379kUAvNeq3dLSEnt5aUJyC+qOjAueqqoNAEqtPDzGea
+ ampmTPkP0sW8sehmHsw1+cmnYpeMtZepDUjKaMCx4RbbBqwydCpQdJ7+JRXqhIemxbzuUtLi4m
+ U0WakRA8q333RJR5wRNCWAoeV0hxHtM0pQpUEtHn2VrJoev6rwAcsmrH1ZHT8rKxdJSEEBszLn
+ gALAWPQ9r0eHcSE+aPJRJqmSTf55BIRp6amuLFizQg4ShtSPazOaTNw3CWiL7LlkqNWCz2cwDT
+ Vt62jYINjCQSulGtaVpSFYXTFtJ6PB74fD7uPYfDWYl5I9Pr9d7D1kqNgwcPhonoXqt2SZ7zy6
+ QYGco4XBkNadm7cx5Jb+I3vIXMobhWiDsd6hPGWQ8PQoh1mRa8ZbOd2btzHslClHewpZzBMIwO
+ AH3LtQmFQrJFVxlJJLWjLpOCRwBWLNeAd1g4j8TqbFgI8Ru2lHNOHhH9wsLT4IrIDiOpHUltL0
+ tK8Hbv3l1jdS0LnrOEw2GZZOM/d3V1hdhajoa1lod5s+BlRfAy5+H5fL4VVm0kyrwwNpAsPvkw
+ W8rx8OpRAFEH+oaRRFI7ViTz2UkJnhCiRmKgcM+x4OU8HR0dQViccsY7Lpz38Kx2W8hokGOCJ+
+ NOckjrfEhr9R7y+/2PsaWch4gOLvfvsViMD+121t4y+pE5Dw9AFQteZgmFLKfmhtrb2+fZUmnB
+ smTU0tISW8lhL8+C6kyGtJanbfDB25kVPCI6yVZKGz0seJn38iyoSOZz03acGC9aOEckEpE5la
+ ybLZUehBAnWPAyi8RJh0lpV1IXERGfrJ1hwZN4KHl3Rfo450QfMY56eElVF042pPU4cMOMJDJn
+ zhIRJ4OlCcMwFmBxdi0f7JNxwUtq72qyIa3l7l4+fNs5JB8mXrBIL3OpvpQYeSSmxJJaFU1Wlb
+ iyp8s8PCEE72JPL8vaNxqNsoUyS1kmBc8JhWYc9PA4pM2uh8eFQHNDPzjuzBOEELy1Jb2wfTM7
+ nnNL8HhOI+NvO55mSC/LrgrynHXmo5pMCh6vwWcQmRVvIqpkS6WVChY8V5FUxQYlk1/GpM/DE0
+ LwiUnppdQBL5xxLqRNyunikDYHkCl5TURr2VLpYc+ePSthMYfH1YFyQz+SFTzeR5NhwZMIa9ex
+ pdLmbVjalo8kzThJ1eRKVvAsa42zh+cckocTr2dLZU/w+NCqjHt4SeWdpk3wOBHTWSQ8iI1spb
+ QJXrNVG4nDoxlnBW82mc9Ndi8tC16GKS0ttWpSq2kah7XpYbeVB15SUsJWchAJ/cic4BGR5Zfx
+ ZuqMCx4AaGyptLCsXUtKSniV1lmP2tLDk9EgDmlzmLKyMpmBcjVbyllUVd0MoCnVvmEc9e4y6+
+ EpinLOoZtmJJH0Iq5hSzkLEVnaVNL7ZhwUPCHEeMYEr7OzcxoWqSlcENHxB0/mwVqnqup2tpaj
+ /K0T3jcjj+SBSKOZDGkBYIQFL7NUVFiX8SeiV7GlnOGKK66oEkK8YLk2Ho8H5eVcANxJZLSDiE
+ YyLXijDqg0Y4OqqioZV/8NbCln8Hg818GiKEMgEODq3lkQPCHE2YwKHhGdtYrDuUaYs5SXl8sc
+ X7dJVdUr2VqOYPnyqKzkmg3ZEDwAmRU8IcSwxb+zl+cwRCTl5QF4E1srNVpaWrYJIfZa9Ud1dT
+ Uby2EkdCMei8UyG9ISUZ9Vm3A4zL3nMCtWSB24/g+tra11bK2UXi7vB7BsrFpRUcFbytKAxJGX
+ wwcPHkxKXFLx8PocuHHGJoFAQOYhKxFC3MjWSo7Ey+IfrNrV1fE7JUse3qlkPzutgsceXnrCWh
+ kvTwjxdlVVOV8iubH9HlhUOPZ4PBzOpoFoNGo5h0dEvRkXvKKiolMAYix4mae+vl5mZbAWwAfY
+ WvbYs2fPShnvuLa2lreTZSeclXK2HBe89vZ2E8AQh7SZp7i4GDU1NTJNP5goXslIEovFPg2Lcu
+ 5EhIaGBjZWGpB0kjIf0iY4biV4nJqSHiQfuIp4PP4vbC05NE27DMBbrNrV1NRwOag0EQqFLNsQ
+ 0bFsCd4Ri7clh7Vpory8XDYR+S0tLS2tbDHr50gI8XVYnGhPRFi9ejVbK3uCF66qqjqRLcF73A
+ nFZpJj3bp1MnN5HiL67u7du7kG+fLe3dsAPNeqXW1tLRcLSCOLi5bngx3ft29f0vtWUxI8Ijri
+ wA9gkqSkpAS1tbUyTXd4vd6vssUuKXY7hRD/Yfnm8HjQ2NjIBksTkhHhkVS+IyXBS7iWERa87N
+ HY2Ciz3QxCiLepqvp6ttiT2bt3b6UQ4sewOIYRAFavXs2Jxmn27qzm/GWcrLQJXsK1XHYCcX5+
+ nnsyjfj9fqxbJ13Z/Vuapu1kq51n69atnkgkcieArVZtS0tLsWrVKjZaGpHRCiHEoawJXkJxDy
+ 7376Zp8sJFmqmtrZXdY1smhPi1pmkclwGorKy8BcDLJcY4Nm7cyFVRXCB4Pp/vYFYFD8ABqwYL
+ Cwvcm2mEiLBhwwbZRNhGIcS+Qt9rq6rqRwG8Q6btqlWruMhnBpDQid6Ojo5zqXxHyoInhNjvhH
+ IzqVFUVIT166WPpr1MCPGblpaWmkK0laqq7wDwBZm2JSUlWLNmDQ+wNCMTCRJRV6rfk7Lg+f3+
+ QwCWXZmYm5vjHs0AdXV1squ2EEJcQUQPFZqnp6rqewHcCotKKMD5VdnNmzdDURQeXGlGRiNknK
+ u0C157e3sUQJeVq8ol3zPDhg0b7JyR+ox4PP5IoczpaZr2CQD/KSN2ANDU1MTnzWaIYDBo2YaI
+ /px1wUvcyH4LZWYvL0N4PB5s2rRJKlUlwXYhRKemaXl7pu3u3buLVFW9TQjxGdlrGhoaZGsPMp
+ kRvCWfz/e4KwQPQLsTCs44Q2lpKTZt2mQnFFsthHhE07Tr8s0Wra2tdR6P53cA3ih7TXV1tZ1U
+ HyZFwuGwTKGRrkTBkuwLXiwW+yMsSkWx4GWWQCCApqYmO6kUJUKIe1RV/fquXbvyYu9Ua2vrC+
+ Px+CEAV8leU1FRgebmZk5BySCzs1Jnav/eie9yRPAOHDgwA4v0lFAoxPl4Gaa2thbr16+3+/C+
+ w+/3G6qq7s3V393W1lauqurN8Xh8HwDpbOGysjJs3ryZ69y5L5wFET3kGsGTVeCZmRnu3Qyzcu
+ VKu54eAGwH8CdN0769d+/e2lz6vZqmvdo0zeMA3gPJxYkLnt22bdvg8/l40GQQIYSMh7fo8/n2
+ O/F9jgkeEbHguZS6urpkRI+EEG+JRCJ9mqZ9WtO0gNvDV1VV/5zYF2tr1bmyshJbt261s9DDOM
+ Tc3Byi0ahVs0edmL8DLGp/2SEej/+JiEJY5iyAYDCIWCzGIUOWRM/v96O3t1dmgD1JD4QQnwJw
+ o6Zp/0lE3+zs7JxwSeiqRCKRFwkhborH489K1i4bNmzgXLssMT09LdPsQae+z7Fe7urqCll5ef
+ F4nBcvskggEMD27duTrdZbLYT4TDweH1RV9Y7W1lY1i2FrQFXV95qmeUII8QAA22JHRFi7di02
+ btzIYpdFZKI+IcT9rhO8xI39j1Wbqakp7uUsUlJSgh07dkjvyHgaigDcEI/HdVVV79i9e3dGk9
+ VUVX2tEKIX5xOINyfzGX6/H9u2bePKxVkmFArJpKP0dXV1nXDqOx0VPCL6HwDCyoWNx+Pc21nE
+ 6/WiubkZGzduTHV64Qav13sPbCwOpCh2VwP4Ec6fyJYU1dXV2LlzJyorK3kgZJnJyUmZZr9y8j
+ sdFTxd10cAPLZcm1gsJpt3w6SZurq6lB9+IcQLNE17aYZu+eZkxdXj8aCpqQlbtmzhxYkcEjwi
+ +qVrBe8vvDwnlJ3JAEVFRdi2bRuampqSFgIhxJ503+cVV1xRBWBbMteuWLECO3fuRH19PXe4S1
+ hYWJAJZ2eqqqr+5GrBA3CP5a+YmUEsFuNedwlEhPr6euzatUv2kO+nXp/2szg9Ho+w690VFxdj
+ 27Zt2LRpE4qK+AyjHPTufp7KgT0ZETxd148DOGwV1nJOnvvwer1oamrCjh07UF1dbefSgXTfm6
+ 7rswB6ZL3WDRs2YOfOnQgEAtyxLkMIIbV4KYS42+nvTtd6vOWNjo+Pc8+7lLKyMmzZskX2hK5Z
+ r9d7Z4Zu7UcyjS6//HKsXLmS001cyuzsrMw203G/3/97p787LSNCCGEZ1s7NzcnE8EwWkTlxjo
+ ju7ujoyFRH3gaLLAAigmma3Hku5tw5qSrtP03U2nS/4HV1dZ0C0Gnl1kr+cCYLRCIR2aTQ2zN1
+ T4Zh9AN4lMdV7mKaptTuCiL6YTq+P20+PxHdJqP0VudQMtlhampKJl+y2zCMP2f41r7P4yp3mZ
+ yclBlXPbqut6fj+9M5yfEjWJx1EQ6HefHCpUxMSG2XvS3T9+X3+38CYMFqXPEWRvchhJAdV9+z
+ mrpwneAlVtV+atWOFy/cx+LiosyReXEiuivT99be3j5PRJbjisNa9xEMBhEKhayaRYnojnTdQ7
+ qXsb5r1WB2dlZqcpxxnXf3oK7rQ1nyFCzD2unpac71dBmjo6MyzR5I7NjKPcEzDOOPAE5aublj
+ Y2M8GlwUdsjkSBHR7dm6R7/f/0cAp5drE4vFeEePi1haWpLaUkpE307nfaTbwxMAvmbVaHJy0m
+ 6NNiZNzM7OyqR1zMbj8V9k6x7b29vjAO6QGVeMe7w7iYWkMz6f71fpvI+0Z2YmVmvnrd7GPJfn
+ DmTmvojo7q6urlA27zORDrPsE8S5nu4gEonIjqtbEi+z3BU8XdfniMhyzmVsbIzLRrlgYMrkSA
+ kh7sj2vSZyPTknLwcYHx+XmU+dj8Vi30n3vWRk7w0R/ReAZdXMNE328rKMjdy7Dpfc8m0yHivn
+ 5GWPWCwmO0d/Z+L0w9wXvM7Ozl5IFPI7e/Yse3nuD2fvcMv9EhHn5OWAdxeJWBY8iRPRLZm4n0
+ zurv6SVQPTNHmiOUssLi5ifn7ecmBCYrEgUySmSzgnz6XE43HZVJRfJKos5Y/gJcKgR6zaDQ8P
+ s5eXBSRfNFnLvUslrOWcvOx5d5KFHP41U/eU0fo5RPQFqzbhcFg28ZVxCBuT+3e47d59Pt8fwD
+ l5riMWi+Hs2bNSL1HDMLryUvB0XX8QgGHVbmRkhN/IGUQ2904I8XO33XsijcGyHh8LXmYZHR2V
+ 9e4+n8n7yniFRCL6nFUb0zR590UGkfTu7s127t0ycE6ei4hEIrJzd48ahvGHvBY8XdfvA2BZUm
+ h0dJR3X2RocEqe/n6bW3+DYRh9kMjJ46mSzDAyMiL17BLR/8v0vWWrBvbHZR7E4eFhHj1pRrI+
+ 2UkX5d5d6uG5Xea3ck5eellaWpLKpyWi+3Vd/1Om7y8rgmcYxu8BPGjVbmxsjMOQDAieE2LiAn
+ 4MzsnLOkNDQzIvUBGPxz+RjfvL2iknRPRxWMy7CCFw5swZHkVpIhdz7y6FrutzAH7mhMAzyREM
+ BmXte29XV9djBSV4uq7rRGR52M/09LRUWRkmPd4dgN+5MPfuUtxm1WBqaoozANKADeckDOBj2b
+ rPbJ9j9xEAlit/AwMDnIychgEquTp7e678Jr/f/wgkcvJk6v0x9hgfH5epkg0ANycOYyo8wdN1
+ /QyAf7dqFwqFZJMYGUkkc++Cbsy9uxTt7e1SZed5q5mzmKaJoSGpIGDU5/N9IZv36oaTir8EwH
+ I5dmRkhBcwHETyob/Hxbl3l/JcbwPn5GWUM2fOyKaQfbyjoyOrq0ZZFzzDMBYAfNiqXTwex8DA
+ AI8uB4hGozmfe7fMeJLKyWMvzxlmZmZkV/oP+P3+72f7fhWXDNIfEdFDMsblgZo6smeDuj33bh
+ ksV5W5Tl7qxGIxnD59WqqpEOLt6a5mnDOCd/6lK94BwDLOGBgYQDgc5tGW5nBW5iB1t0JE90Ii
+ J29ubo4HQwoMDg7KPou3ZrJAQC4IHgzD6JGpphKNRnH69Gl+OydJKBSSyr1TFOXOXP2NiZw8y8
+ UWjhaSJxgMylYoH0rk3LoCxU1GjEajXwZgWQhwZmaGy8Gn0bsD8ND+/fsHc/ynWs4XcU5e0s8p
+ Tp06Jet03Jh4AbHgPZWDBw+GAbwZgOUoHBwc5AO87c8byArebbn+WxM5ecuucnFOXnL09/fLhr
+ I/MQzDVWlNituMaRjGn4noZqt2sVgMfX19nJBsA9ncO9M0f5HrvzWRk2cZlnNYa4/x8XHZl8Q4
+ gH922/0rbjSq1+v9OIBjVu0WFxc5VcX5cPaeQ4cO5YXrzDl5zmLneSOidxqG4bp5J1cKXkdHx5
+ KiKP8IwDKb0cYbp6CxkXt3e7785kROXrtDYX5BE4vF0NvbKxtR/VDX9Z+68XcobjVwZ2enAcnD
+ Pfr7+xEKhXhULkMB5N5dittkPF9e9XfsGTsrhHi3W3+H4mYj+/3+zwF4XMZ76enp4QrJKYazid
+ y7vHryEzl5y4bonJO3PKOjo7KVdQQRvbWrq8u1IZerBa+9vd0UQlwPiyRS4Hx+GefnXdo2knXv
+ 7sq33y5bJ4/D2qcnGAzaqUl5q67rv3Lz71HcbvCurq4TAN4lG7aNjIzwKE3uYX4oUb0m70gsXi
+ wL5+Q9vefb29sr60Q8FovFPuj236TkguENw7gNwA9l2g4PD8tOzhcEQgjZzd135KsNioqKHgaw
+ rJhzTt5f2+PkyZOIRCIyzeeJ6PpEHi0LnhP4fL53AOiTecD7+vpkQriCYHZ2ViZJNBgOh3+Wrz
+ ZI5ORJFRRg/vcZkk3sJ6J36rrenQu/zZsrndDR0RFUVfXVOJ9mUGr1durt7cVll12GoqKigh68
+ kpPNeZN7t8xDebsQ4iYAdKk2c3NzCIfDBT9mzpw5YydK+m9d15+U4O3m4zCVXOoIwzAeB/BW2f
+ mHkydPFvTKbTQalQrT8jmcvUBnZ2cvJHLyCv3s2rGxMdlDtAFgv9/vf3cu/T4l1zrEMIwfyGw9
+ A85nhttIlsw7pqampHLvdF1vLxCTWCZVF3JO3tTUlJ2dS2NE9Jr29nYzl36jkosdU1VV9WEAf5
+ RpOzs7i76+voIcxJLeyh3Is9y7S+Hz+Tgnz5nnJEJE1+XQaXa5LXj79u2LKIryagC9dt5chSR6
+ srl3hRDOXiBxngLn5D2F+fl59PT02ImE3qPr+h9z8bcqudpJnZ2dEwD+DoDUobVjY2MYHBwsmE
+ Es6d39Pl9z7y454BXFMqwtpJy8UCiEkydP2vm9XzIM4xs52/+53FmGYRwD8EoAUvMIZ8+exfDw
+ cN4PYiGE7GLF7SgwvF7v78E5eQCApaUlnDhxQjbXDgDuCwaDN+X0Cy/XO80wjN8T0btk2w8NDe
+ X9boxgMFjwuXeXQrZOnmQ6T86LnUR9xItBlWma13d3d+e066vkQ+fpuv5tAJ+RbT84OJjXnp5k
+ oYB78z33bhkP+HZYLNRIvjRyWuxs/L5eRVFelg/jRcmXTjQM41MApOcWhoaG8lL0ZHPvkEd175
+ IYKz0o0Jy8UChkV+yGFEV5cWLOPOdR8qkzg8Hgu4noB3ZEb3BwMK9WbyXr3vUWUO7dpTzcgju7
+ dnFxEcePH7cjdrNEdG0iaTsvyCvB6+7ujvl8vv8L4Ley14yMjORVWSnJlArLkC7f8Xq996CAcv
+ Lm5uZw/PhxOwsUiwCu1XX9SD71u5JvA7m9vd00TfOVAH4ve834+DhOnTqV8zsyOPdOnkLKyZud
+ nUV3d7edbZaLRPR/8rD6df4JHgAcOnRo0TTNlwH4gx3PyGY+Uk56d0T0cKHl3qUS1uZ6Tt7k5K
+ TdcR0C8Apd1x/Mxz5X8nUwJ0TvWkhuQbvwJjx27FhOrs7J1r1DHpw56xQ+n+8hSOTk5Wp9xZGR
+ EbtHmS4R0SsMw9iXr32u5POATiyj2xK9xcVFHDt2DAsLCzn1WyXTKOYKMfdumekPqbL2uRbWCi
+ HQ399vd0Hugmf323zucyXfB7VhGAumab6YiO6XvcY0TRw/fhwzMzP5Fs4WbO6dhcebNzl50WgU
+ 3d3dGB+3dSRsEMC1hmE8kO+drRTCiD506NCiz+d7JYB7ZK+5UOJ6eHjY9Su4NnLvOJz96xdiDx
+ F1WHlMueDlLS4u4ujRo5idnbVz2SSAFxiG8Ugh9LdSKAO7vb3dDAaDrwfw33ZCg6GhIfT29rp6
+ 4loy966v0HPvlsEyCXtiYsLVL77p6WkcO3YMS0tLdi4bIaJnGYbRVSgdrRTSqO7u7o4ZhvF2Iv
+ qcneumpqZw/Phxu4PJVeGsTOhWqORyTt6Fl3JPT4/dl/JxRVHadF0/Xkh9rRTg+Ba6rn+CiN4M
+ QDoLc2FhAUePHnVdFQ0bZ87eCeZpSeTk/cKhF0vGME0z2WmXRxRFaevs7DxdaH2tFOog13X9ew
+ BeivMTtlJEo1H09PTg9OnTrklSlnwIHzEMYwDMJSGi22Q8fbdMbQSDQRw9ejSZhbW7/H7/NZ2d
+ nQV5lqlSyIPcMIx9RHQVAFtiMDY2lsx8SVrCGckzZ7/PkrY8iZy8ZSvEuqFOnhACw8PDdks7IT
+ Gd8RnDMN6Qa+dQsOA56+kdURRFhY1dGRdC3CeeeMLu8r/jb3mZ3DshxM9Z0pYnkZPn6jp5F8o6
+ DQ0N2Q1h54noukRFoYKex1V4qJ8vF19dXf0iALfauS4Wi6G/vx8nT560+7bNWDhLRPcahrHAvS
+ yFZVibrZy88fFxPPHEEwgGg3YvPQWgTdf1n3D3suBdZN++fRHDMN6VWMywNaKnp6dx5MiRjE5q
+ 28i9u4N7V3qKoweAq3LywuEwuru70d/fb3v+kIgeisVimmEYh7l3WfAuFeJ+D8BVAHrsClBfXx
+ +6u7sz4gHYyL17lHvVlkhYenmZqJMnhMDo6CiOHDmSzMJEHMBnZ2dnrzl48OAk9yoLntWbvsvn
+ 87UQ0Y/tXjszM4MjR45gdHQ0rQ+FjZ0VnHtng0ROXmi5NktLSzKpQElzoVDnwMBAMqvCo0KIqw
+ 3D+GSunz/BgpdBOjo6grquXwfgnQBsLcfGYjEMDAwkO+diSSgUkvncuKIod3FP2u93AJaLPOko
+ /x6NRi+Om2SSnInoIQDP7Orqeoh7kgUvWW/vGx6PRwVwKNk3dU9Pj6NhrmzuXSEmljpERs+uFU
+ JgbGwMhw4dSjYyCBPRh30+39WGYYxy97HgpcT+/fuf8Pv9GoAvAbA9yqempnD48GEMDQ2l/JDY
+ yL27jXsuOfx+/+8ADFl58U7k5F1IID59+rSdisR/ySGPx9Oi6/q/JVJrGBa81GlvbzcNw/gogO
+ cA6LN7fTwex/DwMB5//HGMjo4mvVPDRu4d171Lvq/jkFjdTmW1dmFhASdOnMDx48eTrb0YA/Al
+ v9+v7d+//wnuNRa8dIW47QB2EdHXkcSCwIV5msOHDydVgUNy7ujHnHuX4oOhKJZh7dzcnO2piq
+ WlJfT29iZTxukv6QbwLMMwPlrIuyZY8DInegu6rv9zwtvrTuYzwuEwTp06dTF/T0b4otGoVLlx
+ Irqdeyk1Ojs7T8LBnLxQKIS+vj4cPnwYk5OTya7gRwB8QQjxTMMw/sy9xIKXaeF7NBaL7QLwWQ
+ BJvWn/8kEYHx9fNtSdmpri3LsMIvPisHpZXehfOy+2S6ADaDEM46aurq4Q9w4LXlY4ePBg2DCM
+ T3o8niusPAKrUKe/vx+HDx/G6Ojo0y5uSC5WFPyZsw6SdE7e3NwcTp486YTQzQN4r9/vv5J3TD
+ jwEkumI+rq6thyl7CnpmlvEEL8K4BVqXyQx+NBXV0dVq5cieLiYoRCIRw+bDne44qiNHM6inOo
+ qvoDAH+/XJv6+no0NTVdXEEfGxtzIjFZALhLCPGRrq6us7lks3TkKLLguZi2trZy0zQ/BeBGAP
+ 4UwyrU1NQgHA7LPEQPG4bxfO4B59A07RohhOXhNqtWrcLk5KQjRSSI6ACAd+m6vj8XbeZmweOQ
+ Ng20t7fPG4bxIUVRdkAia3/Z13zCa5D0GG5j6zuLz+d7EBY5eQBw9uxZJ8RumIje7PP5tFwVO7
+ fDgpdGOjs7ew3DeCURXQkbZ+MmyRyAn7LVHX95SeXkpcgMgI8IITbruv49TiBmwctpdF3fbxjG
+ c4joJQDSMvFMRD/h3Lv0QETpErwQgC/F4/EmwzC+zKuvLHj5Jny/9vv9zwTwKgCPO/zxHM6mr9
+ +6kcIK/NOwAODfPR5Pk2EYHz1w4MAMWzlDLy9etMie7TVNe5kQ4iYAWoqfdcowjE3gdJS0oarq
+ 2wB8M8WPCQL4mqIoN3d2dk7kq6140YJ5OoSu6/cZhtEK4BoAv01BsDj3Lv1h7d2wyMlbhhEiuk
+ kI0WQYxk35LHbs4TF2vIjtAN4L4AYAxZKXce5d5vrnhwCut3HJQSK62efz3VNIe145D4+xxd69
+ e2sjkcjbALwZQJOF5/H1xL5eJs20tLTsSOTIFS3TzMT5Q72/YRjGI4VoJxY8Jina2toU0zRfSE
+ RvFUK8HIDvKU0eIaKX67o+x9bKDJqmvVEI8XUApU/5px4i+rYQ4nbDMMbZUu4UVha8HGHPnj0r
+ Y7HY+wDUEdGSEOJRwzDuAc/dZSO03QzgeiK6TAhxGsCDhmE8zH2Rp4LHMAyTi/AqLcMwLHgMwz
+ AseAzDMCx4DMMwLHgMwzAseAzDMCx4DMMwLHgMwzAseAzDMCx4DMMwLHgMw7DgMQzDsOAxDMOw
+ 4DEMw7DgMQzDsOAxDMOw4DEMw7DgMQzDsOAxDMNI8/8HAE8wFAC1H96IAAAAAElFTkSuQmCC
+END:VEVENT
+
+BEGIN:VEVENT
+CREATED:20130718T073555Z
+UID:B968B885-08FB-40E5-B89E-6DA05F26AA79
+URL;VALUE=URI:http://en.wikipedia.org/wiki/Swiss_National_Day
+DTEND;VALUE=DATE:20130802
+TRANSP:TRANSPARENT
+SUMMARY:Swiss National Day
+DTSTART;VALUE=DATE:20130801
+DTSTAMP:20130718T074538Z
+SEQUENCE:2
+DESCRIPTION:German: Schweizer Bundesfeier\nFrench: Fête nationale Suisse
+ \nItalian: Festa nazionale svizzera\nRomansh: Fiasta naziunala Svizra
+END:VEVENT
+
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/dummy.ifb	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,13 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//kolab.org//NONSGML Kolab Server 3//EN
+METHOD:PUBLISH
+BEGIN:VFREEBUSY
+ORGANIZER:MAILTO:nobody@kolabsys.com.ifb
+DTSTAMP:20130824T135913Z
+DTSTART:20130629T135913Z
+DTEND:20131214T135913Z
+COMMENT:This is a dummy vfreebusy that indicates an empty calendar
+FREEBUSY:19700101T000000Z/19700101T000000Z
+END:VFREEBUSY
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/escaped.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,25 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML KOrganizer 4.11.3.0//EN
+VERSION:2.0
+X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:4113ab9d-ffbb-aa00-c372-deb93de6b539
+ORGANIZER;CN="Organizor":MAILTO:organizor@example.org
+DTSTAMP:20140225T160532Z
+ATTENDEE;CN="Master of Desaster";RSVP=TRUE;PARTSTAT=NEEDS-ACTION;
+ ROLE=REQ-PARTICIPANT;X-UID=100156408:mailto:master@desaster.com
+ATTENDEE;CN="Doe, John";RSVP=TRUE;PARTSTAT=NEEDS-ACTION;
+ ROLE=REQ-PARTICIPANT;X-UID=115484392:mailto:doe@example.com
+ATTENDEE;CN="Kolab\, Thomas";RSVP=TRUE;PARTSTAT=NEEDS-ACTION;
+ ROLE=REQ-PARTICIPANT;X-UID=115936264:mailto:thomas@kolab.org
+CREATED:20140225T160335Z
+LAST-MODIFIED:20140225T160335Z
+DESCRIPTION:Me\, meets Them\nThem\, meet Me
+SUMMARY:Meeting w/Them
+LOCATION:House\, Street\, Zip Place
+DTSTART:20140305T150000Z
+DTEND:20140305T170000Z
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/freebusy.ifb	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,25 @@
+BEGIN:VCALENDAR
+PRODID:Libkolab-0.4.2
+VERSION:2.0
+METHOD:PUBLISH
+BEGIN:VFREEBUSY
+CREATED:20130824T140042Z
+ORGANIZER:MAILTO:somebody@somedomain.com
+DTSTART:20130824T123016Z
+DTEND:20131122T140026Z
+FREEBUSY;FBTYPE=FREE:20130826T110000Z/20130826T150000Z
+FREEBUSY:20130826T110000Z/20130826T150000Z
+FREEBUSY:20130826T110000Z/20130826T150000Z
+FREEBUSY:20130827T100000Z/20130827T160000Z
+FREEBUSY:20130828T100000Z/20130828T120000Z
+FREEBUSY:20130828T100000Z/20130828T120000Z
+FREEBUSY:20130830T090000Z/20130830T093000Z
+FREEBUSY:20130830T093000Z/20130830T100000Z
+FREEBUSY:20130930T070000Z/20130930T160000Z
+FREEBUSY:20131104T113000Z/20131104T160000Z
+FREEBUSY;FBTYPE=OOF:20131104T113000Z/20131104T160000Z
+FREEBUSY;FBTYPE=BUSY-TENTATIVE:20131104T113000Z/20131104T160000Z
+FREEBUSY;FBTYPE=FREE:20131104T113000Z/20131104T160000Z
+FREEBUSY;FBTYPE=BUSY:20131104T113000Z/20131104T160000Z
+END:VFREEBUSY
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/invalid-dates.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,15 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 5.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:00001231T000000Z
+LAST-MODIFIED:20130755
+UID:C968B885-08FB-40E5-B89E-6DA05F26AAFF
+TRANSP:TRANSPARENT
+SUMMARY:Swiss National Day
+DTSTART;VALUE=DATE:20130801
+DTEND;VALUE=DATE:20130802
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/invalid-event.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,14 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 5.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20130917T000000Z
+LAST-MODIFIED:20130755
+UID:C968B885-08FB-40E5-B89E-6FA05F26AACC
+TRANSP:TRANSPARENT
+SUMMARY:Event with no end date nor duration
+DTSTART;VALUE=DATE-TIME:20131001T120000Z
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/invalid.txt	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,2 @@
+Some text file that has nothing to do with the iCal format.
+Just to test the sanity checks before attempting to parse iCal files.
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/itip.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,151 @@
+BEGIN:VCALENDAR
+PRODID:-//K Desktop Environment//NONSGML libkcal 4.3//EN
+VERSION:2.0
+X-KDE-ICAL-IMPLEMENTATION-VERSION:1.0
+METHOD:REQUEST
+BEGIN:VEVENT
+ORGANIZER;CN="Rolf Test":MAILTO:rolf@mykolab.com
+DTSTAMP:20130628T190056Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;
+ DELEGATED-FROM=carl@mykolab.com;X-UID=208889384:mailto:rolf2@mykolab.com
+ATTENDEE;RSVP=FALSE;PARTSTAT=DELEGATED;ROLE=NON-PARTICIPANT;CUTYPE=INDIVIDUAL;
+ DELEGATED-TO=rolf2@mykolab.com:mailto:carl@mykolab.com
+CREATED:20130628T190032Z
+UID:ac6b0aee-2519-4e5c-9a25-48c57064c9f0
+LAST-MODIFIED:20130628T190032Z
+SUMMARY:iTip Test
+ATTACH;VALUE=BINARY;FMTTYPE=text/html;ENCODING=BASE64;
+ X-LABEL=calendar.html:
+ PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBYSFRNTCAxLjAgVHJhbnNpdGlvbm
+ FsLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL1RSL3hodG1sMS9EVEQveGh0bWwxLXRyYW5zaXRp
+ b25hbC5kdGQiPgo8aHRtbD48aGVhZD4KICA8bWV0YSBodHRwLWVxdWl2PSJDb250ZW50LVR5cG
+ UiIGNvbnRlbnQ9InRleHQvaHRtbDsgY2hhcnNldD1VVEYtOCIgLz4KICA8dGl0bGU+S2FsZW5k
+ ZXI8L3RpdGxlPgogIDxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+CiAgICBib2R5IHsgYmFja2dyb3
+ VuZC1jb2xvcjp3aGl0ZTsgY29sb3I6YmxhY2sgfQogICAgdGQgeyB0ZXh0LWFsaWduOmNlbnRl
+ cjsgYmFja2dyb3VuZC1jb2xvcjojZWVlIH0KICAgIHRoIHsgdGV4dC1hbGlnbjpjZW50ZXI7IG
+ JhY2tncm91bmQtY29sb3I6IzIyODsgY29sb3I6d2hpdGUgfQogICAgdGQuc3VtIHsgdGV4dC1h
+ bGlnbjpsZWZ0IH0KICAgIHRkLnN1bWRvbmUgeyB0ZXh0LWFsaWduOmxlZnQ7IGJhY2tncm91bm
+ QtY29sb3I6I2NjYyB9CiAgICB0ZC5kb25lIHsgYmFja2dyb3VuZC1jb2xvcjojY2NjIH0KICAg
+ IHRkLnN1YmhlYWQgeyB0ZXh0LWFsaWduOmNlbnRlcjsgYmFja2dyb3VuZC1jb2xvcjojY2NmIH
+ 0KICAgIHRkLmRhdGVoZWFkIHsgdGV4dC1hbGlnbjpjZW50ZXI7IGJhY2tncm91bmQtY29sb3I6
+ I2NjZiB9CiAgICB0ZC5zcGFjZSB7IGJhY2tncm91bmQtY29sb3I6d2hpdGUgfQogICAgdGQuZG
+ F0ZSB7IHRleHQtYWxpZ246bGVmdCB9CiAgICB0ZC5kYXRlaG9saWRheSB7IHRleHQtYWxpZ246
+ bGVmdDsgY29sb3I6cmVkIH0KICA8L3N0eWxlPgo8L2hlYWQ+PGJvZHk+CjxoMT5LYWxlbmRlcj
+ wvaDE+CjxoMj5KdW5pIDIwMTM8L2gyPgo8dGFibGUgYm9yZGVyPSIxIj4KICA8dHI+PHRoPk1v
+ bnRhZzwvdGg+PHRoPkRpZW5zdGFnPC90aD48dGg+TWl0dHdvY2g8L3RoPjx0aD5Eb25uZXJzdG
+ FnPC90aD48dGg+RnJlaXRhZzwvdGg+PHRoPlNhbXN0YWc8L3RoPjx0aD5Tb25udGFnPC90aD48
+ L3RyPgogIDx0cj4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPj
+ x0ZCBjbGFzcz0iZGF0ZSI+Mjc8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48
+ L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMC
+ I+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjg8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+
+ PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcm
+ Rlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjk8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249
+ InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYm
+ xlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MzA8L3RkPjwvdHI+PHRyPjx0ZCB2
+ YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcC
+ I+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MzE8L3RkPjwvdHI+PHRy
+ Pjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ2
+ 49InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MTwvdGQ+PC90
+ cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIH
+ ZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlaG9saWRh
+ eSI+MjwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3
+ RkPgogIDwvdHI+CiAgPHRyPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIw
+ Ij48dHI+PHRkIGNsYXNzPSJkYXRlIj4zPC90ZD48L3RyPjx0cj48dGQgdmFsaWduPSJ0b3AiPj
+ wvdGQ+PC90cj48L3RhYmxlPjwvdGQ+CiAgICA8dGQgdmFsaWduPSJ0b3AiPjx0YWJsZSBib3Jk
+ ZXI9IjAiPjx0cj48dGQgY2xhc3M9ImRhdGUiPjQ8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249In
+ RvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxl
+ IGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+NTwvdGQ+PC90cj48dHI+PHRkIHZhbG
+ lnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48
+ dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj42PC90ZD48L3RyPjx0cj48dG
+ QgdmFsaWduPSJ0b3AiPjwvdGQ+PC90cj48L3RhYmxlPjwvdGQ+CiAgICA8dGQgdmFsaWduPSJ0
+ b3AiPjx0YWJsZSBib3JkZXI9IjAiPjx0cj48dGQgY2xhc3M9ImRhdGUiPjc8L3RkPjwvdHI+PH
+ RyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxp
+ Z249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+ODwvdGQ+PC
+ 90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRk
+ IHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlaG9saW
+ RheSI+OTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48
+ L3RkPgogIDwvdHI+CiAgPHRyPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPS
+ IwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMDwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9w
+ Ij48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm
+ 9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMTwvdGQ+PC90cj48dHI+PHRkIHZhbGln
+ bj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dG
+ FibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMjwvdGQ+PC90cj48dHI+PHRk
+ IHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG
+ 9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xMzwvdGQ+PC90cj48
+ dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbG
+ lnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4xNDwvdGQ+
+ PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPH
+ RkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4x
+ NTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj48L3RkPjwvdHI+PC90YWJsZT48L3RkPg
+ ogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJk
+ YXRlaG9saWRheSI+MTY8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPj
+ wvdGFibGU+PC90ZD4KICA8L3RyPgogIDx0cj4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxl
+ IGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MTc8L3RkPjwvdHI+PHRyPjx0ZCB2YW
+ xpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+
+ PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MTg8L3RkPjwvdHI+PHRyPj
+ x0ZCB2YWxpZ249InRvcCI+PHRhYmxlPiAgPHRyPgogICAgPHRkPiZuYnNwOzwvdGQ+PHRkPiZu
+ YnNwOzwvdGQ+CiAgICA8dGQgY2xhc3M9InN1bSI+CiAgICAgIDxiPnRlcm1pbmJldHJlZmYxPC
+ 9iPgogICAgPC90ZD4KICA8dGQ+CiAgICAmbmJzcDsKICA8L3RkPgogIDx0ZD4KICAgICZuYnNw
+ OwogIDwvdGQ+CiAgPC90cj4KPC90YWJsZT48L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPH
+ RkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4x
+ OTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj4mbmJzcDs8L3RkPjwvdHI+PC90YWJsZT
+ 48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNs
+ YXNzPSJkYXRlIj4yMDwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj4mbmJzcDs8L3RkPj
+ wvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFibGUgYm9yZGVyPSIw
+ Ij48dHI+PHRkIGNsYXNzPSJkYXRlIj4yMTwvdGQ+PC90cj48dHI+PHRkIHZhbGlnbj0idG9wIj
+ 4mbmJzcDs8L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGlnbj0idG9wIj48dGFi
+ bGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlIj4yMjwvdGQ+PC90cj48dHI+PHRkIH
+ ZhbGlnbj0idG9wIj4mbmJzcDs8L3RkPjwvdHI+PC90YWJsZT48L3RkPgogICAgPHRkIHZhbGln
+ bj0idG9wIj48dGFibGUgYm9yZGVyPSIwIj48dHI+PHRkIGNsYXNzPSJkYXRlaG9saWRheSI+Mj
+ M8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+Jm5ic3A7PC90ZD48L3RyPjwvdGFibGU+
+ PC90ZD4KICA8L3RyPgogIDx0cj4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj
+ 0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+MjQ8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRv
+ cCI+PHRhYmxlPiAgPHRyPgogICAgPHRkIHZhbGlnbj0idG9wIj4xNTowMDwvdGQ+CiAgICA8dG
+ QgdmFsaWduPSJ0b3AiPjAwOjMwPC90ZD4KICAgIDx0ZCBjbGFzcz0ic3VtIj4KICAgICAgPGI+
+ dGVzdDwvYj4KICAgIDwvdGQ+CiAgPHRkPgogICAgJm5ic3A7CiAgPC90ZD4KICA8dGQ+CiAgIC
+ AmbmJzcDsKICA8L3RkPgogIDwvdHI+CjwvdGFibGU+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4K
+ ICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZG
+ F0ZSI+MjU8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+Jm5ic3A7PC90ZD48L3RyPjwv
+ dGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPj
+ x0ZCBjbGFzcz0iZGF0ZSI+MjY8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+Jm5ic3A7
+ PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcCI+PHRhYmxlIGJvcm
+ Rlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjc8L3RkPjwvdHI+PHRyPjx0ZCB2YWxpZ249
+ InRvcCI+Jm5ic3A7PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ249InRvcC
+ I+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjg8L3RkPjwvdHI+PHRy
+ Pjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZCB2YWxpZ2
+ 49InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZSI+Mjk8L3RkPjwv
+ dHI+PHRyPjx0ZCB2YWxpZ249InRvcCI+PC90ZD48L3RyPjwvdGFibGU+PC90ZD4KICAgIDx0ZC
+ B2YWxpZ249InRvcCI+PHRhYmxlIGJvcmRlcj0iMCI+PHRyPjx0ZCBjbGFzcz0iZGF0ZWhvbGlk
+ YXkiPjMwPC90ZD48L3RyPjx0cj48dGQgdmFsaWduPSJ0b3AiPjwvdGQ+PC90cj48L3RhYmxlPj
+ wvdGQ+CiAgPC90cj4KPC90YWJsZT4KPGgxPkF1ZmdhYmVubGlzdGU8L2gxPgo8dGFibGUgYm9y
+ ZGVyPSIwIiBjZWxscGFkZGluZz0iMyIgY2VsbHNwYWNpbmc9IjMiPgogIDx0cj4KICAgIDx0aC
+ BjbGFzcz0ic3VtIj5BdWZnYWJlPC90aD4KICAgIDx0aD5Qcmlvcml0w6R0PC90aD4KICAgIDx0
+ aD5BYmdlc2NobG9zc2VuPC90aD4KICAgIDx0aD5Gw6RsbGlna2VpdHNkYXR1bTwvdGg+CiAgIC
+ A8dGg+T3J0PC90aD4KICAgIDx0aD5LYXRlZ29yaWVuPC90aD4KICA8L3RyPgo8dHI+CiAgPHRk
+ IGNsYXNzPSJzdW0iPgogICAgPGEgbmFtZT0iYzJmZjM1NGQtMjhlYi00YjBmLWIwYWItZDQxMT
+ g1Y2JlNGM3Ij48L2E+CiAgICA8Yj5UZXN0YXVmZ2FiZTE8L2I+CiAgICA8cD5BdWZnYWJlbmlu
+ aGFsdDE8L3A+CiAgICA8ZGl2IGFsaWduPSJyaWdodCI+PGEgaHJlZj0iI3N1YmMyZmYzNTRkLT
+ I4ZWItNGIwZi1iMGFiLWQ0MTE4NWNiZTRjNyI+VGVpbGF1ZmdhYmVuPC9hPjwvZGl2PgogIDwv
+ dGQ+CiAgPHRkPgogICAgMAogIDwvdGQ+CiAgPHRkPgogICAgMCAlCiAgPC90ZD4KICA8dGQ+Ci
+ AgICAyNS4wNi4yMDEzCiAgPC90ZD4KICA8dGQ+CiAgICAmbmJzcDsKICA8L3RkPgogIDx0ZD4K
+ ICAgICZuYnNwOwogIDwvdGQ+CjwvdHI+Cjx0cj4KICA8dGQgY2xhc3M9InN1bSI+CiAgICA8YS
+ BuYW1lPSI5MTc4NjRkNi1lN2ZiLTQwYTEtODEzOS05ODU3MzM0MDI3ZGYiPjwvYT4KICAgIDxi
+ PlRlc3RhdWZnYWJlNDg8L2I+CiAgPC90ZD4KICA8dGQ+CiAgICAwCiAgPC90ZD4KICA8dGQ+Ci
+ AgICAwICUKICA8L3RkPgogIDx0ZD4KICAgIDI2LjA2LjIwMTMKICA8L3RkPgogIDx0ZD4KICAg
+ ICZuYnNwOwogIDwvdGQ+CiAgPHRkPgogICAgdGVzdDQ4CiAgPC90ZD4KPC90cj4KICA8dHI+Ci
+ AgICA8dGQgY2xhc3M9InN1YmhlYWQiIGNvbHNwYW49IjYiPjxhIG5hbWU9InN1YmMyZmYzNTRk
+ LTI4ZWItNGIwZi1iMGFiLWQ0MTE4NWNiZTRjNyI+PC9hPlRlaWxhdWZnYWJlbiB2b246IDxhIG
+ hyZWY9IiNjMmZmMzU0ZC0yOGViLTRiMGYtYjBhYi1kNDExODVjYmU0YzciPjxiPlRlc3RhdWZn
+ YWJlMTwvYj48L2E+PC90ZD4KICA8L3RyPgo8dHI+CiAgPHRkIGNsYXNzPSJzdW0iPgogICAgPG
+ EgbmFtZT0iYWFlNzA2NDItNTRlMy00MTdhLTg1OGEtZmFiYjcwZTBhYmZiIj48L2E+CiAgICA8
+ Yj5VbnRlcmF1ZmdhYmU8L2I+CiAgPC90ZD4KICA8dGQ+CiAgICAwCiAgPC90ZD4KICA8dGQ+Ci
+ AgICAwICUKICA8L3RkPgogIDx0ZD4KICAgIDI1LjA2LjIwMTMKICA8L3RkPgogIDx0ZD4KICAg
+ ICZuYnNwOwogIDwvdGQ+CiAgPHRkPgogICAgJm5ic3A7CiAgPC90ZD4KPC90cj4KPC90YWJsZT
+ 4KPHA+RGllc2UgU2VpdGUgd3VyZGUgZXJzdGVsbHQgdm9uIFJvbGYgVGVzdCAobWFpbHRvOnJv
+ bGZAbXlrb2xhYi5jb20pIG1pdCBLT3JnYW5pemVyIChodHRwOi8va29yZ2FuaXplci5rZGUub3
+ JnKTwvcD4KPC9ib2R5PjwvaHRtbD4K
+DTSTART:20130703T183000Z
+DTEND:20130703T203000Z
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/multiple-rdate.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,80 @@
+BEGIN:VCALENDAR
+X-LOTUS-CHARSET:UTF-8
+VERSION:2.0
+PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C
+METHOD:PUBLISH
+BEGIN:VTIMEZONE
+TZID:W. Europe
+BEGIN:STANDARD
+DTSTART:19501029T020000
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19500326T020000
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VEVENT
+DTSTART;TZID="W. Europe":20140520T040000
+DTEND;TZID="W. Europe":20140520T200000
+TRANSP:TRANSPARENT
+RDATE;VALUE=DATE-TIME:20140520T020000Z
+RDATE;VALUE=PERIOD:20150520T020000Z/20150520T180000Z
+ ,20160520T020000Z/20160520T180000Z,20170520T020000Z/20170520T180000Z
+ ,20180520T020000Z/20180520T180000Z,20190520T020000Z/20190520T180000Z
+ ,20200520T020000Z/20200520T180000Z,20210520T020000Z/20210520T180000Z
+ ,20220520T020000Z/20220520T180000Z
+DTSTAMP:20140227T123549Z
+CLASS:PUBLIC
+SUMMARY:Feiertag - Pfingsmontag
+UID:AAAA6A8C3CCE4EE2C1257B5C00FFFFFF-Lotus_Notes_Generated
+X-LOTUS-PARITAL-REPEAT:TRUE
+X-LOTUS-NOTESVERSION:2
+X-LOTUS-APPTTYPE:1
+END:VEVENT
+
+END:VCALENDAR
+
+BEGIN:VCALENDAR
+X-LOTUS-CHARSET:UTF-8
+VERSION:2.0
+PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C
+METHOD:PUBLISH
+BEGIN:VTIMEZONE
+TZID:W. Europe
+BEGIN:STANDARD
+DTSTART:19501029T020000
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19500326T020000
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VEVENT
+DTSTART;TZID="W. Europe":20120330T040000
+DTEND;TZID="W. Europe":20120330T200000
+TRANSP:TRANSPARENT
+RDATE;VALUE=PERIOD:20120330T020000Z/20120330T180000Z
+ ,20130330T030000Z/20130330T190000Z,20140330T020000Z/20140330T180000Z
+ ,20150330T020000Z/20150330T180000Z,20160330T020000Z/20160330T180000Z
+ ,20170330T020000Z/20170330T180000Z,20180330T020000Z/20180330T180000Z
+ ,20190330T030000Z/20190330T190000Z,20200330T020000Z/20200330T180000Z
+ ,20210330T020000Z/20210330T180000Z
+DTSTAMP:20140227T123547Z
+CLASS:PUBLIC
+SUMMARY:Another RDATE repeating event
+UID:AAAA1C572093EC3FC125799C004AFFFF-Lotus_Notes_Generated
+END:VEVENT
+
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/multiple.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,51 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 5.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:Europe/Zurich
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:CEST
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:CET
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20130530T140406Z
+UID:42524DA1-8B43-4CCA-9FDE-1A8F165115C6
+DTEND;TZID=Europe/Zurich:20130607T230000
+TRANSP:OPAQUE
+SUMMARY:Depeche Mode
+LAST-MODIFIED:20130530T140406Z
+DTSTAMP:20130530T140413Z
+DTSTART;TZID=Europe/Zurich:20130607T180000
+LOCATION:Wankdorf Stadium, Bern
+SEQUENCE:0
+BEGIN:VALARM
+UID:E5F5C5CB-F17A-4959-A0A2-80D700197425
+X-WR-ALARMUID:E5F5C5CB-F17A-4959-A0A2-80D700197425
+DESCRIPTION:Reminder
+TRIGGER:-PT1H
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+CREATED:20130718T073555Z
+UID:B968B885-08FB-40E5-B89E-6DA05F26AA79
+DTEND;VALUE=DATE:20130802
+TRANSP:TRANSPARENT
+SUMMARY:Swiss National Day
+DTSTART;VALUE=DATE:20130801
+DTSTAMP:20130718T074538Z
+SEQUENCE:3
+END:VEVENT
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/recurrence-id.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,39 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C
+METHOD:REQUEST
+BEGIN:VTIMEZONE
+TZID:W. Europe
+BEGIN:STANDARD
+DTSTART:19501029T020000
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19500326T020000
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID="W. Europe":20140230T150000
+DTEND;TZID="W. Europe":20140230T163000
+TRANSP:OPAQUE
+RECURRENCE-ID;RANGE=THISANDFUTURE:20140227T130000Z
+SEQUENCE:0
+UID:7e93e8e8eef16f28aa33b78cd73613ebff
+DTSTAMP:20140120T105609Z
+SUMMARY:Invitation with Recurrence-ID
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;TZID="W. Europe":20140305T150000
+DTEND;TZID="W. Europe":20140305T163000
+RECURRENCE-ID;TZID="W. Europe":20140305T150000
+SEQUENCE:2
+UID:7e93e8e8eef16f28aa33b78cd73613ebff
+DTSTAMP:20140120T105609Z
+SUMMARY:Invitation with Recurrence-ID #2
+END:VEVENT
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/recurring.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,51 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 5.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VTIMEZONE
+TZID:Europe/Zurich
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+DTSTART:19810329T020000
+TZNAME:CEST
+TZOFFSETTO:+0200
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+DTSTART:19961027T030000
+TZNAME:CET
+TZOFFSETTO:+0100
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+PRIORITY:3
+DTEND;TZID=Europe/Zurich:20130717T130000
+TRANSP:TRANSPARENT
+UID:7e93e8e8eef16f28aa33b78cd73613eb
+DTSTAMP:20130718T082032Z
+SEQUENCE:6
+CLASS:CONFIDENTIAL
+CATEGORIES:libcalendaring tests
+SUMMARY:Recurring Test
+LAST-MODIFIED:20120621
+DTSTART;TZID=Europe/Zurich:20130717T080000
+CREATED:20081223T232600Z
+RRULE:FREQ=MONTHLY;INTERVAL=1;UNTIL=20140718T215959Z;BYDAY=3WE
+EXDATE;TZID=Europe/Zurich:20131218T080000
+EXDATE;TZID=Europe/Zurich:20140415T080000
+BEGIN:VALARM
+TRIGGER;RELATED=END:-PT12H
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+DTSTART;TZID="Europe/Zurich":20140521T100000
+DTEND;TZID="Europe/Zurich":20140521T150000
+RECURRENCE-ID:20140521T080000Z
+UID:7e93e8e8eef16f28aa33b78cd73613eb
+DTSTAMP:20130718T082032Z
+SUMMARY:Recurring Test (Exception)
+END:VEVENT
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/snd.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,18 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//iCal 5.0.3//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+CREATED:20130718T073555Z
+UID:B968B885-08FB-40E5-B89E-6DA05F26AA79
+URL;VALUE=URI:http://en.wikipedia.org/wiki/Swiss_National_Day
+DTEND;VALUE=DATE:20130802
+TRANSP:TRANSPARENT
+SUMMARY:Swiss National Day
+DTSTART;VALUE=DATE:20130801
+DTSTAMP:20130718T074538Z
+SEQUENCE:2
+DESCRIPTION:German: Schweizer Bundesfeier\nFrench: FĂȘte nationale Suisse
+ \nItalian: Festa nazionale svizzera\nRomansh: Fiasta naziunala Svizra
+END:VEVENT
+END:VCALENDAR
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/libcalendaring/tests/resources/vtodo.ics	Sat Jan 13 08:57:56 2018 -0500
@@ -0,0 +1,55 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Europe/Zurich
+X-LIC-LOCATION:Europe/Zurich
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VTODO
+LAST-MODIFIED:20130919T075227Z
+DTSTAMP:20130919T075227Z
+UID:163A577B800E62BFFEF1CEC9DDDE4E11-FCBB6C4091F28CA0
+SUMMARY:My first task today
+STATUS:IN-PROCESS
+DTSTART;TZID=Europe/Zurich:20130921T000000
+DUE;VALUE=DATE:20130921
+SEQUENCE:2
+CATEGORIES:Tag1,Tag2
+CATEGORIES:Tag3
+CATEGORIES:Tag4
+RELATED-TO:1234567890-12345678-PARENT
+RELATED-TO;RELTYPE=CHILD:1234567890-12345678-CHILD
+X-MOZ-GENERATION:1
+BEGIN:VALARM
+ACTION:DISPLAY
+TRIGGER;VALUE=DURATION:-P1D
+DESCRIPTION:Default Mozilla Description
+END:VALARM
+END:VTODO
+
+BEGIN:VTODO
+SUMMARY:Send and receive iTip messages with task assignments
+UID:7ADB358AD071AA00306962BF329EF317-FCBB6C4091F28CA0
+SEQUENCE:0
+DTSTAMP:20150527T101614Z
+CREATED:20140801T091954Z
+LAST-MODIFIED:20150527T101611Z
+COMPLETED:20150527T101611Z
+X-BUSYMAC-LASTMODBY:Thomas BrĂŒderli
+END:VTODO
+
+END:VCALENDAR