33 commits - plugins/calendar plugins/kolab_addressbook plugins/kolab_auth plugins/kolab_delegation plugins/libcalendaring plugins/libkolab plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Fri May 23 14:30:25 CEST 2014


 plugins/calendar/calendar.php                                  |   26 
 plugins/calendar/calendar_ui.js                                |  165 ++-
 plugins/calendar/drivers/calendar_driver.php                   |    9 
 plugins/calendar/drivers/database/database_driver.php          |   15 
 plugins/calendar/drivers/kolab/kolab_calendar.php              |  117 +-
 plugins/calendar/drivers/kolab/kolab_driver.php                |  227 +++-
 plugins/calendar/drivers/kolab/kolab_user_calendar.php         |  396 ++++++++
 plugins/calendar/lib/calendar_ui.php                           |  188 ++-
 plugins/calendar/localization/en_US.inc                        |    3 
 plugins/calendar/skins/classic/calendar.css                    |  141 ++
 plugins/calendar/skins/classic/images/calendars.gif            |binary
 plugins/calendar/skins/classic/images/calendars.png            |binary
 plugins/calendar/skins/classic/templates/calendar.html         |    9 
 plugins/calendar/skins/larry/calendar.css                      |  174 ++-
 plugins/calendar/skins/larry/images/calendars.png              |binary
 plugins/calendar/skins/larry/templates/calendar.html           |  175 +++
 plugins/kolab_addressbook/kolab_addressbook.php                |    2 
 plugins/kolab_auth/kolab_auth_ldap.php                         |    7 
 plugins/kolab_delegation/kolab_delegation_engine.php           |    6 
 plugins/libcalendaring/libvcalendar.php                        |    5 
 plugins/libkolab/config.inc.php.dist                           |   21 
 plugins/libkolab/js/folderlist.js                              |  225 ++++
 plugins/libkolab/lib/kolab_storage.php                         |  473 ++++++++--
 plugins/libkolab/lib/kolab_storage_folder.php                  |  170 ---
 plugins/libkolab/lib/kolab_storage_folder_api.php              |  298 ++++++
 plugins/libkolab/lib/kolab_storage_folder_user.php             |  101 ++
 plugins/libkolab/lib/kolab_storage_folder_virtual.php          |   59 +
 plugins/tasklist/drivers/database/tasklist_database_driver.php |   12 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php       |  313 +++++-
 plugins/tasklist/drivers/tasklist_driver.php                   |    9 
 plugins/tasklist/localization/en_US.inc                        |    3 
 plugins/tasklist/skins/larry/sprites.png                       |binary
 plugins/tasklist/skins/larry/tasklist.css                      |  145 ++-
 plugins/tasklist/skins/larry/templates/mainview.html           |   37 
 plugins/tasklist/tasklist.js                                   |  157 ++-
 plugins/tasklist/tasklist.php                                  |   50 +
 plugins/tasklist/tasklist_ui.php                               |  127 ++
 37 files changed, 3151 insertions(+), 714 deletions(-)

New commits:
commit 5763fb81e817a1d58cbe57033defa438840165cd
Merge: 69d9d3b 6454bb5
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri May 23 10:12:29 2014 +0200

    Merge new folder navigation from branch 'dev/new-foldernav'

diff --cc plugins/tasklist/skins/larry/tasklist.css
index f8d9d0f,93e0b2b..54372fa
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@@ -74,8 -74,34 +74,34 @@@ body.attachmentwin #topnav .topright 
  	bottom: 0px;
  }
  
+ #tasklistsbox .boxtitle a.iconbutton.search {
+ 	position: absolute;
+ 	top: 8px;
+ 	right: 8px;
+ 	width: 16px;
+ 	cursor: pointer;
+ 	background-position: -2px -317px;
+ }
+ 
+ #tasklistsbox .listsearchbox {
+ 	display: none;
+ }
+ 
+ #tasklistsbox .listsearchbox.expanded {
+ 	display: block;
+ }
+ 
+ #tasklistsbox .scroller {
+ 	top: 34px;
+ }
+ 
+ #tasklistsbox .listsearchbox.expanded + .scroller {
+ 	top: 68px;
+ }
+ 
+ 
  #taskselector {
 -	margin: -4px 40px 0 0;
 +	margin: -1px 40px 0 0;
  	padding: 0;
  }
  
diff --cc plugins/tasklist/tasklist_ui.php
index f6d4b3a,05acafd..377fe68
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@@ -102,48 -170,36 +170,51 @@@ class tasklist_u
              $prop['undelete'] = $this->plugin->driver->undelete;
              $prop['sortable'] = $this->plugin->driver->sortable;
              $prop['attachments'] = $this->plugin->driver->attachments;
- 
-             if (!$prop['virtual'])
-                 $jsenv[$id] = $prop;
- 
-             $html_id = html_identifier($id);
-             $class = 'tasks-'  . asciiwords($id, true);
-             $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
- 
-             if ($prop['virtual'])
-                 $class .= ' virtual';
-             else if (!$prop['editable'])
-                 $class .= ' readonly';
-             if ($prop['class_name'])
-                 $class .= ' '.$prop['class_name'];
- 
-             $li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class),
-                 ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active']))) .
-                 html::span(array('class' => 'handle', 'title' => $this->plugin->gettext('focusview')), ' ') .
-                 html::span(array('class' => 'listname', 'title' => $title), $prop['listname']));
+             $jsenv[$id] = $prop;
          }
  
-         $this->rc->output->set_env('tasklists', $jsenv);
-         $this->rc->output->add_gui_object('folderlist', $attrib['id']);
+         $classes = array('tasklist');
+         $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+           html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
+ 
+         if ($prop['virtual'])
+             $classes[] = 'virtual';
+         else if (!$prop['editable'])
+             $classes[] = 'readonly';
+         if ($prop['subscribed'])
+             $classes[] = 'subscribed';
+         if ($prop['class'])
+             $classes[] = $prop['class'];
+ 
+         if (!$activeonly || $prop['active']) {
+             return html::div(join(' ', $classes),
+                 html::span(array('class' => 'listname', 'title' => $title), $prop['listname'] ?: $prop['name']) .
+                   ($prop['virtual'] ? '' :
+                     html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
+                     html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), ' ') .
+                     (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe')), ' ') : '')
+                 )
+             );
+         }
  
-         return html::tag('ul', $attrib, $li, html::$common_attrib);
+         return '';
      }
  
 +    /**
 +     * Render HTML form for task status selector
 +     */
 +    function status_select($attrib = array())
 +    {
 +        $attrib['name'] = 'status';
 +        $select = new html_select($attrib);
 +        $select->add('---', '');
 +        $select->add($this->plugin->gettext('status-needs-action'), 'NEEDS-ACTION');
 +        $select->add($this->plugin->gettext('status-in-process'),   'IN-PROCESS');
 +        $select->add($this->plugin->gettext('status-completed'),    'COMPLETED');
 +        $select->add($this->plugin->gettext('status-cancelled'),    'CANCELLED');
 +
 +        return $select->show(null);
 +    }
  
      /**
       * Render a HTML select box for list selection


commit 6454bb57cfbf59846eb5f6f8f80a620e95f73682
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri May 23 10:08:20 2014 +0200

    Small fix when listing subscribed user folders

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index dd61513..28c22e9 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -792,8 +792,9 @@ class kolab_storage
             $folders = self::$imap->list_folders_subscribed($root, $mbox);
 
             // add temporarily subscribed folders
-            if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders']))
+            if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
                 $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+            }
         }
         else {
             $folders = self::_imap_list_folders($root, $mbox);
@@ -1494,12 +1495,18 @@ class kolab_storage
             $other_ns = rtrim(self::namespace_root('other'), $delimiter);
             $path_len = count(explode($delimiter, $other_ns));
 
-            foreach ((array)self::list_folders($other_ns . $delimiter, '*', $type, $subscribed) as $foldername) {
+            foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
                 if ($foldername == 'INBOX')  // skip INBOX which is added by default
                     continue;
 
-                // truncate folder path to top-level folders of the 'other' namespace
                 $path = explode($delimiter, $foldername);
+
+                // compare folder type if a subfolder is listed
+                if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
+                    continue;
+                }
+
+                // truncate folder path to top-level folders of the 'other' namespace
                 $foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
 
                 if (!$folders[$foldername]) {


commit 5079da4a90dc69a6de4d8a674fa6c5612c7d743a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri May 23 09:32:43 2014 +0200

    Don't fetch all annotations from 'other' namespace when called from kolab_storage::list_user_folders()

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 609ac5e..dd61513 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -760,12 +760,17 @@ class kolab_storage
 
             return $folders;
         }
-
         $prefix = $root . $mbox;
         $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
 
-        // get folders types
-        $folderdata = self::folders_typedata($prefix);
+        // get folders types for all folders
+        if (!$subscribed || $prefix == '*' || !self::$config->get('kolab_skip_namespace')) {
+            $folderdata = self::folders_typedata($prefix);
+        }
+        else {
+            // fetch folder types for the effective list of (subscribed) folders when post-filtering
+            $folderdata = array();
+        }
 
         if (!is_array($folderdata)) {
             return array();
@@ -1489,7 +1494,7 @@ class kolab_storage
             $other_ns = rtrim(self::namespace_root('other'), $delimiter);
             $path_len = count(explode($delimiter, $other_ns));
 
-            foreach ((array)self::list_folders($other_ns, '*', $type, $subscribed) as $foldername) {
+            foreach ((array)self::list_folders($other_ns . $delimiter, '*', $type, $subscribed) as $foldername) {
                 if ($foldername == 'INBOX')  // skip INBOX which is added by default
                     continue;
 


commit bb0312280cc72e2a5b48a08ba85b6a6282f54630
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri May 23 09:18:35 2014 +0200

    Avoid PHP errors when IMAP doesn't provide valid data

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 1fd3d58..609ac5e 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1023,14 +1023,15 @@ class kolab_storage
                     // GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
                     if ($ns_root == '') {
                         foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
-                            if (!in_array($folder, $blacklist)) {
+                            if (!in_array($folder, $blacklist) &&
+                                ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
                                 $folderdata[$folder] = $metadata;
-                                $folderdata += self::$imap->get_metadata($folder.$delimiter.'*', $type_keys);
+                                $folderdata += $data;
                             }
                         }
                     }
-                    else {
-                        $folderdata += self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys);
+                    else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
+                        $folderdata += $data;
                     }
                 }
             }
@@ -1324,7 +1325,7 @@ class kolab_storage
 
         // check if we have any folder in personal namespace
         // folder(s) may exist but not subscribed
-        foreach ($folders as $f => $data) {
+        foreach ((array)$folders as $f => $data) {
             if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
                 $folder = $f;
                 break;


commit d9247aa5e55259c20dc474bd8ab157d3b6bf2724
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri May 23 09:00:46 2014 +0200

    Add config option to exclude certain namespaces from grouware folder listing

diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index a95e1c2..4df2d85 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -79,7 +79,7 @@ class kolab_driver extends calendar_driver
       return $this->calendars;
 
     // get all folders that have "event" type, sorted by namespace/name
-    $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders(true));
+    $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders('event', true));
     $this->calendars = array();
 
     foreach ($folders as $folder) {
diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist
index 3a3c287..fd8ac84 100644
--- a/plugins/libkolab/config.inc.php.dist
+++ b/plugins/libkolab/config.inc.php.dist
@@ -16,6 +16,10 @@ $rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
 // folders in calendar view or available addressbooks
 $rcmail_config['kolab_use_subscriptions'] = false;
 
+// List any of 'personal','shared','other' namespaces to be excluded from groupware folder listing
+// example: array('other');
+$rcmail_config['kolab_skip_namespace'] = null;
+
 // Enables the use of displayname folder annotations as introduced in KEP:?
 // for displaying resource folder names (experimental!)
 $rcmail_config['kolab_custom_display_names'] = false;
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 4266fb5..1fd3d58 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -40,7 +40,9 @@ class kolab_storage
     public static $encode_ids = false;
 
     private static $ready = false;
+    private static $with_tempsubs = true;
     private static $subscriptions;
+    private static $typedata = array();
     private static $states;
     private static $config;
     private static $imap;
@@ -313,7 +315,7 @@ class kolab_storage
             }
         }
 
-        return '/';
+        return '';
     }
 
 
@@ -748,11 +750,12 @@ class kolab_storage
             if ($subscribed) {
                 $folders = self::$imap->list_folders_subscribed($root, $mbox);
                 // add temporarily subscribed folders
-                if (is_array($_SESSION['kolab_subscribed_folders']))
+                if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
                     $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+                }
             }
             else {
-                $folders = self::$imap->list_folders($root, $mbox);
+                $folders = self::_imap_list_folders($root, $mbox);
             }
 
             return $folders;
@@ -784,11 +787,11 @@ class kolab_storage
             $folders = self::$imap->list_folders_subscribed($root, $mbox);
 
             // add temporarily subscribed folders
-            if (is_array($_SESSION['kolab_subscribed_folders']))
+            if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders']))
                 $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
         }
         else {
-            $folders = self::$imap->list_folders($root, $mbox);
+            $folders = self::_imap_list_folders($root, $mbox);
         }
 
         // In case of an error, return empty list (?)
@@ -798,6 +801,11 @@ class kolab_storage
 
         // Filter folders list
         foreach ($folders as $idx => $folder) {
+            // lookup folder type
+            if (!array_key_exists($folder, $folderdata)) {
+                $folderdata[$folder] = self::folder_type($folder);
+            }
+
             $type = $folderdata[$folder];
 
             if ($filter == 'mail' && empty($type)) {
@@ -811,6 +819,38 @@ class kolab_storage
         return $folders;
     }
 
+    /**
+     * Wrapper for rcube_imap::list_folders() with optional post-filtering
+     */
+    protected static function _imap_list_folders($root, $mbox)
+    {
+        $postfilter = null;
+
+        // compose a post-filter expression for the excluded namespaces
+        if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+            $excludes = array();
+            foreach ((array)$skip_ns as $ns) {
+                if ($ns_root = self::namespace_root($ns)) {
+                    $excludes[] = $ns_root;
+                }
+            }
+
+            if (count($excludes)) {
+                $postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
+            }
+        }
+
+        // use normal LIST command to return all folders, it's fast enough
+        $folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
+
+        if (!empty($postfilter)) {
+            $folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
+            $folders = self::$imap->sort_folder_list($folders);
+        }
+
+        return $folders;
+    }
+
 
     /**
      * Search for shared or otherwise not listed groupware folders the user has access
@@ -959,13 +999,54 @@ class kolab_storage
             return false;
         }
 
-        $folderdata = self::$imap->get_metadata($prefix, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
+        // return cached result
+        if (is_array(self::$typedata[$prefix])) {
+            return self::$typedata[$prefix];
+        }
+
+        $type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
+
+        // fetch metadata from *some* folders only
+        if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+            $delimiter = self::$imap->get_hierarchy_delimiter();
+            $folderdata = $blacklist = array();
+            foreach ((array)$skip_ns as $ns) {
+                if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
+                    $blacklist[] = $ns_root;
+                }
+            }
+            foreach (array('personal','other','shared') as $ns) {
+                if (!in_array($ns, (array)$skip_ns)) {
+                    $ns_root = rtrim(self::namespace_root($ns), $delimiter);
+
+                    // list top-level folders and their childs one by one
+                    // GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
+                    if ($ns_root == '') {
+                        foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
+                            if (!in_array($folder, $blacklist)) {
+                                $folderdata[$folder] = $metadata;
+                                $folderdata += self::$imap->get_metadata($folder.$delimiter.'*', $type_keys);
+                            }
+                        }
+                    }
+                    else {
+                        $folderdata += self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys);
+                    }
+                }
+            }
+        }
+        else {
+            $folderdata = self::$imap->get_metadata($prefix, $type_keys);
+        }
 
         if (!is_array($folderdata)) {
             return false;
         }
 
-        return array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+        // keep list in memory
+        self::$typedata[$prefix] = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+
+        return self::$typedata[$prefix];
     }
 
 
@@ -996,6 +1077,11 @@ class kolab_storage
     {
         self::setup();
 
+        // return in-memory cached result
+        if (is_array(self::$typedata['*']) && array_key_exists($folder, self::$typedata['*'])) {
+            return self::$typedata['*'][$folder];
+        }
+
         $metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
 
         if (!is_array($metadata)) {
@@ -1045,7 +1131,9 @@ class kolab_storage
     {
         if (self::$subscriptions === null) {
             self::setup();
+            self::$with_tempsubs = false;
             self::$subscriptions = self::$imap->list_folders_subscribed();
+            self::$with_tempsubs = true;
         }
 
         return in_array($folder, self::$subscriptions) ||
@@ -1177,7 +1265,9 @@ class kolab_storage
         else {
             self::setup();
             if (self::$subscriptions === null) {
+                self::$with_tempsubs = false;
                 self::$subscriptions = self::$imap->list_folders_subscribed();
+                self::$with_tempsubs = true;
             }
             self::$states = self::$subscriptions;
             $folders = implode(self::$states, '**');
@@ -1372,8 +1462,9 @@ class kolab_storage
         if (!empty($user[$user_attrib])) {
             list($mbox) = explode('@', $user[$user_attrib]);
 
+            $delimiter = self::$imap->get_hierarchy_delimiter();
             $other_ns = self::namespace_root('other');
-            $folders = self::list_folders($other_ns . $mbox, '*', $type, $subscribed, $folderdata);
+            $folders = self::list_folders($other_ns . $mbox . $delimiter, '*', $type, $subscribed, $folderdata);
         }
 
         return $folders;
@@ -1383,11 +1474,12 @@ class kolab_storage
     /**
      * Get a list of (virtual) top-level folders from the other users namespace
      *
+     * @param string  Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
      * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
      *
      * @return array List of kolab_storage_folder_user objects
      */
-    public static function get_user_folders($subscribed)
+    public static function get_user_folders($type, $subscribed)
     {
         $folders = $folderdata = array();
 
@@ -1396,7 +1488,7 @@ class kolab_storage
             $other_ns = rtrim(self::namespace_root('other'), $delimiter);
             $path_len = count(explode($delimiter, $other_ns));
 
-            foreach ((array)self::list_folders($other_ns, '*', '', $subscribed) as $foldername) {
+            foreach ((array)self::list_folders($other_ns, '*', $type, $subscribed) as $foldername) {
                 if ($foldername == 'INBOX')  // skip INBOX which is added by default
                     continue;
 


commit 95491f0886228da853fd48257e1dfabc400bbea1
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu May 22 19:00:49 2014 +0200

    Don't write session data when fetching events/tasks

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 3e5d28a..79df28a 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -302,6 +302,9 @@ class kolab_calendar extends kolab_storage_folder_api
       }
     }
 
+    // avoid session race conditions that will loose temporary subscriptions
+    $this->cal->rc->session->nowrite();
+
     return $events;
   }
 
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index 1ab8679..dc9bcf7 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -222,6 +222,9 @@ class kolab_user_calendar extends kolab_calendar
       }
     }
 
+    // avoid session race conditions that will loose temporary subscriptions
+    $this->cal->rc->session->nowrite();
+
     return $events;
   }
 
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 50fa371..90c3048 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -481,6 +481,9 @@ class tasklist_kolab_driver extends tasklist_driver
             }
         }
 
+        // avoid session race conditions that will loose temporary subscriptions
+        $this->plugin->rc->session->nowrite();
+
         return $counts;
     }
 
@@ -537,6 +540,9 @@ class tasklist_kolab_driver extends tasklist_driver
             }
         }
 
+        // avoid session race conditions that will loose temporary subscriptions
+        $this->plugin->rc->session->nowrite();
+
         return $results;
     }
 
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 75ba386..76d50d5 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -164,7 +164,7 @@ function rcube_tasklist_ui(settings)
                 var prop = { id:p.id, active:list.active?1:0 };
                 if (list.subscribed) prop.permanent = 1;
                 rcmail.http_post('tasklist', { action:'subscribe', l:prop });
-                setTimeout(function(){ list_tasks(); }, 500);
+                list_tasks();
             }
         });
 


commit 6c0985dfe9104d53bca05e6dc797b3c50f59183e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 19:49:34 2014 +0200

    Avoid warnings on empty message lists

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 15c81f9..154b0a3 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2110,7 +2110,7 @@ class calendar extends rcube_plugin
    */
   public function mail_messages_list($p)
   {
-    if (in_array('attachment', (array)$p['cols'])) {
+    if (in_array('attachment', (array)$p['cols']) && !empty($p['messages'])) {
       foreach ($p['messages'] as $i => $header) {
         $part = new StdClass;
         $part->mimetype = $header->ctype;


commit 63993e3a7d82967059b2f176537bbe6d1ee0b4d7
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 14:03:18 2014 +0200

    Additional styles for the tasklist search results to match the calendar view

diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index f33f525..93e0b2b 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -384,6 +384,20 @@ body.attachmentwin #topnav .topright {
 	top: 6px;
 }
 
+#tasklistsbox .searchresults {
+	background: #b0ccd7;
+	margin-top: 8px;
+}
+
+#tasklistsbox .searchresults .boxtitle {
+	background: none;
+	padding: 2px 8px 2px 8px;
+}
+
+#tasklistsbox .searchresults .listing li {
+	background-color: #c7e3ef;
+}
+
 #mainview-right {
 	position: absolute;
 	top: 0;


commit 8c507885c52c7b2fb8212f337960cbd31fbb1c84
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 13:56:44 2014 +0200

    Fix list creation/update/deletion (#3047)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 52d2337..50fa371 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -139,7 +139,7 @@ class tasklist_kolab_driver extends tasklist_driver
             'virtual' => $folder->virtual,
             'children' => true,  // TODO: determine if that folder indeed has child folders
             'subscribed' => (bool)$folder->is_subscribed(),
-            'group'    => $folder->get_namespace(),
+            'group'    => $folder->default ? 'default' : $folder->get_namespace(),
             'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
         );
     }
@@ -284,8 +284,13 @@ class tasklist_kolab_driver extends tasklist_driver
             $this->rc->user->save_prefs($prefs);
 
         // force page reload to properly render folder hierarchy
-        if (!empty($prop['parent']))
+        if (!empty($prop['parent'])) {
             $prop['_reload'] = true;
+        }
+        else {
+            $folder = kolab_storage::get_folder($folder);
+            $prop += $this->folder_props($folder, $this->rc->get_storage()->get_hierarchy_delimiter(), array());
+        }
 
         return $id;
     }
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 774e9b4..75ba386 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -164,7 +164,7 @@ function rcube_tasklist_ui(settings)
                 var prop = { id:p.id, active:list.active?1:0 };
                 if (list.subscribed) prop.permanent = 1;
                 rcmail.http_post('tasklist', { action:'subscribe', l:prop });
-                list_tasks();
+                setTimeout(function(){ list_tasks(); }, 500);
             }
         });
 
@@ -737,10 +737,11 @@ function rcube_tasklist_ui(settings)
 
         var tag = draggable.data('value'),
             drop_id = $(this).data('id'),
-            drop_rec = listdata[drop_id];
+            drop_rec = listdata[drop_id],
+            list = drop_rec && me.tasklists[drop_rec.list] ? me.tasklists[drop_rec.list] : { editable:true };
 
-        // target already has this tag assigned
-        if (!drop_rec || (drop_rec.tags && $.inArray(tag, drop_rec.tags) >= 0)) {
+        // target is not writeable or already has this tag assigned
+        if (!drop_rec || drop_rec.readonly || !list.editable || (drop_rec.tags && $.inArray(tag, drop_rec.tags) >= 0)) {
             return false;
         }
 
@@ -1048,7 +1049,7 @@ function rcube_tasklist_ui(settings)
 
     function task_draggable_start(event, ui)
     {
-        $('.taskhead, #rootdroppable, #'+rcmail.gui_objects.folderlist.id+' li').droppable({
+        $('.taskhead, #rootdroppable, #'+rcmail.gui_objects.tasklistslist.id+' li').droppable({
             hoverClass: 'droptarget',
             accept: task_droppable_accept,
             drop: task_draggable_dropped,
@@ -1187,7 +1188,7 @@ function rcube_tasklist_ui(settings)
      */
     function task_show_dialog(id)
     {
-        var $dialog = $('#taskshow'), rec;
+        var $dialog = $('#taskshow'), rec, list;
 
         if ($dialog.is(':ui-dialog'))
           $dialog.dialog('close');
@@ -1196,6 +1197,7 @@ function rcube_tasklist_ui(settings)
             return;
 
         me.selected_task = rec;
+        list = rec.list && me.tasklists[rec.list] ? me.tasklists[rec.list] : {};
 
         // fill dialog data
         $('#task-parent-title').html(Q(rec.parent_title || '')+' »').css('display', rec.parent_title ? 'block' : 'none');
@@ -1247,22 +1249,24 @@ function rcube_tasklist_ui(settings)
 
         // define dialog buttons
         var buttons = [];
-        buttons.push({
-            text: rcmail.gettext('edit','tasklist'),
-            click: function() {
-                task_edit_dialog(me.selected_task.id, 'edit');
-            },
-            disabled: rcmail.busy
-        });
+        if (list.editable && !rec.readonly) {
+              buttons.push({
+                text: rcmail.gettext('edit','tasklist'),
+                click: function() {
+                    task_edit_dialog(me.selected_task.id, 'edit');
+                },
+                disabled: rcmail.busy
+            });
 
-        buttons.push({
-            text: rcmail.gettext('delete','tasklist'),
-            click: function() {
-                if (delete_task(me.selected_task.id))
-                    $dialog.dialog('close');
-            },
-            disabled: rcmail.busy
-        });
+            buttons.push({
+                text: rcmail.gettext('delete','tasklist'),
+                click: function() {
+                    if (delete_task(me.selected_task.id))
+                        $dialog.dialog('close');
+                },
+                disabled: rcmail.busy
+            });
+        }
 
         // open jquery UI dialog
         $dialog.dialog({
@@ -1828,15 +1832,12 @@ function rcube_tasklist_ui(settings)
             delete_ids.push(prop.id);
         }
 
-        // delete all calendars in the list
+        // delete all subfolders in the list
         for (var i=0; i < delete_ids.length; i++) {
             id = delete_ids[i];
             list = me.tasklists[id];
-            li = rcmail.get_folder_li(id, 'rcmlitasklist');
+            tasklists_widget.remove(id);
 
-            if (li) {
-                $(li).remove();
-            }
             if (list) {
                 list.active = false;
                 // delete me.tasklists[prop.id];
@@ -1857,13 +1858,15 @@ function rcube_tasklist_ui(settings)
             return;
         }
 
-        var li = $('<li>').attr('id', 'rcmlitasklist'+prop.id)
-            .append('<input type="checkbox" name="_list[]" value="'+prop.id+'" checked="checked" />')
-            .append('<span class="handle"> </span>')
-            .append('<span class="listname">'+Q(prop.name)+'</span>');
-        $(rcmail.gui_objects.folderlist).append(li);
+        tasklists_widget.insert({
+            id: prop.id,
+            classes: [ prop.group || '' ],
+            virtual: prop.virtual,
+            html: prop.html
+        }, prop.parent || null, prop.group);
+
+        delete prop.html;
         me.tasklists[prop.id] = prop;
-        init_tasklist_li(li.get(0), prop.id);
 
         // append to list selector in task edit dialog, too (#2985)
         $('<option>').attr('value', prop.id).html(Q(prop.name)).appendTo('#taskedit-tasklist');
@@ -1875,7 +1878,7 @@ function rcube_tasklist_ui(settings)
     function update_list(prop)
     {
         var id = prop.oldid || prop.id,
-            li = rcmail.get_folder_li(id, 'rcmlitasklist');
+            li = tasklists_widget.get_item(id);
 
         if (prop._reload) {
             rcmail.redirect(rcmail.url(''));
@@ -1885,10 +1888,9 @@ function rcube_tasklist_ui(settings)
         if (me.tasklists[id] && li) {
             delete me.tasklists[id];
             me.tasklists[prop.id] = prop;
-            $(li).data('id', prop.id)
-                .attr('id', 'rcmlitasklist'+prop.id)
-                .find('input').data('id', prop.id);
-            $('.listname', li).html(Q(prop.name));
+            $(li).find('input').first().val(prop.id);
+            $(li).find('.listname').first().html(Q(prop.name));
+            tasklists_widget.update(id, { id:prop.id, html:li.children().first() });
         }
     }
 
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 1a70699..3004435 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -607,6 +607,11 @@ class tasklist extends rcube_plugin
             $list += array('showalarms' => true, 'active' => true, 'editable' => true);
             if ($insert_id = $this->driver->create_list($list)) {
                 $list['id'] = $insert_id;
+                if (!$list['_reload']) {
+                    $this->load_ui();
+                    $list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv);
+                    $list += (array)$jsenv[$insert_id];
+                }
                 $this->rc->output->command('plugin.insert_tasklist', $list);
                 $success = true;
             }
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 6f92d26..05acafd 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -188,7 +188,7 @@ class tasklist_ui
 
         if (!$activeonly || $prop['active']) {
             return html::div(join(' ', $classes),
-                html::span(array('class' => 'listname', 'title' => $title), $prop['listname']) .
+                html::span(array('class' => 'listname', 'title' => $title), $prop['listname'] ?: $prop['name']) .
                   ($prop['virtual'] ? '' :
                     html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
                     html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), ' ') .


commit f4f5a30e0ae0d6e0c4e4b6d04c8d64174d0bf53d
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 13:04:18 2014 +0200

    Add new folder navigation to tasks module (#3047)

diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 70e0c37..edcca42 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -119,8 +119,8 @@ class calendar_ui
     $this->rc->output->include_script('treelist.js');
 
     // include kolab folderlist widget if available
-    if (is_readable($this->cal->home . '/lib/js/folderlist.js')) {
-      $this->cal->include_script('lib/js/folderlist.js');
+    if (is_readable($this->cal->api->dir . 'libkolab/js/folderlist.js')) {
+        $this->cal->api->include_script('libkolab/js/folderlist.js');
     }
 
     jqueryui::miniColors();
diff --git a/plugins/calendar/lib/js/folderlist.js b/plugins/calendar/lib/js/folderlist.js
deleted file mode 120000
index c49706b..0000000
--- a/plugins/calendar/lib/js/folderlist.js
+++ /dev/null
@@ -1 +0,0 @@
-../../../libkolab/js/folderlist.js
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_folder_api.php b/plugins/libkolab/lib/kolab_storage_folder_api.php
index 5af8c34..4a90467 100644
--- a/plugins/libkolab/lib/kolab_storage_folder_api.php
+++ b/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -149,6 +149,24 @@ abstract class kolab_storage_folder_api
         return rcube_charset::convert(end($parts), 'UTF7-IMAP');
     }
 
+    /**
+     * Getter for parent folder path
+     *
+     * @return string Full path to parent folder
+     */
+    public function get_parent()
+    {
+        $path = explode('/', $this->name);
+        array_pop($path);
+
+        // don't list top-level namespace folder
+        if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
+            $path = array();
+        }
+
+        return join('/', $path);
+    }
+
 
     /**
      * Get the color value stored in metadata
diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php
index d9bf414..cab4fa7 100644
--- a/plugins/tasklist/drivers/database/tasklist_database_driver.php
+++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php
@@ -200,6 +200,18 @@ class tasklist_database_driver extends tasklist_driver
     }
 
     /**
+     * Search for shared or otherwise not listed tasklists the user has access
+     *
+     * @param string Search string
+     * @param string Section/source to search
+     * @return array List of tasklists
+     */
+    public function search_lists($query, $source)
+    {
+        return array();
+    }
+
+    /**
      * Get number of tasks matching the given filter
      *
      * @param array List of lists to count tasks of
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index ad36777..52d2337 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -45,12 +45,15 @@ class tasklist_kolab_driver extends tasklist_driver
         $this->rc = $plugin->rc;
         $this->plugin = $plugin;
 
-        $this->_read_lists();
-
         if (kolab_storage::$version == '2.0') {
             $this->alarm_absolute = false;
         }
 
+        // tasklist use fully encoded identifiers
+        kolab_storage::$encode_ids = true;
+
+        $this->_read_lists();
+
         $this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
     }
 
@@ -83,87 +86,171 @@ class tasklist_kolab_driver extends tasklist_driver
 
         $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
         $prefs = $this->rc->config->get('kolab_tasklists', array());
-        $listnames = array();
+
+        foreach ($folders as $folder) {
+            $tasklist = $this->folder_props($folder, $delim, $prefs);
+
+            $this->lists[$tasklist['id']] = $tasklist;
+            $this->folders[$tasklist['id']] = $folder;
+            $this->folders[$folder->name] = $folder;
+        }
+    }
+
+    /**
+     * Derive list properties from the given kolab_storage_folder object
+     */
+    protected function folder_props($folder, $delim, $prefs)
+    {
+        if ($folder->get_namespace() == 'personal') {
+            $norename = false;
+            $readonly = false;
+            $alarms = true;
+        }
+        else {
+            $alarms = false;
+            $readonly = true;
+            if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
+                if (strpos($rights, 'i') !== false)
+                  $readonly = false;
+            }
+            $info = $folder->get_folder_info();
+            $norename = $readonly || $info['norename'] || $info['protected'];
+        }
+
+        $list_id = $folder->id; #kolab_storage::folder_id($folder->name);
+        $old_id = kolab_storage::folder_id($folder->name, false);
+
+        if (!isset($prefs[$list_id]['showalarms']) && isset($prefs[$old_id]['showalarms'])) {
+            $prefs[$list_id]['showalarms'] = $prefs[$old_id]['showalarms'];
+        }
+
+        return array(
+            'id' => $list_id,
+            'name' => $folder->get_name(),
+            'listname' => $folder->get_foldername(),
+            'editname' => $folder->get_foldername(),
+            'color' => $folder->get_color('0000CC'),
+            'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
+            'editable' => !$readonly,
+            'norename' => $norename,
+            'active' => $folder->is_active(),
+            'parentfolder' => $folder->get_parent(),
+            'default' => $folder->default,
+            'virtual' => $folder->virtual,
+            'children' => true,  // TODO: determine if that folder indeed has child folders
+            'subscribed' => (bool)$folder->is_subscribed(),
+            'group'    => $folder->get_namespace(),
+            'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
+        );
+    }
+
+    /**
+     * Get a list of available task lists from this source
+     */
+    public function get_lists(&$tree = null)
+    {
+        // attempt to create a default list for this user
+        if (empty($this->lists)) {
+            $prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true);
+            if ($this->create_list($prop))
+                $this->_read_lists(true);
+        }
+
+        $folders = array();
+        foreach ($this->lists as $id => $list) {
+            if (!empty($this->folders[$id])) {
+                $folders[] = $this->folders[$id];
+            }
+        }
 
         // include virtual folders for a full folder tree
-        if (!$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
-            $folders = kolab_storage::folder_hierarchy($folders);
+        if (!is_null($tree)) {
+            $folders = kolab_storage::folder_hierarchy($folders, $tree);
+        }
 
+        $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
+        $prefs = $this->rc->config->get('kolab_tasklists', array());
+
+        $lists = array();
         foreach ($folders as $folder) {
-            $utf7name = $folder->name;
+            $list_id = $folder->id; #kolab_storage::folder_id($folder->name);
+            $imap_path = explode($delim, $folder->name);
 
-            $path_imap = explode($delim, $utf7name);
-            $editname = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');  // pop off raw name part
-            $path_imap = join($delim, $path_imap);
+            // find parent
+            do {
+              array_pop($imap_path);
+              $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
+            }
+            while (count($imap_path) > 1 && !$this->folders[$parent_id]);
+
+            // restore "real" parent ID
+            if ($parent_id && !$this->folders[$parent_id]) {
+                $parent_id = kolab_storage::folder_id($folder->get_parent());
+            }
 
             $fullname = $folder->get_name();
-            $listname = kolab_storage::folder_displayname($fullname, $listnames);
+            $listname = $folder->get_foldername();
 
             // special handling for virtual folders
-            if ($folder->virtual) {
-                $list_id = kolab_storage::folder_id($utf7name);
-                $this->lists[$list_id] = array(
-                    'id' => $list_id,
-                    'name' => $fullname,
+            if ($folder instanceof kolab_storage_folder_user) {
+                $lists[$list_id] = array(
+                    'id'       => $list_id,
+                    'name'     => $folder->get_name(),
                     'listname' => $listname,
-                    'virtual' => true,
+                    'title'    => $folder->get_owner(),
+                    'virtual'  => true,
                     'editable' => false,
+                    'group'    => 'other virtual',
+                    'class'    => 'user',
+                    'parent'   => $parent_id,
                 );
-                continue;
             }
-
-            if ($folder->get_namespace() == 'personal') {
-                $norename = false;
-                $readonly = false;
-                $alarms = true;
+            else if ($folder->virtual) {
+                $lists[$list_id] = array(
+                    'id'       => $list_id,
+                    'name'     => kolab_storage::object_name($fullname),
+                    'listname' => $listname,
+                    'virtual'  => true,
+                    'editable' => false,
+                    'group'    => $folder->get_namespace(),
+                    'class'    => 'folder',
+                    'parent'   => $parent_id,
+                );
             }
             else {
-                $alarms = false;
-                $readonly = true;
-                if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) {
-                    if (strpos($rights, 'i') !== false)
-                      $readonly = false;
+                if (!$this->lists[$list_id]) {
+                    $this->lists[$list_id] = $this->folder_props($folder, $delim, $prefs);
+                    $this->folders[$list_id] = $folder;
                 }
-                $info = $folder->get_folder_info();
-                $norename = $readonly || $info['norename'] || $info['protected'];
+                $this->lists[$list_id]['parent'] = $parent_id;
+                $lists[$list_id] = $this->lists[$list_id];
             }
-
-            $list_id = kolab_storage::folder_id($utf7name);
-            $tasklist = array(
-                'id' => $list_id,
-                'name' => $fullname,
-                'listname' => $listname,
-                'editname' => $editname,
-                'color' => $folder->get_color('0000CC'),
-                'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms,
-                'editable' => !$readionly,
-                'norename' => $norename,
-                'active' => $folder->is_active(),
-                'parentfolder' => $path_imap,
-                'default' => $folder->default,
-                'children' => true,  // TODO: determine if that folder indeed has child folders
-                'class_name' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
-            );
-            $this->lists[$tasklist['id']] = $tasklist;
-            $this->folders[$tasklist['id']] = $folder;
-            $this->folders[$folder->name] = $folder;
         }
+
+        return $lists;
     }
 
     /**
-     * Get a list of available task lists from this source
+     * Get the kolab_calendar instance for the given calendar ID
+     *
+     * @param string List identifier (encoded imap folder name)
+     * @return object kolab_storage_folder Object nor null if list doesn't exist
      */
-    public function get_lists()
+    protected function get_folder($id)
     {
-        // attempt to create a default list for this user
-        if (empty($this->lists)) {
-            if ($this->create_list(array('name' => 'Tasks', 'color' => '0000CC', 'default' => true)))
-                $this->_read_lists(true);
+        // create list and folder instance if necesary
+        if (!$this->lists[$id]) {
+            $folder = kolab_storage::get_folder(kolab_storage::id_decode($id));
+            if ($folder->type) {
+                $this->folders[$id] = $folder;
+                $this->lists[$id] = $this->folder_props($folder, $this->rc->get_storage()->get_hierarchy_delimiter(), $this->rc->config->get('kolab_tasklists', array()));
+            }
         }
 
-        return $this->lists;
+        return $this->folders[$id];
     }
 
+
     /**
      * Create a new list assigned to the current user
      *
@@ -215,7 +302,7 @@ class tasklist_kolab_driver extends tasklist_driver
      */
     public function edit_list(&$prop)
     {
-        if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
+        if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
             $prop['oldname'] = $folder->name;
             $prop['type'] = 'task';
             $newfolder = kolab_storage::folder_update($prop);
@@ -254,12 +341,18 @@ class tasklist_kolab_driver extends tasklist_driver
      * @param array Hash array with list properties
      *          id: List Identifier
      *      active: True if list is active, false if not
+     *   permanent: True if list is to be subscribed permanently
      * @return boolean True on success, Fales on failure
      */
     public function subscribe_list($prop)
     {
-        if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
-            return $folder->activate($prop['active']);
+        if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
+            $ret = false;
+            if (isset($prop['permanent']))
+                $ret |= $folder->subscribe(intval($prop['permanent']));
+            if (isset($prop['active']))
+                $ret |= $folder->activate(intval($prop['active']));
+            return $ret;
         }
         return false;
     }
@@ -273,7 +366,7 @@ class tasklist_kolab_driver extends tasklist_driver
      */
     public function remove_list($prop)
     {
-        if ($prop['id'] && ($folder = $this->folders[$prop['id']])) {
+        if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
           if (kolab_storage::folder_delete($folder->name))
               return true;
           else
@@ -284,6 +377,63 @@ class tasklist_kolab_driver extends tasklist_driver
     }
 
     /**
+     * Search for shared or otherwise not listed tasklists the user has access
+     *
+     * @param string Search string
+     * @param string Section/source to search
+     * @return array List of tasklists
+     */
+    public function search_lists($query, $source)
+    {
+        if (!kolab_storage::setup()) {
+            return array();
+        }
+
+        $this->search_more_results = false;
+        $this->lists = $this->folders = array();
+
+        $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
+
+        // find unsubscribed IMAP folders that have "event" type
+        if ($source == 'folders') {
+            foreach ((array)kolab_storage::search_folders('task', $query, array('other')) as $folder) {
+                $this->folders[$folder->id] = $folder;
+                $this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
+            }
+        }
+        // search other user's namespace via LDAP
+        else if ($source == 'users') {
+            $limit = $this->rc->config->get('autocomplete_max', 15) * 2;  // we have slightly more space, so display twice the number
+            foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
+                $folders = array();
+                // search for tasks folders shared by this user
+                foreach (kolab_storage::list_user_folders($user, 'task', false) as $foldername) {
+                    $folders[] = new kolab_storage_folder($foldername, 'task');
+                }
+
+                if (count($folders)) {
+                    $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
+                    $this->folders[$userfolder->id] = $userfolder;
+                    $this->lists[$userfolder->id] = $this->folder_props($userfolder, $delim, array());
+
+                    foreach ($folders as $folder) {
+                        $this->folders[$folder->id] = $folder;
+                        $this->lists[$folder->id] = $this->folder_props($folder, $delim, array());
+                        $count++;
+                    }
+                }
+
+                if ($count >= $limit) {
+                    $this->search_more_results = true;
+                    break;
+                }
+            }
+        }
+
+        return $this->get_lists();
+    }
+
+    /**
      * Get number of tasks matching the given filter
      *
      * @param array List of lists to count tasks of
@@ -303,7 +453,9 @@ class tasklist_kolab_driver extends tasklist_driver
 
         $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0);
         foreach ($lists as $list_id) {
-            $folder = $this->folders[$list_id];
+            if (!$folder = $this->get_folder($list_id)) {
+                continue;
+            }
             foreach ($folder->select(array(array('tags','!~','x-complete'))) as $record) {
                 $rec = $this->_to_rcube_task($record);
 
@@ -367,7 +519,9 @@ class tasklist_kolab_driver extends tasklist_driver
         }
 
         foreach ($lists as $list_id) {
-            $folder = $this->folders[$list_id];
+            if (!$folder = $this->get_folder($list_id)) {
+                continue;
+            }
             foreach ($folder->select($query) as $record) {
                 $task = $this->_to_rcube_task($record);
                 $task['list'] = $list_id;
@@ -391,11 +545,11 @@ class tasklist_kolab_driver extends tasklist_driver
     {
         $id = is_array($prop) ? ($prop['uid'] ?: $prop['id']) : $prop;
         $list_id = is_array($prop) ? $prop['list'] : null;
-        $folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders;
+        $folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->folders;
 
         // find task in the available folders
         foreach ($folders as $list_id => $folder) {
-            if (is_numeric($list_id))
+            if (is_numeric($list_id) || !$folder)
                 continue;
             if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
                 $this->tasks[$id] = $this->_to_rcube_task($object);
@@ -424,7 +578,7 @@ class tasklist_kolab_driver extends tasklist_driver
         $childs = array();
         $list_id = $prop['list'];
         $task_ids = array($prop['id']);
-        $folder = $this->folders[$list_id];
+        $folder = $this->get_folder($list_id);
 
         // query for childs (recursively)
         while ($folder && !empty($task_ids)) {
@@ -484,7 +638,7 @@ class tasklist_kolab_driver extends tasklist_driver
             if (!$list['showalarms'] || ($lists && !in_array($lid, $lists)))
                 continue;
 
-            $folder = $this->folders[$lid];
+            $folder = $this->get_folder($lid);
             foreach ($folder->select($query) as $record) {
                 if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100)  // don't trust query :-)
                     continue;
@@ -756,11 +910,11 @@ class tasklist_kolab_driver extends tasklist_driver
     public function edit_task($task)
     {
         $list_id = $task['list'];
-        if (!$list_id || !($folder = $this->folders[$list_id]))
+        if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         // moved from another folder
-        if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
+        if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
             if (!$fromfolder->move($task['id'], $folder->name))
                 return false;
 
@@ -809,11 +963,11 @@ class tasklist_kolab_driver extends tasklist_driver
     public function move_task($task)
     {
         $list_id = $task['list'];
-        if (!$list_id || !($folder = $this->folders[$list_id]))
+        if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         // execute move command
-        if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) {
+        if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
             return $fromfolder->move($task['id'], $folder->name);
         }
 
@@ -831,7 +985,7 @@ class tasklist_kolab_driver extends tasklist_driver
     public function delete_task($task, $force = true)
     {
         $list_id = $task['list'];
-        if (!$list_id || !($folder = $this->folders[$list_id]))
+        if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         return $folder->delete($task['id']);
@@ -892,7 +1046,7 @@ class tasklist_kolab_driver extends tasklist_driver
      */
     public function get_attachment_body($id, $task)
     {
-        if ($storage = $this->folders[$task['list']]) {
+        if ($storage = $this->get_folder($task['list'])) {
             return $storage->get_attachment($task['id'], $id);
         }
 
@@ -905,7 +1059,7 @@ class tasklist_kolab_driver extends tasklist_driver
     public function tasklist_edit_form($action, $list, $fieldprop)
     {
         if ($list['id'] && ($list = $this->lists[$list['id']])) {
-            $folder_name = $this->folders[$list['id']]->name; // UTF7
+            $folder_name = $this->get_folder($list['id'])->name; // UTF7
         }
         else {
             $folder_name = '';
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index 6c31fa7..908c808 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -126,6 +126,15 @@ abstract class tasklist_driver
     abstract function remove_list($prop);
 
     /**
+     * Search for shared or otherwise not listed tasklists the user has access
+     *
+     * @param string Search string
+     * @param string Section/source to search
+     * @return array List of tasklists
+     */
+    abstract function search_lists($query, $source);
+
+    /**
      * Get number of tasks matching the given filter
      *
      * @param array List of lists to count tasks of
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 18456fa..0195d64 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -5,6 +5,9 @@ $labels['navtitle'] = 'Tasks';
 $labels['lists'] = 'Tasklists';
 $labels['list'] = 'Tasklist';
 $labels['tags'] = 'Tags';
+$labels['tasklistsubscribe'] = 'List permanently';
+$labels['listsearchresults'] = 'Available Tasklists';
+$labels['findlists'] = 'Find tasklists...';
 
 $labels['newtask'] = 'New Task';
 $labels['createnewtask'] = 'Create new Task (e.g. Saturday, Mow the lawn)';
diff --git a/plugins/tasklist/skins/larry/sprites.png b/plugins/tasklist/skins/larry/sprites.png
index 5c6b9fd..1446573 100644
Binary files a/plugins/tasklist/skins/larry/sprites.png and b/plugins/tasklist/skins/larry/sprites.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index c940dff..f33f525 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -74,6 +74,32 @@ body.attachmentwin #topnav .topright {
 	bottom: 0px;
 }
 
+#tasklistsbox .boxtitle a.iconbutton.search {
+	position: absolute;
+	top: 8px;
+	right: 8px;
+	width: 16px;
+	cursor: pointer;
+	background-position: -2px -317px;
+}
+
+#tasklistsbox .listsearchbox {
+	display: none;
+}
+
+#tasklistsbox .listsearchbox.expanded {
+	display: block;
+}
+
+#tasklistsbox .scroller {
+	top: 34px;
+}
+
+#tasklistsbox .listsearchbox.expanded + .scroller {
+	top: 68px;
+}
+
+
 #taskselector {
 	margin: -4px 40px 0 0;
 	padding: 0;
@@ -225,32 +251,48 @@ body.attachmentwin #topnav .topright {
 	display: none;
 }
 
-#tasklists li {
+#tasklistsbox .treelist li {
+	margin: 0;
+	display: block;
+	position: relative;
+}
+
+#tasklistsbox .treelist li div.tasklist {
 	margin: 0;
 	height: 20px;
 	padding: 6px 8px 2px 6px;
-	display: block;
 	position: relative;
 	white-space: nowrap;
 }
 
-#tasklists li.virtual {
-	height: 12px;
+#tasklistsbox .treelist li.virtual > div.tasklist {
+	height: 14px;
+}
+
+#tasklistsbox .treelist ul li > div.tasklist {
+	margin-left: 16px;
+}
+
+#tasklistsbox .treelist ul ul li > div.tasklist {
+	margin-left: 32px;
+}
+
+#tasklistsbox .treelist ul ul ul li > div.tasklist {
+	margin-left: 48px;
 }
 
-#tasklists li label {
+#tasklistsbox .treelist li label {
 	display: block;
 }
 
-#tasklists li span.listname {
+#tasklistsbox .treelist li span.listname {
 	display: block;
 	position: absolute;
 	top: 7px;
-	left: 26px;
-	right: 26px;
+	left: 38px;
+	right: 40px;
 	cursor: default;
-	padding-bottom: 2px;
-	padding-right: 30px;
+	padding: 0px 30px 2px 2px;
 	color: #004458;
 	overflow: hidden;
 	text-overflow: ellipsis;
@@ -258,8 +300,11 @@ body.attachmentwin #topnav .topright {
 	background: url(sprites.png) right 20px no-repeat;
 }
 
-#tasklists li span.handle {
+#tasklistsbox .treelist li span.quickview {
 	display: inline-block;
+	position: absolute;
+	top: 6px;
+	right: 20px;
 	width: 16px;
 	height: 16px;
 	margin-right: 4px;
@@ -267,52 +312,76 @@ body.attachmentwin #topnav .topright {
 	cursor: pointer;
 }
 
-#tasklists li:hover span.handle {
-	background-position: -20px -101px;
+#tasklistsbox .treelist li a.subscribed {
+	display: inline-block;
+	position: absolute;
+	top: 6px;
+	right: 5px;
+	height: 16px;
+	width: 16px;
+	padding: 0;
+	background: url(sprites.png) -100px 0 no-repeat;
+	overflow: hidden;
+	text-indent: -5000px;
+	cursor: pointer;
 }
 
-#tasklists li.focusview span.handle {
-	background-position: -2px -101px;
+#tasklistsbox .treelist div:hover > a.subscribed {
+	background-position: -2px -215px;
 }
 
-#tasklists li.selected span.listname {
-	font-weight: bold;
+#tasklistsbox .treelist div.subscribed a.subscribed {
+	background-position: -20px -215px;
 }
 
-#tasklists li.readonly span.listname {
-	background-position: right -142px;
+#tasklistsbox .treelist li div:hover > span.quickview {
+	background-position: -20px -101px;
 }
 
-#tasklists li.other span.listname {
-	background-position: right -160px;
+#tasklistsbox .treelist li div.focusview > span.quickview {
+	background-position: -2px -101px;
 }
 
-#tasklists li.other.readonly span.listname {
-	background-position: right -178px;
+#tasklistsbox .searchresults .treelist li span.quickview {
+	display: none;
+}
+
+#tasklistsbox .treelist li.selected > div > span.listname {
+	font-weight: bold;
 }
 
-#tasklists li.shared span.listname {
-	background-position: right -196px;
+#tasklistsbox .treelist .readonly > span.listname {
+	background-position: right -142px;
 }
 
-#tasklists li.shared.readonly span.listname {
-	background-position: right -214px;
+#tasklistsbox .treelist .user > span.listname {
+	background-position: right -160px;
 }
 
-#tasklists li.virtual span.listname {
+#tasklistsbox .treelist .virtual > span.listname {
 	color: #aaa;
-	top: 2px;
+	top: 4px;
+	left: 20px;
+	right: 5px;
 }
 
-#tasklists li.virtual span.handle {
-	background: none;
-	cursor: default;
+#tasklistsbox .treelist.flat li span.calname {
+	left: 24px;
+	right: 22px;
 }
 
-#tasklists li input {
+#tasklistsbox .treelist li input {
 	position: absolute;
 	top: 5px;
-	right: 5px;
+	left: 18px;
+}
+
+#tasklistsbox .treelist li .treetoggle {
+	top: 8px;
+}
+
+#tasklistsbox .treelist li.virtual > .treetoggle {
+	top: 6px;
 }
 
 #mainview-right {
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index fe3f88b..0686646 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -24,9 +24,18 @@
 		</div>
 
 		<div id="tasklistsbox" class="uibox listbox">
-			<h2 class="boxtitle"><roundcube:label name="tasklist.lists" /></h2>
+			<h2 class="boxtitle"><roundcube:label name="tasklist.lists" />
+				<a class="iconbutton search" title="<roundcube:label name='tasklist.findlists' />"></a>
+			</h2>
+			<div class="listsearchbox">
+				<div class="searchbox">
+					<input type="text" name="q" id="tasklistsearch" placeholder="<roundcube:label name='tasklist.findlists' />" />
+					<a class="iconbutton searchicon"></a>
+					<roundcube:button command="reset-listsearch" id="tasklistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
+				</div>
+			</div>
 			<div class="scroller withfooter">
-			<roundcube:object name="plugin.tasklists" id="tasklists" class="listing" />
+			<roundcube:object name="plugin.tasklists" id="tasklists" class="treelist 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="⚙" />
@@ -171,6 +180,30 @@ $(document).ready(function(e){
 		orientation:'v', relative:true, start:240, min:180, size:12 }).init();
 	new rcube_splitter({ id:'taskviewsplitterv', p1:'#tagsbox', p2:'#tasklistsbox',
 		orientation:'h', relative:true, start:242, min:120, size:12, offset:4 }).init();
+
+	// animation to unfold list search box
+	$('#tasklistsbox .boxtitle a.search').click(function(e){
+		var box = $('#tasklistsbox .listsearchbox'),
+			dir = box.is(':visible') ? -1 : 1;
+
+		box.slideToggle({
+			duration: 160,
+			progress: function(animation, progress) {
+				if (dir < 0) progress = 1 - progress;
+				$('#tasklistsbox .scroller').css('top', (34 + 34 * progress) + 'px');
+			},
+			complete: function() {
+				box.toggleClass('expanded');
+				if (box.is(':visible')) {
+					box.find('input[type=text]').focus();
+				}
+				else {
+					$('#tasklistsearch-reset').click();
+				}
+				// TODO: save state in localStorage
+			}
+		});
+	});
 });
 
 </script>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index fcf4306..774e9b4 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -79,6 +79,7 @@ function rcube_tasklist_ui(settings)
     var scroll_speed = 20;
     var scroll_sensitivity = 40;
     var scroll_timer;
+    var tasklists_widget;
     var me = this;
 
     // general datepicker settings
@@ -127,18 +128,80 @@ function rcube_tasklist_ui(settings)
     {
         // initialize task list selectors
         for (var id in me.tasklists) {
-            if ((li = rcmail.get_folder_li(id, 'rcmlitasklist'))) {
-                init_tasklist_li(li, id);
-            }
-
             if (me.tasklists[id].editable && (!me.selected_list || (me.tasklists[id].active && !me.tasklists[me.selected_list].active))) {
                 me.selected_list = id;
+                break;
             }
         }
 
+        // initialize treelist widget that controls the tasklists list
+        var widget_class = window.kolab_folderlist || rcube_treelist_widget;
+        tasklists_widget = new widget_class(rcmail.gui_objects.tasklistslist, {
+            id_prefix: 'rcmlitasklist',
+            selectable: true,
+            save_state: true,
+            searchbox: '#tasklistsearch',
+            search_action: 'tasks/tasklist',
+            search_sources: [ 'folders', 'users' ],
+            search_title: rcmail.gettext('listsearchresults','tasklist')
+        });
+        tasklists_widget.addEventListener('select', function(node) {
+            var id = $(this).data('id');
+            rcmail.enable_command('list-edit', 'list-remove', 'list-import', me.tasklists[node.id].editable);
+            me.selected_list = node.id;
+        });
+        tasklists_widget.addEventListener('subscribe', function(p) {
+            var list;
+            if ((list = me.tasklists[p.id])) {
+                list.subscribed = p.subscribed || false;
+                rcmail.http_post('tasklist', { action:'subscribe', l:{ id:p.id, active:list.active?1:0, permanent:list.subscribed?1:0 } });
+            }
+        });
+        tasklists_widget.addEventListener('insert-item', function(p) {
+            var list = p.data;
+            if (list && list.id && !list.virtual) {
+                me.tasklists[list.id] = list;
+                var prop = { id:p.id, active:list.active?1:0 };
+                if (list.subscribed) prop.permanent = 1;
+                rcmail.http_post('tasklist', { action:'subscribe', l:prop });
+                list_tasks();
+            }
+        });
+
+        // init (delegate) event handler on tasklist checkboxes
+        tasklists_widget.container.on('click', 'input[type=checkbox]', function(e){
+            var list, id = this.value;
+            if ((list = me.tasklists[id])) {
+                list.active = this.checked;
+                fetch_counts();
+                if (!this.checked) remove_tasks(id);
+                else               list_tasks(null);
+                rcmail.http_post('tasklist', { action:'subscribe', l:{ id:id, active:list.active?1:0 } });
+
+                // disable focusview
+                if (!this.checked && focusview == id) {
+                    set_focusview(null);
+                }
+            }
+            e.stopPropagation();
+        });
+
+        // handler for clicks on quickview buttons
+        tasklists_widget.container.on('click', '.quickview', function(e){
+            var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, '');
+            set_focusview(focusview == id ? null : id)
+            e.stopPropagation();
+        });
+
+        // register dbl-click handler to open calendar edit dialog
+        tasklists_widget.container.on('dblclick', ':not(.virtual) > .tasklist', function(e){
+            var id = $(this).closest('li').attr('id').replace(/^rcmlitasklist/, '');
+            list_edit_dialog(id);
+        });
+
         if (me.selected_list) {
             rcmail.enable_command('addtask', true);
-            $(rcmail.get_folder_li(me.selected_list, 'rcmlitasklist')).click();
+            tasklists_widget.select(me.selected_list);
         }
 
         // register server callbacks
@@ -1972,7 +2035,7 @@ function rcube_tasklist_ui(settings)
             me.selected_list = id;
 
             // click on handle icon toggles focusview
-            if (e.target.className == 'handle') {
+            if (e.target.className == 'quickview') {
                 set_focusview(focusview == id ? null : id)
             }
             // disable focusview when selecting another list
@@ -1994,13 +2057,15 @@ function rcube_tasklist_ui(settings)
     function set_focusview(id)
     {
         if (focusview && focusview != id)
-            $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).removeClass('focusview');
+            $(tasklists_widget.get_item(focusview)).find('.tasklist').first().removeClass('focusview');
 
         focusview = id;
 
+        var li = $(tasklists_widget.get_item(id)).find('.tasklist').first();
+
         // activate list if necessary
         if (focusview && !me.tasklists[id].active) {
-            $('input', rcmail.get_folder_li(id, 'rcmlitasklist')).get(0).checked = true;
+            li.find('input[type=checkbox]').get(0).checked = true;
             me.tasklists[id].active = true;
             fetch_counts();
         }
@@ -2009,7 +2074,7 @@ function rcube_tasklist_ui(settings)
         list_tasks(null);
 
         if (focusview) {
-            $(rcmail.get_folder_li(focusview, 'rcmlitasklist')).addClass('focusview');
+            li.addClass('focusview');
         }
     }
 
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 65376d7..1a70699 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -52,6 +52,7 @@ class tasklist extends rcube_plugin
     public $driver;
     public $timezone;
     public $ui;
+    public $home;  // declare public to be used in other classes
 
     private $collapsed_tasks = array();
 
@@ -134,8 +135,7 @@ class tasklist extends rcube_plugin
         }
 
         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->load_ui();
             $this->ui->init();
         }
 
@@ -144,6 +144,16 @@ class tasklist extends rcube_plugin
         $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
     }
 
+    /**
+     *
+     */
+    private function load_ui()
+    {
+        if (!$this->ui) {
+            require_once($this->home . '/tasklist_ui.php');
+            $this->ui = new tasklist_ui($this);
+        }
+    }
 
     /**
      * Helper method to load the backend driver according to local config
@@ -619,6 +629,30 @@ class tasklist extends rcube_plugin
             if (($success = $this->driver->remove_list($list)))
                 $this->rc->output->command('plugin.destroy_tasklist', $list);
             break;
+
+        case 'search':
+            $this->load_ui();
+            $results = array();
+            foreach ((array)$this->driver->search_lists(get_input_value('q', RCUBE_INPUT_GPC), get_input_value('source', RCUBE_INPUT_GPC)) as $id => $prop) {
+                $editname = $prop['editname'];
+                unset($prop['editname']);  // force full name to be displayed
+                $prop['active'] = false;
+
+                // let the UI generate HTML and CSS representation for this calendar
+                $html = $this->ui->tasklist_list_item($id, $prop, $jsenv);
+                $prop += (array)$jsenv[$id];
+                $prop['editname'] = $editname;
+                $prop['html'] = $html;
+
+                $results[] = $prop;
+            }
+            // report more results available
+            if ($this->driver->search_more_results) {
+                $this->rc->output->show_message('autocompletemore', 'info');
+            }
+
+            $this->rc->output->command('multi_thread_http_response', $results, get_input_value('_reqid', RCUBE_INPUT_GPC));
+            return;
         }
 
         if ($success)
@@ -875,6 +909,13 @@ class tasklist extends rcube_plugin
     {
         $this->ui->init();
         $this->ui->init_templates();
+
+        // set autocompletion env
+        $this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
+        $this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
+        $this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
+        $this->rc->output->add_label('autocompletechars', 'autocompletemore');
+
         $this->rc->output->set_pagetitle($this->gettext('navtitle'));
         $this->rc->output->send('tasklist.mainview');
     }
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 6988a61..6f92d26 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -81,6 +81,12 @@ class tasklist_ui
 
         $this->plugin->include_script('jquery.tagedit.js');
         $this->plugin->include_script('tasklist.js');
+        $this->rc->output->include_script('treelist.js');
+
+        // include kolab folderlist widget if available
+        if (is_readable($this->plugin->api->dir . 'libkolab/js/folderlist.js')) {
+            $this->plugin->api->include_script('libkolab/js/folderlist.js');
+        }
 
         $this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tagedit.css');
     }
@@ -88,45 +94,110 @@ class tasklist_ui
     /**
      *
      */
-    function tasklists($attrib = array())
+    public function tasklists($attrib = array())
+    {
+        $tree = true;
+        $jsenv = array();
+        $lists = $this->plugin->driver->get_lists($tree);
+
+        // walk folder tree
+        if (is_object($tree)) {
+            $html = $this->list_tree_html($tree, $lists, $jsenv, $attrib);
+        }
+        else {
+            // fall-back to flat folder listing
+            $attrib['class'] .= ' flat';
+
+            $html = '';
+            foreach ((array)$lists as $id => $prop) {
+                if ($attrib['activeonly'] && !$prop['active'])
+                  continue;
+
+                $html .= html::tag('li', array(
+                        'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
+                        'class' => $prop['group'],
+                    ),
+                    $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly'])
+                );
+            }
+        }
+
+        $this->rc->output->set_env('tasklists', $jsenv);
+        $this->rc->output->add_gui_object('tasklistslist', $attrib['id']);
+
+        return html::tag('ul', $attrib, $html, html::$common_attrib);
+    }
+
+    /**
+     * Return html for a structured list <ul> for the folder tree
+     */
+    public function list_tree_html($node, $data, &$jsenv, $attrib)
     {
-        $lists = $this->plugin->driver->get_lists();
+        $out = '';
+        foreach ($node->children as $folder) {
+            $id = $folder->id;
+            $prop = $data[$id];
+            $is_collapsed = false; // TODO: determine this somehow?
+
+            $content = $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly']);
+
+            if (!empty($folder->children)) {
+                $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
+                    $this->list_tree_html($folder, $data, $jsenv, $attrib));
+            }
+
+            if (strlen($content)) {
+                $out .= html::tag('li', array(
+                      'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
+                      'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
+                    ),
+                    $content);
+            }
+        }
 
-        $li = '';
-        foreach ((array)$lists as $id => $prop) {
-            if ($attrib['activeonly'] && !$prop['active'])
-              continue;
+        return $out;
+    }
 
+    /**
+     * Helper method to build a tasklist item (HTML content and js data)
+     */
+    public function tasklist_list_item($id, $prop, &$jsenv, $activeonly = false)
+    {
+        // enrich list properties with settings from the driver
+        if (!$prop['virtual']) {
             unset($prop['user_id']);
             $prop['alarms'] = $this->plugin->driver->alarms;
             $prop['undelete'] = $this->plugin->driver->undelete;
             $prop['sortable'] = $this->plugin->driver->sortable;
             $prop['attachments'] = $this->plugin->driver->attachments;
-
-            if (!$prop['virtual'])
-                $jsenv[$id] = $prop;
-
-            $html_id = html_identifier($id);
-            $class = 'tasks-'  . asciiwords($id, true);
-            $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
-
-            if ($prop['virtual'])
-                $class .= ' virtual';
-            else if (!$prop['editable'])
-                $class .= ' readonly';
-            if ($prop['class_name'])
-                $class .= ' '.$prop['class_name'];
-
-            $li .= html::tag('li', array('id' => 'rcmlitasklist' . $html_id, 'class' => $class),
-                ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active']))) .
-                html::span(array('class' => 'handle', 'title' => $this->plugin->gettext('focusview')), ' ') .
-                html::span(array('class' => 'listname', 'title' => $title), $prop['listname']));
+            $jsenv[$id] = $prop;
         }
 
-        $this->rc->output->set_env('tasklists', $jsenv);
-        $this->rc->output->add_gui_object('folderlist', $attrib['id']);
+        $classes = array('tasklist');
+        $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+          html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
+
+        if ($prop['virtual'])
+            $classes[] = 'virtual';
+        else if (!$prop['editable'])
+            $classes[] = 'readonly';
+        if ($prop['subscribed'])
+            $classes[] = 'subscribed';
+        if ($prop['class'])
+            $classes[] = $prop['class'];
+
+        if (!$activeonly || $prop['active']) {
+            return html::div(join(' ', $classes),
+                html::span(array('class' => 'listname', 'title' => $title), $prop['listname']) .
+                  ($prop['virtual'] ? '' :
+                    html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'])) .
+                    html::span(array('class' => 'quickview', 'title' => $this->plugin->gettext('focusview')), ' ') .
+                    (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe')), ' ') : '')
+                )
+            );
+        }
 
-        return html::tag('ul', $attrib, $li, html::$common_attrib);
+        return '';
     }
 
 


commit 184730b3477ea1c7410b82913a0282baeb6a5bb2
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 12:44:26 2014 +0200

    Remove invisible text because that's considered for searching

diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index f973e88..70e0c37 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -307,7 +307,7 @@ class calendar_ui
         html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
         ($prop['virtual'] ? '' :
           html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
-          (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), $this->cal->gettext('subscribed')) : '') .
+          (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), ' ') : '') .
           html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')
         )
       );


commit 709bd160167ad33b8c7a86d12213bcd06d7d38bc
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 21 10:50:36 2014 +0200

    Minor bugfixes and visual enhancements for new folder navigation

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index c845c15..3e5d28a 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -52,7 +52,7 @@ class kolab_calendar extends kolab_storage_folder_api
     $imap = $calendar->rc->get_storage();
     $imap_folder = kolab_storage::id_decode($id);
     $info = $imap->folder_info($imap_folder, true);
-    if (empty($info) || $info['noselect'] || kolab_storage::folder_type($imap_folder) != 'event') {
+    if (empty($info) || $info['noselect'] || strpos(kolab_storage::folder_type($imap_folder), 'event') !== 0) {
       return new kolab_user_calendar($imap_folder, $calendar);
     }
     else {
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 217fe42..a95e1c2 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -117,8 +117,9 @@ class kolab_driver extends calendar_driver
       }
     }
 
+    $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
     $folders = $this->filter_calendars(false, $active, $personal);
-    $calendars = $names = array();
+    $calendars = array();
 
     // include virtual folders for a full folder tree
     if (!is_null($tree))
@@ -127,14 +128,19 @@ class kolab_driver extends calendar_driver
     foreach ($folders as $id => $cal) {
       $fullname = $cal->get_name();
       $listname = $cal->get_foldername();
-      $imap_path = explode('/', $cal->name);
+      $imap_path = explode($delim, $cal->name);
 
       // find parent
       do {
         array_pop($imap_path);
-        $parent_id = kolab_storage::folder_id(join('/', $imap_path));
+        $parent_id = kolab_storage::folder_id(join($delim, $imap_path));
+      }
+      while (count($imap_path) > 1 && !$this->calendars[$parent_id]);
+
+      // restore "real" parent ID
+      if ($parent_id && !$this->calendars[$parent_id]) {
+          $parent_id = kolab_storage::folder_id($cal->get_parent());
       }
-      while (count($imap_path) > 0 && !$this->calendars[$parent_id]);
 
       // turn a kolab_storage_folder object into a kolab_calendar
       if ($cal instanceof kolab_storage_folder) {
@@ -360,9 +366,9 @@ class kolab_driver extends calendar_driver
     if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
       $ret = false;
       if (isset($prop['permanent']))
-        $ret |= $cal->storage->subscribe($prop['permanent']);
+        $ret |= $cal->storage->subscribe(intval($prop['permanent']));
       if (isset($prop['active']))
-        $ret |= $cal->storage->activate($prop['active']);
+        $ret |= $cal->storage->activate(intval($prop['active']));
       return $ret;
     }
     else {
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 3329bf4..f973e88 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -273,7 +273,7 @@ class calendar_ui
   /**
    * Helper method to build a calendar list item (HTML content and js data)
    */
-  public function calendar_list_item($id, $prop, &$jsenv, $activeonly)
+  public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
   {
     // enrich calendar properties with settings from the driver
     if (!$prop['virtual']) {
diff --git a/plugins/calendar/skins/larry/images/calendars.png b/plugins/calendar/skins/larry/images/calendars.png
index 1f97abc..117d329 100644
Binary files a/plugins/calendar/skins/larry/images/calendars.png and b/plugins/calendar/skins/larry/images/calendars.png differ
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index ce07112..2e45fcf 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -256,6 +256,9 @@ $(document).ready(function(e){
 				if (box.is(':visible')) {
 					box.find('input[type=text]').focus();
 				}
+				else {
+					$('#calendarlistsearch-reset').click();
+				}
 				// TODO: save state in localStorage
 			}
 		});


commit 36248fb46897c56c0a17bc010fc453c37d00d72c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 20 20:26:47 2014 +0200

    Code cleanup and small fixes

diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 62b19f7..3329bf4 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -230,7 +230,7 @@ class calendar_ui
         continue;
 
       $html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']),
-        $content = $this->calendar_list_item($id, $prop, $jsenv)
+        $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly'])
       );
     }
 
@@ -249,8 +249,9 @@ class calendar_ui
     foreach ($node->children as $folder) {
       $id = $folder->id;
       $prop = $data[$id];
+      $is_collapsed = false; // TODO: determine this somehow?
 
-      $content = $this->calendar_list_item($id, $prop, $jsenv);
+      $content = $this->calendar_list_item($id, $prop, $jsenv, $attrib['activeonly']);
 
       if (!empty($folder->children)) {
         $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
@@ -272,7 +273,7 @@ class calendar_ui
   /**
    * Helper method to build a calendar list item (HTML content and js data)
    */
-  public function calendar_list_item($id, $prop, &$jsenv)
+  public function calendar_list_item($id, $prop, &$jsenv, $activeonly)
   {
     // enrich calendar properties with settings from the driver
     if (!$prop['virtual']) {
@@ -290,19 +291,18 @@ class calendar_ui
     $classes = array('calendar', 'cal-'  . asciiwords($id, true));
     $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
       html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
-    $is_collapsed = false; // TODO: determine this somehow?
 
     if ($prop['virtual'])
       $classes[] = 'virtual';
     else if ($prop['readonly'])
       $classes[] = 'readonly';
     if ($prop['subscribed'])
-      $classes[] = ' subscribed';
+      $classes[] = 'subscribed';
     if ($prop['class'])
       $classes[] = $prop['class'];
 
     $content = '';
-    if (!$attrib['activeonly'] || $prop['active']) {
+    if (!$activeonly || $prop['active']) {
       $content = html::div(join(' ', $classes),
         html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
         ($prop['virtual'] ? '' :


commit 96359552f76d84da3467f5c6ea6652b19bc7720a
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 20 13:55:53 2014 +0200

    Add extended splitter to calendar view that allows resizing and collapsing the sidebar

diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 021f4fb..adca758 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -109,21 +109,21 @@ body.attachmentwin #topnav .topright {
 	top: 4px;
 }
 
-#calendarsidebartoggle {
+#calsidebarsplitter {
 	position: absolute;
 	left: 264px;
-	width: 8px;
-	top: 40px;
+	width: 6px;
+	top: 40px !important;
 	bottom: 0;
-	background: url(images/toggle.gif) 0 48% no-repeat transparent;
-	cursor: pointer;
+	background: url(images/toggle.gif) -1px 48% no-repeat transparent;
 }
 
 div.sidebarclosed {
 	background-position: -8px 48% !important;
+	cursor: pointer;
 }
 
-#calendarsidebartoggle:hover {
+#calsidebarsplitter:hover {
 	background-color: #ddd;
 }
 
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 2a76798..ce07112 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -40,7 +40,6 @@
 			</div>
 		</div>
 	</div>
-	<div id="calendarsidebartoggle"></div>
 
 	<div id="quicksearchbar">
 		<roundcube:object name="plugin.searchform" id="quicksearchbox" />
@@ -235,25 +234,11 @@ var UI = new rcube_mail_ui();
 $(document).ready(function(e){
 	UI.init();
 
-	// initialize sidebar toggle
-	$('#calendarsidebartoggle').click(function() {
-		var width = $(this).data('sidebarwidth');
-		var offset = $(this).data('offset');
-		var $sidebar = $('#calendarsidebar'), time = 250;
-		
-		if ($sidebar.is(':visible')) {
-			$sidebar.animate({ left:'-'+(width+10)+'px' }, time, function(){ $('#calendarsidebar').hide(); });
-			$(this).animate({ left:'8px'}, time, function(){ $('#calendarsidebartoggle').addClass('sidebarclosed') });
-			$('#calendar').animate({ left:'20px'}, time, function(){ $(this).fullCalendar('render'); });
-		}
-		else {
-			$sidebar.show().animate({ left:'10px' }, time);
-			$(this).animate({ left:offset+'px'}, time, function(){ $('#calendarsidebartoggle').removeClass('sidebarclosed'); });
-			$('#calendar').animate({ left:(width+16)+'px'}, time, function(){ $(this).fullCalendar('render'); });
-		}
-	})
-	.data('offset', $('#calendarsidebartoggle').position().left)
-	.data('sidebarwidth', $('#calendarsidebar').width() + $('#calendarsidebar').position().left);
+	new calendarview_splitter({ id:'calsidebarsplitter', p1:'#calendarsidebar', p2:'#calendar',
+		orientation:'v', relative:true, start:270, min:240, size:12, offset:0 });
+
+	new rcube_splitter({ id:'calresourceviewsplitter', p1:'#resource-dialog-left', p2:'#resource-dialog-right',
+		orientation:'v', relative:true, start:380, min:220, size:10, offset:-3 }).init();
 
 	// animation to unfold list search box
 	$('#calendars .boxtitle a.search').click(function(e){
@@ -276,10 +261,123 @@ $(document).ready(function(e){
 		});
 	});
 
-	new rcube_splitter({ id:'calresourceviewsplitter', p1:'#resource-dialog-left', p2:'#resource-dialog-right',
-		orientation:'v', relative:true, start:380, min:220, size:10, offset:-3 }).init();
 });
 
+
+/**
+ * Extended rcube_splitter class that entirely collapses the calendar sidebar
+ */
+function calendarview_splitter(p)
+{
+	this.collapsed = false;
+	this.dragging = false;
+	this.threshold = 80;
+	this.lastpos = 0;
+	this._lastpos = 0;
+	this._min = p.min;
+
+	var me = this;
+	p.callback = function(e){
+		if (me.lastpos != me._lastpos) {
+			me.dragging = true;
+			setTimeout(function(){ me.dragging = false; }, 50);
+			me._lastpos = me.lastpos;
+		}
+	};
+
+	// extend base class
+	p.min = 20;
+	rcube_splitter.call(this, p);
+
+	// @override
+	this.resize = function()
+	{
+		if (this.pos < this.threshold) {
+			if (!this.collapsed)
+				this.collapse();
+		}
+		else if (this.pos < this._min && this.pos > this._min / 2) {
+			if (this.collapsed)
+				this.expand();
+		}
+		else if (this.pos >= this._min) {
+			this.p1.css('width', Math.floor(this.pos - this.p1pos.left - this.halfsize) + 'px');
+			this.p2.css('left', Math.ceil(this.pos + this.halfsize) + 'px');
+			this.handle.css('left', Math.round(this.pos - this.halfsize + this.offset + 3)+'px');
+			if (bw.ie) {
+				var new_width = parseInt(this.parent.outerWidth(), 10) - parseInt(this.p2.css('left'), 10) ;
+				this.p2.css('width', (new_width > 0 ? new_width : 0) + 'px');
+			}
+
+			this.p2.resize();
+			this.p1.resize();
+			this.lastpos = this.pos;
+
+			// also resize iframe covers
+			if (this.drag_active) {
+				$('iframe').each(function(i, elem) {
+					var pos = $(this).offset();
+					$('#iframe-splitter-fix-'+i).css({ top: pos.top+'px', left: pos.left+'px', width:elem.offsetWidth+'px', height: elem.offsetHeight+'px' });
+				});
+			}
+
+			if (typeof this.render == 'function')
+				this.render(this);
+		}
+	}
+
+	this.collapse = function(animated)
+	{
+		var me = this, time = 250;
+		if (animated) {
+			this.p1.animate({ left:'0px' }, time, function(){ $(this).hide(); });
+			this.p2.animate({ left:this.p.size + 'px' }, time, function(){ $(this).resize(); });
+			this.handle.animate({ left:'3px'}, time, function(){ $(this).addClass('sidebarclosed') });
+		}
+		else {
+			this.p1.css('left', 0).hide();
+			this.p2.css('left', this.p.size + 'px');
+			this.handle.css('left', '3px').addClass('sidebarclosed');
+			this.p2.resize();
+		}
+
+		// stop dragging
+		if (this.drag_active) {
+			this.drag_active = false;
+			$(document).unbind('.'+this.id);
+			$('div.iframe-splitter-fix').remove();
+		}
+
+		this.pos = 10;
+		this.collapsed = true;
+		this.set_cookie();
+	}
+
+	this.expand = function()
+	{
+		var me = this, time = 250;
+		this.handle.removeClass('sidebarclosed');
+		this.pos = this.lastpos || this._min;
+		this.p1pos.left = 10;
+		this.p1.show().animate({ left:'10px', width:(this.pos - this.p1pos.left - this.halfsize) + 'px' }, time);
+		this.p2.animate({ left:(this.pos + this.halfsize) + 'px' }, time, function(){ me.resize(); });
+		this.handle.animate({ left:(this.pos - this.halfsize + this.offset + 3) + 'px' }, time);
+
+		this.collapsed = false;
+		this.set_cookie();
+	}
+
+	this.init();
+
+	var me = this;
+	this.handle.bind('click', function(e){
+		if (!me.collapsed && !me.dragging)
+			me.collapse(true);
+		else if (!me.dragging)
+			me.expand();
+	});
+}
+
 </script>
 
 </body>


commit af416196d989560b7aa5086276e7fa034225a627
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 20 12:04:22 2014 +0200

    Hide calendars search box and toggle on click of the search icon

diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 5ef63f7..021f4fb 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -164,7 +164,32 @@ pre {
 	right: 0;
 }
 
+#calendars .boxtitle {
+	position: relative;
+}
+
+#calendars .boxtitle a.iconbutton.search {
+	position: absolute;
+	top: 8px;
+	right: 8px;
+	width: 16px;
+	cursor: pointer;
+	background-position: -2px -317px;
+}
+
+#calendars .listsearchbox {
+	display: none;
+}
+
+#calendars .listsearchbox.expanded {
+	display: block;
+}
+
 #calendars .scroller {
+	top: 34px;
+}
+
+#calendars .listsearchbox.expanded + .scroller {
 	top: 68px;
 }
 
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 7f9f0af..2a76798 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -22,7 +22,9 @@
 		<div id="datepicker" class="uibox"></div>
 
 		<div id="calendars" class="uibox listbox" style="visibility:hidden">
-			<h2 class="boxtitle"><roundcube:label name="calendar.calendars" /></h2>
+			<h2 class="boxtitle"><roundcube:label name="calendar.calendars" />
+				<a class="iconbutton search" title="<roundcube:label name='calendar.findcalendars' />"></a>
+			</h2>
 			<div class="listsearchbox">
 				<div class="searchbox">
 					<input type="text" name="q" id="calendarlistsearch" placeholder="<roundcube:label name='calendar.findcalendars' />" />
@@ -253,6 +255,27 @@ $(document).ready(function(e){
 	.data('offset', $('#calendarsidebartoggle').position().left)
 	.data('sidebarwidth', $('#calendarsidebar').width() + $('#calendarsidebar').position().left);
 
+	// animation to unfold list search box
+	$('#calendars .boxtitle a.search').click(function(e){
+		var box = $('#calendars .listsearchbox'),
+			dir = box.is(':visible') ? -1 : 1;
+
+		box.slideToggle({
+			duration: 160,
+			progress: function(animation, progress) {
+				if (dir < 0) progress = 1 - progress;
+				$('#calendars .scroller').css('top', (34 + 34 * progress) + 'px');
+			},
+			complete: function() {
+				box.toggleClass('expanded');
+				if (box.is(':visible')) {
+					box.find('input[type=text]').focus();
+				}
+				// TODO: save state in localStorage
+			}
+		});
+	});
+
 	new rcube_splitter({ id:'calresourceviewsplitter', p1:'#resource-dialog-left', p2:'#resource-dialog-right',
 		orientation:'v', relative:true, start:380, min:220, size:10, offset:-3 }).init();
 });


commit d569bb86440092d817799a09bdcd0de1dd657bbb
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 20 12:03:24 2014 +0200

    Adapt classic skin to new calendars listing

diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css
index 40350fa..0f3ba5f 100644
--- a/plugins/calendar/skins/classic/calendar.css
+++ b/plugins/calendar/skins/classic/calendar.css
@@ -105,6 +105,14 @@ pre {
 	overflow: hidden;
 }
 
+#calendars .boxlistcontent {
+	top: 43px;
+}
+
+#calendars .listsearchbox {
+	padding: 2px 4px;
+}
+
 #calendarslist {
 	list-style: none;
 	margin: 0;
@@ -122,54 +130,143 @@ pre {
 	cursor: default;
 }
 
-#calendarslist li label {
-	display: block;
+#calendars .treelist li {
+	margin: 0;
+	padding: 0;
+	position: relative;
 }
 
-#calendarslist li span.handle {
+#calendars .treelist ul li:last-child {
+	border-bottom: 0;
+}
+
+#calendars .treelist li div.folder,
+#calendars .treelist li div.calendar {
+	position: relative;
+	height: 22px;
+}
+
+#calendars .treelist li span.calname {
+	display: block;
+	padding: 0px 30px 2px 2px;
+	position: absolute;
+	top: 4px;
+	left: 38px;
+	right: 40px;
 	cursor: default;
-	background: url(images/calendars.png) 0 -2px no-repeat;
+	background: url(images/calendars.png) right 20px no-repeat;
+	overflow: hidden;
+	text-overflow: ellipsis;
+	white-space: nowrap;
+}
+
+#calendars .treelist li div.virtual > span.calname {
+	color: #aaa;
+	left: 20px;
+}
+
+#calendars .treelist.flat li span.calname {
+	left: 24px;
+	right: 22px;
+}
+
+#calendars .treelist li span.handle {
 	display: inline-block;
-	width: 20px;
+	position: absolute;
+	top: 5px;
+	right: 6px;
+	padding: 0;
+	width: 12px;
+	height: 12px;
+	border-radius: 3px;
+	font-size: 0.8em;
 }
 
-#calendarslist li input {
-	margin-right: 5px;
+#calendars .treelist li a.subscribed {
+	display: inline-block;
+	position: absolute;
+	top: 2px;
+	right: 22px;
+	height: 16px;
+	width: 16px;
+	padding: 0;
+	background: url(images/calendars.png) -100px 0 no-repeat;
+	overflow: hidden;
+	text-indent: -5000px;
+	cursor: pointer;
 }
 
-#calendarslist li.selected {
-	background-color: #ccc;
-	border-bottom: 1px solid #bbb;
+#calendars .treelist div:hover > a.subscribed {
+	background-position: 0 -126px;
 }
 
-#calendarslist li.selected span {
-	font-weight: bold;
+#calendars .treelist div.subscribed a.subscribed {
+	background-position: 0 -144px;
+}
+
+#calendars .treelist li input {
+	position: absolute;
+	top: 1px;
+	left: 18px;
+}
+
+#calendars .treelist li div.treetoggle {
+	top: -1px;
+	left: 1px !important;
 }
 
-#calendarslist li.readonly span.handle {
-    background-position: 0 -20px;
+#calendars .treelist ul li div.treetoggle {
+	left: 17px !important;
 }
 
-#calendarslist li.other span.handle {
-    background-position: 0 -38px;
+#calendars .treelist ul ul li div.treetoggle {
+	left: 33px !important;
 }
 
-#calendarslist li.other.readonly span.handle {
-    background-position: 0 -56px;
+#calendars .treelist.flat li input {
+	left: 4px;
 }
 
-#calendarslist li.shared span.handle {
-    background-position: 0 -74px;
+#calendars .treelist ul li div.folder,
+#calendars .treelist ul li div.calendar {
+	margin-left: 16px;
 }
 
-#calendarslist li.shared.readonly span.handle {
-    background-position: 0 -92px;
+#calendars .treelist ul ul li div.folder,
+#calendars .treelist ul ul li div.calendar {
+	margin-left: 32px;
+}
+
+#calendars .treelist ul ul ul li div.folder,
+#calendars .treelist ul ul ul li div.calendar {
+	margin-left: 48px;
+}
+
+#calendars .treelist li.selected {
+	background-color: #ccc;
+}
+
+#calendars .treelist li.selected > span.calname {
+	font-weight: bold;
+}
+
+#calendars .treelist div.readonly span.calname {
+	background-position: right -20px;
+}
+
+#calendars .treelist li.user > div > span.calname {
+	background-position: right -38px;
 }
 
 #calendarslist li.virtual span.calname {
 	color: #666;
 }
 
+#calendars .searchresults .boxtitle {
+	border-top: 1px solid #aaa;
+	margin-bottom: 0;
+}
+
 #calfeedurl,
 #caldavurl {
 	width: 98%;
diff --git a/plugins/calendar/skins/classic/images/calendars.gif b/plugins/calendar/skins/classic/images/calendars.gif
index cf12ebd..c560e74 100644
Binary files a/plugins/calendar/skins/classic/images/calendars.gif and b/plugins/calendar/skins/classic/images/calendars.gif differ
diff --git a/plugins/calendar/skins/classic/images/calendars.png b/plugins/calendar/skins/classic/images/calendars.png
index feb3945..e411c41 100644
Binary files a/plugins/calendar/skins/classic/images/calendars.png and b/plugins/calendar/skins/classic/images/calendars.png differ
diff --git a/plugins/calendar/skins/classic/templates/calendar.html b/plugins/calendar/skins/classic/templates/calendar.html
index fa93afc..219fd83 100644
--- a/plugins/calendar/skins/classic/templates/calendar.html
+++ b/plugins/calendar/skins/classic/templates/calendar.html
@@ -24,8 +24,15 @@
     <div id="datepicker"></div>
     <div id="calendars" style="visibility:hidden">
       <div class="boxtitle"><roundcube:label name="calendar.calendars" /></div>
+      <div class="listsearchbox">
+        <div class="searchbox">
+          <input type="text" name="q" id="calendarlistsearch" placeholder="<roundcube:label name='calendar.findcalendars' />" />
+          <a class="iconbutton searchicon"></a>
+          <roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="reset searchreset" title="resetsearch" content="x" />
+        </div>
+      </div>
       <div class="boxlistcontent">
-      <roundcube:object name="plugin.calendar_list" id="calendarslist" />
+      <roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist" />
       </div>
       <div class="boxfooter">
         <roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="buttonPas addgroup" classAct="button addgroup" content=" " />


commit 7c07ad1d427cd9c75fc4c40fe15c520b5b046273
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 20 09:50:41 2014 +0200

    Display user's email address as hover title

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 3f74f0f..c845c15 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -113,6 +113,14 @@ class kolab_calendar extends kolab_storage_folder_api
     return $this->name;
   }
 
+  /**
+   *
+   */
+  public function get_title()
+  {
+    return null;
+  }
+
 
   /**
    * Return color to display this calendar
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index c07d74a..217fe42 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -151,6 +151,7 @@ class kolab_driver extends calendar_driver
           'editname' => $cal->get_foldername(),
           'color'    => $cal->get_color(),
           'active'   => $cal->is_active(),
+          'title'    => $cal->get_owner(),
           'owner'    => $cal->get_owner(),
           'virtual'  => false,
           'readonly' => true,
@@ -176,6 +177,7 @@ class kolab_driver extends calendar_driver
           'name'     => $fullname,
           'listname' => $listname,
           'editname' => $cal->get_foldername(),
+          'title'    => $cal->get_title(),
           'color'    => $cal->get_color(),
           'readonly' => $cal->readonly,
           'showalarms' => $cal->alarms,
@@ -432,10 +434,8 @@ class kolab_driver extends calendar_driver
 
         // search for calendar folders shared by this user
         foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
-          if (1 || !kolab_storage::folder_is_subscribed($foldername, true)) {
-            $cal = new kolab_calendar($foldername, $this->cal);
-            $this->calendars[$cal->id] = $cal;
-          }
+          $cal = new kolab_calendar($foldername, $this->cal);
+          $this->calendars[$cal->id] = $cal;
         }
       }
 
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index 7fd74ee..1ab8679 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -90,6 +90,15 @@ class kolab_user_calendar extends kolab_calendar
 
 
   /**
+   *
+   */
+  public function get_title()
+  {
+    return $this->userdata['mail'];
+  }
+
+
+  /**
    * Getter for the name of the namespace to which the IMAP folder belongs
    *
    * @return string Name of the namespace (personal, other, shared)
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 74ea5d7..62b19f7 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -288,8 +288,8 @@ class calendar_ui
     }
 
     $classes = array('calendar', 'cal-'  . asciiwords($id, true));
-    $title = $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
-      html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
+    $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+      html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '');
     $is_collapsed = false; // TODO: determine this somehow?
 
     if ($prop['virtual'])


commit 073a6bb3737e1f4c70593c7ce0f4b4f98699fdc0
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon May 19 12:18:39 2014 +0200

    Aggregate shared but unsubscribed calendar folders and free/busy data into a 'user calendar'

diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index 4cf3dab..7fd74ee 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -30,6 +30,7 @@ class kolab_user_calendar extends kolab_calendar
   public $subscriptions = false;
 
   protected $userdata = array();
+  protected $timeindex = array();
 
 
   /**
@@ -165,9 +166,158 @@ class kolab_user_calendar extends kolab_calendar
    */
   public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
   {
-    // TODO: implement this
-    console('kolab_user_calendar::list_events()');
-    return array();
+    // convert to DateTime for comparisons
+    try {
+      $start_dt = new DateTime('@'.$start);
+    }
+    catch (Exception $e) {
+      $start_dt = new DateTime('@0');
+    }
+    try {
+      $end_dt = new DateTime('@'.$end);
+    }
+    catch (Exception $e) {
+      $end_dt = new DateTime('today +10 years');
+    }
+
+    $limit_changed = null;
+    if (!empty($query)) {
+      foreach ($query as $q) {
+        if ($q[0] == 'changed' && $q[1] == '>=') {
+          try { $limit_changed = new DateTime('@'.$q[2]); }
+          catch (Exception $e) { /* ignore */ }
+        }
+      }
+    }
+
+    // aggregate all calendar folders the user shares (but are not subscribed)
+    foreach (kolab_storage::list_user_folders($this->userdata, 'event', false) as $foldername) {
+      if (!kolab_storage::folder_is_subscribed($foldername, true)) {
+        $cal = new kolab_calendar($foldername, $this->cal);
+        foreach ($cal->list_events($start, $end, $search, 1) as $event) {
+          $this->events[$event['id']] = $event;
+          $this->timeindex[$this->time_key($event)] = $event['id'];
+        }
+      }
+    }
+
+    // get events from the user's free/busy feed
+    $this->fetch_freebusy($limit_changed);
+
+    $events = array();
+    foreach ($this->events as $id => $event) {
+      // list events in requested time window
+      if ($event['start'] <= $end_dt && $event['end'] >= $start_dt &&
+           (!$limit_changed || !$event['changed'] || $event['changed'] >= $limit_changed)) {
+        $events[] = $event;
+      }
+    }
+
+    return $events;
+  }
+
+  /**
+   * Helper method to fetch free/busy data for the user and turn it into calendar data
+   */
+  private function fetch_freebusy($limit_changed = null)
+  {
+    // ask kolab server first
+    try {
+      $request_config = array(
+        'store_body'       => true,
+        'follow_redirects' => true,
+      );
+      $request  = libkolab::http_request(kolab_storage::get_freebusy_url($this->userdata['mail']), 'GET', $request_config);
+      $response = $request->send();
+
+      // authentication required
+      if ($response->getStatus() == 401) {
+        $request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
+        $response = $request->send();
+      }
+
+      if ($response->getStatus() == 200)
+        $fbdata = $response->getBody();
+
+      unset($request, $response);
+    }
+    catch (Exception $e) {
+      rcube::raise_error(array(
+        'code' => 900,
+        'type' => 'php',
+        'file' => __FILE__,
+        'line' => __LINE__,
+        'message' => "Error fetching free/busy information: " . $e->getMessage()),
+        true, false);
+
+      return false;
+    }
+
+    $statusmap = array(
+      'FREE' => 'free',
+      'BUSY' => 'busy',
+      'BUSY-TENTATIVE' => 'tentative',
+      'X-OUT-OF-OFFICE' => 'outofoffice',
+      'OOF' => 'outofoffice',
+    );
+    $titlemap = array(
+      'FREE' => $this->cal->gettext('availfree'),
+      'BUSY' => $this->cal->gettext('availbusy'),
+      'BUSY-TENTATIVE' => $this->cal->gettext('availtentative'),
+      'X-OUT-OF-OFFICE' => $this->cal->gettext('availoutofoffice'),
+    );
+
+    // console('_fetch_freebusy', kolab_storage::get_freebusy_url($this->userdata['mail']), $fbdata);
+
+    // parse free-busy information using Horde classes
+    $count = 0;
+    if ($fbdata) {
+      $ical = $this->cal->get_ical();
+      $ical->import($fbdata);
+      if ($fb = $ical->freebusy) {
+        $result = array();
+
+        // consider 'changed >= X' queries
+        if ($limit_changed && $fb['created'] && $fb['created'] < $limit_changed) {
+          return 0;
+        }
+
+        foreach ($fb['periods'] as $tuple) {
+          list($from, $to, $type) = $tuple;
+          $event = array(
+            'id'        => md5($this->id . $from->format('U') . '/' . $to->format('U')),
+            'calendar'  => $this->id,
+            'changed'   => $fb['created'] ?: new DateTime(),
+            'title'     => $titlemap[$type] ?: $type,
+            'start'     => $from,
+            'end'       => $to,
+            'free_busy' => $statusmap[$type] ?: 'busy',
+            'organizer' => array(
+              'email' => $this->userdata['mail'],
+              'name'  => $this->userdata['displayname'],
+            ),
+          );
+
+          // avoid duplicate entries
+          $key = $this->time_key($event);
+          if (!$this->timeindex[$key]) {
+            $this->events[$event['id']] = $event;
+            $this->timeindex[$key] = $event['id'];
+            $count++;
+          }
+        }
+      }
+    }
+
+    return $count;
+  }
+
+  /**
+   * Helper to build a key for the absolute time slot the given event convers
+   */
+  private function time_key($event)
+  {
+    return sprintf('%s/%s', $event['start']->format('U'), is_object($event['end']->format('U')) ?: '0');
   }
 
 
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 1dda548..3b0f159 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -673,9 +673,12 @@ class libvcalendar implements Iterator
                 continue;
 
             switch ($prop->name) {
+            case 'CREATED':
+            case 'LAST-MODIFIED':
+            case 'DTSTAMP':
             case 'DTSTART':
             case 'DTEND':
-                $propmap = array('DTSTART' => 'start', 'DTEND' => 'end');
+                $propmap = array('DTSTART' => 'start', 'DTEND' => 'end', 'CREATED' => 'created', 'LAST-MODIFIED' => 'changed', 'DTSTAMP' => 'changed');
                 $this->freebusy[$propmap[$prop->name]] =  self::convert_datetime($prop);
                 break;
 


commit fd3f93d64eee2495783d434411247270e99a2e36
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon May 19 12:16:14 2014 +0200

    Use the dedicated config option

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 74c0ed3..4266fb5 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1340,7 +1340,7 @@ class kolab_storage
 
         // resolve to IMAP folder name
         $root = self::namespace_root('other');
-        $user_attrib = self::$config->get('kolab_auth_login', 'mail');
+        $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
 
         array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
             list($localpart, $domain) = explode('@', $user[$user_attrib]);
@@ -1368,7 +1368,7 @@ class kolab_storage
         $folders = array();
 
         // use localpart of user attribute as root for folder listing
-        $user_attrib = self::$config->get('kolab_auth_login', 'mail');
+        $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
         if (!empty($user[$user_attrib])) {
             list($mbox) = explode('@', $user[$user_attrib]);
 


commit 2d85ff565f554577e4df8b30fdb08b79cacbb216
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri May 16 10:59:28 2014 +0200

    Find next matching parent folder

diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 8f1af0c..c07d74a 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -128,8 +128,13 @@ class kolab_driver extends calendar_driver
       $fullname = $cal->get_name();
       $listname = $cal->get_foldername();
       $imap_path = explode('/', $cal->name);
-      $topname = array_pop($imap_path);
-      $parent_id = kolab_storage::folder_id(join('/', $imap_path));
+
+      // find parent
+      do {
+        array_pop($imap_path);
+        $parent_id = kolab_storage::folder_id(join('/', $imap_path));
+      }
+      while (count($imap_path) > 0 && !$this->calendars[$parent_id]);
 
       // turn a kolab_storage_folder object into a kolab_calendar
       if ($cal instanceof kolab_storage_folder) {


commit 0fbfff3349d487eac1e5a379c23e1728340d408b
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri May 16 10:38:37 2014 +0200

    Limit the user search results and display message if list is truncated

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index f910b2b..15c81f9 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -746,6 +746,10 @@ class calendar extends rcube_plugin
 
           $results[] = $cal;
         }
+        // report more results available
+        if ($this->driver->search_more_results)
+          $this->rc->output->show_message('autocompletemore', 'info');
+
         $this->rc->output->command('multi_thread_http_response', $results, get_input_value('_reqid', RCUBE_INPUT_GPC));
         return;
     }
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 8126339..8f1af0c 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -409,6 +409,7 @@ class kolab_driver extends calendar_driver
       return array();
 
     $this->calendars = array();
+    $this->search_more_results = false;
 
     // find unsubscribed IMAP folders that have "event" type
     if ($source == 'folders') {
@@ -419,7 +420,8 @@ class kolab_driver extends calendar_driver
     }
     // find other user's virtual calendars
     else if ($source == 'users') {
-      foreach (kolab_storage::search_users($query, 0) as $user) {
+      $limit = $this->rc->config->get('autocomplete_max', 15) * 2;  // we have slightly more space, so display twice the number
+      foreach (kolab_storage::search_users($query, 0, array(), $limit, $count) as $user) {
         $calendar = new kolab_user_calendar($user, $this->cal);
         $this->calendars[$calendar->id] = $calendar;
 
@@ -431,6 +433,10 @@ class kolab_driver extends calendar_driver
           }
         }
       }
+
+      if ($count > $limit) {
+        $this->search_more_results = true;
+      }
     }
 
     // don't list the birthday calendar


commit 7d5fe4c7942bb26b3c91353fbb19e71aea4320c4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri May 16 10:36:57 2014 +0200

    Fix LDAP search calls and return the number of matches

diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index 274d57f..00882a8 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -36,6 +36,7 @@ function kolab_folderlist(node, p)
     var search_results_widget;
     var search_results_container;
     var listsearch_request;
+    var search_messagebox;
 
     var Q = rcmail.quote_html;
 
@@ -164,8 +165,29 @@ function kolab_folderlist(node, p)
         }
         search_results = {};
 
+        if (search_messagebox)
+            rcmail.hide_message(search_messagebox);
+
         // send search request(s) to server
         if (search.query && search.execute) {
+            // require a minimum length for the search string
+            if (rcmail.env.autocomplete_min_length && search.query.length < rcmail.env.autocomplete_min_length) {
+                search_messagebox = rcmail.display_message(
+                    rcmail.get_label('autocompletechars').replace('$min', rcmail.env.autocomplete_min_length));
+                return;
+            }
+
+            if (listsearch_request) {
+                // ignore, let the currently runnung sequest finish
+                if (listsearch_request.query == search.query) {
+                    return;
+                }
+                else { // cancel previous search request
+                    rcmail.multi_thread_request_abort(listsearch_request.id);
+                    listsearch_request = null;
+                }
+            }
+
             var sources = p.search_sources || [ 'folders' ];
             var reqid = rcmail.multi_thread_http_request({
                 items: sources,
@@ -173,10 +195,15 @@ function kolab_folderlist(node, p)
                 action:  p.search_action || 'listsearch',
                 postdata: { action:'search', q:search.query, source:'%s' },
                 lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
-                onresponse: render_search_results
+                onresponse: render_search_results,
+                whendone: function(e){ listsearch_request = null; }
             });
 
-            listsearch_request = { id:reqid, sources:sources.slice(), num:sources.length };
+            listsearch_request = { id:reqid, query:search.query };
+        }
+        else if (!search.query && listsearch_request) {
+            rcmail.multi_thread_request_abort(listsearch_request.id);
+            listsearch_request = null;
         }
     });
 
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 1cbdd76..74c0ed3 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1318,11 +1318,12 @@ class kolab_storage
      * @param mixed   $query    Search value (or array of field => value pairs)
      * @param int     $mode     Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
      * @param array   $required List of fields that shall ot be empty
-     * @param int     $limit    Number of records
+     * @param int     $limit    Maximum number of records
+     * @param int     $count    Returns the number of records found
      *
      * @return array List or false on error
      */
-    public static function search_users($query, $mode = 1, $required = array(), $limit = 0)
+    public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
     {
         // requires a working LDAP setup
         if (!self::ldap()) {
@@ -1330,7 +1331,12 @@ class kolab_storage
         }
 
         // search users using the configured attributes
-        $results = self::$ldap->search(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit);
+        $results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count);
+
+        // exclude myself
+        if ($_SESSION['kolab_dn']) {
+            unset($results[$_SESSION['kolab_dn']]);
+        }
 
         // resolve to IMAP folder name
         $root = self::namespace_root('other');
@@ -1391,6 +1397,9 @@ class kolab_storage
             $path_len = count(explode($delimiter, $other_ns));
 
             foreach ((array)self::list_folders($other_ns, '*', '', $subscribed) as $foldername) {
+                if ($foldername == 'INBOX')  // skip INBOX which is added by default
+                    continue;
+
                 // truncate folder path to top-level folders of the 'other' namespace
                 $path = explode($delimiter, $foldername);
                 $foldername = join($delimiter, array_slice($path, 0, $path_len + 1));


commit 115c4c54b75791d80f7ab620003f89f4b12501fb
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Fri May 16 10:34:32 2014 +0200

    Rename kolab_auth_ldap::search() method because its signature doesn't match rcube_ldap_generic::search() and fails recursive calls in VLV search mode + add return parameter for results count

diff --git a/plugins/kolab_auth/kolab_auth_ldap.php b/plugins/kolab_auth/kolab_auth_ldap.php
index 7044ebf..40237ac 100644
--- a/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/plugins/kolab_auth/kolab_auth_ldap.php
@@ -215,10 +215,11 @@ class kolab_auth_ldap extends rcube_ldap_generic
      *                          2 - prefix (abc*)
      * @param array   $required List of fields that cannot be empty
      * @param int     $limit    Number of records
+     * @param int     $count    Returns the number of records found
      *
      * @return array List or false on error
      */
-    function search($fields, $value, $mode=1, $required = array(), $limit = 0)
+    function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0)
     {
         if (empty($fields)) {
             return array();
@@ -295,7 +296,8 @@ class kolab_auth_ldap extends rcube_ldap_generic
         $attrs   = array_values($this->fieldmap);
         $list    = array();
 
-        if ($result = parent::search($base_dn, $filter, $scope, $attrs)) {
+        if ($result = $this->search($base_dn, $filter, $scope, $attrs)) {
+            $count = $result->count();
             $i = 0;
             foreach ($result as $entry) {
                 if ($limit && $limit <= $i) {
diff --git a/plugins/kolab_delegation/kolab_delegation_engine.php b/plugins/kolab_delegation/kolab_delegation_engine.php
index ad96831..3d3bd33 100644
--- a/plugins/kolab_delegation/kolab_delegation_engine.php
+++ b/plugins/kolab_delegation/kolab_delegation_engine.php
@@ -202,7 +202,7 @@ class kolab_delegation_engine
             return array();
         }
 
-        $list = $ldap->search($this->ldap_login_field, $login, 1);
+        $list = $ldap->dosearch($this->ldap_login_field, $login, 1);
 
         if (count($list) == 1) {
             $dn   = key($list);
@@ -288,7 +288,7 @@ class kolab_delegation_engine
             return array();
         }
 
-        $list = $ldap->search($this->ldap_delegate_field, $this->ldap_dn, 1);
+        $list = $ldap->dosearch($this->ldap_delegate_field, $this->ldap_dn, 1);
 
         foreach ($list as $dn => $delegator) {
             $delegator = $this->parse_ldap_record($delegator, $dn);
@@ -424,7 +424,7 @@ class kolab_delegation_engine
         $fields = array_unique(array_filter(array_merge((array)$this->ldap_name_field, (array)$this->ldap_login_field)));
         $users  = array();
 
-        $result = $ldap->search($fields, $search, $mode, (array)$this->ldap_login_field, $max);
+        $result = $ldap->dosearch($fields, $search, $mode, (array)$this->ldap_login_field, $max);
 
         foreach ($result as $record) {
             // skip self


commit b415c512f00e8b60ddc079cd65fd610f4b612f16
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu May 15 17:20:58 2014 +0200

    Use folder namespace for grouping in client-side treelist

diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index d0e6b7b..3b833ab 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -138,7 +138,7 @@ class database_driver extends calendar_driver
           'color'      => $prefs['color'],
           'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
           'active'     => !in_array($id, $hidden),
-          'class_name' => 'birthdays',
+          'group'      => 'birthdays',
           'readonly'   => true,
           'default'    => false,
           'children'   => false,
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index ff8372b..8126339 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -149,7 +149,8 @@ class kolab_driver extends calendar_driver
           'owner'    => $cal->get_owner(),
           'virtual'  => false,
           'readonly' => true,
-          'class_name' => 'user',
+          'group'    => 'other',
+          'class'    => 'user',
         );
       }
       else if ($cal->virtual) {
@@ -160,6 +161,8 @@ class kolab_driver extends calendar_driver
           'editname' => $cal->get_foldername(),
           'virtual' => true,
           'readonly' => true,
+          'group'    => $cal->get_namespace(),
+          'class'    => 'folder',
         );
       }
       else {
@@ -171,7 +174,7 @@ class kolab_driver extends calendar_driver
           'color'    => $cal->get_color(),
           'readonly' => $cal->readonly,
           'showalarms' => $cal->alarms,
-          'class_name' => $cal->get_namespace(),
+          'group'    => $cal->get_namespace(),
           'default'  => $cal->default,
           'active'   => $cal->is_active(),
           'owner'    => $cal->get_owner(),
@@ -198,7 +201,7 @@ class kolab_driver extends calendar_driver
           'color'      => $prefs[$id]['color'],
           'active'     => $prefs[$id]['active'],
           'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
-          'class_name' => 'birthdays',
+          'group'      => 'birthdays',
           'readonly'   => true,
           'default'    => false,
           'children'   => false,
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index a41e065..4cf3dab 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -56,6 +56,7 @@ class kolab_user_calendar extends kolab_calendar
       $this->id = kolab_storage::folder_id($this->userdata['kolabtargetfolder'], true);
       $this->imap_folder = $this->userdata['kolabtargetfolder'];
       $this->name = $this->storage->get_name();
+      $this->parent = '';  // user calendars are top level
 
       // user-specific alarms settings win
       $prefs = $this->cal->rc->config->get('kolab_calendars', array());
@@ -94,7 +95,7 @@ class kolab_user_calendar extends kolab_calendar
    */
   public function get_namespace()
   {
-    return 'user';
+    return 'other user';
   }
 
 
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 4d41194..74ea5d7 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -229,7 +229,7 @@ class calendar_ui
       if ($attrib['activeonly'] && !$prop['active'])
         continue;
 
-      $html .= html::tag('li', array('id' => 'rcmlical' . $id),
+      $html .= html::tag('li', array('id' => 'rcmlical' . $id, 'class' => $prop['group']),
         $content = $this->calendar_list_item($id, $prop, $jsenv)
       );
     }
@@ -260,7 +260,7 @@ class calendar_ui
       if (strlen($content)) {
         $out .= html::tag('li', array(
             'id' => 'rcmlical' . rcube_utils::html_identifier($id),
-            'class' => $prop['virtual'] ? 'virtual' : '',
+            'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
           ),
           $content);
       }
@@ -287,23 +287,23 @@ class calendar_ui
       $jsenv[$id] = $prop;
     }
 
-    $class = 'calendar cal-'  . asciiwords($id, true);
+    $classes = array('calendar', 'cal-'  . asciiwords($id, true));
     $title = $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
       html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
     $is_collapsed = false; // TODO: determine this somehow?
 
     if ($prop['virtual'])
-      $class = 'folder virtual';
+      $classes[] = 'virtual';
     else if ($prop['readonly'])
-      $class .= ' readonly';
+      $classes[] = 'readonly';
     if ($prop['subscribed'])
-      $class .= ' subscribed';
-    if ($prop['class_name'])
-      $class .= ' '.$prop['class_name'];
+      $classes[] = ' subscribed';
+    if ($prop['class'])
+      $classes[] = $prop['class'];
 
     $content = '';
     if (!$attrib['activeonly'] || $prop['active']) {
-      $content = html::div($class,
+      $content = html::div(join(' ', $classes),
         html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
         ($prop['virtual'] ? '' :
           html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 67675bf..b92f377 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -86,8 +86,9 @@ $labels['nmonthsback'] = '$nr months back';
 $labels['showurl'] = 'Show calendar URL';
 $labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
 $labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
+$labels['findcalendars'] = 'Find calendars...';
 $labels['calsearchresults'] = 'Available Calendars';
-$labels['calendarsubscribe'] = 'Listed permanently';
+$labels['calendarsubscribe'] = 'List permanently';
 
 // agenda view
 $labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index cca70e6..5ef63f7 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -293,7 +293,7 @@ pre {
 	background-position: right -20px;
 }
 
-#calendars .treelist div.user span.calname {
+#calendars .treelist li.user > div > span.calname {
 	background-position: right -38px;
 }
 /*
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 0842cf0..7f9f0af 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -25,7 +25,7 @@
 			<h2 class="boxtitle"><roundcube:label name="calendar.calendars" /></h2>
 			<div class="listsearchbox">
 				<div class="searchbox">
-					<input type="text" name="q" id="calendarlistsearch" />
+					<input type="text" name="q" id="calendarlistsearch" placeholder="<roundcube:label name='calendar.findcalendars' />" />
 					<a class="iconbutton searchicon"></a>
 					<roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
 				</div>
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index 587fe56..274d57f 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -49,10 +49,12 @@ function kolab_folderlist(node, p)
                   .html(p.search_title ? '<h2 class="boxtitle">' + p.search_title + '</h2>' : '')
                   .insertAfter(me.container);
 
-              search_results_widget = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
+              search_results_widget = new rcube_treelist_widget('<ul>', {
                   id_prefix: p.id_prefix,
                   selectable: false
               });
+              // copy classes from main list
+              search_results_widget.container.addClass(me.container.attr('class'));
 
               // register click handler on search result's checkboxes to select the given item for listing
               search_results_widget.container
@@ -94,7 +96,7 @@ function kolab_folderlist(node, p)
               search_results[prop.id] = prop;
               search_results_widget.insert({
                   id: prop.id,
-                  classes: prop.class_name ? String(prop.class_name).split(' ') : [],
+                  classes: [ prop.group || '' ],
                   html: item,
                   collapsed: true
               }, prop.parent);
@@ -142,10 +144,10 @@ function kolab_folderlist(node, p)
             // move this result item to the main list widget
             me.insert({
                 id: id,
-                classes: [],
+                classes: [ prop.group || '' ],
                 virtual: prop.virtual,
                 html: dom_node,
-            }, parent_id, parent_id ? true : false);
+            }, parent_id, prop.group);
         }
 
         delete prop.html;


commit 857078428b952de8aa8537ec1e2238198ab50f12
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu May 15 15:53:35 2014 +0200

    Toggle IMAP subscriptions directly from the calendars/folders list (#3042)

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index fc0069d..f910b2b 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -725,7 +725,7 @@ class calendar extends rcube_plugin
           $this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
         break;
       case "subscribe":
-        if (!$this->driver->subscribe_calendar($cal, intval(get_input_value('perm', RCUBE_INPUT_GPC))))
+        if (!$this->driver->subscribe_calendar($cal))
           $this->rc->output->show_message($this->gettext('errorsaving'), 'error');
         return;
       case "search":
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 38a32a8..68ae94f 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2697,9 +2697,14 @@ function rcube_calendar_ui(settings)
         me.calendars[id].color = color;
       }
 
-      if (fc && cal.active) {
-        fc.fullCalendar('addEventSource', me.calendars[id]);
-        rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:cal.active?1:0 } });
+      if (fc && (cal.active || cal.subscribed)) {
+        if (cal.active)
+          fc.fullCalendar('addEventSource', me.calendars[id]);
+
+        var submit = { id: id, active: cal.active ? 1 : 0 };
+        if (cal.subscribed !== undefined)
+            submit.permanent = cal.subscribed ? 1 : 0;
+        rcmail.http_post('calendar', { action:'subscribe', c:submit });
       }
 
       // insert to #calendar-select options if writeable
@@ -2761,6 +2766,13 @@ function rcube_calendar_ui(settings)
         }
       }
     });
+    calendars_list.addEventListener('subscribe', function(p) {
+      var cal;
+      if ((cal = me.calendars[p.id])) {
+        cal.subscribed = p.subscribed || false;
+        rcmail.http_post('calendar', { action:'subscribe', c:{ id:p.id, active:cal.active?1:0, permanent:cal.subscribed?1:0 } });
+      }
+    });
 
     // init (delegate) event handler on calendar list checkboxes
     $(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e){
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index c4b7dbb..3f74f0f 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -30,6 +30,7 @@ class kolab_calendar extends kolab_storage_folder_api
   public $readonly = true;
   public $attachments = true;
   public $alarms = false;
+  public $subscriptions = true;
   public $categories = array();
   public $storage;
 
@@ -103,19 +104,6 @@ class kolab_calendar extends kolab_storage_folder_api
 
 
   /**
-   * Getter for a nice and human readable name for this calendar
-   * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
-   *
-   * @return string Name of this calendar
-   */
-  public function get_name()
-  {
-    $folder = kolab_storage::object_name($this->name, $this->namespace);
-    return $folder;
-  }
-
-
-  /**
    * Getter for the IMAP folder name
    *
    * @return string Name of the IMAP folder
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index d1bb655..ff8372b 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -180,6 +180,10 @@ class kolab_driver extends calendar_driver
           'caldavurl' => $cal->get_caldav_url(),
         );
       }
+
+      if ($cal->subscriptions) {
+        $calendars[$cal->id]['subscribed'] = (bool)$cal->is_subscribed();
+      }
     }
 
     // append the virtual birthdays calendar
@@ -258,7 +262,6 @@ class kolab_driver extends calendar_driver
     // create calendar object if necesary
     if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
       $calendar = kolab_calendar::factory($id, $this->cal);
-      console($id, $calendar->id, $calendar->ready);
       if ($calendar->ready)
         $this->calendars[$calendar->id] = $calendar;
     }
@@ -342,11 +345,15 @@ class kolab_driver extends calendar_driver
    *
    * @see calendar_driver::subscribe_calendar()
    */
-  public function subscribe_calendar($prop, $permanent = false)
+  public function subscribe_calendar($prop)
   {
     if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
-      if ($permanent) $cal->storage->subscribe($prop['active']);
-      return $cal->storage->activate($prop['active']);
+      $ret = false;
+      if (isset($prop['permanent']))
+        $ret |= $cal->storage->subscribe($prop['permanent']);
+      if (isset($prop['active']))
+        $ret |= $cal->storage->activate($prop['active']);
+      return $ret;
     }
     else {
       // save state in local prefs
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index 3add82a..a41e065 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -27,6 +27,7 @@ class kolab_user_calendar extends kolab_calendar
   public $ready = false;
   public $readonly = true;
   public $attachments = false;
+  public $subscriptions = false;
 
   protected $userdata = array();
 
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 4584f19..4d41194 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -296,6 +296,8 @@ class calendar_ui
       $class = 'folder virtual';
     else if ($prop['readonly'])
       $class .= ' readonly';
+    if ($prop['subscribed'])
+      $class .= ' subscribed';
     if ($prop['class_name'])
       $class .= ' '.$prop['class_name'];
 
@@ -303,8 +305,11 @@ class calendar_ui
     if (!$attrib['activeonly'] || $prop['active']) {
       $content = html::div($class,
         html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
-        ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
-        html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' '))
+        ($prop['virtual'] ? '' :
+          html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
+          (isset($prop['subscribed']) ? html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->cal->gettext('calendarsubscribe')), $this->cal->gettext('subscribed')) : '') .
+          html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')
+        )
       );
     }
 
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 92b4fb5..67675bf 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -87,6 +87,7 @@ $labels['showurl'] = 'Show calendar URL';
 $labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
 $labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
 $labels['calsearchresults'] = 'Available Calendars';
+$labels['calendarsubscribe'] = 'Listed permanently';
 
 // agenda view
 $labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 71dd106..cca70e6 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -190,7 +190,7 @@ pre {
 	position: absolute;
 	top: 7px;
 	left: 38px;
-	right: 22px;
+	right: 40px;
 	cursor: default;
 	background: url(images/calendars.png) right 20px no-repeat;
 	overflow: hidden;
@@ -207,6 +207,7 @@ pre {
 
 #calendars .treelist.flat li span.calname {
 	left: 24px;
+	right: 22px;
 }
 
 #calendars .treelist li span.handle {
@@ -225,6 +226,28 @@ pre {
 	box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
 }
 
+#calendars .treelist li a.subscribed {
+	display: inline-block;
+	position: absolute;
+	top: 7px;
+	right: 24px;
+	height: 16px;
+	width: 16px;
+	padding: 0;
+	background: url(images/calendars.png) -100px 0 no-repeat;
+	overflow: hidden;
+	text-indent: -5000px;
+	cursor: pointer;
+}
+
+#calendars .treelist div:hover > a.subscribed {
+	background-position: 1px -110px;
+}
+
+#calendars .treelist div.subscribed a.subscribed {
+	background-position: -15px -110px;
+}
+
 #calendars .treelist li input {
 	position: absolute;
 	top: 5px;
diff --git a/plugins/calendar/skins/larry/images/calendars.png b/plugins/calendar/skins/larry/images/calendars.png
index c2de67d..1f97abc 100644
Binary files a/plugins/calendar/skins/larry/images/calendars.png and b/plugins/calendar/skins/larry/images/calendars.png differ
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index 3c35846..587fe56 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -57,20 +57,29 @@ function kolab_folderlist(node, p)
               // register click handler on search result's checkboxes to select the given item for listing
               search_results_widget.container
                   .appendTo(search_results_container)
-                  .on('click', 'input[type=checkbox]', function(e) {
-                      if (!this.checked)
-                          return;
-
+                  .on('click', 'input[type=checkbox], a.subscribed', function(e) {
                       var li = $(this).closest('li'),
                           id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '')
                           node = search_results_widget.get_node(id),
                           has_children = node.children && node.children.length;
 
+                      // activate + subscribe
+                      if ($(e.target).hasClass('subscribed')) {
+                          search_results[id].subscribed = true;
+                          li.children().first()
+                              .toggleClass('subscribed')
+                              .find('input[type=checkbox]').get(0).checked = true;
+                      }
+                      else if (!this.checked) {
+                          return;
+                      }
+
                       // copy item to the main list
                       add_result2list(id, li, true);
 
                       if (has_children) {
                           li.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+                          li.find('a.subscribed').first().hide();
                       }
                       else {
                           li.remove();
@@ -93,6 +102,7 @@ function kolab_folderlist(node, p)
               // disable checkbox if item already exists in main list
               if (me.get_node(prop.id) && !me.get_node(prop.id).virtual) {
                   item.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+                  item.find('a.subscribed').hide();
               }
           }
 
@@ -168,6 +178,18 @@ function kolab_folderlist(node, p)
         }
     });
 
+    this.container.on('click', 'a.subscribed', function(e){
+        var li = $(this).closest('li'),
+            id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+            div = li.children().first();
+
+        div.toggleClass('subscribed');
+        me.triggerEvent('subscribe', { id: id, subscribed: div.hasClass('subscribed'), item: li });
+
+        e.stopPropagation();
+        return false;
+    })
+
 }
 
 // link prototype from base class


commit 8d09b78eb1ee58ea19180804ff729000a82f6340
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu May 15 14:18:23 2014 +0200

    Provide sample config for new LDAP user search

diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist
index e1ad0ff..3a3c287 100644
--- a/plugins/libkolab/config.inc.php.dist
+++ b/plugins/libkolab/config.inc.php.dist
@@ -31,3 +31,20 @@ $rcmail_config['kolab_http_request'] = array();
 // 1 - bypass only messages, but use index cache
 $rcmail_config['kolab_messages_cache_bypass'] = 0;
 
+// LDAP directory to find avilable users for folder sharing.
+// Either contains an array with LDAP addressbook configuration or refers to entry in $config['ldap_public'].
+// If not specified, the configuraton from 'kolab_auth_addressbook' will be used.
+$rcmail_config['kolab_users_directory'] = null;
+
+// Filter to be used for resolving user folders in LDAP.
+// Defaults to the 'kolab_auth_filter' configuration option.
+$rcmail_config['kolab_users_filter'] = '(&(objectclass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)))';
+
+// Which property of the LDAP user record to use for user folder mapping in IMAP.
+// Defaults to the 'kolab_auth_login' configuration option.
+$rcmail_config['kolab_users_id_attrib'] = null;
+
+// Use these attributes when searching users in LDAP
+$rcmail_config['kolab_users_search_attrib'] = array('cn','mail','alias');
+
+
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index f10b7fe..1cbdd76 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -111,11 +111,12 @@ class kolab_storage
             return self::$ldap;
         }
 
-        $rcmail = rcube::get_instance();
-        $config = $rcmail->config->get('kolab_users_directory', $rcmail->config->get('kolab_auth_addressbook'));
+        self::setup();
+
+        $config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook'));
 
         if (!is_array($config)) {
-            $ldap_config = (array)$rcmail->config->get('ldap_public');
+            $ldap_config = (array)self::$config->get('ldap_public');
             $config = $ldap_config[$config];
         }
 
@@ -124,8 +125,8 @@ class kolab_storage
         }
 
         // overwrite filter option
-        if ($filter = $rcmail->config->get('kolab_users_filter')) {
-            $rcmail->config->set('kolab_auth_filter', $filter);
+        if ($filter = self::$config->get('kolab_users_filter')) {
+            self::$config->set('kolab_auth_filter', $filter);
         }
 
         // re-use the LDAP wrapper class from kolab_auth plugin
@@ -1328,12 +1329,12 @@ class kolab_storage
             return array();
         }
 
-        // FIXME: make search attributes configurable
-        $results = self::$ldap->search(array('cn','mail','alias'), $query, $mode, $required, $limit);
+        // search users using the configured attributes
+        $results = self::$ldap->search(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit);
 
         // resolve to IMAP folder name
         $root = self::namespace_root('other');
-        $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+        $user_attrib = self::$config->get('kolab_auth_login', 'mail');
 
         array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
             list($localpart, $domain) = explode('@', $user[$user_attrib]);
@@ -1356,10 +1357,12 @@ class kolab_storage
      */
     public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
     {
+        self::setup();
+
         $folders = array();
 
         // use localpart of user attribute as root for folder listing
-        $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+        $user_attrib = self::$config->get('kolab_auth_login', 'mail');
         if (!empty($user[$user_attrib])) {
             list($mbox) = explode('@', $user[$user_attrib]);
 
diff --git a/plugins/libkolab/lib/kolab_storage_folder_api.php b/plugins/libkolab/lib/kolab_storage_folder_api.php
index a2d40b1..5af8c34 100644
--- a/plugins/libkolab/lib/kolab_storage_folder_api.php
+++ b/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -134,7 +134,7 @@ abstract class kolab_storage_folder_api
      */
     public function get_name()
     {
-        return kolab_storage::object_name($this->name, $this->namespace);
+        return kolab_storage::object_name($this->name, $this->get_namespace());
     }
 
 
diff --git a/plugins/libkolab/lib/kolab_storage_folder_virtual.php b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
index 8b85ad5..e419ced 100644
--- a/plugins/libkolab/lib/kolab_storage_folder_virtual.php
+++ b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
@@ -43,8 +43,7 @@ class kolab_storage_folder_virtual extends kolab_storage_folder_api
      */
     public function get_name()
     {
-        // this is already kolab_storage::object_name() result
-        return $this->displayname;
+        return $this->displayname ?: parent::get_name();
     }
 
     /**


commit 510089523ee8753733e0678e93760baa15441a23
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu May 15 13:15:58 2014 +0200

    Refactored kolab_storage_folder classes and consolidated some functions

diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 374ab5d..c4b7dbb 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -24,9 +24,8 @@
  */
 
 
-class kolab_calendar
+class kolab_calendar extends kolab_storage_folder_api
 {
-  public $id;
   public $ready = false;
   public $readonly = true;
   public $attachments = true;
@@ -35,11 +34,9 @@ class kolab_calendar
   public $storage;
 
   public $type = 'event';
-  public $name;
 
   protected $cal;
   protected $events = array();
-  protected $imap_folder = 'INBOX/Calendar';
   protected $search_fields = array('title', 'description', 'location', 'attendees');
 
   /**
@@ -68,16 +65,15 @@ class kolab_calendar
   public function __construct($imap_folder, $calendar)
   {
     $this->cal = $calendar;
-
-    if (strlen($imap_folder))
-      $this->imap_folder = $this->name = $imap_folder;
+    $this->imap = $calendar->rc->get_storage();
+    $this->name = $imap_folder;
 
     // ID is derrived from folder name
-    $this->id = kolab_storage::folder_id($this->imap_folder, true);
-    $old_id   = kolab_storage::folder_id($this->imap_folder, false);
+    $this->id = kolab_storage::folder_id($this->name, true);
+    $old_id   = kolab_storage::folder_id($this->name, false);
 
     // fetch objects from the given IMAP folder
-    $this->storage = kolab_storage::get_folder($this->imap_folder);
+    $this->storage = kolab_storage::get_folder($this->name);
     $this->ready = $this->storage && !PEAR::isError($this->storage) && $this->storage->type !== null;
 
     // Set readonly and alarms flags according to folder permissions
@@ -101,6 +97,8 @@ class kolab_calendar
       else if (isset($prefs[$old_id]['showalarms']))
         $this->alarms = $prefs[$old_id]['showalarms'];
     }
+
+    $this->default = $this->storage->default;
   }
 
 
@@ -112,7 +110,7 @@ class kolab_calendar
    */
   public function get_name()
   {
-    $folder = kolab_storage::object_name($this->imap_folder, $this->namespace);
+    $folder = kolab_storage::object_name($this->name, $this->namespace);
     return $folder;
   }
 
@@ -124,44 +122,11 @@ class kolab_calendar
    */
   public function get_realname()
   {
-    return $this->imap_folder;
-  }
-
-
-  /**
-   * Getter for the IMAP folder owner
-   *
-   * @return string Name of the folder owner
-   */
-  public function get_owner()
-  {
-    return $this->storage->get_owner();
+    return $this->name;
   }
 
 
   /**
-   * Getter for the name of the namespace to which the IMAP folder belongs
-   *
-   * @return string Name of the namespace (personal, other, shared)
-   */
-  public function get_namespace()
-  {
-    return $this->storage->get_namespace();
-  }
-
-
-  /**
-   * Getter for the top-end calendar folder name (not the entire path)
-   *
-   * @return string Name of this calendar
-   */
-  public function get_foldername()
-  {
-    $parts = explode('/', $this->imap_folder);
-    return rcube_charset::convert(end($parts), 'UTF7-IMAP');
-  }
-
-  /**
    * Return color to display this calendar
    */
   public function get_color()
@@ -191,7 +156,7 @@ class kolab_calendar
         '%h' => $_SERVER['HTTP_HOST'],
         '%u' => urlencode($this->cal->rc->get_user_name()),
         '%i' => urlencode($this->storage->get_uid()),
-        '%n' => urlencode($this->imap_folder),
+        '%n' => urlencode($this->name),
       ));
     }
 
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 13c460e..d1bb655 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -83,7 +83,7 @@ class kolab_driver extends calendar_driver
     $this->calendars = array();
 
     foreach ($folders as $folder) {
-      if ($folder instanceof kolab_storage_user_folder)
+      if ($folder instanceof kolab_storage_folder_user)
         $calendar = new kolab_user_calendar($folder->name, $this->cal);
       else
         $calendar = new kolab_calendar($folder->name, $this->cal);
@@ -138,7 +138,7 @@ class kolab_driver extends calendar_driver
       }
 
       // special handling for user or virtual folders
-      if ($cal instanceof kolab_storage_user_folder) {
+      if ($cal instanceof kolab_storage_folder_user) {
         $calendars[$cal->id] = array(
           'id' => $cal->id,
           'name' => kolab_storage::object_name($fullname),
@@ -172,8 +172,8 @@ class kolab_driver extends calendar_driver
           'readonly' => $cal->readonly,
           'showalarms' => $cal->alarms,
           'class_name' => $cal->get_namespace(),
-          'default'  => $cal->storage->default,
-          'active'   => $cal->storage->is_active(),
+          'default'  => $cal->default,
+          'active'   => $cal->is_active(),
           'owner'    => $cal->get_owner(),
           'children' => true,  // TODO: determine if that folder indeed has child folders
           'parent'   => $parent_id,
@@ -234,7 +234,7 @@ class kolab_driver extends calendar_driver
       if ($writeable && $cal->readonly) {
         continue;
       }
-      if ($active && !$cal->storage->is_active()) {
+      if ($active && !$cal->is_active()) {
         continue;
       }
       if ($personal && $cal->get_namespace() != 'personal') {
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index b23d134..3add82a 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -27,7 +27,6 @@ class kolab_user_calendar extends kolab_calendar
   public $ready = false;
   public $readonly = true;
   public $attachments = false;
-  public $name;
 
   protected $userdata = array();
 
@@ -42,10 +41,10 @@ class kolab_user_calendar extends kolab_calendar
     // full user record is provided
     if (is_array($user_or_folder)) {
       $this->userdata = $user_or_folder;
-      $this->storage = new kolab_storage_user_folder($this->userdata['kolabtargetfolder'], '', $this->userdata);
+      $this->storage = new kolab_storage_folder_user($this->userdata['kolabtargetfolder'], '', $this->userdata);
     }
     else {  // get user record from LDAP
-      $this->storage = new kolab_storage_user_folder($user_or_folder);
+      $this->storage = new kolab_storage_folder_user($user_or_folder);
       $this->userdata = $this->storage->ldaprec;
     }
 
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 17c4912..f10b7fe 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -887,7 +887,7 @@ class kolab_storage
         $_folders = array();
         $delim    = self::$imap->get_hierarchy_delimiter();
         $other_ns = rtrim(self::namespace_root('other'), $delim);
-        $tree     = new kolab_storage_virtual_folder('', '<root>', '');  // create tree root
+        $tree     = new kolab_storage_folder_virtual('', '<root>', '');  // create tree root
         $refs     = array('' => $tree);
 
         foreach ($folders as $idx => $folder) {
@@ -913,11 +913,11 @@ class kolab_storage
                             $refs[$parent]->parent = $parent_parent;
                         }
                         else if ($parent_parent == $other_ns) {
-                            $refs[$parent] = new kolab_storage_user_folder($parent, $parent_parent);
+                            $refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
                         }
                         else {
                             $name = kolab_storage::object_name($parent, $folder->get_namespace());
-                            $refs[$parent] = new kolab_storage_virtual_folder($parent, $name, $folder->get_namespace(), $parent_parent);
+                            $refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
                         }
                         $parents[] = $refs[$parent];
                     }
@@ -1376,7 +1376,7 @@ class kolab_storage
      *
      * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
      *
-     * @return array List of kolab_storage_user_folder objects
+     * @return array List of kolab_storage_folder_user objects
      */
     public static function get_user_folders($subscribed)
     {
@@ -1393,7 +1393,7 @@ class kolab_storage
                 $foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
 
                 if (!$folders[$foldername]) {
-                    $folders[$foldername] = new kolab_storage_user_folder($foldername, $other_ns);
+                    $folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
                 }
             }
         }
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 39d1964..db1a761 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -22,50 +22,15 @@
  * 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 kolab_storage_folder
+class kolab_storage_folder extends kolab_storage_folder_api
 {
     /**
-     * Folder identifier
-     * @var string
-     */
-    public $id;
-
-    /**
-     * The folder name.
-     * @var string
-     */
-    public $name;
-
-    /**
-     * The type of this folder.
-     * @var string
-     */
-    public $type;
-
-    /**
-     * Is this folder set to be the default for its type
-     * @var boolean
-     */
-    public $default = false;
-
-    /**
      * The kolab_storage_cache instance for caching operations
      * @var object
      */
     public $cache;
-    
-    /**
-     * List of direct child folders
-     * @var array
-     */
-    public $children = array();
 
     private $type_annotation;
-    private $namespace;
-    private $imap;
-    private $info;
-    private $idata;
-    private $owner;
     private $resource_uri;
 
 
@@ -74,7 +39,7 @@ class kolab_storage_folder
      */
     function __construct($name, $type = null)
     {
-        $this->imap = rcube::get_instance()->get_storage();
+        parent::__construct($name);
         $this->imap->set_options(array('skip_deleted' => true));
         $this->set_folder($name, $type);
     }
@@ -105,160 +70,6 @@ class kolab_storage_folder
         $this->cache->set_folder($this);
     }
 
-    /**
-     *
-     */
-    public function get_folder_info()
-    {
-        if (!isset($this->info))
-            $this->info = $this->imap->folder_info($this->name);
-
-        return $this->info;
-    }
-
-    /**
-     * Make IMAP folder data available for this folder
-     */
-    public function get_imap_data()
-    {
-        if (!isset($this->idata))
-            $this->idata = $this->imap->folder_data($this->name);
-
-        return $this->idata;
-    }
-
-    /**
-     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
-     *
-     * @param array List of metadata keys to read
-     * @return array Metadata entry-value hash array on success, NULL on error
-     */
-    public function get_metadata($keys)
-    {
-        $metadata = $this->imap->get_metadata($this->name, (array)$keys);
-        return $metadata[$this->name];
-    }
-
-
-    /**
-     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
-     *
-     * @param array  $entries Entry-value array (use NULL value as NIL)
-     * @return boolean True on success, False on failure
-     */
-    public function set_metadata($entries)
-    {
-        return $this->imap->set_metadata($this->name, $entries);
-    }
-
-
-    /**
-     * Returns the owner of the folder.
-     *
-     * @return string  The owner of this folder.
-     */
-    public function get_owner()
-    {
-        // return cached value
-        if (isset($this->owner))
-            return $this->owner;
-
-        $info = $this->get_folder_info();
-        $rcmail = rcube::get_instance();
-
-        switch ($info['namespace']) {
-        case 'personal':
-            $this->owner = $rcmail->get_user_name();
-            break;
-
-        case 'shared':
-            $this->owner = 'anonymous';
-            break;
-
-        default:
-            list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
-            if (strpos($user, '@') === false) {
-                $domain = strstr($rcmail->get_user_name(), '@');
-                if (!empty($domain))
-                    $user .= $domain;
-            }
-            $this->owner = $user;
-            break;
-        }
-
-        return $this->owner;
-    }
-
-
-    /**
-     * Getter for the name of the namespace to which the IMAP folder belongs
-     *
-     * @return string Name of the namespace (personal, other, shared)
-     */
-    public function get_namespace()
-    {
-        if (!isset($this->namespace))
-            $this->namespace = $this->imap->folder_namespace($this->name);
-        return $this->namespace;
-    }
-
-
-    /**
-     * Get IMAP ACL information for this folder
-     *
-     * @return string  Permissions as string
-     */
-    public function get_myrights()
-    {
-        $rights = $this->info['rights'];
-
-        if (!is_array($rights))
-            $rights = $this->imap->my_rights($this->name);
-
-        return join('', (array)$rights);
-    }
-
-
-    /**
-     * Get the display name value of this folder
-     *
-     * @return string Folder name
-     */
-    public function get_name()
-    {
-        return kolab_storage::object_name($this->name, $this->namespace);
-    }
-
-
-    /**
-     * Getter for the top-end folder name (not the entire path)
-     *
-     * @return string Name of this folder
-     */
-    public function get_foldername()
-    {
-        $parts = explode('/', $this->name);
-        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
-    }
-
-
-    /**
-     * Get the color value stored in metadata
-     *
-     * @param string Default color value to return if not set
-     * @return mixed Color value from IMAP metadata or $default is not set
-     */
-    public function get_color($default = null)
-    {
-        // color is defined in folder METADATA
-        $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
-        if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
-            return $color;
-        }
-
-        return $default;
-    }
-
 
     /**
      * Compose a unique resource URI for this IMAP folder
diff --git a/plugins/libkolab/lib/kolab_storage_folder_api.php b/plugins/libkolab/lib/kolab_storage_folder_api.php
new file mode 100644
index 0000000..a2d40b1
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -0,0 +1,280 @@
+<?php
+
+/**
+ * Abstract interface class for Kolab storage IMAP folder objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, 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/>.
+ */
+abstract class kolab_storage_folder_api
+{
+    /**
+     * Folder identifier
+     * @var string
+     */
+    public $id;
+
+    /**
+     * The folder name.
+     * @var string
+     */
+    public $name;
+
+    /**
+     * The type of this folder.
+     * @var string
+     */
+    public $type;
+
+    /**
+     * Is this folder set to be the default for its type
+     * @var boolean
+     */
+    public $default = false;
+
+    /**
+     * List of direct child folders
+     * @var array
+     */
+    public $children = array();
+    
+    /**
+     * Name of the parent folder
+     * @var string
+     */
+    public $parent = '';
+
+    protected $imap;
+    protected $owner;
+    protected $info;
+    protected $idata;
+    protected $namespace;
+
+
+    /**
+     * Private constructor
+     */
+    protected function __construct($name)
+    {
+      $this->name = $name;
+      $this->id   = kolab_storage::folder_id($name);
+      $this->imap = rcube::get_instance()->get_storage();
+    }
+
+
+    /**
+     * Returns the owner of the folder.
+     *
+     * @return string  The owner of this folder.
+     */
+    public function get_owner()
+    {
+        // return cached value
+        if (isset($this->owner))
+            return $this->owner;
+
+        $info = $this->get_folder_info();
+        $rcmail = rcube::get_instance();
+
+        switch ($info['namespace']) {
+        case 'personal':
+            $this->owner = $rcmail->get_user_name();
+            break;
+
+        case 'shared':
+            $this->owner = 'anonymous';
+            break;
+
+        default:
+            list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+            if (strpos($user, '@') === false) {
+                $domain = strstr($rcmail->get_user_name(), '@');
+                if (!empty($domain))
+                    $user .= $domain;
+            }
+            $this->owner = $user;
+            break;
+        }
+
+        return $this->owner;
+    }
+
+
+    /**
+     * Getter for the name of the namespace to which the IMAP folder belongs
+     *
+     * @return string Name of the namespace (personal, other, shared)
+     */
+    public function get_namespace()
+    {
+        if (!isset($this->namespace))
+            $this->namespace = $this->imap->folder_namespace($this->name);
+        return $this->namespace;
+    }
+
+
+    /**
+     * Get the display name value of this folder
+     *
+     * @return string Folder name
+     */
+    public function get_name()
+    {
+        return kolab_storage::object_name($this->name, $this->namespace);
+    }
+
+
+    /**
+     * Getter for the top-end folder name (not the entire path)
+     *
+     * @return string Name of this folder
+     */
+    public function get_foldername()
+    {
+        $parts = explode('/', $this->name);
+        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+    }
+
+
+    /**
+     * Get the color value stored in metadata
+     *
+     * @param string Default color value to return if not set
+     * @return mixed Color value from IMAP metadata or $default is not set
+     */
+    public function get_color($default = null)
+    {
+        // color is defined in folder METADATA
+        $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
+        if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
+            return $color;
+        }
+
+        return $default;
+    }
+
+
+    /**
+     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
+     *
+     * @param array List of metadata keys to read
+     * @return array Metadata entry-value hash array on success, NULL on error
+     */
+    public function get_metadata($keys)
+    {
+        $metadata = rcube::get_instance()->get_storage()->get_metadata($this->name, (array)$keys);
+        return $metadata[$this->name];
+    }
+
+
+    /**
+     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+     *
+     * @param array  $entries Entry-value array (use NULL value as NIL)
+     * @return boolean True on success, False on failure
+     */
+    public function set_metadata($entries)
+    {
+        return $this->imap->set_metadata($this->name, $entries);
+    }
+
+
+    /**
+     *
+     */
+    public function get_folder_info()
+    {
+        if (!isset($this->info))
+            $this->info = $this->imap->folder_info($this->name);
+
+        return $this->info;
+    }
+
+    /**
+     * Make IMAP folder data available for this folder
+     */
+    public function get_imap_data()
+    {
+        if (!isset($this->idata))
+            $this->idata = $this->imap->folder_data($this->name);
+
+        return $this->idata;
+    }
+
+
+    /**
+     * Get IMAP ACL information for this folder
+     *
+     * @return string  Permissions as string
+     */
+    public function get_myrights()
+    {
+        $rights = $this->info['rights'];
+
+        if (!is_array($rights))
+            $rights = $this->imap->my_rights($this->name);
+
+        return join('', (array)$rights);
+    }
+
+
+    /**
+     * Check activation status of this folder
+     *
+     * @return boolean True if enabled, false if not
+     */
+    public function is_active()
+    {
+        return kolab_storage::folder_is_active($this->name);
+    }
+
+    /**
+     * Change activation status of this folder
+     *
+     * @param boolean The desired subscription status: true = active, false = not active
+     *
+     * @return True on success, false on error
+     */
+    public function activate($active)
+    {
+        return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
+    }
+
+    /**
+     * Check subscription status of this folder
+     *
+     * @return boolean True if subscribed, false if not
+     */
+    public function is_subscribed()
+    {
+        return kolab_storage::folder_is_subscribed($this->name);
+    }
+
+    /**
+     * Change subscription status of this folder
+     *
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+     *
+     * @return True on success, false on error
+     */
+    public function subscribe($subscribed)
+    {
+        return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
+    }
+
+}
+
diff --git a/plugins/libkolab/lib/kolab_storage_folder_user.php b/plugins/libkolab/lib/kolab_storage_folder_user.php
new file mode 100644
index 0000000..70ded87
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_folder_user.php
@@ -0,0 +1,101 @@
+<?php
+
+/**
+ * Class that represents a (virtual) folder in the 'other' namespace
+ * implementing a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, 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 kolab_storage_folder_user extends kolab_storage_folder_virtual
+{
+    protected static $ldapcache = array();
+
+    public $ldaprec;
+
+    /**
+     * Default constructor
+     */
+    public function __construct($name, $parent = '', $ldaprec = null)
+    {
+        parent::__construct($name, $name, 'other', $parent);
+
+        if (!empty($ldaprec)) {
+            self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
+        }
+        // use value cached in memory for repeated lookups
+        else if (array_key_exists($name, self::$ldapcache)) {
+            $this->ldaprec = self::$ldapcache[$name];
+        }
+        // lookup user in LDAP and set $this->ldaprec
+        else if ($ldap = kolab_storage::ldap()) {
+            // get domain from current user
+            list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
+            $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
+            if (!empty($this->ldaprec)) {
+                $this->ldaprec['kolabtargetfolder'] = $name;
+            }
+            self::$ldapcache[$name] = $this->ldaprec;
+        }
+    }
+
+    /**
+     * Getter for the top-end folder name to be displayed
+     *
+     * @return string Name of this folder
+     */
+    public function get_foldername()
+    {
+        return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
+            parent::get_foldername();
+    }
+
+    /**
+     * Returns the owner of the folder.
+     *
+     * @return string  The owner of this folder.
+     */
+    public function get_owner()
+    {
+        return $this->ldaprec['mail'];
+    }
+
+    /**
+     * Check subscription status of this folder
+     *
+     * @return boolean True if subscribed, false if not
+     */
+    public function is_subscribed()
+    {
+        return kolab_storage::folder_is_subscribed($this->name, true);
+    }
+
+    /**
+     * Change subscription status of this folder
+     *
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+     *
+     * @return True on success, false on error
+     */
+    public function subscribe($subscribed)
+    {
+        return $subscribed ?
+            kolab_storage::folder_subscribe($this->name, true) :
+            kolab_storage::folder_unsubscribe($this->name, true);
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_folder_virtual.php b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
new file mode 100644
index 0000000..8b85ad5
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_folder_virtual.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, 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 kolab_storage_folder_virtual extends kolab_storage_folder_api
+{
+    public $virtual = true;
+
+    protected $displayname;
+
+    public function __construct($name, $dispname, $ns, $parent = '')
+    {
+        parent::__construct($name);
+
+        $this->namespace = $ns;
+        $this->parent    = $parent;
+        $this->displayname = $dispname;
+    }
+
+    /**
+     * Get the display name value of this folder
+     *
+     * @return string Folder name
+     */
+    public function get_name()
+    {
+        // this is already kolab_storage::object_name() result
+        return $this->displayname;
+    }
+
+    /**
+     * Get the color value stored in metadata
+     *
+     * @param string Default color value to return if not set
+     * @return mixed Color value from IMAP metadata or $default is not set
+     */
+    public function get_color($default = null)
+    {
+        return $default;
+    }
+}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_user_folder.php b/plugins/libkolab/lib/kolab_storage_user_folder.php
deleted file mode 100644
index 55e38a0..0000000
--- a/plugins/libkolab/lib/kolab_storage_user_folder.php
+++ /dev/null
@@ -1,123 +0,0 @@
-<?php
-
-/**
- * Class that represents a (virtual) folder in the 'other' namespace
- * implementing a subset of the kolab_storage_folder API.
- *
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2014, 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 kolab_storage_user_folder extends kolab_storage_virtual_folder
-{
-    protected static $ldapcache = array();
-
-    public $ldaprec;
-
-    /**
-     * Default constructor
-     */
-    public function __construct($name, $parent = '', $ldaprec = null)
-    {
-        parent::__construct($name, $name, 'other', $parent);
-
-        if (!empty($ldaprec)) {
-            self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
-        }
-        // use value cached in memory for repeated lookups
-        else if (array_key_exists($name, self::$ldapcache)) {
-            $this->ldaprec = self::$ldapcache[$name];
-        }
-        // lookup user in LDAP and set $this->ldaprec
-        else if ($ldap = kolab_storage::ldap()) {
-            // get domain from current user
-            list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
-            $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
-            if (!empty($this->ldaprec)) {
-                $this->ldaprec['kolabtargetfolder'] = $name;
-            }
-            self::$ldapcache[$name] = $this->ldaprec;
-        }
-    }
-
-    /**
-     * Getter for the top-end folder name to be displayed
-     *
-     * @return string Name of this folder
-     */
-    public function get_foldername()
-    {
-        return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
-            parent::get_foldername();
-    }
-
-    /**
-     * Returns the owner of the folder.
-     *
-     * @return string  The owner of this folder.
-     */
-    public function get_owner()
-    {
-        return $this->ldaprec['mail'];
-    }
-
-    /**
-     * Check activation status of this folder
-     *
-     * @return boolean True if enabled, false if not
-     */
-    public function is_active()
-    {
-        return kolab_storage::folder_is_active($this->name);
-    }
-
-    /**
-     * Change activation status of this folder
-     *
-     * @param boolean The desired subscription status: true = active, false = not active
-     *
-     * @return True on success, false on error
-     */
-    public function activate($active)
-    {
-        return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
-    }
-
-    /**
-     * Check subscription status of this folder
-     *
-     * @return boolean True if subscribed, false if not
-     */
-    public function is_subscribed()
-    {
-        return kolab_storage::folder_is_subscribed($this->name, true);
-    }
-
-    /**
-     * Change subscription status of this folder
-     *
-     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
-     *
-     * @return True on success, false on error
-     */
-    public function subscribe($subscribed)
-    {
-        return $subscribed ?
-            kolab_storage::folder_subscribe($this->name, true) :
-            kolab_storage::folder_unsubscribe($this->name, true);
-    }
-
-}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_virtual_folder.php b/plugins/libkolab/lib/kolab_storage_virtual_folder.php
deleted file mode 100644
index 61d0fe0..0000000
--- a/plugins/libkolab/lib/kolab_storage_virtual_folder.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-
-/**
- * Helper class that represents a virtual IMAP folder
- * with a subset of the kolab_storage_folder API.
- *
- * @author Thomas Bruederli <bruederli at kolabsys.com>
- *
- * Copyright (C) 2014, 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 kolab_storage_virtual_folder
-{
-    public $id;
-    public $name;
-    public $namespace;
-    public $parent = '';
-    public $children = array();
-    public $virtual = true;
-
-    protected $displayname;
-
-    public function __construct($name, $dispname, $ns, $parent = '')
-    {
-        $this->id        = kolab_storage::folder_id($name);
-        $this->name      = $name;
-        $this->namespace = $ns;
-        $this->parent    = $parent;
-        $this->displayname = $dispname;
-    }
-
-    /**
-     * Getter for the name of the namespace to which the IMAP folder belongs
-     *
-     * @return string Name of the namespace (personal, other, shared)
-     */
-    public function get_namespace()
-    {
-        return $this->namespace;
-    }
-
-    /**
-     * Get the display name value of this folder
-     *
-     * @return string Folder name
-     */
-    public function get_name()
-    {
-        // this is already kolab_storage::object_name() result
-        return $this->displayname;
-    }
-
-    /**
-     * Getter for the top-end folder name (not the entire path)
-     *
-     * @return string Name of this folder
-     */
-    public function get_foldername()
-    {
-        $parts = explode('/', $this->name);
-        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
-    }
-
-    /**
-     * Get the color value stored in metadata
-     *
-     * @param string Default color value to return if not set
-     * @return mixed Color value from IMAP metadata or $default is not set
-     */
-    public function get_color($default = null)
-    {
-        return $default;
-    }
-}
\ No newline at end of file


commit 715b2b790a39ab293ba58d8b214cf4fb7faed661
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu May 15 11:57:54 2014 +0200

    Fix listing of other user's calendars and sub-folders

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index a1a60b1..38a32a8 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2705,7 +2705,7 @@ function rcube_calendar_ui(settings)
       // insert to #calendar-select options if writeable
       select = $('#edit-calendar');
       if (fc && !cal.readonly && select.length && !select.find('option[value="'+id+'"]').length) {
-        $('<option>').attr('value', id).text(Q(cal.name)).appendTo(select);
+        $('<option>').attr('value', id).html(cal.name).appendTo(select);
       }
     }
 
@@ -2751,7 +2751,6 @@ function rcube_calendar_ui(settings)
     calendars_list.addEventListener('insert-item', function(p) {
       var cal = p.data;
       if (cal && cal.id) {
-        cal.active = true;
         add_calendar_source(cal);
 
         // add css classes related to this calendar to document
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 97f6a96..374ab5d 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -33,6 +33,8 @@ class kolab_calendar
   public $alarms = false;
   public $categories = array();
   public $storage;
+
+  public $type = 'event';
   public $name;
 
   protected $cal;
@@ -198,6 +200,25 @@ class kolab_calendar
 
 
   /**
+   * Update properties of this calendar folder
+   *
+   * @see calendar_driver::edit_calendar()
+   */
+  public function update(&$prop)
+  {
+    $prop['oldname'] = $this->get_realname();
+    $newfolder = kolab_storage::folder_update($prop);
+
+    if ($newfolder === false) {
+      $this->cal->last_error = $this->cal->gettext(kolab_storage::$last_error);
+      return false;
+    }
+
+    // create ID
+    return kolab_storage::folder_id($newfolder);
+  }
+
+  /**
    * Getter for a single event object
    */
   public function get_event($id)
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 12e4258..13c460e 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -129,7 +129,13 @@ class kolab_driver extends calendar_driver
       $listname = $cal->get_foldername();
       $imap_path = explode('/', $cal->name);
       $topname = array_pop($imap_path);
-      $parent_id = kolab_storage::folder_id(join('/', $imap_path), true);
+      $parent_id = kolab_storage::folder_id(join('/', $imap_path));
+
+      // turn a kolab_storage_folder object into a kolab_calendar
+      if ($cal instanceof kolab_storage_folder) {
+          $cal = new kolab_calendar($cal->name, $this->cal);
+          $this->calendars[$cal->id] = $cal;
+      }
 
       // special handling for user or virtual folders
       if ($cal instanceof kolab_storage_user_folder) {
@@ -141,7 +147,7 @@ class kolab_driver extends calendar_driver
           'color'    => $cal->get_color(),
           'active'   => $cal->is_active(),
           'owner'    => $cal->get_owner(),
-          'virtual' => false,
+          'virtual'  => false,
           'readonly' => true,
           'class_name' => 'user',
         );
@@ -306,16 +312,7 @@ class kolab_driver extends calendar_driver
   public function edit_calendar($prop)
   {
     if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
-      $prop['oldname'] = $cal->get_realname();
-      $newfolder = kolab_storage::folder_update($prop);
-
-      if ($newfolder === false) {
-        $this->last_error = $this->cal->gettext(kolab_storage::$last_error);
-        return false;
-      }
-
-      // create ID
-      $id = kolab_storage::folder_id($newfolder);
+      $id = $cal->update($prop);
     }
     else {
       $id = $prop['id'];
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
index a5795a4..b23d134 100644
--- a/plugins/calendar/drivers/kolab/kolab_user_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -130,6 +130,20 @@ class kolab_user_calendar extends kolab_calendar
     return false;
   }
 
+
+  /**
+   * Update properties of this calendar folder
+   *
+   * @see calendar_driver::edit_calendar()
+   */
+  public function update(&$prop)
+  {
+    // don't change anything.
+    // let kolab_driver save props in local prefs
+    return $prop['id'];
+  }
+
+
   /**
    * Getter for a single event object
    */
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index a3ed443..4584f19 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -229,7 +229,7 @@ class calendar_ui
       if ($attrib['activeonly'] && !$prop['active'])
         continue;
 
-      $html .= html::tag('li', array('id' => 'rcmlical' . rcube_utils::html_identifier($id)),
+      $html .= html::tag('li', array('id' => 'rcmlical' . $id),
         $content = $this->calendar_list_item($id, $prop, $jsenv)
       );
     }
@@ -243,7 +243,7 @@ class calendar_ui
   /**
    * Return html for a structured list <ul> for the folder tree
    */
-  public function list_tree_html(&$node, &$data, &$jsenv, $attrib)
+  public function list_tree_html($node, $data, &$jsenv, $attrib)
   {
     $out = '';
     foreach ($node->children as $folder) {
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index b6e7ff2..92b4fb5 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -86,7 +86,7 @@ $labels['nmonthsback'] = '$nr months back';
 $labels['showurl'] = 'Show calendar URL';
 $labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
 $labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
-$labels['calsearchresults'] = 'Additional Results';
+$labels['calsearchresults'] = 'Available Calendars';
 
 // agenda view
 $labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index a44899b..71dd106 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -297,6 +297,10 @@ pre {
 	padding: 2px 8px 2px 8px;
 }
 
+#calendars .searchresults .listing li {
+	background-color: #c7e3ef;
+}
+
 #calfeedurl,
 #caldavurl {
 	width: 98%;
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index e2119a8..3c35846 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -62,39 +62,12 @@ function kolab_folderlist(node, p)
                           return;
 
                       var li = $(this).closest('li'),
-                          id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+                          id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '')
                           node = search_results_widget.get_node(id),
-                          prop = search_results[id],
-                          parent_id = prop.parent || null,
-                          has_children = node.children && node.children.length,
-                          dom_node = has_children ? li.children().first().clone(true, true) : li.children().first();
-
-                      // find parent node and insert at the right place
-                      if (parent_id && $('#' + p.id_prefix + parent_id, me.container).length) {
-                          prop.listname = prop.editname;
-                          dom_node.children('span,a').first().html(Q(prop.listname));
-                      }
-
-                      // TODO: copy parent tree too
-
-                      // replace virtual node with a real one
-                      if (me.get_node(id)) {
-                          $(me.get_item(id, true)).children().first()
-                              .replaceWith(dom_node)
-                              .removeClass('virtual');
-                      }
-                      else {
-                          // move this result item to the main list widget
-                          me.insert({
-                              id: id,
-                              classes: [],
-                              virtual: prop.virtual,
-                              html: dom_node,
-                          }, parent_id, parent_id ? true : false);
-                      }
+                          has_children = node.children && node.children.length;
 
-                      delete prop.html;
-                      me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+                      // copy item to the main list
+                      add_result2list(id, li, true);
 
                       if (has_children) {
                           li.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
@@ -127,6 +100,49 @@ function kolab_folderlist(node, p)
         }
     }
 
+    // helper method to (recursively) add a search result item to the main list widget
+    function add_result2list(id, li, active)
+    {
+        var node = search_results_widget.get_node(id),
+            prop = search_results[id],
+            parent_id = prop.parent || null,
+            has_children = node.children && node.children.length,
+            dom_node = has_children ? li.children().first().clone(true, true) : li.children().first();
+
+        // find parent node and insert at the right place
+        if (parent_id && me.get_node(parent_id)) {
+            dom_node.children('span,a').first().html(Q(prop.editname));
+        }
+        else if (parent_id && search_results[parent_id]) {
+            // copy parent tree from search results
+            add_result2list(parent_id, $(search_results_widget.get_item(parent_id)), false);
+        }
+        else if (parent_id) {
+            // use full name for list display
+            dom_node.children('span,a').first().html(Q(prop.name));
+        }
+
+        // replace virtual node with a real one
+        if (me.get_node(id)) {
+            $(me.get_item(id, true)).children().first()
+                .replaceWith(dom_node)
+                .removeClass('virtual');
+        }
+        else {
+            // move this result item to the main list widget
+            me.insert({
+                id: id,
+                classes: [],
+                virtual: prop.virtual,
+                html: dom_node,
+            }, parent_id, parent_id ? true : false);
+        }
+
+        delete prop.html;
+        prop.active = active;
+        me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+    }
+
     // do some magic when search is performed on the widget
     this.addEventListener('search', function(search) {
         // hide search results


commit df08826c0346683ee3dc9892fcd67b68e5dcf7c1
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu May 15 11:57:12 2014 +0200

    Improve listing of user folders: also list them if one has access to a child folder

diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 872ce29..17c4912 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -299,6 +299,24 @@ class kolab_storage
 
 
     /**
+     * Return the (first) path of the requested IMAP namespace
+     *
+     * @param string  Namespace name (personal, shared, other)
+     * @return string IMAP root path for that namespace
+     */
+    public static function namespace_root($name)
+    {
+        foreach ((array)self::$imap->get_namespace($name) as $paths) {
+            if (strlen($paths[0]) > 1) {
+                return $paths[0];
+            }
+        }
+
+        return '/';
+    }
+
+
+    /**
      * Deletes IMAP folder
      *
      * @param string $name Folder name (UTF7-IMAP)
@@ -868,14 +886,10 @@ class kolab_storage
     {
         $_folders = array();
         $delim    = self::$imap->get_hierarchy_delimiter();
-        $other_ns = self::$imap->get_namespace('other');
+        $other_ns = rtrim(self::namespace_root('other'), $delim);
         $tree     = new kolab_storage_virtual_folder('', '<root>', '');  // create tree root
         $refs     = array('' => $tree);
 
-        if (is_array($other_ns)) {
-            $other_ns = rtrim($other_ns[0][0], '/');
-        }
-
         foreach ($folders as $idx => $folder) {
             $path = explode($delim, $folder->name);
             array_pop($path);
@@ -894,7 +908,11 @@ class kolab_storage
                     array_pop($path);
                     $parent_parent = join($delim, $path);
                     if (!$refs[$parent]) {
-                        if ($parent_parent == $other_ns) {
+                        if ($folder->type && self::folder_type($parent) == $folder->type) {
+                            $refs[$parent] = new kolab_storage_folder($parent, $folder->type);
+                            $refs[$parent]->parent = $parent_parent;
+                        }
+                        else if ($parent_parent == $other_ns) {
                             $refs[$parent] = new kolab_storage_user_folder($parent, $parent_parent);
                         }
                         else {
@@ -1314,12 +1332,11 @@ class kolab_storage
         $results = self::$ldap->search(array('cn','mail','alias'), $query, $mode, $required, $limit);
 
         // resolve to IMAP folder name
-        $other_ns = self::$imap->get_namespace('other');
+        $root = self::namespace_root('other');
         $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
 
-        array_walk($results, function(&$user, $dn) use ($other_ns, $user_attrib) {
+        array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
             list($localpart, $domain) = explode('@', $user[$user_attrib]);
-            $root = $other_ns[0][0];
             $user['kolabtargetfolder'] = $root . $localpart;
         });
 
@@ -1346,11 +1363,7 @@ class kolab_storage
         if (!empty($user[$user_attrib])) {
             list($mbox) = explode('@', $user[$user_attrib]);
 
-            $other_ns = self::$imap->get_namespace('other');
-            if (is_array($other_ns)) {
-                $other_ns = $other_ns[0][0];
-            }
-
+            $other_ns = self::namespace_root('other');
             $folders = self::list_folders($other_ns . $mbox, '*', $type, $subscribed, $folderdata);
         }
 
@@ -1363,7 +1376,7 @@ class kolab_storage
      *
      * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
      *
-     * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
+     * @return array List of kolab_storage_user_folder objects
      */
     public static function get_user_folders($subscribed)
     {
@@ -1371,19 +1384,15 @@ class kolab_storage
 
         if (self::setup()) {
             $delimiter = self::$imap->get_hierarchy_delimiter();
-            $other_ns = self::$imap->get_namespace('other');
-            if (is_array($other_ns)) {
-                $other_ns = rtrim($other_ns[0][0], $delimiter);
-                $other_depth = count(explode($delimiter, $other_ns));
-            }
+            $other_ns = rtrim(self::namespace_root('other'), $delimiter);
+            $path_len = count(explode($delimiter, $other_ns));
 
             foreach ((array)self::list_folders($other_ns, '*', '', $subscribed) as $foldername) {
+                // truncate folder path to top-level folders of the 'other' namespace
                 $path = explode($delimiter, $foldername);
-                $depth = count($path) - $other_depth;
-                array_pop($path);
+                $foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
 
-                // only list top-level folders of the 'other' namespace
-                if ($depth == 1) {
+                if (!$folders[$foldername]) {
                     $folders[$foldername] = new kolab_storage_user_folder($foldername, $other_ns);
                 }
             }
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 7db493d..39d1964 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -25,6 +25,12 @@
 class kolab_storage_folder
 {
     /**
+     * Folder identifier
+     * @var string
+     */
+    public $id;
+
+    /**
      * The folder name.
      * @var string
      */
@@ -89,6 +95,7 @@ class kolab_storage_folder
         $this->default      = $suffix == 'default';
         $this->name         = $name;
         $this->resource_uri = null;
+        $this->id           = kolab_storage::folder_id($name);
 
         // get a new cache instance of folder type changed
         if (!$this->cache || $type != $oldtype)


commit 701c3391fe149a4e82535ae7d48fedd30ce70a77
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed May 14 20:37:06 2014 +0200

    Search in LDAP and collect accessible folders (#3041)
    - Add LDAP user search capabilities to kolab_storage class (using kolab_auth plugin classes)
    - Introduce virtual 'user' folder objects and add methods to list them
    - New 'user calendar' class in calendar (kolab driver)
    - Render folder search results as hierarchical list

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 52baf11..a1a60b1 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2739,7 +2739,9 @@ function rcube_calendar_ui(settings)
       selectable: true,
       save_state: true,
       searchbox: '#calendarlistsearch',
-      search_action: 'calendar/calendar'
+      search_action: 'calendar/calendar',
+      search_sources: [ 'folders', 'users' ],
+      search_title: rcmail.gettext('calsearchresults','calendar')
     });
     calendars_list.addEventListener('select', function(node) {
       me.select_calendar(node.id);
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 5feed23..97f6a96 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -7,7 +7,7 @@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  * @author Aleksander Machniak <machniak at kolabsys.com>
  *
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2014, 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
@@ -35,11 +35,30 @@ class kolab_calendar
   public $storage;
   public $name;
 
-  private $cal;
-  private $events = array();
-  private $imap_folder = 'INBOX/Calendar';
-  private $search_fields = array('title', 'description', 'location', 'attendees');
+  protected $cal;
+  protected $events = array();
+  protected $imap_folder = 'INBOX/Calendar';
+  protected $search_fields = array('title', 'description', 'location', 'attendees');
 
+  /**
+   * Factory method to instantiate a kolab_calendar object
+   *
+   * @param string  Calendar ID (encoded IMAP folder name)
+   * @param object  calendar plugin object
+   * @return object kolab_calendar instance
+   */
+  public static function factory($id, $calendar)
+  {
+    $imap = $calendar->rc->get_storage();
+    $imap_folder = kolab_storage::id_decode($id);
+    $info = $imap->folder_info($imap_folder, true);
+    if (empty($info) || $info['noselect'] || kolab_storage::folder_type($imap_folder) != 'event') {
+      return new kolab_user_calendar($imap_folder, $calendar);
+    }
+    else {
+      return new kolab_calendar($imap_folder, $calendar);
+    }
+  }
 
   /**
    * Default constructor
@@ -177,14 +196,6 @@ class kolab_calendar
     return false;
   }
 
-  /**
-   * Return the corresponding kolab_storage_folder instance
-   */
-  public function get_folder()
-  {
-    return $this->storage;
-  }
-
 
   /**
    * Getter for a single event object
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 9c45eb6..12e4258 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -24,6 +24,7 @@
  */
 
 require_once(dirname(__FILE__) . '/kolab_calendar.php');
+require_once(dirname(__FILE__) . '/kolab_user_calendar.php');
 
 class kolab_driver extends calendar_driver
 {
@@ -78,14 +79,20 @@ class kolab_driver extends calendar_driver
       return $this->calendars;
 
     // get all folders that have "event" type, sorted by namespace/name
-    $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event'));
+    $folders = kolab_storage::sort_folders(kolab_storage::get_folders('event') + kolab_storage::get_user_folders(true));
     $this->calendars = array();
 
     foreach ($folders as $folder) {
-      $calendar = new kolab_calendar($folder->name, $this->cal);
-      $this->calendars[$calendar->id] = $calendar;
-      if (!$calendar->readonly)
-        $this->has_writeable = true;
+      if ($folder instanceof kolab_storage_user_folder)
+        $calendar = new kolab_user_calendar($folder->name, $this->cal);
+      else
+        $calendar = new kolab_calendar($folder->name, $this->cal);
+
+      if ($calendar->ready) {
+        $this->calendars[$calendar->id] = $calendar;
+        if (!$calendar->readonly)
+          $this->has_writeable = true;
+      }
     }
 
     return $this->calendars;
@@ -114,18 +121,32 @@ class kolab_driver extends calendar_driver
     $calendars = $names = array();
 
     // include virtual folders for a full folder tree
-    if (!$active && !$personal && !$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
+    if (!is_null($tree))
       $folders = kolab_storage::folder_hierarchy($folders, $tree);
 
     foreach ($folders as $id => $cal) {
       $fullname = $cal->get_name();
-      $listname = kolab_storage::folder_displayname($fullname, $names);
+      $listname = $cal->get_foldername();
       $imap_path = explode('/', $cal->name);
       $topname = array_pop($imap_path);
       $parent_id = kolab_storage::folder_id(join('/', $imap_path), true);
 
-      // special handling for virtual folders
-      if ($cal->virtual) {
+      // special handling for user or virtual folders
+      if ($cal instanceof kolab_storage_user_folder) {
+        $calendars[$cal->id] = array(
+          'id' => $cal->id,
+          'name' => kolab_storage::object_name($fullname),
+          'listname' => $listname,
+          'editname' => $cal->get_foldername(),
+          'color'    => $cal->get_color(),
+          'active'   => $cal->is_active(),
+          'owner'    => $cal->get_owner(),
+          'virtual' => false,
+          'readonly' => true,
+          'class_name' => 'user',
+        );
+      }
+      else if ($cal->virtual) {
         $calendars[$cal->id] = array(
           'id' => $cal->id,
           'name' => $fullname,
@@ -230,8 +251,8 @@ class kolab_driver extends calendar_driver
   {
     // create calendar object if necesary
     if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
-      $foldername = kolab_storage::id_decode($id);
-      $calendar = new kolab_calendar($foldername, $this->cal);
+      $calendar = kolab_calendar::factory($id, $this->cal);
+      console($id, $calendar->id, $calendar->ready);
       if ($calendar->ready)
         $this->calendars[$calendar->id] = $calendar;
     }
@@ -389,8 +410,20 @@ class kolab_driver extends calendar_driver
         $this->calendars[$calendar->id] = $calendar;
       }
     }
+    // find other user's virtual calendars
     else if ($source == 'users') {
-      // TODO: implement this
+      foreach (kolab_storage::search_users($query, 0) as $user) {
+        $calendar = new kolab_user_calendar($user, $this->cal);
+        $this->calendars[$calendar->id] = $calendar;
+
+        // search for calendar folders shared by this user
+        foreach (kolab_storage::list_user_folders($user, 'event', false) as $foldername) {
+          if (1 || !kolab_storage::folder_is_subscribed($foldername, true)) {
+            $cal = new kolab_calendar($foldername, $this->cal);
+            $this->calendars[$cal->id] = $cal;
+          }
+        }
+      }
     }
 
     // don't list the birthday calendar
diff --git a/plugins/calendar/drivers/kolab/kolab_user_calendar.php b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
new file mode 100644
index 0000000..a5795a4
--- /dev/null
+++ b/plugins/calendar/drivers/kolab/kolab_user_calendar.php
@@ -0,0 +1,219 @@
+<?php
+
+/**
+ * Kolab calendar storage class simulating a virtual user calendar
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, 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 kolab_user_calendar extends kolab_calendar
+{
+  public $id = 'unknown';
+  public $ready = false;
+  public $readonly = true;
+  public $attachments = false;
+  public $name;
+
+  protected $userdata = array();
+
+
+  /**
+   * Default constructor
+   */
+  public function __construct($user_or_folder, $calendar)
+  {
+    $this->cal = $calendar;
+
+    // full user record is provided
+    if (is_array($user_or_folder)) {
+      $this->userdata = $user_or_folder;
+      $this->storage = new kolab_storage_user_folder($this->userdata['kolabtargetfolder'], '', $this->userdata);
+    }
+    else {  // get user record from LDAP
+      $this->storage = new kolab_storage_user_folder($user_or_folder);
+      $this->userdata = $this->storage->ldaprec;
+    }
+
+    $this->ready = !empty($this->userdata['kolabtargetfolder']);
+
+    if ($this->ready) {
+      // ID is derrived from the user's kolabtargetfolder attribute
+      $this->id = kolab_storage::folder_id($this->userdata['kolabtargetfolder'], true);
+      $this->imap_folder = $this->userdata['kolabtargetfolder'];
+      $this->name = $this->storage->get_name();
+
+      // user-specific alarms settings win
+      $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+      if (isset($prefs[$this->id]['showalarms']))
+        $this->alarms = $prefs[$this->id]['showalarms'];
+    }
+  }
+
+
+  /**
+   * Getter for a nice and human readable name for this calendar
+   *
+   * @return string Name of this calendar
+   */
+  public function get_name()
+  {
+    return $this->userdata['name'] ?: $this->userdata['mail'];
+  }
+
+
+  /**
+   * Getter for the IMAP folder owner
+   *
+   * @return string Name of the folder owner
+   */
+  public function get_owner()
+  {
+    return $this->userdata['mail'];
+  }
+
+
+  /**
+   * Getter for the name of the namespace to which the IMAP folder belongs
+   *
+   * @return string Name of the namespace (personal, other, shared)
+   */
+  public function get_namespace()
+  {
+    return 'user';
+  }
+
+
+  /**
+   * Getter for the top-end calendar folder name (not the entire path)
+   *
+   * @return string Name of this calendar
+   */
+  public function get_foldername()
+  {
+    return $this->get_name();
+  }
+
+  /**
+   * Return color to display this calendar
+   */
+  public function get_color()
+  {
+    // calendar color is stored in local user prefs
+    $prefs = $this->cal->rc->config->get('kolab_calendars', array());
+
+    if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
+      return $prefs[$this->id]['color'];
+
+    return 'cc0000';
+  }
+
+  /**
+   * Compose an URL for CalDAV access to this calendar (if configured)
+   */
+  public function get_caldav_url()
+  {
+    return false;
+  }
+
+  /**
+   * Getter for a single event object
+   */
+  public function get_event($id)
+  {
+    // TODO: implement this
+    return $this->events[$id];
+  }
+
+
+  /**
+   * @param  integer Event's new start (unix timestamp)
+   * @param  integer Event's new end (unix timestamp)
+   * @param  string  Search query (optional)
+   * @param  boolean Include virtual events (optional)
+   * @param  array   Additional parameters to query storage
+   * @return array A list of event records
+   */
+  public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
+  {
+    // TODO: implement this
+    console('kolab_user_calendar::list_events()');
+    return array();
+  }
+
+
+  /**
+   * Create a new event record
+   *
+   * @see calendar_driver::new_event()
+   * 
+   * @return mixed The created record ID on success, False on error
+   */
+  public function insert_event($event)
+  {
+    return false;
+  }
+
+  /**
+   * Update a specific event record
+   *
+   * @see calendar_driver::new_event()
+   * @return boolean True on success, False on error
+   */
+
+  public function update_event($event, $exception_id = null)
+  {
+    return false;
+  }
+
+  /**
+   * Delete an event record
+   *
+   * @see calendar_driver::remove_event()
+   * @return boolean True on success, False on error
+   */
+  public function delete_event($event, $force = true)
+  {
+    return false;
+  }
+
+  /**
+   * Restore deleted event record
+   *
+   * @see calendar_driver::undelete_event()
+   * @return boolean True on success, False on error
+   */
+  public function restore_event($event)
+  {
+    return false;
+  }
+
+
+  /**
+   * Convert from Kolab_Format to internal representation
+   */
+  private function _to_rcube_event($record)
+  {
+    $record['id'] = $record['uid'];
+    $record['calendar'] = $this->id;
+
+    // TODO: implement this
+
+    return $record;
+  }
+
+}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 0f5091a..a3ed443 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -205,6 +205,7 @@ class calendar_ui
   {
     $html = '';
     $jsenv = array();
+    $tree = true;
     $calendars = $this->cal->driver->list_calendars(false, false, $tree);
 
     // walk folder tree
@@ -273,19 +274,22 @@ class calendar_ui
    */
   public function calendar_list_item($id, $prop, &$jsenv)
   {
-    unset($prop['user_id']);
-    $prop['alarms']      = $this->cal->driver->alarms;
-    $prop['attendees']   = $this->cal->driver->attendees;
-    $prop['freebusy']    = $this->cal->driver->freebusy;
-    $prop['attachments'] = $this->cal->driver->attachments;
-    $prop['undelete']    = $this->cal->driver->undelete;
-    $prop['feedurl']     = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
+    // enrich calendar properties with settings from the driver
+    if (!$prop['virtual']) {
+      unset($prop['user_id']);
+      $prop['alarms']      = $this->cal->driver->alarms;
+      $prop['attendees']   = $this->cal->driver->attendees;
+      $prop['freebusy']    = $this->cal->driver->freebusy;
+      $prop['attachments'] = $this->cal->driver->attachments;
+      $prop['undelete']    = $this->cal->driver->undelete;
+      $prop['feedurl']     = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
 
-    if (!$prop['virtual'])
       $jsenv[$id] = $prop;
+    }
 
     $class = 'calendar cal-'  . asciiwords($id, true);
-    $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
+    $title = $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+      html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
     $is_collapsed = false; // TODO: determine this somehow?
 
     if ($prop['virtual'])
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 01a4c3d..a44899b 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -269,12 +269,12 @@ pre {
 #calendars .treelist div.readonly span.calname {
 	background-position: right -20px;
 }
-/*
-#calendars .treelist div.other span.calname {
+
+#calendars .treelist div.user span.calname {
 	background-position: right -38px;
 }
-
-#calendars .treelist div.other.readonly span.calname {
+/*
+#calendars .treelist div.user.readonly span.calname {
 	background-position: right -56px;
 }
 
diff --git a/plugins/kolab_auth/kolab_auth_ldap.php b/plugins/kolab_auth/kolab_auth_ldap.php
index d529b73..7044ebf 100644
--- a/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/plugins/kolab_auth/kolab_auth_ldap.php
@@ -213,7 +213,6 @@ class kolab_auth_ldap extends rcube_ldap_generic
      *                          0 - partial (*abc*),
      *                          1 - strict (=),
      *                          2 - prefix (abc*)
-     * @param boolean $select   True if results are requested, False if count only
      * @param array   $required List of fields that cannot be empty
      * @param int     $limit    Number of records
      *
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
index eed8b50..e2119a8 100644
--- a/plugins/libkolab/js/folderlist.js
+++ b/plugins/libkolab/js/folderlist.js
@@ -46,7 +46,7 @@ function kolab_folderlist(node, p)
           // create treelist widget to present the search results
           if (!search_results_widget) {
               search_results_container = $('<div class="searchresults"></div>')
-                  .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
+                  .html(p.search_title ? '<h2 class="boxtitle">' + p.search_title + '</h2>' : '')
                   .insertAfter(me.container);
 
               search_results_widget = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
@@ -63,36 +63,64 @@ function kolab_folderlist(node, p)
 
                       var li = $(this).closest('li'),
                           id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+                          node = search_results_widget.get_node(id),
                           prop = search_results[id],
-                          parent_id = prop.parent || null;
+                          parent_id = prop.parent || null,
+                          has_children = node.children && node.children.length,
+                          dom_node = has_children ? li.children().first().clone(true, true) : li.children().first();
 
                       // find parent node and insert at the right place
                       if (parent_id && $('#' + p.id_prefix + parent_id, me.container).length) {
                           prop.listname = prop.editname;
-                          li.children().first().children('span,a').first().html(Q(prop.listname));
+                          dom_node.children('span,a').first().html(Q(prop.listname));
                       }
 
-                      // move this result item to the main list widget
-                      me.insert({
-                          id: id,
-                          classes: [],
-                          html: li.children().first()
-                      }, parent_id, parent_id ? true : false);
+                      // TODO: copy parent tree too
+
+                      // replace virtual node with a real one
+                      if (me.get_node(id)) {
+                          $(me.get_item(id, true)).children().first()
+                              .replaceWith(dom_node)
+                              .removeClass('virtual');
+                      }
+                      else {
+                          // move this result item to the main list widget
+                          me.insert({
+                              id: id,
+                              classes: [],
+                              virtual: prop.virtual,
+                              html: dom_node,
+                          }, parent_id, parent_id ? true : false);
+                      }
 
                       delete prop.html;
                       me.triggerEvent('insert-item', { id: id, data: prop, item: li });
-                      li.remove();
+
+                      if (has_children) {
+                          li.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+                      }
+                      else {
+                          li.remove();
+                      }
                   });
           }
 
           // add results to list
-          for (var prop, i=0; i < results.length; i++) {
+          for (var prop, item, i=0; i < results.length; i++) {
               prop = results[i];
+              item = $(prop.html);
               search_results[prop.id] = prop;
-              $('<li>')
-                  .attr('id', p.id_prefix + prop.id)
-                  .html(prop.html)
-                  .appendTo(search_results_widget.container);
+              search_results_widget.insert({
+                  id: prop.id,
+                  classes: prop.class_name ? String(prop.class_name).split(' ') : [],
+                  html: item,
+                  collapsed: true
+              }, prop.parent);
+
+              // disable checkbox if item already exists in main list
+              if (me.get_node(prop.id) && !me.get_node(prop.id).virtual) {
+                  item.find('input[type=checkbox]').first().prop('disabled', true).get(0).checked = true;
+              }
           }
 
           search_results_container.show();
@@ -110,7 +138,7 @@ function kolab_folderlist(node, p)
 
         // send search request(s) to server
         if (search.query && search.execute) {
-            var sources = [ 'folders' /*, 'users'*/ ];
+            var sources = p.search_sources || [ 'folders' ];
             var reqid = rcmail.multi_thread_http_request({
                 items: sources,
                 threads: rcmail.env.autocomplete_threads || 1,
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 970316e..872ce29 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -44,6 +44,7 @@ class kolab_storage
     private static $states;
     private static $config;
     private static $imap;
+    private static $ldap;
 
     // Default folder names
     private static $default_folders = array(
@@ -101,6 +102,40 @@ class kolab_storage
         return self::$ready;
     }
 
+    /**
+     * Initializes LDAP object to resolve Kolab users
+     */
+    public static function ldap()
+    {
+        if (self::$ldap) {
+            return self::$ldap;
+        }
+
+        $rcmail = rcube::get_instance();
+        $config = $rcmail->config->get('kolab_users_directory', $rcmail->config->get('kolab_auth_addressbook'));
+
+        if (!is_array($config)) {
+            $ldap_config = (array)$rcmail->config->get('ldap_public');
+            $config = $ldap_config[$config];
+        }
+
+        if (empty($config)) {
+            return null;
+        }
+
+        // overwrite filter option
+        if ($filter = $rcmail->config->get('kolab_users_filter')) {
+            $rcmail->config->set('kolab_auth_filter', $filter);
+        }
+
+        // re-use the LDAP wrapper class from kolab_auth plugin
+        require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php';
+
+        self::$ldap = new kolab_auth_ldap($config);
+
+        return self::$ldap;
+    }
+
 
     /**
      * Get a list of storage folders for the given data type
@@ -490,7 +525,7 @@ class kolab_storage
                         $folder = substr($folder, $pos+1);
                     }
                     else {
-                        $prefix = $folder;
+                        $prefix = '('.$folder.')';
                         $folder = '';
                     }
 
@@ -811,7 +846,7 @@ class kolab_storage
 
         // $folders is a result of get_folders() we can assume folders were already sorted
         foreach (array_keys($nsnames) as $ns) {
-            // asort($nsnames[$ns], SORT_LOCALE_STRING);
+            asort($nsnames[$ns], SORT_LOCALE_STRING);
             foreach (array_keys($nsnames[$ns]) as $utf7name) {
                 $out[] = $folders[$utf7name];
             }
@@ -829,13 +864,18 @@ class kolab_storage
      *
      * @return array Flat folders list
      */
-    public static function folder_hierarchy($folders, &$tree)
+    public static function folder_hierarchy($folders, &$tree = null)
     {
         $_folders = array();
-        $delim    = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
-        $tree     = new virtual_kolab_storage_folder('', '<root>', '');  // create tree root
+        $delim    = self::$imap->get_hierarchy_delimiter();
+        $other_ns = self::$imap->get_namespace('other');
+        $tree     = new kolab_storage_virtual_folder('', '<root>', '');  // create tree root
         $refs     = array('' => $tree);
 
+        if (is_array($other_ns)) {
+            $other_ns = rtrim($other_ns[0][0], '/');
+        }
+
         foreach ($folders as $idx => $folder) {
             $path = explode($delim, $folder->name);
             array_pop($path);
@@ -852,9 +892,15 @@ class kolab_storage
 
                 while (count($path) >= $depth && ($parent = join($delim, $path))) {
                     array_pop($path);
-                    $name = kolab_storage::object_name($parent, $folder->get_namespace());
+                    $parent_parent = join($delim, $path);
                     if (!$refs[$parent]) {
-                        $refs[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace(), join($delim, $path));
+                        if ($parent_parent == $other_ns) {
+                            $refs[$parent] = new kolab_storage_user_folder($parent, $parent_parent);
+                        }
+                        else {
+                            $name = kolab_storage::object_name($parent, $folder->get_namespace());
+                            $refs[$parent] = new kolab_storage_virtual_folder($parent, $name, $folder->get_namespace(), $parent_parent);
+                        }
                         $parents[] = $refs[$parent];
                     }
                 }
@@ -1023,14 +1069,22 @@ class kolab_storage
      * Change subscription status of this folder
      *
      * @param string $folder Folder name
+     * @param boolean $temp  Only remove temporary subscription
      *
      * @return True on success, false on error
      */
-    public static function folder_unsubscribe($folder)
+    public static function folder_unsubscribe($folder, $temp = false)
     {
         self::setup();
 
-        if (self::$imap->unsubscribe($folder)) {
+        // temporary/session subscription
+        if ($temp) {
+            if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
+                unset($_SESSION['kolab_subscribed_folders'][$i]);
+            }
+            return true;
+        }
+        else if (self::$imap->unsubscribe($folder)) {
             self::$subscriptions === null;
             return true;
         }
@@ -1078,10 +1132,8 @@ class kolab_storage
      */
     public static function folder_deactivate($folder)
     {
-        // remove from temp subscriptions
-        if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
-            unset($_SESSION['kolab_subscribed_folders'][$i]);
-        }
+        // remove from temp subscriptions, really?
+        self::folder_unsubscribe($folder, true);
 
         return self::set_state($folder, false);
     }
@@ -1241,57 +1293,117 @@ class kolab_storage
         }
     }
 
+
     /**
-     * Handler for user_delete plugin hooks
      *
-     * Remove all cache data from the local database related to the given user.
+     * @param mixed   $query    Search value (or array of field => value pairs)
+     * @param int     $mode     Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
+     * @param array   $required List of fields that shall ot be empty
+     * @param int     $limit    Number of records
+     *
+     * @return array List or false on error
      */
-    public static function delete_user_folders($args)
+    public static function search_users($query, $mode = 1, $required = array(), $limit = 0)
     {
-        $db = rcmail::get_instance()->get_dbh();
-        $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
-        $db->query("DELETE FROM " . $db->table_name('kolab_folders') . " WHERE resource LIKE ?", $prefix);
-    }
+        // requires a working LDAP setup
+        if (!self::ldap()) {
+            return array();
+        }
 
-}
+        // FIXME: make search attributes configurable
+        $results = self::$ldap->search(array('cn','mail','alias'), $query, $mode, $required, $limit);
 
-/**
- * Helper class that represents a virtual IMAP folder
- * with a subset of the kolab_storage_folder API.
- */
-class virtual_kolab_storage_folder
-{
-    public $id;
-    public $name;
-    public $namespace;
-    public $parent = '';
-    public $children = array();
-    public $virtual = true;
-    protected $displayname;
-
-    public function __construct($name, $dispname, $ns, $parent = '')
-    {
-        $this->id        = kolab_storage::folder_id($name);
-        $this->name      = $name;
-        $this->namespace = $ns;
-        $this->parent    = $parent;
-        $this->displayname = $dispname;
+        // resolve to IMAP folder name
+        $other_ns = self::$imap->get_namespace('other');
+        $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+
+        array_walk($results, function(&$user, $dn) use ($other_ns, $user_attrib) {
+            list($localpart, $domain) = explode('@', $user[$user_attrib]);
+            $root = $other_ns[0][0];
+            $user['kolabtargetfolder'] = $root . $localpart;
+        });
+
+        return $results;
     }
 
-    public function get_namespace()
+
+    /**
+     * Returns a list of IMAP folders shared by the given user
+     *
+     * @param array   User entry from LDAP
+     * @param string  Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
+     * @param boolean Return subscribed folders only (null to use configured subscription mode)
+     * @param array   Will be filled with folder-types data
+     *
+     * @return array List of folders
+     */
+    public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
     {
-        return $this->namespace;
+        $folders = array();
+
+        // use localpart of user attribute as root for folder listing
+        $user_attrib = rcube::get_instance()->config->get('kolab_auth_login', 'mail');
+        if (!empty($user[$user_attrib])) {
+            list($mbox) = explode('@', $user[$user_attrib]);
+
+            $other_ns = self::$imap->get_namespace('other');
+            if (is_array($other_ns)) {
+                $other_ns = $other_ns[0][0];
+            }
+
+            $folders = self::list_folders($other_ns . $mbox, '*', $type, $subscribed, $folderdata);
+        }
+
+        return $folders;
     }
 
-    public function get_name()
+
+    /**
+     * Get a list of (virtual) top-level folders from the other users namespace
+     *
+     * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
+     *
+     * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
+     */
+    public static function get_user_folders($subscribed)
     {
-        // this is already kolab_storage::object_name() result
-        return $this->displayname;
+        $folders = $folderdata = array();
+
+        if (self::setup()) {
+            $delimiter = self::$imap->get_hierarchy_delimiter();
+            $other_ns = self::$imap->get_namespace('other');
+            if (is_array($other_ns)) {
+                $other_ns = rtrim($other_ns[0][0], $delimiter);
+                $other_depth = count(explode($delimiter, $other_ns));
+            }
+
+            foreach ((array)self::list_folders($other_ns, '*', '', $subscribed) as $foldername) {
+                $path = explode($delimiter, $foldername);
+                $depth = count($path) - $other_depth;
+                array_pop($path);
+
+                // only list top-level folders of the 'other' namespace
+                if ($depth == 1) {
+                    $folders[$foldername] = new kolab_storage_user_folder($foldername, $other_ns);
+                }
+            }
+        }
+
+        return $folders;
     }
 
-    public function get_foldername()
+
+    /**
+     * Handler for user_delete plugin hooks
+     *
+     * Remove all cache data from the local database related to the given user.
+     */
+    public static function delete_user_folders($args)
     {
-        $parts = explode('/', $this->name);
-        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+        $db = rcmail::get_instance()->get_dbh();
+        $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
+        $db->query("DELETE FROM " . $db->table_name('kolab_folders') . " WHERE resource LIKE ?", $prefix);
     }
+
 }
+
diff --git a/plugins/libkolab/lib/kolab_storage_user_folder.php b/plugins/libkolab/lib/kolab_storage_user_folder.php
new file mode 100644
index 0000000..55e38a0
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_user_folder.php
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * Class that represents a (virtual) folder in the 'other' namespace
+ * implementing a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, 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 kolab_storage_user_folder extends kolab_storage_virtual_folder
+{
+    protected static $ldapcache = array();
+
+    public $ldaprec;
+
+    /**
+     * Default constructor
+     */
+    public function __construct($name, $parent = '', $ldaprec = null)
+    {
+        parent::__construct($name, $name, 'other', $parent);
+
+        if (!empty($ldaprec)) {
+            self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
+        }
+        // use value cached in memory for repeated lookups
+        else if (array_key_exists($name, self::$ldapcache)) {
+            $this->ldaprec = self::$ldapcache[$name];
+        }
+        // lookup user in LDAP and set $this->ldaprec
+        else if ($ldap = kolab_storage::ldap()) {
+            // get domain from current user
+            list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
+            $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
+            if (!empty($this->ldaprec)) {
+                $this->ldaprec['kolabtargetfolder'] = $name;
+            }
+            self::$ldapcache[$name] = $this->ldaprec;
+        }
+    }
+
+    /**
+     * Getter for the top-end folder name to be displayed
+     *
+     * @return string Name of this folder
+     */
+    public function get_foldername()
+    {
+        return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
+            parent::get_foldername();
+    }
+
+    /**
+     * Returns the owner of the folder.
+     *
+     * @return string  The owner of this folder.
+     */
+    public function get_owner()
+    {
+        return $this->ldaprec['mail'];
+    }
+
+    /**
+     * Check activation status of this folder
+     *
+     * @return boolean True if enabled, false if not
+     */
+    public function is_active()
+    {
+        return kolab_storage::folder_is_active($this->name);
+    }
+
+    /**
+     * Change activation status of this folder
+     *
+     * @param boolean The desired subscription status: true = active, false = not active
+     *
+     * @return True on success, false on error
+     */
+    public function activate($active)
+    {
+        return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
+    }
+
+    /**
+     * Check subscription status of this folder
+     *
+     * @return boolean True if subscribed, false if not
+     */
+    public function is_subscribed()
+    {
+        return kolab_storage::folder_is_subscribed($this->name, true);
+    }
+
+    /**
+     * Change subscription status of this folder
+     *
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+     *
+     * @return True on success, false on error
+     */
+    public function subscribe($subscribed)
+    {
+        return $subscribed ?
+            kolab_storage::folder_subscribe($this->name, true) :
+            kolab_storage::folder_unsubscribe($this->name, true);
+    }
+
+}
\ No newline at end of file
diff --git a/plugins/libkolab/lib/kolab_storage_virtual_folder.php b/plugins/libkolab/lib/kolab_storage_virtual_folder.php
new file mode 100644
index 0000000..61d0fe0
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_virtual_folder.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, 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 kolab_storage_virtual_folder
+{
+    public $id;
+    public $name;
+    public $namespace;
+    public $parent = '';
+    public $children = array();
+    public $virtual = true;
+
+    protected $displayname;
+
+    public function __construct($name, $dispname, $ns, $parent = '')
+    {
+        $this->id        = kolab_storage::folder_id($name);
+        $this->name      = $name;
+        $this->namespace = $ns;
+        $this->parent    = $parent;
+        $this->displayname = $dispname;
+    }
+
+    /**
+     * Getter for the name of the namespace to which the IMAP folder belongs
+     *
+     * @return string Name of the namespace (personal, other, shared)
+     */
+    public function get_namespace()
+    {
+        return $this->namespace;
+    }
+
+    /**
+     * Get the display name value of this folder
+     *
+     * @return string Folder name
+     */
+    public function get_name()
+    {
+        // this is already kolab_storage::object_name() result
+        return $this->displayname;
+    }
+
+    /**
+     * Getter for the top-end folder name (not the entire path)
+     *
+     * @return string Name of this folder
+     */
+    public function get_foldername()
+    {
+        $parts = explode('/', $this->name);
+        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+    }
+
+    /**
+     * Get the color value stored in metadata
+     *
+     * @param string Default color value to return if not set
+     * @return mixed Color value from IMAP metadata or $default is not set
+     */
+    public function get_color($default = null)
+    {
+        return $default;
+    }
+}
\ No newline at end of file


commit 8a47c676d52645f424a5234c9c6d2fdfa7536f49
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 13 19:14:08 2014 +0200

    Move new calendar list widget and folder searching to libkolab for shared use

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 4db8593..52baf11 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1548,7 +1548,8 @@ function rcube_calendar_ui(settings)
           id_prefix: 'rcres',
           id_encode: rcmail.html_identifier_encode,
           id_decode: rcmail.html_identifier_decode,
-          selectable: true
+          selectable: true,
+          save_state: true
         });
         resources_treelist.addEventListener('select', function(node) {
           if (resources_data[node.id]) {
@@ -2671,72 +2672,6 @@ function rcube_calendar_ui(settings)
       this.selected_calendar = id;
     };
 
-    // render the results for calendar list search
-    var calendar_search_results = function(results)
-    {
-      if (results.length) {
-        // create treelist widget to present the search results
-        if (!calenders_search_list) {
-          calenders_search_container = $('<div class="searchresults"></div>')
-            .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
-            .insertAfter(rcmail.gui_objects.calendarslist);
-
-          calenders_search_list = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
-            id_prefix: 'rcmlical',
-            selectable: false
-          });
-
-          // register click handler on search result's checkboxes to select the given calendar for listing
-          calenders_search_list.container
-            .appendTo(calenders_search_container)
-            .on('click', 'input[type=checkbox]', function(e){
-              var li = $(this).closest('li'),
-                id = li.attr('id').replace(/^rcmlical/, ''),
-                prop = search_calendars[id],
-                parent_id = prop.parent || null;
-
-              if (!this.checked)
-                return;
-
-              // find parent node and insert at the right place
-              if (parent_id && $('#rcmlical'+parent_id, rcmail.gui_objects.calendarslist).length) {
-                prop.listname = prop.editname;
-                li.children().first().find('.calname').html(Q(prop.listname));
-              }
-
-              // move this calendar to the calendars_list widget
-              calendars_list.insert({
-                id: id,
-                classes: [],
-                html: li.children().first()
-              }, parent_id, parent_id ? true : false);
-
-              search_calendars[id].active = true;
-              add_calendar_source(prop);
-              li.remove();
-
-              // add css classes related to this calendar to document
-              if (cal.css) {
-                $('<style type="text/css"></style>')
-                  .html(cal.css)
-                  .appendTo('head');
-              }
-            });
-        }
-
-        for (var cal, i=0; i < results.length; i++) {
-          cal = results[i];
-          search_calendars[cal.id] = cal;
-          $('<li>')
-            .attr('id', 'rcmlical' + cal.id)
-            .html(cal.html)
-            .appendTo(calenders_search_list.container);
-        }
-
-        calenders_search_container.show();
-      }
-    };
-
     // register the given calendar to the current view
     var add_calendar_source = function(cal)
     {
@@ -2798,37 +2733,31 @@ function rcube_calendar_ui(settings)
     }
 
     // initialize treelist widget that controls the calendars list
-    calendars_list = new rcube_treelist_widget(rcmail.gui_objects.calendarslist, {
+    var widget_class = window.kolab_folderlist || rcube_treelist_widget;
+    calendars_list = new widget_class(rcmail.gui_objects.calendarslist, {
       id_prefix: 'rcmlical',
       selectable: true,
-      searchbox: '#calendarlistsearch'
+      save_state: true,
+      searchbox: '#calendarlistsearch',
+      search_action: 'calendar/calendar'
     });
-    calendars_list.addEventListener('select', function(node){
+    calendars_list.addEventListener('select', function(node) {
       me.select_calendar(node.id);
       rcmail.enable_command('calendar-edit', 'calendar-showurl', true);
       rcmail.enable_command('calendar-remove', !me.calendars[node.id].readonly);
     });
-    calendars_list.addEventListener('search', function(search){
-      // hide search results
-      if (calenders_search_list) {
-        calenders_search_container.hide();
-        calenders_search_list.reset();
-      }
-      search_calendars = {};
-
-      // send search request(s) to server
-      if (search.query && search.execute) {
-        var sources = [ 'folders' /*, 'users'*/ ];
-        var reqid = rcmail.multi_thread_http_request({
-          items: sources,
-          threads: rcmail.env.autocomplete_threads || 1,
-          action:  'calendar/calendar',
-          postdata: { action:'search', q:search.query, source:'%s' },
-          lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
-          onresponse: calendar_search_results
-        });
-
-        listsearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
+    calendars_list.addEventListener('insert-item', function(p) {
+      var cal = p.data;
+      if (cal && cal.id) {
+        cal.active = true;
+        add_calendar_source(cal);
+
+        // add css classes related to this calendar to document
+        if (cal.css) {
+          $('<style type="text/css"></style>')
+            .html(cal.css)
+            .appendTo('head');
+        }
       }
     });
 
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 786556f..9c45eb6 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -381,23 +381,10 @@ class kolab_driver extends calendar_driver
       return array();
 
     $this->calendars = array();
-    $imap = $this->rc->get_storage();
 
     // find unsubscribed IMAP folders that have "event" type
     if ($source == 'folders') {
-      $folders = array();
-      foreach ((array)kolab_storage::list_folders('', '*', 'event', false, $folderdata) as $foldername) {
-        // FIXME: only consider the last part of the folder path for searching?
-        $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
-        if (strpos($realname, $query) !== false &&
-            !kolab_storage::folder_is_subscribed($foldername, true) &&
-            $imap->folder_namespace($foldername) != 'other'
-          ) {
-          $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
-        }
-      }
-
-      foreach ($folders as $folder) {
+      foreach ((array)kolab_storage::search_folders('event', $query, array('other')) as $folder) {
         $calendar = new kolab_calendar($folder->name, $this->cal);
         $this->calendars[$calendar->id] = $calendar;
       }
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 69ba9c2..0f5091a 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -117,6 +117,12 @@ class calendar_ui
     $this->cal->include_script('calendar_ui.js');
     $this->cal->include_script('lib/js/fullcalendar.js');
     $this->rc->output->include_script('treelist.js');
+
+    // include kolab folderlist widget if available
+    if (is_readable($this->cal->home . '/lib/js/folderlist.js')) {
+      $this->cal->include_script('lib/js/folderlist.js');
+    }
+
     jqueryui::miniColors();
   }
 
@@ -292,9 +298,9 @@ class calendar_ui
     $content = '';
     if (!$attrib['activeonly'] || $prop['active']) {
       $content = html::div($class,
+        html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname']) .
         ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
-        html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')) .
-        html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname'])
+        html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' '))
       );
     }
 
diff --git a/plugins/calendar/lib/js/folderlist.js b/plugins/calendar/lib/js/folderlist.js
new file mode 120000
index 0000000..c49706b
--- /dev/null
+++ b/plugins/calendar/lib/js/folderlist.js
@@ -0,0 +1 @@
+../../../libkolab/js/folderlist.js
\ No newline at end of file
diff --git a/plugins/libkolab/js/folderlist.js b/plugins/libkolab/js/folderlist.js
new file mode 100644
index 0000000..eed8b50
--- /dev/null
+++ b/plugins/libkolab/js/folderlist.js
@@ -0,0 +1,130 @@
+/**
+ * Kolab groupware folders treelist widget
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * @licstart  The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (C) 2014, 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/>.
+ *
+ * @licend  The above is the entire license notice
+ * for the JavaScript code in this file.
+ */
+
+function kolab_folderlist(node, p)
+{
+    // extends treelist.js
+    rcube_treelist_widget.call(this, node, p);
+
+    // private vars
+    var me = this;
+    var search_results;
+    var search_results_widget;
+    var search_results_container;
+    var listsearch_request;
+
+    var Q = rcmail.quote_html;
+
+    // render the results for folderlist search
+    function render_search_results(results)
+    {
+        if (results.length) {
+          // create treelist widget to present the search results
+          if (!search_results_widget) {
+              search_results_container = $('<div class="searchresults"></div>')
+                  .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
+                  .insertAfter(me.container);
+
+              search_results_widget = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
+                  id_prefix: p.id_prefix,
+                  selectable: false
+              });
+
+              // register click handler on search result's checkboxes to select the given item for listing
+              search_results_widget.container
+                  .appendTo(search_results_container)
+                  .on('click', 'input[type=checkbox]', function(e) {
+                      if (!this.checked)
+                          return;
+
+                      var li = $(this).closest('li'),
+                          id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+                          prop = search_results[id],
+                          parent_id = prop.parent || null;
+
+                      // find parent node and insert at the right place
+                      if (parent_id && $('#' + p.id_prefix + parent_id, me.container).length) {
+                          prop.listname = prop.editname;
+                          li.children().first().children('span,a').first().html(Q(prop.listname));
+                      }
+
+                      // move this result item to the main list widget
+                      me.insert({
+                          id: id,
+                          classes: [],
+                          html: li.children().first()
+                      }, parent_id, parent_id ? true : false);
+
+                      delete prop.html;
+                      me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+                      li.remove();
+                  });
+          }
+
+          // add results to list
+          for (var prop, i=0; i < results.length; i++) {
+              prop = results[i];
+              search_results[prop.id] = prop;
+              $('<li>')
+                  .attr('id', p.id_prefix + prop.id)
+                  .html(prop.html)
+                  .appendTo(search_results_widget.container);
+          }
+
+          search_results_container.show();
+        }
+    }
+
+    // do some magic when search is performed on the widget
+    this.addEventListener('search', function(search) {
+        // hide search results
+        if (search_results_widget) {
+            search_results_container.hide();
+            search_results_widget.reset();
+        }
+        search_results = {};
+
+        // send search request(s) to server
+        if (search.query && search.execute) {
+            var sources = [ 'folders' /*, 'users'*/ ];
+            var reqid = rcmail.multi_thread_http_request({
+                items: sources,
+                threads: rcmail.env.autocomplete_threads || 1,
+                action:  p.search_action || 'listsearch',
+                postdata: { action:'search', q:search.query, source:'%s' },
+                lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
+                onresponse: render_search_results
+            });
+
+            listsearch_request = { id:reqid, sources:sources.slice(), num:sources.length };
+        }
+    });
+
+}
+
+// link prototype from base class
+kolab_folderlist.prototype = rcube_treelist_widget.prototype;
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 55526fd..970316e 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -759,6 +759,39 @@ class kolab_storage
 
 
     /**
+     * Search for shared or otherwise not listed groupware folders the user has access
+     *
+     * @param string Folder type of folders to search for
+     * @param string Search string
+     * @param array  Namespace(s) to exclude results from
+     *
+     * @return array List of matching kolab_storage_folder objects
+     */
+    public static function search_folders($type, $query, $exclude_ns = array())
+    {
+        if (!self::setup()) {
+            return array();
+        }
+
+        $folders = array();
+
+        // find unsubscribed IMAP folders of the given type
+        foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
+            // FIXME: only consider the last part of the folder path for searching?
+            $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
+            if (strpos($realname, $query) !== false &&
+                !self::folder_is_subscribed($foldername, true) &&
+                !in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
+              ) {
+                $folders[] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+            }
+        }
+
+        return $folders;
+    }
+
+
+    /**
      * Sort the given list of kolab folders by namespace/name
      *
      * @param array List of kolab_storage_folder objects


commit 008c5db5d950f6dfee3458a20264f68a26e5cf6e
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Tue May 13 17:09:53 2014 +0200

    Implement searching for unsubscribed IMAP folders and temporary/session subscriptions

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 1d40efe..fc0069d 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -725,9 +725,29 @@ class calendar extends rcube_plugin
           $this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
         break;
       case "subscribe":
-        if (!$this->driver->subscribe_calendar($cal))
+        if (!$this->driver->subscribe_calendar($cal, intval(get_input_value('perm', RCUBE_INPUT_GPC))))
           $this->rc->output->show_message($this->gettext('errorsaving'), 'error');
         return;
+      case "search":
+        $results = array();
+        $color_mode = $this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
+        foreach ((array)$this->driver->search_calendars(get_input_value('q', RCUBE_INPUT_GPC), get_input_value('source', RCUBE_INPUT_GPC)) as $id => $prop) {
+          $editname = $prop['editname'];
+          unset($prop['editname']);  // force full name to be displayed
+          $prop['active'] = false;
+
+          // let the UI generate HTML and CSS representation for this calendar
+          $html = $this->ui->calendar_list_item($id, $prop, $jsenv);
+          $cal = $jsenv[$id];
+          $cal['editname'] = $editname;
+          $cal['html'] = $html;
+          if (!empty($prop['color']))
+            $cal['css'] = $this->ui->calendar_css_classes($id, $prop, $color_mode);
+
+          $results[] = $cal;
+        }
+        $this->rc->output->command('multi_thread_http_response', $results, get_input_value('_reqid', RCUBE_INPUT_GPC));
+        return;
     }
     
     if ($success)
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 2a8a8d2..4db8593 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -39,6 +39,7 @@ function rcube_calendar_ui(settings)
     this.selected_calendar = null;
     this.search_request = null;
     this.saving_lock;
+    this.calendars = {};
 
 
     /***  private vars  ***/
@@ -52,6 +53,9 @@ function rcube_calendar_ui(settings)
     var event_defaults = { free_busy:'busy', alarms:'' };
     var event_attendees = [];
     var calendars_list;
+    var calenders_search_list;
+    var calenders_search_container;
+    var search_calendars = {};
     var attendees_list;
     var resources_list;
     var resources_treelist;
@@ -2667,16 +2671,79 @@ function rcube_calendar_ui(settings)
       this.selected_calendar = id;
     };
 
+    // render the results for calendar list search
+    var calendar_search_results = function(results)
+    {
+      if (results.length) {
+        // create treelist widget to present the search results
+        if (!calenders_search_list) {
+          calenders_search_container = $('<div class="searchresults"></div>')
+            .html('<h2 class="boxtitle">' + rcmail.gettext('calsearchresults','calendar') + '</h2>')
+            .insertAfter(rcmail.gui_objects.calendarslist);
 
-    /***  startup code  ***/
+          calenders_search_list = new rcube_treelist_widget('<ul class="treelist listing"></ul>', {
+            id_prefix: 'rcmlical',
+            selectable: false
+          });
 
-    // create list of event sources AKA calendars
-    this.calendars = {};
-    var id, li, cal, active, color, brightness, event_sources = [];
-    for (id in rcmail.env.calendars) {
-      cal = rcmail.env.calendars[id];
-      this.calendars[id] = $.extend({
-        url: "./?_task=calendar&_action=load_events&source="+escape(id),
+          // register click handler on search result's checkboxes to select the given calendar for listing
+          calenders_search_list.container
+            .appendTo(calenders_search_container)
+            .on('click', 'input[type=checkbox]', function(e){
+              var li = $(this).closest('li'),
+                id = li.attr('id').replace(/^rcmlical/, ''),
+                prop = search_calendars[id],
+                parent_id = prop.parent || null;
+
+              if (!this.checked)
+                return;
+
+              // find parent node and insert at the right place
+              if (parent_id && $('#rcmlical'+parent_id, rcmail.gui_objects.calendarslist).length) {
+                prop.listname = prop.editname;
+                li.children().first().find('.calname').html(Q(prop.listname));
+              }
+
+              // move this calendar to the calendars_list widget
+              calendars_list.insert({
+                id: id,
+                classes: [],
+                html: li.children().first()
+              }, parent_id, parent_id ? true : false);
+
+              search_calendars[id].active = true;
+              add_calendar_source(prop);
+              li.remove();
+
+              // add css classes related to this calendar to document
+              if (cal.css) {
+                $('<style type="text/css"></style>')
+                  .html(cal.css)
+                  .appendTo('head');
+              }
+            });
+        }
+
+        for (var cal, i=0; i < results.length; i++) {
+          cal = results[i];
+          search_calendars[cal.id] = cal;
+          $('<li>')
+            .attr('id', 'rcmlical' + cal.id)
+            .html(cal.html)
+            .appendTo(calenders_search_list.container);
+        }
+
+        calenders_search_container.show();
+      }
+    };
+
+    // register the given calendar to the current view
+    var add_calendar_source = function(cal)
+    {
+      var color, brightness, select, id = cal.id;
+
+      me.calendars[id] = $.extend({
+        url: rcmail.url('calendar/load_events', { source: id }),
         editable: !cal.readonly,
         className: 'fc-event-cal-'+id,
         id: id
@@ -2689,18 +2756,40 @@ function rcube_calendar_ui(settings)
           // http://javascriptrules.com/2009/08/05/css-color-brightness-contrast-using-javascript/
           brightness = (parseInt(RegExp.$1, 16) * 299 + parseInt(RegExp.$2, 16) * 587 + parseInt(RegExp.$3, 16) * 114) / 1000;
           if (brightness > 125)
-            this.calendars[id].textColor = 'black';
+            me.calendars[id].textColor = 'black';
         }
+
+        me.calendars[id].color = color;
       }
 
-      this.calendars[id].color = color;
+      if (fc && cal.active) {
+        fc.fullCalendar('addEventSource', me.calendars[id]);
+        rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:cal.active?1:0 } });
+      }
 
-      if ((active = cal.active || false)) {
-        event_sources.push(this.calendars[id]);
+      // insert to #calendar-select options if writeable
+      select = $('#edit-calendar');
+      if (fc && !cal.readonly && select.length && !select.find('option[value="'+id+'"]').length) {
+        $('<option>').attr('value', id).text(Q(cal.name)).appendTo(select);
       }
+    }
+
+
+    /***  startup code  ***/
+
+    // create list of event sources AKA calendars
+    var id, cal, active, event_sources = [];
+    for (id in rcmail.env.calendars) {
+      cal = rcmail.env.calendars[id];
+      active = cal.active || false;
+      add_calendar_source(cal);
 
       // check active calendars
-      $('#rcmlical'+id+' > .calendar input').data('id', id).get(0).checked = active;
+      $('#rcmlical'+id+' > .calendar input').get(0).checked = active;
+
+      if (active) {
+        event_sources.push(this.calendars[id]);
+      }
 
       if (!cal.readonly && !this.selected_calendar) {
         this.selected_calendar = id;
@@ -2720,12 +2809,32 @@ function rcube_calendar_ui(settings)
       rcmail.enable_command('calendar-remove', !me.calendars[node.id].readonly);
     });
     calendars_list.addEventListener('search', function(search){
-      console.log(search);
+      // hide search results
+      if (calenders_search_list) {
+        calenders_search_container.hide();
+        calenders_search_list.reset();
+      }
+      search_calendars = {};
+
+      // send search request(s) to server
+      if (search.query && search.execute) {
+        var sources = [ 'folders' /*, 'users'*/ ];
+        var reqid = rcmail.multi_thread_http_request({
+          items: sources,
+          threads: rcmail.env.autocomplete_threads || 1,
+          action:  'calendar/calendar',
+          postdata: { action:'search', q:search.query, source:'%s' },
+          lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
+          onresponse: calendar_search_results
+        });
+
+        listsearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
+      }
     });
 
     // init (delegate) event handler on calendar list checkboxes
     $(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e){
-      var id = $(this).data('id');
+      var id = this.value;
       if (me.calendars[id]) {  // add or remove event source on click
         var action;
         if (this.checked) {
diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php
index c5eff9d..9fb6ffb 100644
--- a/plugins/calendar/drivers/calendar_driver.php
+++ b/plugins/calendar/drivers/calendar_driver.php
@@ -166,6 +166,15 @@ abstract class calendar_driver
   abstract function remove_calendar($prop);
 
   /**
+   * Search for shared or otherwise not listed calendars the user has access
+   *
+   * @param string Search string
+   * @param string Section/source to search
+   * @return array List of calendars
+   */
+  abstract function search_calendars($query, $source);
+
+  /**
    * Add a single event to the database
    *
    * @param array Hash array with event properties (see header of this file)
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 059cd1d..d0e6b7b 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -248,6 +248,19 @@ class database_driver extends calendar_driver
   }
 
   /**
+   * Search for shared or otherwise not listed calendars the user has access
+   *
+   * @param string Search string
+   * @param string Section/source to search
+   * @return array List of calendars
+   */
+  public function search_calendars($query, $source)
+  {
+    // not implemented
+    return array();
+  }
+
+  /**
    * Add a single event to the database
    *
    * @param array Hash array with event properties
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 46fbed9..5feed23 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -52,11 +52,12 @@ class kolab_calendar
       $this->imap_folder = $this->name = $imap_folder;
 
     // ID is derrived from folder name
-    $this->id = kolab_storage::folder_id($this->imap_folder);
+    $this->id = kolab_storage::folder_id($this->imap_folder, true);
+    $old_id   = kolab_storage::folder_id($this->imap_folder, false);
 
     // fetch objects from the given IMAP folder
     $this->storage = kolab_storage::get_folder($this->imap_folder);
-    $this->ready = $this->storage && !PEAR::isError($this->storage);
+    $this->ready = $this->storage && !PEAR::isError($this->storage) && $this->storage->type !== null;
 
     // Set readonly and alarms flags according to folder permissions
     if ($this->ready) {
@@ -76,6 +77,8 @@ class kolab_calendar
       $prefs = $this->cal->rc->config->get('kolab_calendars', array());
       if (isset($prefs[$this->id]['showalarms']))
         $this->alarms = $prefs[$this->id]['showalarms'];
+      else if (isset($prefs[$old_id]['showalarms']))
+        $this->alarms = $prefs[$old_id]['showalarms'];
     }
   }
 
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 974a3d3..786556f 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -62,6 +62,9 @@ class kolab_driver extends calendar_driver
         $this->alarm_types = array('DISPLAY');
         $this->alarm_absolute = false;
     }
+
+    // calendar uses fully encoded identifiers
+    kolab_storage::$encode_ids = true;
   }
 
 
@@ -117,6 +120,9 @@ class kolab_driver extends calendar_driver
     foreach ($folders as $id => $cal) {
       $fullname = $cal->get_name();
       $listname = kolab_storage::folder_displayname($fullname, $names);
+      $imap_path = explode('/', $cal->name);
+      $topname = array_pop($imap_path);
+      $parent_id = kolab_storage::folder_id(join('/', $imap_path), true);
 
       // special handling for virtual folders
       if ($cal->virtual) {
@@ -143,6 +149,7 @@ class kolab_driver extends calendar_driver
           'active'   => $cal->storage->is_active(),
           'owner'    => $cal->get_owner(),
           'children' => true,  // TODO: determine if that folder indeed has child folders
+          'parent'   => $parent_id,
           'caldavurl' => $cal->get_caldav_url(),
         );
       }
@@ -212,6 +219,26 @@ class kolab_driver extends calendar_driver
     return $calendars;
   }
 
+
+  /**
+   * Get the kolab_calendar instance for the given calendar ID
+   *
+   * @param string Calendar identifier (encoded imap folder name)
+   * @return object kolab_calendar Object nor null if calendar doesn't exist
+   */
+  protected function get_calendar($id)
+  {
+    // create calendar object if necesary
+    if (!$this->calendars[$id] && $id !== self::BIRTHDAY_CALENDAR_ID) {
+      $foldername = kolab_storage::id_decode($id);
+      $calendar = new kolab_calendar($foldername, $this->cal);
+      if ($calendar->ready)
+        $this->calendars[$calendar->id] = $calendar;
+    }
+
+    return $this->calendars[$id];
+  }
+
   /**
    * Create a new calendar assigned to the current user
    *
@@ -257,7 +284,7 @@ class kolab_driver extends calendar_driver
    */
   public function edit_calendar($prop)
   {
-    if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
+    if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
       $prop['oldname'] = $cal->get_realname();
       $newfolder = kolab_storage::folder_update($prop);
 
@@ -297,9 +324,10 @@ class kolab_driver extends calendar_driver
    *
    * @see calendar_driver::subscribe_calendar()
    */
-  public function subscribe_calendar($prop)
+  public function subscribe_calendar($prop, $permanent = false)
   {
-    if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
+    if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
+      if ($permanent) $cal->storage->subscribe($prop['active']);
       return $cal->storage->activate($prop['active']);
     }
     else {
@@ -321,8 +349,9 @@ class kolab_driver extends calendar_driver
    */
   public function remove_calendar($prop)
   {
-    if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
+    if ($prop['id'] && ($cal = $this->get_calendar($prop['id']))) {
       $folder = $cal->get_realname();
+      // TODO: unsubscribe if no admin rights
       if (kolab_storage::folder_delete($folder)) {
         // remove color in user prefs (temp. solution)
         $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
@@ -340,6 +369,51 @@ class kolab_driver extends calendar_driver
 
 
   /**
+   * Search for shared or otherwise not listed calendars the user has access
+   *
+   * @param string Search string
+   * @param string Section/source to search
+   * @return array List of calendars
+   */
+  public function search_calendars($query, $source)
+  {
+    if (!kolab_storage::setup())
+      return array();
+
+    $this->calendars = array();
+    $imap = $this->rc->get_storage();
+
+    // find unsubscribed IMAP folders that have "event" type
+    if ($source == 'folders') {
+      $folders = array();
+      foreach ((array)kolab_storage::list_folders('', '*', 'event', false, $folderdata) as $foldername) {
+        // FIXME: only consider the last part of the folder path for searching?
+        $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
+        if (strpos($realname, $query) !== false &&
+            !kolab_storage::folder_is_subscribed($foldername, true) &&
+            $imap->folder_namespace($foldername) != 'other'
+          ) {
+          $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+        }
+      }
+
+      foreach ($folders as $folder) {
+        $calendar = new kolab_calendar($folder->name, $this->cal);
+        $this->calendars[$calendar->id] = $calendar;
+      }
+    }
+    else if ($source == 'users') {
+      // TODO: implement this
+    }
+
+    // don't list the birthday calendar
+    $this->rc->config->set('calendar_contact_birthdays', false);
+
+    return $this->list_calendars();
+  }
+
+
+  /**
    * Fetch a single event
    *
    * @see calendar_driver::get_event()
@@ -356,7 +430,7 @@ class kolab_driver extends calendar_driver
     }
 
     if ($cal) {
-      if ($storage = $this->calendars[$cal]) {
+      if ($storage = $this->get_calendar($cal)) {
         return $storage->get_event($id);
       }
     }
@@ -383,7 +457,7 @@ class kolab_driver extends calendar_driver
       return false;
 
     $cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
-    if ($storage = $this->calendars[$cid]) {
+    if ($storage = $this->get_calendar($cid)) {
       // handle attachments to add
       if (!empty($event['attachments'])) {
         foreach ($event['attachments'] as $idx => $attachment) {
@@ -425,7 +499,7 @@ class kolab_driver extends calendar_driver
    */
   public function move_event($event)
   {
-    if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) {
+    if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
       unset($ev['sequence']);
       return $this->update_event($event + $ev);
     }
@@ -441,7 +515,7 @@ class kolab_driver extends calendar_driver
    */
   public function resize_event($event)
   {
-    if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id']))) {
+    if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
       unset($ev['sequence']);
       return $this->update_event($event + $ev);
     }
@@ -463,7 +537,7 @@ class kolab_driver extends calendar_driver
     $success = false;
     $savemode = $event['_savemode'];
 
-    if (($storage = $this->calendars[$event['calendar']]) && ($event = $storage->get_event($event['id']))) {
+    if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
       $event['_savemode'] = $savemode;
       $savemode = 'all';
       $master = $event;
@@ -566,7 +640,7 @@ class kolab_driver extends calendar_driver
    */
   public function restore_event($event)
   {
-    if ($storage = $this->calendars[$event['calendar']]) {
+    if ($storage = $this->get_calendar($event['calendar'])) {
       if (!empty($_SESSION['calendar_restore_event_data']))
         $success = $storage->update_event($_SESSION['calendar_restore_event_data']);
       else
@@ -586,12 +660,12 @@ class kolab_driver extends calendar_driver
    */
   private function update_event($event)
   {
-    if (!($storage = $this->calendars[$event['calendar']]))
+    if (!($storage = $this->get_calendar($event['calendar'])))
       return false;
 
     // move event to another folder/calendar
     if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
-      if (!($fromcalendar = $this->calendars[$event['_fromcalendar']]))
+      if (!($fromcalendar = $this->get_calendar($event['_fromcalendar'])))
         return false;
 
       if ($event['_savemode'] != 'new') {
@@ -780,18 +854,19 @@ class kolab_driver extends calendar_driver
   {
     if ($calendars && is_string($calendars))
       $calendars = explode(',', $calendars);
+    else if (!$calendars)
+      $calendars = array_keys($this->calendars);
 
     $query = array();
     if ($modifiedsince)
       $query[] = array('changed', '>=', $modifiedsince);
 
     $events = $categories = array();
-    foreach (array_keys($this->calendars) as $cid) {
-      if ($calendars && !in_array($cid, $calendars))
-        continue;
-
-      $events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual, $query));
-      $categories += $this->calendars[$cid]->categories;
+    foreach ($calendars as $cid) {
+      if ($storage = $this->get_calendar($cid)) {
+        $events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query));
+        $categories += $storage->categories;
+      }
     }
 
     // add events from the address books birthday calendar
@@ -928,7 +1003,7 @@ class kolab_driver extends calendar_driver
    */
   public function list_attachments($event)
   {
-    if (!($storage = $this->calendars[$event['calendar']]))
+    if (!($storage = $this->get_calendar($event['calendar'])))
       return false;
 
     $event = $storage->get_event($event['id']);
@@ -941,7 +1016,7 @@ class kolab_driver extends calendar_driver
    */
   public function get_attachment($id, $event)
   {
-    if (!($storage = $this->calendars[$event['calendar']]))
+    if (!($storage = $this->get_calendar($event['calendar'])))
       return false;
 
     $event = $storage->get_event($event['id']);
@@ -963,7 +1038,7 @@ class kolab_driver extends calendar_driver
    */
   public function get_attachment_body($id, $event)
   {
-    if (!($cal = $this->calendars[$event['calendar']]))
+    if (!($cal = $this->get_calendar($event['calendar'])))
       return false;
 
     return $cal->storage->get_attachment($event['id'], $id);
@@ -1080,7 +1155,7 @@ class kolab_driver extends calendar_driver
     ignore_user_abort(true);
 
     $cal = get_input_value('source', RCUBE_INPUT_GPC);
-    if (!($cal = $this->calendars[$cal]))
+    if (!($cal = $this->get_calendar($cal)))
       return false;
 
     // trigger updates on folder
@@ -1275,7 +1350,7 @@ class kolab_driver extends calendar_driver
   public function calendar_acl_form()
   {
     $calid = get_input_value('_id', RCUBE_INPUT_GPC);
-    if ($calid && ($cal = $this->calendars[$calid])) {
+    if ($calid && ($cal = $this->get_calendar($calid))) {
       $folder = $cal->get_realname(); // UTF7
       $color  = $cal->get_color();
     }
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 117b924..69ba9c2 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -157,26 +157,7 @@ class calendar_ui
     foreach ((array)$calendars as $id => $prop) {
       if (!$prop['color'])
         continue;
-      $color = $prop['color'];
-      $class = 'cal-' . asciiwords($id, true);
-      $css .= "li.$class, #eventshow .$class { color: #$color }\n";
-      if ($mode != 1) {
-        if ($mode == 3) {
-          $css .= ".fc-event-$class .fc-event-bg {";
-          $css .= " opacity: 0.9;";
-          $css .= " filter: alpha(opacity=90);";
-        }
-        else {
-          $css .= ".fc-event-$class, ";
-          $css .= ".fc-event-$class .fc-event-inner {";
-        }
-        if (!$attrib['printmode'])
-          $css .= " background-color: #$color;";
-        if ($mode % 2 == 0)
-        $css .= " border-color: #$color;";
-        $css .= "}\n";
-      }
-      $css .= ".$class .handle { background-color: #$color; }";
+      $css .= $this->calendar_css_classes($id, $prop, $mode);
     }
     
     return html::tag('style', array('type' => 'text/css'), $css);
@@ -185,6 +166,35 @@ class calendar_ui
   /**
    *
    */
+  public function calendar_css_classes($id, $prop, $mode)
+  {
+    $color = $prop['color'];
+    $class = 'cal-' . asciiwords($id, true);
+    $css .= "li.$class, #eventshow .$class { color: #$color }\n";
+
+    if ($mode != 1) {
+      if ($mode == 3) {
+        $css .= ".fc-event-$class .fc-event-bg {";
+        $css .= " opacity: 0.9;";
+        $css .= " filter: alpha(opacity=90);";
+      }
+      else {
+        $css .= ".fc-event-$class, ";
+        $css .= ".fc-event-$class .fc-event-inner {";
+      }
+      if (!$attrib['printmode'])
+        $css .= " background-color: #$color;";
+      if ($mode % 2 == 0)
+      $css .= " border-color: #$color;";
+      $css .= "}\n";
+    }
+
+    return $css . ".$class .handle { background-color: #$color; }\n";
+  }
+
+  /**
+   *
+   */
   function calendar_list($attrib = array())
   {
     $html = '';
@@ -222,9 +232,9 @@ class calendar_ui
 
     return html::tag('ul', $attrib, $html, html::$common_attrib);
   }
-  
+
   /**
-   * Return html for a structured list <ul> for the mailbox tree
+   * Return html for a structured list <ul> for the folder tree
    */
   public function list_tree_html(&$node, &$data, &$jsenv, $attrib)
   {
@@ -255,7 +265,7 @@ class calendar_ui
   /**
    * Helper method to build a calendar list item (HTML content and js data)
    */
-  protected function calendar_list_item($id, $prop, &$jsenv)
+  public function calendar_list_item($id, $prop, &$jsenv)
   {
     unset($prop['user_id']);
     $prop['alarms']      = $this->cal->driver->alarms;
@@ -283,7 +293,7 @@ class calendar_ui
     if (!$attrib['activeonly'] || $prop['active']) {
       $content = html::div($class,
         ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
-        html::span('handle', ' ')) .
+        html::span(array('class' => 'handle', 'style' => "background-color: #" . ($prop['color'] ?: 'f00')), ' ')) .
         html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname'])
       );
     }
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 4621feb..b6e7ff2 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -86,6 +86,7 @@ $labels['nmonthsback'] = '$nr months back';
 $labels['showurl'] = 'Show calendar URL';
 $labels['showurldescription'] = 'Use the following address to access (read only) your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
 $labels['caldavurldescription'] = 'Copy this address to a <a href="http://en.wikipedia.org/wiki/CalDAV" target="_blank">CalDAV</a> client application (e.g. Evolution or Mozilla Thunderbird) to fully synchronize this specific calendar with your computer or mobile device.';
+$labels['calsearchresults'] = 'Additional Results';
 
 // agenda view
 $labels['listrange'] = 'Range to display:';
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 1ffaea8..01a4c3d 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -168,27 +168,23 @@ pre {
 	top: 68px;
 }
 
-#calendarslist li {
+#calendars .treelist li {
 	margin: 0;
 	position: relative;
 }
 
-#calendarslist li label {
-	display: block;
-}
-
-#calendarslist li div.folder,
-#calendarslist li div.calendar {
+#calendars .treelist li div.folder,
+#calendars .treelist li div.calendar {
 	position: relative;
 	height: 28px;
 }
 
-#calendarslist li div.virtual {
+#calendars .treelist li div.virtual {
 	height: 22px;
 }
 
 
-#calendarslist li span.calname {
+#calendars .treelist li span.calname {
 	display: block;
 	padding: 0px 30px 2px 2px;
 	position: absolute;
@@ -203,17 +199,17 @@ pre {
 	color: #004458;
 }
 
-#calendarslist li div.virtual > span.calname {
+#calendars .treelist li div.virtual > span.calname {
 	color: #aaa;
 	top: 4px;
 	left: 20px;
 }
 
-#calendarslist.flat li span.calname {
+#calendars .treelist.flat li span.calname {
 	left: 24px;
 }
 
-#calendarslist li span.handle {
+#calendars .treelist li span.handle {
 	display: inline-block;
 	position: absolute;
 	top: 8px;
@@ -229,66 +225,77 @@ pre {
 	box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
 }
 
-#calendarslist li input {
+#calendars .treelist li input {
 	position: absolute;
 	top: 5px;
 	left: 18px;
 }
 
-#calendarslist li div.treetoggle {
+#calendars .treelist li div.treetoggle {
 	top: 8px;
 }
 
-#calendarslist li.virtual div.treetoggle {
+#calendars .treelist li.virtual div.treetoggle {
 	top: 6px;
 }
 
-#calendarslist.flat li input {
+#calendars .treelist.flat li input {
 	left: 4px;
 }
 
-#calendarslist ul li div.folder,
-#calendarslist ul li div.calendar {
+#calendars .treelist ul li div.folder,
+#calendars .treelist ul li div.calendar {
 	margin-left: 16px;
 }
 
-#calendarslist ul ul li div.folder,
-#calendarslist ul ul li div.calendar {
+#calendars .treelist ul ul li div.folder,
+#calendars .treelist ul ul li div.calendar {
 	margin-left: 32px;
 }
 
-#calendarslist ul ul ul li div.folder,
-#calendarslist ul ul ul li div.calendar {
+#calendars .treelist ul ul ul li div.folder,
+#calendars .treelist ul ul ul li div.calendar {
 	margin-left: 48px;
 }
 
-#calendarslist li.selected {
+#calendars .treelist li.selected {
 	background-color: #c7e3ef;
 }
 
-#calendarslist li.selected > span.calname {
+#calendars .treelist li.selected > span.calname {
 	font-weight: bold;
 }
 
-#calendarslist div.readonly span.calname {
+#calendars .treelist div.readonly span.calname {
 	background-position: right -20px;
 }
-
-#calendarslist div.other span.calname {
+/*
+#calendars .treelist div.other span.calname {
 	background-position: right -38px;
 }
 
-#calendarslist div.other.readonly span.calname {
+#calendars .treelist div.other.readonly span.calname {
 	background-position: right -56px;
 }
 
-#calendarslist div.shared span.calname {
+#calendars .treelist div.shared span.calname {
 	background-position: right -74px;
 }
 
-#calendarslist div.shared.readonly span.calname {
+#calendars .treelist div.shared.readonly span.calname {
 	background-position: right -92px;
 }
+*/
+
+#calendars .searchresults {
+	background: #b0ccd7;
+	margin-top: 8px;
+}
+
+#calendars .searchresults .boxtitle {
+	background: none;
+	padding: 2px 8px 2px 8px;
+}
 
 #calfeedurl,
 #caldavurl {
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index c8dd59e..f28d496 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -247,7 +247,7 @@ class kolab_addressbook extends rcube_plugin
             $names = array();
             foreach ($folders as $folder) {
                 // create instance of rcube_contacts
-                $abook_id = kolab_storage::folder_id($folder->name);
+                $abook_id = kolab_storage::folder_id($folder->name, false);
                 $abook = new rcube_kolab_contacts($folder->name);
                 $this->sources[$abook_id] = $abook;
             }
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 9504e8f..55526fd 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -7,7 +7,7 @@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  * @author Aleksander Machniak <machniak at kolabsys.com>
  *
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2014, 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
@@ -37,6 +37,7 @@ class kolab_storage
 
     public static $version = '3.0';
     public static $last_error;
+    public static $encode_ids = false;
 
     private static $ready = false;
     private static $subscriptions;
@@ -226,13 +227,39 @@ class kolab_storage
     /**
      * Creates folder ID from folder name
      *
-     * @param string $folder Folder name (UTF7-IMAP)
-     *
+     * @param string  $folder Folder name (UTF7-IMAP)
+     * @param boolean $enc    Use lossless encoding
      * @return string Folder ID string
      */
-    public static function folder_id($folder)
+    public static function folder_id($folder, $enc = null)
     {
-        return asciiwords(strtr($folder, '/.-', '___'));
+        return $enc == true || ($enc === null && self::$encode_ids) ?
+            self::id_encode($folder) :
+            asciiwords(strtr($folder, '/.-', '___'));
+    }
+
+
+    /**
+     * Encode the given ID to a safe ascii representation
+     *
+     * @param string $id Arbitrary identifier string
+     *
+     * @return string Ascii representation
+     */
+    public static function id_encode($id)
+    {
+        return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
+    }
+
+    /**
+     * Convert the given identifier back to it's raw value
+     *
+     * @param string $id Ascii identifier
+     * @return string Raw identifier string
+     */
+    public static function id_decode($id)
+    {
+      return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
     }
 
 
@@ -665,11 +692,16 @@ class kolab_storage
         if (!$filter) {
             // Get ALL folders list, standard way
             if ($subscribed) {
-                return self::$imap->list_folders_subscribed($root, $mbox);
+                $folders = self::$imap->list_folders_subscribed($root, $mbox);
+                // add temporarily subscribed folders
+                if (is_array($_SESSION['kolab_subscribed_folders']))
+                    $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
             }
             else {
-                return self::$imap->list_folders($root, $mbox);
+                $folders = self::$imap->list_folders($root, $mbox);
             }
+
+            return $folders;
         }
 
         $prefix = $root . $mbox;
@@ -696,6 +728,10 @@ class kolab_storage
         // Get folders list
         if ($subscribed) {
             $folders = self::$imap->list_folders_subscribed($root, $mbox);
+
+            // add temporarily subscribed folders
+            if (is_array($_SESSION['kolab_subscribed_folders']))
+                $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
         }
         else {
             $folders = self::$imap->list_folders($root, $mbox);
@@ -903,17 +939,19 @@ class kolab_storage
      * Check subscription status of this folder
      *
      * @param string $folder Folder name
+     * @param boolean $temp  Include temporary/session subscriptions
      *
      * @return boolean True if subscribed, false if not
      */
-    public static function folder_is_subscribed($folder)
+    public static function folder_is_subscribed($folder, $temp = false)
     {
         if (self::$subscriptions === null) {
             self::setup();
             self::$subscriptions = self::$imap->list_folders_subscribed();
         }
 
-        return in_array($folder, self::$subscriptions);
+        return in_array($folder, self::$subscriptions) ||
+            ($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
     }
 
 
@@ -921,14 +959,25 @@ class kolab_storage
      * Change subscription status of this folder
      *
      * @param string $folder Folder name
+     * @param boolean $temp  Only subscribe temporarily for the current session
      *
      * @return True on success, false on error
      */
-    public static function folder_subscribe($folder)
+    public static function folder_subscribe($folder, $temp = false)
     {
         self::setup();
 
-        if (self::$imap->subscribe($folder)) {
+        // temporary/session subscription
+        if ($temp) {
+            if (self::folder_is_subscribed($folder)) {
+                return true;
+            }
+            else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
+                $_SESSION['kolab_subscribed_folders'][] = $folder;
+                return true;
+            }
+        }
+        else if (self::$imap->subscribe($folder)) {
             self::$subscriptions === null;
             return true;
         }
@@ -981,6 +1030,8 @@ class kolab_storage
      */
     public static function folder_activate($folder)
     {
+        // activation implies temporary subscription
+        self::folder_subscribe($folder, true);
         return self::set_state($folder, true);
     }
 
@@ -994,6 +1045,11 @@ class kolab_storage
      */
     public static function folder_deactivate($folder)
     {
+        // remove from temp subscriptions
+        if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
+            unset($_SESSION['kolab_subscribed_folders'][$i]);
+        }
+
         return self::set_state($folder, false);
     }
 


commit 00b1c7631bd8bf00061a77d9363464bcf03dfa49
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Mon May 12 20:47:47 2014 +0200

    Render calendar folders as a searchable treelist widget

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 116b490..2a8a8d2 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -51,6 +51,7 @@ function rcube_calendar_ui(settings)
     var ignore_click = false;
     var event_defaults = { free_busy:'busy', alarms:'' };
     var event_attendees = [];
+    var calendars_list;
     var attendees_list;
     var resources_list;
     var resources_treelist;
@@ -2658,15 +2659,10 @@ function rcube_calendar_ui(settings)
     // mark the given calendar folder as selected
     this.select_calendar = function(id)
     {
-      var prefix = 'rcmlical';
-
-      $(rcmail.gui_objects.calendarslist).find('li.selected')
-        .removeClass('selected').addClass('unfocused');
-      $('#' + prefix + id, rcmail.gui_objects.calendarslist)
-        .removeClass('unfocused').addClass('selected');
+      calendars_list.select(id);
 
       // trigger event hook
-      rcmail.triggerEvent('selectfolder', { folder:name, prefix:prefix });
+      rcmail.triggerEvent('selectfolder', { folder:id, prefix:'rcmlical' });
 
       this.selected_calendar = id;
     };
@@ -2703,42 +2699,58 @@ function rcube_calendar_ui(settings)
         event_sources.push(this.calendars[id]);
       }
 
-      // init event handler on calendar list checkbox
-      if ((li = rcube_find_object('rcmlical' + id))) {
-        $('#'+li.id+' input').click(function(e){
-          var id = $(this).data('id');
-          if (me.calendars[id]) {  // add or remove event source on click
-            var action;
-            if (this.checked) {
-              action = 'addEventSource';
-              me.calendars[id].active = true;
-            }
-            else {
-              action = 'removeEventSource';
-              me.calendars[id].active = false;
-            }
-            
-            // add/remove event source
-            fc.fullCalendar(action, me.calendars[id]);
-            rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
-          }
-        }).data('id', id).get(0).checked = active;
-        
-        $(li).click(function(e){
-          me.select_calendar($(this).data('id'));
-          rcmail.enable_command('calendar-edit', true);
-          rcmail.enable_command('calendar-remove', 'calendar-showurl', true);
-        })
-        .dblclick(function(){ me.calendar_edit_dialog(me.calendars[me.selected_calendar]); })
-        .data('id', id);
-      }
-      
+      // check active calendars
+      $('#rcmlical'+id+' > .calendar input').data('id', id).get(0).checked = active;
+
       if (!cal.readonly && !this.selected_calendar) {
         this.selected_calendar = id;
         rcmail.enable_command('addevent', true);
       }
     }
-    
+
+    // initialize treelist widget that controls the calendars list
+    calendars_list = new rcube_treelist_widget(rcmail.gui_objects.calendarslist, {
+      id_prefix: 'rcmlical',
+      selectable: true,
+      searchbox: '#calendarlistsearch'
+    });
+    calendars_list.addEventListener('select', function(node){
+      me.select_calendar(node.id);
+      rcmail.enable_command('calendar-edit', 'calendar-showurl', true);
+      rcmail.enable_command('calendar-remove', !me.calendars[node.id].readonly);
+    });
+    calendars_list.addEventListener('search', function(search){
+      console.log(search);
+    });
+
+    // init (delegate) event handler on calendar list checkboxes
+    $(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e){
+      var id = $(this).data('id');
+      if (me.calendars[id]) {  // add or remove event source on click
+        var action;
+        if (this.checked) {
+          action = 'addEventSource';
+          me.calendars[id].active = true;
+        }
+        else {
+          action = 'removeEventSource';
+          me.calendars[id].active = false;
+        }
+
+        // add/remove event source
+        fc.fullCalendar(action, me.calendars[id]);
+        rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
+
+        e.stopPropagation();
+      }
+    });
+
+    // register dbl-click handler to open calendar edit dialog
+    $(rcmail.gui_objects.calendarslist).on('dblclick', ':not(.virtual) > .calname', function(e){
+      var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
+      me.calendar_edit_dialog(me.calendars[id]);
+    });
+
     // select default calendar
     if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly)
       this.selected_calendar = settings.default_calendar;
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 28eb8ba..974a3d3 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -88,16 +88,16 @@ class kolab_driver extends calendar_driver
     return $this->calendars;
   }
 
-
   /**
    * Get a list of available calendars from this source
    *
    * @param bool $active   Return only active calendars
    * @param bool $personal Return only personal calendars
+   * @param object $tree   Reference to hierarchical folder tree object
    *
    * @return array List of calendars
    */
-  public function list_calendars($active = false, $personal = false)
+  public function list_calendars($active = false, $personal = false, &$tree = null)
   {
     // attempt to create a default calendar for this user
     if (!$this->has_writeable) {
@@ -112,7 +112,7 @@ class kolab_driver extends calendar_driver
 
     // include virtual folders for a full folder tree
     if (!$active && !$personal && !$this->rc->output->ajax_call && in_array($this->rc->action, array('index','')))
-      $folders = kolab_storage::folder_hierarchy($folders);
+      $folders = kolab_storage::folder_hierarchy($folders, $tree);
 
     foreach ($folders as $id => $cal) {
       $fullname = $cal->get_name();
@@ -124,6 +124,7 @@ class kolab_driver extends calendar_driver
           'id' => $cal->id,
           'name' => $fullname,
           'listname' => $listname,
+          'editname' => $cal->get_foldername(),
           'virtual' => true,
           'readonly' => true,
         );
@@ -1106,6 +1107,11 @@ class kolab_driver extends calendar_driver
    */
   public function calendar_form($action, $calendar, $formfields)
   {
+    // show default dialog for birthday calendar
+    if ($calendar['id'] == self::BIRTHDAY_CALENDAR_ID) {
+      return parent::calendar_form($action, $calendar, $formfields);
+    }
+
     if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
       $folder = $cal->get_realname(); // UTF7
       $color  = $cal->get_color();
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 984ce03..117b924 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -187,45 +187,108 @@ class calendar_ui
    */
   function calendar_list($attrib = array())
   {
-    $calendars = $this->cal->driver->list_calendars();
+    $html = '';
+    $jsenv = array();
+    $calendars = $this->cal->driver->list_calendars(false, false, $tree);
+
+    // walk folder tree
+    if (is_object($tree)) {
+      $html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib);
+
+      // append birthdays calendar which isn't part of $tree
+      if ($bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID]) {
+        $calendars = array(calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal);
+      }
+      else {
+        $calendars = array();  // clear array for flat listing
+      }
+    }
+    else {
+      // fall-back to flat folder listing
+      $attrib['class'] .= ' flat';
+    }
 
-    $li = '';
     foreach ((array)$calendars as $id => $prop) {
       if ($attrib['activeonly'] && !$prop['active'])
         continue;
-      
-      unset($prop['user_id']);
-      $prop['alarms'] = $this->cal->driver->alarms;
-      $prop['attendees'] = $this->cal->driver->attendees;
-      $prop['freebusy'] = $this->cal->driver->freebusy;
-      $prop['attachments'] = $this->cal->driver->attachments;
-      $prop['undelete'] = $this->cal->driver->undelete;
-      $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
-
-      if (!$prop['virtual'])
-        $jsenv[$id] = $prop;
-
-      $html_id = html_identifier($id);
-      $class = 'cal-'  . asciiwords($id, true);
-      $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
-
-      if ($prop['virtual'])
-        $class .= ' virtual';
-      else if ($prop['readonly'])
-        $class .= ' readonly';
-      if ($prop['class_name'])
-        $class .= ' '.$prop['class_name'];
-
-      $li .= html::tag('li', array('id' => 'rcmlical' . $html_id, 'class' => $class),
-        ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
-        html::span('handle', ' ')) .
-        html::span(array('class' => 'calname', 'title' => $title), $prop['listname']));
+
+      $html .= html::tag('li', array('id' => 'rcmlical' . rcube_utils::html_identifier($id)),
+        $content = $this->calendar_list_item($id, $prop, $jsenv)
+      );
     }
 
     $this->rc->output->set_env('calendars', $jsenv);
     $this->rc->output->add_gui_object('calendarslist', $attrib['id']);
 
-    return html::tag('ul', $attrib, $li, html::$common_attrib);
+    return html::tag('ul', $attrib, $html, html::$common_attrib);
+  }
+  
+  /**
+   * Return html for a structured list <ul> for the mailbox tree
+   */
+  public function list_tree_html(&$node, &$data, &$jsenv, $attrib)
+  {
+    $out = '';
+    foreach ($node->children as $folder) {
+      $id = $folder->id;
+      $prop = $data[$id];
+
+      $content = $this->calendar_list_item($id, $prop, $jsenv);
+
+      if (!empty($folder->children)) {
+        $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
+          $this->list_tree_html($folder, $data, $jsenv, $attrib));
+      }
+
+      if (strlen($content)) {
+        $out .= html::tag('li', array(
+            'id' => 'rcmlical' . rcube_utils::html_identifier($id),
+            'class' => $prop['virtual'] ? 'virtual' : '',
+          ),
+          $content);
+      }
+    }
+
+    return $out;
+  }
+
+  /**
+   * Helper method to build a calendar list item (HTML content and js data)
+   */
+  protected function calendar_list_item($id, $prop, &$jsenv)
+  {
+    unset($prop['user_id']);
+    $prop['alarms']      = $this->cal->driver->alarms;
+    $prop['attendees']   = $this->cal->driver->attendees;
+    $prop['freebusy']    = $this->cal->driver->freebusy;
+    $prop['attachments'] = $this->cal->driver->attachments;
+    $prop['undelete']    = $this->cal->driver->undelete;
+    $prop['feedurl']     = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
+
+    if (!$prop['virtual'])
+      $jsenv[$id] = $prop;
+
+    $class = 'calendar cal-'  . asciiwords($id, true);
+    $title = $prop['name'] != $prop['listname'] ? html_entity_decode($prop['name'], ENT_COMPAT, RCMAIL_CHARSET) : '';
+    $is_collapsed = false; // TODO: determine this somehow?
+
+    if ($prop['virtual'])
+      $class = 'folder virtual';
+    else if ($prop['readonly'])
+      $class .= ' readonly';
+    if ($prop['class_name'])
+      $class .= ' '.$prop['class_name'];
+
+    $content = '';
+    if (!$attrib['activeonly'] || $prop['active']) {
+      $content = html::div($class,
+        ($prop['virtual'] ? '' : html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
+        html::span('handle', ' ')) .
+        html::span(array('class' => 'calname', 'title' => $title), $prop['editname'] ? Q($prop['editname']) : $prop['listname'])
+      );
+    }
+
+    return $content;
   }
 
   /**
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index f47032f..1ffaea8 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -164,45 +164,64 @@ pre {
 	right: 0;
 }
 
+#calendars .scroller {
+	top: 68px;
+}
+
 #calendarslist li {
 	margin: 0;
-	height: 20px;
-	padding: 6px 8px 2px;
-	display: block;
 	position: relative;
 }
 
-#calendarslist li.virtual {
-	height: 12px;
-}
-
 #calendarslist li label {
 	display: block;
 }
 
+#calendarslist li div.folder,
+#calendarslist li div.calendar {
+	position: relative;
+	height: 28px;
+}
+
+#calendarslist li div.virtual {
+	height: 22px;
+}
+
+
 #calendarslist li span.calname {
 	display: block;
+	padding: 0px 30px 2px 2px;
 	position: absolute;
-	top: 6px;
-	left: 26px;
-	right: 24px;
+	top: 7px;
+	left: 38px;
+	right: 22px;
 	cursor: default;
 	background: url(images/calendars.png) right 20px no-repeat;
-	padding-bottom: 2px;
-	padding-right: 30px;
 	overflow: hidden;
 	text-overflow: ellipsis;
 	white-space: nowrap;
 	color: #004458;
 }
 
+#calendarslist li div.virtual > span.calname {
+	color: #aaa;
+	top: 4px;
+	left: 20px;
+}
+
+#calendarslist.flat li span.calname {
+	left: 24px;
+}
+
 #calendarslist li span.handle {
 	display: inline-block;
+	position: absolute;
+	top: 8px;
+	right: 6px;
 	padding: 0;
-	border-radius: 7px;
-	margin-right: 6px;
 	width: 10px;
 	height: 10px;
+	border-radius: 7px;
 	font-size: 0.8em;
 	border: 1px solid rgba(0, 0, 0, 0.5);
 	-webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
@@ -212,43 +231,65 @@ pre {
 
 #calendarslist li input {
 	position: absolute;
-	top: 4px;
-	right: 5px;
+	top: 5px;
+	left: 18px;
+}
+
+#calendarslist li div.treetoggle {
+	top: 8px;
+}
+
+#calendarslist li.virtual div.treetoggle {
+	top: 6px;
+}
+
+#calendarslist.flat li input {
+	left: 4px;
+}
+
+#calendarslist ul li div.folder,
+#calendarslist ul li div.calendar {
+	margin-left: 16px;
+}
+
+#calendarslist ul ul li div.folder,
+#calendarslist ul ul li div.calendar {
+	margin-left: 32px;
+}
+
+#calendarslist ul ul ul li div.folder,
+#calendarslist ul ul ul li div.calendar {
+	margin-left: 48px;
 }
 
 #calendarslist li.selected {
 	background-color: #c7e3ef;
 }
 
-#calendarslist li.selected span.calname {
+#calendarslist li.selected > span.calname {
 	font-weight: bold;
 }
 
-#calendarslist li.readonly span.calname {
+#calendarslist div.readonly span.calname {
 	background-position: right -20px;
 }
 
-#calendarslist li.other span.calname {
+#calendarslist div.other span.calname {
 	background-position: right -38px;
 }
 
-#calendarslist li.other.readonly span.calname {
+#calendarslist div.other.readonly span.calname {
 	background-position: right -56px;
 }
 
-#calendarslist li.shared span.calname {
+#calendarslist div.shared span.calname {
 	background-position: right -74px;
 }
 
-#calendarslist li.shared.readonly span.calname {
+#calendarslist div.shared.readonly span.calname {
 	background-position: right -92px;
 }
 
-#calendarslist li.virtual span.calname {
-	color: #aaa;
-	top: 2px;
-}
-
 #calfeedurl,
 #caldavurl {
 	width: 98%;
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index debe7ec..0842cf0 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -23,8 +23,15 @@
 
 		<div id="calendars" class="uibox listbox" style="visibility:hidden">
 			<h2 class="boxtitle"><roundcube:label name="calendar.calendars" /></h2>
+			<div class="listsearchbox">
+				<div class="searchbox">
+					<input type="text" name="q" id="calendarlistsearch" />
+					<a class="iconbutton searchicon"></a>
+					<roundcube:button command="reset-listsearch" id="calendarlistsearch-reset" class="iconbutton reset" title="resetsearch" content="x" />
+				</div>
+			</div>
 			<div class="scroller withfooter">
-			<roundcube:object name="plugin.calendar_list" id="calendarslist" class="listing" />
+			<roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist listing" />
 			</div>
 			<div class="boxfooter">
 				<roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="calendaroptionslink" id="calendaroptionsmenulink" type="link" title="moreactions" class="listbutton groupactions" onclick="UI.show_popup('calendaroptionsmenu', undefined, { above:true });return false" innerClass="inner" content="⚙" />
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 52df016..9504e8f 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -756,43 +756,54 @@ class kolab_storage
      * Check the folder tree and add the missing parents as virtual folders
      *
      * @param array $folders Folders list
+     * @param object $tree   Reference to the root node of the folder tree
      *
-     * @return array Folders list
+     * @return array Flat folders list
      */
-    public static function folder_hierarchy($folders)
+    public static function folder_hierarchy($folders, &$tree)
     {
         $_folders = array();
-        $existing = array_map(function($folder){ return $folder->get_name(); }, $folders);
         $delim    = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
+        $tree     = new virtual_kolab_storage_folder('', '<root>', '');  // create tree root
+        $refs     = array('' => $tree);
 
         foreach ($folders as $idx => $folder) {
             $path = explode($delim, $folder->name);
             array_pop($path);
+            $folder->parent = join($delim, $path);
+            $folder->children = array();  // reset list
 
             // skip top folders or ones with a custom displayname
-            if (count($path) <= 1 || kolab_storage::custom_displayname($folder->name)) {
+            if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
+                $tree->children[] = $folder;
             }
             else {
                 $parents = array();
+                $depth = $folder->get_namespace() == 'personal' ? 1 : 2;
 
-                while (count($path) > 1 && ($parent = join($delim, $path))) {
+                while (count($path) >= $depth && ($parent = join($delim, $path))) {
+                    array_pop($path);
                     $name = kolab_storage::object_name($parent, $folder->get_namespace());
-                    if (!in_array($name, $existing)) {
-                        $parents[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace());
-                        $existing[] = $name;
+                    if (!$refs[$parent]) {
+                        $refs[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace(), join($delim, $path));
+                        $parents[] = $refs[$parent];
                     }
-
-                    array_pop($path);
                 }
 
                 if (!empty($parents)) {
-                    $parents = array_reverse(array_values($parents));
+                    $parents = array_reverse($parents);
                     foreach ($parents as $parent) {
+                        $parent_node = $refs[$parent->parent] ?: $tree;
+                        $parent_node->children[] = $parent;
                         $_folders[] = $parent;
                     }
                 }
+
+                $parent_node = $refs[$folder->parent] ?: $tree;
+                $parent_node->children[] = $folder;
             }
 
+            $refs[$folder->name] = $folder;
             $_folders[] = $folder;
             unset($folders[$idx]);
         }
@@ -1164,13 +1175,18 @@ class virtual_kolab_storage_folder
     public $id;
     public $name;
     public $namespace;
+    public $parent = '';
+    public $children = array();
     public $virtual = true;
+    protected $displayname;
 
-    public function __construct($realname, $name, $ns)
+    public function __construct($name, $dispname, $ns, $parent = '')
     {
-        $this->id        = kolab_storage::folder_id($realname);
+        $this->id        = kolab_storage::folder_id($name);
         $this->name      = $name;
         $this->namespace = $ns;
+        $this->parent    = $parent;
+        $this->displayname = $dispname;
     }
 
     public function get_namespace()
@@ -1181,6 +1197,12 @@ class virtual_kolab_storage_folder
     public function get_name()
     {
         // this is already kolab_storage::object_name() result
-        return $this->name;
+        return $this->displayname;
+    }
+
+    public function get_foldername()
+    {
+        $parts = explode('/', $this->name);
+        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
     }
 }
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index d625cc5..7db493d 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -47,6 +47,12 @@ class kolab_storage_folder
      * @var object
      */
     public $cache;
+    
+    /**
+     * List of direct child folders
+     * @var array
+     */
+    public $children = array();
 
     private $type_annotation;
     private $namespace;
@@ -218,6 +224,18 @@ class kolab_storage_folder
 
 
     /**
+     * Getter for the top-end folder name (not the entire path)
+     *
+     * @return string Name of this folder
+     */
+    public function get_foldername()
+    {
+        $parts = explode('/', $this->name);
+        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+    }
+
+
+    /**
      * Get the color value stored in metadata
      *
      * @param string Default color value to return if not set





More information about the commits mailing list