3 commits - plugins/calendar plugins/libcalendaring

Thomas Brüderli bruederli at kolabsys.com
Thu Jul 18 12:41:06 CEST 2013


 plugins/calendar/calendar.php                        |    3 
 plugins/calendar/lib/Horde_Date.php                  | 1305 -------
 plugins/calendar/lib/Horde_iCalendar.php             | 3300 -------------------
 plugins/calendar/lib/calendar_ical.php               |  569 ---
 plugins/calendar/lib/get_horde_icalendar.sh          |   31 
 plugins/libcalendaring/get_horde_icalendar.sh        |   31 
 plugins/libcalendaring/lib/Horde_Date.php            | 1304 +++++++
 plugins/libcalendaring/lib/Horde_iCalendar.php       | 3300 +++++++++++++++++++
 plugins/libcalendaring/libcalendaring.php            |   13 
 plugins/libcalendaring/libvcalendar.php              |  575 +++
 plugins/libcalendaring/tests/libvcalendar.php        |  267 +
 plugins/libcalendaring/tests/resources/invalid.txt   |    2 
 plugins/libcalendaring/tests/resources/itip.ics      |  149 
 plugins/libcalendaring/tests/resources/multiple.ics  |   51 
 plugins/libcalendaring/tests/resources/recurring.ics |   43 
 plugins/libcalendaring/tests/resources/snd.ics       |   18 
 16 files changed, 5753 insertions(+), 5208 deletions(-)

New commits:
commit af49088b38a7861fe940a4ad4883364024069418
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 18 12:40:46 2013 +0200

    Add unit tests for ical import/export

diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
new file mode 100644
index 0000000..b28c7ac
--- /dev/null
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -0,0 +1,267 @@
+<?php
+
+/**
+ * libcalendaring plugin's iCalendar functions tests
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class 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['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 some extended ical properties such as attendees, recurrence rules, alarms and attachments
+     *
+     * @depends test_import_from_file
+     */
+    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(2, 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 at mykolab.com', $attendee['email'], 'Attendee mailto:');
+        $this->assertTrue($attendee['rsvp'], 'Attendee 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");
+
+        // alarms
+        $this->assertEquals('-12H:DISPLAY', $event['alarms'], "Serialized alarms string");
+        $alarm = libcalendaring::parse_alaram_value($event['alarms']);
+        $this->assertEquals('12', $alarm[0], "Alarm value");
+        $this->assertEquals('-H', $alarm[1], "Alarm unit");
+
+        // categories, class
+        $this->assertEquals('libcalendaring tests', $event['categories'], "Event categories");
+        $this->assertEquals(2, $event['sensitivity'], "Class/sensitivity = confidential");
+    }
+
+    /**
+     * Test for iCal export from internal hash array representation
+     *
+     * @depend test_extended
+     */
+    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*';
+
+        $ics = $ical->export(array($event), 'REQUEST', false, array($this, 'get_attachment_data'));
+
+        $this->assertContains('BEGIN:VCALENDAR',    $ics, "VCALENDAR encapsulation BEGIN");
+        $this->assertContains('METHOD:REQUEST',     $ics, "iTip method");
+        $this->assertContains('BEGIN:VEVENT',       $ics, "VEVENT encapsulation BEGIN");
+
+        $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('/ATTENDEE.*: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=20140718/',                   $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:-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");
+    }
+
+    /**
+     * @depend 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");
+    }
+
+    /**
+     * @depend 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];
+        $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:20130814', $ics, "Recurrence-ID (1) being the exception date");
+        $this->assertContains('RECURRENCE-ID:20131113', $ics, "Recurrence-ID (2) being the exception date");
+        $this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title");
+    }
+    
+    /**
+     * @depend 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 get_attachment_data($id, $event)
+    {
+        return $this->attachment_data;
+    }
+}
+
diff --git a/plugins/libcalendaring/tests/resources/invalid.txt b/plugins/libcalendaring/tests/resources/invalid.txt
new file mode 100644
index 0000000..bb9c7bc
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/invalid.txt
@@ -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
diff --git a/plugins/libcalendaring/tests/resources/itip.ics b/plugins/libcalendaring/tests/resources/itip.ics
new file mode 100644
index 0000000..af283e3
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/itip.ics
@@ -0,0 +1,149 @@
+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 at mykolab.com
+DTSTAMP:20130628T190056Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;
+ X-UID=208889384:mailto:rolf2 at 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
diff --git a/plugins/libcalendaring/tests/resources/multiple.ics b/plugins/libcalendaring/tests/resources/multiple.ics
new file mode 100644
index 0000000..b0b0d17
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/multiple.ics
@@ -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
diff --git a/plugins/libcalendaring/tests/resources/recurring.ics b/plugins/libcalendaring/tests/resources/recurring.ics
new file mode 100644
index 0000000..92db9e2
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/recurring.ics
@@ -0,0 +1,43 @@
+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:-PT12H
+ACTION:DISPLAY
+END:VALARM
+END:VEVENT
+END:VCALENDAR
diff --git a/plugins/libcalendaring/tests/resources/snd.ics b/plugins/libcalendaring/tests/resources/snd.ics
new file mode 100644
index 0000000..4efb2dd
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/snd.ics
@@ -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


commit 000bac244bc0b352410b55ba7e38f65ed2b6b3d9
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 18 12:39:56 2013 +0200

    Add symlink to moved Horde_Date class; move Horde import script, too

diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
new file mode 120000
index 0000000..5cd5ce2
--- /dev/null
+++ b/plugins/calendar/lib/Horde_Date.php
@@ -0,0 +1 @@
+../../libcalendaring/lib/Horde_Date.php
\ No newline at end of file
diff --git a/plugins/calendar/lib/get_horde_icalendar.sh b/plugins/calendar/lib/get_horde_icalendar.sh
deleted file mode 100755
index 1992bf2..0000000
--- a/plugins/calendar/lib/get_horde_icalendar.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/sh
-
-# Copy Horde_iCalendar classes and dependencies to stdout.
-# This will create a standalone copy of the classes requried for iCal parsing.
-
-SRCDIR=$1
-
-if [ ! -d "$SRCDIR" ]; then
-  echo "Usage: get_horde_icalendar.sh SRCDIR"
-  echo "Please enter a valid source directory of the Horde lib"
-  exit 1
-fi
-
-echo "<?php
-
-/**
- * This is a concatenated copy of the following files:
- *   Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
- * Pull the latest version of these file from the PEAR channel of the Horde project at http://pear.horde.org
- */
-
-require_once(dirname(__FILE__) . '/Horde_Date.php');"
-
-sed 's/<?php//; s/?>//' $SRCDIR/String.php
-echo "\n"
-sed 's/<?php//; s/?>//' $SRCDIR/iCalendar.php | sed -E "s/include_once.+//; s/NLS::getCharset\(\)/'UTF-8'/"
-echo "\n"
-
-for fn in `ls $SRCDIR/iCalendar/*.php | grep -v 'vcard.php'`; do
-	sed 's/<?php//; s/?>//' $fn | sed -E "s/(include|require)_once.+//"
-done;
diff --git a/plugins/libcalendaring/get_horde_icalendar.sh b/plugins/libcalendaring/get_horde_icalendar.sh
new file mode 100755
index 0000000..1992bf2
--- /dev/null
+++ b/plugins/libcalendaring/get_horde_icalendar.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+# Copy Horde_iCalendar classes and dependencies to stdout.
+# This will create a standalone copy of the classes requried for iCal parsing.
+
+SRCDIR=$1
+
+if [ ! -d "$SRCDIR" ]; then
+  echo "Usage: get_horde_icalendar.sh SRCDIR"
+  echo "Please enter a valid source directory of the Horde lib"
+  exit 1
+fi
+
+echo "<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ *   Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
+ * Pull the latest version of these file from the PEAR channel of the Horde project at http://pear.horde.org
+ */
+
+require_once(dirname(__FILE__) . '/Horde_Date.php');"
+
+sed 's/<?php//; s/?>//' $SRCDIR/String.php
+echo "\n"
+sed 's/<?php//; s/?>//' $SRCDIR/iCalendar.php | sed -E "s/include_once.+//; s/NLS::getCharset\(\)/'UTF-8'/"
+echo "\n"
+
+for fn in `ls $SRCDIR/iCalendar/*.php | grep -v 'vcard.php'`; do
+	sed 's/<?php//; s/?>//' $fn | sed -E "s/(include|require)_once.+//"
+done;


commit 18f9fa5c860f04622497fb822c4ca8d8022fe6f7
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 18 12:38:14 2013 +0200

    Move iCal parsing/writing classes to libcalendaring

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 63bfe11..03cdb8b 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -219,8 +219,7 @@ class calendar extends rcube_plugin
   public function get_ical()
   {
     if (!$this->ical) {
-      require_once($this->home . '/lib/calendar_ical.php');
-      $this->ical = new calendar_ical($this);
+      $this->ical = libcalendaring::get_ical();
     }
     
     return $this->ical;
diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
deleted file mode 100644
index 9197f84..0000000
--- a/plugins/calendar/lib/Horde_Date.php
+++ /dev/null
@@ -1,1304 +0,0 @@
-<?php
-
-/**
- * This is a concatenated copy of the following files:
- *   Horde/Date/Utils.php, Horde/Date/Recurrence.php
- * Pull the latest version of these files from the PEAR channel of the Horde
- * project at http://pear.horde.org by installing the Horde_Date package.
- */
-
-
-/**
- * Horde Date wrapper/logic class, including some calculation
- * functions.
- *
- * @category Horde
- * @package  Date
- *
- * @TODO in format():
- *   http://php.net/intldateformatter
- *
- * @TODO on timezones:
- *   http://trac.agavi.org/ticket/1008
- *   http://trac.agavi.org/changeset/3659
- *
- * @TODO on switching to PHP::DateTime:
- *   The only thing ever stored in the database *IS* Unix timestamps. Doing
- *   anything other than that is unmanageable, yet some frameworks use 'server
- *   based' times in their systems, simply because they do not bother with
- *   daylight saving and only 'serve' one timezone!
- *
- *   The second you have to manage 'real' time across timezones then daylight
- *   saving becomes essential, BUT only on the display side! Since the browser
- *   only provides a time offset, this is useless and to be honest should simply
- *   be ignored ( until it is upgraded to provide the correct information ;)
- *   ). So we need a 'display' function that takes a simple numeric epoch, and a
- *   separate timezone id into which the epoch is to be 'converted'. My W3C
- *   mapping works simply because ADOdb then converts that to it's own simple
- *   offset abbreviation - in my case GMT or BST. As long as DateTime passes the
- *   full 64 bit number the date range from 100AD is also preserved ( and
- *   further back if 2 digit years are disabled ). If I want to display the
- *   'real' timezone with this 'time' then I just add it in place of ADOdb's
- *   'timezone'. I am tempted to simply adjust the ADOdb class to take a
- *   timezone in place of the simple GMT switch it currently uses.
- *
- *   The return path is just the reverse and simply needs to take the client
- *   display offset off prior to storage of the UTC epoch. SO we use
- *   DateTimeZone to get an offset value for the clients timezone and simply add
- *   or subtract this from a timezone agnostic display on the client end when
- *   entering new times.
- *
- *
- *   It's not really feasible to store dates in specific timezone, as most
- *   national/local timezones support DST - and that is a pain to support, as
- *   eg.  sorting breaks when some timestamps get repeated. That's why it's
- *   usually better to store datetimes as either UTC datetime or plain unix
- *   timestamp. I usually go with the former - using database datetime type.
- */
-
-/**
- * @category Horde
- * @package  Date
- */
-class Horde_Date
-{
-    const DATE_SUNDAY = 0;
-    const DATE_MONDAY = 1;
-    const DATE_TUESDAY = 2;
-    const DATE_WEDNESDAY = 3;
-    const DATE_THURSDAY = 4;
-    const DATE_FRIDAY = 5;
-    const DATE_SATURDAY = 6;
-
-    const MASK_SUNDAY = 1;
-    const MASK_MONDAY = 2;
-    const MASK_TUESDAY = 4;
-    const MASK_WEDNESDAY = 8;
-    const MASK_THURSDAY = 16;
-    const MASK_FRIDAY = 32;
-    const MASK_SATURDAY = 64;
-    const MASK_WEEKDAYS = 62;
-    const MASK_WEEKEND = 65;
-    const MASK_ALLDAYS = 127;
-
-    const MASK_SECOND = 1;
-    const MASK_MINUTE = 2;
-    const MASK_HOUR = 4;
-    const MASK_DAY = 8;
-    const MASK_MONTH = 16;
-    const MASK_YEAR = 32;
-    const MASK_ALLPARTS = 63;
-
-    const DATE_DEFAULT = 'Y-m-d H:i:s';
-    const DATE_JSON = 'Y-m-d\TH:i:s';
-
-    /**
-     * Year
-     *
-     * @var integer
-     */
-    protected $_year;
-
-    /**
-     * Month
-     *
-     * @var integer
-     */
-    protected $_month;
-
-    /**
-     * Day
-     *
-     * @var integer
-     */
-    protected $_mday;
-
-    /**
-     * Hour
-     *
-     * @var integer
-     */
-    protected $_hour = 0;
-
-    /**
-     * Minute
-     *
-     * @var integer
-     */
-    protected $_min = 0;
-
-    /**
-     * Second
-     *
-     * @var integer
-     */
-    protected $_sec = 0;
-
-    /**
-     * String representation of the date's timezone.
-     *
-     * @var string
-     */
-    protected $_timezone;
-
-    /**
-     * Default format for __toString()
-     *
-     * @var string
-     */
-    protected $_defaultFormat = self::DATE_DEFAULT;
-
-    /**
-     * Default specs that are always supported.
-     * @var string
-     */
-    protected static $_defaultSpecs = '%CdDeHImMnRStTyY';
-
-    /**
-     * Internally supported strftime() specifiers.
-     * @var string
-     */
-    protected static $_supportedSpecs = '';
-
-    /**
-     * Map of required correction masks.
-     *
-     * @see __set()
-     *
-     * @var array
-     */
-    protected static $_corrections = array(
-        'year'  => self::MASK_YEAR,
-        'month' => self::MASK_MONTH,
-        'mday'  => self::MASK_DAY,
-        'hour'  => self::MASK_HOUR,
-        'min'   => self::MASK_MINUTE,
-        'sec'   => self::MASK_SECOND,
-    );
-
-    protected $_formatCache = array();
-
-    /**
-     * Builds a new date object. If $date contains date parts, use them to
-     * initialize the object.
-     *
-     * Recognized formats:
-     * - arrays with keys 'year', 'month', 'mday', 'day'
-     *   'hour', 'min', 'minute', 'sec'
-     * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
-     * - yyyy-mm-dd hh:mm:ss
-     * - yyyymmddhhmmss
-     * - yyyymmddThhmmssZ
-     * - yyyymmdd (might conflict with unix timestamps between 31 Oct 1966 and
-     *   03 Mar 1973)
-     * - unix timestamps
-     * - anything parsed by strtotime()/DateTime.
-     *
-     * @throws Horde_Date_Exception
-     */
-    public function __construct($date = null, $timezone = null)
-    {
-        if (!self::$_supportedSpecs) {
-            self::$_supportedSpecs = self::$_defaultSpecs;
-            if (function_exists('nl_langinfo')) {
-                self::$_supportedSpecs .= 'bBpxX';
-            }
-        }
-
-        if (func_num_args() > 2) {
-            // Handle args in order: year month day hour min sec tz
-            $this->_initializeFromArgs(func_get_args());
-            return;
-        }
-
-        $this->_initializeTimezone($timezone);
-
-        if (is_null($date)) {
-            return;
-        }
-
-        if (is_string($date)) {
-            $date = trim($date, '"');
-        }
-
-        if (is_object($date)) {
-            $this->_initializeFromObject($date);
-        } elseif (is_array($date)) {
-            $this->_initializeFromArray($date);
-        } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $date, $parts)) {
-            $this->_year  = (int)$parts[1];
-            $this->_month = (int)$parts[2];
-            $this->_mday  = (int)$parts[3];
-            $this->_hour  = (int)$parts[4];
-            $this->_min   = (int)$parts[5];
-            $this->_sec   = (int)$parts[6];
-            if ($parts[7]) {
-                $this->_initializeTimezone('UTC');
-            }
-        } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $date, $parts) &&
-                  $parts[2] > 0 && $parts[2] <= 12 &&
-                  $parts[3] > 0 && $parts[3] <= 31) {
-            $this->_year  = (int)$parts[1];
-            $this->_month = (int)$parts[2];
-            $this->_mday  = (int)$parts[3];
-            $this->_hour = $this->_min = $this->_sec = 0;
-        } elseif ((string)(int)$date == $date) {
-            // Try as a timestamp.
-            $parts = @getdate($date);
-            if ($parts) {
-                $this->_year  = $parts['year'];
-                $this->_month = $parts['mon'];
-                $this->_mday  = $parts['mday'];
-                $this->_hour  = $parts['hours'];
-                $this->_min   = $parts['minutes'];
-                $this->_sec   = $parts['seconds'];
-            }
-        } else {
-            // Use date_create() so we can catch errors with PHP 5.2. Use
-            // "new DateTime() once we require 5.3.
-            $parsed = date_create($date);
-            if (!$parsed) {
-                throw new Horde_Date_Exception(sprintf(Horde_Date_Translation::t("Failed to parse time string (%s)"), $date));
-            }
-            $parsed->setTimezone(new DateTimeZone(date_default_timezone_get()));
-            $this->_year  = (int)$parsed->format('Y');
-            $this->_month = (int)$parsed->format('m');
-            $this->_mday  = (int)$parsed->format('d');
-            $this->_hour  = (int)$parsed->format('H');
-            $this->_min   = (int)$parsed->format('i');
-            $this->_sec   = (int)$parsed->format('s');
-            $this->_initializeTimezone(date_default_timezone_get());
-        }
-    }
-
-    /**
-     * Returns a simple string representation of the date object
-     *
-     * @return string  This object converted to a string.
-     */
-    public function __toString()
-    {
-        try {
-            return $this->format($this->_defaultFormat);
-        } catch (Exception $e) {
-            return '';
-        }
-    }
-
-    /**
-     * Returns a DateTime object representing this object.
-     *
-     * @return DateTime
-     */
-    public function toDateTime()
-    {
-        $date = new DateTime(null, new DateTimeZone($this->_timezone));
-        $date->setDate($this->_year, $this->_month, $this->_mday);
-        $date->setTime($this->_hour, $this->_min, $this->_sec);
-        return $date;
-    }
-
-    /**
-     * Converts a date in the proleptic Gregorian calendar to the no of days
-     * since 24th November, 4714 B.C.
-     *
-     * Returns the no of days since Monday, 24th November, 4714 B.C. in the
-     * proleptic Gregorian calendar (which is 24th November, -4713 using
-     * 'Astronomical' year numbering, and 1st January, 4713 B.C. in the
-     * proleptic Julian calendar).  This is also the first day of the 'Julian
-     * Period' proposed by Joseph Scaliger in 1583, and the number of days
-     * since this date is known as the 'Julian Day'.  (It is not directly
-     * to do with the Julian calendar, although this is where the name
-     * is derived from.)
-     *
-     * The algorithm is valid for all years (positive and negative), and
-     * also for years preceding 4714 B.C.
-     *
-     * Algorithm is from PEAR::Date_Calc
-     *
-     * @author Monte Ohrt <monte at ispi.net>
-     * @author Pierre-Alain Joye <pajoye at php.net>
-     * @author Daniel Convissor <danielc at php.net>
-     * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
-     *
-     * @return integer  The number of days since 24th November, 4714 B.C.
-     */
-    public function toDays()
-    {
-        if (function_exists('GregorianToJD')) {
-            return gregoriantojd($this->_month, $this->_mday, $this->_year);
-        }
-
-        $day = $this->_mday;
-        $month = $this->_month;
-        $year = $this->_year;
-
-        if ($month > 2) {
-            // March = 0, April = 1, ..., December = 9,
-            // January = 10, February = 11
-            $month -= 3;
-        } else {
-            $month += 9;
-            --$year;
-        }
-
-        $hb_negativeyear = $year < 0;
-        $century         = intval($year / 100);
-        $year            = $year % 100;
-
-        if ($hb_negativeyear) {
-            // Subtract 1 because year 0 is a leap year;
-            // And N.B. that we must treat the leap years as occurring
-            // one year earlier than they do, because for the purposes
-            // of calculation, the year starts on 1st March:
-            //
-            return intval((14609700 * $century + ($year == 0 ? 1 : 0)) / 400) +
-                   intval((1461 * $year + 1) / 4) +
-                   intval((153 * $month + 2) / 5) +
-                   $day + 1721118;
-        } else {
-            return intval(146097 * $century / 4) +
-                   intval(1461 * $year / 4) +
-                   intval((153 * $month + 2) / 5) +
-                   $day + 1721119;
-        }
-    }
-
-    /**
-     * Converts number of days since 24th November, 4714 B.C. (in the proleptic
-     * Gregorian calendar, which is year -4713 using 'Astronomical' year
-     * numbering) to Gregorian calendar date.
-     *
-     * Returned date belongs to the proleptic Gregorian calendar, using
-     * 'Astronomical' year numbering.
-     *
-     * The algorithm is valid for all years (positive and negative), and
-     * also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'),
-     * and so the only limitation is platform-dependent (for 32-bit systems
-     * the maximum year would be something like about 1,465,190 A.D.).
-     *
-     * N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'.
-     *
-     * Algorithm is from PEAR::Date_Calc
-     *
-     * @author Monte Ohrt <monte at ispi.net>
-     * @author Pierre-Alain Joye <pajoye at php.net>
-     * @author Daniel Convissor <danielc at php.net>
-     * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
-     *
-     * @param int    $days   the number of days since 24th November, 4714 B.C.
-     * @param string $format the string indicating how to format the output
-     *
-     * @return  Horde_Date  A Horde_Date object representing the date.
-     */
-    public static function fromDays($days)
-    {
-        if (function_exists('JDToGregorian')) {
-            list($month, $day, $year) = explode('/', JDToGregorian($days));
-        } else {
-            $days = intval($days);
-
-            $days   -= 1721119;
-            $century = floor((4 * $days - 1) / 146097);
-            $days    = floor(4 * $days - 1 - 146097 * $century);
-            $day     = floor($days / 4);
-
-            $year = floor((4 * $day +  3) / 1461);
-            $day  = floor(4 * $day +  3 - 1461 * $year);
-            $day  = floor(($day +  4) / 4);
-
-            $month = floor((5 * $day - 3) / 153);
-            $day   = floor(5 * $day - 3 - 153 * $month);
-            $day   = floor(($day +  5) /  5);
-
-            $year = $century * 100 + $year;
-            if ($month < 10) {
-                $month +=3;
-            } else {
-                $month -=9;
-                ++$year;
-            }
-        }
-
-        return new Horde_Date($year, $month, $day);
-    }
-
-    /**
-     * Getter for the date and time properties.
-     *
-     * @param string $name  One of 'year', 'month', 'mday', 'hour', 'min' or
-     *                      'sec'.
-     *
-     * @return integer  The property value, or null if not set.
-     */
-    public function __get($name)
-    {
-        if ($name == 'day') {
-            $name = 'mday';
-        }
-
-        return $this->{'_' . $name};
-    }
-
-    /**
-     * Setter for the date and time properties.
-     *
-     * @param string $name    One of 'year', 'month', 'mday', 'hour', 'min' or
-     *                        'sec'.
-     * @param integer $value  The property value.
-     */
-    public function __set($name, $value)
-    {
-        if ($name == 'timezone') {
-            $this->_initializeTimezone($value);
-            return;
-        }
-        if ($name == 'day') {
-            $name = 'mday';
-        }
-
-        if ($name != 'year' && $name != 'month' && $name != 'mday' &&
-            $name != 'hour' && $name != 'min' && $name != 'sec') {
-            throw new InvalidArgumentException('Undefined property ' . $name);
-        }
-
-        $down = $value < $this->{'_' . $name};
-        $this->{'_' . $name} = $value;
-        $this->_correct(self::$_corrections[$name], $down);
-        $this->_formatCache = array();
-    }
-
-    /**
-     * Returns whether a date or time property exists.
-     *
-     * @param string $name  One of 'year', 'month', 'mday', 'hour', 'min' or
-     *                      'sec'.
-     *
-     * @return boolen  True if the property exists and is set.
-     */
-    public function __isset($name)
-    {
-        if ($name == 'day') {
-            $name = 'mday';
-        }
-        return ($name == 'year' || $name == 'month' || $name == 'mday' ||
-                $name == 'hour' || $name == 'min' || $name == 'sec') &&
-            isset($this->{'_' . $name});
-    }
-
-    /**
-     * Adds a number of seconds or units to this date, returning a new Date
-     * object.
-     */
-    public function add($factor)
-    {
-        $d = clone($this);
-        if (is_array($factor) || is_object($factor)) {
-            foreach ($factor as $property => $value) {
-                $d->$property += $value;
-            }
-        } else {
-            $d->sec += $factor;
-        }
-
-        return $d;
-    }
-
-    /**
-     * Subtracts a number of seconds or units from this date, returning a new
-     * Horde_Date object.
-     */
-    public function sub($factor)
-    {
-        if (is_array($factor)) {
-            foreach ($factor as &$value) {
-                $value *= -1;
-            }
-        } else {
-            $factor *= -1;
-        }
-
-        return $this->add($factor);
-    }
-
-    /**
-     * Converts this object to a different timezone.
-     *
-     * @param string $timezone  The new timezone.
-     *
-     * @return Horde_Date  This object.
-     */
-    public function setTimezone($timezone)
-    {
-        $date = $this->toDateTime();
-        $date->setTimezone(new DateTimeZone($timezone));
-        $this->_timezone = $timezone;
-        $this->_year     = (int)$date->format('Y');
-        $this->_month    = (int)$date->format('m');
-        $this->_mday     = (int)$date->format('d');
-        $this->_hour     = (int)$date->format('H');
-        $this->_min      = (int)$date->format('i');
-        $this->_sec      = (int)$date->format('s');
-        $this->_formatCache = array();
-        return $this;
-    }
-
-    /**
-     * Sets the default date format used in __toString()
-     *
-     * @param string $format
-     */
-    public function setDefaultFormat($format)
-    {
-        $this->_defaultFormat = $format;
-    }
-
-    /**
-     * Returns the day of the week (0 = Sunday, 6 = Saturday) of this date.
-     *
-     * @return integer  The day of the week.
-     */
-    public function dayOfWeek()
-    {
-        if ($this->_month > 2) {
-            $month = $this->_month - 2;
-            $year = $this->_year;
-        } else {
-            $month = $this->_month + 10;
-            $year = $this->_year - 1;
-        }
-
-        $day = (floor((13 * $month - 1) / 5) +
-                $this->_mday + ($year % 100) +
-                floor(($year % 100) / 4) +
-                floor(($year / 100) / 4) - 2 *
-                floor($year / 100) + 77);
-
-        return (int)($day - 7 * floor($day / 7));
-    }
-
-    /**
-     * Returns the day number of the year (1 to 365/366).
-     *
-     * @return integer  The day of the year.
-     */
-    public function dayOfYear()
-    {
-        return $this->format('z') + 1;
-    }
-
-    /**
-     * Returns the week of the month.
-     *
-     * @return integer  The week number.
-     */
-    public function weekOfMonth()
-    {
-        return ceil($this->_mday / 7);
-    }
-
-    /**
-     * Returns the week of the year, first Monday is first day of first week.
-     *
-     * @return integer  The week number.
-     */
-    public function weekOfYear()
-    {
-        return $this->format('W');
-    }
-
-    /**
-     * Returns the number of weeks in the given year (52 or 53).
-     *
-     * @param integer $year  The year to count the number of weeks in.
-     *
-     * @return integer $numWeeks   The number of weeks in $year.
-     */
-    public static function weeksInYear($year)
-    {
-        // Find the last Thursday of the year.
-        $date = new Horde_Date($year . '-12-31');
-        while ($date->dayOfWeek() != self::DATE_THURSDAY) {
-            --$date->mday;
-        }
-        return $date->weekOfYear();
-    }
-
-    /**
-     * Sets the date of this object to the $nth weekday of $weekday.
-     *
-     * @param integer $weekday  The day of the week (0 = Sunday, etc).
-     * @param integer $nth      The $nth $weekday to set to (defaults to 1).
-     */
-    public function setNthWeekday($weekday, $nth = 1)
-    {
-        if ($weekday < self::DATE_SUNDAY || $weekday > self::DATE_SATURDAY) {
-            return;
-        }
-
-        if ($nth < 0) {  // last $weekday of month
-            $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
-            $last = $this->dayOfWeek();
-            $this->_mday += ($weekday - $last);
-            if ($this->_mday > $lastday)
-                $this->_mday -= 7;
-        }
-        else {
-            $this->_mday = 1;
-            $first = $this->dayOfWeek();
-            if ($weekday < $first) {
-                $this->_mday = 8 + $weekday - $first;
-            } else {
-                $this->_mday = $weekday - $first + 1;
-            }
-            $diff = 7 * $nth - 7;
-            $this->_mday += $diff;
-            $this->_correct(self::MASK_DAY, $diff < 0);
-        }
-    }
-
-    /**
-     * Is the date currently represented by this object a valid date?
-     *
-     * @return boolean  Validity, counting leap years, etc.
-     */
-    public function isValid()
-    {
-        return ($this->_year >= 0 && $this->_year <= 9999);
-    }
-
-    /**
-     * Compares this date to another date object to see which one is
-     * greater (later). Assumes that the dates are in the same
-     * timezone.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return integer  ==  0 if they are on the same date
-     *                  >=  1 if $this is greater (later)
-     *                  <= -1 if $other is greater (later)
-     */
-    public function compareDate($other)
-    {
-        if (!($other instanceof Horde_Date)) {
-            $other = new Horde_Date($other);
-        }
-
-        if ($this->_year != $other->year) {
-            return $this->_year - $other->year;
-        }
-        if ($this->_month != $other->month) {
-            return $this->_month - $other->month;
-        }
-
-        return $this->_mday - $other->mday;
-    }
-
-    /**
-     * Returns whether this date is after the other.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return boolean  True if this date is after the other.
-     */
-    public function after($other)
-    {
-        return $this->compareDate($other) > 0;
-    }
-
-    /**
-     * Returns whether this date is before the other.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return boolean  True if this date is before the other.
-     */
-    public function before($other)
-    {
-        return $this->compareDate($other) < 0;
-    }
-
-    /**
-     * Returns whether this date is the same like the other.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return boolean  True if this date is the same like the other.
-     */
-    public function equals($other)
-    {
-        return $this->compareDate($other) == 0;
-    }
-
-    /**
-     * Compares this to another date object by time, to see which one
-     * is greater (later). Assumes that the dates are in the same
-     * timezone.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return integer  ==  0 if they are at the same time
-     *                  >=  1 if $this is greater (later)
-     *                  <= -1 if $other is greater (later)
-     */
-    public function compareTime($other)
-    {
-        if (!($other instanceof Horde_Date)) {
-            $other = new Horde_Date($other);
-        }
-
-        if ($this->_hour != $other->hour) {
-            return $this->_hour - $other->hour;
-        }
-        if ($this->_min != $other->min) {
-            return $this->_min - $other->min;
-        }
-
-        return $this->_sec - $other->sec;
-    }
-
-    /**
-     * Compares this to another date object, including times, to see
-     * which one is greater (later). Assumes that the dates are in the
-     * same timezone.
-     *
-     * @param mixed $other  The date to compare to.
-     *
-     * @return integer  ==  0 if they are equal
-     *                  >=  1 if $this is greater (later)
-     *                  <= -1 if $other is greater (later)
-     */
-    public function compareDateTime($other)
-    {
-        if (!($other instanceof Horde_Date)) {
-            $other = new Horde_Date($other);
-        }
-
-        if ($diff = $this->compareDate($other)) {
-            return $diff;
-        }
-
-        return $this->compareTime($other);
-    }
-
-    /**
-     * Returns number of days between this date and another.
-     *
-     * @param Horde_Date $other  The other day to diff with.
-     *
-     * @return integer  The absolute number of days between the two dates.
-     */
-    public function diff($other)
-    {
-        return abs($this->toDays() - $other->toDays());
-    }
-
-    /**
-     * Returns the time offset for local time zone.
-     *
-     * @param boolean $colon  Place a colon between hours and minutes?
-     *
-     * @return string  Timezone offset as a string in the format +HH:MM.
-     */
-    public function tzOffset($colon = true)
-    {
-        return $colon ? $this->format('P') : $this->format('O');
-    }
-
-    /**
-     * Returns the unix timestamp representation of this date.
-     *
-     * @return integer  A unix timestamp.
-     */
-    public function timestamp()
-    {
-        if ($this->_year >= 1970 && $this->_year < 2038) {
-            return mktime($this->_hour, $this->_min, $this->_sec,
-                          $this->_month, $this->_mday, $this->_year);
-        }
-        return $this->format('U');
-    }
-
-    /**
-     * Returns the unix timestamp representation of this date, 12:00am.
-     *
-     * @return integer  A unix timestamp.
-     */
-    public function datestamp()
-    {
-        if ($this->_year >= 1970 && $this->_year < 2038) {
-            return mktime(0, 0, 0, $this->_month, $this->_mday, $this->_year);
-        }
-        $date = new DateTime($this->format('Y-m-d'));
-        return $date->format('U');
-    }
-
-    /**
-     * Formats date and time to be passed around as a short url parameter.
-     *
-     * @return string  Date and time.
-     */
-    public function dateString()
-    {
-        return sprintf('%04d%02d%02d', $this->_year, $this->_month, $this->_mday);
-    }
-
-    /**
-     * Formats date and time to the ISO format used by JSON.
-     *
-     * @return string  Date and time.
-     */
-    public function toJson()
-    {
-        return $this->format(self::DATE_JSON);
-    }
-
-    /**
-     * Formats date and time to the RFC 2445 iCalendar DATE-TIME format.
-     *
-     * @param boolean $floating  Whether to return a floating date-time
-     *                           (without time zone information).
-     *
-     * @return string  Date and time.
-     */
-    public function toiCalendar($floating = false)
-    {
-        if ($floating) {
-            return $this->format('Ymd\THis');
-        }
-        $dateTime = $this->toDateTime();
-        $dateTime->setTimezone(new DateTimeZone('UTC'));
-        return $dateTime->format('Ymd\THis\Z');
-    }
-
-    /**
-     * Formats time using the specifiers available in date() or in the DateTime
-     * class' format() method.
-     *
-     * To format in languages other than English, use strftime() instead.
-     *
-     * @param string $format
-     *
-     * @return string  Formatted time.
-     */
-    public function format($format)
-    {
-        if (!isset($this->_formatCache[$format])) {
-            $this->_formatCache[$format] = $this->toDateTime()->format($format);
-        }
-        return $this->_formatCache[$format];
-    }
-
-    /**
-     * Formats date and time using strftime() format.
-     *
-     * @return string  strftime() formatted date and time.
-     */
-    public function strftime($format)
-    {
-        if (preg_match('/%[^' . self::$_supportedSpecs . ']/', $format)) {
-            return strftime($format, $this->timestamp());
-        } else {
-            return $this->_strftime($format);
-        }
-    }
-
-    /**
-     * Formats date and time using a limited set of the strftime() format.
-     *
-     * @return string  strftime() formatted date and time.
-     */
-    protected function _strftime($format)
-    {
-        return preg_replace(
-            array('/%b/e',
-                  '/%B/e',
-                  '/%C/e',
-                  '/%d/e',
-                  '/%D/e',
-                  '/%e/e',
-                  '/%H/e',
-                  '/%I/e',
-                  '/%m/e',
-                  '/%M/e',
-                  '/%n/',
-                  '/%p/e',
-                  '/%R/e',
-                  '/%S/e',
-                  '/%t/',
-                  '/%T/e',
-                  '/%x/e',
-                  '/%X/e',
-                  '/%y/e',
-                  '/%Y/',
-                  '/%%/'),
-            array('$this->_strftime(Horde_Nls::getLangInfo(constant(\'ABMON_\' . (int)$this->_month)))',
-                  '$this->_strftime(Horde_Nls::getLangInfo(constant(\'MON_\' . (int)$this->_month)))',
-                  '(int)($this->_year / 100)',
-                  'sprintf(\'%02d\', $this->_mday)',
-                  '$this->_strftime(\'%m/%d/%y\')',
-                  'sprintf(\'%2d\', $this->_mday)',
-                  'sprintf(\'%02d\', $this->_hour)',
-                  'sprintf(\'%02d\', $this->_hour == 0 ? 12 : ($this->_hour > 12 ? $this->_hour - 12 : $this->_hour))',
-                  'sprintf(\'%02d\', $this->_month)',
-                  'sprintf(\'%02d\', $this->_min)',
-                  "\n",
-                  '$this->_strftime(Horde_Nls::getLangInfo($this->_hour < 12 ? AM_STR : PM_STR))',
-                  '$this->_strftime(\'%H:%M\')',
-                  'sprintf(\'%02d\', $this->_sec)',
-                  "\t",
-                  '$this->_strftime(\'%H:%M:%S\')',
-                  '$this->_strftime(Horde_Nls::getLangInfo(D_FMT))',
-                  '$this->_strftime(Horde_Nls::getLangInfo(T_FMT))',
-                  'substr(sprintf(\'%04d\', $this->_year), -2)',
-                  (int)$this->_year,
-                  '%'),
-            $format);
-    }
-
-    /**
-     * Corrects any over- or underflows in any of the date's members.
-     *
-     * @param integer $mask  We may not want to correct some overflows.
-     * @param integer $down  Whether to correct the date up or down.
-     */
-    protected function _correct($mask = self::MASK_ALLPARTS, $down = false)
-    {
-        if ($mask & self::MASK_SECOND) {
-            if ($this->_sec < 0 || $this->_sec > 59) {
-                $mask |= self::MASK_MINUTE;
-
-                $this->_min += (int)($this->_sec / 60);
-                $this->_sec %= 60;
-                if ($this->_sec < 0) {
-                    $this->_min--;
-                    $this->_sec += 60;
-                }
-            }
-        }
-
-        if ($mask & self::MASK_MINUTE) {
-            if ($this->_min < 0 || $this->_min > 59) {
-                $mask |= self::MASK_HOUR;
-
-                $this->_hour += (int)($this->_min / 60);
-                $this->_min %= 60;
-                if ($this->_min < 0) {
-                    $this->_hour--;
-                    $this->_min += 60;
-                }
-            }
-        }
-
-        if ($mask & self::MASK_HOUR) {
-            if ($this->_hour < 0 || $this->_hour > 23) {
-                $mask |= self::MASK_DAY;
-
-                $this->_mday += (int)($this->_hour / 24);
-                $this->_hour %= 24;
-                if ($this->_hour < 0) {
-                    $this->_mday--;
-                    $this->_hour += 24;
-                }
-            }
-        }
-
-        if ($mask & self::MASK_MONTH) {
-            $this->_correctMonth($down);
-            /* When correcting the month, always correct the day too. Months
-             * have different numbers of days. */
-            $mask |= self::MASK_DAY;
-        }
-
-        if ($mask & self::MASK_DAY) {
-            while ($this->_mday > 28 &&
-                   $this->_mday > Horde_Date_Utils::daysInMonth($this->_month, $this->_year)) {
-                if ($down) {
-                    $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month + 1, $this->_year) - Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
-                } else {
-                    $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
-                    $this->_month++;
-                }
-                $this->_correctMonth($down);
-            }
-            while ($this->_mday < 1) {
-                --$this->_month;
-                $this->_correctMonth($down);
-                $this->_mday += Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
-            }
-        }
-    }
-
-    /**
-     * Corrects the current month.
-     *
-     * This cannot be done in _correct() because that would also trigger a
-     * correction of the day, which would result in an infinite loop.
-     *
-     * @param integer $down  Whether to correct the date up or down.
-     */
-    protected function _correctMonth($down = false)
-    {
-        $this->_year += (int)($this->_month / 12);
-        $this->_month %= 12;
-        if ($this->_month < 1) {
-            $this->_year--;
-            $this->_month += 12;
-        }
-    }
-
-    /**
-     * Handles args in order: year month day hour min sec tz
-     */
-    protected function _initializeFromArgs($args)
-    {
-        $tz = (isset($args[6])) ? array_pop($args) : null;
-        $this->_initializeTimezone($tz);
-
-        $args = array_slice($args, 0, 6);
-        $keys = array('year' => 1, 'month' => 1, 'mday' => 1, 'hour' => 0, 'min' => 0, 'sec' => 0);
-        $date = array_combine(array_slice(array_keys($keys), 0, count($args)), $args);
-        $date = array_merge($keys, $date);
-
-        $this->_initializeFromArray($date);
-    }
-
-    protected function _initializeFromArray($date)
-    {
-        if (isset($date['year']) && is_string($date['year']) && strlen($date['year']) == 2) {
-            if ($date['year'] > 70) {
-                $date['year'] += 1900;
-            } else {
-                $date['year'] += 2000;
-            }
-        }
-
-        foreach ($date as $key => $val) {
-            if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
-                $this->{'_'. $key} = (int)$val;
-            }
-        }
-
-        // If $date['day'] is present and numeric we may have been passed
-        // a Horde_Form_datetime array.
-        if (isset($date['day']) &&
-            (string)(int)$date['day'] == $date['day']) {
-            $this->_mday = (int)$date['day'];
-        }
-        // 'minute' key also from Horde_Form_datetime
-        if (isset($date['minute']) &&
-            (string)(int)$date['minute'] == $date['minute']) {
-            $this->_min = (int)$date['minute'];
-        }
-
-        $this->_correct();
-    }
-
-    protected function _initializeFromObject($date)
-    {
-        if ($date instanceof DateTime) {
-            $this->_year  = (int)$date->format('Y');
-            $this->_month = (int)$date->format('m');
-            $this->_mday  = (int)$date->format('d');
-            $this->_hour  = (int)$date->format('H');
-            $this->_min   = (int)$date->format('i');
-            $this->_sec   = (int)$date->format('s');
-            $this->_initializeTimezone($date->getTimezone()->getName());
-        } else {
-            $is_horde_date = $date instanceof Horde_Date;
-            foreach (array('year', 'month', 'mday', 'hour', 'min', 'sec') as $key) {
-                if ($is_horde_date || isset($date->$key)) {
-                    $this->{'_' . $key} = (int)$date->$key;
-                }
-            }
-            if (!$is_horde_date) {
-                $this->_correct();
-            } else {
-                $this->_initializeTimezone($date->timezone);
-            }
-        }
-    }
-
-    protected function _initializeTimezone($timezone)
-    {
-        if (empty($timezone)) {
-            $timezone = date_default_timezone_get();
-        }
-        $this->_timezone = $timezone;
-    }
-
-}
-
-/**
- * @category Horde
- * @package  Date
- */
-
-/**
- * Horde Date wrapper/logic class, including some calculation
- * functions.
- *
- * @category Horde
- * @package  Date
- */
-class Horde_Date_Utils
-{
-    /**
-     * Returns whether a year is a leap year.
-     *
-     * @param integer $year  The year.
-     *
-     * @return boolean  True if the year is a leap year.
-     */
-    public static function isLeapYear($year)
-    {
-        if (strlen($year) != 4 || preg_match('/\D/', $year)) {
-            return false;
-        }
-
-        return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
-    }
-
-    /**
-     * Returns the date of the year that corresponds to the first day of the
-     * given week.
-     *
-     * @param integer $week  The week of the year to find the first day of.
-     * @param integer $year  The year to calculate for.
-     *
-     * @return Horde_Date  The date of the first day of the given week.
-     */
-    public static function firstDayOfWeek($week, $year)
-    {
-        return new Horde_Date(sprintf('%04dW%02d', $year, $week));
-    }
-
-    /**
-     * Returns the number of days in the specified month.
-     *
-     * @param integer $month  The month
-     * @param integer $year   The year.
-     *
-     * @return integer  The number of days in the month.
-     */
-    public static function daysInMonth($month, $year)
-    {
-        static $cache = array();
-        if (!isset($cache[$year][$month])) {
-            $date = new DateTime(sprintf('%04d-%02d-01', $year, $month));
-            $cache[$year][$month] = $date->format('t');
-        }
-        return $cache[$year][$month];
-    }
-
-    /**
-     * Returns a relative, natural language representation of a timestamp
-     *
-     * @todo Wider range of values ... maybe future time as well?
-     * @todo Support minimum resolution parameter.
-     *
-     * @param mixed $time          The time. Any format accepted by Horde_Date.
-     * @param string $date_format  Format to display date if timestamp is
-     *                             more then 1 day old.
-     * @param string $time_format  Format to display time if timestamp is 1
-     *                             day old.
-     *
-     * @return string  The relative time (i.e. 2 minutes ago)
-     */
-    public static function relativeDateTime($time, $date_format = '%x',
-                                            $time_format = '%X')
-    {
-        $date = new Horde_Date($time);
-
-        $delta = time() - $date->timestamp();
-        if ($delta < 60) {
-            return sprintf(Horde_Date_Translation::ngettext("%d second ago", "%d seconds ago", $delta), $delta);
-        }
-
-        $delta = round($delta / 60);
-        if ($delta < 60) {
-            return sprintf(Horde_Date_Translation::ngettext("%d minute ago", "%d minutes ago", $delta), $delta);
-        }
-
-        $delta = round($delta / 60);
-        if ($delta < 24) {
-            return sprintf(Horde_Date_Translation::ngettext("%d hour ago", "%d hours ago", $delta), $delta);
-        }
-
-        if ($delta > 24 && $delta < 48) {
-            $date = new Horde_Date($time);
-            return sprintf(Horde_Date_Translation::t("yesterday at %s"), $date->strftime($time_format));
-        }
-
-        $delta = round($delta / 24);
-        if ($delta < 7) {
-            return sprintf(Horde_Date_Translation::t("%d days ago"), $delta);
-        }
-
-        if (round($delta / 7) < 5) {
-            $delta = round($delta / 7);
-            return sprintf(Horde_Date_Translation::ngettext("%d week ago", "%d weeks ago", $delta), $delta);
-        }
-
-        // Default to the user specified date format.
-        return $date->strftime($date_format);
-    }
-
-    /**
-     * Tries to convert strftime() formatters to date() formatters.
-     *
-     * Unsupported formatters will be removed.
-     *
-     * @param string $format  A strftime() formatting string.
-     *
-     * @return string  A date() formatting string.
-     */
-    public static function strftime2date($format)
-    {
-        $replace = array(
-            '/%a/'  => 'D',
-            '/%A/'  => 'l',
-            '/%d/'  => 'd',
-            '/%e/'  => 'j',
-            '/%j/'  => 'z',
-            '/%u/'  => 'N',
-            '/%w/'  => 'w',
-            '/%U/'  => '',
-            '/%V/'  => 'W',
-            '/%W/'  => '',
-            '/%b/'  => 'M',
-            '/%B/'  => 'F',
-            '/%h/'  => 'M',
-            '/%m/'  => 'm',
-            '/%C/'  => '',
-            '/%g/'  => '',
-            '/%G/'  => 'o',
-            '/%y/'  => 'y',
-            '/%Y/'  => 'Y',
-            '/%H/'  => 'H',
-            '/%I/'  => 'h',
-            '/%i/'  => 'g',
-            '/%M/'  => 'i',
-            '/%p/'  => 'A',
-            '/%P/'  => 'a',
-            '/%r/'  => 'h:i:s A',
-            '/%R/'  => 'H:i',
-            '/%S/'  => 's',
-            '/%T/'  => 'H:i:s',
-            '/%X/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(T_FMT))',
-            '/%z/'  => 'O',
-            '/%Z/'  => '',
-            '/%c/'  => '',
-            '/%D/'  => 'm/d/y',
-            '/%F/'  => 'Y-m-d',
-            '/%s/'  => 'U',
-            '/%x/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(D_FMT))',
-            '/%n/'  => "\n",
-            '/%t/'  => "\t",
-            '/%%/'  => '%'
-        );
-
-        return preg_replace(array_keys($replace), array_values($replace), $format);
-    }
-
-}
diff --git a/plugins/calendar/lib/Horde_iCalendar.php b/plugins/calendar/lib/Horde_iCalendar.php
deleted file mode 100644
index 6d75d27..0000000
--- a/plugins/calendar/lib/Horde_iCalendar.php
+++ /dev/null
@@ -1,3300 +0,0 @@
-<?php
-
-/**
- * This is a concatenated copy of the following files:
- *   Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
- */
-
-if (!class_exists('Horde_Date'))
-    require_once(dirname(__FILE__) . '/Horde_Date.php');
-
-
-$GLOBALS['_HORDE_STRING_CHARSET'] = 'iso-8859-1';
-
-/**
- * The String:: class provides static methods for charset and locale safe
- * string manipulation.
- *
- * $Horde: framework/Util/String.php,v 1.43.6.38 2009-09-15 16:36:14 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Jan Schneider <jan at horde.org>
- * @since   Horde 3.0
- * @package Horde_Util
- */
-class String {
-
-    /**
-     * Caches the result of extension_loaded() calls.
-     *
-     * @param string $ext  The extension name.
-     *
-     * @return boolean  Is the extension loaded?
-     *
-     * @see Util::extensionExists()
-     */
-    function extensionExists($ext)
-    {
-        static $cache = array();
-
-        if (!isset($cache[$ext])) {
-            $cache[$ext] = extension_loaded($ext);
-        }
-
-        return $cache[$ext];
-    }
-
-    /**
-     * Sets a default charset that the String:: methods will use if none is
-     * explicitly specified.
-     *
-     * @param string $charset  The charset to use as the default one.
-     */
-    function setDefaultCharset($charset)
-    {
-        $GLOBALS['_HORDE_STRING_CHARSET'] = $charset;
-        if (String::extensionExists('mbstring') &&
-            function_exists('mb_regex_encoding')) {
-            $old_error = error_reporting(0);
-            mb_regex_encoding(String::_mbstringCharset($charset));
-            error_reporting($old_error);
-        }
-    }
-
-    /**
-     * Converts a string from one charset to another.
-     *
-     * Works only if either the iconv or the mbstring extension
-     * are present and best if both are available.
-     * The original string is returned if conversion failed or none
-     * of the extensions were available.
-     *
-     * @param mixed $input  The data to be converted. If $input is an an array,
-     *                      the array's values get converted recursively.
-     * @param string $from  The string's current charset.
-     * @param string $to    The charset to convert the string to. If not
-     *                      specified, the global variable
-     *                      $_HORDE_STRING_CHARSET will be used.
-     *
-     * @return mixed  The converted input data.
-     */
-    function convertCharset($input, $from, $to = null)
-    {
-        /* Don't bother converting numbers. */
-        if (is_numeric($input)) {
-            return $input;
-        }
-
-        /* Get the user's default character set if none passed in. */
-        if (is_null($to)) {
-            $to = $GLOBALS['_HORDE_STRING_CHARSET'];
-        }
-
-        /* If the from and to character sets are identical, return now. */
-        if ($from == $to) {
-            return $input;
-        }
-        $from = String::lower($from);
-        $to = String::lower($to);
-        if ($from == $to) {
-            return $input;
-        }
-
-        if (is_array($input)) {
-            $tmp = array();
-            reset($input);
-            while (list($key, $val) = each($input)) {
-                $tmp[String::_convertCharset($key, $from, $to)] = String::convertCharset($val, $from, $to);
-            }
-            return $tmp;
-        }
-        if (is_object($input)) {
-            // PEAR_Error objects are almost guaranteed to contain recursion,
-            // which will cause a segfault in PHP.  We should never reach
-            // this line, but add a check and a log message to help the devs
-            // track down and fix this issue.
-            if (is_a($input, 'PEAR_Error')) {
-                Horde::logMessage('Called convertCharset() on a PEAR_Error object. ' . print_r($input, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
-                return '';
-            }
-            $vars = get_object_vars($input);
-            while (list($key, $val) = each($vars)) {
-                $input->$key = String::convertCharset($val, $from, $to);
-            }
-            return $input;
-        }
-
-        if (!is_string($input)) {
-            return $input;
-        }
-
-        return String::_convertCharset($input, $from, $to);
-    }
-
-    /**
-     * Internal function used to do charset conversion.
-     *
-     * @access private
-     *
-     * @param string $input  See String::convertCharset().
-     * @param string $from   See String::convertCharset().
-     * @param string $to     See String::convertCharset().
-     *
-     * @return string  The converted string.
-     */
-    function _convertCharset($input, $from, $to)
-    {
-        $output = '';
-        $from_check = (($from == 'iso-8859-1') || ($from == 'us-ascii'));
-        $to_check = (($to == 'iso-8859-1') || ($to == 'us-ascii'));
-
-        /* Use utf8_[en|de]code() if possible and if the string isn't too
-         * large (less than 16 MB = 16 * 1024 * 1024 = 16777216 bytes) - these
-         * functions use more memory. */
-        if (strlen($input) < 16777216 || !(String::extensionExists('iconv') || String::extensionExists('mbstring'))) {
-            if ($from_check && ($to == 'utf-8')) {
-                return utf8_encode($input);
-            }
-
-            if (($from == 'utf-8') && $to_check) {
-                return utf8_decode($input);
-            }
-        }
-
-        /* First try iconv with transliteration. */
-        if (($from != 'utf7-imap') &&
-            ($to != 'utf7-imap') &&
-            String::extensionExists('iconv')) {
-            /* We need to tack an extra character temporarily because of a bug
-             * in iconv() if the last character is not a 7 bit ASCII
-             * character. */
-            $oldTrackErrors = ini_set('track_errors', 1);
-            unset($php_errormsg);
-            $output = @iconv($from, $to . '//TRANSLIT', $input . 'x');
-            $output = (isset($php_errormsg)) ? false : String::substr($output, 0, -1, $to);
-            ini_set('track_errors', $oldTrackErrors);
-        }
-
-        /* Next try mbstring. */
-        if (!$output && String::extensionExists('mbstring')) {
-            $old_error = error_reporting(0);
-            $output = mb_convert_encoding($input, $to, String::_mbstringCharset($from));
-            error_reporting($old_error);
-        }
-
-        /* At last try imap_utf7_[en|de]code if appropriate. */
-        if (!$output && String::extensionExists('imap')) {
-            if ($from_check && ($to == 'utf7-imap')) {
-                return @imap_utf7_encode($input);
-            }
-            if (($from == 'utf7-imap') && $to_check) {
-                return @imap_utf7_decode($input);
-            }
-        }
-
-        return (!$output) ? $input : $output;
-    }
-
-    /**
-     * Makes a string lowercase.
-     *
-     * @param string  $string   The string to be converted.
-     * @param boolean $locale   If true the string will be converted based on a
-     *                          given charset, locale independent else.
-     * @param string  $charset  If $locale is true, the charset to use when
-     *                          converting. If not provided the current charset.
-     *
-     * @return string  The string with lowercase characters
-     */
-    function lower($string, $locale = false, $charset = null)
-    {
-        static $lowers;
-
-        if ($locale) {
-            /* The existence of mb_strtolower() depends on the platform. */
-            if (String::extensionExists('mbstring') &&
-                function_exists('mb_strtolower')) {
-                if (is_null($charset)) {
-                    $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-                }
-                $old_error = error_reporting(0);
-                $ret = mb_strtolower($string, String::_mbstringCharset($charset));
-                error_reporting($old_error);
-                if (!empty($ret)) {
-                    return $ret;
-                }
-            }
-            return strtolower($string);
-        }
-
-        if (!isset($lowers)) {
-            $lowers = array();
-        }
-        if (!isset($lowers[$string])) {
-            $language = setlocale(LC_CTYPE, 0);
-            setlocale(LC_CTYPE, 'C');
-            $lowers[$string] = strtolower($string);
-            setlocale(LC_CTYPE, $language);
-        }
-
-        return $lowers[$string];
-    }
-
-    /**
-     * Makes a string uppercase.
-     *
-     * @param string  $string   The string to be converted.
-     * @param boolean $locale   If true the string will be converted based on a
-     *                          given charset, locale independent else.
-     * @param string  $charset  If $locale is true, the charset to use when
-     *                          converting. If not provided the current charset.
-     *
-     * @return string  The string with uppercase characters
-     */
-    function upper($string, $locale = false, $charset = null)
-    {
-        static $uppers;
-
-        if ($locale) {
-            /* The existence of mb_strtoupper() depends on the
-             * platform. */
-            if (function_exists('mb_strtoupper')) {
-                if (is_null($charset)) {
-                    $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-                }
-                $old_error = error_reporting(0);
-                $ret = mb_strtoupper($string, String::_mbstringCharset($charset));
-                error_reporting($old_error);
-                if (!empty($ret)) {
-                    return $ret;
-                }
-            }
-            return strtoupper($string);
-        }
-
-        if (!isset($uppers)) {
-            $uppers = array();
-        }
-        if (!isset($uppers[$string])) {
-            $language = setlocale(LC_CTYPE, 0);
-            setlocale(LC_CTYPE, 'C');
-            $uppers[$string] = strtoupper($string);
-            setlocale(LC_CTYPE, $language);
-        }
-
-        return $uppers[$string];
-    }
-
-    /**
-     * Returns a string with the first letter capitalized if it is
-     * alphabetic.
-     *
-     * @param string  $string   The string to be capitalized.
-     * @param boolean $locale   If true the string will be converted based on a
-     *                          given charset, locale independent else.
-     * @param string  $charset  The charset to use, defaults to current charset.
-     *
-     * @return string  The capitalized string.
-     */
-    function ucfirst($string, $locale = false, $charset = null)
-    {
-        if ($locale) {
-            $first = String::substr($string, 0, 1, $charset);
-            if (String::isAlpha($first, $charset)) {
-                $string = String::upper($first, true, $charset) . String::substr($string, 1, null, $charset);
-            }
-        } else {
-            $string = String::upper(substr($string, 0, 1), false) . substr($string, 1);
-        }
-        return $string;
-    }
-
-    /**
-     * Returns part of a string.
-     *
-     * @param string $string   The string to be converted.
-     * @param integer $start   The part's start position, zero based.
-     * @param integer $length  The part's length.
-     * @param string $charset  The charset to use when calculating the part's
-     *                         position and length, defaults to current
-     *                         charset.
-     *
-     * @return string  The string's part.
-     */
-    function substr($string, $start, $length = null, $charset = null)
-    {
-        if (is_null($length)) {
-            $length = String::length($string, $charset) - $start;
-        }
-
-        if ($length == 0) {
-            return '';
-        }
-
-        /* Try iconv. */
-        if (function_exists('iconv_substr')) {
-            if (is_null($charset)) {
-                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-            }
-
-            $old_error = error_reporting(0);
-            $ret = iconv_substr($string, $start, $length, $charset);
-            error_reporting($old_error);
-            /* iconv_substr() returns false on failure. */
-            if ($ret !== false) {
-                return $ret;
-            }
-        }
-
-        /* Try mbstring. */
-        if (String::extensionExists('mbstring')) {
-            if (is_null($charset)) {
-                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-            }
-            $old_error = error_reporting(0);
-            $ret = mb_substr($string, $start, $length, String::_mbstringCharset($charset));
-            error_reporting($old_error);
-            /* mb_substr() returns empty string on failure. */
-            if (strlen($ret)) {
-                return $ret;
-            }
-        }
-
-        return substr($string, $start, $length);
-    }
-
-    /**
-     * Returns the character (not byte) length of a string.
-     *
-     * @param string $string  The string to return the length of.
-     * @param string $charset The charset to use when calculating the string's
-     *                        length.
-     *
-     * @return string  The string's part.
-     */
-    function length($string, $charset = null)
-    {
-        if (is_null($charset)) {
-            $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-        }
-        $charset = String::lower($charset);
-        if ($charset == 'utf-8' || $charset == 'utf8') {
-            return strlen(utf8_decode($string));
-        }
-        if (String::extensionExists('mbstring')) {
-            $old_error = error_reporting(0);
-            $ret = mb_strlen($string, String::_mbstringCharset($charset));
-            error_reporting($old_error);
-            if (!empty($ret)) {
-                return $ret;
-            }
-        }
-        return strlen($string);
-    }
-
-    /**
-     * Returns the numeric position of the first occurrence of $needle
-     * in the $haystack string.
-     *
-     * @param string $haystack  The string to search through.
-     * @param string $needle    The string to search for.
-     * @param integer $offset   Allows to specify which character in haystack
-     *                          to start searching.
-     * @param string $charset   The charset to use when searching for the
-     *                          $needle string.
-     *
-     * @return integer  The position of first occurrence.
-     */
-    function pos($haystack, $needle, $offset = 0, $charset = null)
-    {
-        if (String::extensionExists('mbstring')) {
-            if (is_null($charset)) {
-                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-            }
-            $track_errors = ini_set('track_errors', 1);
-            $old_error = error_reporting(0);
-            $ret = mb_strpos($haystack, $needle, $offset, String::_mbstringCharset($charset));
-            error_reporting($old_error);
-            ini_set('track_errors', $track_errors);
-            if (!isset($php_errormsg)) {
-                return $ret;
-            }
-        }
-        return strpos($haystack, $needle, $offset);
-    }
-
-    /**
-     * Returns a string padded to a certain length with another string.
-     *
-     * This method behaves exactly like str_pad but is multibyte safe.
-     *
-     * @param string $input    The string to be padded.
-     * @param integer $length  The length of the resulting string.
-     * @param string $pad      The string to pad the input string with. Must
-     *                         be in the same charset like the input string.
-     * @param const $type      The padding type. One of STR_PAD_LEFT,
-     *                         STR_PAD_RIGHT, or STR_PAD_BOTH.
-     * @param string $charset  The charset of the input and the padding
-     *                         strings.
-     *
-     * @return string  The padded string.
-     */
-    function pad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT,
-                 $charset = null)
-    {
-        $mb_length = String::length($input, $charset);
-        $sb_length = strlen($input);
-        $pad_length = String::length($pad, $charset);
-
-        /* Return if we already have the length. */
-        if ($mb_length >= $length) {
-            return $input;
-        }
-
-        /* Shortcut for single byte strings. */
-        if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
-            return str_pad($input, $length, $pad, $type);
-        }
-
-        switch ($type) {
-        case STR_PAD_LEFT:
-            $left = $length - $mb_length;
-            $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) . $input;
-            break;
-        case STR_PAD_BOTH:
-            $left = floor(($length - $mb_length) / 2);
-            $right = ceil(($length - $mb_length) / 2);
-            $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) .
-                $input .
-                String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
-            break;
-        case STR_PAD_RIGHT:
-            $right = $length - $mb_length;
-            $output = $input . String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
-            break;
-        }
-
-        return $output;
-    }
-
-    /**
-     * Wraps the text of a message.
-     *
-     * @since Horde 3.2
-     *
-     * @param string $string         String containing the text to wrap.
-     * @param integer $width         Wrap the string at this number of
-     *                               characters.
-     * @param string $break          Character(s) to use when breaking lines.
-     * @param boolean $cut           Whether to cut inside words if a line
-     *                               can't be wrapped.
-     * @param string $charset        Character set to use when breaking lines.
-     * @param boolean $line_folding  Whether to apply line folding rules per
-     *                               RFC 822 or similar. The correct break
-     *                               characters including leading whitespace
-     *                               have to be specified too.
-     *
-     * @return string  String containing the wrapped text.
-     */
-    function wordwrap($string, $width = 75, $break = "\n", $cut = false,
-                      $charset = null, $line_folding = false)
-    {
-        /* Get the user's default character set if none passed in. */
-        if (is_null($charset)) {
-            $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
-        }
-        $charset = String::_mbstringCharset($charset);
-        $string = String::convertCharset($string, $charset, 'utf-8');
-        $wrapped = '';
-
-        while (String::length($string, 'utf-8') > $width) {
-            $line = String::substr($string, 0, $width, 'utf-8');
-            $string = String::substr($string, String::length($line, 'utf-8'), null, 'utf-8');
-            // Make sure didn't cut a word, unless we want hard breaks anyway.
-            if (!$cut && preg_match('/^(.+?)((\s|\r?\n).*)/us', $string, $match)) {
-                $line .= $match[1];
-                $string = $match[2];
-            }
-            // Wrap at existing line breaks.
-            if (preg_match('/^(.*?)(\r?\n)(.*)$/u', $line, $match)) {
-                $wrapped .= $match[1] . $match[2];
-                $string = $match[3] . $string;
-                continue;
-            }
-            // Wrap at the last colon or semicolon followed by a whitespace if
-            // doing line folding.
-            if ($line_folding &&
-                preg_match('/^(.*?)(;|:)(\s+.*)$/u', $line, $match)) {
-                $wrapped .= $match[1] . $match[2] . $break;
-                $string = $match[3] . $string;
-                continue;
-            }
-            // Wrap at the last whitespace of $line.
-            if ($line_folding) {
-                $sub = '(.+[^\s])';
-            } else {
-                $sub = '(.*)';
-            }
-            if (preg_match('/^' . $sub . '(\s+)(.*)$/u', $line, $match)) {
-                $wrapped .= $match[1] . $break;
-                $string = ($line_folding ? $match[2] : '') . $match[3] . $string;
-                continue;
-            }
-            // Hard wrap if necessary.
-            if ($cut) {
-                $wrapped .= $line . $break;
-                continue;
-            }
-            $wrapped .= $line;
-        }
-
-        return String::convertCharset($wrapped . $string, 'utf-8', $charset);
-    }
-
-    /**
-     * Wraps the text of a message.
-     *
-     * @param string $text        String containing the text to wrap.
-     * @param integer $length     Wrap $text at this number of characters.
-     * @param string $break_char  Character(s) to use when breaking lines.
-     * @param string $charset     Character set to use when breaking lines.
-     * @param boolean $quote      Ignore lines that are wrapped with the '>'
-     *                            character (RFC 2646)? If true, we don't
-     *                            remove any padding whitespace at the end of
-     *                            the string.
-     *
-     * @return string  String containing the wrapped text.
-     */
-    function wrap($text, $length = 80, $break_char = "\n", $charset = null,
-                  $quote = false)
-    {
-        $paragraphs = array();
-
-        foreach (preg_split('/\r?\n/', $text) as $input) {
-            if ($quote && (strpos($input, '>') === 0)) {
-                $line = $input;
-            } else {
-                /* We need to handle the Usenet-style signature line
-                 * separately; since the space after the two dashes is
-                 * REQUIRED, we don't want to trim the line. */
-                if ($input != '-- ') {
-                    $input = rtrim($input);
-                }
-                $line = String::wordwrap($input, $length, $break_char, false, $charset);
-            }
-
-            $paragraphs[] = $line;
-        }
-
-        return implode($break_char, $paragraphs);
-    }
-
-    /**
-     * Returns true if the every character in the parameter is an alphabetic
-     * character.
-     *
-     * @param $string   The string to test.
-     * @param $charset  The charset to use when testing the string.
-     *
-     * @return boolean  True if the parameter was alphabetic only.
-     */
-    function isAlpha($string, $charset = null)
-    {
-        if (!String::extensionExists('mbstring')) {
-            return ctype_alpha($string);
-        }
-
-        $charset = String::_mbstringCharset($charset);
-        $old_charset = mb_regex_encoding();
-        $old_error = error_reporting(0);
-
-        if ($charset != $old_charset) {
-            mb_regex_encoding($charset);
-        }
-        $alpha = !mb_ereg_match('[^[:alpha:]]', $string);
-        if ($charset != $old_charset) {
-            mb_regex_encoding($old_charset);
-        }
-
-        error_reporting($old_error);
-
-        return $alpha;
-    }
-
-    /**
-     * Returns true if ever character in the parameter is a lowercase letter in
-     * the current locale.
-     *
-     * @param $string   The string to test.
-     * @param $charset  The charset to use when testing the string.
-     *
-     * @return boolean  True if the parameter was lowercase.
-     */
-    function isLower($string, $charset = null)
-    {
-        return ((String::lower($string, true, $charset) === $string) &&
-                String::isAlpha($string, $charset));
-    }
-
-    /**
-     * Returns true if every character in the parameter is an uppercase letter
-     * in the current locale.
-     *
-     * @param string $string   The string to test.
-     * @param string $charset  The charset to use when testing the string.
-     *
-     * @return boolean  True if the parameter was uppercase.
-     */
-    function isUpper($string, $charset = null)
-    {
-        return ((String::upper($string, true, $charset) === $string) &&
-                String::isAlpha($string, $charset));
-    }
-
-    /**
-     * Performs a multibyte safe regex match search on the text provided.
-     *
-     * @since Horde 3.1
-     *
-     * @param string $text     The text to search.
-     * @param array $regex     The regular expressions to use, without perl
-     *                         regex delimiters (e.g. '/' or '|').
-     * @param string $charset  The character set of the text.
-     *
-     * @return array  The matches array from the first regex that matches.
-     */
-    function regexMatch($text, $regex, $charset = null)
-    {
-        if (!empty($charset)) {
-            $regex = String::convertCharset($regex, $charset, 'utf-8');
-            $text = String::convertCharset($text, $charset, 'utf-8');
-        }
-
-        $matches = array();
-        foreach ($regex as $val) {
-            if (preg_match('/' . $val . '/u', $text, $matches)) {
-                break;
-            }
-        }
-
-        if (!empty($charset)) {
-            $matches = String::convertCharset($matches, 'utf-8', $charset);
-        }
-
-        return $matches;
-    }
-
-    /**
-     * Workaround charsets that don't work with mbstring functions.
-     *
-     * @access private
-     *
-     * @param string $charset  The original charset.
-     *
-     * @return string  The charset to use with mbstring functions.
-     */
-    function _mbstringCharset($charset)
-    {
-        /* mbstring functions do not handle the 'ks_c_5601-1987' &
-         * 'ks_c_5601-1989' charsets. However, these charsets are used, for
-         * example, by various versions of Outlook to send Korean characters.
-         * Use UHC (CP949) encoding instead. See, e.g.,
-         * http://lists.w3.org/Archives/Public/ietf-charsets/2001AprJun/0030.html */
-        if (in_array(String::lower($charset), array('ks_c_5601-1987', 'ks_c_5601-1989'))) {
-            $charset = 'UHC';
-        }
-
-        return $charset;
-    }
-
-}
-
-
-
-/**
- * @package Horde_iCalendar
- */
-
-/**
- * String package
- */
-
-
-
-/**
- * Class representing iCalendar files.
- *
- * $Horde: framework/iCalendar/iCalendar.php,v 1.57.4.81 2010-11-10 14:34:25 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar {
-
-    /**
-     * The parent (containing) iCalendar object.
-     *
-     * @var Horde_iCalendar
-     */
-    var $_container = false;
-
-    /**
-     * The name/value pairs of attributes for this object (UID,
-     * DTSTART, etc.). Which are present depends on the object and on
-     * what kind of component it is.
-     *
-     * @var array
-     */
-    var $_attributes = array();
-
-    /**
-     * Any children (contained) iCalendar components of this object.
-     *
-     * @var array
-     */
-    var $_components = array();
-
-    /**
-     * According to RFC 2425, we should always use CRLF-terminated lines.
-     *
-     * @var string
-     */
-    var $_newline = "\r\n";
-
-    /**
-     * iCalendar format version (different behavior for 1.0 and 2.0
-     * especially with recurring events).
-     *
-     * @var string
-     */
-    var $_version;
-
-    function Horde_iCalendar($version = '2.0')
-    {
-        $this->_version = $version;
-        $this->setAttribute('VERSION', $version);
-    }
-
-    /**
-     * Return a reference to a new component.
-     *
-     * @param string          $type       The type of component to return
-     * @param Horde_iCalendar $container  A container that this component
-     *                                    will be associated with.
-     *
-     * @return object  Reference to a Horde_iCalendar_* object as specified.
-     *
-     * @static
-     */
-    function &newComponent($type, &$container)
-    {
-        $type = String::lower($type);
-        $class = 'Horde_iCalendar_' . $type;
-        if (!class_exists($class)) {
-            include 'Horde/iCalendar/' . $type . '.php';
-        }
-        if (class_exists($class)) {
-            $component = new $class();
-            if ($container !== false) {
-                $component->_container = &$container;
-                // Use version of container, not default set by component
-                // constructor.
-                $component->_version = $container->_version;
-            }
-        } else {
-            // Should return an dummy x-unknown type class here.
-            $component = false;
-        }
-
-        return $component;
-    }
-
-    /**
-     * Sets the value of an attribute.
-     *
-     * @param string $name     The name of the attribute.
-     * @param string $value    The value of the attribute.
-     * @param array $params    Array containing any addition parameters for
-     *                         this attribute.
-     * @param boolean $append  True to append the attribute, False to replace
-     *                         the first matching attribute found.
-     * @param array $values    Array representation of $value.  For
-     *                         comma/semicolon seperated lists of values.  If
-     *                         not set use $value as single array element.
-     */
-    function setAttribute($name, $value, $params = array(), $append = true,
-                          $values = false)
-    {
-        // Make sure we update the internal format version if
-        // setAttribute('VERSION', ...) is called.
-        if ($name == 'VERSION') {
-            $this->_version = $value;
-            if ($this->_container !== false) {
-                $this->_container->_version = $value;
-            }
-        }
-
-        if (!$values) {
-            $values = array($value);
-        }
-        $found = false;
-        if (!$append) {
-            foreach (array_keys($this->_attributes) as $key) {
-                if ($this->_attributes[$key]['name'] == String::upper($name)) {
-                    $this->_attributes[$key]['params'] = $params;
-                    $this->_attributes[$key]['value'] = $value;
-                    $this->_attributes[$key]['values'] = $values;
-                    $found = true;
-                    break;
-                }
-            }
-        }
-
-        if ($append || !$found) {
-            $this->_attributes[] = array(
-                'name'      => String::upper($name),
-                'params'    => $params,
-                'value'     => $value,
-                'values'    => $values
-            );
-        }
-    }
-
-    /**
-     * Sets parameter(s) for an (already existing) attribute.  The
-     * parameter set is merged into the existing set.
-     *
-     * @param string $name   The name of the attribute.
-     * @param array $params  Array containing any additional parameters for
-     *                       this attribute.
-     * @return boolean  True on success, false if no attribute $name exists.
-     */
-    function setParameter($name, $params = array())
-    {
-        $keys = array_keys($this->_attributes);
-        foreach ($keys as $key) {
-            if ($this->_attributes[$key]['name'] == $name) {
-                $this->_attributes[$key]['params'] =
-                    array_merge($this->_attributes[$key]['params'], $params);
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Get the value of an attribute.
-     *
-     * @param string $name     The name of the attribute.
-     * @param boolean $params  Return the parameters for this attribute instead
-     *                         of its value.
-     *
-     * @return mixed (object)  PEAR_Error if the attribute does not exist.
-     *               (string)  The value of the attribute.
-     *               (array)   The parameters for the attribute or
-     *                         multiple values for an attribute.
-     */
-    function getAttribute($name, $params = false)
-    {
-        $result = array();
-        foreach ($this->_attributes as $attribute) {
-            if ($attribute['name'] == $name) {
-                if ($params) {
-                    $result[] = $attribute['params'];
-                } else {
-                    $result[] = $attribute['value'];
-                }
-            }
-        }
-        if (!count($result)) {
-            require_once 'PEAR.php';
-            return PEAR::raiseError('Attribute "' . $name . '" Not Found');
-        } if (count($result) == 1 && !$params) {
-            return $result[0];
-        } else {
-            return $result;
-        }
-    }
-
-    /**
-     * Gets the values of an attribute as an array.  Multiple values
-     * are possible due to:
-     *
-     *  a) multiplce occurences of 'name'
-     *  b) (unsecapd) comma seperated lists.
-     *
-     * So for a vcard like "KEY:a,b\nKEY:c" getAttributesValues('KEY')
-     * will return array('a', 'b', 'c').
-     *
-     * @param string  $name    The name of the attribute.
-     * @return mixed (object)  PEAR_Error if the attribute does not exist.
-     *               (array)   Multiple values for an attribute.
-     */
-    function getAttributeValues($name)
-    {
-        $result = array();
-        foreach ($this->_attributes as $attribute) {
-            if ($attribute['name'] == $name) {
-                $result = array_merge($attribute['values'], $result);
-            }
-        }
-        if (!count($result)) {
-            return PEAR::raiseError('Attribute "' . $name . '" Not Found');
-        }
-        return $result;
-    }
-
-    /**
-     * Returns the value of an attribute, or a specified default value
-     * if the attribute does not exist.
-     *
-     * @param string $name    The name of the attribute.
-     * @param mixed $default  What to return if the attribute specified by
-     *                        $name does not exist.
-     *
-     * @return mixed (string) The value of $name.
-     *               (mixed)  $default if $name does not exist.
-     */
-    function getAttributeDefault($name, $default = '')
-    {
-        $value = $this->getAttribute($name);
-        return is_a($value, 'PEAR_Error') ? $default : $value;
-    }
-
-    /**
-     * Remove all occurences of an attribute.
-     *
-     * @param string $name  The name of the attribute.
-     */
-    function removeAttribute($name)
-    {
-        $keys = array_keys($this->_attributes);
-        foreach ($keys as $key) {
-            if ($this->_attributes[$key]['name'] == $name) {
-                unset($this->_attributes[$key]);
-            }
-        }
-    }
-
-    /**
-     * Get attributes for all tags or for a given tag.
-     *
-     * @param string $tag  Return attributes for this tag, or all attributes if
-     *                     not given.
-     *
-     * @return array  An array containing all the attributes and their types.
-     */
-    function getAllAttributes($tag = false)
-    {
-        if ($tag === false) {
-            return $this->_attributes;
-        }
-        $result = array();
-        foreach ($this->_attributes as $attribute) {
-            if ($attribute['name'] == $tag) {
-                $result[] = $attribute;
-            }
-        }
-        return $result;
-    }
-
-    /**
-     * Add a vCalendar component (eg vEvent, vTimezone, etc.).
-     *
-     * @param Horde_iCalendar $component  Component (subclass) to add.
-     */
-    function addComponent($component)
-    {
-        if (is_a($component, 'Horde_iCalendar')) {
-            $component->_container = &$this;
-            $this->_components[] = &$component;
-        }
-    }
-
-    /**
-     * Retrieve all the components.
-     *
-     * @return array  Array of Horde_iCalendar objects.
-     */
-    function getComponents()
-    {
-        return $this->_components;
-    }
-
-    function getType()
-    {
-        return 'vcalendar';
-    }
-
-    /**
-     * Return the classes (entry types) we have.
-     *
-     * @return array  Hash with class names Horde_iCalendar_xxx as keys
-     *                and number of components of this class as value.
-     */
-    function getComponentClasses()
-    {
-        $r = array();
-        foreach ($this->_components as $c) {
-            $cn = strtolower(get_class($c));
-            if (empty($r[$cn])) {
-                $r[$cn] = 1;
-            } else {
-                $r[$cn]++;
-            }
-        }
-
-        return $r;
-    }
-
-    /**
-     * Number of components in this container.
-     *
-     * @return integer  Number of components in this container.
-     */
-    function getComponentCount()
-    {
-        return count($this->_components);
-    }
-
-    /**
-     * Retrieve a specific component.
-     *
-     * @param integer $idx  The index of the object to retrieve.
-     *
-     * @return mixed    (boolean) False if the index does not exist.
-     *                  (Horde_iCalendar_*) The requested component.
-     */
-    function getComponent($idx)
-    {
-        if (isset($this->_components[$idx])) {
-            return $this->_components[$idx];
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Locates the first child component of the specified class, and returns a
-     * reference to it.
-     *
-     * @param string $type  The type of component to find.
-     *
-     * @return boolean|Horde_iCalendar_*  False if no subcomponent of the
-     *                                    specified class exists or a reference
-     *                                    to the requested component.
-     */
-    function &findComponent($childclass)
-    {
-        $childclass = 'Horde_iCalendar_' . String::lower($childclass);
-        $keys = array_keys($this->_components);
-        foreach ($keys as $key) {
-            if (is_a($this->_components[$key], $childclass)) {
-                return $this->_components[$key];
-            }
-        }
-
-        $component = false;
-        return $component;
-    }
-
-    /**
-     * Locates the first matching child component of the specified class, and
-     * returns a reference to it.
-     *
-     * @param string $childclass  The type of component to find.
-     * @param string $attribute   This attribute must be set in the component
-     *                            for it to match.
-     * @param string $value       Optional value that $attribute must match.
-     *
-     * @return boolean|Horde_iCalendar_*  False if no matching subcomponent of
-     *                                    the specified class exists, or a
-     *                                    reference to the requested component.
-     */
-    function &findComponentByAttribute($childclass, $attribute, $value = null)
-    {
-        $childclass = 'Horde_iCalendar_' . String::lower($childclass);
-        $keys = array_keys($this->_components);
-        foreach ($keys as $key) {
-            if (is_a($this->_components[$key], $childclass)) {
-                $attr = $this->_components[$key]->getAttribute($attribute);
-                if (is_a($attr, 'PEAR_Error')) {
-                    continue;
-                }
-                if ($value !== null && $value != $attr) {
-                    continue;
-                }
-                return $this->_components[$key];
-            }
-        }
-
-        $component = false;
-        return $component;
-    }
-
-    /**
-     * Clears the iCalendar object (resets the components and attributes
-     * arrays).
-     */
-    function clear()
-    {
-        $this->_components = array();
-        $this->_attributes = array();
-    }
-
-    /**
-     * Checks if entry is vcalendar 1.0, vcard 2.1 or vnote 1.1.
-     *
-     * These 'old' formats are defined by www.imc.org. The 'new' (non-old)
-     * formats icalendar 2.0 and vcard 3.0 are defined in rfc2426 and rfc2445
-     * respectively.
-     *
-     * @since Horde 3.1.2
-     */
-    function isOldFormat()
-    {
-        if ($this->getType() == 'vcard') {
-            return ($this->_version < 3);
-        }
-        if ($this->getType() == 'vNote') {
-            return ($this->_version < 2);
-        }
-        if ($this->_version >= 2) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Export as vCalendar format.
-     */
-    function exportvCalendar()
-    {
-        // Default values.
-        $requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
-        $requiredAttributes['METHOD'] = 'PUBLISH';
-
-        foreach ($requiredAttributes as $name => $default_value) {
-            if (is_a($this->getattribute($name), 'PEAR_Error')) {
-                $this->setAttribute($name, $default_value);
-            }
-        }
-
-        return $this->_exportvData('VCALENDAR');
-    }
-
-    /**
-     * Export this entry as a hash array with tag names as keys.
-     *
-     * @param boolean $paramsInKeys
-     *                If false, the operation can be quite lossy as the
-     *                parameters are ignored when building the array keys.
-     *                So if you export a vcard with
-     *                LABEL;TYPE=WORK:foo
-     *                LABEL;TYPE=HOME:bar
-     *                the resulting hash contains only one label field!
-     *                If set to true, array keys look like 'LABEL;TYPE=WORK'
-     * @return array  A hash array with tag names as keys.
-     */
-    function toHash($paramsInKeys = false)
-    {
-        $hash = array();
-        foreach ($this->_attributes as $a)  {
-            $k = $a['name'];
-            if ($paramsInKeys && is_array($a['params'])) {
-                foreach ($a['params'] as $p => $v) {
-                    $k .= ";$p=$v";
-                }
-            }
-            $hash[$k] = $a['value'];
-        }
-
-        return $hash;
-    }
-
-    /**
-     * Parses a string containing vCalendar data.
-     *
-     * @todo This method doesn't work well at all, if $base is VCARD.
-     *
-     * @param string $text     The data to parse.
-     * @param string $base     The type of the base object.
-     * @param string $charset  The encoding charset for $text. Defaults to
-     *                         utf-8 for new format, iso-8859-1 for old format.
-     * @param boolean $clear   If true clears the iCal object before parsing.
-     *
-     * @return boolean  True on successful import, false otherwise.
-     */
-    function parsevCalendar($text, $base = 'VCALENDAR', $charset = null,
-                            $clear = true)
-    {
-        if ($clear) {
-            $this->clear();
-        }
-        if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) {
-            $container = true;
-            $vCal = $matches[1];
-        } else {
-            // Text isn't enclosed in BEGIN:VCALENDAR
-            // .. END:VCALENDAR. We'll try to parse it anyway.
-            $container = false;
-            $vCal = $text;
-        }
-        $vCal = trim($vCal);
-
-        // Extract all subcomponents.
-        $matches = $components = null;
-        if (preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $components)) {
-            foreach ($components[0] as $key => $data) {
-                // Remove from the vCalendar data.
-                $vCal = str_replace($data, '', $vCal);
-            }
-        } elseif (!$container) {
-            return false;
-        }
-
-        // Unfold "quoted printable" folded lines like:
-        //  BODY;ENCODING=QUOTED-PRINTABLE:=
-        //  another=20line=
-        //  last=20line
-        while (preg_match_all('/^([^:]+;\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\r?\n)+(.*[^=])?\r?\n)/mU', $vCal, $matches)) {
-            foreach ($matches[1] as $s) {
-                $r = preg_replace('/=\r?\n/', '', $s);
-                $vCal = str_replace($s, $r, $vCal);
-            }
-        }
-
-        // Unfold any folded lines.
-        if ($this->isOldFormat()) {
-            $vCal = preg_replace('/[\r\n]+([ \t])/', '$1', $vCal);
-        } else {
-            $vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal);
-        }
-
-        // Parse the remaining attributes.
-        if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\r\n]*)\r?$/m', $vCal, $matches)) {
-            foreach ($matches[0] as $attribute) {
-                preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts);
-                $tag = trim(String::upper($parts[1]));
-                $value = $parts[4];
-                $params = array();
-
-                // Parse parameters.
-                if (!empty($parts[2])) {
-                    preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts);
-                    foreach ($param_parts[2] as $key => $paramName) {
-                        $paramName = String::upper($paramName);
-                        $paramValue = $param_parts[4][$key];
-                        if ($paramName == 'TYPE') {
-                            $paramValue = preg_split('/(?<!\\\\),/', $paramValue);
-                            if (count($paramValue) == 1) {
-                                $paramValue = $paramValue[0];
-                            }
-                        }
-                        if (is_string($paramValue)) {
-                            if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
-                                $paramValue = $parts[1];
-                            }
-                        } else {
-                            foreach ($paramValue as $k => $tmp) {
-                                if (preg_match('/"([^"]*)"/', $tmp, $parts)) {
-                                    $paramValue[$k] = $parts[1];
-                                }
-                            }
-                        }
-                        $params[$paramName] = $paramValue;
-                    }
-                }
-
-                // Charset and encoding handling.
-                if ((isset($params['ENCODING']) &&
-                     String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') ||
-                    isset($params['QUOTED-PRINTABLE'])) {
-
-                    $value = quoted_printable_decode($value);
-                    if (isset($params['CHARSET'])) {
-                        $value = String::convertCharset($value, $params['CHARSET']);
-                    } else {
-                        $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
-                    }
-                } elseif (isset($params['CHARSET'])) {
-                    $value = String::convertCharset($value, $params['CHARSET']);
-                } else {
-                    // As per RFC 2279, assume UTF8 if we don't have an
-                    // explicit charset parameter.
-                    $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
-                }
-
-                // Get timezone info for date fields from $params.
-                $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false;
-
-                switch ($tag) {
-                // Date fields.
-                case 'COMPLETED':
-                case 'CREATED':
-                case 'LAST-MODIFIED':
-                case 'X-MOZ-LASTACK': 
-                case 'X-MOZ-SNOOZE-TIME': 
-                    $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
-                    break;
-
-                case 'BDAY':
-                case 'X-SYNCJE-ANNIVERSARY':
-                case 'X-ANNIVERSARY':
-                    $this->setAttribute($tag, $this->_parseDate($value), $params);
-                    break;
-
-                case 'DTEND':
-                case 'DTSTART':
-                case 'DTSTAMP':
-                case 'DUE':
-                case 'AALARM':
-                case 'RECURRENCE-ID':
-                    // types like AALARM may contain additional data after a ;
-                    // ignore these.
-                    $ts = explode(';', $value);
-                    if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') {
-                        $this->setAttribute($tag, $this->_parseDate($ts[0]), $params);
-                    } else {
-                        $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params);
-                    }
-                    break;
-
-                case 'TRIGGER':
-                    if (isset($params['VALUE']) &&
-                        $params['VALUE'] == 'DATE-TIME') {
-                            $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
-                    } else {
-                        $this->setAttribute($tag, $this->_parseDuration($value), $params);
-                    }
-                    break;
-
-                // Comma seperated dates.
-                case 'EXDATE':
-                case 'RDATE':
-                    if (!strlen($value)) {
-                        break;
-                    }
-                    $dates = array();
-                    $separator = $this->isOldFormat() ? ';' : ',';
-                    preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values);
-
-                    foreach ($values[1] as $value) {
-                        $dates[] = $this->_parseDate($value);
-                    }
-                    $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates);
-                    break;
-
-                // Duration fields.
-                case 'DURATION':
-                    $this->setAttribute($tag, $this->_parseDuration($value), $params);
-                    break;
-
-                // Period of time fields.
-                case 'FREEBUSY':
-                    $periods = array();
-                    preg_match_all('/,([^,]*)/', ',' . $value, $values);
-                    foreach ($values[1] as $value) {
-                        $periods[] = $this->_parsePeriod($value);
-                    }
-
-                    $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods);
-                    break;
-
-                // UTC offset fields.
-                case 'TZOFFSETFROM':
-                case 'TZOFFSETTO':
-                    $this->setAttribute($tag, $this->_parseUtcOffset($value), $params);
-                    break;
-
-                // Integer fields.
-                case 'PERCENT-COMPLETE':
-                case 'PRIORITY':
-                case 'REPEAT':
-                case 'SEQUENCE':
-                    $this->setAttribute($tag, intval($value), $params);
-                    break;
-
-                // Geo fields.
-                case 'GEO':
-                    if ($this->isOldFormat()) {
-                        $floats = explode(',', $value);
-                        $value = array('latitude' => floatval($floats[1]),
-                                       'longitude' => floatval($floats[0]));
-                    } else {
-                        $floats = explode(';', $value);
-                        $value = array('latitude' => floatval($floats[0]),
-                                       'longitude' => floatval($floats[1]));
-                    }
-                    $this->setAttribute($tag, $value, $params);
-                    break;
-
-                // Recursion fields.
-                case 'EXRULE':
-                case 'RRULE':
-                    $this->setAttribute($tag, trim($value), $params);
-                    break;
-
-                // ADR, ORG and N are lists seperated by unescaped semicolons
-                // with a specific number of slots.
-                case 'ADR':
-                case 'N':
-                case 'ORG':
-                    $value = trim($value);
-                    // As of rfc 2426 2.4.2 semicolon, comma, and colon must
-                    // be escaped (comma is unescaped after splitting below).
-                    $value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
-                                         array($this->_newline, $this->_newline, ';', ':'),
-                                         $value);
-
-                    // Split by unescaped semicolons:
-                    $values = preg_split('/(?<!\\\\);/', $value);
-                    $value = str_replace('\\;', ';', $value);
-                    $values = str_replace('\\;', ';', $values);
-                    $this->setAttribute($tag, trim($value), $params, true, $values);
-                    break;
-
-                // String fields.
-                default:
-                    if ($this->isOldFormat()) {
-                        // vCalendar 1.0 and vCard 2.1 only escape semicolons
-                        // and use unescaped semicolons to create lists.
-                        $value = trim($value);
-                        // Split by unescaped semicolons:
-                        $values = preg_split('/(?<!\\\\);/', $value);
-                        $value = str_replace('\\;', ';', $value);
-                        $values = str_replace('\\;', ';', $values);
-                        $this->setAttribute($tag, trim($value), $params, true, $values);
-                    } else {
-                        $value = trim($value);
-                        // As of rfc 2426 2.4.2 semicolon, comma, and colon
-                        // must be escaped (comma is unescaped after splitting
-                        // below).
-                        $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'),
-                                             array($this->_newline, $this->_newline, ';', ':', '\\'),
-                                             $value);
-
-                        // Split by unescaped commas.
-                        $values = preg_split('/(?<!\\\\),/', $value);
-                        $value = str_replace('\\,', ',', $value);
-                        $values = str_replace('\\,', ',', $values);
-
-                        $this->setAttribute($tag, trim($value), $params, true, $values);
-                    }
-                    break;
-                }
-            }
-        }
-
-        // Process all components.
-        if ($components) {
-            // vTimezone components are processed first. They are
-            // needed to process vEvents that may use a TZID.
-            foreach ($components[0] as $key => $data) {
-                $type = trim($components[1][$key]);
-                if ($type != 'VTIMEZONE') {
-                    continue;
-                }
-                $component = &Horde_iCalendar::newComponent($type, $this);
-                if ($component === false) {
-                    return PEAR::raiseError("Unable to create object for type $type");
-                }
-                $component->parsevCalendar($data, $type, $charset);
-
-                $this->addComponent($component);
-            }
-
-            // Now process the non-vTimezone components.
-            foreach ($components[0] as $key => $data) {
-                $type = trim($components[1][$key]);
-                if ($type == 'VTIMEZONE') {
-                    continue;
-                }
-                $component = &Horde_iCalendar::newComponent($type, $this);
-                if ($component === false) {
-                    return PEAR::raiseError("Unable to create object for type $type");
-                }
-                $component->parsevCalendar($data, $type, $charset);
-
-                $this->addComponent($component);
-            }
-        }
-
-        return true;
-    }
-
-    /**
-     * Export this component in vCal format.
-     *
-     * @param string $base  The type of the base object.
-     *
-     * @return string  vCal format data.
-     */
-    function _exportvData($base = 'VCALENDAR')
-    {
-        $result = 'BEGIN:' . String::upper($base) . $this->_newline;
-
-        // VERSION is not allowed for entries enclosed in VCALENDAR/ICALENDAR,
-        // as it is part of the enclosing VCALENDAR/ICALENDAR. See rfc2445
-        if ($base !== 'VEVENT' && $base !== 'VTODO' && $base !== 'VALARM' &&
-            $base !== 'VJOURNAL' && $base !== 'VFREEBUSY') {
-            // Ensure that version is the first attribute.
-            $result .= 'VERSION:' . $this->_version . $this->_newline;
-        }
-        foreach ($this->_attributes as $attribute) {
-            $name = $attribute['name'];
-            if ($name == 'VERSION') {
-                // Already done.
-                continue;
-            }
-
-            $params_str = '';
-            $params = $attribute['params'];
-            if ($params) {
-                foreach ($params as $param_name => $param_value) {
-                    /* Skip CHARSET for iCalendar 2.0 data, not allowed. */
-                    if ($param_name == 'CHARSET' && !$this->isOldFormat()) {
-                        continue;
-                    }
-                    /* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */
-                    if ($this->isOldFormat() &&
-                        $param_name == 'VALUE' && $param_value == 'DATE') {
-                        continue;
-                    }
-
-                    if ($param_value === null) {
-                        $params_str .= ";$param_name";
-                    } else {
-                        $len = strlen($param_value);
-                        $safe_value = '';
-                        $quote = false;
-                        for ($i = 0; $i < $len; ++$i) {
-                            $ord = ord($param_value[$i]);
-                            // Accept only valid characters.
-                            if ($ord == 9 || $ord == 32 || $ord == 33 ||
-                                ($ord >= 35 && $ord <= 126) ||
-                                $ord >= 128) {
-                                $safe_value .= $param_value[$i];
-                                // Characters above 128 do not need to be
-                                // quoted as per RFC2445 but Outlook requires
-                                // this.
-                                if ($ord == 44 || $ord == 58 || $ord == 59 ||
-                                    $ord >= 128) {
-                                    $quote = true;
-                                }
-                            }
-                        }
-                        if ($quote) {
-                            $safe_value = '"' . $safe_value . '"';
-                        }
-                        $params_str .= ";$param_name=$safe_value";
-                    }
-                }
-            }
-
-            $value = $attribute['value'];
-            switch ($name) {
-            // Date fields.
-            case 'COMPLETED':
-            case 'CREATED':
-            case 'DCREATED':
-            case 'LAST-MODIFIED':
-            case 'X-MOZ-LASTACK':
-            case 'X-MOZ-SNOOZE-TIME':
-                $value = $this->_exportDateTime($value);
-                break;
-
-            case 'DTEND':
-            case 'DTSTART':
-            case 'DTSTAMP':
-            case 'DUE':
-            case 'AALARM':
-            case 'RECURRENCE-ID':
-                if (isset($params['VALUE'])) {
-                    if ($params['VALUE'] == 'DATE') {
-                        // VCALENDAR 1.0 uses T000000 - T235959 for all day events:
-                        if ($this->isOldFormat() && $name == 'DTEND') {
-                            $d = new Horde_Date($value);
-                            $value = new Horde_Date(array(
-                                'year' => $d->year,
-                                'month' => $d->month,
-                                'mday' => $d->mday - 1));
-                            $value->correct();
-                            $value = $this->_exportDate($value, '235959');
-                        } else {
-                            $value = $this->_exportDate($value, '000000');
-                        }
-                    } else {
-                        $value = $this->_exportDateTime($value);
-                    }
-                } else {
-                    $value = $this->_exportDateTime($value);
-                }
-                break;
-
-            // Comma seperated dates.
-            case 'EXDATE':
-            case 'RDATE':
-                $dates = array();
-                foreach ($value as $date) {
-                    if (isset($params['VALUE'])) {
-                        if ($params['VALUE'] == 'DATE') {
-                            $dates[] = $this->_exportDate($date, '000000');
-                        } elseif ($params['VALUE'] == 'PERIOD') {
-                            $dates[] = $this->_exportPeriod($date);
-                        } else {
-                            $dates[] = $this->_exportDateTime($date);
-                        }
-                    } else {
-                        $dates[] = $this->_exportDateTime($date);
-                    }
-                }
-                $value = implode($this->isOldFormat() ? ';' : ',', $dates);
-                break;
-
-            case 'TRIGGER':
-                if (isset($params['VALUE'])) {
-                    if ($params['VALUE'] == 'DATE-TIME') {
-                        $value = $this->_exportDateTime($value);
-                    } elseif ($params['VALUE'] == 'DURATION') {
-                        $value = $this->_exportDuration($value);
-                    }
-                } else {
-                    $value = $this->_exportDuration($value);
-                }
-                break;
-
-            // Duration fields.
-            case 'DURATION':
-                $value = $this->_exportDuration($value);
-                break;
-
-            // Period of time fields.
-            case 'FREEBUSY':
-                $value_str = '';
-                foreach ($value as $period) {
-                    $value_str .= empty($value_str) ? '' : ',';
-                    $value_str .= $this->_exportPeriod($period);
-                }
-                $value = $value_str;
-                break;
-
-            // UTC offset fields.
-            case 'TZOFFSETFROM':
-            case 'TZOFFSETTO':
-                $value = $this->_exportUtcOffset($value);
-                break;
-
-            // Integer fields.
-            case 'PERCENT-COMPLETE':
-            case 'PRIORITY':
-            case 'REPEAT':
-            case 'SEQUENCE':
-                $value = "$value";
-                break;
-
-            // Geo fields.
-            case 'GEO':
-                if ($this->isOldFormat()) {
-                    $value = $value['longitude'] . ',' . $value['latitude'];
-                } else {
-                    $value = $value['latitude'] . ';' . $value['longitude'];
-                }
-                break;
-
-            // Recurrence fields.
-            case 'EXRULE':
-            case 'RRULE':
-                break;
-
-            default:
-                if ($this->isOldFormat()) {
-                    if (is_array($attribute['values']) &&
-                        count($attribute['values']) > 1) {
-                        $values = $attribute['values'];
-                        if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
-                            $glue = ';';
-                        } else {
-                            $glue = ',';
-                        }
-                        $values = str_replace(';', '\\;', $values);
-                        $value = implode($glue, $values);
-                    } else {
-                        /* vcard 2.1 and vcalendar 1.0 escape only
-                         * semicolons */
-                        $value = str_replace(';', '\\;', $value);
-                    }
-                    // Text containing newlines or ASCII >= 127 must be BASE64
-                    // or QUOTED-PRINTABLE encoded. Currently we use
-                    // QUOTED-PRINTABLE as default.
-                    if (preg_match("/[^\x20-\x7F]/", $value) &&
-                        empty($params['ENCODING']))  {
-                        $params['ENCODING'] = 'QUOTED-PRINTABLE';
-                        $params_str .= ';ENCODING=QUOTED-PRINTABLE';
-                        // Add CHARSET as well. At least the synthesis client
-                        // gets confused otherwise
-                        if (empty($params['CHARSET'])) {
-                            $params['CHARSET'] = 'UTF-8';
-                            $params_str .= ';CHARSET=' . $params['CHARSET'];
-                        }
-                    }
-                } else {
-                    if (is_array($attribute['values']) &&
-                        count($attribute['values'])) {
-                        $values = $attribute['values'];
-                        if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
-                            $glue = ';';
-                        } else {
-                            $glue = ',';
-                        }
-                        // As of rfc 2426 2.5 semicolon and comma must be
-                        // escaped.
-                        $values = str_replace(array('\\', ';', ','),
-                                              array('\\\\', '\\;', '\\,'),
-                                              $values);
-                        $value = implode($glue, $values);
-                    } else {
-                        // As of rfc 2426 2.5 semicolon and comma must be
-                        // escaped.
-                        $value = str_replace(array('\\', ';', ','),
-                                             array('\\\\', '\\;', '\\,'),
-                                             $value);
-                    }
-                    $value = preg_replace('/\r?\n/', '\n', $value);
-                }
-                break;
-            }
-
-            $value = str_replace("\r", '', $value);
-            if (!empty($params['ENCODING']) &&
-                $params['ENCODING'] == 'QUOTED-PRINTABLE' &&
-                strlen(trim($value))) {
-                $result .= $name . $params_str . ':'
-                    . str_replace('=0A', '=0D=0A',
-                                  $this->_quotedPrintableEncode($value))
-                    . $this->_newline;
-            } else {
-                $attr_string = $name . $params_str . ':' . $value;
-                if (!$this->isOldFormat()) {
-                    $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ',
-                                                    true, 'utf-8', true);
-                }
-                $result .= $attr_string . $this->_newline;
-            }
-        }
-
-        foreach ($this->_components as $component) {
-            $result .= $component->exportvCalendar();
-        }
-
-        return $result . 'END:' . $base . $this->_newline;
-    }
-
-    /**
-     * Parse a UTC Offset field.
-     */
-    function _parseUtcOffset($text)
-    {
-        $offset = array();
-        if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $timeParts)) {
-            $offset['ahead']  = (bool)($timeParts[1] == '+');
-            $offset['hour']   = intval($timeParts[2]);
-            $offset['minute'] = intval($timeParts[3]);
-            if (isset($timeParts[4])) {
-                $offset['second'] = intval($timeParts[4]);
-            }
-            return $offset;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Export a UTC Offset field.
-     */
-    function _exportUtcOffset($value)
-    {
-        $offset = $value['ahead'] ? '+' : '-';
-        $offset .= sprintf('%02d%02d',
-                           $value['hour'], $value['minute']);
-        if (isset($value['second'])) {
-            $offset .= sprintf('%02d', $value['second']);
-        }
-
-        return $offset;
-    }
-
-    /**
-     * Parse a Time Period field.
-     */
-    function _parsePeriod($text)
-    {
-        $periodParts = explode('/', $text);
-
-        $start = $this->_parseDateTime($periodParts[0]);
-
-        if ($duration = $this->_parseDuration($periodParts[1])) {
-            return array('start' => $start, 'duration' => $duration);
-        } elseif ($end = $this->_parseDateTime($periodParts[1])) {
-            return array('start' => $start, 'end' => $end);
-        }
-    }
-
-    /**
-     * Export a Time Period field.
-     */
-    function _exportPeriod($value)
-    {
-        $period = $this->_exportDateTime($value['start']);
-        $period .= '/';
-        if (isset($value['duration'])) {
-            $period .= $this->_exportDuration($value['duration']);
-        } else {
-            $period .= $this->_exportDateTime($value['end']);
-        }
-        return $period;
-    }
-
-    /**
-     * Grok the TZID and return an offset in seconds from UTC for this
-     * date and time.
-     */
-    function _parseTZID($date, $time, $tzid)
-    {
-        $vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid);
-        if (!$vtimezone) {
-            // use PHP's standard timezone db to determine tzoffset
-            try {
-                $tz = new DateTimeZone($tzid);
-                $dt = new DateTime('now', $tz);
-                $dt->setDate($date['year'], $date['month'], $date['mday']);
-                $dt->setTime($time['hour'], $time['minute'], $date['recond']);
-                return $tz->getOffset($dt);
-            }
-            catch (Exception $e) {
-                return false;
-            }
-        }
-
-        $change_times = array();
-        foreach ($vtimezone->getComponents() as $o) {
-            $t = $vtimezone->parseChild($o, $date['year']);
-            if ($t !== false) {
-                $change_times[] = $t;
-            }
-        }
-
-        if (!$change_times) {
-            return false;
-        }
-
-        sort($change_times);
-
-        // Time is arbitrarily based on UTC for comparison.
-        $t = @gmmktime($time['hour'], $time['minute'], $time['second'],
-                       $date['month'], $date['mday'], $date['year']);
-
-        if ($t < $change_times[0]['time']) {
-            return $change_times[0]['from'];
-        }
-
-        for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) {
-            if (($t >= $change_times[$i]['time']) &&
-                ($t < $change_times[$i + 1]['time'])) {
-                return $change_times[$i]['to'];
-            }
-        }
-
-        if ($t >= $change_times[$n - 1]['time']) {
-            return $change_times[$n - 1]['to'];
-        }
-
-        return false;
-    }
-
-    /**
-     * Parses a DateTime field and returns a unix timestamp. If the
-     * field cannot be parsed then the original text is returned
-     * unmodified.
-     *
-     * @todo This function should be moved to Horde_Date and made public.
-     */
-    function _parseDateTime($text, $tzid = false)
-    {
-        $dateParts = explode('T', $text);
-        if (count($dateParts) != 2 && !empty($text)) {
-            // Not a datetime field but may be just a date field.
-            if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
-                // Or not
-                return $text;
-            }
-            $newtext = $text.'T000000';
-            $dateParts = explode('T', $newtext);
-        }
-
-        if (!$date = Horde_iCalendar::_parseDate($dateParts[0])) {
-            return $text;
-        }
-        if (!$time = Horde_iCalendar::_parseTime($dateParts[1])) {
-            return $text;
-        }
-
-        // Get timezone info for date fields from $tzid and container.
-        $tzoffset = ($time['zone'] == 'Local' && $tzid && is_a($this->_container, 'Horde_iCalendar'))
-            ? $this->_parseTZID($date, $time, $tzid) : false;
-        if ($time['zone'] == 'UTC' || $tzoffset !== false) {
-            $result = @gmmktime($time['hour'], $time['minute'], $time['second'],
-                                $date['month'], $date['mday'], $date['year']);
-            if ($tzoffset) {
-                $result -= $tzoffset;
-            }
-        } else {
-            // We don't know the timezone so assume local timezone.
-            // FIXME: shouldn't this be based on the user's timezone
-            // preference rather than the server's timezone?
-            $result = @mktime($time['hour'], $time['minute'], $time['second'],
-                              $date['month'], $date['mday'], $date['year']);
-        }
-
-        return ($result !== false) ? $result : $text;
-    }
-
-    /**
-     * Export a DateTime field.
-     */
-    function _exportDateTime($value)
-    {
-        $temp = array();
-        if (!is_object($value) && !is_array($value)) {
-            $tz = date('O', $value);
-            $TZOffset = (3600 * substr($tz, 0, 3)) + (60 * substr($tz, 3, 2));
-            $value -= $TZOffset;
-
-            $temp['zone']   = 'UTC';
-            list($temp['year'], $temp['month'], $temp['mday'], $temp['hour'], $temp['minute'], $temp['second']) = explode('-', date('Y-n-j-G-i-s', $value));
-        } else {
-            $dateOb = new Horde_Date($value);
-            return Horde_iCalendar::_exportDateTime($dateOb->timestamp());
-        }
-
-        return Horde_iCalendar::_exportDate($temp) . 'T' . Horde_iCalendar::_exportTime($temp);
-    }
-
-    /**
-     * Parses a Time field.
-     *
-     * @static
-     */
-    function _parseTime($text)
-    {
-        if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) {
-            $time['hour'] = intval($timeParts[1]);
-            $time['minute'] = intval($timeParts[2]);
-            $time['second'] = intval($timeParts[3]);
-            if (isset($timeParts[4])) {
-                $time['zone'] = 'UTC';
-            } else {
-                $time['zone'] = 'Local';
-            }
-            return $time;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Exports a Time field.
-     */
-    function _exportTime($value)
-    {
-        $time = sprintf('%02d%02d%02d',
-                        $value['hour'], $value['minute'], $value['second']);
-        if ($value['zone'] == 'UTC') {
-            $time .= 'Z';
-        }
-        return $time;
-    }
-
-    /**
-     * Parses a Date field.
-     *
-     * @static
-     */
-    function _parseDate($text)
-    {
-        $parts = explode('T', $text);
-        if (count($parts) == 2) {
-            $text = $parts[0];
-        }
-
-        if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
-            return false;
-        }
-
-        return array('year' => $match[1],
-                     'month' => $match[2],
-                     'mday' => $match[3]);
-    }
-
-    /**
-     * Exports a date field.
-     *
-     * @param object|array $value  Date object or hash.
-     * @param string $autoconvert  If set, use this as time part to export the
-     *                             date as datetime when exporting to Vcalendar
-     *                             1.0. Examples: '000000' or '235959'
-     */
-    function _exportDate($value, $autoconvert = false)
-    {
-        if (is_object($value)) {
-            $value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday);
-        }
-        if ($autoconvert !== false && $this->isOldFormat()) {
-            return sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert);
-        } else {
-            return sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']);
-        }
-    }
-
-    /**
-     * Parse a Duration Value field.
-     */
-    function _parseDuration($text)
-    {
-        if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $durvalue)) {
-            // Weeks.
-            $duration = 7 * 86400 * intval($durvalue[3]);
-
-            if (count($durvalue) > 4) {
-                // Days.
-                $duration += 86400 * intval($durvalue[4]);
-            }
-            if (count($durvalue) > 5) {
-                // Hours.
-                $duration += 3600 * intval($durvalue[7]);
-
-                // Mins.
-                if (isset($durvalue[8])) {
-                    $duration += 60 * intval($durvalue[8]);
-                }
-
-                // Secs.
-                if (isset($durvalue[9])) {
-                    $duration += intval($durvalue[9]);
-                }
-            }
-
-            // Sign.
-            if ($durvalue[1] == "-") {
-                $duration *= -1;
-            }
-
-            return $duration;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Export a duration value.
-     */
-    function _exportDuration($value)
-    {
-        $duration = '';
-        if ($value < 0) {
-            $value *= -1;
-            $duration .= '-';
-        }
-        $duration .= 'P';
-
-        $weeks = floor($value / (7 * 86400));
-        $value = $value % (7 * 86400);
-        if ($weeks) {
-            $duration .= $weeks . 'W';
-        }
-
-        $days = floor($value / (86400));
-        $value = $value % (86400);
-        if ($days) {
-            $duration .= $days . 'D';
-        }
-
-        if ($value) {
-            $duration .= 'T';
-
-            $hours = floor($value / 3600);
-            $value = $value % 3600;
-            if ($hours) {
-                $duration .= $hours . 'H';
-            }
-
-            $mins = floor($value / 60);
-            $value = $value % 60;
-            if ($mins) {
-                $duration .= $mins . 'M';
-            }
-
-            if ($value) {
-                $duration .= $value . 'S';
-            }
-        }
-
-        return $duration;
-    }
-
-    /**
-     * Converts an 8bit string to a quoted-printable string according to RFC
-     * 2045, section 6.7.
-     *
-     * imap_8bit() does not apply all necessary rules.
-     *
-     * @param string $input  The string to be encoded.
-     *
-     * @return string  The quoted-printable encoded string.
-     */
-    function _quotedPrintableEncode($input = '')
-    {
-        $output = $line = '';
-        $len = strlen($input);
-
-        for ($i = 0; $i < $len; ++$i) {
-            $ord = ord($input[$i]);
-            // Encode non-printable characters (rule 2).
-            if ($ord == 9 ||
-                ($ord >= 32 && $ord <= 60) ||
-                ($ord >= 62 && $ord <= 126)) {
-                $chunk = $input[$i];
-            } else {
-                // Quoted printable encoding (rule 1).
-                $chunk = '=' . String::upper(sprintf('%02X', $ord));
-            }
-            $line .= $chunk;
-            // Wrap long lines (rule 5)
-            if (strlen($line) + 1 > 76) {
-                $line = String::wordwrap($line, 75, "=\r\n", true, 'us-ascii', true);
-                $newline = strrchr($line, "\r\n");
-                if ($newline !== false) {
-                    $output .= substr($line, 0, -strlen($newline) + 2);
-                    $line = substr($newline, 2);
-                } else {
-                    $output .= $line;
-                }
-                continue;
-            }
-            // Wrap at line breaks for better readability (rule 4).
-            if (substr($line, -3) == '=0A') {
-                $output .= $line . "=\r\n";
-                $line = '';
-            }
-        }
-        $output .= $line;
-
-        // Trailing whitespace must be encoded (rule 3).
-        $lastpos = strlen($output) - 1;
-        if ($output[$lastpos] == chr(9) ||
-            $output[$lastpos] == chr(32)) {
-            $output[$lastpos] = '=';
-            $output .= String::upper(sprintf('%02X', ord($output[$lastpos])));
-        }
-
-        return $output;
-    }
-
-}
-
-
-
-/**
- * Class representing vAlarms.
- *
- * $Horde: framework/iCalendar/iCalendar/valarm.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_valarm extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vAlarm';
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('VALARM');
-    }
-
-}
-
-/**
- * Class representing vEvents.
- *
- * $Horde: framework/iCalendar/iCalendar/vevent.php,v 1.31.10.16 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vevent extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vEvent';
-    }
-
-    function exportvCalendar()
-    {
-        // Default values.
-        $requiredAttributes = array();
-        $requiredAttributes['DTSTAMP'] = time();
-        $requiredAttributes['UID'] = $this->_exportDateTime(time())
-            . substr(str_pad(base_convert(microtime(), 10, 36), 16, uniqid(mt_rand()), STR_PAD_LEFT), -16)
-            . '@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost');
-
-        $method = !empty($this->_container) ?
-            $this->_container->getAttribute('METHOD') : 'PUBLISH';
-
-        switch ($method) {
-        case 'PUBLISH':
-            $requiredAttributes['DTSTART'] = time();
-            $requiredAttributes['SUMMARY'] = '';
-            break;
-
-        case 'REQUEST':
-            $requiredAttributes['ATTENDEE'] = '';
-            $requiredAttributes['DTSTART'] = time();
-            $requiredAttributes['SUMMARY'] = '';
-            break;
-
-        case 'REPLY':
-            $requiredAttributes['ATTENDEE'] = '';
-            break;
-
-        case 'ADD':
-            $requiredAttributes['DTSTART'] = time();
-            $requiredAttributes['SEQUENCE'] = 1;
-            $requiredAttributes['SUMMARY'] = '';
-            break;
-
-        case 'CANCEL':
-            $requiredAttributes['ATTENDEE'] = '';
-            $requiredAttributes['SEQUENCE'] = 1;
-            break;
-
-        case 'REFRESH':
-            $requiredAttributes['ATTENDEE'] = '';
-            break;
-        }
-
-        foreach ($requiredAttributes as $name => $default_value) {
-            if (is_a($this->getAttribute($name), 'PEAR_Error')) {
-                $this->setAttribute($name, $default_value);
-            }
-        }
-
-        return parent::_exportvData('VEVENT');
-    }
-
-    /**
-     * Update the status of an attendee of an event.
-     *
-     * @param $email    The email address of the attendee.
-     * @param $status   The participant status to set.
-     * @param $fullname The full name of the participant to set.
-     */
-    function updateAttendee($email, $status, $fullname = '')
-    {
-        foreach ($this->_attributes as $key => $attribute) {
-            if ($attribute['name'] == 'ATTENDEE' &&
-                $attribute['value'] == 'mailto:' . $email) {
-                $this->_attributes[$key]['params']['PARTSTAT'] = $status;
-                if (!empty($fullname)) {
-                    $this->_attributes[$key]['params']['CN'] = $fullname;
-                }
-                unset($this->_attributes[$key]['params']['RSVP']);
-                return;
-            }
-        }
-        $params = array('PARTSTAT' => $status);
-        if (!empty($fullname)) {
-            $params['CN'] = $fullname;
-        }
-        $this->setAttribute('ATTENDEE', 'mailto:' . $email, $params);
-    }
-
-    /**
-     * Return the organizer display name or email.
-     *
-     * @return string  The organizer name to display for this event.
-     */
-    function organizerName()
-    {
-        $organizer = $this->getAttribute('ORGANIZER', true);
-        if (is_a($organizer, 'PEAR_Error')) {
-            return _("An unknown person");
-        }
-
-        if (isset($organizer[0]['CN'])) {
-            return $organizer[0]['CN'];
-        }
-
-        $organizer = parse_url($this->getAttribute('ORGANIZER'));
-
-        return $organizer['path'];
-    }
-
-    /**
-     * Update this event with details from another event.
-     *
-     * @param Horde_iCalendar_vEvent $vevent  The vEvent with latest details.
-     */
-    function updateFromvEvent($vevent)
-    {
-        $newAttributes = $vevent->getAllAttributes();
-        foreach ($newAttributes as $newAttribute) {
-            $currentValue = $this->getAttribute($newAttribute['name']);
-            if (is_a($currentValue, 'PEAR_error')) {
-                // Already exists so just add it.
-                $this->setAttribute($newAttribute['name'],
-                                    $newAttribute['value'],
-                                    $newAttribute['params']);
-            } else {
-                // Already exists so locate and modify.
-                $found = false;
-
-                // Try matching the attribte name and value incase
-                // only the params changed (eg attendee updating
-                // status).
-                foreach ($this->_attributes as $id => $attr) {
-                    if ($attr['name'] == $newAttribute['name'] &&
-                        $attr['value'] == $newAttribute['value']) {
-                        // merge the params
-                        foreach ($newAttribute['params'] as $param_id => $param_name) {
-                            $this->_attributes[$id]['params'][$param_id] = $param_name;
-                        }
-                        $found = true;
-                        break;
-                    }
-                }
-                if (!$found) {
-                    // Else match the first attribute with the same
-                    // name (eg changing start time).
-                    foreach ($this->_attributes as $id => $attr) {
-                        if ($attr['name'] == $newAttribute['name']) {
-                            $this->_attributes[$id]['value'] = $newAttribute['value'];
-                            // Merge the params.
-                            foreach ($newAttribute['params'] as $param_id => $param_name) {
-                                $this->_attributes[$id]['params'][$param_id] = $param_name;
-                            }
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Update just the attendess of event with details from another
-     * event.
-     *
-     * @param Horde_iCalendar_vEvent $vevent  The vEvent with latest details
-     */
-    function updateAttendeesFromvEvent($vevent)
-    {
-        $newAttributes = $vevent->getAllAttributes();
-        foreach ($newAttributes as $newAttribute) {
-            if ($newAttribute['name'] != 'ATTENDEE') {
-                continue;
-            }
-            $currentValue = $this->getAttribute($newAttribute['name']);
-            if (is_a($currentValue, 'PEAR_error')) {
-                // Already exists so just add it.
-                $this->setAttribute($newAttribute['name'],
-                                    $newAttribute['value'],
-                                    $newAttribute['params']);
-            } else {
-                // Already exists so locate and modify.
-                $found = false;
-                // Try matching the attribte name and value incase
-                // only the params changed (eg attendee updating
-                // status).
-                foreach ($this->_attributes as $id => $attr) {
-                    if ($attr['name'] == $newAttribute['name'] &&
-                        $attr['value'] == $newAttribute['value']) {
-                        // Merge the params.
-                        foreach ($newAttribute['params'] as $param_id => $param_name) {
-                            $this->_attributes[$id]['params'][$param_id] = $param_name;
-                        }
-                        $found = true;
-                        break;
-                    }
-                }
-
-                if (!$found) {
-                    // Else match the first attribute with the same
-                    // name (eg changing start time).
-                    foreach ($this->_attributes as $id => $attr) {
-                        if ($attr['name'] == $newAttribute['name']) {
-                            $this->_attributes[$id]['value'] = $newAttribute['value'];
-                            // Merge the params.
-                            foreach ($newAttribute['params'] as $param_id => $param_name) {
-                                $this->_attributes[$id]['params'][$param_id] = $param_name;
-                            }
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-}
-
-/**
- * Class representing vFreebusy components.
- *
- * $Horde: framework/iCalendar/iCalendar/vfreebusy.php,v 1.16.10.18 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @todo Don't use timestamps
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vfreebusy extends Horde_iCalendar {
-
-    var $_busyPeriods = array();
-    var $_extraParams = array();
-
-    /**
-     * Returns the type of this calendar component.
-     *
-     * @return string  The type of this component.
-     */
-    function getType()
-    {
-        return 'vFreebusy';
-    }
-
-    /**
-     * Parses a string containing vFreebusy data.
-     *
-     * @param string $data     The data to parse.
-     */
-    function parsevCalendar($data, $type = null, $charset = null)
-    {
-        parent::parsevCalendar($data, 'VFREEBUSY', $charset);
-
-        // Do something with all the busy periods.
-        foreach ($this->_attributes as $key => $attribute) {
-            if ($attribute['name'] != 'FREEBUSY') {
-                continue;
-            }
-            foreach ($attribute['values'] as $value) {
-                $params = isset($attribute['params'])
-                    ? $attribute['params']
-                    : array();
-                if (isset($value['duration'])) {
-                    $this->addBusyPeriod('BUSY', $value['start'], null,
-                                         $value['duration'], $params);
-                } else {
-                    $this->addBusyPeriod('BUSY', $value['start'],
-                                         $value['end'], null, $params);
-                }
-            }
-            unset($this->_attributes[$key]);
-        }
-    }
-
-    /**
-     * Returns the component exported as string.
-     *
-     * @return string  The exported vFreeBusy information according to the
-     *                 iCalender format specification.
-     */
-    function exportvCalendar()
-    {
-        foreach ($this->_busyPeriods as $start => $end) {
-            $periods = array(array('start' => $start, 'end' => $end));
-            $this->setAttribute('FREEBUSY', $periods,
-                                isset($this->_extraParams[$start])
-                                ? $this->_extraParams[$start] : array());
-        }
-
-        $res = parent::_exportvData('VFREEBUSY');
-
-        foreach ($this->_attributes as $key => $attribute) {
-            if ($attribute['name'] == 'FREEBUSY') {
-                unset($this->_attributes[$key]);
-            }
-        }
-
-        return $res;
-    }
-
-    /**
-     * Returns a display name for this object.
-     *
-     * @return string  A clear text name for displaying this object.
-     */
-    function getName()
-    {
-        $name = '';
-        $method = !empty($this->_container) ?
-            $this->_container->getAttribute('METHOD') : 'PUBLISH';
-
-        if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
-            $attr = 'ORGANIZER';
-        } elseif ($method == 'REPLY') {
-            $attr = 'ATTENDEE';
-        }
-
-        $name = $this->getAttribute($attr, true);
-        if (!is_a($name, 'PEAR_Error') && isset($name[0]['CN'])) {
-            return $name[0]['CN'];
-        }
-
-        $name = $this->getAttribute($attr);
-        if (is_a($name, 'PEAR_Error')) {
-            return '';
-        } else {
-            $name = parse_url($name);
-            return $name['path'];
-        }
-    }
-
-    /**
-     * Returns the email address for this object.
-     *
-     * @return string  The email address of this object's owner.
-     */
-    function getEmail()
-    {
-        $name = '';
-        $method = !empty($this->_container)
-                  ? $this->_container->getAttribute('METHOD') : 'PUBLISH';
-
-        if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
-            $attr = 'ORGANIZER';
-        } elseif ($method == 'REPLY') {
-            $attr = 'ATTENDEE';
-        }
-
-        $name = $this->getAttribute($attr);
-        if (is_a($name, 'PEAR_Error')) {
-            return '';
-        } else {
-            $name = parse_url($name);
-            return $name['path'];
-        }
-    }
-
-    /**
-     * Returns the busy periods.
-     *
-     * @return array  All busy periods.
-     */
-    function getBusyPeriods()
-    {
-        return $this->_busyPeriods;
-    }
-
-    /**
-     * Returns any additional freebusy parameters.
-     *
-     * @return array  Additional parameters of the freebusy periods.
-     */
-    function getExtraParams()
-    {
-        return $this->_extraParams;
-    }
-
-    /**
-     * Returns all the free periods of time in a given period.
-     *
-     * @param integer $startStamp  The start timestamp.
-     * @param integer $endStamp    The end timestamp.
-     *
-     * @return array  A hash with free time periods, the start times as the
-     *                keys and the end times as the values.
-     */
-    function getFreePeriods($startStamp, $endStamp)
-    {
-        $this->simplify();
-        $periods = array();
-
-        // Check that we have data for some part of this period.
-        if ($this->getEnd() < $startStamp || $this->getStart() > $endStamp) {
-            return $periods;
-        }
-
-        // Locate the first time in the requested period we have data for.
-        $nextstart = max($startStamp, $this->getStart());
-
-        // Check each busy period and add free periods in between.
-        foreach ($this->_busyPeriods as $start => $end) {
-            if ($start <= $endStamp && $end >= $nextstart) {
-                if ($nextstart <= $start) {
-                    $periods[$nextstart] = min($start, $endStamp);
-                }
-                $nextstart = min($end, $endStamp);
-            }
-        }
-
-        // If we didn't read the end of the requested period but still have
-        // data then mark as free to the end of the period or available data.
-        if ($nextstart < $endStamp && $nextstart < $this->getEnd()) {
-            $periods[$nextstart] = min($this->getEnd(), $endStamp);
-        }
-
-        return $periods;
-    }
-
-    /**
-     * Adds a busy period to the info.
-     *
-     * This function may throw away data in case you add a period with a start
-     * date that already exists. The longer of the two periods will be chosen
-     * (and all information associated with the shorter one will be removed).
-     *
-     * @param string $type       The type of the period. Either 'FREE' or
-     *                           'BUSY'; only 'BUSY' supported at the moment.
-     * @param integer $start     The start timestamp of the period.
-     * @param integer $end       The end timestamp of the period.
-     * @param integer $duration  The duration of the period. If specified, the
-     *                           $end parameter will be ignored.
-     * @param array   $extra     Additional parameters for this busy period.
-     */
-    function addBusyPeriod($type, $start, $end = null, $duration = null,
-                           $extra = array())
-    {
-        if ($type == 'FREE') {
-            // Make sure this period is not marked as busy.
-            return false;
-        }
-
-        // Calculate the end time if duration was specified.
-        $tempEnd = is_null($duration) ? $end : $start + $duration;
-
-        // Make sure the period length is always positive.
-        $end = max($start, $tempEnd);
-        $start = min($start, $tempEnd);
-
-        if (isset($this->_busyPeriods[$start])) {
-            // Already a period starting at this time. Change the current
-            // period only if the new one is longer. This might be a problem
-            // if the callee assumes that there is no simplification going
-            // on. But since the periods are stored using the start time of
-            // the busy periods we have to throw away data here.
-            if ($end > $this->_busyPeriods[$start]) {
-                $this->_busyPeriods[$start] = $end;
-                $this->_extraParams[$start] = $extra;
-            }
-        } else {
-            // Add a new busy period.
-            $this->_busyPeriods[$start] = $end;
-            $this->_extraParams[$start] = $extra;
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns the timestamp of the start of the time period this free busy
-     * information covers.
-     *
-     * @return integer  A timestamp.
-     */
-    function getStart()
-    {
-        if (!is_a($this->getAttribute('DTSTART'), 'PEAR_Error')) {
-            return $this->getAttribute('DTSTART');
-        } elseif (count($this->_busyPeriods)) {
-            return min(array_keys($this->_busyPeriods));
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Returns the timestamp of the end of the time period this free busy
-     * information covers.
-     *
-     * @return integer  A timestamp.
-     */
-    function getEnd()
-    {
-        if (!is_a($this->getAttribute('DTEND'), 'PEAR_Error')) {
-            return $this->getAttribute('DTEND');
-        } elseif (count($this->_busyPeriods)) {
-            return max(array_values($this->_busyPeriods));
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Merges the busy periods of another Horde_iCalendar_vfreebusy object
-     * into this one.
-     *
-     * This might lead to simplification no matter what you specify for the
-     * "simplify" flag since periods with the same start date will lead to the
-     * shorter period being removed (see addBusyPeriod).
-     *
-     * @param Horde_iCalendar_vfreebusy $freebusy  A freebusy object.
-     * @param boolean $simplify                    If true, simplify() will
-     *                                             called after the merge.
-     */
-    function merge($freebusy, $simplify = true)
-    {
-        if (!is_a($freebusy, 'Horde_iCalendar_vfreebusy')) {
-            return false;
-        }
-
-        $extra = $freebusy->getExtraParams();
-        foreach ($freebusy->getBusyPeriods() as $start => $end) {
-            // This might simplify the busy periods without taking the
-            // "simplify" flag into account.
-            $this->addBusyPeriod('BUSY', $start, $end, null,
-                                 isset($extra[$start])
-                                 ? $extra[$start] : array());
-        }
-
-        $thisattr = $this->getAttribute('DTSTART');
-        $thatattr = $freebusy->getAttribute('DTSTART');
-        if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
-            $this->setAttribute('DTSTART', $thatattr, array(), false);
-        } elseif (!is_a($thatattr, 'PEAR_Error')) {
-            if ($thatattr < $thisattr) {
-                $this->setAttribute('DTSTART', $thatattr, array(), false);
-            }
-        }
-
-        $thisattr = $this->getAttribute('DTEND');
-        $thatattr = $freebusy->getAttribute('DTEND');
-        if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
-            $this->setAttribute('DTEND', $thatattr, array(), false);
-        } elseif (!is_a($thatattr, 'PEAR_Error')) {
-            if ($thatattr > $thisattr) {
-                $this->setAttribute('DTEND', $thatattr, array(), false);
-            }
-        }
-
-        if ($simplify) {
-            $this->simplify();
-        }
-
-        return true;
-    }
-
-    /**
-     * Removes all overlaps and simplifies the busy periods array as much as
-     * possible.
-     */
-    function simplify()
-    {
-        $clean = false;
-        $busy  = array($this->_busyPeriods, $this->_extraParams);
-        while (!$clean) {
-            $result = $this->_simplify($busy[0], $busy[1]);
-            $clean = $result === $busy;
-            $busy = $result;
-        }
-
-        ksort($result[1], SORT_NUMERIC);
-        $this->_extraParams = $result[1];
-
-        ksort($result[0], SORT_NUMERIC);
-        $this->_busyPeriods = $result[0];
-    }
-
-    function _simplify($busyPeriods, $extraParams = array())
-    {
-        $checked = array();
-        $checkedExtra = array();
-        $checkedEmpty = true;
-
-        foreach ($busyPeriods as $start => $end) {
-            if ($checkedEmpty) {
-                $checked[$start] = $end;
-                $checkedExtra[$start] = isset($extraParams[$start])
-                    ? $extraParams[$start] : array();
-                $checkedEmpty = false;
-            } else {
-                $added = false;
-                foreach ($checked as $testStart => $testEnd) {
-                    // Replace old period if the new period lies around the
-                    // old period.
-                    if ($start <= $testStart && $end >= $testEnd) {
-                        // Remove old period entry.
-                        unset($checked[$testStart]);
-                        unset($checkedExtra[$testStart]);
-                        // Add replacing entry.
-                        $checked[$start] = $end;
-                        $checkedExtra[$start] = isset($extraParams[$start])
-                            ? $extraParams[$start] : array();
-                        $added = true;
-                    } elseif ($start >= $testStart && $end <= $testEnd) {
-                        // The new period lies fully within the old
-                        // period. Just forget about it.
-                        $added = true;
-                    } elseif (($end <= $testEnd && $end >= $testStart) ||
-                              ($start >= $testStart && $start <= $testEnd)) {
-                        // Now we are in trouble: Overlapping time periods. If
-                        // we allow for additional parameters we cannot simply
-                        // choose one of the two parameter sets. It's better
-                        // to leave two separated time periods.
-                        $extra = isset($extraParams[$start])
-                            ? $extraParams[$start] : array();
-                        $testExtra = isset($checkedExtra[$testStart])
-                            ? $checkedExtra[$testStart] : array();
-                        // Remove old period entry.
-                        unset($checked[$testStart]);
-                        unset($checkedExtra[$testStart]);
-                        // We have two periods overlapping. Are their
-                        // additional parameters the same or different?
-                        $newStart = min($start, $testStart);
-                        $newEnd = max($end, $testEnd);
-                        if ($extra === $testExtra) {
-                            // Both periods have the same information. So we
-                            // can just merge.
-                            $checked[$newStart] = $newEnd;
-                            $checkedExtra[$newStart] = $extra;
-                        } else {
-                            // Extra parameters are different. Create one
-                            // period at the beginning with the params of the
-                            // first period and create a trailing period with
-                            // the params of the second period. The break
-                            // point will be the end of the first period.
-                            $break = min($end, $testEnd);
-                            $checked[$newStart] = $break;
-                            $checkedExtra[$newStart] =
-                                isset($extraParams[$newStart])
-                                ? $extraParams[$newStart] : array();
-                            $checked[$break] = $newEnd;
-                            $highStart = max($start, $testStart);
-                            $checkedExtra[$break] =
-                                isset($extraParams[$highStart])
-                                ? $extraParams[$highStart] : array();
-
-                            // Ensure we also have the extra data in the
-                            // extraParams.
-                            $extraParams[$break] =
-                                isset($extraParams[$highStart])
-                                ? $extraParams[$highStart] : array();
-                        }
-                        $added = true;
-                    }
-
-                    if ($added) {
-                        break;
-                    }
-                }
-
-                if (!$added) {
-                    $checked[$start] = $end;
-                    $checkedExtra[$start] = isset($extraParams[$start])
-                        ? $extraParams[$start] : array();
-                }
-            }
-        }
-
-        return array($checked, $checkedExtra);
-    }
-
-}
-
-/**
- * Class representing vJournals.
- *
- * $Horde: framework/iCalendar/iCalendar/vjournal.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vjournal extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vJournal';
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('VJOURNAL');
-    }
-
-}
-
-
-
-
-/**
- * Class representing vNotes.
- *
- * $Horde: framework/iCalendar/iCalendar/vnote.php,v 1.3.10.10 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @author  Karsten Fourmont <fourmont at gmx.de>
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vnote extends Horde_iCalendar {
-
-    function Horde_iCalendar_vnote($version = '1.1')
-    {
-        return parent::Horde_iCalendar($version);
-    }
-
-    function getType()
-    {
-        return 'vNote';
-    }
-
-    /**
-     * Unlike vevent and vtodo, a vnote is normally not enclosed in an
-     * iCalendar container. (BEGIN..END)
-     */
-    function exportvCalendar()
-    {
-        $requiredAttributes['BODY'] = '';
-        $requiredAttributes['VERSION'] = '1.1';
-
-        foreach ($requiredAttributes as $name => $default_value) {
-            if (is_a($this->getattribute($name), 'PEAR_Error')) {
-                $this->setAttribute($name, $default_value);
-            }
-        }
-
-        return $this->_exportvData('VNOTE');
-    }
-
-}
-
-/**
- * Class representing vTimezones.
- *
- * $Horde: framework/iCalendar/iCalendar/vtimezone.php,v 1.8.10.10 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vtimezone extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vTimeZone';
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('VTIMEZONE');
-    }
-
-    /**
-     * Parse child components of the vTimezone component. Returns an
-     * array with the exact time of the time change as well as the
-     * 'from' and 'to' offsets around the change. Time is arbitrarily
-     * based on UTC for comparison.
-     */
-    function parseChild(&$child, $year)
-    {
-        // Make sure 'time' key is first for sort().
-        $result['time'] = 0;
-
-        $t = $child->getAttribute('TZOFFSETFROM');
-        if (is_a($t, 'PEAR_Error')) {
-            return false;
-        }
-        $result['from'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
-
-        $t = $child->getAttribute('TZOFFSETTO');
-        if (is_a($t, 'PEAR_Error')) {
-            return false;
-        }
-        $result['to'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
-
-        $switch_time = $child->getAttribute('DTSTART');
-        if (is_a($switch_time, 'PEAR_Error')) {
-            return false;
-        }
-
-        $rrules = $child->getAttribute('RRULE');
-        if (is_a($rrules, 'PEAR_Error')) {
-            if (!is_int($switch_time)) {
-                return false;
-            }
-            // Convert this timestamp from local time to UTC for
-            // comparison (All dates are compared as if they are UTC).
-            $t = getdate($switch_time);
-            $result['time'] = @gmmktime($t['hours'], $t['minutes'], $t['seconds'],
-                                        $t['mon'], $t['mday'], $t['year']);
-            return $result;
-        }
-
-        $rrules = explode(';', $rrules);
-        foreach ($rrules as $rrule) {
-            $t = explode('=', $rrule);
-            switch ($t[0]) {
-            case 'FREQ':
-                if ($t[1] != 'YEARLY') {
-                    return false;
-                }
-                break;
-
-            case 'INTERVAL':
-                if ($t[1] != '1') {
-                    return false;
-                }
-                break;
-
-            case 'BYMONTH':
-                $month = intval($t[1]);
-                break;
-
-            case 'BYDAY':
-                $len = strspn($t[1], '1234567890-+');
-                if ($len == 0) {
-                    return false;
-                }
-                $weekday = substr($t[1], $len);
-                $weekdays = array(
-                    'SU' => 0,
-                    'MO' => 1,
-                    'TU' => 2,
-                    'WE' => 3,
-                    'TH' => 4,
-                    'FR' => 5,
-                    'SA' => 6
-                );
-                $weekday = $weekdays[$weekday];
-                $which = intval(substr($t[1], 0, $len));
-                break;
-
-            case 'UNTIL':
-                if (intval($year) > intval(substr($t[1], 0, 4))) {
-                    return false;
-                }
-                break;
-            }
-        }
-
-        if (empty($month) || !isset($weekday)) {
-            return false;
-        }
-
-        if (is_int($switch_time)) {
-            // Was stored as localtime.
-            $switch_time = strftime('%H:%M:%S', $switch_time);
-            $switch_time = explode(':', $switch_time);
-        } else {
-            $switch_time = explode('T', $switch_time);
-            if (count($switch_time) != 2) {
-                return false;
-            }
-            $switch_time[0] = substr($switch_time[1], 0, 2);
-            $switch_time[2] = substr($switch_time[1], 4, 2);
-            $switch_time[1] = substr($switch_time[1], 2, 2);
-        }
-
-        // Get the timestamp for the first day of $month.
-        $when = gmmktime($switch_time[0], $switch_time[1], $switch_time[2],
-                         $month, 1, $year);
-        // Get the day of the week for the first day of $month.
-        $first_of_month_weekday = intval(gmstrftime('%w', $when));
-
-        // Go to the first $weekday before first day of $month.
-        if ($weekday >= $first_of_month_weekday) {
-            $weekday -= 7;
-        }
-        $when -= ($first_of_month_weekday - $weekday) * 60 * 60 * 24;
-
-        // If going backwards go to the first $weekday after last day
-        // of $month.
-        if ($which < 0) {
-            do {
-                $when += 60*60*24*7;
-            } while (intval(gmstrftime('%m', $when)) == $month);
-        }
-
-        // Calculate $weekday number $which.
-        $when += $which * 60 * 60 * 24 * 7;
-
-        $result['time'] = $when;
-
-        return $result;
-    }
-
-}
-
-/**
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_standard extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'standard';
-    }
-
-    function parsevCalendar($data)
-    {
-        parent::parsevCalendar($data, 'STANDARD');
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('STANDARD');
-    }
-
-}
-
-/**
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_daylight extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'daylight';
-    }
-
-    function parsevCalendar($data)
-    {
-        parent::parsevCalendar($data, 'DAYLIGHT');
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('DAYLIGHT');
-    }
-
-}
-
-/**
- * Class representing vTodos.
- *
- * $Horde: framework/iCalendar/iCalendar/vtodo.php,v 1.13.10.9 2009-01-06 15:23:53 jan Exp $
- *
- * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Mike Cochrane <mike at graftonhall.co.nz>
- * @since   Horde 3.0
- * @package Horde_iCalendar
- */
-class Horde_iCalendar_vtodo extends Horde_iCalendar {
-
-    function getType()
-    {
-        return 'vTodo';
-    }
-
-    function exportvCalendar()
-    {
-        return parent::_exportvData('VTODO');
-    }
-
-    /**
-     * Convert this todo to an array of attributes.
-     *
-     * @return array  Array containing the details of the todo in a hash
-     *                as used by Horde applications.
-     */
-    function toArray()
-    {
-        $todo = array();
-
-        $name = $this->getAttribute('SUMMARY');
-        if (!is_array($name) && !is_a($name, 'PEAR_Error')) {
-            $todo['name'] = $name;
-        }
-        $desc = $this->getAttribute('DESCRIPTION');
-        if (!is_array($desc) && !is_a($desc, 'PEAR_Error')) {
-            $todo['desc'] = $desc;
-        }
-
-        $priority = $this->getAttribute('PRIORITY');
-        if (!is_array($priority) && !is_a($priority, 'PEAR_Error')) {
-            $todo['priority'] = $priority;
-        }
-
-        $due = $this->getAttribute('DTSTAMP');
-        if (!is_array($due) && !is_a($due, 'PEAR_Error')) {
-            $todo['due'] = $due;
-        }
-
-        return $todo;
-    }
-
-    /**
-     * Set the attributes for this todo item from an array.
-     *
-     * @param array $todo  Array containing the details of the todo in
-     *                     the same format that toArray() exports.
-     */
-    function fromArray($todo)
-    {
-        if (isset($todo['name'])) {
-            $this->setAttribute('SUMMARY', $todo['name']);
-        }
-        if (isset($todo['desc'])) {
-            $this->setAttribute('DESCRIPTION', $todo['desc']);
-        }
-
-        if (isset($todo['priority'])) {
-            $this->setAttribute('PRIORITY', $todo['priority']);
-        }
-
-        if (isset($todo['due'])) {
-            $this->setAttribute('DTSTAMP', $todo['due']);
-        }
-    }
-
-}
diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php
deleted file mode 100644
index 66557fd..0000000
--- a/plugins/calendar/lib/calendar_ical.php
+++ /dev/null
@@ -1,569 +0,0 @@
-<?php
-
-/**
- * iCalendar functions for the Calendar plugin
- *
- * @version @package_version@
- * @author Lazlo Westerhof <hello at lazlo.me>
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- * @author Bogomil "Bogo" Shopov <shopov at kolabsys.com>
- *
- * Copyright (C) 2010, Lazlo Westerhof <hello at lazlo.me>
- * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
- *
- * This program is free software: you can redistribute it and/or modify
- * 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 to parse and build vCalendar (iCalendar) files
- *
- * Uses the Horde:iCalendar class for parsing. To install:
- * > pear channel-discover pear.horde.org
- * > pear install horde/Horde_Icalendar
- *
- */
-class calendar_ical
-{
-  const EOL = "\r\n";
-  
-  private $rc;
-  private $cal;
-
-  public $method;
-  public $events = array();
-
-  function __construct($cal)
-  {
-    $this->cal = $cal;
-    $this->rc = $cal->rc;
-  }
-
-  /**
-   * Import events from iCalendar format
-   *
-   * @param  string vCalendar input
-   * @param  string Input charset (from envelope)
-   * @return array List of events extracted from the input
-   */
-  public function import($vcal, $charset = RCMAIL_CHARSET)
-  {
-    $parser = $this->get_parser();
-    $parser->parsevCalendar($vcal, 'VCALENDAR', $charset);
-    $this->method = $parser->getAttributeDefault('METHOD', '');
-    $this->events = $seen = array();
-    if ($data = $parser->getComponents()) {
-      foreach ($data as $comp) {
-        if ($comp->getType() == 'vEvent') {
-          $event = $this->_to_rcube_format($comp);
-          if (!$seen[$event['uid']]++)
-            $this->events[] = $event;
-        }
-      }
-    }
-    
-    return $this->events;
-  }
-
-  /**
-   * Read iCalendar events from a file
-   *
-   * @param string File path to read from
-   * @return array List of events extracted from the file
-   */
-  public function import_from_file($filepath)
-  {
-    $this->events = $seen = array();
-    $fp = fopen($filepath, 'r');
-
-    // check file content first
-    $begin = fread($fp, 1024);
-    if (!preg_match('/BEGIN:VCALENDAR/i', $begin))
-      return $this->events;
-
-    $parser = $this->get_parser();
-    $buffer = '';
-
-    fseek($fp, 0);
-    while (($line = fgets($fp, 2048)) !== false) {
-      $buffer .= $line;
-      if (preg_match('/END:VEVENT/i', $line)) {
-        if (preg_match('/BEGIN:VCALENDAR/i', $buffer))
-          $buffer .= self::EOL ."END:VCALENDAR";
-        $parser->parsevCalendar($buffer, 'VCALENDAR', RCMAIL_CHARSET, false);
-        $buffer = '';
-      }
-    }
-    fclose($fp);
-
-    if ($data = $parser->getComponents()) {
-      foreach ($data as $comp) {
-        if ($comp->getType() == 'vEvent') {
-          $event = $this->_to_rcube_format($comp);
-          if (!$seen[$event['uid']]++)
-            $this->events[] = $event;
-        }
-      }
-    }
-
-    return $this->events;
-  }
-
-  /**
-   * Load iCal parser from the Horde lib
-   */
-  public function get_parser()
-  {
-    if (!class_exists('Horde_iCalendar'))
-      require_once($this->cal->home . '/lib/Horde_iCalendar.php');
-
-    // set target charset for parsed events
-    $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;
-
-    return new Horde_iCalendar;
-  }
-
-  /**
-   * Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format
-   */
-  private function _to_rcube_format($ve)
-  {
-    $event = array(
-      'uid' => $ve->getAttributeDefault('UID'),
-      'changed' => $ve->getAttributeDefault('DTSTAMP', 0),
-      'title' => $ve->getAttributeDefault('SUMMARY'),
-      'start' => $this->_date2time($ve->getAttribute('DTSTART')),
-      'end' => $this->_date2time($ve->getAttribute('DTEND')),
-      // set defaults
-      'free_busy' => 'busy',
-      'priority' => 0,
-      'attendees' => array(),
-    );
-
-    // check for all-day dates
-    if (is_array($ve->getAttribute('DTSTART')))
-      $event['allday'] = true;
-
-    if ($event['allday'])
-      $event['end']->sub(new DateInterval('PT23H'));
-
-    // assign current timezone to event start/end
-    if (is_a($event['start'], 'DateTime'))
-        $event['start']->setTimezone($this->cal->timezone);
-    else
-        unset($event['start']);
-
-    if (is_a($event['end'], 'DateTime'))
-        $event['end']->setTimezone($this->cal->timezone);
-    else
-        unset($event['end']);
-
-    // map other attributes to internal fields
-    $_attendees = array();
-    foreach ($ve->getAllAttributes() as $attr) {
-      switch ($attr['name']) {
-        case 'ORGANIZER':
-          $organizer = array(
-            'name' => $attr['params']['CN'],
-            'email' => preg_replace('/^mailto:/i', '', $attr['value']),
-            'role' => 'ORGANIZER',
-            'status' => 'ACCEPTED',
-          );
-          if (isset($_attendees[$organizer['email']])) {
-            $i = $_attendees[$organizer['email']];
-            $event['attendees'][$i]['role'] = $organizer['role'];
-          }
-          break;
-        
-        case 'ATTENDEE':
-          $attendee = array(
-            'name' => $attr['params']['CN'],
-            'email' => preg_replace('/^mailto:/i', '', $attr['value']),
-            'role' => $attr['params']['ROLE'] ? $attr['params']['ROLE'] : 'REQ-PARTICIPANT',
-            'status' => $attr['params']['PARTSTAT'],
-            'rsvp' => $attr['params']['RSVP'] == 'TRUE',
-          );
-          if ($organizer && $organizer['email'] == $attendee['email'])
-            $attendee['role'] = 'ORGANIZER';
-          
-          $event['attendees'][] = $attendee;
-          $_attendees[$attendee['email']] = count($event['attendees']) - 1;
-          break;
-          
-        case 'TRANSP':
-          $event['free_busy'] = $attr['value'] == 'TRANSPARENT' ? 'free' : 'busy';
-          break;
-        
-        case 'STATUS':
-          if ($attr['value'] == 'TENTATIVE')
-            $event['free_busy'] = 'tentative';
-          else if ($attr['value'] == 'CANCELLED')
-            $event['cancelled'] = true;
-          break;
-        
-        case 'PRIORITY':
-          if (is_numeric($attr['value'])) {
-            $event['priority'] = $attr['value'];
-          }
-          break;
-        
-        case 'RRULE':
-          // parse recurrence rule attributes
-          foreach (explode(';', $attr['value']) as $par) {
-            list($k, $v) = explode('=', $par);
-            $params[$k] = $v;
-          }
-          if ($params['UNTIL'])
-            $params['UNTIL'] = date_create($params['UNTIL']);
-          if (!$params['INTERVAL'])
-            $params['INTERVAL'] = 1;
-          
-          $event['recurrence'] = $params;
-          break;
-        
-        case 'EXDATE':
-          break;
-          
-        case 'RECURRENCE-ID':
-          $event['recurrence_id'] = $this->_date2time($attr['value']);
-          break;
-        
-        case 'SEQUENCE':
-          $event['sequence'] = intval($attr['value']);
-          break;
-        
-        case 'DESCRIPTION':
-        case 'LOCATION':
-        case 'URL':
-          $event[strtolower($attr['name'])] = $attr['value'];
-          break;
-        
-        case 'CLASS':
-        case 'X-CALENDARSERVER-ACCESS':
-          $sensitivity_map = array('PUBLIC' => 0, 'PRIVATE' => 1, 'CONFIDENTIAL' => 2);
-          $event['sensitivity'] = $sensitivity_map[$attr['value']];
-          break;
-
-        case 'X-MICROSOFT-CDO-BUSYSTATUS':
-          if ($attr['value'] == 'OOF')
-            $event['free_busy'] == 'outofoffice';
-          else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE')))
-            $event['free_busy'] = strtolower($attr['value']);
-          break;
-
-        case 'ATTACH':
-          // decode inline attachment
-          if (strtoupper($attr['params']['VALUE']) == 'BINARY' && !empty($attr['value'])) {
-            $data = !strcasecmp($attr['params']['ENCODING'], 'BASE64') ? base64_decode($attr['value']) : $attr['value'];
-            $mimetype = $attr['params']['FMTTYPE'] ? $attr['params']['FMTTYPE'] : rcube_mime::file_content_type($data, $attr['params']['X-LABEL'], 'application/octet-stream', true);
-            $extensions = rcube_mime::get_mime_extensions($mimetype);
-            $filename = $attr['params']['X-LABEL'] ? $attr['params']['X-LABEL'] : 'attachment' . count($event['attachments']) . '.' . $extensions[0];
-            $event['attachments'][] = array(
-              'mimetype' => $mimetype,
-              'name' => $filename,
-              'data' => $data,
-              'size' => strlen($data),
-            );
-          }
-          else if (!empty($attr['value']) && preg_match('!^[hftps]+://!', $attr['value'])) {
-            // TODO: add support for displaying/managing link attachments in UI
-            $event['links'][] = $attr['value'];
-          }
-          break;
-
-        default:
-          if (substr($attr['name'], 0, 2) == 'X-')
-            $event['x-custom'][] = array($attr['name'], $attr['value']);
-      }
-    }
-
-    // find alarms
-    if ($valarm = $ve->findComponent('valarm')) {
-      $action = 'DISPLAY';
-      $trigger = null;
-      
-      foreach ($valarm->getAllAttributes() as $attr) {
-        switch ($attr['name']) {
-          case 'TRIGGER':
-            if ($attr['params']['VALUE'] == 'DATE-TIME') {
-              $trigger = '@' . $attr['value'];
-            }
-            else {
-              $trigger = $attr['value'];
-              $offset = abs($trigger);
-              $unit = 'S';
-              if ($offset % 86400 == 0) {
-                $unit = 'D';
-                $trigger = intval($trigger / 86400);
-              }
-              else if ($offset % 3600 == 0) {
-                $unit = 'H';
-                $trigger = intval($trigger / 3600);
-              }
-              else if ($offset % 60 == 0) {
-                $unit = 'M';
-                $trigger = intval($trigger / 60);
-              }
-            }
-            break;
-
-          case 'ACTION':
-            $action = $attr['value'];
-            break;
-        }
-      }
-      if ($trigger)
-        $event['alarms'] = $trigger . $unit . ':' . $action;
-    }
-
-    // add organizer to attendees list if not already present
-    if ($organizer && !isset($_attendees[$organizer['email']]))
-      array_unshift($event['attendees'], $organizer);
-
-    // make sure the event has an UID
-    if (!$event['uid'])
-      $event['uid'] = $this->cal->generate_uid();
-    
-    return $event;
-  }
-  
-  /**
-   * Helper method to correctly interpret an all-day date value
-   */
-  private function _date2time($prop)
-  {
-    // create timestamp at 12:00 in user's timezone
-    if (is_array($prop))
-      return date_create(sprintf('%04d%02d%02dT120000', $prop['year'], $prop['month'], $prop['mday']), $this->cal->timezone);
-    else if (is_numeric($prop))
-      return date_create('@'.$prop);
-    
-    return $prop;
-  }
-
-
-  /**
-   * Free resources by clearing member vars
-   */
-  public function reset()
-  {
-    $this->method = '';
-    $this->events = array();
-  }
-
-  /**
-   * Export events to iCalendar format
-   *
-   * @param  array   Events as array
-   * @param  string  VCalendar method to advertise
-   * @param  boolean Directly send data to stdout instead of returning
-   * @param  callable Callback function to fetch attachment contents, false if no attachment export
-   * @return string  Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
-   */
-  public function export($events, $method = null, $write = false, $get_attachment = false, $recurrence_id = null)
-  {
-      $memory_limit = parse_bytes(ini_get('memory_limit'));
-
-      if (!$recurrence_id) {
-        $ical = "BEGIN:VCALENDAR" . self::EOL;
-        $ical .= "VERSION:2.0" . self::EOL;
-        $ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL;
-        $ical .= "CALSCALE:GREGORIAN" . self::EOL;
-
-        if ($method)
-          $ical .= "METHOD:" . strtoupper($method) . self::EOL;
-
-        if ($write) {
-          echo $ical;
-          $ical = '';
-        }
-      }
-
-      foreach ($events as $event) {
-        $vevent = "BEGIN:VEVENT" . self::EOL;
-        $vevent .= "UID:" . self::escape($event['uid']) . self::EOL;
-        $vevent .= $this->format_datetime("DTSTAMP", $event['changed'] ?: new DateTime(), false, true) . self::EOL;
-        if ($event['sequence'])
-          $vevent .= "SEQUENCE:" . intval($event['sequence']) . self::EOL;
-        if ($recurrence_id)
-          $vevent .= $recurrence_id . self::EOL;
-        
-        // correctly set all-day dates
-        if ($event['allday']) {
-          $event['end'] = clone $event['end'];
-          $event['end']->add(new DateInterval('P1D'));  // ends the next day
-          $vevent .= $this->format_datetime("DTSTART", $event['start'], true) . self::EOL;
-          $vevent .= $this->format_datetime("DTEND",   $event['end'], true) . self::EOL;
-        }
-        else {
-          $vevent .= $this->format_datetime("DTSTART", $event['start'], false) . self::EOL;
-          $vevent .= $this->format_datetime("DTEND",   $event['end'], false) . self::EOL;
-        }
-        $vevent .= "SUMMARY:" . self::escape($event['title']) . self::EOL;
-        $vevent .= "DESCRIPTION:" . self::escape($event['description']) . self::EOL;
-        
-        if (!empty($event['attendees'])){
-          $vevent .= $this->_get_attendees($event['attendees']);
-        }
-
-        if (!empty($event['location'])) {
-          $vevent .= "LOCATION:" . self::escape($event['location']) . self::EOL;
-        }
-        if (!empty($event['url'])) {
-          $vevent .= "URL:" . self::escape($event['url']) . self::EOL;
-        }
-        if ($event['recurrence'] && !$recurrence_id) {
-          $vevent .= "RRULE:" . libcalendaring::to_rrule($event['recurrence'], self::EOL) . self::EOL;
-        }
-        if(!empty($event['categories'])) {
-          $vevent .= "CATEGORIES:" . self::escape(strtoupper($event['categories'])) . self::EOL;
-        }
-        if ($event['sensitivity'] > 0) {
-          $vevent .= "CLASS:" . ($event['sensitivity'] == 2 ? 'CONFIDENTIAL' : 'PRIVATE') . self::EOL;
-        }
-        if ($event['alarms']) {
-          list($trigger, $action) = explode(':', $event['alarms']);
-          $val = libcalendaring::parse_alaram_value($trigger);
-          
-          $vevent .= "BEGIN:VALARM\n";
-          if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL;
-          else         $vevent .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL;
-          if ($action) $vevent .= "ACTION:" . self::escape(strtoupper($action)) . self::EOL;
-          $vevent .= "END:VALARM\n";
-        }
-        
-        $vevent .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL;
-        
-        if ($event['priority']) {
-          $vevent .= "PRIORITY:" . $event['priority'] . self::EOL;
-        }
-        
-        if ($event['cancelled'])
-          $vevent .= "STATUS:CANCELLED" . self::EOL;
-        else if ($event['free_busy'] == 'tentative')
-          $vevent .= "STATUS:TENTATIVE" . self::EOL;
-        
-        foreach ((array)$event['x-custom'] as $prop)
-          $vevent .= $prop[0] . ':' . self::escape($prop[1]) . self::EOL;
-        
-        // export attachments using the given callback function
-        if (is_callable($get_attachment) && !empty($event['attachments'])) {
-          foreach ((array)$event['attachments'] as $attach) {
-            // check available memory and skip attachment export if we can't buffer it
-            if ($memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024)
-                  && $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) {
-                continue;
-            }
-            // TODO: let the callback print the data directly to stdout (with b64 encoding)
-            if ($data = call_user_func($get_attachment, $attach['id'], $event)) {
-              $vevent .= sprintf('ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=%s;X-LABEL=%s:',
-                self::escape($attach['mimetype']), self::escape($attach['name']));
-              $vevent .= base64_encode($data) . self::EOL;
-            }
-            unset($data);  // attempt to free memory
-          }
-        }
-        
-        $vevent .= "END:VEVENT" . self::EOL;
-
-        // append recurrence exceptions
-        if ($event['recurrence']['EXCEPTIONS'] && !$recurrence_id) {
-          foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
-            $exdate = clone $event['start'];
-            $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
-            $vevent .= $this->export(array($ex), null, false, $get_attachment,
-              $this->format_datetime('RECURRENCE-ID', $exdate, $event['allday']));
-          }
-        }
-
-        if ($write)
-          echo rcube_vcard::rfc2425_fold($vevent);
-        else
-          $ical .= $vevent;
-      }
-      
-      if (!$recurrence_id) {
-        $ical .= "END:VCALENDAR" . self::EOL;
-      
-        if ($write) {
-          echo $ical;
-          return true;
-        }
-      }
-
-      // fold lines to 75 chars
-      return rcube_vcard::rfc2425_fold($ical);
-  }
-
-  private function format_datetime($attr, $dt, $dateonly = false, $utc = false)
-  {
-    if (is_numeric($dt))
-        $dt = new DateTime('@'.$dt);
-
-    if ($utc)
-      $dt->setTimezone(new DateTimeZone('UTC'));
-
-    if ($dateonly) {
-      return $attr . ';VALUE=DATE:' . $dt->format('Ymd');
-    }
-    else {
-      // <ATTR>;TZID=Europe/Zurich:20120706T210000
-      $tz = $dt->getTimezone();
-      $tzname = $tz ? $tz->getName() : null;
-      $tzid = $tzname && $tzname != 'UTC' && $tzname != '+00:00' ? ';TZID=' . self::escape($tzname) : '';
-      return $attr . $tzid . ':' . $dt->format('Ymd\THis' . ($tzid ? '' : '\Z'));
-    }
-  }
-
-  /**
-   * Escape values according to RFC 2445 4.3.11
-   */
-  private function escape($str)
-  {
-    return strtr($str, array('\\' => '\\\\', "\n" => '\n', ';' => '\;', ',' => '\,'));
-  }
-
-  /**
-  * Construct the orginizer of the event.
-  * @param Array Attendees and roles
-  *
-  */
-  private function _get_attendees($ats)
-  {
-    $organizer = "";
-    $attendees = "";
-    foreach ($ats as $at) {
-      if ($at['role'] == "ORGANIZER") {
-        if ($at['email']) {
-          $organizer .= "ORGANIZER;";
-          if (!empty($at['name']))
-            $organizer .= 'CN="' . $at['name'] . '"';
-          $organizer .= ":mailto:". $at['email'] . self::EOL;
-        }
-      }
-      else if ($at['email']) {
-        //I am an attendee 
-        $attendees .= "ATTENDEE;ROLE=" . $at['role'] . ";PARTSTAT=" . $at['status'];
-        if ($at['rsvp'])
-          $attendees .= ";RSVP=TRUE";
-        if (!empty($at['name']))
-          $attendees .= ';CN="' . $at['name'] . '"';
-        $attendees .= ":mailto:" . $at['email'] . self::EOL;
-      }
-    }
-
-    return $organizer . $attendees;
-  }
-
-}
diff --git a/plugins/libcalendaring/lib/Horde_Date.php b/plugins/libcalendaring/lib/Horde_Date.php
new file mode 100644
index 0000000..9197f84
--- /dev/null
+++ b/plugins/libcalendaring/lib/Horde_Date.php
@@ -0,0 +1,1304 @@
+<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ *   Horde/Date/Utils.php, Horde/Date/Recurrence.php
+ * Pull the latest version of these files from the PEAR channel of the Horde
+ * project at http://pear.horde.org by installing the Horde_Date package.
+ */
+
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * @category Horde
+ * @package  Date
+ *
+ * @TODO in format():
+ *   http://php.net/intldateformatter
+ *
+ * @TODO on timezones:
+ *   http://trac.agavi.org/ticket/1008
+ *   http://trac.agavi.org/changeset/3659
+ *
+ * @TODO on switching to PHP::DateTime:
+ *   The only thing ever stored in the database *IS* Unix timestamps. Doing
+ *   anything other than that is unmanageable, yet some frameworks use 'server
+ *   based' times in their systems, simply because they do not bother with
+ *   daylight saving and only 'serve' one timezone!
+ *
+ *   The second you have to manage 'real' time across timezones then daylight
+ *   saving becomes essential, BUT only on the display side! Since the browser
+ *   only provides a time offset, this is useless and to be honest should simply
+ *   be ignored ( until it is upgraded to provide the correct information ;)
+ *   ). So we need a 'display' function that takes a simple numeric epoch, and a
+ *   separate timezone id into which the epoch is to be 'converted'. My W3C
+ *   mapping works simply because ADOdb then converts that to it's own simple
+ *   offset abbreviation - in my case GMT or BST. As long as DateTime passes the
+ *   full 64 bit number the date range from 100AD is also preserved ( and
+ *   further back if 2 digit years are disabled ). If I want to display the
+ *   'real' timezone with this 'time' then I just add it in place of ADOdb's
+ *   'timezone'. I am tempted to simply adjust the ADOdb class to take a
+ *   timezone in place of the simple GMT switch it currently uses.
+ *
+ *   The return path is just the reverse and simply needs to take the client
+ *   display offset off prior to storage of the UTC epoch. SO we use
+ *   DateTimeZone to get an offset value for the clients timezone and simply add
+ *   or subtract this from a timezone agnostic display on the client end when
+ *   entering new times.
+ *
+ *
+ *   It's not really feasible to store dates in specific timezone, as most
+ *   national/local timezones support DST - and that is a pain to support, as
+ *   eg.  sorting breaks when some timestamps get repeated. That's why it's
+ *   usually better to store datetimes as either UTC datetime or plain unix
+ *   timestamp. I usually go with the former - using database datetime type.
+ */
+
+/**
+ * @category Horde
+ * @package  Date
+ */
+class Horde_Date
+{
+    const DATE_SUNDAY = 0;
+    const DATE_MONDAY = 1;
+    const DATE_TUESDAY = 2;
+    const DATE_WEDNESDAY = 3;
+    const DATE_THURSDAY = 4;
+    const DATE_FRIDAY = 5;
+    const DATE_SATURDAY = 6;
+
+    const MASK_SUNDAY = 1;
+    const MASK_MONDAY = 2;
+    const MASK_TUESDAY = 4;
+    const MASK_WEDNESDAY = 8;
+    const MASK_THURSDAY = 16;
+    const MASK_FRIDAY = 32;
+    const MASK_SATURDAY = 64;
+    const MASK_WEEKDAYS = 62;
+    const MASK_WEEKEND = 65;
+    const MASK_ALLDAYS = 127;
+
+    const MASK_SECOND = 1;
+    const MASK_MINUTE = 2;
+    const MASK_HOUR = 4;
+    const MASK_DAY = 8;
+    const MASK_MONTH = 16;
+    const MASK_YEAR = 32;
+    const MASK_ALLPARTS = 63;
+
+    const DATE_DEFAULT = 'Y-m-d H:i:s';
+    const DATE_JSON = 'Y-m-d\TH:i:s';
+
+    /**
+     * Year
+     *
+     * @var integer
+     */
+    protected $_year;
+
+    /**
+     * Month
+     *
+     * @var integer
+     */
+    protected $_month;
+
+    /**
+     * Day
+     *
+     * @var integer
+     */
+    protected $_mday;
+
+    /**
+     * Hour
+     *
+     * @var integer
+     */
+    protected $_hour = 0;
+
+    /**
+     * Minute
+     *
+     * @var integer
+     */
+    protected $_min = 0;
+
+    /**
+     * Second
+     *
+     * @var integer
+     */
+    protected $_sec = 0;
+
+    /**
+     * String representation of the date's timezone.
+     *
+     * @var string
+     */
+    protected $_timezone;
+
+    /**
+     * Default format for __toString()
+     *
+     * @var string
+     */
+    protected $_defaultFormat = self::DATE_DEFAULT;
+
+    /**
+     * Default specs that are always supported.
+     * @var string
+     */
+    protected static $_defaultSpecs = '%CdDeHImMnRStTyY';
+
+    /**
+     * Internally supported strftime() specifiers.
+     * @var string
+     */
+    protected static $_supportedSpecs = '';
+
+    /**
+     * Map of required correction masks.
+     *
+     * @see __set()
+     *
+     * @var array
+     */
+    protected static $_corrections = array(
+        'year'  => self::MASK_YEAR,
+        'month' => self::MASK_MONTH,
+        'mday'  => self::MASK_DAY,
+        'hour'  => self::MASK_HOUR,
+        'min'   => self::MASK_MINUTE,
+        'sec'   => self::MASK_SECOND,
+    );
+
+    protected $_formatCache = array();
+
+    /**
+     * Builds a new date object. If $date contains date parts, use them to
+     * initialize the object.
+     *
+     * Recognized formats:
+     * - arrays with keys 'year', 'month', 'mday', 'day'
+     *   'hour', 'min', 'minute', 'sec'
+     * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
+     * - yyyy-mm-dd hh:mm:ss
+     * - yyyymmddhhmmss
+     * - yyyymmddThhmmssZ
+     * - yyyymmdd (might conflict with unix timestamps between 31 Oct 1966 and
+     *   03 Mar 1973)
+     * - unix timestamps
+     * - anything parsed by strtotime()/DateTime.
+     *
+     * @throws Horde_Date_Exception
+     */
+    public function __construct($date = null, $timezone = null)
+    {
+        if (!self::$_supportedSpecs) {
+            self::$_supportedSpecs = self::$_defaultSpecs;
+            if (function_exists('nl_langinfo')) {
+                self::$_supportedSpecs .= 'bBpxX';
+            }
+        }
+
+        if (func_num_args() > 2) {
+            // Handle args in order: year month day hour min sec tz
+            $this->_initializeFromArgs(func_get_args());
+            return;
+        }
+
+        $this->_initializeTimezone($timezone);
+
+        if (is_null($date)) {
+            return;
+        }
+
+        if (is_string($date)) {
+            $date = trim($date, '"');
+        }
+
+        if (is_object($date)) {
+            $this->_initializeFromObject($date);
+        } elseif (is_array($date)) {
+            $this->_initializeFromArray($date);
+        } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})(?:\.\d+)?(Z?)$/', $date, $parts)) {
+            $this->_year  = (int)$parts[1];
+            $this->_month = (int)$parts[2];
+            $this->_mday  = (int)$parts[3];
+            $this->_hour  = (int)$parts[4];
+            $this->_min   = (int)$parts[5];
+            $this->_sec   = (int)$parts[6];
+            if ($parts[7]) {
+                $this->_initializeTimezone('UTC');
+            }
+        } elseif (preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $date, $parts) &&
+                  $parts[2] > 0 && $parts[2] <= 12 &&
+                  $parts[3] > 0 && $parts[3] <= 31) {
+            $this->_year  = (int)$parts[1];
+            $this->_month = (int)$parts[2];
+            $this->_mday  = (int)$parts[3];
+            $this->_hour = $this->_min = $this->_sec = 0;
+        } elseif ((string)(int)$date == $date) {
+            // Try as a timestamp.
+            $parts = @getdate($date);
+            if ($parts) {
+                $this->_year  = $parts['year'];
+                $this->_month = $parts['mon'];
+                $this->_mday  = $parts['mday'];
+                $this->_hour  = $parts['hours'];
+                $this->_min   = $parts['minutes'];
+                $this->_sec   = $parts['seconds'];
+            }
+        } else {
+            // Use date_create() so we can catch errors with PHP 5.2. Use
+            // "new DateTime() once we require 5.3.
+            $parsed = date_create($date);
+            if (!$parsed) {
+                throw new Horde_Date_Exception(sprintf(Horde_Date_Translation::t("Failed to parse time string (%s)"), $date));
+            }
+            $parsed->setTimezone(new DateTimeZone(date_default_timezone_get()));
+            $this->_year  = (int)$parsed->format('Y');
+            $this->_month = (int)$parsed->format('m');
+            $this->_mday  = (int)$parsed->format('d');
+            $this->_hour  = (int)$parsed->format('H');
+            $this->_min   = (int)$parsed->format('i');
+            $this->_sec   = (int)$parsed->format('s');
+            $this->_initializeTimezone(date_default_timezone_get());
+        }
+    }
+
+    /**
+     * Returns a simple string representation of the date object
+     *
+     * @return string  This object converted to a string.
+     */
+    public function __toString()
+    {
+        try {
+            return $this->format($this->_defaultFormat);
+        } catch (Exception $e) {
+            return '';
+        }
+    }
+
+    /**
+     * Returns a DateTime object representing this object.
+     *
+     * @return DateTime
+     */
+    public function toDateTime()
+    {
+        $date = new DateTime(null, new DateTimeZone($this->_timezone));
+        $date->setDate($this->_year, $this->_month, $this->_mday);
+        $date->setTime($this->_hour, $this->_min, $this->_sec);
+        return $date;
+    }
+
+    /**
+     * Converts a date in the proleptic Gregorian calendar to the no of days
+     * since 24th November, 4714 B.C.
+     *
+     * Returns the no of days since Monday, 24th November, 4714 B.C. in the
+     * proleptic Gregorian calendar (which is 24th November, -4713 using
+     * 'Astronomical' year numbering, and 1st January, 4713 B.C. in the
+     * proleptic Julian calendar).  This is also the first day of the 'Julian
+     * Period' proposed by Joseph Scaliger in 1583, and the number of days
+     * since this date is known as the 'Julian Day'.  (It is not directly
+     * to do with the Julian calendar, although this is where the name
+     * is derived from.)
+     *
+     * The algorithm is valid for all years (positive and negative), and
+     * also for years preceding 4714 B.C.
+     *
+     * Algorithm is from PEAR::Date_Calc
+     *
+     * @author Monte Ohrt <monte at ispi.net>
+     * @author Pierre-Alain Joye <pajoye at php.net>
+     * @author Daniel Convissor <danielc at php.net>
+     * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
+     *
+     * @return integer  The number of days since 24th November, 4714 B.C.
+     */
+    public function toDays()
+    {
+        if (function_exists('GregorianToJD')) {
+            return gregoriantojd($this->_month, $this->_mday, $this->_year);
+        }
+
+        $day = $this->_mday;
+        $month = $this->_month;
+        $year = $this->_year;
+
+        if ($month > 2) {
+            // March = 0, April = 1, ..., December = 9,
+            // January = 10, February = 11
+            $month -= 3;
+        } else {
+            $month += 9;
+            --$year;
+        }
+
+        $hb_negativeyear = $year < 0;
+        $century         = intval($year / 100);
+        $year            = $year % 100;
+
+        if ($hb_negativeyear) {
+            // Subtract 1 because year 0 is a leap year;
+            // And N.B. that we must treat the leap years as occurring
+            // one year earlier than they do, because for the purposes
+            // of calculation, the year starts on 1st March:
+            //
+            return intval((14609700 * $century + ($year == 0 ? 1 : 0)) / 400) +
+                   intval((1461 * $year + 1) / 4) +
+                   intval((153 * $month + 2) / 5) +
+                   $day + 1721118;
+        } else {
+            return intval(146097 * $century / 4) +
+                   intval(1461 * $year / 4) +
+                   intval((153 * $month + 2) / 5) +
+                   $day + 1721119;
+        }
+    }
+
+    /**
+     * Converts number of days since 24th November, 4714 B.C. (in the proleptic
+     * Gregorian calendar, which is year -4713 using 'Astronomical' year
+     * numbering) to Gregorian calendar date.
+     *
+     * Returned date belongs to the proleptic Gregorian calendar, using
+     * 'Astronomical' year numbering.
+     *
+     * The algorithm is valid for all years (positive and negative), and
+     * also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'),
+     * and so the only limitation is platform-dependent (for 32-bit systems
+     * the maximum year would be something like about 1,465,190 A.D.).
+     *
+     * N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'.
+     *
+     * Algorithm is from PEAR::Date_Calc
+     *
+     * @author Monte Ohrt <monte at ispi.net>
+     * @author Pierre-Alain Joye <pajoye at php.net>
+     * @author Daniel Convissor <danielc at php.net>
+     * @author C.A. Woodcock <c01234 at netcomuk.co.uk>
+     *
+     * @param int    $days   the number of days since 24th November, 4714 B.C.
+     * @param string $format the string indicating how to format the output
+     *
+     * @return  Horde_Date  A Horde_Date object representing the date.
+     */
+    public static function fromDays($days)
+    {
+        if (function_exists('JDToGregorian')) {
+            list($month, $day, $year) = explode('/', JDToGregorian($days));
+        } else {
+            $days = intval($days);
+
+            $days   -= 1721119;
+            $century = floor((4 * $days - 1) / 146097);
+            $days    = floor(4 * $days - 1 - 146097 * $century);
+            $day     = floor($days / 4);
+
+            $year = floor((4 * $day +  3) / 1461);
+            $day  = floor(4 * $day +  3 - 1461 * $year);
+            $day  = floor(($day +  4) / 4);
+
+            $month = floor((5 * $day - 3) / 153);
+            $day   = floor(5 * $day - 3 - 153 * $month);
+            $day   = floor(($day +  5) /  5);
+
+            $year = $century * 100 + $year;
+            if ($month < 10) {
+                $month +=3;
+            } else {
+                $month -=9;
+                ++$year;
+            }
+        }
+
+        return new Horde_Date($year, $month, $day);
+    }
+
+    /**
+     * Getter for the date and time properties.
+     *
+     * @param string $name  One of 'year', 'month', 'mday', 'hour', 'min' or
+     *                      'sec'.
+     *
+     * @return integer  The property value, or null if not set.
+     */
+    public function __get($name)
+    {
+        if ($name == 'day') {
+            $name = 'mday';
+        }
+
+        return $this->{'_' . $name};
+    }
+
+    /**
+     * Setter for the date and time properties.
+     *
+     * @param string $name    One of 'year', 'month', 'mday', 'hour', 'min' or
+     *                        'sec'.
+     * @param integer $value  The property value.
+     */
+    public function __set($name, $value)
+    {
+        if ($name == 'timezone') {
+            $this->_initializeTimezone($value);
+            return;
+        }
+        if ($name == 'day') {
+            $name = 'mday';
+        }
+
+        if ($name != 'year' && $name != 'month' && $name != 'mday' &&
+            $name != 'hour' && $name != 'min' && $name != 'sec') {
+            throw new InvalidArgumentException('Undefined property ' . $name);
+        }
+
+        $down = $value < $this->{'_' . $name};
+        $this->{'_' . $name} = $value;
+        $this->_correct(self::$_corrections[$name], $down);
+        $this->_formatCache = array();
+    }
+
+    /**
+     * Returns whether a date or time property exists.
+     *
+     * @param string $name  One of 'year', 'month', 'mday', 'hour', 'min' or
+     *                      'sec'.
+     *
+     * @return boolen  True if the property exists and is set.
+     */
+    public function __isset($name)
+    {
+        if ($name == 'day') {
+            $name = 'mday';
+        }
+        return ($name == 'year' || $name == 'month' || $name == 'mday' ||
+                $name == 'hour' || $name == 'min' || $name == 'sec') &&
+            isset($this->{'_' . $name});
+    }
+
+    /**
+     * Adds a number of seconds or units to this date, returning a new Date
+     * object.
+     */
+    public function add($factor)
+    {
+        $d = clone($this);
+        if (is_array($factor) || is_object($factor)) {
+            foreach ($factor as $property => $value) {
+                $d->$property += $value;
+            }
+        } else {
+            $d->sec += $factor;
+        }
+
+        return $d;
+    }
+
+    /**
+     * Subtracts a number of seconds or units from this date, returning a new
+     * Horde_Date object.
+     */
+    public function sub($factor)
+    {
+        if (is_array($factor)) {
+            foreach ($factor as &$value) {
+                $value *= -1;
+            }
+        } else {
+            $factor *= -1;
+        }
+
+        return $this->add($factor);
+    }
+
+    /**
+     * Converts this object to a different timezone.
+     *
+     * @param string $timezone  The new timezone.
+     *
+     * @return Horde_Date  This object.
+     */
+    public function setTimezone($timezone)
+    {
+        $date = $this->toDateTime();
+        $date->setTimezone(new DateTimeZone($timezone));
+        $this->_timezone = $timezone;
+        $this->_year     = (int)$date->format('Y');
+        $this->_month    = (int)$date->format('m');
+        $this->_mday     = (int)$date->format('d');
+        $this->_hour     = (int)$date->format('H');
+        $this->_min      = (int)$date->format('i');
+        $this->_sec      = (int)$date->format('s');
+        $this->_formatCache = array();
+        return $this;
+    }
+
+    /**
+     * Sets the default date format used in __toString()
+     *
+     * @param string $format
+     */
+    public function setDefaultFormat($format)
+    {
+        $this->_defaultFormat = $format;
+    }
+
+    /**
+     * Returns the day of the week (0 = Sunday, 6 = Saturday) of this date.
+     *
+     * @return integer  The day of the week.
+     */
+    public function dayOfWeek()
+    {
+        if ($this->_month > 2) {
+            $month = $this->_month - 2;
+            $year = $this->_year;
+        } else {
+            $month = $this->_month + 10;
+            $year = $this->_year - 1;
+        }
+
+        $day = (floor((13 * $month - 1) / 5) +
+                $this->_mday + ($year % 100) +
+                floor(($year % 100) / 4) +
+                floor(($year / 100) / 4) - 2 *
+                floor($year / 100) + 77);
+
+        return (int)($day - 7 * floor($day / 7));
+    }
+
+    /**
+     * Returns the day number of the year (1 to 365/366).
+     *
+     * @return integer  The day of the year.
+     */
+    public function dayOfYear()
+    {
+        return $this->format('z') + 1;
+    }
+
+    /**
+     * Returns the week of the month.
+     *
+     * @return integer  The week number.
+     */
+    public function weekOfMonth()
+    {
+        return ceil($this->_mday / 7);
+    }
+
+    /**
+     * Returns the week of the year, first Monday is first day of first week.
+     *
+     * @return integer  The week number.
+     */
+    public function weekOfYear()
+    {
+        return $this->format('W');
+    }
+
+    /**
+     * Returns the number of weeks in the given year (52 or 53).
+     *
+     * @param integer $year  The year to count the number of weeks in.
+     *
+     * @return integer $numWeeks   The number of weeks in $year.
+     */
+    public static function weeksInYear($year)
+    {
+        // Find the last Thursday of the year.
+        $date = new Horde_Date($year . '-12-31');
+        while ($date->dayOfWeek() != self::DATE_THURSDAY) {
+            --$date->mday;
+        }
+        return $date->weekOfYear();
+    }
+
+    /**
+     * Sets the date of this object to the $nth weekday of $weekday.
+     *
+     * @param integer $weekday  The day of the week (0 = Sunday, etc).
+     * @param integer $nth      The $nth $weekday to set to (defaults to 1).
+     */
+    public function setNthWeekday($weekday, $nth = 1)
+    {
+        if ($weekday < self::DATE_SUNDAY || $weekday > self::DATE_SATURDAY) {
+            return;
+        }
+
+        if ($nth < 0) {  // last $weekday of month
+            $this->_mday = $lastday = Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+            $last = $this->dayOfWeek();
+            $this->_mday += ($weekday - $last);
+            if ($this->_mday > $lastday)
+                $this->_mday -= 7;
+        }
+        else {
+            $this->_mday = 1;
+            $first = $this->dayOfWeek();
+            if ($weekday < $first) {
+                $this->_mday = 8 + $weekday - $first;
+            } else {
+                $this->_mday = $weekday - $first + 1;
+            }
+            $diff = 7 * $nth - 7;
+            $this->_mday += $diff;
+            $this->_correct(self::MASK_DAY, $diff < 0);
+        }
+    }
+
+    /**
+     * Is the date currently represented by this object a valid date?
+     *
+     * @return boolean  Validity, counting leap years, etc.
+     */
+    public function isValid()
+    {
+        return ($this->_year >= 0 && $this->_year <= 9999);
+    }
+
+    /**
+     * Compares this date to another date object to see which one is
+     * greater (later). Assumes that the dates are in the same
+     * timezone.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return integer  ==  0 if they are on the same date
+     *                  >=  1 if $this is greater (later)
+     *                  <= -1 if $other is greater (later)
+     */
+    public function compareDate($other)
+    {
+        if (!($other instanceof Horde_Date)) {
+            $other = new Horde_Date($other);
+        }
+
+        if ($this->_year != $other->year) {
+            return $this->_year - $other->year;
+        }
+        if ($this->_month != $other->month) {
+            return $this->_month - $other->month;
+        }
+
+        return $this->_mday - $other->mday;
+    }
+
+    /**
+     * Returns whether this date is after the other.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return boolean  True if this date is after the other.
+     */
+    public function after($other)
+    {
+        return $this->compareDate($other) > 0;
+    }
+
+    /**
+     * Returns whether this date is before the other.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return boolean  True if this date is before the other.
+     */
+    public function before($other)
+    {
+        return $this->compareDate($other) < 0;
+    }
+
+    /**
+     * Returns whether this date is the same like the other.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return boolean  True if this date is the same like the other.
+     */
+    public function equals($other)
+    {
+        return $this->compareDate($other) == 0;
+    }
+
+    /**
+     * Compares this to another date object by time, to see which one
+     * is greater (later). Assumes that the dates are in the same
+     * timezone.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return integer  ==  0 if they are at the same time
+     *                  >=  1 if $this is greater (later)
+     *                  <= -1 if $other is greater (later)
+     */
+    public function compareTime($other)
+    {
+        if (!($other instanceof Horde_Date)) {
+            $other = new Horde_Date($other);
+        }
+
+        if ($this->_hour != $other->hour) {
+            return $this->_hour - $other->hour;
+        }
+        if ($this->_min != $other->min) {
+            return $this->_min - $other->min;
+        }
+
+        return $this->_sec - $other->sec;
+    }
+
+    /**
+     * Compares this to another date object, including times, to see
+     * which one is greater (later). Assumes that the dates are in the
+     * same timezone.
+     *
+     * @param mixed $other  The date to compare to.
+     *
+     * @return integer  ==  0 if they are equal
+     *                  >=  1 if $this is greater (later)
+     *                  <= -1 if $other is greater (later)
+     */
+    public function compareDateTime($other)
+    {
+        if (!($other instanceof Horde_Date)) {
+            $other = new Horde_Date($other);
+        }
+
+        if ($diff = $this->compareDate($other)) {
+            return $diff;
+        }
+
+        return $this->compareTime($other);
+    }
+
+    /**
+     * Returns number of days between this date and another.
+     *
+     * @param Horde_Date $other  The other day to diff with.
+     *
+     * @return integer  The absolute number of days between the two dates.
+     */
+    public function diff($other)
+    {
+        return abs($this->toDays() - $other->toDays());
+    }
+
+    /**
+     * Returns the time offset for local time zone.
+     *
+     * @param boolean $colon  Place a colon between hours and minutes?
+     *
+     * @return string  Timezone offset as a string in the format +HH:MM.
+     */
+    public function tzOffset($colon = true)
+    {
+        return $colon ? $this->format('P') : $this->format('O');
+    }
+
+    /**
+     * Returns the unix timestamp representation of this date.
+     *
+     * @return integer  A unix timestamp.
+     */
+    public function timestamp()
+    {
+        if ($this->_year >= 1970 && $this->_year < 2038) {
+            return mktime($this->_hour, $this->_min, $this->_sec,
+                          $this->_month, $this->_mday, $this->_year);
+        }
+        return $this->format('U');
+    }
+
+    /**
+     * Returns the unix timestamp representation of this date, 12:00am.
+     *
+     * @return integer  A unix timestamp.
+     */
+    public function datestamp()
+    {
+        if ($this->_year >= 1970 && $this->_year < 2038) {
+            return mktime(0, 0, 0, $this->_month, $this->_mday, $this->_year);
+        }
+        $date = new DateTime($this->format('Y-m-d'));
+        return $date->format('U');
+    }
+
+    /**
+     * Formats date and time to be passed around as a short url parameter.
+     *
+     * @return string  Date and time.
+     */
+    public function dateString()
+    {
+        return sprintf('%04d%02d%02d', $this->_year, $this->_month, $this->_mday);
+    }
+
+    /**
+     * Formats date and time to the ISO format used by JSON.
+     *
+     * @return string  Date and time.
+     */
+    public function toJson()
+    {
+        return $this->format(self::DATE_JSON);
+    }
+
+    /**
+     * Formats date and time to the RFC 2445 iCalendar DATE-TIME format.
+     *
+     * @param boolean $floating  Whether to return a floating date-time
+     *                           (without time zone information).
+     *
+     * @return string  Date and time.
+     */
+    public function toiCalendar($floating = false)
+    {
+        if ($floating) {
+            return $this->format('Ymd\THis');
+        }
+        $dateTime = $this->toDateTime();
+        $dateTime->setTimezone(new DateTimeZone('UTC'));
+        return $dateTime->format('Ymd\THis\Z');
+    }
+
+    /**
+     * Formats time using the specifiers available in date() or in the DateTime
+     * class' format() method.
+     *
+     * To format in languages other than English, use strftime() instead.
+     *
+     * @param string $format
+     *
+     * @return string  Formatted time.
+     */
+    public function format($format)
+    {
+        if (!isset($this->_formatCache[$format])) {
+            $this->_formatCache[$format] = $this->toDateTime()->format($format);
+        }
+        return $this->_formatCache[$format];
+    }
+
+    /**
+     * Formats date and time using strftime() format.
+     *
+     * @return string  strftime() formatted date and time.
+     */
+    public function strftime($format)
+    {
+        if (preg_match('/%[^' . self::$_supportedSpecs . ']/', $format)) {
+            return strftime($format, $this->timestamp());
+        } else {
+            return $this->_strftime($format);
+        }
+    }
+
+    /**
+     * Formats date and time using a limited set of the strftime() format.
+     *
+     * @return string  strftime() formatted date and time.
+     */
+    protected function _strftime($format)
+    {
+        return preg_replace(
+            array('/%b/e',
+                  '/%B/e',
+                  '/%C/e',
+                  '/%d/e',
+                  '/%D/e',
+                  '/%e/e',
+                  '/%H/e',
+                  '/%I/e',
+                  '/%m/e',
+                  '/%M/e',
+                  '/%n/',
+                  '/%p/e',
+                  '/%R/e',
+                  '/%S/e',
+                  '/%t/',
+                  '/%T/e',
+                  '/%x/e',
+                  '/%X/e',
+                  '/%y/e',
+                  '/%Y/',
+                  '/%%/'),
+            array('$this->_strftime(Horde_Nls::getLangInfo(constant(\'ABMON_\' . (int)$this->_month)))',
+                  '$this->_strftime(Horde_Nls::getLangInfo(constant(\'MON_\' . (int)$this->_month)))',
+                  '(int)($this->_year / 100)',
+                  'sprintf(\'%02d\', $this->_mday)',
+                  '$this->_strftime(\'%m/%d/%y\')',
+                  'sprintf(\'%2d\', $this->_mday)',
+                  'sprintf(\'%02d\', $this->_hour)',
+                  'sprintf(\'%02d\', $this->_hour == 0 ? 12 : ($this->_hour > 12 ? $this->_hour - 12 : $this->_hour))',
+                  'sprintf(\'%02d\', $this->_month)',
+                  'sprintf(\'%02d\', $this->_min)',
+                  "\n",
+                  '$this->_strftime(Horde_Nls::getLangInfo($this->_hour < 12 ? AM_STR : PM_STR))',
+                  '$this->_strftime(\'%H:%M\')',
+                  'sprintf(\'%02d\', $this->_sec)',
+                  "\t",
+                  '$this->_strftime(\'%H:%M:%S\')',
+                  '$this->_strftime(Horde_Nls::getLangInfo(D_FMT))',
+                  '$this->_strftime(Horde_Nls::getLangInfo(T_FMT))',
+                  'substr(sprintf(\'%04d\', $this->_year), -2)',
+                  (int)$this->_year,
+                  '%'),
+            $format);
+    }
+
+    /**
+     * Corrects any over- or underflows in any of the date's members.
+     *
+     * @param integer $mask  We may not want to correct some overflows.
+     * @param integer $down  Whether to correct the date up or down.
+     */
+    protected function _correct($mask = self::MASK_ALLPARTS, $down = false)
+    {
+        if ($mask & self::MASK_SECOND) {
+            if ($this->_sec < 0 || $this->_sec > 59) {
+                $mask |= self::MASK_MINUTE;
+
+                $this->_min += (int)($this->_sec / 60);
+                $this->_sec %= 60;
+                if ($this->_sec < 0) {
+                    $this->_min--;
+                    $this->_sec += 60;
+                }
+            }
+        }
+
+        if ($mask & self::MASK_MINUTE) {
+            if ($this->_min < 0 || $this->_min > 59) {
+                $mask |= self::MASK_HOUR;
+
+                $this->_hour += (int)($this->_min / 60);
+                $this->_min %= 60;
+                if ($this->_min < 0) {
+                    $this->_hour--;
+                    $this->_min += 60;
+                }
+            }
+        }
+
+        if ($mask & self::MASK_HOUR) {
+            if ($this->_hour < 0 || $this->_hour > 23) {
+                $mask |= self::MASK_DAY;
+
+                $this->_mday += (int)($this->_hour / 24);
+                $this->_hour %= 24;
+                if ($this->_hour < 0) {
+                    $this->_mday--;
+                    $this->_hour += 24;
+                }
+            }
+        }
+
+        if ($mask & self::MASK_MONTH) {
+            $this->_correctMonth($down);
+            /* When correcting the month, always correct the day too. Months
+             * have different numbers of days. */
+            $mask |= self::MASK_DAY;
+        }
+
+        if ($mask & self::MASK_DAY) {
+            while ($this->_mday > 28 &&
+                   $this->_mday > Horde_Date_Utils::daysInMonth($this->_month, $this->_year)) {
+                if ($down) {
+                    $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month + 1, $this->_year) - Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+                } else {
+                    $this->_mday -= Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+                    $this->_month++;
+                }
+                $this->_correctMonth($down);
+            }
+            while ($this->_mday < 1) {
+                --$this->_month;
+                $this->_correctMonth($down);
+                $this->_mday += Horde_Date_Utils::daysInMonth($this->_month, $this->_year);
+            }
+        }
+    }
+
+    /**
+     * Corrects the current month.
+     *
+     * This cannot be done in _correct() because that would also trigger a
+     * correction of the day, which would result in an infinite loop.
+     *
+     * @param integer $down  Whether to correct the date up or down.
+     */
+    protected function _correctMonth($down = false)
+    {
+        $this->_year += (int)($this->_month / 12);
+        $this->_month %= 12;
+        if ($this->_month < 1) {
+            $this->_year--;
+            $this->_month += 12;
+        }
+    }
+
+    /**
+     * Handles args in order: year month day hour min sec tz
+     */
+    protected function _initializeFromArgs($args)
+    {
+        $tz = (isset($args[6])) ? array_pop($args) : null;
+        $this->_initializeTimezone($tz);
+
+        $args = array_slice($args, 0, 6);
+        $keys = array('year' => 1, 'month' => 1, 'mday' => 1, 'hour' => 0, 'min' => 0, 'sec' => 0);
+        $date = array_combine(array_slice(array_keys($keys), 0, count($args)), $args);
+        $date = array_merge($keys, $date);
+
+        $this->_initializeFromArray($date);
+    }
+
+    protected function _initializeFromArray($date)
+    {
+        if (isset($date['year']) && is_string($date['year']) && strlen($date['year']) == 2) {
+            if ($date['year'] > 70) {
+                $date['year'] += 1900;
+            } else {
+                $date['year'] += 2000;
+            }
+        }
+
+        foreach ($date as $key => $val) {
+            if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
+                $this->{'_'. $key} = (int)$val;
+            }
+        }
+
+        // If $date['day'] is present and numeric we may have been passed
+        // a Horde_Form_datetime array.
+        if (isset($date['day']) &&
+            (string)(int)$date['day'] == $date['day']) {
+            $this->_mday = (int)$date['day'];
+        }
+        // 'minute' key also from Horde_Form_datetime
+        if (isset($date['minute']) &&
+            (string)(int)$date['minute'] == $date['minute']) {
+            $this->_min = (int)$date['minute'];
+        }
+
+        $this->_correct();
+    }
+
+    protected function _initializeFromObject($date)
+    {
+        if ($date instanceof DateTime) {
+            $this->_year  = (int)$date->format('Y');
+            $this->_month = (int)$date->format('m');
+            $this->_mday  = (int)$date->format('d');
+            $this->_hour  = (int)$date->format('H');
+            $this->_min   = (int)$date->format('i');
+            $this->_sec   = (int)$date->format('s');
+            $this->_initializeTimezone($date->getTimezone()->getName());
+        } else {
+            $is_horde_date = $date instanceof Horde_Date;
+            foreach (array('year', 'month', 'mday', 'hour', 'min', 'sec') as $key) {
+                if ($is_horde_date || isset($date->$key)) {
+                    $this->{'_' . $key} = (int)$date->$key;
+                }
+            }
+            if (!$is_horde_date) {
+                $this->_correct();
+            } else {
+                $this->_initializeTimezone($date->timezone);
+            }
+        }
+    }
+
+    protected function _initializeTimezone($timezone)
+    {
+        if (empty($timezone)) {
+            $timezone = date_default_timezone_get();
+        }
+        $this->_timezone = $timezone;
+    }
+
+}
+
+/**
+ * @category Horde
+ * @package  Date
+ */
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * @category Horde
+ * @package  Date
+ */
+class Horde_Date_Utils
+{
+    /**
+     * Returns whether a year is a leap year.
+     *
+     * @param integer $year  The year.
+     *
+     * @return boolean  True if the year is a leap year.
+     */
+    public static function isLeapYear($year)
+    {
+        if (strlen($year) != 4 || preg_match('/\D/', $year)) {
+            return false;
+        }
+
+        return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
+    }
+
+    /**
+     * Returns the date of the year that corresponds to the first day of the
+     * given week.
+     *
+     * @param integer $week  The week of the year to find the first day of.
+     * @param integer $year  The year to calculate for.
+     *
+     * @return Horde_Date  The date of the first day of the given week.
+     */
+    public static function firstDayOfWeek($week, $year)
+    {
+        return new Horde_Date(sprintf('%04dW%02d', $year, $week));
+    }
+
+    /**
+     * Returns the number of days in the specified month.
+     *
+     * @param integer $month  The month
+     * @param integer $year   The year.
+     *
+     * @return integer  The number of days in the month.
+     */
+    public static function daysInMonth($month, $year)
+    {
+        static $cache = array();
+        if (!isset($cache[$year][$month])) {
+            $date = new DateTime(sprintf('%04d-%02d-01', $year, $month));
+            $cache[$year][$month] = $date->format('t');
+        }
+        return $cache[$year][$month];
+    }
+
+    /**
+     * Returns a relative, natural language representation of a timestamp
+     *
+     * @todo Wider range of values ... maybe future time as well?
+     * @todo Support minimum resolution parameter.
+     *
+     * @param mixed $time          The time. Any format accepted by Horde_Date.
+     * @param string $date_format  Format to display date if timestamp is
+     *                             more then 1 day old.
+     * @param string $time_format  Format to display time if timestamp is 1
+     *                             day old.
+     *
+     * @return string  The relative time (i.e. 2 minutes ago)
+     */
+    public static function relativeDateTime($time, $date_format = '%x',
+                                            $time_format = '%X')
+    {
+        $date = new Horde_Date($time);
+
+        $delta = time() - $date->timestamp();
+        if ($delta < 60) {
+            return sprintf(Horde_Date_Translation::ngettext("%d second ago", "%d seconds ago", $delta), $delta);
+        }
+
+        $delta = round($delta / 60);
+        if ($delta < 60) {
+            return sprintf(Horde_Date_Translation::ngettext("%d minute ago", "%d minutes ago", $delta), $delta);
+        }
+
+        $delta = round($delta / 60);
+        if ($delta < 24) {
+            return sprintf(Horde_Date_Translation::ngettext("%d hour ago", "%d hours ago", $delta), $delta);
+        }
+
+        if ($delta > 24 && $delta < 48) {
+            $date = new Horde_Date($time);
+            return sprintf(Horde_Date_Translation::t("yesterday at %s"), $date->strftime($time_format));
+        }
+
+        $delta = round($delta / 24);
+        if ($delta < 7) {
+            return sprintf(Horde_Date_Translation::t("%d days ago"), $delta);
+        }
+
+        if (round($delta / 7) < 5) {
+            $delta = round($delta / 7);
+            return sprintf(Horde_Date_Translation::ngettext("%d week ago", "%d weeks ago", $delta), $delta);
+        }
+
+        // Default to the user specified date format.
+        return $date->strftime($date_format);
+    }
+
+    /**
+     * Tries to convert strftime() formatters to date() formatters.
+     *
+     * Unsupported formatters will be removed.
+     *
+     * @param string $format  A strftime() formatting string.
+     *
+     * @return string  A date() formatting string.
+     */
+    public static function strftime2date($format)
+    {
+        $replace = array(
+            '/%a/'  => 'D',
+            '/%A/'  => 'l',
+            '/%d/'  => 'd',
+            '/%e/'  => 'j',
+            '/%j/'  => 'z',
+            '/%u/'  => 'N',
+            '/%w/'  => 'w',
+            '/%U/'  => '',
+            '/%V/'  => 'W',
+            '/%W/'  => '',
+            '/%b/'  => 'M',
+            '/%B/'  => 'F',
+            '/%h/'  => 'M',
+            '/%m/'  => 'm',
+            '/%C/'  => '',
+            '/%g/'  => '',
+            '/%G/'  => 'o',
+            '/%y/'  => 'y',
+            '/%Y/'  => 'Y',
+            '/%H/'  => 'H',
+            '/%I/'  => 'h',
+            '/%i/'  => 'g',
+            '/%M/'  => 'i',
+            '/%p/'  => 'A',
+            '/%P/'  => 'a',
+            '/%r/'  => 'h:i:s A',
+            '/%R/'  => 'H:i',
+            '/%S/'  => 's',
+            '/%T/'  => 'H:i:s',
+            '/%X/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(T_FMT))',
+            '/%z/'  => 'O',
+            '/%Z/'  => '',
+            '/%c/'  => '',
+            '/%D/'  => 'm/d/y',
+            '/%F/'  => 'Y-m-d',
+            '/%s/'  => 'U',
+            '/%x/e' => 'Horde_Date_Utils::strftime2date(Horde_Nls::getLangInfo(D_FMT))',
+            '/%n/'  => "\n",
+            '/%t/'  => "\t",
+            '/%%/'  => '%'
+        );
+
+        return preg_replace(array_keys($replace), array_values($replace), $format);
+    }
+
+}
diff --git a/plugins/libcalendaring/lib/Horde_iCalendar.php b/plugins/libcalendaring/lib/Horde_iCalendar.php
new file mode 100644
index 0000000..6d75d27
--- /dev/null
+++ b/plugins/libcalendaring/lib/Horde_iCalendar.php
@@ -0,0 +1,3300 @@
+<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ *   Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
+ */
+
+if (!class_exists('Horde_Date'))
+    require_once(dirname(__FILE__) . '/Horde_Date.php');
+
+
+$GLOBALS['_HORDE_STRING_CHARSET'] = 'iso-8859-1';
+
+/**
+ * The String:: class provides static methods for charset and locale safe
+ * string manipulation.
+ *
+ * $Horde: framework/Util/String.php,v 1.43.6.38 2009-09-15 16:36:14 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Jan Schneider <jan at horde.org>
+ * @since   Horde 3.0
+ * @package Horde_Util
+ */
+class String {
+
+    /**
+     * Caches the result of extension_loaded() calls.
+     *
+     * @param string $ext  The extension name.
+     *
+     * @return boolean  Is the extension loaded?
+     *
+     * @see Util::extensionExists()
+     */
+    function extensionExists($ext)
+    {
+        static $cache = array();
+
+        if (!isset($cache[$ext])) {
+            $cache[$ext] = extension_loaded($ext);
+        }
+
+        return $cache[$ext];
+    }
+
+    /**
+     * Sets a default charset that the String:: methods will use if none is
+     * explicitly specified.
+     *
+     * @param string $charset  The charset to use as the default one.
+     */
+    function setDefaultCharset($charset)
+    {
+        $GLOBALS['_HORDE_STRING_CHARSET'] = $charset;
+        if (String::extensionExists('mbstring') &&
+            function_exists('mb_regex_encoding')) {
+            $old_error = error_reporting(0);
+            mb_regex_encoding(String::_mbstringCharset($charset));
+            error_reporting($old_error);
+        }
+    }
+
+    /**
+     * Converts a string from one charset to another.
+     *
+     * Works only if either the iconv or the mbstring extension
+     * are present and best if both are available.
+     * The original string is returned if conversion failed or none
+     * of the extensions were available.
+     *
+     * @param mixed $input  The data to be converted. If $input is an an array,
+     *                      the array's values get converted recursively.
+     * @param string $from  The string's current charset.
+     * @param string $to    The charset to convert the string to. If not
+     *                      specified, the global variable
+     *                      $_HORDE_STRING_CHARSET will be used.
+     *
+     * @return mixed  The converted input data.
+     */
+    function convertCharset($input, $from, $to = null)
+    {
+        /* Don't bother converting numbers. */
+        if (is_numeric($input)) {
+            return $input;
+        }
+
+        /* Get the user's default character set if none passed in. */
+        if (is_null($to)) {
+            $to = $GLOBALS['_HORDE_STRING_CHARSET'];
+        }
+
+        /* If the from and to character sets are identical, return now. */
+        if ($from == $to) {
+            return $input;
+        }
+        $from = String::lower($from);
+        $to = String::lower($to);
+        if ($from == $to) {
+            return $input;
+        }
+
+        if (is_array($input)) {
+            $tmp = array();
+            reset($input);
+            while (list($key, $val) = each($input)) {
+                $tmp[String::_convertCharset($key, $from, $to)] = String::convertCharset($val, $from, $to);
+            }
+            return $tmp;
+        }
+        if (is_object($input)) {
+            // PEAR_Error objects are almost guaranteed to contain recursion,
+            // which will cause a segfault in PHP.  We should never reach
+            // this line, but add a check and a log message to help the devs
+            // track down and fix this issue.
+            if (is_a($input, 'PEAR_Error')) {
+                Horde::logMessage('Called convertCharset() on a PEAR_Error object. ' . print_r($input, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+                return '';
+            }
+            $vars = get_object_vars($input);
+            while (list($key, $val) = each($vars)) {
+                $input->$key = String::convertCharset($val, $from, $to);
+            }
+            return $input;
+        }
+
+        if (!is_string($input)) {
+            return $input;
+        }
+
+        return String::_convertCharset($input, $from, $to);
+    }
+
+    /**
+     * Internal function used to do charset conversion.
+     *
+     * @access private
+     *
+     * @param string $input  See String::convertCharset().
+     * @param string $from   See String::convertCharset().
+     * @param string $to     See String::convertCharset().
+     *
+     * @return string  The converted string.
+     */
+    function _convertCharset($input, $from, $to)
+    {
+        $output = '';
+        $from_check = (($from == 'iso-8859-1') || ($from == 'us-ascii'));
+        $to_check = (($to == 'iso-8859-1') || ($to == 'us-ascii'));
+
+        /* Use utf8_[en|de]code() if possible and if the string isn't too
+         * large (less than 16 MB = 16 * 1024 * 1024 = 16777216 bytes) - these
+         * functions use more memory. */
+        if (strlen($input) < 16777216 || !(String::extensionExists('iconv') || String::extensionExists('mbstring'))) {
+            if ($from_check && ($to == 'utf-8')) {
+                return utf8_encode($input);
+            }
+
+            if (($from == 'utf-8') && $to_check) {
+                return utf8_decode($input);
+            }
+        }
+
+        /* First try iconv with transliteration. */
+        if (($from != 'utf7-imap') &&
+            ($to != 'utf7-imap') &&
+            String::extensionExists('iconv')) {
+            /* We need to tack an extra character temporarily because of a bug
+             * in iconv() if the last character is not a 7 bit ASCII
+             * character. */
+            $oldTrackErrors = ini_set('track_errors', 1);
+            unset($php_errormsg);
+            $output = @iconv($from, $to . '//TRANSLIT', $input . 'x');
+            $output = (isset($php_errormsg)) ? false : String::substr($output, 0, -1, $to);
+            ini_set('track_errors', $oldTrackErrors);
+        }
+
+        /* Next try mbstring. */
+        if (!$output && String::extensionExists('mbstring')) {
+            $old_error = error_reporting(0);
+            $output = mb_convert_encoding($input, $to, String::_mbstringCharset($from));
+            error_reporting($old_error);
+        }
+
+        /* At last try imap_utf7_[en|de]code if appropriate. */
+        if (!$output && String::extensionExists('imap')) {
+            if ($from_check && ($to == 'utf7-imap')) {
+                return @imap_utf7_encode($input);
+            }
+            if (($from == 'utf7-imap') && $to_check) {
+                return @imap_utf7_decode($input);
+            }
+        }
+
+        return (!$output) ? $input : $output;
+    }
+
+    /**
+     * Makes a string lowercase.
+     *
+     * @param string  $string   The string to be converted.
+     * @param boolean $locale   If true the string will be converted based on a
+     *                          given charset, locale independent else.
+     * @param string  $charset  If $locale is true, the charset to use when
+     *                          converting. If not provided the current charset.
+     *
+     * @return string  The string with lowercase characters
+     */
+    function lower($string, $locale = false, $charset = null)
+    {
+        static $lowers;
+
+        if ($locale) {
+            /* The existence of mb_strtolower() depends on the platform. */
+            if (String::extensionExists('mbstring') &&
+                function_exists('mb_strtolower')) {
+                if (is_null($charset)) {
+                    $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+                }
+                $old_error = error_reporting(0);
+                $ret = mb_strtolower($string, String::_mbstringCharset($charset));
+                error_reporting($old_error);
+                if (!empty($ret)) {
+                    return $ret;
+                }
+            }
+            return strtolower($string);
+        }
+
+        if (!isset($lowers)) {
+            $lowers = array();
+        }
+        if (!isset($lowers[$string])) {
+            $language = setlocale(LC_CTYPE, 0);
+            setlocale(LC_CTYPE, 'C');
+            $lowers[$string] = strtolower($string);
+            setlocale(LC_CTYPE, $language);
+        }
+
+        return $lowers[$string];
+    }
+
+    /**
+     * Makes a string uppercase.
+     *
+     * @param string  $string   The string to be converted.
+     * @param boolean $locale   If true the string will be converted based on a
+     *                          given charset, locale independent else.
+     * @param string  $charset  If $locale is true, the charset to use when
+     *                          converting. If not provided the current charset.
+     *
+     * @return string  The string with uppercase characters
+     */
+    function upper($string, $locale = false, $charset = null)
+    {
+        static $uppers;
+
+        if ($locale) {
+            /* The existence of mb_strtoupper() depends on the
+             * platform. */
+            if (function_exists('mb_strtoupper')) {
+                if (is_null($charset)) {
+                    $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+                }
+                $old_error = error_reporting(0);
+                $ret = mb_strtoupper($string, String::_mbstringCharset($charset));
+                error_reporting($old_error);
+                if (!empty($ret)) {
+                    return $ret;
+                }
+            }
+            return strtoupper($string);
+        }
+
+        if (!isset($uppers)) {
+            $uppers = array();
+        }
+        if (!isset($uppers[$string])) {
+            $language = setlocale(LC_CTYPE, 0);
+            setlocale(LC_CTYPE, 'C');
+            $uppers[$string] = strtoupper($string);
+            setlocale(LC_CTYPE, $language);
+        }
+
+        return $uppers[$string];
+    }
+
+    /**
+     * Returns a string with the first letter capitalized if it is
+     * alphabetic.
+     *
+     * @param string  $string   The string to be capitalized.
+     * @param boolean $locale   If true the string will be converted based on a
+     *                          given charset, locale independent else.
+     * @param string  $charset  The charset to use, defaults to current charset.
+     *
+     * @return string  The capitalized string.
+     */
+    function ucfirst($string, $locale = false, $charset = null)
+    {
+        if ($locale) {
+            $first = String::substr($string, 0, 1, $charset);
+            if (String::isAlpha($first, $charset)) {
+                $string = String::upper($first, true, $charset) . String::substr($string, 1, null, $charset);
+            }
+        } else {
+            $string = String::upper(substr($string, 0, 1), false) . substr($string, 1);
+        }
+        return $string;
+    }
+
+    /**
+     * Returns part of a string.
+     *
+     * @param string $string   The string to be converted.
+     * @param integer $start   The part's start position, zero based.
+     * @param integer $length  The part's length.
+     * @param string $charset  The charset to use when calculating the part's
+     *                         position and length, defaults to current
+     *                         charset.
+     *
+     * @return string  The string's part.
+     */
+    function substr($string, $start, $length = null, $charset = null)
+    {
+        if (is_null($length)) {
+            $length = String::length($string, $charset) - $start;
+        }
+
+        if ($length == 0) {
+            return '';
+        }
+
+        /* Try iconv. */
+        if (function_exists('iconv_substr')) {
+            if (is_null($charset)) {
+                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+            }
+
+            $old_error = error_reporting(0);
+            $ret = iconv_substr($string, $start, $length, $charset);
+            error_reporting($old_error);
+            /* iconv_substr() returns false on failure. */
+            if ($ret !== false) {
+                return $ret;
+            }
+        }
+
+        /* Try mbstring. */
+        if (String::extensionExists('mbstring')) {
+            if (is_null($charset)) {
+                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+            }
+            $old_error = error_reporting(0);
+            $ret = mb_substr($string, $start, $length, String::_mbstringCharset($charset));
+            error_reporting($old_error);
+            /* mb_substr() returns empty string on failure. */
+            if (strlen($ret)) {
+                return $ret;
+            }
+        }
+
+        return substr($string, $start, $length);
+    }
+
+    /**
+     * Returns the character (not byte) length of a string.
+     *
+     * @param string $string  The string to return the length of.
+     * @param string $charset The charset to use when calculating the string's
+     *                        length.
+     *
+     * @return string  The string's part.
+     */
+    function length($string, $charset = null)
+    {
+        if (is_null($charset)) {
+            $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+        }
+        $charset = String::lower($charset);
+        if ($charset == 'utf-8' || $charset == 'utf8') {
+            return strlen(utf8_decode($string));
+        }
+        if (String::extensionExists('mbstring')) {
+            $old_error = error_reporting(0);
+            $ret = mb_strlen($string, String::_mbstringCharset($charset));
+            error_reporting($old_error);
+            if (!empty($ret)) {
+                return $ret;
+            }
+        }
+        return strlen($string);
+    }
+
+    /**
+     * Returns the numeric position of the first occurrence of $needle
+     * in the $haystack string.
+     *
+     * @param string $haystack  The string to search through.
+     * @param string $needle    The string to search for.
+     * @param integer $offset   Allows to specify which character in haystack
+     *                          to start searching.
+     * @param string $charset   The charset to use when searching for the
+     *                          $needle string.
+     *
+     * @return integer  The position of first occurrence.
+     */
+    function pos($haystack, $needle, $offset = 0, $charset = null)
+    {
+        if (String::extensionExists('mbstring')) {
+            if (is_null($charset)) {
+                $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+            }
+            $track_errors = ini_set('track_errors', 1);
+            $old_error = error_reporting(0);
+            $ret = mb_strpos($haystack, $needle, $offset, String::_mbstringCharset($charset));
+            error_reporting($old_error);
+            ini_set('track_errors', $track_errors);
+            if (!isset($php_errormsg)) {
+                return $ret;
+            }
+        }
+        return strpos($haystack, $needle, $offset);
+    }
+
+    /**
+     * Returns a string padded to a certain length with another string.
+     *
+     * This method behaves exactly like str_pad but is multibyte safe.
+     *
+     * @param string $input    The string to be padded.
+     * @param integer $length  The length of the resulting string.
+     * @param string $pad      The string to pad the input string with. Must
+     *                         be in the same charset like the input string.
+     * @param const $type      The padding type. One of STR_PAD_LEFT,
+     *                         STR_PAD_RIGHT, or STR_PAD_BOTH.
+     * @param string $charset  The charset of the input and the padding
+     *                         strings.
+     *
+     * @return string  The padded string.
+     */
+    function pad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT,
+                 $charset = null)
+    {
+        $mb_length = String::length($input, $charset);
+        $sb_length = strlen($input);
+        $pad_length = String::length($pad, $charset);
+
+        /* Return if we already have the length. */
+        if ($mb_length >= $length) {
+            return $input;
+        }
+
+        /* Shortcut for single byte strings. */
+        if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
+            return str_pad($input, $length, $pad, $type);
+        }
+
+        switch ($type) {
+        case STR_PAD_LEFT:
+            $left = $length - $mb_length;
+            $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) . $input;
+            break;
+        case STR_PAD_BOTH:
+            $left = floor(($length - $mb_length) / 2);
+            $right = ceil(($length - $mb_length) / 2);
+            $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) .
+                $input .
+                String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
+            break;
+        case STR_PAD_RIGHT:
+            $right = $length - $mb_length;
+            $output = $input . String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
+            break;
+        }
+
+        return $output;
+    }
+
+    /**
+     * Wraps the text of a message.
+     *
+     * @since Horde 3.2
+     *
+     * @param string $string         String containing the text to wrap.
+     * @param integer $width         Wrap the string at this number of
+     *                               characters.
+     * @param string $break          Character(s) to use when breaking lines.
+     * @param boolean $cut           Whether to cut inside words if a line
+     *                               can't be wrapped.
+     * @param string $charset        Character set to use when breaking lines.
+     * @param boolean $line_folding  Whether to apply line folding rules per
+     *                               RFC 822 or similar. The correct break
+     *                               characters including leading whitespace
+     *                               have to be specified too.
+     *
+     * @return string  String containing the wrapped text.
+     */
+    function wordwrap($string, $width = 75, $break = "\n", $cut = false,
+                      $charset = null, $line_folding = false)
+    {
+        /* Get the user's default character set if none passed in. */
+        if (is_null($charset)) {
+            $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+        }
+        $charset = String::_mbstringCharset($charset);
+        $string = String::convertCharset($string, $charset, 'utf-8');
+        $wrapped = '';
+
+        while (String::length($string, 'utf-8') > $width) {
+            $line = String::substr($string, 0, $width, 'utf-8');
+            $string = String::substr($string, String::length($line, 'utf-8'), null, 'utf-8');
+            // Make sure didn't cut a word, unless we want hard breaks anyway.
+            if (!$cut && preg_match('/^(.+?)((\s|\r?\n).*)/us', $string, $match)) {
+                $line .= $match[1];
+                $string = $match[2];
+            }
+            // Wrap at existing line breaks.
+            if (preg_match('/^(.*?)(\r?\n)(.*)$/u', $line, $match)) {
+                $wrapped .= $match[1] . $match[2];
+                $string = $match[3] . $string;
+                continue;
+            }
+            // Wrap at the last colon or semicolon followed by a whitespace if
+            // doing line folding.
+            if ($line_folding &&
+                preg_match('/^(.*?)(;|:)(\s+.*)$/u', $line, $match)) {
+                $wrapped .= $match[1] . $match[2] . $break;
+                $string = $match[3] . $string;
+                continue;
+            }
+            // Wrap at the last whitespace of $line.
+            if ($line_folding) {
+                $sub = '(.+[^\s])';
+            } else {
+                $sub = '(.*)';
+            }
+            if (preg_match('/^' . $sub . '(\s+)(.*)$/u', $line, $match)) {
+                $wrapped .= $match[1] . $break;
+                $string = ($line_folding ? $match[2] : '') . $match[3] . $string;
+                continue;
+            }
+            // Hard wrap if necessary.
+            if ($cut) {
+                $wrapped .= $line . $break;
+                continue;
+            }
+            $wrapped .= $line;
+        }
+
+        return String::convertCharset($wrapped . $string, 'utf-8', $charset);
+    }
+
+    /**
+     * Wraps the text of a message.
+     *
+     * @param string $text        String containing the text to wrap.
+     * @param integer $length     Wrap $text at this number of characters.
+     * @param string $break_char  Character(s) to use when breaking lines.
+     * @param string $charset     Character set to use when breaking lines.
+     * @param boolean $quote      Ignore lines that are wrapped with the '>'
+     *                            character (RFC 2646)? If true, we don't
+     *                            remove any padding whitespace at the end of
+     *                            the string.
+     *
+     * @return string  String containing the wrapped text.
+     */
+    function wrap($text, $length = 80, $break_char = "\n", $charset = null,
+                  $quote = false)
+    {
+        $paragraphs = array();
+
+        foreach (preg_split('/\r?\n/', $text) as $input) {
+            if ($quote && (strpos($input, '>') === 0)) {
+                $line = $input;
+            } else {
+                /* We need to handle the Usenet-style signature line
+                 * separately; since the space after the two dashes is
+                 * REQUIRED, we don't want to trim the line. */
+                if ($input != '-- ') {
+                    $input = rtrim($input);
+                }
+                $line = String::wordwrap($input, $length, $break_char, false, $charset);
+            }
+
+            $paragraphs[] = $line;
+        }
+
+        return implode($break_char, $paragraphs);
+    }
+
+    /**
+     * Returns true if the every character in the parameter is an alphabetic
+     * character.
+     *
+     * @param $string   The string to test.
+     * @param $charset  The charset to use when testing the string.
+     *
+     * @return boolean  True if the parameter was alphabetic only.
+     */
+    function isAlpha($string, $charset = null)
+    {
+        if (!String::extensionExists('mbstring')) {
+            return ctype_alpha($string);
+        }
+
+        $charset = String::_mbstringCharset($charset);
+        $old_charset = mb_regex_encoding();
+        $old_error = error_reporting(0);
+
+        if ($charset != $old_charset) {
+            mb_regex_encoding($charset);
+        }
+        $alpha = !mb_ereg_match('[^[:alpha:]]', $string);
+        if ($charset != $old_charset) {
+            mb_regex_encoding($old_charset);
+        }
+
+        error_reporting($old_error);
+
+        return $alpha;
+    }
+
+    /**
+     * Returns true if ever character in the parameter is a lowercase letter in
+     * the current locale.
+     *
+     * @param $string   The string to test.
+     * @param $charset  The charset to use when testing the string.
+     *
+     * @return boolean  True if the parameter was lowercase.
+     */
+    function isLower($string, $charset = null)
+    {
+        return ((String::lower($string, true, $charset) === $string) &&
+                String::isAlpha($string, $charset));
+    }
+
+    /**
+     * Returns true if every character in the parameter is an uppercase letter
+     * in the current locale.
+     *
+     * @param string $string   The string to test.
+     * @param string $charset  The charset to use when testing the string.
+     *
+     * @return boolean  True if the parameter was uppercase.
+     */
+    function isUpper($string, $charset = null)
+    {
+        return ((String::upper($string, true, $charset) === $string) &&
+                String::isAlpha($string, $charset));
+    }
+
+    /**
+     * Performs a multibyte safe regex match search on the text provided.
+     *
+     * @since Horde 3.1
+     *
+     * @param string $text     The text to search.
+     * @param array $regex     The regular expressions to use, without perl
+     *                         regex delimiters (e.g. '/' or '|').
+     * @param string $charset  The character set of the text.
+     *
+     * @return array  The matches array from the first regex that matches.
+     */
+    function regexMatch($text, $regex, $charset = null)
+    {
+        if (!empty($charset)) {
+            $regex = String::convertCharset($regex, $charset, 'utf-8');
+            $text = String::convertCharset($text, $charset, 'utf-8');
+        }
+
+        $matches = array();
+        foreach ($regex as $val) {
+            if (preg_match('/' . $val . '/u', $text, $matches)) {
+                break;
+            }
+        }
+
+        if (!empty($charset)) {
+            $matches = String::convertCharset($matches, 'utf-8', $charset);
+        }
+
+        return $matches;
+    }
+
+    /**
+     * Workaround charsets that don't work with mbstring functions.
+     *
+     * @access private
+     *
+     * @param string $charset  The original charset.
+     *
+     * @return string  The charset to use with mbstring functions.
+     */
+    function _mbstringCharset($charset)
+    {
+        /* mbstring functions do not handle the 'ks_c_5601-1987' &
+         * 'ks_c_5601-1989' charsets. However, these charsets are used, for
+         * example, by various versions of Outlook to send Korean characters.
+         * Use UHC (CP949) encoding instead. See, e.g.,
+         * http://lists.w3.org/Archives/Public/ietf-charsets/2001AprJun/0030.html */
+        if (in_array(String::lower($charset), array('ks_c_5601-1987', 'ks_c_5601-1989'))) {
+            $charset = 'UHC';
+        }
+
+        return $charset;
+    }
+
+}
+
+
+
+/**
+ * @package Horde_iCalendar
+ */
+
+/**
+ * String package
+ */
+
+
+
+/**
+ * Class representing iCalendar files.
+ *
+ * $Horde: framework/iCalendar/iCalendar.php,v 1.57.4.81 2010-11-10 14:34:25 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar {
+
+    /**
+     * The parent (containing) iCalendar object.
+     *
+     * @var Horde_iCalendar
+     */
+    var $_container = false;
+
+    /**
+     * The name/value pairs of attributes for this object (UID,
+     * DTSTART, etc.). Which are present depends on the object and on
+     * what kind of component it is.
+     *
+     * @var array
+     */
+    var $_attributes = array();
+
+    /**
+     * Any children (contained) iCalendar components of this object.
+     *
+     * @var array
+     */
+    var $_components = array();
+
+    /**
+     * According to RFC 2425, we should always use CRLF-terminated lines.
+     *
+     * @var string
+     */
+    var $_newline = "\r\n";
+
+    /**
+     * iCalendar format version (different behavior for 1.0 and 2.0
+     * especially with recurring events).
+     *
+     * @var string
+     */
+    var $_version;
+
+    function Horde_iCalendar($version = '2.0')
+    {
+        $this->_version = $version;
+        $this->setAttribute('VERSION', $version);
+    }
+
+    /**
+     * Return a reference to a new component.
+     *
+     * @param string          $type       The type of component to return
+     * @param Horde_iCalendar $container  A container that this component
+     *                                    will be associated with.
+     *
+     * @return object  Reference to a Horde_iCalendar_* object as specified.
+     *
+     * @static
+     */
+    function &newComponent($type, &$container)
+    {
+        $type = String::lower($type);
+        $class = 'Horde_iCalendar_' . $type;
+        if (!class_exists($class)) {
+            include 'Horde/iCalendar/' . $type . '.php';
+        }
+        if (class_exists($class)) {
+            $component = new $class();
+            if ($container !== false) {
+                $component->_container = &$container;
+                // Use version of container, not default set by component
+                // constructor.
+                $component->_version = $container->_version;
+            }
+        } else {
+            // Should return an dummy x-unknown type class here.
+            $component = false;
+        }
+
+        return $component;
+    }
+
+    /**
+     * Sets the value of an attribute.
+     *
+     * @param string $name     The name of the attribute.
+     * @param string $value    The value of the attribute.
+     * @param array $params    Array containing any addition parameters for
+     *                         this attribute.
+     * @param boolean $append  True to append the attribute, False to replace
+     *                         the first matching attribute found.
+     * @param array $values    Array representation of $value.  For
+     *                         comma/semicolon seperated lists of values.  If
+     *                         not set use $value as single array element.
+     */
+    function setAttribute($name, $value, $params = array(), $append = true,
+                          $values = false)
+    {
+        // Make sure we update the internal format version if
+        // setAttribute('VERSION', ...) is called.
+        if ($name == 'VERSION') {
+            $this->_version = $value;
+            if ($this->_container !== false) {
+                $this->_container->_version = $value;
+            }
+        }
+
+        if (!$values) {
+            $values = array($value);
+        }
+        $found = false;
+        if (!$append) {
+            foreach (array_keys($this->_attributes) as $key) {
+                if ($this->_attributes[$key]['name'] == String::upper($name)) {
+                    $this->_attributes[$key]['params'] = $params;
+                    $this->_attributes[$key]['value'] = $value;
+                    $this->_attributes[$key]['values'] = $values;
+                    $found = true;
+                    break;
+                }
+            }
+        }
+
+        if ($append || !$found) {
+            $this->_attributes[] = array(
+                'name'      => String::upper($name),
+                'params'    => $params,
+                'value'     => $value,
+                'values'    => $values
+            );
+        }
+    }
+
+    /**
+     * Sets parameter(s) for an (already existing) attribute.  The
+     * parameter set is merged into the existing set.
+     *
+     * @param string $name   The name of the attribute.
+     * @param array $params  Array containing any additional parameters for
+     *                       this attribute.
+     * @return boolean  True on success, false if no attribute $name exists.
+     */
+    function setParameter($name, $params = array())
+    {
+        $keys = array_keys($this->_attributes);
+        foreach ($keys as $key) {
+            if ($this->_attributes[$key]['name'] == $name) {
+                $this->_attributes[$key]['params'] =
+                    array_merge($this->_attributes[$key]['params'], $params);
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Get the value of an attribute.
+     *
+     * @param string $name     The name of the attribute.
+     * @param boolean $params  Return the parameters for this attribute instead
+     *                         of its value.
+     *
+     * @return mixed (object)  PEAR_Error if the attribute does not exist.
+     *               (string)  The value of the attribute.
+     *               (array)   The parameters for the attribute or
+     *                         multiple values for an attribute.
+     */
+    function getAttribute($name, $params = false)
+    {
+        $result = array();
+        foreach ($this->_attributes as $attribute) {
+            if ($attribute['name'] == $name) {
+                if ($params) {
+                    $result[] = $attribute['params'];
+                } else {
+                    $result[] = $attribute['value'];
+                }
+            }
+        }
+        if (!count($result)) {
+            require_once 'PEAR.php';
+            return PEAR::raiseError('Attribute "' . $name . '" Not Found');
+        } if (count($result) == 1 && !$params) {
+            return $result[0];
+        } else {
+            return $result;
+        }
+    }
+
+    /**
+     * Gets the values of an attribute as an array.  Multiple values
+     * are possible due to:
+     *
+     *  a) multiplce occurences of 'name'
+     *  b) (unsecapd) comma seperated lists.
+     *
+     * So for a vcard like "KEY:a,b\nKEY:c" getAttributesValues('KEY')
+     * will return array('a', 'b', 'c').
+     *
+     * @param string  $name    The name of the attribute.
+     * @return mixed (object)  PEAR_Error if the attribute does not exist.
+     *               (array)   Multiple values for an attribute.
+     */
+    function getAttributeValues($name)
+    {
+        $result = array();
+        foreach ($this->_attributes as $attribute) {
+            if ($attribute['name'] == $name) {
+                $result = array_merge($attribute['values'], $result);
+            }
+        }
+        if (!count($result)) {
+            return PEAR::raiseError('Attribute "' . $name . '" Not Found');
+        }
+        return $result;
+    }
+
+    /**
+     * Returns the value of an attribute, or a specified default value
+     * if the attribute does not exist.
+     *
+     * @param string $name    The name of the attribute.
+     * @param mixed $default  What to return if the attribute specified by
+     *                        $name does not exist.
+     *
+     * @return mixed (string) The value of $name.
+     *               (mixed)  $default if $name does not exist.
+     */
+    function getAttributeDefault($name, $default = '')
+    {
+        $value = $this->getAttribute($name);
+        return is_a($value, 'PEAR_Error') ? $default : $value;
+    }
+
+    /**
+     * Remove all occurences of an attribute.
+     *
+     * @param string $name  The name of the attribute.
+     */
+    function removeAttribute($name)
+    {
+        $keys = array_keys($this->_attributes);
+        foreach ($keys as $key) {
+            if ($this->_attributes[$key]['name'] == $name) {
+                unset($this->_attributes[$key]);
+            }
+        }
+    }
+
+    /**
+     * Get attributes for all tags or for a given tag.
+     *
+     * @param string $tag  Return attributes for this tag, or all attributes if
+     *                     not given.
+     *
+     * @return array  An array containing all the attributes and their types.
+     */
+    function getAllAttributes($tag = false)
+    {
+        if ($tag === false) {
+            return $this->_attributes;
+        }
+        $result = array();
+        foreach ($this->_attributes as $attribute) {
+            if ($attribute['name'] == $tag) {
+                $result[] = $attribute;
+            }
+        }
+        return $result;
+    }
+
+    /**
+     * Add a vCalendar component (eg vEvent, vTimezone, etc.).
+     *
+     * @param Horde_iCalendar $component  Component (subclass) to add.
+     */
+    function addComponent($component)
+    {
+        if (is_a($component, 'Horde_iCalendar')) {
+            $component->_container = &$this;
+            $this->_components[] = &$component;
+        }
+    }
+
+    /**
+     * Retrieve all the components.
+     *
+     * @return array  Array of Horde_iCalendar objects.
+     */
+    function getComponents()
+    {
+        return $this->_components;
+    }
+
+    function getType()
+    {
+        return 'vcalendar';
+    }
+
+    /**
+     * Return the classes (entry types) we have.
+     *
+     * @return array  Hash with class names Horde_iCalendar_xxx as keys
+     *                and number of components of this class as value.
+     */
+    function getComponentClasses()
+    {
+        $r = array();
+        foreach ($this->_components as $c) {
+            $cn = strtolower(get_class($c));
+            if (empty($r[$cn])) {
+                $r[$cn] = 1;
+            } else {
+                $r[$cn]++;
+            }
+        }
+
+        return $r;
+    }
+
+    /**
+     * Number of components in this container.
+     *
+     * @return integer  Number of components in this container.
+     */
+    function getComponentCount()
+    {
+        return count($this->_components);
+    }
+
+    /**
+     * Retrieve a specific component.
+     *
+     * @param integer $idx  The index of the object to retrieve.
+     *
+     * @return mixed    (boolean) False if the index does not exist.
+     *                  (Horde_iCalendar_*) The requested component.
+     */
+    function getComponent($idx)
+    {
+        if (isset($this->_components[$idx])) {
+            return $this->_components[$idx];
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Locates the first child component of the specified class, and returns a
+     * reference to it.
+     *
+     * @param string $type  The type of component to find.
+     *
+     * @return boolean|Horde_iCalendar_*  False if no subcomponent of the
+     *                                    specified class exists or a reference
+     *                                    to the requested component.
+     */
+    function &findComponent($childclass)
+    {
+        $childclass = 'Horde_iCalendar_' . String::lower($childclass);
+        $keys = array_keys($this->_components);
+        foreach ($keys as $key) {
+            if (is_a($this->_components[$key], $childclass)) {
+                return $this->_components[$key];
+            }
+        }
+
+        $component = false;
+        return $component;
+    }
+
+    /**
+     * Locates the first matching child component of the specified class, and
+     * returns a reference to it.
+     *
+     * @param string $childclass  The type of component to find.
+     * @param string $attribute   This attribute must be set in the component
+     *                            for it to match.
+     * @param string $value       Optional value that $attribute must match.
+     *
+     * @return boolean|Horde_iCalendar_*  False if no matching subcomponent of
+     *                                    the specified class exists, or a
+     *                                    reference to the requested component.
+     */
+    function &findComponentByAttribute($childclass, $attribute, $value = null)
+    {
+        $childclass = 'Horde_iCalendar_' . String::lower($childclass);
+        $keys = array_keys($this->_components);
+        foreach ($keys as $key) {
+            if (is_a($this->_components[$key], $childclass)) {
+                $attr = $this->_components[$key]->getAttribute($attribute);
+                if (is_a($attr, 'PEAR_Error')) {
+                    continue;
+                }
+                if ($value !== null && $value != $attr) {
+                    continue;
+                }
+                return $this->_components[$key];
+            }
+        }
+
+        $component = false;
+        return $component;
+    }
+
+    /**
+     * Clears the iCalendar object (resets the components and attributes
+     * arrays).
+     */
+    function clear()
+    {
+        $this->_components = array();
+        $this->_attributes = array();
+    }
+
+    /**
+     * Checks if entry is vcalendar 1.0, vcard 2.1 or vnote 1.1.
+     *
+     * These 'old' formats are defined by www.imc.org. The 'new' (non-old)
+     * formats icalendar 2.0 and vcard 3.0 are defined in rfc2426 and rfc2445
+     * respectively.
+     *
+     * @since Horde 3.1.2
+     */
+    function isOldFormat()
+    {
+        if ($this->getType() == 'vcard') {
+            return ($this->_version < 3);
+        }
+        if ($this->getType() == 'vNote') {
+            return ($this->_version < 2);
+        }
+        if ($this->_version >= 2) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Export as vCalendar format.
+     */
+    function exportvCalendar()
+    {
+        // Default values.
+        $requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
+        $requiredAttributes['METHOD'] = 'PUBLISH';
+
+        foreach ($requiredAttributes as $name => $default_value) {
+            if (is_a($this->getattribute($name), 'PEAR_Error')) {
+                $this->setAttribute($name, $default_value);
+            }
+        }
+
+        return $this->_exportvData('VCALENDAR');
+    }
+
+    /**
+     * Export this entry as a hash array with tag names as keys.
+     *
+     * @param boolean $paramsInKeys
+     *                If false, the operation can be quite lossy as the
+     *                parameters are ignored when building the array keys.
+     *                So if you export a vcard with
+     *                LABEL;TYPE=WORK:foo
+     *                LABEL;TYPE=HOME:bar
+     *                the resulting hash contains only one label field!
+     *                If set to true, array keys look like 'LABEL;TYPE=WORK'
+     * @return array  A hash array with tag names as keys.
+     */
+    function toHash($paramsInKeys = false)
+    {
+        $hash = array();
+        foreach ($this->_attributes as $a)  {
+            $k = $a['name'];
+            if ($paramsInKeys && is_array($a['params'])) {
+                foreach ($a['params'] as $p => $v) {
+                    $k .= ";$p=$v";
+                }
+            }
+            $hash[$k] = $a['value'];
+        }
+
+        return $hash;
+    }
+
+    /**
+     * Parses a string containing vCalendar data.
+     *
+     * @todo This method doesn't work well at all, if $base is VCARD.
+     *
+     * @param string $text     The data to parse.
+     * @param string $base     The type of the base object.
+     * @param string $charset  The encoding charset for $text. Defaults to
+     *                         utf-8 for new format, iso-8859-1 for old format.
+     * @param boolean $clear   If true clears the iCal object before parsing.
+     *
+     * @return boolean  True on successful import, false otherwise.
+     */
+    function parsevCalendar($text, $base = 'VCALENDAR', $charset = null,
+                            $clear = true)
+    {
+        if ($clear) {
+            $this->clear();
+        }
+        if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) {
+            $container = true;
+            $vCal = $matches[1];
+        } else {
+            // Text isn't enclosed in BEGIN:VCALENDAR
+            // .. END:VCALENDAR. We'll try to parse it anyway.
+            $container = false;
+            $vCal = $text;
+        }
+        $vCal = trim($vCal);
+
+        // Extract all subcomponents.
+        $matches = $components = null;
+        if (preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $components)) {
+            foreach ($components[0] as $key => $data) {
+                // Remove from the vCalendar data.
+                $vCal = str_replace($data, '', $vCal);
+            }
+        } elseif (!$container) {
+            return false;
+        }
+
+        // Unfold "quoted printable" folded lines like:
+        //  BODY;ENCODING=QUOTED-PRINTABLE:=
+        //  another=20line=
+        //  last=20line
+        while (preg_match_all('/^([^:]+;\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\r?\n)+(.*[^=])?\r?\n)/mU', $vCal, $matches)) {
+            foreach ($matches[1] as $s) {
+                $r = preg_replace('/=\r?\n/', '', $s);
+                $vCal = str_replace($s, $r, $vCal);
+            }
+        }
+
+        // Unfold any folded lines.
+        if ($this->isOldFormat()) {
+            $vCal = preg_replace('/[\r\n]+([ \t])/', '$1', $vCal);
+        } else {
+            $vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal);
+        }
+
+        // Parse the remaining attributes.
+        if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\r\n]*)\r?$/m', $vCal, $matches)) {
+            foreach ($matches[0] as $attribute) {
+                preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts);
+                $tag = trim(String::upper($parts[1]));
+                $value = $parts[4];
+                $params = array();
+
+                // Parse parameters.
+                if (!empty($parts[2])) {
+                    preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts);
+                    foreach ($param_parts[2] as $key => $paramName) {
+                        $paramName = String::upper($paramName);
+                        $paramValue = $param_parts[4][$key];
+                        if ($paramName == 'TYPE') {
+                            $paramValue = preg_split('/(?<!\\\\),/', $paramValue);
+                            if (count($paramValue) == 1) {
+                                $paramValue = $paramValue[0];
+                            }
+                        }
+                        if (is_string($paramValue)) {
+                            if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
+                                $paramValue = $parts[1];
+                            }
+                        } else {
+                            foreach ($paramValue as $k => $tmp) {
+                                if (preg_match('/"([^"]*)"/', $tmp, $parts)) {
+                                    $paramValue[$k] = $parts[1];
+                                }
+                            }
+                        }
+                        $params[$paramName] = $paramValue;
+                    }
+                }
+
+                // Charset and encoding handling.
+                if ((isset($params['ENCODING']) &&
+                     String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') ||
+                    isset($params['QUOTED-PRINTABLE'])) {
+
+                    $value = quoted_printable_decode($value);
+                    if (isset($params['CHARSET'])) {
+                        $value = String::convertCharset($value, $params['CHARSET']);
+                    } else {
+                        $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
+                    }
+                } elseif (isset($params['CHARSET'])) {
+                    $value = String::convertCharset($value, $params['CHARSET']);
+                } else {
+                    // As per RFC 2279, assume UTF8 if we don't have an
+                    // explicit charset parameter.
+                    $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
+                }
+
+                // Get timezone info for date fields from $params.
+                $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false;
+
+                switch ($tag) {
+                // Date fields.
+                case 'COMPLETED':
+                case 'CREATED':
+                case 'LAST-MODIFIED':
+                case 'X-MOZ-LASTACK': 
+                case 'X-MOZ-SNOOZE-TIME': 
+                    $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
+                    break;
+
+                case 'BDAY':
+                case 'X-SYNCJE-ANNIVERSARY':
+                case 'X-ANNIVERSARY':
+                    $this->setAttribute($tag, $this->_parseDate($value), $params);
+                    break;
+
+                case 'DTEND':
+                case 'DTSTART':
+                case 'DTSTAMP':
+                case 'DUE':
+                case 'AALARM':
+                case 'RECURRENCE-ID':
+                    // types like AALARM may contain additional data after a ;
+                    // ignore these.
+                    $ts = explode(';', $value);
+                    if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') {
+                        $this->setAttribute($tag, $this->_parseDate($ts[0]), $params);
+                    } else {
+                        $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params);
+                    }
+                    break;
+
+                case 'TRIGGER':
+                    if (isset($params['VALUE']) &&
+                        $params['VALUE'] == 'DATE-TIME') {
+                            $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
+                    } else {
+                        $this->setAttribute($tag, $this->_parseDuration($value), $params);
+                    }
+                    break;
+
+                // Comma seperated dates.
+                case 'EXDATE':
+                case 'RDATE':
+                    if (!strlen($value)) {
+                        break;
+                    }
+                    $dates = array();
+                    $separator = $this->isOldFormat() ? ';' : ',';
+                    preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values);
+
+                    foreach ($values[1] as $value) {
+                        $dates[] = $this->_parseDate($value);
+                    }
+                    $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates);
+                    break;
+
+                // Duration fields.
+                case 'DURATION':
+                    $this->setAttribute($tag, $this->_parseDuration($value), $params);
+                    break;
+
+                // Period of time fields.
+                case 'FREEBUSY':
+                    $periods = array();
+                    preg_match_all('/,([^,]*)/', ',' . $value, $values);
+                    foreach ($values[1] as $value) {
+                        $periods[] = $this->_parsePeriod($value);
+                    }
+
+                    $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods);
+                    break;
+
+                // UTC offset fields.
+                case 'TZOFFSETFROM':
+                case 'TZOFFSETTO':
+                    $this->setAttribute($tag, $this->_parseUtcOffset($value), $params);
+                    break;
+
+                // Integer fields.
+                case 'PERCENT-COMPLETE':
+                case 'PRIORITY':
+                case 'REPEAT':
+                case 'SEQUENCE':
+                    $this->setAttribute($tag, intval($value), $params);
+                    break;
+
+                // Geo fields.
+                case 'GEO':
+                    if ($this->isOldFormat()) {
+                        $floats = explode(',', $value);
+                        $value = array('latitude' => floatval($floats[1]),
+                                       'longitude' => floatval($floats[0]));
+                    } else {
+                        $floats = explode(';', $value);
+                        $value = array('latitude' => floatval($floats[0]),
+                                       'longitude' => floatval($floats[1]));
+                    }
+                    $this->setAttribute($tag, $value, $params);
+                    break;
+
+                // Recursion fields.
+                case 'EXRULE':
+                case 'RRULE':
+                    $this->setAttribute($tag, trim($value), $params);
+                    break;
+
+                // ADR, ORG and N are lists seperated by unescaped semicolons
+                // with a specific number of slots.
+                case 'ADR':
+                case 'N':
+                case 'ORG':
+                    $value = trim($value);
+                    // As of rfc 2426 2.4.2 semicolon, comma, and colon must
+                    // be escaped (comma is unescaped after splitting below).
+                    $value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
+                                         array($this->_newline, $this->_newline, ';', ':'),
+                                         $value);
+
+                    // Split by unescaped semicolons:
+                    $values = preg_split('/(?<!\\\\);/', $value);
+                    $value = str_replace('\\;', ';', $value);
+                    $values = str_replace('\\;', ';', $values);
+                    $this->setAttribute($tag, trim($value), $params, true, $values);
+                    break;
+
+                // String fields.
+                default:
+                    if ($this->isOldFormat()) {
+                        // vCalendar 1.0 and vCard 2.1 only escape semicolons
+                        // and use unescaped semicolons to create lists.
+                        $value = trim($value);
+                        // Split by unescaped semicolons:
+                        $values = preg_split('/(?<!\\\\);/', $value);
+                        $value = str_replace('\\;', ';', $value);
+                        $values = str_replace('\\;', ';', $values);
+                        $this->setAttribute($tag, trim($value), $params, true, $values);
+                    } else {
+                        $value = trim($value);
+                        // As of rfc 2426 2.4.2 semicolon, comma, and colon
+                        // must be escaped (comma is unescaped after splitting
+                        // below).
+                        $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'),
+                                             array($this->_newline, $this->_newline, ';', ':', '\\'),
+                                             $value);
+
+                        // Split by unescaped commas.
+                        $values = preg_split('/(?<!\\\\),/', $value);
+                        $value = str_replace('\\,', ',', $value);
+                        $values = str_replace('\\,', ',', $values);
+
+                        $this->setAttribute($tag, trim($value), $params, true, $values);
+                    }
+                    break;
+                }
+            }
+        }
+
+        // Process all components.
+        if ($components) {
+            // vTimezone components are processed first. They are
+            // needed to process vEvents that may use a TZID.
+            foreach ($components[0] as $key => $data) {
+                $type = trim($components[1][$key]);
+                if ($type != 'VTIMEZONE') {
+                    continue;
+                }
+                $component = &Horde_iCalendar::newComponent($type, $this);
+                if ($component === false) {
+                    return PEAR::raiseError("Unable to create object for type $type");
+                }
+                $component->parsevCalendar($data, $type, $charset);
+
+                $this->addComponent($component);
+            }
+
+            // Now process the non-vTimezone components.
+            foreach ($components[0] as $key => $data) {
+                $type = trim($components[1][$key]);
+                if ($type == 'VTIMEZONE') {
+                    continue;
+                }
+                $component = &Horde_iCalendar::newComponent($type, $this);
+                if ($component === false) {
+                    return PEAR::raiseError("Unable to create object for type $type");
+                }
+                $component->parsevCalendar($data, $type, $charset);
+
+                $this->addComponent($component);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Export this component in vCal format.
+     *
+     * @param string $base  The type of the base object.
+     *
+     * @return string  vCal format data.
+     */
+    function _exportvData($base = 'VCALENDAR')
+    {
+        $result = 'BEGIN:' . String::upper($base) . $this->_newline;
+
+        // VERSION is not allowed for entries enclosed in VCALENDAR/ICALENDAR,
+        // as it is part of the enclosing VCALENDAR/ICALENDAR. See rfc2445
+        if ($base !== 'VEVENT' && $base !== 'VTODO' && $base !== 'VALARM' &&
+            $base !== 'VJOURNAL' && $base !== 'VFREEBUSY') {
+            // Ensure that version is the first attribute.
+            $result .= 'VERSION:' . $this->_version . $this->_newline;
+        }
+        foreach ($this->_attributes as $attribute) {
+            $name = $attribute['name'];
+            if ($name == 'VERSION') {
+                // Already done.
+                continue;
+            }
+
+            $params_str = '';
+            $params = $attribute['params'];
+            if ($params) {
+                foreach ($params as $param_name => $param_value) {
+                    /* Skip CHARSET for iCalendar 2.0 data, not allowed. */
+                    if ($param_name == 'CHARSET' && !$this->isOldFormat()) {
+                        continue;
+                    }
+                    /* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */
+                    if ($this->isOldFormat() &&
+                        $param_name == 'VALUE' && $param_value == 'DATE') {
+                        continue;
+                    }
+
+                    if ($param_value === null) {
+                        $params_str .= ";$param_name";
+                    } else {
+                        $len = strlen($param_value);
+                        $safe_value = '';
+                        $quote = false;
+                        for ($i = 0; $i < $len; ++$i) {
+                            $ord = ord($param_value[$i]);
+                            // Accept only valid characters.
+                            if ($ord == 9 || $ord == 32 || $ord == 33 ||
+                                ($ord >= 35 && $ord <= 126) ||
+                                $ord >= 128) {
+                                $safe_value .= $param_value[$i];
+                                // Characters above 128 do not need to be
+                                // quoted as per RFC2445 but Outlook requires
+                                // this.
+                                if ($ord == 44 || $ord == 58 || $ord == 59 ||
+                                    $ord >= 128) {
+                                    $quote = true;
+                                }
+                            }
+                        }
+                        if ($quote) {
+                            $safe_value = '"' . $safe_value . '"';
+                        }
+                        $params_str .= ";$param_name=$safe_value";
+                    }
+                }
+            }
+
+            $value = $attribute['value'];
+            switch ($name) {
+            // Date fields.
+            case 'COMPLETED':
+            case 'CREATED':
+            case 'DCREATED':
+            case 'LAST-MODIFIED':
+            case 'X-MOZ-LASTACK':
+            case 'X-MOZ-SNOOZE-TIME':
+                $value = $this->_exportDateTime($value);
+                break;
+
+            case 'DTEND':
+            case 'DTSTART':
+            case 'DTSTAMP':
+            case 'DUE':
+            case 'AALARM':
+            case 'RECURRENCE-ID':
+                if (isset($params['VALUE'])) {
+                    if ($params['VALUE'] == 'DATE') {
+                        // VCALENDAR 1.0 uses T000000 - T235959 for all day events:
+                        if ($this->isOldFormat() && $name == 'DTEND') {
+                            $d = new Horde_Date($value);
+                            $value = new Horde_Date(array(
+                                'year' => $d->year,
+                                'month' => $d->month,
+                                'mday' => $d->mday - 1));
+                            $value->correct();
+                            $value = $this->_exportDate($value, '235959');
+                        } else {
+                            $value = $this->_exportDate($value, '000000');
+                        }
+                    } else {
+                        $value = $this->_exportDateTime($value);
+                    }
+                } else {
+                    $value = $this->_exportDateTime($value);
+                }
+                break;
+
+            // Comma seperated dates.
+            case 'EXDATE':
+            case 'RDATE':
+                $dates = array();
+                foreach ($value as $date) {
+                    if (isset($params['VALUE'])) {
+                        if ($params['VALUE'] == 'DATE') {
+                            $dates[] = $this->_exportDate($date, '000000');
+                        } elseif ($params['VALUE'] == 'PERIOD') {
+                            $dates[] = $this->_exportPeriod($date);
+                        } else {
+                            $dates[] = $this->_exportDateTime($date);
+                        }
+                    } else {
+                        $dates[] = $this->_exportDateTime($date);
+                    }
+                }
+                $value = implode($this->isOldFormat() ? ';' : ',', $dates);
+                break;
+
+            case 'TRIGGER':
+                if (isset($params['VALUE'])) {
+                    if ($params['VALUE'] == 'DATE-TIME') {
+                        $value = $this->_exportDateTime($value);
+                    } elseif ($params['VALUE'] == 'DURATION') {
+                        $value = $this->_exportDuration($value);
+                    }
+                } else {
+                    $value = $this->_exportDuration($value);
+                }
+                break;
+
+            // Duration fields.
+            case 'DURATION':
+                $value = $this->_exportDuration($value);
+                break;
+
+            // Period of time fields.
+            case 'FREEBUSY':
+                $value_str = '';
+                foreach ($value as $period) {
+                    $value_str .= empty($value_str) ? '' : ',';
+                    $value_str .= $this->_exportPeriod($period);
+                }
+                $value = $value_str;
+                break;
+
+            // UTC offset fields.
+            case 'TZOFFSETFROM':
+            case 'TZOFFSETTO':
+                $value = $this->_exportUtcOffset($value);
+                break;
+
+            // Integer fields.
+            case 'PERCENT-COMPLETE':
+            case 'PRIORITY':
+            case 'REPEAT':
+            case 'SEQUENCE':
+                $value = "$value";
+                break;
+
+            // Geo fields.
+            case 'GEO':
+                if ($this->isOldFormat()) {
+                    $value = $value['longitude'] . ',' . $value['latitude'];
+                } else {
+                    $value = $value['latitude'] . ';' . $value['longitude'];
+                }
+                break;
+
+            // Recurrence fields.
+            case 'EXRULE':
+            case 'RRULE':
+                break;
+
+            default:
+                if ($this->isOldFormat()) {
+                    if (is_array($attribute['values']) &&
+                        count($attribute['values']) > 1) {
+                        $values = $attribute['values'];
+                        if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
+                            $glue = ';';
+                        } else {
+                            $glue = ',';
+                        }
+                        $values = str_replace(';', '\\;', $values);
+                        $value = implode($glue, $values);
+                    } else {
+                        /* vcard 2.1 and vcalendar 1.0 escape only
+                         * semicolons */
+                        $value = str_replace(';', '\\;', $value);
+                    }
+                    // Text containing newlines or ASCII >= 127 must be BASE64
+                    // or QUOTED-PRINTABLE encoded. Currently we use
+                    // QUOTED-PRINTABLE as default.
+                    if (preg_match("/[^\x20-\x7F]/", $value) &&
+                        empty($params['ENCODING']))  {
+                        $params['ENCODING'] = 'QUOTED-PRINTABLE';
+                        $params_str .= ';ENCODING=QUOTED-PRINTABLE';
+                        // Add CHARSET as well. At least the synthesis client
+                        // gets confused otherwise
+                        if (empty($params['CHARSET'])) {
+                            $params['CHARSET'] = 'UTF-8';
+                            $params_str .= ';CHARSET=' . $params['CHARSET'];
+                        }
+                    }
+                } else {
+                    if (is_array($attribute['values']) &&
+                        count($attribute['values'])) {
+                        $values = $attribute['values'];
+                        if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
+                            $glue = ';';
+                        } else {
+                            $glue = ',';
+                        }
+                        // As of rfc 2426 2.5 semicolon and comma must be
+                        // escaped.
+                        $values = str_replace(array('\\', ';', ','),
+                                              array('\\\\', '\\;', '\\,'),
+                                              $values);
+                        $value = implode($glue, $values);
+                    } else {
+                        // As of rfc 2426 2.5 semicolon and comma must be
+                        // escaped.
+                        $value = str_replace(array('\\', ';', ','),
+                                             array('\\\\', '\\;', '\\,'),
+                                             $value);
+                    }
+                    $value = preg_replace('/\r?\n/', '\n', $value);
+                }
+                break;
+            }
+
+            $value = str_replace("\r", '', $value);
+            if (!empty($params['ENCODING']) &&
+                $params['ENCODING'] == 'QUOTED-PRINTABLE' &&
+                strlen(trim($value))) {
+                $result .= $name . $params_str . ':'
+                    . str_replace('=0A', '=0D=0A',
+                                  $this->_quotedPrintableEncode($value))
+                    . $this->_newline;
+            } else {
+                $attr_string = $name . $params_str . ':' . $value;
+                if (!$this->isOldFormat()) {
+                    $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ',
+                                                    true, 'utf-8', true);
+                }
+                $result .= $attr_string . $this->_newline;
+            }
+        }
+
+        foreach ($this->_components as $component) {
+            $result .= $component->exportvCalendar();
+        }
+
+        return $result . 'END:' . $base . $this->_newline;
+    }
+
+    /**
+     * Parse a UTC Offset field.
+     */
+    function _parseUtcOffset($text)
+    {
+        $offset = array();
+        if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $timeParts)) {
+            $offset['ahead']  = (bool)($timeParts[1] == '+');
+            $offset['hour']   = intval($timeParts[2]);
+            $offset['minute'] = intval($timeParts[3]);
+            if (isset($timeParts[4])) {
+                $offset['second'] = intval($timeParts[4]);
+            }
+            return $offset;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Export a UTC Offset field.
+     */
+    function _exportUtcOffset($value)
+    {
+        $offset = $value['ahead'] ? '+' : '-';
+        $offset .= sprintf('%02d%02d',
+                           $value['hour'], $value['minute']);
+        if (isset($value['second'])) {
+            $offset .= sprintf('%02d', $value['second']);
+        }
+
+        return $offset;
+    }
+
+    /**
+     * Parse a Time Period field.
+     */
+    function _parsePeriod($text)
+    {
+        $periodParts = explode('/', $text);
+
+        $start = $this->_parseDateTime($periodParts[0]);
+
+        if ($duration = $this->_parseDuration($periodParts[1])) {
+            return array('start' => $start, 'duration' => $duration);
+        } elseif ($end = $this->_parseDateTime($periodParts[1])) {
+            return array('start' => $start, 'end' => $end);
+        }
+    }
+
+    /**
+     * Export a Time Period field.
+     */
+    function _exportPeriod($value)
+    {
+        $period = $this->_exportDateTime($value['start']);
+        $period .= '/';
+        if (isset($value['duration'])) {
+            $period .= $this->_exportDuration($value['duration']);
+        } else {
+            $period .= $this->_exportDateTime($value['end']);
+        }
+        return $period;
+    }
+
+    /**
+     * Grok the TZID and return an offset in seconds from UTC for this
+     * date and time.
+     */
+    function _parseTZID($date, $time, $tzid)
+    {
+        $vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid);
+        if (!$vtimezone) {
+            // use PHP's standard timezone db to determine tzoffset
+            try {
+                $tz = new DateTimeZone($tzid);
+                $dt = new DateTime('now', $tz);
+                $dt->setDate($date['year'], $date['month'], $date['mday']);
+                $dt->setTime($time['hour'], $time['minute'], $date['recond']);
+                return $tz->getOffset($dt);
+            }
+            catch (Exception $e) {
+                return false;
+            }
+        }
+
+        $change_times = array();
+        foreach ($vtimezone->getComponents() as $o) {
+            $t = $vtimezone->parseChild($o, $date['year']);
+            if ($t !== false) {
+                $change_times[] = $t;
+            }
+        }
+
+        if (!$change_times) {
+            return false;
+        }
+
+        sort($change_times);
+
+        // Time is arbitrarily based on UTC for comparison.
+        $t = @gmmktime($time['hour'], $time['minute'], $time['second'],
+                       $date['month'], $date['mday'], $date['year']);
+
+        if ($t < $change_times[0]['time']) {
+            return $change_times[0]['from'];
+        }
+
+        for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) {
+            if (($t >= $change_times[$i]['time']) &&
+                ($t < $change_times[$i + 1]['time'])) {
+                return $change_times[$i]['to'];
+            }
+        }
+
+        if ($t >= $change_times[$n - 1]['time']) {
+            return $change_times[$n - 1]['to'];
+        }
+
+        return false;
+    }
+
+    /**
+     * Parses a DateTime field and returns a unix timestamp. If the
+     * field cannot be parsed then the original text is returned
+     * unmodified.
+     *
+     * @todo This function should be moved to Horde_Date and made public.
+     */
+    function _parseDateTime($text, $tzid = false)
+    {
+        $dateParts = explode('T', $text);
+        if (count($dateParts) != 2 && !empty($text)) {
+            // Not a datetime field but may be just a date field.
+            if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
+                // Or not
+                return $text;
+            }
+            $newtext = $text.'T000000';
+            $dateParts = explode('T', $newtext);
+        }
+
+        if (!$date = Horde_iCalendar::_parseDate($dateParts[0])) {
+            return $text;
+        }
+        if (!$time = Horde_iCalendar::_parseTime($dateParts[1])) {
+            return $text;
+        }
+
+        // Get timezone info for date fields from $tzid and container.
+        $tzoffset = ($time['zone'] == 'Local' && $tzid && is_a($this->_container, 'Horde_iCalendar'))
+            ? $this->_parseTZID($date, $time, $tzid) : false;
+        if ($time['zone'] == 'UTC' || $tzoffset !== false) {
+            $result = @gmmktime($time['hour'], $time['minute'], $time['second'],
+                                $date['month'], $date['mday'], $date['year']);
+            if ($tzoffset) {
+                $result -= $tzoffset;
+            }
+        } else {
+            // We don't know the timezone so assume local timezone.
+            // FIXME: shouldn't this be based on the user's timezone
+            // preference rather than the server's timezone?
+            $result = @mktime($time['hour'], $time['minute'], $time['second'],
+                              $date['month'], $date['mday'], $date['year']);
+        }
+
+        return ($result !== false) ? $result : $text;
+    }
+
+    /**
+     * Export a DateTime field.
+     */
+    function _exportDateTime($value)
+    {
+        $temp = array();
+        if (!is_object($value) && !is_array($value)) {
+            $tz = date('O', $value);
+            $TZOffset = (3600 * substr($tz, 0, 3)) + (60 * substr($tz, 3, 2));
+            $value -= $TZOffset;
+
+            $temp['zone']   = 'UTC';
+            list($temp['year'], $temp['month'], $temp['mday'], $temp['hour'], $temp['minute'], $temp['second']) = explode('-', date('Y-n-j-G-i-s', $value));
+        } else {
+            $dateOb = new Horde_Date($value);
+            return Horde_iCalendar::_exportDateTime($dateOb->timestamp());
+        }
+
+        return Horde_iCalendar::_exportDate($temp) . 'T' . Horde_iCalendar::_exportTime($temp);
+    }
+
+    /**
+     * Parses a Time field.
+     *
+     * @static
+     */
+    function _parseTime($text)
+    {
+        if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) {
+            $time['hour'] = intval($timeParts[1]);
+            $time['minute'] = intval($timeParts[2]);
+            $time['second'] = intval($timeParts[3]);
+            if (isset($timeParts[4])) {
+                $time['zone'] = 'UTC';
+            } else {
+                $time['zone'] = 'Local';
+            }
+            return $time;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Exports a Time field.
+     */
+    function _exportTime($value)
+    {
+        $time = sprintf('%02d%02d%02d',
+                        $value['hour'], $value['minute'], $value['second']);
+        if ($value['zone'] == 'UTC') {
+            $time .= 'Z';
+        }
+        return $time;
+    }
+
+    /**
+     * Parses a Date field.
+     *
+     * @static
+     */
+    function _parseDate($text)
+    {
+        $parts = explode('T', $text);
+        if (count($parts) == 2) {
+            $text = $parts[0];
+        }
+
+        if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
+            return false;
+        }
+
+        return array('year' => $match[1],
+                     'month' => $match[2],
+                     'mday' => $match[3]);
+    }
+
+    /**
+     * Exports a date field.
+     *
+     * @param object|array $value  Date object or hash.
+     * @param string $autoconvert  If set, use this as time part to export the
+     *                             date as datetime when exporting to Vcalendar
+     *                             1.0. Examples: '000000' or '235959'
+     */
+    function _exportDate($value, $autoconvert = false)
+    {
+        if (is_object($value)) {
+            $value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday);
+        }
+        if ($autoconvert !== false && $this->isOldFormat()) {
+            return sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert);
+        } else {
+            return sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']);
+        }
+    }
+
+    /**
+     * Parse a Duration Value field.
+     */
+    function _parseDuration($text)
+    {
+        if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $durvalue)) {
+            // Weeks.
+            $duration = 7 * 86400 * intval($durvalue[3]);
+
+            if (count($durvalue) > 4) {
+                // Days.
+                $duration += 86400 * intval($durvalue[4]);
+            }
+            if (count($durvalue) > 5) {
+                // Hours.
+                $duration += 3600 * intval($durvalue[7]);
+
+                // Mins.
+                if (isset($durvalue[8])) {
+                    $duration += 60 * intval($durvalue[8]);
+                }
+
+                // Secs.
+                if (isset($durvalue[9])) {
+                    $duration += intval($durvalue[9]);
+                }
+            }
+
+            // Sign.
+            if ($durvalue[1] == "-") {
+                $duration *= -1;
+            }
+
+            return $duration;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Export a duration value.
+     */
+    function _exportDuration($value)
+    {
+        $duration = '';
+        if ($value < 0) {
+            $value *= -1;
+            $duration .= '-';
+        }
+        $duration .= 'P';
+
+        $weeks = floor($value / (7 * 86400));
+        $value = $value % (7 * 86400);
+        if ($weeks) {
+            $duration .= $weeks . 'W';
+        }
+
+        $days = floor($value / (86400));
+        $value = $value % (86400);
+        if ($days) {
+            $duration .= $days . 'D';
+        }
+
+        if ($value) {
+            $duration .= 'T';
+
+            $hours = floor($value / 3600);
+            $value = $value % 3600;
+            if ($hours) {
+                $duration .= $hours . 'H';
+            }
+
+            $mins = floor($value / 60);
+            $value = $value % 60;
+            if ($mins) {
+                $duration .= $mins . 'M';
+            }
+
+            if ($value) {
+                $duration .= $value . 'S';
+            }
+        }
+
+        return $duration;
+    }
+
+    /**
+     * Converts an 8bit string to a quoted-printable string according to RFC
+     * 2045, section 6.7.
+     *
+     * imap_8bit() does not apply all necessary rules.
+     *
+     * @param string $input  The string to be encoded.
+     *
+     * @return string  The quoted-printable encoded string.
+     */
+    function _quotedPrintableEncode($input = '')
+    {
+        $output = $line = '';
+        $len = strlen($input);
+
+        for ($i = 0; $i < $len; ++$i) {
+            $ord = ord($input[$i]);
+            // Encode non-printable characters (rule 2).
+            if ($ord == 9 ||
+                ($ord >= 32 && $ord <= 60) ||
+                ($ord >= 62 && $ord <= 126)) {
+                $chunk = $input[$i];
+            } else {
+                // Quoted printable encoding (rule 1).
+                $chunk = '=' . String::upper(sprintf('%02X', $ord));
+            }
+            $line .= $chunk;
+            // Wrap long lines (rule 5)
+            if (strlen($line) + 1 > 76) {
+                $line = String::wordwrap($line, 75, "=\r\n", true, 'us-ascii', true);
+                $newline = strrchr($line, "\r\n");
+                if ($newline !== false) {
+                    $output .= substr($line, 0, -strlen($newline) + 2);
+                    $line = substr($newline, 2);
+                } else {
+                    $output .= $line;
+                }
+                continue;
+            }
+            // Wrap at line breaks for better readability (rule 4).
+            if (substr($line, -3) == '=0A') {
+                $output .= $line . "=\r\n";
+                $line = '';
+            }
+        }
+        $output .= $line;
+
+        // Trailing whitespace must be encoded (rule 3).
+        $lastpos = strlen($output) - 1;
+        if ($output[$lastpos] == chr(9) ||
+            $output[$lastpos] == chr(32)) {
+            $output[$lastpos] = '=';
+            $output .= String::upper(sprintf('%02X', ord($output[$lastpos])));
+        }
+
+        return $output;
+    }
+
+}
+
+
+
+/**
+ * Class representing vAlarms.
+ *
+ * $Horde: framework/iCalendar/iCalendar/valarm.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_valarm extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vAlarm';
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('VALARM');
+    }
+
+}
+
+/**
+ * Class representing vEvents.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vevent.php,v 1.31.10.16 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vevent extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vEvent';
+    }
+
+    function exportvCalendar()
+    {
+        // Default values.
+        $requiredAttributes = array();
+        $requiredAttributes['DTSTAMP'] = time();
+        $requiredAttributes['UID'] = $this->_exportDateTime(time())
+            . substr(str_pad(base_convert(microtime(), 10, 36), 16, uniqid(mt_rand()), STR_PAD_LEFT), -16)
+            . '@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost');
+
+        $method = !empty($this->_container) ?
+            $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+        switch ($method) {
+        case 'PUBLISH':
+            $requiredAttributes['DTSTART'] = time();
+            $requiredAttributes['SUMMARY'] = '';
+            break;
+
+        case 'REQUEST':
+            $requiredAttributes['ATTENDEE'] = '';
+            $requiredAttributes['DTSTART'] = time();
+            $requiredAttributes['SUMMARY'] = '';
+            break;
+
+        case 'REPLY':
+            $requiredAttributes['ATTENDEE'] = '';
+            break;
+
+        case 'ADD':
+            $requiredAttributes['DTSTART'] = time();
+            $requiredAttributes['SEQUENCE'] = 1;
+            $requiredAttributes['SUMMARY'] = '';
+            break;
+
+        case 'CANCEL':
+            $requiredAttributes['ATTENDEE'] = '';
+            $requiredAttributes['SEQUENCE'] = 1;
+            break;
+
+        case 'REFRESH':
+            $requiredAttributes['ATTENDEE'] = '';
+            break;
+        }
+
+        foreach ($requiredAttributes as $name => $default_value) {
+            if (is_a($this->getAttribute($name), 'PEAR_Error')) {
+                $this->setAttribute($name, $default_value);
+            }
+        }
+
+        return parent::_exportvData('VEVENT');
+    }
+
+    /**
+     * Update the status of an attendee of an event.
+     *
+     * @param $email    The email address of the attendee.
+     * @param $status   The participant status to set.
+     * @param $fullname The full name of the participant to set.
+     */
+    function updateAttendee($email, $status, $fullname = '')
+    {
+        foreach ($this->_attributes as $key => $attribute) {
+            if ($attribute['name'] == 'ATTENDEE' &&
+                $attribute['value'] == 'mailto:' . $email) {
+                $this->_attributes[$key]['params']['PARTSTAT'] = $status;
+                if (!empty($fullname)) {
+                    $this->_attributes[$key]['params']['CN'] = $fullname;
+                }
+                unset($this->_attributes[$key]['params']['RSVP']);
+                return;
+            }
+        }
+        $params = array('PARTSTAT' => $status);
+        if (!empty($fullname)) {
+            $params['CN'] = $fullname;
+        }
+        $this->setAttribute('ATTENDEE', 'mailto:' . $email, $params);
+    }
+
+    /**
+     * Return the organizer display name or email.
+     *
+     * @return string  The organizer name to display for this event.
+     */
+    function organizerName()
+    {
+        $organizer = $this->getAttribute('ORGANIZER', true);
+        if (is_a($organizer, 'PEAR_Error')) {
+            return _("An unknown person");
+        }
+
+        if (isset($organizer[0]['CN'])) {
+            return $organizer[0]['CN'];
+        }
+
+        $organizer = parse_url($this->getAttribute('ORGANIZER'));
+
+        return $organizer['path'];
+    }
+
+    /**
+     * Update this event with details from another event.
+     *
+     * @param Horde_iCalendar_vEvent $vevent  The vEvent with latest details.
+     */
+    function updateFromvEvent($vevent)
+    {
+        $newAttributes = $vevent->getAllAttributes();
+        foreach ($newAttributes as $newAttribute) {
+            $currentValue = $this->getAttribute($newAttribute['name']);
+            if (is_a($currentValue, 'PEAR_error')) {
+                // Already exists so just add it.
+                $this->setAttribute($newAttribute['name'],
+                                    $newAttribute['value'],
+                                    $newAttribute['params']);
+            } else {
+                // Already exists so locate and modify.
+                $found = false;
+
+                // Try matching the attribte name and value incase
+                // only the params changed (eg attendee updating
+                // status).
+                foreach ($this->_attributes as $id => $attr) {
+                    if ($attr['name'] == $newAttribute['name'] &&
+                        $attr['value'] == $newAttribute['value']) {
+                        // merge the params
+                        foreach ($newAttribute['params'] as $param_id => $param_name) {
+                            $this->_attributes[$id]['params'][$param_id] = $param_name;
+                        }
+                        $found = true;
+                        break;
+                    }
+                }
+                if (!$found) {
+                    // Else match the first attribute with the same
+                    // name (eg changing start time).
+                    foreach ($this->_attributes as $id => $attr) {
+                        if ($attr['name'] == $newAttribute['name']) {
+                            $this->_attributes[$id]['value'] = $newAttribute['value'];
+                            // Merge the params.
+                            foreach ($newAttribute['params'] as $param_id => $param_name) {
+                                $this->_attributes[$id]['params'][$param_id] = $param_name;
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Update just the attendess of event with details from another
+     * event.
+     *
+     * @param Horde_iCalendar_vEvent $vevent  The vEvent with latest details
+     */
+    function updateAttendeesFromvEvent($vevent)
+    {
+        $newAttributes = $vevent->getAllAttributes();
+        foreach ($newAttributes as $newAttribute) {
+            if ($newAttribute['name'] != 'ATTENDEE') {
+                continue;
+            }
+            $currentValue = $this->getAttribute($newAttribute['name']);
+            if (is_a($currentValue, 'PEAR_error')) {
+                // Already exists so just add it.
+                $this->setAttribute($newAttribute['name'],
+                                    $newAttribute['value'],
+                                    $newAttribute['params']);
+            } else {
+                // Already exists so locate and modify.
+                $found = false;
+                // Try matching the attribte name and value incase
+                // only the params changed (eg attendee updating
+                // status).
+                foreach ($this->_attributes as $id => $attr) {
+                    if ($attr['name'] == $newAttribute['name'] &&
+                        $attr['value'] == $newAttribute['value']) {
+                        // Merge the params.
+                        foreach ($newAttribute['params'] as $param_id => $param_name) {
+                            $this->_attributes[$id]['params'][$param_id] = $param_name;
+                        }
+                        $found = true;
+                        break;
+                    }
+                }
+
+                if (!$found) {
+                    // Else match the first attribute with the same
+                    // name (eg changing start time).
+                    foreach ($this->_attributes as $id => $attr) {
+                        if ($attr['name'] == $newAttribute['name']) {
+                            $this->_attributes[$id]['value'] = $newAttribute['value'];
+                            // Merge the params.
+                            foreach ($newAttribute['params'] as $param_id => $param_name) {
+                                $this->_attributes[$id]['params'][$param_id] = $param_name;
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+}
+
+/**
+ * Class representing vFreebusy components.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vfreebusy.php,v 1.16.10.18 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @todo Don't use timestamps
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vfreebusy extends Horde_iCalendar {
+
+    var $_busyPeriods = array();
+    var $_extraParams = array();
+
+    /**
+     * Returns the type of this calendar component.
+     *
+     * @return string  The type of this component.
+     */
+    function getType()
+    {
+        return 'vFreebusy';
+    }
+
+    /**
+     * Parses a string containing vFreebusy data.
+     *
+     * @param string $data     The data to parse.
+     */
+    function parsevCalendar($data, $type = null, $charset = null)
+    {
+        parent::parsevCalendar($data, 'VFREEBUSY', $charset);
+
+        // Do something with all the busy periods.
+        foreach ($this->_attributes as $key => $attribute) {
+            if ($attribute['name'] != 'FREEBUSY') {
+                continue;
+            }
+            foreach ($attribute['values'] as $value) {
+                $params = isset($attribute['params'])
+                    ? $attribute['params']
+                    : array();
+                if (isset($value['duration'])) {
+                    $this->addBusyPeriod('BUSY', $value['start'], null,
+                                         $value['duration'], $params);
+                } else {
+                    $this->addBusyPeriod('BUSY', $value['start'],
+                                         $value['end'], null, $params);
+                }
+            }
+            unset($this->_attributes[$key]);
+        }
+    }
+
+    /**
+     * Returns the component exported as string.
+     *
+     * @return string  The exported vFreeBusy information according to the
+     *                 iCalender format specification.
+     */
+    function exportvCalendar()
+    {
+        foreach ($this->_busyPeriods as $start => $end) {
+            $periods = array(array('start' => $start, 'end' => $end));
+            $this->setAttribute('FREEBUSY', $periods,
+                                isset($this->_extraParams[$start])
+                                ? $this->_extraParams[$start] : array());
+        }
+
+        $res = parent::_exportvData('VFREEBUSY');
+
+        foreach ($this->_attributes as $key => $attribute) {
+            if ($attribute['name'] == 'FREEBUSY') {
+                unset($this->_attributes[$key]);
+            }
+        }
+
+        return $res;
+    }
+
+    /**
+     * Returns a display name for this object.
+     *
+     * @return string  A clear text name for displaying this object.
+     */
+    function getName()
+    {
+        $name = '';
+        $method = !empty($this->_container) ?
+            $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+        if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
+            $attr = 'ORGANIZER';
+        } elseif ($method == 'REPLY') {
+            $attr = 'ATTENDEE';
+        }
+
+        $name = $this->getAttribute($attr, true);
+        if (!is_a($name, 'PEAR_Error') && isset($name[0]['CN'])) {
+            return $name[0]['CN'];
+        }
+
+        $name = $this->getAttribute($attr);
+        if (is_a($name, 'PEAR_Error')) {
+            return '';
+        } else {
+            $name = parse_url($name);
+            return $name['path'];
+        }
+    }
+
+    /**
+     * Returns the email address for this object.
+     *
+     * @return string  The email address of this object's owner.
+     */
+    function getEmail()
+    {
+        $name = '';
+        $method = !empty($this->_container)
+                  ? $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+        if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
+            $attr = 'ORGANIZER';
+        } elseif ($method == 'REPLY') {
+            $attr = 'ATTENDEE';
+        }
+
+        $name = $this->getAttribute($attr);
+        if (is_a($name, 'PEAR_Error')) {
+            return '';
+        } else {
+            $name = parse_url($name);
+            return $name['path'];
+        }
+    }
+
+    /**
+     * Returns the busy periods.
+     *
+     * @return array  All busy periods.
+     */
+    function getBusyPeriods()
+    {
+        return $this->_busyPeriods;
+    }
+
+    /**
+     * Returns any additional freebusy parameters.
+     *
+     * @return array  Additional parameters of the freebusy periods.
+     */
+    function getExtraParams()
+    {
+        return $this->_extraParams;
+    }
+
+    /**
+     * Returns all the free periods of time in a given period.
+     *
+     * @param integer $startStamp  The start timestamp.
+     * @param integer $endStamp    The end timestamp.
+     *
+     * @return array  A hash with free time periods, the start times as the
+     *                keys and the end times as the values.
+     */
+    function getFreePeriods($startStamp, $endStamp)
+    {
+        $this->simplify();
+        $periods = array();
+
+        // Check that we have data for some part of this period.
+        if ($this->getEnd() < $startStamp || $this->getStart() > $endStamp) {
+            return $periods;
+        }
+
+        // Locate the first time in the requested period we have data for.
+        $nextstart = max($startStamp, $this->getStart());
+
+        // Check each busy period and add free periods in between.
+        foreach ($this->_busyPeriods as $start => $end) {
+            if ($start <= $endStamp && $end >= $nextstart) {
+                if ($nextstart <= $start) {
+                    $periods[$nextstart] = min($start, $endStamp);
+                }
+                $nextstart = min($end, $endStamp);
+            }
+        }
+
+        // If we didn't read the end of the requested period but still have
+        // data then mark as free to the end of the period or available data.
+        if ($nextstart < $endStamp && $nextstart < $this->getEnd()) {
+            $periods[$nextstart] = min($this->getEnd(), $endStamp);
+        }
+
+        return $periods;
+    }
+
+    /**
+     * Adds a busy period to the info.
+     *
+     * This function may throw away data in case you add a period with a start
+     * date that already exists. The longer of the two periods will be chosen
+     * (and all information associated with the shorter one will be removed).
+     *
+     * @param string $type       The type of the period. Either 'FREE' or
+     *                           'BUSY'; only 'BUSY' supported at the moment.
+     * @param integer $start     The start timestamp of the period.
+     * @param integer $end       The end timestamp of the period.
+     * @param integer $duration  The duration of the period. If specified, the
+     *                           $end parameter will be ignored.
+     * @param array   $extra     Additional parameters for this busy period.
+     */
+    function addBusyPeriod($type, $start, $end = null, $duration = null,
+                           $extra = array())
+    {
+        if ($type == 'FREE') {
+            // Make sure this period is not marked as busy.
+            return false;
+        }
+
+        // Calculate the end time if duration was specified.
+        $tempEnd = is_null($duration) ? $end : $start + $duration;
+
+        // Make sure the period length is always positive.
+        $end = max($start, $tempEnd);
+        $start = min($start, $tempEnd);
+
+        if (isset($this->_busyPeriods[$start])) {
+            // Already a period starting at this time. Change the current
+            // period only if the new one is longer. This might be a problem
+            // if the callee assumes that there is no simplification going
+            // on. But since the periods are stored using the start time of
+            // the busy periods we have to throw away data here.
+            if ($end > $this->_busyPeriods[$start]) {
+                $this->_busyPeriods[$start] = $end;
+                $this->_extraParams[$start] = $extra;
+            }
+        } else {
+            // Add a new busy period.
+            $this->_busyPeriods[$start] = $end;
+            $this->_extraParams[$start] = $extra;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the timestamp of the start of the time period this free busy
+     * information covers.
+     *
+     * @return integer  A timestamp.
+     */
+    function getStart()
+    {
+        if (!is_a($this->getAttribute('DTSTART'), 'PEAR_Error')) {
+            return $this->getAttribute('DTSTART');
+        } elseif (count($this->_busyPeriods)) {
+            return min(array_keys($this->_busyPeriods));
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the timestamp of the end of the time period this free busy
+     * information covers.
+     *
+     * @return integer  A timestamp.
+     */
+    function getEnd()
+    {
+        if (!is_a($this->getAttribute('DTEND'), 'PEAR_Error')) {
+            return $this->getAttribute('DTEND');
+        } elseif (count($this->_busyPeriods)) {
+            return max(array_values($this->_busyPeriods));
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Merges the busy periods of another Horde_iCalendar_vfreebusy object
+     * into this one.
+     *
+     * This might lead to simplification no matter what you specify for the
+     * "simplify" flag since periods with the same start date will lead to the
+     * shorter period being removed (see addBusyPeriod).
+     *
+     * @param Horde_iCalendar_vfreebusy $freebusy  A freebusy object.
+     * @param boolean $simplify                    If true, simplify() will
+     *                                             called after the merge.
+     */
+    function merge($freebusy, $simplify = true)
+    {
+        if (!is_a($freebusy, 'Horde_iCalendar_vfreebusy')) {
+            return false;
+        }
+
+        $extra = $freebusy->getExtraParams();
+        foreach ($freebusy->getBusyPeriods() as $start => $end) {
+            // This might simplify the busy periods without taking the
+            // "simplify" flag into account.
+            $this->addBusyPeriod('BUSY', $start, $end, null,
+                                 isset($extra[$start])
+                                 ? $extra[$start] : array());
+        }
+
+        $thisattr = $this->getAttribute('DTSTART');
+        $thatattr = $freebusy->getAttribute('DTSTART');
+        if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
+            $this->setAttribute('DTSTART', $thatattr, array(), false);
+        } elseif (!is_a($thatattr, 'PEAR_Error')) {
+            if ($thatattr < $thisattr) {
+                $this->setAttribute('DTSTART', $thatattr, array(), false);
+            }
+        }
+
+        $thisattr = $this->getAttribute('DTEND');
+        $thatattr = $freebusy->getAttribute('DTEND');
+        if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
+            $this->setAttribute('DTEND', $thatattr, array(), false);
+        } elseif (!is_a($thatattr, 'PEAR_Error')) {
+            if ($thatattr > $thisattr) {
+                $this->setAttribute('DTEND', $thatattr, array(), false);
+            }
+        }
+
+        if ($simplify) {
+            $this->simplify();
+        }
+
+        return true;
+    }
+
+    /**
+     * Removes all overlaps and simplifies the busy periods array as much as
+     * possible.
+     */
+    function simplify()
+    {
+        $clean = false;
+        $busy  = array($this->_busyPeriods, $this->_extraParams);
+        while (!$clean) {
+            $result = $this->_simplify($busy[0], $busy[1]);
+            $clean = $result === $busy;
+            $busy = $result;
+        }
+
+        ksort($result[1], SORT_NUMERIC);
+        $this->_extraParams = $result[1];
+
+        ksort($result[0], SORT_NUMERIC);
+        $this->_busyPeriods = $result[0];
+    }
+
+    function _simplify($busyPeriods, $extraParams = array())
+    {
+        $checked = array();
+        $checkedExtra = array();
+        $checkedEmpty = true;
+
+        foreach ($busyPeriods as $start => $end) {
+            if ($checkedEmpty) {
+                $checked[$start] = $end;
+                $checkedExtra[$start] = isset($extraParams[$start])
+                    ? $extraParams[$start] : array();
+                $checkedEmpty = false;
+            } else {
+                $added = false;
+                foreach ($checked as $testStart => $testEnd) {
+                    // Replace old period if the new period lies around the
+                    // old period.
+                    if ($start <= $testStart && $end >= $testEnd) {
+                        // Remove old period entry.
+                        unset($checked[$testStart]);
+                        unset($checkedExtra[$testStart]);
+                        // Add replacing entry.
+                        $checked[$start] = $end;
+                        $checkedExtra[$start] = isset($extraParams[$start])
+                            ? $extraParams[$start] : array();
+                        $added = true;
+                    } elseif ($start >= $testStart && $end <= $testEnd) {
+                        // The new period lies fully within the old
+                        // period. Just forget about it.
+                        $added = true;
+                    } elseif (($end <= $testEnd && $end >= $testStart) ||
+                              ($start >= $testStart && $start <= $testEnd)) {
+                        // Now we are in trouble: Overlapping time periods. If
+                        // we allow for additional parameters we cannot simply
+                        // choose one of the two parameter sets. It's better
+                        // to leave two separated time periods.
+                        $extra = isset($extraParams[$start])
+                            ? $extraParams[$start] : array();
+                        $testExtra = isset($checkedExtra[$testStart])
+                            ? $checkedExtra[$testStart] : array();
+                        // Remove old period entry.
+                        unset($checked[$testStart]);
+                        unset($checkedExtra[$testStart]);
+                        // We have two periods overlapping. Are their
+                        // additional parameters the same or different?
+                        $newStart = min($start, $testStart);
+                        $newEnd = max($end, $testEnd);
+                        if ($extra === $testExtra) {
+                            // Both periods have the same information. So we
+                            // can just merge.
+                            $checked[$newStart] = $newEnd;
+                            $checkedExtra[$newStart] = $extra;
+                        } else {
+                            // Extra parameters are different. Create one
+                            // period at the beginning with the params of the
+                            // first period and create a trailing period with
+                            // the params of the second period. The break
+                            // point will be the end of the first period.
+                            $break = min($end, $testEnd);
+                            $checked[$newStart] = $break;
+                            $checkedExtra[$newStart] =
+                                isset($extraParams[$newStart])
+                                ? $extraParams[$newStart] : array();
+                            $checked[$break] = $newEnd;
+                            $highStart = max($start, $testStart);
+                            $checkedExtra[$break] =
+                                isset($extraParams[$highStart])
+                                ? $extraParams[$highStart] : array();
+
+                            // Ensure we also have the extra data in the
+                            // extraParams.
+                            $extraParams[$break] =
+                                isset($extraParams[$highStart])
+                                ? $extraParams[$highStart] : array();
+                        }
+                        $added = true;
+                    }
+
+                    if ($added) {
+                        break;
+                    }
+                }
+
+                if (!$added) {
+                    $checked[$start] = $end;
+                    $checkedExtra[$start] = isset($extraParams[$start])
+                        ? $extraParams[$start] : array();
+                }
+            }
+        }
+
+        return array($checked, $checkedExtra);
+    }
+
+}
+
+/**
+ * Class representing vJournals.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vjournal.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vjournal extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vJournal';
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('VJOURNAL');
+    }
+
+}
+
+
+
+
+/**
+ * Class representing vNotes.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vnote.php,v 1.3.10.10 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @author  Karsten Fourmont <fourmont at gmx.de>
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vnote extends Horde_iCalendar {
+
+    function Horde_iCalendar_vnote($version = '1.1')
+    {
+        return parent::Horde_iCalendar($version);
+    }
+
+    function getType()
+    {
+        return 'vNote';
+    }
+
+    /**
+     * Unlike vevent and vtodo, a vnote is normally not enclosed in an
+     * iCalendar container. (BEGIN..END)
+     */
+    function exportvCalendar()
+    {
+        $requiredAttributes['BODY'] = '';
+        $requiredAttributes['VERSION'] = '1.1';
+
+        foreach ($requiredAttributes as $name => $default_value) {
+            if (is_a($this->getattribute($name), 'PEAR_Error')) {
+                $this->setAttribute($name, $default_value);
+            }
+        }
+
+        return $this->_exportvData('VNOTE');
+    }
+
+}
+
+/**
+ * Class representing vTimezones.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vtimezone.php,v 1.8.10.10 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vtimezone extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vTimeZone';
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('VTIMEZONE');
+    }
+
+    /**
+     * Parse child components of the vTimezone component. Returns an
+     * array with the exact time of the time change as well as the
+     * 'from' and 'to' offsets around the change. Time is arbitrarily
+     * based on UTC for comparison.
+     */
+    function parseChild(&$child, $year)
+    {
+        // Make sure 'time' key is first for sort().
+        $result['time'] = 0;
+
+        $t = $child->getAttribute('TZOFFSETFROM');
+        if (is_a($t, 'PEAR_Error')) {
+            return false;
+        }
+        $result['from'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
+
+        $t = $child->getAttribute('TZOFFSETTO');
+        if (is_a($t, 'PEAR_Error')) {
+            return false;
+        }
+        $result['to'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
+
+        $switch_time = $child->getAttribute('DTSTART');
+        if (is_a($switch_time, 'PEAR_Error')) {
+            return false;
+        }
+
+        $rrules = $child->getAttribute('RRULE');
+        if (is_a($rrules, 'PEAR_Error')) {
+            if (!is_int($switch_time)) {
+                return false;
+            }
+            // Convert this timestamp from local time to UTC for
+            // comparison (All dates are compared as if they are UTC).
+            $t = getdate($switch_time);
+            $result['time'] = @gmmktime($t['hours'], $t['minutes'], $t['seconds'],
+                                        $t['mon'], $t['mday'], $t['year']);
+            return $result;
+        }
+
+        $rrules = explode(';', $rrules);
+        foreach ($rrules as $rrule) {
+            $t = explode('=', $rrule);
+            switch ($t[0]) {
+            case 'FREQ':
+                if ($t[1] != 'YEARLY') {
+                    return false;
+                }
+                break;
+
+            case 'INTERVAL':
+                if ($t[1] != '1') {
+                    return false;
+                }
+                break;
+
+            case 'BYMONTH':
+                $month = intval($t[1]);
+                break;
+
+            case 'BYDAY':
+                $len = strspn($t[1], '1234567890-+');
+                if ($len == 0) {
+                    return false;
+                }
+                $weekday = substr($t[1], $len);
+                $weekdays = array(
+                    'SU' => 0,
+                    'MO' => 1,
+                    'TU' => 2,
+                    'WE' => 3,
+                    'TH' => 4,
+                    'FR' => 5,
+                    'SA' => 6
+                );
+                $weekday = $weekdays[$weekday];
+                $which = intval(substr($t[1], 0, $len));
+                break;
+
+            case 'UNTIL':
+                if (intval($year) > intval(substr($t[1], 0, 4))) {
+                    return false;
+                }
+                break;
+            }
+        }
+
+        if (empty($month) || !isset($weekday)) {
+            return false;
+        }
+
+        if (is_int($switch_time)) {
+            // Was stored as localtime.
+            $switch_time = strftime('%H:%M:%S', $switch_time);
+            $switch_time = explode(':', $switch_time);
+        } else {
+            $switch_time = explode('T', $switch_time);
+            if (count($switch_time) != 2) {
+                return false;
+            }
+            $switch_time[0] = substr($switch_time[1], 0, 2);
+            $switch_time[2] = substr($switch_time[1], 4, 2);
+            $switch_time[1] = substr($switch_time[1], 2, 2);
+        }
+
+        // Get the timestamp for the first day of $month.
+        $when = gmmktime($switch_time[0], $switch_time[1], $switch_time[2],
+                         $month, 1, $year);
+        // Get the day of the week for the first day of $month.
+        $first_of_month_weekday = intval(gmstrftime('%w', $when));
+
+        // Go to the first $weekday before first day of $month.
+        if ($weekday >= $first_of_month_weekday) {
+            $weekday -= 7;
+        }
+        $when -= ($first_of_month_weekday - $weekday) * 60 * 60 * 24;
+
+        // If going backwards go to the first $weekday after last day
+        // of $month.
+        if ($which < 0) {
+            do {
+                $when += 60*60*24*7;
+            } while (intval(gmstrftime('%m', $when)) == $month);
+        }
+
+        // Calculate $weekday number $which.
+        $when += $which * 60 * 60 * 24 * 7;
+
+        $result['time'] = $when;
+
+        return $result;
+    }
+
+}
+
+/**
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_standard extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'standard';
+    }
+
+    function parsevCalendar($data)
+    {
+        parent::parsevCalendar($data, 'STANDARD');
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('STANDARD');
+    }
+
+}
+
+/**
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_daylight extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'daylight';
+    }
+
+    function parsevCalendar($data)
+    {
+        parent::parsevCalendar($data, 'DAYLIGHT');
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('DAYLIGHT');
+    }
+
+}
+
+/**
+ * Class representing vTodos.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vtodo.php,v 1.13.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Mike Cochrane <mike at graftonhall.co.nz>
+ * @since   Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vtodo extends Horde_iCalendar {
+
+    function getType()
+    {
+        return 'vTodo';
+    }
+
+    function exportvCalendar()
+    {
+        return parent::_exportvData('VTODO');
+    }
+
+    /**
+     * Convert this todo to an array of attributes.
+     *
+     * @return array  Array containing the details of the todo in a hash
+     *                as used by Horde applications.
+     */
+    function toArray()
+    {
+        $todo = array();
+
+        $name = $this->getAttribute('SUMMARY');
+        if (!is_array($name) && !is_a($name, 'PEAR_Error')) {
+            $todo['name'] = $name;
+        }
+        $desc = $this->getAttribute('DESCRIPTION');
+        if (!is_array($desc) && !is_a($desc, 'PEAR_Error')) {
+            $todo['desc'] = $desc;
+        }
+
+        $priority = $this->getAttribute('PRIORITY');
+        if (!is_array($priority) && !is_a($priority, 'PEAR_Error')) {
+            $todo['priority'] = $priority;
+        }
+
+        $due = $this->getAttribute('DTSTAMP');
+        if (!is_array($due) && !is_a($due, 'PEAR_Error')) {
+            $todo['due'] = $due;
+        }
+
+        return $todo;
+    }
+
+    /**
+     * Set the attributes for this todo item from an array.
+     *
+     * @param array $todo  Array containing the details of the todo in
+     *                     the same format that toArray() exports.
+     */
+    function fromArray($todo)
+    {
+        if (isset($todo['name'])) {
+            $this->setAttribute('SUMMARY', $todo['name']);
+        }
+        if (isset($todo['desc'])) {
+            $this->setAttribute('DESCRIPTION', $todo['desc']);
+        }
+
+        if (isset($todo['priority'])) {
+            $this->setAttribute('PRIORITY', $todo['priority']);
+        }
+
+        if (isset($todo['due'])) {
+            $this->setAttribute('DTSTAMP', $todo['due']);
+        }
+    }
+
+}
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 5185f17..4beef89 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -7,7 +7,7 @@
  * - alarms display and dismissal
  * - attachment handling
  * - recurrence computation and UI elements (TODO)
- * - ical parsing and exporting (TODO)
+ * - ical parsing and exporting
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
@@ -97,7 +97,16 @@ class libcalendaring extends rcube_plugin
         }
     }
 
-
+    /**
+     * Load iCalendar functions
+     */
+    public static function get_ical()
+    {
+        $self = self::get_instance();
+        require_once($self->home . '/libvcalendar.php');
+        return new libvcalendar($self->timezone);
+    }
+    
     /**
      * Shift dates into user's current timezone
      *
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
new file mode 100644
index 0000000..be6f93d
--- /dev/null
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -0,0 +1,575 @@
+<?php
+
+/**
+ * iCalendar functions for the Calendar plugin
+ *
+ * @version @package_version@
+ * @author Lazlo Westerhof <hello at lazlo.me>
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ * @author Bogomil "Bogo" Shopov <shopov at kolabsys.com>
+ *
+ * Copyright (C) 2010, Lazlo Westerhof <hello at lazlo.me>
+ * Copyright (C) 2013, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * 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 to parse and build vCalendar (iCalendar) files
+ *
+ * Uses the Horde:iCalendar class for parsing. To install:
+ * > pear channel-discover pear.horde.org
+ * > pear install horde/Horde_Icalendar
+ *
+ */
+class libvcalendar
+{
+  const EOL = "\r\n";
+  
+  private $timezone;
+
+  public $method;
+  public $events = array();
+
+  function __construct($timezone = null)
+  {
+    $this->timezone = $timezone ? $timezone : new DateTimezone('UTC');
+  }
+
+  /**
+   * Import events from iCalendar format
+   *
+   * @param  string vCalendar input
+   * @param  string Input charset (from envelope)
+   * @return array List of events extracted from the input
+   */
+  public function import($vcal, $charset = RCMAIL_CHARSET)
+  {
+    $parser = $this->get_parser();
+    $parser->parsevCalendar($vcal, 'VCALENDAR', $charset);
+    $this->method = $parser->getAttributeDefault('METHOD', '');
+    $this->events = $seen = array();
+    if ($data = $parser->getComponents()) {
+      foreach ($data as $comp) {
+        if ($comp->getType() == 'vEvent') {
+          $event = $this->_to_rcube_format($comp);
+          if (!$seen[$event['uid']]++)
+            $this->events[] = $event;
+        }
+      }
+    }
+    
+    return $this->events;
+  }
+
+  /**
+   * Read iCalendar events from a file
+   *
+   * @param string File path to read from
+   * @return array List of events extracted from the file
+   */
+  public function import_from_file($filepath)
+  {
+    $this->events = $seen = array();
+    $fp = fopen($filepath, 'r');
+
+    // check file content first
+    $begin = fread($fp, 1024);
+    if (!preg_match('/BEGIN:VCALENDAR/i', $begin))
+      return $this->events;
+
+    $parser = $this->get_parser();
+    $buffer = '';
+
+    fseek($fp, 0);
+    while (($line = fgets($fp, 2048)) !== false) {
+      $buffer .= $line;
+      if (preg_match('/END:VEVENT/i', $line)) {
+        if (preg_match('/BEGIN:VCALENDAR/i', $buffer))
+          $buffer .= self::EOL ."END:VCALENDAR";
+        $parser->parsevCalendar($buffer, 'VCALENDAR', RCMAIL_CHARSET, false);
+        $buffer = '';
+      }
+    }
+    fclose($fp);
+
+    if ($data = $parser->getComponents()) {
+      foreach ($data as $comp) {
+        if ($comp->getType() == 'vEvent') {
+          $event = $this->_to_rcube_format($comp);
+          if (!$seen[$event['uid']]++)
+            $this->events[] = $event;
+        }
+      }
+    }
+
+    $this->method = $parser->getAttributeDefault('METHOD', '');
+
+    return $this->events;
+  }
+
+  /**
+   * Load iCal parser from the Horde lib
+   */
+  public function get_parser()
+  {
+    if (!class_exists('Horde_iCalendar'))
+      require_once(__DIR__ . '/lib/Horde_iCalendar.php');
+
+    // set target charset for parsed events
+    $GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;
+
+    return new Horde_iCalendar;
+  }
+
+  /**
+   * Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format
+   */
+  private function _to_rcube_format($ve)
+  {
+    $event = array(
+      'uid' => $ve->getAttributeDefault('UID'),
+      'changed' => $ve->getAttributeDefault('DTSTAMP', 0),
+      'title' => $ve->getAttributeDefault('SUMMARY'),
+      'start' => $this->_date2time($ve->getAttribute('DTSTART')),
+      'end' => $this->_date2time($ve->getAttribute('DTEND')),
+      // set defaults
+      'free_busy' => 'busy',
+      'priority' => 0,
+      'attendees' => array(),
+    );
+
+    // check for all-day dates
+    if (is_array($ve->getAttribute('DTSTART')))
+      $event['allday'] = true;
+
+    if ($event['allday'])
+      $event['end']->sub(new DateInterval('PT23H'));
+
+    // assign current timezone to event start/end
+    if (is_a($event['start'], 'DateTime'))
+        $event['start']->setTimezone($this->timezone);
+    else
+        unset($event['start']);
+
+    if (is_a($event['end'], 'DateTime'))
+        $event['end']->setTimezone($this->timezone);
+    else
+        unset($event['end']);
+
+    // map other attributes to internal fields
+    $_attendees = array();
+    foreach ($ve->getAllAttributes() as $attr) {
+      switch ($attr['name']) {
+        case 'ORGANIZER':
+          $organizer = array(
+            'name' => $attr['params']['CN'],
+            'email' => preg_replace('/^mailto:/i', '', $attr['value']),
+            'role' => 'ORGANIZER',
+            'status' => 'ACCEPTED',
+          );
+          if (isset($_attendees[$organizer['email']])) {
+            $i = $_attendees[$organizer['email']];
+            $event['attendees'][$i]['role'] = $organizer['role'];
+          }
+          break;
+        
+        case 'ATTENDEE':
+          $attendee = array(
+            'name' => $attr['params']['CN'],
+            'email' => preg_replace('/^mailto:/i', '', $attr['value']),
+            'role' => $attr['params']['ROLE'] ? $attr['params']['ROLE'] : 'REQ-PARTICIPANT',
+            'status' => $attr['params']['PARTSTAT'],
+            'rsvp' => $attr['params']['RSVP'] == 'TRUE',
+          );
+          if ($organizer && $organizer['email'] == $attendee['email'])
+            $attendee['role'] = 'ORGANIZER';
+          
+          $event['attendees'][] = $attendee;
+          $_attendees[$attendee['email']] = count($event['attendees']) - 1;
+          break;
+          
+        case 'TRANSP':
+          $event['free_busy'] = $attr['value'] == 'TRANSPARENT' ? 'free' : 'busy';
+          break;
+        
+        case 'STATUS':
+          if ($attr['value'] == 'TENTATIVE')
+            $event['free_busy'] = 'tentative';
+          else if ($attr['value'] == 'CANCELLED')
+            $event['cancelled'] = true;
+          break;
+        
+        case 'PRIORITY':
+          if (is_numeric($attr['value'])) {
+            $event['priority'] = $attr['value'];
+          }
+          break;
+        
+        case 'RRULE':
+          // parse recurrence rule attributes
+          foreach (explode(';', $attr['value']) as $par) {
+            list($k, $v) = explode('=', $par);
+            $params[$k] = $v;
+          }
+          if ($params['UNTIL'])
+            $params['UNTIL'] = date_create($params['UNTIL']);
+          if (!$params['INTERVAL'])
+            $params['INTERVAL'] = 1;
+          
+          $event['recurrence'] = $params;
+          break;
+        
+        case 'EXDATE':
+          $event['recurrence']['EXDATE'][] = $this->_date2time($attr['value']);
+          break;
+        
+        case 'RECURRENCE-ID':
+          $event['recurrence_id'] = $this->_date2time($attr['value']);
+          break;
+        
+        case 'SEQUENCE':
+          $event['sequence'] = intval($attr['value']);
+          break;
+        
+        case 'CATEGORIES':
+        case 'DESCRIPTION':
+        case 'LOCATION':
+        case 'URL':
+          $event[strtolower($attr['name'])] = $attr['value'];
+          break;
+        
+        case 'CLASS':
+        case 'X-CALENDARSERVER-ACCESS':
+          $sensitivity_map = array('PUBLIC' => 0, 'PRIVATE' => 1, 'CONFIDENTIAL' => 2);
+          $event['sensitivity'] = $sensitivity_map[$attr['value']];
+          break;
+
+        case 'X-MICROSOFT-CDO-BUSYSTATUS':
+          if ($attr['value'] == 'OOF')
+            $event['free_busy'] == 'outofoffice';
+          else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE')))
+            $event['free_busy'] = strtolower($attr['value']);
+          break;
+
+        case 'ATTACH':
+          // decode inline attachment
+          if (strtoupper($attr['params']['VALUE']) == 'BINARY' && !empty($attr['value'])) {
+            $data = !strcasecmp($attr['params']['ENCODING'], 'BASE64') ? base64_decode($attr['value']) : $attr['value'];
+            $mimetype = $attr['params']['FMTTYPE'] ? $attr['params']['FMTTYPE'] : rcube_mime::file_content_type($data, $attr['params']['X-LABEL'], 'application/octet-stream', true);
+            $extensions = rcube_mime::get_mime_extensions($mimetype);
+            $filename = $attr['params']['X-LABEL'] ? $attr['params']['X-LABEL'] : 'attachment' . count($event['attachments']) . '.' . $extensions[0];
+            $event['attachments'][] = array(
+              'mimetype' => $mimetype,
+              'name' => $filename,
+              'data' => $data,
+              'size' => strlen($data),
+            );
+          }
+          else if (!empty($attr['value']) && preg_match('!^[hftps]+://!', $attr['value'])) {
+            // TODO: add support for displaying/managing link attachments in UI
+            $event['links'][] = $attr['value'];
+          }
+          break;
+
+        default:
+          if (substr($attr['name'], 0, 2) == 'X-')
+            $event['x-custom'][] = array($attr['name'], $attr['value']);
+      }
+    }
+
+    // find alarms
+    if ($valarm = $ve->findComponent('valarm')) {
+      $action = 'DISPLAY';
+      $trigger = null;
+      
+      foreach ($valarm->getAllAttributes() as $attr) {
+        switch ($attr['name']) {
+          case 'TRIGGER':
+            if ($attr['params']['VALUE'] == 'DATE-TIME') {
+              $trigger = '@' . $attr['value'];
+            }
+            else {
+              $trigger = $attr['value'];
+              $offset = abs($trigger);
+              $unit = 'S';
+              if ($offset % 86400 == 0) {
+                $unit = 'D';
+                $trigger = intval($trigger / 86400);
+              }
+              else if ($offset % 3600 == 0) {
+                $unit = 'H';
+                $trigger = intval($trigger / 3600);
+              }
+              else if ($offset % 60 == 0) {
+                $unit = 'M';
+                $trigger = intval($trigger / 60);
+              }
+            }
+            break;
+
+          case 'ACTION':
+            $action = $attr['value'];
+            break;
+        }
+      }
+      if ($trigger)
+        $event['alarms'] = $trigger . $unit . ':' . $action;
+    }
+
+    // add organizer to attendees list if not already present
+    if ($organizer && !isset($_attendees[$organizer['email']]))
+      array_unshift($event['attendees'], $organizer);
+
+    // make sure the event has an UID
+#    if (!$event['uid'])
+#      $event['uid'] = $this->cal->generate_uid();
+    
+    return $event;
+  }
+  
+  /**
+   * Helper method to correctly interpret an all-day date value
+   */
+  private function _date2time($prop)
+  {
+    // create timestamp at 12:00 in user's timezone
+    if (is_array($prop))
+      return date_create(sprintf('%04d%02d%02dT120000', $prop['year'], $prop['month'], $prop['mday']), $this->timezone);
+    else if (is_numeric($prop))
+      return date_create('@'.$prop);
+    
+    return $prop;
+  }
+
+
+  /**
+   * Free resources by clearing member vars
+   */
+  public function reset()
+  {
+    $this->method = '';
+    $this->events = array();
+  }
+
+  /**
+   * Export events to iCalendar format
+   *
+   * @param  array   Events as array
+   * @param  string  VCalendar method to advertise
+   * @param  boolean Directly send data to stdout instead of returning
+   * @param  callable Callback function to fetch attachment contents, false if no attachment export
+   * @return string  Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
+   */
+  public function export($events, $method = null, $write = false, $get_attachment = false, $recurrence_id = null)
+  {
+      $memory_limit = parse_bytes(ini_get('memory_limit'));
+
+      if (!$recurrence_id) {
+        $ical = "BEGIN:VCALENDAR" . self::EOL;
+        $ical .= "VERSION:2.0" . self::EOL;
+        $ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL;
+        $ical .= "CALSCALE:GREGORIAN" . self::EOL;
+
+        if ($method)
+          $ical .= "METHOD:" . strtoupper($method) . self::EOL;
+
+        if ($write) {
+          echo $ical;
+          $ical = '';
+        }
+      }
+
+      foreach ($events as $event) {
+        $vevent = "BEGIN:VEVENT" . self::EOL;
+        $vevent .= "UID:" . self::escape($event['uid']) . self::EOL;
+        $vevent .= $this->format_datetime("DTSTAMP", $event['changed'] ?: new DateTime(), false, true) . self::EOL;
+        if ($event['sequence'])
+          $vevent .= "SEQUENCE:" . intval($event['sequence']) . self::EOL;
+        if ($recurrence_id)
+          $vevent .= $recurrence_id . self::EOL;
+        
+        // correctly set all-day dates
+        if ($event['allday']) {
+          $event['end'] = clone $event['end'];
+          $event['end']->add(new DateInterval('P1D'));  // ends the next day
+          $vevent .= $this->format_datetime("DTSTART", $event['start'], true) . self::EOL;
+          $vevent .= $this->format_datetime("DTEND",   $event['end'], true) . self::EOL;
+        }
+        else {
+          $vevent .= $this->format_datetime("DTSTART", $event['start'], false) . self::EOL;
+          $vevent .= $this->format_datetime("DTEND",   $event['end'], false) . self::EOL;
+        }
+        $vevent .= "SUMMARY:" . self::escape($event['title']) . self::EOL;
+        $vevent .= "DESCRIPTION:" . self::escape($event['description']) . self::EOL;
+        
+        if (!empty($event['attendees'])){
+          $vevent .= $this->_get_attendees($event['attendees']);
+        }
+
+        if (!empty($event['location'])) {
+          $vevent .= "LOCATION:" . self::escape($event['location']) . self::EOL;
+        }
+        if (!empty($event['url'])) {
+          $vevent .= "URL:" . self::escape($event['url']) . self::EOL;
+        }
+        if ($event['recurrence'] && !$recurrence_id) {
+          $vevent .= "RRULE:" . libcalendaring::to_rrule($event['recurrence'], self::EOL) . self::EOL;
+          
+          foreach ((array)$event['recurrence']['EXDATE'] as $ex) {
+             $vevent .= $this->format_datetime("EXDATE", $ex, false, true) . self::EOL;
+          }
+        }
+        if (!empty($event['categories'])) {
+          $vevent .= "CATEGORIES:" . self::escape(strtoupper($event['categories'])) . self::EOL;
+        }
+        if ($event['sensitivity'] > 0) {
+          $vevent .= "CLASS:" . ($event['sensitivity'] == 2 ? 'CONFIDENTIAL' : 'PRIVATE') . self::EOL;
+        }
+        if ($event['alarms']) {
+          list($trigger, $action) = explode(':', $event['alarms']);
+          $val = libcalendaring::parse_alaram_value($trigger);
+          
+          $vevent .= "BEGIN:VALARM\n";
+          if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL;
+          else         $vevent .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL;
+          if ($action) $vevent .= "ACTION:" . self::escape(strtoupper($action)) . self::EOL;
+          $vevent .= "END:VALARM\n";
+        }
+        
+        $vevent .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL;
+        
+        if ($event['priority']) {
+          $vevent .= "PRIORITY:" . $event['priority'] . self::EOL;
+        }
+        
+        if ($event['cancelled'])
+          $vevent .= "STATUS:CANCELLED" . self::EOL;
+        else if ($event['free_busy'] == 'tentative')
+          $vevent .= "STATUS:TENTATIVE" . self::EOL;
+        
+        foreach ((array)$event['x-custom'] as $prop)
+          $vevent .= $prop[0] . ':' . self::escape($prop[1]) . self::EOL;
+        
+        // export attachments using the given callback function
+        if (is_callable($get_attachment) && !empty($event['attachments'])) {
+          foreach ((array)$event['attachments'] as $attach) {
+            // check available memory and skip attachment export if we can't buffer it
+            if ($memory_limit > 0 && ($memory_used = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024)
+                  && $attach['size'] && $memory_used + $attach['size'] * 3 > $memory_limit) {
+                continue;
+            }
+            // TODO: let the callback print the data directly to stdout (with b64 encoding)
+            if ($data = call_user_func($get_attachment, $attach['id'], $event)) {
+              $vevent .= sprintf('ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=%s;X-LABEL=%s:',
+                self::escape($attach['mimetype']), self::escape($attach['name']));
+              $vevent .= base64_encode($data) . self::EOL;
+            }
+            unset($data);  // attempt to free memory
+          }
+        }
+        
+        $vevent .= "END:VEVENT" . self::EOL;
+
+        // append recurrence exceptions
+        if ($event['recurrence']['EXCEPTIONS'] && !$recurrence_id) {
+          foreach ($event['recurrence']['EXCEPTIONS'] as $ex) {
+            $exdate = clone $event['start'];
+            $exdate->setDate($ex['start']->format('Y'), $ex['start']->format('n'), $ex['start']->format('j'));
+            $vevent .= $this->export(array($ex), null, false, $get_attachment,
+              $this->format_datetime('RECURRENCE-ID', $exdate, $event['allday']));
+          }
+        }
+
+        if ($write)
+          echo rcube_vcard::rfc2425_fold($vevent);
+        else
+          $ical .= $vevent;
+      }
+      
+      if (!$recurrence_id) {
+        $ical .= "END:VCALENDAR" . self::EOL;
+      
+        if ($write) {
+          echo $ical;
+          return true;
+        }
+      }
+
+      // fold lines to 75 chars
+      return rcube_vcard::rfc2425_fold($ical);
+  }
+
+  private function format_datetime($attr, $dt, $dateonly = false, $utc = false)
+  {
+    if (is_numeric($dt))
+        $dt = new DateTime('@'.$dt);
+
+    if ($utc)
+      $dt->setTimezone(new DateTimeZone('UTC'));
+
+    if ($dateonly) {
+      return $attr . ';VALUE=DATE:' . $dt->format('Ymd');
+    }
+    else {
+      // <ATTR>;TZID=Europe/Zurich:20120706T210000
+      $tz = $dt->getTimezone();
+      $tzname = $tz ? $tz->getName() : null;
+      $tzid = $tzname && $tzname != 'UTC' && $tzname != '+00:00' ? ';TZID=' . self::escape($tzname) : '';
+      return $attr . $tzid . ':' . $dt->format('Ymd\THis' . ($tzid ? '' : '\Z'));
+    }
+  }
+
+  /**
+   * Escape values according to RFC 2445 4.3.11
+   */
+  private function escape($str)
+  {
+    return strtr($str, array('\\' => '\\\\', "\n" => '\n', ';' => '\;', ',' => '\,'));
+  }
+
+  /**
+  * Construct the orginizer of the event.
+  * @param Array Attendees and roles
+  *
+  */
+  private function _get_attendees($ats)
+  {
+    $organizer = "";
+    $attendees = "";
+    foreach ($ats as $at) {
+      if ($at['role'] == "ORGANIZER") {
+        if ($at['email']) {
+          $organizer .= "ORGANIZER;";
+          if (!empty($at['name']))
+            $organizer .= 'CN="' . $at['name'] . '"';
+          $organizer .= ":mailto:". $at['email'] . self::EOL;
+        }
+      }
+      else if ($at['email']) {
+        //I am an attendee 
+        $attendees .= "ATTENDEE;ROLE=" . $at['role'] . ";PARTSTAT=" . $at['status'];
+        if ($at['rsvp'])
+          $attendees .= ";RSVP=TRUE";
+        if (!empty($at['name']))
+          $attendees .= ';CN="' . $at['name'] . '"';
+        $attendees .= ":mailto:" . $at['email'] . self::EOL;
+      }
+    }
+
+    return $organizer . $attendees;
+  }
+
+}




More information about the commits mailing list