6 commits - plugins/calendar plugins/libcalendaring plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Thu Aug 16 21:26:27 CEST 2012


 plugins/calendar/calendar.php                                  |  650 --------
 plugins/calendar/calendar_base.js                              |  164 --
 plugins/calendar/calendar_ui.js                                |  112 -
 plugins/calendar/drivers/database/database_driver.php          |    4 
 plugins/calendar/drivers/kolab/kolab_driver.php                |    2 
 plugins/calendar/lib/calendar_ical.php                         |    6 
 plugins/calendar/lib/calendar_itip.php                         |    2 
 plugins/calendar/lib/calendar_recurrence.php                   |    2 
 plugins/calendar/lib/calendar_ui.php                           |   83 -
 plugins/calendar/localization/de_CH.inc                        |   23 
 plugins/calendar/localization/de_DE.inc                        |   23 
 plugins/calendar/localization/en_US.inc                        |   23 
 plugins/calendar/localization/pl_PL.inc                        |   22 
 plugins/calendar/localization/ru_RU.inc                        |   22 
 plugins/calendar/package.xml                                   |    9 
 plugins/calendar/skins/classic/templates/calendar.html         |    4 
 plugins/calendar/skins/larry/calendar.css                      |   49 
 plugins/calendar/skins/larry/templates/attachment.html         |    9 
 plugins/calendar/skins/larry/templates/calendar.html           |    4 
 plugins/libcalendaring/libcalendaring.js                       |  443 +++++
 plugins/libcalendaring/libcalendaring.php                      |  788 ++++++++++
 plugins/libcalendaring/localization/de_CH.inc                  |   29 
 plugins/libcalendaring/localization/de_DE.inc                  |   29 
 plugins/libcalendaring/localization/en_US.inc                  |   30 
 plugins/libcalendaring/localization/pl_PL.inc                  |   28 
 plugins/libcalendaring/localization/ru_RU.inc                  |   29 
 plugins/libcalendaring/skins/larry/libcal.css                  |   54 
 plugins/tasklist/drivers/database/tasklist_database_driver.php |   19 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php       |   16 
 plugins/tasklist/localization/de_CH.inc                        |    1 
 plugins/tasklist/localization/en_US.inc                        |    1 
 plugins/tasklist/skins/larry/tasklist.css                      |   14 
 plugins/tasklist/skins/larry/templates/attachment.html         |    9 
 plugins/tasklist/skins/larry/templates/mainview.html           |    2 
 plugins/tasklist/skins/larry/templates/taskedit.html           |    2 
 plugins/tasklist/tasklist.js                                   |  166 --
 plugins/tasklist/tasklist.php                                  |  252 ---
 plugins/tasklist/tasklist_base.js                              |    2 
 plugins/tasklist/tasklist_ui.php                               |   66 
 39 files changed, 1627 insertions(+), 1566 deletions(-)

New commits:
commit 47857456ebf4d4bf4d9979028d14c570305af346
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Aug 16 21:26:37 2012 +0200

    Move alarm translations to libcalendaring

diff --git a/plugins/calendar/localization/ru_RU.inc b/plugins/calendar/localization/ru_RU.inc
index 862c4ee..978462e 100644
--- a/plugins/calendar/localization/ru_RU.inc
+++ b/plugins/calendar/localization/ru_RU.inc
@@ -107,30 +107,8 @@ $labels['futureevents'] = 'Будующее';
 
 // alarm/reminder settings
 $labels['showalarms'] = 'Показать уведомление';
-$labels['alarmemail'] = 'Послать e-mail';
-$labels['alarmdisplay'] = 'Показать сообщение';
-$labels['alarmdisplayoption'] = 'Сообщение';
-$labels['alarmemailoption'] = 'Email';
-$labels['alarmat'] = 'на $datetime';
-$labels['trigger@'] = 'на дату';
-$labels['trigger-M'] = 'минут до';
-$labels['trigger-H'] = 'часов до';
-$labels['trigger-D'] = 'дней до';
-$labels['trigger+M'] = 'минут после';
-$labels['trigger+H'] = 'часов после';
-$labels['trigger+D'] = 'дней после';
-$labels['addalarm'] = 'добавить уведомление';
 $labels['defaultalarmtype'] = 'Настройки напоминания по умолчанию';
 $labels['defaultalarmoffset'] = 'Время напоминания по умолчанию';
-$labels['dismissall'] = 'Отменить все';
-$labels['dismiss'] = 'Отменить';
-$labels['snooze'] = 'Отложить';
-$labels['repeatinmin'] = 'Повторить через $min minutes';
-$labels['repeatinhr'] = 'Повторить через 1 час';
-$labels['repeatinhrs'] = 'Повторить через $hrs часов';
-$labels['repeattomorrow'] = 'Повторить завтра';
-$labels['repeatinweek'] = 'Повторить через неделю';
-$labels['alarmtitle'] = 'Предстоящие события';
 
 // attendees
 $labels['attendee'] = 'Участник';
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 4a4eee2..702aac5 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -15,6 +15,7 @@ $labels['trigger+M'] = 'minutes after';
 $labels['trigger+H'] = 'hours after';
 $labels['trigger+D'] = 'days after';
 $labels['addalarm'] = 'add alarm';
+
 $labels['alarmtitle'] = 'Upcoming events';
 $labels['dismissall'] = 'Dismiss all';
 $labels['dismiss'] = 'Dismiss';
diff --git a/plugins/libcalendaring/localization/ru_RU.inc b/plugins/libcalendaring/localization/ru_RU.inc
new file mode 100644
index 0000000..0057ebb
--- /dev/null
+++ b/plugins/libcalendaring/localization/ru_RU.inc
@@ -0,0 +1,29 @@
+<?php
+
+$labels = array();
+
+$labels['alarmemail'] = 'Послать e-mail';
+$labels['alarmdisplay'] = 'Показать сообщение';
+$labels['alarmdisplayoption'] = 'Сообщение';
+$labels['alarmemailoption'] = 'Email';
+$labels['alarmat'] = 'на $datetime';
+$labels['trigger@'] = 'на дату';
+$labels['trigger-M'] = 'минут до';
+$labels['trigger-H'] = 'часов до';
+$labels['trigger-D'] = 'дней до';
+$labels['trigger+M'] = 'минут после';
+$labels['trigger+H'] = 'часов после';
+$labels['trigger+D'] = 'дней после';
+$labels['addalarm'] = 'добавить уведомление';
+
+$labels['alarmtitle'] = 'Предстоящие события';
+$labels['dismissall'] = 'Отменить все';
+$labels['dismiss'] = 'Отменить';
+$labels['snooze'] = 'Отложить';
+$labels['repeatinmin'] = 'Повторить через $min minutes';
+$labels['repeatinhr'] = 'Повторить через 1 час';
+$labels['repeatinhrs'] = 'Повторить через $hrs часов';
+$labels['repeattomorrow'] = 'Повторить завтра';
+$labels['repeatinweek'] = 'Повторить через неделю';
+
+$labels['showmore'] = 'Показать больше...';


commit d3a3885e6dabb88a02a9fb890e3f182d2c0131f4
Merge: 758f7a1 4a8a320
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Aug 16 21:22:28 2012 +0200

    Merge branch 'master' of ssh://git.kolabsys.com/git/roundcube



commit 758f7a120f8ca402f95c4c1afc72f8168540005f
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Aug 16 19:25:06 2012 +0200

    Register handler for plugin.display_alarms only once; small code cleanup

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index ce993e5..f835f01 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2625,7 +2625,6 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
   rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true);
 
   // register callback commands
-  rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); });
   rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); });
   rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); });
   rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); });
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index 0d43384..35897d7 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -122,7 +122,7 @@ function rcube_libcalendaring(settings)
         }
 
         // derived from http://delete.me.uk/2005/03/iso8601.html
-        var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
+        var m = s && s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
         if (!m) {
             return null;
         }
@@ -317,7 +317,7 @@ function rcube_libcalendaring(settings)
         this.alarm_dialog = $('<div>').attr('id', 'alarm-display');
 
         var actions, adismiss, asnooze, alarm, html, event_ids = [];
-        for (var actions, html, alarm, i=0; i < alarms.length; i++) {
+        for (var i=0; i < alarms.length; i++) {
             alarm = alarms[i];
             alarm.start = parseISO8601(alarm.start);
             alarm.end = parseISO8601(alarm.end);


commit d919e2b4bdd5709953fbe575f35de266f81a840f
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Aug 16 19:03:17 2012 +0200

    - Move attachment handling functions to libcalendaring module.
    - Removed redundant code in calendar and tasklist
    - Add more vertical space in attachment view window
    - Minor bugfixes

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index b7ad656..9d46086 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -31,7 +31,9 @@ class calendar extends rcube_plugin
   const FREEBUSY_BUSY = 2;
   const FREEBUSY_TENTATIVE = 3;
   const FREEBUSY_OOF = 4;
-  
+
+  const SESSION_KEY = 'calendar_temp';
+
   public $task = '?(?!logout).*';
   public $rc;
   public $lib;
@@ -1193,106 +1195,7 @@ class calendar extends rcube_plugin
    */
   public function attachment_upload()
   {
-    // Upload progress update
-    if (!empty($_GET['_progress'])) {
-      rcube_upload_progress();
-    }
-
-    $event    = get_input_value('_id', RCUBE_INPUT_GPC);
-    $uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
-
-    $eventid = 'cal:'.$event;
-
-    if (!is_array($_SESSION['event_session']) || $_SESSION['event_session']['id'] != $eventid) {
-      $_SESSION['event_session'] = array();
-      $_SESSION['event_session']['id'] = $eventid;
-      $_SESSION['event_session']['attachments'] = array();
-    }
-
-    // clear all stored output properties (like scripts and env vars)
-    $this->rc->output->reset();
-
-    if (is_array($_FILES['_attachments']['tmp_name'])) {
-      foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
-        // Process uploaded attachment if there is no error
-        $err = $_FILES['_attachments']['error'][$i];
-
-        if (!$err) {
-          $attachment = array(
-            'path' => $filepath,
-            'size' => $_FILES['_attachments']['size'][$i],
-            'name' => $_FILES['_attachments']['name'][$i],
-            'mimetype' => rc_mime_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
-            'group' => $eventid,
-          );
-
-          $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
-        }
-
-        if (!$err && $attachment['status'] && !$attachment['abort']) {
-          $id = $attachment['id'];
-
-          // store new attachment in session
-          unset($attachment['status'], $attachment['abort']);
-          $_SESSION['event_session']['attachments'][$id] = $attachment;
-
-          if (($icon = $_SESSION['calendar_deleteicon']) && is_file($icon)) {
-            $button = html::img(array(
-              'src' => $icon,
-              'alt' => rcube_label('delete')
-            ));
-          }
-          else {
-            $button = Q(rcube_label('delete'));
-          }
-
-          $content = html::a(array(
-            'href' => "#delete",
-            'class' => 'delete',
-            'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
-            'title' => rcube_label('delete'),
-          ), $button);
-
-          $content .= Q($attachment['name']);
-
-          $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
-            'html' => $content,
-            'name' => $attachment['name'],
-            'mimetype' => $attachment['mimetype'],
-            'classname' => rcmail_filetype2classname($attachment['mimetype'], $attachment['name']),
-            'complete' => true), $uploadid);
-        }
-        else {  // upload failed
-          if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
-            $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
-                'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
-          }
-          else if ($attachment['error']) {
-            $msg = $attachment['error'];
-          }
-          else {
-            $msg = rcube_label('fileuploaderror');
-          }
-
-          $this->rc->output->command('display_message', $msg, 'error');
-          $this->rc->output->command('remove_from_attachment_list', $uploadid);
-        }
-      }
-    }
-    else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
-      // if filesize exceeds post_max_size then $_FILES array is empty,
-      // show filesizeerror instead of fileuploaderror
-      if ($maxsize = ini_get('post_max_size'))
-        $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
-            'size' => show_bytes(parse_bytes($maxsize)))));
-      else
-        $msg = rcube_label('fileuploaderror');
-
-      $this->rc->output->command('display_message', $msg, 'error');
-      $this->rc->output->command('remove_from_attachment_list', $uploadid);
-    }
-
-    $this->rc->output->send('iframe');
+    $this->lib->attachment_upload(self::SESSION_KEY, 'cal:');
   }
 
   /**
@@ -1300,102 +1203,29 @@ class calendar extends rcube_plugin
    */
   public function attachment_get()
   {
-    $event    = get_input_value('_event', RCUBE_INPUT_GPC);
-    $calendar = get_input_value('_cal', RCUBE_INPUT_GPC);
-    $id       = get_input_value('_id', RCUBE_INPUT_GPC);
-
-    $event = array('id' => $event, 'calendar' => $calendar);
-
     // show loading page
     if (!empty($_GET['_preload'])) {
-      $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
-      $message = rcube_label('loadingdata');
-
-      header('Content-Type: text/html; charset=' . RCMAIL_CHARSET);
-      print "<html>\n<head>\n"
-        . '<meta http-equiv="refresh" content="0; url='.Q($url).'">' . "\n"
-        . '<meta http-equiv="content-type" content="text/html; charset='.RCMAIL_CHARSET.'">' . "\n"
-        . "</head>\n<body>\n$message\n</body>\n</html>";
-      exit;
+        return $this->lib->attachment_loading_page();
     }
 
-    ob_end_clean();
+    $event_id = get_input_value('_event', RCUBE_INPUT_GPC);
+    $calendar = get_input_value('_cal', RCUBE_INPUT_GPC);
+    $id       = get_input_value('_id', RCUBE_INPUT_GPC);
 
-    $attachment = $GLOBALS['calendar_attachment'] = $this->driver->get_attachment($id, $event);
+    $event = array('id' => $event_id, 'calendar' => $calendar);
+    $attachment = $this->driver->get_attachment($id, $event);
 
     // show part page
     if (!empty($_GET['_frame'])) {
-      $this->attachment = $attachment;
-      $this->register_handler('plugin.attachmentframe', array($this, 'attachment_frame'));
-      $this->register_handler('plugin.attachmentcontrols', array($this->ui, 'attachment_controls'));
-      $this->rc->output->send('calendar.attachment');
-      exit;
+        $this->lib->attachment = $attachment;
+        $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
+        $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
+        $this->rc->output->send('calendar.attachment');
     }
-
-    if ($attachment) {
-      // allow post-processing of the attachment body
-      $part = new rcube_message_part;
-      $part->filename  = $attachment['name'];
-      $part->size      = $attachment['size'];
-      $part->mimetype  = $attachment['mimetype'];
-
-      $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
-        'body' => $this->driver->get_attachment_body($id, $event),
-        'mimetype' => strtolower($attachment['mimetype']),
-        'download' => !empty($_GET['_download']),
-        'part' => $part,
-      ));
-
-      if ($plugin['abort'])
-        exit;
-
-      $mimetype = $plugin['mimetype'];
-      list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
-
-      $browser = $this->rc->output->browser;
-
-      // send download headers
-      if ($plugin['download']) {
-        header("Content-Type: application/octet-stream");
-        if ($browser->ie)
-          header("Content-Type: application/force-download");
-      }
-      else if ($ctype_primary == 'text') {
-        header("Content-Type: text/$ctype_secondary");
-      }
-      else {
-//        $mimetype = rcmail_fix_mimetype($mimetype);
-        header("Content-Type: $mimetype");
-        header("Content-Transfer-Encoding: binary");
-      }
-
-      // display page, @TODO: support text/plain (and maybe some other text formats)
-      if ($mimetype == 'text/html' && empty($_GET['_download'])) {
-        $OUTPUT = new rcube_html_page();
-        // @TODO: use washtml on $body
-        $OUTPUT->write($plugin['body']);
-      }
-      else {
-        // don't kill the connection if download takes more than 30 sec.
-        @set_time_limit(0);
-
-        $filename = $attachment['name'];
-        $filename = preg_replace('[\r\n]', '', $filename);
-
-        if ($browser->ie && $browser->ver < 7)
-          $filename = rawurlencode(abbreviate_string($filename, 55));
-        else if ($browser->ie)
-          $filename = rawurlencode($filename);
-        else
-          $filename = addcslashes($filename, '"');
-
-        $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
-        header("Content-Disposition: $disposition; filename=\"$filename\"");
-
-        echo $plugin['body'];
-      }
-
-      exit;
+    // deliver attachment content
+    else if ($attachment) {
+        $attachment['body'] = $this->driver->get_attachment_body($id, $event);
+        $this->lib->attachment_get($attachment);
     }
 
     // if we arrive here, the requested part was not found
@@ -1403,20 +1233,6 @@ class calendar extends rcube_plugin
     exit;
   }
 
-  /**
-   * Template object for attachment display frame
-   */
-  public function attachment_frame($attrib)
-  {
-    $attachment = $GLOBALS['calendar_attachment'];
-
-    $mimetype = strtolower($attachment['mimetype']);
-    list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
-
-    $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
-
-    return html::iframe($attrib);
-  }
 
   /**
    * Prepares new/edited event properties before save
@@ -1432,9 +1248,9 @@ class calendar extends rcube_plugin
 
     $attachments = array();
     $eventid = 'cal:'.$event['id'];
-    if (is_array($_SESSION['event_session']) && $_SESSION['event_session']['id'] == $eventid) {
-      if (!empty($_SESSION['event_session']['attachments'])) {
-        foreach ($_SESSION['event_session']['attachments'] as $id => $attachment) {
+    if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) {
+      if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
+        foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
           if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
             $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
           }
@@ -1474,9 +1290,9 @@ class calendar extends rcube_plugin
   private function cleanup_event(&$event)
   {
     // remove temp. attachment files
-    if (!empty($_SESSION['event_session']) && ($eventid = $_SESSION['event_session']['id'])) {
+    if (!empty($_SESSION[self::SESSION_KEY]) && ($eventid = $_SESSION[self::SESSION_KEY]['id'])) {
       $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
-      $this->rc->session->remove('event_session');
+      $this->rc->session->remove(self::SESSION_KEY);
     }
   }
 
@@ -2089,10 +1905,10 @@ class calendar extends rcube_plugin
       // copy mail attachments to event
       if ($message->attachments) {
         $eventid = 'cal:';
-        if (!is_array($_SESSION['event_session']) || $_SESSION['event_session']['id'] != $eventid) {
-          $_SESSION['event_session'] = array();
-          $_SESSION['event_session']['id'] = $eventid;
-          $_SESSION['event_session']['attachments'] = array();
+        if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $eventid) {
+          $_SESSION[self::SESSION_KEY] = array();
+          $_SESSION[self::SESSION_KEY]['id'] = $eventid;
+          $_SESSION[self::SESSION_KEY]['attachments'] = array();
         }
 
         foreach ((array)$message->attachments as $part) {
@@ -2112,7 +1928,7 @@ class calendar extends rcube_plugin
 
             // store new attachment in session
             unset($attachment['status'], $attachment['abort'], $attachment['data']);
-            $_SESSION['event_session']['attachments'][$id] = $attachment;
+            $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
 
             $attachment['id'] = 'rcmfile' . $attachment['id'];  # add prefix to consider it 'new'
             $event['attachments'][] = $attachment;
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index bdf2250..21ea4ca 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -680,7 +680,7 @@ class kolab_driver extends calendar_driver
 
       foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
         // add to list if alarm is set
-        $alarm = calendarlibcalendaring::get_next_alarm($e);
+        $alarm = libcalendaring::get_next_alarm($e);
         if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') {
           $id = $e['id'];
           $events[$id] = $e;
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 34e0dd3..cfa6684 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -595,7 +595,7 @@ class calendar_ui
 
     $skin_path = $this->cal->local_skin_path();
     if ($attrib['deleteicon']) {
-      $_SESSION['calendar_deleteicon'] = $skin_path . $attrib['deleteicon'];
+      $_SESSION[calendar::SESSION_KEY . '_deleteicon'] = $skin_path . $attrib['deleteicon'];
       $this->rc->output->set_env('deleteicon', $skin_path . $attrib['deleteicon']);
     }
     if ($attrib['cancelicon'])
@@ -608,24 +608,6 @@ class calendar_ui
     return html::tag('ul', $attrib, '', html::$common_attrib);
   }
 
-  function attachment_controls($attrib = array())
-  {
-    $table = new html_table(array('cols' => 3));
-
-    if (!empty($this->cal->attachment['name'])) {
-      $table->add('title', Q(rcube_label('filename')));
-      $table->add('header', Q($this->cal->attachment['name']));
-      $table->add('download-link', html::a('?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING']), Q(rcube_label('download'))));
-    }
-
-    if (!empty($this->cal->attachment['size'])) {
-      $table->add('title', Q(rcube_label('filesize')));
-      $table->add('header', Q(show_bytes($this->cal->attachment['size'])));
-    }
-
-    return $table->show($attrib);
-  }
-
   /**
    * Handler for calendar form template.
    * The form content could be overriden by the driver
diff --git a/plugins/calendar/package.xml b/plugins/calendar/package.xml
index 1dc6625..c422d2a 100644
--- a/plugins/calendar/package.xml
+++ b/plugins/calendar/package.xml
@@ -19,10 +19,10 @@
 		<email>machniak at kolabsys.com</email>
 		<active>yes</active>
 	</developer>
-	<date>2011-11-01</date>
+	<date>2012-12-16</date>
 	<version>
-		<release>0.8</release>
-		<api>0.8</api>
+		<release>0.8-alpha</release>
+		<api>0.9-alpha</api>
 	</version>
 	<stability>
 		<release>stable</release>
@@ -112,7 +112,6 @@
 				<tasks:replace from="@package_version@" to="version" type="package-info"/>
 			</file>
 
-
 			<file name="skins/classic/calendar.css" role="data"></file>
 			<file name="skins/classic/fullcalendar.css" role="data"></file>
 			<file name="skins/classic/fullcalendar.print.css" role="data"></file>
@@ -178,7 +177,7 @@
 	<dependencies>
 		<required>
 			<php>
-				<min>5.2.1</min>
+				<min>5.3.1</min>
 			</php>
 			<pearinstaller>
 				<min>1.7.0</min>
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 1a7325e..76ccecb 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -18,6 +18,14 @@ body.calendarmain #mainscreen {
 	left: 0;
 }
 
+body.attachmentwin #mainscreen {
+	top: 60px;
+}
+
+body.attachmentwin #topnav .topright {
+	margin-top: 20px;
+}
+
 #calendarsidebar {
 	position: absolute;
 	top: 0;
diff --git a/plugins/calendar/skins/larry/templates/attachment.html b/plugins/calendar/skins/larry/templates/attachment.html
index 4d4789d..9879a10 100644
--- a/plugins/calendar/skins/larry/templates/attachment.html
+++ b/plugins/calendar/skins/larry/templates/attachment.html
@@ -4,19 +4,16 @@
 <title><roundcube:object name="pagetitle" /></title>
 <roundcube:include file="/includes/links.html" />
 </head>
-<body class="extwin">
+<body class="extwin attachmentwin">
 
 <div id="header">
-	<div id="topline">
+	<div id="topnav">
+		<roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" />
 		<div class="topright">
 			<a href="#close" class="closelink" onclick="self.close()"><roundcube:label name="close" /></a>
 		</div>
 	</div>
 
-	<div id="topnav">
-		<roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" />
-	</div>
-
 	<br style="clear:both" />
 </div>
 
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index c7cf6e8..2d530a3 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -5,8 +5,9 @@
  *
  * Provides utility functions for calendar-related modules such as
  * - alarms display and dismissal
- * - recurrence computation and UI elements
- * - ical parsing and exporting
+ * - attachment handling
+ * - recurrence computation and UI elements (TODO)
+ * - ical parsing and exporting (TODO)
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
@@ -456,6 +457,246 @@ class libcalendaring extends rcube_plugin
         return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib);
     }
 
+
+    /*********  Attachments handling  *********/
+
+    /**
+     * Handler for attachment uploads
+     */
+    public function attachment_upload($session_key, $id_prefix = '')
+    {
+        // Upload progress update
+        if (!empty($_GET['_progress'])) {
+            rcube_upload_progress();
+        }
+
+        $recid = $id_prefix . get_input_value('_id', RCUBE_INPUT_GPC);
+        $uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
+
+        if (!is_array($_SESSION[$session_key]) || $_SESSION[$session_key]['id'] != $recid) {
+            $_SESSION[$session_key] = array();
+            $_SESSION[$session_key]['id'] = $recid;
+            $_SESSION[$session_key]['attachments'] = array();
+        }
+
+        // clear all stored output properties (like scripts and env vars)
+        $this->rc->output->reset();
+
+        if (is_array($_FILES['_attachments']['tmp_name'])) {
+            foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
+              // Process uploaded attachment if there is no error
+              $err = $_FILES['_attachments']['error'][$i];
+
+              if (!$err) {
+                $attachment = array(
+                    'path' => $filepath,
+                    'size' => $_FILES['_attachments']['size'][$i],
+                    'name' => $_FILES['_attachments']['name'][$i],
+                    'mimetype' => rc_mime_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
+                    'group' => $recid,
+                );
+
+                $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
+              }
+
+              if (!$err && $attachment['status'] && !$attachment['abort']) {
+                  $id = $attachment['id'];
+
+                  // store new attachment in session
+                  unset($attachment['status'], $attachment['abort']);
+                  $_SESSION[$session_key]['attachments'][$id] = $attachment;
+
+                  if (($icon = $_SESSION[$session_key . '_deleteicon']) && is_file($icon)) {
+                      $button = html::img(array(
+                          'src' => $icon,
+                          'alt' => rcube_label('delete')
+                      ));
+                  }
+                  else {
+                      $button = Q(rcube_label('delete'));
+                  }
+
+                  $content = html::a(array(
+                      'href' => "#delete",
+                      'class' => 'delete',
+                      'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
+                      'title' => rcube_label('delete'),
+                  ), $button);
+
+                  $content .= Q($attachment['name']);
+
+                  $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
+                      'html' => $content,
+                      'name' => $attachment['name'],
+                      'mimetype' => $attachment['mimetype'],
+                      'classname' => rcmail_filetype2classname($attachment['mimetype'], $attachment['name']),
+                      'complete' => true), $uploadid);
+              }
+              else {  // upload failed
+                  if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
+                    $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
+                        'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
+                  }
+                  else if ($attachment['error']) {
+                      $msg = $attachment['error'];
+                  }
+                  else {
+                      $msg = rcube_label('fileuploaderror');
+                  }
+
+                  $this->rc->output->command('display_message', $msg, 'error');
+                  $this->rc->output->command('remove_from_attachment_list', $uploadid);
+                }
+            }
+        }
+        else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+            // if filesize exceeds post_max_size then $_FILES array is empty,
+            // show filesizeerror instead of fileuploaderror
+            if ($maxsize = ini_get('post_max_size'))
+                $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
+                    'size' => show_bytes(parse_bytes($maxsize)))));
+            else
+                $msg = rcube_label('fileuploaderror');
+
+            $this->rc->output->command('display_message', $msg, 'error');
+            $this->rc->output->command('remove_from_attachment_list', $uploadid);
+        }
+
+        $this->rc->output->send('iframe');
+    }
+
+
+    /**
+     * Deliver an event/task attachment to the client
+     * (similar as in Roundcube core program/steps/mail/get.inc)
+     */
+    public function attachment_get($attachment)
+    {
+        ob_end_clean();
+
+        if ($attachment && $attachment['body']) {
+            // allow post-processing of the attachment body
+            $part = new rcube_message_part;
+            $part->filename  = $attachment['name'];
+            $part->size      = $attachment['size'];
+            $part->mimetype  = $attachment['mimetype'];
+
+            $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
+                'body'     => $attachment['body'],
+                'mimetype' => strtolower($attachment['mimetype']),
+                'download' => !empty($_GET['_download']),
+                'part'     => $part,
+            ));
+
+            if ($plugin['abort'])
+                exit;
+
+            $mimetype = $plugin['mimetype'];
+            list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+            $browser = $this->rc->output->browser;
+
+            // send download headers
+            if ($plugin['download']) {
+                header("Content-Type: application/octet-stream");
+                if ($browser->ie)
+                    header("Content-Type: application/force-download");
+            }
+            else if ($ctype_primary == 'text') {
+                header("Content-Type: text/$ctype_secondary");
+            }
+            else {
+                header("Content-Type: $mimetype");
+                header("Content-Transfer-Encoding: binary");
+            }
+
+            // display page, @TODO: support text/plain (and maybe some other text formats)
+            if ($mimetype == 'text/html' && empty($_GET['_download'])) {
+                $OUTPUT = new rcube_html_page();
+                // @TODO: use washtml on $body
+                $OUTPUT->write($plugin['body']);
+            }
+            else {
+                // don't kill the connection if download takes more than 30 sec.
+                @set_time_limit(0);
+
+                $filename = $attachment['name'];
+                $filename = preg_replace('[\r\n]', '', $filename);
+
+                if ($browser->ie && $browser->ver < 7)
+                    $filename = rawurlencode(abbreviate_string($filename, 55));
+                else if ($browser->ie)
+                    $filename = rawurlencode($filename);
+                else
+                    $filename = addcslashes($filename, '"');
+
+                $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
+                header("Content-Disposition: $disposition; filename=\"$filename\"");
+
+                echo $plugin['body'];
+            }
+
+            exit;
+        }
+
+        // if we arrive here, the requested part was not found
+        header('HTTP/1.1 404 Not Found');
+        exit;
+    }
+
+    /**
+     * Show "loading..." page in attachment iframe
+     */
+    public function attachment_loading_page()
+    {
+        $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
+        $message = rcube_label('loadingdata');
+
+        header('Content-Type: text/html; charset=' . RCMAIL_CHARSET);
+        print "<html>\n<head>\n"
+            . '<meta http-equiv="refresh" content="0; url='.Q($url).'">' . "\n"
+            . '<meta http-equiv="content-type" content="text/html; charset='.RCMAIL_CHARSET.'">' . "\n"
+            . "</head>\n<body>\n$message\n</body>\n</html>";
+        exit;
+    }
+
+    /**
+     * Template object for attachment display frame
+     */
+    public function attachment_frame($attrib = array())
+    {
+        $mimetype = strtolower($this->attachment['mimetype']);
+        list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
+
+        $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
+
+        return html::iframe($attrib);
+    }
+
+    /**
+     *
+     */
+    public function attachment_header($attrib = array())
+    {
+        $table = new html_table(array('cols' => 3));
+
+        if (!empty($this->attachment['name'])) {
+            $table->add('title', Q(rcube_label('filename')));
+            $table->add('header', Q($this->attachment['name']));
+            $table->add('download-link', html::a('?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING']), Q(rcube_label('download'))));
+        }
+
+        if (!empty($this->attachment['size'])) {
+            $table->add('title', Q(rcube_label('filesize')));
+            $table->add('header', Q(show_bytes($this->attachment['size'])));
+        }
+
+        return $table->show($attrib);
+    }
+
+
+    /*********  Static utility functions  *********/
+
     /**
      * Convert the internal structured data into a vcalendar rrule 2.0 string
      */
diff --git a/plugins/tasklist/localization/de_CH.inc b/plugins/tasklist/localization/de_CH.inc
index 038c05b..a20a6f5 100644
--- a/plugins/tasklist/localization/de_CH.inc
+++ b/plugins/tasklist/localization/de_CH.inc
@@ -17,6 +17,7 @@ $labels['title'] = 'Titel';
 $labels['description'] = 'Beschreibung';
 $labels['datetime'] = 'Datum/Zeit';
 $labels['start'] = 'Beginn';
+$labels['alarms'] = 'Erinnerung';
 
 $labels['all'] = 'Alle';
 $labels['flagged'] = 'Markiert';
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 35bae10..98a8076 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -17,6 +17,7 @@ $labels['title'] = 'Title';
 $labels['description'] = 'Description';
 $labels['datetime'] = 'Date/Time';
 $labels['start'] = 'Start';
+$labels['alarms'] = 'Reminder';
 
 $labels['all'] = 'All';
 $labels['flagged'] = 'Flagged';
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index b502361..8a9bc50 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -29,6 +29,14 @@ div.uidialog {
 	display: none;
 }
 
+body.attachmentwin #mainscreen {
+	top: 60px;
+}
+
+body.attachmentwin #topnav .topright {
+	margin-top: 20px;
+}
+
 #sidebar {
 	position: absolute;
 	top: 0;
diff --git a/plugins/tasklist/skins/larry/templates/attachment.html b/plugins/tasklist/skins/larry/templates/attachment.html
index 4d4789d..9879a10 100644
--- a/plugins/tasklist/skins/larry/templates/attachment.html
+++ b/plugins/tasklist/skins/larry/templates/attachment.html
@@ -4,19 +4,16 @@
 <title><roundcube:object name="pagetitle" /></title>
 <roundcube:include file="/includes/links.html" />
 </head>
-<body class="extwin">
+<body class="extwin attachmentwin">
 
 <div id="header">
-	<div id="topline">
+	<div id="topnav">
+		<roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" />
 		<div class="topright">
 			<a href="#close" class="closelink" onclick="self.close()"><roundcube:label name="close" /></a>
 		</div>
 	</div>
 
-	<div id="topnav">
-		<roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" />
-	</div>
-
 	<br style="clear:both" />
 </div>
 
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index b347a0c..eb98e98 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -113,7 +113,7 @@
 		<span id="task-starttime"></span>
 	</div>
 	<div id="task-alarm" class="form-section">
-		<label><roundcube:label name="calendar.alarms" /></label>
+		<label><roundcube:label name="tasklist.alarms" /></label>
 		<span class="task-text"></span>
 	</div>
 	<div id="task-list" class="form-section">
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index 1773fea..f67d20a 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -32,7 +32,7 @@
 				<a href="#nodate" style="margin-left:1em" class="edit-nodate" rel="#taskedit-startdate,#taskedit-starttime"><roundcube:label name="tasklist.nodate" /></a>
 			</div>
 			<div class="form-section" id="taskedit-alarms">
-				<label for="taskedit-alarm"><roundcube:label name="calendar.alarms" /></label>
+				<label for="taskedit-alarm"><roundcube:label name="tasklist.alarms" /></label>
 				<roundcube:object name="plugin.alarm_select" />
 			</div>
 			<div class="form-section">
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index a2647fb..f01e238 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -33,6 +33,8 @@ class tasklist extends rcube_plugin
     const FILTER_MASK_FLAGGED = 64;
     const FILTER_MASK_COMPLETE = 128;
 
+    const SESSION_KEY = 'tasklist_temp';
+
     public static $filter_masks = array(
         'today'    => self::FILTER_MASK_TODAY,
         'tomorrow' => self::FILTER_MASK_TOMORROW,
@@ -308,9 +310,9 @@ class tasklist extends rcube_plugin
 
         $attachments = array();
         $taskid = $rec['id'];
-        if (is_array($_SESSION['tasklist_session']) && $_SESSION['tasklist_session']['id'] == $taskid) {
-            if (!empty($_SESSION['tasklist_session']['attachments'])) {
-                foreach ($_SESSION['tasklist_session']['attachments'] as $id => $attachment) {
+        if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
+            if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
+                foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
                     if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) {
                         $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
                         unset($attachments[$id]['abort'], $attachments[$id]['group']);
@@ -334,9 +336,9 @@ class tasklist extends rcube_plugin
     private function cleanup_task(&$rec)
     {
         // remove temp. attachment files
-        if (!empty($_SESSION['tasklist_session']) && ($taskid = $_SESSION['tasklist_session']['id'])) {
+        if (!empty($_SESSION[self::SESSION_KEY]) && ($taskid = $_SESSION[self::SESSION_KEY]['id'])) {
             $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $taskid));
-            $this->rc->session->remove('tasklist_session');
+            $this->rc->session->remove(self::SESSION_KEY);
         }
     }
 
@@ -656,104 +658,13 @@ class tasklist extends rcube_plugin
 
 
     /******* Attachment handling  *******/
-    /*** pretty much the same as in plugins/calendar/calendar.php ***/
 
     /**
      * Handler for attachments upload
     */
     public function attachment_upload()
     {
-        // Upload progress update
-        if (!empty($_GET['_progress'])) {
-            rcube_upload_progress();
-        }
-
-        $taskid = get_input_value('_id', RCUBE_INPUT_GPC);
-        $uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
-
-        // prepare session storage
-        if (!is_array($_SESSION['tasklist_session']) || $_SESSION['tasklist_session']['id'] != $taskid) {
-            $_SESSION['tasklist_session'] = array();
-            $_SESSION['tasklist_session']['id'] = $taskid;
-            $_SESSION['tasklist_session']['attachments'] = array();
-        }
-
-        // clear all stored output properties (like scripts and env vars)
-        $this->rc->output->reset();
-
-        if (is_array($_FILES['_attachments']['tmp_name'])) {
-            foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
-                // Process uploaded attachment if there is no error
-                $err = $_FILES['_attachments']['error'][$i];
-
-                if (!$err) {
-                    $attachment = array(
-                        'path' => $filepath,
-                        'size' => $_FILES['_attachments']['size'][$i],
-                        'name' => $_FILES['_attachments']['name'][$i],
-                        'mimetype' => rc_mime_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
-                        'group' => $taskid,
-                    );
-
-                    $attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
-                }
-
-                if (!$err && $attachment['status'] && !$attachment['abort']) {
-                    $id = $attachment['id'];
-
-                    // store new attachment in session
-                    unset($attachment['status'], $attachment['abort']);
-                    $_SESSION['tasklist_session']['attachments'][$id] = $attachment;
-
-                    $content = html::a(array(
-                        'href' => "#delete",
-                        'class' => 'delete',
-                        'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
-                        'title' => rcube_label('delete'),
-                    ), Q(rcube_label('delete')));
-
-                    $content .= Q($attachment['name']);
-
-                    $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
-                        'html' => $content,
-                        'name' => $attachment['name'],
-                        'mimetype' => $attachment['mimetype'],
-                        'classname' => rcmail_filetype2classname($attachment['mimetype'], $attachment['name']),
-                        'complete' => true), $uploadid);
-                }
-                else {  // upload failed
-                    if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
-                        $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
-                            'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
-                    }
-                    else if ($attachment['error']) {
-                        $msg = $attachment['error'];
-                    }
-                    else {
-                        $msg = rcube_label('fileuploaderror');
-                    }
-
-                    $this->rc->output->command('display_message', $msg, 'error');
-                    $this->rc->output->command('remove_from_attachment_list', $uploadid);
-                }
-            }
-        }
-        else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
-            // if filesize exceeds post_max_size then $_FILES array is empty,
-            // show filesizeerror instead of fileuploaderror
-            if ($maxsize = ini_get('post_max_size')) {
-                $msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
-                    'size' => show_bytes(parse_bytes($maxsize)))));
-            }
-            else {
-                $msg = rcube_label('fileuploaderror');
-            }
-
-            $this->rc->output->command('display_message', $msg, 'error');
-            $this->rc->output->command('remove_from_attachment_list', $uploadid);
-        }
-
-        $this->rc->output->send('iframe');
+        $this->lib->attachment_upload(self::SESSION_KEY);
     }
 
     /**
@@ -761,100 +672,29 @@ class tasklist extends rcube_plugin
      */
     public function attachment_get()
     {
-        $task = get_input_value('_t', RCUBE_INPUT_GPC);
-        $list = get_input_value('_list', RCUBE_INPUT_GPC);
-        $id   = get_input_value('_id', RCUBE_INPUT_GPC);
-
-        $task = array('id' => $task, 'list' => $list);
-
         // show loading page
         if (!empty($_GET['_preload'])) {
-          $url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
-          $message = rcube_label('loadingdata');
-
-          header('Content-Type: text/html; charset=' . RCMAIL_CHARSET);
-          print "<html>\n<head>\n"
-              . '<meta http-equiv="refresh" content="0; url='.Q($url).'">' . "\n"
-              . '<meta http-equiv="content-type" content="text/html; charset='.RCMAIL_CHARSET.'">' . "\n"
-              . "</head>\n<body>\n$message\n</body>\n</html>";
-          exit;
+            return $this->lib->attachment_loading_page();
         }
 
-        ob_end_clean();
+        $task = get_input_value('_t', RCUBE_INPUT_GPC);
+        $list = get_input_value('_list', RCUBE_INPUT_GPC);
+        $id   = get_input_value('_id', RCUBE_INPUT_GPC);
 
-        $attachment = $this->attachment = $this->driver->get_attachment($id, $task);
+        $task = array('id' => $task, 'list' => $list);
+        $attachment = $this->driver->get_attachment($id, $task);
 
         // show part page
         if (!empty($_GET['_frame'])) {
-            $this->register_handler('plugin.attachmentframe', array($this, 'attachment_frame'));
-            $this->register_handler('plugin.attachmentcontrols', array($this->ui, 'attachment_controls'));
+            $this->lib->attachment = $attachment;
+            $this->register_handler('plugin.attachmentframe', array($this->lib, 'attachment_frame'));
+            $this->register_handler('plugin.attachmentcontrols', array($this->lib, 'attachment_header'));
             $this->rc->output->send('tasklist.attachment');
-            exit;
         }
-
-        if ($attachment) {
-            // allow post-processing of the attachment body
-            $part = new rcube_message_part;
-            $part->filename  = $attachment['name'];
-            $part->size      = $attachment['size'];
-            $part->mimetype  = $attachment['mimetype'];
-
-            $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
-                'body' => $this->driver->get_attachment_body($id, $task),
-                'mimetype' => strtolower($attachment['mimetype']),
-                'download' => !empty($_GET['_download']),
-                'part' => $part,
-            ));
-
-            if ($plugin['abort'])
-                exit;
-
-            $mimetype = $plugin['mimetype'];
-            list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
-
-            $browser = $this->rc->output->browser;
-
-            // send download headers
-            if ($plugin['download']) {
-                header("Content-Type: application/octet-stream");
-                if ($browser->ie)
-                    header("Content-Type: application/force-download");
-            }
-            else if ($ctype_primary == 'text') {
-                header("Content-Type: text/$ctype_secondary");
-            }
-            else {
-                header("Content-Type: $mimetype");
-                header("Content-Transfer-Encoding: binary");
-            }
-
-            // display page, @TODO: support text/plain (and maybe some other text formats)
-            if ($mimetype == 'text/html' && empty($_GET['_download'])) {
-                $OUTPUT = new rcube_html_page();
-                // @TODO: use washtml on $body
-                $OUTPUT->write($plugin['body']);
-            }
-            else {
-                // don't kill the connection if download takes more than 30 sec.
-                @set_time_limit(0);
-
-                $filename = $attachment['name'];
-                $filename = preg_replace('[\r\n]', '', $filename);
-
-                if ($browser->ie && $browser->ver < 7)
-                    $filename = rawurlencode(abbreviate_string($filename, 55));
-                else if ($browser->ie)
-                    $filename = rawurlencode($filename);
-                else
-                    $filename = addcslashes($filename, '"');
-
-                $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
-                header("Content-Disposition: $disposition; filename=\"$filename\"");
-
-                echo $plugin['body'];
-            }
-
-            exit;
+        // deliver attachment content
+        else if ($attachment) {
+            $attachment['body'] = $this->driver->get_attachment_body($id, $task);
+            $this->lib->attachment_get($attachment);
         }
 
         // if we arrive here, the requested part was not found
@@ -862,21 +702,6 @@ class tasklist extends rcube_plugin
         exit;
     }
 
-    /**
-     * Template object for attachment display frame
-     */
-    public function attachment_frame($attrib)
-    {
-        $attachment = $this->attachment;
-
-        $mimetype = strtolower($attachment['mimetype']);
-        list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
-
-        $attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
-
-        return html::iframe($attrib);
-    }
-
 
     /*******  Email related function *******/
 
@@ -900,10 +725,10 @@ class tasklist extends rcube_plugin
 
             // copy mail attachments to task
             if ($message->attachments && $this->driver->attachments) {
-                if (!is_array($_SESSION['tasklist_session']) || $_SESSION['tasklist_session']['id'] != $task['id']) {
-                    $_SESSION['tasklist_session'] = array();
-                    $_SESSION['tasklist_session']['id'] = $task['id'];
-                    $_SESSION['tasklist_session']['attachments'] = array();
+                if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $task['id']) {
+                    $_SESSION[self::SESSION_KEY] = array();
+                    $_SESSION[self::SESSION_KEY]['id'] = $task['id'];
+                    $_SESSION[self::SESSION_KEY]['attachments'] = array();
                 }
 
                 foreach ((array)$message->attachments as $part) {
@@ -923,7 +748,7 @@ class tasklist extends rcube_plugin
 
                         // store new attachment in session
                         unset($attachment['status'], $attachment['abort'], $attachment['data']);
-                        $_SESSION['tasklist_session']['attachments'][$id] = $attachment;
+                        $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;
 
                         $attachment['id'] = 'rcmfile' . $attachment['id'];  # add prefix to consider it 'new'
                         $task['attachments'][] = $attachment;
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 5ee6c64..a740670 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -277,25 +277,4 @@ class tasklist_ui
         }
     }
 
-    /**
-     *
-     */
-    function attachment_controls($attrib = array())
-    {
-        $table = new html_table(array('cols' => 3));
-
-        if (!empty($this->plugin->attachment['name'])) {
-            $table->add('title', Q(rcube_label('filename')));
-            $table->add('header', Q($this->plugin->attachment['name']));
-            $table->add('download-link', html::a('?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING']), Q(rcube_label('download'))));
-        }
-
-      if (!empty($this->plugin->attachment['size'])) {
-        $table->add('title', Q(rcube_label('filesize')));
-        $table->add('header', Q(show_bytes($this->plugin->attachment['size'])));
-      }
-
-      return $table->show($attrib);
-    }
-
 }


commit 9b2c7953915dd7c722a74aba947a356fd37baa67
Merge: 9cc400f 87d096e
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Aug 16 09:25:56 2012 +0200

    Merge branch 'master' of ssh://git.kolabsys.com/git/roundcube



commit 9cc400f09f928353d6d9125c2edd640232a59d80
Author: Thomas Bruederli <thomas at roundcube.net>
Date:   Thu Aug 16 08:57:25 2012 +0200

    Refactoring: move common calendaring-related functionality into a new plugin 'libcalendaring'

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 722e1cb..3906774 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -34,6 +34,7 @@ class calendar extends rcube_plugin
   
   public $task = '?(?!logout).*';
   public $rc;
+  public $lib;
   public $driver;
   public $home;  // declare public to be used in other classes
   public $urlbase;
@@ -46,29 +47,13 @@ class calendar extends rcube_plugin
 
   public $defaults = array(
     'calendar_default_view' => "agendaWeek",
-    'calendar_date_format'  => "yyyy-MM-dd",
-    'calendar_date_short'   => "M-d",
-    'calendar_date_long'    => "MMM d yyyy",
-    'calendar_date_agenda'  => "ddd MM-dd",
-    'calendar_time_format'  => "HH:mm",
     'calendar_timeslots'    => 2,
-    'calendar_first_day'    => 1,
-    'calendar_first_hour'   => 6,
     'calendar_work_start'   => 6,
     'calendar_work_end'     => 18,
     'calendar_agenda_range' => 60,
     'calendar_agenda_sections' => 'smart',
     'calendar_event_coloring'  => 0,
     'calendar_time_indicator'  => true,
-    'calendar_date_format_sets' => array(
-      'yyyy-MM-dd' => array('MMM d yyyy',   'M-d',  'ddd MM-dd'),
-      'dd-MM-yyyy' => array('d MMM yyyy',   'd-M',  'ddd dd-MM'),
-      'yyyy/MM/dd' => array('MMM d yyyy',   'M/d',  'ddd MM/dd'),
-      'MM/dd/yyyy' => array('MMM d yyyy',   'M/d',  'ddd MM/dd'),
-      'dd/MM/yyyy' => array('d MMM yyyy',   'd/M',  'ddd dd/MM'),
-      'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M',  'ddd dd.MM.'),
-      'd.M.yyyy'   => array('d. MMM yyyy',  'd.M',  'ddd d.MM.'),
-    ),
   );
 
   private $default_categories = array(
@@ -86,7 +71,10 @@ class calendar extends rcube_plugin
    */
   function init()
   {
+    $this->require_plugin('libcalendaring');
+
     $this->rc = rcmail::get_instance();
+    $this->lib = libcalendaring::get_instance();
 
     $this->register_task('calendar', 'calendar');
 
@@ -96,11 +84,9 @@ class calendar extends rcube_plugin
     // load localizations
     $this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print'));
 
-    // set user's timezone
-    $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
-    $now = new DateTime('now', $this->timezone);
-    $this->gmt_offset = $now->getOffset();
-    $this->dst_active = $now->format('I');
+    $this->timezone = $this->lib->timezone;
+    $this->gmt_offset = $this->lib->gmt_offset;
+    $this->dst_active = $this->lib->dst_active;
     $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
 
     require($this->home . '/lib/calendar_ui.php');
@@ -108,8 +94,6 @@ class calendar extends rcube_plugin
 
     // load Calendar user interface which includes jquery-ui
     if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
-      $this->require_plugin('jqueryui');
-
       $this->ui->init();
 
       // settings are required in (almost) every GUI step
@@ -185,8 +169,9 @@ class calendar extends rcube_plugin
       }
     }
     
-    // add hook to display alarms
-    $this->add_hook('keep_alive', array($this, 'keep_alive'));
+    // add hooks to display alarms
+    $this->add_hook('pending_alarms', array($this, 'pending_alarms'));
+    $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
   }
 
   /**
@@ -358,7 +343,7 @@ class calendar extends rcube_plugin
         'content' => $select->show(strval($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']))),
       );
       
-      $time_format = $this->rc->config->get('time_format', self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])));
+      $time_format = $this->rc->config->get('time_format', libcalendaring::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])));
       $select_hours = new html_select();
       for ($h = 0; $h < 24; $h++)
         $select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
@@ -403,7 +388,7 @@ class calendar extends rcube_plugin
         'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))),
         'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')),
       );
-      $preset = self::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
+      $preset = libcalendaring::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
       $p['blocks']['view']['options']['alarmoffset'] = array(
         'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))),
         'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]),
@@ -831,35 +816,38 @@ class calendar extends rcube_plugin
     echo $this->encode($events, !empty($query));
     exit;
   }
-  
+
   /**
-   * Handler for keep-alive requests
+   * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
    * This will check for pending notifications and pass them to the client
    */
-  function keep_alive($attr)
+  public function pending_alarms($p)
   {
-    $timestamp = time();
     $this->load_driver();
-    $alarms = (array)$this->driver->pending_alarms($timestamp);
-    foreach ($alarms as $i => $alarm) {
-        $alarms[$i]['id'] = 'cal:' . $alarm['id'];  // prefix ID with cal:
+    if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) {
+      foreach ($alarms as $i => $alarm) {
+        $alarm['id'] = 'cal:' . $alarm['id'];  // prefix ID with cal:
+        $p['alarms'][] = $alarm;
+      }
     }
 
-    $plugin = $this->rc->plugins->exec_hook('pending_alarms', array(
-      'time' => $timestamp,
-      'alarms' => $alarms,
-    ));
+    return $p;
+  }
 
-    if (!$plugin['abort'] && $plugin['alarms']) {
-      // make sure texts and env vars are available on client
-      if ($this->rc->task != 'calendar') {
-        $this->add_texts('localization/', true);
-        $this->rc->output->set_env('snooze_select', $this->ui->snooze_select());
+  /**
+   * Handler for alarm dismiss hook triggered by libcalendaring
+   */
+  public function dismiss_alarms($p)
+  {
+      $this->load_driver();
+      foreach ((array)$p['ids'] as $id) {
+          if (strpos($id, 'cal:') === 0)
+              $p['success'] |= $this->driver->dismiss_alarm(substr($id, 4), $p['snooze']);
       }
-      $this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms']));
-    }
+
+      return $p;
   }
-  
+
   /**
    * Handler for check-recent requests which are accidentally sent to calendar taks
    */
@@ -1021,20 +1009,16 @@ class calendar extends rcube_plugin
    */
   function load_settings()
   {
-    $this->date_format_defaults();
+    $this->lib->load_settings();
+    $this->defaults += $this->lib->defaults;
+
     $settings = array();
-    
+
     // configuration
     $settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');
     $settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);
-    
-    $settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
-    $settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);
-    $settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', $this->defaults['calendar_date_short']);
-    $settings['date_long']  = (string)$this->rc->config->get('calendar_date_long', $this->defaults['calendar_date_long']);
-    $settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '—' " . $settings['date_long'] . '}';
     $settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']);
-    
+
     $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
     $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
     $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
@@ -1044,39 +1028,6 @@ class calendar extends rcube_plugin
     $settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']);
     $settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
     $settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
-    $settings['timezone'] = $this->timezone_offset;
-    $settings['dst'] = $this->dst_active;
-
-    // localization
-    $settings['days'] = array(
-      rcube_label('sunday'),   rcube_label('monday'),
-      rcube_label('tuesday'),  rcube_label('wednesday'),
-      rcube_label('thursday'), rcube_label('friday'),
-      rcube_label('saturday')
-    );
-    $settings['days_short'] = array(
-      rcube_label('sun'), rcube_label('mon'),
-      rcube_label('tue'), rcube_label('wed'),
-      rcube_label('thu'), rcube_label('fri'),
-      rcube_label('sat')
-    );
-    $settings['months'] = array(
-      $this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
-      $this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
-      $this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
-      $this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
-      $this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
-      $this->rc->gettext('longnov'), $this->rc->gettext('longdec')
-    );
-    $settings['months_short'] = array(
-      $this->rc->gettext('jan'), $this->rc->gettext('feb'),
-      $this->rc->gettext('mar'), $this->rc->gettext('apr'),
-      $this->rc->gettext('may'), $this->rc->gettext('jun'),
-      $this->rc->gettext('jul'), $this->rc->gettext('aug'),
-      $this->rc->gettext('sep'), $this->rc->gettext('oct'),
-      $this->rc->gettext('nov'), $this->rc->gettext('dec')
-    );
-    $settings['today'] = $this->rc->gettext('today');
 
     // get user identity to create default attendee
     if ($this->ui->screen == 'calendar') {
@@ -1089,56 +1040,8 @@ class calendar extends rcube_plugin
       $settings['identity'] = array('name' => $identity['name'], 'email' => $identity['email'], 'emails' => ';' . join(';', $identity['emails']));
     }
 
-    // define list of file types which can be displayed inline
-    // same as in program/steps/mail/show.inc
-    $mimetypes = $this->rc->config->get('client_mimetypes', 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/x-javascript,application/pdf,application/x-shockwave-flash');
-    $settings['mimetypes'] = is_string($mimetypes) ? explode(',', $mimetypes) : (array)$mimetypes;
-
     return $settings;
   }
-  
-  /**
-   * Helper function to set date/time format according to config and user preferences
-   */
-  private function date_format_defaults()
-  {
-    static $defaults = array();
-    
-    // nothing to be done
-    if (isset($defaults['date_format']))
-      return;
-    
-    $defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format')));
-    $defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format')));
-    
-    // override defaults
-    if ($defaults['date_format'])
-      $this->defaults['calendar_date_format'] = $defaults['date_format'];
-    if ($defaults['time_format'])
-      $this->defaults['calendar_time_format'] = $defaults['time_format'];
-    
-    // derive format variants from basic date format
-    $format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
-    if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
-      $this->defaults['calendar_date_long'] = $format_set[0];
-      $this->defaults['calendar_date_short'] = $format_set[1];
-      $this->defaults['calendar_date_agenda'] = $format_set[2];
-    }
-  }
-
-  /**
-   * Shift dates into user's current timezone
-   */
-  private function adjust_timezone($dt)
-  {
-    if (is_numeric($dt))
-      $dt = new DateTime('@'.$td);
-    else if (is_string($dt))
-      $dt = new DateTime($dt);
-
-    $dt->setTimezone($this->timezone);
-    return $dt;
-  }
 
   /**
    * Encode events as JSON
@@ -1163,11 +1066,11 @@ class calendar extends rcube_plugin
   {
     // compose a human readable strings for alarms_text and recurrence_text
     if ($event['alarms'])
-      $event['alarms_text'] = self::alarms_text($event['alarms']);
+      $event['alarms_text'] = libcalendaring::alarms_text($event['alarms']);
     if ($event['recurrence']) {
       $event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
       if ($event['recurrence']['UNTIL'])
-        $event['recurrence']['UNTIL'] = $this->adjust_timezone($event['recurrence']['UNTIL'])->format('c');
+        $event['recurrence']['UNTIL'] = $this->lib->adjust_timezone($event['recurrence']['UNTIL'])->format('c');
     }
 
     foreach ((array)$event['attachments'] as $k => $attachment) {
@@ -1176,8 +1079,8 @@ class calendar extends rcube_plugin
 
     return array(
       '_id'   => $event['calendar'] . ':' . $event['id'],  // unique identifier for fullcalendar
-      'start' => $this->adjust_timezone($event['start'])->format('c'),
-      'end'   => $this->adjust_timezone($event['end'])->format('c'),
+      'start' => $this->lib->adjust_timezone($event['start'])->format('c'),
+      'end'   => $this->lib->adjust_timezone($event['end'])->format('c'),
       'title'       => strval($event['title']),
       'description' => strval($event['description']),
       'location'    => strval($event['location']),
@@ -1188,56 +1091,6 @@ class calendar extends rcube_plugin
 
 
   /**
-   * Generate reduced and streamlined output for pending alarms
-   */
-  private function _alarms_output($alarms)
-  {
-    $out = array();
-    foreach ($alarms as $alarm) {
-      $out[] = array(
-        'id'       => $alarm['id'],
-        'start'    => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '',
-        'end'      => $alarm['end']   ? $this->adjust_timezone($alarm['end'])->format('c') : '',
-        'allDay'   => ($alarm['allday'] == 1)?true:false,
-        'title'    => $alarm['title'],
-        'location' => $alarm['location'],
-        'calendar' => $alarm['calendar'],
-      );
-    }
-    
-    return $out;
-  }
-
-  /**
-   * Render localized text for alarm settings
-   */
-  public static function alarms_text($alarm)
-  {
-    list($trigger, $action) = explode(':', $alarm);
-    
-    $text = '';
-    switch ($action) {
-      case 'EMAIL':
-        $text = rcube_label('calendar.alarmemail');
-        break;
-      case 'DISPLAY':
-        $text = rcube_label('calendar.alarmdisplay');
-        break;
-    }
-    
-    if (preg_match('/@(\d+)/', $trigger, $m)) {
-      $text .= ' ' . rcube_label(array('name' => 'calendar.alarmat', 'vars' => array('datetime' => format_date($m[1]))));
-    }
-    else if ($val = self::parse_alaram_value($trigger)) {
-      $text .= ' ' . intval($val[0]) . ' ' . rcube_label('calendar.trigger' . $val[1]);
-    }
-    else
-      return false;
-    
-    return $text;
-  }
-
-  /**
    * Render localized text describing the recurrence rule of an event
    */
   private function _recurrence_text($rrule)
@@ -1266,7 +1119,7 @@ class calendar extends rcube_plugin
     if ($rrule['COUNT'])
       $until =  $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
     else if ($rrule['UNTIL'])
-      $until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])));
+      $until = $this->gettext('recurrencend') . ' ' . format_date($rrule['UNTIL'], libcalendaring::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])));
     else
       $until = $this->gettext('forever');
     
@@ -1281,149 +1134,8 @@ class calendar extends rcube_plugin
     return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
   }
 
-  /**
-   * Helper function to convert alarm trigger strings
-   * into two-field values (e.g. "-45M" => 45, "-M")
-   */
-  public static function parse_alaram_value($val)
-  {
-    if ($val[0] == '@')
-      return array(substr($val, 1));
-    else if (preg_match('/([+-])(\d+)([HMD])/', $val, $m))
-      return array($m[2], $m[1].$m[3]);
-    
-    return false;
-  }
 
   /**
-   * Get the next alarm (time & action) for the given event
-   *
-   * @param array Event data
-   * @return array Hash array with alarm time/type or null if no alarms are configured
-   */
-  public static function get_next_alarm($event)
-  {
-      if (!$event['alarms'])
-        return null;
-
-      // TODO: handle multiple alarms (currently not supported)
-      list($trigger, $action) = explode(':', $event['alarms'], 2);
-
-      $notify = self::parse_alaram_value($trigger);
-      if (!empty($notify[1])){  // offset
-        $mult = 1;
-        switch ($notify[1]) {
-          case '-S': $mult =     -1; break;
-          case '+S': $mult =      1; break;
-          case '-M': $mult =    -60; break;
-          case '+M': $mult =     60; break;
-          case '-H': $mult =  -3600; break;
-          case '+H': $mult =   3600; break;
-          case '-D': $mult = -86400; break;
-          case '+D': $mult =  86400; break;
-          case '-W': $mult = -604800; break;
-          case '+W': $mult =  604800; break;
-        }
-        $offset = $notify[0] * $mult;
-        $refdate = $mult > 0 ? $event['end'] : $event['start'];
-        $notify_at = $refdate->format('U') + $offset;
-      }
-      else {  // absolute timestamp
-        $notify_at = $notify[0];
-      }
-
-      return array('time' => $notify_at, 'action' => $action ? strtoupper($action) : 'DISPLAY');
-  }
-
-  /**
-   * Convert the internal structured data into a vcalendar rrule 2.0 string
-   */
-  public static function to_rrule($recurrence)
-  {
-    if (is_string($recurrence))
-      return $recurrence;
-    
-    $rrule = '';
-    foreach ((array)$recurrence as $k => $val) {
-      $k = strtoupper($k);
-      switch ($k) {
-        case 'UNTIL':
-          $val = $val->format('Ymd\THis');
-          break;
-        case 'EXDATE':
-          foreach ((array)$val as $i => $ex)
-            $val[$i] = $ex->format('Ymd\THis');
-          $val = join(',', (array)$val);
-          break;
-      }
-      $rrule .= $k . '=' . $val . ';';
-    }
-    
-    return rtrim($rrule, ';');
-  }
-  
-  /**
-   * Convert from fullcalendar date format to PHP date() format string
-   */
-  private static function to_php_date_format($from)
-  {
-    // "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
-    return strtr(strtr($from, array(
-      'yyyy' => 'Y',
-      'yy'   => 'y',
-      'MMMM' => 'F',
-      'MMM'  => 'M',
-      'MM'   => 'm',
-      'M'    => 'n',
-      'dddd' => 'l',
-      'ddd'  => 'D',
-      'dd'   => 'd',
-      'HH'   => '**',
-      'hh'   => '%%',
-      'H'    => 'G',
-      'h'    => 'g',
-      'mm'   => 'i',
-      'ss'   => 's',
-      'TT'   => 'A',
-      'tt'   => 'a',
-      'T'    => 'A',
-      't'    => 'a',
-      'u'    => 'c',
-    )), array(
-      '**'   => 'H',
-      '%%'   => 'h',
-    ));
-  }
-  
-  /**
-   * Convert from PHP date() format to fullcalendar format string
-   */
-  private static function from_php_date_format($from)
-  {
-    // "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
-    return strtr($from, array(
-      'y' => 'yy',
-      'Y' => 'yyyy',
-      'M' => 'MMM',
-      'F' => 'MMMM',
-      'm' => 'MM',
-      'n' => 'M',
-      'd' => 'dd',
-      'D' => 'ddd',
-      'l' => 'dddd',
-      'H' => 'HH',
-      'h' => 'hh',
-      'G' => 'H',
-      'g' => 'h',
-      'i' => 'mm',
-      's' => 'ss',
-      'A' => 'TT',
-      'a' => 'tt',
-      'c' => 'u',
-    ));
-  }
-  
-  /**
    * TEMPORARY: generate random event data for testing
    * Create events by opening http://<roundcubeurl>/?_task=calendar&_action=randomdata&_num=500
    */
@@ -1812,40 +1524,6 @@ class calendar extends rcube_plugin
     
     return $sent;
   }
-  
-  /**
-   * Compose a date string for the given event
-   */
-  public function event_date_text($event, $tzinfo = false)
-  {
-    $fromto = '';
-    $duration = $event['start']->diff($event['end'])->format('s');
-    
-    $this->date_format_defaults();
-    $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
-    $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
-    
-    if ($event['allday']) {
-      $fromto = format_date($event['start'], $date_format);
-      if (($todate = format_date($event['end'], $date_format)) != $fromto)
-        $fromto .= ' - ' . $todate;
-    }
-    else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) {
-      $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
-        ' - ' . format_date($event['end'], $time_format);
-    }
-    else {
-      $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
-        ' - ' . format_date($event['end'], $date_format) . ' ' . format_date($event['end'], $time_format);
-    }
-    
-    // add timezone information
-    if ($tzinfo && ($tzname = $this->timezone->getName())) {
-      $fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
-    }
-    
-    return $fromto;
-  }
 
   /**
    * Echo simple free/busy status text for the given user and time range
diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js
index 48c43b6..0b8c904 100644
--- a/plugins/calendar/calendar_base.js
+++ b/plugins/calendar/calendar_base.js
@@ -25,158 +25,16 @@
 // Basic setup for Roundcube calendar client class
 function rcube_calendar(settings)
 {
+    // extend base class
+    rcube_libcalendaring.call(this, settings);
+
     // member vars
     this.ui;
     this.ui_loaded = false;
-    this.settings = settings;
-    this.alarm_ids = [];
-    this.alarm_dialog = null;
-    this.snooze_popup = null;
-    this.dismiss_link = null;
 
     // private vars
     var me = this;
 
-    // quote html entities
-    var Q = this.quote_html = function(str)
-    {
-      return String(str).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
-    };
-
-    // create a nice human-readable string for the date/time range
-    this.event_date_text = function(event)
-    {
-      if (!event.start)
-        return '';
-      if (!event.end)
-        event.end = event.start;
-
-      var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
-      if (event.allDay) {
-        fromto = $.fullCalendar.formatDate(event.start, settings['date_format'])
-          + (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' — ' + $.fullCalendar.formatDate(event.end, settings['date_format']) : '');
-      }
-      else if (duration < 86400 && event.start.getDay() == event.end.getDay()) {
-        fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format'])
-          + (duration > 0 ? ' — ' + $.fullCalendar.formatDate(event.end, settings['time_format']) : '');
-      }
-      else {
-        fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format'])
-          + (duration > 0 ? ' — ' + $.fullCalendar.formatDate(event.end, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.end, settings['time_format']) : '');
-      }
-
-      return fromto;
-    };
-
-    // display a notification for the given pending alarms
-    this.display_alarms = function(alarms) {
-      // clear old alert first
-      if (this.alarm_dialog)
-        this.alarm_dialog.dialog('destroy');
-      
-      this.alarm_dialog = $('<div>').attr('id', 'alarm-display');
-      
-      var actions, adismiss, asnooze, alarm, html, event_ids = [];
-      for (var actions, html, alarm, i=0; i < alarms.length; i++) {
-        alarm = alarms[i];
-        alarm.start = $.fullCalendar.parseISO8601(alarm.start, true);
-        alarm.end = $.fullCalendar.parseISO8601(alarm.end, true);
-        event_ids.push(alarm.id);
-        
-        html = '<h3 class="event-title">' + Q(alarm.title) + '</h3>';
-        html += '<div class="event-section">' + Q(alarm.location || '') + '</div>';
-        html += '<div class="event-section">' + Q(this.event_date_text(alarm)) + '</div>';
-        
-        adismiss = $('<a href="#" class="alarm-action-dismiss"></a>').html(rcmail.gettext('dismiss','calendar')).click(function(){
-          me.dismiss_link = $(this);
-          me.dismiss_alarm(me.dismiss_link.data('id'), 0);
-        });
-        asnooze = $('<a href="#" class="alarm-action-snooze"></a>').html(rcmail.gettext('snooze','calendar')).click(function(e){
-          me.snooze_dropdown($(this));
-          e.stopPropagation();
-          return false;
-        });
-        actions = $('<div>').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id));
-        
-        $('<div>').addClass('alarm-item').html(html).append(actions).appendTo(this.alarm_dialog);
-      }
-      
-      var buttons = {};
-      buttons[rcmail.gettext('dismissall','calendar')] = function() {
-        // submit dismissed event_ids to server
-        me.dismiss_alarm(me.alarm_ids.join(','), 0);
-        $(this).dialog('close');
-      };
-      
-      this.alarm_dialog.appendTo(document.body).dialog({
-        modal: false,
-        resizable: true,
-        closeOnEscape: false,
-        dialogClass: 'alarm',
-        title: '<span class="ui-icon ui-icon-alert" style="float:left; margin:0 4px 0 0"></span>' + rcmail.gettext('alarmtitle', 'calendar'),
-        buttons: buttons,
-        close: function() {
-          $('#alarm-snooze-dropdown').hide();
-          $(this).dialog('destroy').remove();
-          me.alarm_dialog = null;
-          me.alarm_ids = null;
-        },
-        drag: function(event, ui) {
-          $('#alarm-snooze-dropdown').hide();
-        }
-      });
-      this.alarm_ids = event_ids;
-    };
-
-    // show a drop-down menu with a selection of snooze times
-    this.snooze_dropdown = function(link)
-    {
-      if (!this.snooze_popup) {
-        this.snooze_popup = $('#alarm-snooze-dropdown');
-        // create popup if not found
-        if (!this.snooze_popup.length) {
-          this.snooze_popup = $('<div>').attr('id', 'alarm-snooze-dropdown').addClass('popupmenu').appendTo(document.body);
-          this.snooze_popup.html(rcmail.env.snooze_select)
-        }
-        $('#alarm-snooze-dropdown a').click(function(e){
-          var time = String(this.href).replace(/.+#/, '');
-          me.dismiss_alarm($('#alarm-snooze-dropdown').data('id'), time);
-          return false;
-        });
-      }
-      
-      // hide visible popup
-      if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) {
-        this.snooze_popup.hide();
-        this.dismiss_link = null;
-      }
-      else {  // open popup below the clicked link
-        var pos = link.offset();
-        pos.top += link.height() + 2;
-        this.snooze_popup.data('id', link.data('id')).css({ top:Math.floor(pos.top)+'px', left:Math.floor(pos.left)+'px' }).show();
-        this.dismiss_link = link;
-      }
-    };
-
-    // dismiss or snooze alarms for the given event
-    this.dismiss_alarm = function(id, snooze)
-    {
-      $('#alarm-snooze-dropdown').hide();
-      rcmail.http_post('calendar/event', { action:'dismiss', e:{ id:id, snooze:snooze } });
-      
-      // remove dismissed alarm from list
-      if (this.dismiss_link) {
-        this.dismiss_link.closest('div.alarm-item').hide();
-        var new_ids = jQuery.grep(this.alarm_ids, function(v){ return v != id; });
-        if (new_ids.length)
-          this.alarm_ids = new_ids;
-        else
-          this.alarm_dialog.dialog('close');
-      }
-      
-      this.dismiss_link = null;
-    };
-
     // create new event from current mail message
     this.create_from_mail = function()
     {
@@ -186,6 +44,7 @@ function rcube_calendar(settings)
         if (!this.ui_loaded) {
           $.when(
             $.getScript('./plugins/calendar/calendar_ui.js'),
+            $.getScript('./plugins/calendar/lib/js/fullcalendar.js'),
             $.get(rcmail.url('calendar/inlineui'), function(html){ $(document.body).append(html); }, 'html')
           ).then(function() {
             // register attachments form
@@ -261,24 +120,11 @@ rcube_calendar.fetch_event_rsvp_status = function(event)
 };
 
 
-// extend jQuery
-(function($){
-  $.fn.serializeJSON = function(){
-    var json = {};
-    jQuery.map($(this).serializeArray(), function(n, i) {
-      json[n['name']] = n['value'];
-    });
-    return json;
-  };
-})(jQuery);
-
 /* calendar plugin initialization (for non-calendar tasks) */
 window.rcmail && rcmail.addEventListener('init', function(evt) {
   if (rcmail.task != 'calendar') {
-    var cal = new rcube_calendar(rcmail.env.calendar_settings);
+    var cal = new rcube_calendar($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));
 
-    rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); });
-    
     rcmail.addEventListener('plugin.update_event_rsvp_status', function(p){
       if (p.html)
         $('#loading-'+p.id).hide().after(p.html);
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 1f9d448..ce993e5 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -67,64 +67,18 @@ function rcube_calendar_ui(settings)
       selectOtherMonths: true
     };
 
-
-    /***  private methods  ***/
-
+    /***  imports  ***/
     var Q = this.quote_html;
+    var text2html = this.text2html;
     var event_date_text = this.event_date_text;
+    var parse_datetime = this.parse_datetime;
+    var date2unixtime = this.date2unixtime;
+    var fromunixtime = this.fromunixtime;
+    var init_alarms_edit = this.init_alarms_edit;
 
-    var text2html = function(str, maxlen, maxlines)
-    {
-      var html = Q(String(str));
-      
-      // limit visible text length
-      if (maxlen) {
-        var morelink = ' <a href="#more" onclick="$(this).hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','calendar')+'</a><span style="display:none">',
-          lines = html.split(/\r?\n/),
-          words, out = '', len = 0;
-        
-        for (var i=0; i < lines.length; i++) {
-          len += lines[i].length;
-          if (maxlines && i == maxlines - 1) {
-            out += lines[i] + '\n' + morelink;
-            maxlen = html.length * 2;
-          }
-          else if (len > maxlen) {
-            len = out.length;
-            words = lines[i].split(' ');
-            for (var j=0; j < words.length; j++) {
-              len += words[j].length + 1;
-              out += words[j] + ' ';
-              if (len > maxlen) {
-                out += morelink;
-                maxlen = html.length * 2;
-              }
-            }
-            out += '\n';
-          }
-          else
-            out += lines[i] + '\n';
-        }
-        
-        if (maxlen > str.length)
-          out += '</span>';
-        
-        html = out;
-      }
-      
-      // simple link parser (similar to rcube_string_replacer class in PHP)
-      var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
-      var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
-      var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)?', 'ig');
-      var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
 
-      return html
-        .replace(link_pattern, '<a href="$1$2" target="_blank">$1$2</a>')
-        .replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
-        .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
-        .replace(/\n/g, "<br/>");
-    };
-    
+    /***  private methods  ***/
+
     // same as str.split(delimiter) but it ignores delimiters within quoted strings
     var explode_quoted_string = function(str, delimiter)
     {
@@ -148,25 +102,6 @@ function rcube_calendar_ui(settings)
       return result;
     };
 
-    // from time and date strings to a real date object
-    var parse_datetime = function(time, date)
-    {
-      // we use the utility function from datepicker to parse dates
-      var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date();
-      
-      var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/);
-      if (!isNaN(time_arr[0])) {
-        date.setHours(time_arr[0]);
-        if (time.match(/p[.m]*/i) && date.getHours() < 12)
-          date.setHours(parseInt(time_arr[0]) + 12);
-        else if (time.match(/a[.m]*/i) && date.getHours() == 12)
-          date.setHours(0);
-      }
-      if (!isNaN(time_arr[1]))
-        date.setMinutes(time_arr[1]);
-      
-      return date;
-    };
 
     // clone the given date object and optionally adjust time
     var clone_date = function(date, adjust)
@@ -194,23 +129,6 @@ function rcube_calendar_ui(settings)
           + 'T'+date.getHours()+':'+date.getMinutes()+':'+date.getSeconds();
     }
 
-    // convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
-    var date2unixtime = function(date)
-    {
-      var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;  // adjust DST offset
-      return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset);
-    };
-    
-    var fromunixtime = function(ts)
-    {
-      ts -= gmt_offset * 3600;
-      var date = new Date(ts * 1000),
-        dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;
-      if (dst_offset)  // adjust DST offset
-        date.setTime((ts + 3600) * 1000);
-      return date;
-    };
-    
     // determine whether the given date is on a weekend
     var is_weekend = function(date)
     {
@@ -2561,7 +2479,7 @@ function rcube_calendar_ui(settings)
           }
         }
       });
-      $('#edit-enddate, input.edit-alarm-date').datepicker(datepicker_settings);
+      $('#edit-enddate').datepicker(datepicker_settings);
       $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); });
       $('#edit-enddate').datepicker('option', 'onSelect', event_times_changed).change(event_times_changed);
       $('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); event_times_changed(); });
@@ -2592,14 +2510,7 @@ function rcube_calendar_ui(settings)
         });
 
       // register events on alarm fields
-      $('#eventedit select.edit-alarm-type').change(function(){
-        $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
-      });
-      $('#eventedit select.edit-alarm-offset').change(function(){
-        var mode = $(this).val() == '@' ? 'show' : 'hide';
-        $(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode]();
-        $(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show');
-      });
+      init_alarms_edit('#eventedit');
 
       // toggle recurrence frequency forms
       $('#edit-recurrence-frequency').change(function(e){
@@ -2721,7 +2632,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
   rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
 
   // let's go
-  var cal = new rcube_calendar_ui(rcmail.env.calendar_settings);
+  var cal = new rcube_calendar_ui($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));
 
   $(window).resize(function(e) {
     // check target due to bugs in jquery
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 039276e..6d3a5a8 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -378,7 +378,7 @@ class database_driver extends calendar_driver
     $event['end']->setTimezone($this->server_timezone);
     
     // compose vcalendar-style recurrencue rule from structured data
-    $rrule = $event['recurrence'] ? calendar::to_rrule($event['recurrence']) : '';
+    $rrule = $event['recurrence'] ? libcalendaring::to_rrule($event['recurrence']) : '';
     $event['_recurrence'] = rtrim($rrule, ';');
     $event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
     
@@ -411,7 +411,7 @@ class database_driver extends calendar_driver
   private function _get_notification($event)
   {
     if ($event['alarms'] && $event['start'] > new DateTime()) {
-      $alarm = calendar::get_next_alarm($event);
+      $alarm = libcalendaring::get_next_alarm($event);
 
       if ($alarm['time'] && $alarm['action'] == 'DISPLAY')
         return date('Y-m-d H:i:s', $alarm['time']);
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 9346f82..bdf2250 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -680,7 +680,7 @@ class kolab_driver extends calendar_driver
 
       foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
         // add to list if alarm is set
-        $alarm = calendar::get_next_alarm($e);
+        $alarm = calendarlibcalendaring::get_next_alarm($e);
         if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') {
           $id = $e['id'];
           $events[$id] = $e;
diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php
index 83ff1be..719c1dc 100644
--- a/plugins/calendar/lib/calendar_ical.php
+++ b/plugins/calendar/lib/calendar_ical.php
@@ -124,7 +124,7 @@ class calendar_ical
   public function get_parser()
   {
     // use Horde:iCalendar to parse vcalendar file format
-    @include_once('Horde/iCalendar.php');
+    // @include_once('Horde/iCalendar.php');
 
     if (!class_exists('Horde_iCalendar'))
       require_once($this->cal->home . '/lib/Horde_iCalendar.php');
@@ -377,7 +377,7 @@ class calendar_ical
           $vevent .= "LOCATION:" . self::escpape($event['location']) . self::EOL;
         }
         if ($event['recurrence']) {
-          $vevent .= "RRULE:" . calendar::to_rrule($event['recurrence'], self::EOL) . self::EOL;
+          $vevent .= "RRULE:" . libcalendaring::to_rrule($event['recurrence'], self::EOL) . self::EOL;
         }
         if(!empty($event['categories'])) {
           $vevent .= "CATEGORIES:" . self::escpape(strtoupper($event['categories'])) . self::EOL;
@@ -387,7 +387,7 @@ class calendar_ical
         }
         if ($event['alarms']) {
           list($trigger, $action) = explode(':', $event['alarms']);
-          $val = calendar::parse_alaram_value($trigger);
+          $val = libcalendaring::parse_alaram_value($trigger);
           
           $vevent .= "BEGIN:VALARM\n";
           if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL;
diff --git a/plugins/calendar/lib/calendar_itip.php b/plugins/calendar/lib/calendar_itip.php
index 8008aae..b27c17c 100644
--- a/plugins/calendar/lib/calendar_itip.php
+++ b/plugins/calendar/lib/calendar_itip.php
@@ -77,7 +77,7 @@ class calendar_itip
       'name' => $bodytext,
       'vars' => array(
         'title' => $event['title'],
-        'date' => $this->cal->event_date_text($event, true),
+        'date' => $this->cal->lib->event_date_text($event, true),
         'attendees' => join(', ', $attendees_list),
         'sender' => $this->sender['name'],
         'organizer' => $this->sender['name'],
diff --git a/plugins/calendar/lib/calendar_recurrence.php b/plugins/calendar/lib/calendar_recurrence.php
index 7326717..03831c1 100644
--- a/plugins/calendar/lib/calendar_recurrence.php
+++ b/plugins/calendar/lib/calendar_recurrence.php
@@ -54,7 +54,7 @@ class calendar_recurrence
     require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php');
 
     $this->engine = new Horde_Date_Recurrence($event['start']);
-    $this->engine->fromRRule20(calendar::to_rrule($event['recurrence']));
+    $this->engine->fromRRule20(libcalendaring::to_rrule($event['recurrence']));
 
     if (is_array($event['recurrence']['EXDATE'])) {
       foreach ($event['recurrence']['EXDATE'] as $exdate)
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 1cbcf0a..34e0dd3 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -55,8 +55,7 @@ class calendar_ui
       'label'   => 'calendar.calendar',
       ), 'taskbar');
     
-    // load basic client script (which - unfortunately - requires fullcalendar)
-    $this->cal->include_script('lib/js/fullcalendar.js');
+    // load basic client script
     $this->cal->include_script('calendar_base.js');
     
     $skin_path = $this->cal->local_skin_path();
@@ -78,7 +77,6 @@ class calendar_ui
     $this->cal->register_handler('plugin.priority_select', array($this, 'priority_select'));
     $this->cal->register_handler('plugin.sensitivity_select', array($this, 'sensitivity_select'));
     $this->cal->register_handler('plugin.alarm_select', array($this, 'alarm_select'));
-    $this->cal->register_handler('plugin.snooze_select', array($this, 'snooze_select'));
     $this->cal->register_handler('plugin.recurrence_form', array($this, 'recurrence_form'));
     $this->cal->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
     $this->cal->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
@@ -110,6 +108,7 @@ class calendar_ui
   public function addJS()
   {
     $this->cal->include_script('calendar_ui.js');
+    $this->cal->include_script('lib/js/fullcalendar.js');
     $this->cal->include_script('lib/js/jquery.miniColors.min.js');
   }
 
@@ -326,63 +325,9 @@ class calendar_ui
    */
   function alarm_select($attrib = array())
   {
-    unset($attrib['name']);
-    $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type'));
-    $select_type->add($this->cal->gettext('none'), '');
-    foreach ($this->cal->driver->alarm_types as $type)
-      $select_type->add($this->cal->gettext(strtolower("alarm{$type}option")), $type);
-     
-    $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
-    $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
-    $input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
-    
-    $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
-    foreach (array('-M','-H','-D','+M','+H','+D','@') as $trigger)
-      $select_offset->add($this->cal->gettext('trigger' . $trigger), $trigger);
-     
-    // pre-set with default values from user settings
-    $preset = calendar::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
-    $hidden = array('style' => 'display:none');
-    $html = html::span('edit-alarm-set',
-      $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
-      html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
-        $input_value->show($preset[0]) . ' ' .
-        $select_offset->show($preset[1]) . ' ' .
-        $input_date->show('', $hidden) . ' ' .
-        $input_time->show('', $hidden)
-      )
-    );
-    
-    // TODO: support adding more alarms
-    #$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->cal->gettext('addalarm')),
-    #  $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
-     
-    return $html;
+    return $this->cal->lib->alarm_select($attrib, $this->cal->driver->alarm_types);
   }
 
-  function snooze_select($attrib = array())
-  {
-    $steps = array(
-       5 => 'repeatinmin',
-      10 => 'repeatinmin',
-      15 => 'repeatinmin',
-      20 => 'repeatinmin',
-      30 => 'repeatinmin',
-      60 => 'repeatinhr',
-      120 => 'repeatinhrs',
-      1440 => 'repeattomorrow',
-      10080 => 'repeatinweek',
-    );
-    
-    $items = array();
-    foreach ($steps as $n => $label) {
-      $items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
-        $this->cal->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
-    }
-    
-    return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib);
-  }
-  
   /**
    *
    */
@@ -780,7 +725,7 @@ class calendar_ui
     $table->add('ititle', $title);
     $table->add('title', Q($event['title']));
     $table->add('label', $this->cal->gettext('date'));
-    $table->add('location', Q($this->cal->event_date_text($event)));
+    $table->add('location', Q($this->cal->lib->event_date_text($event)));
     if ($event['location']) {
       $table->add('label', $this->cal->gettext('location'));
       $table->add('location', Q($event['location']));
diff --git a/plugins/calendar/localization/de_CH.inc b/plugins/calendar/localization/de_CH.inc
index 37ace18..12614dd 100644
--- a/plugins/calendar/localization/de_CH.inc
+++ b/plugins/calendar/localization/de_CH.inc
@@ -68,7 +68,6 @@ $labels['parentcalendar'] = 'Erstellen in';
 $labels['searchearlierdates'] = '« Frühere Termine suchen';
 $labels['searchlaterdates'] = 'Spätere Termine suchen »';
 $labels['andnmore'] = '$nr weitere...';
-$labels['showmore'] = 'Mehr anzeigen...';
 $labels['togglerole'] = 'Zum Ändern der Rolle klicken';
 $labels['createfrommail'] = 'Als Termin speichern';
 $labels['importevents'] = 'Termine importieren';
@@ -95,30 +94,8 @@ $labels['futureevents'] = 'Zukünftige';
 
 // alarm/reminder settings
 $labels['showalarms'] = 'Erinnerungen anzeigen';
-$labels['alarmemail'] = 'E-Mail senden';
-$labels['alarmdisplay'] = 'Nachricht anzeigen';
-$labels['alarmdisplayoption'] = 'Nachricht';
-$labels['alarmemailoption'] = 'E-Mail';
-$labels['alarmat'] = 'um $datetime';
-$labels['trigger@'] = 'genau um';
-$labels['trigger-M'] = 'Minuten davor';
-$labels['trigger-H'] = 'Stunden davor';
-$labels['trigger-D'] = 'Tage davor';
-$labels['trigger+M'] = 'Minuten danach';
-$labels['trigger+H'] = 'Stunden danach';
-$labels['trigger+D'] = 'Tage danach';
-$labels['addalarm'] = 'Erinnerung hinzufügen';
 $labels['defaultalarmtype'] = 'Standard-Erinnerungseinstellung';
 $labels['defaultalarmoffset'] = 'Standard-Erinnerungszeit';
-$labels['dismissall'] = 'Alle ignorieren';
-$labels['dismiss'] = 'Ignorieren';
-$labels['snooze'] = 'Später erinnern';
-$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
-$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
-$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
-$labels['repeattomorrow'] = 'Wiederholung morgen';
-$labels['repeatinweek'] = 'Wiederholung in einer Woche';
-$labels['alarmtitle'] = 'Anstehende Termine';
 
 // attendees
 $labels['attendee'] = 'Teilnehmer';
diff --git a/plugins/calendar/localization/de_DE.inc b/plugins/calendar/localization/de_DE.inc
index 0cadf51..7c0d00f 100644
--- a/plugins/calendar/localization/de_DE.inc
+++ b/plugins/calendar/localization/de_DE.inc
@@ -68,7 +68,6 @@ $labels['parentcalendar'] = 'Erstellen in';
 $labels['searchearlierdates'] = '« Frühere Termine suchen';
 $labels['searchlaterdates'] = 'Spätere Termine suchen »';
 $labels['andnmore'] = '$nr weitere...';
-$labels['showmore'] = 'Mehr anzeigen...';
 $labels['togglerole'] = 'Zum Ändern der Rolle klicken';
 $labels['createfrommail'] = 'Als Termin speichern';
 $labels['importevents'] = 'Termine importieren';
@@ -95,30 +94,8 @@ $labels['futureevents'] = 'Zukünftige';
 
 // alarm/reminder settings
 $labels['showalarms'] = 'Erinnerungen anzeigen';
-$labels['alarmemail'] = 'E-Mail senden';
-$labels['alarmdisplay'] = 'Nachricht anzeigen';
-$labels['alarmdisplayoption'] = 'Nachricht';
-$labels['alarmemailoption'] = 'E-Mail';
-$labels['alarmat'] = 'um $datetime';
-$labels['trigger@'] = 'genau um';
-$labels['trigger-M'] = 'Minuten davor';
-$labels['trigger-H'] = 'Stunden davor';
-$labels['trigger-D'] = 'Tage davor';
-$labels['trigger+M'] = 'Minuten danach';
-$labels['trigger+H'] = 'Stunden danach';
-$labels['trigger+D'] = 'Tage danach';
-$labels['addalarm'] = 'Erinnerung hinzufügen';
 $labels['defaultalarmtype'] = 'Standard-Erinnerungseinstellung';
 $labels['defaultalarmoffset'] = 'Standard-Erinnerungszeit';
-$labels['dismissall'] = 'Alle ignorieren';
-$labels['dismiss'] = 'Ignorieren';
-$labels['snooze'] = 'Später erinnern';
-$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
-$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
-$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
-$labels['repeattomorrow'] = 'Wiederholung morgen';
-$labels['repeatinweek'] = 'Wiederholung in einer Woche';
-$labels['alarmtitle'] = 'Anstehende Termine';
 
 // attendees
 $labels['attendee'] = 'Teilnehmer';
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index e13cd88..2137b39 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -68,7 +68,6 @@ $labels['parentcalendar'] = 'Insert inside';
 $labels['searchearlierdates'] = '« Search for earlier events';
 $labels['searchlaterdates'] = 'Search for later events »';
 $labels['andnmore'] = '$nr more...';
-$labels['showmore'] = 'Show more...';
 $labels['togglerole'] = 'Click to toggle role';
 $labels['createfrommail'] = 'Save as event';
 $labels['importevents'] = 'Import events';
@@ -95,30 +94,8 @@ $labels['futureevents'] = 'Future';
 
 // alarm/reminder settings
 $labels['showalarms'] = 'Show alarms';
-$labels['alarmemail'] = 'Send Email';
-$labels['alarmdisplay'] = 'Show message';
-$labels['alarmdisplayoption'] = 'Message';
-$labels['alarmemailoption'] = 'Email';
-$labels['alarmat'] = 'at $datetime';
-$labels['trigger@'] = 'on date';
-$labels['trigger-M'] = 'minutes before';
-$labels['trigger-H'] = 'hours before';
-$labels['trigger-D'] = 'days before';
-$labels['trigger+M'] = 'minutes after';
-$labels['trigger+H'] = 'hours after';
-$labels['trigger+D'] = 'days after';
-$labels['addalarm'] = 'add alarm';
 $labels['defaultalarmtype'] = 'Default reminder setting';
 $labels['defaultalarmoffset'] = 'Default reminder time';
-$labels['dismissall'] = 'Dismiss all';
-$labels['dismiss'] = 'Dismiss';
-$labels['snooze'] = 'Snooze';
-$labels['repeatinmin'] = 'Repeat in $min minutes';
-$labels['repeatinhr'] = 'Repeat in 1 hour';
-$labels['repeatinhrs'] = 'Repeat in $hrs hours';
-$labels['repeattomorrow'] = 'Repeat tomorrow';
-$labels['repeatinweek'] = 'Repeat in a week';
-$labels['alarmtitle'] = 'Upcoming events';
 
 // attendees
 $labels['attendee'] = 'Participant';
diff --git a/plugins/calendar/localization/pl_PL.inc b/plugins/calendar/localization/pl_PL.inc
index 706e999..3403ea5 100644
--- a/plugins/calendar/localization/pl_PL.inc
+++ b/plugins/calendar/localization/pl_PL.inc
@@ -90,30 +90,8 @@ $labels['futureevents'] = 'Przyszłość';
 
 // alarm/reminder settings
 $labels['showalarms'] = 'Wyświetlaj alarmy';
-$labels['alarmemail'] = 'Wyślij pocztę';
-$labels['alarmdisplay'] = 'Pokaż wiadomość';
-$labels['alarmdisplayoption'] = 'Wiadomość';
-$labels['alarmemailoption'] = 'Poczta';
-$labels['alarmat'] = 'o $datetime';
-$labels['trigger@'] = 'w dniu';
-$labels['trigger-M'] = 'minuty przed';
-$labels['trigger-H'] = 'godziny przed';
-$labels['trigger-D'] = 'dni przed';
-$labels['trigger+M'] = 'minut po';
-$labels['trigger+H'] = 'godziny po';
-$labels['trigger+D'] = 'dni po';
-$labels['addalarm'] = 'dodaj alarm';
 $labels['defaultalarmtype'] = 'Domyślne ustawienia przypomnienia';
 $labels['defaultalarmoffset'] = 'Domyślny czas przypomnienia';
-$labels['dismissall'] = 'Odrzuć wszystkie';
-$labels['dismiss'] = 'Odrzuć';
-$labels['snooze'] = 'Odłóż';
-$labels['repeatinmin'] = 'Powtórz po $min minutach';
-$labels['repeatinhr'] = 'Powtórz po godzinie';
-$labels['repeatinhrs'] = 'Powtórz po $hrs godzinach';
-$labels['repeattomorrow'] = 'Powtórz jutro';
-$labels['repeatinweek'] = 'Powtórz za tydzień';
-$labels['alarmtitle'] = 'NadchodzÄ…ce zdarzenia';
 
 // attendees
 $labels['attendee'] = 'Uczestnik';
diff --git a/plugins/calendar/skins/classic/templates/calendar.html b/plugins/calendar/skins/classic/templates/calendar.html
index a22056d..59b880d 100644
--- a/plugins/calendar/skins/classic/templates/calendar.html
+++ b/plugins/calendar/skins/classic/templates/calendar.html
@@ -148,10 +148,6 @@
   <textarea id="calfeedurl" rows="2" readonly="readonly"></textarea>
 </div>
 
-<div id="alarm-snooze-dropdown" class="popupmenu">
-  <roundcube:object name="plugin.snooze_select" type="ul" />
-</div>
-
 <div id="calendartoolbar">
   <roundcube:button command="addevent" type="link" class="buttonPas addevent" classAct="button addevent" classSel="button addeventSel" title="calendar.new_event" content=" " />
   <roundcube:button command="print" type="link" class="buttonPas print" classAct="button print" classSel="button printSel" title="calendar.print" content=" " />
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 6765211..1a7325e 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1,15 +1,13 @@
 /**
  * Roundcube Calendar plugin styles for skin "Larry"
  *
- * Copyright (c) 2012, The Roundcube Dev Team
+ * Copyright (c) 2012, Kolab Systems AG <contact at kolabsys.com>
  * Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
  *
  * The contents are subject to the Creative Commons Attribution-ShareAlike
  * License. It is allowed to copy, distribute, transmit and to adapt the work
  * by keeping credits to the original autors in the README file.
  * See http://creativecommons.org/licenses/by-sa/3.0/ for details.
- *
- * $Id$
  */
 
 body.calendarmain {
@@ -887,10 +885,6 @@ td.topalign {
 	text-align: center;
 }
 
-span.edit-alarm-set {
-	white-space: nowrap;
-}
-
 a.dropdown-link {
 	font-size: 12px;
 	text-decoration: none;
@@ -906,39 +900,6 @@ a.dropdown-link:after {
 	min-height: 24em;
 }
 
-.alarm-item {
-	margin: 0.4em 0 1em 0;
-}
-
-.alarm-item .event-title {
-	font-size: 14px;
-	margin: 0.1em 0 0.3em 0;
-}
-
-.alarm-item div.event-section {
-	margin-top: 0.1em;
-	margin-bottom: 0.3em;
-}
-
-.alarm-item .alarm-actions {
-	margin-top: 0.4em;
-}
-
-.alarm-item div.alarm-actions a {
-	margin-right: 0.8em;
-	text-decoration: none;
-}
-
-a.alarm-action-snooze:after {
-	content: ' â–¼';
-	font-size: 10px;
-	color: #666;
-}
-
-#alarm-snooze-dropdown {
-	z-index: 5000;
-}
-
 .ui-dialog-buttonset a.dropdown-link {
 	margin-right: 1em;
 }
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index ecc6e5f..e4df042 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -161,10 +161,6 @@
 	<textarea id="calfeedurl" rows="2" readonly="readonly"></textarea>
 </div>
 
-<div id="alarm-snooze-dropdown" class="popupmenu">
-	<roundcube:object name="plugin.snooze_select" type="ul" />
-</div>
-
 <roundcube:object name="plugin.calendar_css" />
 
 <script type="text/javascript">
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
new file mode 100644
index 0000000..0d43384
--- /dev/null
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -0,0 +1,443 @@
+/**
+ * Basic Javascript utilities for calendar-related plugins
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+function rcube_libcalendaring(settings)
+{
+    // member vars
+    this.settings = settings;
+    this.alarm_ids = [];
+    this.alarm_dialog = null;
+    this.snooze_popup = null;
+    this.dismiss_link = null;
+
+    // private vars
+    var me = this;
+    var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0);
+    var client_timezone = new Date().getTimezoneOffset();
+
+    // general datepicker settings
+    var datepicker_settings = {
+        // translate from fullcalendar format to datepicker format
+        dateFormat: settings.date_format.replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
+        firstDay : settings.first_day,
+        dayNamesMin: settings.days_short,
+        monthNames: settings.months,
+        monthNamesShort: settings.months,
+        changeMonth: false,
+        showOtherMonths: true,
+        selectOtherMonths: true
+    };
+
+
+    /**
+     * Quote html entities
+     */
+    var Q = this.quote_html = function(str)
+    {
+      return String(str).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
+    };
+
+    /**
+     * Create a nice human-readable string for the date/time range
+     */
+    this.event_date_text = function(event)
+    {
+      if (!event.start)
+        return '';
+      if (!event.end)
+        event.end = event.start;
+
+      var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
+      if (event.allDay) {
+        fromto = this.format_datetime(event.start, 1)
+          + (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' — ' + this.format_datetime(event.end, 1) : '');
+      }
+      else if (duration < 86400 && event.start.getDay() == event.end.getDay()) {
+        fromto = this.format_datetime(event.start, 0)
+          + (duration > 0 ? ' — ' + this.format_datetime(event.end, 2) : '');
+      }
+      else {
+        fromto = this.format_datetime(event.start, 0)
+          + (duration > 0 ? ' — ' + this.format_datetime(event.end, 0) : '');
+      }
+
+      return fromto;
+    };
+
+
+    /**
+     * From time and date strings to a real date object
+     */
+    this.parse_datetime = function(time, date)
+    {
+        // we use the utility function from datepicker to parse dates
+        var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date();
+
+        var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/);
+        if (!isNaN(time_arr[0])) {
+            date.setHours(time_arr[0]);
+        if (time.match(/p[.m]*/i) && date.getHours() < 12)
+            date.setHours(parseInt(time_arr[0]) + 12);
+        else if (time.match(/a[.m]*/i) && date.getHours() == 12)
+            date.setHours(0);
+      }
+      if (!isNaN(time_arr[1]))
+            date.setMinutes(time_arr[1]);
+
+      return date;
+    }
+
+    /**
+     * Convert an ISO 8601 formatted date string from the server into a Date object.
+     * Timezone information will be ignored, the server already provides dates in user's timezone.
+     */
+    function parseISO8601(s)
+    {
+        // force d to be on check's YMD, for daylight savings purposes
+        var fixDate = function(d, check) {
+            if (+d) { // prevent infinite looping on invalid dates
+                while (d.getDate() != check.getDate()) {
+                    d.setTime(+d + (d < check ? 1 : -1) * 3600000);
+                }
+            }
+        }
+
+        // derived from http://delete.me.uk/2005/03/iso8601.html
+        var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
+        if (!m) {
+            return null;
+        }
+
+        var date = new Date(m[1], 0, 1),
+            check = new Date(m[1], 0, 1, 9, 0);
+        if (m[3]) {
+            date.setMonth(m[3] - 1);
+            check.setMonth(m[3] - 1);
+        }
+        if (m[5]) {
+            date.setDate(m[5]);
+            check.setDate(m[5]);
+        }
+        fixDate(date, check);
+        if (m[7]) {
+            date.setHours(m[7]);
+        }
+        if (m[8]) {
+            date.setMinutes(m[8]);
+        }
+        if (m[10]) {
+            date.setSeconds(m[10]);
+        }
+        if (m[12]) {
+            date.setMilliseconds(Number("0." + m[12]) * 1000);
+        }
+        fixDate(date, check);
+
+        return date;
+    }
+
+    /**
+     * Format the given date object according to user's prefs
+     */
+    this.format_datetime = function(date, mode)
+    {
+        var res = '';
+        if (!mode || mode == 1)
+            res += $.datepicker.formatDate(datepicker_settings.dateFormat, date, datepicker_settings);
+        if (!mode)
+            res += ' ';
+        if (!mode || mode == 2)
+            res += this.format_time(date);
+
+        return res;
+    }
+
+    /**
+     * Clone from fullcalendar.js
+     */
+    this.format_time = function(date)
+    {
+        var zeroPad = function(n) { return (n < 10 ? '0' : '') + n; }
+        var formatters = {
+            s   : function(d) { return d.getSeconds() },
+            ss  : function(d) { return zeroPad(d.getSeconds()) },
+            m   : function(d) { return d.getMinutes() },
+            mm  : function(d) { return zeroPad(d.getMinutes()) },
+            h   : function(d) { return d.getHours() % 12 || 12 },
+            hh  : function(d) { return zeroPad(d.getHours() % 12 || 12) },
+            H   : function(d) { return d.getHours() },
+            HH  : function(d) { return zeroPad(d.getHours()) },
+            t   : function(d) { return d.getHours() < 12 ? 'a' : 'p' },
+            tt  : function(d) { return d.getHours() < 12 ? 'am' : 'pm' },
+            T   : function(d) { return d.getHours() < 12 ? 'A' : 'P' },
+            TT  : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }
+        };
+
+        var i, i2, c, formatter, res = '', format = settings['time_format'];
+        for (i=0; i < format.length; i++) {
+            c = format.charAt(i);
+            for (i2=Math.min(i+2, format.length); i2 > i; i2--) {
+                if (formatter = formatters[format.substring(i, i2)]) {
+                    res += formatter(date);
+                    i = i2 - 1;
+                    break;
+                }
+            }
+            if (i2 == i) {
+                res += c;
+            }
+        }
+
+        return res;
+    }
+
+    /**
+     * Convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
+     */
+    this.date2unixtime = function(date)
+    {
+        var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;  // adjust DST offset
+        return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset);
+    }
+
+    /**
+     * Turn a unix timestamp value into a Date object
+     */
+    this.fromunixtime = function(ts)
+    {
+        ts -= gmt_offset * 3600;
+        var date = new Date(ts * 1000),
+            dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;
+        if (dst_offset)  // adjust DST offset
+            date.setTime((ts + 3600) * 1000);
+        return date;
+    }
+
+    /**
+     * Simple plaintext to HTML converter, makig URLs clickable
+     */
+    this.text2html = function(str, maxlen, maxlines)
+    {
+        var html = Q(String(str));
+
+        // limit visible text length
+        if (maxlen) {
+            var morelink = ' <a href="#more" onclick="$(this).hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','libcalendaring')+'</a><span style="display:none">',
+                lines = html.split(/\r?\n/),
+                words, out = '', len = 0;
+
+            for (var i=0; i < lines.length; i++) {
+                len += lines[i].length;
+                if (maxlines && i == maxlines - 1) {
+                    out += lines[i] + '\n' + morelink;
+                    maxlen = html.length * 2;
+                }
+                else if (len > maxlen) {
+                    len = out.length;
+                    words = lines[i].split(' ');
+                    for (var j=0; j < words.length; j++) {
+                        len += words[j].length + 1;
+                        out += words[j] + ' ';
+                        if (len > maxlen) {
+                            out += morelink;
+                            maxlen = html.length * 2;
+                        }
+                    }
+                    out += '\n';
+                }
+                else
+                    out += lines[i] + '\n';
+            }
+
+            if (maxlen > str.length)
+                out += '</span>';
+
+            html = out;
+        }
+
+        // simple link parser (similar to rcube_string_replacer class in PHP)
+        var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
+        var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
+        var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)?', 'ig');
+        var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
+
+        return html
+            .replace(link_pattern, '<a href="$1$2" target="_blank">$1$2</a>')
+            .replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
+            .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
+            .replace(/\n/g, "<br/>");
+    };
+
+    this.init_alarms_edit = function(prefix)
+    {
+        // register events on alarm fields
+        $(prefix+' select.edit-alarm-type').change(function(){
+            $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
+        });
+        $(prefix+' select.edit-alarm-offset').change(function(){
+            var mode = $(this).val() == '@' ? 'show' : 'hide';
+            $(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode]();
+            $(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show');
+        });
+
+        $(prefix+' .edit-alarm-date').datepicker(datepicker_settings);
+    }
+
+
+    /*****  Alarms handling  *****/
+
+    /**
+     * Display a notification for the given pending alarms
+     */
+    this.display_alarms = function(alarms)
+    {
+        // clear old alert first
+        if (this.alarm_dialog)
+            this.alarm_dialog.dialog('destroy');
+
+        this.alarm_dialog = $('<div>').attr('id', 'alarm-display');
+
+        var actions, adismiss, asnooze, alarm, html, event_ids = [];
+        for (var actions, html, alarm, i=0; i < alarms.length; i++) {
+            alarm = alarms[i];
+            alarm.start = parseISO8601(alarm.start);
+            alarm.end = parseISO8601(alarm.end);
+            event_ids.push(alarm.id);
+
+            html = '<h3 class="event-title">' + Q(alarm.title) + '</h3>';
+            html += '<div class="event-section">' + Q(alarm.location || '') + '</div>';
+            html += '<div class="event-section">' + Q(this.event_date_text(alarm)) + '</div>';
+
+            adismiss = $('<a href="#" class="alarm-action-dismiss"></a>').html(rcmail.gettext('dismiss','libcalendaring')).click(function(){
+                me.dismiss_link = $(this);
+                me.dismiss_alarm(me.dismiss_link.data('id'), 0);
+            });
+            asnooze = $('<a href="#" class="alarm-action-snooze"></a>').html(rcmail.gettext('snooze','libcalendaring')).click(function(e){
+                me.snooze_dropdown($(this));
+                e.stopPropagation();
+                return false;
+            });
+            actions = $('<div>').addClass('alarm-actions').append(adismiss.data('id', alarm.id)).append(asnooze.data('id', alarm.id));
+
+            $('<div>').addClass('alarm-item').html(html).append(actions).appendTo(this.alarm_dialog);
+        }
+
+        var buttons = {};
+        buttons[rcmail.gettext('dismissall','libcalendaring')] = function() {
+            // submit dismissed event_ids to server
+            me.dismiss_alarm(me.alarm_ids.join(','), 0);
+            $(this).dialog('close');
+        };
+
+        this.alarm_dialog.appendTo(document.body).dialog({
+            modal: false,
+            resizable: true,
+            closeOnEscape: false,
+            dialogClass: 'alarm',
+            title: '<span class="ui-icon ui-icon-alarms" style="float:left; margin:0 4px 0 0"></span>' + rcmail.gettext('alarmtitle','libcalendaring'),
+            buttons: buttons,
+            close: function() {
+              $('#alarm-snooze-dropdown').hide();
+              $(this).dialog('destroy').remove();
+              me.alarm_dialog = null;
+              me.alarm_ids = null;
+            },
+            drag: function(event, ui) {
+              $('#alarm-snooze-dropdown').hide();
+            }
+        });
+
+        this.alarm_ids = event_ids;
+    };
+
+    /**
+     * Show a drop-down menu with a selection of snooze times
+     */
+    this.snooze_dropdown = function(link)
+    {
+        if (!this.snooze_popup) {
+            this.snooze_popup = $('#alarm-snooze-dropdown');
+            // create popup if not found
+            if (!this.snooze_popup.length) {
+                this.snooze_popup = $('<div>').attr('id', 'alarm-snooze-dropdown').addClass('popupmenu').appendTo(document.body);
+                this.snooze_popup.html(rcmail.env.snooze_select)
+            }
+            $('#alarm-snooze-dropdown a').click(function(e){
+                var time = String(this.href).replace(/.+#/, '');
+                me.dismiss_alarm($('#alarm-snooze-dropdown').data('id'), time);
+                return false;
+            });
+        }
+
+        // hide visible popup
+        if (this.snooze_popup.is(':visible') && this.snooze_popup.data('id') == link.data('id')) {
+            this.snooze_popup.hide();
+            this.dismiss_link = null;
+        }
+        else {  // open popup below the clicked link
+            var pos = link.offset();
+            pos.top += link.height() + 2;
+            this.snooze_popup.data('id', link.data('id')).css({ top:Math.floor(pos.top)+'px', left:Math.floor(pos.left)+'px' }).show();
+            this.dismiss_link = link;
+        }
+    };
+
+    /**
+     * Dismiss or snooze alarms for the given event
+     */
+    this.dismiss_alarm = function(id, snooze)
+    {
+        $('#alarm-snooze-dropdown').hide();
+        rcmail.http_post('utils/plugin.alarms', { action:'dismiss', data:{ id:id, snooze:snooze } });
+
+        // remove dismissed alarm from list
+        if (this.dismiss_link) {
+            this.dismiss_link.closest('div.alarm-item').hide();
+            var new_ids = jQuery.grep(this.alarm_ids, function(v){ return v != id; });
+            if (new_ids.length)
+                this.alarm_ids = new_ids;
+            else
+                this.alarm_dialog.dialog('close');
+        }
+
+        this.dismiss_link = null;
+    };
+}
+
+
+// extend jQuery
+(function($){
+  $.fn.serializeJSON = function(){
+    var json = {};
+    jQuery.map($(this).serializeArray(), function(n, i) {
+      json[n['name']] = n['value'];
+    });
+    return json;
+  };
+})(jQuery);
+
+
+/* libcalendaring plugin initialization */
+window.rcmail && rcmail.addEventListener('init', function(evt) {
+    var libcal = new rcube_libcalendaring(rcmail.env.libcal_settings);
+    rcmail.addEventListener('plugin.display_alarms', function(alarms){ libcal.display_alarms(alarms); });
+});
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
new file mode 100644
index 0000000..c7cf6e8
--- /dev/null
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -0,0 +1,547 @@
+<?php
+
+/**
+ * Library providing common functions for calendaring plugins
+ *
+ * Provides utility functions for calendar-related modules such as
+ * - alarms display and dismissal
+ * - recurrence computation and UI elements
+ * - ical parsing and exporting
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class libcalendaring extends rcube_plugin
+{
+    public $rc;
+    public $timezone;
+    public $gmt_offset;
+    public $dst_active;
+    public $timezone_offset;
+
+    public $defaults = array(
+      'calendar_date_format'  => "yyyy-MM-dd",
+      'calendar_date_short'   => "M-d",
+      'calendar_date_long'    => "MMM d yyyy",
+      'calendar_date_agenda'  => "ddd MM-dd",
+      'calendar_time_format'  => "HH:mm",
+      'calendar_first_day'    => 1,
+      'calendar_first_hour'   => 6,
+      'calendar_date_format_sets' => array(
+        'yyyy-MM-dd' => array('MMM d yyyy',   'M-d',  'ddd MM-dd'),
+        'dd-MM-yyyy' => array('d MMM yyyy',   'd-M',  'ddd dd-MM'),
+        'yyyy/MM/dd' => array('MMM d yyyy',   'M/d',  'ddd MM/dd'),
+        'MM/dd/yyyy' => array('MMM d yyyy',   'M/d',  'ddd MM/dd'),
+        'dd/MM/yyyy' => array('d MMM yyyy',   'd/M',  'ddd dd/MM'),
+        'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M',  'ddd dd.MM.'),
+        'd.M.yyyy'   => array('d. MMM yyyy',  'd.M',  'ddd d.MM.'),
+      ),
+    );
+
+    private static $instance;
+
+    /**
+     * Singleton getter to allow direct access from other plugins
+     */
+    public static function get_instance()
+    {
+        return self::$instance;
+    }
+
+    /**
+     * Required plugin startup method
+     */
+    public function init()
+    {
+        self::$instance = $this;
+
+        $this->rc = rcmail::get_instance();
+
+        // set user's timezone
+        $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
+        $now = new DateTime('now', $this->timezone);
+        $this->gmt_offset = $now->getOffset();
+        $this->dst_active = $now->format('I');
+        $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
+
+        $this->add_texts('localization/', false);
+
+        // include client scripts and styles
+        $this->include_script('libcalendaring.js');
+        $this->rc->output->set_env('libcal_settings', $this->load_settings());
+
+        $this->include_stylesheet($this->local_skin_path() . '/libcal.css');
+
+        // add hook to display alarms
+        $this->add_hook('keep_alive', array($this, 'keep_alive'));
+        $this->register_action('plugin.alarms', array($this, 'alarms_action'));
+    }
+
+
+    /**
+     * Shift dates into user's current timezone
+     *
+     * @param mixed Any kind of a date representation (DateTime object, string or unix timestamp)
+     * @return object DateTime object in user's timezone
+     */
+    public function adjust_timezone($dt)
+    {
+        if (is_numeric($dt))
+            $dt = new DateTime('@'.$td);
+        else if (is_string($dt))
+            $dt = new DateTime($dt);
+
+        $dt->setTimezone($this->timezone);
+        return $dt;
+    }
+
+
+    /**
+     *
+     */
+    public function load_settings()
+    {
+        $this->date_format_defaults();
+        $settings = array();
+
+        // configuration
+        $settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
+        $settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);
+        $settings['date_short']  = (string)$this->rc->config->get('calendar_date_short', $this->defaults['calendar_date_short']);
+        $settings['date_long']   = (string)$this->rc->config->get('calendar_date_long', $this->defaults['calendar_date_long']);
+        $settings['dates_long']  = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '—' " . $settings['date_long'] . '}';
+        $settings['first_day']   = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
+
+        $settings['timezone'] = $this->timezone_offset;
+        $settings['dst'] = $this->dst_active;
+
+        // localization
+        $settings['days'] = array(
+            rcube_label('sunday'),   rcube_label('monday'),
+            rcube_label('tuesday'),  rcube_label('wednesday'),
+            rcube_label('thursday'), rcube_label('friday'),
+            rcube_label('saturday')
+        );
+        $settings['days_short'] = array(
+            rcube_label('sun'), rcube_label('mon'),
+            rcube_label('tue'), rcube_label('wed'),
+            rcube_label('thu'), rcube_label('fri'),
+            rcube_label('sat')
+        );
+        $settings['months'] = array(
+            $this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
+            $this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
+            $this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
+            $this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
+            $this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
+            $this->rc->gettext('longnov'), $this->rc->gettext('longdec')
+        );
+        $settings['months_short'] = array(
+            $this->rc->gettext('jan'), $this->rc->gettext('feb'),
+            $this->rc->gettext('mar'), $this->rc->gettext('apr'),
+            $this->rc->gettext('may'), $this->rc->gettext('jun'),
+            $this->rc->gettext('jul'), $this->rc->gettext('aug'),
+            $this->rc->gettext('sep'), $this->rc->gettext('oct'),
+            $this->rc->gettext('nov'), $this->rc->gettext('dec')
+        );
+        $settings['today'] = $this->rc->gettext('today');
+
+        // define list of file types which can be displayed inline
+        // same as in program/steps/mail/show.inc
+        $mimetypes = $this->rc->config->get('client_mimetypes', 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/x-javascript,application/pdf,application/x-shockwave-flash');
+        $settings['mimetypes'] = is_string($mimetypes) ? explode(',', $mimetypes) : (array)$mimetypes;
+
+        return $settings;
+    }
+
+
+    /**
+     * Helper function to set date/time format according to config and user preferences
+     */
+    private function date_format_defaults()
+    {
+        static $defaults = array();
+
+        // nothing to be done
+        if (isset($defaults['date_format']))
+          return;
+
+        $defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format')));
+        $defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format')));
+
+        // override defaults
+        if ($defaults['date_format'])
+            $this->defaults['calendar_date_format'] = $defaults['date_format'];
+        if ($defaults['time_format'])
+            $this->defaults['calendar_time_format'] = $defaults['time_format'];
+
+        // derive format variants from basic date format
+        $format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
+        if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
+            $this->defaults['calendar_date_long'] = $format_set[0];
+            $this->defaults['calendar_date_short'] = $format_set[1];
+            $this->defaults['calendar_date_agenda'] = $format_set[2];
+        }
+    }
+
+    /**
+     * Compose a date string for the given event
+     */
+    public function event_date_text($event, $tzinfo = false)
+    {
+        $fromto = '';
+        $duration = $event['start']->diff($event['end'])->format('s');
+
+        $this->date_format_defaults();
+        $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
+        $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
+
+        if ($event['allday']) {
+            $fromto = format_date($event['start'], $date_format);
+            if (($todate = format_date($event['end'], $date_format)) != $fromto)
+                $fromto .= ' - ' . $todate;
+        }
+        else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) {
+            $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
+                ' - ' . format_date($event['end'], $time_format);
+        }
+        else {
+            $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
+                ' - ' . format_date($event['end'], $date_format) . ' ' . format_date($event['end'], $time_format);
+        }
+
+        // add timezone information
+        if ($tzinfo && ($tzname = $this->timezone->getName())) {
+            $fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
+        }
+
+        return $fromto;
+    }
+
+
+    /**
+     * Render HTML form for alarm configuration
+     */
+    public function alarm_select($attrib, $alarm_types)
+    {
+        unset($attrib['name']);
+        $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type'));
+        $select_type->add($this->gettext('none'), '');
+        foreach ($alarm_types as $type)
+            $select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
+
+        $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
+        $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
+        $input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
+
+        $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
+        foreach (array('-M','-H','-D','+M','+H','+D','@') as $trigger)
+            $select_offset->add($this->gettext('trigger' . $trigger), $trigger);
+
+        // pre-set with default values from user settings
+        $preset = self::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
+        $hidden = array('style' => 'display:none');
+        $html = html::span('edit-alarm-set',
+            $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
+            html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
+                $input_value->show($preset[0]) . ' ' .
+                $select_offset->show($preset[1]) . ' ' .
+                $input_date->show('', $hidden) . ' ' .
+                $input_time->show('', $hidden)
+            )
+        );
+
+        // TODO: support adding more alarms
+        #$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->gettext('addalarm')),
+        #  $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
+
+        return $html;
+    }
+
+
+    /*********  Alarms handling  *********/
+
+    /**
+     * Helper function to convert alarm trigger strings
+     * into two-field values (e.g. "-45M" => 45, "-M")
+     */
+    public static function parse_alaram_value($val)
+    {
+        if ($val[0] == '@')
+            return array(substr($val, 1));
+        else if (preg_match('/([+-])(\d+)([HMD])/', $val, $m))
+            return array($m[2], $m[1].$m[3]);
+
+        return false;
+    }
+
+    /**
+     * Render localized text for alarm settings
+     */
+    public static function alarms_text($alarm)
+    {
+        list($trigger, $action) = explode(':', $alarm);
+
+        $text = '';
+        switch ($action) {
+        case 'EMAIL':
+            $text = rcube_label('libcalendaring.alarmemail');
+            break;
+        case 'DISPLAY':
+            $text = rcube_label('libcalendaring.alarmdisplay');
+            break;
+        }
+
+        if (preg_match('/@(\d+)/', $trigger, $m)) {
+            $text .= ' ' . rcube_label(array('name' => 'libcalendaring.alarmat', 'vars' => array('datetime' => format_date($m[1]))));
+        }
+        else if ($val = self::parse_alaram_value($trigger)) {
+            $text .= ' ' . intval($val[0]) . ' ' . rcube_label('libcalendaring.trigger' . $val[1]);
+        }
+        else
+            return false;
+
+        return $text;
+    }
+
+    /**
+     * Get the next alarm (time & action) for the given event
+     *
+     * @param array Record data
+     * @return array Hash array with alarm time/type or null if no alarms are configured
+     */
+    public static function get_next_alarm($rec, $type = 'event')
+    {
+        if (!$rec['alarms'])
+            return null;
+
+        if ($type == 'task') {
+            $timezone = self::get_instance()->timezone;
+            if ($rec['date'])
+                $rec['start'] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone);
+            if ($rec['startdate'])
+                $rec['end'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone);
+        }
+
+        if (!$rec['end'])
+            $rec['end'] = $rec['start'];
+
+
+        // TODO: handle multiple alarms (currently not supported)
+        list($trigger, $action) = explode(':', $rec['alarms'], 2);
+
+        $notify = self::parse_alaram_value($trigger);
+        if (!empty($notify[1])){  // offset
+            $mult = 1;
+            switch ($notify[1]) {
+                case '-S': $mult =     -1; break;
+                case '+S': $mult =      1; break;
+                case '-M': $mult =    -60; break;
+                case '+M': $mult =     60; break;
+                case '-H': $mult =  -3600; break;
+                case '+H': $mult =   3600; break;
+                case '-D': $mult = -86400; break;
+                case '+D': $mult =  86400; break;
+                case '-W': $mult = -604800; break;
+                case '+W': $mult =  604800; break;
+            }
+            $offset = $notify[0] * $mult;
+            $refdate = $mult > 0 ? $rec['end'] : $rec['start'];
+            $notify_at = $refdate->format('U') + $offset;
+        }
+        else {  // absolute timestamp
+            $notify_at = $notify[0];
+        }
+
+        return array('time' => $notify_at, 'action' => $action ? strtoupper($action) : 'DISPLAY');
+    }
+
+    /**
+     * Handler for keep-alive requests
+     * This will check for pending notifications and pass them to the client
+     */
+    public function keep_alive($attr)
+    {
+        // collect pending alarms from all providers (e.g. calendar, tasks)
+        $plugin = $this->rc->plugins->exec_hook('pending_alarms', array(
+            'time' => time(),
+            'alarms' => $alarms,
+        ));
+
+        if (!$plugin['abort'] && $plugin['alarms']) {
+            // make sure texts and env vars are available on client
+            $this->add_texts('localization/', true);
+            $this->rc->output->set_env('snooze_select', $this->snooze_select());
+            $this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms']));
+        }
+    }
+
+    /**
+     * Handler for alarm dismiss/snooze requests
+     */
+    public function alarms_action()
+    {
+        $action = get_input_value('action', RCUBE_INPUT_GPC);
+        $data  = get_input_value('data', RCUBE_INPUT_POST, true);
+
+        $data['ids'] = explode(',', $data['id']);
+        $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data);
+
+        if ($plugin['success'])
+            $this->rc->output->show_message('successfullysaved', 'confirmation');
+        else
+            $this->rc->output->show_message('calendar.errorsaving', 'error');
+    }
+
+    /**
+     * Generate reduced and streamlined output for pending alarms
+     */
+    private function _alarms_output($alarms)
+    {
+        $out = array();
+        foreach ($alarms as $alarm) {
+            $out[] = array(
+                'id'       => $alarm['id'],
+                'start'    => $alarm['start'] ? $this->adjust_timezone($alarm['start'])->format('c') : '',
+                'end'      => $alarm['end']   ? $this->adjust_timezone($alarm['end'])->format('c') : '',
+                'allDay'   => ($alarm['allday'] == 1)?true:false,
+                'title'    => $alarm['title'],
+                'location' => $alarm['location'],
+                'calendar' => $alarm['calendar'],
+            );
+        }
+
+        return $out;
+    }
+
+    /**
+     * Render a dropdown menu to choose snooze time
+     */
+    private function snooze_select($attrib = array())
+    {
+        $steps = array(
+             5 => 'repeatinmin',
+            10 => 'repeatinmin',
+            15 => 'repeatinmin',
+            20 => 'repeatinmin',
+            30 => 'repeatinmin',
+            60 => 'repeatinhr',
+            120 => 'repeatinhrs',
+            1440 => 'repeattomorrow',
+            10080 => 'repeatinweek',
+        );
+
+        $items = array();
+        foreach ($steps as $n => $label) {
+            $items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
+                $this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
+        }
+
+        return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib);
+    }
+
+    /**
+     * Convert the internal structured data into a vcalendar rrule 2.0 string
+     */
+    public static function to_rrule($recurrence)
+    {
+        if (is_string($recurrence))
+            return $recurrence;
+
+        $rrule = '';
+        foreach ((array)$recurrence as $k => $val) {
+            $k = strtoupper($k);
+            switch ($k) {
+            case 'UNTIL':
+                $val = $val->format('Ymd\THis');
+                break;
+            case 'EXDATE':
+                foreach ((array)$val as $i => $ex)
+                    $val[$i] = $ex->format('Ymd\THis');
+                $val = join(',', (array)$val);
+                break;
+            }
+            $rrule .= $k . '=' . $val . ';';
+        }
+
+        return rtrim($rrule, ';');
+    }
+
+    /**
+     * Convert from fullcalendar date format to PHP date() format string
+     */
+    public static function to_php_date_format($from)
+    {
+        // "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
+        return strtr(strtr($from, array(
+            'yyyy' => 'Y',
+            'yy'   => 'y',
+            'MMMM' => 'F',
+            'MMM'  => 'M',
+            'MM'   => 'm',
+            'M'    => 'n',
+            'dddd' => 'l',
+            'ddd'  => 'D',
+            'dd'   => 'd',
+            'HH'   => '**',
+            'hh'   => '%%',
+            'H'    => 'G',
+            'h'    => 'g',
+            'mm'   => 'i',
+            'ss'   => 's',
+            'TT'   => 'A',
+            'tt'   => 'a',
+            'T'    => 'A',
+            't'    => 'a',
+            'u'    => 'c',
+        )), array(
+            '**'   => 'H',
+            '%%'   => 'h',
+        ));
+    }
+
+    /**
+     * Convert from PHP date() format to fullcalendar format string
+     */
+    public static function from_php_date_format($from)
+    {
+        // "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
+        return strtr($from, array(
+            'y' => 'yy',
+            'Y' => 'yyyy',
+            'M' => 'MMM',
+            'F' => 'MMMM',
+            'm' => 'MM',
+            'n' => 'M',
+            'd' => 'dd',
+            'D' => 'ddd',
+            'l' => 'dddd',
+            'H' => 'HH',
+            'h' => 'hh',
+            'G' => 'H',
+            'g' => 'h',
+            'i' => 'mm',
+            's' => 'ss',
+            'A' => 'TT',
+            'a' => 'tt',
+            'c' => 'u',
+        ));
+    }
+
+}
diff --git a/plugins/libcalendaring/localization/de_CH.inc b/plugins/libcalendaring/localization/de_CH.inc
new file mode 100644
index 0000000..3760931
--- /dev/null
+++ b/plugins/libcalendaring/localization/de_CH.inc
@@ -0,0 +1,29 @@
+<?php
+
+$labels = array();
+
+$labels['alarmemail'] = 'E-Mail senden';
+$labels['alarmdisplay'] = 'Nachricht anzeigen';
+$labels['alarmdisplayoption'] = 'Nachricht';
+$labels['alarmemailoption'] = 'E-Mail';
+$labels['alarmat'] = 'um $datetime';
+$labels['trigger@'] = 'genau um';
+$labels['trigger-M'] = 'Minuten davor';
+$labels['trigger-H'] = 'Stunden davor';
+$labels['trigger-D'] = 'Tage davor';
+$labels['trigger+M'] = 'Minuten danach';
+$labels['trigger+H'] = 'Stunden danach';
+$labels['trigger+D'] = 'Tage danach';
+$labels['addalarm'] = 'Erinnerung hinzufügen';
+
+$labels['alarmtitle'] = 'Anstehende Termine';
+$labels['dismissall'] = 'Alle ignorieren';
+$labels['dismiss'] = 'Ignorieren';
+$labels['snooze'] = 'Später erinnern';
+$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
+$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
+$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
+$labels['repeattomorrow'] = 'Wiederholung morgen';
+$labels['repeatinweek'] = 'Wiederholung in einer Woche';
+
+$labels['showmore'] = 'Mehr anzeigen...';
diff --git a/plugins/libcalendaring/localization/de_DE.inc b/plugins/libcalendaring/localization/de_DE.inc
new file mode 100644
index 0000000..3760931
--- /dev/null
+++ b/plugins/libcalendaring/localization/de_DE.inc
@@ -0,0 +1,29 @@
+<?php
+
+$labels = array();
+
+$labels['alarmemail'] = 'E-Mail senden';
+$labels['alarmdisplay'] = 'Nachricht anzeigen';
+$labels['alarmdisplayoption'] = 'Nachricht';
+$labels['alarmemailoption'] = 'E-Mail';
+$labels['alarmat'] = 'um $datetime';
+$labels['trigger@'] = 'genau um';
+$labels['trigger-M'] = 'Minuten davor';
+$labels['trigger-H'] = 'Stunden davor';
+$labels['trigger-D'] = 'Tage davor';
+$labels['trigger+M'] = 'Minuten danach';
+$labels['trigger+H'] = 'Stunden danach';
+$labels['trigger+D'] = 'Tage danach';
+$labels['addalarm'] = 'Erinnerung hinzufügen';
+
+$labels['alarmtitle'] = 'Anstehende Termine';
+$labels['dismissall'] = 'Alle ignorieren';
+$labels['dismiss'] = 'Ignorieren';
+$labels['snooze'] = 'Später erinnern';
+$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
+$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
+$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
+$labels['repeattomorrow'] = 'Wiederholung morgen';
+$labels['repeatinweek'] = 'Wiederholung in einer Woche';
+
+$labels['showmore'] = 'Mehr anzeigen...';
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
new file mode 100644
index 0000000..4a4eee2
--- /dev/null
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -0,0 +1,29 @@
+<?php
+
+$labels = array();
+
+$labels['alarmemail'] = 'Send Email';
+$labels['alarmdisplay'] = 'Show message';
+$labels['alarmdisplayoption'] = 'Message';
+$labels['alarmemailoption'] = 'Email';
+$labels['alarmat'] = 'at $datetime';
+$labels['trigger@'] = 'on date';
+$labels['trigger-M'] = 'minutes before';
+$labels['trigger-H'] = 'hours before';
+$labels['trigger-D'] = 'days before';
+$labels['trigger+M'] = 'minutes after';
+$labels['trigger+H'] = 'hours after';
+$labels['trigger+D'] = 'days after';
+$labels['addalarm'] = 'add alarm';
+$labels['alarmtitle'] = 'Upcoming events';
+$labels['dismissall'] = 'Dismiss all';
+$labels['dismiss'] = 'Dismiss';
+$labels['snooze'] = 'Snooze';
+$labels['repeatinmin'] = 'Repeat in $min minutes';
+$labels['repeatinhr'] = 'Repeat in 1 hour';
+$labels['repeatinhrs'] = 'Repeat in $hrs hours';
+$labels['repeattomorrow'] = 'Repeat tomorrow';
+$labels['repeatinweek'] = 'Repeat in a week';
+
+$labels['showmore'] = 'Show more...';
+
diff --git a/plugins/libcalendaring/localization/pl_PL.inc b/plugins/libcalendaring/localization/pl_PL.inc
new file mode 100644
index 0000000..4b9c11a
--- /dev/null
+++ b/plugins/libcalendaring/localization/pl_PL.inc
@@ -0,0 +1,28 @@
+<?php
+
+$labels = array();
+
+$labels['alarmemail'] = 'Wyślij pocztę';
+$labels['alarmdisplay'] = 'Pokaż wiadomość';
+$labels['alarmdisplayoption'] = 'Wiadomość';
+$labels['alarmemailoption'] = 'Poczta';
+$labels['alarmat'] = 'o $datetime';
+$labels['trigger@'] = 'w dniu';
+$labels['trigger-M'] = 'minuty przed';
+$labels['trigger-H'] = 'godziny przed';
+$labels['trigger-D'] = 'dni przed';
+$labels['trigger+M'] = 'minut po';
+$labels['trigger+H'] = 'godziny po';
+$labels['trigger+D'] = 'dni po';
+$labels['addalarm'] = 'dodaj alarm';
+
+$labels['alarmtitle'] = 'NadchodzÄ…ce zdarzenia';
+$labels['dismissall'] = 'Odrzuć wszystkie';
+$labels['dismiss'] = 'Odrzuć';
+$labels['snooze'] = 'Odłóż';
+$labels['repeatinmin'] = 'Powtórz po $min minutach';
+$labels['repeatinhr'] = 'Powtórz po godzinie';
+$labels['repeatinhrs'] = 'Powtórz po $hrs godzinach';
+$labels['repeattomorrow'] = 'Powtórz jutro';
+$labels['repeatinweek'] = 'Powtórz za tydzień';
+
diff --git a/plugins/libcalendaring/skins/larry/libcal.css b/plugins/libcalendaring/skins/larry/libcal.css
new file mode 100644
index 0000000..1e3a4c8
--- /dev/null
+++ b/plugins/libcalendaring/skins/larry/libcal.css
@@ -0,0 +1,54 @@
+/**
+ * Roundcube libcalendaring plugin styles for skin "Larry"
+ *
+ * Copyright (c) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * The contents are subject to the Creative Commons Attribution-ShareAlike
+ * License. It is allowed to copy, distribute, transmit and to adapt the work
+ * by keeping credits to the original autors in the README file.
+ * See http://creativecommons.org/licenses/by-sa/3.0/ for details.
+ */
+
+.alarm-item {
+	margin: 0.4em 0 1em 0;
+}
+
+.alarm-item .event-title {
+	font-size: 14px;
+	margin: 0.1em 0 0.3em 0;
+}
+
+.alarm-item div.event-section {
+	margin-top: 0.1em;
+	margin-bottom: 0.3em;
+}
+
+.alarm-item .alarm-actions {
+	margin-top: 0.4em;
+}
+
+.alarm-item div.alarm-actions a {
+	margin-right: 0.8em;
+	text-decoration: none;
+}
+
+a.alarm-action-snooze:after {
+	content: ' â–¼';
+	font-size: 10px;
+	color: #666;
+}
+
+#alarm-snooze-dropdown {
+	z-index: 5000;
+}
+
+span.edit-alarm-set {
+	white-space: nowrap;
+}
+
+.ui-widget-header .ui-dialog-title .ui-icon-alarms {
+	background-image: url(../../../../skins/larry/images/messages.png);
+	background-position: 0 -91px;
+	width: 20px;
+	height: 16px;
+}
diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
index e2c9687..3cbddb3 100644
--- a/plugins/tasklist/drivers/database/tasklist_database_driver.php
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -365,9 +365,8 @@ class tasklist_database_driver extends tasklist_driver
             $result = $this->rc->db->query(sprintf(
                 "SELECT * FROM " . $this->db_tasks . "
                  WHERE tasklist_id IN (%s)
-                 AND notify <= %s AND date > %s",
+                 AND notify <= %s AND complete < 1",
                 join(',', $list_ids),
-                $this->rc->db->fromunixtime($time),
                 $this->rc->db->fromunixtime($time)
             ));
 
@@ -575,20 +574,8 @@ class tasklist_database_driver extends tasklist_driver
      */
     private function _get_notification($task)
     {
-        // fake object properties to suit the expectations of calendar::get_next_alarm()
-        // TODO: move all that to libcalendaring plugin
-        if ($task['date'])
-            $task['start'] = new DateTime($task['date'] . ' ' . ($task['time'] ?: '12:00'), $this->plugin->timezone);
-        if ($task['startdate'])
-            $task['end'] = new DateTime($task['startdate'] . ' ' . ($task['starttime'] ?: '12:00'), $this->plugin->timezone);
-        else
-            $task['end'] = $tast['start'];
-
-        if (!$task['start'])
-            $task['end'] = $task['start'];
-
-        if ($task['alarms'] && $task['start'] > new DateTime() || strpos($task['alarms'], '@') !== false) {
-            $alarm = calendar::get_next_alarm($task);
+        if ($task['alarms'] && $task['complete'] < 1 || strpos($task['alarms'], '@') !== false) {
+            $alarm = calendarlibcalendaring::get_next_alarm($task);
 
         if ($alarm['time'] && $alarm['action'] == 'DISPLAY')
           return date('Y-m-d H:i:s', $alarm['time']);
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index ec104d0..74442bd 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -370,7 +370,7 @@ class tasklist_kolab_driver extends tasklist_driver
         $time = $slot + $interval;
 
         $tasks = array();
-        $query = array(array('tags', '=', 'x-has-alarms'));
+        $query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete'));
         foreach ($this->lists as $lid => $list) {
             // skip lists with alarms disabled
             if (!$list['showalarms'] || ($lists && !in_array($lid, $lists)))
@@ -383,20 +383,8 @@ class tasklist_kolab_driver extends tasklist_driver
 
                 $task = $this->_to_rcube_task($record);
 
-                // fake object properties to suit the expectations of calendar::get_next_alarm()
-                // TODO: move all that to libcalendaring plugin
-                if ($task['date'])
-                    $task['start'] = new DateTime($task['date'] . ' ' . ($task['time'] ?: '12:00'), $this->plugin->timezone);
-                if ($task['startdate'])
-                    $task['end'] = new DateTime($task['startdate'] . ' ' . ($task['starttime'] ?: '12:00'), $this->plugin->timezone);
-                else
-                    $task['end'] = $tast['start'];
-
-                if (!$task['start'])
-                    $task['end'] = $task['start'];
-
                 // add to list if alarm is set
-                $alarm = calendar::get_next_alarm($task);
+                $alarm = libcalendaring::get_next_alarm($task, 'task');
                 if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') {
                     $id = $task['id'];
                     $tasks[$id] = $task;
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index df82cc7..b502361 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -8,8 +8,6 @@
  * License. It is allowed to copy, distribute, transmit and to adapt the work
  * by keeping credits to the original autors in the README file.
  * See http://creativecommons.org/licenses/by-sa/3.0/ for details.
- *
- * $Id$
  */
 
 #taskbar a.button-tasklist span.button-inner {
@@ -27,6 +25,10 @@ ul.toolbarmenu li span.icon.taskadd {
 	background-position: -4px -90px;
 }
 
+div.uidialog {
+	display: none;
+}
+
 #sidebar {
 	position: absolute;
 	top: 0;
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 3727d08..6842d8b 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -22,6 +22,9 @@
  
 function rcube_tasklist_ui(settings)
 {
+    // extend base class
+    rcube_libcalendaring.call(this, settings);
+
     /*  constants  */
     var FILTER_MASK_ALL = 0;
     var FILTER_MASK_TODAY = 1;
@@ -93,6 +96,15 @@ function rcube_tasklist_ui(settings)
     this.list_edit_dialog = list_edit_dialog;
     this.unlock_saving = unlock_saving;
 
+    /* imports */
+    var Q = this.quote_html;
+    var text2html = this.text2html;
+    var event_date_text = this.event_date_text;
+    var format_datetime = this.format_datetime;
+    var parse_datetime = this.parse_datetime;
+    var date2unixtime = this.date2unixtime;
+    var fromunixtime = this.fromunixtime;
+    var init_alarms_edit = this.init_alarms_edit;
 
     /**
      * initialize the tasks UI
@@ -318,16 +330,9 @@ function rcube_tasklist_ui(settings)
         });
 
         // register events on alarm fields
-        $('#taskedit select.edit-alarm-type').change(function(){
-            $(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
-        });
-        $('#taskedit select.edit-alarm-offset').change(function(){
-            var mode = $(this).val() == '@' ? 'show' : 'hide';
-            $(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode]();
-            $(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show');
-        });
+        init_alarms_edit('#taskedit');
 
-        $('#taskedit-date, #taskedit-startdate, #taskedit .edit-alarm-date').datepicker(datepicker_settings);
+        $('#taskedit-date, #taskedit-startdate').datepicker(datepicker_settings);
 
         $('a.edit-nodate').click(function(){
             var sel = $(this).attr('rel');
@@ -370,7 +375,7 @@ function rcube_tasklist_ui(settings)
             rcmail.http_request('fetch', { filter:basefilter, lists:active.join(','), q:search_query }, true);
         }
         else if (reload)
-            data_ready([]);
+            data_ready({ data:[], lists:'', filter:basefilter, search:search_query });
         else
             render_tasklist();
 
@@ -688,7 +693,7 @@ function rcube_tasklist_ui(settings)
     {
         var drag_id = draggable.data('id'),
             parent_id = $(this).data('id'),
-            drag_rec = listdata[drag_id],
+            drag_rec = listdata[drag_id] || {},
             drop_rec = listdata[parent_id];
 
         if (drop_rec && drop_rec.list != drag_rec.list)
@@ -1263,70 +1268,6 @@ function rcube_tasklist_ui(settings)
     /**** Utility functions ****/
 
     /**
-     * quote html entities
-     */
-    function Q(str)
-    {
-      return String(str).replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
-    }
-
-    /**
-     * Name says it all
-     * (cloned from calendar plugin)
-     */
-    function text2html(str, maxlen, maxlines)
-    {
-      var html = Q(String(str));
-
-      // limit visible text length
-      if (maxlen) {
-        var morelink = ' <a href="#more" onclick="$(this).hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','tasklist')+'</a><span style="display:none">',
-          lines = html.split(/\r?\n/),
-          words, out = '', len = 0;
-
-        for (var i=0; i < lines.length; i++) {
-          len += lines[i].length;
-          if (maxlines && i == maxlines - 1) {
-            out += lines[i] + '\n' + morelink;
-            maxlen = html.length * 2;
-          }
-          else if (len > maxlen) {
-            len = out.length;
-            words = lines[i].split(' ');
-            for (var j=0; j < words.length; j++) {
-              len += words[j].length + 1;
-              out += words[j] + ' ';
-              if (len > maxlen) {
-                out += morelink;
-                maxlen = html.length * 2;
-              }
-            }
-            out += '\n';
-          }
-          else
-            out += lines[i] + '\n';
-        }
-
-        if (maxlen > str.length)
-          out += '</span>';
-
-        html = out;
-      }
-      
-      // simple link parser (similar to rcube_string_replacer class in PHP)
-      var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
-      var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
-      var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)?', 'ig');
-      var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
-
-      return html
-        .replace(link_pattern, '<a href="$1$2" target="_blank">$1$2</a>')
-        .replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
-        .replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
-        .replace(/\n/g, "<br/>");
-    }
-
-    /**
      * Clear any text selection
      * (text is probably selected when double-clicking somewhere)
      */
@@ -1415,85 +1356,12 @@ function rcube_tasklist_ui(settings)
     }
 
 
-    /****  calendaring utility functions  *****/
-    /*  TO BE MOVED TO libcalendaring plugin  */
-
-    var gmt_offset = (new Date().getTimezoneOffset() / -60) - (rcmail.env.calendar_settings.timezone || 0) - (rcmail.env.calendar_settings.dst || 0);
-    var client_timezone = new Date().getTimezoneOffset();
-
-    /**
-     * from time and date strings to a real date object
-     */
-    function parse_datetime(time, date)
-    {
-        // we use the utility function from datepicker to parse dates
-        var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date();
-
-        var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/);
-        if (!isNaN(time_arr[0])) {
-            date.setHours(time_arr[0]);
-        if (time.match(/p[.m]*/i) && date.getHours() < 12)
-            date.setHours(parseInt(time_arr[0]) + 12);
-        else if (time.match(/a[.m]*/i) && date.getHours() == 12)
-            date.setHours(0);
-      }
-      if (!isNaN(time_arr[1]))
-            date.setMinutes(time_arr[1]);
-
-      return date;
-    }
-
-    /**
-     * Format the given date object according to user's prefs
-     */
-    function format_datetime(date, mode)
-    {
-        var format =
-             mode == 2 ?  rcmail.env.calendar_settings['time_format'] :
-            (mode == 1 ? rcmail.env.calendar_settings['date_format'] :
-             rcmail.env.calendar_settings['date_format'] + '  '+ rcmail.env.calendar_settings['time_format']);
-
-        return $.fullCalendar.formatDate(date, format);
-    }
-
-    /**
-     * convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
-     */
-    function date2unixtime(date)
-    {
-        var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;  // adjust DST offset
-        return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset);
-    }
-
-    /**
-     *
-     */
-    function fromunixtime(ts)
-    {
-        ts -= gmt_offset * 3600;
-        var date = new Date(ts * 1000),
-            dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;
-        if (dst_offset)  // adjust DST offset
-            date.setTime((ts + 3600) * 1000);
-        return date;
-    }
-
     // init dialog by default
     init_taskedit();
 }
 
 
 // extend jQuery
-(function($){
-  $.fn.serializeJSON = function(){
-    var json = {};
-    jQuery.map($(this).serializeArray(), function(n, i) {
-      json[n['name']] = n['value'];
-    });
-    return json;
-  };
-})(jQuery);
-
 // from http://james.padolsey.com/javascript/sorting-elements-with-jquery/
 jQuery.fn.sortElements = (function(){
     var sort = [].sort;
@@ -1518,7 +1386,7 @@ jQuery.fn.sortElements = (function(){
 var rctasks;
 window.rcmail && rcmail.addEventListener('init', function(evt) {
 
-  rctasks = new rcube_tasklist_ui(rcmail.env.tasklist_settings);
+  rctasks = new rcube_tasklist_ui(rcmail.env.libcal_settings);
 
   // register button commands
   rcmail.register_command('newtask', function(){ rctasks.edit_task(null, 'new', {}); }, true);
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 9589354..a2647fb 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -46,23 +46,21 @@ class tasklist extends rcube_plugin
 
     public $task = '?(?!login|logout).*';
     public $rc;
+    public $lib;
     public $driver;
     public $timezone;
     public $ui;
 
-    public $defaults = array(
-        'date_format'  => "Y-m-d",
-        'time_format'  => "H:i",
-        'first_day' => 1,
-    );
-
 
     /**
      * Plugin initialization.
      */
     function init()
     {
+        $this->require_plugin('libcalendaring');
+
         $this->rc = rcmail::get_instance();
+        $this->lib = libcalendaring::get_instance();
 
         $this->register_task('tasks', 'tasklist');
 
@@ -72,6 +70,8 @@ class tasklist extends rcube_plugin
         // load localizations
         $this->add_texts('localization/', $this->rc->task == 'tasks' && (!$this->rc->action || $this->rc->action == 'print'));
 
+        $this->timezone = $this->lib->timezone;
+
         if ($this->rc->task == 'tasks' && $this->rc->action != 'save-pref') {
             $this->load_driver();
 
@@ -141,14 +141,11 @@ class tasklist extends rcube_plugin
             $this->driver = new $driver_class($this);
             break;
         }
-
-        // get user's timezone
-        $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
     }
 
 
     /**
-     *
+     * Dispatcher for task-related actions initiated by the client
      */
     public function task_action()
     {
@@ -345,7 +342,7 @@ class tasklist extends rcube_plugin
 
 
     /**
-     *
+     * Dispatcher for tasklist actions initiated by the client
      */
     public function tasklist_action()
     {
@@ -389,7 +386,7 @@ class tasklist extends rcube_plugin
     }
 
     /**
-     *
+     * Get counts for active tasks divided into different selectors
      */
     public function fetch_counts()
     {
@@ -400,8 +397,6 @@ class tasklist extends rcube_plugin
 
     /**
      * Adjust the cached counts after changing a task
-     *
-     * 
      */
     public function update_counts($oldrec, $newrec)
     {
@@ -510,7 +505,7 @@ class tasklist extends rcube_plugin
         }
 
         if ($rec['alarms'])
-            $rec['alarms_text'] = calendar::alarms_text($rec['alarms']);
+            $rec['alarms_text'] = libcalendaring::alarms_text($rec['alarms']);
 
         foreach ((array)$rec['attachments'] as $k => $attachment) {
             $rec['attachments'][$k]['classname'] = rcmail_filetype2classname($attachment['mimetype'], $attachment['name']);
diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js
index 3b1673b..e3a889c 100644
--- a/plugins/tasklist/tasklist_base.js
+++ b/plugins/tasklist/tasklist_base.js
@@ -80,7 +80,7 @@ function rcube_tasklist(settings)
 
 /* tasklist plugin initialization (for email task) */
 window.rcmail && rcmail.env.task == 'mail' && rcmail.addEventListener('init', function(evt) {
-    var tasks = new rcube_tasklist(rcmail.env.tasklist_settings);
+    var tasks = new rcube_tasklist(rcmail.env.libcal_settings);
 
     rcmail.register_command('tasklist-create-from-mail', function() { tasks.create_from_mail() });
     rcmail.addEventListener('plugin.mail2taskdialog', function(p){ tasks.mail2taskdialog(p) });
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 330e7a6..5ee6c64 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -55,14 +55,7 @@ class tasklist_ui
         $this->plugin->include_script('tasklist_base.js');
 
         // copy config to client
-        $defaults = $this->plugin->defaults;
-        $settings = array(
-            'date_format' => $this->rc->config->get('date_format', $defaults['date_format']),
-            'time_format' => $this->rc->config->get('time_format', $defaults['time_format']),
-            'first_day' => $this->rc->config->get('calendar_first_day', $defaults['first_day']),
-        );
-
-        $this->rc->output->set_env('tasklist_settings', $settings);
+        // $this->rc->output->set_env('tasklist_settings', $settings);
 
         $this->ready = true;
   }
@@ -86,13 +79,6 @@ class tasklist_ui
         $this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
         $this->plugin->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));
 
-        // define list of file types which can be displayed inline
-        // same as in program/steps/mail/show.inc
-        $mimetypes = $this->rc->config->get('client_mimetypes', 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/x-javascript,application/pdf,application/x-shockwave-flash');
-        $settings = $this->rc->output->get_env('tasklist_settings');
-        $settings['mimetypes'] = is_string($mimetypes) ? explode(',', $mimetypes) : (array)$mimetypes;
-        $this->rc->output->set_env('tasklist_settings', $settings);
-
         $this->plugin->include_script('jquery.tagedit.js');
         $this->plugin->include_script('tasklist.js');
     }
@@ -185,34 +171,7 @@ class tasklist_ui
      */
     function alarm_select($attrib = array())
     {
-        unset($attrib['name']);
-        $select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type'));
-        $select_type->add(rcube_label('none'), '');
-        foreach ($this->plugin->driver->alarm_types as $type)
-            $select_type->add(rcube_label(strtolower("calendar.alarm{$type}option")), $type);
-
-        $input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
-        $input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
-        $input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
-
-        $select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
-        foreach (array('-M','-H','-D','+M','+H','+D','@') as $trigger)
-            $select_offset->add(rcube_label('calendar.trigger' . $trigger), $trigger);
-
-        // pre-set with default values from user settings
-        $preset = calendar::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
-        $hidden = array('style' => 'display:none');
-        $html = html::span('edit-alarm-set',
-            $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
-            html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
-            $input_value->show($preset[0]) . ' ' .
-            $select_offset->show($preset[1]) . ' ' .
-            $input_date->show('', $hidden) . ' ' .
-            $input_time->show('', $hidden)
-            )
-        );
-
-      return $html;
+        return $this->plugin->lib->alarm_select($attrib, $this->plugin->driver->alarm_types);
     }
 
     /**





More information about the commits mailing list