plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Fri Jun 8 14:57:41 CEST 2012


 plugins/tasklist/.gitignore                                    |    1 
 plugins/tasklist/config.inc.php.dist                           |    4 
 plugins/tasklist/drivers/database/SQL/mysql.sql                |   46 
 plugins/tasklist/drivers/database/tasklist_database_driver.php |  486 +++++
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php       |  422 ++++
 plugins/tasklist/drivers/tasklist_driver.php                   |  184 ++
 plugins/tasklist/localization/de_CH.inc                        |   43 
 plugins/tasklist/localization/en_US.inc                        |   43 
 plugins/tasklist/skins/larry/sprites.png                       |binary
 plugins/tasklist/skins/larry/taskbaricon.png                   |binary
 plugins/tasklist/skins/larry/tasklist.css                      |  504 +++++
 plugins/tasklist/skins/larry/templates/mainview.html           |  141 +
 plugins/tasklist/tasklist.js                                   |  872 ++++++++++
 plugins/tasklist/tasklist.php                                  |  485 +++++
 plugins/tasklist/tasklist_ui.php                               |  181 ++
 15 files changed, 3412 insertions(+)

New commits:
commit aed27f7d1147cb93f0f0c072374880852c03371e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri Jun 8 14:57:16 2012 +0200

    Import of a basic task management module for Roundcube working with libkolab storage

diff --git a/plugins/tasklist/.gitignore b/plugins/tasklist/.gitignore
new file mode 100644
index 0000000..2b9ce0a
--- /dev/null
+++ b/plugins/tasklist/.gitignore
@@ -0,0 +1 @@
+config.inc.php
\ No newline at end of file
diff --git a/plugins/tasklist/config.inc.php.dist b/plugins/tasklist/config.inc.php.dist
new file mode 100644
index 0000000..2fc5e28
--- /dev/null
+++ b/plugins/tasklist/config.inc.php.dist
@@ -0,0 +1,4 @@
+<?php
+
+$rcmail_config['tasklist_driver'] = 'kolab';
+
diff --git a/plugins/tasklist/drivers/database/SQL/mysql.sql b/plugins/tasklist/drivers/database/SQL/mysql.sql
new file mode 100644
index 0000000..eb9b454
--- /dev/null
+++ b/plugins/tasklist/drivers/database/SQL/mysql.sql
@@ -0,0 +1,46 @@
+/**
+ * Roundcube Tasklist plugin database
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli
+ * @licence GNU AGPL
+ * @copyright (C) 2012, Kolab Systems AG
+ */
+
+CREATE TABLE `tasklists` (
+  `tasklist_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) unsigned NOT NULL,
+  `name` varchar(255) NOT NULL,
+  `color` varchar(8) NOT NULL,
+  `showalarms` tinyint(2) unsigned NOT NULL DEFAULT '0',
+  PRIMARY KEY (`tasklist_id`),
+  KEY `user_id` (`user_id`),
+  CONSTRAINT `fk_tasklist_user_id` FOREIGN KEY (`user_id`)
+    REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+CREATE TABLE `tasks` (
+  `task_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `tasklist_id` int(10) unsigned NOT NULL,
+  `parent_id` int(10) unsigned DEFAULT NULL,
+  `uid` varchar(255) NOT NULL,
+  `created` datetime NOT NULL,
+  `changed` datetime NOT NULL,
+  `del` tinyint(1) unsigned NOT NULL DEFAULT '0',
+  `title` varchar(255) NOT NULL,
+  `description` text,
+  `date` varchar(10) DEFAULT NULL,
+  `time` varchar(5) DEFAULT NULL,
+  `flagged` tinyint(4) NOT NULL DEFAULT '0',
+  `complete` float NOT NULL DEFAULT '0',
+  `alarms` varchar(255) NOT NULL,
+  `recurrence` varchar(255) DEFAULT NULL,
+  `organizer` varchar(255) DEFAULT NULL,
+  `attendees` text,
+  `notify` datetime DEFAULT NULL,
+  PRIMARY KEY (`task_id`),
+  KEY `tasklisting` (`tasklist_id`,`del`,`date`),
+  KEY `uid` (`uid`),
+  CONSTRAINT `fk_tasks_tasklist_id` FOREIGN KEY (`tasklist_id`)
+    REFERENCES `tasklists`(`tasklist_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) /*!40000 ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci */;
diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
new file mode 100644
index 0000000..975f96e
--- /dev/null
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -0,0 +1,486 @@
+<?php
+
+/**
+ * Database driver for the Tasklist plugin
+ *
+ * @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 tasklist_database_driver extends tasklist_driver
+{
+    public $undelete = true; // yes, we can
+    public $sortable = false;
+
+    private $rc;
+    private $plugin;
+    private $cache = array();
+    private $lists = array();
+    private $list_ids = '';
+
+    private $db_tasks = 'tasks';
+    private $db_lists = 'tasklists';
+    private $sequence_tasks = 'task_ids';
+    private $sequence_lists = 'tasklist_ids';
+
+
+    /**
+     * Default constructor
+     */
+    public function __construct($plugin)
+    {
+        $this->rc = $plugin->rc;
+        $this->plugin = $plugin;
+
+        // read database config
+        $this->db_lists = $this->rc->config->get('db_table_lists', $this->db_lists);
+        $this->db_tasks = $this->rc->config->get('db_table_tasks', $this->db_tasks);
+        $this->sequence_lists = $this->rc->config->get('db_sequence_lists', $this->sequence_lists);
+        $this->sequence_tasks = $this->rc->config->get('db_sequence_tasks', $this->sequence_tasks);
+
+        $this->_read_lists();
+    }
+
+    /**
+     * Read available calendars for the current user and store them internally
+     */
+    private function _read_lists()
+    {
+      $hidden = array_filter(explode(',', $this->rc->config->get('hidden_tasklists', '')));
+
+      if (!empty($this->rc->user->ID)) {
+        $list_ids = array();
+        $result = $this->rc->db->query(
+          "SELECT *, tasklist_id AS id FROM " . $this->db_lists . "
+           WHERE user_id=?
+           ORDER BY name",
+           $this->rc->user->ID
+        );
+
+        while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
+          $arr['showalarms'] = intval($arr['showalarms']);
+          $arr['active'] = !in_array($arr['id'], $hidden);
+          $this->lists[$arr['id']] = $arr;
+          $list_ids[] = $this->rc->db->quote($arr['id']);
+        }
+        $this->list_ids = join(',', $list_ids);
+      }
+    }
+
+    /**
+     * Get a list of available tasks lists from this source
+     */
+    public function get_lists()
+    {
+      // attempt to create a default list for this user
+      if (empty($this->lists)) {
+        if ($this->create_list(array('name' => 'Default', 'color' => '000000')))
+          $this->_read_lists();
+      }
+
+      return $this->lists;
+    }
+
+    /**
+     * Create a new list assigned to the current user
+     *
+     * @param array Hash array with list properties
+     * @return mixed ID of the new list on success, False on error
+     * @see tasklist_driver::create_list()
+     */
+    public function create_list($prop)
+    {
+        $result = $this->rc->db->query(
+            "INSERT INTO " . $this->db_lists . "
+             (user_id, name, color, showalarms)
+             VALUES (?, ?, ?, ?)",
+            $this->rc->user->ID,
+            $prop['name'],
+            $prop['color'],
+            $prop['showalarms']?1:0
+        );
+
+        if ($result)
+            return $this->rc->db->insert_id($this->sequence_lists);
+
+        return false;
+    }
+
+    /**
+     * Update properties of an existing tasklist
+     *
+     * @param array Hash array with list properties
+     * @return boolean True on success, Fales on failure
+     * @see tasklist_driver::edit_list()
+     */
+    public function edit_list($prop)
+    {
+        $query = $this->rc->db->query(
+            "UPDATE " . $this->db_lists . "
+             SET   name=?, color=?, showalarms=?
+             WHERE calendar_id=?
+             AND   user_id=?",
+            $prop['name'],
+            $prop['color'],
+            $prop['showalarms']?1:0,
+            $prop['id'],
+            $this->rc->user->ID
+        );
+
+        return $this->rc->db->affected_rows($query);
+    }
+
+    /**
+     * Set active/subscribed state of a list
+     *
+     * @param array Hash array with list properties
+     * @return boolean True on success, Fales on failure
+     * @see tasklist_driver::subscribe_list()
+     */
+    public function subscribe_list($prop)
+    {
+        $hidden = array_flip(explode(',', $this->rc->config->get('hidden_tasklists', '')));
+
+        if ($prop['active'])
+            unset($hidden[$prop['id']]);
+        else
+            $hidden[$prop['id']] = 1;
+
+        return $this->rc->user->save_prefs(array('hidden_tasklists' => join(',', array_keys($hidden))));
+    }
+
+    /**
+     * Delete the given list with all its contents
+     *
+     * @param array Hash array with list properties
+     * @return boolean True on success, Fales on failure
+     * @see tasklist_driver::remove_list()
+     */
+    public function remove_list($prop)
+    {
+        // TODO: implement this
+        return false;
+    }
+
+    /**
+     * Get number of tasks matching the given filter
+     *
+     * @param array List of lists to count tasks of
+     * @return array Hash array with counts grouped by status (all|flagged|today|tomorrow|overdue|nodate)
+     * @see tasklist_driver::count_tasks()
+     */
+    function count_tasks($lists = null)
+    {
+        if (empty($lists))
+            $lists = array_keys($this->lists);
+        else if (is_string($lists))
+            $lists = explode(',', $lists);
+
+        // only allow to select from lists of this user
+        $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists)));
+
+        $today_date = new DateTime('now', $this->plugin->timezone);
+        $today = $today_date->format('Y-m-d');
+        $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
+        $tomorrow = $tomorrow_date->format('Y-m-d');
+
+        $result = $this->rc->db->query(sprintf(
+            "SELECT task_id, flagged, date FROM " . $this->db_tasks . "
+             WHERE tasklist_id IN (%s)
+             AND del=0 AND complete<1",
+            join(',', $list_ids)
+        ));
+
+        $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
+        while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+            $counts['all']++;
+            if ($rec['flagged'])
+                $counts['flagged']++;
+            if (empty($rec['date']))
+                $counts['nodate']++;
+            else if ($rec['date'] == $today)
+                $counts['today']++;
+            else if ($rec['date'] == $tomorrow)
+                $counts['tomorrow']++;
+            else if ($rec['date'] < $today)
+                $counts['overdue']++;
+        }
+
+        return $counts;
+    }
+
+    /**
+     * Get all taks records matching the given filter
+     *
+     * @param array Hash array wiht filter criterias
+     * @param array List of lists to get tasks from
+     * @return array List of tasks records matchin the criteria
+     * @see tasklist_driver::list_tasks()
+     */
+    function list_tasks($filter, $lists = null)
+    {
+        if (empty($lists))
+            $lists = array_keys($this->lists);
+        else if (is_string($lists))
+            $lists = explode(',', $lists);
+
+        // only allow to select from lists of this user
+        $list_ids = array_map(array($this->rc->db, 'quote'), array_intersect($lists, array_keys($this->lists)));
+        $sql_add = '';
+
+        // add filter criteria
+        if ($filter['from'] || ($filter['mask'] & tasklist::FILTER_MASK_TODAY)) {
+            $sql_add .= ' AND (date IS NULL OR date >= ?)';
+            $datefrom = $filter['from'];
+        }
+        if ($filter['to']) {
+            if ($filter['mask'] & tasklist::FILTER_MASK_OVERDUE)
+                $sql_add .= ' AND (date IS NOT NULL AND date <= ' . $this->rc->db->quote($filter['to']) . ')';
+            else
+                $sql_add .= ' AND (date IS NULL OR date <= ' . $this->rc->db->quote($filter['to']) . ')';
+        }
+
+        // special case 'today': also show all events with date before today
+        if ($filter['mask'] & tasklist::FILTER_MASK_TODAY) {
+            $datefrom = date('Y-m-d', 0);
+        }
+
+        if ($filter['mask'] & tasklist::FILTER_MASK_NODATE)
+            $sql_add = ' AND date IS NULL';
+
+        if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
+            $sql_add .= ' AND complete=1';
+        else  // don't show complete tasks by default
+            $sql_add .= ' AND complete<1';
+
+        if ($filter['mask'] & tasklist::FILTER_MASK_FLAGGED)
+            $sql_add .= ' AND flagged=1';
+
+        // compose (slow) SQL query for searching
+        // FIXME: improve searching using a dedicated col and normalized values
+        if ($filter['search']) {
+            $sql_query = array();
+            foreach (array('title','description','organizer','attendees') as $col)
+                $sql_query[] = $this->rc->db->ilike($col, '%'.$filter['search'].'%');
+            $sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
+        }
+
+        $tasks = array();
+        if (!empty($list_ids)) {
+            $datecol = $this->rc->db->quote_identifier('date');
+            $timecol = $this->rc->db->quote_identifier('time');
+            $result = $this->rc->db->query(sprintf(
+                "SELECT * FROM " . $this->db_tasks . "
+                 WHERE tasklist_id IN (%s)
+                 AND del=0
+                 %s",
+                 join(',', $list_ids),
+                 $sql_add
+                ),
+                $datefrom
+           );
+
+            while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+                $tasks[] = $this->_read_postprocess($rec);
+            }
+        }
+
+        return $tasks;
+    }
+
+    /**
+     * Return data of a specific task
+     *
+     * @param mixed  Hash array with task properties or task UID
+     * @return array Hash array with task properties or false if not found
+     */
+    public function get_task($prop)
+    {
+        if (is_string($prop))
+            $prop['uid'] = $prop;
+
+        $query_col = $prop['id'] ? 'task_id' : 'uid';
+
+        $result = $this->rc->db->query(sprintf(
+             "SELECT * FROM " . $this->db_tasks . "
+              WHERE tasklist_id IN (%s)
+              AND %s=?
+              AND del=0",
+              $this->list_ids,
+              $query_col
+             ),
+             $prop['id'] ? $prop['id'] : $prop['uid']
+        );
+
+        if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
+             return $this->_read_postprocess($rec);
+        }
+
+        return false;
+    }
+
+    /**
+     * Map some internal database values to match the generic "API"
+     */
+    private function _read_postprocess($rec)
+    {
+        $rec['id'] = $rec['task_id'];
+        $rec['list'] = $rec['tasklist_id'];
+        $rec['changed'] = strtotime($rec['changed']);
+
+        if (!$rec['parent_id'])
+            unset($rec['parent_id']);
+
+        unset($rec['task_id'], $rec['tasklist_id'], $rec['created']);
+        return $rec;
+    }
+
+    /**
+     * Add a single task to the database
+     *
+     * @param array Hash array with task properties (see header of this file)
+     * @return mixed New event ID on success, False on error
+     * @see tasklist_driver::create_task()
+     */
+    public function create_task($prop)
+    {
+        // check list permissions
+        $list_id = $prop['list'] ? $prop['list'] : reset(array_keys($this->lists));
+        if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly'])
+            return false;
+
+        foreach (array('parent_id', 'date', 'time') as $col) {
+            if (empty($prop[$col]))
+                $prop[$col] = null;
+        }
+
+        $result = $this->rc->db->query(sprintf(
+            "INSERT INTO " . $this->db_tasks . "
+             (tasklist_id, uid, parent_id, created, changed, title, date, time)
+             VALUES (?, ?, ?, %s, %s, ?, ?, ?)",
+             $this->rc->db->now(),
+             $this->rc->db->now()
+            ),
+            $list_id,
+            $prop['uid'],
+            $prop['parent_id'],
+            $prop['title'],
+            $prop['date'],
+            $prop['time']
+        );
+
+        if ($result)
+            return $this->rc->db->insert_id($this->sequence_tasks);
+
+        return false;
+    }
+
+    /**
+     * Update an task entry with the given data
+     *
+     * @param array Hash array with task properties
+     * @return boolean True on success, False on error
+     * @see tasklist_driver::edit_task()
+     */
+    public function edit_task($prop)
+    {
+        $sql_set = array();
+        foreach (array('title', 'description', 'flagged', 'complete') as $col) {
+            if (isset($prop[$col]))
+                $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($prop[$col]);
+        }
+        foreach (array('parent_id', 'date', 'time') as $col) {
+            if (isset($prop[$col]))
+                $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . (empty($prop[$col]) ? 'NULL' : $this->rc->db->quote($prop[$col]));
+        }
+
+        $query = $this->rc->db->query(sprintf(
+            "UPDATE " . $this->db_tasks . "
+             SET   changed=%s %s
+             WHERE task_id=?
+             AND   tasklist_id IN (%s)",
+            $this->rc->db->now(),
+            ($sql_set ? ', ' . join(', ', $sql_set) : ''),
+            $this->list_ids
+          ),
+          $prop['id']
+        );
+
+        return $this->rc->db->affected_rows($query);
+    }
+
+    /**
+     * Remove a single task from the database
+     *
+     * @param array   Hash array with task properties
+     * @param boolean Remove record irreversible
+     * @return boolean True on success, False on error
+     * @see tasklist_driver::delete_task()
+     */
+    public function delete_task($prop, $force = true)
+    {
+        $task_id = $prop['id'];
+
+        if ($task_id && $force) {
+            $query = $this->rc->db->query(
+                "DELETE FROM " . $this->db_tasks . "
+                 WHERE task_id=?
+                 AND tasklist_id IN (" . $this->list_ids . ")",
+                $task_id
+            );
+        }
+        else if ($task_id) {
+            $query = $this->rc->db->query(sprintf(
+                "UPDATE " . $this->db_tasks . "
+                 SET   changed=%s, del=1
+                 WHERE task_id=?
+                 AND   tasklist_id IN (%s)",
+                $this->rc->db->now(),
+                $this->list_ids
+              ),
+              $task_id
+            );
+        }
+
+        return $this->rc->db->affected_rows($query);
+    }
+
+    /**
+     * Restores a single deleted task (if supported)
+     *
+     * @param array Hash array with task properties
+     * @return boolean True on success, False on error
+     * @see tasklist_driver::undelete_task()
+     */
+    public function undelete_task($prop)
+    {
+        $query = $this->rc->db->query(sprintf(
+            "UPDATE " . $this->db_tasks . "
+             SET   changed=%s, del=0
+             WHERE task_id=?
+             AND   tasklist_id IN (%s)",
+            $this->rc->db->now(),
+            $this->list_ids
+          ),
+          $prop['id']
+        );
+
+        return $this->rc->db->affected_rows($query);
+    }
+
+}
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
new file mode 100644
index 0000000..5ccb3de
--- /dev/null
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -0,0 +1,422 @@
+<?php
+
+/**
+ * Kolab Groupware driver for the Tasklist plugin
+ *
+ * @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 tasklist_kolab_driver extends tasklist_driver
+{
+    // features supported by the backend
+    public $alarms = false;
+    public $attachments = false;
+    public $undelete = false; // task undelete action
+
+    private $rc;
+    private $plugin;
+    private $lists;
+    private $folders = array();
+    private $tasks = array();
+
+
+    /**
+     * Default constructor
+     */
+    public function __construct($plugin)
+    {
+        $this->rc = $plugin->rc;
+        $this->plugin = $plugin;
+
+        $this->_read_lists();
+    }
+
+    /**
+     * Read available calendars for the current user and store them internally
+     */
+    private function _read_lists()
+    {
+        // already read sources
+        if (isset($this->lists))
+            return $this->lists;
+
+        // get all folders that have type "task"
+        $this->folders = kolab_storage::get_folders('task');
+        $this->lists = array();
+
+        // convert to UTF8 and sort
+        $names = array();
+        foreach ($this->folders as $i => $folder) {
+            $names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP');
+            $this->folders[$folder->name] = $folder;
+        }
+
+        asort($names, SORT_LOCALE_STRING);
+
+        foreach ($names as $utf7name => $name) {
+            $folder = $this->folders[$utf7name];
+            $tasklist = array(
+                'id' => kolab_storage::folder_id($utf7name),
+                'name' => kolab_storage::object_name($utf7name),
+                'color' => 'CC0000',
+                'showalarms' => false,
+                'active' => 1, #$folder->is_subscribed(kolab_storage::SERVERSIDE_SUBSCRIPTION),
+            );
+            $this->lists[$tasklist['id']] = $tasklist;
+            $this->folders[$tasklist['id']] = $folder;
+        }
+    }
+
+    /**
+     * Get a list of available task lists from this source
+     */
+    public function get_lists()
+    {
+        // attempt to create a default list for this user
+        if (empty($this->lists)) {
+          if ($this->create_list(array('name' => 'Default', 'color' => '000000')))
+            $this->_read_lists();
+        }
+
+        return $this->lists;
+    }
+
+    /**
+     * Create a new list assigned to the current user
+     *
+     * @param array Hash array with list properties
+     *        name: List name
+     *       color: The color of the list
+     *  showalarms: True if alarms are enabled
+     * @return mixed ID of the new list on success, False on error
+     */
+    public function create_list($prop)
+    {
+        return false;
+    }
+
+    /**
+     * Update properties of an existing tasklist
+     *
+     * @param array Hash array with list properties
+     *          id: List Identifier
+     *        name: List name
+     *       color: The color of the list
+     *  showalarms: True if alarms are enabled (if supported)
+     * @return boolean True on success, Fales on failure
+     */
+    public function edit_list($prop)
+    {
+        return false;
+    }
+
+    /**
+     * Set active/subscribed state of a list
+     *
+     * @param array Hash array with list properties
+     *          id: List Identifier
+     *      active: True if list is active, false if not
+     * @return boolean True on success, Fales on failure
+     */
+    public function subscribe_list($prop)
+    {
+        return false;
+    }
+
+    /**
+     * Delete the given list with all its contents
+     *
+     * @param array Hash array with list properties
+     *      id: list Identifier
+     * @return boolean True on success, Fales on failure
+     */
+    public function remove_list($prop)
+    {
+        return false;
+    }
+
+    /**
+     * Get number of tasks matching the given filter
+     *
+     * @param array List of lists to count tasks of
+     * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
+     */
+    public function count_tasks($lists = null)
+    {
+        if (empty($lists))
+            $lists = array_keys($this->lists);
+        else if (is_string($lists))
+            $lists = explode(',', $lists);
+
+        $today_date = new DateTime('now', $this->plugin->timezone);
+        $today = $today_date->format('Y-m-d');
+        $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
+        $tomorrow = $tomorrow_date->format('Y-m-d');
+
+        $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
+        foreach ($lists as $list_id) {
+            $folder = $this->folders[$list_id];
+            foreach ((array)$folder->select(array(array('tags','!~','complete'))) as $record) {
+                $rec = $this->_to_rcube_task($record);
+
+                if ($rec['complete'])  // don't count complete tasks
+                    continue;
+
+                $counts['all']++;
+                if ($rec['flagged'])
+                    $counts['flagged']++;
+                if (empty($rec['date']))
+                    $counts['nodate']++;
+                else if ($rec['date'] == $today)
+                    $counts['today']++;
+                else if ($rec['date'] == $tomorrow)
+                    $counts['tomorrow']++;
+                else if ($rec['date'] < $today)
+                    $counts['overdue']++;
+            }
+        }
+
+        return $counts;
+    }
+
+    /**
+     * Get all taks records matching the given filter
+     *
+     * @param array Hash array with filter criterias:
+     *  - mask:  Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
+     *  - from:  Date range start as string (Y-m-d)
+     *  - to:    Date range end as string (Y-m-d)
+     *  - search: Search query string
+     * @param array List of lists to get tasks from
+     * @return array List of tasks records matchin the criteria
+     */
+    public function list_tasks($filter, $lists = null)
+    {
+        if (empty($lists))
+            $lists = array_keys($this->lists);
+        else if (is_string($lists))
+            $lists = explode(',', $lists);
+
+        $results = array();
+
+        // query Kolab storage
+        $query = array();
+        if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
+            $query[] = array('tags','~','complete');
+        else
+            $query[] = array('tags','!~','complete');
+
+        // full text search (only works with cache enabled)
+        if ($filter['search']) {
+            $search = mb_strtolower($filter['search']);
+            foreach (rcube_utils::normalize_string($search, true) as $word) {
+                $query[] = array('words', '~', $word);
+            }
+        }
+
+        foreach ($lists as $list_id) {
+            $folder = $this->folders[$list_id];
+            foreach ((array)$folder->select($query) as $record) {
+                $task = $this->_to_rcube_task($record);
+                $task['list'] = $list_id;
+
+                // TODO: post-filter tasks returned from storage
+
+                $results[] = $task;
+            }
+        }
+
+        return $results;
+    }
+
+    /**
+     * Return data of a specific task
+     *
+     * @param mixed  Hash array with task properties or task UID
+     * @return array Hash array with task properties or false if not found
+     */
+    public function get_task($prop)
+    {
+        $id = is_array($prop) ? $prop['uid'] : $prop;
+        $list_id = is_array($prop) ? $prop['list'] : null;
+        $folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders;
+
+        // find task in the available folders
+        foreach ($folders as $folder) {
+            if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
+                $this->tasks[$id] = $this->_to_rcube_task($object);
+                break;
+            }
+        }
+
+        return $this->tasks[$id];
+    }
+
+    /**
+     * Convert from Kolab_Format to internal representation
+     */
+    private function _to_rcube_task($record)
+    {
+        $task = array(
+            'id' => $record['uid'],
+            'uid' => $record['uid'],
+            'title' => $record['title'],
+#            'location' => $record['location'],
+            'description' => $record['description'],
+            'flagged' => $record['priority'] == 1,
+            'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100),
+            'parent_id' => $record['parent_id'],
+        );
+
+        // convert from DateTime to internal date format
+        if (is_a($record['due'], 'DateTime')) {
+            $task['date'] = $record['due']->format('Y-m-d');
+            $task['time'] = $record['due']->format('h:i');
+        }
+        if (is_a($record['dtstamp'], 'DateTime')) {
+            $task['changed'] = $record['dtstamp']->format('U');
+        }
+
+        return $task;
+    }
+
+    /**
+    * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
+    * (opposite of self::_to_rcube_event())
+     */
+    private function _from_rcube_task($task, $old = array())
+    {
+        $object = $task;
+
+        if (!empty($task['date'])) {
+            $object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone);
+            if (empty($task['time']))
+                $object['due']->_dateonly = true;
+            unset($object['date']);
+        }
+
+        $object['complete'] = $task['complete'] * 100;
+        if ($task['complete'] == 1.0)
+            $object['status'] = 'COMPLETED';
+
+        if ($task['flagged'])
+            $object['priority'] = 1;
+        else
+            $object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0;
+
+        // copy meta data (starting with _) from old object
+        foreach ((array)$old as $key => $val) {
+          if (!isset($object[$key]) && $key[0] == '_')
+            $object[$key] = $val;
+        }
+
+        unset($object['tempid'], $object['raw']);
+        return $object;
+    }
+
+    /**
+     * Add a single task to the database
+     *
+     * @param array Hash array with task properties (see header of tasklist_driver.php)
+     * @return mixed New task ID on success, False on error
+     */
+    public function create_task($task)
+    {
+        return $this->edit_task($task);
+    }
+
+    /**
+     * Update an task entry with the given data
+     *
+     * @param array Hash array with task properties (see header of tasklist_driver.php)
+     * @return boolean True on success, False on error
+     */
+    public function edit_task($task)
+    {
+        $list_id = $task['list'];
+        if (!$list_id || !($folder = $this->folders[$list_id]))
+            return false;
+
+        // moved from another folder
+        if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
+            if (!$fromfolder->move($task['uid'], $folder->name))
+                return false;
+
+            unset($task['_fromlist']);
+        }
+
+        // load previous version of this task to merge
+        if ($task['id']) {
+            $old = $folder->get_object($task['uid']);
+            if (!$old || PEAR::isError($old))
+                return false;
+        }
+
+        // generate new task object from RC input
+        $object = $this->_from_rcube_task($task, $old);
+        $saved = $folder->save($object, 'task', $task['id']);
+
+        if (!$saved) {
+            raise_error(array(
+                'code' => 600, 'type' => 'php',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Error saving task object to Kolab server"),
+                true, false);
+            $saved = false;
+        }
+        else {
+            $task['id'] = $task['uid'];
+            $this->tasks[$task['uid']] = $task;
+        }
+
+        return $saved;
+    }
+
+    /**
+     * Remove a single task from the database
+     *
+     * @param array   Hash array with task properties:
+     *      id: Task identifier
+     * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
+     * @return boolean True on success, False on error
+     */
+    public function delete_task($task, $force = true)
+    {
+        $list_id = $task['list'];
+        if (!$list_id || !($folder = $this->folders[$list_id]))
+            return false;
+
+        return $folder->delete($task['uid']);
+    }
+
+    /**
+     * Restores a single deleted task (if supported)
+     *
+     * @param array Hash array with task properties:
+     *      id: Task identifier
+     * @return boolean True on success, False on error
+     */
+    public function undelete_task($prop)
+    {
+        // TODO: implement this
+        return false;
+    }
+
+
+}
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
new file mode 100644
index 0000000..a3b0a85
--- /dev/null
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * Driver interface for the Tasklist plugin
+ *
+ * @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/>.
+ */
+
+ /**
+  * Struct of an internal task object how it is passed from/to the driver classes:
+  *
+  *  $task = array(
+  *            'id' => 'Task ID used for editing',  // must be unique for the current user
+  *     'parent_id' => 'ID of parent task',  // null if top-level task
+  *           'uid' => 'Unique identifier of this task',
+  *          'list' => 'Task list identifier to add the task to or where the task is stored',
+  *       'changed' => <unixtime>, // Last modification date of record
+  *         'title' => 'Event title/summary',
+  *   'description' => 'Event description',
+  *          'date' => 'Due date',   // as string of format YYYY-MM-DD or null if no date is set
+  *          'time' => 'Due time',   // as string of format hh::ii or null if no due time is set
+  *    'categories' => 'Task category',
+  *       'flagged' => 'Boolean value whether this record is flagged',
+  *      'complete' => 'Float value representing the completeness state (range 0..1)',
+  *   'sensitivity' => 0|1|2,   // Event sensitivity (0=public, 1=private, 2=confidential)
+  *        'alarms' => '-15M:DISPLAY',  // Reminder settings inspired by valarm definition (e.g. display alert 15 minutes before due time)
+  *     '_fromlist' => 'List identifier where the task was stored before',
+  *  );
+  */
+
+/**
+ * Driver interface for the Tasklist plugin
+ */
+abstract class tasklist_driver
+{
+    // features supported by the backend
+    public $alarms = false;
+    public $attachments = false;
+    public $undelete = false; // task undelete action
+    public $sortable = false;
+    public $alarm_types = array('DISPLAY');
+    public $last_error;
+
+    /**
+     * Get a list of available task lists from this source
+     */
+    abstract function get_lists();
+
+    /**
+     * Create a new list assigned to the current user
+     *
+     * @param array Hash array with list properties
+     *        name: List name
+     *       color: The color of the list
+     *  showalarms: True if alarms are enabled
+     * @return mixed ID of the new list on success, False on error
+     */
+    abstract function create_list($prop);
+
+    /**
+     * Update properties of an existing tasklist
+     *
+     * @param array Hash array with list properties
+     *          id: List Identifier
+     *        name: List name
+     *       color: The color of the list
+     *  showalarms: True if alarms are enabled (if supported)
+     * @return boolean True on success, Fales on failure
+     */
+    abstract function edit_list($prop);
+
+    /**
+     * Set active/subscribed state of a list
+     *
+     * @param array Hash array with list properties
+     *          id: List Identifier
+     *      active: True if list is active, false if not
+     * @return boolean True on success, Fales on failure
+     */
+    abstract function subscribe_list($prop);
+
+    /**
+     * Delete the given list with all its contents
+     *
+     * @param array Hash array with list properties
+     *      id: list Identifier
+     * @return boolean True on success, Fales on failure
+     */
+    abstract function remove_list($prop);
+
+    /**
+     * Get number of tasks matching the given filter
+     *
+     * @param array List of lists to count tasks of
+     * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
+     */
+    abstract function count_tasks($lists = null);
+
+    /**
+     * Get all taks records matching the given filter
+     *
+     * @param array Hash array with filter criterias:
+     *  - mask:  Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
+     *  - from:  Date range start as string (Y-m-d)
+     *  - to:    Date range end as string (Y-m-d)
+     *  - search: Search query string
+     * @param array List of lists to get tasks from
+     * @return array List of tasks records matchin the criteria
+     */
+    abstract function list_tasks($filter, $lists = null);
+
+    /**
+     * Return data of a specific task
+     *
+     * @param mixed  Hash array with task properties or task UID
+     * @return array Hash array with task properties or false if not found
+     */
+    abstract public function get_task($prop);
+
+    /**
+     * Add a single task to the database
+     *
+     * @param array Hash array with task properties (see header of this file)
+     * @return mixed New event ID on success, False on error
+     */
+    abstract function create_task($prop);
+
+    /**
+     * Update an task entry with the given data
+     *
+     * @param array Hash array with task properties (see header of this file)
+     * @return boolean True on success, False on error
+     */
+    abstract function edit_task($prop);
+
+    /**
+     * Remove a single task from the database
+     *
+     * @param array   Hash array with task properties:
+     *      id: Task identifier
+     * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
+     * @return boolean True on success, False on error
+     */
+    abstract function delete_task($prop, $force = true);
+
+    /**
+     * Restores a single deleted task (if supported)
+     *
+     * @param array Hash array with task properties:
+     *      id: Task identifier
+     * @return boolean True on success, False on error
+     */
+    public function undelete_task($prop)
+    {
+        return false;
+    }
+
+    /**
+     * List availabale categories
+     * The default implementation reads them from config/user prefs
+     */
+    public function list_categories()
+    {
+        $rcmail = rcmail::get_instance();
+        return $rcmail->config->get('tasklist_categories', array());
+    }
+
+}
diff --git a/plugins/tasklist/localization/de_CH.inc b/plugins/tasklist/localization/de_CH.inc
new file mode 100644
index 0000000..a7b3712
--- /dev/null
+++ b/plugins/tasklist/localization/de_CH.inc
@@ -0,0 +1,43 @@
+<?php
+
+$labels = array();
+$labels['navtitle'] = 'Aufgaben';
+$labels['lists'] = 'Ressourcen';
+$labels['list'] = 'Ressource';
+
+$labels['createnewtask'] = 'Neue Aufgabe eingeben';
+$labels['mark'] = 'Markieren';
+$labels['unmark'] = 'Markierung aufheben';
+$labels['edit'] = 'Bearbeiten';
+$labels['delete'] = 'Löschen';
+$labels['title'] = 'Titel';
+$labels['description'] = 'Beschreibung';
+$labels['datetime'] = 'Datum/Zeit';
+
+$labels['all'] = 'Alle';
+$labels['flagged'] = 'Markiert';
+$labels['complete'] = 'Erledigt';
+$labels['overdue'] = 'Überfällig';
+$labels['today'] = 'Heute';
+$labels['tomorrow'] = 'Morgen';
+$labels['next7days'] = 'Nächste 7 Tage';
+$labels['later'] = 'Später';
+$labels['nodate'] = 'kein Datum';
+
+$labels['taskdetails'] = 'Details';
+$labels['newtask'] = 'Neue Aufgabe';
+$labels['edittask'] = 'Aufgabe bearbeiten';
+$labels['save'] = 'Speichern';
+$labels['cancel'] = 'Abbrechen';
+$labels['addsubtask'] = 'Neue Teilaufgabe';
+
+// date words
+$labels['on'] = 'am';
+$labels['at'] = 'um';
+$labels['this'] = 'diesen';
+$labels['next'] = 'nächsten';
+
+// mesages
+$labels['savingdata'] = 'Daten werden gespeichert...';
+$labels['errorsaving'] = 'Fehler beim Speichern.';
+$labels['notasksfound'] = 'Für die aktuellen Kriterien wurden keine Aufgaben gefunden.';
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
new file mode 100644
index 0000000..cbdcffb
--- /dev/null
+++ b/plugins/tasklist/localization/en_US.inc
@@ -0,0 +1,43 @@
+<?php
+
+$labels = array();
+$labels['navtitle'] = 'Tasks';
+$labels['lists'] = 'Resources';
+$labels['list'] = 'Resource';
+
+$labels['createnewtask'] = 'Create new Task';
+$labels['mark'] = 'Mark';
+$labels['unmark'] = 'Unmark';
+$labels['edit'] = 'Edit';
+$labels['delete'] = 'Delete';
+$labels['title'] = 'Title';
+$labels['description'] = 'Description';
+$labels['datetime'] = 'Date/Time';
+
+$labels['all'] = 'All';
+$labels['flagged'] = 'Flagged';
+$labels['complete'] = 'Complete';
+$labels['overdue'] = 'Overdue';
+$labels['today'] = 'Today';
+$labels['tomorrow'] = 'Tomorrow';
+$labels['next7days'] = 'Next 7 days';
+$labels['later'] = 'Later';
+$labels['nodate'] = 'no date';
+
+$labels['taskdetails'] = 'Details';
+$labels['newtask'] = 'New Task';
+$labels['edittask'] = 'Edit Task';
+$labels['save'] = 'Save';
+$labels['cancel'] = 'Cancel';
+$labels['addsubtask'] = 'Add subtask';
+
+// date words
+$labels['on'] = 'on';
+$labels['at'] = 'at';
+$labels['this'] = 'this';
+$labels['next'] = 'next';
+
+// mesages
+$labels['savingdata'] = 'Saving data...';
+$labels['errorsaving'] = 'Failed to save data.';
+$labels['notasksfound'] = 'No tasks found for the given criteria';
diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png
new file mode 100644
index 0000000..fa00d6e
Binary files /dev/null and b/plugins/tasklist/skins/larry/sprites.png differ
diff --git a/plugins/tasklist/skins/larry/taskbaricon.png b/plugins/tasklist/skins/larry/taskbaricon.png
new file mode 100644
index 0000000..b0141ff
Binary files /dev/null and b/plugins/tasklist/skins/larry/taskbaricon.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
new file mode 100644
index 0000000..8811766
--- /dev/null
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -0,0 +1,504 @@
+/**
+ * Roundcube Taklist plugin styles for skin "Larry"
+ *
+ * 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$
+ */
+
+#taskbar a.button-tasklist span.button-inner {
+	background-image: url(taskbaricon.png);
+	background-position: 0 0;
+}
+
+#taskbar a.button-tasklist:hover span.button-inner,
+#taskbar a.button-tasklist.button-selected span.button-inner {
+	background-position: 0 -44px;
+}
+
+#sidebar {
+	position: absolute;
+	top: 0;
+	left: 0;
+	bottom: 0;
+	width: 240px;
+}
+
+body.tasklistview #searchmenulink {
+	width: 15px;
+}
+
+#selectorbox {
+	position: absolute;
+	top: 42px;
+	left: 0;
+	width: 100%;
+	height: 242px;
+}
+
+#tasklistsbox {
+	position: absolute;
+	top: 300px;
+	left: 0;
+	width: 100%;
+	bottom: 0px;
+}
+
+#taskselector li {
+	position: relative;
+}
+
+#taskselector li:first-child {
+	border-top: 0;
+	border-radius: 4px 4px 0 0;
+}
+
+#taskselector li:last-child {
+	border-bottom: 0;
+	border-radius: 0 0 4px 4px;
+}
+
+#taskselector li.selected {
+	background-color: #c7e3ef;
+}
+
+#taskselector li.overdue a {
+	color: #b72a2a;
+	font-weight: bold;
+}
+
+#taskselector li.inactive a {
+	color: #97b3bf;
+}
+
+#taskselector li .count {
+	display: none;
+	position: absolute;
+	top: 3px;
+	right: 6px;
+	min-width: 1.8em;
+	padding: 2px 4px;
+	background: #d9ecf4;
+	background: -moz-linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
+	background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d9ecf4), color-stop(100%,#c7e3ef));
+	background: -o-linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
+	background: -ms-linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
+	background: linear-gradient(top, #d9ecf4 0%, #c7e3ef 100%);
+	box-shadow: inset 0 1px 1px 0 #b7d3df;
+	-o-box-shadow: inset 0 1px 1px 0 #b7d3df;
+	-webkit-box-shadow: inset 0 1px 1px 0 #b7d3df;
+	-moz-box-shadow: inset 0 1px 1px 0 #b7d3df;
+	border: 1px solid #a7c3cf;
+	border-radius: 9px;
+	color: #69939e;
+	text-align: center;
+	font-weight: bold;
+	text-shadow: none;
+}
+
+#taskselector li.selected .count {
+	color: #fff;
+	background: #005d76;
+	background: -moz-linear-gradient(top, #005d76 0%, #004558 100%);
+	background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#005d76), color-stop(100%,#004558));
+	background: -o-linear-gradient(top, #005d76 0%, #004558 100%);
+	background: -ms-linear-gradient(top, #005d76 0%, #004558 100%);
+	background: linear-gradient(top, #005d76 0%, #004558 100%);
+	box-shadow: inset 0 1px 1px 0 #003645;
+	-o-box-shadow: inset 0 1px 1px 0 #003645;
+	-webkit-box-shadow: inset 0 1px 1px 0 #003645;
+	-moz-box-shadow: inset 0 1px 1px 0 #003645;
+	border-color: #003645;
+}
+
+#taskselector li.overdue.selected .count {
+	background: #db3333;
+	background: -moz-linear-gradient(top, #db3333 0%, #a82727 100%);
+	background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#db3333), color-stop(100%,#a82727));
+	background: -o-linear-gradient(top, #db3333 0%, #a82727 100%);
+	background: -ms-linear-gradient(top, #db3333 0%, #a82727 100%);
+	background: linear-gradient(top, #db3333 0%, #a82727 100%);
+	box-shadow: inset 0 1px 1px 0 #831f1f;
+	-o-box-shadow: inset 0 1px 1px 0 #831f1f;
+	-webkit-box-shadow: inset 0 1px 1px 0 #831f1f;
+	-moz-box-shadow: inset 0 1px 1px 0 #831f1f;
+	border-color: #831f1f;
+}
+
+#tasklists li {
+	margin: 0;
+	height: 20px;
+	padding: 6px 8px 2px;
+	display: block;
+	position: relative;
+	white-space: nowrap;
+}
+
+#tasklists li label {
+	display: block;
+}
+
+#tasklists li span.listname {
+	cursor: default;
+	padding-bottom: 2px;
+	color: #004458;
+}
+
+#tasklists li span.handle {
+	display: none;
+}
+
+#tasklists li input {
+	position: absolute;
+	top: 5px;
+	right: 5px;
+}
+
+#tasklists li.selected {
+	background-color: #c7e3ef;
+}
+
+#tasklists li.selected span.calname {
+	font-weight: bold;
+}
+
+#mainview-right {
+	position: absolute;
+	top: 0;
+	left: 256px;
+	right: 0;
+	bottom: 0;
+}
+
+#taskstoolbar {
+	position: absolute;
+	top: -6px;
+	right: 0;
+	width: 40%;
+	height: 40px;
+	white-space: nowrap;
+	text-align: right;
+}
+
+#quickaddbox {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 60%;
+	height: 32px;
+	white-space: nowrap;
+}
+
+#quickaddinput {
+	width: 85%;
+	margin: 0;
+	padding: 5px 8px;
+	background: #f1f1f1;
+	background: rgba(255, 255, 255, 0.7);
+	border-color: #a3a3a3;
+	font-weight: bold;
+}
+
+#quickaddbox .button {
+	margin-left: 5px;
+	padding: 3px 10px;
+	font-weight: bold;
+}
+
+#tasksview {
+	position: absolute;
+	top: 42px;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	padding-bottom: 28px;
+	background: rgba(255, 255, 255, 0.3);
+}
+
+#message.statusbar {
+	border-top: 1px solid #c3c3c3;
+}
+
+#tasksview .scroller {
+	position: absolute;
+	top: 0;
+	left: 0;
+	width: 100%;
+	bottom: 28px;
+	overflow: auto;
+}
+
+#thelist {
+	padding: 0;
+	margin: 1em;
+	list-style: none;
+}
+
+#listmessagebox {
+	display: none;
+	font-size: 14px;
+	color: #666;
+	margin: 1.5em;
+	text-shadow: 0px 1px 1px #fff;
+	text-align:center;
+}
+
+.taskitem {
+	display: block;
+	margin-bottom: 5px;
+}
+
+.taskitem.dragging {
+	opacity: 0.5;
+}
+
+.taskitem .childtasks {
+	padding: 0;
+	margin: 0.5em 0 0 2em;
+	list-style: none;
+}
+
+.taskhead {
+	position: relative;
+	padding: 4px 5px 3px 5px;
+	border: 1px solid #fff;
+	border-radius: 5px;
+	background: #fff;
+	-webkit-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
+	-moz-box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
+	box-shadow: 0 1px 1px 0 rgba(50, 50, 50, 0.5);
+	padding-right: 11em;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	cursor: default;
+}
+
+.taskhead.droptarget {
+	border-color: #4787b1;
+	box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+	-moz-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+	-webkit-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+	-o-box-shadow: 0 0 5px 2px rgba(71,135,177, 0.9);
+}
+
+.taskhead .complete {
+	margin: -1px 1em 0 0;
+}
+
+.taskhead .title {
+	font-size: 12px;
+}
+
+.taskhead .flagged {
+	display: inline-block;
+	visibility: hidden;
+	width: 16px;
+	height: 16px;
+	background: url(sprites.png) -2px -3px no-repeat;
+	margin: -3px 1em 0 0;
+	vertical-align: middle;
+	cursor: pointer;
+}
+
+.taskhead:hover .flagged {
+	visibility: visible;
+}
+
+.taskhead.flagged .flagged {
+	visibility: visible;
+	background-position: -2px -23px;
+}
+
+.taskhead .date {
+	position: absolute;
+	top: 6px;
+	right: 30px;
+	text-align: right;
+	cursor: pointer;
+}
+
+.taskhead.nodate .date {
+	color: #ddd;
+}
+
+.taskhead.overdue .date {
+	color: #d00;
+}
+
+.taskhead.nodate:hover .date {
+	color: #999;
+}
+
+.taskhead .date input {
+	padding: 1px 2px;
+	border: 1px solid #ddd;
+	-webkit-box-shadow: none;
+	-moz-box-shadow: none;
+	box-shadow: none;
+	outline: none;
+	text-align: right;
+}
+
+.taskhead .actions,
+.taskhead .delete {
+	display: block;
+	visibility: hidden;
+	position: absolute;
+	top: 3px;
+	right: 6px;
+	width: 18px;
+	height: 18px;
+	background: url(sprites.png) 0 -80px no-repeat;
+	text-indent: -1000px;
+	overflow: hidden;
+	cursor: pointer;
+}
+
+.taskhead .delete {
+	background-position: 0 -40px;
+}
+
+.taskhead:hover .actions,
+.taskhead:hover .delete {
+	visibility: visible;
+}
+
+.taskhead.complete {
+	opacity: 0.6;
+}
+
+.taskhead.complete .title {
+	text-decoration: line-through;
+}
+
+.taskhead .progressbar {
+	position: absolute;
+	bottom: 1px;
+	left: 6px;
+	right: 6px;
+	height: 2px;
+}
+
+.taskhead.complete .progressbar {
+	display: none;
+}
+
+.taskhead .progressvalue {
+	height: 1px;
+	background: rgba(1, 124, 180, 0.2);
+	border-top: 1px solid #219de6;
+}
+
+ul.toolbarmenu li span.add {
+	background-image: url(sprites.png);
+	background-position: 0 -100px;
+}
+
+ul.toolbarmenu li span.delete {
+	background-position: 0 -1508px;
+}
+
+.taskitem-draghelper {
+/*
+	width: 32px;
+	height: 26px;
+*/
+	background: #444;
+	border: 1px solid #555;
+	border-radius: 4px;
+	box-shadow: 0 2px 6px 0 #333;
+	-moz-box-shadow: 0 2px 6px 0 #333;
+	-webkit-box-shadow: 0 2px 6px 0 #333;
+	-o-box-shadow: 0 2px 6px 0 #333;
+	z-index: 5000;
+	padding: 2px 10px;
+	font-size: 20px;
+	color: #ccc;
+	opacity: 0.92;
+	filter: alpha(opacity=92);
+	text-shadow: 0px 1px 1px #333;
+}
+
+#rootdroppable {
+	display: none;
+	position: absolute;
+	top: 3px;
+	left: 1em;
+	right: 1em;
+	height: 5px;
+	background: #ddd;
+	border-radius: 3px;
+}
+
+#rootdroppable.droptarget {
+	background: #4787b1;
+	box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
+	-moz-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
+	-webkit-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
+	-o-box-shadow: 0 0 2px 1px rgba(71,135,177, 0.9);
+	
+}
+
+/*** task edit form ***/
+
+#taskedit,
+#taskshow {
+	display:none;
+}
+
+#taskshow h2 {
+	margin-top: -0.5em;
+}
+
+#taskshow label {
+	color: #999;
+}
+
+a.morelink {
+	font-size: 90%;
+	color: #0069a6;
+	text-decoration: none;
+	outline: none;
+}
+
+a.morelink:hover {
+	text-decoration: underline;
+}
+
+#taskeditform input.text,
+#taskeditform textarea {
+	width: 97%;
+}
+
+div.form-section {
+	position: relative;
+	margin-top: 0.2em;
+	margin-bottom: 0.8em;
+}
+
+.form-section label {
+	display: inline-block;
+	min-width: 7em;
+	padding-right: 0.5em;
+}
+
+label.block {
+	display: block;
+	margin-bottom: 0.3em;
+}
+
+#edit-completeness-slider {
+	display: inline-block;
+	margin-left: 2em;
+	width: 30em;
+	height: 0.8em;
+	border: 1px solid #ccc;
+}
+
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
new file mode 100644
index 0000000..fd8fa5c
--- /dev/null
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -0,0 +1,141 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+</head>
+<body class="tasklistview noscroll">
+
+<roundcube:include file="/includes/header.html" />
+
+<div id="mainscreen">
+	<div id="sidebar">
+		<div id="quicksearchbar">
+			<roundcube:object name="plugin.searchform" id="quicksearchbox" />
+			<a id="searchmenulink" class="iconbutton searchoptions" > </a>
+			<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
+		</div>
+		
+		<div id="selectorbox" class="uibox listbox">
+			<div class="scroller">
+			<ul id="taskselector" class="listing">
+				<li class="all selected"><a href="#all"><roundcube:label name="tasklist.all" /><span class="count"></span></a></li>
+				<li class="overdue inactive"><a href="#overdue"><roundcube:label name="tasklist.overdue" /><span class="count"></span></a></li>
+				<li class="flagged"><a href="#flagged"><roundcube:label name="tasklist.flagged" /><span class="count"></span></a></li>
+				<li class="today"><a href="#today"><roundcube:label name="tasklist.today" /><span class="count"></span></a></li>
+				<li class="tomorrow"><a href="#tomorrow"><roundcube:label name="tasklist.tomorrow" /><span class="count"></span></a></li>
+				<li class="week"><a href="#week"><roundcube:label name="tasklist.next7days" /></a></li>
+				<li class="later"><a href="#later"><roundcube:label name="tasklist.later" /></a></li>
+				<li class="nodate"><a href="#nodate"><roundcube:label name="tasklist.nodate" ucfirst="true" /></a></li>
+				<li class="complete"><a href="#complete"><roundcube:label name="tasklist.complete" /><span class="count"></span></a></li>
+			</ul>
+			</div>
+		</div>
+
+		<div id="tasklistsbox" class="uibox listbox">
+			<h2 class="boxtitle"><roundcube:label name="tasklist.lists" /></h2>
+			<div class="scroller withfooter">
+			<roundcube:object name="plugin.tasklists" id="tasklists" class="listing" />
+			</div>
+			<div class="boxfooter">
+				<roundcube:button command="list-create" type="link" title="tasklist.createlist" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="tasklistoptionslink" id="tasklistoptionsmenulink" type="link" title="tasklist.listactions" class="listbutton groupactions" onclick="UI.show_popup('tasklistoptionsmenu', undefined, { above:true });return false" innerClass="inner" content="⚙" />
+			</div>
+		</div>
+	</div>
+	
+	<div id="mainview-right">
+	
+	<div id="quickaddbox">
+		<roundcube:object name="plugin.quickaddform" />
+	</div>
+
+	<div id="taskstoolbar" class="toolbar">
+		<roundcube:container name="toolbar" id="taskstoolbar" />
+	</div>
+
+	<div id="tasksview" class="uibox">
+		<div class="scroller">
+			<roundcube:object name="plugin.tasks" id="thelist" />
+			<div id="listmessagebox"></div>
+		</div>
+		<div id="rootdroppable"></div>
+		<roundcube:object name="message" id="message" class="statusbar" />
+	</div>
+	
+	</div>
+
+</div>
+
+<div id="taskitemmenu" class="popupmenu">
+	<ul class="toolbarmenu iconized">
+		<li><roundcube:button name="edit" type="link" onclick="rctasks.edit_task(rctasks.selected_task.id, 'edit'); return false" label="edit" class="icon active" innerclass="icon edit" /></li>
+		<li><roundcube:button name="delete" type="link" onclick="rctasks.delete_task(rctasks.selected_task.id); return false" label="delete" class="icon active" innerclass="icon delete" /></li>
+		<li><roundcube:button name="addchild" type="link" onclick="rctasks.add_childtask(rctasks.selected_task.id); return false" label="tasklist.addsubtask" class="icon active" innerclass="icon add" /></li>
+	</ul>
+</div>
+
+<div id="taskshow">
+	<div class="form-section">
+		<h2 id="task-title"></h2>
+	</div>
+	<div id="task-description" class="form-section">
+	</div>
+	<div id="task-date" class="form-section">
+		<label><roundcube:label name="tasklist.datetime" /></label>
+		<span class="task-text"></span>
+		<span id="task-time"></span>
+	</div>
+	<div id="task-list" class="form-section">
+		<label><roundcube:label name="tasklist.list" /></label>
+		<span class="task-text"></span>
+	</div>
+	<div id="task-completeness" class="form-section">
+		<label><roundcube:label name="tasklist.complete" /></label>
+		<span class="task-text"></span>
+	</div>
+</div>
+
+<div id="taskedit">
+	<form id="taskeditform" action="#" method="post" enctype="multipart/form-data">
+		<div class="form-section">
+			<label for="edit-title"><roundcube:label name="tasklist.title" /></label>
+			<br />
+			<input type="text" class="text" name="title" id="edit-title" size="40" />
+		</div>
+		<div class="form-section">
+			<label for="edit-description"><roundcube:label name="tasklist.description" /></label>
+			<br />
+			<textarea name="description" id="edit-description" class="text" rows="5" cols="40"></textarea>
+		</div>
+		<div class="form-section">
+			<label for="edit-date"><roundcube:label name="tasklist.datetime" /></label>
+			<input type="text" name="date" size="10" id="edit-date" />  
+			<input type="text" name="time" size="6" id="edit-time" />
+			<a href="#nodate" style="margin-left:1em" id="edit-nodate"><roundcube:label name="tasklist.nodate" /></a>
+		</div>
+		<div class="form-section">
+			<label for="edit-completeness"><roundcube:label name="tasklist.complete" /></label>
+			<input type="text" name="title" id="edit-completeness" size="3" /> %
+			<div id="edit-completeness-slider"></div>
+		</div>
+		<div class="form-section" id="tasklist-select">
+			<label for="edit-tasklist"><roundcube:label name="tasklist.list" /></label>
+			<roundcube:object name="plugin.tasklist_select" id="edit-tasklist" />
+		</div>
+	</form>
+</div>
+
+<script type="text/javascript">
+
+// UI startup
+var UI = new rcube_mail_ui();
+
+$(document).ready(function(e){
+	UI.init();
+
+});
+
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
new file mode 100644
index 0000000..c76e411
--- /dev/null
+++ b/plugins/tasklist/tasklist.js
@@ -0,0 +1,872 @@
+/**
+ * Client scripts for the Tasklist plugin
+ *
+ * @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_tasklist(settings)
+{
+    /*  constants  */
+    var FILTER_MASK_ALL = 0;
+    var FILTER_MASK_TODAY = 1;
+    var FILTER_MASK_TOMORROW = 2;
+    var FILTER_MASK_WEEK = 4;
+    var FILTER_MASK_LATER = 8;
+    var FILTER_MASK_NODATE = 16;
+    var FILTER_MASK_OVERDUE = 32;
+    var FILTER_MASK_FLAGGED = 64;
+    var FILTER_MASK_COMPLETE = 128;
+    
+    var filter_masks = {
+        all:      FILTER_MASK_ALL,
+        today:    FILTER_MASK_TODAY,
+        tomorrow: FILTER_MASK_TOMORROW,
+        week:     FILTER_MASK_WEEK,
+        later:    FILTER_MASK_LATER,
+        nodate:   FILTER_MASK_NODATE,
+        overdue:  FILTER_MASK_OVERDUE,
+        flagged:  FILTER_MASK_FLAGGED,
+        complete: FILTER_MASK_COMPLETE
+    };
+    
+    /*  private vars  */
+    var selector = 'all';
+    var filtermask = FILTER_MASK_ALL;
+    var idcount = 0;
+    var selected_list;
+    var saving_lock;
+    var ui_loading;
+    var taskcounts = {};
+    var listdata = {};
+    var draghelper;
+    var completeness_slider;
+    var search_request;
+    var search_query;
+    var me = this;
+    
+    // general datepicker settings
+    var datepicker_settings = {
+      // translate from PHP format to datepicker format
+      dateFormat: settings['date_format'].replace(/m/, 'mm').replace(/n/g, 'm').replace(/F/, 'MM').replace(/l/, 'DD').replace(/dd/, 'D').replace(/d/, 'dd').replace(/j/, 'd').replace(/Y/g, 'yy'),
+      firstDay : settings['first_day'],
+//      dayNamesMin: settings['days_short'],
+//      monthNames: settings['months'],
+//      monthNamesShort: settings['months'],
+      changeMonth: false,
+      showOtherMonths: true,
+      selectOtherMonths: true
+    };
+    var extended_datepicker_settings;
+
+    /*  public members  */
+    this.tasklists = rcmail.env.tasklists;
+    this.selected_task;
+
+    /*  public methods  */
+    this.init = init;
+    this.edit_task = task_edit_dialog;
+    this.delete_task = delete_task;
+    this.add_childtask = add_childtask;
+    this.quicksearch = quicksearch;
+    this.reset_search = reset_search;
+
+
+    /**
+     * initialize the tasks UI
+     */
+    function init()
+    {
+        // select the first task list
+        for (var s in me.tasklists) {
+            selected_list = s;
+            break;
+        };
+
+        // register server callbacks
+        rcmail.addEventListener('plugin.data_ready', data_ready);
+        rcmail.addEventListener('plugin.refresh_task', update_taskitem);
+        rcmail.addEventListener('plugin.update_counts', update_counts);
+        rcmail.addEventListener('plugin.reload_data', function(){ list_tasks(null); });
+        rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, saving_lock); });
+
+        // start loading tasks
+        fetch_counts();
+        list_tasks();
+
+        // register event handlers for UI elements
+        $('#taskselector a').click(function(e){
+            if (!$(this).parent().hasClass('inactive'))
+                list_tasks(this.href.replace(/^.*#/, ''));
+            return false;
+        });
+
+        // quick-add a task
+        $(rcmail.gui_objects.quickaddform).submit(function(e){
+            var tasktext = this.elements.text.value;
+            var rec = { id:-(++idcount), title:tasktext, readonly:true, mask:0, complete:0 };
+
+            save_task({ tempid:rec.id, raw:tasktext, list:selected_list }, 'new');
+            render_task(rec);
+
+            // clear form
+            this.reset();
+            return false;
+        });
+        
+        // click-handler on task list items (delegate)
+        $(rcmail.gui_objects.resultlist).click(function(e){
+            var item = $(e.target);
+
+            if (!item.hasClass('taskhead'))
+                item = item.closest('div.taskhead');
+
+            // ignore
+            if (!item.length)
+                return;
+
+            var id = item.data('id'),
+                li = item.parent(),
+                rec = listdata[id];
+            
+            switch (e.target.className) {
+                case 'complete':
+                    rec.complete = e.target.checked ? 1 : 0;
+                    li.toggleClass('complete');
+                    save_task(rec, 'edit');
+                    return true;
+                
+                case 'flagged':
+                    rec.flagged = rec.flagged ? 0 : 1;
+                    li.toggleClass('flagged');
+                    save_task(rec, 'edit');
+                    break;
+                
+                case 'date':
+                    var link = $(e.target).html(''),
+                        input = $('<input type="text" size="10" />').appendTo(link).val(rec.date || '')
+
+                    input.datepicker($.extend({
+                        onClose: function(dateText, inst) {
+                            if (dateText != rec.date) {
+                                rec.date = dateText;
+                                save_task(rec, 'edit');
+                            }
+                            input.datepicker('destroy').remove();
+                            link.html(dateText || rcmail.gettext('nodate','tasklist'));
+                        },
+                      }, extended_datepicker_settings)
+                    )
+                    .datepicker('setDate', rec.date)
+                    .datepicker('show');
+                    break;
+                
+                case 'delete':
+                    delete_task(id);
+                    break;
+
+                case 'actions':
+                    var pos, ref = $(e.target),
+                        menu = $('#taskitemmenu');
+                    if (menu.is(':visible') && menu.data('refid') == id) {
+                        menu.hide();
+                    }
+                    else {
+                        pos = ref.offset();
+                        pos.top += ref.outerHeight();
+                        pos.left += ref.width() - menu.outerWidth();
+                        menu.css({ top:pos.top+'px', left:pos.left+'px' }).show();
+                        menu.data('refid', id);
+                        me.selected_task = rec;
+                    }
+                    e.bubble = false;
+                    break;
+                
+                default:
+                    if (e.target.nodeName != 'INPUT')
+                        task_show_dialog(id);
+                    break;
+            }
+
+            return false;
+        })
+        .dblclick(function(e){
+            var id, rec, item = $(e.target);
+            if (!item.hasClass('taskhead'))
+                item = item.closest('div.taskhead');
+
+            if (item.length && (id = item.data('id')) && (rec = listdata[id])) {
+                var list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {};
+                if (rec.readonly || list.readonly)
+                    task_show_dialog(id);
+                else
+                    task_edit_dialog(id, 'edit');
+                clearSelection();
+            }
+        });
+
+        completeness_slider = $('#edit-completeness-slider').slider({
+            range: 'min',
+            slide: function(e, ui){
+                var v = completeness_slider.slider('value');
+                if (v >= 98) v = 100;
+                if (v <= 2)  v = 0;
+                $('#edit-completeness').val(v);
+            }
+        });
+        $('#edit-completeness').change(function(e){ completeness_slider.slider('value', parseInt(this.value)) });
+
+        // handle global document clicks: close popup menus
+        $(document.body).click(clear_popups);
+
+        // extended datepicker settings
+        extended_datepicker_settings = $.extend({
+            showButtonPanel: true,
+            beforeShow: function(input, inst) {
+                setTimeout(function(){
+                    $(input).datepicker('widget').find('button.ui-datepicker-close')
+                        .html(rcmail.gettext('nodate','tasklist'))
+                        .attr('onclick', '')
+                        .click(function(e){
+                            $(input).datepicker('setDate', null).datepicker('hide');
+                        });
+                }, 1);
+            },
+        }, datepicker_settings);
+    }
+
+    /**
+     * Request counts from the server
+     */
+    function fetch_counts()
+    {
+        rcmail.http_request('counts');
+    }
+
+    /**
+     * fetch tasks from server
+     */
+    function list_tasks(sel)
+    {
+        if (sel && filter_masks[sel] !== undefined) {
+            filtermask = filter_masks[sel];
+            selector = sel;
+        }
+
+        ui_loading = rcmail.set_busy(true, 'loading');
+        rcmail.http_request('fetch', { filter:filtermask, q:search_query }, true);
+
+        $('#taskselector li.selected').removeClass('selected');
+        $('#taskselector li.'+selector).addClass('selected');
+    }
+
+    /**
+     * callback if task data from server is ready
+     */
+    function data_ready(data)
+    {
+        // clear display
+        var msgbox = $('#listmessagebox').hide(),
+            list = $(rcmail.gui_objects.resultlist).html('');
+        listdata = {};
+
+        for (var i=0; i < data.length; i++) {
+            listdata[data[i].id] = data[i];
+            render_task(data[i]);
+        }
+
+        if (!data.length)
+            msgbox.html(rcmail.gettext('notasksfound','tasklist')).show();
+
+        rcmail.set_busy(false, 'loading', ui_loading);
+    }
+
+    /**
+     *
+     */
+    function update_counts(counts)
+    {
+        // got new data
+        if (counts)
+            taskcounts = counts;
+
+        // iterate over all selector links and update counts
+        $('#taskselector a').each(function(i, elem){
+            var link = $(elem),
+                f = link.parent().attr('class').replace(/\s\w+/, '');
+            link.children('span').html(taskcounts[f] || '')[(taskcounts[f] ? 'show' : 'hide')]();
+        });
+
+        // spacial case: overdue
+        $('#taskselector li.overdue')[(taskcounts.overdue ? 'removeClass' : 'addClass')]('inactive');
+    }
+
+    /**
+     * Callback from server to update a single task item
+     */
+    function update_taskitem(rec)
+    {
+        var id = rec.id;
+        listdata[id] = rec;
+        render_task(rec, rec.tempid || id);
+    }
+
+    /**
+     * Submit the given (changed) task record to the server
+     */
+    function save_task(rec, action)
+    {
+        if (!rcmail.busy) {
+            saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
+            rcmail.http_post('task', { action:action, t:rec, filter:filtermask });
+            return true;
+        }
+        
+        return false;
+    }
+
+    /**
+     * Render the given task into the tasks list
+     */
+    function render_task(rec, replace)
+    {
+        var div = $('<div>').addClass('taskhead').html(
+            '<div class="progressbar"><div class="progressvalue" style="width:' + (rec.complete * 100) + '%"></div></div>' +
+            '<input type="checkbox" name="completed[]" value="1" class="complete" ' + (rec.complete == 1.0 ? 'checked="checked" ' : '') + '/>' + 
+            '<span class="flagged"></span>' +
+            '<span class="title">' + Q(rec.title) + '</span>' +
+            '<span class="date">' + Q(rec.date || rcmail.gettext('nodate','tasklist')) + '</span>' +
+            '<a href="#" class="actions">V</a>'
+            )
+            .data('id', rec.id)
+            .draggable({
+                revert: 'invalid',
+                addClasses: false,
+                cursorAt: { left:-10, top:12 },
+                helper: draggable_helper,
+                appendTo: 'body',
+                start: draggable_start,
+                stop: draggable_stop,
+                revertDuration: 300
+            });
+
+        if (rec.complete == 1.0)
+            div.addClass('complete');
+        if (rec.flagged)
+            div.addClass('flagged');
+        if (!rec.date)
+            div.addClass('nodate');
+        if ((rec.mask & FILTER_MASK_OVERDUE))
+            div.addClass('overdue');
+console.log(replace)
+        var li, parent;
+        if (replace && (li = $('li[rel="'+replace+'"]', rcmail.gui_objects.resultlist)) && li.length) {
+            li.children('div.taskhead').first().replaceWith(div);
+            li.attr('rel', rec.id);
+        }
+        else {
+            li = $('<li>')
+                .attr('rel', rec.id)
+                .addClass('taskitem')
+                .append(div)
+                .append('<ul class="childtasks"></ul>');
+
+            if (rec.parent_id && (parent = $('li[rel="'+rec.parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist)) && parent.length)
+                li.appendTo(parent);
+            else
+                li.appendTo(rcmail.gui_objects.resultlist);
+        }
+        
+        if (replace) {
+            resort_task(rec, li, true);
+            // TODO: remove the item after a while if it doesn't match the current filter anymore
+        }
+    }
+
+    /**
+     * Move the given task item to the right place in the list
+     */
+    function resort_task(rec, li, animated)
+    {
+        var dir = 0, next_li, next_id, next_rec;
+
+        // animated moving
+        var insert_animated = function(li, before, after) {
+            if (before && li.next().get(0) == before.get(0))
+                return; // nothing to do
+            else if (after && li.prev().get(0) == after.get(0))
+                return; // nothing to do
+            
+            var speed = 300;
+            li.slideUp(speed, function(){
+                if (before)     li.insertBefore(before);
+                else if (after) li.insertAfter(after);
+                li.slideDown(speed);
+            });
+        }
+
+        // find the right place to insert the task item
+        li.siblings().each(function(i, elem){
+            next_li = $(elem);
+            next_id = next_li.attr('rel');
+            next_rec = listdata[next_id];
+
+            if (next_id == rec.id) {
+                next_li = null;
+                return 1; // continue
+            }
+
+            if (next_rec && task_cmp(rec, next_rec) > 0) {
+                return 1; // continue;
+            }
+            else if (next_rec && next_li && task_cmp(rec, next_rec) < 0) {
+                if (animated) insert_animated(li, next_li);
+                else          li.insertBefore(next_li)
+                next_li = null;
+                return false;
+            }
+        });
+
+        if (next_li) {
+            if (animated) insert_animated(li, null, next_li);
+            else          li.insertAfter(next_li);
+        }
+        return;
+    }
+
+    /**
+     * Compare function of two task records.
+     * (used for sorting)
+     */
+    function task_cmp(a, b)
+    {
+        var d = Math.floor(a.complete) - Math.floor(b.complete);
+        if (!d) d = (b._hasdate-0) - (a._hasdate-0);
+        if (!d) d = (a.datetime||99999999999) - (b.datetime||99999999999);
+        return d;
+    }
+
+
+    /*  Helper functions for drag & drop functionality  */
+    
+    function draggable_helper()
+    {
+        if (!draghelper)
+            draghelper = $('<div class="taskitem-draghelper">&#x2714;</div>');
+
+        return draghelper;
+    }
+
+    function draggable_start(event, ui)
+    {
+        $('.taskhead, #rootdroppable').droppable({
+            hoverClass: 'droptarget',
+            accept: droppable_accept,
+            drop: draggable_dropped,
+            addClasses: false
+        });
+
+        $(this).parent().addClass('dragging');
+        $('#rootdroppable').show();
+    }
+
+    function draggable_stop(event, ui)
+    {
+        $(this).parent().removeClass('dragging');
+        $('#rootdroppable').hide();
+    }
+
+    function droppable_accept(draggable)
+    {
+        var drag_id = draggable.data('id'),
+            parent_id = $(this).data('id'),
+            rec = listdata[parent_id];
+
+        if (parent_id == listdata[drag_id].parent_id)
+            return false;
+
+        while (rec && rec.parent_id) {
+            if (rec.parent_id == drag_id)
+                return false;
+            rec = listdata[rec.parent_id];
+        }
+
+        return true;
+    }
+
+    function draggable_dropped(event, ui)
+    {
+        var parent_id = $(this).data('id'),
+            task_id = ui.draggable.data('id'),
+            parent = parent_id ? $('li[rel="'+parent_id+'"] > ul.childtasks', rcmail.gui_objects.resultlist) : $(rcmail.gui_objects.resultlist),
+            rec = listdata[task_id],
+            li;
+
+        if (rec && parent.length) {
+            // submit changes to server
+            rec.parent_id = parent_id || 0;
+            save_task(rec, 'edit');
+
+            li = ui.draggable.parent();
+            li.slideUp(300, function(){
+                li.appendTo(parent);
+                resort_task(rec, li);
+                li.slideDown(300);
+            });
+        }
+    }
+
+
+    /**
+     * Show task details in a dialog
+     */
+    function task_show_dialog(id)
+    {
+        var $dialog = $('#taskshow').dialog('close'), rec;;
+
+        if (!(rec = listdata[id]) || clear_popups({}))
+            return;
+
+        me.selected_task = rec;
+
+        // fill dialog data
+        $('#task-title').html(Q(rec.title || ''));
+        $('#task-description').html(text2html(rec.description || '', 300, 6))[(rec.description ? 'show' : 'hide')]();
+        $('#task-date')[(rec.date ? 'show' : 'hide')]().children('.task-text').html(Q(rec.date || rcmail.gettext('nodate','tasklist')));
+        $('#task-time').html(Q(rec.time || ''));
+        $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%');
+        $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
+
+        // define dialog buttons
+        var buttons = {};
+        buttons[rcmail.gettext('edit','tasklist')] = function() {
+            task_edit_dialog(me.selected_task.id, 'edit');
+            $dialog.dialog('close');
+        };
+
+        buttons[rcmail.gettext('delete','tasklist')] = function() {
+            if (delete_task(me.selected_task.id))
+                $dialog.dialog('close');
+        };
+
+        // open jquery UI dialog
+        $dialog.dialog({
+          modal: false,
+          resizable: true,
+          closeOnEscape: true,
+          title: rcmail.gettext('taskdetails', 'tasklist'),
+          close: function() {
+            $dialog.dialog('destroy').appendTo(document.body);
+          },
+          buttons: buttons,
+          minWidth: 500,
+          width: 580
+        }).show();
+    }
+
+    /**
+     * Opens the dialog to edit a task
+     */
+    function task_edit_dialog(id, action, presets)
+    {
+        $('#taskshow').dialog('close');
+
+        var rec = listdata[id] || presets,
+            $dialog = $('<div>'),
+            editform = $('#taskedit'),
+            list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] :
+                (selected_list ? me.tasklists[selected_list] : { editable: action=='new' });
+
+        if (list.readonly || (action == 'edit' && (!rec || rec.readonly || rec.temp)))
+            return false;
+
+        me.selected_task = $.extend({}, rec);  // clone task object
+
+        // fill form data
+        var title = $('#edit-title').val(rec.title || '');
+        var description = $('#edit-description').val(rec.description || '');
+        var recdate = $('#edit-date').val(rec.date || '').datepicker(datepicker_settings);
+        var rectime = $('#edit-time').val(rec.time || '');
+        var complete = $('#edit-completeness').val((rec.complete || 0) * 100);
+        completeness_slider.slider('value', complete.val());
+        var tasklist = $('#edit-tasklist').val(rec.list || 0);
+
+        $('#edit-nodate').unbind('click').click(function(){
+            recdate.val('');
+            rectime.val('');
+            return false;
+        })
+
+        // define dialog buttons
+        var buttons = {};
+        buttons[rcmail.gettext('save', 'tasklist')] = function() {
+            me.selected_task.title = title.val();
+            me.selected_task.description = description.val();
+            me.selected_task.date = recdate.val();
+            me.selected_task.time = rectime.val();
+            me.selected_task.list = tasklist.val();
+
+            if (me.selected_task.list && me.selected_task.list != rec.list)
+              me.selected_task._fromlist = rec.list;
+
+            me.selected_task.complete = complete.val() / 100;
+            if (isNaN(me.selected_task.complete))
+                me.selected_task.complete = null;
+
+            if (!me.selected_task.list && list.id)
+                me.selected_task.list = list.id;
+
+            if (save_task(me.selected_task, action))
+                $dialog.dialog('close');
+        };
+
+        if (rec.id) {
+          buttons[rcmail.gettext('delete', 'tasklist')] = function() {
+            if (delete_task(rec.id))
+                $dialog.dialog('close');
+          };
+        }
+
+        buttons[rcmail.gettext('cancel', 'tasklist')] = function() {
+          $dialog.dialog('close');
+        };
+
+        // open jquery UI dialog
+        $dialog.dialog({
+          modal: true,
+          resizable: (!bw.ie6 && !bw.ie7),  // disable for performance reasons
+          closeOnEscape: false,
+          title: rcmail.gettext((action == 'edit' ? 'edittask' : 'newtask'), 'tasklist'),
+          close: function() {
+            editform.hide().appendTo(document.body);
+            $dialog.dialog('destroy').remove();
+          },
+          buttons: buttons,
+          minWidth: 500,
+          width: 580
+        }).append(editform.show());  // adding form content AFTERWARDS massively speeds up opening on IE6
+
+        title.select();
+    }
+
+    /**
+     *
+     */
+    function add_childtask(id)
+    {
+        task_edit_dialog(null, 'new', { parent_id:id });
+    }
+
+    /**
+     * Delete the given task
+     */
+    function delete_task(id)
+    {
+        var rec = listdata[id];
+        if (rec && confirm("Delete this?")) {
+            saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
+            rcmail.http_post('task', { action:'delete', t:rec, filter:filtermask });
+            $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
+            return true;
+        }
+        
+        return false;
+    }
+
+    /**
+     * Check if the given task matches the current filtermask
+     */
+    function match_filter(rec)
+    {
+        // TBD.
+        return true;
+    }
+
+    /**
+     * Execute search
+     */
+    function quicksearch()
+    {
+        var q;
+        if (rcmail.gui_objects.qsearchbox && (q = rcmail.gui_objects.qsearchbox.value)) {
+            var id = 'search-'+q;
+            var resources = [];
+
+            for (var rid in me.tasklists) {
+                if (me.tasklists[rid].active) {
+                    resources.push(rid);
+                }
+            }
+            id += '@'+resources.join(',');
+
+            // ignore if query didn't change
+            if (search_request == id)
+                return;
+
+            search_request = id;
+            search_query = q;
+
+            list_tasks('all');
+        }
+        else  // empty search input equals reset
+            this.reset_search();
+    }
+
+    /**
+     * Reset search and get back to normal listing
+     */
+    function reset_search()
+    {
+        $(rcmail.gui_objects.qsearchbox).val('');
+
+        if (search_request) {
+            search_request = search_query = null;
+            list_tasks();
+        }
+    }
+
+
+    /**** 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)
+     */
+    function clearSelection()
+    {
+        if (document.selection && document.selection.empty) {
+            document.selection.empty() ;
+        }
+        else if (window.getSelection) {
+            var sel = window.getSelection();
+            if (sel && sel.removeAllRanges)
+                sel.removeAllRanges();
+        }
+    }
+
+    /**
+     * Hide all open popup menus
+     */
+    function clear_popups(e)
+    {
+        var count = 0;
+        $('.popupmenu:visible').each(function(i, elem){
+            var menu = $(elem);
+            if (!menu.data('sticky') || !target_overlaps(e.target, elem)) {
+                menu.hide();
+                count++;
+            }
+        });
+        return count;
+    }
+
+    /**
+     * Check whether the event target is a descentand of the given element
+     */
+    function target_overlaps(target, elem)
+    {
+        while (target.parentNode) {
+            if (target.parentNode == elem)
+                return true;
+            target = target.parentNode;
+        }
+        return false;
+    }
+
+}
+
+
+/* tasklist plugin UI initialization */
+var rctasks;
+window.rcmail && rcmail.addEventListener('init', function(evt) {
+
+  rctasks = new rcube_tasklist(rcmail.env.tasklist_settings);
+
+  // register button commands
+  //rcmail.register_command('addtask', function(){ tasks.add_task(); }, true);
+  //rcmail.register_command('print', function(){ tasks.print_list(); }, true);
+
+  rcmail.register_command('search', function(){ rctasks.quicksearch(); }, true);
+  rcmail.register_command('reset-search', function(){ rctasks.reset_search(); }, true);
+
+  rctasks.init();
+});
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
new file mode 100644
index 0000000..2a2d815
--- /dev/null
+++ b/plugins/tasklist/tasklist.php
@@ -0,0 +1,485 @@
+<?php
+
+/**
+ * Tasks plugin for Roundcube webmail
+ *
+ * @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 tasklist extends rcube_plugin
+{
+    const FILTER_MASK_TODAY = 1;
+    const FILTER_MASK_TOMORROW = 2;
+    const FILTER_MASK_WEEK = 4;
+    const FILTER_MASK_LATER = 8;
+    const FILTER_MASK_NODATE = 16;
+    const FILTER_MASK_OVERDUE = 32;
+    const FILTER_MASK_FLAGGED = 64;
+    const FILTER_MASK_COMPLETE = 128;
+
+    public static $filter_masks = array(
+        'today'    => self::FILTER_MASK_TODAY,
+        'tomorrow' => self::FILTER_MASK_TOMORROW,
+        'week'     => self::FILTER_MASK_WEEK,
+        'later'    => self::FILTER_MASK_LATER,
+        'nodate'   => self::FILTER_MASK_NODATE,
+        'overdue'  => self::FILTER_MASK_OVERDUE,
+        'flagged'  => self::FILTER_MASK_FLAGGED,
+        'complete' => self::FILTER_MASK_COMPLETE,
+    );
+
+    public $task = '?(?!login|logout).*';
+    public $rc;
+    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->rc = rcmail::get_instance();
+
+        $this->register_task('tasks', 'tasklist');
+
+        // load plugin configuration
+        $this->load_config();
+
+        // load localizations
+        $this->add_texts('localization/', $this->rc->task == 'tasks' && (!$this->rc->action || $this->rc->action == 'print'));
+
+        if ($this->rc->task == 'tasks' && $this->rc->action != 'save-pref') {
+            $this->load_driver();
+
+            // register calendar actions
+            $this->register_action('index', array($this, 'tasklist_view'));
+            $this->register_action('task', array($this, 'task_action'));
+            $this->register_action('tasklist', array($this, 'tasklist_action'));
+            $this->register_action('counts', array($this, 'fetch_counts'));
+            $this->register_action('fetch', array($this, 'fetch_tasks'));
+        }
+
+        if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
+            require_once($this->home . '/tasklist_ui.php');
+            $this->ui = new tasklist_ui($this);
+            $this->ui->init();
+        }
+    }
+
+
+    /**
+     * Helper method to load the backend driver according to local config
+     */
+    private function load_driver()
+    {
+        if (is_object($this->driver))
+            return;
+
+        $driver_name = $this->rc->config->get('tasklist_driver', 'database');
+        $driver_class = 'tasklist_' . $driver_name . '_driver';
+
+        require_once($this->home . '/drivers/tasklist_driver.php');
+        require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
+
+        switch ($driver_name) {
+        case "kolab":
+            $this->require_plugin('libkolab');
+        default:
+            $this->driver = new $driver_class($this);
+            break;
+        }
+
+        // get user's timezone
+        $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
+    }
+
+
+    /**
+     *
+     */
+    public function task_action()
+    {
+        $action = get_input_value('action', RCUBE_INPUT_GPC);
+        $rec  = get_input_value('t', RCUBE_INPUT_POST, true);
+        $oldrec = $rec;
+        $success = $refresh = false;
+
+        switch ($action) {
+        case 'new':
+            $oldrec = null;
+            $rec = $this->prepare_task($rec);
+            $rec['uid'] = $this->generate_uid();
+            $temp_id = $rec['tempid'];
+            if ($success = $this->driver->create_task($rec)) {
+                $refresh = $this->driver->get_task($rec);
+                if ($temp_id) $refresh['tempid'] = $temp_id;
+            }
+            break;
+
+        case 'edit':
+            $rec = $this->prepare_task($rec);
+            if ($success = $this->driver->edit_task($rec))
+                $refresh = $this->driver->get_task($rec);
+            break;
+
+        case 'delete':
+            if (!($success = $this->driver->delete_task($rec, false)))
+                $this->rc->output->command('plugin.reload_data');
+            break;
+
+        case 'undelete':
+            if ($success = $this->driver->undelete_task($rec))
+                $refresh = $this->driver->get_task($rec);
+            break;
+        }
+
+        if ($success) {
+            $this->rc->output->show_message('successfullysaved', 'confirmation');
+            $this->update_counts($oldrec, $refresh);
+        }
+        else
+            $this->rc->output->show_message('tasklist.errorsaving', 'error');
+
+        // unlock client
+        $this->rc->output->command('plugin.unlock_saving');
+
+        if ($refresh) {
+            $this->encode_task($refresh);
+            $this->rc->output->command('plugin.refresh_task', $refresh);
+        }
+    }
+
+    /**
+     * repares new/edited task properties before save
+     */
+    private function prepare_task($rec)
+    {
+        // try to be smart and extract date from raw input
+        if ($rec['raw']) {
+            foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) {
+                $locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i';
+                $normwords[] = $word;
+                $datewords[] = $word;
+            }
+            foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) {
+                $locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i';
+                $normwords[] = $month;
+                $datewords[] = $month;
+            }
+            foreach (array('on','this','next','at') as $word) {
+                $fillwords[] = preg_quote(mb_strtolower($this->gettext($word)));
+                $fillwords[] = $word;
+            }
+
+            $raw = trim($rec['raw']);
+            $date_str = '';
+
+            // translate localized keywords
+            $raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw);
+            $raw = preg_replace($locwords, $normwords, $raw);
+
+            // find date pattern
+            $date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i';
+            if (preg_match($date_pattern, $raw, $m)) {
+                $date_str .= $m[1] . $m[2] . $m[3];
+                $raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw);
+                // add year to date string
+                if ($m[1] && !$m[3])
+                    $date_str .= date('Y');
+            }
+
+            // find time pattern
+            $time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i';
+            if (preg_match($time_pattern, $raw, $m)) {
+                $has_time = true;
+                $date_str .= ($date_str ? ' ' : 'today ') . $m[1];
+                $raw = preg_replace($time_pattern, '', $raw);
+            }
+
+            // yes, raw input matched a (valid) date
+            if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) {
+                $rec['date'] = $date->format('Y-m-d');
+                if ($has_time)
+                    $rec['time'] = $date->format('H:i');
+                $rec['title'] = $raw;
+            }
+            else
+                $rec['title'] = $rec['raw'];
+        }
+
+        // normalize input from client
+        if (isset($rec['complete'])) {
+            $rec['complete'] = floatval($rec['complete']);
+            if ($rec['complete'] > 1)
+                $rec['complete'] /= 100;
+        }
+        if (isset($rec['flagged']))
+            $rec['flagged'] = intval($rec['flagged']);
+
+        // fix for garbage input
+        if ($rec['description'] == 'null')
+            $rec['description'] = '';
+
+        foreach ($rec as $key => $val) {
+            if ($val == 'null')
+                $rec[$key] = null;
+        }
+
+        if (!empty($rec['date'])) {
+            try {
+                $date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
+                $rec['date'] = $date->format('Y-m-d');
+                if (!empty($rec['time']))
+                    $rec['time'] = $date->format('H:i');
+            }
+            catch (Exception $e) {
+                $rec['date'] = $rec['time'] = null;
+            }
+        }
+
+        return $rec;
+    }
+
+    /**
+     *
+     */
+    public function tasklist_action()
+    {
+        $action = get_input_value('action', RCUBE_INPUT_GPC);
+        $list  = get_input_value('l', RCUBE_INPUT_POST, true);
+        $success = false;
+
+        switch ($action) {
+            
+        }
+
+        if ($success)
+            $this->rc->output->show_message('successfullysaved', 'confirmation');
+        else
+            $this->rc->output->show_message('tasklist.errorsaving', 'error');
+    }
+
+    /**
+     *
+     */
+    public function fetch_counts()
+    {
+        $lists = null;
+        $counts = $this->driver->count_tasks($lists);
+        $this->rc->output->command('plugin.update_counts', $counts);
+    }
+
+    /**
+     * Adjust the cached counts after changing a task
+     *
+     * 
+     */
+    public function update_counts($oldrec, $newrec)
+    {
+        // rebuild counts until this function is finally implemented
+        $this->fetch_counts();
+
+        // $this->rc->output->command('plugin.update_counts', $counts);
+    }
+
+    /**
+     *
+     */
+    public function fetch_tasks()
+    {
+        $f = intval(get_input_value('filter', RCUBE_INPUT_GPC));
+        $search = get_input_value('q', RCUBE_INPUT_GPC);
+        $filter = array('mask' => $f, 'search' => $search);
+        $lists = null;
+
+        // convert magic date filters into a real date range
+        switch ($f) {
+        case self::FILTER_MASK_TODAY:
+            $today = new DateTime('now', $this->timezone);
+            $filter['from'] = $filter['to'] = $today->format('Y-m-d');
+            break;
+
+        case self::FILTER_MASK_TOMORROW:
+            $tomorrow = new DateTime('now + 1 day', $this->timezone);
+            $filter['from'] = $filter['to'] = $tomorrow->format('Y-m-d');
+            break;
+
+        case self::FILTER_MASK_OVERDUE:
+            $yesterday = new DateTime('yesterday', $this->timezone);
+            $filter['to'] = $yesterday->format('Y-m-d');
+            break;
+
+        case self::FILTER_MASK_WEEK:
+            $today = new DateTime('now', $this->timezone);
+            $filter['from'] = $today->format('Y-m-d');
+            $weekend = new DateTime('now + 7 days', $this->timezone);
+            $filter['to'] = $weekend->format('Y-m-d');
+            break;
+
+        case self::FILTER_MASK_LATER:
+            $date = new DateTime('now + 8 days', $this->timezone);
+            $filter['from'] = $date->format('Y-m-d');
+            break;
+
+        }
+
+        $data = $this->task_tree = $this->tasks_childs = array();
+        foreach ($this->driver->list_tasks($filter, $lists) as $rec) {
+            if ($rec['parent_id']) {
+                $this->tasks_childs[$rec['parent_id']]++;
+                $this->task_tree[$rec['id']] = $rec['parent_id'];
+            }
+            $this->encode_task($rec);
+
+            // apply filter; don't trust the driver on this :-)
+            if ((!$f && $rec['complete'] < 1.0) || ($rec['mask'] & $f))
+                $data[] = $rec;
+        }
+
+        // sort tasks according to their hierarchy level and due date
+        usort($data, array($this, 'task_sort_cmp'));
+
+        $this->rc->output->command('plugin.data_ready', $data);
+    }
+
+    /**
+     * Prepare the given task record before sending it to the client
+     */
+    private function encode_task(&$rec)
+    {
+        $rec['mask'] = $this->filter_mask($rec);
+        $rec['flagged'] = intval($rec['flagged']);
+        $rec['complete'] = floatval($rec['complete']);
+
+        if ($rec['date']) {
+            try {
+                $date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
+                $rec['datetime'] = intval($date->format('U'));
+                $rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
+                $rec['_hasdate'] = 1;
+            }
+            catch (Exception $e) {
+                $rec['date'] = $rec['datetime'] = null;
+            }
+        }
+        else {
+            $rec['date'] = $rec['datetime'] = null;
+            $rec['_hasdate'] = 0;
+        }
+
+        if ($this->tasks_childs[$rec['id']])
+            $rec['_haschilds'] = $this->tasks_childs[$rec['id']];
+
+        if (!isset($rec['_depth'])) {
+            $rec['_depth'] = 0;
+            $parent_id = $this->task_tree[$rec['id']];
+            while ($parent_id) {
+                $rec['_depth']++;
+                $parent_id = $this->task_tree[$parent_id];
+            }
+        }
+    }
+
+    /**
+     * Compare function for task list sorting.
+     * Nested tasks need to be sorted to the end.
+     */
+    private function task_sort_cmp($a, $b)
+    {
+        $d = $a['_depth'] - $b['_depth'];
+        if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
+        if (!$d) $d = $a['datetime'] - $b['datetime'];
+        return $d;
+    }
+
+    /**
+     * Compute the filter mask of the given task
+     *
+     * @param array Hash array with Task record properties
+     * @return int Filter mask
+     */
+    public function filter_mask($rec)
+    {
+        static $today, $tomorrow, $weeklimit;
+
+        if (!$today) {
+            $today_date = new DateTime('now', $this->timezone);
+            $today = $today_date->format('Y-m-d');
+            $tomorrow_date = new DateTime('now + 1 day', $this->timezone);
+            $tomorrow = $tomorrow_date->format('Y-m-d');
+            $week_date = new DateTime('now + 7 days', $this->timezone);
+            $weeklimit = $week_date->format('Y-m-d');
+        }
+        
+        $mask = 0;
+        if ($rec['flagged'])
+            $mask |= self::FILTER_MASK_FLAGGED;
+        if ($rec['complete'] == 1.0)
+            $mask |= self::FILTER_MASK_COMPLETE;
+        if (empty($rec['date']))
+            $mask |= self::FILTER_MASK_NODATE;
+        else if ($rec['date'] == $today)
+            $mask |= self::FILTER_MASK_TODAY;
+        else if ($rec['date'] == $tomorrow)
+            $mask |= self::FILTER_MASK_TOMORROW;
+        else if ($rec['date'] < $today)
+            $mask |= self::FILTER_MASK_OVERDUE;
+        else if ($rec['date'] > $tomorrow && $rec['date'] <= $weeklimit)
+            $mask |= self::FILTER_MASK_LATER;
+        else if ($rec['date'] > $weeklimit)
+            $mask |= self::FILTER_MASK_LATER;
+
+        return $mask;
+    }
+
+
+    /*******  UI functions  ********/
+
+    /**
+     * Render main view of the tasklist task
+     */
+    public function tasklist_view()
+    {
+        $this->ui->init();
+        $this->ui->init_templates();
+        $this->rc->output->set_pagetitle($this->gettext('navtitle'));
+        $this->rc->output->send('tasklist.mainview');
+    }
+
+
+    /*******  Utility functions  *******/
+
+    /**
+     * Generate a unique identifier for an event
+     */
+    public function generate_uid()
+    {
+      return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
+    }
+
+}
+
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
new file mode 100644
index 0000000..50ae64a
--- /dev/null
+++ b/plugins/tasklist/tasklist_ui.php
@@ -0,0 +1,181 @@
+<?php
+/**
+ * User Interface class for the Tasklist plugin
+ *
+ * @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 tasklist_ui
+{
+    private $rc;
+    private $plugin;
+    private $ready = false;
+
+    function __construct($plugin)
+    {
+        $this->plugin = $plugin;
+        $this->rc = $plugin->rc;
+    }
+
+    /**
+    * Calendar UI initialization and requests handlers
+    */
+    public function init()
+    {
+        if ($this->ready)  // already done
+            return;
+
+        // add taskbar button
+        $this->plugin->add_button(array(
+            'command' => 'tasks',
+            'class'   => 'button-tasklist',
+            'classsel' => 'button-tasklist button-selected',
+            'innerclass' => 'button-inner',
+            'label'   => 'tasklist.navtitle',
+        ), 'taskbar');
+
+        $skin = $this->rc->config->get('skin');
+        $this->plugin->include_stylesheet('skins/' . $skin . '/tasklist.css');
+        $this->ready = true;
+  }
+
+    /**
+    * Register handler methods for the template engine
+    */
+    public function init_templates()
+    {
+        $this->plugin->register_handler('plugin.tasklists', array($this, 'tasklists'));
+        $this->plugin->register_handler('plugin.tasklist_select', array($this, 'tasklist_select'));
+        $this->plugin->register_handler('plugin.category_select', array($this, 'category_select'));
+        $this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));
+        $this->plugin->register_handler('plugin.quickaddform', array($this, 'quickadd_form'));
+        $this->plugin->register_handler('plugin.tasks', array($this, 'tasks_resultview'));
+
+        $this->plugin->include_script('tasklist.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);
+    }
+
+    /**
+     *
+     */
+    function tasklists($attrib = array())
+    {
+        $lists = $this->plugin->driver->get_lists();
+
+        $li = '';
+        foreach ((array)$lists as $id => $prop) {
+            if ($attrib['activeonly'] && !$prop['active'])
+              continue;
+
+            unset($prop['user_id']);
+            $prop['alarms'] = $this->plugin->driver->alarms;
+            $prop['undelete'] = $this->plugin->driver->undelete;
+            $prop['sortable'] = $this->plugin->driver->sortable;
+            $jsenv[$id] = $prop;
+
+            $html_id = html_identifier($id);
+            $class = 'tasks-'  . asciiwords($id, true);
+
+            if ($prop['readonly'])
+                $class .= ' readonly';
+            if ($prop['class_name'])
+                $class .= ' '.$prop['class_name'];
+
+            $li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class),
+                html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'disabled' => true)) .
+                html::span('handle', ' ') .
+                html::span('listname', Q($prop['name'])));
+        }
+
+        $this->rc->output->set_env('tasklists', $jsenv);
+        $this->rc->output->add_gui_object('folderlist', $attrib['id']);
+
+        return html::tag('ul', $attrib, $li, html::$common_attrib);
+    }
+
+
+    /**
+     * Render a HTML select box for list selection
+     */
+    function tasklist_select($attrib = array())
+    {
+        $attrib['name'] = 'list';
+        $select = new html_select($attrib);
+        foreach ((array)$this->plugin->driver->get_lists() as $id => $prop) {
+            if (!$prop['readonly'])
+                $select->add($prop['name'], $id);
+        }
+
+        return $select->show(null);
+    }
+
+    /**
+     * Render a HTML select box to select a task category
+     */
+    function category_select($attrib = array())
+    {
+        $attrib['name'] = 'categories';
+        $select = new html_select($attrib);
+        $select->add('---', '');
+        foreach ((array)$this->plugin->driver->list_categories() as $cat => $color) {
+            $select->add($cat, $cat);
+        }
+
+        return $select->show(null);
+    }
+
+    /**
+     *
+     */
+    function quickadd_form($attrib)
+    {
+        $attrib += array('action' => $this->rc->url('add'), 'method' => 'post', 'id' => 'quickaddform');
+
+        $input = new html_inputfield(array('name' => 'text', 'id' => 'quickaddinput', 'placeholder' => $this->plugin->gettext('createnewtask')));
+        $button = html::tag('input', array('type' => 'submit', 'value' => '+', 'class' => 'button mainaction'));
+
+        $this->rc->output->add_gui_object('quickaddform', $attrib['id']);
+        return html::tag('form', $attrib, $input->show() . $button);
+    }
+
+    /**
+     * The result view
+     */
+    function tasks_resultview($attrib)
+    {
+        $attrib += array('id' => 'rcmtaskslist');
+
+        $this->rc->output->add_gui_object('resultlist', $attrib['id']);
+
+        unset($attrib['name']);
+        return html::tag('ul', $attrib, '');
+    }
+
+
+}





More information about the commits mailing list