4 commits - plugins/calendar plugins/libcalendaring plugins/libkolab

Thomas Brüderli bruederli at kolabsys.com
Fri Feb 28 16:53:01 CET 2014


 plugins/calendar/calendar.php                             |   17 ++
 plugins/calendar/drivers/kolab/kolab_calendar.php         |    6 
 plugins/calendar/lib/Horde_Date_Recurrence.php            |   34 ++++
 plugins/calendar/lib/calendar_recurrence.php              |    4 
 plugins/libcalendaring/libvcalendar.php                   |  104 +++++++++++---
 plugins/libcalendaring/tests/libvcalendar.php             |   44 +++++
 plugins/libcalendaring/tests/resources/multiple-rdate.ics |   80 ++++++++++
 plugins/libkolab/lib/kolab_format_xcal.php                |   17 ++
 8 files changed, 283 insertions(+), 23 deletions(-)

New commits:
commit 0ba3e8382d60dfa47cb23706787bb70238dd3fd0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Feb 28 16:51:55 2014 +0100

    Keep RDATE values when updating an event (the client doesn't submit these values back)

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 2fe072a..49f8fa7 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -346,7 +346,6 @@ class kolab_calendar
     if (!$old || PEAR::isError($old))
       return false;
 
-    $old['recurrence'] = '';  # clear old field, could have been removed in new, too
     $object = $this->_from_rcube_event($event, $old);
     $saved = $this->storage->save($object, 'event', $event['id']);
 
@@ -648,6 +647,11 @@ class kolab_calendar
 
     $event['_owner'] = $identity['email'];
 
+    # copy RDATE values as the UI doesn't yet support these
+    if (empty($event['recurrence']['FREQ']) && $old['recurrence']['RDATE'] && empty($old['recurrence']['FREQ'])) {
+      $event['recurrence']['RDATE'] = $old['recurrence']['RDATE'];
+    }
+
     // remove some internal properties which should not be saved
     unset($event['_savemode'], $event['_fromcalendar'], $event['_identity']);
 


commit 1f6729eb1402ce9b05691b4ce23cdffc4b951976
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Feb 28 16:16:56 2014 +0100

    Add basic support for RDATE recurrence properties (#2886).
    This only uses these values as a fall-back if no RRULE is defined but not in combination with it.

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index ddaa427..092a224 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1439,6 +1439,23 @@ class calendar extends rcube_plugin
    */
   private function _recurrence_text($rrule)
   {
+    // derive missing FREQ and INTERVAL from RDATE list
+    if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
+      $first = $rrule['RDATE'][0];
+      $second = $rrule['RDATE'][1];
+      if (is_a($first, 'DateTime') && is_a($second, 'DateTime')) {
+        $diff = $first->diff($second);
+        foreach (array('y' => 'YEARLY', 'm' => 'MONTHLY', 'd' => 'DAILY') as $k => $freq) {
+          if ($diff->$k != 0) {
+            $rrule['FREQ'] = $freq;
+            $rrule['INTERVAL'] = $diff->$k;
+            break;
+          }
+        }
+      }
+      $rrule['UNTIL'] = end($rrule['RDATE']);
+    }
+
     // TODO: finish this
     $freq = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL']);
     $details = '';
diff --git a/plugins/calendar/lib/Horde_Date_Recurrence.php b/plugins/calendar/lib/Horde_Date_Recurrence.php
index 81f0857..35f884c 100644
--- a/plugins/calendar/lib/Horde_Date_Recurrence.php
+++ b/plugins/calendar/lib/Horde_Date_Recurrence.php
@@ -127,6 +127,13 @@ class Horde_Date_Recurrence
     public $recurMonths = array();
 
     /**
+     * RDATE recurrence values
+     *
+     * @var array
+     */
+    public $rdates = array();
+
+    /**
      * All the exceptions from recurrence for this event.
      *
      * @var array
@@ -427,7 +434,7 @@ class Horde_Date_Recurrence
             return clone $this->start;
         }
 
-        if ($this->recurInterval == 0) {
+        if ($this->recurInterval == 0 && empty($this->rdates)) {
             return false;
         }
 
@@ -779,6 +786,19 @@ class Horde_Date_Recurrence
             return $next;
         }
 
+        // fall-back to RDATE properties
+        if (!empty($this->rdates)) {
+            $next = clone $this->start;
+            foreach ($this->rdates as $rdate) {
+                $next->year  = $rdate->year;
+                $next->month = $rdate->month;
+                $next->mday  = $rdate->mday;
+                if ($next->compareDateTime($after) > 0) {
+                    return $next;
+                }
+            }
+        }
+
         // We didn't find anything, the recurType was bad, or something else
         // went wrong - return false.
         return false;
@@ -835,6 +855,18 @@ class Horde_Date_Recurrence
     }
 
     /**
+     * Adds an absolute recurrence date.
+     *
+     * @param integer $year   The year of the instance.
+     * @param integer $month  The month of the instance.
+     * @param integer $mday   The day of the month of the instance.
+     */
+    public function addRDate($year, $month, $mday)
+    {
+        $this->rdates[] = new Horde_Date($year, $month, $mday);
+    }
+
+    /**
      * Adds an exception to a recurring event.
      *
      * @param integer $year   The year of the execption.
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index d4a3641..5c21852 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -60,6 +60,10 @@ class calendar_recurrence
       foreach ($event['recurrence']['EXDATE'] as $exdate)
         $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
     }
+    if (is_array($event['recurrence']['RDATE'])) {
+      foreach ($event['recurrence']['RDATE'] as $rdate)
+        $this->engine->addRDate($rdate->format('Y'), $rdate->format('n'), $rdate->format('j'));
+    }
   }
 
   /**


commit 79ae6282f81709e8554ca81b2c9b3f2c3da78421
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Feb 28 16:12:24 2014 +0100

    Read/write RDATE properties from/to ical and libkolabxml (#2885)

diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index eff85c1..0ce7902 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -368,8 +368,8 @@ class libvcalendar implements Iterator
     private function _to_array($ve)
     {
         $event = array(
-            'uid'     => strval($ve->UID),
-            'title'   => strval($ve->SUMMARY),
+            'uid'     => self::convert_string($ve->UID),
+            'title'   => self::convert_string($ve->SUMMARY),
             '_type'   => $ve->name == 'VTODO' ? 'task' : 'event',
             // set defaults
             'priority' => 0,
@@ -435,7 +435,11 @@ class libvcalendar implements Iterator
                 break;
 
             case 'EXDATE':
-                $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], (array)self::convert_datetime($prop));
+                $event['recurrence']['EXDATE'] = array_merge((array)$event['recurrence']['EXDATE'], self::convert_datetime($prop, true));
+                break;
+
+            case 'RDATE':
+                $event['recurrence']['RDATE'] = array_merge((array)$event['recurrence']['RDATE'], self::convert_datetime($prop, true));
                 break;
 
             case 'RECURRENCE-ID':
@@ -459,7 +463,7 @@ class libvcalendar implements Iterator
             case 'LOCATION':
             case 'DESCRIPTION':
             case 'URL':
-                $event[strtolower($prop->name)] = str_replace('\,', ',', $prop->value);
+                $event[strtolower($prop->name)] = self::convert_string($prop);
                 break;
 
             case 'CATEGORY':
@@ -672,12 +676,20 @@ class libvcalendar implements Iterator
     }
 
     /**
+     *
+     */
+    public static function convert_string($prop)
+    {
+        return str_replace('\,', ',', strval($prop->value));
+    }
+
+    /**
      * Helper method to correctly interpret an all-day date value
      */
-    public static function convert_datetime($prop)
+    public static function convert_datetime($prop, $as_array = false)
     {
         if (empty($prop)) {
-            return null;
+            return $as_array ? array() : null;
         }
         else if ($prop instanceof VObject\Property\MultiDateTime) {
             $dt = array();
@@ -693,10 +705,38 @@ class libvcalendar implements Iterator
                 $dt->_dateonly = true;
             }
         }
+        else if ($prop instanceof VObject\Property && ($prop['VALUE'] == 'DATE' || $prop['VALUE'] == 'DATE-TIME')) {
+            try {
+                list($type, $dt) = VObject\Property\DateTime::parseData($prop->value, $prop);
+                $dt->_dateonly = ($type & VObject\Property\DateTime::DATE);
+            }
+            catch (Exception $e) {
+                // ignore date parse errors
+            }
+        }
+        else if ($prop instanceof VObject\Property && $prop['VALUE'] == 'PERIOD') {
+            $dt = array();
+            foreach(explode(',', $prop->value) as $val) {
+                try {
+                    list($start, $end) = explode('/', $val);
+                    list($type, $item) = VObject\Property\DateTime::parseData($start, $prop);
+                    $item->_dateonly = ($type & VObject\Property\DateTime::DATE);
+                    $dt[] = $item;
+                }
+                catch (Exception $e) {
+                    // ignore single date parse errors
+                }
+            }
+        }
         else if ($prop instanceof DateTime) {
             $dt = $prop;
         }
 
+        // force return value to array if requested
+        if ($as_array && !is_array($dt)) {
+            $dt = empty($dt) ? array() : array($dt);
+        }
+
         return $dt;
     }
 
@@ -839,8 +879,13 @@ class libvcalendar implements Iterator
             if ($exdates = $event['recurrence']['EXDATE']) {
                 unset($event['recurrence']['EXDATE']);  // don't serialize EXDATEs into RRULE value
             }
+            if ($rdates = $event['recurrence']['RDATE']) {
+                unset($event['recurrence']['RDATE']);  // don't serialize RDATEs into RRULE value
+            }
 
-            $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence']));
+            if ($event['recurrence']['FREQ']) {
+                $ve->add('RRULE', libcalendaring::to_rrule($event['recurrence']));
+            }
 
             // add EXDATEs each one per line (for Thunderbird Lightning)
             if ($exdates) {
@@ -853,6 +898,13 @@ class libvcalendar implements Iterator
                     }
                 }
             }
+            // add RDATEs
+            if (!empty($rdates)) {
+                $sample = self::datetime_prop('RDATE', $rdates[0]);
+                $rdprop = new VObject\Property\MultiDateTime('RDATE', null);
+                $rdprop->setDateTimes($rdates, $sample->getDateType());
+                $ve->add($rdprop);
+            }
         }
 
         if ($event['categories']) {
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
index 91cb9e8..32e3aa8 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -238,6 +238,19 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
     }
 
     /**
+     * Parse RDATE properties (#2885)
+     */
+    function test_rdate()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
+        $event = $events[0];
+
+        $this->assertEquals(9, count($event['recurrence']['RDATE']));
+        $this->assertInstanceOf('DateTime', $event['recurrence']['RDATE'][0]);
+    }
+
+    /**
      * @depends test_import
      */
     function test_freebusy()
@@ -388,7 +401,19 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
         $this->assertContains('RECURRENCE-ID;VALUE=DATE-TIME:20131113', $ics, "Recurrence-ID (2) being the exception date");
         $this->assertContains('SUMMARY:'.$exception2['title'], $ics, "Exception title");
     }
-    
+
+    /**
+     *
+     */
+    function test_export_rdate()
+    {
+        $ical = new libvcalendar();
+        $events = $ical->import_from_file(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
+        $ics = $ical->export($events, null, false);
+
+        $this->assertContains('RDATE;VALUE=DATE-TIME:20140520T020000Z', $ics, "VALUE=PERIOD is translated into single DATE-TIME values");
+    }
+
     /**
      * @depends test_export
      */
diff --git a/plugins/libcalendaring/tests/resources/multiple-rdate.ics b/plugins/libcalendaring/tests/resources/multiple-rdate.ics
index 51b938d..a501706 100644
--- a/plugins/libcalendaring/tests/resources/multiple-rdate.ics
+++ b/plugins/libcalendaring/tests/resources/multiple-rdate.ics
@@ -23,8 +23,8 @@ BEGIN:VEVENT
 DTSTART;TZID="W. Europe":20140520T040000
 DTEND;TZID="W. Europe":20140520T200000
 TRANSP:TRANSPARENT
-RDATE;VALUE=PERIOD:20140520T020000Z/20140520T180000Z
- ,PERIOD:20150520T020000Z/20150520T180000Z
+RDATE;VALUE=DATE-TIME:20140520T020000Z
+RDATE;VALUE=PERIOD:20150520T020000Z/20150520T180000Z
  ,20160520T020000Z/20160520T180000Z,20170520T020000Z/20170520T180000Z
  ,20180520T020000Z/20180520T180000Z,20190520T020000Z/20190520T180000Z
  ,20200520T020000Z/20200520T180000Z,20210520T020000Z/20210520T180000Z
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 979aeef..4af692c 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -203,6 +203,13 @@ abstract class kolab_format_xcal extends kolab_format
             }
         }
 
+        if ($rdates = $this->obj->recurrenceDates()) {
+            for ($i=0; $i < $rdates->size(); $i++) {
+                if ($rdate = self::php_datetime($rdates->get($i)))
+                    $object['recurrence']['RDATE'][] = $rdate;
+            }
+        }
+
         // read alarm
         $valarms = $this->obj->alarms();
         $alarm_types = array_flip($this->alarm_type_map);
@@ -338,7 +345,7 @@ abstract class kolab_format_xcal extends kolab_format
         $rr = new RecurrenceRule;
         $rr->setFrequency(RecurrenceRule::FreqNone);
 
-        if ($object['recurrence']) {
+        if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
             $rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
 
             if ($object['recurrence']['INTERVAL'])
@@ -395,6 +402,14 @@ abstract class kolab_format_xcal extends kolab_format
 
         $this->obj->setRecurrenceRule($rr);
 
+        // save recurrence dates (aka RDATE)
+        if (!empty($object['recurrence']['RDATE'])) {
+            $rdates = new vectordatetime;
+            foreach ((array)$object['recurrence']['RDATE'] as $rdate)
+                $rdates->push(self::get_datetime($rdate, null, true));
+            $this->obj->setRecurrenceDates($rdates);
+        }
+
         // save alarm
         $valarms = new vectoralarm;
         if ($object['alarms']) {


commit 1ef785c8c63412a6f6e2e0caad2f60a8fe5af97c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Feb 28 12:41:31 2014 +0100

    Handle multiple VCALENDAR blocks when reading ics files (#2884)

diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 369f08a..eff85c1 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -48,6 +48,7 @@ class libvcalendar implements Iterator
     private $iteratorkey = 0;
     private $charset;
     private $forward_exceptions;
+    private $vhead;
     private $fp;
 
     public $method;
@@ -101,6 +102,7 @@ class libvcalendar implements Iterator
      */
     public function reset()
     {
+        $this->vhead = '';
         $this->method = '';
         $this->objects = array();
         $this->freebusy = array();
@@ -202,16 +204,7 @@ class libvcalendar implements Iterator
             return false;
         }
 
-        // read vcalendar header (with timezone defintion)
-        $this->vhead = '';
         fseek($this->fp, 0);
-        while (($line = fgets($this->fp, 512)) !== false) {
-            if (preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $line))
-                break;
-            $this->vhead .= $line;
-        }
-        fseek($this->fp, -strlen($line), SEEK_CUR);
-
         return $this->_parse_next();
     }
 
@@ -269,10 +262,31 @@ class libvcalendar implements Iterator
     private function _next_component()
     {
         $buffer = '';
+        $vcalendar_head = false;
         while (($line = fgets($this->fp, 1024)) !== false) {
-            $buffer .= $line;
-            if (preg_match('/END:(VEVENT|VTODO|VFREEBUSY)/i', $line)) {
-                break;
+            // ignore END:VCALENDAR lines
+            if (preg_match('/END:VCALENDAR/i', $line)) {
+                continue;
+            }
+            // read vcalendar header (with timezone defintion)
+            if (preg_match('/BEGIN:VCALENDAR/i', $line)) {
+                $this->vhead = '';
+                $vcalendar_head = true;
+            }
+
+            // end of VCALENDAR header part
+            if ($vcalendar_head && preg_match('/BEGIN:(VEVENT|VTODO|VFREEBUSY)/i', $line)) {
+                $vcalendar_head = false;
+            }
+
+            if ($vcalendar_head) {
+                $this->vhead .= $line;
+            }
+            else {
+                $buffer .= $line;
+                if (preg_match('/END:(VEVENT|VTODO|VFREEBUSY)/i', $line)) {
+                    break;
+                }
             }
         }
 
diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php
index a74eaf3..91cb9e8 100644
--- a/plugins/libcalendaring/tests/libvcalendar.php
+++ b/plugins/libcalendaring/tests/libvcalendar.php
@@ -74,6 +74,23 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase
         $this->assertEmpty($events);
     }
 
+    /**
+     * Test parsing from files with multiple VCALENDAR blocks (#2884)
+     */
+    function test_import_from_file_multiple()
+    {
+        $ical = new libvcalendar();
+        $ical->fopen(__DIR__ . '/resources/multiple-rdate.ics', 'UTF-8');
+        $events = array();
+        foreach ($ical as $event) {
+            $events[] = $event;
+        }
+
+        $this->assertEquals(2, count($events));
+        $this->assertEquals("AAAA6A8C3CCE4EE2C1257B5C00FFFFFF-Lotus_Notes_Generated", $events[0]['uid']);
+        $this->assertEquals("AAAA1C572093EC3FC125799C004AFFFF-Lotus_Notes_Generated", $events[1]['uid']);
+    }
+
     function test_invalid_dates()
     {
         $ical = new libvcalendar();
diff --git a/plugins/libcalendaring/tests/resources/multiple-rdate.ics b/plugins/libcalendaring/tests/resources/multiple-rdate.ics
new file mode 100644
index 0000000..51b938d
--- /dev/null
+++ b/plugins/libcalendaring/tests/resources/multiple-rdate.ics
@@ -0,0 +1,80 @@
+BEGIN:VCALENDAR
+X-LOTUS-CHARSET:UTF-8
+VERSION:2.0
+PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C
+METHOD:PUBLISH
+BEGIN:VTIMEZONE
+TZID:W. Europe
+BEGIN:STANDARD
+DTSTART:19501029T020000
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19500326T020000
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VEVENT
+DTSTART;TZID="W. Europe":20140520T040000
+DTEND;TZID="W. Europe":20140520T200000
+TRANSP:TRANSPARENT
+RDATE;VALUE=PERIOD:20140520T020000Z/20140520T180000Z
+ ,PERIOD:20150520T020000Z/20150520T180000Z
+ ,20160520T020000Z/20160520T180000Z,20170520T020000Z/20170520T180000Z
+ ,20180520T020000Z/20180520T180000Z,20190520T020000Z/20190520T180000Z
+ ,20200520T020000Z/20200520T180000Z,20210520T020000Z/20210520T180000Z
+ ,20220520T020000Z/20220520T180000Z
+DTSTAMP:20140227T123549Z
+CLASS:PUBLIC
+SUMMARY:Feiertag - Pfingsmontag
+UID:AAAA6A8C3CCE4EE2C1257B5C00FFFFFF-Lotus_Notes_Generated
+X-LOTUS-PARITAL-REPEAT:TRUE
+X-LOTUS-NOTESVERSION:2
+X-LOTUS-APPTTYPE:1
+END:VEVENT
+
+END:VCALENDAR
+
+BEGIN:VCALENDAR
+X-LOTUS-CHARSET:UTF-8
+VERSION:2.0
+PRODID:-//Lotus Development Corporation//NONSGML Notes 8.5.3//EN_C
+METHOD:PUBLISH
+BEGIN:VTIMEZONE
+TZID:W. Europe
+BEGIN:STANDARD
+DTSTART:19501029T020000
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19500326T020000
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+RRULE:FREQ=YEARLY;BYMINUTE=0;BYHOUR=2;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VEVENT
+DTSTART;TZID="W. Europe":20120330T040000
+DTEND;TZID="W. Europe":20120330T200000
+TRANSP:TRANSPARENT
+RDATE;VALUE=PERIOD:20120330T020000Z/20120330T180000Z
+ ,20130330T030000Z/20130330T190000Z,20140330T020000Z/20140330T180000Z
+ ,20150330T020000Z/20150330T180000Z,20160330T020000Z/20160330T180000Z
+ ,20170330T020000Z/20170330T180000Z,20180330T020000Z/20180330T180000Z
+ ,20190330T030000Z/20190330T190000Z,20200330T020000Z/20200330T180000Z
+ ,20210330T020000Z/20210330T180000Z
+DTSTAMP:20140227T123547Z
+CLASS:PUBLIC
+SUMMARY:Another RDATE repeating event
+UID:AAAA1C572093EC3FC125799C004AFFFF-Lotus_Notes_Generated
+END:VEVENT
+
+END:VCALENDAR




More information about the commits mailing list