diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index 408e4d7f..ac5d3086 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -1,957 +1,957 @@
 <?php
 
 /**
  * Kolab calendar storage class
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2012-2015, Kolab Systems AG <contact@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_calendar extends kolab_storage_folder_api
 {
     public $ready         = false;
     public $rights        = 'lrs';
     public $editable      = false;
     public $attachments   = true;
     public $alarms        = false;
     public $history       = false;
     public $subscriptions = true;
     public $categories    = [];
     public $storage;
 
     public $type = 'event';
 
     protected $cal;
     protected $events = [];
     protected $search_fields = ['title', 'description', 'location', 'attendees', 'categories'];
 
     /**
      * Factory method to instantiate a kolab_calendar object
      *
      * @param string  Calendar ID (encoded IMAP folder name)
      * @param object  Calendar plugin object
      *
      * @return kolab_calendar Self 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)
             || !empty($info['noselect'])
             || strpos(kolab_storage::folder_type($imap_folder), 'event') !== 0
         ) {
             return new kolab_user_calendar($imap_folder, $calendar);
         }
 
         return new kolab_calendar($imap_folder, $calendar);
     }
 
     /**
      * Default constructor
      */
     public function __construct($imap_folder, $calendar)
     {
         $this->cal  = $calendar;
         $this->imap = $calendar->rc->get_storage();
         $this->name = $imap_folder;
 
         // ID is derrived from folder name
         $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->name);
         $this->ready   = $this->storage && $this->storage->valid;
 
         // Set writeable and alarms flags according to folder permissions
         if ($this->ready) {
             if ($this->storage->get_namespace() == 'personal') {
                 $this->editable = true;
                 $this->rights = 'lrswikxteav';
                 $this->alarms = true;
             }
             else {
                 $rights = $this->storage->get_myrights();
                 if ($rights && !PEAR::isError($rights)) {
                     $this->rights = $rights;
                     if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) {
                         $this->editable = strpos($rights, 'i');;
                     }
                 }
             }
 
             // user-specific alarms settings win
             $prefs = $this->cal->rc->config->get('kolab_calendars', []);
             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'];
             }
         }
 
         $this->default = $this->storage->default;
         $this->subtype = $this->storage->subtype;
     }
 
     /**
      * Getter for the IMAP folder name
      *
      * @return string Name of the IMAP folder
      */
     public function get_realname()
     {
         return $this->name;
     }
 
     /**
      *
      */
     public function get_title()
     {
         return null;
     }
 
     /**
      * Return color to display this calendar
      */
     public function get_color($default = null)
     {
         // color is defined in folder METADATA
         if ($color = $this->storage->get_color()) {
             return $color;
         }
 
         // calendar color is stored in user prefs (temporary solution)
         $prefs = $this->cal->rc->config->get('kolab_calendars', []);
 
         if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color'])) {
             return $prefs[$this->id]['color'];
         }
 
         return $default ?: 'cc0000';
     }
 
     /**
      * Compose an URL for CalDAV access to this calendar (if configured)
      */
     public function get_caldav_url()
     {
         if ($template = $this->cal->rc->config->get('calendar_caldav_url', null)) {
             return strtr($template, [
                     '%h' => $_SERVER['HTTP_HOST'],
                     '%u' => urlencode($this->cal->rc->get_user_name()),
                     '%i' => urlencode($this->storage->get_uid()),
                     '%n' => urlencode($this->name),
             ]);
         }
 
         return false;
     }
 
     /**
      * 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)
     {
         // remove our occurrence identifier if it's there
         $master_id = preg_replace('/-\d{8}(T\d{6})?$/', '', $id);
 
         // directly access storage object
         if (empty($this->events[$id]) && $master_id == $id && ($record = $this->storage->get_object($id))) {
             $this->events[$id] = $this->_to_driver_event($record, true);
         }
 
         // maybe a recurring instance is requested
         if (empty($this->events[$id]) && $master_id != $id) {
             $instance_id = substr($id, strlen($master_id) + 1);
 
             if ($record = $this->storage->get_object($master_id)) {
                 $master = $this->_to_driver_event($record);
             }
 
             if ($master) {
                 // check for match in top-level exceptions (aka loose single occurrences)
                 if (!empty($master['_formatobj']) && ($instance = $master['_formatobj']->get_instance($instance_id))) {
                     $this->events[$id] = $this->_to_driver_event($instance, false, true, $master);
                 }
                 // check for match on the first instance already
                 else if (!empty($master['_instance']) && $master['_instance'] == $instance_id) {
                     $this->events[$id] = $master;
                 }
                 else if (!empty($master['recurrence'])) {
                     $start_date = $master['start'];
                     // For performance reasons we'll get only the specific instance
                     if (($date = substr($id, strlen($master_id) + 1, 8)) && strlen($date) == 8 && is_numeric($date)) {
                         $start_date = new DateTime($date . 'T000000', $master['start']->getTimezone());
                     }
 
                     $this->get_recurring_events($record, $start_date, null, $id, 1);
                 }
             }
         }
 
         return $this->events[$id];
     }
 
     /**
      * Get attachment body
      * @see calendar_driver::get_attachment_body()
      */
     public function get_attachment_body($id, $event)
     {
         if (!$this->ready) {
             return false;
         }
 
         $data = $this->storage->get_attachment($event['id'], $id);
 
         if ($data == null) {
             // try again with master UID
             $uid = preg_replace('/-\d+(T\d{6})?$/', '', $event['id']);
             if ($uid != $event['id']) {
                 $data = $this->storage->get_attachment($uid, $id);
             }
         }
 
         return $data;
     }
 
     /**
      * @param  int    Event's new start (unix timestamp)
      * @param  int    Event's new end (unix timestamp)
      * @param  string Search query (optional)
      * @param  bool   Include virtual events (optional)
      * @param  array  Additional parameters to query storage
      * @param  array  Additional query to filter events
      *
      * @return array A list of event records
      */
     public function list_events($start, $end, $search = null, $virtual = 1, $query = [], $filter_query = null)
     {
         // convert to DateTime for comparisons
         // #5190: make the range a little bit wider
         // to workaround possible timezone differences
         try {
             $start = new DateTime('@' . ($start - 12 * 3600));
         }
         catch (Exception $e) {
             $start = new DateTime('@0');
         }
         try {
             $end = new DateTime('@' . ($end + 12 * 3600));
         }
         catch (Exception $e) {
             $end = new DateTime('today +10 years');
         }
 
         // get email addresses of the current user
         $user_emails = $this->cal->get_user_emails();
 
         // query Kolab storage
         $query[] = ['dtstart', '<=', $end];
         $query[] = ['dtend',   '>=', $start];
 
         if (is_array($filter_query)) {
             $query = array_merge($query, $filter_query);
         }
 
         $words = [];
         $partstat_exclude = [];
         $events = [];
 
         if (!empty($search)) {
             $search = mb_strtolower($search);
             $words  = rcube_utils::tokenize_string($search, 1);
             foreach (rcube_utils::normalize_string($search, true) as $word) {
                 $query[] = ['words', 'LIKE', $word];
             }
         }
 
         // set partstat filter to skip pending and declined invitations
         if (empty($filter_query)
             && $this->cal->rc->config->get('kolab_invitation_calendars')
             && $this->get_namespace() != 'other'
         ) {
             $partstat_exclude = ['NEEDS-ACTION', 'DECLINED'];
         }
 
         foreach ($this->storage->select($query) as $record) {
             $event = $this->_to_driver_event($record, !$virtual, false);
 
             // remember seen categories
             if (!empty($event['categories'])) {
                 $cat = is_array($event['categories']) ? $event['categories'][0] : $event['categories'];
                 $this->categories[$cat]++;
             }
 
             // list events in requested time window
             if ($event['start'] <= $end && $event['end'] >= $start) {
                 unset($event['_attendees']);
                 $add = true;
 
                 // skip the first instance of a recurring event if listed in exdate
                 if ($virtual && !empty($event['recurrence']['EXDATE'])) {
                     $event_date = $event['start']->format('Ymd');
                     $event_tz   = $event['start']->getTimezone();
 
                     foreach ((array) $event['recurrence']['EXDATE'] as $exdate) {
                         $ex = clone $exdate;
                         $ex->setTimezone($event_tz);
 
                         if ($ex->format('Ymd') == $event_date) {
                             $add = false;
                             break;
                         }
                     }
                 }
 
                 // find and merge exception for the first instance
                 if ($virtual && !empty($event['recurrence']) && !empty($event['recurrence']['EXCEPTIONS'])) {
                     foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
                         if ($event['_instance'] == $exception['_instance']) {
                             unset($exception['calendar'], $exception['className'], $exception['_folder_id']);
                             // clone date objects from main event before adjusting them with exception data
                             if (is_object($event['start'])) {
                                 $event['start'] = clone $record['start'];
                             }
                             if (is_object($event['end'])) {
                                 $event['end'] = clone $record['end'];
                             }
                             kolab_driver::merge_exception_data($event, $exception);
                         }
                     }
                 }
 
                 if ($add) {
                     $events[] = $event;
                 }
             }
 
             // resolve recurring events
             if (!empty($record['recurrence']) && $virtual == 1) {
                 $events = array_merge($events, $this->get_recurring_events($record, $start, $end));
             }
             // add top-level exceptions (aka loose single occurrences)
             else if (!empty($record['exceptions'])) {
                 foreach ($record['exceptions'] as $ex) {
                     $component = $this->_to_driver_event($ex, false, false, $record);
                     if ($component['start'] <= $end && $component['end'] >= $start) {
                         $events[] = $component;
                     }
                 }
             }
         }
 
         // post-filter all events by fulltext search and partstat values
         $me = $this;
         $events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
             // fulltext search
             if (count($words)) {
                 $hits = 0;
                 foreach ($words as $word) {
                     $hits += $me->fulltext_match($event, $word, false);
                 }
                 if ($hits < count($words)) {
                     return false;
                 }
             }
 
             // partstat filter
             if (count($partstat_exclude) && !empty($event['attendees'])) {
                 foreach ($event['attendees'] as $attendee) {
                     if (
                         in_array($attendee['email'], $user_emails)
-                        && in_array($attendee['status'], $partstat_exclude)
+                        && in_array($attendee['status'] ?? null, $partstat_exclude)
                     ) {
                         return false;
                     }
                 }
             }
 
             return true;
         });
 
         // Apply event-to-mail relations
         $config = kolab_storage_config::get_instance();
         $config->apply_links($events);
 
         // avoid session race conditions that will loose temporary subscriptions
         $this->cal->rc->session->nowrite = true;
 
         return $events;
     }
 
     /**
      * Get number of events in the given calendar
      *
      * @param int   Date range start (unix timestamp)
      * @param int   Date range end (unix timestamp)
      * @param array Additional query to filter events
      *
      * @return int Count
      */
     public function count_events($start, $end = null, $filter_query = null)
     {
         // convert to DateTime for comparisons
         try {
             $start = new DateTime('@'.$start);
         }
         catch (Exception $e) {
             $start = new DateTime('@0');
         }
         if ($end) {
             try {
                 $end = new DateTime('@'.$end);
             }
             catch (Exception $e) {
                 $end = null;
             }
         }
 
         // query Kolab storage
         $query[] = ['dtend',   '>=', $start];
 
         if ($end) {
             $query[] = ['dtstart', '<=', $end];
         }
 
         // add query to exclude pending/declined invitations
         if (empty($filter_query)) {
             foreach ($this->cal->get_user_emails() as $email) {
                 $query[] = ['tags', '!=', 'x-partstat:' . $email . ':needs-action'];
                 $query[] = ['tags', '!=', 'x-partstat:' . $email . ':declined'];
             }
         }
         else if (is_array($filter_query)) {
             $query = array_merge($query, $filter_query);
         }
 
         // we rely the Kolab storage query (no post-filtering)
         return $this->storage->count($query);
     }
 
     /**
      * Create a new event record
      *
      * @see calendar_driver::new_event()
      *
      * @return array|false The created record ID on success, False on error
      */
     public function insert_event($event)
     {
         if (!is_array($event)) {
             return false;
         }
 
         // email links are stored separately
         $links = !empty($event['links']) ? $event['links'] : [];
         unset($event['links']);
 
         //generate new event from RC input
         $object = $this->_from_driver_event($event);
         $saved  = $this->storage->save($object, 'event');
 
         if (!$saved) {
             rcube::raise_error([
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Error saving event object to Kolab server"
                 ],
                 true, false
             );
             $saved = false;
         }
         else {
             // save links in configuration.relation object
             if ($this->save_links($event['uid'], $links)) {
                 $object['links'] = $links;
             }
 
             $this->events = [$event['uid'] => $this->_to_driver_event($object, true)];
         }
 
         return $saved;
     }
 
     /**
      * Update a specific event record
      *
      * @see calendar_driver::new_event()
      *
      * @return bool True on success, False on error
      */
     public function update_event($event, $exception_id = null)
     {
         $updated = false;
         $old = $this->storage->get_object(!empty($event['uid']) ? $event['uid'] : $event['id']);
 
         if (!$old || PEAR::isError($old)) {
             return false;
         }
 
         // email links are stored separately
         $links = !empty($event['links']) ? $event['links'] : [];
         unset($event['links']);
 
         $object = $this->_from_driver_event($event, $old);
         $saved  = $this->storage->save($object, 'event', $old['uid']);
 
         if (!$saved) {
             rcube::raise_error([
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Error saving event object to Kolab server"
                 ],
                 true, false
             );
         }
         else {
             // save links in configuration.relation object
             if ($this->save_links($event['uid'], $links)) {
                 $object['links'] = $links;
             }
 
             $updated = true;
             $this->events = [$event['uid'] => $this->_to_driver_event($object, true)];
 
             // refresh local cache with recurring instances
             if ($exception_id) {
                 $this->get_recurring_events($object, $event['start'], $event['end'], $exception_id);
             }
         }
 
         return $updated;
     }
 
     /**
      * Delete an event record
      *
      * @see calendar_driver::remove_event()
      *
      * @return bool True on success, False on error
      */
     public function delete_event($event, $force = true)
     {
         $deleted = $this->storage->delete(!empty($event['uid']) ? $event['uid'] : $event['id'], $force);
 
         if (!$deleted) {
             rcube::raise_error([
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => sprintf("Error deleting event object '%s' from Kolab server", $event['id'])
                 ],
                 true, false
             );
         }
 
         return $deleted;
     }
 
     /**
      * Restore deleted event record
      *
      * @see calendar_driver::undelete_event()
      *
      * @return bool True on success, False on error
      */
     public function restore_event($event)
     {
         // Make sure this is not an instance identifier
         $uid = preg_replace('/-\d{8}(T\d{6})?$/', '', $event['id']);
 
         if ($this->storage->undelete($uid)) {
             return true;
         }
 
         rcube::raise_error([
                 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                 'message' => sprintf("Error undeleting the event object '%s' from the Kolab server", $event['id'])
             ],
             true, false
         );
 
         return false;
     }
 
     /**
      * Find messages linked with an event
      */
     protected function get_links($uid)
     {
         $storage = kolab_storage_config::get_instance();
         return $storage->get_object_links($uid);
     }
 
     /**
      *
      */
     protected function save_links($uid, $links)
     {
         $storage = kolab_storage_config::get_instance();
         return $storage->save_object_links($uid, (array) $links);
     }
 
     /**
      * Create instances of a recurring event
      *
      * @param array    $event    Hash array with event properties
      * @param DateTime $start    Start date of the recurrence window
      * @param DateTime $end      End date of the recurrence window
      * @param string   $event_id ID of a specific recurring event instance
      * @param int      $limit    Max. number of instances to return
      *
      * @return array List of recurring event instances
      */
     public function get_recurring_events($event, $start, $end = null, $event_id = null, $limit = null)
     {
         if (empty($event['_formatobj'])) {
             $rec    = $this->storage->get_object(!empty($event['uid']) ? $event['uid'] : $event['id']);
             $object = $rec['_formatobj'];
         }
         else {
             $object = $event['_formatobj'];
         }
 
         if (!is_object($object)) {
             return [];
         }
 
         // determine a reasonable end date if none given
         if (!$end) {
             $end = clone $event['start'];
             $end->add(new DateInterval('P100Y'));
         }
 
         // read recurrence exceptions first
         $events = [];
         $exdata = [];
         $futuredata = [];
         $recurrence_id_format = libcalendaring::recurrence_id_format($event);
 
         if (!empty($event['recurrence'])) {
             // copy the recurrence rule from the master event (to be used in the UI)
             $recurrence_rule = $event['recurrence'];
             unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']);
 
             if (!empty($event['recurrence']['EXCEPTIONS'])) {
                 foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
                     if (empty($exception['_instance'])) {
                         $exception['_instance'] = libcalendaring::recurrence_instance_identifier($exception, !empty($event['allday']));
                     }
 
                     $rec_event = $this->_to_driver_event($exception, false, false, $event);
                     $rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
                     $rec_event['isexception'] = 1;
 
                     // found the specifically requested instance: register exception (single occurrence wins)
                     if (
                         $rec_event['id'] == $event_id
                         && (empty($this->events[$event_id]) || !empty($this->events[$event_id]['thisandfuture']))
                     ) {
                         $rec_event['recurrence'] = $recurrence_rule;
                         $rec_event['recurrence_id'] = $event['uid'];
                         $this->events[$rec_event['id']] = $rec_event;
                     }
 
                     // remember this exception's date
                     $exdate = substr($exception['_instance'], 0, 8);
                     if (empty($exdata[$exdate]) || !empty($exdata[$exdate]['thisandfuture'])) {
                         $exdata[$exdate] = $rec_event;
                     }
                     if (!empty($rec_event['thisandfuture'])) {
                         $futuredata[$exdate] = $rec_event;
                     }
                 }
             }
         }
 
         // found the specifically requested instance, exiting...
         if ($event_id && !empty($this->events[$event_id])) {
             return [$this->events[$event_id]];
         }
 
         // Check first occurrence, it might have been moved
         if (!empty($exdata[$event['start']->format('Ymd')])) {
             $first = $exdata[$event['start']->format('Ymd')];
             // return it only if not already in the result, but in the requested period
             if (!($event['start'] <= $end && $event['end'] >= $start)
                 && ($first['start'] <= $end && $first['end'] >= $start)
             ) {
                 $events[] = $first;
             }
         }
 
         if ($limit && count($events) >= $limit) {
             return $events;
         }
 
         // use libkolab to compute recurring events
         $recurrence = new kolab_date_recurrence($object);
 
         $i = 0;
         while ($next_event = $recurrence->next_instance()) {
             $datestr     = $next_event['start']->format('Ymd');
             $instance_id = $next_event['start']->format($recurrence_id_format);
 
             // use this event data for future recurring instances
             $overlay_data = $futuredata[$datestr] ?? null;
 
             $rec_id      = $event['uid'] . '-' . $instance_id;
             $exception   = !empty($exdata[$datestr]) ? $exdata[$datestr] : $overlay_data;
             $event_start = $next_event['start'];
             $event_end   = $next_event['end'];
 
             // copy some event from exception to get proper start/end dates
             if ($exception) {
                 $event_copy = $next_event;
                 kolab_driver::merge_exception_dates($event_copy, $exception);
                 $event_start = $event_copy['start'];
                 $event_end   = $event_copy['end'];
             }
 
             // add to output if in range
             if (($event_start <= $end && $event_end >= $start) || ($event_id && $rec_id == $event_id)) {
                 $rec_event = $this->_to_driver_event($next_event, false, false, $event);
                 $rec_event['_instance'] = $instance_id;
                 $rec_event['_count'] = $i + 1;
 
                 if ($exception) {
                     // copy data from exception
                     kolab_driver::merge_exception_data($rec_event, $exception);
                 }
 
                 $rec_event['id'] = $rec_id;
                 $rec_event['recurrence_id'] = $event['uid'];
                 $rec_event['recurrence'] = $recurrence_rule;
                 unset($rec_event['_attendees']);
                 $events[] = $rec_event;
 
                 if ($rec_id == $event_id) {
                     $this->events[$rec_id] = $rec_event;
                     break;
                 }
 
                 if ($limit && count($events) >= $limit) {
                     return $events;
                 }
             }
             else if ($next_event['start'] > $end) {
                 // stop loop if out of range
                 break;
             }
 
             // avoid endless recursion loops
             if (++$i > 100000) {
                 break;
             }
         }
 
         return $events;
     }
 
     /**
      * Convert from Kolab_Format to internal representation
      */
     private function _to_driver_event($record, $noinst = false, $links = true, $master_event = null)
     {
         $record['calendar'] = $this->id;
 
         // remove (possibly outdated) cached parameters
         unset($record['_folder_id'], $record['className']);
 
         if ($links && !array_key_exists('links', $record)) {
             $record['links'] = $this->get_links($record['uid']);
         }
 
         $ns = $this->get_namespace();
 
         if ($ns == 'other') {
             $record['className'] = 'fc-event-ns-other';
         }
 
         if ($ns == 'other' || !$this->cal->rc->config->get('kolab_invitation_calendars')) {
             $record = kolab_driver::add_partstat_class($record, ['NEEDS-ACTION', 'DECLINED'], $this->get_owner());
 
             // Modify invitation status class name, when invitation calendars are disabled
             // we'll use opacity only for declined/needs-action events
             $record['className'] = str_replace('-invitation', '', $record['className']);
         }
 
         // add instance identifier to first occurrence (master event)
         $recurrence_id_format = libcalendaring::recurrence_id_format($master_event ? $master_event : $record);
         if (!$noinst && !empty($record['recurrence']) && empty($record['recurrence_id']) && empty($record['_instance'])) {
             $record['_instance'] = $record['start']->format($recurrence_id_format);
         }
         else if (isset($record['recurrence_date']) && $record['recurrence_date'] instanceof DateTimeInterface) {
             $record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
         }
 
         // clean up exception data
         if (!empty($record['recurrence']) && !empty($record['recurrence']['EXCEPTIONS'])) {
             array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
                 unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
             });
         }
 
         return $record;
     }
 
     /**
      * Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
      * (opposite of self::_to_driver_event())
      */
     private function _from_driver_event($event, $old = [])
     {
         // set current user as ORGANIZER
         if ($identity = $this->cal->rc->user->list_emails(true)) {
             $event['attendees'] = !empty($event['attendees']) ? $event['attendees'] : [];
             $found = false;
 
             // there can be only resources on attendees list (T1484)
             // let's check the existence of an organizer
             foreach ($event['attendees'] as $attendee) {
                 if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER') {
                     $found = true;
                     break;
                 }
             }
 
             if (!$found) {
                 $event['attendees'][] = ['role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']];
             }
 
             $event['_owner'] = $identity['email'];
         }
 
         // remove EXDATE values if RDATE is given
         if (!empty($event['recurrence']['RDATE'])) {
             $event['recurrence']['EXDATE'] = [];
         }
 
         // remove recurrence information (e.g. EXDATES and EXCEPTIONS) entirely
         if (!empty($event['recurrence']) && empty($event['recurrence']['FREQ']) && empty($event['recurrence']['RDATE'])) {
             $event['recurrence'] = [];
         }
 
         // keep 'comment' from initial itip invitation
         if (!empty($old['comment'])) {
             $event['comment'] = $old['comment'];
         }
 
         // remove some internal properties which should not be cached
         $cleanup_fn = function(&$event) {
             unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'],
                 $event['calendar'], $event['className'], $event['recurrence_id'],
                 $event['attachments'], $event['deleted_attachments']);
         };
 
         $cleanup_fn($event);
 
         // clean up exception data
         if (!empty($event['exceptions'])) {
             array_walk($event['exceptions'], function(&$exception) use ($cleanup_fn) {
                 unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj']);
                 $cleanup_fn($exception);
             });
         }
 
         // copy meta data (starting with _) from old object
         foreach ((array) $old as $key => $val) {
             if (!isset($event[$key]) && $key[0] == '_') {
                 $event[$key] = $val;
             }
         }
 
         return $event;
     }
 
     /**
      * Match the given word in the event contents
      */
     public function fulltext_match($event, $word, $recursive = true)
     {
         $hits = 0;
         foreach ($this->search_fields as $col) {
             if (empty($event[$col])) {
                 continue;
             }
 
             $sval = is_array($event[$col]) ? self::_complex2string($event[$col]) : $event[$col];
             if (empty($sval)) {
                 continue;
             }
 
             // do a simple substring matching (to be improved)
             $val = mb_strtolower($sval);
             if (strpos($val, $word) !== false) {
                 $hits++;
                 break;
             }
         }
 
         return $hits;
     }
 
     /**
      * Convert a complex event attribute to a string value
      */
     private static function _complex2string($prop)
     {
         static $ignorekeys = ['role', 'status', 'rsvp'];
 
         $out = '';
         if (is_array($prop)) {
             foreach ($prop as $key => $val) {
                 if (is_numeric($key)) {
                     $out .= self::_complex2string($val);
                 }
                 else if (!in_array($key, $ignorekeys)) {
                     $out .= $val . ' ';
                 }
             }
         }
         else if (is_string($prop) || is_numeric($prop)) {
             $out .= $prop . ' ';
         }
 
         return rtrim($out);
     }
 }
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 84bfddb0..10d301d1 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1,2666 +1,2666 @@
 <?php
 
 /**
  * Kolab driver for the Calendar plugin
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2012-2015, Kolab Systems AG <contact@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_driver extends calendar_driver
 {
     const INVITATIONS_CALENDAR_PENDING  = '--invitation--pending';
     const INVITATIONS_CALENDAR_DECLINED = '--invitation--declined';
 
     // features this backend supports
     public $alarms              = true;
     public $attendees           = true;
     public $freebusy            = true;
     public $attachments         = true;
     public $undelete            = true;
     public $alarm_types         = ['DISPLAY', 'AUDIO'];
     public $categoriesimmutable = true;
 
     protected $rc;
     protected $cal;
     protected $calendars;
     protected $storage;
     protected $has_writeable    = false;
     protected $freebusy_trigger = false;
     protected $bonnie_api       = false;
 
     /**
      * Default constructor
      */
     public function __construct($cal)
     {
         $cal->require_plugin('libkolab');
 
         // load helper classes *after* libkolab has been loaded (#3248)
         require_once(__DIR__ . '/kolab_calendar.php');
         require_once(__DIR__ . '/kolab_user_calendar.php');
         require_once(__DIR__ . '/kolab_invitation_calendar.php');
 
         $this->cal     = $cal;
         $this->rc      = $cal->rc;
         $this->storage = new kolab_storage();
 
         $this->cal->register_action('push-freebusy', [$this, 'push_freebusy']);
         $this->cal->register_action('calendar-acl', [$this, 'calendar_acl']);
 
         $this->freebusy_trigger = $this->rc->config->get('calendar_freebusy_trigger', false);
 
         if (!$this->rc->config->get('kolab_freebusy_server', false)) {
             $this->freebusy = false;
         }
 
         if (kolab_storage::$version == '2.0') {
             $this->alarm_types    = ['DISPLAY'];
             $this->alarm_absolute = false;
         }
 
         // get configuration for the Bonnie API
         $this->bonnie_api = libkolab::get_bonnie_api();
 
         // calendar uses fully encoded identifiers
         kolab_storage::$encode_ids = true;
     }
 
     /**
      * Read available calendars from server
      */
     protected function _read_calendars()
     {
         // already read sources
         if (isset($this->calendars)) {
             return $this->calendars;
         }
 
         // get all folders that have "event" type, sorted by namespace/name
         $folders = $this->storage->sort_folders(
             $this->storage->get_folders('event') + kolab_storage::get_user_folders('event', true)
         );
 
         $this->calendars = [];
 
         foreach ($folders as $folder) {
             $calendar = $this->_to_calendar($folder);
             if ($calendar->ready) {
                 $this->calendars[$calendar->id] = $calendar;
                 if ($calendar->editable) {
                     $this->has_writeable = true;
                 }
             }
         }
 
         return $this->calendars;
     }
 
     /**
      * Convert kolab_storage_folder into kolab_calendar
      */
     protected function _to_calendar($folder)
     {
         if ($folder instanceof kolab_calendar) {
             return $folder;
         }
 
         if ($folder instanceof kolab_storage_folder_user) {
             $calendar = new kolab_user_calendar($folder, $this->cal);
             $calendar->subscriptions = count($folder->children) > 0;
         }
         else {
             $calendar = new kolab_calendar($folder->name, $this->cal);
         }
 
         return $calendar;
     }
 
     /**
      * Get a list of available calendars from this source
      *
      * @param int    $filter Bitmask defining filter criterias
      * @param object $tree   Reference to hierarchical folder tree object
      *
      * @return array List of calendars
      */
     public function list_calendars($filter = 0, &$tree = null)
     {
         $this->_read_calendars();
 
         // attempt to create a default calendar for this user
         if (!$this->has_writeable) {
             if ($this->create_calendar(['name' => 'Calendar', 'color' => 'cc0000'])) {
                 unset($this->calendars);
                 $this->_read_calendars();
             }
         }
 
         $delim     = $this->rc->get_storage()->get_hierarchy_delimiter();
         $folders   = $this->filter_calendars($filter);
         $calendars = [];
 
         // include virtual folders for a full folder tree
         if (!is_null($tree)) {
             $folders = $this->storage->folder_hierarchy($folders, $tree);
         }
 
         $parents = array_keys($this->calendars);
 
         foreach ($folders as $id => $cal) {
             $imap_path = explode($delim, $cal->name);
 
             // find parent
             do {
                 array_pop($imap_path);
                 $parent_id = $this->storage->folder_id(join($delim, $imap_path));
             }
             while (count($imap_path) > 1 && !in_array($parent_id, $parents));
 
             // restore "real" parent ID
             if ($parent_id && !in_array($parent_id, $parents)) {
                 $parent_id = $this->storage->folder_id($cal->get_parent());
             }
 
             $parents[] = $cal->id;
 
-            if ($cal->virtual) {
+            if (property_exists($cal, "virtual") && $cal->virtual) {
                 $calendars[$cal->id] = [
                     'id'       => $cal->id,
                     'name'     => $cal->get_name(),
                     'listname' => $cal->get_foldername(),
                     'editname' => $cal->get_foldername(),
                     'virtual'  => true,
                     'editable' => false,
                     'group'    => $cal->get_namespace(),
                 ];
             }
             else {
                 // additional folders may come from kolab_storage::folder_hierarchy() above
                 // make sure we deal with kolab_calendar instances
                 $cal = $this->_to_calendar($cal);
                 $this->calendars[$cal->id] = $cal;
 
                 $is_user = ($cal instanceof kolab_user_calendar);
 
                 $calendars[$cal->id] = [
                     'id'        => $cal->id,
                     'name'      => $cal->get_name(),
                     'listname'  => $cal->get_foldername(),
                     'editname'  => $cal->get_foldername(),
                     'title'     => $cal->get_title(),
                     'color'     => $cal->get_color(),
                     'editable'  => $cal->editable,
                     'group'     => $is_user ? 'other user' : $cal->get_namespace(),
                     'active'    => $cal->is_active(),
                     'owner'     => $cal->get_owner(),
                     'removable' => !$cal->default,
                 ];
 
                 if (!$is_user) {
                     $calendars[$cal->id] += [
                         'default'    => $cal->default,
                         'rights'     => $cal->rights,
                         'showalarms' => $cal->alarms,
                         'history'    => !empty($this->bonnie_api),
                         'children'   => true,  // TODO: determine if that folder indeed has child folders
                         'parent'     => $parent_id,
                         'subtype'    => $cal->subtype,
                         'caldavurl'  => $cal->get_caldav_url(),
                     ];
                 }
             }
 
             if ($cal->subscriptions) {
                 $calendars[$cal->id]['subscribed'] = $cal->is_subscribed();
             }
         }
 
         // list virtual calendars showing invitations
         if ($this->rc->config->get('kolab_invitation_calendars') && !($filter & self::FILTER_INSERTABLE)) {
             foreach ([self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED] as $id) {
                 $cal = new kolab_invitation_calendar($id, $this->cal);
                 if (!($filter & self::FILTER_ACTIVE) || $cal->is_active()) {
                     $calendars[$id] = [
                         'id'         => $cal->id,
                         'name'       => $cal->get_name(),
                         'listname'   => $cal->get_name(),
                         'editname'   => $cal->get_foldername(),
                         'title'      => $cal->get_title(),
                         'color'      => $cal->get_color(),
                         'editable'   => $cal->editable,
                         'rights'     => $cal->rights,
                         'showalarms' => $cal->alarms,
                         'history'    => !empty($this->bonnie_api),
                         'group'      => 'x-invitations',
                         'default'    => false,
                         'active'     => $cal->is_active(),
                         'owner'      => $cal->get_owner(),
                         'children'   => false,
                         'counts'     => $id == self::INVITATIONS_CALENDAR_PENDING,
                     ];
 
 
                     if (is_object($tree)) {
                         $tree->children[] = $cal;
                     }
                 }
             }
         }
 
         // append the virtual birthdays calendar
         if ($this->rc->config->get('calendar_contact_birthdays', false) && !($filter & self::FILTER_INSERTABLE)) {
             $id    = self::BIRTHDAY_CALENDAR_ID;
             $prefs = $this->rc->config->get('kolab_calendars', []);  // read local prefs
 
             if (!($filter & self::FILTER_ACTIVE) || !empty($prefs[$id]['active'])) {
                 $calendars[$id] = [
                     'id'         => $id,
                     'name'       => $this->cal->gettext('birthdays'),
                     'listname'   => $this->cal->gettext('birthdays'),
                     'color'      => !empty($prefs[$id]['color']) ? $prefs[$id]['color'] : '87CEFA',
                     'active'     => !empty($prefs[$id]['active']),
                     'showalarms' => (bool) $this->rc->config->get('calendar_birthdays_alarm_type'),
                     'group'      => 'x-birthdays',
                     'editable'   => false,
                     'default'    => false,
                     'children'   => false,
                     'history'    => false,
                 ];
             }
         }
 
         return $calendars;
     }
 
     /**
      * Get list of calendars according to specified filters
      *
      * @param int Bitmask defining restrictions. See FILTER_* constants for possible values.
      *
      * @return array List of calendars
      */
     protected function filter_calendars($filter)
     {
         $this->_read_calendars();
 
         $calendars = [];
 
         $plugin = $this->rc->plugins->exec_hook('calendar_list_filter', [
             'list'      => $this->calendars,
             'calendars' => $calendars,
             'filter'    => $filter,
         ]);
 
         if ($plugin['abort']) {
             return $plugin['calendars'];
         }
 
         $personal = $filter & self::FILTER_PERSONAL;
         $shared   = $filter & self::FILTER_SHARED;
 
         foreach ($this->calendars as $cal) {
             if (!$cal->ready) {
                 continue;
             }
             if (($filter & self::FILTER_WRITEABLE) && !$cal->editable) {
                 continue;
             }
             if (($filter & self::FILTER_INSERTABLE) && !$cal->editable) {
                 continue;
             }
             if (($filter & self::FILTER_ACTIVE) && !$cal->is_active()) {
                 continue;
             }
             if (($filter & self::FILTER_PRIVATE) && $cal->subtype != 'private') {
                 continue;
             }
             if (($filter & self::FILTER_CONFIDENTIAL) && $cal->subtype != 'confidential') {
                 continue;
             }
             if ($personal || $shared) {
                 $ns = $cal->get_namespace();
                 if (!(($personal && $ns == 'personal') || ($shared && $ns == 'shared'))) {
                     continue;
                 }
             }
 
             $calendars[$cal->id] = $cal;
         }
 
         return $calendars;
     }
 
     /**
      * Get the kolab_calendar instance for the given calendar ID
      *
      * @param string Calendar identifier (encoded imap folder name)
      *
      * @return kolab_calendar Object nor null if calendar doesn't exist
      */
     public function get_calendar($id)
     {
         $this->_read_calendars();
 
         // create calendar object if necesary
         if (empty($this->calendars[$id])) {
             if (in_array($id, [self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED])) {
                 return new kolab_invitation_calendar($id, $this->cal);
             }
 
             // for unsubscribed calendar folders
             if ($id !== self::BIRTHDAY_CALENDAR_ID) {
                 $calendar = kolab_calendar::factory($id, $this->cal);
                 if ($calendar->ready) {
                     $this->calendars[$calendar->id] = $calendar;
                 }
             }
         }
 
         return !empty($this->calendars[$id]) ? $this->calendars[$id] : null;
     }
 
     /**
      * Create a new calendar assigned to the current user
      *
      * @param array Hash array with calendar properties
      *    name: Calendar name
      *   color: The color of the calendar
      *
      * @return mixed ID of the calendar on success, False on error
      */
     public function create_calendar($prop)
     {
         $prop['type']       = 'event';
         $prop['active']     = true;
         $prop['subscribed'] = true;
 
         $folder = $this->storage->folder_update($prop);
 
         if ($folder === false) {
             $this->last_error = $this->cal->gettext($this->storage->last_error);
             return false;
         }
 
         // create ID
         $id = $this->storage->folder_id($folder);
 
         // save color in user prefs (temp. solution)
         $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
 
         if (isset($prop['color'])) {
             $prefs['kolab_calendars'][$id]['color'] = $prop['color'];
         }
 
         if (isset($prop['showalarms'])) {
             $prefs['kolab_calendars'][$id]['showalarms'] = !empty($prop['showalarms']);
         }
 
         if (!empty($prefs['kolab_calendars'][$id])) {
             $this->rc->user->save_prefs($prefs);
         }
 
         return $id;
     }
 
     /**
      * Update properties of an existing calendar
      *
      * @see calendar_driver::edit_calendar()
      */
     public function edit_calendar($prop)
     {
         if (!empty($prop['id']) && ($cal = $this->get_calendar($prop['id']))) {
             $id = $cal->update($prop);
         }
         else {
             $id = $prop['id'];
         }
 
         // fallback to local prefs
         $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
         unset($prefs['kolab_calendars'][$prop['id']]['color'], $prefs['kolab_calendars'][$prop['id']]['showalarms']);
 
         if (isset($prop['color'])) {
             $prefs['kolab_calendars'][$id]['color'] = $prop['color'];
         }
 
         if (isset($prop['showalarms']) && $id == self::BIRTHDAY_CALENDAR_ID) {
             $prefs['calendar_birthdays_alarm_type'] = $prop['showalarms'] ? $this->alarm_types[0] : '';
         }
         else if (isset($prop['showalarms'])) {
             $prefs['kolab_calendars'][$id]['showalarms'] = !empty($prop['showalarms']);
         }
 
         if (!empty($prefs['kolab_calendars'][$id])) {
             $this->rc->user->save_prefs($prefs);
         }
 
         return true;
     }
 
     /**
      * Set active/subscribed state of a calendar
      *
      * @see calendar_driver::subscribe_calendar()
      */
     public function subscribe_calendar($prop)
     {
         if (!empty($prop['id']) && ($cal = $this->get_calendar($prop['id'])) && !empty($cal->storage)) {
             $ret = false;
             if (isset($prop['permanent'])) {
                 $ret |= $cal->storage->subscribe(intval($prop['permanent']));
             }
             if (isset($prop['active'])) {
                 $ret |= $cal->storage->activate(intval($prop['active']));
             }
 
             // apply to child folders, too
             if (!empty($prop['recursive'])) {
                 foreach ((array) $this->storage->list_folders($cal->storage->name, '*', 'event') as $subfolder) {
                     if (isset($prop['permanent'])) {
                         if ($prop['permanent']) {
                             $this->storage->folder_subscribe($subfolder);
                         }
                         else {
                             $this->storage->folder_unsubscribe($subfolder);
                         }
                     }
 
                     if (isset($prop['active'])) {
                         if ($prop['active']) {
                             $this->storage->folder_activate($subfolder);
                         }
                         else {
                             $this->storage->folder_deactivate($subfolder);
                         }
                     }
                 }
             }
             return $ret;
         }
         else {
             // save state in local prefs
             $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
             $prefs['kolab_calendars'][$prop['id']]['active'] = !empty($prop['active']);
             $this->rc->user->save_prefs($prefs);
             return true;
         }
 
         return false;
     }
 
     /**
      * Delete the given calendar with all its contents
      *
      * @see calendar_driver::delete_calendar()
      */
     public function delete_calendar($prop)
     {
         if (!empty($prop['id']) && ($cal = $this->get_calendar($prop['id']))) {
             $folder = $cal->get_realname();
 
             // TODO: unsubscribe if no admin rights
             if ($this->storage->folder_delete($folder)) {
                 // remove color in user prefs (temp. solution)
                 $prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', []);
                 unset($prefs['kolab_calendars'][$prop['id']]);
 
                 $this->rc->user->save_prefs($prefs);
                 return true;
             }
             else {
                 $this->last_error = $this->storage->last_error;
             }
         }
 
         return false;
     }
 
     /**
      * 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 (!$this->storage->setup()) {
             return [];
         }
 
         $this->calendars = [];
         $this->search_more_results = false;
 
         // find unsubscribed IMAP folders that have "event" type
         if ($source == 'folders') {
             foreach ((array) $this->storage->search_folders('event', $query, ['other']) as $folder) {
                 $calendar = new kolab_calendar($folder->name, $this->cal);
                 $this->calendars[$calendar->id] = $calendar;
             }
         }
         // find other user's virtual calendars
         else if ($source == 'users') {
             // we have slightly more space, so display twice the number
             $limit = $this->rc->config->get('autocomplete_max', 15) * 2;
 
             foreach ($this->storage->search_users($query, 0, [], $limit, $count) as $user) {
                 $calendar = new kolab_user_calendar($user, $this->cal);
                 $this->calendars[$calendar->id] = $calendar;
 
                 // search for calendar folders shared by this user
                 foreach ($this->storage->list_user_folders($user, 'event', false) as $foldername) {
                     $cal = new kolab_calendar($foldername, $this->cal);
                     $this->calendars[$cal->id] = $cal;
                     $calendar->subscriptions = true;
                 }
             }
 
             if ($count > $limit) {
                 $this->search_more_results = true;
             }
         }
 
         // don't list the birthday calendar
         $this->rc->config->set('calendar_contact_birthdays', false);
         $this->rc->config->set('kolab_invitation_calendars', false);
 
         return $this->list_calendars();
     }
 
     /**
      * Fetch a single event
      *
      * @see calendar_driver::get_event()
      * @return array Hash array with event properties, false if not found
      */
     public function get_event($event, $scope = 0, $full = false)
     {
         if (is_array($event)) {
             $id  = !empty($event['id']) ? $event['id'] : $event['uid'];
             $cal = $event['calendar'];
 
             // we're looking for a recurring instance: expand the ID to our internal convention for recurring instances
             if (empty($event['id']) && !empty($event['_instance'])) {
                 $id .= '-' . $event['_instance'];
             }
         }
         else {
             $id = $event;
         }
 
         if (!empty($cal)) {
             if ($storage = $this->get_calendar($cal)) {
                 $result = $storage->get_event($id);
                 return self::to_rcube_event($result);
             }
 
             // get event from the address books birthday calendar
             if ($cal == self::BIRTHDAY_CALENDAR_ID) {
                 return $this->get_birthday_event($id);
             }
         }
         // iterate over all calendar folders and search for the event ID
         else {
             foreach ($this->filter_calendars($scope) as $calendar) {
                 if ($result = $calendar->get_event($id)) {
                     return self::to_rcube_event($result);
                 }
             }
         }
 
         return false;
     }
 
     /**
      * Add a single event to the database
      *
      * @see calendar_driver::new_event()
      */
     public function new_event($event)
     {
         if (!$this->validate($event)) {
             return false;
         }
 
         $event = self::from_rcube_event($event);
 
         if (!$event['calendar']) {
             $this->_read_calendars();
             $cal_ids = array_keys($this->calendars);
             $event['calendar'] = reset($cal_ids);
         }
 
         if ($storage = $this->get_calendar($event['calendar'])) {
             // if this is a recurrence instance, append as exception to an already existing object for this UID
             if (!empty($event['recurrence_date']) && ($master = $storage->get_event($event['uid']))) {
                 self::add_exception($master, $event);
                 $success = $storage->update_event($master);
             }
             else {
                 $success = $storage->insert_event($event);
             }
 
             if ($success && $this->freebusy_trigger) {
                 $this->rc->output->command('plugin.ping_url', ['action' => 'calendar/push-freebusy', 'source' => $storage->id]);
                 $this->freebusy_trigger = false; // disable after first execution (#2355)
             }
 
             return $success;
         }
 
         return false;
     }
 
     /**
      * Update an event entry with the given data
      *
      * @see calendar_driver::new_event()
      * @return bool True on success, False on error
      */
     public function edit_event($event)
     {
         if (!($storage = $this->get_calendar($event['calendar']))) {
             return false;
         }
 
         return $this->update_event(self::from_rcube_event($event, $storage->get_event($event['id'])));
     }
 
     /**
      * Extended event editing with possible changes to the argument
      *
      * @param array  Hash array with event properties
      * @param string New participant status
      * @param array  List of hash arrays with updated attendees
      *
      * @return bool True on success, False on error
      */
     public function edit_rsvp(&$event, $status, $attendees)
     {
         $update_event = $event;
 
         // apply changes to master (and all exceptions)
         if ($event['_savemode'] == 'all' && !empty($event['recurrence_id'])) {
             if ($storage = $this->get_calendar($event['calendar'])) {
                 $update_event = $storage->get_event($event['recurrence_id']);
                 $update_event['_savemode'] = $event['_savemode'];
                 $update_event['id'] = $update_event['uid'];
                 unset($update_event['recurrence_id']);
                 calendar::merge_attendee_data($update_event, $attendees);
             }
         }
 
         if ($ret = $this->update_attendees($update_event, $attendees)) {
             // replace with master event (for iTip reply)
             $event = self::to_rcube_event($update_event);
 
             // re-assign to the according (virtual) calendar
             if ($this->rc->config->get('kolab_invitation_calendars')) {
                 if (strtoupper($status) == 'DECLINED') {
                     $event['calendar'] = self::INVITATIONS_CALENDAR_DECLINED;
                 }
                 else if (strtoupper($status) == 'NEEDS-ACTION') {
                     $event['calendar'] = self::INVITATIONS_CALENDAR_PENDING;
                 }
                 else if (!empty($event['_folder_id'])) {
                     $event['calendar'] = $event['_folder_id'];
                 }
             }
         }
 
         return $ret;
     }
 
     /**
      * Update the participant status for the given attendees
      *
      * @see calendar_driver::update_attendees()
      */
     public function update_attendees(&$event, $attendees)
     {
         // for this-and-future updates, merge the updated attendees onto all exceptions in range
         if (
             ($event['_savemode'] == 'future' && !empty($event['recurrence_id']))
             || (!empty($event['recurrence']) && empty($event['recurrence_id']))
         ) {
             if (!($storage = $this->get_calendar($event['calendar']))) {
                 return false;
             }
 
             // load master event
             $master = !empty($event['recurrence_id']) ? $storage->get_event($event['recurrence_id']) : $event;
 
             // apply attendee update to each existing exception
             if (!empty($master['recurrence']) && !empty($master['recurrence']['EXCEPTIONS'])) {
                 $saved = false;
                 foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
                     // merge the new event properties onto future exceptions
                     if ($exception['_instance'] >= strval($event['_instance'])) {
                         calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $attendees);
                     }
                     // update a specific instance
                     if ($exception['_instance'] == $event['_instance'] && $exception['thisandfuture']) {
                         $saved = true;
                     }
                 }
 
                 // add the given event as new exception
                 if (!$saved && $event['id'] != $master['id']) {
                     $event['thisandfuture'] = true;
                     $master['recurrence']['EXCEPTIONS'][] = $event;
                 }
 
                 // set link to top-level exceptions
                 $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
 
                 return $this->update_event($master);
             }
         }
 
         // just update the given event (instance)
         return $this->update_event($event);
     }
 
     /**
      * Move a single event
      *
      * @see calendar_driver::move_event()
      * @return boolean True on success, False on error
      */
     public function move_event($event)
     {
         if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
             unset($ev['sequence']);
             self::clear_attandee_noreply($ev);
 
             return $this->update_event($event + $ev);
         }
 
         return false;
     }
 
     /**
      * Resize a single event
      *
      * @see calendar_driver::resize_event()
      * @return boolean True on success, False on error
      */
     public function resize_event($event)
     {
         if (($storage = $this->get_calendar($event['calendar'])) && ($ev = $storage->get_event($event['id']))) {
             unset($ev['sequence']);
             self::clear_attandee_noreply($ev);
 
             return $this->update_event($event + $ev);
         }
 
         return false;
     }
 
     /**
      * Remove a single event
      *
      * @param array Hash array with event properties:
      *      id: Event identifier
      * @param bool  Remove record(s) irreversible (mark as deleted otherwise)
      *
      * @return bool True on success, False on error
      */
     public function remove_event($event, $force = true)
     {
         $ret      = true;
         $success  = false;
         $savemode = $event['_savemode'] ?? null;
 
         if (!$force) {
             unset($event['attendees']);
             $this->rc->session->remove('calendar_event_undo');
             $this->rc->session->remove('calendar_restore_event_data');
             $sess_data = $event;
         }
 
         if (($storage = $this->get_calendar($event['calendar'])) && ($event = $storage->get_event($event['id']))) {
             $event['_savemode'] = $savemode;
             $decline  = !empty($event['_decline']);
             $savemode = 'all';
             $master   = $event;
 
             // read master if deleting a recurring event
             if (!empty($event['recurrence']) || !empty($event['recurrence_id']) || !empty($event['isexception'])) {
                 $master = $storage->get_event($event['uid']);
 
                 if (!empty($event['_savemode'])) {
                     $savemode = $event['_savemode'];
                 }
                 else if (!empty($event['_instance']) || !empty($event['isexception'])) {
                     $savemode = 'current';
                 }
 
                 // force 'current' mode for single occurrences stored as exception
                 if (empty($event['recurrence']) && empty($event['recurrence_id']) && !empty($event['isexception'])) {
                     $savemode = 'current';
                 }
             }
 
             // removing an exception instance
             if ((!empty($event['recurrence_id']) || !empty($event['isexception'])) && !empty($master['exceptions'])) {
                 foreach ($master['exceptions'] as $i => $exception) {
                     if (libcalendaring::is_recurrence_exception($event, $exception)) {
                         unset($master['exceptions'][$i]);
                         // set event date back to the actual occurrence
                         if (!empty($exception['recurrence_date'])) {
                             $event['start'] = $exception['recurrence_date'];
                         }
                     }
                 }
 
                 if (!empty($master['recurrence'])) {
                     $master['recurrence']['EXCEPTIONS'] = &$master['exceptions'];
                 }
             }
 
             switch ($savemode) {
             case 'current':
                 $_SESSION['calendar_restore_event_data'] = $master;
 
                 // remove the matching RDATE entry
                 if (!empty($master['recurrence']['RDATE'])) {
                     foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
                         if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
                             unset($master['recurrence']['RDATE'][$j]);
                             break;
                         }
                     }
                 }
 
                 // add exception to master event
                 $master['recurrence']['EXDATE'][] = $event['start'];
 
                 $success = $storage->update_event($master);
                 break;
 
             case 'future':
                 $master['_instance'] = libcalendaring::recurrence_instance_identifier($master);
                 if ($master['_instance'] != $event['_instance']) {
                     $_SESSION['calendar_restore_event_data'] = $master;
 
                     // set until-date on master event
                     $master['recurrence']['UNTIL'] = clone $event['start'];
                     $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
                     unset($master['recurrence']['COUNT']);
 
                     // if all future instances are deleted, remove recurrence rule entirely (bug #1677)
                     if ($master['recurrence']['UNTIL']->format('Ymd') == $master['start']->format('Ymd')) {
                         $master['recurrence'] = [];
                     }
                     // remove matching RDATE entries
                     else if (!empty($master['recurrence']['RDATE'])) {
                         foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
                             if ($rdate->format('Ymd') == $event['start']->format('Ymd')) {
                                 $master['recurrence']['RDATE'] = array_slice($master['recurrence']['RDATE'], 0, $j);
                                 break;
                             }
                         }
                     }
 
                     $success = $storage->update_event($master);
                     $ret = $master['uid'];
                     break;
                 }
 
             default:  // 'all' is default
                 // removing the master event with loose exceptions (not recurring though)
                 if (!empty($event['recurrence_date']) && empty($master['recurrence']) && !empty($master['exceptions'])) {
                     // make the first exception the new master
                     $newmaster = array_shift($master['exceptions']);
                     $newmaster['exceptions']   = $master['exceptions'];
                     $newmaster['_attachments'] = $master['_attachments'];
                     $newmaster['_mailbox']     = $master['_mailbox'];
                     $newmaster['_msguid']      = $master['_msguid'];
 
                     $success = $storage->update_event($newmaster);
                 }
                 else if ($decline && $this->rc->config->get('kolab_invitation_calendars')) {
                     // don't delete but set PARTSTAT=DECLINED
                     if ($this->cal->lib->set_partstat($master, 'DECLINED')) {
                         $success = $storage->update_event($master);
                     }
                 }
 
                 if (!$success) {
                     $success = $storage->delete_event($master, $force);
                 }
                 break;
             }
         }
 
         if ($success && !$force) {
             if (!empty($master['_folder_id'])) {
                 $sess_data['_folder_id'] = $master['_folder_id'];
             }
             $_SESSION['calendar_event_undo'] = ['ts' => time(), 'data' => $sess_data];
         }
 
         if ($success && $this->freebusy_trigger) {
             $this->rc->output->command('plugin.ping_url', [
                     'action' => 'calendar/push-freebusy',
                     // _folder_id may be set by invitations calendar
                     'source' => !empty($master['_folder_id']) ? $master['_folder_id'] : $storage->id,
             ]);
         }
 
         return $success ? $ret : false;
     }
 
     /**
      * Restore a single deleted event
      *
      * @param array Hash array with event properties:
      *                    id: Event identifier
      *              calendar: Event calendar
      *
      * @return bool True on success, False on error
      */
     public function restore_event($event)
     {
         if ($storage = $this->get_calendar($event['calendar'])) {
             if (!empty($_SESSION['calendar_restore_event_data'])) {
                 $success = $storage->update_event($event = $_SESSION['calendar_restore_event_data']);
             }
             else {
                 $success = $storage->restore_event($event);
             }
 
             if ($success && $this->freebusy_trigger) {
                 $this->rc->output->command('plugin.ping_url', [
                         'action' => 'calendar/push-freebusy',
                         // _folder_id may be set by invitations calendar
                         'source' => !empty($event['_folder_id']) ? $event['_folder_id'] : $storage->id,
                 ]);
             }
 
             return $success;
         }
 
         return false;
     }
 
     /**
      * Wrapper to update an event object depending on the given savemode
      */
     protected function update_event($event)
     {
         if (!($storage = $this->get_calendar($event['calendar']))) {
             return false;
         }
 
         // move event to another folder/calendar
         if (!empty($event['_fromcalendar']) && $event['_fromcalendar'] != $event['calendar']) {
             if (!($fromcalendar = $this->get_calendar($event['_fromcalendar']))) {
                 return false;
             }
 
             $old = $fromcalendar->get_event($event['id']);
 
             if ($event['_savemode'] != 'new') {
                 if (!$fromcalendar->storage->move($old['uid'], $storage->storage)) {
                     return false;
                 }
 
                 $fromcalendar = $storage;
             }
         }
         else {
             $fromcalendar = $storage;
         }
 
         $success  = false;
         $savemode = 'all';
 
         $old = $master = $storage->get_event($event['id']);
 
         if (!$old || empty($old['start'])) {
             rcube::raise_error([
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Failed to load event object to update: id=" . $event['id']
                 ],
                 true, false
             );
             return false;
         }
 
         // modify a recurring event, check submitted savemode to do the right things
         if (!empty($old['recurrence']) || !empty($old['recurrence_id']) || !empty($old['isexception'])) {
             $master = $storage->get_event($old['uid']);
 
             if (!empty($event['_savemode'])) {
                 $savemode = $event['_savemode'];
             }
             else {
                 $savemode = (!empty($old['recurrence_id']) || !empty($old['isexception'])) ? 'current' : 'all';
             }
 
             // this-and-future on the first instance equals to 'all'
             if ($savemode == 'future' && !empty($master['start'])
                 && $old['_instance'] == libcalendaring::recurrence_instance_identifier($master)
             ) {
                 $savemode = 'all';
             }
             // force 'current' mode for single occurrences stored as exception
             else if (empty($old['recurrence']) && empty($old['recurrence_id']) && !empty($old['isexception'])) {
                 $savemode = 'current';
             }
 
             // Stick to the master timezone for all occurrences (Bifrost#T104637)
             if (empty($master['allday']) || !empty($event['allday'])) {
                 $master_tz = $master['start']->getTimezone();
                 $event_tz  = $event['start']->getTimezone();
 
                 if ($master_tz->getName() != $event_tz->getName()) {
                     $event['start']->setTimezone($master_tz);
                     $event['end']->setTimezone($master_tz);
                 }
             }
         }
 
         // check if update affects scheduling and update attendee status accordingly
         $reschedule = $this->check_scheduling($event, $old, true);
 
         // keep saved exceptions (not submitted by the client)
         if (!empty($old['recurrence']['EXDATE']) && !isset($event['recurrence']['EXDATE'])) {
             $event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
         }
 
         if (isset($event['recurrence']['EXCEPTIONS'])) {
             // exceptions already provided (e.g. from iCal import)
             $with_exceptions = true;
         }
         else if (!empty($old['recurrence']['EXCEPTIONS'])) {
             $event['recurrence']['EXCEPTIONS'] = $old['recurrence']['EXCEPTIONS'];
         }
         else if (!empty($old['exceptions'])) {
             $event['exceptions'] = $old['exceptions'];
         }
 
         // remove some internal properties which should not be saved
         unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_owner'],
             $event['_notify'], $event['_method'], $event['_sender'], $event['_sender_utf'], $event['_size']
         );
 
         switch ($savemode) {
         case 'new':
             // save submitted data as new (non-recurring) event
             $event['recurrence'] = [];
             $event['_copyfrom']  = $master['_msguid'];
             $event['_mailbox']   = $master['_mailbox'];
             $event['uid']        = $this->cal->generate_uid();
 
             unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
 
             // copy attachment metadata to new event
             $event = self::from_rcube_event($event, $master);
 
             self::clear_attandee_noreply($event);
             if ($success = $storage->insert_event($event)) {
                 $success = $event['uid'];
             }
             break;
 
         case 'future':
             // create a new recurring event
             $event['_copyfrom'] = $master['_msguid'];
             $event['_mailbox']  = $master['_mailbox'];
             $event['uid']       = $this->cal->generate_uid();
 
             unset($event['recurrence_id'], $event['recurrence_date'], $event['_instance'], $event['id']);
 
             // copy attachment metadata to new event
             $event = self::from_rcube_event($event, $master);
 
             // remove recurrence exceptions on re-scheduling
             if ($reschedule) {
                 unset($event['recurrence']['EXCEPTIONS'], $event['exceptions'], $master['recurrence']['EXDATE']);
             }
             else {
                 if (isset($event['recurrence']['EXCEPTIONS']) && is_array($event['recurrence']['EXCEPTIONS'])) {
                     // only keep relevant exceptions
                     $event['recurrence']['EXCEPTIONS'] = array_filter(
                         $event['recurrence']['EXCEPTIONS'],
                         function($exception) use ($event) {
                             return $exception['start'] > $event['start'];
                         }
                     );
 
                     // set link to top-level exceptions
                     $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
                 }
 
                 if (isset($event['recurrence']['EXDATE']) && is_array($event['recurrence']['EXDATE'])) {
                     $event['recurrence']['EXDATE'] = array_filter(
                         $event['recurrence']['EXDATE'],
                         function($exdate) use ($event) {
                             return $exdate > $event['start'];
                         }
                     );
                 }
             }
 
             // compute remaining occurrences
             if ($event['recurrence']['COUNT']) {
                 if (empty($old['_count'])) {
                     $old['_count'] = $this->get_recurrence_count($master, $old['start']);
                 }
                 $event['recurrence']['COUNT'] -= intval($old['_count']);
             }
 
             // remove fixed weekday when date changed
             if ($old['start']->format('Y-m-d') != $event['start']->format('Y-m-d')) {
                 if (!empty($event['recurrence']['BYDAY']) && strlen($event['recurrence']['BYDAY']) == 2) {
                     unset($event['recurrence']['BYDAY']);
                 }
                 if (!empty($old['recurrence']['BYMONTH']) && $old['recurrence']['BYMONTH'] == $old['start']->format('n')) {
                     unset($event['recurrence']['BYMONTH']);
                 }
             }
 
             // set until-date on master event
             $master['recurrence']['UNTIL'] = clone $old['start'];
             $master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
             unset($master['recurrence']['COUNT']);
 
             // remove all exceptions after $event['start']
             if (isset($master['recurrence']['EXCEPTIONS']) && is_array($master['recurrence']['EXCEPTIONS'])) {
                 $master['recurrence']['EXCEPTIONS'] = array_filter(
                     $master['recurrence']['EXCEPTIONS'],
                     function($exception) use ($event) {
                         return $exception['start'] < $event['start'];
                     }
                 );
                 // set link to top-level exceptions
                 $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
             }
 
             if (isset($master['recurrence']['EXDATE']) && is_array($master['recurrence']['EXDATE'])) {
                 $master['recurrence']['EXDATE'] = array_filter(
                     $master['recurrence']['EXDATE'],
                     function($exdate) use ($event) {
                         return $exdate < $event['start'];
                     }
                 );
             }
 
             // save new event
             if ($success = $storage->insert_event($event)) {
                 $success = $event['uid'];
 
                 // update master event (no rescheduling!)
                 self::clear_attandee_noreply($master);
                 $storage->update_event($master);
             }
             break;
 
         case 'current':
             // recurring instances shall not store recurrence rules and attachments
             $event['recurrence']    = [];
             $event['thisandfuture'] = $savemode == 'future';
             unset($event['attachments'], $event['id']);
 
             // increment sequence of this instance if scheduling is affected
             if ($reschedule) {
                 $event['sequence'] = max($old['sequence'] ?? 0, $master['sequence'] ?? 0) + 1;
             }
             else if (!isset($event['sequence'])) {
                 $event['sequence'] = !empty($old['sequence']) ? $old['sequence'] : $master['sequence'] ?? 1;
             }
 
             // save properties to a recurrence exception instance
             if (!empty($old['_instance']) && isset($master['recurrence']['EXCEPTIONS'])) {
                 if ($this->update_recurrence_exceptions($master, $event, $old, $savemode)) {
                     $success = $storage->update_event($master, $old['id']);
                     break;
                 }
             }
 
             $add_exception = true;
 
             // adjust matching RDATE entry if dates changed
             if (
                 !empty($master['recurrence']['RDATE'])
                 && ($old_date = $old['start']->format('Ymd')) != $event['start']->format('Ymd')
             ) {
                 foreach ($master['recurrence']['RDATE'] as $j => $rdate) {
                     if ($rdate->format('Ymd') == $old_date) {
                         $master['recurrence']['RDATE'][$j] = $event['start'];
                         sort($master['recurrence']['RDATE']);
                         $add_exception = false;
                         break;
                     }
                 }
             }
 
             // save as new exception to master event
             if ($add_exception) {
                 self::add_exception($master, $event, $old);
             }
 
             $success = $storage->update_event($master);
             break;
 
         default:  // 'all' is the default
             $event['id']  = $master['uid'];
             $event['uid'] = $master['uid'];
 
             // use start date from master but try to be smart on time or duration changes
             $old_start_date = $old['start']->format('Y-m-d');
             $old_start_time = !empty($old['allday']) ? '' : $old['start']->format('H:i');
             $old_duration   = self::event_duration($old['start'], $old['end'], !empty($old['allday']));
 
             $new_start_date = $event['start']->format('Y-m-d');
             $new_start_time = !empty($event['allday']) ? '' : $event['start']->format('H:i');
             $new_duration   = self::event_duration($event['start'], $event['end'], !empty($event['allday']));
 
             $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
             $date_shift = $old['start']->diff($event['start']);
 
             // shifted or resized
             if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
                 $event['start'] = $master['start']->add($date_shift);
                 $event['end'] = clone $event['start'];
                 $event['end']->add(new DateInterval($new_duration));
 
                 // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
                 if ($old_start_date != $new_start_date && !empty($event['recurrence'])) {
                     if (!empty($event['recurrence']['BYDAY']) && strlen($event['recurrence']['BYDAY']) == 2)
                         unset($event['recurrence']['BYDAY']);
                     if (!empty($old['recurrence']['BYMONTH']) && $old['recurrence']['BYMONTH'] == $old['start']->format('n'))
                         unset($event['recurrence']['BYMONTH']);
                 }
             }
             // dates did not change, use the ones from master
             else if ($new_start_date . $new_start_time == $old_start_date . $old_start_time) {
                 $event['start'] = $master['start'];
                 $event['end'] = $master['end'];
             }
 
             // when saving an instance in 'all' mode, copy recurrence exceptions over
             if (!empty($old['recurrence_id'])) {
                 $event['recurrence']['EXCEPTIONS'] = $master['recurrence']['EXCEPTIONS'] ?? [];
                 $event['recurrence']['EXDATE']     = $master['recurrence']['EXDATE'] ?? [];
             }
             else if (!empty($master['_instance'])) {
                 $event['_instance']       = $master['_instance'];
                 $event['recurrence_date'] = $master['recurrence_date'];
             }
 
             // TODO: forward changes to exceptions (which do not yet have differing values stored)
             if (!empty($event['recurrence']) && !empty($event['recurrence']['EXCEPTIONS']) && empty($with_exceptions)) {
                 // determine added and removed attendees
                 $old_attendees = $current_attendees = $added_attendees = [];
 
                 if (!empty($old['attendees'])) {
                     foreach ((array) $old['attendees'] as $attendee) {
                         $old_attendees[] = $attendee['email'];
                     }
                 }
 
                 if (!empty($event['attendees'])) {
                     foreach ((array) $event['attendees'] as $attendee) {
                         $current_attendees[] = $attendee['email'];
                         if (!in_array($attendee['email'], $old_attendees)) {
                             $added_attendees[] = $attendee;
                         }
                     }
                 }
 
                 $removed_attendees = array_diff($old_attendees, $current_attendees);
 
                 foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
                     calendar::merge_attendee_data($event['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
                 }
 
                 // adjust recurrence-id when start changed and therefore the entire recurrence chain changes
                 if ($old_start_date != $new_start_date || $old_start_time != $new_start_time) {
                     $recurrence_id_format = libcalendaring::recurrence_id_format($event);
 
                     foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
                         if (isset($exception['recurrence_date']) && $exception['recurrence_date'] instanceof DateTimeInterface) {
                             $recurrence_id = $exception['recurrence_date'];
                         }
                         else {
                             $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $old['start']->getTimezone());
                         }
 
                         if ($recurrence_id instanceof DateTimeInterface) {
                             $recurrence_id->add($date_shift);
                             $event['recurrence']['EXCEPTIONS'][$i]['recurrence_date'] = $recurrence_id;
                             $event['recurrence']['EXCEPTIONS'][$i]['_instance'] = $recurrence_id->format($recurrence_id_format);
                         }
                     }
                 }
 
                 // set link to top-level exceptions
                 $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
             }
 
             // unset _dateonly flags in (cached) date objects
             unset($event['start']->_dateonly, $event['end']->_dateonly);
 
             $success = $storage->update_event($event) ? $event['id'] : false;  // return master UID
             break;
         }
 
         if ($success && $this->freebusy_trigger) {
             $this->rc->output->command('plugin.ping_url', [
                     'action' => 'calendar/push-freebusy',
                     'source' => $storage->id
             ]);
         }
 
         return $success;
     }
 
     /**
      * Calculate event duration, returns string in DateInterval format
      */
     protected static function event_duration($start, $end, $allday = false)
     {
         if ($allday) {
             $diff = $start->diff($end);
             return 'P' . $diff->days . 'D';
         }
 
         return 'PT' . ($end->format('U') - $start->format('U')) . 'S';
     }
 
     /**
      * Determine whether the current change affects scheduling and reset attendee status accordingly
      */
     protected function check_scheduling(&$event, $old, $update = true)
     {
         // skip this check when importing iCal/iTip events
         if (isset($event['sequence']) || !empty($event['_method'])) {
             return false;
         }
 
         // iterate through the list of properties considered 'significant' for scheduling
         $kolab_event = !empty($old['_formatobj']) ? $old['_formatobj'] : new kolab_format_event();
         $reschedule  = $kolab_event->check_rescheduling($event, $old);
 
         // reset all attendee status to needs-action (#4360)
         if ($update && $reschedule && !empty($event['attendees'])) {
             $is_organizer = false;
             $emails       = $this->cal->get_user_emails();
             $attendees    = $event['attendees'];
 
             foreach ($attendees as $i => $attendee) {
                 if ($attendee['role'] == 'ORGANIZER'
                     && !empty($attendee['email'])
                     && in_array(strtolower($attendee['email']), $emails)
                 ) {
                     $is_organizer = true;
                 }
                 else if ($attendee['role'] != 'ORGANIZER'
                     && $attendee['role'] != 'NON-PARTICIPANT'
                     && $attendee['status'] != 'DELEGATED'
                 ) {
                     $attendees[$i]['status'] = 'NEEDS-ACTION';
                     $attendees[$i]['rsvp'] = true;
                 }
             }
 
             // update attendees only if I'm the organizer
             if ($is_organizer || (!empty($event['organizer']) && in_array(strtolower($event['organizer']['email']), $emails))) {
                 $event['attendees'] = $attendees;
             }
         }
 
         return $reschedule;
     }
 
     /**
      * Apply the given changes to already existing exceptions
      */
     protected function update_recurrence_exceptions(&$master, $event, $old, $savemode)
     {
         $saved    = false;
         $existing = null;
 
         // determine added and removed attendees
         $added_attendees = $removed_attendees = [];
 
         if ($savemode == 'future') {
             $old_attendees = $current_attendees = [];
 
             if (!empty($old['attendees'])) {
                 foreach ((array) $old['attendees'] as $attendee) {
                     $old_attendees[] = $attendee['email'];
                 }
             }
 
             if (!empty($event['attendees'])) {
                 foreach ((array) $event['attendees'] as $attendee) {
                     $current_attendees[] = $attendee['email'];
                     if (!in_array($attendee['email'], $old_attendees)) {
                         $added_attendees[] = $attendee;
                     }
                 }
             }
 
             $removed_attendees = array_diff($old_attendees, $current_attendees);
         }
 
         foreach ($master['recurrence']['EXCEPTIONS'] as $i => $exception) {
             // update a specific instance
             if (libcalendaring::is_recurrence_exception($old, $exception)) {
                 $existing = $i;
 
                 // check savemode against existing exception mode.
                 // if matches, we can update this existing exception
                 $thisandfuture = !empty($exception['thisandfuture']);
                 if ($thisandfuture === ($savemode == 'future')) {
                     $event['_instance']       = $old['_instance'];
                     $event['thisandfuture']   = !empty($old['thisandfuture']);
                     $event['recurrence_date'] = $old['recurrence_date'];
                     $master['recurrence']['EXCEPTIONS'][$i] = $event;
                     $saved = true;
                 }
             }
 
             // merge the new event properties onto future exceptions
             if ($savemode == 'future') {
                 $exception_instance = libcalendaring::recurrence_instance_identifier($exception, true);
                 $old_instance = libcalendaring::recurrence_instance_identifier($old, true);
 
                 if ($exception_instance >= $old_instance) {
                     unset($event['thisandfuture']);
                     self::merge_exception_data($master['recurrence']['EXCEPTIONS'][$i], $event, ['attendees']);
 
                     if (!empty($added_attendees) || !empty($removed_attendees)) {
                         calendar::merge_attendee_data($master['recurrence']['EXCEPTIONS'][$i], $added_attendees, $removed_attendees);
                     }
                 }
             }
         }
 /*
         // we could not update the existing exception due to savemode mismatch...
         if (!$saved && isset($existing) && !empty($master['recurrence']['EXCEPTIONS'][$existing]['thisandfuture'])) {
             // ... try to move the existing this-and-future exception to the next occurrence
             foreach ($this->get_recurring_events($master, $existing['start']) as $candidate) {
                 // our old this-and-future exception is obsolete
                 if (!empty($candidate['thisandfuture'])) {
                     unset($master['recurrence']['EXCEPTIONS'][$existing]);
                     $saved = true;
                     break;
                 }
                 // this occurrence doesn't yet have an exception
                 else if (empty($candidate['isexception'])) {
                     $event['_instance'] = $candidate['_instance'];
                     $event['recurrence_date'] = $candidate['recurrence_date'];
                     $master['recurrence']['EXCEPTIONS'][$i] = $event;
                     $saved = true;
                     break;
                 }
             }
         }
 */
 
         // set link to top-level exceptions
         $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
 
         // returning false here will add a new exception
         return $saved;
     }
 
     /**
      * Add or update the given event as an exception to $master
      */
     public static function add_exception(&$master, $event, $old = null)
     {
         if ($old) {
             $event['_instance'] = $old['_instance'] ?? null;
             if (empty($event['recurrence_date'])) {
                 $event['recurrence_date'] = !empty($old['recurrence_date']) ? $old['recurrence_date'] : $old['start'];
             }
         }
         else if (empty($event['recurrence_date'])) {
             $event['recurrence_date'] = $event['start'];
         }
 
         if (!isset($master['exceptions'])) {
             if (isset($master['recurrence']['EXCEPTIONS'])) {
                 $master['exceptions'] = &$master['recurrence']['EXCEPTIONS'];
             }
             else {
                 $master['exceptions'] = [];
             }
         }
 
         $existing = false;
         foreach ($master['exceptions'] as $i => $exception) {
             if (libcalendaring::is_recurrence_exception($event, $exception)) {
                 $master['exceptions'][$i] = $event;
                 $existing = true;
             }
         }
 
         if (!$existing) {
             $master['exceptions'][] = $event;
         }
 
         return true;
     }
 
     /**
      * Remove the noreply flags from attendees
      */
     public static function clear_attandee_noreply(&$event)
     {
         if (!empty($event['attendees'])) {
             foreach ((array) $event['attendees'] as $i => $attendee) {
                 unset($event['attendees'][$i]['noreply']);
             }
         }
     }
 
     /**
      * Merge certain properties from the overlay event to the base event object
      *
      * @param array The event object to be altered
      * @param array The overlay event object to be merged over $event
      * @param array List of properties not allowed to be overwritten
      */
     public static function merge_exception_data(&$event, $overlay, $blacklist = null)
     {
         $forbidden = ['id','uid','recurrence','recurrence_date','thisandfuture','organizer','_attachments'];
 
         if (is_array($blacklist)) {
             $forbidden = array_merge($forbidden, $blacklist);
         }
 
         foreach ($overlay as $prop => $value) {
             if ($prop == 'start' || $prop == 'end') {
                 // handled by merge_exception_dates() below
             }
             else if ($prop == 'thisandfuture' && $overlay['_instance'] == $event['_instance']) {
                 $event[$prop] = $value;
             }
             else if ($prop[0] != '_' && !in_array($prop, $forbidden)) {
                 $event[$prop] = $value;
             }
         }
 
         self::merge_exception_dates($event, $overlay);
     }
 
     /**
      * Merge start/end date from the overlay event to the base event object
      *
      * @param array The event object to be altered
      * @param array The overlay event object to be merged over $event
      */
     public static function merge_exception_dates(&$event, $overlay)
     {
         // compute date offset from the exception
         if ($overlay['start'] instanceof DateTimeInterface && $overlay['recurrence_date'] instanceof DateTimeInterface) {
             $date_offset = $overlay['recurrence_date']->diff($overlay['start']);
         }
 
         foreach (['start', 'end'] as $prop) {
             $value = $overlay[$prop];
             if (isset($event[$prop]) && $event[$prop] instanceof DateTimeInterface) {
                 // set date value if overlay is an exception of the current instance
                 if (substr($overlay['_instance'], 0, 8) == substr($event['_instance'], 0, 8)) {
                     $event[$prop]->setDate(intval($value->format('Y')), intval($value->format('n')), intval($value->format('j')));
                 }
                 // apply date offset
                 else if (!empty($date_offset)) {
                     $event[$prop]->add($date_offset);
                 }
                 // adjust time of the recurring event instance
                 $event[$prop]->setTime($value->format('G'), intval($value->format('i')), intval($value->format('s')));
             }
         }
     }
 
     /**
      * Get events from source.
      *
      * @param int    Event's new start (unix timestamp)
      * @param int    Event's new end (unix timestamp)
      * @param string Search query (optional)
      * @param mixed  List of calendar IDs to load events from (either as array or comma-separated string)
      * @param bool   Include virtual events (optional)
      * @param int    Only list events modified since this time (unix timestamp)
      *
      * @return array A list of event records
      */
     public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
     {
         if ($calendars && is_string($calendars)) {
             $calendars = explode(',', $calendars);
         }
         else if (!$calendars) {
             $this->_read_calendars();
             $calendars = array_keys($this->calendars);
         }
 
         $query      = [];
         $events     = [];
         $categories = [];
 
         if ($modifiedsince) {
             $query[] = ['changed', '>=', $modifiedsince];
         }
 
         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
         if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) {
             $events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
         }
 
         // add new categories to user prefs
         $old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
         $newcats = array_udiff(
             array_keys($categories),
             array_keys($old_categories),
             function($a, $b) { return strcasecmp($a, $b); }
         );
 
         if (!empty($newcats)) {
             foreach ($newcats as $category) {
                 $old_categories[$category] = '';  // no color set yet
             }
             $this->rc->user->save_prefs(['calendar_categories' => $old_categories]);
         }
 
         array_walk($events, 'kolab_driver::to_rcube_event');
 
         return $events;
     }
 
     /**
      * Get number of events in the given calendar
      *
      * @param mixed List of calendar IDs to count events (either as array or comma-separated string)
      * @param int   Date range start (unix timestamp)
      * @param int   Date range end (unix timestamp)
      *
      * @return array Hash array with counts grouped by calendar ID
      */
     public function count_events($calendars, $start, $end = null)
     {
         $counts = [];
 
         if ($calendars && is_string($calendars)) {
             $calendars = explode(',', $calendars);
         }
         else if (!$calendars) {
             $this->_read_calendars();
             $calendars = array_keys($this->calendars);
         }
 
         foreach ($calendars as $cid) {
             if ($storage = $this->get_calendar($cid)) {
                 $counts[$cid] = $storage->count_events($start, $end);
             }
         }
 
          return $counts;
     }
 
     /**
      * Get a list of pending alarms to be displayed to the user
      *
      * @see calendar_driver::pending_alarms()
      */
     public function pending_alarms($time, $calendars = null)
     {
         $interval = 300;
         $time -= $time % 60;
 
         $slot = $time;
         $slot -= $slot % $interval;
 
         $last = $time - max(60, $this->rc->config->get('refresh_interval', 0));
         $last -= $last % $interval;
 
         // only check for alerts once in 5 minutes
         if ($last == $slot) {
             return [];
         }
 
         if ($calendars && is_string($calendars)) {
             $calendars = explode(',', $calendars);
         }
 
         $time = $slot + $interval;
 
         $alarms     = [];
         $candidates = [];
         $query      = [['tags', '=', 'x-has-alarms']];
 
         $this->_read_calendars();
 
         foreach ($this->calendars as $cid => $calendar) {
             // skip calendars with alarms disabled
             if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars))) {
                 continue;
             }
 
             foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
                 // add to list if alarm is set
                 $alarm = libcalendaring::get_next_alarm($e);
                 if ($alarm && !empty($alarm['time']) && $alarm['time'] >= $last
                     && in_array($alarm['action'], $this->alarm_types)
                 ) {
                     $id = $alarm['id'];  // use alarm-id as primary identifier
                     $candidates[$id] = [
                         'id'       => $id,
                         'title'    => $e['title'],
                         'location' => $e['location'],
                         'start'    => $e['start'],
                         'end'      => $e['end'],
                         'notifyat' => $alarm['time'],
                         'action'   => $alarm['action'],
                     ];
                 }
             }
         }
 
         // get alarm information stored in local database
         if (!empty($candidates)) {
             $dbdata = [];
             $alarm_ids = array_map([$this->rc->db, 'quote'], array_keys($candidates));
 
             $result = $this->rc->db->query("SELECT *"
                 . " FROM " . $this->rc->db->table_name('kolab_alarms', true)
                 . " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")"
                     . " AND `user_id` = ?",
                 $this->rc->user->ID
             );
 
             while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
                 $dbdata[$e['alarm_id']] = $e;
             }
 
             foreach ($candidates as $id => $alarm) {
                 // skip dismissed alarms
                 if ($dbdata[$id]['dismissed']) {
                     continue;
                 }
 
                 // snooze function may have shifted alarm time
                 $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat'];
                 if ($notifyat <= $time) {
                     $alarms[] = $alarm;
                 }
             }
         }
 
         return $alarms;
     }
 
     /**
      * Feedback after showing/sending an alarm notification
      *
      * @see calendar_driver::dismiss_alarm()
      */
     public function dismiss_alarm($alarm_id, $snooze = 0)
     {
         $alarms_table = $this->rc->db->table_name('kolab_alarms', true);
 
         // delete old alarm entry
         $this->rc->db->query("DELETE FROM $alarms_table"
             . " WHERE `alarm_id` = ? AND `user_id` = ?",
             $alarm_id,
             $this->rc->user->ID
         );
 
         // set new notifyat time or unset if not snoozed
         $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
 
         $query = $this->rc->db->query("INSERT INTO $alarms_table"
             . " (`alarm_id`, `user_id`, `dismissed`, `notifyat`)"
             . " VALUES (?, ?, ?, ?)",
             $alarm_id,
             $this->rc->user->ID,
             $snooze > 0 ? 0 : 1,
             $notifyat
         );
 
         return $this->rc->db->affected_rows($query);
     }
 
     /**
      * List attachments from the given event
      */
     public function list_attachments($event)
     {
         if (!($storage = $this->get_calendar($event['calendar']))) {
             return false;
         }
 
         $event = $storage->get_event($event['id']);
 
         return $event['attachments'];
     }
 
     /**
      * Get attachment properties
      */
     public function get_attachment($id, $event)
     {
         if (!($storage = $this->get_calendar($event['calendar']))) {
             return false;
         }
 
         // get old revision of event
         if (!empty($event['rev'])) {
             $event = $this->get_event_revison($event, $event['rev'], true);
         }
         else {
             $event = $storage->get_event($event['id']);
         }
 
         if ($event) {
             $attachments = isset($event['_attachments']) ? $event['_attachments'] : $event['attachments'];
             foreach ((array) $attachments as $idx => $att) {
                 if ((isset($att['id']) && $att['id'] == $id) || (!isset($att['id']) && $idx == $id)) {
                     return $att;
                 }
             }
         }
     }
 
     /**
      * Get attachment body
      * @see calendar_driver::get_attachment_body()
      */
     public function get_attachment_body($id, $event)
     {
         if (!($cal = $this->get_calendar($event['calendar']))) {
             return false;
         }
 
         // get old revision of event
         if (!empty($event['rev'])) {
             if (empty($this->bonnie_api)) {
                 return false;
             }
 
             $cid = substr($id, 4);
 
             // call Bonnie API and get the raw mime message
             list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
             if ($msg_raw = $this->bonnie_api->rawdata('event', $uid, $event['rev'], $mailbox, $msguid)) {
                 // parse the message and find the part with the matching content-id
                 $message = rcube_mime::parse_message($msg_raw);
                 foreach ((array) $message->parts as $part) {
                     if (!empty($part->headers['content-id']) && trim($part->headers['content-id'], '<>') == $cid) {
                         return $part->body;
                     }
                 }
             }
 
             return false;
         }
 
         return $cal->get_attachment_body($id, $event);
     }
 
     /**
      * Build a struct representing the given message reference
      *
      * @see calendar_driver::get_message_reference()
      */
     public function get_message_reference($uri_or_headers, $folder = null)
     {
         if (is_object($uri_or_headers)) {
             $uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
         }
 
         if (is_string($uri_or_headers)) {
             return kolab_storage_config::get_message_reference($uri_or_headers, 'event');
         }
 
         return false;
     }
 
     /**
      * List availabale categories
      * The default implementation reads them from config/user prefs
      */
     public function list_categories()
     {
         // FIXME: complete list with categories saved in config objects (KEP:12)
         return $this->rc->config->get('calendar_categories', $this->default_categories);
     }
 
     /**
      * Create instances of a recurring event
      *
      * @param array    Hash array with event properties
      * @param DateTime Start date of the recurrence window
      * @param DateTime End date of the recurrence window
      *
      * @return array List of recurring event instances
      */
     public function get_recurring_events($event, $start, $end = null)
     {
         // load the given event data into a libkolabxml container
         if (empty($event['_formatobj'])) {
             $event_xml = new kolab_format_event();
             $event_xml->set($event);
             $event['_formatobj'] = $event_xml;
         }
 
         $this->_read_calendars();
         $storage = reset($this->calendars);
 
         return $storage->get_recurring_events($event, $start, $end);
     }
 
     /**
      *
      */
     protected function get_recurrence_count($event, $dtstart)
     {
         // load the given event data into a libkolabxml container
         if (empty($event['_formatobj'])) {
             $event_xml = new kolab_format_event();
             $event_xml->set($event);
             $event['_formatobj'] = $event_xml;
         }
 
         // use libkolab to compute recurring events
         $recurrence = new kolab_date_recurrence($event['_formatobj']);
 
         $count = 0;
         while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
             $count++;
         }
 
         return $count;
     }
 
     /**
      * Fetch free/busy information from a person within the given range
      */
     public function get_freebusy_list($email, $start, $end)
     {
         if (empty($email)/* || $end < time()*/) {
             return false;
         }
 
         // map vcalendar fbtypes to internal values
         $fbtypemap = [
             'FREE'            => calendar::FREEBUSY_FREE,
             'BUSY-TENTATIVE'  => calendar::FREEBUSY_TENTATIVE,
             'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF,
             'OOF'             => calendar::FREEBUSY_OOF
         ];
 
         // ask kolab server first
         try {
             $request_config = [
                 'store_body'       => true,
                 'follow_redirects' => true,
             ];
 
             $request  = libkolab::http_request($this->storage->get_freebusy_url($email), '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) {
             PEAR::raiseError("Error fetching free/busy information: " . $e->getMessage());
         }
 
         // get free-busy url from contacts
         if (empty($fbdata)) {
             $fburl = null;
             foreach ((array) $this->rc->config->get('autocomplete_addressbooks', 'sql') as $book) {
                 $abook = $this->rc->get_address_book($book);
 
                 if ($result = $abook->search(['email'], $email, true, true, true/*, 'freebusyurl'*/)) {
                     while ($contact = $result->iterate()) {
                         if (!empty($contact['freebusyurl'])) {
                             $fbdata = @file_get_contents($contact['freebusyurl']);
                             break;
                         }
                     }
                 }
 
                 if (!empty($fbdata)) {
                     break;
                 }
             }
         }
 
         // parse free-busy information using Horde classes
         if (!empty($fbdata)) {
             $ical = $this->cal->get_ical();
             $ical->import($fbdata);
             if ($fb = $ical->freebusy) {
                 $result = [];
                 foreach ($fb['periods'] as $tuple) {
                     list($from, $to, $type) = $tuple;
                     $result[] = [
                         $from->format('U'),
                         $to->format('U'),
                         isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY
                     ];
                 }
 
                 // we take 'dummy' free-busy lists as "unknown"
                 if (empty($result) && !empty($fb['comment']) && stripos($fb['comment'], 'dummy')) {
                     return false;
                 }
 
                 // set period from $start till the begin of the free-busy information as 'unknown'
                 if (!empty($fb['start']) && ($fbstart = $fb['start']->format('U')) && $start < $fbstart) {
                     array_unshift($result, [$start, $fbstart, calendar::FREEBUSY_UNKNOWN]);
                 }
                 // pad period till $end with status 'unknown'
                 if (!empty($fb['end']) && ($fbend = $fb['end']->format('U')) && $fbend < $end) {
                     $result[] = [$fbend, $end, calendar::FREEBUSY_UNKNOWN];
                 }
 
                 return $result;
             }
         }
 
         return false;
     }
 
     /**
      * Handler to push folder triggers when sent from client.
      * Used to push free-busy changes asynchronously after updating an event
      */
     public function push_freebusy()
     {
         // make shure triggering completes
         set_time_limit(0);
         ignore_user_abort(true);
 
         $cal = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
         if (!($cal = $this->get_calendar($cal))) {
             return false;
         }
 
         // trigger updates on folder
         $trigger = $cal->storage->trigger();
         if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
             rcube::raise_error([
                     'code' => 900, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Failed triggering folder. Error was " . $trigger->getMessage()
                 ],
                 true, false
             );
         }
 
         exit;
     }
 
     /**
      * Convert from driver format to external caledar app data
      */
     public static function to_rcube_event(&$record)
     {
         if (!is_array($record)) {
             return $record;
         }
 
         $record['id'] = $record['uid'] ?? null;
 
         if (!empty($record['_instance'])) {
             $record['id'] .= '-' . $record['_instance'];
 
             if (empty($record['recurrence_id']) && !empty($record['recurrence'])) {
                 $record['recurrence_id'] = $record['uid'];
             }
         }
 
         // all-day events go from 12:00 - 13:00
         if ($record['start'] instanceof DateTimeInterface && $record['end'] <= $record['start'] && !empty($record['allday'])) {
             $record['end'] = clone $record['start'];
             $record['end']->add(new DateInterval('PT1H'));
         }
 
         // translate internal '_attachments' to external 'attachments' list
         if (!empty($record['_attachments'])) {
             $attachments = [];
 
             foreach ($record['_attachments'] as $key => $attachment) {
                 if ($attachment !== false) {
                     if (empty($attachment['name'])) {
                         $attachment['name'] = $key;
                     }
 
                     unset($attachment['path'], $attachment['content']);
                     $attachments[] = $attachment;
                 }
             }
 
             $record['attachments'] = $attachments;
         }
 
         if (!empty($record['attendees'])) {
             foreach ((array) $record['attendees'] as $i => $attendee) {
                 if (isset($attendee['delegated-from']) && is_array($attendee['delegated-from'])) {
                     $record['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
                 }
                 if (isset($attendee['delegated-to']) && is_array($attendee['delegated-to'])) {
                     $record['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
                 }
             }
         }
 
         // Roundcube only supports one category assignment
         if (!empty($record['categories']) && is_array($record['categories'])) {
             $record['categories'] = $record['categories'][0];
         }
 
         // the cancelled flag transltes into status=CANCELLED
         if (!empty($record['cancelled'])) {
             $record['status'] = 'CANCELLED';
         }
 
         // The web client only supports DISPLAY type of alarms
         if (!empty($record['alarms'])) {
             $record['alarms'] = preg_replace('/:[A-Z]+$/', ':DISPLAY', $record['alarms']);
         }
 
         // remove empty recurrence array
         if (empty($record['recurrence'])) {
             unset($record['recurrence']);
         }
         // clean up exception data
         else if (!empty($record['recurrence']['EXCEPTIONS'])) {
             array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
                 unset($exception['_mailbox'], $exception['_msguid'],
                     $exception['_formatobj'], $exception['_attachments']
                 );
             });
         }
 
         unset($record['_mailbox'], $record['_msguid'], $record['_type'], $record['_size'],
             $record['_formatobj'], $record['_attachments'], $record['exceptions'], $record['x-custom']
         );
 
         return $record;
     }
 
     /**
      *
      */
     public static function from_rcube_event($event, $old = [])
     {
         kolab_format::merge_attachments($event, $old);
 
         return $event;
     }
 
 
     /**
      * Set CSS class according to the event's attendde partstat
      */
     public static function add_partstat_class($event, $partstats, $user = null)
     {
         // set classes according to PARTSTAT
         if (!empty($event['attendees'])) {
             $user_emails = libcalendaring::get_instance()->get_user_emails($user);
             $partstat = 'UNKNOWN';
 
             foreach ($event['attendees'] as $attendee) {
                 if (in_array($attendee['email'], $user_emails)) {
                     if (!empty($attendee['status'])) {
                         $partstat = $attendee['status'];
                     }
                     break;
                 }
             }
 
             if (in_array($partstat, $partstats)) {
                 $event['className'] = trim($event['className'] . ' fc-invitation-' . strtolower($partstat));
             }
         }
 
         return $event;
     }
 
     /**
      * Provide a list of revisions for the given event
      *
      * @param array $event Hash array with event properties
      *
      * @return array List of changes, each as a hash array
      * @see calendar_driver::get_event_changelog()
      */
     public function get_event_changelog($event)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
 
         $result = $this->bonnie_api->changelog('event', $uid, $mailbox, $msguid);
         if (is_array($result) && $result['uid'] == $uid) {
             return $result['changes'];
         }
 
         return false;
     }
 
     /**
      * Get a list of property changes beteen two revisions of an event
      *
      * @param array $event Hash array with event properties
      * @param mixed $rev1  Old Revision
      * @param mixed $rev2  New Revision
      *
      * @return array List of property changes, each as a hash array
      * @see calendar_driver::get_event_diff()
      */
     public function get_event_diff($event, $rev1, $rev2)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
 
         // get diff for the requested recurrence instance
         $instance_id = $event['id'] != $uid ? substr($event['id'], strlen($uid) + 1) : null;
 
         // call Bonnie API
         $result = $this->bonnie_api->diff('event', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id);
 
         if (is_array($result) && $result['uid'] == $uid) {
             $result['rev1'] = $rev1;
             $result['rev2'] = $rev2;
 
             $keymap = [
                 'dtstart'  => 'start',
                 'dtend'    => 'end',
                 'dstamp'   => 'changed',
                 'summary'  => 'title',
                 'alarm'    => 'alarms',
                 'attendee' => 'attendees',
                 'attach'   => 'attachments',
                 'rrule'    => 'recurrence',
                 'transparency' => 'free_busy',
                 'lastmodified-date' => 'changed',
             ];
 
             $prop_keymaps = [
                 'attachments' => ['fmttype' => 'mimetype', 'label' => 'name'],
                 'attendees'   => ['partstat' => 'status'],
             ];
 
             $special_changes = [];
 
             // map kolab event properties to keys the client expects
             array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
                 if (array_key_exists($change['property'], $keymap)) {
                     $change['property'] = $keymap[$change['property']];
                 }
                 // translate free_busy values
                 if ($change['property'] == 'free_busy') {
                     $change['old'] = !empty($old['old']) ? 'free' : 'busy';
                     $change['new'] = !empty($old['new']) ? 'free' : 'busy';
                 }
 
                 // map alarms trigger value
                 if ($change['property'] == 'alarms') {
                     if (!empty($change['old']['trigger'])) {
                         $change['old']['trigger'] = $change['old']['trigger']['value'];
                     }
                     if (!empty($change['new']['trigger'])) {
                         $change['new']['trigger'] = $change['new']['trigger']['value'];
                     }
                 }
 
                 // make all property keys uppercase
                 if ($change['property'] == 'recurrence') {
                     $special_changes['recurrence'] = $i;
                     foreach (['old', 'new'] as $m) {
                         if (!empty($change[$m])) {
                             $props = [];
                             foreach ($change[$m] as $k => $v) {
                                 $props[strtoupper($k)] = $v;
                             }
                             $change[$m] = $props;
                         }
                     }
                 }
 
                 // map property keys names
                 if (!empty($prop_keymaps[$change['property']])) {
                     foreach ($prop_keymaps[$change['property']] as $k => $dest) {
                         if (!empty($change['old']) && array_key_exists($k, $change['old'])) {
                             $change['old'][$dest] = $change['old'][$k];
                             unset($change['old'][$k]);
                         }
                         if (!empty($change['new']) && array_key_exists($k, $change['new'])) {
                             $change['new'][$dest] = $change['new'][$k];
                             unset($change['new'][$k]);
                         }
                     }
                 }
 
                 if ($change['property'] == 'exdate') {
                     $special_changes['exdate'] = $i;
                 }
                 else if ($change['property'] == 'rdate') {
                     $special_changes['rdate'] = $i;
                 }
             });
 
             // merge some recurrence changes
             foreach (['exdate', 'rdate'] as $prop) {
                 if (array_key_exists($prop, $special_changes)) {
                     $exdate = $result['changes'][$special_changes[$prop]];
                     if (array_key_exists('recurrence', $special_changes)) {
                         $recurrence = &$result['changes'][$special_changes['recurrence']];
                     }
                     else {
                         $i = count($result['changes']);
                         $result['changes'][$i] = ['property' => 'recurrence', 'old' => [], 'new' => []];
                         $recurrence = &$result['changes'][$i]['recurrence'];
                     }
                     $key = strtoupper($prop);
                     $recurrence['old'][$key] = $exdate['old'];
                     $recurrence['new'][$key] = $exdate['new'];
                     unset($result['changes'][$special_changes[$prop]]);
                 }
             }
 
             return $result;
         }
 
         return false;
     }
 
     /**
      * Return full data of a specific revision of an event
      *
      * @param array Hash array with event properties
      * @param mixed $rev Revision number
      *
      * @return array Event object as hash array
      * @see calendar_driver::get_event_revison()
      */
     public function get_event_revison($event, $rev, $internal = false)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         $eventid = $event['id'];
         $calid   = $event['calendar'];
 
         list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
 
         // call Bonnie API
         $result = $this->bonnie_api->get('event', $uid, $rev, $mailbox, $msguid);
         if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
             $format = kolab_format::factory('event');
             $format->load($result['xml']);
             $event = $format->to_array();
             $format->get_attachments($event, true);
 
             // get the right instance from a recurring event
             if ($eventid != $event['uid']) {
                 $instance_id = substr($eventid, strlen($event['uid']) + 1);
 
                 // check for recurrence exception first
                 if ($instance = $format->get_instance($instance_id)) {
                     $event = $instance;
                 }
                 else {
                     // not a exception, compute recurrence...
                     $event['_formatobj'] = $format;
                     $recurrence_date = rcube_utils::anytodatetime($instance_id, $event['start']->getTimezone());
                     foreach ($this->get_recurring_events($event, $event['start'], $recurrence_date) as $instance) {
                         if ($instance['id'] == $eventid) {
                             $event = $instance;
                             break;
                         }
                     }
                 }
             }
 
             if ($format->is_valid()) {
                 $event['calendar'] = $calid;
                 $event['rev']      = $result['rev'];
 
                 return $internal ? $event : self::to_rcube_event($event);
             }
         }
 
         return false;
     }
 
     /**
      * Command the backend to restore a certain revision of an event.
      * This shall replace the current event with an older version.
      *
      * @param mixed $event UID string or hash array with event properties:
      *              id: Event identifier
      *        calendar: Calendar identifier
      * @param mixed $rev   Revision number
      *
      * @return bool True on success, False on failure
      */
     public function restore_event_revision($event, $rev)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         list($uid, $mailbox, $msguid) = $this->_resolve_event_identity($event);
 
         $calendar = $this->get_calendar($event['calendar']);
         $success  = false;
 
         if ($calendar && $calendar->storage && $calendar->editable) {
             if ($raw_msg = $this->bonnie_api->rawdata('event', $uid, $rev, $mailbox)) {
                 $imap = $this->rc->get_storage();
 
                 // insert $raw_msg as new message
                 if ($imap->save_message($calendar->storage->name, $raw_msg, null, false)) {
                     $success = true;
 
                     // delete old revision from imap and cache
                     $imap->delete_message($msguid, $calendar->storage->name);
                     $calendar->storage->cache->set($msguid, false);
                 }
             }
         }
 
         return $success;
     }
 
     /**
      * Helper method to resolved the given event identifier into uid and folder
      *
      * @return array (uid,folder,msguid) tuple
      */
     protected function _resolve_event_identity($event)
     {
         $mailbox = $msguid = null;
 
         if (is_array($event)) {
             $uid = !empty($event['uid']) ? $event['uid'] : $event['id'];
 
             if (($cal = $this->get_calendar($event['calendar'])) && !($cal instanceof kolab_invitation_calendar)) {
                 $mailbox = $cal->get_mailbox_id();
 
                 // get event object from storage in order to get the real object uid an msguid
                 if ($ev = $cal->get_event($event['id'])) {
                     $msguid = $ev['_msguid'];
                     $uid = $ev['uid'];
                 }
             }
         }
         else {
             $uid = $event;
 
             // get event object from storage in order to get the real object uid an msguid
             if ($ev = $this->get_event($event)) {
                 $mailbox = $ev['_mailbox'];
                 $msguid  = $ev['_msguid'];
                 $uid     = $ev['uid'];
             }
         }
 
         return array($uid, $mailbox, $msguid);
     }
 
     /**
      * Callback function to produce driver-specific calendar create/edit form
      *
      * @param string Request action 'form-edit|form-new'
      * @param array  Calendar properties (e.g. id, color)
      * @param array  Edit form fields
      *
      * @return string HTML content of the form
      */
     public function calendar_form($action, $calendar, $formfields)
     {
         $special_calendars = [
             self::BIRTHDAY_CALENDAR_ID,
             self::INVITATIONS_CALENDAR_PENDING,
             self::INVITATIONS_CALENDAR_DECLINED
         ];
 
         // show default dialog for birthday calendar
         if (in_array($calendar['id'], $special_calendars)) {
             if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID) {
                 unset($formfields['showalarms']);
             }
 
             // General tab
             $form['props'] = [
                 'name'   => $this->rc->gettext('properties'),
                 'fields' => $formfields,
             ];
 
             return kolab_utils::folder_form($form, '', 'calendar');
         }
 
         $this->_read_calendars();
 
         if (!empty($calendar['id']) && ($cal = $this->calendars[$calendar['id']])) {
             $folder = $cal->get_realname(); // UTF7
             $color  = $cal->get_color();
         }
         else {
             $folder = '';
             $color  = '';
         }
 
         $hidden_fields[] = ['name' => 'oldname', 'value' => $folder];
 
         $storage = $this->rc->get_storage();
         $delim   = $storage->get_hierarchy_delimiter();
         $form    = [];
 
         if (strlen($folder)) {
             $path_imap = explode($delim, $folder);
             array_pop($path_imap);  // pop off name part
             $path_imap = implode($delim, $path_imap);
 
             $options = $storage->folder_info($folder);
         }
         else {
             $path_imap = '';
         }
 
         // General tab
         $form['props'] = [
             'name'   => $this->rc->gettext('properties'),
             'fields' => [],
         ];
 
         $protected = !empty($options) && (!empty($options['norename']) || !empty($options['protected']));
         // Disable folder name input
         if ($protected) {
             $input_name = new html_hiddenfield(['name' => 'name', 'id' => 'calendar-name']);
             $formfields['name']['value'] = $this->storage->object_name($folder)
                 . $input_name->show($folder);
         }
 
         // calendar name (default field)
         $form['props']['fields']['location'] = $formfields['name'];
 
         if ($protected) {
             // prevent user from moving folder
             $hidden_fields[] = ['name' => 'parent', 'value' => $path_imap];
         }
         else {
             $select = $this->storage->folder_selector('event', ['name' => 'parent', 'id' => 'calendar-parent'], $folder);
 
             $form['props']['fields']['path'] = [
                 'id'    => 'calendar-parent',
                 'label' => $this->cal->gettext('parentcalendar'),
                 'value' => $select->show(strlen($folder) ? $path_imap : ''),
             ];
         }
 
         // calendar color (default field)
         $form['props']['fields']['color']  = $formfields['color'];
         $form['props']['fields']['alarms'] = $formfields['showalarms'];
 
         return kolab_utils::folder_form($form, $folder, 'calendar', $hidden_fields);
     }
 
     /**
      * Handler for user_delete plugin hook
      */
     public function user_delete($args)
     {
         $db = $this->rc->get_dbh();
         foreach (['kolab_alarms', 'itipinvitations'] as $table) {
             $db->query("DELETE FROM " . $this->rc->db->table_name($table, true)
                 . " WHERE `user_id` = ?", $args['user']->ID);
         }
     }
 }
diff --git a/plugins/kolab_activesync/kolab_activesync_ui.php b/plugins/kolab_activesync/kolab_activesync_ui.php
index f39f39d9..45122b04 100644
--- a/plugins/kolab_activesync/kolab_activesync_ui.php
+++ b/plugins/kolab_activesync/kolab_activesync_ui.php
@@ -1,324 +1,325 @@
 <?php
 
 /**
  * ActiveSync configuration user interface builder
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2011-2013, Kolab Systems AG <contact@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_activesync_ui
 {
     private $rc;
     private $plugin;
     private $force_subscriptions = array();
     public  $device = array();
 
     const SETUP_URL = 'https://kb.kolabenterprise.com/documentation/setting-up-an-activesync-client';
 
 
     public function __construct($plugin)
     {
         $this->plugin    = $plugin;
         $this->rc        = rcube::get_instance();
         $skin_path       = $this->plugin->local_skin_path() . '/';
         $this->skin_path = 'plugins/kolab_activesync/' . $skin_path;
 
         $this->plugin->load_config();
         $this->force_subscriptions = $this->rc->config->get('activesync_force_subscriptions', array());
 
         $this->plugin->include_stylesheet($skin_path . 'config.css');
     }
 
     public function device_list($attrib = array())
     {
         $attrib += array('id' => 'devices-list');
 
         $devices = $this->plugin->list_devices();
         $table   = new html_table();
 
         foreach ($devices as $id => $device) {
             $name = $device['ALIAS'] ? $device['ALIAS'] : $id;
             $table->add_row(array('id' => 'rcmrow' . $id));
             $table->add(null, html::span('devicealias', rcube::Q($name))
                 . ' ' . html::span('devicetype secondary', rcube::Q($device['TYPE'])));
         }
 
         $this->rc->output->add_gui_object('devicelist', $attrib['id']);
         $this->rc->output->set_env('devicecount', count($devices));
 
         $this->rc->output->include_script('list.js');
 
         return $table->show($attrib);
     }
 
 
     public function device_config_form($attrib = array())
     {
         $table = new html_table(array('cols' => 2));
 
         $field_id = 'config-device-alias';
         $input = new html_inputfield(array('name' => 'devicealias', 'id' => $field_id, 'size' => 40));
         $table->add('title', html::label($field_id, $this->plugin->gettext('devicealias')));
         $table->add(null, $input->show($this->device['ALIAS'] ? $this->device['ALIAS'] : $this->device['_id']));
 
         // read-only device information
         $info = $this->plugin->device_info($this->device['ID']);
 
         if (!empty($info)) {
             foreach ($info as $key => $value) {
                 if ($value) {
                     $table->add('title', html::label(null, rcube::Q($this->plugin->gettext($key))));
                     $table->add(null, rcube::Q($value));
                 }
             }
         }
 
         if ($attrib['form']) {
             $this->rc->output->add_gui_object('editform', $attrib['form']);
         }
 
         return $table->show($attrib);
     }
 
 
     private function is_protected($folder, $devicetype)
     {
         $devicetype = strtolower($devicetype);
         if (array_key_exists($devicetype, $this->force_subscriptions)) {
             return array_key_exists($folder, $this->force_subscriptions[$devicetype]);
         }
         return false;
     }
 
     public function folder_subscriptions($attrib = array())
     {
         if (!$attrib['id']) {
             $attrib['id'] = 'foldersubscriptions';
         }
 
         // group folders by type (show only known types)
         $folder_groups = array('mail' => array(), 'contact' => array(), 'event' => array(), 'task' => array(), 'note' => array());
         $folder_types  = kolab_storage::folders_typedata();
         $use_fieldsets = rcube_utils::get_boolean($attrib['use-fieldsets']);
         $imei          = $this->device['_id'];
         $subscribed    = array();
 
         if ($imei) {
             $folder_meta = $this->plugin->folder_meta();
         }
 
         $devicetype = strtolower($this->device['TYPE']);
-        $device_force_subscriptions = $this->force_subscriptions[$devicetype];
+        $device_force_subscriptions = $this->force_subscriptions[$devicetype] ?? null;
 
         foreach ($this->plugin->list_folders() as $folder) {
-            if ($folder_types[$folder]) {
+            if ($folder_types[$folder] ?? null) {
                 list($type, ) = explode('.', $folder_types[$folder]);
             }
             else {
                 $type = 'mail';
             }
 
-            if (is_array($folder_groups[$type])) {
+            if (is_array($folder_groups[$type] ?? null)) {
                 $folder_groups[$type][] = $folder;
 
                 if ($device_force_subscriptions && array_key_exists($folder, $device_force_subscriptions)) {
-                    $subscribed[$folder] = intval($device_force_subscriptions[$folder]);
-                } else if (!empty($folder_meta) && ($meta = $folder_meta[$folder])
-                    && $meta['FOLDER'] && $meta['FOLDER'][$imei]['S']
+                    $subscribed[$folder] = intval($device_force_subscriptions[$folder] ?? null);
+                } else if (!empty($folder_meta) && ($meta = ($folder_meta[$folder] ?? null))
+                    && ($meta['FOLDER'] ?? false) && $meta['FOLDER'][$imei]['S']
                 ) {
                     $subscribed[$folder] = intval($meta['FOLDER'][$imei]['S']);
                 }
             }
         }
 
         // build block for every folder type
+        $html = null;
         foreach ($folder_groups as $type => $group) {
             if (empty($group)) {
                 continue;
             }
 
             $attrib['type'] = $type;
             $table = $this->folder_subscriptions_block($group, $attrib, $subscribed);
             $label = $this->plugin->gettext($type);
 
             if ($use_fieldsets) {
                 $html .= html::tag('fieldset', 'subscriptionblock', html::tag('legend', $type, $label) . $table);
             }
             else {
                 $html .= html::div('subscriptionblock', html::tag('h3', $type, $label) . $table);
             }
         }
 
         $this->rc->output->add_gui_object('subscriptionslist', $attrib['id']);
 
         return html::div($attrib, $html);
     }
 
     public function folder_subscriptions_block($a_folders, $attrib, $subscribed)
     {
         $alarms = ($attrib['type'] == 'event' || $attrib['type'] == 'task');
 
         $table = new html_table(array('cellspacing' => 0, 'class' => 'table-striped'));
         $table->add_header(array(
                 'class'    => 'subscription checkbox-cell',
                 'title'    => $this->plugin->gettext('synchronize'),
                 'tabindex' => 0
             ),
-            $attrib['syncicon'] ? html::img(array('src' => $this->skin_path . $attrib['syncicon'])) :
+            ($attrib['syncicon'] ?? false) ? html::img(array('src' => $this->skin_path . $attrib['syncicon'])) :
                 $this->plugin->gettext('synchronize')
         );
 
         if ($alarms) {
             $table->add_header(array(
                     'class'    => 'alarm checkbox-cell',
                     'title'    => $this->plugin->gettext('withalarms'),
                     'tabindex' => 0
                 ),
-                $attrib['alarmicon'] ? html::img(array('src' => $this->skin_path . $attrib['alarmicon'])) :
+                ($attrib['alarmicon'] ?? null) ? html::img(array('src' => $this->skin_path . $attrib['alarmicon'])) :
                     $this->plugin->gettext('withalarms')
             );
         }
 
         $table->add_header('foldername', $this->plugin->gettext('folder'));
 
         $checkbox_sync  = new html_checkbox(array('name' => 'subscribed[]', 'class' => 'subscription'));
         $checkbox_alarm = new html_checkbox(array('name' => 'alarm[]', 'class' => 'alarm'));
 
         $names = array();
         foreach ($a_folders as $folder) {
             $foldername = $origname = kolab_storage::object_prettyname($folder);
 
             // find folder prefix to truncate (the same code as in kolab_addressbook plugin)
             for ($i = count($names)-1; $i >= 0; $i--) {
                 if (strpos($foldername, $names[$i].' &raquo; ') === 0) {
                     $length = strlen($names[$i].' &raquo; ');
                     $prefix = substr($foldername, 0, $length);
                     $count  = count(explode(' &raquo; ', $prefix));
                     $foldername = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($foldername, $length);
                     break;
                 }
             }
 
             $folder_id = 'rcmf' . rcube_utils::html_identifier($folder);
             $names[] = $origname;
             $classes = array('mailbox');
 
             if ($folder_class = $this->rc->folder_classname($folder)) {
                 if ($this->rc->text_exists($folder_class)) {
                     $foldername = html::quote($this->rc->gettext($folder_class));
                 }
                 $classes[] = $folder_class;
             }
 
             $table->add_row();
 
             $disabled = $this->is_protected($folder, $this->device['TYPE']);
 
             $table->add('subscription checkbox-cell', $checkbox_sync->show(
                 !empty($subscribed[$folder]) ? $folder : null,
                 array('value' => $folder, 'id' => $folder_id, 'disabled' => $disabled)));
 
             if ($alarms) {
                 $table->add('alarm checkbox-cell', $checkbox_alarm->show(
-                    intval($subscribed[$folder]) > 1 ? $folder : null,
+                    intval($subscribed[$folder] ?? 0) > 1 ? $folder : null,
                     array('value' => $folder, 'id' => $folder_id.'_alarm', 'disabled' => $disabled)));
             }
 
             $table->add(join(' ', $classes), html::label($folder_id, $foldername));
         }
 
         return $table->show();
     }
 
     public function folder_options_table($folder_name, $devices, $type)
     {
         $alarms      = $type == 'event' || $type == 'task';
         $meta        = $this->plugin->folder_meta();
         $folder_data = (array) ($meta[$folder_name] ? $meta[$folder_name]['FOLDER'] : null);
 
         $table = new html_table(array('cellspacing' => 0, 'id' => 'folder-sync-options', 'class' => 'records-table'));
 
         // table header
         $table->add_header(array('class' => 'device'), $this->plugin->gettext('devicealias'));
         $table->add_header(array('class' => 'subscription'), $this->plugin->gettext('synchronize'));
         if ($alarms) {
             $table->add_header(array('class' => 'alarm'), $this->plugin->gettext('withalarms'));
         }
 
         // table records
         foreach ($devices as $id => $device) {
             $info     = $this->plugin->device_info($device['ID']);
             $name     = $id;
             $title    = '';
             $checkbox = new html_checkbox(array('name' => "_subscriptions[$id]", 'value' => 1,
                 'onchange' => 'return activesync_object.update_sync_data(this)'));
 
             if (!empty($info)) {
                 $_name = trim($info['friendlyname'] . ' ' . $info['os']);
                 $title = $info['useragent'];
 
                 if ($_name) {
                     $name .= " ($_name)";
                 }
             }
 
             $disabled = $this->is_protected($folder_name, $device['TYPE']);
 
             $table->add_row();
             $table->add(array('class' => 'device', 'title' => $title), $name);
             $table->add('subscription checkbox-cell', $checkbox->show(!empty($folder_data[$id]['S']) ? 1 : 0, array('disabled' => $disabled)));
 
             if ($alarms) {
                 $checkbox_alarm = new html_checkbox(array('name' => "_alarms[$id]", 'value' => 1,
                     'onchange' => 'return activesync_object.update_sync_data(this)'));
 
                 $table->add('alarm checkbox-cell', $checkbox_alarm->show($folder_data[$id]['S'] > 1 ? 1 : 0, array('disabled' => $disabled)));
             }
         }
 
         return $table->show();
     }
 
     /**
      * Displays initial page (when no devices are registered)
      */
     function init_message()
     {
         $this->plugin->load_config();
 
         $this->rc->output->add_handlers(array(
                 'initmessage' => array($this, 'init_message_content')
         ));
 
         $this->rc->output->send('kolab_activesync.configempty');
     }
 
     /**
      * Handler for initmessage template object
      */
     function init_message_content()
     {
         $url  = $this->rc->config->get('activesync_setup_url', self::SETUP_URL);
         $vars = array('url' => $url);
         $msg  = $this->plugin->gettext(array('name' => 'nodevices', 'vars' => $vars));
 
         return $msg;
     }
 }
diff --git a/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php b/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php
index 0c5e7dc1..53147f5c 100644
--- a/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php
+++ b/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php
@@ -1,1400 +1,1400 @@
 <?php
 
 /**
  * Backend class for a custom address book
  *
  * This part of the Roundcube+Kolab integration and connects the
  * rcube_addressbook interface with the kolab_storage wrapper from libkolab
  *
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2011, Kolab Systems AG <contact@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/>.
  *
  * @see rcube_addressbook
  */
 class kolab_contacts extends rcube_addressbook
 {
     public $primary_key = 'ID';
     public $rights   = 'lrs';
     public $readonly = true;
     public $undelete = true;
     public $groups = true;
     public $coltypes = array(
         'name'         => array('limit' => 1),
         'firstname'    => array('limit' => 1),
         'surname'      => array('limit' => 1),
         'middlename'   => array('limit' => 1),
         'prefix'       => array('limit' => 1),
         'suffix'       => array('limit' => 1),
         'nickname'     => array('limit' => 1),
         'jobtitle'     => array('limit' => 1),
         'organization' => array('limit' => 1),
         'department'   => array('limit' => 1),
         'email'        => array('subtypes' => array('home','work','other')),
         'phone'        => array(),
         'address'      => array('subtypes' => array('home','work','office')),
         'website'      => array('subtypes' => array('homepage','blog')),
         'im'           => array('subtypes' => null),
         'gender'       => array('limit' => 1),
         'birthday'     => array('limit' => 1),
         'anniversary'  => array('limit' => 1),
         'profession'   => array(
             'type'      => 'text',
             'size'      => 40,
             'maxlength' => 80,
             'limit'     => 1,
             'label'     => 'kolab_addressbook.profession',
             'category'  => 'personal'
         ),
         'manager'      => array('limit' => null),
         'assistant'    => array('limit' => null),
         'spouse'       => array('limit' => 1),
         'children'     => array(
             'type'      => 'text',
             'size'      => 40,
             'maxlength' => 80,
             'limit'     => null,
             'label'     => 'kolab_addressbook.children',
             'category'  => 'personal'
         ),
         'freebusyurl'  => array(
             'type'  => 'text',
             'size'  => 40,
             'limit' => 1,
             'label' => 'kolab_addressbook.freebusyurl'
         ),
         'pgppublickey' => array(
             'type' => 'textarea',
             'size' => 70,
             'rows' => 10,
             'limit' => 1,
             'label' => 'kolab_addressbook.pgppublickey'
         ),
         'pkcs7publickey' => array(
             'type' => 'textarea',
             'size' => 70,
             'rows' => 10,
             'limit' => 1,
             'label' => 'kolab_addressbook.pkcs7publickey'
         ),
         'notes'        => array('limit' => 1),
         'photo'        => array('limit' => 1),
         // TODO: define more Kolab-specific fields such as: language, latitude, longitude, crypto settings
     );
 
     /**
      * vCard additional fields mapping
      */
     public $vcard_map = array(
         'profession'     => 'X-PROFESSION',
         'officelocation' => 'X-OFFICE-LOCATION',
         'initials'       => 'X-INITIALS',
         'children'       => 'X-CHILDREN',
         'freebusyurl'    => 'X-FREEBUSY-URL',
         'pgppublickey'   => 'KEY',
     );
 
     /**
      * List of date type fields
      */
     public $date_cols = array('birthday', 'anniversary');
 
     private $gid;
     private $storagefolder;
     private $dataset;
     private $sortindex;
     private $contacts;
     private $distlists;
     private $groupmembers;
     private $filter;
     private $result;
     private $namespace;
     private $imap_folder = 'INBOX/Contacts';
     private $action;
 
     // list of fields used for searching in "All fields" mode
     private $search_fields = array(
         'name',
         'firstname',
         'surname',
         'middlename',
         'prefix',
         'suffix',
         'nickname',
         'jobtitle',
         'organization',
         'department',
         'email',
         'phone',
         'address',
         'profession',
         'manager',
         'assistant',
         'spouse',
         'children',
         'notes',
     );
 
 
     public function __construct($imap_folder = null)
     {
         if ($imap_folder) {
             $this->imap_folder = $imap_folder;
         }
 
         // extend coltypes configuration 
         $format = kolab_format::factory('contact');
 
         $this->coltypes['phone']['subtypes']   = array_keys($format->phonetypes);
         $this->coltypes['address']['subtypes'] = array_keys($format->addresstypes);
 
         $rcube = rcube::get_instance();
 
         // set localized labels for proprietary cols
         foreach ($this->coltypes as $col => $prop) {
-            if (is_string($prop['label'])) {
+            if (is_string($prop['label'] ?? null)) {
                 $this->coltypes[$col]['label'] = $rcube->gettext($prop['label']);
             }
         }
 
         // fetch objects from the given IMAP folder
         $this->storagefolder = kolab_storage::get_folder($this->imap_folder);
         $this->ready = $this->storagefolder && !PEAR::isError($this->storagefolder);
 
         // Set readonly and rights flags according to folder permissions
         if ($this->ready) {
             if ($this->storagefolder->get_owner() == $_SESSION['username']) {
                 $this->readonly = false;
                 $this->rights = 'lrswikxtea';
             }
             else {
                 $rights = $this->storagefolder->get_myrights();
                 if ($rights && !PEAR::isError($rights)) {
                     $this->rights = $rights;
                     if (strpos($rights, 'i') !== false && strpos($rights, 't') !== false) {
                         $this->readonly = false;
                     }
                 }
             }
         }
 
         $this->action = rcube::get_instance()->action;
     }
 
     /**
      * Getter for the address book name to be displayed
      *
      * @return string Name of this address book
      */
     public function get_name()
     {
         return $this->storagefolder->get_name();
     }
 
     /**
      * Wrapper for kolab_storage_folder::get_foldername()
      */
     public function get_foldername()
     {
         return $this->storagefolder->get_foldername();
     }
 
     /**
      * Getter for the IMAP folder name
      *
      * @return string Name of the IMAP folder
      */
     public function get_realname()
     {
         return $this->imap_folder;
     }
 
     /**
      * 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 ($this->namespace === null && $this->ready) {
             $this->namespace = $this->storagefolder->get_namespace();
         }
 
         return $this->namespace;
     }
 
     /**
      * Getter for parent folder path
      *
      * @return string Full path to parent folder
      */
     public function get_parent()
     {
         return $this->storagefolder->get_parent();
     }
 
     /**
      * 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->imap_folder);
     }
 
     /**
      * Compose an URL for CardDAV access to this address book (if configured)
      */
     public function get_carddav_url()
     {
         $rcmail = rcmail::get_instance();
         if ($template = $rcmail->config->get('kolab_addressbook_carddav_url', null)) {
             return strtr($template, array(
                     '%h' => $_SERVER['HTTP_HOST'],
                     '%u' => urlencode($rcmail->get_user_name()),
                     '%i' => urlencode($this->storagefolder->get_uid()),
                     '%n' => urlencode($this->imap_folder),
             ));
         }
 
         return false;
     }
 
     /**
      * Setter for the current group
      */
     public function set_group($gid)
     {
         $this->gid = $gid;
     }
 
     /**
      * Save a search string for future listings
      *
      * @param mixed Search params to use in listing method, obtained by get_search_set()
      */
     public function set_search_set($filter)
     {
         $this->filter = $filter;
     }
 
     /**
      * Getter for saved search properties
      *
      * @return mixed Search properties used by this class
      */
     public function get_search_set()
     {
         return $this->filter;
     }
 
     /**
      * Reset saved results and search parameters
      */
     public function reset()
     {
         $this->result = null;
         $this->filter = null;
     }
 
     /**
      * List all active contact groups of this source
      *
      * @param string Optional search string to match group name
      * @param int    Search mode. Sum of self::SEARCH_*
      *
      * @return array Indexed list of contact groups, each a hash array
      */
     function list_groups($search = null, $mode = 0)
     {
         $this->_fetch_groups();
         $groups = array();
 
         foreach ((array)$this->distlists as $group) {
             if (!$search || strstr(mb_strtolower($group['name']), mb_strtolower($search))) {
                 $groups[$group['ID']] = array('ID' => $group['ID'], 'name' => $group['name']);
             }
         }
 
         // sort groups by name
         uasort($groups, function($a, $b) { return strcoll($a['name'], $b['name']); });
 
         return array_values($groups);
     }
 
     /**
      * List the current set of contact records
      *
      * @param array List of cols to show
      * @param int   Only return this number of records, use negative values for tail
      * @param bool  True to skip the count query (select only)
      *
      * @return array Indexed list of contact records, each a hash array
      */
     public function list_records($cols = null, $subset = 0, $nocount = false)
     {
         $this->result = new rcube_result_set(0, ($this->list_page-1) * $this->page_size);
 
         $fetch_all = false;
         $fast_mode = !empty($cols) && is_array($cols);
 
         // list member of the selected group
         if ($this->gid) {
             $this->_fetch_groups();
 
             $this->sortindex = array();
             $this->contacts  = array();
             $local_sortindex = array();
             $uids            = array();
 
             // get members with email specified
             foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
                 // skip member that don't match the search filter
                 if (!empty($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false) {
                     continue;
                 }
 
                 if (!empty($member['uid'])) {
                     $uids[] = $member['uid'];
                 }
                 else if (!empty($member['email'])) {
                     $this->contacts[$member['ID']] = $member;
                     $local_sortindex[$member['ID']] = $this->_sort_string($member);
                     $fetch_all = true;
                 }
             }
 
             // get members by UID
             if (!empty($uids)) {
                 $this->_fetch_contacts($query = array(array('uid', '=', $uids)), $fetch_all ? false : count($uids), $fast_mode);
                 $this->sortindex = array_merge($this->sortindex, $local_sortindex);
             }
         }
         else if (is_array($this->filter['ids'])) {
             $ids = $this->filter['ids'];
             if (count($ids)) {
                 $uids = array_map(array($this, 'id2uid'), $this->filter['ids']);
                 $this->_fetch_contacts($query = array(array('uid', '=', $uids)), count($ids), $fast_mode);
             }
         }
         else {
             $this->_fetch_contacts($query = 'contact', true, $fast_mode);
         }
 
         if ($fetch_all) {
             // sort results (index only)
             asort($this->sortindex, SORT_LOCALE_STRING);
             $ids = array_keys($this->sortindex);
 
             // fill contact data into the current result set
             $this->result->count = count($ids);
             $start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
             $last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $this->result->count);
 
             for ($i = $start_row; $i < $last_row; $i++) {
                 if (array_key_exists($i, $ids)) {
                     $idx = $ids[$i];
                     $this->result->add($this->contacts[$idx] ?: $this->_to_rcube_contact($this->dataset[$idx]));
                 }
             }
         }
         else if (!empty($this->dataset)) {
             // get all records count, skip the query if possible
             if (!isset($query) || count($this->dataset) < $this->page_size) {
                 $this->result->count = count($this->dataset) + $this->page_size * ($this->list_page - 1);
             }
             else {
                 $this->result->count = $this->storagefolder->count($query);
             }
 
             $start_row = $subset < 0 ? $this->page_size + $subset : 0;
             $last_row  = min($subset != 0 ? $start_row + abs($subset) : $this->page_size, $this->result->count);
 
             for ($i = $start_row; $i < $last_row; $i++) {
                 $this->result->add($this->_to_rcube_contact($this->dataset[$i]));
             }
         }
 
         return $this->result;
     }
 
     /**
      * Search records
      *
      * @param mixed $fields   The field name of array of field names to search in
      * @param mixed $value    Search value (or array of values when $fields is array)
      * @param int   $mode     Matching mode:
      *                        0 - partial (*abc*),
      *                        1 - strict (=),
      *                        2 - prefix (abc*)
      *                        4 - include groups (if supported)
      * @param bool  $select   True if results are requested, False if count only
      * @param bool  $nocount  True to skip the count query (select only)
      * @param array $required List of fields that cannot be empty
      *
      * @return rcube_result_set List of contact records and 'count' value
      */
     public function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
     {
         // search by ID
         if ($fields == $this->primary_key) {
             $ids    = !is_array($value) ? explode(',', $value) : $value;
             $result = new rcube_result_set();
 
             foreach ($ids as $id) {
                 if ($rec = $this->get_record($id, true)) {
                     $result->add($rec);
                     $result->count++;
                 }
             }
             return $result;
         }
         else if ($fields == '*') {
             $fields = $this->search_fields;
         }
 
         if (!is_array($fields)) {
             $fields = array($fields);
         }
         if (!is_array($required) && !empty($required)) {
             $required = array($required);
         }
 
         // advanced search
         if (is_array($value)) {
             $advanced = true;
             $value = array_map('mb_strtolower', $value);
         }
         else {
             $value = mb_strtolower($value);
         }
 
         $scount = count($fields);
         // build key name regexp
         $regexp = '/^(' . implode('|', $fields) . ')(?:.*)$/';
 
         // pass query to storage if only indexed cols are involved
         // NOTE: this is only some rough pre-filtering but probably includes false positives
         $squery = $this->_search_query($fields, $value, $mode);
 
         // add magic selector to select contacts with birthday dates only
         if (in_array('birthday', $required)) {
             $squery[] = array('tags', '=', 'x-has-birthday');
         }
 
         $squery[] = array('type', '=', 'contact');
 
         // get all/matching records
         $this->_fetch_contacts($squery);
 
         // save searching conditions
         $this->filter = array('fields' => $fields, 'value' => $value, 'mode' => $mode, 'ids' => array());
 
         // search by iterating over all records in dataset
         foreach ($this->dataset as $record) {
             $contact = $this->_to_rcube_contact($record);
             $id = $contact['ID'];
 
             // check if current contact has required values, otherwise skip it
             if ($required) {
                 foreach ($required as $f) {
                     // required field might be 'email', but contact might contain 'email:home'
                     if (!($v = rcube_addressbook::get_col_values($f, $contact, true)) || empty($v)) {
                         continue 2;
                     }
                 }
             }
 
             $found = array();
             $contents = '';
             foreach (preg_grep($regexp, array_keys($contact)) as $col) {
                 $pos     = strpos($col, ':');
                 $colname = $pos ? substr($col, 0, $pos) : $col;
 
                 foreach ((array)$contact[$col] as $val) {
                     if ($advanced) {
                         $found[$colname] = $this->compare_search_value($colname, $val, $value[array_search($colname, $fields)], $mode);
                     }
                     else {
                         $contents .= ' ' . join(' ', (array)$val);
                     }
                 }
             }
 
             // compare matches
             if (($advanced && count($found) >= $scount) ||
                 (!$advanced && rcube_utils::words_match(mb_strtolower($contents), $value))) {
                 $this->filter['ids'][] = $id;
             }
         }
 
         // dummy result with contacts count
         if (!$select) {
             return new rcube_result_set(count($this->filter['ids']), ($this->list_page-1) * $this->page_size);
         }
 
         // list records (now limited by $this->filter)
         return $this->list_records();
     }
 
     /**
      * Refresh saved search results after data has changed
      */
     public function refresh_search()
     {
         if ($this->filter) {
             $this->search($this->filter['fields'], $this->filter['value'], $this->filter['mode']);
         }
 
         return $this->get_search_set();
     }
 
     /**
      * Count number of available contacts in database
      *
      * @return rcube_result_set Result set with values for 'count' and 'first'
      */
     public function count()
     {
         if ($this->gid) {
             $this->_fetch_groups();
             $count = count($this->distlists[$this->gid]['member']);
         }
         else if (is_array($this->filter['ids'])) {
             $count = count($this->filter['ids']);
         }
         else {
             $count = $this->storagefolder->count('contact');
         }
 
         return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
     }
 
     /**
      * Return the last result set
      *
      * @return rcube_result_set Current result set or NULL if nothing selected yet
      */
     public function get_result()
     {
         return $this->result;
     }
 
     /**
      * Get a specific contact record
      *
      * @param mixed Record identifier(s)
      * @param bool  True to return record as associative array, otherwise a result set is returned
      *
      * @return mixed Result object with all record fields or False if not found
      */
     public function get_record($id, $assoc = false)
     {
         $rec = null;
         $uid = $this->id2uid($id);
         $rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
 
         if (strpos($uid, 'mailto:') === 0) {
             $this->_fetch_groups(true);
             $rec = $this->contacts[$id];
             $this->readonly = true;  // set source to read-only
         }
         else if (!empty($rev)) {
             $rcmail = rcube::get_instance();
             $plugin = $rcmail->plugins->get_plugin('kolab_addressbook');
             if ($plugin && ($object = $plugin->get_revision($id, kolab_storage::id_encode($this->imap_folder), $rev))) {
                 $rec = $this->_to_rcube_contact($object);
                 $rec['rev'] = $rev;
             }
             $this->readonly = true;  // set source to read-only
         }
         else if ($object = $this->storagefolder->get_object($uid)) {
             $rec = $this->_to_rcube_contact($object);
         }
 
         if ($rec) {
             $this->result = new rcube_result_set(1);
             $this->result->add($rec);
             return $assoc ? $rec : $this->result;
         }
 
         return false;
     }
 
     /**
      * Get group assignments of a specific contact record
      *
      * @param mixed Record identifier
      *
      * @return array List of assigned groups as ID=>Name pairs
      */
     public function get_record_groups($id)
     {
         $out = array();
         $this->_fetch_groups();
 
         if (!empty($this->groupmembers[$id])) {
             foreach ((array) $this->groupmembers[$id] as $gid) {
                 if (!empty($this->distlists[$gid])) {
                     $group = $this->distlists[$gid];
                     $out[$gid] = $group['name'];
                 }
             }
         }
 
         return $out;
     }
 
     /**
      * Create a new contact record
      *
      * @param array Associative array with save data
      *  Keys:   Field name with optional section in the form FIELD:SECTION
      *  Values: Field value. Can be either a string or an array of strings for multiple values
      * @param bool  True to check for duplicates first
      *
      * @return mixed The created record ID on success, False on error
      */
     public function insert($save_data, $check=false)
     {
         if (!is_array($save_data)) {
             return false;
         }
 
         $insert_id = $existing = false;
 
         // check for existing records by e-mail comparison
         if ($check) {
             foreach ($this->get_col_values('email', $save_data, true) as $email) {
                 if (($res = $this->search('email', $email, true, false)) && $res->count) {
                     $existing = true;
                     break;
                 }
             }
         }
 
         if (!$existing) {
             // remove existing id attributes (#1101)
             unset($save_data['ID'], $save_data['uid']);
 
             // generate new Kolab contact item
             $object = $this->_from_rcube_contact($save_data);
             $saved  = $this->storagefolder->save($object, 'contact');
 
             if (!$saved) {
                 rcube::raise_error(array(
                   'code' => 600, 'type' => 'php',
                   'file' => __FILE__, 'line' => __LINE__,
                   'message' => "Error saving contact object to Kolab server"),
                 true, false);
             }
             else {
                 $insert_id = $this->uid2id($object['uid']);
             }
         }
 
         return $insert_id;
     }
 
     /**
      * Update a specific contact record
      *
      * @param mixed Record identifier
      * @param array Associative array with save data
      *  Keys:   Field name with optional section in the form FIELD:SECTION
      *  Values: Field value. Can be either a string or an array of strings for multiple values
      *
      * @return bool True on success, False on error
      */
     public function update($id, $save_data)
     {
         $updated = false;
         if ($old = $this->storagefolder->get_object($this->id2uid($id))) {
             $object = $this->_from_rcube_contact($save_data, $old);
 
             if (!$this->storagefolder->save($object, 'contact', $old['uid'])) {
                 rcube::raise_error(array(
                         'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                         'message' => "Error saving contact object to Kolab server"
                     ),
                     true, false
                 );
             }
             else {
                 $updated = true;
 
                 // TODO: update data in groups this contact is member of
             }
         }
 
         return $updated;
     }
 
     /**
      * Mark one or more contact records as deleted
      *
      * @param array Record identifiers
      * @param bool  Remove record(s) irreversible (mark as deleted otherwise)
      *
      * @return int Number of records deleted
      */
     public function delete($ids, $force=true)
     {
         $this->_fetch_groups();
 
         if (!is_array($ids)) {
             $ids = explode(',', $ids);
         }
 
         $count = 0;
         foreach ($ids as $id) {
             if ($uid = $this->id2uid($id)) {
                 $is_mailto = strpos($uid, 'mailto:') === 0;
                 $deleted = $is_mailto || $this->storagefolder->delete($uid, $force);
 
                 if (!$deleted) {
                     rcube::raise_error(array(
                             'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                             'message' => "Error deleting a contact object $uid from the Kolab server"
                         ),
                         true, false
                     );
                 }
                 else {
                     // remove from distribution lists
                     foreach ((array) $this->groupmembers[$id] as $gid) {
                         if (!$is_mailto || $gid == $this->gid) {
                             $this->remove_from_group($gid, $id);
                         }
                     }
 
                     // clear internal cache
                     unset($this->groupmembers[$id]);
                     $count++;
                 }
             }
         }
 
         return $count;
     }
 
     /**
      * Undelete one or more contact records.
      * Only possible just after delete (see 2nd argument of delete() method).
      *
      * @param array Record identifiers
      *
      * @return int Number of records restored
      */
     public function undelete($ids)
     {
         if (!is_array($ids)) {
             $ids = explode(',', $ids);
         }
 
         $count = 0;
         foreach ($ids as $id) {
             $uid = $this->id2uid($id);
             if ($this->storagefolder->undelete($uid)) {
                 $count++;
             }
             else {
                 rcube::raise_error(array(
                         'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                         'message' => "Error undeleting a contact object $uid from the Kolab server"
                     ),
                     true, false
                 );
             }
         }
 
         return $count;
     }
 
     /**
      * Remove all records from the database
      *
      * @param bool $with_groups Remove also groups
      */
     public function delete_all($with_groups = false)
     {
         if ($this->storagefolder->delete_all()) {
             $this->contacts  = array();
             $this->sortindex = array();
             $this->dataset   = null;
             $this->result    = null;
         }
     }
 
     /**
      * Close connection to source
      * Called on script shutdown
      */
     public function close()
     {
     }
 
     /**
      * Create a contact group with the given name
      *
      * @param string The group name
      *
      * @return mixed False on error, array with record props in success
      */
     function create_group($name)
     {
         $this->_fetch_groups();
         $result = false;
 
         $list = array(
             'name' => $name,
             'member' => array(),
         );
         $saved = $this->storagefolder->save($list, 'distribution-list');
 
         if (!$saved) {
             rcube::raise_error(array(
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Error saving distribution-list object to Kolab server"
                 ),
                 true, false
             );
             return false;
         }
         else {
             $id = $this->uid2id($list['uid']);
             $this->distlists[$id] = $list;
             $result = array('id' => $id, 'name' => $name);
         }
 
         return $result;
     }
 
     /**
      * Delete the given group and all linked group members
      *
      * @param string Group identifier
      *
      * @return bool True on success, false if no data was changed
      */
     function delete_group($gid)
     {
         $this->_fetch_groups();
         $result = false;
 
         if ($list = $this->distlists[$gid]) {
             $deleted = $this->storagefolder->delete($list['uid']);
         }
 
         if (!$deleted) {
             rcube::raise_error(array(
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Error deleting distribution-list object from the Kolab server"
                 ),
                 true, false
             );
         }
         else {
             $result = true;
         }
 
         return $result;
     }
 
     /**
      * Rename a specific contact group
      *
      * @param string Group identifier
      * @param string New name to set for this group
      * @param string New group identifier (if changed, otherwise don't set)
      *
      * @return bool New name on success, false if no data was changed
      */
     function rename_group($gid, $newname, &$newid)
     {
         $this->_fetch_groups();
         $list = $this->distlists[$gid];
 
         if ($newname != $list['name']) {
             $list['name'] = $newname;
             $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
         }
 
         if (!$saved) {
             rcube::raise_error(array(
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Error saving distribution-list object to Kolab server"
                 ),
                 true, false
             );
             return false;
         }
 
         return $newname;
     }
 
     /**
      * Add the given contact records the a certain group
      *
      * @param string Group identifier
      * @param array  List of contact identifiers to be added
      * @return int   Number of contacts added
      */
     function add_to_group($gid, $ids)
     {
         if (!is_array($ids)) {
             $ids = explode(',', $ids);
         }
 
         $this->_fetch_groups(true);
 
         $list   = $this->distlists[$gid];
         $added  = 0;
         $uids   = array();
         $exists = array();
 
         foreach ((array)$list['member'] as $member) {
             $exists[] = $member['ID'];
         }
 
         // substract existing assignments from list
         $ids = array_unique(array_diff($ids, $exists));
 
         // add mailto: members
         foreach ($ids as $contact_id) {
             $uid = $this->id2uid($contact_id);
             if (strpos($uid, 'mailto:') === 0 && ($contact = $this->contacts[$contact_id])) {
                 $list['member'][] = array(
                     'email' => $contact['email'],
                     'name'  => $contact['name'],
                 );
                 $this->groupmembers[$contact_id][] = $gid;
                 $added++;
             }
             else {
                 $uids[$uid] = $contact_id;
             }
         }
 
         // add members with UID
         if (!empty($uids)) {
             foreach ($uids as $uid => $contact_id) {
                 $list['member'][] = array('uid' => $uid);
                 $this->groupmembers[$contact_id][] = $gid;
                 $added++;
             }
         }
 
         if ($added) {
             $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
         }
         else {
             $saved = true;
         }
 
         if (!$saved) {
             rcube::raise_error(array(
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Error saving distribution-list to Kolab server"
                 ),
                 true, false
             );
 
             $added = false;
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
         }
         else {
             $this->distlists[$gid] = $list;
         }
 
         return $added;
     }
 
     /**
      * Remove the given contact records from a certain group
      *
      * @param string Group identifier
      * @param array  List of contact identifiers to be removed
      * @return int   Number of deleted group members
      */
     function remove_from_group($gid, $ids)
     {
         if (!is_array($ids)) {
             $ids = explode(',', $ids);
         }
 
         $this->_fetch_groups();
         if (!($list = $this->distlists[$gid])) {
             return false;
         }
 
         $new_member = array();
         foreach ((array)$list['member'] as $member) {
             if (!in_array($member['ID'], $ids)) {
                 $new_member[] = $member;
             }
         }
 
         // write distribution list back to server
         $list['member'] = $new_member;
         $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
 
         if (!$saved) {
             rcube::raise_error(array(
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Error saving distribution-list object to Kolab server"
                 ),
                 true, false
             );
         }
         else {
             // remove group assigments in local cache
             foreach ($ids as $id) {
                 $j = array_search($gid, $this->groupmembers[$id]);
                 unset($this->groupmembers[$id][$j]);
             }
             $this->distlists[$gid] = $list;
             return true;
         }
 
         return false;
     }
 
     /**
      * Check the given data before saving.
      * If input not valid, the message to display can be fetched using get_error()
      *
      * @param array Associative array with contact data to save
      * @param bool  Attempt to fix/complete data automatically
      *
      * @return bool True if input is valid, False if not.
      */
     public function validate(&$save_data, $autofix = false)
     {
         // validate e-mail addresses
         $valid = parent::validate($save_data);
 
         // require at least one e-mail address if there's no name
         // (syntax check is already done)
         if ($valid) {
             if (!strlen($save_data['name'])
                 && !strlen($save_data['organization'])
                 && !array_filter($this->get_col_values('email', $save_data, true))
             ) {
                 $this->set_error('warning', 'kolab_addressbook.noemailnamewarning');
                 $valid = false;
             }
         }
 
         return $valid;
     }
 
     /**
      * Query storage layer and store records in private member var
      */
     private function _fetch_contacts($query = array(), $limit = false, $fast_mode = false)
     {
         if (!isset($this->dataset) || !empty($query)) {
             if ($limit) {
                 $size = is_int($limit) && $limit < $this->page_size ? $limit : $this->page_size;
                 $this->storagefolder->set_order_and_limit($this->_sort_columns(), $size, ($this->list_page-1) * $this->page_size);
             }
 
             $this->sortindex = array();
             $this->dataset   = $this->storagefolder->select($query, $fast_mode);
 
             foreach ($this->dataset as $idx => $record) {
                 $contact = $this->_to_rcube_contact($record);
                 $this->sortindex[$idx] = $this->_sort_string($contact);
             }
         }
     }
 
     /**
      * Extract a string for sorting from the given contact record
      */
     private function _sort_string($rec)
     {
         $str = '';
 
         switch ($this->sort_col) {
         case 'name':
             $str = $rec['name'] . $rec['prefix'];
         case 'firstname':
             $str .= $rec['firstname'] . $rec['middlename'] . $rec['surname'];
             break;
 
         case 'surname':
             $str = $rec['surname'] . $rec['firstname'] . $rec['middlename'];
             break;
 
         default:
             $str = $rec[$this->sort_col];
             break;
         }
 
         $str .= is_array($rec['email']) ? $rec['email'][0] : $rec['email'];
         return mb_strtolower($str);
     }
 
     /**
      * Return the cache table columns to order by
      */
     private function _sort_columns()
     {
         $sortcols = array();
 
         switch ($this->sort_col) {
         case 'name':
             $sortcols[] = 'name';
         case 'firstname':
             $sortcols[] = 'firstname';
             break;
 
         case 'surname':
             $sortcols[] = 'surname';
             break;
         }
 
         $sortcols[] = 'email';
         return $sortcols;
     }
 
     /**
      * Read distribution-lists AKA groups from server
      */
     private function _fetch_groups($with_contacts = false)
     {
         if (!isset($this->distlists)) {
             $this->distlists = $this->groupmembers = array();
             foreach ($this->storagefolder->select('distribution-list', true) as $record) {
                 $record['ID'] = $this->uid2id($record['uid']);
                 foreach ((array)$record['member'] as $i => $member) {
                     $mid = $this->uid2id($member['uid'] ? $member['uid'] : 'mailto:' . $member['email']);
                     $record['member'][$i]['ID'] = $mid;
                     $record['member'][$i]['readonly'] = empty($member['uid']);
                     $this->groupmembers[$mid][] = $record['ID'];
 
                     if ($with_contacts && empty($member['uid'])) {
                         $this->contacts[$mid] = $record['member'][$i];
                     }
                 }
                 $this->distlists[$record['ID']] = $record;
             }
         }
     }
 
     /**
      * Encode object UID into a safe identifier
      */
     public function uid2id($uid)
     {
         return rtrim(strtr(base64_encode($uid), '+/', '-_'), '=');
     }
 
     /**
      * Convert Roundcube object identifier back into the original UID
      */
     public function id2uid($id)
     {
         return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
     }
 
     /**
      * Build SQL query for fulltext matches
      */
     private function _search_query($fields, $value, $mode)
     {
         $query = array();
         $cols  = array();
 
         // $fulltext_cols might contain composite field names e.g. 'email:address' while $fields not
         foreach (kolab_format_contact::$fulltext_cols as $col) {
             if ($pos = strpos($col, ':')) {
                 $col = substr($col, 0, $pos);
             }
             if (in_array($col, $fields)) {
                 $cols[] = $col;
             }
         }
 
         if (count($cols) == count($fields)) {
             if ($mode & rcube_addressbook::SEARCH_STRICT) {
                 $prefix = '^'; $suffix = '$';
             }
             else if ($mode & rcube_addressbook::SEARCH_PREFIX) {
                 $prefix = '^'; $suffix = '';
             }
             else {
                 $prefix = ''; $suffix = '';
             }
 
             $search_string = is_array($value) ? join(' ', $value) : $value;
             foreach (rcube_utils::normalize_string($search_string, true) as $word) {
                 $query[] = array('words', 'LIKE', $prefix . $word . $suffix);
             }
         }
 
         return $query;
     }
 
     /**
      * Map fields from internal Kolab_Format to Roundcube contact format
      */
     private function _to_rcube_contact($record)
     {
         $record['ID'] = $this->uid2id($record['uid']);
 
         // convert email, website, phone values
         foreach (array('email'=>'address', 'website'=>'url', 'phone'=>'number') as $col => $propname) {
             if (is_array($record[$col])) {
                 $values = $record[$col];
                 unset($record[$col]);
                 foreach ((array)$values as $i => $val) {
                     $key = $col . ($val['type'] ? ':' . $val['type'] : '');
                     $record[$key][] = $val[$propname];
                 }
             }
         }
 
         if (is_array($record['address'])) {
             $addresses = $record['address'];
             unset($record['address']);
             foreach ($addresses as $i => $adr) {
                 $key = 'address' . ($adr['type'] ? ':' . $adr['type'] : '');
                 $record[$key][] = array(
                     'street'   => $adr['street'],
                     'locality' => $adr['locality'],
                     'zipcode'  => $adr['code'],
                     'region'   => $adr['region'],
                     'country'  => $adr['country'],
                 );
             }
         }
 
         // photo is stored as separate attachment
         if ($record['photo'] && strlen($record['photo']) < 255 && !empty($record['_attachments'][$record['photo']])) {
             $att = $record['_attachments'][$record['photo']];
             // only fetch photo content if requested
             if ($this->action == 'photo') {
                 if (!empty($att['content'])) {
                     $record['photo'] = $att['content'];
                 }
                 else {
                     $record['photo'] = $this->storagefolder->get_attachment($record['uid'], $att['id']);
                 }
             }
         }
 
         // truncate publickey value for display
         if (!empty($record['pgppublickey']) && $this->action == 'show') {
             $record['pgppublickey'] = substr($record['pgppublickey'], 0, 140) . '...';
         }
 
         // remove empty fields
         $record = array_filter($record);
 
         // remove kolab_storage internal data
         unset($record['_msguid'], $record['_formatobj'], $record['_mailbox'], $record['_type'], $record['_size']);
 
         return $record;
     }
 
     /**
      * Map fields from Roundcube format to internal kolab_format_contact properties
      */
     private function _from_rcube_contact($contact, $old = array())
     {
-        if (!$contact['uid'] && $contact['ID']) {
+        if (!($contact['uid'] ?? null) && ($contact['ID'] ?? null)) {
             $contact['uid'] = $this->id2uid($contact['ID']);
         }
-        else if (!$contact['uid'] && $old['uid']) {
+        else if (!($contact['uid'] ?? null) && ($old['uid'] ?? null)) {
             $contact['uid'] = $old['uid'];
         }
 
         $contact['im'] = array_filter($this->get_col_values('im', $contact, true));
 
         // convert email, website, phone values
         foreach (array('email'=>'address', 'website'=>'url', 'phone'=>'number') as $col => $propname) {
             $col_values = $this->get_col_values($col, $contact);
             $contact[$col] = array();
             foreach ($col_values as $type => $values) {
                 foreach ((array)$values as $val) {
                     if (!empty($val)) {
                         $contact[$col][] = array($propname => $val, 'type' => $type);
                     }
                 }
                 unset($contact[$col.':'.$type]);
             }
         }
 
         $addresses = array();
         foreach ($this->get_col_values('address', $contact) as $type => $values) {
             foreach ((array)$values as $adr) {
                 // skip empty address
                 $adr = array_filter($adr);
                 if (empty($adr)) {
                     continue;
                 }
 
                 $addresses[] = array(
                     'type' => $type,
                     'street' => $adr['street'],
                     'locality' => $adr['locality'],
                     'code' => $adr['zipcode'],
                     'region' => $adr['region'],
                     'country' => $adr['country'],
                 );
             }
 
             unset($contact['address:'.$type]);
         }
 
         $contact['address'] = $addresses;
 
         // categories are not supported in the web client but should be preserved (#2608)
-        $contact['categories'] = $old['categories'];
+        $contact['categories'] = $old['categories'] ?? null;
 
         // copy meta data (starting with _) from old object
         foreach ((array)$old as $key => $val) {
             if (!isset($contact[$key]) && $key[0] == '_') {
                 $contact[$key] = $val;
             }
         }
 
         // convert one-item-array elements into string element
         // this is needed e.g. to properly import birthday field
         foreach ($this->coltypes as $type => $col_def) {
-            if ($col_def['limit'] == 1 && is_array($contact[$type])) {
+            if (($col_def['limit'] ?? null) == 1 && is_array($contact[$type] ?? null)) {
                 $contact[$type] = array_shift(array_filter($contact[$type]));
             }
         }
 
         // When importing contacts 'vcard' data is added, we don't need it (Bug #1711)
         unset($contact['vcard']);
 
         // add empty values for some fields which can be removed in the UI
         return array_filter($contact) + array(
                 'nickname' => '',
                 'birthday' => '',
                 'anniversary' => '',
                 'freebusyurl' => '',
                 'photo' => $contact['photo']
             );
     }
 }
diff --git a/plugins/kolab_addressbook/drivers/kolab/kolab_contacts_driver.php b/plugins/kolab_addressbook/drivers/kolab/kolab_contacts_driver.php
index c29ae117..2f4635fc 100644
--- a/plugins/kolab_addressbook/drivers/kolab/kolab_contacts_driver.php
+++ b/plugins/kolab_addressbook/drivers/kolab/kolab_contacts_driver.php
@@ -1,275 +1,275 @@
 <?php
 
 /**
  * Backend class for a custom address book
  *
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@apheleia-it.ch>
  *
  * Copyright (C) 2011-2022, Apheleia IT AG <contact@apheleia-it.ch>
  *
  * 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/>.
  *
  * @see rcube_addressbook
  */
 class kolab_contacts_driver
 {
     protected $plugin;
     protected $rc;
 
     public function __construct($plugin)
     {
         $this->plugin = $plugin;
         $this->rc     = rcube::get_instance();
     }
  
     /**
      * List addressbook sources (folders)
      */
     public static function list_folders()
     {
         kolab_storage::$encode_ids = true;
 
         // get all folders that have "contact" type
         $folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact'));
 
         if (PEAR::isError($folders)) {
             rcube::raise_error([
                     'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Failed to list contact folders from Kolab server:" . $folders->getMessage()
                 ],
                 true, false);
 
             return [];
         }
 
         // we need at least one folder to prevent from errors in Roundcube core
         // when there's also no sql nor ldap addressbook (Bug #2086)
         if (empty($folders)) {
             if ($folder = kolab_storage::create_default_folder('contact')) {
                 $folders = [new kolab_storage_folder($folder, 'contact')];
             }
         }
 
         $sources = [];
         foreach ($folders as $folder) {
             $sources[$folder->id] = new kolab_contacts($folder->name);
         }
 
         return $sources;
     }
 
     /**
      * Getter for the rcube_addressbook instance
      *
      * @param string $id Addressbook (folder) ID
      *
      * @return ?kolab_contacts
      */
     public static function get_address_book($id)
     {
         $folderId = kolab_storage::id_decode($id);
         $folder   = kolab_storage::get_folder($folderId);
 
         // try with unencoded (old-style) identifier
         if ((!$folder || $folder->type != 'contact') && $folderId != $id) {
             $folder = kolab_storage::get_folder($id);
         }
 
         if ($folder && $folder->type == 'contact') {
             return new kolab_contacts($folder->name);
         }
     }
 
     /**
      * Delete address book folder
      *
      * @param string $source Addressbook identifier
      *
      * @return bool
      */
     public function folder_delete($folder)
     {
         $folderId = kolab_storage::id_decode($folder);
         $folder   = kolab_storage::get_folder($folderId);
 
         if ($folder && kolab_storage::folder_delete($folder->name)) {
             return $folderId;
         }
 
         return false;
     }
 
     /**
      * Address book folder form content for book create/edit
      *
      * @param string $action Action name (edit, create)
      * @param string $source Addressbook identifier
      *
      * @return string HTML output
      */
     public function folder_form($action, $source)
     {
         $hidden_fields[] = ['name' => '_source', 'value' => $source];
 
         $rcube   = rcube::get_instance();
         $folder  = rcube_charset::convert($source, RCUBE_CHARSET, 'UTF7-IMAP');
         $storage = $rcube->get_storage();
         $delim   = $storage->get_hierarchy_delimiter();
 
         if ($action == 'edit') {
             $path_imap = explode($delim, $folder);
             $name      = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');
             $path_imap = implode($delim, $path_imap);
         }
         else { // create
             $path_imap = $folder;
             $name      = '';
             $folder    = '';
         }
 
         // Store old name, get folder options
         if (strlen($folder)) {
             $hidden_fields[] = array('name' => '_oldname', 'value' => $folder);
 
             $options = $storage->folder_info($folder);
         }
 
         $form = array();
 
         // General tab
         $form['properties'] = array(
             'name'   => $rcube->gettext('properties'),
             'fields' => array(),
         );
 
         if (!empty($options) && ($options['norename'] || $options['protected'])) {
             $foldername = rcube::Q(str_replace($delim, ' &raquo; ', kolab_storage::object_name($folder)));
         }
         else {
             $foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30));
             $foldername = $foldername->show($name);
         }
 
         $form['properties']['fields']['name'] = array(
             'label' => $rcube->gettext('bookname', 'kolab_addressbook'),
             'value' => $foldername,
             'id'    => '_name',
         );
 
         if (!empty($options) && ($options['norename'] || $options['protected'])) {
             // prevent user from moving folder
             $hidden_fields[] = array('name' => '_parent', 'value' => $path_imap);
         }
         else {
             $prop   = array('name' => '_parent', 'id' => '_parent');
             $select = kolab_storage::folder_selector('contact', $prop, $folder);
 
             $form['properties']['fields']['parent'] = array(
                 'label' => $rcube->gettext('parentbook', 'kolab_addressbook'),
                 'value' => $select->show(strlen($folder) ? $path_imap : ''),
                 'id'    => '_parent',
             );
         }
 
         return kolab_utils::folder_form($form, $folder, 'calendar', $hidden_fields);
     }
 
     /**
      * Handler for address book create/edit form submit
      */
     public function folder_save()
     {
         $prop  = [
             'name'    => trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST)),
             'oldname' => trim(rcube_utils::get_input_value('_oldname', rcube_utils::INPUT_POST, true)), // UTF7-IMAP
             'parent'  => trim(rcube_utils::get_input_value('_parent', rcube_utils::INPUT_POST, true)), // UTF7-IMAP
             'type'    => 'contact',
             'subscribed' => true,
         ];
 
         $result = $error = false;
         $type = strlen($prop['oldname']) ? 'update' : 'create';
         $prop = $this->rc->plugins->exec_hook('addressbook_'.$type, $prop);
 
         if (!$prop['abort']) {
             if ($newfolder = kolab_storage::folder_update($prop)) {
                 $folder = $newfolder;
                 $result = true;
             }
             else {
                 $error = kolab_storage::$last_error;
             }
         }
         else {
             $result = $prop['result'];
             $folder = $prop['name'];
         }
 
         if ($result) {
             $kolab_folder = kolab_storage::get_folder($folder);
 
             // get folder/addressbook properties
             $abook = new kolab_contacts($folder);
             $props = $this->abook_prop(kolab_storage::folder_id($folder, true), $abook);
             $props['parent'] = kolab_storage::folder_id($kolab_folder->get_parent(), true);
 
             $this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation');
             $this->rc->output->command('book_update', $props, kolab_storage::folder_id($prop['oldname'], true));
         }
         else {
             if (!$error) {
                 $error = $plugin['message'] ? $plugin['message'] : 'kolab_addressbook.book'.$type.'error';
             }
 
             $this->rc->output->show_message($error, 'error');
         }
     }
 
     /**
      * Helper method to build a hash array of address book properties
      */
     public function abook_prop($id, $abook)
     {
-        if ($abook->virtual) {
+        if (property_exists($abook, 'virtual') && $abook->virtual) {
             return [
                 'id'       => $id,
                 'name'     => $abook->get_name(),
                 'listname' => $abook->get_foldername(),
                 'group'    => $abook instanceof kolab_storage_folder_user ? 'user' : $abook->get_namespace(),
                 'readonly' => true,
                 'rights'   => 'l',
                 'kolab'    => true,
                 'virtual'  => true,
             ];
         }
 
         return [
             'id'         => $id,
             'name'       => $abook->get_name(),
             'listname'   => $abook->get_foldername(),
             'readonly'   => $abook->readonly,
             'rights'     => $abook->rights,
             'groups'     => $abook->groups,
             'undelete'   => $abook->undelete && $this->rc->config->get('undo_timeout'),
             'realname'   => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
             'group'      => $abook->get_namespace(),
             'subscribed' => $abook->is_subscribed(),
             'carddavurl' => $abook->get_carddav_url(),
             'removable'  => true,
             'kolab'      => true,
             'audittrail' => !empty($this->plugin->bonnie_api),
         ];
     }
 }
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index 8a6f0729..2c239979 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -1,1142 +1,1142 @@
 <?php
 
 /**
  * Kolab address book
  *
  * Sample plugin to add a new address book source with data from Kolab storage
  * It provides also a possibilities to manage contact folders
  * (create/rename/delete/acl) directly in Addressbook UI.
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2011-2015, Kolab Systems AG <contact@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_addressbook extends rcube_plugin
 {
     public $task = '?(?!logout).*';
 
     public $driver;
     public $bonnie_api = false;
 
     private $sources;
     private $rc;
     private $ui;
     private $recurrent = false;
 
     const GLOBAL_FIRST = 0;
     const PERSONAL_FIRST = 1;
     const GLOBAL_ONLY = 2;
     const PERSONAL_ONLY = 3;
 
     /**
      * Startup method of a Roundcube plugin
      */
     public function init()
     {
         $this->rc = rcube::get_instance();
 
         // load required plugin
         $this->require_plugin('libkolab');
 
         $this->load_config();
 
         $driver       = $this->rc->config->get('kolab_addressbook_driver') ?: 'kolab';
         $driver_class = "{$driver}_contacts_driver";
 
         require_once dirname(__FILE__) . "/drivers/{$driver}/{$driver}_contacts_driver.php";
         require_once dirname(__FILE__) . "/drivers/{$driver}/{$driver}_contacts.php";
 
         $this->driver = new $driver_class($this);
 
         // register hooks
         $this->add_hook('addressbooks_list', array($this, 'address_sources'));
         $this->add_hook('addressbook_get', array($this, 'get_address_book'));
         $this->add_hook('config_get', array($this, 'config_get'));
 
         if ($this->rc->task == 'addressbook') {
             $this->add_texts('localization');
 
             if ($this->driver instanceof kolab_contacts_driver) {
                 $this->add_hook('contact_form', array($this, 'contact_form'));
                 $this->add_hook('contact_photo', array($this, 'contact_photo'));
             }
 
             $this->add_hook('template_object_directorylist', array($this, 'directorylist_html'));
 
             // Plugin actions
             $this->register_action('plugin.book', array($this, 'book_actions'));
             $this->register_action('plugin.book-save', array($this, 'book_save'));
             $this->register_action('plugin.book-search', array($this, 'book_search'));
             $this->register_action('plugin.book-subscribe', array($this, 'book_subscribe'));
 
             $this->register_action('plugin.contact-changelog', array($this, 'contact_changelog'));
             $this->register_action('plugin.contact-diff', array($this, 'contact_diff'));
             $this->register_action('plugin.contact-restore', array($this, 'contact_restore'));
 
             // get configuration for the Bonnie API
             $this->bonnie_api = libkolab::get_bonnie_api();
 
             // Load UI elements
             if ($this->api->output->type == 'html') {
                 require_once $this->home . '/lib/kolab_addressbook_ui.php';
                 $this->ui = new kolab_addressbook_ui($this);
 
                 if ($this->bonnie_api) {
                     $this->add_button(array(
                         'command'    => 'contact-history-dialog',
                         'class'      => 'history contact-history disabled',
                         'classact'   => 'history contact-history active',
                         'innerclass' => 'icon inner',
                         'label'      => 'kolab_addressbook.showhistory',
                         'type'       => 'link-menuitem'
                     ), 'contactmenu');
                 }
             }
         }
         else if ($this->rc->task == 'settings') {
             $this->add_texts('localization');
             $this->add_hook('preferences_list', array($this, 'prefs_list'));
             $this->add_hook('preferences_save', array($this, 'prefs_save'));
         }
 
         if ($this->driver instanceof kolab_contacts_driver) {
             $this->add_hook('folder_delete', array($this, 'prefs_folder_delete'));
             $this->add_hook('folder_rename', array($this, 'prefs_folder_rename'));
             $this->add_hook('folder_update', array($this, 'prefs_folder_update'));
         }
     }
 
     /**
      * Handler for the addressbooks_list hook.
      *
      * This will add all instances of available Kolab-based address books
      * to the list of address sources of Roundcube.
      * This will also hide some addressbooks according to kolab_addressbook_prio setting.
      *
      * @param array $p Hash array with hook parameters
      *
      * @return array Hash array with modified hook parameters
      */
     public function address_sources($p)
     {
         $abook_prio = $this->addressbook_prio();
 
         // Disable all global address books
         // Assumes that all non-kolab_addressbook sources are global
         if ($abook_prio == self::PERSONAL_ONLY) {
             $p['sources'] = array();
         }
 
         $sources = array();
         foreach ($this->_list_sources() as $abook_id => $abook) {
             // register this address source
             $sources[$abook_id] = $this->driver->abook_prop($abook_id, $abook);
 
             // flag folders with 'i' right as writeable
             if ($this->rc->action == 'add' && strpos($abook->rights, 'i') !== false) {
                 $sources[$abook_id]['readonly'] = false;
             }
         }
 
         // Add personal address sources to the list
         if ($abook_prio == self::PERSONAL_FIRST) {
             // $p['sources'] = array_merge($sources, $p['sources']);
             // Don't use array_merge(), because if you have folders name
             // that resolve to numeric identifier it will break output array keys
             foreach ($p['sources'] as $idx => $value) {
                 $sources[$idx] = $value;
             }
             $p['sources'] = $sources;
         }
         else {
             // $p['sources'] = array_merge($p['sources'], $sources);
             foreach ($sources as $idx => $value) {
                 $p['sources'][$idx] = $value;
             }
         }
 
         return $p;
     }
 
     /**
      *
      */
     public function directorylist_html($args)
     {
         $out     = '';
         $spec    = '';
         $kolab   = '';
         $jsdata  = [];
         $sources = (array) $this->rc->get_address_sources();
 
         // list all non-kolab sources first (also exclude hidden sources), special folders will go last
         foreach ($sources  as $j => $source) {
             $id = strval(strlen($source['id']) ? $source['id'] : $j);
             if (!empty($source['kolab']) || !empty($source['hidden'])) {
                 continue;
             }
 
             // Roundcube >= 1.5, Collected Recipients and Trusted Senders sources will be listed at the end
             if ((defined('rcube_addressbook::TYPE_RECIPIENT') && $source['id'] == (string) rcube_addressbook::TYPE_RECIPIENT)
                 || (defined('rcube_addressbook::TYPE_TRUSTED_SENDER') && $source['id'] == (string) rcube_addressbook::TYPE_TRUSTED_SENDER)
             ) {
                 $spec .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
             }
             else {
                 $out .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
             }
         }
 
         // render a hierarchical list of kolab contact folders
         // TODO: Move this to the drivers
         if ($this->driver instanceof kolab_contacts_driver) {
             $folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact'));
             kolab_storage::folder_hierarchy($folders, $tree);
             if ($tree && !empty($tree->children)) {
                 $kolab .= $this->folder_tree_html($tree, $sources, $jsdata);
             }
         }
         else {
             $filter = function($source) { return !empty($source['kolab']) && empty($source['hidden']); };
             foreach (array_filter($sources, $filter) as $j => $source) {
                 $id = strval(strlen($source['id']) ? $source['id'] : $j);
                 $kolab .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
             }
         }
 
         $out .= $kolab . $spec;
 
         $this->rc->output->set_env('contactgroups', array_filter($jsdata, function($src){ return isset($src['type']) && $src['type'] == 'group'; }));
         $this->rc->output->set_env('address_sources', array_filter($jsdata, function($src){ return !isset($src['type']) || $src['type'] != 'group'; }));
 
         $args['content'] = html::tag('ul', $args, $out, html::$common_attrib);
         return $args;
     }
 
     /**
      * Return html for a structured list <ul> for the folder tree
      */
     protected function folder_tree_html($node, $data, &$jsdata)
     {
         $out = '';
         foreach ($node->children as $folder) {
             $id = $folder->id;
             $source = $data[$id];
             $is_collapsed = strpos($this->rc->config->get('collapsed_abooks',''), '&'.rawurlencode($id).'&') !== false;
 
             if ($folder->virtual) {
                 $source = $this->driver->abook_prop($folder->id, $folder);
             }
             else if (empty($source)) {
                 $this->sources[$id] = new kolab_contacts($folder->name);
                 $source = $this->driver->abook_prop($id, $this->sources[$id]);
             }
 
             $content = $this->addressbook_list_item($id, $source, $jsdata);
 
             if (!empty($folder->children)) {
                 $child_html = $this->folder_tree_html($folder, $data, $jsdata);
 
                 // copy group items...
                 if (preg_match('!<ul[^>]*>(.*)</ul>\n*$!Ums', $content, $m)) {
                     $child_html = $m[1] . $child_html;
                     $content = substr($content, 0, -strlen($m[0]) - 1);
                 }
                 // ... and re-create the subtree
                 if (!empty($child_html)) {
                     $content .= html::tag('ul', array('class' => 'groups', 'style' => ($is_collapsed ? "display:none;" : null)), $child_html);
                 }
             }
 
             $out .= $content . '</li>';
         }
 
         return $out;
     }
 
     /**
      *
      */
     protected function addressbook_list_item($id, $source, &$jsdata, $search_mode = false)
     {
         $current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
 
         if (empty($source['virtual'])) {
             $jsdata[$id] = $source;
             $jsdata[$id]['name'] = html_entity_decode($source['name'], ENT_NOQUOTES, RCUBE_CHARSET);
         }
 
         // set class name(s)
         $classes = array('addressbook');
         if (!empty($source['group']))
             $classes[] = $source['group'];
         if ($current === $id)
             $classes[] = 'selected';
         if (!empty($source['readonly']))
             $classes[] = 'readonly';
         if (!empty($source['virtual']))
             $classes[] = 'virtual';
         if (!empty($source['class_name']))
             $classes[] = $source['class_name'];
 
         $name = !empty($source['listname']) ? $source['listname'] : (!empty($source['name']) ? $source['name'] : $id);
         $label_id = 'kabt:' . $id;
         $inner = (!empty($source['virtual']) ?
             html::a(array('tabindex' => '0'), $name) :
             html::a(array(
                     'href' => $this->rc->url(array('_source' => $id)),
                     'rel' => $source['id'],
                     'id' => $label_id,
                     'onclick' => "return " . rcmail_output::JS_OBJECT_NAME.".command('list','" . rcube::JQ($id) . "',this)",
                 ), $name)
         );
 
         if ($this->driver instanceof kolab_contacts_driver && isset($source['subscribed'])) {
             $inner .= html::span(array(
                 'class' => 'subscribed',
                 'title' => $this->gettext('foldersubscribe'),
                 'role' => 'checkbox',
                 'aria-checked' => $source['subscribed'] ? 'true' : 'false',
             ), '');
         }
 
         // don't wrap in <li> but add a checkbox for search results listing
         if ($search_mode) {
             $jsdata[$id]['group'] = join(' ', $classes);
 
             if (!$source['virtual']) {
                 $inner .= html::tag('input', array(
                     'type' => 'checkbox',
                     'name' => '_source[]',
                     'value' => $id,
                     'checked' => false,
                     'aria-labelledby' => $label_id,
                 ));
             }
             return html::div(null, $inner);
         }
 
         $out = html::tag('li', array(
                 'id' => 'rcmli' . rcube_utils::html_identifier($id, true),
                 'class' => join(' ', $classes), 
                 'noclose' => true,
             ),
             html::div(!empty($source['subscribed']) ? 'subscribed' : null, $inner)
         );
 
         $groupdata = array('out' => '', 'jsdata' => $jsdata, 'source' => $id);
         if ($source['groups']) {
             if (function_exists('rcmail_contact_groups')) {
                 $groupdata = rcmail_contact_groups($groupdata);
             }
             else {
                 // Roundcube >= 1.5
                 $groupdata = rcmail_action_contacts_index::contact_groups($groupdata);
             }
         }
 
         $jsdata = $groupdata['jsdata'];
         $out .= $groupdata['out'];
 
         return $out;
     }
 
     /**
      * Sets autocomplete_addressbooks option according to
      * kolab_addressbook_prio setting extending list of address sources
      * to be used for autocompletion.
      */
     public function config_get($args)
     {
         if ($args['name'] != 'autocomplete_addressbooks' || $this->recurrent) {
             return $args;
         }
 
         $abook_prio = $this->addressbook_prio();
 
         // Get the original setting, use temp flag to prevent from an infinite recursion
         $this->recurrent = true;
         $sources = $this->rc->config->get('autocomplete_addressbooks');
         $this->recurrent = false;
 
         // Disable all global address books
         // Assumes that all non-kolab_addressbook sources are global
         if ($abook_prio == self::PERSONAL_ONLY) {
             $sources = array();
         }
 
         if (!is_array($sources)) {
             $sources = array();
         }
 
         $kolab_sources = array();
         foreach (array_keys($this->_list_sources()) as $abook_id) {
             if (!in_array($abook_id, $sources))
                 $kolab_sources[] = $abook_id;
         }
 
         // Add personal address sources to the list
         if (!empty($kolab_sources)) {
             if ($abook_prio == self::PERSONAL_FIRST) {
                 $sources = array_merge($kolab_sources, $sources);
             }
             else {
                 $sources = array_merge($sources, $kolab_sources);
             }
         }
 
         $args['result'] = $sources;
 
         return $args;
     }
 
     /**
      * Getter for the rcube_addressbook instance
      *
      * @param array $p Hash array with hook parameters
      *
      * @return array Hash array with modified hook parameters
      */
     public function get_address_book($p)
     {
         if ($p['id']) {
             if ($source = $this->driver->get_address_book($p['id'])) {
                 $p['instance'] = $source;
 
                 // flag source as writeable if 'i' right is given
                 if ($p['writeable'] && $this->rc->action == 'save' && strpos($p['instance']->rights, 'i') !== false) {
                     $p['instance']->readonly = false;
                 }
                 else if ($this->rc->action == 'delete' && strpos($p['instance']->rights, 't') !== false) {
                     $p['instance']->readonly = false;
                 }
             }
         }
 
         return $p;
     }
 
     /**
      * List addressbook sources list
      */
     private function _list_sources()
     {
         // already read sources
         if (isset($this->sources)) {
             return $this->sources;
         }
 
         $this->sources = [];
 
         $abook_prio = $this->addressbook_prio();
 
         // Personal address source(s) disabled?
         if ($abook_prio == kolab_addressbook::GLOBAL_ONLY) {
             return $this->sources;
         }
 
         $folders = $this->driver->list_folders();
 
         // get all folders that have "contact" type
         foreach ($folders as $id => $source) {
             $this->sources[$id] = $source;
         }
 
         return $this->sources;
     }
 
     /**
      * Plugin hook called before rendering the contact form or detail view
      *
      * @param array $p Hash array with hook parameters
      *
      * @return array Hash array with modified hook parameters
      */
     public function contact_form($p)
     {
         // none of our business
-        if (!is_object($GLOBALS['CONTACTS']) || !is_a($GLOBALS['CONTACTS'], 'kolab_contacts')) {
+        if (!is_object($GLOBALS['CONTACTS'] ?? null) || !is_a($GLOBALS['CONTACTS'], 'kolab_contacts')) {
             return $p;
         }
 
         // extend the list of contact fields to be displayed in the 'personal' section
         if (is_array($p['form']['personal'])) {
             $p['form']['personal']['content']['profession']    = array('size' => 40);
             $p['form']['personal']['content']['children']      = array('size' => 40);
             $p['form']['personal']['content']['freebusyurl']   = array('size' => 40);
             $p['form']['personal']['content']['pgppublickey']  = array('size' => 70);
             $p['form']['personal']['content']['pkcs7publickey'] = array('size' => 70);
 
             // re-order fields according to the coltypes list
             $p['form']['contact']['content']  = $this->_sort_form_fields($p['form']['contact']['content'], $GLOBALS['CONTACTS']);
             $p['form']['personal']['content'] = $this->_sort_form_fields($p['form']['personal']['content'], $GLOBALS['CONTACTS']);
 
             /* define a separate section 'settings'
             $p['form']['settings'] = array(
                 'name'    => $this->gettext('settings'),
                 'content' => array(
                     'freebusyurl'  => array('size' => 40, 'visible' => true),
                     'pgppublickey' => array('size' => 70, 'visible' => true),
                     'pkcs7publickey' => array('size' => 70, 'visible' => false),
                 )
             );
             */
         }
 
         if ($this->bonnie_api && $this->rc->action == 'show' && empty($p['record']['rev'])) {
             $this->rc->output->set_env('kolab_audit_trail', true);
         }
 
         return $p;
     }
 
     /**
      * Plugin hook for the contact photo image
      */
     public function contact_photo($p)
     {
         // add photo data from old revision inline as data url
         if (!empty($p['record']['rev']) && !empty($p['data'])) {
             $p['url'] = 'data:image/gif;base64,' . base64_encode($p['data']);
         }
 
         return $p;
     }
 
     /**
      * Handler for contact audit trail changelog requests
      */
     public function contact_changelog()
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
         $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
 
         list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);
 
         $result = $uid && $mailbox ? $this->bonnie_api->changelog('contact', $uid, $mailbox, $msguid) : null;
         if (is_array($result) && $result['uid'] == $uid) {
             if (is_array($result['changes'])) {
                 $rcmail = $this->rc;
                 $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
                 array_walk($result['changes'], function(&$change) use ($rcmail, $dtformat) {
                   if ($change['date']) {
                       $dt = rcube_utils::anytodatetime($change['date']);
                       if ($dt instanceof DateTime) {
                           $change['date'] = $rcmail->format_date($dt, $dtformat);
                       }
                   }
                 });
             }
             $this->rc->output->command('contact_render_changelog', $result['changes']);
         }
         else {
             $this->rc->output->command('contact_render_changelog', false);
         }
 
         $this->rc->output->send();
     }
 
     /**
      * Handler for audit trail diff view requests
      */
     public function contact_diff()
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
         $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
         $rev1 = rcube_utils::get_input_value('rev1', rcube_utils::INPUT_POST);
         $rev2 = rcube_utils::get_input_value('rev2', rcube_utils::INPUT_POST);
 
         list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);
 
         $result = $this->bonnie_api->diff('contact', $uid, $rev1, $rev2, $mailbox, $msguid);
         if (is_array($result) && $result['uid'] == $uid) {
             $result['rev1'] = $rev1;
             $result['rev2'] = $rev2;
             $result['cid'] = $contact;
 
             // convert some properties, similar to kolab_contacts::_to_rcube_contact()
             $keymap = array(
                 'lastmodified-date' => 'changed',
                 'additional' => 'middlename',
                 'fn' => 'name',
                 'tel' => 'phone',
                 'url' => 'website',
                 'bday' => 'birthday',
                 'note' => 'notes',
                 'role' => 'profession',
                 'title' => 'jobtitle',
             );
 
             $propmap = array('email' => 'address', 'website' => 'url', 'phone' => 'number');
             $date_format = $this->rc->config->get('date_format', 'Y-m-d');
 
             // map kolab object properties to keys and values the client expects
             array_walk($result['changes'], function(&$change, $i) use ($keymap, $propmap, $date_format) {
                 if (array_key_exists($change['property'], $keymap)) {
                     $change['property'] = $keymap[$change['property']];
                 }
 
                 // format date-time values
                 if ($change['property'] == 'created' || $change['property'] == 'changed') {
                     if ($old_ = rcube_utils::anytodatetime($change['old'])) {
                         $change['old_'] = $this->rc->format_date($old_);
                     }
                     if ($new_ = rcube_utils::anytodatetime($change['new'])) {
                         $change['new_'] = $this->rc->format_date($new_);
                     }
                 }
                 // format dates
                 else if ($change['property'] == 'birthday' || $change['property'] == 'anniversary') {
                     if ($old_ = rcube_utils::anytodatetime($change['old'])) {
                         $change['old_'] = $this->rc->format_date($old_, $date_format);
                     }
                     if ($new_ = rcube_utils::anytodatetime($change['new'])) {
                         $change['new_'] = $this->rc->format_date($new_, $date_format);
                     }
                 }
                 // convert email, website, phone values
                 else if (array_key_exists($change['property'], $propmap)) {
                     $propname = $propmap[$change['property']];
                     foreach (array('old','new') as $k) {
                         $k_ = $k . '_';
                         if (!empty($change[$k])) {
                             $change[$k_] = html::quote($change[$k][$propname] ?: '--');
                             if ($change[$k]['type']) {
                                 $change[$k_] .= '&nbsp;' . html::span('subtype', $this->get_type_label($change[$k]['type']));
                             }
                             $change['ishtml'] = true;
                         }
                     }
                 }
                 // serialize address structs
                 if ($change['property'] == 'address') {
                     foreach (array('old','new') as $k) {
                         $k_ = $k . '_';
                         $change[$k]['zipcode'] = $change[$k]['code'];
                         $template = $this->rc->config->get('address_template', '{'.join('} {', array_keys($change[$k])).'}');
                         $composite = array();
                         foreach ($change[$k] as $p => $val) {
                             if (strlen($val))
                                 $composite['{'.$p.'}'] = $val;
                         }
                         $change[$k_] = preg_replace('/\{\w+\}/', '', strtr($template, $composite));
                         if ($change[$k]['type']) {
                             $change[$k_] .= html::div('subtype', $this->get_type_label($change[$k]['type']));
                         }
                         $change['ishtml'] = true;
                     }
 
                     $change['diff_'] = libkolab::html_diff($change['old_'], $change['new_'], true);
                 }
                 // localize gender values
                 else if ($change['property'] == 'gender') {
                     if ($change['old']) $change['old_'] = $this->rc->gettext($change['old']);
                     if ($change['new']) $change['new_'] = $this->rc->gettext($change['new']);
                 }
                 // translate 'key' entries in individual properties
                 else if ($change['property'] == 'key') {
                     $p = $change['old'] ?: $change['new'];
                     $t = $p['type'];
                     $change['property'] = $t . 'publickey';
                     $change['old'] = $change['old'] ? $change['old']['key'] : '';
                     $change['new'] = $change['new'] ? $change['new']['key'] : '';
                 }
                 // compute a nice diff of notes
                 else if ($change['property'] == 'notes') {
                     $change['diff_'] = libkolab::html_diff($change['old'], $change['new'], false);
                 }
             });
 
             $this->rc->output->command('contact_show_diff', $result);
         }
         else {
             $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
         }
 
         $this->rc->output->send();
     }
 
     /**
      * Handler for audit trail revision restore requests
      */
     public function contact_restore()
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         $success = false;
         $contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
         $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
         $rev = rcube_utils::get_input_value('rev', rcube_utils::INPUT_POST);
 
         list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source, $folder);
 
         if ($folder && ($raw_msg = $this->bonnie_api->rawdata('contact', $uid, $rev, $mailbox))) {
             $imap = $this->rc->get_storage();
 
             // insert $raw_msg as new message
             if ($imap->save_message($folder->name, $raw_msg, null, false)) {
                 $success = true;
 
                 // delete old revision from imap and cache
                 $imap->delete_message($msguid, $folder->name);
                 $folder->cache->set($msguid, false);
                 $this->cache = array();
             }
         }
 
         if ($success) {
             $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rev))), 'confirmation');
             $this->rc->output->command('close_contact_history_dialog', $contact);
         }
         else {
             $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
         }
 
         $this->rc->output->send();
     }
 
     /**
      * Get a previous revision of the given contact record from the Bonnie API
      */
     public function get_revision($cid, $source, $rev)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($cid, $source);
 
         // call Bonnie API
         $result = $this->bonnie_api->get('contact', $uid, $rev, $mailbox, $msguid);
         if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
             $format = kolab_format::factory('contact');
             $format->load($result['xml']);
             $rec = $format->to_array();
 
             if ($format->is_valid()) {
                 $rec['rev'] = $result['rev'];
                 return $rec;
             }
         }
 
         return false;
     }
 
     /**
      * Helper method to resolved the given contact identifier into uid and mailbox
      *
      * @return array (uid,mailbox,msguid) tuple
      */
     private function _resolve_contact_identity($id, $abook, &$folder = null)
     {
         $mailbox = $msguid = null;
 
         $source = $this->get_address_book(array('id' => $abook));
         if ($source['instance']) {
             $uid = $source['instance']->id2uid($id);
             $list = kolab_storage::id_decode($abook);
         }
         else {
             return array(null, $mailbox, $msguid);
         }
 
         // get resolve message UID and mailbox identifier
         if ($folder = kolab_storage::get_folder($list)) {
             $mailbox = $folder->get_mailbox_id();
             $msguid = $folder->cache->uid2msguid($uid);
         }
 
         return array($uid, $mailbox, $msguid);
     }
 
     /**
      *
      */
     private function _sort_form_fields($contents, $source)
     {
         $block = [];
 
         foreach (array_keys($source->coltypes) as $col) {
             if (isset($contents[$col])) {
                 $block[$col] = $contents[$col];
             }
         }
 
         return $block;
     }
 
     /**
      * Handler for user preferences form (preferences_list hook)
      *
      * @param array $args Hash array with hook parameters
      *
      * @return array Hash array with modified hook parameters
      */
     public function prefs_list($args)
     {
         if ($args['section'] != 'addressbook') {
             return $args;
         }
 
         $ldap_public = $this->rc->config->get('ldap_public');
 
         // Hide option if there's no global addressbook
         if (empty($ldap_public)) {
             return $args;
         }
 
         // Check that configuration is not disabled
         $dont_override = (array) $this->rc->config->get('dont_override', array());
         $prio          = $this->addressbook_prio();
 
         if (!in_array('kolab_addressbook_prio', $dont_override)) {
             // Load localization
             $this->add_texts('localization');
 
             $field_id = '_kolab_addressbook_prio';
             $select   = new html_select(array('name' => $field_id, 'id' => $field_id));
 
             $select->add($this->gettext('globalfirst'), self::GLOBAL_FIRST);
             $select->add($this->gettext('personalfirst'), self::PERSONAL_FIRST);
             $select->add($this->gettext('globalonly'), self::GLOBAL_ONLY);
             $select->add($this->gettext('personalonly'), self::PERSONAL_ONLY);
 
             $args['blocks']['main']['options']['kolab_addressbook_prio'] = array(
                 'title' => html::label($field_id, rcube::Q($this->gettext('addressbookprio'))),
                 'content' => $select->show($prio),
             );
         }
 
         return $args;
     }
 
     /**
      * Handler for user preferences save (preferences_save hook)
      *
      * @param array $args Hash array with hook parameters
      *
      * @return array Hash array with modified hook parameters
      */
     public function prefs_save($args)
     {
         if ($args['section'] != 'addressbook') {
             return $args;
         }
 
         // Check that configuration is not disabled
         $dont_override = (array) $this->rc->config->get('dont_override', array());
         $key           = 'kolab_addressbook_prio';
 
         if (!in_array('kolab_addressbook_prio', $dont_override) || !isset($_POST['_'.$key])) {
             $args['prefs'][$key] = (int) rcube_utils::get_input_value('_'.$key, rcube_utils::INPUT_POST);
         }
 
         return $args;
     }
 
 
     /**
      * Handler for plugin actions
      */
     public function book_actions()
     {
         $action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC));
 
         if ($action == 'create') {
             $this->ui->book_edit();
         }
         else if ($action == 'edit') {
             $this->ui->book_edit();
         }
         else if ($action == 'delete') {
             $this->book_delete();
         }
     }
 
     /**
      * Handler for address book create/edit form submit
      */
     public function book_save()
     {
         $this->driver->folder_save();
         $this->rc->output->send('iframe');
     }
 
     /**
      *
      */
     public function book_search()
     {
         $results = [];
         $query   = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
         $source  = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
 
         kolab_storage::$encode_ids = true;
         $search_more_results = false;
         $this->sources = array();
         $this->folders = array();
 
         // find unsubscribed IMAP folders that have "event" type
         if ($source == 'folders') {
             foreach ((array)kolab_storage::search_folders('contact', $query, array('other')) as $folder) {
                 $this->folders[$folder->id] = $folder;
                 $this->sources[$folder->id] = new kolab_contacts($folder->name);
             }
         }
         // 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 contact folders shared by this user
                 foreach (kolab_storage::list_user_folders($user, 'contact', false) as $foldername) {
                     $folders[] = new kolab_storage_folder($foldername, 'contact');
                 }
 
                 if (count($folders)) {
                     $userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
                     $this->folders[$userfolder->id] = $userfolder;
                     $this->sources[$userfolder->id] = $userfolder;
 
                     foreach ($folders as $folder) {
                         $this->folders[$folder->id] = $folder;
                         $this->sources[$folder->id] = new kolab_contacts($folder->name);;
                         $count++;
                     }
                 }
 
                 if ($count >= $limit) {
                     $search_more_results = true;
                     break;
                 }
             }
         }
 
         $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
 
         // build results list
         foreach ($this->sources as $id => $source) {
             $folder = $this->folders[$id];
             $imap_path = explode($delim, $folder->name);
 
             // 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());
             }
 
             $prop = $this->driver->abook_prop($id, $source);
             $prop['parent'] = $parent_id;
 
             $html = $this->addressbook_list_item($id, $prop, $jsdata, true);
             unset($prop['group']);
             $prop += (array)$jsdata[$id];
             $prop['html'] = $html;
 
             $results[] = $prop;
         }
 
         // report more results available
         if ($search_more_results) {
             $this->rc->output->show_message('autocompletemore', 'notice');
         }
 
         $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
     }
 
     /**
      *
      */
     public function book_subscribe()
     {
         $success = false;
         $id = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
 
         if ($id && ($folder = kolab_storage::get_folder(kolab_storage::id_decode($id)))) {
             if (isset($_POST['_permanent']))
                 $success |= $folder->subscribe(intval($_POST['_permanent']));
             if (isset($_POST['_active']))
                 $success |= $folder->activate(intval($_POST['_active']));
 
             // list groups for this address book
             if (!empty($_POST['_groups'])) {
                 $abook = new kolab_contacts($folder->name);
                 foreach ((array)$abook->list_groups() as $prop) {
                     $prop['source'] = $id;
                     $prop['id'] = $prop['ID'];
                     unset($prop['ID']);
                     $this->rc->output->command('insert_contact_group', $prop);
                 }
             }
         }
 
         if ($success) {
             $this->rc->output->show_message('successfullysaved', 'confirmation');
         }
         else {
             $this->rc->output->show_message($this->gettext('errorsaving'), 'error');
         }
 
         $this->rc->output->send();
     }
 
 
     /**
      * Handler for address book delete action (AJAX)
      */
     private function book_delete()
     {
         $source = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true));
 
         if ($source && ($result = $this->driver->folder_delete($source))) {
             $storage   = $this->rc->get_storage();
             $delimiter = $storage->get_hierarchy_delimiter();
 
             $this->rc->output->show_message('kolab_addressbook.bookdeleted', 'confirmation');
             $this->rc->output->set_env('pagecount', 0);
             $this->rc->output->command('set_rowcount', $this->rc->gettext('nocontactsfound'));
             $this->rc->output->command('set_env', 'delimiter', $delimiter);
             $this->rc->output->command('list_contacts_clear');
             $this->rc->output->command('book_delete_done', $source);
         }
         else {
             $this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error');
         }
 
         $this->rc->output->send();
     }
 
     /**
      * Returns value of kolab_addressbook_prio setting
      */
     private function addressbook_prio()
     {
         $abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
 
         // Make sure any global addressbooks are defined
         if ($abook_prio == 0 || $abook_prio == 2) {
             $ldap_public = $this->rc->config->get('ldap_public');
 
             if (empty($ldap_public)) {
                 $abook_prio = 1;
             }
         }
 
         return $abook_prio;
     }
 
     /**
      * Hook for (contact) folder deletion
      */
     function prefs_folder_delete($args)
     {
         // ignore...
         if ($args['abort'] && !$args['result']) {
             return $args;
         }
 
         $this->_contact_folder_rename($args['name'], false);
     }
 
     /**
      * Hook for (contact) folder renaming
      */
     function prefs_folder_rename($args)
     {
         // ignore...
         if ($args['abort'] && !$args['result']) {
             return $args;
         }
 
         $this->_contact_folder_rename($args['oldname'], $args['newname']);
     }
 
     /**
      * Hook for (contact) folder updates. Forward to folder_rename handler if name was changed
      */
     function prefs_folder_update($args)
     {
         // ignore...
         if ($args['abort'] && !$args['result']) {
             return $args;
         }
 
         if ($args['record']['name'] != $args['record']['oldname']) {
             $this->_contact_folder_rename($args['record']['oldname'], $args['record']['name']);
         }
     }
 
     /**
      * Apply folder renaming or deletion to the registered birthday calendar address books
      */
     private function _contact_folder_rename($oldname, $newname = false)
     {
         $update = false;
         $delimiter = $this->rc->get_storage()->get_hierarchy_delimiter();
         $bday_addressbooks = (array) $this->rc->config->get('calendar_birthday_adressbooks', []);
 
         foreach ($bday_addressbooks as $i => $id) {
             $folder_name = kolab_storage::id_decode($id);
             if ($oldname === $folder_name || strpos($folder_name, $oldname.$delimiter) === 0) {
                 if ($newname) {  // rename
                     $new_folder = $newname . substr($folder_name, strlen($oldname));
                     $bday_addressbooks[$i] = kolab_storage::id_encode($new_folder);
                 }
                 else {  // delete
                     unset($bday_addressbooks[$i]);
                 }
                 $update = true;
             }
         }
 
         if ($update) {
             $this->rc->user->save_prefs(['calendar_birthday_adressbooks' => $bday_addressbooks]);
         }
     }
 
     /**
      * Get a localization label for specified field type
      */
     private function get_type_label($type)
     {
         // Roundcube < 1.5
         if (function_exists('rcmail_get_type_label')) {
             return rcmail_get_type_label($type);
         }
 
         // Roundcube >= 1.5
         return rcmail_action_contacts_index::get_type_label($type);
     }
 }
diff --git a/plugins/kolab_auth/kolab_auth.php b/plugins/kolab_auth/kolab_auth.php
index 85f97da1..0a7ea953 100644
--- a/plugins/kolab_auth/kolab_auth.php
+++ b/plugins/kolab_auth/kolab_auth.php
@@ -1,890 +1,890 @@
 <?php
 
 /**
  * Kolab Authentication (based on ldap_authentication plugin)
  *
  * Authenticates on LDAP server, finds canonized authentication ID for IMAP
  * and for new users creates identity based on LDAP information.
  *
  * Supports impersonate feature (login as another user). To use this feature
  * imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
  *
  * @version @package_version@
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2011-2013, Kolab Systems AG <contact@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_auth extends rcube_plugin
 {
     static $ldap;
     private $username;
     private $data = array();
 
     public function init()
     {
         $rcmail = rcube::get_instance();
 
         $this->load_config();
         $this->require_plugin('libkolab');
 
         $this->add_hook('authenticate', array($this, 'authenticate'));
         $this->add_hook('startup', array($this, 'startup'));
         $this->add_hook('ready', array($this, 'ready'));
         $this->add_hook('user_create', array($this, 'user_create'));
 
         // Hook for password change
         $this->add_hook('password_ldap_bind', array($this, 'password_ldap_bind'));
 
         // Hooks related to "Login As" feature
         $this->add_hook('template_object_loginform', array($this, 'login_form'));
         $this->add_hook('storage_connect', array($this, 'imap_connect'));
         $this->add_hook('managesieve_connect', array($this, 'imap_connect'));
         $this->add_hook('smtp_connect', array($this, 'smtp_connect'));
         $this->add_hook('identity_form', array($this, 'identity_form'));
 
         // Hook to modify some configuration, e.g. ldap
         $this->add_hook('config_get', array($this, 'config_get'));
 
         // Hook to modify logging directory
         $this->add_hook('write_log', array($this, 'write_log'));
-        $this->username = $_SESSION['username'];
+        $this->username = $_SESSION['username'] ?? null;
 
         // Enable debug logs (per-user), when logged as another user
         if (!empty($_SESSION['kolab_auth_admin']) && $rcmail->config->get('kolab_auth_auditlog')) {
             $rcmail->config->set('debug_level', 1);
             $rcmail->config->set('smtp_log', true);
             $rcmail->config->set('log_logins', true);
             $rcmail->config->set('log_session', true);
             $rcmail->config->set('memcache_debug', true);
             $rcmail->config->set('imap_debug', true);
             $rcmail->config->set('ldap_debug', true);
             $rcmail->config->set('smtp_debug', true);
             $rcmail->config->set('sql_debug', true);
 
             // SQL debug need to be set directly on DB object
             // setting config variable will not work here because
             // the object is already initialized/configured
             if ($db = $rcmail->get_dbh()) {
                 $db->set_debug(true);
             }
         }
     }
 
     /**
      * Ready hook handler
      */
     public function ready($args)
     {
         $rcmail = rcube::get_instance();
 
         // Store user unique identifier for freebusy_session_auth feature
         if (!($uniqueid = $rcmail->config->get('kolab_uniqueid'))) {
             $uniqueid = $_SESSION['kolab_auth_uniqueid'];
 
             if (!$uniqueid) {
                 // Find user record in LDAP
                 if (($ldap = self::ldap()) && $ldap->ready) {
                     if ($record = $ldap->get_user_record($rcmail->get_user_name(), $_SESSION['kolab_host'])) {
                         $uniqueid = $record['uniqueid'];
                     }
                 }
             }
 
             if ($uniqueid) {
                 $uniqueid = md5($uniqueid);
                 $rcmail->user->save_prefs(array('kolab_uniqueid' => $uniqueid));
             }
         }
 
         // Set/update freebusy_session_auth entry
         if ($uniqueid && empty($_SESSION['kolab_auth_admin'])
             && ($ttl = $rcmail->config->get('freebusy_session_auth'))
         ) {
             if ($ttl === true) {
                 $ttl = $rcmail->config->get('session_lifetime', 0) * 60;
 
                 if (!$ttl) {
                     $ttl = 10 * 60;
                 }
             }
 
             $rcmail->config->set('freebusy_auth_cache', 'db');
             $rcmail->config->set('freebusy_auth_cache_ttl', $ttl);
 
             if ($cache = $rcmail->get_cache_shared('freebusy_auth', false)) {
                 $key      = md5($uniqueid . ':' . rcube_utils::remote_addr() . ':' . $rcmail->get_user_name());
                 $value    = $cache->get($key);
                 $deadline = new DateTime('now', new DateTimeZone('UTC'));
 
                 // We don't want to do the cache update on every request
                 // do it once in a 1/10 of the ttl
                 if ($value) {
                     $value = new DateTime($value);
                     $value->sub(new DateInterval('PT' . intval($ttl * 9/10) . 'S'));
                     if ($value > $deadline) {
                         return;
                     }
                 }
 
                 $deadline->add(new DateInterval('PT' . $ttl . 'S'));
 
                 $cache->set($key, $deadline->format(DateTime::ISO8601));
             }
         }
     }
 
     /**
      * Startup hook handler
      */
     public function startup($args)
     {
         // Check access rights when logged in as another user
         if (!empty($_SESSION['kolab_auth_admin']) && $args['task'] != 'login' && $args['task'] != 'logout') {
             // access to specified task is forbidden,
             // redirect to the first task on the list
             if (!empty($_SESSION['kolab_auth_allowed_tasks'])) {
                 $tasks = (array)$_SESSION['kolab_auth_allowed_tasks'];
                 if (!in_array($args['task'], $tasks) && !in_array('*', $tasks)) {
                     header('Location: ?_task=' . array_shift($tasks));
                     die;
                 }
 
                 // add script that will remove disabled taskbar buttons
                 if (!in_array('*', $tasks)) {
                     $this->add_hook('render_page', array($this, 'render_page'));
                 }
             }
         }
 
         // load per-user settings
         $this->load_user_role_plugins_and_settings();
 
         return $args;
     }
 
     /**
      * Modify some configuration according to LDAP user record
      */
     public function config_get($args)
     {
         // Replaces ldap_vars (%dc, etc) in public kolab ldap addressbooks
         // config based on the users base_dn. (for multi domain support)
         if ($args['name'] == 'ldap_public' && !empty($args['result'])) {
             $rcmail      = rcube::get_instance();
             $kolab_books = (array) $rcmail->config->get('kolab_auth_ldap_addressbooks');
 
             foreach ($args['result'] as $name => $config) {
                 if (in_array($name, $kolab_books) || in_array('*', $kolab_books)) {
                     $args['result'][$name] = $this->patch_ldap_config($config);
                 }
             }
         }
         else if ($args['name'] == 'kolab_users_directory' && !empty($args['result'])) {
             $args['result'] = $this->patch_ldap_config($args['result']);
         }
 
         return $args;
     }
 
     /**
      * Helper method to patch the given LDAP directory config with user-specific values
      */
     protected function patch_ldap_config($config)
     {
         if (is_array($config)) {
             $config['base_dn']        = self::parse_ldap_vars($config['base_dn']);
             $config['search_base_dn'] = self::parse_ldap_vars($config['search_base_dn']);
             $config['bind_dn']        = str_replace('%dn', $_SESSION['kolab_dn'], $config['bind_dn']);
 
             if (!empty($config['groups'])) {
                 $config['groups']['base_dn'] = self::parse_ldap_vars($config['groups']['base_dn']);
             }
         }
 
         return $config;
     }
 
     /**
      * Modifies list of plugins and settings according to
      * specified LDAP roles
      */
     public function load_user_role_plugins_and_settings($startup = false)
     {
         if (empty($_SESSION['user_roledns'])) {
             return;
         }
 
         $rcmail = rcube::get_instance();
 
         // Example 'kolab_auth_role_plugins' =
         //
         //  Array(
         //      '<role_dn>' => Array('plugin1', 'plugin2'),
         //  );
         //
         // NOTE that <role_dn> may in fact be something like: 'cn=role,%dc'
 
         $role_plugins = $rcmail->config->get('kolab_auth_role_plugins');
 
         // Example $rcmail_config['kolab_auth_role_settings'] =
         //
         //  Array(
         //      '<role_dn>' => Array(
         //          '$setting' => Array(
         //              'mode' => '(override|merge)', (default: override)
         //              'value' => <>,
         //              'allow_override' => (true|false) (default: false)
         //          ),
         //      ),
         //  );
         //
         // NOTE that <role_dn> may in fact be something like: 'cn=role,%dc'
 
         $role_settings = $rcmail->config->get('kolab_auth_role_settings');
 
         if (!empty($role_plugins)) {
             foreach ($role_plugins as $role_dn => $plugins) {
                 $role_dn = self::parse_ldap_vars($role_dn);
                 if (!empty($role_plugins[$role_dn])) {
                     $role_plugins[$role_dn] = array_unique(array_merge((array)$role_plugins[$role_dn], $plugins));
                 } else {
                     $role_plugins[$role_dn] = $plugins;
                 }
             }
         }
 
         if (!empty($role_settings)) {
             foreach ($role_settings as $role_dn => $settings) {
                 $role_dn = self::parse_ldap_vars($role_dn);
                 if (!empty($role_settings[$role_dn])) {
                     $role_settings[$role_dn] = array_merge((array)$role_settings[$role_dn], $settings);
                 } else {
                     $role_settings[$role_dn] = $settings;
                 }
             }
         }
 
         foreach ($_SESSION['user_roledns'] as $role_dn) {
             if (!empty($role_settings[$role_dn]) && is_array($role_settings[$role_dn])) {
                 foreach ($role_settings[$role_dn] as $setting_name => $setting) {
                     if (!isset($setting['mode'])) {
                         $setting['mode'] = 'override';
                     }
 
                     if ($setting['mode'] == "override") {
                         $rcmail->config->set($setting_name, $setting['value']);
                     } elseif ($setting['mode'] == "merge") {
                         $orig_setting = $rcmail->config->get($setting_name);
 
                         if (!empty($orig_setting)) {
                             if (is_array($orig_setting)) {
                                 $rcmail->config->set($setting_name, array_merge($orig_setting, $setting['value']));
                             }
                         } else {
                             $rcmail->config->set($setting_name, $setting['value']);
                         }
                     }
 
                     $dont_override = (array) $rcmail->config->get('dont_override');
 
                     if (empty($setting['allow_override'])) {
                         $rcmail->config->set('dont_override', array_merge($dont_override, array($setting_name)));
                     }
                     else {
                         if (in_array($setting_name, $dont_override)) {
                             $_dont_override = array();
                             foreach ($dont_override as $_setting) {
                                 if ($_setting != $setting_name) {
                                     $_dont_override[] = $_setting;
                                 }
                             }
                             $rcmail->config->set('dont_override', $_dont_override);
                         }
                     }
 
                     if ($setting_name == 'skin') {
                         if ($rcmail->output->type == 'html') {
                             $rcmail->output->set_skin($setting['value']);
                             $rcmail->output->set_env('skin', $setting['value']);
                         }
                     }
                 }
             }
 
             if (!empty($role_plugins[$role_dn])) {
                 foreach ((array)$role_plugins[$role_dn] as $plugin) {
                     $loaded = $this->api->load_plugin($plugin);
 
                     // Some plugins e.g. kolab_2fa use 'startup' hook to
                     // register other hooks, but when called on 'authenticate' hook
                     // we're already after 'startup', so we'll call it directly
                     if ($loaded && $startup && $plugin == 'kolab_2fa'
                         && ($plugin = $this->api->get_plugin($plugin))
                     ) {
                         $plugin->startup(array('task' => $rcmail->task, 'action' => $rcmail->action));
                     }
                 }
             }
         }
     }
 
     /**
      * Logging method replacement to print debug/errors into
      * a separate (sub)folder for each user
      */
     public function write_log($args)
     {
         $rcmail = rcube::get_instance();
 
         if ($rcmail->config->get('log_driver') == 'syslog') {
             return $args;
         }
 
         // log_driver == 'file' is assumed here
         $log_dir  = $rcmail->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
 
         // Append original username + target username for audit-logging
         if ($rcmail->config->get('kolab_auth_auditlog') && !empty($_SESSION['kolab_auth_admin'])) {
             $args['dir'] = $log_dir . '/' . strtolower($_SESSION['kolab_auth_admin']) . '/' . strtolower($this->username);
 
             // Attempt to create the directory
             if (!is_dir($args['dir'])) {
                 @mkdir($args['dir'], 0750, true);
             }
         }
         // Define the user log directory if a username is provided
         else if ($rcmail->config->get('per_user_logging') && !empty($this->username)
             && !stripos($log_dir, '/' . $this->username) // maybe already set by syncroton, skip
         ) {
             $user_log_dir = $log_dir . '/' . strtolower($this->username);
             if (is_writable($user_log_dir)) {
                 $args['dir'] = $user_log_dir;
             }
             else if (!in_array($args['name'], array('errors', 'userlogins', 'sendmail'))) {
                 $args['abort'] = true;  // don't log if unauthenticed or no per-user log dir
             }
         }
 
         return $args;
     }
 
     /**
      * Sets defaults for new user.
      */
     public function user_create($args)
     {
         if (!empty($this->data['user_email'])) {
             // addresses list is supported
             if (array_key_exists('email_list', $args)) {
                 $email_list = array_unique($this->data['user_email']);
 
                 // add organization to the list
                 if (!empty($this->data['user_organization'])) {
                     foreach ($email_list as $idx => $email) {
                         $email_list[$idx] = array(
                             'organization' => $this->data['user_organization'],
                             'email'        => $email,
                         );
                     }
                 }
 
                 $args['email_list'] = $email_list;
             }
             else {
                 $args['user_email'] = $this->data['user_email'][0];
             }
         }
 
         if (!empty($this->data['user_name'])) {
             $args['user_name'] = $this->data['user_name'];
         }
 
         return $args;
     }
 
     /**
      * Modifies login form adding additional "Login As" field
      */
     public function login_form($args)
     {
         $this->add_texts('localization/');
 
         $rcmail      = rcube::get_instance();
         $admin_login = $rcmail->config->get('kolab_auth_admin_login');
         $group       = $rcmail->config->get('kolab_auth_group');
         $role_attr   = $rcmail->config->get('kolab_auth_role');
 
         // Show "Login As" input
         if (empty($admin_login) || (empty($group) && empty($role_attr))) {
             return $args;
         }
 
         // Don't add the extra field on 2FA form
         if (strpos($args['content'], 'plugin.kolab-2fa-login')) {
             return $args;
         }
 
         $input = new html_inputfield(array('name' => '_loginas', 'id' => 'rcmloginas',
             'type' => 'text', 'autocomplete' => 'off'));
         $row = html::tag('tr', null,
             html::tag('td', 'title', html::label('rcmloginas', rcube::Q($this->gettext('loginas'))))
             . html::tag('td', 'input', $input->show(trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST))))
         );
         // add icon style for Elastic
         $style = html::tag('style', [], '#login-form .input-group .icon.loginas::before { content: "\f508"; } ');
         $args['content'] = preg_replace('/<\/tbody>/i', $row . '</tbody>' . $style, $args['content']);
 
         return $args;
     }
 
     /**
      * Find user credentials In LDAP.
      */
     public function authenticate($args)
     {
         // get username and host
         $host    = $args['host'];
         $user    = $args['user'];
         $pass    = $args['pass'];
         $loginas = trim(rcube_utils::get_input_value('_loginas', rcube_utils::INPUT_POST));
 
         if (empty($user) || (empty($pass) && empty($_SERVER['REMOTE_USER']))) {
             $args['abort'] = true;
             return $args;
         }
 
         // temporarily set the current username to the one submitted
         $this->username = $user;
 
         $ldap = self::ldap();
         if (!$ldap || !$ldap->ready) {
             self::log_login_error($user, "LDAP not ready");
 
             $args['abort']            = true;
             $args['kolab_ldap_error'] = true;
 
             return $args;
         }
 
         // Find user record in LDAP
         $record = $ldap->get_user_record($user, $host);
 
         if (empty($record)) {
             self::log_login_error($user, "No user record found");
 
             $args['abort'] = true;
 
             return $args;
         }
 
         $rcmail      = rcube::get_instance();
         $admin_login = $rcmail->config->get('kolab_auth_admin_login');
         $admin_pass  = $rcmail->config->get('kolab_auth_admin_password');
         $login_attr  = $rcmail->config->get('kolab_auth_login');
         $name_attr   = $rcmail->config->get('kolab_auth_name');
         $email_attr  = $rcmail->config->get('kolab_auth_email');
         $org_attr    = $rcmail->config->get('kolab_auth_organization');
         $role_attr   = $rcmail->config->get('kolab_auth_role');
         $imap_attr   = $rcmail->config->get('kolab_auth_mailhost');
 
         if (!empty($role_attr) && !empty($record[$role_attr])) {
             $_SESSION['user_roledns'] = (array)($record[$role_attr]);
         }
 
         if (!empty($imap_attr) && !empty($record[$imap_attr])) {
             $default_host = $rcmail->config->get('default_host');
             if (!empty($default_host)) {
                 rcube::write_log("errors", "Both default host and kolab_auth_mailhost set. Incompatible.");
             } else {
                 $args['host'] = "tls://" . $record[$imap_attr];
             }
         }
 
         // Login As...
         if (!empty($loginas) && $admin_login) {
             // Authenticate to LDAP
             $result = $ldap->bind($record['dn'], $pass);
 
             if (!$result) {
                 self::log_login_error($user, "Unable to bind with '" . $record['dn'] . "'");
 
                 $args['abort'] = true;
 
                 return $args;
             }
 
             $isadmin = false;
             $admin_rights = $rcmail->config->get('kolab_auth_admin_rights', array());
 
             // @deprecated: fall-back to the old check if the original user has/belongs to administrative role/group
             if (empty($admin_rights)) {
                 $group   = $rcmail->config->get('kolab_auth_group');
                 $role_dn = $rcmail->config->get('kolab_auth_role_value');
 
                 // check role attribute
                 if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) {
                     $role_dn = $ldap->parse_vars($role_dn, $user, $host);
                     if (in_array($role_dn, (array)$record[$role_attr])) {
                         $isadmin = true;
                     }
                 }
 
                 // check group
                 if (!$isadmin && !empty($group)) {
                     $groups = $ldap->get_user_groups($record['dn'], $user, $host);
                     if (in_array($group, $groups)) {
                         $isadmin = true;
                     }
                 }
 
                 if ($isadmin) {
                     // user has admin privileges privilage, get "login as" user credentials
                     $target_entry = $ldap->get_user_record($loginas, $host);
                     $allowed_tasks = $rcmail->config->get('kolab_auth_allowed_tasks');
                 }
             }
             else {
                 // get "login as" user credentials
                 $target_entry = $ldap->get_user_record($loginas, $host);
 
                 if (!empty($target_entry)) {
                     // get effective rights to determine login-as permissions
                     $effective_rights = (array)$ldap->effective_rights($target_entry['dn']);
 
                     if (!empty($effective_rights)) {
                         // compat with out of date Net_LDAP3
                         $effective_rights = array_change_key_case($effective_rights, CASE_LOWER);
 
                         $effective_rights['attrib'] = $effective_rights['attributelevelrights'];
                         $effective_rights['entry']  = $effective_rights['entrylevelrights'];
 
                         // compare the rights with the permissions mapping
                         $allowed_tasks = array();
                         foreach ($admin_rights as $task => $perms) {
                             $perms_ = explode(':', $perms);
                             $type   = array_shift($perms_);
                             $req    = array_pop($perms_);
                             $attrib = array_pop($perms_);
 
                             if (array_key_exists($type, $effective_rights)) {
                                 if ($type == 'entry' && in_array($req, $effective_rights[$type])) {
                                     $allowed_tasks[] = $task;
                                 }
                                 else if ($type == 'attrib' && array_key_exists($attrib, $effective_rights[$type]) &&
                                         in_array($req, $effective_rights[$type][$attrib])) {
                                     $allowed_tasks[] = $task;
                                 }
                             }
                         }
 
                         $isadmin = !empty($allowed_tasks);
                     }
                 }
             }
 
             // Save original user login for log (see below)
             if ($login_attr) {
                 $origname = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
             }
             else {
                 $origname = $user;
             }
 
             if (!$isadmin || empty($target_entry)) {
                 $this->add_texts('localization/');
 
                 $args['abort'] = true;
                 $args['error'] = $this->gettext(array(
                     'name' => 'loginasnotallowed',
                     'vars' => array('user' => rcube::Q($loginas)),
                 ));
 
                 self::log_login_error($user, "No privileges to login as '" . $loginas . "'", $loginas);
 
                 return $args;
             }
 
             // replace $record with target entry
             $record = $target_entry;
 
             $args['user'] = $this->username = $loginas;
 
             // Mark session to use SASL proxy for IMAP authentication
             $_SESSION['kolab_auth_admin']    = strtolower($origname);
             $_SESSION['kolab_auth_login']    = $rcmail->encrypt($admin_login);
             $_SESSION['kolab_auth_password'] = $rcmail->encrypt($admin_pass);
             $_SESSION['kolab_auth_allowed_tasks'] = $allowed_tasks;
         }
 
         // Store UID and DN of logged user in session for use by other plugins
         $_SESSION['kolab_uid'] = is_array($record['uid']) ? $record['uid'][0] : $record['uid'];
         $_SESSION['kolab_dn']  = $record['dn'];
 
         // Store LDAP replacement variables used for current user
         // This improves performance of load_user_role_plugins_and_settings()
         // which is executed on every request (via startup hook) and where
         // we don't like to use LDAP (connection + bind + search)
         $_SESSION['kolab_auth_vars'] = $ldap->get_parse_vars();
 
         // Store user unique identifier for freebusy_session_auth feature
         $_SESSION['kolab_auth_uniqueid'] = is_array($record['uniqueid']) ? $record['uniqueid'][0] : $record['uniqueid'];
 
         // Store also host as we need it for get_user_reacod() in 'ready' hook handler
         $_SESSION['kolab_host'] = $host;
 
         // Set user login
         if ($login_attr) {
             $this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
         }
         if ($this->data['user_login']) {
             $args['user'] = $this->username = $this->data['user_login'];
         }
 
         // User name for identity (first log in)
         foreach ((array)$name_attr as $field) {
             $name = is_array($record[$field]) ? $record[$field][0] : $record[$field];
             if (!empty($name)) {
                 $this->data['user_name'] = $name;
                 break;
             }
         }
         // User email(s) for identity (first log in)
         foreach ((array)$email_attr as $field) {
             $email = is_array($record[$field]) ? array_filter($record[$field]) : $record[$field];
             if (!empty($email)) {
-                $this->data['user_email'] = array_merge((array)$this->data['user_email'], (array)$email);
+                $this->data['user_email'] = array_merge((array)($this->data['user_email'] ?? null), (array)$email);
             }
         }
         // Organization name for identity (first log in)
         foreach ((array)$org_attr as $field) {
             $organization = is_array($record[$field]) ? $record[$field][0] : $record[$field];
             if (!empty($organization)) {
                 $this->data['user_organization'] = $organization;
                 break;
             }
         }
 
         // Log "Login As" usage
         if (!empty($origname)) {
             rcube::write_log('userlogins', sprintf('Admin login for %s by %s from %s',
                 $args['user'], $origname, rcube_utils::remote_ip()));
         }
 
         // load per-user settings/plugins
         $this->load_user_role_plugins_and_settings(true);
 
         return $args;
     }
 
     /**
      * Set user DN for password change (password plugin with ldap_simple driver)
      */
     public function password_ldap_bind($args)
     {
         $args['user_dn'] = $_SESSION['kolab_dn'];
 
         $rcmail = rcube::get_instance();
 
         $rcmail->config->set('password_ldap_method', 'user');
 
         return $args;
     }
 
     /**
      * Sets SASL Proxy login/password for IMAP and Managesieve auth
      */
     public function imap_connect($args)
     {
         if (!empty($_SESSION['kolab_auth_admin'])) {
             $rcmail      = rcube::get_instance();
             $admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
             $admin_pass  = $rcmail->decrypt($_SESSION['kolab_auth_password']);
 
             $args['auth_cid'] = $admin_login;
             $args['auth_pw']  = $admin_pass;
         }
 
         return $args;
     }
 
     /**
      * Sets SASL Proxy login/password for SMTP auth
      */
     public function smtp_connect($args)
     {
         if (!empty($_SESSION['kolab_auth_admin'])) {
             $rcmail      = rcube::get_instance();
             $admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
             $admin_pass  = $rcmail->decrypt($_SESSION['kolab_auth_password']);
 
             $args['smtp_auth_cid'] = $admin_login;
             $args['smtp_auth_pw']  = $admin_pass;
         }
 
         return $args;
     }
 
     /**
      * Hook to replace the plain text input field for email address by a drop-down list
      * with all email addresses (including aliases) from this user's LDAP record.
      */
     public function identity_form($args)
     {
         $rcmail      = rcube::get_instance();
         $ident_level = intval($rcmail->config->get('identities_level', 0));
 
         // do nothing if email address modification is disabled
         if ($ident_level == 1 || $ident_level == 3) {
             return $args;
         }
 
         $ldap = self::ldap();
         if (!$ldap || !$ldap->ready || empty($_SESSION['kolab_dn'])) {
             return $args;
         }
 
         $emails      = array();
         $user_record = $ldap->get_record($_SESSION['kolab_dn']);
 
         foreach ((array)$rcmail->config->get('kolab_auth_email', array()) as $col) {
             $values = rcube_addressbook::get_col_values($col, $user_record, true);
             if (!empty($values))
                 $emails = array_merge($emails, array_filter($values));
         }
 
         // kolab_delegation might want to modify this addresses list
         $plugin = $rcmail->plugins->exec_hook('kolab_auth_emails', array('emails' => $emails));
         $emails = $plugin['emails'];
 
         if (!empty($emails)) {
             $args['form']['addressing']['content']['email'] = array(
                 'type' => 'select',
                 'options' => array_combine($emails, $emails),
             );
         }
 
         return $args;
     }
 
     /**
      * Action executed before the page is rendered to add an onload script
      * that will remove all taskbar buttons for disabled tasks
      */
     public function render_page($args)
     {
         $rcmail  = rcube::get_instance();
         $tasks   = (array)$_SESSION['kolab_auth_allowed_tasks'];
         $tasks[] = 'logout';
 
         // disable buttons in taskbar
         $script = "
         \$('a').filter(function() {
             var ev = \$(this).attr('onclick');
             return ev && ev.match(/'switch-task','([a-z]+)'/)
                 && \$.inArray(RegExp.\$1, " . json_encode($tasks) . ") < 0;
         }).remove();
         ";
 
         $rcmail->output->add_script($script, 'docready');
     }
 
     /**
      * Initializes LDAP object and connects to LDAP server
      */
     public static function ldap()
     {
         self::$ldap = kolab_storage::ldap('kolab_auth_addressbook');
 
         if (self::$ldap) {
             self::$ldap->extend_fieldmap(array('uniqueid' => 'nsuniqueid'));
         }
 
         return self::$ldap;
     }
 
     /**
      * Close LDAP connection
      */
     public static function ldap_close()
     {
         if (self::$ldap) {
             self::$ldap->close();
             self::$ldap = null;
         }
     }
 
     /**
      * Parses LDAP DN string with replacing supported variables.
      * See kolab_ldap::parse_vars()
      *
      * @param string $str LDAP DN string
      *
      * @return string Parsed DN string
      */
     public static function parse_ldap_vars($str)
     {
         if (!empty($_SESSION['kolab_auth_vars'])) {
             $str = strtr($str, $_SESSION['kolab_auth_vars']);
         }
 
         return $str;
     }
 
     /**
      * Log failed logins
      *
      * @param string $username Username/Login
      * @param string $message  Error message (failure reason)
      * @param string $login_as Username/Login of "login as" user
      */
     public static function log_login_error($username, $message = null, $login_as = null)
     {
         $config = rcube::get_instance()->config;
 
         if ($config->get('log_logins')) {
             // don't fill the log with complete input, which could
             // have been prepared by a hacker
             if (strlen($username) > 256) {
                 $username = substr($username, 0, 256) . '...';
             }
             if (strlen($login_as) > 256) {
                 $login_as = substr($login_as, 0, 256) . '...';
             }
 
             if ($login_as) {
                 $username = sprintf('%s (as user %s)', $username, $login_as);
             }
 
             // Don't log full session id for better security
             $session_id = session_id();
             $session_id = $session_id ? substr($session_id, 0, 16) : 'no-session';
 
             $message = sprintf(
                 "Failed login for %s from %s in session %s %s",
                 $username,
                 rcube_utils::remote_ip(),
                 $session_id,
                 $message ? "($message)" : ''
             );
 
             rcube::write_log('userlogins', $message);
 
             // disable log_logins to prevent from duplicate log entries
             $config->set('log_logins', false);
         }
     }
 }
diff --git a/plugins/kolab_files/lib/kolab_files_engine.php b/plugins/kolab_files/lib/kolab_files_engine.php
index 52b706be..7c8e9c0d 100644
--- a/plugins/kolab_files/lib/kolab_files_engine.php
+++ b/plugins/kolab_files/lib/kolab_files_engine.php
@@ -1,1808 +1,1810 @@
 <?php
 
 /**
  * Kolab files storage engine
  *
  * @version @package_version@
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2013-2015, Kolab Systems AG <contact@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_files_engine
 {
     private $plugin;
     private $rc;
     private $url;
     private $url_srv;
     private $timeout = 600;
     private $files_sort_cols    = array('name', 'mtime', 'size');
     private $sessions_sort_cols = array('name');
+    private $mimetypes = null;
 
     const API_VERSION = 4;
 
 
     /**
      * Class constructor
      */
     public function __construct($plugin, $client_url, $server_url = null)
     {
         $this->url     = rtrim(rcube_utils::resolve_url($client_url), '/ ');
         $this->url_srv = $server_url ? rtrim(rcube_utils::resolve_url($server_url), '/ ') : $this->url;
         $this->plugin  = $plugin;
         $this->rc      = $plugin->rc;
         $this->timeout = $this->rc->config->get('session_lifetime') * 60;
     }
 
     /**
      * User interface initialization
      */
     public function ui()
     {
         $this->plugin->add_texts('localization/');
 
         $templates = array();
+        $list_widget = false;
 
         // set templates of Files UI and widgets
         if ($this->rc->task == 'mail') {
             if (in_array($this->rc->action, array('', 'show', 'compose'))) {
                 $templates[] = 'compose_plugin';
             }
             if (in_array($this->rc->action, array('show', 'preview', 'get'))) {
                 $templates[] = 'message_plugin';
 
                 if ($this->rc->action == 'get') {
                     // add "Save as" button into attachment toolbar
                     $this->plugin->add_button(array(
                         'id'         => 'saveas',
                         'name'       => 'saveas',
                         'type'       => 'link',
                         'onclick'    => 'kolab_directory_selector_dialog()',
                         'class'      => 'button buttonPas saveas',
                         'classact'   => 'button saveas',
                         'label'      => 'kolab_files.save',
                         'title'      => 'kolab_files.saveto',
                         ), 'toolbar');
                 }
                 else {
                     // add "Save as" button into attachment menu
                     $this->plugin->add_button(array(
                         'id'         => 'attachmenusaveas',
                         'name'       => 'attachmenusaveas',
                         'type'       => 'link',
                         'wrapper'    => 'li',
                         'onclick'    => 'return false',
                         'class'      => 'icon active saveas',
                         'classact'   => 'icon active saveas',
                         'innerclass' => 'icon active saveas',
                         'label'      => 'kolab_files.saveto',
                         ), 'attachmentmenu');
                 }
             }
 
             $list_widget = true;
         }
         else if (!$this->rc->action && in_array($this->rc->task, array('calendar', 'tasks'))) {
             $list_widget = true;
             $templates[] = 'compose_plugin';
         }
         else if ($this->rc->task == 'files') {
             $templates[] = 'files';
 
             // get list of external sources
             $this->get_external_storage_drivers();
 
             // these labels may be needed even if fetching ext sources failed
             $this->plugin->add_label('folderauthtitle', 'authenticating', 'foldershare', 'saving');
         }
 
         if ($list_widget) {
             $this->folder_list_env();
 
             $this->plugin->add_label('save', 'cancel', 'saveto',
                 'saveall', 'fromcloud', 'attachsel', 'selectfiles', 'attaching',
                 'collection_audio', 'collection_video', 'collection_image', 'collection_document',
                 'folderauthtitle', 'authenticating'
             );
         }
 
         // add taskbar button
         if (empty($_REQUEST['framed'])) {
             $this->plugin->add_button(array(
                 'command'    => 'files',
                 'class'      => 'button-files',
                 'classsel'   => 'button-files button-selected',
                 'innerclass' => 'button-inner',
                 'label'      => 'kolab_files.files',
                 'type'       => 'link'
                 ), 'taskbar');
         }
 
         $caps = $this->capabilities();
 
         $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/style.css');
         $this->plugin->include_script($this->url . '/js/files_api.js');
         $this->plugin->include_script('kolab_files.js');
 
         $this->rc->output->set_env('files_url', $this->url . '/api/');
         $this->rc->output->set_env('files_token', $this->get_api_token());
         $this->rc->output->set_env('files_caps', $caps);
-        $this->rc->output->set_env('files_api_version', $caps['VERSION'] ?: 3);
+        $this->rc->output->set_env('files_api_version', $caps['VERSION'] ?? 3);
         $this->rc->output->set_env('files_user', $this->rc->get_user_name());
 
         if ($caps['DOCEDIT']) {
             $this->plugin->add_label('declinednotice', 'invitednotice', 'acceptedownernotice',
                 'declinedownernotice', 'requestednotice', 'acceptednotice', 'declinednotice',
                 'more', 'accept', 'decline', 'join', 'status', 'when', 'file', 'comment',
                 'statusaccepted', 'statusinvited', 'statusdeclined', 'statusrequested',
                 'invitationaccepting', 'invitationdeclining', 'invitationrequesting',
                 'close', 'invitationtitle', 'sessions', 'saving');
         }
 
         if (!empty($templates)) {
             $collapsed_folders = (string) $this->rc->config->get('kolab_files_collapsed_folders');
 
             $this->rc->output->include_script('treelist.js');
             $this->rc->output->set_env('kolab_files_collapsed_folders', $collapsed_folders);
 
             // register template objects for dialogs (and main interface)
             $this->rc->output->add_handlers(array(
                 'folder-create-form' => array($this, 'folder_create_form'),
                 'folder-edit-form'   => array($this, 'folder_edit_form'),
                 'folder-mount-form'  => array($this, 'folder_mount_form'),
                 'folder-auth-options'=> array($this, 'folder_auth_options'),
                 'file-search-form'   => array($this, 'file_search_form'),
                 'file-rename-form'   => array($this, 'file_rename_form'),
                 'file-create-form'   => array($this, 'file_create_form'),
                 'file-edit-dialog'   => array($this, 'file_edit_dialog'),
                 'file-session-dialog' => array($this, 'file_session_dialog'),
                 'filelist'           => array($this, 'file_list'),
                 'sessionslist'       => array($this, 'sessions_list'),
                 'filequotadisplay'   => array($this, 'quota_display'),
                 'document-editors-dialog' => array($this, 'document_editors_dialog'),
             ));
 
             if ($this->rc->task != 'files') {
                 // add dialog(s) content at the end of page body
                 foreach ($templates as $template) {
                     $this->rc->output->add_footer(
                         $this->rc->output->parse('kolab_files.' . $template, false, false));
                 }
             }
         }
     }
 
     /**
      * Engine actions handler
      */
     public function actions()
     {
         if ($this->rc->task == 'files' && $this->rc->action) {
             $action = $this->rc->action;
         }
         else if ($this->rc->task != 'files' && $_POST['act']) {
             $action = $_POST['act'];
         }
         else {
             $action = 'index';
         }
 
         $method = 'action_' . str_replace('-', '_', $action);
 
         if (method_exists($this, $method)) {
             $this->plugin->add_texts('localization/');
             $this->{$method}();
         }
     }
 
     /**
      * Template object for folder creation form
      */
     public function folder_create_form($attrib)
     {
         $attrib['name'] = 'folder-create-form';
         if (empty($attrib['id'])) {
             $attrib['id'] = 'folder-create-form';
         }
 
         $input_name    = new html_inputfield(array('id' => 'folder-name', 'name' => 'name', 'size' => 30));
         $select_parent = new html_select(array('id' => 'folder-parent', 'name' => 'parent'));
         $table         = new html_table(array('cols' => 2, 'class' => 'propform'));
 
         $table->add('title', html::label('folder-name', rcube::Q($this->plugin->gettext('foldername'))));
         $table->add(null, $input_name->show());
         $table->add('title', html::label('folder-parent', rcube::Q($this->plugin->gettext('folderinside'))));
         $table->add(null, $select_parent->show());
 
         $out = $table->show();
 
         // add form tag around text field
         if (empty($attrib['form'])) {
             $out = $this->rc->output->form_tag($attrib, $out);
         }
 
         $this->plugin->add_label('foldercreating', 'foldercreatenotice', 'create', 'foldercreate', 'cancel', 'addfolder');
         $this->rc->output->add_gui_object('folder-create-form', $attrib['id']);
 
         return $out;
     }
 
     /**
      * Template object for folder editing form
      */
     public function folder_edit_form($attrib)
     {
         $attrib['name'] = 'folder-edit-form';
         if (empty($attrib['id'])) {
             $attrib['id'] = 'folder-edit-form';
         }
 
         $input_name    = new html_inputfield(array('id' => 'folder-edit-name', 'name' => 'name', 'size' => 30));
         $select_parent = new html_select(array('id' => 'folder-edit-parent', 'name' => 'parent'));
         $table         = new html_table(array('cols' => 2, 'class' => 'propform'));
 
         $table->add('title', html::label('folder-edit-name', rcube::Q($this->plugin->gettext('foldername'))));
         $table->add(null, $input_name->show());
         $table->add('title', html::label('folder-edit-parent', rcube::Q($this->plugin->gettext('folderinside'))));
         $table->add(null, $select_parent->show());
 
         $out = $table->show();
 
         // add form tag around text field
         if (empty($attrib['form'])) {
             $out = $this->rc->output->form_tag($attrib, $out);
         }
 
         $this->plugin->add_label('folderupdating', 'folderupdatenotice', 'save', 'folderedit', 'cancel');
         $this->rc->output->add_gui_object('folder-edit-form', $attrib['id']);
 
         return $out;
     }
 
     /**
      * Template object for folder mounting form
      */
     public function folder_mount_form($attrib)
     {
         $sources = $this->rc->output->get_env('external_sources');
 
         if (empty($sources) || !is_array($sources)) {
             return '';
         }
 
         $attrib['name'] = 'folder-mount-form';
         if (empty($attrib['id'])) {
             $attrib['id'] = 'folder-mount-form';
         }
 
         // build form content
         $table        = new html_table(array('cols' => 2, 'class' => 'propform'));
         $input_name   = new html_inputfield(array('id' => 'folder-mount-name', 'name' => 'name', 'size' => 30));
         $input_driver = new html_radiobutton(array('name' => 'driver', 'size' => 30));
 
         $table->add('title', html::label('folder-mount-name', rcube::Q($this->plugin->gettext('name'))));
         $table->add(null, $input_name->show());
 
         foreach ($sources as $key => $source) {
             $id    = 'source-' . $key;
             $form  = new html_table(array('cols' => 2, 'class' => 'propform driverform'));
 
             foreach ((array) $source['form'] as $idx => $label) {
                 $iid = $id . '-' . $idx;
                 $type  = stripos($idx, 'pass') !== false ? 'html_passwordfield' : 'html_inputfield';
                 $input = new $type(array('size' => 30));
 
                 $form->add('title', html::label($iid, rcube::Q($label)));
                 $form->add(null, $input->show('', array(
                         'id'   => $iid,
                         'name' => $key . '[' . $idx . ']'
                 )));
             }
 
             $row = $input_driver->show(null, array('value' => $key))
                 . html::img(array('src' => $source['image'], 'alt' => $key, 'title' => $source['name']))
                 . html::div(null, html::span('name', rcube::Q($source['name']))
                     . html::br()
                     . html::span('description hint', rcube::Q($source['description']))
                     . $form->show()
                 );
 
             $table->add(array('id' => $id, 'colspan' => 2, 'class' => 'source'), $row);
         }
 
         $out = $table->show() . $this->folder_auth_options(array('suffix' => '-form'));
 
         // add form tag around text field
         if (empty($attrib['form'])) {
             $out = $this->rc->output->form_tag($attrib, $out);
         }
 
         $this->plugin->add_label('foldermounting', 'foldermountnotice', 'foldermount',
             'save', 'cancel', 'folderauthtitle', 'authenticating'
         );
         $this->rc->output->add_gui_object('folder-mount-form', $attrib['id']);
 
         return $out;
     }
 
     /**
      * Template object for folder authentication options
      */
     public function folder_auth_options($attrib)
     {
         $checkbox = new html_checkbox(array(
             'name'  => 'store_passwords',
             'value' => '1',
             'class' => 'pretty-checkbox',
         ));
 
         return html::div('auth-options',
             html::label(null, $checkbox->show() . ' ' . $this->plugin->gettext('storepasswords'))
             . html::p('description hint', $this->plugin->gettext('storepasswordsdesc'))
         );
     }
 
     /**
      * Template object for sharing form
      */
     public function folder_share_form($attrib)
     {
         $folder = rcube_utils::get_input_value('_folder', rcube_utils::INPUT_GET, true);
 
         $info = $this->get_share_info($folder);
 
         if (empty($info) || empty($info['form'])) {
             $msg = $this->plugin->gettext($info === false ? 'sharepermissionerror' : 'sharestorageerror');
             return html::div(array('class' => 'boxerror', 'id' => 'share-notice'), rcube::Q($msg));
         }
 
         if (empty($attrib['id'])) {
             $attrib['id'] = 'foldershareform';
         }
 
         $out = '';
 
         foreach ($info['form'] as $mode => $tab) {
             $table  = new html_table(array(
                     'cols'        => ($tab['list_column'] ? 1 : count($tab['form'])) + 1,
                     'data-mode'   => $mode,
                     'data-single' => $tab['single'] ? 1 : 0,
             ));
             $submit = new html_button(array('class' => 'btn btn-secondary submit'));
             $delete = new html_button(array('class' => 'btn btn-secondary btn-danger delete'));
             $fields = array();
 
             // Table header
             if (!empty($tab['list_column'])) {
                 $table->add_header(null, rcube::Q($tab['list_column_label']));
             }
             else {
                 foreach ($tab['form'] as $field) {
                     $table->add_header(null, rcube::Q($field['title']));
                 }
             }
             $table->add_header(null, '');
 
             // Submit form
             $record = '';
             foreach ($tab['form'] as $index => $field) {
                 $add = '';
                 if ($field['type'] == 'select') {
                     $ff = new html_select(array('name' => $index));
                     foreach ($field['options'] as $opt_idx => $opt) {
                         $ff->add($opt, $opt_idx);
                     }
                 }
                 else if ($field['type'] == 'password') {
                     $ff = new html_passwordfield(array(
                             'name'        => $index,
                             'placeholder' => $this->rc->gettext('password'),
                     ));
                     $add = new html_passwordfield(array(
                             'name'        => $index . 'confirm',
                             'placeholder' => $this->plugin->gettext('confirmpassword'),
                     ));
                     $add = $add->show();
                 }
                 else {
                     $ff = new html_inputfield(array(
                             'name'              => $index,
                             'data-autocomplete' => $field['autocomplete'],
                             'placeholder'       => $field['placeholder'],
                     ));
                 }
 
                 if (!empty($tab['list_column'])) {
                     $record .= $ff->show() . $add;
                 }
                 else {
                     $table->add(null, $ff->show() . $add);
                 }
                 $fields[$index] = $ff;
             }
 
             if (!empty($tab['list_column'])) {
                 $table->add('form', $record);
             }
 
             $hidden = '';
             foreach ((array) $tab['extra_fields'] as $key => $default) {
                 $h = new html_hiddenfield(array('name' => $key, 'value' => $default));
                 $hidden .= $h->show();
             }
 
             $table->add(null, $hidden . $submit->show(rcube::Q($tab['label'] ?: $this->plugin->gettext('submit'))));
 
             // Existing entries
             foreach ((array) $info['rights'] as $entry) {
                 if ($entry['mode'] == $mode) {
                     if (!empty($tab['list_column'])) {
                         $table->add(null, html::span(array('title' => $entry['title'], 'class' => 'name'), rcube::Q($entry[$tab['list_column']])));
                     }
                     else {
                         foreach ($tab['form'] as $index => $field) {
                             if ($fields[$index] instanceof html_select) {
                                 $table->add(null, $fields[$index]->show($entry[$index]));
                             }
                             else if ($fields[$index] instanceof html_inputfield) {
                                 $table->add(null, html::span(array('title' => $entry['title'], 'class' => 'name'), rcube::Q($entry[$index])));
                             }
                         }
                     }
 
                     $hidden = '';
                     foreach ((array) $tab['extra_fields'] as $key => $default) {
                         if (isset($entry[$key])) {
                             $h = new html_hiddenfield(array('name' => $key, 'value' => $entry[$key]));
                             $hidden .= $h->show();
                         }
                     }
 
                     $table->add(null, $hidden . $delete->show(rcube::Q($this->rc->gettext('delete'))));
                 }
             }
 
             $this->rc->output->add_label('kolab_files.updatingfolder' . $mode);
 
             $out .= html::tag('fieldset', $mode, html::tag('legend', null, rcube::Q($tab['title'])) . $table->show()) . "\n";
         }
 
         $this->rc->autocomplete_init();
 
         $this->rc->output->set_env('folder', $folder);
         $this->rc->output->set_env('form_info', $info['form']);
         $this->rc->output->add_gui_object('shareform', $attrib['id']);
         $this->rc->output->add_label('kolab_files.submit', 'kolab_files.passwordconflict', 'delete');
 
         return html::div($attrib, $out);
     }
 
     /**
      * Template object for file edit dialog/warnings
      */
     public function file_edit_dialog($attrib)
     {
         $this->plugin->add_label('select', 'create', 'cancel', 'editfiledialog', 'editfilesessions',
             'newsession', 'ownedsession', 'invitedsession', 'joinsession', 'editfilero', 'editfilerotitle',
             'newsessionro'
         );
 
         return '<div></div>';
     }
 
     /**
      * Template object for file session dialog
      */
     public function file_session_dialog($attrib)
     {
         $this->plugin->add_label('join', 'open', 'close', 'request', 'cancel',
             'sessiondialog', 'sessiondialogcontent');
 
         return '<div></div>';
     }
 
     /**
      * Template object for dcument editors dialog
      */
     public function document_editors_dialog($attrib)
     {
         $table = new html_table($attrib + array('cols' => 3, 'border' => 0, 'cellpadding' => 0));
 
         $table->add_header('username', $this->plugin->gettext('participant'));
         $table->add_header('status', $this->plugin->gettext('status'));
         $table->add_header('options', null);
 
         $input    = new html_inputfield(array('name' => 'participant', 'id' => 'invitation-editor-name', 'size' => 30, 'class' => 'form-control'));
         $textarea = new html_textarea(array('name' => 'comment', 'id' => 'invitation-comment',
             'rows' => 4, 'cols' => 55, 'class' => 'form-control', 'title' => $this->plugin->gettext('invitationtexttitle')));
         $button   = new html_inputfield(array('type' => 'button', 'class' => 'button', 'id' => 'invitation-editor-add',
             'value' => $this->plugin->gettext('addparticipant')));
 
         $this->plugin->add_label('manageeditors', 'statusorganizer', 'addparticipant');
 
         // initialize attendees autocompletion
         $this->rc->autocomplete_init();
 
         return html::div(null, $table->show() . html::div(null,
             html::div('form-searchbar', $input->show() . " " . $button->show())
             . html::p('attendees-commentbox', html::label(null,
                 $this->plugin->gettext('invitationtextlabel') . $textarea->show())
             )
         ));
     }
 
     /**
      * Template object for file_rename form
      */
     public function file_rename_form($attrib)
     {
         $attrib['name'] = 'file-rename-form';
         if (empty($attrib['id'])) {
             $attrib['id'] = 'file-rename-form';
         }
 
         $input_name = new html_inputfield(array('id' => 'file-rename-name', 'name' => 'name', 'size' => 50));
         $table      = new html_table(array('cols' => 2, 'class' => 'propform'));
 
         $table->add('title', html::label('file-rename-name', rcube::Q($this->plugin->gettext('filename'))));
         $table->add(null, $input_name->show());
 
         $out = $table->show();
 
         // add form tag around text field
         if (empty($attrib['form'])) {
             $out = $this->rc->output->form_tag($attrib, $out);
         }
 
         $this->plugin->add_label('save', 'cancel', 'fileupdating', 'renamefile');
         $this->rc->output->add_gui_object('file-rename-form', $attrib['id']);
 
         return $out;
     }
 
     /**
      * Template object for file_create form
      */
     public function file_create_form($attrib)
     {
         $attrib['name'] = 'file-create-form';
         if (empty($attrib['id'])) {
             $attrib['id'] = 'file-create-form';
         }
 
         $input_name    = new html_inputfield(array('id' => 'file-create-name', 'name' => 'name', 'size' => 30));
         $select_parent = new html_select(array('id' => 'file-create-parent', 'name' => 'parent'));
         $select_type   = new html_select(array('id' => 'file-create-type', 'name' => 'type'));
         $table         = new html_table(array('cols' => 2, 'class' => 'propform'));
 
         $types = array();
 
         foreach ($this->get_mimetypes('edit') as $type => $mimetype) {
             $types[$type] = $mimetype['ext'];
             $select_type->add($mimetype['label'], $type);
         }
 
         $table->add('title', html::label('file-create-name', rcube::Q($this->plugin->gettext('filename'))));
         $table->add(null, $input_name->show());
         $table->add('title', html::label('file-create-type', rcube::Q($this->plugin->gettext('type'))));
         $table->add(null, $select_type->show());
         $table->add('title', html::label('file-create-parent', rcube::Q($this->plugin->gettext('folderinside'))));
         $table->add(null, $select_parent->show());
 
         $out = $table->show();
 
         // add form tag around text field
         if (empty($attrib['form'])) {
             $out = $this->rc->output->form_tag($attrib, $out);
         }
 
         $this->plugin->add_label('create', 'cancel', 'filecreating', 'createfile', 'createandedit',
             'copyfile', 'copyandedit');
         $this->rc->output->add_gui_object('file-create-form', $attrib['id']);
         $this->rc->output->set_env('file_extensions', $types);
 
         return $out;
     }
 
     /**
      * Template object for file search form in "From cloud" dialog
      */
     public function file_search_form($attrib)
     {
         $attrib += array(
             'name'          => '_q',
             'gui-object'    => 'filesearchbox',
             'form-name'     => 'filesearchform',
             'command'       => 'files-search',
             'reset-command' => 'files-search-reset',
         );
 
         // add form tag around text field
         return $this->rc->output->search_form($attrib);
     }
 
     /**
      * Template object for files list
      */
     public function file_list($attrib)
     {
         return $this->list_handler($attrib, 'files');
     }
 
     /**
      * Template object for sessions list
      */
     public function sessions_list($attrib)
     {
         return $this->list_handler($attrib, 'sessions');
     }
 
     /**
      * Creates unified template object for files|sessions list
      */
     protected function list_handler($attrib, $type = 'files')
     {
         $prefix   = 'kolab_' . $type . '_';
         $c_prefix = 'kolab_files' . ($type != 'files' ? '_' . $type : '') . '_';
 
         // define list of cols to be displayed based on parameter or config
         if (empty($attrib['columns'])) {
             $list_cols     = $this->rc->config->get($c_prefix . 'list_cols');
             $dont_override = $this->rc->config->get('dont_override');
             $a_show_cols = is_array($list_cols) ? $list_cols : array('name');
             $this->rc->output->set_env($type . '_col_movable', !in_array($c_prefix . 'list_cols', (array)$dont_override));
         }
         else {
             $columns     = str_replace(array("'", '"'), '', $attrib['columns']);
             $a_show_cols = preg_split('/[\s,;]+/', $columns);
         }
 
         // make sure 'name' and 'options' column is present
         if (!in_array('name', $a_show_cols)) {
             array_unshift($a_show_cols, 'name');
         }
         if (!in_array('options', $a_show_cols)) {
             array_unshift($a_show_cols, 'options');
         }
 
         $attrib['columns'] = $a_show_cols;
 
         // save some variables for use in ajax list
         $_SESSION[$prefix . 'list_attrib'] = $attrib;
 
         // For list in dialog(s) remove all option-like columns
         if ($this->rc->task != 'files') {
             $a_show_cols = array_intersect($a_show_cols, $this->{$type . '_sort_cols'});
         }
 
         // set default sort col/order to session
         if (!isset($_SESSION[$prefix . 'sort_col']))
             $_SESSION[$prefix . 'sort_col'] = $this->rc->config->get($c_prefix . 'sort_col') ?: 'name';
         if (!isset($_SESSION[$prefix . 'sort_order']))
             $_SESSION[$prefix . 'sort_order'] = strtoupper($this->rc->config->get($c_prefix . 'sort_order') ?: 'asc');
 
         // set client env
         $this->rc->output->add_gui_object($type . 'list', $attrib['id']);
         $this->rc->output->set_env($type . '_sort_col', $_SESSION[$prefix . 'sort_col']);
         $this->rc->output->set_env($type . '_sort_order', $_SESSION[$prefix . 'sort_order']);
         $this->rc->output->set_env($type . '_coltypes', $a_show_cols);
 
         $this->rc->output->include_script('list.js');
 
         $this->rc->output->add_label('kolab_files.abort', 'searching');
 
         // attach css rules for mimetype icons
         if (!$this->filetypes_style) {
             $this->plugin->include_stylesheet($this->url . '/skins/default/images/mimetypes/style.css');
             $this->filetypes_style = true;
         }
 
         $thead = '';
         foreach ($this->list_head($attrib, $a_show_cols, $type) as $cell) {
             $thead .= html::tag('th', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']);
         }
 
         return html::tag('table', $attrib,
             html::tag('thead', null, html::tag('tr', null, $thead)) . html::tag('tbody', null, ''),
             array('style', 'class', 'id', 'cellpadding', 'cellspacing', 'border', 'summary'));
     }
 
     /**
      * Creates <THEAD> for message list table
      */
     protected function list_head($attrib, $a_show_cols, $type = 'files')
     {
         $prefix    = 'kolab_' . $type . '_';
         $c_prefix  = 'kolab_files_' . ($type != 'files' ? $type : '') . '_';
         $skin_path = $_SESSION['skin_path'];
 
         // check to see if we have some settings for sorting
         $sort_col   = $_SESSION[$prefix . 'sort_col'];
         $sort_order = $_SESSION[$prefix . 'sort_order'];
 
         $dont_override  = (array)$this->rc->config->get('dont_override');
         $disabled_sort  = in_array($c_prefix . 'sort_col', $dont_override);
         $disabled_order = in_array($c_prefix . 'sort_order', $dont_override);
 
         $this->rc->output->set_env($prefix . 'disabled_sort_col', $disabled_sort);
         $this->rc->output->set_env($prefix . 'disabled_sort_order', $disabled_order);
 
         // define sortable columns
         if ($disabled_sort)
             $a_sort_cols = $sort_col && !$disabled_order ? array($sort_col) : array();
         else
             $a_sort_cols = $this->{$type . '_sort_cols'};
 
         if (!empty($attrib['optionsmenuicon'])) {
             $onclick = 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('menu-open', '{$type}listmenu', this, event)";
             $inner   = $this->rc->gettext('listoptions');
 
             if (is_string($attrib['optionsmenuicon']) && $attrib['optionsmenuicon'] != 'true') {
                 $inner = html::img(array('src' => $skin_path . $attrib['optionsmenuicon'], 'alt' => $this->rc->gettext('listoptions')));
             }
 
             $list_menu = html::a(array(
                 'href'     => '#list-options',
                 'onclick'  => $onclick,
                 'class'    => 'listmenu',
                 'id'       => $type . 'listmenulink',
                 'title'    => $this->rc->gettext('listoptions'),
                 'tabindex' => '0',
             ), $inner);
         }
         else {
             $list_menu = '';
         }
 
         $cells = array();
 
         foreach ($a_show_cols as $col) {
             // get column name
             switch ($col) {
             case 'options':
                 $col_name = $list_menu;
                 break;
             default:
                 $col_name = rcube::Q($this->plugin->gettext($col));
             }
 
             // make sort links
             if (in_array($col, $a_sort_cols)) {
                 $col_name = html::a(array(
                         'href'    => "#sort",
                         'onclick' => 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('$type-sort','$col',this)",
                         'title'   => $this->plugin->gettext('sortby')
                     ), $col_name);
             }
             else if ($col_name[0] != '<') {
                 $col_name = '<span class="' . $col .'">' . $col_name . '</span>';
             }
 
             $sort_class = $col == $sort_col && !$disabled_order ? " sorted$sort_order" : '';
             $class_name = $col.$sort_class;
 
             // put it all together
             $cells[] = array('className' => $class_name, 'id' => "rcm$col", 'html' => $col_name);
         }
 
         return $cells;
     }
 
     /**
      * Update files|sessions list object
      */
     protected function list_update($prefs, $type = 'files')
     {
         $prefix   = 'kolab_' . $type . '_list_';
         $c_prefix = 'kolab_files' . ($type != 'files' ? '_' . $type : '') . '_list_';
         $attrib   = $_SESSION[$prefix . 'attrib'];
 
         if (!empty($prefs[$c_prefix . 'cols'])) {
             $attrib['columns'] = $prefs[$c_prefix . 'cols'];
             $_SESSION[$prefix . 'attrib'] = $attrib;
         }
 
         $a_show_cols = $attrib['columns'];
         $head        = '';
 
         foreach ($this->list_head($attrib, $a_show_cols, $type) as $cell) {
             $head .= html::tag('th', array('class' => $cell['className'], 'id' => $cell['id']), $cell['html']);
         }
 
         $head = html::tag('tr', null, $head);
 
         $this->rc->output->set_env($type . '_coltypes', $a_show_cols);
         $this->rc->output->command($type . '_list_update', $head);
     }
 
     /**
      * Template object for file info box
      */
     public function file_info_box($attrib)
     {
         // print_r($this->file_data, true);
         $table = new html_table(array('cols' => 2, 'class' => $attrib['class']));
 
         // file name
         $table->add('title', $this->plugin->gettext('name').':');
         $table->add('data filename', $this->file_data['name']);
 
         // file type
         // @TODO: human-readable type name
         $table->add('title', $this->plugin->gettext('type').':');
         $table->add('data filetype', $this->file_data['type']);
 
         // file size
         $table->add('title', $this->plugin->gettext('size').':');
         $table->add('data filesize', $this->rc->show_bytes($this->file_data['size']));
 
         // file modification time
         $table->add('title', $this->plugin->gettext('mtime').':');
         $table->add('data filemtime', $this->file_data['mtime']);
 
         // @TODO: for images: width, height, color depth, etc.
         // @TODO: for text files: count of characters, lines, words
 
         return $table->show();
     }
 
     /**
      * Template object for file preview frame
      */
     public function file_preview_frame($attrib)
     {
         if (empty($attrib['id'])) {
             $attrib['id'] = 'filepreviewframe';
         }
 
         if ($frame = $this->file_data['viewer']['frame']) {
             return $frame;
         }
 
         if ($href = $this->file_data['viewer']['href']) {
             // file href attribute must be an absolute URL (Bug #2063)
             if (!empty($href)) {
                 if (!preg_match('|^https?://|', $href)) {
                     $href = $this->url . '/api/' . $href;
                 }
             }
         }
         else {
             $token = $this->get_api_token();
             $href  = $this->url . '/api/?method=file_get'
                 . '&file=' . urlencode($this->file_data['filename'])
                 . '&token=' . urlencode($token);
         }
 
         $this->rc->output->add_gui_object('preview_frame', $attrib['id']);
 
         $attrib['allowfullscreen'] = true;
         $attrib['src']             = $href;
         $attrib['onload']          = 'kolab_files_frame_load(this)';
 
         // editor requires additional arguments via POST
         if (!empty($this->file_data['viewer']['post'])) {
             $attrib['src'] = 'program/resources/blank.gif';
 
             $form_content = new html_hiddenfield();
             $form_attrib  = array(
                 'action' => $href,
                 'id'     => $attrib['id'] . '-form',
                 'target' => $attrib['name'],
                 'method' => 'post',
             );
 
             foreach ($this->file_data['viewer']['post'] as $name => $value) {
                 $form_content->add(array('name' => $name, 'value' => $value));
             }
 
             $form = html::tag('form', $form_attrib, $form_content->show())
                 . html::script(array(), "\$('#{$attrib['id']}-form').submit()");
         }
 
         return html::iframe($attrib) . $form;
     }
 
     /**
      * Template object for quota display
      */
     public function quota_display($attrib)
     {
-        if (!$attrib['id']) {
+        if (!($attrib['id'] ?? false)) {
             $attrib['id'] = 'rcmquotadisplay';
         }
 
         $quota_type = !empty($attrib['display']) ? $attrib['display'] : 'text';
 
         $this->rc->output->add_gui_object('quotadisplay', $attrib['id']);
         $this->rc->output->set_env('quota_type', $quota_type);
 
         // get quota
         $token   = $this->get_api_token();
         $request = $this->get_request(array('method' => 'quota'), $token);
 
         // send request to the API
         try {
             $response = $request->send();
             $status   = $response->getStatus();
             $body     = @json_decode($response->getBody(), true);
 
             if ($status == 200 && $body['status'] == 'OK') {
                 $quota = $body['result'];
             }
             else {
                 throw new Exception($body['reason'] ?: "Failed to get quota. Status: $status");
             }
         }
         catch (Exception $e) {
             rcube::raise_error($e, true, false);
             $quota = array('total' => 0, 'percent' => 0);
         }
 
         $quota = rcube_output::json_serialize($quota);
 
         $this->rc->output->add_script(rcmail_output::JS_OBJECT_NAME . ".files_set_quota($quota);", 'docready');
 
         return html::span($attrib, '');
     }
 
     /**
      * Get API token for current user session, authenticate if needed
      */
     public function get_api_token($configure = true)
     {
         $token = $_SESSION['kolab_files_token'];
         $time  = $_SESSION['kolab_files_time'];
 
         if ($token && time() - $this->timeout < $time) {
             if (time() - $time <= $this->timeout / 2) {
                 return $token;
             }
         }
 
         $request = $this->get_request(array('method' => 'ping'), $token);
 
         try {
             $url = $request->getUrl();
 
             // Send ping request
             if ($token) {
                 $url->setQueryVariables(array('method' => 'ping'));
                 $request->setUrl($url);
                 $response = $request->send();
                 $status   = $response->getStatus();
 
                 if ($status == 200 && ($body = json_decode($response->getBody(), true))) {
                     if ($body['status'] == 'OK') {
                         $_SESSION['kolab_files_time']  = time();
                         return $token;
                     }
                 }
             }
 
             // Go with authenticate request
             $url->setQueryVariables(array('method' => 'authenticate', 'version' => self::API_VERSION));
             $request->setUrl($url);
             $request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
 
             // Allow plugins (e.g. kolab_sso) to modify the request
             $this->rc->plugins->exec_hook('chwala_authenticate', array('request' => $request));
 
             $response = $request->send();
             $status   = $response->getStatus();
 
             if ($status == 200 && ($body = json_decode($response->getBody(), true))) {
                 $token = $body['result']['token'];
 
                 if ($token) {
                     $_SESSION['kolab_files_token'] = $token;
                     $_SESSION['kolab_files_time']  = time();
                     $_SESSION['kolab_files_caps']  = $body['result']['capabilities'];
                 }
             }
             else {
                 throw new Exception(sprintf("Authenticate error (Status: %d)", $status));
             }
 
             // Configure session
             if ($configure && $token) {
                 $this->configure($token);
             }
         }
         catch (Exception $e) {
             rcube::raise_error($e, true, false);
         }
 
         return $token;
     }
 
     protected function capabilities()
     {
         if (empty($_SESSION['kolab_files_caps'])) {
             $token = $this->get_api_token();
 
             if (empty($_SESSION['kolab_files_caps'])) {
                 $request = $this->get_request(array('method' => 'capabilities'), $token);
 
                 // send request to the API
                 try {
                     $response = $request->send();
                     $status   = $response->getStatus();
                     $body     = @json_decode($response->getBody(), true);
 
                     if ($status == 200 && $body['status'] == 'OK') {
                         $_SESSION['kolab_files_caps'] = $body['result'];
                     }
                     else {
                         throw new Exception($body['reason'] ?: "Failed to get capabilities. Status: $status");
                     }
                 }
                 catch (Exception $e) {
                     rcube::raise_error($e, true, false);
                     return array();
                 }
             }
         }
 
         if ($_SESSION['kolab_files_caps']['MANTICORE'] || $_SESSION['kolab_files_caps']['WOPI']) {
             $_SESSION['kolab_files_caps']['DOCEDIT'] = true;
             $_SESSION['kolab_files_caps']['DOCTYPE'] = $_SESSION['kolab_files_caps']['MANTICORE'] ? 'manticore' : 'wopi';
         }
 
         if (!empty($_SESSION['kolab_files_caps']) && !isset($_SESSION['kolab_files_caps']['MOUNTPOINTS'])) {
             $_SESSION['kolab_files_caps']['MOUNTPOINTS'] = array();
         }
 
         return $_SESSION['kolab_files_caps'];
     }
 
     /**
      * Initialize HTTP_Request object
      */
     protected function get_request($get = null, $token = null)
     {
         $url = $this->url_srv . '/api/';
 
-        if (!$this->request) {
+        if (!property_exists($this, "request") || !$this->request) {
             $config = array(
                 'store_body'       => true,
                 'follow_redirects' => true,
             );
 
             $this->request = libkolab::http_request($url, 'GET', $config);
         }
         else {
             // cleanup
             try {
                 $this->request->setBody('');
                 $this->request->setUrl($url);
                 $this->request->setMethod(HTTP_Request2::METHOD_GET);
             }
             catch (Exception $e) {
                 rcube::raise_error($e, true, true);
             }
         }
 
         if ($token) {
             $this->request->setHeader('X-Session-Token', $token);
         }
 
         if (!empty($get)) {
             $url = $this->request->getUrl();
             $url->setQueryVariables($get);
             $this->request->setUrl($url);
         }
 
         // some HTTP server configurations require this header
         $this->request->setHeader('accept', "application/json,text/javascript,*/*");
 
         // Localization
         $this->request->setHeader('accept-language', $_SESSION['language']);
 
         // set Referer which is used as an origin for cross-window
         // communication with document editor iframe
         $host = $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'];
         $this->request->setHeader('referer', $host);
 
         return $this->request;
     }
 
     /**
      * Configure chwala session
      */
     public function configure($token = null, $prefs = array())
     {
         if (!$token) {
             $token = $this->get_api_token(false);
         }
 
         try {
             // Configure session
             $query = array(
                 'method'      => 'configure',
                 'timezone'    => $prefs['timezone'] ?: $this->rc->config->get('timezone'),
                 'date_format' => $prefs['date_long'] ?: $this->rc->config->get('date_long', 'Y-m-d H:i'),
             );
 
             $request  = $this->get_request($query, $token);
             $response = $request->send();
             $status   = $response->getStatus();
 
             if ($status != 200) {
                 throw new Exception(sprintf("Failed to configure chwala session (Status: %d)", $status));
             }
         }
         catch (Exception $e) {
             rcube::raise_error($e, true, false);
         }
     }
 
     /**
      * Handler for main files interface (Files task)
      */
     protected function action_index()
     {
         $this->plugin->add_label(
             'uploading', 'attaching', 'uploadsizeerror',
             'filedeleting', 'filedeletenotice', 'filedeleteconfirm',
             'filemoving', 'filemovenotice', 'filemoveconfirm', 'filecopying', 'filecopynotice',
             'fileskip', 'fileskipall', 'fileoverwrite', 'fileoverwriteall'
         );
 
         $this->folder_list_env();
 
         if ($this->rc->task == 'files') {
             $this->rc->output->set_env('folder', rcube_utils::get_input_value('folder', rcube_utils::INPUT_GET));
             $this->rc->output->set_env('collection', rcube_utils::get_input_value('collection', rcube_utils::INPUT_GET));
         }
 
         $caps = $this->capabilities();
 
         $this->rc->output->add_label('uploadprogress', 'GB', 'MB', 'KB', 'B');
         $this->rc->output->set_pagetitle($this->plugin->gettext('files'));
         $this->rc->output->set_env('file_mimetypes', $this->get_mimetypes());
         $this->rc->output->set_env('files_quota', $caps['QUOTA']);
         $this->rc->output->set_env('files_max_upload', $caps['MAX_UPLOAD']);
-        $this->rc->output->set_env('files_progress_name', $caps['PROGRESS_NAME']);
-        $this->rc->output->set_env('files_progress_time', $caps['PROGRESS_TIME']);
+        $this->rc->output->set_env('files_progress_name', $caps['PROGRESS_NAME'] ?? null);
+        $this->rc->output->set_env('files_progress_time', $caps['PROGRESS_TIME'] ?? null);
         $this->rc->output->send('kolab_files.files');
     }
 
     /**
      * Handler for resetting some session/cached information
      */
     protected function action_reset()
     {
         $this->rc->session->remove('kolab_files_caps');
         if (($caps = $this->capabilities()) && !empty($caps)) {
             $this->rc->output->set_env('files_caps', $caps);
         }
     }
 
     /**
      * Handler for preferences save action
      */
     protected function action_prefs()
     {
         $dont_override = (array)$this->rc->config->get('dont_override');
         $prefs = array();
         $type  = rcube_utils::get_input_value('type', rcube_utils::INPUT_POST);
         $opts  = array(
             'kolab_files_sort_col'   => true,
             'kolab_files_sort_order' => true,
             'kolab_files_list_cols'  => false,
         );
 
         foreach ($opts as $o => $sess) {
             if (isset($_POST[$o])) {
                 $value       = rcube_utils::get_input_value($o, rcube_utils::INPUT_POST);
                 $session_key = $o;
                 $config_key  = $o;
 
                 if ($type != 'files') {
                     $config_key = str_replace('files', 'files_' . $type, $config_key);
                 }
 
                 if (in_array($config_key, $dont_override)) {
                     continue;
                 }
 
                 if ($o == 'kolab_files_list_cols') {
                     $update_list = true;
                 }
 
                 $prefs[$config_key] = $value;
                 if ($sess) {
                     $_SESSION[$session_key] = $prefs[$config_key];
                 }
             }
         }
 
         // save preference values
         if (!empty($prefs)) {
             $this->rc->user->save_prefs($prefs);
         }
 
         if (!empty($update_list)) {
             $this->list_update($prefs, $type);
         }
 
         $this->rc->output->send();
     }
 
     /**
      * Handler for file open action
      */
     protected function action_open()
     {
         $this->rc->output->set_env('file_mimetypes', $this->get_mimetypes());
 
         $this->file_opener(intval($_GET['_viewer']) & ~4);
     }
 
     /**
      * Handler for file open action
      */
     protected function action_edit()
     {
         $this->plugin->add_label('sessionterminating', 'unsavedchanges', 'documentinviting',
             'documentcancelling', 'removeparticipant', 'sessionterminated', 'sessionterminatedtitle');
 
         $this->file_opener(intval($_GET['_viewer']));
     }
 
     /**
      * Handler for folder sharing action
      */
     protected function action_share()
     {
         $this->rc->output->add_handler('share-form', array($this, 'folder_share_form'));
 
         $this->rc->output->send('kolab_files.share');
     }
 
     /**
      * Handler for "save all attachments into cloud" action
      */
     protected function action_save_file()
     {
 //        $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
         $uid    = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
         $dest   = rcube_utils::get_input_value('dest', rcube_utils::INPUT_POST);
         $id     = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
         $name   = rcube_utils::get_input_value('name', rcube_utils::INPUT_POST);
 
         $temp_dir = unslashify($this->rc->config->get('temp_dir'));
         $message  = new rcube_message($uid);
         $request  = $this->get_request();
         $url      = $request->getUrl();
         $files    = array();
         $errors   = array();
         $attachments = array();
 
         $request->setMethod(HTTP_Request2::METHOD_POST);
         $request->setHeader('X-Session-Token', $this->get_api_token());
         $url->setQueryVariables(array('method' => 'file_upload', 'folder' => $dest));
         $request->setUrl($url);
 
         foreach ($message->attachments as $attach_prop) {
             if (empty($id) || $id == $attach_prop->mime_id) {
                 $filename = strlen($name) ? $name : rcmail_attachment_name($attach_prop, true);
                 $attachments[$filename] = $attach_prop;
             }
         }
 
         // @TODO: handle error
         // @TODO: implement file upload using file URI instead of body upload
 
         foreach ($attachments as $attach_name => $attach_prop) {
             $path = tempnam($temp_dir, 'rcmAttmnt');
 
             // save attachment to file
             if ($fp = fopen($path, 'w+')) {
                 $message->get_part_body($attach_prop->mime_id, false, 0, $fp);
             }
             else {
                 $errors[] = true;
                 rcube::raise_error(array(
                     'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
                     'message' => "Unable to save attachment into file $path"),
                     true, false);
                 continue;
             }
 
             fclose($fp);
 
             // send request to the API
             try {
                 $request->setBody('');
                 $request->addUpload('file[]', $path, $attach_name, $attach_prop->mimetype);
                 $response = $request->send();
                 $status   = $response->getStatus();
                 $body     = @json_decode($response->getBody(), true);
 
                 if ($status == 200 && $body['status'] == 'OK') {
                     $files[] = $attach_name;
                 }
                 else {
                     throw new Exception($body['reason'] ?: "Failed to post file_upload. Status: $status");
                 }
             }
             catch (Exception $e) {
                 unlink($path);
                 $errors[] = $e->getMessage();
                 rcube::raise_error(array(
                     'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
                     'message' => $e->getMessage()),
                     true, false);
                 continue;
             }
 
             // clean up
             unlink($path);
             $request->setBody('');
         }
 
         if ($count = count($files)) {
             $msg = $this->plugin->gettext(array('name' => 'saveallnotice', 'vars' => array('n' => $count)));
             $this->rc->output->show_message($msg, 'confirmation');
         }
         if ($count = count($errors)) {
             $msg = $this->plugin->gettext(array('name' => 'saveallerror', 'vars' => array('n' => $count)));
             $this->rc->output->show_message($msg, 'error');
         }
 
         // @TODO: update quota indicator, make this optional in case files aren't stored in IMAP
 
         $this->rc->output->send();
     }
 
     /**
      * Handler for "add attachments from the cloud" action
      */
     protected function action_attach_file()
     {
         $files       = rcube_utils::get_input_value('files', rcube_utils::INPUT_POST);
         $uploadid    = rcube_utils::get_input_value('uploadid', rcube_utils::INPUT_POST);
         $COMPOSE_ID  = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
         $COMPOSE     = null;
         $errors      = array();
         $attachments = array();
 
         if ($this->rc->task == 'mail') {
             if ($COMPOSE_ID && $_SESSION['compose_data_'.$COMPOSE_ID]) {
                 $COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
             }
 
             if (!$COMPOSE) {
                 die("Invalid session var!");
             }
 
             // attachment upload action
             if (!is_array($COMPOSE['attachments'])) {
                 $COMPOSE['attachments'] = array();
             }
         }
 
         // clear all stored output properties (like scripts and env vars)
         $this->rc->output->reset();
 
         $temp_dir = unslashify($this->rc->config->get('temp_dir'));
         $request  = $this->get_request();
         $url      = $request->getUrl();
 
         // Use observer object to store HTTP response into a file
         require_once $this->plugin->home . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'kolab_files_observer.php';
         $observer = new kolab_files_observer();
 
         $request->setHeader('X-Session-Token', $this->get_api_token());
 
         // download files from the API and attach them
         foreach ($files as $file) {
             // decode filename
             $file = urldecode($file);
 
             // get file information
             try {
                 $url->setQueryVariables(array('method' => 'file_info', 'file' => $file));
                 $request->setUrl($url);
                 $response = $request->send();
                 $status   = $response->getStatus();
                 $body     = @json_decode($response->getBody(), true);
 
                 if ($status == 200 && $body['status'] == 'OK') {
                     $file_params = $body['result'];
                 }
                 else {
                     throw new Exception($body['reason'] ?: "Failed to get file_info. Status: $status");
                 }
             }
             catch (Exception $e) {
                 $errors[] = $e->getMessage();
                 rcube::raise_error(array(
                     'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
                     'message' => $e->getMessage()),
                     true, false);
                 continue;
             }
 
             // set location of downloaded file
             $path = tempnam($temp_dir, 'rcmAttmnt');
             $observer->set_file($path);
 
             // download file
             try {
                 $url->setQueryVariables(array('method' => 'file_get', 'file' => $file));
                 $request->setUrl($url);
                 $request->attach($observer);
                 $response = $request->send();
                 $status   = $response->getStatus();
                 $response->getBody(); // returns nothing
                 $request->detach($observer);
 
                 if ($status != 200 || !file_exists($path)) {
                     throw new Exception("Unable to save file");
                 }
             }
             catch (Exception $e) {
                 $errors[] = $e->getMessage();
                 rcube::raise_error(array(
                     'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
                     'message' => $e->getMessage()),
                     true, false);
                 continue;
             }
 
             $attachment = array(
                 'path'     => $path,
                 'size'     => $file_params['size'],
                 'name'     => $file_params['name'],
                 'mimetype' => $file_params['type'],
                 'group'    => $COMPOSE_ID,
             );
 
             if ($this->rc->task != 'mail') {
                 $attachments[] = $attachment;
                 continue;
             }
 
             $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
 
             if ($attachment['status'] && !$attachment['abort']) {
                 $this->compose_attach_success($attachment, $COMPOSE, $COMPOSE_ID, $uploadid);
             }
             else if ($attachment['error']) {
                 $errors[] = $attachment['error'];
             }
             else {
                 $errors[] = $this->plugin->gettext('attacherror');
             }
         }
 
         if (!empty($errors)) {
             $this->rc->output->command('display_message', $this->plugin->gettext('attacherror'), 'error');
             $this->rc->output->command('remove_from_attachment_list', $uploadid);
         }
         else if ($this->rc->task == 'calendar' || $this->rc->task == 'tasks') {
             // for uploads in events/tasks we'll use its standard upload handler,
             // for this we have to fake $_FILES and some other POST args
             foreach ($attachments as $attach) {
                 $_FILES['_attachments']['tmp_name'][] = $attachment['path'];
                 $_FILES['_attachments']['name'][]     = $attachment['name'];
                 $_FILES['_attachments']['size'][]     = $attachment['size'];
                 $_FILES['_attachments']['type'][]     = $attachment['mimetype'];
                 $_FILES['_attachments']['error'][]    = null;
             }
 
             $_GET['_uploadid'] = $uploadid;
             $_GET['_id']       = $COMPOSE_ID;
 
             switch ($this->rc->task) {
             case 'tasks':
                 $handler = new kolab_attachments_handler();
                 $handler->attachment_upload(tasklist::SESSION_KEY);
                 break;
 
             case 'calendar':
                 $handler = new kolab_attachments_handler();
                 $handler->attachment_upload(calendar::SESSION_KEY, 'cal-');
                 break;
             }
         }
 
         // send html page with JS calls as response
         $this->rc->output->command('auto_save_start', false);
         $this->rc->output->send();
     }
 
     protected function compose_attach_success($attachment, $COMPOSE, $COMPOSE_ID, $uploadid)
     {
         $id = $attachment['id'];
 
         // store new attachment in session
         unset($attachment['data'], $attachment['status'], $attachment['abort']);
         $this->rc->session->append('compose_data_' . $COMPOSE_ID . '.attachments', $id, $attachment);
 
         if (($icon = $COMPOSE['deleteicon']) && is_file($icon)) {
             $button = html::img(array(
                 'src' => $icon,
                 'alt' => $this->rc->gettext('delete')
             ));
         }
         else if ($COMPOSE['textbuttons']) {
             $button = rcube::Q($this->rc->gettext('delete'));
         }
         else {
             $button = '';
         }
 
         if (version_compare(version_parse(RCMAIL_VERSION), '1.3.0', '>=')) {
             $link_content = sprintf('%s <span class="attachment-size"> (%s)</span>',
                 rcube::Q($attachment['name']), $this->rc->show_bytes($attachment['size']));
 
             $content_link = html::a(array(
                     'href'    => "#load",
                     'class'   => 'filename',
                     'onclick' => sprintf("return %s.command('load-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
                 ), $link_content);
 
             $delete_link = html::a(array(
                     'href'    => "#delete",
                     'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this, event)", rcmail_output::JS_OBJECT_NAME, $id),
                     'title'   => $this->rc->gettext('delete'),
                     'class'   => 'delete',
                     'aria-label' => $this->rc->gettext('delete') . ' ' . $attachment['name'],
                 ), $button);
 
             $content = $COMPOSE['icon_pos'] == 'left' ? $delete_link.$content_link : $content_link.$delete_link;
         }
         else {
             $content = html::a(array(
                     'href'    => "#delete",
                     'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this)", rcmail_output::JS_OBJECT_NAME, $id),
                     'title'   => $this->rc->gettext('delete'),
                     'class'   => 'delete',
             ), $button);
 
             $content .= rcube::Q($attachment['name']);
         }
 
         $this->rc->output->command('add2attachment_list', "rcmfile$id", array(
             'html'      => $content,
             'name'      => $attachment['name'],
             'mimetype'  => $attachment['mimetype'],
             'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
             'complete'  => true), $uploadid);
     }
 
     /**
      * Handler for file open/edit action
      */
     protected function file_opener($viewer)
     {
         $file    = rcube_utils::get_input_value('_file', rcube_utils::INPUT_GET);
         $session = rcube_utils::get_input_value('_session', rcube_utils::INPUT_GET);
 
         // get file info
         $token   = $this->get_api_token();
         $request = $this->get_request(array(
             'method'  => 'file_info',
             'file'    => $file,
             'viewer'  => $viewer,
             'session' => $session,
             ), $token);
 
         // send request to the API
         try {
             $response = $request->send();
             $status   = $response->getStatus();
             $body     = @json_decode($response->getBody(), true);
 
             if ($status == 200 && $body['status'] == 'OK') {
                 $this->file_data = $body['result'];
             }
             else {
                 throw new Exception($body['reason'] ?: "Failed to get file_info. Status: $status");
             }
         }
         catch (Exception $e) {
             rcube::raise_error(array(
                 'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
                 'message' => $e->getMessage()),
                 true, true);
         }
 
         if ($file === null || $file === '') {
             $file = $this->file_data['file'];
         }
 
         $this->file_data['filename'] = $file;
 
         $this->plugin->add_label('filedeleteconfirm', 'filedeleting', 'filedeletenotice', 'terminate');
 
         // register template objects for dialogs (and main interface)
         $this->rc->output->add_handlers(array(
             'fileinfobox'      => array($this, 'file_info_box'),
             'filepreviewframe' => array($this, 'file_preview_frame'),
         ));
 
         $placeholder = $this->rc->output->asset_url('program/resources/blank.gif');
 
         if ($this->file_data['viewer']['wopi']) {
             $editor_type = 'wopi';
             $got_editor  = ($viewer & 4);
         }
         else if ($this->file_data['viewer']['manticore']) {
             $editor_type = 'manticore';
             $got_editor = ($viewer & 4);
         }
 
         // this one is for styling purpose
         $this->rc->output->set_env('extwin', true);
         $this->rc->output->set_env('file', $file);
         $this->rc->output->set_env('file_data', $this->file_data);
         $this->rc->output->set_env('mimetype', $this->file_data['type']);
         $this->rc->output->set_env('filename', pathinfo($file, PATHINFO_BASENAME));
         $this->rc->output->set_env('editor_type', $editor_type);
         $this->rc->output->set_env('photo_placeholder', $placeholder);
         $this->rc->output->set_pagetitle(rcube::Q($file));
         $this->rc->output->send('kolab_files.' . ($got_editor ? 'docedit' : 'filepreview'));
     }
 
     /**
      * Returns mimetypes supported by File API viewers
      */
     protected function get_mimetypes($type = 'view')
     {
         $mimetypes = array();
 
         // send request to the API
         try {
             if ($this->mimetypes === null) {
                 $this->mimetypes = false;
 
                 $token    = $this->get_api_token();
                 $caps     = $this->capabilities();
                 $request  = $this->get_request(array('method' => 'mimetypes'), $token);
                 $response = $request->send();
                 $status   = $response->getStatus();
                 $body     = @json_decode($response->getBody(), true);
 
                 if ($status == 200 && $body['status'] == 'OK') {
                     $this->mimetypes = $body['result'];
                 }
                 else {
                     throw new Exception($body['reason'] ?: "Failed to get mimetypes. Status: $status");
                 }
             }
 
             if (is_array($this->mimetypes)) {
                 if (array_key_exists($type, $this->mimetypes)) {
                     $mimetypes = $this->mimetypes[$type];
                 }
                 // fallback to static definition if old Chwala is used
                 else if ($type == 'edit') {
                     $mimetypes = array(
                         'text/plain' => 'txt',
                         'text/html'  => 'html',
                     );
                     if (!empty($caps['MANTICORE'])) {
                         $mimetypes = array_merge(array('application/vnd.oasis.opendocument.text' => 'odt'), $mimetypes);
                     }
 
                     foreach (array_keys($mimetypes) as $type) {
                         list ($app, $label) = explode('/', $type);
                         $label = preg_replace('/[^a-z]/', '', $label);
                         $mimetypes[$type] = array(
                             'ext'   => $mimetypes[$type],
                             'label' => $this->plugin->gettext('type.' . $label),
                         );
                     }
                 }
                 else {
                     $mimetypes = $this->mimetypes;
                 }
             }
         }
         catch (Exception $e) {
             rcube::raise_error(array(
                 'code' => 500, 'type' => 'php', 'line' => __LINE__, 'file' => __FILE__,
                 'message' => $e->getMessage()),
                 true, false);
         }
 
         return $mimetypes;
     }
 
     /**
      * Get list of available external storage drivers
      */
     protected function get_external_storage_drivers()
     {
         // first get configured sources from Chwala
         $token   = $this->get_api_token();
         $request = $this->get_request(array('method' => 'folder_types'), $token);
 
         // send request to the API
         try {
             $response = $request->send();
             $status   = $response->getStatus();
             $body     = @json_decode($response->getBody(), true);
 
             if ($status == 200 && $body['status'] == 'OK') {
                 $sources = $body['result'];
             }
             else {
                 throw new Exception($body['reason'] ?: "Failed to get folder_types. Status: $status");
             }
         }
         catch (Exception $e) {
             rcube::raise_error($e, true, false);
             return;
         }
 
         $this->rc->output->set_env('external_sources', $sources);
     }
 
     /**
      * Get folder share dialog data
      */
     protected function get_share_info($folder)
     {
         // first get configured sources from Chwala
         $token   = $this->get_api_token();
         $request = $this->get_request(array('method' => 'sharing', 'folder' => $folder), $token);
 
         // send request to the API
         try {
             $response = $request->send();
             $status   = $response->getStatus();
             $body     = @json_decode($response->getBody(), true);
 
             if ($status == 200 && $body['status'] == 'OK') {
                 $info = $body['result'];
             }
             else if ($body['code'] == 530) {
                 return false;
             }
             else {
                 throw new Exception($body['reason'] ?: "Failed to get sharing form information. Status: $status");
             }
         }
         catch (Exception $e) {
             rcube::raise_error($e, true, false);
             return;
         }
 
         return $info;
     }
 
     /**
      * Registers translation labels for folder lists in UI
      */
     protected function folder_list_env()
     {
         // folder list and actions
         $this->plugin->add_label(
             'folderdeleting', 'folderdeleteconfirm', 'folderdeletenotice',
             'collection_audio', 'collection_video', 'collection_image', 'collection_document',
             'additionalfolders', 'listpermanent', 'storageautherror'
         );
         $this->rc->output->add_label('foldersubscribing', 'foldersubscribed',
             'folderunsubscribing', 'folderunsubscribed', 'searching'
         );
     }
 }
diff --git a/plugins/kolab_folders/kolab_folders.php b/plugins/kolab_folders/kolab_folders.php
index 30a47ad6..b9b9f914 100644
--- a/plugins/kolab_folders/kolab_folders.php
+++ b/plugins/kolab_folders/kolab_folders.php
@@ -1,851 +1,852 @@
 <?php
 
 /**
  * Type-aware folder management/listing for Kolab
  *
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2011-2017, Kolab Systems AG <contact@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_folders extends rcube_plugin
 {
     public $task = '?(?!login).*';
 
     public $types      = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration', 'file', 'freebusy');
     public $subtypes   = array(
         'mail'          => array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail'),
         'event'         => array('default'),
         'task'          => array('default'),
         'journal'       => array('default'),
         'note'          => array('default'),
         'contact'       => array('default'),
         'configuration' => array('default'),
         'file'          => array('default'),
         'freebusy'      => array('default'),
     );
     public $act_types  = array('event', 'task');
 
     private $rc;
     private static $instance;
     private $expire_annotation = '/shared/vendor/cmu/cyrus-imapd/expire';
+    private $is_processing = false;
 
 
     /**
      * Plugin initialization.
      */
     function init()
     {
         self::$instance = $this;
         $this->rc = rcube::get_instance();
 
         // load required plugin
         $this->require_plugin('libkolab');
 
         // Folder listing hooks
         $this->add_hook('storage_folders', array($this, 'mailboxes_list'));
 
         // Folder manager hooks
         $this->add_hook('folder_form', array($this, 'folder_form'));
         $this->add_hook('folder_update', array($this, 'folder_save'));
         $this->add_hook('folder_create', array($this, 'folder_save'));
         $this->add_hook('folder_delete', array($this, 'folder_save'));
         $this->add_hook('folder_rename', array($this, 'folder_save'));
         $this->add_hook('folders_list', array($this, 'folders_list'));
 
         // Special folders setting
         $this->add_hook('preferences_save', array($this, 'prefs_save'));
 
         // ACL plugin hooks
         $this->add_hook('acl_rights_simple', array($this, 'acl_rights_simple'));
         $this->add_hook('acl_rights_supported', array($this, 'acl_rights_supported'));
 
         // Resolving other user folder names
         $this->add_hook('render_mailboxlist', array($this, 'render_folderlist'));
         $this->add_hook('render_folder_selector', array($this, 'render_folderlist'));
         $this->add_hook('folders_list', array($this, 'render_folderlist'));
     }
 
     /**
      * Handler for mailboxes_list hook. Enables type-aware lists filtering.
      */
     function mailboxes_list($args)
     {
         // infinite loop prevention
         if ($this->is_processing) {
             return $args;
         }
 
         if (!$this->metadata_support()) {
             return $args;
         }
 
         $this->is_processing = true;
 
         // get folders
         $folders = kolab_storage::list_folders($args['root'], $args['name'], $args['filter'], $args['mode'] == 'LSUB', $folderdata);
 
         $this->is_processing = false;
 
         if (!is_array($folders)) {
             return $args;
         }
 
         // Create default folders
         if ($args['root'] == '' && $args['name'] = '*') {
             $this->create_default_folders($folders, $args['filter'], $folderdata, $args['mode'] == 'LSUB');
         }
 
         $args['folders'] = $folders;
 
         return $args;
     }
 
     /**
      * Handler for folders_list hook. Add css classes to folder rows.
      */
     function folders_list($args)
     {
         if (!$this->metadata_support()) {
             return $args;
         }
 
         // load translations
         $this->add_texts('localization/', false);
 
         // Add javascript script to the client
         $this->include_script('kolab_folders.js');
 
         $this->add_label('folderctype');
         foreach ($this->types as $type) {
             $this->add_label('foldertype' . $type);
         }
 
         $skip_namespace = $this->rc->config->get('kolab_skip_namespace');
         $skip_roots     = array();
 
         if (!empty($skip_namespace)) {
             $storage = $this->rc->get_storage();
             foreach ((array)$skip_namespace as $ns) {
                 foreach((array)$storage->get_namespace($ns) as $root) {
                     $skip_roots[] = rtrim($root[0], $root[1]);
                 }
             }
         }
 
         $this->rc->output->set_env('skip_roots', $skip_roots);
         $this->rc->output->set_env('foldertypes', $this->types);
 
         // get folders types
         $folderdata = kolab_storage::folders_typedata();
 
         if (!is_array($folderdata)) {
             return $args;
         }
 
         // Add type-based style for table rows
         // See kolab_folders::folder_class_name()
-        if ($table = $args['table']) {
+        if ($table = ($args['table'] ?? null)) {
             for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
                 $attrib = $table->get_row_attribs($i);
                 $folder = $attrib['foldername']; // UTF7-IMAP
                 $type   = $folderdata[$folder];
 
                 if (!$type) {
                     $type = 'mail';
                 }
 
                 $class_name = self::folder_class_name($type);
                 $attrib['class'] = trim($attrib['class'] . ' ' . $class_name);
                 $table->set_row_attribs($attrib, $i);
             }
         }
 
         // Add type-based class for list items
-        if (is_array($args['list'])) {
+        if (is_array($args['list'] ?? null)) {
             foreach ((array)$args['list'] as $k => $item) {
                 $folder = $item['folder_imap']; // UTF7-IMAP
-                $type   = $folderdata[$folder];
+                $type   = $folderdata[$folder] ?? null;
 
                 if (!$type) {
                     $type = 'mail';
                 }
 
                 $class_name = self::folder_class_name($type);
                 $args['list'][$k]['class'] = trim($item['class'] . ' ' . $class_name);
             }
         }
 
         return $args;
     }
 
     /**
      * Handler for folder info/edit form (folder_form hook).
      * Adds folder type selector.
      */
     function folder_form($args)
     {
         if (!$this->metadata_support()) {
             return $args;
         }
         // load translations
         $this->add_texts('localization/', false);
 
         // INBOX folder is of type mail.inbox and this cannot be changed
         if ($args['name'] == 'INBOX') {
             $args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
                 'label' => $this->gettext('folderctype'),
                 'value' => sprintf('%s (%s)', $this->gettext('foldertypemail'), $this->gettext('inbox')),
             );
 
             $this->add_expire_input($args['form'], 'INBOX');
 
             return $args;
         }
 
         if ($args['options']['is_root']) {
             return $args;
         }
 
         $mbox = strlen($args['name']) ? $args['name'] : $args['parent_name'];
 
         if (isset($_POST['_ctype'])) {
             $new_ctype   = trim(rcube_utils::get_input_value('_ctype', rcube_utils::INPUT_POST));
             $new_subtype = trim(rcube_utils::get_input_value('_subtype', rcube_utils::INPUT_POST));
         }
 
         // Get type of the folder or the parent
         if (strlen($mbox)) {
             list($ctype, $subtype) = $this->get_folder_type($mbox);
             if (strlen($args['parent_name']) && $subtype == 'default')
                 $subtype = ''; // there can be only one
         }
 
         if (!$ctype) {
             $ctype = 'mail';
         }
 
         $storage = $this->rc->get_storage();
 
         // Don't allow changing type of shared folder, according to ACL
         if (strlen($mbox)) {
             $options = $storage->folder_info($mbox);
             if ($options['namespace'] != 'personal' && !in_array('a', (array)$options['rights'])) {
                 if (in_array($ctype, $this->types)) {
                     $value = $this->gettext('foldertype'.$ctype);
                 }
                 else {
                     $value = $ctype;
                 }
                 if ($subtype) {
                     $value .= ' ('. ($subtype == 'default' ? $this->gettext('default') : $subtype) .')';
                 }
 
                 $args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
                     'label' => $this->gettext('folderctype'),
                     'value' => $value,
                 );
 
                 return $args;
             }
         }
 
         // Add javascript script to the client
         $this->include_script('kolab_folders.js');
 
         // build type SELECT fields
         $type_select = new html_select(array('name' => '_ctype', 'id' => '_folderctype',
             'onchange' => "\$('[name=\"_expire\"]').attr('disabled', \$(this).val() != 'mail')"
         ));
         $sub_select  = new html_select(array('name' => '_subtype', 'id' => '_subtype'));
         $sub_select->add('', '');
 
         foreach ($this->types as $type) {
             $type_select->add($this->gettext('foldertype'.$type), $type);
         }
         // add non-supported type
         if (!in_array($ctype, $this->types)) {
             $type_select->add($ctype, $ctype);
         }
 
         $sub_types = array();
         foreach ($this->subtypes as $ftype => $subtypes) {
             $sub_types[$ftype] = array_combine($subtypes, array_map(array($this, 'gettext'), $subtypes));
 
             // fill options for the current folder type
-            if ($ftype == $ctype || $ftype == $new_ctype) {
+            if ($ftype == $ctype || (isset($new_ctype) && $ftype == $new_ctype)) {
                 $sub_select->add(array_values($sub_types[$ftype]), $subtypes);
             }
         }
 
         $args['form']['props']['fieldsets']['settings']['content']['folderctype'] = array(
             'label' => $this->gettext('folderctype'),
             'value' => html::div('input-group',
                 $type_select->show(isset($new_ctype) ? $new_ctype : $ctype)
                 . $sub_select->show(isset($new_subtype) ? $new_subtype : $subtype)
             ),
         );
 
         $this->rc->output->set_env('kolab_folder_subtypes', $sub_types);
         $this->rc->output->set_env('kolab_folder_subtype', isset($new_subtype) ? $new_subtype : $subtype);
 
         $this->add_expire_input($args['form'], $args['name'], $ctype);
 
         return $args;
     }
 
     /**
      * Handler for folder update/create action (folder_update/folder_create hook).
      */
     function folder_save($args)
     {
         // Folder actions from folders list
         if (empty($args['record'])) {
             return $args;
         }
 
         // Folder create/update with form
         $ctype     = trim(rcube_utils::get_input_value('_ctype', rcube_utils::INPUT_POST));
         $subtype   = trim(rcube_utils::get_input_value('_subtype', rcube_utils::INPUT_POST));
         $mbox      = $args['record']['name'];
         $old_mbox  = $args['record']['oldname'];
         $subscribe = $args['record']['subscribe'];
 
         if (empty($ctype)) {
             return $args;
         }
 
         // load translations
         $this->add_texts('localization/', false);
 
         // Skip folder creation/rename in core
         // @TODO: Maybe we should provide folder_create_after and folder_update_after hooks?
         //        Using create_mailbox/rename_mailbox here looks bad
         $args['abort']  = true;
 
         // There can be only one default folder of specified type
         if ($subtype == 'default') {
             $default = $this->get_default_folder($ctype);
 
             if ($default !== null && $old_mbox != $default) {
                 $args['result'] = false;
                 $args['message'] = $this->gettext('defaultfolderexists');
                 return $args;
             }
         }
         // Subtype sanity-checks
         else if ($subtype && (!($subtypes = $this->subtypes[$ctype]) || !in_array($subtype, $subtypes))) {
             $subtype = '';
         }
 
         $ctype .= $subtype ? '.'.$subtype : '';
 
         $storage = $this->rc->get_storage();
 
         // Create folder
         if (!strlen($old_mbox)) {
             // By default don't subscribe to non-mail folders
             if ($subscribe)
                 $subscribe = (bool) preg_match('/^mail/', $ctype);
 
             $result = $storage->create_folder($mbox, $subscribe);
             // Set folder type
             if ($result) {
                 $this->set_folder_type($mbox, $ctype);
             }
         }
         // Rename folder
         else {
             if ($old_mbox != $mbox) {
                 $result = $storage->rename_folder($old_mbox, $mbox);
             }
             else {
                 $result = true;
             }
 
             if ($result) {
                 list($oldtype, $oldsubtype) = $this->get_folder_type($mbox);
                 $oldtype .= $oldsubtype ? '.'.$oldsubtype : '';
 
                 if ($ctype != $oldtype) {
                     $this->set_folder_type($mbox, $ctype);
                 }
             }
         }
 
         // Set messages expiration in days
         if ($result && isset($_POST['_expire'])) {
             $expire = trim(rcube_utils::get_input_value('_expire', rcube_utils::INPUT_POST));
             $expire = intval($expire) && preg_match('/^mail/', $ctype) ? intval($expire) : null;
 
             $storage->set_metadata($mbox, array($this->expire_annotation => $expire));
         }
 
         $args['record']['class']     = self::folder_class_name($ctype);
         $args['record']['subscribe'] = $subscribe;
         $args['result'] = $result;
 
         return $args;
     }
 
     /**
      * Handler for user preferences save (preferences_save hook)
      *
      * @param array $args Hash array with hook parameters
      *
      * @return array Hash array with modified hook parameters
      */
     public function prefs_save($args)
     {
         if ($args['section'] != 'folders') {
             return $args;
         }
 
         $dont_override = (array) $this->rc->config->get('dont_override', array());
 
         // map config option name to kolab folder type annotation
         $opts = array(
             'drafts_mbox' => 'mail.drafts',
             'sent_mbox'   => 'mail.sentitems',
             'junk_mbox'   => 'mail.junkemail',
             'trash_mbox'  => 'mail.wastebasket',
         );
 
         // check if any of special folders has been changed
         foreach ($opts as $opt_name => $type) {
             $new = $args['prefs'][$opt_name];
             $old = $this->rc->config->get($opt_name);
             if (!strlen($new) || $new === $old || in_array($opt_name, $dont_override)) {
                 unset($opts[$opt_name]);
             }
         }
 
         if (empty($opts)) {
             return $args;
         }
 
         $folderdata = kolab_storage::folders_typedata();
 
         if (!is_array($folderdata)) {
              return $args;
         }
 
         foreach ($opts as $opt_name => $type) {
             $foldername = $args['prefs'][$opt_name];
 
             // get all folders of specified type
             $folders = array_intersect($folderdata, array($type));
 
             // folder already annotated with specified type
             if (!empty($folders[$foldername])) {
                 continue;
             }
 
             // set type to the new folder
             $this->set_folder_type($foldername, $type);
 
             // unset old folder(s) type annotation
             list($maintype, $subtype) = explode('.', $type);
             foreach (array_keys($folders) as $folder) {
                 $this->set_folder_type($folder, $maintype);
             }
         }
 
         return $args;
     }
 
     /**
      * Handler for ACL permissions listing (acl_rights_simple hook)
      *
      * This shall combine the write and delete permissions into one item for
      * groupware folders as updating groupware objects is an insert + delete operation.
      *
      * @param array $args Hash array with hook parameters
      *
      * @return array Hash array with modified hook parameters
      */
     public function acl_rights_simple($args)
     {
         if ($args['folder']) {
             list($type,) = $this->get_folder_type($args['folder']);
 
             // we're dealing with a groupware folder here...
             if ($type && $type !== 'mail') {
                 if ($args['rights']['write'] && $args['rights']['delete']) {
                     $write_perms = $args['rights']['write'] . $args['rights']['delete'];
                     $rw_perms    = $write_perms . $args['rights']['read'];
 
                     $args['rights']['write'] = $write_perms;
                     $args['rights']['other'] = preg_replace("/[$rw_perms]/", '', $args['rights']['other']);
 
                     // add localized labels and titles for the altered items
                     $args['labels'] = array(
                         'other'  => $this->rc->gettext('shortacla','acl'),
                     );
                     $args['titles'] = array(
                         'other'  => $this->rc->gettext('longaclother','acl'),
                     );
                 }
             }
         }
 
         return $args;
     }
 
     /**
      * Handler for ACL permissions listing (acl_rights_supported hook)
      *
      * @param array $args Hash array with hook parameters
      *
      * @return array Hash array with modified hook parameters
      */
     public function acl_rights_supported($args)
     {
         if ($args['folder']) {
             list($type,) = $this->get_folder_type($args['folder']);
 
             // we're dealing with a groupware folder here...
             if ($type && $type !== 'mail') {
                 // remove some irrelevant (for groupware objects) rights
                 $args['rights'] = str_split(preg_replace('/[p]/', '', join('', $args['rights'])));
             }
         }
 
         return $args;
     }
 
     /**
      * Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
      *
      * @return boolean
      */
     function metadata_support()
     {
         $storage = $this->rc->get_storage();
 
         return $storage->get_capability('METADATA') ||
             $storage->get_capability('ANNOTATEMORE') ||
             $storage->get_capability('ANNOTATEMORE2');
     }
 
     /**
      * Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
      *
      * @param string $folder Folder name
      *
      * @return array Folder content-type
      */
     function get_folder_type($folder)
     {
         return explode('.', (string)kolab_storage::folder_type($folder));
     }
 
     /**
      * Sets folder content-type.
      *
      * @param string $folder Folder name
      * @param string $type   Content type
      *
      * @return boolean True on success
      */
     function set_folder_type($folder, $type = 'mail')
     {
         return kolab_storage::set_folder_type($folder, $type);
     }
 
     /**
      * Returns the name of default folder
      *
      * @param string $type Folder type
      *
      * @return string Folder name
      */
     function get_default_folder($type)
     {
         $folderdata = kolab_storage::folders_typedata();
 
         if (!is_array($folderdata)) {
             return null;
         }
 
         // get all folders of specified type
         $folderdata = array_intersect($folderdata, array($type.'.default'));
 
         return key($folderdata);
     }
 
     /**
      * Returns CSS class name for specified folder type
      *
      * @param string $type Folder type
      *
      * @return string Class name
      */
     static function folder_class_name($type)
     {
-        list($ctype, $subtype) = explode('.', $type);
+        list($ctype, $subtype) = array_pad(explode('.', $type), 2, null);
 
         $class[] = 'type-' . ($ctype ? $ctype : 'mail');
 
         if ($subtype)
             $class[] = 'subtype-' . $subtype;
 
         return implode(' ', $class);
     }
 
     /**
      * Creates default folders if they doesn't exist
      */
     private function create_default_folders(&$folders, $filter, $folderdata = null, $lsub = false)
     {
         $storage     = $this->rc->get_storage();
         $namespace   = $storage->get_namespace();
         $defaults    = array();
         $prefix      = '';
 
         // Find personal namespace prefix
         if (is_array($namespace['personal']) && count($namespace['personal']) == 1) {
             $prefix = $namespace['personal'][0][0];
         }
 
         $this->load_config();
 
         // get configured defaults
         foreach ($this->types as $type) {
             foreach ((array)$this->subtypes[$type] as $subtype) {
                 $opt_name = 'kolab_folders_' . $type . '_' . $subtype;
                 if ($folder = $this->rc->config->get($opt_name)) {
                     // convert configuration value to UTF7-IMAP charset
                     $folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP');
                     // and namespace prefix if needed
                     if ($prefix && strpos($folder, $prefix) === false && $folder != 'INBOX') {
                         $folder = $prefix . $folder;
                     }
                     $defaults[$type . '.' . $subtype] = $folder;
                 }
             }
         }
 
         if (empty($defaults)) {
             return;
         }
 
         if ($folderdata === null) {
             $folderdata = kolab_storage::folders_typedata();
         }
 
         if (!is_array($folderdata)) {
             return;
         }
 
         // find default folders
         foreach ($defaults as $type => $foldername) {
             // get all folders of specified type
             $_folders = array_intersect($folderdata, array($type));
 
             // default folder found
             if (!empty($_folders)) {
                 continue;
             }
 
             list($type1, $type2) = explode('.', $type);
 
             $activate = in_array($type1, $this->act_types);
             $exists   = false;
             $result   = false;
 
             // check if folder exists
             if (!empty($folderdata[$foldername]) || $foldername == 'INBOX') {
                 $exists = true;
             }
             else if ((!$filter || $filter == $type1) && in_array($foldername, $folders)) {
                 // this assumes also that subscribed folder exists
                 $exists = true;
             }
             else {
                 $exists = $storage->folder_exists($foldername);
             }
 
             // create folder
             if (!$exists) {
                 $exists = $storage->create_folder($foldername);
             }
 
             // set type + subscribe + activate
             if ($exists) {
                 if ($result = kolab_storage::set_folder_type($foldername, $type)) {
                     // check if folder is subscribed
                     if ((!$filter || $filter == $type1) && $lsub && in_array($foldername, $folders)) {
                         // already subscribed
                         $subscribed = true;
                     }
                     else {
                         $subscribed = $storage->subscribe($foldername);
                     }
 
                     // activate folder
                     if ($activate) {
                         kolab_storage::folder_activate($foldername, true);
                     }
                 }
             }
 
             // add new folder to the result
             if ($result && (!$filter || $filter == $type1) && (!$lsub || $subscribed)) {
                 $folders[] = $foldername;
             }
         }
     }
 
     /**
      * Static getter for default folder of the given type
      *
      * @param string $type Folder type
      *
      * @return string Folder name
      */
     public static function default_folder($type)
     {
         return self::$instance->get_default_folder($type);
     }
 
     /**
      * Get /shared/vendor/cmu/cyrus-imapd/expire value
      *
      * @param string $folder IMAP folder name
      *
      * @return int|false The annotation value or False if not supported
      */
     private function get_expire_annotation($folder)
     {
         $storage = $this->rc->get_storage();
 
         if ($storage->get_vendor() != 'cyrus') {
             return false;
         }
 
         if (!strlen($folder)) {
             return 0;
         }
 
         $value = $storage->get_metadata($folder, $this->expire_annotation);
 
         if (is_array($value)) {
-            return $value[$folder] ? intval($value[$folder][$this->expire_annotation]) : 0;
+            return ($value[$folder] ?? false) ? intval($value[$folder][$this->expire_annotation]) : 0;
         }
 
         return false;
     }
 
     /**
      * Add expiration time input to the form if supported
      */
     private function add_expire_input(&$form, $folder, $type = null)
     {
         if (($expire = $this->get_expire_annotation($folder)) !== false) {
             $post    = trim(rcube_utils::get_input_value('_expire', rcube_utils::INPUT_POST));
             $is_mail = empty($type) || preg_match('/^mail/i', $type);
             $label   = $this->gettext('xdays');
             $input   = new html_inputfield(array(
                     'id'       => '_kolabexpire',
                     'name'     => '_expire',
                     'size'     => 3,
                     'disabled' => !$is_mail
             ));
 
             if ($post && $is_mail) {
                 $expire = (int) $post;
             }
 
             if (strpos($label, '$') === 0) {
                 $label = str_replace('$x', '', $label);
                 $html  = $input->show($expire ?: '')
                     . html::span('input-group-append', html::span('input-group-text', rcube::Q($label)));
             }
             else {
                 $label = str_replace('$x', '', $label);
                 $html  = html::span('input-group-prepend', html::span('input-group-text', rcube::Q($label)))
                     . $input->show($expire ?: '');
             }
 
             $form['props']['fieldsets']['settings']['content']['kolabexpire'] = array(
                 'label' => $this->gettext('folderexpire'),
                 'value' => html::div('input-group', $html),
             );
         }
     }
 
     /**
      * Handler for various folders list widgets (hooks)
      *
      * @param array $args Hash array with hook parameters
      *
      * @return array Hash array with modified hook parameters
      */
     public function render_folderlist($args)
     {
         $storage  = $this->rc->get_storage();
         $ns_other = $storage->get_namespace('other');
         $is_fl    = $this->rc->plugins->is_processing('folders_list');
 
         foreach ((array) $ns_other as $root) {
             $delim  = $root[1];
             $prefix = rtrim($root[0], $delim);
             $length = strlen($prefix);
 
             if (!$length) {
                 continue;
             }
 
             // folders_list hook mode
             if ($is_fl) {
                 foreach ((array) $args['list'] as $folder_name => $folder) {
                     if (strpos($folder_name, $root[0]) === 0 && !substr_count($folder_name, $root[1], $length+1)) {
                         if ($name = kolab_storage::folder_id2user(substr($folder_name, $length+1), true)) {
                             $old     = $args['list'][$folder_name]['display'];
                             $content = $args['list'][$folder_name]['content'];
 
                             $name    = rcube::Q($name);
                             $content = str_replace(">$old<", ">$name<", $content);
 
                             $args['list'][$folder_name]['display'] = $name;
                             $args['list'][$folder_name]['content'] = $content;
                         }
                     }
                 }
 
                 // TODO: Re-sort the list
             }
             // render_* hooks mode
             else if (!empty($args['list'][$prefix]) && !empty($args['list'][$prefix]['folders'])) {
                 $map = array();
                 foreach ($args['list'][$prefix]['folders'] as $folder_name => $folder) {
                     if ($name = kolab_storage::folder_id2user($folder_name, true)) {
                         $args['list'][$prefix]['folders'][$folder_name]['name'] = $name;
                     }
 
                     $map[$folder_name] = $name ?: $args['list'][$prefix]['folders'][$folder_name]['name'];
                 }
 
                 // Re-sort the list
                 uasort($map, 'strcoll');
                 $args['list'][$prefix]['folders'] = array_replace($map, $args['list'][$prefix]['folders']);
             }
         }
 
         return $args;
     }
 }
diff --git a/plugins/kolab_notes/kolab_notes.php b/plugins/kolab_notes/kolab_notes.php
index 9d1c240d..a73d17a4 100644
--- a/plugins/kolab_notes/kolab_notes.php
+++ b/plugins/kolab_notes/kolab_notes.php
@@ -1,1482 +1,1485 @@
 <?php
 
 /**
  * Kolab notes module
  *
  * Adds simple notes management features to the web client
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2014-2015, Kolab Systems AG <contact@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_notes extends rcube_plugin
 {
     public $task = '?(?!login|logout).*';
     public $allowed_prefs = array('kolab_notes_sort_col');
     public $rc;
 
     private $ui;
     private $lists;
     private $folders;
     private $cache = array();
     private $message_notes = array();
     private $bonnie_api = false;
 
     /**
      * Required startup method of a Roundcube plugin
      */
     public function init()
     {
         $this->require_plugin('libkolab');
 
         $this->rc = rcube::get_instance();
 
         // proceed initialization in startup hook
         $this->add_hook('startup', array($this, 'startup'));
     }
 
     /**
      * Startup hook
      */
     public function startup($args)
     {
         // the notes module can be enabled/disabled by the kolab_auth plugin
         if ($this->rc->config->get('kolab_notes_disabled', false) || !$this->rc->config->get('kolab_notes_enabled', true)) {
             return;
         }
 
         $this->register_task('notes');
 
         // load plugin configuration
         $this->load_config();
 
         // load localizations
         $this->add_texts('localization/', $args['task'] == 'notes' && (!$args['action'] || $args['action'] == 'dialog-ui'));
         $this->rc->load_language($_SESSION['language'], array('notes.notes' => $this->gettext('navtitle')));  // add label for task title
 
         if ($args['task'] == 'notes') {
             $this->add_hook('storage_init', array($this, 'storage_init'));
 
             // register task actions
             $this->register_action('index', array($this, 'notes_view'));
             $this->register_action('fetch', array($this, 'notes_fetch'));
             $this->register_action('get',   array($this, 'note_record'));
             $this->register_action('action', array($this, 'note_action'));
             $this->register_action('list',  array($this, 'list_action'));
             $this->register_action('dialog-ui', array($this, 'dialog_view'));
             $this->register_action('print', array($this, 'print_note'));
 
             if (!$this->rc->output->ajax_call && in_array($args['action'], array('dialog-ui', 'list'))) {
                 $this->load_ui();
             }
         }
         else if ($args['task'] == 'mail') {
             $this->add_hook('storage_init', array($this, 'storage_init'));
             $this->add_hook('message_compose', array($this, 'mail_message_compose'));
 
             if (in_array($args['action'], array('show', 'preview', 'print'))) {
                 $this->add_hook('message_load', array($this, 'mail_message_load'));
                 $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
             }
 
             // add 'Append note' item to message menu
             if ($this->api->output->type == 'html' && $_REQUEST['_rel'] != 'note') {
                 $this->api->add_content(html::tag('li', array('role' => 'menuitem'),
                     $this->api->output->button(array(
                       'command'  => 'append-kolab-note',
                       'label'    => 'kolab_notes.appendnote',
                       'type'     => 'link',
                       'classact' => 'icon appendnote active',
                       'class'    => 'icon appendnote disabled',
                       'innerclass' => 'icon note',
                     ))),
                     'messagemenu');
 
                 $this->api->output->add_label('kolab_notes.appendnote', 'kolab_notes.editnote', 'kolab_notes.deletenotesconfirm', 'kolab_notes.entertitle', 'save', 'delete', 'cancel', 'close');
                 $this->include_script('notes_mail.js');
             }
         }
 
         if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
             $this->load_ui();
         }
 
         // get configuration for the Bonnie API
         $this->bonnie_api = libkolab::get_bonnie_api();
 
         // notes use fully encoded identifiers
         kolab_storage::$encode_ids = true;
     }
 
     /**
      * Hook into IMAP FETCH HEADER.FIELDS command and request MESSAGE-ID
      */
     public function storage_init($p)
     {
         $p['fetch_headers'] = trim($p['fetch_headers'] . ' MESSAGE-ID');
         return $p;
     }
 
     /**
      * Load and initialize UI class
      */
     private function load_ui()
     {
         if (!$this->ui) {
             require_once($this->home . '/kolab_notes_ui.php');
             $this->ui = new kolab_notes_ui($this);
             $this->ui->init();
         }
     }
 
     /**
      * Read available calendars for the current user and store them internally
      */
     private function _read_lists($force = false)
     {
         // already read sources
         if (isset($this->lists) && !$force)
             return $this->lists;
 
         // get all folders that have type "task"
         $folders = kolab_storage::sort_folders(kolab_storage::get_folders('note'));
         $this->lists = $this->folders = array();
 
         // find default folder
         $default_index = 0;
         foreach ($folders as $i => $folder) {
             if ($folder->default)
                 $default_index = $i;
         }
 
         // put default folder on top of the list
         if ($default_index > 0) {
             $default_folder = $folders[$default_index];
             unset($folders[$default_index]);
             array_unshift($folders, $default_folder);
         }
 
         foreach ($folders as $folder) {
             $item = $this->folder_props($folder);
             $this->lists[$item['id']] = $item;
             $this->folders[$item['id']] = $folder;
             $this->folders[$folder->name] = $folder;
         }
     }
 
     /**
      * Get a list of available folders from this source
      */
     public function get_lists(&$tree = null)
     {
         $this->_read_lists();
 
         // attempt to create a default folder for this user
         if (empty($this->lists)) {
             $folder = array('name' => 'Notes', 'type' => 'note', 'default' => true, 'subscribed' => true);
             if (kolab_storage::folder_update($folder)) {
                 $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 (!is_null($tree)) {
             $folders = kolab_storage::folder_hierarchy($folders, $tree);
         }
 
         $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
 
         $lists = array();
         foreach ($folders as $folder) {
             $list_id = $folder->id;
             $imap_path = explode($delim, $folder->name);
 
             // 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 = $folder->get_foldername();
 
             // special handling for virtual folders
             if ($folder instanceof kolab_storage_folder_user) {
                 $lists[$list_id] = array(
                     'id'       => $list_id,
                     'name'     => $fullname,
                     'listname' => $listname,
                     'title'    => $folder->get_title(),
                     'virtual'  => true,
                     'editable' => false,
                     'rights'   => 'l',
                     'group'    => 'other virtual',
                     'class'    => 'user',
                     'parent'   => $parent_id,
                 );
             }
             else if ($folder->virtual) {
                 $lists[$list_id] = array(
                     'id'       => $list_id,
                     'name'     => $fullname,
                     'listname' => $listname,
                     'virtual'  => true,
                     'editable' => false,
                     'rights'   => 'l',
                     'group'    => $folder->get_namespace(),
                     'parent'   => $parent_id,
                 );
             }
             else {
                 if (!$this->lists[$list_id]) {
                     $this->lists[$list_id] = $this->folder_props($folder);
                     $this->folders[$list_id] = $folder;
                 }
                 $this->lists[$list_id]['parent'] = $parent_id;
                 $lists[$list_id] = $this->lists[$list_id];
             }
         }
 
         return $lists;
     }
 
     /**
      * Search for shared or otherwise not listed folders the user has access
      *
      * @param string Search string
      * @param string Section/source to search
      * @return array List of notes folders
      */
     protected function search_lists($query, $source)
     {
         if (!kolab_storage::setup()) {
             return array();
         }
 
         $this->search_more_results = false;
         $this->lists = $this->folders = array();
 
         // find unsubscribed IMAP folders that have "event" type
         if ($source == 'folders') {
             foreach ((array)kolab_storage::search_folders('note', $query, array('other')) as $folder) {
                 $this->folders[$folder->id] = $folder;
                 $this->lists[$folder->id] = $this->folder_props($folder);
             }
         }
         // 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 note folders shared by this user
                 foreach (kolab_storage::list_user_folders($user, 'note', false) as $foldername) {
                     $folders[] = new kolab_storage_folder($foldername, 'note');
                 }
 
                 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);
 
                     foreach ($folders as $folder) {
                         $this->folders[$folder->id] = $folder;
                         $this->lists[$folder->id] = $this->folder_props($folder);
                         $count++;
                     }
                 }
 
                 if ($count >= $limit) {
                     $this->search_more_results = true;
                     break;
                 }
             }
 
         }
 
         return $this->get_lists();
     }
 
     /**
      * Derive list properties from the given kolab_storage_folder object
      */
     protected function folder_props($folder)
     {
         if ($folder->get_namespace() == 'personal') {
             $norename = false;
             $editable = true;
             $rights = 'lrswikxtea';
             $alarms = true;
         }
         else {
             $alarms = false;
             $rights = 'lr';
             $editable = false;
             if (($myrights = $folder->get_myrights()) && !PEAR::isError($myrights)) {
                 $rights = $myrights;
                 if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false)
                     $editable = strpos($rights, 'i');
             }
             $info = $folder->get_folder_info();
             $norename = $readonly || $info['norename'] || $info['protected'];
         }
 
         $list_id = $folder->id;
         return array(
             'id' => $list_id,
             'name' => $folder->get_name(),
             'listname' => $folder->get_foldername(),
             'editname' => $folder->get_foldername(),
             'editable' => $editable,
             'rights'   => $rights,
             'norename' => $norename,
             'parentfolder' => $folder->get_parent(),
             'subscribed' => (bool)$folder->is_subscribed(),
             'default'  => $folder->default,
             'group'    => $folder->default ? 'default' : $folder->get_namespace(),
             'class'    => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
         );
     }
 
     /**
      * 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_folder($id)
     {
         // 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);
             }
         }
 
         return $this->folders[$id];
     }
 
     /*******  UI functions  ********/
 
     /**
      * Render main view of the tasklist task
      */
     public function notes_view()
     {
         $this->ui->init();
         $this->ui->init_templates();
         $this->rc->output->set_pagetitle($this->gettext('navtitle'));
         $this->rc->output->send('kolab_notes.notes');
     }
 
     /**
      * Deliver a rediced UI for inline (dialog)
      */
     public function dialog_view()
     {
         // resolve message reference
         if ($msgref = rcube_utils::get_input_value('_msg', rcube_utils::INPUT_GPC, true)) {
             $storage = $this->rc->get_storage();
             list($uid, $folder) = explode('-', $msgref, 2);
             if ($message = $storage->get_message_headers($msgref)) {
                 $this->rc->output->set_env('kolab_notes_template', array(
                     '_from_mail' => true,
                     'title' => $message->get('subject'),
                     'links' => array(kolab_storage_config::get_message_reference(
                         kolab_storage_config::get_message_uri($message, $folder),
                         'note'
                     )),
                 ));
             }
         }
 
         $this->ui->init_templates();
         $this->rc->output->send('kolab_notes.dialogview');
     }
 
     /**
      * Handler to retrieve note records for the given list and/or search query
      */
     public function notes_fetch()
     {
         $search = rcube_utils::get_input_value('_q', rcube_utils::INPUT_GPC, true);
         $list   = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC);
 
         $data = $this->notes_data($this->list_notes($list, $search), $tags);
 
         $this->rc->output->command('plugin.data_ready', array(
                 'list'   => $list,
                 'search' => $search,
                 'data'   => $data,
                 'tags'   => array_values($tags)
         ));
     }
 
     /**
      * Convert the given note records for delivery to the client
      */
     protected function notes_data($records, &$tags)
     {
         $config = kolab_storage_config::get_instance();
         $tags   = $config->apply_tags($records);
         $config->apply_links($records);
 
         foreach ($records as $i => $rec) {
             unset($records[$i]['description']);
             $this->_client_encode($records[$i]);
         }
 
         return $records;
     }
 
     /**
      * Read note records for the given list from the storage backend
      */
     protected function list_notes($list_id, $search = null)
     {
         $results = array();
 
         // query Kolab storage
         $query = array();
 
         // full text search (only works with cache enabled)
         if (strlen($search)) {
             $words = array_filter(rcube_utils::normalize_string(mb_strtolower($search), true));
             foreach ($words as $word) {
                 if (strlen($word) > 2) {  // only words > 3 chars are stored in DB
                     $query[] = array('words', '~', $word);
                 }
             }
         }
 
         $this->_read_lists();
         if ($folder = $this->get_folder($list_id)) {
             foreach ($folder->select($query, empty($query)) as $record) {
                 // post-filter search results
                 if (strlen($search)) {
                     $matches = 0;
                     $contents = mb_strtolower(
                         $record['title'] .
                         ($this->is_html($record) ? strip_tags($record['description']) : $record['description'])
                     );
                     foreach ($words as $word) {
                         if (mb_strpos($contents, $word) !== false) {
                             $matches++;
                         }
                     }
 
                     // skip records not matching all search words
                     if ($matches < count($words)) {
                         continue;
                     }
                 }
                 $record['list'] = $list_id;
                 $results[] = $record;
             }
         }
 
         return $results;
     }
 
     /**
      * Handler for delivering a full note record to the client
      */
     public function note_record()
     {
         $data = $this->get_note(array(
             'uid'  => rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC),
             'list' => rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC),
         ));
 
         // encode for client use
         if (is_array($data)) {
             $this->_client_encode($data);
         }
 
         $this->rc->output->command('plugin.render_note', $data);
     }
 
     /**
      * Get the full note record identified by the given UID + Lolder identifier
      */
     public function get_note($note)
     {
         if (is_array($note)) {
             $uid = $note['uid'] ?: $note['id'];
             $list_id = $note['list'];
         }
         else {
             $uid = $note;
         }
 
         // deliver from in-memory cache
         $key = $list_id . ':' . $uid;
-        if ($this->cache[$key]) {
+        if ($this->cache[$key] ?? false) {
             return $this->cache[$key];
         }
 
         $result = false;
 
         $this->_read_lists();
         if ($list_id) {
             if ($folder = $this->get_folder($list_id)) {
                 $result = $folder->get_object($uid);
             }
         }
         // iterate over all calendar folders and search for the event ID
         else {
             foreach ($this->folders as $list_id => $folder) {
                 if ($result = $folder->get_object($uid)) {
                     $result['list'] = $list_id;
                     break;
                 }
             }
         }
 
         if ($result) {
             // get note tags
             $result['tags'] = $this->get_tags($result['uid']);
             // get note links
             $result['links'] = $this->get_links($result['uid']);
         }
 
         return $result;
     }
 
     /**
      * Helper method to encode the given note record for use in the client
      */
     private function _client_encode(&$note)
     {
         foreach ($note as $key => $prop) {
             if ($key[0] == '_' || $key == 'x-custom') {
                 unset($note[$key]);
             }
         }
 
         foreach (array('created','changed') as $key) {
             if (is_object($note[$key]) && $note[$key] instanceof DateTime) {
                 $note[$key.'_'] = $note[$key]->format('U');
                 $note[$key] = $this->rc->format_date($note[$key]);
             }
         }
 
         // clean HTML contents
         if (!empty($note['description']) && $this->is_html($note)) {
             $note['html'] = $this->_wash_html($note['description']);
         }
 
         // convert link URIs references into structs
         if (array_key_exists('links', $note)) {
             foreach ((array)$note['links'] as $i => $link) {
                 if (strpos($link, 'imap://') === 0 && ($msgref = kolab_storage_config::get_message_reference($link, 'note'))) {
                     $note['links'][$i] = $msgref;
                 }
             }
         }
 
         return $note;
     }
 
     /**
      * Handler for client-initiated actions on a single note record
      */
     public function note_action()
     {
         $action = rcube_utils::get_input_value('_do', rcube_utils::INPUT_POST);
         $note   = rcube_utils::get_input_value('_data', rcube_utils::INPUT_POST, true);
 
         $success = $silent = false;
         switch ($action) {
             case 'new':
             case 'edit':
                 if ($success = $this->save_note($note)) {
                     $refresh = $this->get_note($note);
                 }
                 break;
 
             case 'move':
                 $uids = explode(',', $note['uid']);
                 foreach ($uids as $uid) {
                     $note['uid'] = $uid;
                     if (!($success = $this->move_note($note, $note['to']))) {
                         $refresh = $this->get_note($note);
                         break;
                     }
                 }
                 break;
 
             case 'delete':
                 $uids = explode(',', $note['uid']);
                 foreach ($uids as $uid) {
                     $note['uid'] = $uid;
                     if (!($success = $this->delete_note($note))) {
                         $refresh = $this->get_note($note);
                         break;
                     }
                 }
                 break;
 
             case 'changelog':
                 $data = $this->get_changelog($note);
                 if (is_array($data) && !empty($data)) {
                     $rcmail = $this->rc;
                     $dtformat = $rcmail->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
                     array_walk($data, function(&$change) use ($lib, $rcmail, $dtformat) {
                       if ($change['date']) {
                           $dt = rcube_utils::anytodatetime($change['date']);
                           if ($dt instanceof DateTime) {
                               $change['date'] = $rcmail->format_date($dt, $dtformat);
                           }
                       }
                     });
                     $this->rc->output->command('plugin.note_render_changelog', $data);
                 }
                 else {
                     $this->rc->output->command('plugin.note_render_changelog', false);
                 }
                 $silent = true;
                 break;
 
             case 'diff':
                 $silent = true;
                 $data = $this->get_diff($note, $note['rev1'], $note['rev2']);
                 if (is_array($data)) {
                     $this->rc->output->command('plugin.note_show_diff', $data);
                 }
                 else {
                     $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
                 }
                 break;
 
             case 'show':
                 if ($rec = $this->get_revison($note, $note['rev'])) {
                     $this->rc->output->command('plugin.note_show_revision', $this->_client_encode($rec));
                 }
                 else {
                     $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
                 }
                 $silent = true;
                 break;
 
             case 'restore':
                 if ($this->restore_revision($note, $note['rev'])) {
                     $refresh = $this->get_note($note);
                     $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $note['rev']))), 'confirmation');
                     $this->rc->output->command('plugin.close_history_dialog');
                 }
                 else {
                     $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
                 }
                 $silent = true;
                 break;
         }
 
         // show confirmation/error message
         if ($success) {
             $this->rc->output->show_message('successfullysaved', 'confirmation');
         }
         else if (!$silent) {
             $this->rc->output->show_message('errorsaving', 'error');
         }
 
         // unlock client
         $this->rc->output->command('plugin.unlock_saving');
 
         if ($refresh) {
             $this->rc->output->command('plugin.update_note', $this->_client_encode($refresh));
         }
     }
 
     /**
      * Update an note record with the given data
      *
      * @param array Hash array with note properties (id, list)
      * @return boolean True on success, False on error
      */
     private function save_note(&$note)
     {
         $this->_read_lists();
 
         $list_id = $note['list'];
         if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         // moved from another folder
-        if ($note['_fromlist'] && ($fromfolder = $this->get_folder($note['_fromlist']))) {
+        if (($note['_fromlist'] ?? false) && ($fromfolder = $this->get_folder($note['_fromlist']))) {
             if (!$fromfolder->move($note['uid'], $folder->name))
                 return false;
 
             unset($note['_fromlist']);
         }
 
         // load previous version of this record to merge
+        $old = null;
         if ($note['uid']) {
             $old = $folder->get_object($note['uid']);
             if (!$old || PEAR::isError($old))
                 return false;
 
             // merge existing properties if the update isn't complete
             if (!isset($note['title']) || !isset($note['description']))
                 $note += $old;
         }
 
         // generate new note object from input
         $object = $this->_write_preprocess($note, $old);
 
         // email links and tags are handled separately
-        $links = $object['links'];
-        $tags  = $object['tags'];
+        $links = $object['links'] ?? null;
+        $tags  = $object['tags'] ?? null;
 
         unset($object['links']);
         unset($object['tags']);
 
         $saved = $folder->save($object, 'note', $note['uid']);
 
         if (!$saved) {
             rcube::raise_error(array(
                 'code' => 600, 'type' => 'php',
                 'file' => __FILE__, 'line' => __LINE__,
                 'message' => "Error saving note object to Kolab server"),
                 true, false);
             $saved = false;
         }
         else {
             // save links in configuration.relation object
             $this->save_links($object['uid'], $links);
             // save tags in configuration.relation object
             $this->save_tags($object['uid'], $tags);
 
             $note         = $object;
             $note['list'] = $list_id;
             $note['tags'] = (array) $tags;
 
             // cache this in memory for later read
             $key = $list_id . ':' . $note['uid'];
             $this->cache[$key] = $note;
         }
 
         return $saved;
     }
 
     /**
      * Move the given note to another folder
      */
     function move_note($note, $list_id)
     {
         $this->_read_lists();
 
         $tofolder   = $this->get_folder($list_id);
         $fromfolder = $this->get_folder($note['list']);
 
         if ($fromfolder && $tofolder) {
             return $fromfolder->move($note['uid'], $tofolder->name);
         }
 
         return false;
     }
 
     /**
      * Remove a single note record from the backend
      *
      * @param array   Hash array with note properties (id, list)
      * @param boolean Remove record irreversible (mark as deleted otherwise)
      * @return boolean True on success, False on error
      */
     public function delete_note($note, $force = true)
     {
         $this->_read_lists();
 
         $list_id = $note['list'];
         if (!$list_id || !($folder = $this->get_folder($list_id))) {
             return false;
         }
 
         $status = $folder->delete($note['uid'], $force);
 
         if ($status) {
             $this->save_links($note['uid'], null);
             $this->save_tags($note['uid'], null);
         }
 
         return $status;
     }
 
     /**
      * Render the template for printing with placeholders
      */
     public function print_note()
     {
         $uid  = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
         $list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GET);
 
         $this->note = $this->get_note(array('uid' => $uid, 'list' => $list));
 
         // encode for client use
         if (is_array($this->note)) {
             $this->_client_encode($this->note);
         }
 
         $this->rc->output->set_pagetitle($this->note['title']);
         $this->rc->output->add_handlers(array(
                 'noteheader' => array($this, 'print_note_header'),
                 'notebody'   => array($this, 'print_note_body'),
         ));
 
         $this->include_script('notes.js');
 
         $this->rc->output->send('kolab_notes.print');
     }
 
     public function print_note_header()
     {
         $tags = array_map(array('rcube', 'Q'), (array) $this->note['tags']);
         $tags = implode(' ', $tags);
 
         return html::tag('h1', array('id' => 'notetitle'), rcube::Q($this->note['title']))
             . html::div(array('id' => 'notetags', 'class' => 'tagline'), $tags)
             . html::div('dates',
                 html::label(null, rcube::Q($this->gettext('created')))
                 . html::span(array('id' => 'notecreated'), rcube::Q($this->note['created']))
                 . html::label(null, rcube::Q($this->gettext('changed')))
                 . html::span(array('id' => 'notechanged'), rcube::Q($this->note['changed']))
             );
     }
 
     public function print_note_body()
     {
         return isset($this->note['html']) ? $this->note['html'] : rcube::Q($this->note['description']);
     }
 
     /**
      * Provide a list of revisions for the given object
      *
      * @param array  $note Hash array with note properties
      * @return array List of changes, each as a hash array
      */
     public function get_changelog($note)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
 
         $result = $uid && $mailbox ? $this->bonnie_api->changelog('note', $uid, $mailbox, $msguid) : null;
         if (is_array($result) && $result['uid'] == $uid) {
             return $result['changes'];
         }
 
         return false;
     }
 
     /**
      * Return full data of a specific revision of a note record
      *
      * @param mixed  $note UID string or hash array with note properties
      * @param mixed  $rev Revision number
      *
      * @return array Note object as hash array
      */
     public function get_revison($note, $rev)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
 
         // call Bonnie API
         $result = $this->bonnie_api->get('note', $uid, $rev, $mailbox, $msguid);
         if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
             $format = kolab_format::factory('note');
             $format->load($result['xml']);
             $rec = $format->to_array();
 
             if ($format->is_valid()) {
                 $rec['rev'] = $result['rev'];
                 return $rec;
             }
         }
 
         return false;
     }
 
     /**
      * Get a list of property changes beteen two revisions of a note object
      *
      * @param array  $$note Hash array with note properties
      * @param mixed  $rev   Revisions: "from:to"
      *
      * @return array List of property changes, each as a hash array
      */
     public function get_diff($note, $rev1, $rev2)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
 
         // call Bonnie API
         $result = $this->bonnie_api->diff('note', $uid, $rev1, $rev2, $mailbox, $msguid);
         if (is_array($result) && $result['uid'] == $uid) {
             $result['rev1'] = $rev1;
             $result['rev2'] = $rev2;
 
             // convert some properties, similar to self::_client_encode()
             $keymap = array(
                 'summary'  => 'title',
                 'lastmodified-date' => 'changed',
             );
 
             // map kolab object properties to keys and values the client expects
             array_walk($result['changes'], function(&$change, $i) use ($keymap) {
                 if (array_key_exists($change['property'], $keymap)) {
                     $change['property'] = $keymap[$change['property']];
                 }
 
                 if ($change['property'] == 'created' || $change['property'] == 'changed') {
                     if ($old_ = rcube_utils::anytodatetime($change['old'])) {
                         $change['old_'] = $this->rc->format_date($old_);
                     }
                     if ($new_ = rcube_utils::anytodatetime($change['new'])) {
                         $change['new_'] = $this->rc->format_date($new_);
                     }
                 }
 
                 // compute a nice diff of note contents
                 if ($change['property'] == 'description') {
                     $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
                     if (!empty($change['diff_'])) {
                         unset($change['old'], $change['new']);
                         $change['diff_'] = preg_replace(array('!^.*<body[^>]*>!Uims','!</body>.*$!Uims'), '', $change['diff_']);
                         $change['diff_'] = preg_replace("!</(p|li|span)>\n!", '</\\1>', $change['diff_']);
                     }
                 }
             });
 
             return $result;
         }
 
         return false;
     }
 
     /**
      * Command the backend to restore a certain revision of a note.
      * This shall replace the current object with an older version.
      *
      * @param array  $note Hash array with note properties (id, list)
      * @param mixed  $rev Revision number
      *
      * @return boolean True on success, False on failure
      */
     public function restore_revision($note, $rev)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         list($uid, $mailbox, $msguid) = $this->_resolve_note_identity($note);
 
         $folder = $this->get_folder($note['list']);
         $success = false;
 
         if ($folder && ($raw_msg = $this->bonnie_api->rawdata('note', $uid, $rev, $mailbox))) {
             $imap = $this->rc->get_storage();
 
             // insert $raw_msg as new message
             if ($imap->save_message($folder->name, $raw_msg, null, false)) {
                 $success = true;
 
                 // delete old revision from imap and cache
                 $imap->delete_message($msguid, $folder->name);
                 $folder->cache->set($msguid, false);
                 $this->cache = array();
             }
         }
 
         return $success;
     }
 
     /**
      * Helper method to resolved the given note identifier into uid and mailbox
      *
      * @return array (uid,mailbox,msguid) tuple
      */
     private function _resolve_note_identity($note)
     {
         $mailbox = $msguid = null;
 
         if (!is_array($note)) {
             $note = $this->get_note($note);
         }
 
         if (is_array($note)) {
             $uid = $note['uid'] ?: $note['id'];
             $list = $note['list'];
         }
         else {
             return array(null, $mailbox, $msguid);
         }
 
         if ($folder = $this->get_folder($list)) {
             $mailbox = $folder->get_mailbox_id();
 
             // get object from storage in order to get the real object uid an msguid
             if ($rec = $folder->get_object($uid)) {
                 $msguid = $rec['_msguid'];
                 $uid = $rec['uid'];
             }
         }
 
         return array($uid, $mailbox, $msguid);
     }
 
 
     /**
      * Handler for client requests to list (aka folder) actions
      */
     public function list_action()
     {
         $action  = rcube_utils::get_input_value('_do', rcube_utils::INPUT_GPC);
         $list    = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC, true);
         $success = $update_cmd = false;
 
         if (empty($action)) {
             $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
         }
 
         switch ($action) {
             case 'form-new':
             case 'form-edit':
                 $this->_read_lists();
                 $this->ui->list_editform($action, $this->lists[$list['id']], $this->folders[$list['id']]);
                 exit;
 
             case 'new':
                 $list['type'] = 'note';
                 $list['subscribed'] = true;
                 $folder = kolab_storage::folder_update($list);
 
                 if ($folder === false) {
                     $save_error = $this->gettext(kolab_storage::$last_error);
                 }
                 else {
                     $success = true;
                     $update_cmd = 'plugin.update_list';
                     $list['id'] = kolab_storage::folder_id($folder);
                     $list['_reload'] = true;
                 }
                 break;
 
             case 'edit':
                 $this->_read_lists();
                 $oldparent = $this->lists[$list['id']]['parentfolder'];
                 $newfolder = kolab_storage::folder_update($list);
 
                 if ($newfolder === false) {
                     $save_error = $this->gettext(kolab_storage::$last_error);
                 }
                 else {
                     $success = true;
                     $update_cmd = 'plugin.update_list';
                     $list['newid'] = kolab_storage::folder_id($newfolder);
                     $list['_reload'] = $list['parent'] != $oldparent;
 
                     // compose the new display name
                     $delim            = $this->rc->get_storage()->get_hierarchy_delimiter();
                     $path_imap        = explode($delim, $newfolder);
                     $list['name']     = kolab_storage::object_name($newfolder);
                     $list['editname'] = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');
                     $list['listname'] = $list['editname'];
                 }
                 break;
 
             case 'delete':
                 $this->_read_lists();
                 $folder = $this->get_folder($list['id']);
                 if ($folder && kolab_storage::folder_delete($folder->name)) {
                     $success = true;
                     $update_cmd = 'plugin.destroy_list';
                 }
                 else {
                     $save_error = $this->gettext(kolab_storage::$last_error);
                 }
                 break;
 
             case 'search':
                 $this->load_ui();
                 $results = array();
                 foreach ((array)$this->search_lists(rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC), rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC)) as $id => $prop) {
                     $editname = $prop['editname'];
                     unset($prop['editname']);  // force full name to be displayed
 
                     // let the UI generate HTML and CSS representation for this calendar
                     $html = $this->ui->folder_list_item($id, $prop, $jsenv, true);
                     $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', 'notice');
                 }
 
                 $this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
                 return;
 
             case 'subscribe':
                 $success = false;
                 if ($list['id'] && ($folder = $this->get_folder($list['id']))) {
                     if (isset($list['permanent']))
                         $success |= $folder->subscribe(intval($list['permanent']));
                     if (isset($list['active']))
                         $success |= $folder->activate(intval($list['active']));
 
                     // apply to child folders, too
                     if ($list['recursive']) {
                         foreach ((array)kolab_storage::list_folders($folder->name, '*', 'node') as $subfolder) {
                             if (isset($list['permanent']))
                                 ($list['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
                             if (isset($list['active']))
                                 ($list['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder));
                         }
                     }
                 }
                 break;
         }
 
         $this->rc->output->command('plugin.unlock_saving');
 
         if ($success) {
             $this->rc->output->show_message('successfullysaved', 'confirmation');
 
             if ($update_cmd) {
                 $this->rc->output->command($update_cmd, $list);
             }
         }
         else {
             $error_msg = $this->gettext('errorsaving') . ($save_error ? ': ' . $save_error :'');
             $this->rc->output->show_message($error_msg, 'error');
         }
     }
 
     /**
      * Hook to add note attachments to message compose if the according parameter is present.
      * This completes the 'send note by mail' feature.
      */
     public function mail_message_compose($args)
     {
         if (!empty($args['param']['with_notes'])) {
             $uids = explode(',', $args['param']['with_notes']);
             $list = $args['param']['notes_list'];
 
             foreach ($uids as $uid) {
                 if ($note = $this->get_note(array('uid' => $uid, 'list' => $list))) {
                     $data = $this->note2message($note);
                     $args['attachments'][] = array(
                         'name'     => abbreviate_string($note['title'], 50, ''),
                         'mimetype' => 'message/rfc822',
                         'data'     => $data,
                         'size'     => strlen($data),
                     );
 
                     if (empty($args['param']['subject'])) {
                         $args['param']['subject'] = $note['title'];
                     }
                 }
             }
 
             unset($args['param']['with_notes'], $args['param']['notes_list']);
         }
 
         return $args;
     }
 
     /**
      * Lookup backend storage and find notes associated with the given message
      */
     public function mail_message_load($p)
     {
         if (!$p['object']->headers->others['x-kolab-type']) {
             $this->message_notes = $this->get_message_notes($p['object']->headers, $p['object']->folder);
         }
     }
 
     /**
      * Handler for 'messagebody_html' hook
      */
     public function mail_messagebody_html($args)
     {
         $html = '';
         foreach ($this->message_notes as $note) {
             $html .= html::a(array(
                 'href' => $this->rc->url(array('task' => 'notes', '_list' => $note['list'], '_id' => $note['uid'])),
                 'class' => 'kolabnotesref',
                 'rel' => $note['uid'] . '@' . $note['list'],
                 'target' => '_blank',
             ), rcube::Q($note['title']));
         }
 
         // prepend note links to message body
         if ($html) {
             $this->load_ui();
             $args['content'] = html::div('kolabmessagenotes boxinformation', $html) . $args['content'];
         }
 
         return $args;
     }
 
     /**
      * Determine whether the given note is HTML formatted
      */
     private function is_html($note)
     {
         // check for opening and closing <html> or <body> tags
         return (preg_match('/<(html|body)(\s+[a-z]|>)/', $note['description'], $m) && strpos($note['description'], '</'.$m[1].'>') > 0);
     }
 
     /**
      * Build an RFC 822 message from the given note
      */
     private function note2message($note)
     {
         $message = new Mail_mime("\r\n");
 
         $message->setParam('text_encoding', '8bit');
         $message->setParam('html_encoding', 'quoted-printable');
         $message->setParam('head_encoding', 'quoted-printable');
         $message->setParam('head_charset', RCUBE_CHARSET);
         $message->setParam('html_charset', RCUBE_CHARSET);
         $message->setParam('text_charset', RCUBE_CHARSET);
 
         $message->headers(array(
             'Subject' => $note['title'],
             'Date' => $note['changed']->format('r'),
         ));
 
         if ($this->is_html($note)) {
             $message->setHTMLBody($note['description']);
 
             // add a plain text version of the note content as an alternative part.
             $h2t = new rcube_html2text($note['description'], false, true, 0, RCUBE_CHARSET);
             $plain_part = rcube_mime::wordwrap($h2t->get_text(), $this->rc->config->get('line_length', 72), "\r\n", false, RCUBE_CHARSET);
             $plain_part = trim(wordwrap($plain_part, 998, "\r\n", true));
 
             // make sure all line endings are CRLF
             $plain_part = preg_replace('/\r?\n/', "\r\n", $plain_part);
 
             $message->setTXTBody($plain_part);
         }
         else {
             $message->setTXTBody($note['description']);
         }
 
         return $message->getMessage();
     }
 
     private function save_links($uid, $links)
     {
         $config = kolab_storage_config::get_instance();
         return $config->save_object_links($uid, (array) $links);
     }
 
     /**
      * Find messages assigned to specified note
      */
     private function get_links($uid)
     {
         $config = kolab_storage_config::get_instance();
         return $config->get_object_links($uid);
     }
 
     /**
      * Get note tags
      */
     private function get_tags($uid)
     {
         $config = kolab_storage_config::get_instance();
         $tags   = $config->get_tags($uid);
         $tags   = array_map(function($v) { return $v['name']; }, $tags);
 
         return $tags;
     }
 
     /**
      * Find notes assigned to specified message
      */
     private function get_message_notes($message, $folder)
     {
         $config = kolab_storage_config::get_instance();
         $result = $config->get_message_relations($message, $folder, 'note');
 
         foreach ($result as $idx => $note) {
             $result[$idx]['list'] = kolab_storage::folder_id($note['_mailbox']);
         }
 
         return $result;
     }
 
     /**
      * Update note tags
      */
     private function save_tags($uid, $tags)
     {
         $config = kolab_storage_config::get_instance();
         $config->save_tags($uid, $tags);
     }
 
     /**
      * Process the given note data (submitted by the client) before saving it
      */
     private function _write_preprocess($note, $old = array())
     {
         $object = $note;
 
         // TODO: handle attachments
 
         // convert link references into simple URIs
         if (array_key_exists('links', $note)) {
             $object['links'] = array_map(function($link){ return is_array($link) ? $link['uri'] : strval($link); }, $note['links']);
         }
         else {
-            $object['links'] = $old['links'];
+            if ($old) {
+                $object['links'] = $old['links'] ?? null;
+            }
         }
 
         // clean up HTML content
         $object['description'] = $this->_wash_html($note['description']);
         $is_html = true;
 
         // try to be smart and convert to plain-text if no real formatting is detected
         if (preg_match('!<body><(?:p|pre)>(.*)</(?:p|pre)></body>!Uims', $object['description'], $m)) {
             if (!preg_match('!<(a|b|i|strong|em|p|span|div|pre|li|img)(\s+[a-z]|>)!im', $m[1], $n)
                 || ($n[1] != 'img' && !strpos($m[1], '</'.$n[1].'>'))
             ) {
                 // $converter = new rcube_html2text($m[1], false, true, 0);
                 // $object['description'] = rtrim($converter->get_text());
                 $object['description'] = html_entity_decode(preg_replace('!<br(\s+/)>!', "\n", $m[1]));
                 $is_html = false;
             }
         }
 
         // Add proper HTML header, otherwise Kontact renders it as plain text
         if ($is_html) {
             $object['description'] = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">'."\n" .
                 str_replace('<head>', '<head><meta name="qrichtext" content="1" />', $object['description']);
         }
 
         // copy meta data (starting with _) from old object
         foreach ((array)$old as $key => $val) {
             if (!isset($object[$key]) && $key[0] == '_')
                 $object[$key] = $val;
         }
 
         // make list of categories unique
-        if (is_array($object['tags'])) {
+        if (is_array($object['tags'] ?? null)) {
             $object['tags'] = array_unique(array_filter($object['tags']));
         }
 
         unset($object['list'], $object['tempid'], $object['created'], $object['changed'], $object['created_'], $object['changed_']);
         return $object;
     }
 
     /**
      * Sanity checks/cleanups HTML content
      */
     private function _wash_html($html)
     {
         // Add header with charset spec., washtml cannot work without that
         $html = '<html><head>'
             . '<meta http-equiv="Content-Type" content="text/html; charset='.RCUBE_CHARSET.'" />'
             . '</head><body>' . $html . '</body></html>';
 
         // clean HTML with washtml by Frederic Motte
         $wash_opts = array(
             'show_washed'   => false,
             'allow_remote'  => 1,
             'charset'       => RCUBE_CHARSET,
             'html_elements' => array('html', 'head', 'meta', 'body', 'link'),
             'html_attribs'  => array('rel', 'type', 'name', 'http-equiv'),
         );
 
         // initialize HTML washer
         $washer = new rcube_washtml($wash_opts);
 
         $washer->add_callback('form', array($this, '_washtml_callback'));
         $washer->add_callback('a',    array($this, '_washtml_callback'));
 
         // Remove non-UTF8 characters
         $html = rcube_charset::clean($html);
 
         $html = $washer->wash($html);
 
         // remove unwanted comments (produced by washtml)
         $html = preg_replace('/<!--[^>]+-->/', '', $html);
 
         return $html;
     }
 
     /**
      * Callback function for washtml cleaning class
      */
     public function _washtml_callback($tagname, $attrib, $content, $washtml)
     {
         switch ($tagname) {
         case 'form':
             $out = html::div('form', $content);
             break;
 
         case 'a':
             // strip temporary link tags from plain-text markup
             $attrib = html::parse_attrib_string($attrib);
             if (!empty($attrib['class']) && strpos($attrib['class'], 'x-templink') !== false) {
                 // remove link entirely
                 if (strpos($attrib['href'], html_entity_decode($content)) !== false) {
                     $out = $content;
                     break;
                 }
                 $attrib['class'] = trim(str_replace('x-templink', '', $attrib['class']));
             }
             $out = html::a($attrib, $content);
             break;
 
         default:
             $out = '';
         }
 
         return $out;
     }
 
 }
diff --git a/plugins/kolab_notes/kolab_notes_ui.php b/plugins/kolab_notes/kolab_notes_ui.php
index dcbb35a6..2d3ad12d 100644
--- a/plugins/kolab_notes/kolab_notes_ui.php
+++ b/plugins/kolab_notes/kolab_notes_ui.php
@@ -1,347 +1,347 @@
 <?php
 
 class kolab_notes_ui
 {
     private $rc;
     private $plugin;
     private $ready = false;
 
     function __construct($plugin)
     {
         $this->plugin = $plugin;
         $this->rc = $plugin->rc;
     }
 
     /**
     * Calendar UI initialization and requests handlers
     */
     public function init()
     {
         if ($this->ready)  // already done
             return;
 
         // add taskbar button
         $this->plugin->add_button(array(
             'command'    => 'notes',
             'class'      => 'button-notes',
             'classsel'   => 'button-notes button-selected',
             'innerclass' => 'button-inner',
             'label'      => 'kolab_notes.navtitle',
             'type'       => 'link'
         ), 'taskbar');
 
         $this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/notes.css');
 
         $this->ready = true;
   }
 
     /**
     * Register handler methods for the template engine
     */
     public function init_templates()
     {
         $this->plugin->register_handler('plugin.notebooks', array($this, 'folders'));
         #$this->plugin->register_handler('plugin.folders_select', array($this, 'folders_select'));
         $this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));
         $this->plugin->register_handler('plugin.listing', array($this, 'listing'));
         $this->plugin->register_handler('plugin.editform', array($this, 'editform'));
         $this->plugin->register_handler('plugin.notetitle', array($this, 'notetitle'));
         $this->plugin->register_handler('plugin.detailview', array($this, 'detailview'));
         $this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
         $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
 
         $this->rc->output->include_script('list.js');
         $this->rc->output->include_script('treelist.js');
         $this->plugin->include_script('notes.js');
         $this->plugin->api->include_script('libkolab/libkolab.js');
 
         // load config options and user prefs relevant for the UI
         $settings = array(
             'sort_col' => $this->rc->config->get('kolab_notes_sort_col', 'changed'),
         );
 
         if ($list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC)) {
             $settings['selected_list'] = $list;
         }
         if ($uid = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC)) {
             $settings['selected_uid'] = $uid;
         }
 
         $this->rc->html_editor();
 
         $this->rc->output->set_env('kolab_notes_settings', $settings);
         $this->rc->output->add_label('save','cancel','delete','close','listoptionstitle');
     }
 
     public function folders($attrib)
     {
         $attrib += array('id' => 'rcmkolabnotebooks');
 
-        if ($attrib['type'] == 'select') {
+        if (($attrib['type'] ?? null) == 'select') {
             $attrib['is_escaped'] = true;
             $select = new html_select($attrib);
         }
 
-        $tree = $attrib['type'] != 'select' ? true : null;
+        $tree = ($attrib['type'] ?? null) != 'select' ? true : null;
         $lists = $this->plugin->get_lists($tree);
         $jsenv = array();
 
         if (is_object($tree)) {
             $html = $this->folder_tree_html($tree, $lists, $jsenv, $attrib);
         }
         else {
             $html = '';
             foreach ($lists as $prop) {
                 $id = $prop['id'];
 
                 if (!$prop['virtual']) {
                     unset($prop['user_id']);
                     $jsenv[$id] = $prop;
                 }
 
                 if ($attrib['type'] == 'select') {
                     if ($prop['editable'] || strpos($prop['rights'], 'i') !== false) {
                         $select->add($prop['name'], $prop['id']);
                     }
                 }
                 else {
                     $html .= html::tag('li', array('id' => 'rcmliknb' . rcube_utils::html_identifier($id), 'class' => $prop['group']),
                         $this->folder_list_item($id, $prop, $jsenv)
                     );
                 }
             }
         }
 
         $this->rc->output->set_env('kolab_notebooks', $jsenv);
         $this->rc->output->add_gui_object('notebooks', $attrib['id']);
 
-        return $attrib['type'] == 'select' ? $select->show() : html::tag('ul', $attrib, $html, html::$common_attrib);
+        return ($attrib['type'] ?? null) == 'select' ? $select->show() : html::tag('ul', $attrib, $html, html::$common_attrib);
     }
 
     /**
      * Return html for a structured list <ul> for the folder tree
      */
     public function folder_tree_html($node, $data, &$jsenv, $attrib)
     {
         $out = '';
         foreach ($node->children as $folder) {
             $id = $folder->id;
             $prop = $data[$id];
             $is_collapsed = false; // TODO: determine this somehow?
 
             $content = $this->folder_list_item($id, $prop, $jsenv);
 
             if (!empty($folder->children)) {
                 $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)),
                     $this->folder_tree_html($folder, $data, $jsenv, $attrib));
             }
 
             if (strlen($content)) {
                 $out .= html::tag('li', array(
                       'id' => 'rcmliknb' . rcube_utils::html_identifier($id),
-                      'class' => $prop['group'] . ($prop['virtual'] ? ' virtual' : ''),
+                      'class' => $prop['group'] . (($prop['virtual'] ?? false) ? ' virtual' : ''),
                     ),
                     $content);
             }
         }
 
         return $out;
     }
 
     /**
      * Helper method to build a tasklist item (HTML content and js data)
      */
     public function folder_list_item($id, $prop, &$jsenv, $checkbox = false)
     {
-        if (!$prop['virtual']) {
+        if (!($prop['virtual'] ?? false)) {
             unset($prop['user_id']);
             $jsenv[$id] = $prop;
         }
 
         $classes = array('folder');
-        if ($prop['virtual']) {
+        if ($prop['virtual'] ?? false) {
             $classes[] = 'virtual';
         }
         else if (!$prop['editable']) {
             $classes[] = 'readonly';
         }
         if ($prop['subscribed']) {
             $classes[] = 'subscribed';
         }
         if ($prop['class']) {
             $classes[] = $prop['class'];
         }
 
-        $title = $prop['title'] ?: ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
+        $title = $prop['title'] ?? ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25 ?
           html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET) : '');
 
         $label_id = 'nl:' . $id;
-        $attr = $prop['virtual'] ? array('tabindex' => '0') : array('href' => $this->rc->url(array('_list' => $id)));
+        $attr = ($prop['virtual'] ?? false) ? array('tabindex' => '0') : array('href' => $this->rc->url(array('_list' => $id)));
         return html::div(join(' ', $classes),
             html::a($attr + array('class' => 'listname', 'title' => $title, 'id' => $label_id), $prop['listname'] ?: $prop['name']) .
-            ($prop['virtual'] ? '' :
+            (($prop['virtual'] ?? false) ? '' :
                 ($checkbox ?
                     html::tag('input', array('type' => 'checkbox', 'name' => '_list[]', 'value' => $id, 'checked' => $prop['active'], 'aria-labelledby' => $label_id)) :
                     ''
                 ) .
                 html::span('handle', '') .
                 html::span('actions',
                     (!$prop['default'] ?
                         html::a(array('href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')), ' ') :
                         ''
                     ) .
                     (isset($prop['subscribed']) ?
                         html::a(array('href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('foldersubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'), ' ') :
                         ''
                     )
                 )
             )
         );
 
         return '';
     }
 
     public function listing($attrib)
     {
         $attrib += array('id' => 'rcmkolabnoteslist');
         $this->rc->output->add_gui_object('noteslist', $attrib['id']);
         return html::tag('table', $attrib, '<tbody></tbody>', html::$common_attrib);
     }
 
     public function editform($attrib)
     {
         $attrib += array('action' => '#', 'id' => 'rcmkolabnoteseditform');
 
         $textarea = new html_textarea(array(
                 'name'     => 'content',
                 'id'       => 'notecontent',
                 'cols'     => 60,
                 'rows'     => 20,
                 'tabindex' => 0,
                 'class'    => 'mce_editor form-control',
         ));
 
         $this->rc->output->add_gui_object('noteseditform', $attrib['id']);
 
         return html::tag('form', $attrib, $textarea->show(), array_merge(html::$common_attrib, array('action')));
     }
 
     public function detailview($attrib)
     {
         $attrib += array('id' => 'rcmkolabnotesdetailview');
         $this->rc->output->add_gui_object('notesdetailview', $attrib['id']);
         return html::div($attrib, '');
     }
 
     public function notetitle($attrib)
     {
         $attrib += array('id' => 'rcmkolabnotestitle');
         $this->rc->output->add_gui_object('noteviewtitle', $attrib['id']);
 
         $summary = new html_inputfield(array(
                 'name'     => 'summary',
                 'class'    => 'notetitle inline-edit form-control',
                 'size'     => 60,
                 'id'       => 'notetitleinput',
                 'tabindex' => 0
         ));
 
         $html = html::div('form-group row',
                 html::label(array('class' => 'col-sm-2 col-form-label', 'for' => 'notetitleinput'), $this->plugin->gettext('kolab_notes.title'))
                     . html::span('col-sm-10', $summary->show())
             )
             . html::div('form-group row',
                 html::label(array('class' => 'col-sm-2 col-form-label'), $this->plugin->gettext('kolab_notes.tags'))
                     . html::div(array('class' => 'tagline tagedit col-sm-10'), '&nbsp;')
             )
             . html::div(array('class' => 'dates text-only', 'style' => 'display:none'),
                 html::div('form-group row',
                     html::label(array('class' => 'col-sm-2 col-form-label'), $this->plugin->gettext('created'))
                     . html::span('col-sm-10', html::span('notecreated form-control-plaintext', ''))
                 )
                 . html::div('form-group row',
                     html::label(array('class' => 'col-sm-2 col-form-label'), $this->plugin->gettext('changed'))
                     . html::span('col-sm-10', html::span('notechanged form-control-plaintext', ''))
                 )
             );
 
         return html::div($attrib, $html);
     }
 
     public function attachments_list($attrib)
     {
         $attrib += array('id' => 'rcmkolabnotesattachmentslist');
         $this->rc->output->add_gui_object('notesattachmentslist', $attrib['id']);
         return html::tag('ul', $attrib, '', html::$common_attrib);
     }
 
     /**
      * Render create/edit form for notes lists (folders)
      */
     public function list_editform($action, $list, $folder)
     {
         $this->action = $action;
         $this->list   = $list;
         $this->folder = is_object($folder) ? $folder->name : ''; // UTF7;
 
         $this->rc->output->set_env('pagetitle', $this->plugin->gettext('arialabelnotebookform'));
         $this->rc->output->add_handler('folderform', array($this, 'notebookform'));
         $this->rc->output->send('libkolab.folderform');
     }
 
     /**
      * Render create/edit form for notes lists (folders)
      */
     public function notebookform($attrib)
     {
         $folder_name     = $this->folder;
         $hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name);
 
         $storage = $this->rc->get_storage();
         $delim   = $storage->get_hierarchy_delimiter();
         $form   = array();
 
         if (strlen($folder_name)) {
             $options = $storage->folder_info($folder_name);
 
             $path_imap = explode($delim, $folder_name);
             array_pop($path_imap);  // pop off name part
             $path_imap = implode($delim, $path_imap);
         }
         else {
             $path_imap = '';
             $options   = array();
         }
 
         // General tab
         $form['properties'] = array(
             'name'   => $this->rc->gettext('properties'),
             'fields' => array(),
         );
 
         // folder name (default field)
         $input_name = new html_inputfield(array('name' => 'name', 'id' => 'noteslist-name', 'size' => 20));
         $form['properties']['fields']['name'] = array(
             'label' => $this->plugin->gettext('listname'),
             'value' => $input_name->show($this->list['editname'], array('disabled' => ($options['norename'] || $options['protected']))),
             'id'    => 'noteslist-name',
         );
 
         // prevent user from moving folder
         if (!empty($options) && ($options['norename'] || $options['protected'])) {
             $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
         }
         else {
             $select = kolab_storage::folder_selector('note', array('name' => 'parent', 'id' => 'parent-folder'), $folder_name);
             $form['properties']['fields']['path'] = array(
                 'label' => $this->plugin->gettext('parentfolder'),
                 'value' => $select->show(strlen($folder_name) ? $path_imap : ''),
                 'id'    => 'parent-folder',
             );
         }
 
         $form_html = kolab_utils::folder_form($form, $folder_name, 'kolab_notes', $hidden_fields);
 
         return html::tag('form', $attrib + array('action' => '#', 'method' => 'post', 'id' => 'noteslistpropform'), $form_html);
     }
 }
diff --git a/plugins/kolab_tags/lib/kolab_tags_engine.php b/plugins/kolab_tags/lib/kolab_tags_engine.php
index 1c45040f..a350e6ee 100644
--- a/plugins/kolab_tags/lib/kolab_tags_engine.php
+++ b/plugins/kolab_tags/lib/kolab_tags_engine.php
@@ -1,540 +1,540 @@
 <?php
 
 /**
  * Kolab Tags engine
  *
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2014, Kolab Systems AG <contact@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_tags_engine
 {
     private $backend;
     private $plugin;
     private $rc;
 
     /**
      * Class constructor
      */
     public function __construct($plugin)
     {
         $plugin->require_plugin('libkolab');
 
         require_once $plugin->home . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'kolab_tags_backend.php';
 
         $this->backend = new kolab_tags_backend;
         $this->plugin  = $plugin;
         $this->rc      = $plugin->rc;
     }
 
     /**
      * User interface initialization
      */
     public function ui()
     {
         if ($this->rc->action && !in_array($this->rc->action, array('show', 'preview', 'dialog-ui'))) {
             return;
         }
 
         $this->plugin->add_texts('localization/');
 
         $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/style.css');
         $this->plugin->include_script('kolab_tags.js');
         $this->rc->output->add_label('cancel', 'save');
         $this->plugin->add_label('tags', 'add', 'edit', 'delete', 'saving',
             'nameempty', 'nameexists', 'colorinvalid', 'untag', 'tagname',
             'tagcolor', 'tagsearchnew', 'newtag', 'notags');
 
         $this->rc->output->add_handlers(array(
             'plugin.taglist' => array($this, 'taglist'),
         ));
 
         $ui = $this->rc->output->parse('kolab_tags.ui', false, false);
         $this->rc->output->add_footer($ui);
 
         // load miniColors and tagedit
         jqueryui::miniColors();
         jqueryui::tagedit();
 
         // Modify search filter (and set selected tags)
         if ($this->rc->task == 'mail' && ($this->rc->action == 'show' || !$this->rc->action)) {
             $this->search_filter_mods();
         }
     }
 
     /**
      * Engine actions handler (managing tag objects)
      */
     public function actions()
     {
         $this->plugin->add_texts('localization/');
 
         $action = rcube_utils::get_input_value('_act', rcube_utils::INPUT_POST);
 
         if ($action) {
             $this->{'action_' . $action}();
         }
         // manage tag objects
         else {
             $delete   = (array) rcube_utils::get_input_value('delete', rcube_utils::INPUT_POST);
             $update   = (array) rcube_utils::get_input_value('update', rcube_utils::INPUT_POST, true);
             $add      = (array) rcube_utils::get_input_value('add', rcube_utils::INPUT_POST, true);
             $response = array();
 
             // tags deletion
             foreach ($delete as $uid) {
                 if ($this->backend->remove($uid)) {
                     $response['delete'][] = $uid;
                 }
                 else {
                     $error = true;
                 }
             }
 
             // tags creation
             foreach ($add as $tag) {
                 if ($tag = $this->backend->create($tag)) {
                     $response['add'][] = $this->parse_tag($tag);
                 }
                 else {
                     $error = true;
                 }
             }
 
             // tags update
             foreach ($update as $tag) {
                 if ($this->backend->update($tag)) {
                     $response['update'][] = $this->parse_tag($tag);
                 }
                 else {
                     $error = true;
                 }
             }
 
             if (!empty($error)) {
                 $this->rc->output->show_message($this->plugin->gettext('updateerror'), 'error');
             }
             else {
                 $this->rc->output->show_message($this->plugin->gettext('updatesuccess'), 'confirmation');
             }
 
             $this->rc->output->command('plugin.kolab_tags', $response);
         }
 
 
         $this->rc->output->send();
     }
 
     /**
      * Remove tag from message(s)
      */
     public function action_remove()
     {
         $tag     = rcube_utils::get_input_value('_tag', rcube_utils::INPUT_POST);
         $filter  = $tag == '*' ? array() : array(array('uid', '=', explode(',', $tag)));
         $taglist = $this->backend->list_tags($filter);
         $filter  = array();
         $tags    = array();
 
         foreach (rcmail::get_uids() as $mbox => $uids) {
             if ($uids === '*') {
                 $filter[$mbox] = $this->build_member_url(array('folder' => $mbox));
             }
             else {
                 foreach ((array)$uids as $uid) {
                     $filter[$mbox][] = $this->build_member_url(array(
                             'folder' => $mbox,
                             'uid'    => $uid
                     ));
                 }
             }
         }
 
         // for every tag...
         foreach ($taglist as $tag) {
             $updated = false;
 
             // @todo: make sure members list is up-to-date (UIDs are up-to-date)
 
             // ...filter members by folder/uid prefix
             foreach ((array) $tag['members'] as $idx => $member) {
                 foreach ($filter as $members) {
                     // list of prefixes
                     if (is_array($members)) {
                         foreach ($members as $message) {
                             if ($member == $message || strpos($member, $message . '?') === 0) {
                                 unset($tag['members'][$idx]);
                                 $updated = true;
                             }
                         }
                     }
                     // one prefix (all messages in a folder)
                     else {
                         if (preg_match('/^' . preg_quote($members, '/') . '\/[0-9]+(\?|$)/', $member)) {
                             unset($tag['members'][$idx]);
                             $updated = true;
                         }
                     }
                 }
             }
 
             // update tag object
             if ($updated) {
                 if (!$this->backend->update($tag)) {
                     $error = true;
                 }
             }
 
             $tags[] = $tag['uid'];
         }
 
         if ($error) {
             if ($_POST['_from'] != 'show') {
                 $this->rc->output->show_message($this->plugin->gettext('untaggingerror'), 'error');
                 $this->rc->output->command('list_mailbox');
             }
         }
         else {
             $this->rc->output->show_message($this->plugin->gettext('untaggingsuccess'), 'confirmation');
             $this->rc->output->command('plugin.kolab_tags', array('mark' => 1, 'delete' => $tags));
         }
     }
 
     /**
      * Add tag to message(s)
      */
     public function action_add()
     {
         $tag     = rcube_utils::get_input_value('_tag', rcube_utils::INPUT_POST);
         $storage = $this->rc->get_storage();
         $members = array();
 
         // build list of members
         foreach (rcmail::get_uids() as $mbox => $uids) {
             if ($uids === '*') {
                 $index = $storage->index($mbox, null, null, true);
                 $uids  = $index->get();
                 $msgs  = $storage->fetch_headers($mbox, $uids, false);
             }
             else {
                 $msgs = $storage->fetch_headers($mbox, $uids, false);
             }
 
             $members = array_merge($members, $this->build_members($mbox, $msgs));
         }
 
         // create a new tag?
         if (!empty($_POST['_new'])) {
             $object = array(
                 'name'    => $tag,
                 'members' => $members,
             );
 
             $object = $this->backend->create($object);
             $error  = $object === false;
         }
         // use existing tags (by UID)
         else {
             $filter  = array(array('uid', '=', explode(',', $tag)));
             $taglist = $this->backend->list_tags($filter);
 
             // for every tag...
             foreach ($taglist as $tag) {
                 $tag['members'] = array_unique(array_merge((array) $tag['members'], $members));
 
                 // update tag object
                 if (!$this->backend->update($tag)) {
                     $error = true;
                 }
             }
         }
 
         if ($error) {
             $this->rc->output->show_message($this->plugin->gettext('taggingerror'), 'error');
 
             if ($_POST['_from'] != 'show') {
                 $this->rc->output->command('list_mailbox');
             }
         }
         else {
             $this->rc->output->show_message($this->plugin->gettext('taggingsuccess'), 'confirmation');
 
             if (isset($object)) {
                 $this->rc->output->command('plugin.kolab_tags', array('mark' => 1, 'add' => array($this->parse_tag($object))));
             }
         }
     }
 
     /**
      * Refresh tags list
      */
     public function action_refresh()
     {
         $taglist = $this->backend->list_tags();
         $taglist = array_map(array($this, 'parse_tag'), $taglist);
 
         $this->rc->output->set_env('tags', $taglist);
         $this->rc->output->command('plugin.kolab_tags', array('refresh' => 1));
     }
 
     /**
      * Template object building tags list/cloud
      */
     public function taglist($attrib)
     {
         $taglist = $this->backend->list_tags();
 
         // Performance: Save the list for later
         if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
             $this->taglist = $taglist;
         }
 
         $taglist = array_map(array($this, 'parse_tag'), $taglist);
 
         $this->rc->output->set_env('tags', $taglist);
         $this->rc->output->add_gui_object('taglist', $attrib['id']);
 
         return html::tag('ul', $attrib, '', html::$common_attrib);
     }
 
     /**
      * Handler for messages list (add tag-boxes in subject line on the list)
      */
     public function messages_list_handler($args)
     {
         if (empty($args['messages'])) {
             return;
         }
 
         // get tags list
         $taglist = $this->backend->list_tags();
 
         // get message UIDs
         foreach ($args['messages'] as $msg) {
             $message_tags[$msg->uid . '-' . $msg->folder] = null;
         }
 
         $uids = array_keys($message_tags);
 
         foreach ($taglist as $tag) {
             $tag = $this->parse_tag($tag, true);
 
             foreach ((array) $tag['uids'] as $folder => $_uids) {
                 array_walk($_uids, function(&$uid, $key, $folder) { $uid .= '-' . $folder; }, $folder);
 
                 foreach (array_intersect($uids, $_uids) as $uid) {
                     $message_tags[$uid][] = $tag['uid'];
                 }
             }
         }
 
         $this->rc->output->set_env('message_tags', array_filter($message_tags));
 
         // @TODO: tag counters for the whole folder (search result)
 
         return $args;
     }
 
     /**
      * Handler for a single message (add tag-boxes in subject line)
      */
     public function message_headers_handler($args)
     {
         $taglist = $this->taglist ?: $this->backend->list_tags();
         $uid     = $args['uid'];
         $folder  = $args['folder'];
         $tags    = array();
 
         foreach ($taglist as $tag) {
             $tag = $this->parse_tag($tag, true, false);
             if (in_array($uid, (array)$tag['uids'][$folder])) {
                 unset($tag['uids']);
                 $tags[] = $tag;
             }
         }
 
         if (!empty($tags)) {
             $this->rc->output->set_env('message_tags', $tags);
         }
 
         return $args;
     }
 
     /**
      * Handler for messages searching requests
      */
     public function imap_search_handler($args)
     {
         if (empty($args['search_tags'])) {
             return $args;
         }
 
         // we'll reset to current folder to fix issues when searching in multi-folder mode
         $storage     = $this->rc->get_storage();
         $orig_folder = $storage->get_folder();
 
         // get tags
         $tags = $this->backend->list_tags(array(array('uid', '=', $args['search_tags'])));
 
         // sanity check (that should not happen)
         if (empty($tags)) {
             if ($orig_folder) {
                 $storage->set_folder($orig_folder);
             }
 
             return $args;
         }
 
         $search  = array();
         $folders = (array) $args['folder'];
 
         // collect folders and uids
         foreach ($tags as $tag) {
             $tag = $this->parse_tag($tag, true);
 
             // tag has no members -> empty search result
             if (empty($tag['uids'])) {
                 goto empty_result;
             }
 
             foreach ($tag['uids'] as $folder => $uid_list) {
                 $search[$folder] = array_merge((array)$search[$folder], $uid_list);
             }
         }
 
         $search   = array_map('array_unique', $search);
         $criteria = array();
 
         // modify search folders/criteria
         $args['folder'] = array_intersect($folders, array_keys($search));
 
         foreach ($args['folder'] as $folder) {
             $criteria[$folder] = ($args['search'] != 'ALL' ? trim($args['search']).' ' : '')
                 . 'UID ' . rcube_imap_generic::compressMessageSet($search[$folder]);
         }
 
         if (!empty($args['folder'])) {
             $args['search'] = $criteria;
         }
         else {
             // return empty result
             empty_result:
 
             if (count($folders) > 1) {
                 $args['result'] = new rcube_result_multifolder($args['folder']);
                 foreach ($args['folder'] as $folder) {
                     $index = new rcube_result_index($folder, '* SORT');
                     $args['result']->add($index);
                 }
             }
             else {
                 $class  = 'rcube_result_' . ($args['threading'] ? 'thread' : 'index');
                 $result = $args['threading'] ? '* THREAD' : '* SORT';
 
                 $args['result'] = new $class($folder, $result);
             }
         }
 
         if ($orig_folder) {
             $storage->set_folder($orig_folder);
         }
 
         return $args;
     }
 
     /**
      * Get selected tags when in search-mode
      */
     protected function search_filter_mods()
     {
        if (!empty($_REQUEST['_search']) && !empty($_SESSION['search'])
             && $_SESSION['search_request'] == $_REQUEST['_search']
             && ($filter = $_SESSION['search_filter'])
        ) {
             if (preg_match('/^(kolab_tags_[0-9]{10,}:([^:]+):)/', $filter, $m)) {
                 $search_tags   = explode(',', $m[2]);
                 $search_filter = substr($filter, strlen($m[1]));
 
                 // send current search properties to the browser
                 $this->rc->output->set_env('search_filter_selected', $search_filter);
                 $this->rc->output->set_env('selected_tags', $search_tags);
             }
         }
     }
 
     /**
      * "Convert" tag object to simple array for use in javascript
      */
     private function parse_tag($tag, $list = false, $force = true)
     {
         $result = array(
             'uid'   => $tag['uid'],
             'name'  => $tag['name'],
-            'color' => $tag['color'],
+            'color' => $tag['color'] ?? null,
         );
 
         if ($list) {
             $result['uids'] = $this->get_tag_messages($tag, $force);
         }
 
         return $result;
     }
 
     /**
      * Resolve members to folder/UID
      *
      * @param array $tag Tag object
      *
      * @return array Folder/UID list
      */
     protected function get_tag_messages(&$tag, $force = true)
     {
         return kolab_storage_config::resolve_members($tag, $force);
     }
 
     /**
      * Build array of member URIs from set of messages
      */
     protected function build_members($folder, $messages)
     {
         return kolab_storage_config::build_members($folder, $messages);
     }
 
     /**
      * Parses tag member string
      *
      * @param string $url Member URI
      *
      * @return array Message folder, UID, Search headers (Message-Id, Date)
      */
     protected function parse_member_url($url)
     {
         return kolab_storage_config::parse_member_url($url);
     }
 
     /**
      * Builds member URI
      *
      * @param array Message folder, UID, Search headers (Message-Id, Date)
      *
      * @return string $url Member URI
      */
     protected function build_member_url($params)
     {
         return kolab_storage_config::build_member_url($params);
     }
 }
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index dbb0066c..aad7ce97 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -1,1605 +1,1605 @@
 <?php
 
 /**
  * Library providing common functions for calendaring plugins
  *
  * Provides utility functions for calendar-related modules such as
  * - alarms display and dismissal
  * - attachment handling
  * - recurrence computation and UI elements
  * - ical parsing and exporting
  * - itip scheduling protocol
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
  * published by the Free Software Foundation, either version 3 of the
  * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  * GNU Affero General Public License for more details.
  *
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
 class libcalendaring extends rcube_plugin
 {
     public $rc;
     public $timezone;
     public $gmt_offset;
     public $dst_active;
     public $timezone_offset;
     public $ical_parts = [];
     public $ical_message;
 
     public $defaults = array(
         'calendar_date_format'  => "Y-m-d",
         'calendar_date_short'   => "M-j",
         'calendar_date_long'    => "F j Y",
         'calendar_date_agenda'  => "l M-d",
         'calendar_time_format'  => "H:m",
         'calendar_first_day'    => 1,
         'calendar_first_hour'   => 6,
         'calendar_date_format_sets' => array(
             'Y-m-d' => array('d M Y',   'm-d',  'l m-d'),
             'Y/m/d' => array('d M Y',   'm/d',  'l m/d'),
             'Y.m.d' => array('d M Y',   'm.d',  'l m.d'),
             'd-m-Y' => array('d M Y',   'd-m',  'l d-m'),
             'd/m/Y' => array('d M Y',   'd/m',  'l d/m'),
             'd.m.Y' => array('d M Y',  'd.m',  'l d.m'),
             'j.n.Y' => array('d M Y',  'd.m',  'l d.m'),
             'm/d/Y' => array('M d Y',   'm/d',  'l m/d'),
         ),
     );
 
     private static $instance;
 
     private $mail_ical_parser;
 
     /**
      * Singleton getter to allow direct access from other plugins
      */
     public static function get_instance()
     {
         if (!self::$instance) {
             self::$instance = new libcalendaring(rcube::get_instance()->plugins);
             self::$instance->init_instance();
         }
 
         return self::$instance;
     }
 
     /**
      * Initializes class properties
      */
     public function init_instance()
     {
         $this->rc = rcube::get_instance();
 
         // set user's timezone
         try {
             $this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
         }
         catch (Exception $e) {
             $this->timezone = new DateTimeZone('GMT');
         }
 
         $now = new DateTime('now', $this->timezone);
 
         $this->gmt_offset      = $now->getOffset();
         $this->dst_active      = $now->format('I');
         $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
 
         $this->add_texts('localization/', false);
     }
 
     /**
      * Required plugin startup method
      */
     public function init()
     {
         // extend include path to load bundled lib classes
         $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
         set_include_path($include_path);
 
         self::$instance = $this;
 
         $this->rc = rcube::get_instance();
         $this->init_instance();
 
         // include client scripts and styles
         if ($this->rc->output) {
             // add hook to display alarms
             $this->add_hook('refresh', array($this, 'refresh'));
             $this->register_action('plugin.alarms', array($this, 'alarms_action'));
             $this->register_action('plugin.expand_attendee_group', array($this, 'expand_attendee_group'));
         }
 
         // proceed initialization in startup hook
         $this->add_hook('startup', array($this, 'startup'));
     }
 
     /**
      * Startup hook
      */
     public function startup($args)
     {
         if ($this->rc->output && $this->rc->output->type == 'html') {
             $this->rc->output->set_env('libcal_settings', $this->load_settings());
             $this->include_script('libcalendaring.js');
             $this->include_stylesheet($this->local_skin_path() . '/libcal.css');
 
             $this->add_label(
                 'itipaccepted', 'itiptentative', 'itipdeclined',
                 'itipdelegated', 'expandattendeegroup', 'expandattendeegroupnodata',
                 'statusorganizer', 'statusaccepted', 'statusdeclined',
                 'statusdelegated', 'statusunknown', 'statusneeds-action',
                 'statustentative', 'statuscompleted', 'statusin-process',
                 'delegatedto', 'delegatedfrom', 'showmore'
             );
         }
 
-        if ($args['task'] == 'mail') {
+        if (($args['task'] ?? null) == 'mail') {
             if ($args['action'] == 'show' || $args['action'] == 'preview') {
                 $this->add_hook('message_load', array($this, 'mail_message_load'));
             }
         }
     }
 
     /**
      * Load iCalendar functions
      */
     public static function get_ical()
     {
         $self = self::get_instance();
         return new libcalendaring_vcalendar();
     }
 
     /**
      * Load iTip functions
      */
     public static function get_itip($domain = 'libcalendaring')
     {
         $self = self::get_instance();
         return new libcalendaring_itip($self, $domain);
     }
 
     /**
      * Load recurrence computation engine
      */
     public static function get_recurrence($object = null)
     {
         $self = self::get_instance();
         return new libcalendaring_recurrence($self, $object);
     }
 
     /**
      * Shift dates into user's current timezone
      *
      * @param mixed Any kind of a date representation (DateTime object, string or unix timestamp)
      *
      * @return object DateTime object in user's timezone
      */
     public function adjust_timezone($dt, $dateonly = false)
     {
         if (is_numeric($dt)) {
             $dt = new DateTime('@'.$dt);
         }
         else if (is_string($dt)) {
             $dt = rcube_utils::anytodatetime($dt);
         }
 
         if ($dt instanceof DateTimeInterface && empty($dt->_dateonly) && !$dateonly) {
             $dt = $dt->setTimezone($this->timezone);
         }
 
         return $dt;
     }
 
     /**
      *
      */
     public function load_settings()
     {
         $this->date_format_defaults();
 
         $settings = array();
         $keys     = array('date_format', 'time_format', 'date_short', 'date_long', 'date_agenda');
 
         foreach ($keys as $key) {
             $settings[$key] = (string)$this->rc->config->get('calendar_' . $key, $this->defaults['calendar_' . $key]);
             $settings[$key] = self::from_php_date_format($settings[$key]);
         }
 
         $settings['dates_long']  = $settings['date_long'];
         $settings['first_day']   = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
         $settings['timezone']    = $this->timezone_offset;
         $settings['dst']         = $this->dst_active;
 
         // localization
         $settings['days'] = array(
             $this->rc->gettext('sunday'),   $this->rc->gettext('monday'),
             $this->rc->gettext('tuesday'),  $this->rc->gettext('wednesday'),
             $this->rc->gettext('thursday'), $this->rc->gettext('friday'),
             $this->rc->gettext('saturday')
         );
         $settings['days_short'] = array(
             $this->rc->gettext('sun'), $this->rc->gettext('mon'),
             $this->rc->gettext('tue'), $this->rc->gettext('wed'),
             $this->rc->gettext('thu'), $this->rc->gettext('fri'),
             $this->rc->gettext('sat')
         );
         $settings['months'] = array(
             $this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
             $this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
             $this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
             $this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
             $this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
             $this->rc->gettext('longnov'), $this->rc->gettext('longdec')
         );
         $settings['months_short'] = array(
             $this->rc->gettext('jan'), $this->rc->gettext('feb'),
             $this->rc->gettext('mar'), $this->rc->gettext('apr'),
             $this->rc->gettext('may'), $this->rc->gettext('jun'),
             $this->rc->gettext('jul'), $this->rc->gettext('aug'),
             $this->rc->gettext('sep'), $this->rc->gettext('oct'),
             $this->rc->gettext('nov'), $this->rc->gettext('dec')
         );
         $settings['today'] = $this->rc->gettext('today');
 
         return $settings;
     }
 
 
     /**
      * Helper function to set date/time format according to config and user preferences
      */
     private function date_format_defaults()
     {
         static $defaults = array();
 
         // nothing to be done
         if (isset($defaults['date_format']))
           return;
 
         $defaults['date_format'] = $this->rc->config->get('calendar_date_format', $this->rc->config->get('date_format'));
         $defaults['time_format'] = $this->rc->config->get('calendar_time_format', $this->rc->config->get('time_format'));
 
         // override defaults
         if ($defaults['date_format'])
             $this->defaults['calendar_date_format'] = $defaults['date_format'];
         if ($defaults['time_format'])
             $this->defaults['calendar_time_format'] = $defaults['time_format'];
 
         // derive format variants from basic date format
         $format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
         if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
             $this->defaults['calendar_date_long'] = $format_set[0];
             $this->defaults['calendar_date_short'] = $format_set[1];
             $this->defaults['calendar_date_agenda'] = $format_set[2];
         }
     }
 
     /**
      * Compose a date string for the given event
      */
     public function event_date_text($event)
     {
         $fromto  = '--';
         $is_task = !empty($event['_type']) && $event['_type'] == 'task';
 
         $this->date_format_defaults();
 
         $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
         $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
 
         $getTimezone = function ($date) {
             if ($newTz = $date->getTimezone()) {
                 return $newTz->getName();
             }
 
             return '';
         };
 
         $formatDate = function ($date, $format) use ($getTimezone) {
             // This is a workaround for the rcmail::format_date() which does not play nice with timezone
             $tz = $this->rc->config->get('timezone');
             if ($dateTz = $getTimezone($date)) {
                 $this->rc->config->set('timezone', $dateTz);
             }
             $result = $this->rc->format_date($date, $format);
             $this->rc->config->set('timezone', $tz);
 
             return $result;
         };
 
         // handle task objects
         if ($is_task && !empty($event['due']) && is_object($event['due'])) {
             $fromto = $formatDate($event['due'], !empty($event['due']->_dateonly) ? $date_format : null);
 
             // add timezone information
             if ($fromto && empty($event['due']->_dateonly) && ($tz = $getTimezone($event['due']))) {
                 $fromto .= ' (' . strtr($tz, '_', ' ') . ')';
             }
 
             return $fromto;
         }
 
         // abort if no valid event dates are given
         if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
             return $fromto;
         }
 
         if ($event['allday']) {
             $fromto = $formatDate($event['start'], $date_format);
             if (($todate = $formatDate($event['end'], $date_format)) != $fromto) {
                 $fromto .= ' - ' . $todate;
             }
         }
         else if ($event['start']->format('Ymd') === $event['end']->format('Ymd')) {
             $fromto = $formatDate($event['start'], $date_format) . ' ' . $formatDate($event['start'], $time_format) .
                 ' - ' . $formatDate($event['end'], $time_format);
         }
         else {
             $fromto = $formatDate($event['start'], $date_format) . ' ' . $formatDate($event['start'], $time_format) .
                 ' - ' . $formatDate($event['end'], $date_format) . ' ' . $formatDate($event['end'], $time_format);
         }
 
         // add timezone information
         if ($fromto && empty($event['allday']) && ($tz = $getTimezone($event['start']))) {
             $fromto .= ' (' . strtr($tz, '_', ' ') . ')';
         }
 
         return $fromto;
     }
 
 
     /**
      * Render HTML form for alarm configuration
      */
     public function alarm_select($attrib, $alarm_types, $absolute_time = true)
     {
         unset($attrib['name']);
 
         $input_value    = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value form-control', 'size' => 3));
         $input_date     = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date form-control', 'size' => 10));
         $input_time     = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time form-control', 'size' => 6));
         $select_type    = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type form-control', 'id' => $attrib['id']));
         $select_offset  = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset form-control'));
         $select_related = new html_select(array('name' => 'alarmrelated[]', 'class' => 'edit-alarm-related form-control'));
         $object_type    = !empty($attrib['_type']) ? $attrib['_type'] : 'event';
 
         $select_type->add($this->gettext('none'), '');
         foreach ($alarm_types as $type) {
             $select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
         }
 
         foreach (array('-M','-H','-D','+M','+H','+D') as $trigger) {
             $select_offset->add($this->gettext('trigger' . $trigger), $trigger);
         }
 
         $select_offset->add($this->gettext('trigger0'), '0');
         if ($absolute_time) {
             $select_offset->add($this->gettext('trigger@'), '@');
         }
 
         $select_related->add($this->gettext('relatedstart'), 'start');
         $select_related->add($this->gettext('relatedend' . $object_type), 'end');
 
         // pre-set with default values from user settings
         $preset = self::parse_alarm_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
         $hidden = array('style' => 'display:none');
 
         return html::span('edit-alarm-set',
             $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
             html::span(array('class' => 'edit-alarm-values input-group', 'style' => 'display:none'),
                 $input_value->show($preset[0]) . ' ' .
                 $select_offset->show($preset[1]) . ' ' .
                 $select_related->show() . ' ' .
                 $input_date->show('', $hidden) . ' ' .
                 $input_time->show('', $hidden)
             )
         );
     }
 
     /**
      * Get a list of email addresses of the given user (from login and identities)
      *
      * @param string User Email (default to current user)
      *
      * @return array Email addresses related to the user
      */
     public function get_user_emails($user = null)
     {
         static $_emails = array();
 
         if (empty($user)) {
             $user = $this->rc->user->get_username();
         }
 
         // return cached result
         if (isset($_emails[$user])) {
             return $_emails[$user];
         }
 
         $emails = array($user);
         $plugin = $this->rc->plugins->exec_hook('calendar_user_emails', array('emails' => $emails));
         $emails = array_map('strtolower', $plugin['emails']);
 
         // add all emails from the current user's identities
         if (!$plugin['abort'] && ($user == $this->rc->user->get_username())) {
             foreach ($this->rc->user->list_emails() as $identity) {
                 $emails[] = strtolower($identity['email']);
             }
         }
 
         $_emails[$user] = array_unique($emails);
         return $_emails[$user];
     }
 
     /**
      * Set the given participant status to the attendee matching the current user's identities
      * Unsets 'rsvp' flag too.
      *
      * @param array  &$event    Event data
      * @param string $status    The PARTSTAT value to set
      * @param bool   $recursive Recurive call
      *
      * @return mixed Email address of the updated attendee or False if none matching found
      */
     public function set_partstat(&$event, $status, $recursive = true)
     {
         $success = false;
         $emails = $this->get_user_emails();
         foreach ((array)$event['attendees'] as $i => $attendee) {
             if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
                 $event['attendees'][$i]['status'] = strtoupper($status);
                 unset($event['attendees'][$i]['rsvp']);
                 $success = $attendee['email'];
             }
         }
 
         // apply partstat update to each existing exception
         if ($event['recurrence'] && is_array($event['recurrence']['EXCEPTIONS'])) {
             foreach ($event['recurrence']['EXCEPTIONS'] as $i => $exception) {
                 $this->set_partstat($event['recurrence']['EXCEPTIONS'][$i], $status, false);
             }
 
             // set link to top-level exceptions
             $event['exceptions'] = &$event['recurrence']['EXCEPTIONS'];
         }
 
         return $success;
     }
 
 
     /*********  Alarms handling  *********/
 
     /**
      * Helper function to convert alarm trigger strings
      * into two-field values (e.g. "-45M" => 45, "-M")
      */
     public static function parse_alarm_value($val)
     {
         if ($val[0] == '@') {
             return array(new DateTime($val));
         }
         else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) {
             if ($m[1] == '')
                 $m[1] = '+';
             foreach ($m2 as $seg) {
                 $prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT';
                 if ($seg[1] > 0) {  // ignore zero values
                     // convert seconds to minutes
                     if ($seg[2] == 'S') {
                         $seg[2] = 'M';
                         $seg[1] = max(1, round($seg[1]/60));
                     }
 
                     return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
                 }
             }
 
             // return zero value nevertheless
             return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]);
         }
 
         return false;
     }
 
     /**
      * Convert the alarms list items to be processed on the client
      */
     public static function to_client_alarms($valarms)
     {
         return array_map(function($alarm) {
             if ($alarm['trigger'] instanceof DateTimeInterface) {
                 $alarm['trigger'] = '@' . $alarm['trigger']->format('U');
             }
             else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
                 $alarm['trigger'] = $trigger[2];
             }
             return $alarm;
         }, (array)$valarms);
     }
 
     /**
      * Process the alarms values submitted by the client
      */
     public static function from_client_alarms($valarms)
     {
         return array_map(function($alarm){
             if ($alarm['trigger'][0] == '@') {
                 try {
                     $alarm['trigger'] = new DateTime($alarm['trigger']);
                     $alarm['trigger']->setTimezone(new DateTimeZone('UTC'));
                 }
                 catch (Exception $e) { /* handle this ? */ }
             }
             else if ($trigger = libcalendaring::parse_alarm_value($alarm['trigger'])) {
                 $alarm['trigger'] = $trigger[3];
             }
             return $alarm;
         }, (array)$valarms);
     }
 
     /**
      * Render localized text for alarm settings
      */
     public static function alarms_text($alarms)
     {
         if (is_array($alarms) && is_array($alarms[0])) {
             $texts = array();
             foreach ($alarms as $alarm) {
                 if ($text = self::alarm_text($alarm))
                     $texts[] = $text;
             }
 
             return join(', ', $texts);
         }
         else {
             return self::alarm_text($alarms);
         }
     }
 
     /**
      * Render localized text for a single alarm property
      */
     public static function alarm_text($alarm)
     {
         $related = null;
 
         if (is_string($alarm)) {
             list($trigger, $action) = explode(':', $alarm);
         }
         else {
             $trigger = $alarm['trigger'];
             $action  = $alarm['action'];
 
             if (!empty($alarm['related'])) {
                 $related = $alarm['related'];
             }
         }
 
         $text  = '';
         $rcube = rcube::get_instance();
 
         switch ($action) {
         case 'EMAIL':
             $text = $rcube->gettext('libcalendaring.alarmemail');
             break;
         case 'DISPLAY':
             $text = $rcube->gettext('libcalendaring.alarmdisplay');
             break;
         case 'AUDIO':
             $text = $rcube->gettext('libcalendaring.alarmaudio');
             break;
         }
 
         if ($trigger instanceof DateTimeInterface) {
             $text .= ' ' . $rcube->gettext(array(
                 'name' => 'libcalendaring.alarmat',
                 'vars' => array('datetime' => $rcube->format_date($trigger))
             ));
         }
         else if (preg_match('/@(\d+)/', $trigger, $m)) {
             $text .= ' ' . $rcube->gettext(array(
                 'name' => 'libcalendaring.alarmat',
                 'vars' => array('datetime' => $rcube->format_date($m[1]))
             ));
         }
         else if ($val = self::parse_alarm_value($trigger)) {
             $r = $related && strtoupper($related) == 'END' ? 'end' : '';
             // TODO: for all-day events say 'on date of event at XX' ?
             if ($val[0] == 0) {
                 $text .= ' ' . $rcube->gettext('libcalendaring.triggerattime' . $r);
             }
             else {
                 $label = 'libcalendaring.trigger' . $r . $val[1];
                 $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext($label);
             }
         }
         else {
             return false;
         }
 
         return $text;
     }
 
     /**
      * Get the next alarm (time & action) for the given event
      *
      * @param array Record data
      * @return array Hash array with alarm time/type or null if no alarms are configured
      */
     public static function get_next_alarm($rec, $type = 'event')
     {
         if (
             (empty($rec['valarms']) && empty($rec['alarms']))
             || !empty($rec['cancelled'])
             || (!empty($rec['status']) && $rec['status'] == 'CANCELLED')
         ) {
             return null;
         }
 
         if ($type == 'task') {
             $timezone = self::get_instance()->timezone;
             if (!empty($rec['startdate'])) {
                 $time = !empty($rec['starttime']) ? $rec['starttime'] : '12:00';
                 $rec['start'] = new DateTime($rec['startdate'] . ' ' . $time, $timezone);
             }
             if (!empty($rec['date'])) {
                 $time = !empty($rec['time']) ? $rec['time'] : '12:00';
                 $rec[!empty($rec['start']) ? 'end' : 'start'] = new DateTime($rec['date'] . ' ' . $time, $timezone);
             }
         }
 
         if (empty($rec['end'])) {
             $rec['end'] = $rec['start'];
         }
 
         // support legacy format
         if (empty($rec['valarms'])) {
             list($trigger, $action) = explode(':', $rec['alarms'], 2);
             if ($alarm = self::parse_alarm_value($trigger)) {
                 $rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0]));
             }
         }
 
         // alarm ID eq. record ID by default to keep backwards compatibility
         $alarm_id   = isset($rec['id']) ? $rec['id'] : null;
         $alarm_prop = null;
         $expires    = new DateTime('now - 12 hours');
         $notify_at  = null;
 
         // handle multiple alarms
         foreach ($rec['valarms'] as $alarm) {
             $notify_time = null;
 
             if ($alarm['trigger'] instanceof DateTimeInterface) {
                 $notify_time = $alarm['trigger'];
             }
             else if (is_string($alarm['trigger'])) {
                 $refdate = !empty($alarm['related']) && $alarm['related'] == 'END' ? $rec['end'] : $rec['start'];
 
                 // abort if no reference date is available to compute notification time
                 if (!is_a($refdate, 'DateTime')) {
                     continue;
                 }
 
                 // TODO: for all-day events, take start @ 00:00 as reference date ?
 
                 try {
                     $interval = new DateInterval(trim($alarm['trigger'], '+-'));
                     $interval->invert = $alarm['trigger'][0] == '-';
                     $notify_time = clone $refdate;
                     $notify_time->add($interval);
                 }
                 catch (Exception $e) {
                     rcube::raise_error($e, true);
                     continue;
                 }
             }
 
             if ($notify_time && (!$notify_at || ($notify_time > $notify_at && $notify_time > $expires))) {
                 $notify_at  = $notify_time;
                 $action     = isset($alarm['action']) ? $alarm['action'] : null;
                 $alarm_prop = $alarm;
 
                 // generate a unique alarm ID if multiple alarms are set
                 if (count($rec['valarms']) > 1) {
                     $rec_id = substr(md5(isset($rec['id']) ? $rec['id'] : 'none'), 0, 16);
                     $alarm_id = $rec_id . '-' . $notify_at->format('Ymd\THis');
                 }
             }
         }
 
         return !$notify_at ? null : array(
             'time'   => $notify_at->format('U'),
             'action' => !empty($action) ? strtoupper($action) : 'DISPLAY',
             'id'     => $alarm_id,
             'prop'   => $alarm_prop,
         );
     }
 
     /**
      * Handler for keep-alive requests
      * This will check for pending notifications and pass them to the client
      */
     public function refresh($attr)
     {
         // collect pending alarms from all providers (e.g. calendar, tasks)
         $plugin = $this->rc->plugins->exec_hook('pending_alarms', array(
             'time' => time(),
             'alarms' => array(),
         ));
 
         if (!$plugin['abort'] && !empty($plugin['alarms'])) {
             // make sure texts and env vars are available on client
             $this->add_texts('localization/', true);
             $this->rc->output->add_label('close');
             $this->rc->output->set_env('snooze_select', $this->snooze_select());
             $this->rc->output->command('plugin.display_alarms', $this->_alarms_output($plugin['alarms']));
         }
     }
 
     /**
      * Handler for alarm dismiss/snooze requests
      */
     public function alarms_action()
     {
 //        $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
         $data  = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
 
         $data['ids'] = explode(',', $data['id']);
         $plugin = $this->rc->plugins->exec_hook('dismiss_alarms', $data);
 
         if (!empty($plugin['success'])) {
             $this->rc->output->show_message('successfullysaved', 'confirmation');
         }
         else {
             $this->rc->output->show_message('calendar.errorsaving', 'error');
         }
     }
 
     /**
      * Generate reduced and streamlined output for pending alarms
      */
     private function _alarms_output($alarms)
     {
         $out = array();
         foreach ($alarms as $alarm) {
             $out[] = array(
                 'id'       => $alarm['id'],
                 'start'    => !empty($alarm['start']) ? $this->adjust_timezone($alarm['start'])->format('c') : '',
                 'end'      => !empty($alarm['end'])? $this->adjust_timezone($alarm['end'])->format('c') : '',
                 'allDay'   => !empty($alarm['allday']),
                 'action'   => $alarm['action'],
                 'title'    => $alarm['title'],
                 'location' => $alarm['location'],
                 'calendar' => $alarm['calendar'],
             );
         }
 
         return $out;
     }
 
     /**
      * Render a dropdown menu to choose snooze time
      */
     private function snooze_select($attrib = array())
     {
         $steps = array(
              5 => 'repeatinmin',
             10 => 'repeatinmin',
             15 => 'repeatinmin',
             20 => 'repeatinmin',
             30 => 'repeatinmin',
             60 => 'repeatinhr',
             120 => 'repeatinhrs',
             1440 => 'repeattomorrow',
             10080 => 'repeatinweek',
         );
 
         $items = array();
         foreach ($steps as $n => $label) {
             $items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
                 $this->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
         }
 
         return html::tag('ul', $attrib + array('class' => 'toolbarmenu menu'), join("\n", $items), html::$common_attrib);
     }
 
 
     /*********  Recurrence rules handling ********/
 
     /**
      * Render localized text describing the recurrence rule of an event
      */
     public function recurrence_text($rrule)
     {
         $limit     = 10;
         $exdates   = array();
         $format    = $this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
         $format    = self::to_php_date_format($format);
         $format_fn = function($dt) use ($format) {
             return rcmail::get_instance()->format_date($dt, $format);
         };
 
         if (!empty($rrule['EXDATE']) && is_array($rrule['EXDATE'])) {
             $exdates = array_map($format_fn, $rrule['EXDATE']);
         }
 
         if (empty($rrule['FREQ']) && !empty($rrule['RDATE'])) {
             $rdates = array_map($format_fn, $rrule['RDATE']);
             $more   = false;
 
             if (!empty($exdates)) {
                 $rdates = array_diff($rdates, $exdates);
             }
 
             if (count($rdates) > $limit) {
                 $rdates = array_slice($rdates, 0, $limit);
                 $more   = true;
             }
 
             return $this->gettext('ondate') . ' ' . join(', ', $rdates) . ($more ? '...' : '');
         }
 
         $output  = sprintf('%s %d ', $this->gettext('every'), $rrule['INTERVAL'] ?: 1);
 
         switch ($rrule['FREQ']) {
         case 'DAILY':
             $output .= $this->gettext('days');
             break;
         case 'WEEKLY':
             $output .= $this->gettext('weeks');
             break;
         case 'MONTHLY':
             $output .= $this->gettext('months');
             break;
         case 'YEARLY':
             $output .= $this->gettext('years');
             break;
         }
 
         if (!empty($rrule['COUNT'])) {
             $until = $this->gettext(array('name' => 'forntimes', 'vars' => array('nr' => $rrule['COUNT'])));
         }
         else if (!empty($rrule['UNTIL'])) {
             $until = $this->gettext('recurrencend') . ' ' . $this->rc->format_date($rrule['UNTIL'], $format);
         }
         else {
             $until = $this->gettext('forever');
         }
 
         $output .= ', ' . $until;
 
         if (!empty($exdates)) {
             $more = false;
             if (count($exdates) > $limit) {
                 $exdates = array_slice($exdates, 0, $limit);
                 $more    = true;
             }
 
             $output  .= '; ' . $this->gettext('except') . ' ' . join(', ', $exdates) . ($more ? '...' : '');
         }
 
         return $output;
     }
 
     /**
      * Generate the form for recurrence settings
      */
     public function recurrence_form($attrib = array())
     {
         switch ($attrib['part']) {
             // frequency selector
             case 'frequency':
                 $select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency', 'class' => 'form-control'));
                 $select->add($this->gettext('never'),   '');
                 $select->add($this->gettext('daily'),   'DAILY');
                 $select->add($this->gettext('weekly'),  'WEEKLY');
                 $select->add($this->gettext('monthly'), 'MONTHLY');
                 $select->add($this->gettext('yearly'),  'YEARLY');
                 $select->add($this->gettext('rdate'),   'RDATE');
                 $html = html::label(array('for' => 'edit-recurrence-frequency', 'class' => 'col-form-label col-sm-2'), $this->gettext('frequency'))
                     . html::div('col-sm-10', $select->show(''));
                 break;
 
             // daily recurrence
             case 'daily':
                 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-daily'));
                 $html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-daily', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
                     . html::div('col-sm-10 input-group', $select->show(1) . html::span('label-after input-group-append', html::span('input-group-text', $this->gettext('days')))));
                 break;
 
             // weekly recurrence form
             case 'weekly':
                 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-weekly'));
                 $html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-weekly', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
                     . html::div('col-sm-10 input-group', $select->show(1) . html::span('label-after input-group-append', html::span('input-group-text', $this->gettext('weeks')))));
 
                 // weekday selection
                 $daymap   = array('sun','mon','tue','wed','thu','fri','sat');
                 $checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
                 $first    = $this->rc->config->get('calendar_first_day', 1);
 
                 for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
                     $d = $j % 7;
                     $weekdays .= html::label(array('class' => 'weekday'),
                         $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) .
                         $this->gettext($daymap[$d])
                     ) . ' ';
                 }
 
                 $html .= html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), $this->gettext('bydays'))
                     . html::div('col-sm-10 form-control-plaintext', $weekdays));
                 break;
 
             // monthly recurrence form
             case 'monthly':
                 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-monthly'));
                 $html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-monthly', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
                     . html::div('col-sm-10 input-group', $select->show(1) . html::span('label-after input-group-append', html::span('input-group-text', $this->gettext('months')))));
 
                 $checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
                 for ($monthdays = '', $d = 1; $d <= 31; $d++) {
                     $monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
                     $monthdays .= $d % 7 ? ' ' : html::br();
                 }
 
                 // rule selectors
                 $radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
                 $table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
                 $table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->gettext('each')));
                 $table->add(null, $monthdays);
                 $table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->gettext('every')));
                 $table->add('recurrence-onevery', $this->rrule_selectors($attrib['part']));
 
                 $html .= html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), $this->gettext('bydays'))
                     . html::div('col-sm-10 form-control-plaintext', $table->show()));
                 break;
 
             // annually recurrence form
             case 'yearly':
                 $select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval form-control', 'id' => 'edit-recurrence-interval-yearly'));
                 $html = html::div($attrib, html::label(array('for' => 'edit-recurrence-interval-yearly', 'class' => 'col-form-label col-sm-2'), $this->gettext('every'))
                     . html::div('col-sm-10 input-group', $select->show(1) . html::span('label-after input-group-append', html::span('input-group-text', $this->gettext('years')))));
 
                 // month selector
                 $monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
                 $checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
 
                 for ($months = '', $m = 1; $m <= 12; $m++) {
                     $months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->gettext($monthmap[$m]));
                     $months .= $m % 4 ? ' ' : html::br();
                 }
 
                 $html .= html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), $this->gettext('bymonths'))
                     . html::div('col-sm-10 form-control-plaintext',
                         html::div(array('id' => 'edit-recurrence-yearly-bymonthblock'), $months)
                         . html::div('recurrence-onevery', $this->rrule_selectors($attrib['part'], '---'))
                     ));
                 break;
 
             // end of recurrence form
             case 'until':
                 $radio  = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
                 $select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times', 'class' => 'form-control'));
                 $input  = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => '10', 'class' => 'form-control datepicker'));
 
                 $html = html::div('line first',
                     $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever'))
                         . ' ' . html::label('edit-recurrence-repeat-forever', $this->gettext('forever'))
                 );
 
                 $label = $this->gettext('ntimes');
                 if (strpos($label, '$') === 0) {
                     $label = str_replace('$n', '', $label);
                     $group  = $select->show(1)
                         . html::span('input-group-append', html::span('input-group-text', rcube::Q($label)));
                 }
                 else {
                     $label = str_replace('$n', '', $label);
                     $group  = html::span('input-group-prepend', html::span('input-group-text', rcube::Q($label)))
                         . $select->show(1);
                 }
 
                 $html .= html::div('line',
                     $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count'))
                         . ' ' . html::label('edit-recurrence-repeat-count', $this->gettext('for'))
                         . ' ' . html::span('input-group', $group)
                 );
 
                 $html .= html::div('line',
                     $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until', 'aria-label' => $this->gettext('untilenddate')))
                         . ' ' . html::label('edit-recurrence-repeat-until', $this->gettext('untildate'))
                         . ' ' . $input->show('', array('aria-label' => $this->gettext('untilenddate')))
                 );
 
                 $html = html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2'), ucfirst($this->gettext('recurrencend')))
                     . html::div('col-sm-10', $html));
                 break;
 
             case 'rdate':
                 $ul     = html::tag('ul', array('id' => 'edit-recurrence-rdates', 'class' => 'recurrence-rdates'), '');
                 $input  = new html_inputfield(array('name' => 'rdate', 'id' => 'edit-recurrence-rdate-input', 'size' => "10", 'class' => 'form-control datepicker'));
                 $button = new html_inputfield(array('type' => 'button', 'class' => 'button add', 'value' => $this->gettext('addrdate')));
 
                 $html = html::div($attrib, html::label(array('class' => 'col-form-label col-sm-2', 'for' => 'edit-recurrence-rdate-input'), $this->gettext('bydates'))
                     . html::div('col-sm-10', $ul . html::div('inputform', $input->show() . $button->show())));
                 break;
         }
 
         return $html;
     }
 
     /**
      * Input field for interval selection
      */
     private function interval_selector($attrib)
     {
         $select = new html_select($attrib);
         $select->add(range(1,30), range(1,30));
         return $select;
     }
 
     /**
      * Drop-down menus for recurrence rules like "each last sunday of"
      */
     private function rrule_selectors($part, $noselect = null)
     {
         // rule selectors
         $select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix", 'class' => 'form-control'));
         if ($noselect) $select_prefix->add($noselect, '');
         $select_prefix->add(array(
                 $this->gettext('first'),
                 $this->gettext('second'),
                 $this->gettext('third'),
                 $this->gettext('fourth'),
                 $this->gettext('last')
             ),
             array(1, 2, 3, 4, -1));
 
         $select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday", 'class' => 'form-control'));
         if ($noselect) $select_wday->add($noselect, '');
 
         $daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
         $first = $this->rc->config->get('calendar_first_day', 1);
         for ($j = $first; $j <= $first+6; $j++) {
             $d = $j % 7;
             $select_wday->add($this->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
         }
 
         return $select_prefix->show() . '&nbsp;' . $select_wday->show();
     }
 
     /**
      * Convert the recurrence settings to be processed on the client
      */
     public function to_client_recurrence($recurrence, $allday = false)
     {
         if (!empty($recurrence['UNTIL'])) {
             $recurrence['UNTIL'] = $this->adjust_timezone($recurrence['UNTIL'], $allday)->format('c');
         }
 
         // format RDATE values
         if (!empty($recurrence['RDATE'])) {
             $libcal = $this;
             $recurrence['RDATE'] = array_map(function($rdate) use ($libcal) {
                 return $libcal->adjust_timezone($rdate, true)->format('c');
             }, (array) $recurrence['RDATE']);
         }
 
         unset($recurrence['EXCEPTIONS']);
 
         return $recurrence;
     }
 
     /**
      * Process the alarms values submitted by the client
      */
     public function from_client_recurrence($recurrence, $start = null)
     {
         if (is_array($recurrence) && !empty($recurrence['UNTIL'])) {
             $recurrence['UNTIL'] = new DateTime($recurrence['UNTIL'], $this->timezone);
         }
 
         if (is_array($recurrence) && !empty($recurrence['RDATE'])) {
             $tz = $this->timezone;
             $recurrence['RDATE'] = array_map(function($rdate) use ($tz, $start) {
                 try {
                     $dt = new DateTime($rdate, $tz);
                     if (is_a($start, 'DateTime'))
                         $dt->setTime($start->format('G'), $start->format('i'));
                     return $dt;
                 }
                 catch (Exception $e) {
                     return null;
                 }
             }, $recurrence['RDATE']);
         }
 
         return $recurrence;
     }
 
 
     /*********  iTip message detection  *********/
 
     /**
      * Check mail message structure of there are .ics files attached
      */
     public function mail_message_load($p)
     {
         $this->ical_message = $p['object'];
         $itip_part          = null;
 
         // check all message parts for .ics files
         foreach ((array)$this->ical_message->mime_parts as $part) {
             if (self::part_is_vcalendar($part, $this->ical_message)) {
                 if (!empty($part->ctype_parameters['method'])) {
                     $itip_part = $part->mime_id;
                 }
                 else {
                     $this->ical_parts[] = $part->mime_id;
                 }
             }
         }
 
         // priorize part with method parameter
         if ($itip_part) {
             $this->ical_parts = array($itip_part);
         }
     }
 
     /**
      * Getter for the parsed iCal objects attached to the current email message
      *
      * @return object libcalendaring_vcalendar parser instance with the parsed objects
      */
     public function get_mail_ical_objects()
     {
         // create parser and load ical objects
         if (!$this->mail_ical_parser) {
             $this->mail_ical_parser = $this->get_ical();
 
             foreach ($this->ical_parts as $mime_id) {
                 $part    = $this->ical_message->mime_parts[$mime_id];
                 $charset = $part->ctype_parameters['charset'] ?: RCUBE_CHARSET;
                 $this->mail_ical_parser->import($this->ical_message->get_part_body($mime_id, true), $charset);
 
                 // check if the parsed object is an instance of a recurring event/task
                 array_walk($this->mail_ical_parser->objects, 'libcalendaring::identify_recurrence_instance');
 
                 // stop on the part that has an iTip method specified
                 if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
                     $this->mail_ical_parser->message_date = $this->ical_message->headers->date;
                     $this->mail_ical_parser->mime_id = $mime_id;
 
                     // store the message's sender address for comparisons
                     $from = rcube_mime::decode_address_list($this->ical_message->headers->from, 1, true, null, true);
                     $this->mail_ical_parser->sender = !empty($from) ? $from[1] : '';
 
                     if (!empty($this->mail_ical_parser->sender)) {
                         foreach ($this->mail_ical_parser->objects as $i => $object) {
                             $this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender;
                             $this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender);
                         }
                     }
 
                     break;
                 }
             }
         }
 
         return $this->mail_ical_parser;
     }
 
     /**
      * Read the given mime message from IMAP and parse ical data
      *
      * @param string Mailbox name
      * @param string Message UID
      * @param string Message part ID and object index (e.g. '1.2:0')
      * @param string Object type filter (optional)
      *
      * @return array Hash array with the parsed iCal 
      */
     public function mail_get_itip_object($mbox, $uid, $mime_id, $type = null)
     {
         $charset = RCUBE_CHARSET;
 
         // establish imap connection
         $imap = $this->rc->get_storage();
         $imap->set_folder($mbox);
 
         if ($uid && $mime_id) {
             list($mime_id, $index) = explode(':', $mime_id);
 
             $part    = $imap->get_message_part($uid, $mime_id);
             $headers = $imap->get_message_headers($uid);
             $parser  = $this->get_ical();
 
             if (!empty($part->ctype_parameters['charset'])) {
                 $charset = $part->ctype_parameters['charset'];
             }
 
             if ($part) {
                 $objects = $parser->import($part, $charset);
             }
         }
 
         // successfully parsed events/tasks?
         if (!empty($objects) && ($object = $objects[$index]) && (!$type || $object['_type'] == $type)) {
             if ($parser->method)
                 $object['_method'] = $parser->method;
 
             // store the message's sender address for comparisons
             $from = rcube_mime::decode_address_list($headers->from, 1, true, null, true);
             $object['_sender'] = !empty($from) ? $from[1] : '';
             $object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
 
             // check if this is an instance of a recurring event/task
             self::identify_recurrence_instance($object);
 
             return $object;
         }
 
         return null;
     }
 
     /**
      * Checks if specified message part is a vcalendar data
      *
      * @param rcube_message_part Part object
      * @param rcube_message      Message object
      *
      * @return boolean True if part is of type vcard
      */
     public static function part_is_vcalendar($part, $message = null)
     {
         // First check if the message is "valid" (i.e. not multipart/report)
         if ($message) {
             $level = explode('.', $part->mime_id);
 
             while (array_pop($level) !== null) {
                 $id     = join('.', $level) ?: 0;
                 $parent = !empty($message->mime_parts[$id]) ? $message->mime_parts[$id] : null;
                 if ($parent && $parent->mimetype == 'multipart/report') {
                     return false;
                 }
             }
         }
 
         return (
             in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
             // Apple sends files as application/x-any (!?)
             ($part->mimetype == 'application/x-any' && !empty($part->filename) && preg_match('/\.ics$/i', $part->filename))
         );
     }
 
     /**
      * Single occourrences of recurring events are identified by their RECURRENCE-ID property
      * in iCal which is represented as 'recurrence_date' in our internal data structure.
      *
      * Check if such a property exists and derive the '_instance' identifier and '_savemode'
      * attributes which are used in the storage backend to identify the nested exception item.
      */
     public static function identify_recurrence_instance(&$object)
     {
         // for savemode=all, remove recurrence instance identifiers
         if (!empty($object['_savemode']) && $object['_savemode'] == 'all' && !empty($object['recurrence'])) {
             unset($object['_instance'], $object['recurrence_date']);
         }
         // set instance and 'savemode' according to recurrence-id
         else if (!empty($object['recurrence_date']) && $object['recurrence_date'] instanceof DateTimeInterface) {
             $object['_instance'] = self::recurrence_instance_identifier($object);
             $object['_savemode'] = !empty($object['thisandfuture']) ? 'future' : 'current';
         }
         else if (!empty($object['recurrence_id']) && !empty($object['_instance'])) {
             if (strlen($object['_instance']) > 4) {
                 $object['recurrence_date'] = rcube_utils::anytodatetime($object['_instance'], $object['start']->getTimezone());
             }
             else {
                 $object['recurrence_date'] = clone $object['start'];
             }
         }
     }
 
     /**
      * Return a date() format string to render identifiers for recurrence instances
      *
      * @param array Hash array with event properties
      * @return string Format string
      */
     public static function recurrence_id_format($event)
     {
         return !empty($event['allday']) ? 'Ymd' : 'Ymd\THis';
     }
 
     /**
      * Return the identifer for the given instance of a recurring event
      *
      * @param array Hash array with event properties
      * @param bool  All-day flag from the main event
      *
      * @return mixed Format string or null if identifier cannot be generated
      */
     public static function recurrence_instance_identifier($event, $allday = null)
     {
         $instance_date = !empty($event['recurrence_date']) ? $event['recurrence_date'] : $event['start'];
 
         if ($instance_date instanceof DateTimeInterface) {
             // According to RFC5545 (3.8.4.4) RECURRENCE-ID format should
             // be date/date-time depending on the main event type, not the exception
             if ($allday === null) {
                 $allday = !empty($event['allday']);
             }
 
             return $instance_date->format($allday ? 'Ymd' : 'Ymd\THis');
         }
     }
 
     /**
      * Check if a specified event is "identical" to the specified recurrence exception
      *
      * @param array Hash array with occurrence properties
      * @param array Hash array with exception properties
      *
      * @return bool
      */
     public static function is_recurrence_exception($event, $exception)
     {
         $instance_date = !empty($event['recurrence_date']) ? $event['recurrence_date'] : $event['start'];
         $exception_date = !empty($exception['recurrence_date']) ? $exception['recurrence_date'] : $exception['start'];
 
         if ($instance_date instanceof DateTimeInterface && $exception_date instanceof DateTimeInterface) {
             // Timezone???
             return $instance_date->format('Ymd') === $exception_date->format('Ymd');
         }
 
         return false;
     }
 
 
     /*********  Attendee handling functions  *********/
 
     /**
      * Handler for attendee group expansion requests
      */
     public function expand_attendee_group()
     {
         $id     = rcube_utils::get_input_value('id', rcube_utils::INPUT_POST);
         $data   = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
         $result = array('id' => $id, 'members' => array());
         $maxnum = 500;
 
         // iterate over all autocomplete address books (we don't know the source of the group)
         foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $abook_id) {
             if (($abook = $this->rc->get_address_book($abook_id)) && $abook->groups) {
                 foreach ($abook->list_groups($data['name'], 1) as $group) {
                     // this is the matching group to expand
                     if (in_array($data['email'], (array)$group['email'])) {
                         $abook->set_pagesize($maxnum);
                         $abook->set_group($group['ID']);
 
                         // get all members
                         $res = $abook->list_records($this->rc->config->get('contactlist_fields'));
 
                         // handle errors (e.g. sizelimit, timelimit)
                         if ($abook->get_error()) {
                             $result['error'] = $this->rc->gettext('expandattendeegrouperror', 'libcalendaring');
                             $res = false;
                         }
                         // check for maximum number of members (we don't wanna bloat the UI too much)
                         else if ($res->count > $maxnum) {
                             $result['error'] = $this->rc->gettext('expandattendeegroupsizelimit', 'libcalendaring');
                             $res = false;
                         }
 
                         while ($res && ($member = $res->iterate())) {
                             $emails = (array)$abook->get_col_values('email', $member, true);
                             if (!empty($emails) && ($email = array_shift($emails))) {
                                 $result['members'][] = array(
                                     'email' => $email,
                                     'name' => rcube_addressbook::compose_list_name($member),
                                 );
                             }
                         }
 
                         break 2;
                     }
                 }
             }
         }
 
         $this->rc->output->command('plugin.expand_attendee_callback', $result);
     }
 
     /**
      * Merge attendees of the old and new event version
      * with keeping current user and his delegatees status
      *
      * @param array &$new   New object data
      * @param array $old    Old object data
      * @param bool  $status New status of the current user
      */
     public function merge_attendees(&$new, $old, $status = null)
     {
         if (empty($status)) {
             $emails    = $this->get_user_emails();
             $delegates = array();
             $attendees = array();
 
             // keep attendee status of the current user
             foreach ((array) $new['attendees'] as $i => $attendee) {
                 if (empty($attendee['email'])) {
                     continue;
                 }
 
                 $attendees[] = $email = strtolower($attendee['email']);
 
                 if (in_array($email, $emails)) {
                     foreach ($old['attendees'] as $_attendee) {
                         if ($attendee['email'] == $_attendee['email']) {
                             $new['attendees'][$i] = $_attendee;
                             if ($_attendee['status'] == 'DELEGATED' && ($email = $_attendee['delegated-to'])) {
                                 $delegates[] = strtolower($email);
                             }
 
                             break;
                         }
                     }
                 }
             }
 
             // make sure delegated attendee is not lost
             foreach ($delegates as $delegatee) {
                 if (!in_array($delegatee, $attendees)) {
                     foreach ((array) $old['attendees'] as $attendee) {
                         if ($attendee['email'] && ($email = strtolower($attendee['email'])) && $email == $delegatee) {
                             $new['attendees'][] = $attendee;
                             break;
                         }
                     }
                 }
             }
         }
 
         // We also make sure that status of any attendee
         // is not overriden by NEEDS-ACTION if it was already set
         // which could happen if you work with shared events
         foreach ((array) $new['attendees'] as $i => $attendee) {
             if ($attendee['email'] && $attendee['status'] == 'NEEDS-ACTION') {
                 foreach ($old['attendees'] as $_attendee) {
                     if ($attendee['email'] == $_attendee['email']) {
                         $new['attendees'][$i]['status'] = $_attendee['status'];
                         unset($new['attendees'][$i]['rsvp']);
                         break;
                     }
                 }
             }
         }
     }
 
 
     /*********  Static utility functions  *********/
 
     /**
      * Convert the internal structured data into a vcalendar rrule 2.0 string
      */
     public static function to_rrule($recurrence, $allday = false)
     {
         if (is_string($recurrence)) {
             return $recurrence;
         }
 
         $rrule = '';
         foreach ((array)$recurrence as $k => $val) {
             $k = strtoupper($k);
             switch ($k) {
             case 'UNTIL':
                 // convert to UTC according to RFC 5545
                 if (is_a($val, 'DateTime')) {
                     if (!$allday && empty($val->_dateonly)) {
                         $until = clone $val;
                         $until->setTimezone(new DateTimeZone('UTC'));
                         $val = $until->format('Ymd\THis\Z');
                     }
                     else {
                         $val = $val->format('Ymd');
                     }
                 }
                 break;
             case 'RDATE':
             case 'EXDATE':
                 foreach ((array)$val as $i => $ex) {
                     if (is_a($ex, 'DateTime')) {
                         $val[$i] = $ex->format('Ymd\THis');
                     }
                 }
                 $val = join(',', (array)$val);
                 break;
             case 'EXCEPTIONS':
                 continue 2;
             }
 
             if (strlen($val)) {
                 $rrule .= $k . '=' . $val . ';';
             }
         }
 
         return rtrim($rrule, ';');
     }
 
     /**
      * Convert from fullcalendar date format to PHP date() format string
      */
     public static function to_php_date_format($from)
     {
         // "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
         return strtr(strtr($from, array(
             'YYYY' => 'Y',
             'YY'   => 'y',
             'yyyy' => 'Y',
             'yy'   => 'y',
             'MMMM' => 'F',
             'MMM'  => 'M',
             'MM'   => 'm',
             'M'    => 'n',
             'dddd' => 'l',
             'ddd'  => 'D',
             'DD'   => 'd',
             'D'    => 'j',
             'HH'   => '**',
             'hh'   => '%%',
             'H'    => 'G',
             'h'    => 'g',
             'mm'   => 'i',
             'ss'   => 's',
             'TT'   => 'A',
             'tt'   => 'a',
             'T'    => 'A',
             't'    => 'a',
             'u'    => 'c',
         )), array(
             '**'   => 'H',
             '%%'   => 'h',
         ));
     }
 
     /**
      * Convert from PHP date() format to fullcalendar (MomentJS) format string
      */
     public static function from_php_date_format($from)
     {
         // "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
         return strtr($from, array(
             'y' => 'YY',
             'Y' => 'YYYY',
             'M' => 'MMM',
             'F' => 'MMMM',
             'm' => 'MM',
             'n' => 'M',
             'j' => 'D',
             'd' => 'DD',
             'D' => 'ddd',
             'l' => 'dddd',
             'H' => 'HH',
             'h' => 'hh',
             'G' => 'H',
             'g' => 'h',
             'i' => 'mm',
             's' => 'ss',
             'c' => '',
         ));
     }
 }
diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index 5d98fb15..8e8194c0 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -1,797 +1,797 @@
 <?php
 
 /**
  * Kolab format model class wrapping libkolabxml bindings
  *
  * Abstract base class for different Kolab groupware objects read from/written
  * to the new Kolab 3 format using the PHP bindings of libkolabxml.
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012, Kolab Systems AG <contact@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_format
 {
     public static $timezone;
 
     public /*abstract*/ $CTYPE;
     public /*abstract*/ $CTYPEv2;
 
     protected /*abstract*/ $objclass;
     protected /*abstract*/ $read_func;
     protected /*abstract*/ $write_func;
 
     protected $obj;
     protected $data;
     protected $xmldata;
     protected $xmlobject;
     protected $formaterror;
     protected $loaded = false;
     protected $version = '3.0';
 
     const KTYPE_PREFIX = 'application/x-vnd.kolab.';
     const PRODUCT_ID   = 'Roundcube-libkolab-1.1';
 
     // mapping table for valid PHP timezones not supported by libkolabxml
     // basically the entire list of ftp://ftp.iana.org/tz/data/backward
     protected static $timezone_map = array(
         'Africa/Asmera' => 'Africa/Asmara',
         'Africa/Timbuktu' => 'Africa/Abidjan',
         'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca',
         'America/Atka' => 'America/Adak',
         'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
         'America/Catamarca' => 'America/Argentina/Catamarca',
         'America/Coral_Harbour' => 'America/Atikokan',
         'America/Cordoba' => 'America/Argentina/Cordoba',
         'America/Ensenada' => 'America/Tijuana',
         'America/Fort_Wayne' => 'America/Indiana/Indianapolis',
         'America/Indianapolis' => 'America/Indiana/Indianapolis',
         'America/Jujuy' => 'America/Argentina/Jujuy',
         'America/Knox_IN' => 'America/Indiana/Knox',
         'America/Louisville' => 'America/Kentucky/Louisville',
         'America/Mendoza' => 'America/Argentina/Mendoza',
         'America/Porto_Acre' => 'America/Rio_Branco',
         'America/Rosario' => 'America/Argentina/Cordoba',
         'America/Virgin' => 'America/Port_of_Spain',
         'Asia/Ashkhabad' => 'Asia/Ashgabat',
         'Asia/Calcutta' => 'Asia/Kolkata',
         'Asia/Chungking' => 'Asia/Shanghai',
         'Asia/Dacca' => 'Asia/Dhaka',
         'Asia/Katmandu' => 'Asia/Kathmandu',
         'Asia/Macao' => 'Asia/Macau',
         'Asia/Saigon' => 'Asia/Ho_Chi_Minh',
         'Asia/Tel_Aviv' => 'Asia/Jerusalem',
         'Asia/Thimbu' => 'Asia/Thimphu',
         'Asia/Ujung_Pandang' => 'Asia/Makassar',
         'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
         'Atlantic/Faeroe' => 'Atlantic/Faroe',
         'Atlantic/Jan_Mayen' => 'Europe/Oslo',
         'Australia/ACT' => 'Australia/Sydney',
         'Australia/Canberra' => 'Australia/Sydney',
         'Australia/LHI' => 'Australia/Lord_Howe',
         'Australia/NSW' => 'Australia/Sydney',
         'Australia/North' => 'Australia/Darwin',
         'Australia/Queensland' => 'Australia/Brisbane',
         'Australia/South' => 'Australia/Adelaide',
         'Australia/Tasmania' => 'Australia/Hobart',
         'Australia/Victoria' => 'Australia/Melbourne',
         'Australia/West' => 'Australia/Perth',
         'Australia/Yancowinna' => 'Australia/Broken_Hill',
         'Brazil/Acre' => 'America/Rio_Branco',
         'Brazil/DeNoronha' => 'America/Noronha',
         'Brazil/East' => 'America/Sao_Paulo',
         'Brazil/West' => 'America/Manaus',
         'Canada/Atlantic' => 'America/Halifax',
         'Canada/Central' => 'America/Winnipeg',
         'Canada/East-Saskatchewan' => 'America/Regina',
         'Canada/Eastern' => 'America/Toronto',
         'Canada/Mountain' => 'America/Edmonton',
         'Canada/Newfoundland' => 'America/St_Johns',
         'Canada/Pacific' => 'America/Vancouver',
         'Canada/Saskatchewan' => 'America/Regina',
         'Canada/Yukon' => 'America/Whitehorse',
         'Chile/Continental' => 'America/Santiago',
         'Chile/EasterIsland' => 'Pacific/Easter',
         'Cuba' => 'America/Havana',
         'Egypt' => 'Africa/Cairo',
         'Eire' => 'Europe/Dublin',
         'Europe/Belfast' => 'Europe/London',
         'Europe/Tiraspol' => 'Europe/Chisinau',
         'GB' => 'Europe/London',
         'GB-Eire' => 'Europe/London',
         'Greenwich' => 'Etc/GMT',
         'Hongkong' => 'Asia/Hong_Kong',
         'Iceland' => 'Atlantic/Reykjavik',
         'Iran' => 'Asia/Tehran',
         'Israel' => 'Asia/Jerusalem',
         'Jamaica' => 'America/Jamaica',
         'Japan' => 'Asia/Tokyo',
         'Kwajalein' => 'Pacific/Kwajalein',
         'Libya' => 'Africa/Tripoli',
         'Mexico/BajaNorte' => 'America/Tijuana',
         'Mexico/BajaSur' => 'America/Mazatlan',
         'Mexico/General' => 'America/Mexico_City',
         'NZ' => 'Pacific/Auckland',
         'NZ-CHAT' => 'Pacific/Chatham',
         'Navajo' => 'America/Denver',
         'PRC' => 'Asia/Shanghai',
         'Pacific/Ponape' => 'Pacific/Pohnpei',
         'Pacific/Samoa' => 'Pacific/Pago_Pago',
         'Pacific/Truk' => 'Pacific/Chuuk',
         'Pacific/Yap' => 'Pacific/Chuuk',
         'Poland' => 'Europe/Warsaw',
         'Portugal' => 'Europe/Lisbon',
         'ROC' => 'Asia/Taipei',
         'ROK' => 'Asia/Seoul',
         'Singapore' => 'Asia/Singapore',
         'Turkey' => 'Europe/Istanbul',
         'UCT' => 'Etc/UCT',
         'US/Alaska' => 'America/Anchorage',
         'US/Aleutian' => 'America/Adak',
         'US/Arizona' => 'America/Phoenix',
         'US/Central' => 'America/Chicago',
         'US/East-Indiana' => 'America/Indiana/Indianapolis',
         'US/Eastern' => 'America/New_York',
         'US/Hawaii' => 'Pacific/Honolulu',
         'US/Indiana-Starke' => 'America/Indiana/Knox',
         'US/Michigan' => 'America/Detroit',
         'US/Mountain' => 'America/Denver',
         'US/Pacific' => 'America/Los_Angeles',
         'US/Samoa' => 'Pacific/Pago_Pago',
         'Universal' => 'Etc/UTC',
         'W-SU' => 'Europe/Moscow',
         'Zulu' => 'Etc/UTC',
     );
 
     /**
      * Factory method to instantiate a kolab_format object of the given type and version
      *
      * @param string Object type to instantiate
      * @param float  Format version
      * @param string Cached xml data to initialize with
      * @return object kolab_format
      */
     public static function factory($type, $version = '3.0', $xmldata = null)
     {
         if (!isset(self::$timezone))
             self::$timezone = new DateTimeZone('UTC');
 
         if (!self::supports($version))
             return PEAR::raiseError("No support for Kolab format version " . $version);
 
         $type = preg_replace('/configuration\.[a-z._]+$/', 'configuration', $type);
         $suffix = preg_replace('/[^a-z]+/', '', $type);
         $classname = 'kolab_format_' . $suffix;
         if (class_exists($classname))
             return new $classname($xmldata, $version);
 
         return PEAR::raiseError("Failed to load Kolab Format wrapper for type " . $type);
     }
 
     /**
      * Determine support for the given format version
      *
      * @param float Format version to check
      * @return boolean True if supported, False otherwise
      */
     public static function supports($version)
     {
         if ($version == '2.0')
             return class_exists('kolabobject');
         // default is version 3
         return class_exists('kolabformat');
     }
 
     /**
      * Convert the given date/time value into a cDateTime object
      *
      * @param mixed         Date/Time value either as unix timestamp, date string or PHP DateTime object
      * @param DateTimeZone  The timezone the date/time is in. Use global default if Null, local time if False
      * @param boolean       True of the given date has no time component
      * @param DateTimeZone  The timezone to convert the date to before converting to cDateTime
      *
      * @return cDateTime The libkolabxml date/time object
      */
     public static function get_datetime($datetime, $tz = null, $dateonly = false, $dest_tz = null)
     {
         // use timezone information from datetime or global setting
         if (!$tz && $tz !== false) {
             if ($datetime instanceof DateTimeInterface)
                 $tz = $datetime->getTimezone();
             if (!$tz)
                 $tz = self::$timezone;
         }
 
         $result = new cDateTime();
 
         try {
             // got a unix timestamp (in UTC)
             if (is_numeric($datetime)) {
                 $datetime = new libcalendaring_datetime('@'.$datetime, new DateTimeZone('UTC'));
                 if ($tz) $datetime->setTimezone($tz);
             }
             else if (is_string($datetime) && strlen($datetime)) {
                 $datetime = $tz ? new libcalendaring_datetime($datetime, $tz) : new libcalendaring_datetime($datetime);
             }
             else if ($datetime instanceof DateTimeInterface) {
                 $datetime = clone $datetime;
             }
         }
         catch (Exception $e) {}
 
         if ($datetime instanceof DateTimeInterface) {
             if ($dest_tz instanceof DateTimeZone && $dest_tz !== $datetime->getTimezone()) {
                 $datetime->setTimezone($dest_tz);
                 $tz = $dest_tz;
             }
 
             $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
 
             if ($dateonly) {
                 // Dates should be always in local time only
                 return $result;
             }
 
             $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
 
             // libkolabxml throws errors on some deprecated timezone names
             $utc_aliases = array('UTC', 'GMT', '+00:00', 'Z', 'Etc/GMT', 'Etc/UTC');
 
             if ($tz && in_array($tz->getName(), $utc_aliases)) {
                 $result->setUTC(true);
             }
             else if ($tz !== false) {
                 $tzid = $tz->getName();
                 if (array_key_exists($tzid, self::$timezone_map))
                     $tzid = self::$timezone_map[$tzid];
                 $result->setTimezone($tzid);
             }
         }
 
         return $result;
     }
 
     /**
      * Convert the given cDateTime into a PHP DateTime object
      *
      * @param cDateTime    The libkolabxml datetime object
      * @param DateTimeZone The timezone to convert the date to
      *
      * @return libcalendaring_datetime PHP datetime instance
      */
     public static function php_datetime($cdt, $dest_tz = null)
     {
         if (!is_object($cdt) || !$cdt->isValid()) {
             return null;
         }
 
         $d = new libcalendaring_datetime(null, self::$timezone);
 
         if ($dest_tz) {
             $d->setTimezone($dest_tz);
         }
         else {
             try {
                 if ($tzs = $cdt->timezone()) {
                     $tz = new DateTimeZone($tzs);
                     $d->setTimezone($tz);
                 }
                 else if ($cdt->isUTC()) {
                     $d->setTimezone(new DateTimeZone('UTC'));
                 }
             }
             catch (Exception $e) { }
         }
 
         $d->setDate($cdt->year(), $cdt->month(), $cdt->day());
 
         if ($cdt->isDateOnly()) {
             $d->_dateonly = true;
             $d->setTime(12, 0, 0);  // set time to noon to avoid timezone troubles
         }
         else {
             $d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
         }
 
         return $d;
     }
 
     /**
      * Convert a libkolabxml vector to a PHP array
      *
      * @param object vector Object
      * @return array Indexed array containing vector elements
      */
     public static function vector2array($vec, $max = PHP_INT_MAX)
     {
         $arr = array();
         for ($i=0; $i < $vec->size() && $i < $max; $i++)
             $arr[] = $vec->get($i);
         return $arr;
     }
 
     /**
      * Build a libkolabxml vector (string) from a PHP array
      *
      * @param array Array with vector elements
      * @return object vectors
      */
     public static function array2vector($arr)
     {
         $vec = new vectors;
         foreach ((array)$arr as $val) {
             if (strlen($val))
                 $vec->push($val);
         }
         return $vec;
     }
 
     /**
      * Parse the X-Kolab-Type header from MIME messages and return the object type in short form
      *
      * @param string X-Kolab-Type header value
      * @return string Kolab object type (contact,event,task,note,etc.)
      */
     public static function mime2object_type($x_kolab_type)
     {
         return preg_replace(
             array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
             array( 'dictionary',            'distribution-list'),
             substr($x_kolab_type, strlen(self::KTYPE_PREFIX))
         );
     }
 
 
     /**
      * Default constructor of all kolab_format_* objects
      */
     public function __construct($xmldata = null, $version = null)
     {
         $this->obj = new $this->objclass;
         $this->xmldata = $xmldata;
 
         if ($version)
             $this->version = $version;
 
         // use libkolab module if available
         if (class_exists('kolabobject'))
             $this->xmlobject = new XMLObject();
     }
 
     /**
      * Check for format errors after calling kolabformat::write*()
      *
      * @return boolean True if there were errors, False if OK
      */
     protected function format_errors()
     {
         $ret = $log = false;
         switch (kolabformat::error()) {
             case kolabformat::NoError:
                 $ret = false;
                 break;
             case kolabformat::Warning:
                 $ret = false;
                 $uid = is_object($this->obj) ? $this->obj->uid() : $this->data['uid'];
                 $log = "Warning @ $uid";
                 break;
             default:
                 $ret = true;
                 $log = "Error";
         }
 
         if ($log && !isset($this->formaterror)) {
             rcube::raise_error(array(
                 'code' => 660,
                 'type' => 'php',
                 'file' => __FILE__,
                 'line' => __LINE__,
                 'message' => "kolabformat $log: " . kolabformat::errorMessage(),
             ), true);
 
             $this->formaterror = $ret;
         }
 
         return $ret;
     }
 
     /**
      * Save the last generated UID to the object properties.
      * Should be called after kolabformat::writeXXXX();
      */
     protected function update_uid()
     {
         // get generated UID
-        if (!$this->data['uid']) {
+        if (!($this->data['uid'] ?? null)) {
             if ($this->xmlobject) {
                 $this->data['uid'] = $this->xmlobject->getSerializedUID();
             }
             if (empty($this->data['uid'])) {
                 $this->data['uid'] = kolabformat::getSerializedUID();
             }
             $this->obj->setUid($this->data['uid']);
         }
     }
 
     /**
      * Initialize libkolabxml object with cached xml data
      */
     protected function init()
     {
         if (!$this->loaded) {
             if ($this->xmldata) {
                 $this->load($this->xmldata);
                 $this->xmldata = null;
             }
             $this->loaded = true;
         }
     }
 
     /**
      * Get constant value for libkolab's version parameter
      *
      * @param float Version value to convert
      * @return int Constant value of either kolabobject::KolabV2 or kolabobject::KolabV3 or false if kolabobject module isn't available
      */
     protected function libversion($v = null)
     {
         if (class_exists('kolabobject')) {
             $version = $v ?: $this->version;
             if ($version <= '2.0')
                 return kolabobject::KolabV2;
             else
                 return kolabobject::KolabV3;
         }
 
         return false;
     }
 
     /**
      * Determine the correct libkolab(xml) wrapper function for the given call
      * depending on the available PHP modules
      */
     protected function libfunc($func)
     {
         if (is_array($func) || strpos($func, '::'))
             return $func;
         else if (class_exists('kolabobject'))
             return array($this->xmlobject, $func);
         else
             return 'kolabformat::' . $func;
     }
 
     /**
      * Direct getter for object properties
      */
     public function __get($var)
     {
         return $this->data[$var];
     }
 
     /**
      * Load Kolab object data from the given XML block
      *
      * @param string XML data
      * @return boolean True on success, False on failure
      */
     public function load($xml)
     {
         $this->formaterror = null;
         $read_func = $this->libfunc($this->read_func);
 
         if (is_array($read_func))
             $r = call_user_func($read_func, $xml, $this->libversion());
         else
             $r = call_user_func($read_func, $xml, false);
 
         if (is_resource($r))
             $this->obj = new $this->objclass($r);
         else if (is_a($r, $this->objclass))
             $this->obj = $r;
 
         $this->loaded = !$this->format_errors();
     }
 
     /**
      * Write object data to XML format
      *
      * @param float Format version to write
      * @return string XML data
      */
     public function write($version = null)
     {
         $this->formaterror = null;
 
         $this->init();
         $write_func = $this->libfunc($this->write_func);
         if (is_array($write_func))
             $this->xmldata = call_user_func($write_func, $this->obj, $this->libversion($version), self::PRODUCT_ID);
         else
             $this->xmldata = call_user_func($write_func, $this->obj, self::PRODUCT_ID);
 
         if (!$this->format_errors())
             $this->update_uid();
         else
             $this->xmldata = null;
 
         return $this->xmldata;
     }
 
     /**
      * Set properties to the kolabformat object
      *
      * @param array  Object data as hash array
      */
     public function set(&$object)
     {
         $this->init();
 
         if (!empty($object['uid']))
             $this->obj->setUid($object['uid']);
 
         // set some automatic values if missing
         if (method_exists($this->obj, 'setCreated')) {
             // Always set created date to workaround libkolabxml (>1.1.4) bug
-            $created = $object['created'] ?: new DateTime('now');
+            $created = $object['created'] ?? new DateTime('now');
             $created->setTimezone(new DateTimeZone('UTC')); // must be UTC
             $this->obj->setCreated(self::get_datetime($created));
             $object['created'] = $created;
         }
 
         $object['changed'] = new DateTime('now', new DateTimeZone('UTC'));
         $this->obj->setLastModified(self::get_datetime($object['changed']));
 
         // Save custom properties of the given object
         if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
             $vcustom = new vectorcs;
             foreach ((array)$object['x-custom'] as $cp) {
                 if (is_array($cp))
                     $vcustom->push(new CustomProperty($cp[0], $cp[1]));
             }
             $this->obj->setCustomProperties($vcustom);
         }
         // load custom properties from XML for caching (#2238) if method exists (#3125)
         else if (method_exists($this->obj, 'customProperties')) {
             $object['x-custom'] = array();
             $vcustom = $this->obj->customProperties();
             for ($i=0; $i < $vcustom->size(); $i++) {
                 $cp = $vcustom->get($i);
                 $object['x-custom'][] = array($cp->identifier, $cp->value);
             }
         }
     }
 
     /**
      * Convert the Kolab object into a hash array data structure
      *
      * @param array Additional data for merge
      *
      * @return array  Kolab object data as hash array
      */
     public function to_array($data = array())
     {
         $this->init();
 
         // read object properties into local data object
         $object = array(
             'uid'     => $this->obj->uid(),
             'changed' => self::php_datetime($this->obj->lastModified()),
         );
 
         // not all container support the created property
         if (method_exists($this->obj, 'created')) {
             $object['created'] = self::php_datetime($this->obj->created());
         }
 
         // read custom properties
         if (method_exists($this->obj, 'customProperties')) {
             $vcustom = $this->obj->customProperties();
             for ($i=0; $i < $vcustom->size(); $i++) {
                 $cp = $vcustom->get($i);
                 $object['x-custom'][] = array($cp->identifier, $cp->value);
             }
         }
 
         // merge with additional data, e.g. attachments from the message
         if ($data) {
             foreach ($data as $idx => $value) {
                 if (is_array($value)) {
-                    $object[$idx] = array_merge((array)$object[$idx], $value);
+                    $object[$idx] = array_merge((array)($object[$idx] ?? []), $value);
                 }
                 else {
                     $object[$idx] = $value;
                 }
             }
         }
 
         return $object;
     }
 
     /**
      * Object validation method to be implemented by derived classes
      */
     abstract public function is_valid();
 
     /**
      * Callback for kolab_storage_cache to get object specific tags to cache
      *
      * @return array List of tags to save in cache
      */
     public function get_tags()
     {
         return array();
     }
 
     /**
      * Callback for kolab_storage_cache to get words to index for fulltext search
      *
      * @return array List of words to save in cache
      */
     public function get_words()
     {
         return array();
     }
 
     /**
      * Utility function to extract object attachment data
      *
      * @param array Hash array reference to append attachment data into
      */
     public function get_attachments(&$object, $all = false)
     {
         $this->init();
 
         // handle attachments
         $vattach = $this->obj->attachments();
         for ($i=0; $i < $vattach->size(); $i++) {
             $attach = $vattach->get($i);
 
             // skip cid: attachments which are mime message parts handled by kolab_storage_folder
             if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
                 $name    = $attach->label();
                 $key     = $name . (isset($object['_attachments'][$name]) ? '.'.$i : '');
                 $content = $attach->data();
                 $object['_attachments'][$key] = array(
                     'id'       => 'i:'.$i,
                     'name'     => $name,
                     'mimetype' => $attach->mimetype(),
                     'size'     => strlen($content),
                     'content'  => $content,
                 );
             }
             else if ($all && substr($attach->uri(), 0, 4) == 'cid:') {
                 $key = $attach->uri();
                 $object['_attachments'][$key] = array(
                     'id'       => $key,
                     'name'     => $attach->label(),
                     'mimetype' => $attach->mimetype(),
                 );
             }
             else if (in_array(substr($attach->uri(), 0, 4), array('http','imap'))) {
                 $object['links'][] = $attach->uri();
             }
         }
     }
 
     /**
      * Utility function to set attachment properties to the kolabformat object
      *
      * @param array  Object data as hash array
      * @param boolean True to always overwrite attachment information
      */
     protected function set_attachments($object, $write = true)
     {
         // save attachments
         $vattach = new vectorattachment;
-        foreach ((array) $object['_attachments'] as $cid => $attr) {
+        foreach ((array)($object['_attachments'] ?? []) as $cid => $attr) {
             if (empty($attr))
                 continue;
             $attach = new Attachment;
             $attach->setLabel((string)$attr['name']);
             $attach->setUri('cid:' . $cid, $attr['mimetype'] ?: 'application/octet-stream');
             if ($attach->isValid()) {
                 $vattach->push($attach);
                 $write = true;
             }
             else {
                 rcube::raise_error(array(
                     'code' => 660,
                     'type' => 'php',
                     'file' => __FILE__,
                     'line' => __LINE__,
                     'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
                 ), true);
             }
         }
 
-        foreach ((array) $object['links'] as $link) {
+        foreach ((array)($object['links'] ?? []) as $link) {
             $attach = new Attachment;
             $attach->setUri($link, 'unknown');
             $vattach->push($attach);
             $write = true;
         }
 
         if ($write) {
             $this->obj->setAttachments($vattach);
         }
     }
 
     /**
      * Unified way of updating/deleting attachments of edited object
      *
      * @param array $object Kolab object data
      * @param array $old    Old version of Kolab object
      */
     public static function merge_attachments(&$object, $old)
     {
         $object['_attachments'] = isset($old['_attachments']) && is_array($old['_attachments']) ? $old['_attachments'] : [];
 
         // delete existing attachment(s)
         if (!empty($object['deleted_attachments'])) {
             foreach ($object['_attachments'] as $idx => $att) {
                 if ($object['deleted_attachments'] === true || in_array($att['id'], $object['deleted_attachments'])) {
                     $object['_attachments'][$idx] = false;
                 }
             }
         }
 
         // in kolab_storage attachments are indexed by content-id
         foreach ((array) ($object['attachments'] ?? []) as $attachment) {
             $key = null;
 
             // Roundcube ID has nothing to do with the storage ID, remove it
             // for uploaded/new attachments
             // FIXME: Roundcube uses 'data', kolab_format uses 'content'
             if (!empty($attachment['content']) || !empty($attachment['path']) || !empty($attachment['data'])) {
                 unset($attachment['id']);
             }
 
             if (!empty($attachment['id'])) {
                 foreach ((array) $object['_attachments'] as $cid => $att) {
                     if ($att && $attachment['id'] == $att['id']) {
                         $key = $cid;
                     }
                 }
             }
             else {
                 // find attachment by name, so we can update it if exists
                 // and make sure there are no duplicates
                 foreach ($object['_attachments'] as $cid => $att) {
                     if ($att && $attachment['name'] == $att['name']) {
                         $key = $cid;
                     }
                 }
             }
 
             if ($key && $attachment['_deleted']) {
                 $object['_attachments'][$key] = false;
             }
             // replace existing entry
             else if ($key) {
                 $object['_attachments'][$key] = $attachment;
             }
             // append as new attachment
             else {
                 $object['_attachments'][] = $attachment;
             }
         }
 
         unset($object['attachments']);
         unset($object['deleted_attachments']);
     }
 }
diff --git a/plugins/libkolab/lib/kolab_format_configuration.php b/plugins/libkolab/lib/kolab_format_configuration.php
index ceb7ebb6..7d7bba4a 100644
--- a/plugins/libkolab/lib/kolab_format_configuration.php
+++ b/plugins/libkolab/lib/kolab_format_configuration.php
@@ -1,284 +1,284 @@
 <?php
 
 /**
  * Kolab Configuration data model class
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012, Kolab Systems AG <contact@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_format_configuration extends kolab_format
 {
     public $CTYPE   = 'application/vnd.kolab+xml';
     public $CTYPEv2 = 'application/x-vnd.kolab.configuration';
 
     protected $objclass   = 'Configuration';
     protected $read_func  = 'readConfiguration';
     protected $write_func = 'writeConfiguration';
 
     private $type_map = array(
         'category'    => Configuration::TypeCategoryColor,
         'dictionary'  => Configuration::TypeDictionary,
         'file_driver' => Configuration::TypeFileDriver,
         'relation'    => Configuration::TypeRelation,
         'snippet'     => Configuration::TypeSnippet,
     );
 
     private $driver_settings_fields = array('host', 'port', 'username', 'password');
 
     /**
      * Set properties to the kolabformat object
      *
      * @param array  Object data as hash array
      */
     public function set(&$object)
     {
         // read type-specific properties
         switch ($object['type']) {
         case 'dictionary':
             $dict = new Dictionary($object['language']);
             $dict->setEntries(self::array2vector($object['e']));
             $this->obj = new Configuration($dict);
             break;
 
         case 'category':
             // TODO: implement this
             $categories = new vectorcategorycolor;
             $this->obj = new Configuration($categories);
             break;
 
         case 'file_driver':
             $driver = new FileDriver($object['driver'], $object['title']);
 
             $driver->setEnabled((bool) $object['enabled']);
 
             foreach ($this->driver_settings_fields as $field) {
                 $value = $object[$field];
                 if ($value !== null) {
                     $driver->{'set' . ucfirst($field)}($value);
                 }
             }
 
             $this->obj = new Configuration($driver);
             break;
 
         case 'relation':
             $relation = new Relation(strval($object['name']), strval($object['category']));
 
-            if ($object['color']) {
+            if ($object['color'] ?? false) {
                 $relation->setColor($object['color']);
             }
-            if ($object['parent']) {
+            if ($object['parent'] ?? false) {
                 $relation->setParent($object['parent']);
             }
-            if ($object['iconName']) {
+            if ($object['iconName'] ?? false) {
                 $relation->setIconName($object['iconName']);
             }
-            if ($object['priority'] > 0) {
+            if (($object['priority'] ?? 0) > 0) {
                 $relation->setPriority((int) $object['priority']);
             }
-            if (!empty($object['members'])) {
+            if (!empty($object['members'] ?? null)) {
                 $relation->setMembers(self::array2vector($object['members']));
             }
 
             $this->obj = new Configuration($relation);
             break;
 
         case 'snippet':
             $collection = new SnippetCollection($object['name']);
             $snippets   = new vectorsnippets;
 
-            foreach ((array) $object['snippets'] as $item) {
+            foreach ((array)($object['snippets'] ?? []) as $item) {
                 $snippet = new snippet($item['name'], $item['text']);
                 $snippet->setTextType(strtolower($item['type']) == 'html' ? Snippet::HTML : Snippet::Plain);
-                if ($item['shortcut']) {
+                if ($item['shortcut'] ?? false) {
                     $snippet->setShortCut($item['shortcut']);
                 }
 
                 $snippets->push($snippet);
             }
 
             $collection->setSnippets($snippets);
 
             $this->obj = new Configuration($collection);
             break;
 
         default:
             return false;
         }
 
         // adjust content-type string
         $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
 
         // reset old object data, otherwise set() will overwrite current data (#4095)
         $this->xmldata = null;
         // set common object properties
         parent::set($object);
 
         // cache this data
         $this->data = $object;
         unset($this->data['_formatobj']);
     }
 
     /**
      *
      */
     public function is_valid()
     {
         return $this->data || (is_object($this->obj) && $this->obj->isValid());
     }
 
     /**
      * Convert the Configuration object into a hash array data structure
      *
      * @param array Additional data for merge
      *
      * @return array  Config object data as hash array
      */
     public function to_array($data = array())
     {
         // return cached result
         if (!empty($this->data)) {
             return $this->data;
         }
 
         // read common object props into local data object
         $object = parent::to_array($data);
 
         $type_map = array_flip($this->type_map);
 
         $object['type'] = $type_map[$this->obj->type()];
 
         // read type-specific properties
         switch ($object['type']) {
         case 'dictionary':
             $dict = $this->obj->dictionary();
             $object['language'] = $dict->language();
             $object['e'] = self::vector2array($dict->entries());
             break;
 
         case 'category':
             // TODO: implement this
             break;
 
         case 'file_driver':
             $driver = $this->obj->fileDriver();
 
             $object['driver']  = $driver->driver();
             $object['title']   = $driver->title();
             $object['enabled'] = $driver->enabled();
 
             foreach ($this->driver_settings_fields as $field) {
                 $object[$field] = $driver->{$field}();
             }
 
             break;
 
         case 'relation':
             $relation = $this->obj->relation();
 
             $object['name']     = $relation->name();
             $object['category'] = $relation->type();
             $object['color']    = $relation->color();
             $object['parent']   = $relation->parent();
             $object['iconName'] = $relation->iconName();
             $object['priority'] = $relation->priority();
             $object['members']  = self::vector2array($relation->members());
 
             break;
 
         case 'snippet':
             $collection = $this->obj->snippets();
 
             $object['name']     = $collection->name();
             $object['snippets'] = array();
 
             $snippets = $collection->snippets();
             for ($i=0; $i < $snippets->size(); $i++) {
                 $snippet = $snippets->get($i);
                 $object['snippets'][] = array(
                     'name'     => $snippet->name(),
                     'text'     => $snippet->text(),
                     'type'     => $snippet->textType() == Snippet::HTML ? 'html' : 'plain',
                     'shortcut' => $snippet->shortCut(),
                 );
             }
 
             break;
         }
 
         // adjust content-type string
         if ($object['type']) {
             $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
         }
 
         $this->data = $object;
         return $this->data;
     }
 
     /**
      * Callback for kolab_storage_cache to get object specific tags to cache
      *
      * @return array List of tags to save in cache
      */
     public function get_tags()
     {
         $tags = array();
 
         switch ($this->data['type']) {
         case 'dictionary':
             $tags = array($this->data['language']);
             break;
 
         case 'relation':
             $tags = array('category:' . $this->data['category']);
             break;
         }
 
         return $tags;
     }
 
     /**
      * Callback for kolab_storage_cache to get words to index for fulltext search
      *
      * @return array List of words to save in cache
      */
     public function get_words()
     {
         $words = array();
 
         foreach ((array)$this->data['members'] as $url) {
             $member = kolab_storage_config::parse_member_url($url);
 
             if (empty($member)) {
                 if (strpos($url, 'urn:uuid:') === 0) {
                     $words[] = substr($url, 9);
                 }
             }
             else if (!empty($member['params']['message-id'])) {
                 $words[] = $member['params']['message-id'];
             }
             else {
                 // derive message identifier from URI
                 $words[] = md5($url);
             }
         }
 
         return $words;
     }
 }
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index 806a8197..ac5be839 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -1,482 +1,482 @@
 <?php
 
 /**
  * Kolab Contact model class
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012, Kolab Systems AG <contact@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_format_contact extends kolab_format
 {
     public $CTYPE = 'application/vcard+xml';
     public $CTYPEv2 = 'application/x-vnd.kolab.contact';
 
     protected $objclass = 'Contact';
     protected $read_func = 'readContact';
     protected $write_func = 'writeContact';
 
     public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email:address');
 
     public $phonetypes = array(
         'home'    => Telephone::Home,
         'work'    => Telephone::Work,
         'text'    => Telephone::Text,
         'main'    => Telephone::Voice,
         'homefax' => Telephone::Fax,
         'workfax' => Telephone::Fax,
         'mobile'  => Telephone::Cell,
         'video'   => Telephone::Video,
         'pager'   => Telephone::Pager,
         'car'     => Telephone::Car,
         'other'   => Telephone::Textphone,
     );
 
     public $emailtypes = array(
         'home' => Email::Home,
         'work' => Email::Work,
         'other' => Email::NoType,
     );
 
     public $addresstypes = array(
         'home' => Address::Home,
         'work' => Address::Work,
         'office' => 0,
     );
 
     private $gendermap = array(
         'female' => Contact::Female,
         'male'   => Contact::Male,
     );
 
     private $relatedmap = array(
         'manager'   => Related::Manager,
         'assistant' => Related::Assistant,
         'spouse'    => Related::Spouse,
         'children'  => Related::Child,
     );
 
 
     /**
      * Default constructor
      */
     function __construct($xmldata = null, $version = 3.0)
     {
         parent::__construct($xmldata, $version);
 
         // complete phone types
         $this->phonetypes['homefax'] |= Telephone::Home;
         $this->phonetypes['workfax'] |= Telephone::Work;
     }
 
     /**
      * Set contact properties to the kolabformat object
      *
      * @param array  Contact data as hash array
      */
     public function set(&$object)
     {
         // set common object properties
         parent::set($object);
 
         // do the hard work of setting object values
         $nc = new NameComponents;
-        $nc->setSurnames(self::array2vector($object['surname']));
-        $nc->setGiven(self::array2vector($object['firstname']));
-        $nc->setAdditional(self::array2vector($object['middlename']));
-        $nc->setPrefixes(self::array2vector($object['prefix']));
-        $nc->setSuffixes(self::array2vector($object['suffix']));
+        $nc->setSurnames(self::array2vector($object['surname'] ?? null));
+        $nc->setGiven(self::array2vector($object['firstname'] ?? null));
+        $nc->setAdditional(self::array2vector($object['middlename'] ?? null));
+        $nc->setPrefixes(self::array2vector($object['prefix'] ?? null));
+        $nc->setSuffixes(self::array2vector($object['suffix'] ?? null));
         $this->obj->setNameComponents($nc);
-        $this->obj->setName($object['name']);
-        $this->obj->setCategories(self::array2vector($object['categories']));
+        $this->obj->setName($object['name'] ?? null);
+        $this->obj->setCategories(self::array2vector($object['categories'] ?? null));
 
         if (isset($object['nickname']))
             $this->obj->setNickNames(self::array2vector($object['nickname']));
         if (isset($object['jobtitle']))
             $this->obj->setTitles(self::array2vector($object['jobtitle']));
 
         // organisation related properties (affiliation)
         $org = new Affiliation;
         $offices = new vectoraddress;
-        if ($object['organization'])
+        if ($object['organization'] ?? null)
             $org->setOrganisation($object['organization']);
-        if ($object['department'])
+        if ($object['department'] ?? null)
             $org->setOrganisationalUnits(self::array2vector($object['department']));
-        if ($object['profession'])
+        if ($object['profession'] ?? null)
             $org->setRoles(self::array2vector($object['profession']));
 
         $rels = new vectorrelated;
         foreach (array('manager','assistant') as $field) {
-            if (!empty($object[$field])) {
+            if (!empty($object[$field] ?? null)) {
                 $reltype = $this->relatedmap[$field];
                 foreach ((array)$object[$field] as $value) {
                     $rels->push(new Related(Related::Text, $value, $reltype));
                 }
             }
         }
         $org->setRelateds($rels);
 
         // im, email, url
-        $this->obj->setIMaddresses(self::array2vector($object['im']));
+        $this->obj->setIMaddresses(self::array2vector($object['im'] ?? null));
 
         if (class_exists('vectoremail')) {
             $vemails = new vectoremail;
-            foreach ((array)$object['email'] as $email) {
+            foreach ((array)($object['email'] ?? []) as $email) {
                 $type = $this->emailtypes[$email['type']];
                 $vemails->push(new Email($email['address'], intval($type)));
             }
         }
         else {
             $vemails = self::array2vector(array_map(function($v){ return $v['address']; }, $object['email']));
         }
         $this->obj->setEmailAddresses($vemails);
 
         $vurls = new vectorurl;
-        foreach ((array)$object['website'] as $url) {
+        foreach ((array)($object['website'] ?? []) as $url) {
             $type = $url['type'] == 'blog' ? Url::Blog : Url::NoType;
             $vurls->push(new Url($url['url'], $type));
         }
         $this->obj->setUrls($vurls);
 
         // addresses
         $adrs = new vectoraddress;
-        foreach ((array)$object['address'] as $address) {
+        foreach ((array)($object['address'] ?? [])as $address) {
             $adr = new Address;
             $type = $this->addresstypes[$address['type']];
             if (isset($type))
                 $adr->setTypes($type);
-            else if ($address['type'])
+            else if ($address['type'] ?? null)
                 $adr->setLabel($address['type']);
-            if ($address['street'])
+            if ($address['street'] ?? null)
                 $adr->setStreet($address['street']);
-            if ($address['locality'])
+            if ($address['locality'] ?? null)
                 $adr->setLocality($address['locality']);
-            if ($address['code'])
+            if ($address['code'] ?? null)
                 $adr->setCode($address['code']);
-            if ($address['region'])
+            if ($address['region'] ?? null)
                 $adr->setRegion($address['region']);
-            if ($address['country'])
+            if ($address['country'] ?? null)
                 $adr->setCountry($address['country']);
 
-            if ($address['type'] == 'office')
+            if (($address['type'] ?? null) == 'office')
                 $offices->push($adr);
             else
                 $adrs->push($adr);
         }
         $this->obj->setAddresses($adrs);
         $org->setAddresses($offices);
 
         // add org affiliation after addresses are set
         $orgs = new vectoraffiliation;
         $orgs->push($org);
         $this->obj->setAffiliations($orgs);
 
         // telephones
         $tels = new vectortelephone;
-        foreach ((array)$object['phone'] as $phone) {
+        foreach ((array)($object['phone'] ?? []) as $phone) {
             $tel = new Telephone;
-            if (isset($this->phonetypes[$phone['type']]))
+            if (isset($this->phonetypes[$phone['type'] ?? null]))
                 $tel->setTypes($this->phonetypes[$phone['type']]);
-            $tel->setNumber($phone['number']);
+            $tel->setNumber($phone['number'] ?? null);
             $tels->push($tel);
         }
         $this->obj->setTelephones($tels);
 
         if (isset($object['gender']))
             $this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
         if (isset($object['notes']))
             $this->obj->setNote($object['notes']);
         if (isset($object['freebusyurl']))
             $this->obj->setFreeBusyUrl($object['freebusyurl']);
         if (isset($object['lang']))
             $this->obj->setLanguages(self::array2vector($object['lang']));
         if (isset($object['birthday']))
             $this->obj->setBDay(self::get_datetime($object['birthday'], false, true));
         if (isset($object['anniversary']))
             $this->obj->setAnniversary(self::get_datetime($object['anniversary'], false, true));
 
-        if (!empty($object['photo'])) {
+        if (!empty($object['photo'] ?? null)) {
             if ($type = rcube_mime::image_content_type($object['photo']))
                 $this->obj->setPhoto($object['photo'], $type);
         }
         else if (isset($object['photo']))
             $this->obj->setPhoto('','');
         else if ($this->obj->photoMimetype())  // load saved photo for caching
             $object['photo'] = $this->obj->photo();
 
         // spouse and children are relateds
         $rels = new vectorrelated;
         foreach (array('spouse','children') as $field) {
             if (!empty($object[$field])) {
                 $reltype = $this->relatedmap[$field];
                 foreach ((array)$object[$field] as $value) {
                     $rels->push(new Related(Related::Text, $value, $reltype));
                 }
             }
         }
         // add other relateds
-        if (is_array($object['related'])) {
+        if (is_array($object['related'] ?? null)) {
             foreach ($object['related'] as $value) {
                 $rels->push(new Related(Related::Text, $value));
             }
         }
         $this->obj->setRelateds($rels);
 
         // insert/replace crypto keys
         $pgp_index = $pkcs7_index = -1;
         $keys = $this->obj->keys();
         for ($i=0; $i < $keys->size(); $i++) {
             $key = $keys->get($i);
             if ($pgp_index < 0 && $key->type() == Key::PGP)
                 $pgp_index = $i;
             else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
                 $pkcs7_index = $i;
         }
 
-        $pgpkey   = $object['pgppublickey']   ? new Key($object['pgppublickey'], Key::PGP) : new Key();
-        $pkcs7key = $object['pkcs7publickey'] ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
+        $pgpkey   = ($object['pgppublickey'] ?? false)   ? new Key($object['pgppublickey'], Key::PGP) : new Key();
+        $pkcs7key = ($object['pkcs7publickey'] ?? false) ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
 
         if ($pgp_index >= 0)
             $keys->set($pgp_index, $pgpkey);
         else if (!empty($object['pgppublickey']))
             $keys->push($pgpkey);
         if ($pkcs7_index >= 0)
             $keys->set($pkcs7_index, $pkcs7key);
         else if (!empty($object['pkcs7publickey']))
             $keys->push($pkcs7key);
 
         $this->obj->setKeys($keys);
 
         // TODO: handle language, gpslocation, etc.
 
         // set type property for proper caching
         $object['_type'] = 'contact';
 
         // cache this data
         $this->data = $object;
         unset($this->data['_formatobj']);
     }
 
     /**
      *
      */
     public function is_valid()
     {
         return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/));
     }
 
     /**
      * Convert the Contact object into a hash array data structure
      *
      * @param array Additional data for merge
      *
      * @return array  Contact data as hash array
      */
     public function to_array($data = array())
     {
         // return cached result
         if (!empty($this->data))
             return $this->data;
 
         // read common object props into local data object
         $object = parent::to_array($data);
 
         $object['name'] = $this->obj->name();
 
         $nc = $this->obj->nameComponents();
         $object['surname']    = join(' ', self::vector2array($nc->surnames()));
         $object['firstname']  = join(' ', self::vector2array($nc->given()));
         $object['middlename'] = join(' ', self::vector2array($nc->additional()));
         $object['prefix']     = join(' ', self::vector2array($nc->prefixes()));
         $object['suffix']     = join(' ', self::vector2array($nc->suffixes()));
         $object['nickname']   = join(' ', self::vector2array($this->obj->nickNames()));
         $object['jobtitle']   = join(' ', self::vector2array($this->obj->titles()));
         $object['categories'] = self::vector2array($this->obj->categories());
 
         // organisation related properties (affiliation)
         $orgs = $this->obj->affiliations();
         if ($orgs->size()) {
             $org = $orgs->get(0);
             $object['organization']   = $org->organisation();
             $object['profession']     = join(' ', self::vector2array($org->roles()));
             $object['department']     = join(' ', self::vector2array($org->organisationalUnits()));
             $this->read_relateds($org->relateds(), $object);
         }
 
         $object['im'] = self::vector2array($this->obj->imAddresses());
 
         $emails = $this->obj->emailAddresses();
         if ($emails instanceof vectoremail) {
             $emailtypes = array_flip($this->emailtypes);
             for ($i=0; $i < $emails->size(); $i++) {
                 $email = $emails->get($i);
                 $object['email'][] = array('address' => $email->address(), 'type' => $emailtypes[$email->types()]);
             }
         }
         else {
             $object['email'] = self::vector2array($emails);
         }
 
         $urls = $this->obj->urls();
         for ($i=0; $i < $urls->size(); $i++) {
             $url = $urls->get($i);
             $subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
             $object['website'][] = array('url' => $url->url(), 'type' => $subtype);
         }
 
         // addresses
         $this->read_addresses($this->obj->addresses(), $object);
         if ($org && ($offices = $org->addresses()))
             $this->read_addresses($offices, $object, 'office');
 
         // telehones
         $tels = $this->obj->telephones();
         $teltypes = array_flip($this->phonetypes);
         for ($i=0; $i < $tels->size(); $i++) {
             $tel = $tels->get($i);
             $object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
         }
 
         $object['notes'] = $this->obj->note();
         $object['freebusyurl'] = $this->obj->freeBusyUrl();
         $object['lang'] = self::vector2array($this->obj->languages());
 
         if ($bday = self::php_datetime($this->obj->bDay()))
             $object['birthday'] = $bday;
 
         if ($anniversary = self::php_datetime($this->obj->anniversary()))
             $object['anniversary'] = $anniversary;
 
         $gendermap = array_flip($this->gendermap);
         if (($g = $this->obj->gender()) && $gendermap[$g])
             $object['gender'] = $gendermap[$g];
 
         if ($this->obj->photoMimetype())
             $object['photo'] = $this->obj->photo();
         else if ($this->xmlobject && ($photo_name = $this->xmlobject->pictureAttachmentName()))
             $object['photo'] = $photo_name;
 
         // relateds -> spouse, children
         $this->read_relateds($this->obj->relateds(), $object, 'related');
 
         // crypto settings: currently only key values are supported
         $keys = $this->obj->keys();
         for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
             $key = $keys->get($i);
             if ($key->type() == Key::PGP)
                 $object['pgppublickey'] = $key->key();
             else if ($key->type() == Key::PKCS7_MIME)
                 $object['pkcs7publickey'] = $key->key();
         }
 
         $this->data = $object;
         return $this->data;
     }
 
     /**
      * Callback for kolab_storage_cache to get words to index for fulltext search
      *
      * @return array List of words to save in cache
      */
     public function get_words()
     {
         $data = '';
         foreach (self::$fulltext_cols as $colname) {
-            list($col, $field) = explode(':', $colname);
+            list($col, $field) = array_pad(explode(':', $colname), 2, null);
 
             if ($field) {
                 $a = array();
-                foreach ((array)$this->data[$col] as $attr)
+                foreach ((array)($this->data[$col] ?? []) as $attr)
                     $a[] = $attr[$field];
                 $val = join(' ', $a);
             }
             else {
-                $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
+                $val = is_array($this->data[$col] ?? null) ? join(' ', $this->data[$col] ?? null) : ($this->data[$col] ?? null);
             }
 
             if (strlen($val))
                 $data .= $val . ' ';
         }
 
         return array_unique(rcube_utils::normalize_string($data, true));
     }
 
     /**
      * Callback for kolab_storage_cache to get object specific tags to cache
      *
      * @return array List of tags to save in cache
      */
     public function get_tags()
     {
         $tags = array();
 
         if (!empty($this->data['birthday'])) {
             $tags[] = 'x-has-birthday';
         }
 
         return $tags;
     }
 
     /**
      * Helper method to copy contents of an Address vector to the contact data object
      */
     private function read_addresses($addresses, &$object, $type = null)
     {
         $adrtypes = array_flip($this->addresstypes);
 
         for ($i=0; $i < $addresses->size(); $i++) {
             $adr = $addresses->get($i);
             $object['address'][] = array(
                 'type'     => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
                 'street'   => $adr->street(),
                 'code'     => $adr->code(),
                 'locality' => $adr->locality(),
                 'region'   => $adr->region(),
                 'country'  => $adr->country()
             );
         }
     }
 
     /**
      * Helper method to map contents of a Related vector to the contact data object
      */
     private function read_relateds($rels, &$object, $catchall = null)
     {
         $typemap = array_flip($this->relatedmap);
 
         for ($i=0; $i < $rels->size(); $i++) {
             $rel = $rels->get($i);
             if ($rel->type() != Related::Text)  // we can't handle UID relations yet
                 continue;
 
             $known = false;
             $types = $rel->relationTypes();
             foreach ($typemap as $t => $field) {
                 if ($types & $t) {
                     $object[$field][] = $rel->text();
                     $known = true;
                     break;
                 }
             }
 
             if (!$known && $catchall) {
                 $object[$catchall][] = $rel->text();
             }
         }
     }
 }
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index f2900810..ad5e8b71 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -1,326 +1,326 @@
 <?php
 
 /**
  * Kolab Event model class
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012, Kolab Systems AG <contact@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_format_event extends kolab_format_xcal
 {
     public $CTYPEv2 = 'application/x-vnd.kolab.event';
 
     public static $scheduling_properties = array('start', 'end', 'allday', 'recurrence', 'location', 'status', 'cancelled');
 
     protected $objclass = 'Event';
     protected $read_func = 'readEvent';
     protected $write_func = 'writeEvent';
 
     /**
      * Default constructor
      */
     function __construct($data = null, $version = 3.0)
     {
         parent::__construct(is_string($data) ? $data : null, $version);
 
         // got an Event object as argument
         if (is_object($data) && is_a($data, $this->objclass)) {
             $this->obj = $data;
             $this->loaded = true;
         }
 
         // copy static property overriden by this class
         $this->_scheduling_properties = self::$scheduling_properties;
     }
 
     /**
      * Set event properties to the kolabformat object
      *
      * @param array  Event data as hash array
      */
     public function set(&$object)
     {
         // set common xcal properties
         parent::set($object);
 
         // do the hard work of setting object values
         $this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
         $this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
         $this->obj->setTransparency($object['free_busy'] == 'free');
 
         $status = kolabformat::StatusUndefined;
         if ($object['free_busy'] == 'tentative')
             $status = kolabformat::StatusTentative;
-        if ($object['cancelled'])
+        if ($object['cancelled'] ?? false)
             $status = kolabformat::StatusCancelled;
         else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
             $status = $this->status_map[$object['status']];
         $this->obj->setStatus($status);
 
         // save (recurrence) exceptions
-        if (is_array($object['recurrence']) && is_array($object['recurrence']['EXCEPTIONS']) && !isset($object['exceptions'])) {
+        if (is_array($object['recurrence'] ?? null) && is_array($object['recurrence']['EXCEPTIONS'] ?? null) && !isset($object['exceptions'])) {
             $object['exceptions'] = $object['recurrence']['EXCEPTIONS'];
         }
 
-        if (is_array($object['exceptions'])) {
+        if (is_array($object['exceptions'] ?? null)) {
             $recurrence_id_format = libkolab::recurrence_id_format($object);
             $vexceptions = new vectorevent;
             foreach ($object['exceptions'] as $i => $exception) {
                 $exevent = new kolab_format_event;
                 $exevent->set($compacted = $this->compact_exception($exception, $object));  // only save differing values
 
                 // get value for recurrence-id
                 $recurrence_id = null;
                 if (!empty($exception['recurrence_date']) && $exception['recurrence_date'] instanceof DateTimeInterface) {
                     $recurrence_id = $exception['recurrence_date'];
                     $compacted['_instance'] = $recurrence_id->format($recurrence_id_format);
                 }
                 else if (!empty($exception['_instance']) && strlen($exception['_instance']) > 4) {
                     $recurrence_id = rcube_utils::anytodatetime($exception['_instance'], $object['start']->getTimezone());
                     $compacted['recurrence_date'] = $recurrence_id;
                 }
 
                 $exevent->obj->setRecurrenceID(self::get_datetime($recurrence_id ?: $exception['start'], null,  $object['allday']), (bool)$exception['thisandfuture']);
 
                 $vexceptions->push($exevent->obj);
 
                 // write cleaned-up exception data back to memory/cache
                 $object['exceptions'][$i] = $this->expand_exception($exevent->data, $object);
                 $object['exceptions'][$i]['_instance'] = $compacted['_instance'];
             }
             $this->obj->setExceptions($vexceptions);
 
             // link with recurrence.EXCEPTIONS for compatibility
-            if (is_array($object['recurrence'])) {
+            if (is_array($object['recurrence'] ?? null)) {
                 $object['recurrence']['EXCEPTIONS'] = &$object['exceptions'];
             }
         }
 
-        if ($object['recurrence_date'] && $object['recurrence_date'] instanceof DateTimeInterface) {
-            if ($object['recurrence']) {
+        if (($object['recurrence_date'] ?? false) && $object['recurrence_date'] instanceof DateTimeInterface) {
+            if ($object['recurrence'] ?? false) {
                 // unset recurrence_date for master events with rrule
                 $object['recurrence_date'] = null;
             }
             $this->obj->setRecurrenceID(self::get_datetime($object['recurrence_date'], null, $object['allday']), (bool)$object['thisandfuture']);
         }
 
         // cache this data
         $this->data = $object;
         unset($this->data['_formatobj']);
     }
 
     /**
      *
      */
     public function is_valid()
     {
         return !$this->formaterror && (($this->data && !empty($this->data['start']) && !empty($this->data['end'])) ||
             (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid()));
     }
 
     /**
      * Convert the Event object into a hash array data structure
      *
      * @param array Additional data for merge
      *
      * @return array  Event data as hash array
      */
     public function to_array($data = array())
     {
         // return cached result
         if (!empty($this->data))
             return $this->data;
 
         // read common xcal props
         $object = parent::to_array($data);
 
         // read object properties
         $object += array(
             'end'         => self::php_datetime($this->obj->end()),
             'allday'      => $this->obj->start()->isDateOnly(),
             'free_busy'   => $this->obj->transparency() ? 'free' : 'busy',  // TODO: transparency is only boolean
             'attendees'   => array(),
         );
 
         // derive event end from duration (#1916)
         if (!$object['end'] && $object['start'] && ($duration = $this->obj->duration()) && $duration->isValid()) {
             $interval = new DateInterval('PT0S');
             $interval->d = $duration->weeks() * 7 + $duration->days();
             $interval->h = $duration->hours();
             $interval->i = $duration->minutes();
             $interval->s = $duration->seconds();
             $object['end'] = clone $object['start'];
             $object['end']->add($interval);
         }
         // make sure end date is specified (#5307) RFC5545 3.6.1
         else if (!$object['end'] && $object['start']) {
             $object['end'] = clone $object['start'];
         }
 
         // organizer is part of the attendees list in Roundcube
         if ($object['organizer']) {
             $object['organizer']['role'] = 'ORGANIZER';
             array_unshift($object['attendees'], $object['organizer']);
         }
 
         // status defines different event properties...
         $status = $this->obj->status();
         if ($status == kolabformat::StatusTentative)
           $object['free_busy'] = 'tentative';
         else if ($status == kolabformat::StatusCancelled)
           $object['cancelled'] = true;
 
         // this is an exception object
         if ($this->obj->recurrenceID()->isValid()) {
             $object['thisandfuture'] = $this->obj->thisAndFuture();
             $object['recurrence_date'] = self::php_datetime($this->obj->recurrenceID());
         }
         // read exception event objects
         if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
             $recurrence_exceptions = array();
             $recurrence_id_format = libkolab::recurrence_id_format($object);
             for ($i=0; $i < $exceptions->size(); $i++) {
                 if (($exobj = $exceptions->get($i))) {
                     $exception = new kolab_format_event($exobj);
                     if ($exception->is_valid()) {
                         $exdata = $exception->to_array();
 
                         // fix date-only recurrence ID saved by old versions
                         if ($exdata['recurrence_date'] && $exdata['recurrence_date']->_dateonly && !$object['allday']) {
                             $exdata['recurrence_date']->setTimezone($object['start']->getTimezone());
                             $exdata['recurrence_date']->setTime($object['start']->format('G'), intval($object['start']->format('i')), intval($object['start']->format('s')));
                         }
 
                         $recurrence_id = $exdata['recurrence_date'] ?: $exdata['start'];
                         $exdata['_instance'] = $recurrence_id->format($recurrence_id_format);
                         $recurrence_exceptions[] = $this->expand_exception($exdata, $object);
                     }
                 }
             }
             $object['exceptions'] = $recurrence_exceptions;
 
             // also link with recurrence.EXCEPTIONS for compatibility
-            if (is_array($object['recurrence'])) {
+            if (is_array($object['recurrence'] ?? null)) {
                 $object['recurrence']['EXCEPTIONS'] = &$object['exceptions'];
             }
         }
 
         return $this->data = $object;
     }
 
     /**
      * Getter for a single instance from a recurrence series or stored subcomponents
      *
      * @param mixed The recurrence-id of the requested instance, either as string or a DateTime object
      * @return array Event data as hash array or null if not found
      */
     public function get_instance($recurrence_id)
     {
         $result = null;
         $object = $this->to_array();
 
         $recurrence_id_format = libkolab::recurrence_id_format($object);
         $instance_id = $recurrence_id instanceof DateTimeInterface ? $recurrence_id->format($recurrence_id_format) : strval($recurrence_id);
 
         if ($object['recurrence_date'] instanceof DateTimeInterface) {
             if ($object['recurrence_date']->format($recurrence_id_format) == $instance_id) {
                 $result = $object;
             }
         }
 
         if (!$result && is_array($object['exceptions'])) {
             foreach ($object['exceptions'] as $exception) {
                 if ($exception['_instance'] == $instance_id) {
                     $result = $exception;
                     $result['isexception'] = 1;
                     break;
                 }
             }
         }
 
         // TODO: compute instances from recurrence rule and return the matching instance
         // clone from plugins/calendar/drivers/kolab/kolab_calendar::get_recurring_events()
 
         return $result;
     }
 
     /**
      * Callback for kolab_storage_cache to get object specific tags to cache
      *
      * @return array List of tags to save in cache
      */
     public function get_tags($obj = null)
     {
         $tags = parent::get_tags($obj);
         $object = $obj ?: $this->data;
 
         foreach ((array)$object['categories'] as $cat) {
             $tags[] = rcube_utils::normalize_string($cat);
         }
 
         return array_unique($tags);
     }
 
     /**
      * Remove some attributes from the exception container
      */
     private function compact_exception($exception, $master)
     {
         $forbidden = array('recurrence','exceptions','organizer','_attachments');
 
         foreach ($forbidden as $prop) {
             if (array_key_exists($prop, $exception)) {
                 unset($exception[$prop]);
             }
         }
 
         // preserve this property for date serialization
         if (!isset($exception['allday'])) {
             $exception['allday'] = $master['allday'];
         }
 
         return $exception;
     }
 
     /**
      * Copy attributes not specified by the exception from the master event
      */
     private function expand_exception($exception, $master)
     {
         // Note: If an exception has no attendees it means there's "no attendees
         // for this occurrence", not "attendees are the same as in the event" (#5300)
 
         $forbidden    = array('exceptions', 'attendees', 'allday');
         $is_recurring = !empty($master['recurrence']);
 
         foreach ($master as $prop => $value) {
             if (empty($exception[$prop]) && !empty($value) && $prop[0] != '_'
                 && !in_array($prop, $forbidden)
                 && ($is_recurring || in_array($prop, array('uid','organizer')))
             ) {
                 $exception[$prop] = $value;
                 if ($prop == 'recurrence') {
                     unset($exception[$prop]['EXCEPTIONS']);
                 }
             }
         }
 
         return $exception;
     }
 }
diff --git a/plugins/libkolab/lib/kolab_format_file.php b/plugins/libkolab/lib/kolab_format_file.php
index 8d9ddbd2..ba182d3c 100644
--- a/plugins/libkolab/lib/kolab_format_file.php
+++ b/plugins/libkolab/lib/kolab_format_file.php
@@ -1,146 +1,146 @@
 <?php
 
 /**
  * Kolab File model class
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2012, Kolab Systems AG <contact@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_format_file extends kolab_format
 {
     public $CTYPE = 'application/vnd.kolab+xml';
 
     protected $objclass = 'File';
     protected $read_func = 'kolabformat::readKolabFile';
     protected $write_func = 'kolabformat::writeKolabFile';
 
     /**
      * Set properties to the kolabformat object
      *
      * @param array  Object data as hash array
      */
     public function set(&$object)
     {
         // set common object properties
         parent::set($object);
 
-        $this->obj->setCategories(self::array2vector($object['categories']));
+        $this->obj->setCategories(self::array2vector($object['categories'] ?? null));
 
         if (isset($object['notes'])) {
             $this->obj->setNote($object['notes']);
         }
 
         // Add file attachment
-        if (!empty($object['_attachments'])) {
+        if (!empty($object['_attachments'] ?? null)) {
             $cid         = key($object['_attachments']);
             $attach_attr = $object['_attachments'][$cid];
             $attach      = new Attachment;
 
             $attach->setLabel((string)$attach_attr['name']);
             $attach->setUri('cid:' . $cid, $attach_attr['mimetype']);
             $this->obj->setFile($attach);
 
             // make sure size is set, so object saved in cache contains this info
             if (!isset($attach_attr['size'])) {
                 $size = 0;
 
                 if (!empty($attach_attr['content'])) {
                     if (is_resource($attach_attr['content'])) {
                         $stat = fstat($attach_attr['content']);
                         $size = $stat ? $stat['size'] : 0;
                     }
                     else {
                         $size = strlen($attach_attr['content']);
                     }
                 }
                 else if (isset($attach_attr['path'])) {
                     $size = @filesize($attach_attr['path']);
                 }
 
                 $object['_attachments'][$cid]['size'] = $size;
             }
         }
 
         // cache this data
         $this->data = $object;
         unset($this->data['_formatobj']);
     }
 
     /**
      * Check if object's data validity
      */
     public function is_valid()
     {
         return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
     }
 
     /**
      * Convert the Configuration object into a hash array data structure
      *
      * @param array Additional data for merge
      *
      * @return array  Config object data as hash array
      */
     public function to_array($data = array())
     {
         // return cached result
         if (!empty($this->data)) {
             return $this->data;
         }
 
         // read common object props into local data object
         $object = parent::to_array($data);
 
         // read object properties
         $object += array(
             'categories'  => self::vector2array($this->obj->categories()),
             'notes'       => $this->obj->note(),
         );
 
         return $this->data = $object;
     }
 
     /**
      * Callback for kolab_storage_cache to get object specific tags to cache
      *
      * @return array List of tags to save in cache
      */
     public function get_tags()
     {
         $tags = array();
 
-        foreach ((array)$this->data['categories'] as $cat) {
+        foreach ((array)($this->data['categories'] ?? null) as $cat) {
             $tags[] = rcube_utils::normalize_string($cat);
         }
 
         // Add file mimetype to tags
-        if (!empty($this->data['_attachments'])) {
+        if (!empty($this->data['_attachments'] ?? null)) {
             reset($this->data['_attachments']);
             $key        = key($this->data['_attachments']);
             $attachment = $this->data['_attachments'][$key];
 
-            if ($attachment['mimetype']) {
+            if ($attachment['mimetype'] ?? false) {
                 $tags[] = $attachment['mimetype'];
             }
         }
 
         return $tags;
     }
 }
diff --git a/plugins/libkolab/lib/kolab_format_note.php b/plugins/libkolab/lib/kolab_format_note.php
index b7c23e2b..b279eb23 100644
--- a/plugins/libkolab/lib/kolab_format_note.php
+++ b/plugins/libkolab/lib/kolab_format_note.php
@@ -1,143 +1,143 @@
 <?php
 
 /**
  * Kolab Note model class
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012, Kolab Systems AG <contact@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_format_note extends kolab_format
 {
     public $CTYPE = 'application/vnd.kolab+xml';
     public $CTYPEv2 = 'application/x-vnd.kolab.note';
 
     public static $fulltext_cols = array('title', 'description', 'categories');
 
     protected $objclass = 'Note';
     protected $read_func = 'readNote';
     protected $write_func = 'writeNote';
 
     /**
      * Set properties to the kolabformat object
      *
      * @param array  Object data as hash array
      */
     public function set(&$object)
     {
         // set common object properties
         parent::set($object);
 
         $this->obj->setSummary($object['title']);
-        $this->obj->setDescription($object['description']);
-        $this->obj->setCategories(self::array2vector($object['categories']));
+        $this->obj->setDescription($object['description'] ?? null);
+        $this->obj->setCategories(self::array2vector($object['categories'] ?? null));
 
         $this->set_attachments($object);
 
         // cache this data
         $this->data = $object;
         unset($this->data['_formatobj']);
     }
 
     /**
      *
      */
     public function is_valid()
     {
         return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
     }
 
     /**
      * Convert the Configuration object into a hash array data structure
      *
      * @param array Additional data for merge
      *
      * @return array  Config object data as hash array
      */
     public function to_array($data = array())
     {
         // return cached result
         if (!empty($this->data))
             return $this->data;
 
         // read common object props into local data object
         $object = parent::to_array($data);
 
         // read object properties
         $object += array(
             'categories'  => self::vector2array($this->obj->categories()),
             'title'       => $this->obj->summary(),
             'description' => $this->obj->description(),
         );
 
         $this->get_attachments($object);
 
         return $this->data = $object;
     }
 
     /**
      * Callback for kolab_storage_cache to get object specific tags to cache
      *
      * @return array List of tags to save in cache
      */
     public function get_tags()
     {
         $tags = array();
 
-        foreach ((array)$this->data['categories'] as $cat) {
+        foreach ((array)($this->data['categories'] ?? null) as $cat) {
             $tags[] = rcube_utils::normalize_string($cat);
         }
 
         // add tag for message references
-        foreach ((array)$this->data['links'] as $link) {
+        foreach ((array)($this->data['links'] ?? []) as $link) {
             $url = parse_url($link);
             if ($url['scheme'] == 'imap') {
                 parse_str($url['query'], $param);
                 $tags[] = 'ref:' . trim($param['message-id'] ?: urldecode($url['fragment']), '<> ');
             }
         }
 
         return $tags;
     }
 
     /**
      * Callback for kolab_storage_cache to get words to index for fulltext search
      *
      * @return array List of words to save in cache
      */
     public function get_words()
     {
         $data = '';
         foreach (self::$fulltext_cols as $col) {
             // convert HTML content to plain text
             if ($col == 'description' && preg_match('/<(html|body)(\s[a-z]|>)/', $this->data[$col], $m) && strpos($this->data[$col], '</'.$m[1].'>')) {
-                $converter = new rcube_html2text($this->data[$col], false, false, 0);
+                $converter = new rcube_html2text($this->data[$col] ?? null, false, false, 0);
                 $val = $converter->get_text();
             }
             else {
-                $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
+                $val = is_array($this->data[$col] ?? null) ? join(' ', $this->data[$col] ?? null) : ($this->data[$col] ?? null);
             }
 
             if (strlen($val))
                 $data .= $val . ' ';
         }
 
         return array_filter(array_unique(rcube_utils::normalize_string($data, true)));
     }
 
 }
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index 23ba2cc2..25fc6ace 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -1,154 +1,154 @@
 <?php
 
 /**
  * Kolab Task (ToDo) model class
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012, Kolab Systems AG <contact@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_format_task extends kolab_format_xcal
 {
     public $CTYPEv2 = 'application/x-vnd.kolab.task';
 
     public static $scheduling_properties = array('start', 'due', 'summary', 'status');
 
     protected $objclass = 'Todo';
     protected $read_func = 'readTodo';
     protected $write_func = 'writeTodo';
 
     /**
      * Default constructor
      */
     function __construct($data = null, $version = 3.0)
     {
         parent::__construct(is_string($data) ? $data : null, $version);
 
         // copy static property overriden by this class
         $this->_scheduling_properties = self::$scheduling_properties;
     }
 
     /**
      * Set properties to the kolabformat object
      *
      * @param array  Object data as hash array
      */
     public function set(&$object)
     {
         // set common xcal properties
         parent::set($object);
 
         $this->obj->setPercentComplete(intval($object['complete']));
 
         $status = kolabformat::StatusUndefined;
         if ($object['complete'] == 100 && !array_key_exists('status', $object))
             $status = kolabformat::StatusCompleted;
         else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
             $status = $this->status_map[$object['status']];
         $this->obj->setStatus($status);
 
-        $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
-        $this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
+        $this->obj->setStart(self::get_datetime($object['start'] ?? null, null, ($object['start'] ?? null) ? $object['start']->_dateonly : null));
+        $this->obj->setDue(self::get_datetime($object['due'] ?? null, null, ($object['due'] ?? null) ? $object['due']->_dateonly : null));
 
         $related = new vectors;
         if (!empty($object['parent_id']))
             $related->push($object['parent_id']);
         $this->obj->setRelatedTo($related);
 
         // cache this data
         $this->data = $object;
         unset($this->data['_formatobj']);
     }
 
     /**
      *
      */
     public function is_valid()
     {
         return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
     }
 
     /**
      * Convert the Configuration object into a hash array data structure
      *
      * @param array Additional data for merge
      *
      * @return array  Config object data as hash array
      */
     public function to_array($data = array())
     {
         // return cached result
         if (!empty($this->data))
             return $this->data;
 
         // read common xcal props
         $object = parent::to_array($data);
 
         $object['complete'] = intval($this->obj->percentComplete());
 
         // if due date is set
         if ($due = $this->obj->due())
             $object['due'] = self::php_datetime($due);
 
         // related-to points to parent task; we only support one relation
         $related = self::vector2array($this->obj->relatedTo());
         if (count($related))
             $object['parent_id'] = $related[0];
 
         // TODO: map more properties
 
         $this->data = $object;
         return $this->data;
     }
 
     /**
      * Return the reference date for recurrence and alarms
      *
      * @return mixed DateTime instance of null if no refdate is available
      */
     public function get_reference_date()
     {
         if ($this->data['due'] && $this->data['due'] instanceof DateTimeInterface) {
             return $this->data['due'];
         }
 
         return self::php_datetime($this->obj->due()) ?: parent::get_reference_date();
     }
 
     /**
      * Callback for kolab_storage_cache to get object specific tags to cache
      *
      * @return array List of tags to save in cache
      */
     public function get_tags($obj = null)
     {
         $tags = parent::get_tags($obj);
         $object = $obj ?: $this->data;
 
-        if ($object['status'] == 'COMPLETED' || ($object['complete'] == 100 && empty($object['status'])))
+        if (($object['status'] ?? null) == 'COMPLETED' || (($object['complete'] ?? null) == 100 && empty($object['status'] ?? null)))
             $tags[] = 'x-complete';
 
-        if ($object['priority'] == 1)
+        if (($object['priority'] ?? 0) == 1)
             $tags[] = 'x-flagged';
 
-        if ($object['parent_id'])
+        if ($object['parent_id'] ?? false)
             $tags[] = 'x-parent:' . $object['parent_id'];
 
         return array_unique($tags);
     }
 }
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 72d8b5ed..b8b4513f 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -1,775 +1,775 @@
 <?php
 
 /**
  * Xcal based Kolab format class wrapping libkolabxml bindings
  *
  * Base class for xcal-based Kolab groupware objects such as event, todo, journal
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012, Kolab Systems AG <contact@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_format_xcal extends kolab_format
 {
     public $CTYPE = 'application/calendar+xml';
 
     public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
 
     public static $scheduling_properties = array('start', 'end', 'location');
 
     protected $_scheduling_properties = null;
 
     protected $role_map = array(
         'REQ-PARTICIPANT' => kolabformat::Required,
         'OPT-PARTICIPANT' => kolabformat::Optional,
         'NON-PARTICIPANT' => kolabformat::NonParticipant,
         'CHAIR' => kolabformat::Chair,
     );
 
     protected $cutype_map = array(
         'INDIVIDUAL' => kolabformat::CutypeIndividual,
         'GROUP'      => kolabformat::CutypeGroup,
         'ROOM'       => kolabformat::CutypeRoom,
         'RESOURCE'   => kolabformat::CutypeResource,
         'UNKNOWN'    => kolabformat::CutypeUnknown,
     );
 
     protected $rrule_type_map = array(
         'MINUTELY' => RecurrenceRule::Minutely,
         'HOURLY' => RecurrenceRule::Hourly,
         'DAILY' => RecurrenceRule::Daily,
         'WEEKLY' => RecurrenceRule::Weekly,
         'MONTHLY' => RecurrenceRule::Monthly,
         'YEARLY' => RecurrenceRule::Yearly,
     );
 
     protected $weekday_map = array(
         'MO' => kolabformat::Monday,
         'TU' => kolabformat::Tuesday,
         'WE' => kolabformat::Wednesday,
         'TH' => kolabformat::Thursday,
         'FR' => kolabformat::Friday,
         'SA' => kolabformat::Saturday,
         'SU' => kolabformat::Sunday,
     );
 
     protected $alarm_type_map = array(
         'DISPLAY' => Alarm::DisplayAlarm,
         'EMAIL' => Alarm::EMailAlarm,
         'AUDIO' => Alarm::AudioAlarm,
     );
 
     protected $status_map = array(
         'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
         'IN-PROCESS'   => kolabformat::StatusInProcess,
         'COMPLETED'    => kolabformat::StatusCompleted,
         'CANCELLED'    => kolabformat::StatusCancelled,
         'TENTATIVE'    => kolabformat::StatusTentative,
         'CONFIRMED'    => kolabformat::StatusConfirmed,
         'DRAFT'        => kolabformat::StatusDraft,
         'FINAL'        => kolabformat::StatusFinal,
     );
 
     protected $part_status_map = array(
         'UNKNOWN'      => kolabformat::PartNeedsAction,
         'NEEDS-ACTION' => kolabformat::PartNeedsAction,
         'TENTATIVE'    => kolabformat::PartTentative,
         'ACCEPTED'     => kolabformat::PartAccepted,
         'DECLINED'     => kolabformat::PartDeclined,
         'DELEGATED'    => kolabformat::PartDelegated,
         'IN-PROCESS'   => kolabformat::PartInProcess,
         'COMPLETED'    => kolabformat::PartCompleted,
       );
 
 
     /**
      * Convert common xcard properties into a hash array data structure
      *
      * @param array Additional data for merge
      *
      * @return array  Object data as hash array
      */
     public function to_array($data = array())
     {
         // read common object props
         $object = parent::to_array($data);
 
         $status_map = array_flip($this->status_map);
 
         $object += array(
             'sequence'    => intval($this->obj->sequence()),
             'title'       => $this->obj->summary(),
             'location'    => $this->obj->location(),
             'description' => $this->obj->description(),
             'url'         => $this->obj->url(),
-            'status'      => $status_map[$this->obj->status()],
+            'status'      => $status_map[$this->obj->status()] ?? null,
             'priority'    => $this->obj->priority(),
             'categories'  => self::vector2array($this->obj->categories()),
             'start'       => self::php_datetime($this->obj->start()),
         );
 
         if (method_exists($this->obj, 'comment')) {
             $object['comment'] = $this->obj->comment();
         }
 
         // read organizer and attendees
         if (($organizer = $this->obj->organizer()) && ($organizer->email() || $organizer->name())) {
             $object['organizer'] = array(
                 'email' => $organizer->email(),
                 'name' => $organizer->name(),
             );
         }
 
         $role_map = array_flip($this->role_map);
         $cutype_map = array_flip($this->cutype_map);
         $part_status_map = array_flip($this->part_status_map);
         $attvec = $this->obj->attendees();
         for ($i=0; $i < $attvec->size(); $i++) {
             $attendee = $attvec->get($i);
             $cr = $attendee->contact();
             if ($cr->email() != $object['organizer']['email']) {
                 $delegators = $delegatees = array();
                 $vdelegators = $attendee->delegatedFrom();
                 for ($j=0; $j < $vdelegators->size(); $j++) {
                     $delegators[] = $vdelegators->get($j)->email();
                 }
                 $vdelegatees = $attendee->delegatedTo();
                 for ($j=0; $j < $vdelegatees->size(); $j++) {
                     $delegatees[] = $vdelegatees->get($j)->email();
                 }
 
                 $object['attendees'][] = array(
                     'role' => $role_map[$attendee->role()],
                     'cutype' => $cutype_map[$attendee->cutype()],
                     'status' => $part_status_map[$attendee->partStat()],
                     'rsvp' => $attendee->rsvp(),
                     'email' => $cr->email(),
                     'name' => $cr->name(),
                     'delegated-from' => $delegators,
                     'delegated-to' => $delegatees,
                 );
             }
         }
 
         if ($object['start'] instanceof DateTimeInterface) {
             $start_tz = $object['start']->getTimezone();
         }
 
         // read recurrence rule
         if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
             $rrule_type_map = array_flip($this->rrule_type_map);
             $object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
 
             if ($intvl = $rr->interval())
                 $object['recurrence']['INTERVAL'] = $intvl;
 
             if (($count = $rr->count()) && $count > 0) {
                 $object['recurrence']['COUNT'] = $count;
             }
             else if ($until = self::php_datetime($rr->end(), $start_tz)) {
                 $refdate = $this->get_reference_date();
                 if ($refdate && $refdate instanceof DateTimeInterface && empty($refdate->_dateonly)) {
                     $until->setTime($refdate->format('G'), $refdate->format('i'), 0);
                 }
                 $object['recurrence']['UNTIL'] = $until;
             }
 
             if (($byday = $rr->byday()) && $byday->size()) {
                 $weekday_map = array_flip($this->weekday_map);
                 $weekdays = array();
                 for ($i=0; $i < $byday->size(); $i++) {
                     $daypos = $byday->get($i);
                     $prefix = $daypos->occurence();
                     $weekdays[] = ($prefix ?: '') . $weekday_map[$daypos->weekday()];
                 }
                 $object['recurrence']['BYDAY'] = join(',', $weekdays);
             }
 
             if (($bymday = $rr->bymonthday()) && $bymday->size()) {
                 $object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
             }
 
             if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
                 $object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
             }
 
             if ($exdates = $this->obj->exceptionDates()) {
                 for ($i=0; $i < $exdates->size(); $i++) {
                     if ($exdate = self::php_datetime($exdates->get($i), $start_tz)) {
                         $object['recurrence']['EXDATE'][] = $exdate;
                     }
                 }
             }
         }
 
         if ($rdates = $this->obj->recurrenceDates()) {
             for ($i=0; $i < $rdates->size(); $i++) {
                 if ($rdate = self::php_datetime($rdates->get($i), $start_tz)) {
                     $object['recurrence']['RDATE'][] = $rdate;
                 }
             }
         }
 
         // read alarm
         $valarms = $this->obj->alarms();
         $alarm_types = array_flip($this->alarm_type_map);
         $object['valarms'] = array();
         for ($i=0; $i < $valarms->size(); $i++) {
             $alarm = $valarms->get($i);
             $type  = $alarm_types[$alarm->type()];
 
             if ($type == 'DISPLAY' || $type == 'EMAIL' || $type == 'AUDIO') {  // only some alarms are supported
                 $valarm = array(
                     'action'      => $type,
                     'summary'     => $alarm->summary(),
                     'description' => $alarm->description(),
                 );
 
                 if ($type == 'EMAIL') {
                     $valarm['attendees'] = array();
                     $attvec = $alarm->attendees();
                     for ($j=0; $j < $attvec->size(); $j++) {
                         $cr = $attvec->get($j);
                         $valarm['attendees'][] = $cr->email();
                     }
                 }
                 else if ($type == 'AUDIO') {
                     $attach = $alarm->audioFile();
                     $valarm['uri'] = $attach->uri();
                 }
 
                 if ($start = self::php_datetime($alarm->start())) {
                     $object['alarms']  = '@' . $start->format('U');
                     $valarm['trigger'] = $start;
                 }
                 else if ($offset = $alarm->relativeStart()) {
                     $prefix = $offset->isNegative() ? '-' : '+';
                     $value  = '';
                     $time   = '';
 
                     if      ($w = $offset->weeks())     $value .= $w . 'W';
                     else if ($d = $offset->days())      $value .= $d . 'D';
                     else if ($h = $offset->hours())     $time  .= $h . 'H';
                     else if ($m = $offset->minutes())   $time  .= $m . 'M';
                     else if ($s = $offset->seconds())   $time  .= $s . 'S';
 
                     // assume 'at event time'
                     if (empty($value) && empty($time)) {
                         $prefix = '';
                         $time   = '0S';
                     }
 
                     $object['alarms']  = $prefix . $value . $time;
                     $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : '');
 
                     if ($alarm->relativeTo() == kolabformat::End) {
                         $valarm['related'] = 'END';
                     }
                 }
 
                 // read alarm duration and repeat properties
                 if (($duration = $alarm->duration()) && $duration->isValid()) {
                     $value = $time = '';
 
                     if      ($w = $duration->weeks())     $value .= $w . 'W';
                     else if ($d = $duration->days())      $value .= $d . 'D';
                     else if ($h = $duration->hours())     $time  .= $h . 'H';
                     else if ($m = $duration->minutes())   $time  .= $m . 'M';
                     else if ($s = $duration->seconds())   $time  .= $s . 'S';
 
                     $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : '');
                     $valarm['repeat']   = $alarm->numrepeat();
                 }
 
                 $object['alarms']  .= ':' . $type;  // legacy property
                 $object['valarms'][] = array_filter($valarm);
             }
         }
 
         $this->get_attachments($object);
 
         return $object;
     }
 
 
     /**
      * Set common xcal properties to the kolabformat object
      *
      * @param array  Event data as hash array
      */
     public function set(&$object)
     {
         $this->init();
 
         $is_new = !$this->obj->uid();
         $old_sequence = $this->obj->sequence();
         $reschedule = $is_new;
 
         // set common object properties
         parent::set($object);
 
         // set sequence value
         if (!isset($object['sequence'])) {
             if ($is_new) {
                 $object['sequence'] = 0;
             }
             else {
                 $object['sequence'] = $old_sequence;
 
                 // increment sequence when updating properties relevant for scheduling.
                 // RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
                 if ($this->check_rescheduling($object)) {
                     $object['sequence']++;
                 }
             }
         }
         $this->obj->setSequence(intval($object['sequence']));
 
         if ($object['sequence'] > $old_sequence) {
             $reschedule = true;
         }
 
         $this->obj->setSummary($object['title']);
-        $this->obj->setLocation($object['location']);
+        $this->obj->setLocation($object['location'] ?? null);
         $this->obj->setDescription($object['description']);
         $this->obj->setPriority($object['priority']);
-        $this->obj->setCategories(self::array2vector($object['categories']));
-        $this->obj->setUrl(strval($object['url']));
+        $this->obj->setCategories(self::array2vector($object['categories'] ?? null));
+        $this->obj->setUrl(strval($object['url'] ?? null));
 
         if (method_exists($this->obj, 'setComment')) {
-            $this->obj->setComment($object['comment']);
+            $this->obj->setComment($object['comment'] ?? null);
         }
 
         // process event attendees
         $attendees = new vectorattendee;
-        foreach ((array)$object['attendees'] as $i => $attendee) {
+        foreach ((array)($object['attendees'] ?? []) as $i => $attendee) {
             if ($attendee['role'] == 'ORGANIZER') {
                 $object['organizer'] = $attendee;
             }
             else if ($attendee['email'] != $object['organizer']['email']) {
                 $cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
                 $cr->setName($attendee['name']);
 
                 // set attendee RSVP if missing
                 if (!isset($attendee['rsvp'])) {
                     $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = $reschedule;
                 }
 
                 $att = new Attendee;
                 $att->setContact($cr);
                 $att->setPartStat($this->part_status_map[$attendee['status']]);
                 $att->setRole($this->role_map[$attendee['role']] ?: kolabformat::Required);
                 $att->setCutype($this->cutype_map[$attendee['cutype']] ?: kolabformat::CutypeIndividual);
                 $att->setRSVP((bool)$attendee['rsvp']);
 
                 if (!empty($attendee['delegated-from'])) {
                     $vdelegators = new vectorcontactref;
                     foreach ((array)$attendee['delegated-from'] as $delegator) {
                         $vdelegators->push(new ContactReference(ContactReference::EmailReference, $delegator));
                     }
                     $att->setDelegatedFrom($vdelegators);
                 }
                 if (!empty($attendee['delegated-to'])) {
                     $vdelegatees = new vectorcontactref;
                     foreach ((array)$attendee['delegated-to'] as $delegatee) {
                         $vdelegatees->push(new ContactReference(ContactReference::EmailReference, $delegatee));
                     }
                     $att->setDelegatedTo($vdelegatees);
                 }
 
                 if ($att->isValid()) {
                     $attendees->push($att);
                 }
                 else {
                     rcube::raise_error(array(
                         'code' => 600, 'type' => 'php',
                         'file' => __FILE__, 'line' => __LINE__,
                         'message' => "Invalid event attendee: " . json_encode($attendee),
                     ), true);
                 }
             }
         }
         $this->obj->setAttendees($attendees);
 
         if ($object['organizer']) {
             $organizer = new ContactReference(ContactReference::EmailReference, $object['organizer']['email']);
             $organizer->setName($object['organizer']['name']);
             $this->obj->setOrganizer($organizer);
         }
 
-        if ($object['start'] instanceof DateTimeInterface) {
+        if (($object['start'] ?? null) instanceof DateTimeInterface) {
             $start_tz = $object['start']->getTimezone();
         }
 
         // save recurrence rule
         $rr = new RecurrenceRule;
         $rr->setFrequency(RecurrenceRule::FreqNone);
 
-        if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
+        if (($object['recurrence'] ?? null) && !empty($object['recurrence']['FREQ'])) {
             $freq     = $object['recurrence']['FREQ'];
             $bysetpos = explode(',', $object['recurrence']['BYSETPOS']);
 
             $rr->setFrequency($this->rrule_type_map[$freq]);
 
             if ($object['recurrence']['INTERVAL'])
                 $rr->setInterval(intval($object['recurrence']['INTERVAL']));
 
             if ($object['recurrence']['BYDAY']) {
                 $byday = new vectordaypos;
                 foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
                     $occurrence = 0;
                     if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
                         $occurrence = intval($m[1]);
                         $day = $m[2];
                     }
 
                     if (isset($this->weekday_map[$day])) {
                         // @TODO: libkolabxml does not support BYSETPOS, neither we.
                         // However, we can convert most common cases to BYDAY
                         if (!$occurrence && $freq == 'MONTHLY' && !empty($bysetpos)) {
                             foreach ($bysetpos as $pos) {
                                 $byday->push(new DayPos(intval($pos), $this->weekday_map[$day]));
                             }
                         }
                         else {
                             $byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
                         }
                     }
                 }
                 $rr->setByday($byday);
             }
 
             if ($object['recurrence']['BYMONTHDAY']) {
                 $bymday = new vectori;
                 foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
                     $bymday->push(intval($day));
                 $rr->setBymonthday($bymday);
             }
 
             if ($object['recurrence']['BYMONTH']) {
                 $bymonth = new vectori;
                 foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
                     $bymonth->push(intval($month));
                 $rr->setBymonth($bymonth);
             }
 
             if ($object['recurrence']['COUNT'])
                 $rr->setCount(intval($object['recurrence']['COUNT']));
             else if ($object['recurrence']['UNTIL'])
                 $rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true, $start_tz));
 
             if ($rr->isValid()) {
                 // add exception dates (only if recurrence rule is valid)
                 $exdates = new vectordatetime;
                 foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
                     $exdates->push(self::get_datetime($exdate, null, true, $start_tz));
                 $this->obj->setExceptionDates($exdates);
             }
             else {
                 rcube::raise_error(array(
                     'code' => 600, 'type' => 'php',
                     'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
                 ), true);
             }
         }
 
         $this->obj->setRecurrenceRule($rr);
 
         // save recurrence dates (aka RDATE)
         if (!empty($object['recurrence']['RDATE'])) {
             $rdates = new vectordatetime;
             foreach ((array)$object['recurrence']['RDATE'] as $rdate)
                 $rdates->push(self::get_datetime($rdate, null, true, $start_tz));
             $this->obj->setRecurrenceDates($rdates);
         }
 
         // save alarm(s)
         $valarms = new vectoralarm;
         $valarm_hashes = array();
-        if ($object['valarms']) {
+        if ($object['valarms'] ?? null) {
             foreach ($object['valarms'] as $valarm) {
                 if (!array_key_exists($valarm['action'], $this->alarm_type_map)) {
                     continue;  // skip unknown alarm types
                 }
 
                 // Get rid of duplicates, some CalDAV clients can set them
                 $hash = serialize($valarm);
                 if (in_array($hash, $valarm_hashes)) {
                     continue;
                 }
                 $valarm_hashes[] = $hash;
 
                 if ($valarm['action'] == 'EMAIL') {
                     $recipients = new vectorcontactref;
                     foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) {
                         $recipients->push(new ContactReference(ContactReference::EmailReference, $email));
                     }
                     $alarm = new Alarm(
                         strval($valarm['summary'] ?: $object['title']),
                         strval($valarm['description'] ?: $object['description']),
                         $recipients
                     );
                 }
                 else if ($valarm['action'] == 'AUDIO') {
                     $attach = new Attachment;
                     $attach->setUri($valarm['uri'] ?: 'null', 'unknown');
                     $alarm = new Alarm($attach);
                 }
                 else {
                     // action == DISPLAY
                     $alarm = new Alarm(strval($valarm['summary'] ?: $object['title']));
                 }
 
                 if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTimeInterface) {
                     $alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC')));
                 }
                 else if (preg_match('/^@([0-9]+)$/', $valarm['trigger'], $m)) {
                     $alarm->setStart(self::get_datetime($m[1], new DateTimeZone('UTC')));
                 }
                 else {
                     // Support also interval in format without PT, e.g. -10M
                     if (preg_match('/^([-+]*)([0-9]+[DHMS])$/', strtoupper($valarm['trigger']), $m)) {
                         $valarm['trigger'] = $m[1] . ($m[2][strlen($m[2])-1] == 'D' ? 'P' : 'PT') . $m[2];
                     }
 
                     try {
                         $period   = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger']));
                         $duration = new Duration($period->d, $period->h, $period->i, $period->s, $valarm['trigger'][0] == '-');
                     }
                     catch (Exception $e) {
                         // skip alarm with invalid trigger values
                         rcube::raise_error($e, true);
                         continue;
                     }
 
                     $related = strtoupper($valarm['related']) == 'END' ? kolabformat::End : kolabformat::Start;
                     $alarm->setRelativeStart($duration, $related);
                 }
 
                 if ($valarm['duration']) {
                     try {
                         $d = new DateInterval($valarm['duration']);
                         $duration = new Duration($d->d, $d->h, $d->i, $d->s);
                         $alarm->setDuration($duration, intval($valarm['repeat']));
                     }
                     catch (Exception $e) {
                         // ignore
                     }
                 }
 
                 $valarms->push($alarm);
             }
         }
         // legacy support
-        else if ($object['alarms']) {
+        else if ($object['alarms'] ?? null) {
             list($offset, $type) = explode(":", $object['alarms']);
 
             if ($type == 'EMAIL' && !empty($object['_owner'])) {  // email alarms implicitly go to event owner
                 $recipients = new vectorcontactref;
                 $recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
                 $alarm = new Alarm($object['title'], strval($object['description']), $recipients);
             }
             else {  // default: display alarm
                 $alarm = new Alarm($object['title']);
             }
 
             if (preg_match('/^@(\d+)/', $offset, $d)) {
                 $alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
             }
             else if (preg_match('/^([-+]?)P?T?(\d+)([SMHDW])/', $offset, $d)) {
                 $days = $hours = $minutes = $seconds = 0;
                 switch ($d[3]) {
                     case 'W': $days  = 7*intval($d[2]); break;
                     case 'D': $days    = intval($d[2]); break;
                     case 'H': $hours   = intval($d[2]); break;
                     case 'M': $minutes = intval($d[2]); break;
                     case 'S': $seconds = intval($d[2]); break;
                 }
                 $alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
             }
 
             $valarms->push($alarm);
         }
         $this->obj->setAlarms($valarms);
 
         $this->set_attachments($object);
     }
 
     /**
      * Return the reference date for recurrence and alarms
      *
      * @return mixed DateTime instance of null if no refdate is available
      */
     public function get_reference_date()
     {
         if ($this->data['start'] && $this->data['start'] instanceof DateTimeInterface) {
             return $this->data['start'];
         }
 
         return self::php_datetime($this->obj->start());
     }
 
     /**
      * Callback for kolab_storage_cache to get words to index for fulltext search
      *
      * @return array List of words to save in cache
      */
     public function get_words($obj = null)
     {
         $data = '';
         $object = $obj ?: $this->data;
 
         foreach (self::$fulltext_cols as $colname) {
-            list($col, $field) = explode(':', $colname);
+            list($col, $field) = array_pad(explode(':', $colname), 2, null);
 
             if ($field) {
                 $a = array();
-                foreach ((array)$object[$col] as $attr)
+                foreach ((array)($object[$col] ?? []) as $attr)
                     $a[] = $attr[$field];
                 $val = join(' ', $a);
             }
             else {
-                $val = is_array($object[$col]) ? join(' ', $object[$col]) : $object[$col];
+                $val = is_array($object[$col] ?? null) ? join(' ', $object[$col]) : $object[$col] ?? null;
             }
 
             if (strlen($val))
                 $data .= $val . ' ';
         }
 
         $words = rcube_utils::normalize_string($data, true);
 
         // collect words from recurrence exceptions
-        if (is_array($object['exceptions'])) {
+        if (is_array($object['exceptions'] ?? null)) {
             foreach ($object['exceptions'] as $exception) {
                 $words = array_merge($words, $this->get_words($exception));
             }
         }
 
         return array_unique($words);
     }
 
     /**
      * Callback for kolab_storage_cache to get object specific tags to cache
      *
      * @return array List of tags to save in cache
      */
     public function get_tags($obj = null)
     {
         $tags = array();
         $object = $obj ?: $this->data;
 
-        if (!empty($object['valarms'])) {
+        if (!empty($object['valarms'] ?? null)) {
             $tags[] = 'x-has-alarms';
         }
 
         // create tags reflecting participant status
-        if (is_array($object['attendees'])) {
+        if (is_array($object['attendees'] ?? null)) {
             foreach ($object['attendees'] as $attendee) {
                 if (!empty($attendee['email']) && !empty($attendee['status']))
                     $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
             }
         }
 
         // collect tags from recurrence exceptions
-        if (is_array($object['exceptions'])) {
+        if (is_array($object['exceptions'] ?? null)) {
             foreach ($object['exceptions'] as $exception) {
                 $tags = array_merge($tags, $this->get_tags($exception));
             }
         }
 
         if (!empty($object['status'])) {
           $tags[] = 'x-status:' . strtolower($object['status']);
         }
 
         return array_unique($tags);
     }
 
     /**
      * Identify changes considered relevant for scheduling
      * 
      * @param array Hash array with NEW object properties
      * @param array Hash array with OLD object properties
      *
      * @return boolean True if changes affect scheduling, False otherwise
      */
     public function check_rescheduling($object, $old = null)
     {
         $reschedule = false;
 
         if (!is_array($old)) {
             $old = $this->data['uid'] ? $this->data : $this->to_array();
         }
 
         foreach ($this->_scheduling_properties ?: self::$scheduling_properties as $prop) {
-            $a = $old[$prop];
-            $b = $object[$prop];
+            $a = $old[$prop] ?? null;
+            $b = $object[$prop] ?? null;
 
-            if ($object['allday']
+            if (($object['allday'] ?? false)
                 && ($prop == 'start' || $prop == 'end')
                 && $a instanceof DateTimeInterface
                 && $b instanceof DateTimeInterface
             ) {
                 $a = $a->format('Y-m-d');
                 $b = $b->format('Y-m-d');
             }
             if ($prop == 'recurrence' && is_array($a) && is_array($b)) {
                 unset($a['EXCEPTIONS'], $b['EXCEPTIONS']);
                 $a = array_filter($a);
                 $b = array_filter($b);
 
                 // advanced rrule comparison: no rescheduling if series was shortened
                 if ($a['COUNT'] && $b['COUNT'] && $b['COUNT'] < $a['COUNT']) {
                   unset($a['COUNT'], $b['COUNT']);
                 }
                 else if ($a['UNTIL'] && $b['UNTIL'] && $b['UNTIL'] < $a['UNTIL']) {
                   unset($a['UNTIL'], $b['UNTIL']);
                 }
             }
             if ($a != $b) {
                 $reschedule = true;
                 break;
             }
         }
 
         return $reschedule;
     }
 
     /**
      * Clones into an instance of libcalendaring's extended EventCal class
      *
      * @return mixed EventCal object or false on failure
      */
     public function to_libcal()
     {
         static $error_logged = false;
 
         if (class_exists('kolabcalendaring')) {
             return new EventCal($this->obj);
         }
         else if (!$error_logged) {
             $error_logged = true;
             rcube::raise_error(array(
                 'code'    => 900,
                 'message' => "Required kolabcalendaring module not found"
             ), true);
         }
 
         return false;
     }
 }
diff --git a/plugins/libkolab/lib/kolab_ldap.php b/plugins/libkolab/lib/kolab_ldap.php
index 29984b09..d7266f8c 100644
--- a/plugins/libkolab/lib/kolab_ldap.php
+++ b/plugins/libkolab/lib/kolab_ldap.php
@@ -1,611 +1,613 @@
 <?php
 
 /**
  * Kolab Authentication and User Base
  *
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2011-2019, Kolab Systems AG <contact@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/>.
  */
 
 /**
  * Wrapper class for rcube_ldap_generic
  */
 class kolab_ldap extends rcube_ldap_generic
 {
     private $conf     = array();
     private $fieldmap = array();
     private $rcache;
 
 
     function __construct($p)
     {
         $rcmail = rcube::get_instance();
 
         $this->conf = $p;
         $this->conf['kolab_auth_user_displayname'] = $rcmail->config->get('kolab_auth_user_displayname', '{name}');
 
         $this->fieldmap = $p['fieldmap'];
         $this->fieldmap['uid'] = 'uid';
 
         $p['attributes'] = array_values($this->fieldmap);
         $p['debug']      = (bool) $rcmail->config->get('ldap_debug');
 
         if ($cache_type = $rcmail->config->get('ldap_cache', 'db')) {
             $cache_ttl   = $rcmail->config->get('ldap_cache_ttl', '10m');
             $this->cache = $rcmail->get_cache('LDAP.kolab_cache', $cache_type, $cache_ttl);
         }
 
         // Connect to the server (with bind)
         parent::__construct($p);
         $this->_connect();
 
         $rcmail->add_shutdown_function(array($this, 'close'));
     }
 
     /**
     * Establish a connection to the LDAP server
     */
     private function _connect()
     {
         // try to connect + bind for every host configured
         // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
         // see http://www.php.net/manual/en/function.ldap-connect.php
         foreach ((array)$this->config['hosts'] as $host) {
             // skip host if connection failed
             if (!$this->connect($host)) {
                 continue;
             }
 
-            $bind_pass      = $this->config['bind_pass'];
-            $bind_user      = $this->config['bind_user'];
+            $bind_pass      = $this->config['bind_pass'] ?? null;
+            $bind_user      = $this->config['bind_user'] ?? null;
             $bind_dn        = $this->config['bind_dn'];
             $base_dn        = $this->config['base_dn'];
             $groups_base_dn = $this->config['groups']['base_dn'] ?: $base_dn;
 
             // User specific access, generate the proper values to use.
             if ($this->config['user_specific']) {
                 $rcube = rcube::get_instance();
 
                 // No password set, use the session password
                 if (empty($bind_pass)) {
                     $bind_pass = $rcube->get_user_password();
                 }
 
+                $u = null;
                 // Get the pieces needed for variable replacement.
-                if ($fu = ($rcube->get_user_email() ?: $this->config['username'])) {
+                if ($fu = ($rcube->get_user_email() ?: ($this->config['username'] ?? null))) {
                     list($u, $d) = explode('@', $fu);
                 }
                 else {
-                    $d = $this->config['mail_domain'];
+                    $d = $this->config['mail_domain'] ?? null;
                 }
 
                 $dc = 'dc=' . strtr($d, array('.' => ',dc=')); // hierarchal domain string
 
                 // resolve $dc through LDAP
                 if (!empty($this->config['domain_filter']) && !empty($this->config['search_bind_dn'])) {
                     $this->bind($this->config['search_bind_dn'], $this->config['search_bind_pw']);
                     $dc = $this->domain_root_dn($d);
                 }
 
                 $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
 
                 // Search for the dn to use to authenticate
                 if ($this->config['search_base_dn'] && $this->config['search_filter']
                     && (strstr($bind_dn, '%dn') || strstr($base_dn, '%dn') || strstr($groups_base_dn, '%dn'))
                 ) {
                     $search_attribs = array('uid');
                     if ($search_bind_attrib = (array) $this->config['search_bind_attrib']) {
                         foreach ($search_bind_attrib as $r => $attr) {
                             $search_attribs[] = $attr;
                             $replaces[$r] = '';
                         }
                     }
 
                     $search_bind_dn = strtr($this->config['search_bind_dn'], $replaces);
                     $search_base_dn = strtr($this->config['search_base_dn'], $replaces);
                     $search_filter  = strtr($this->config['search_filter'], $replaces);
 
                     $cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:" . $this->config['search_bind_pw']);
 
                     if ($this->cache && ($dn = $this->cache->get($cache_key))) {
                         $replaces['%dn'] = $dn;
                     }
                     else {
                         $ldap = $this;
                         if (!empty($search_bind_dn) && !empty($this->config['search_bind_pw'])) {
                             // To protect from "Critical extension is unavailable" error
                             // we need to use a separate LDAP connection
                             if (!empty($this->config['vlv'])) {
                                 $ldap = new rcube_ldap_generic($this->config);
                                 $ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug));
                                 if (!$ldap->connect($host)) {
                                     continue;
                                 }
                             }
 
                             if (!$ldap->bind($search_bind_dn, $this->config['search_bind_pw'])) {
                                 continue;  // bind failed, try next host
                             }
                         }
 
                         $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs);
                         if ($res) {
                             $res->rewind();
                             $replaces['%dn'] = key($res->entries(true));
 
                             // add more replacements from 'search_bind_attrib' config
                             if ($search_bind_attrib) {
                                 $res = $res->current();
                                 foreach ($search_bind_attrib as $r => $attr) {
                                     $replaces[$r] = $res[$attr][0];
                                 }
                             }
                         }
 
                         if ($ldap != $this) {
                             $ldap->close();
                         }
                     }
 
                     // DN not found
                     if (empty($replaces['%dn'])) {
                         if (!empty($this->config['search_dn_default']))
                             $replaces['%dn'] = $this->config['search_dn_default'];
                         else {
                             rcube::raise_error(array(
                                 'code' => 100, 'type' => 'ldap',
                                 'file' => __FILE__, 'line' => __LINE__,
                                 'message' => "DN not found using LDAP search."), true);
                             continue;
                         }
                     }
 
                     if ($this->cache && !empty($replaces['%dn'])) {
                         $this->cache->set($cache_key, $replaces['%dn']);
                     }
                 }
 
                 // Replace the bind_dn and base_dn variables.
                 $bind_dn        = strtr($bind_dn, $replaces);
                 $base_dn        = strtr($base_dn, $replaces);
                 $groups_base_dn = strtr($groups_base_dn, $replaces);
 
                 // replace placeholders in filter settings
                 if (!empty($this->config['filter'])) {
                     $this->config['filter'] = strtr($this->config['filter'], $replaces);
                 }
 
                 foreach (array('base_dn', 'filter', 'member_filter') as $k) {
                     if (!empty($this->config['groups'][$k])) {
                         $this->config['groups'][$k] = strtr($this->config['groups'][$k], $replaces);
                     }
                 }
 
                 if (empty($bind_user)) {
                     $bind_user = $u;
                 }
             }
 
             if (empty($bind_pass)) {
                 $this->ready = true;
             }
             else {
                 if (!empty($this->config['auth_cid'])) {
                     $this->ready = $this->sasl_bind($this->config['auth_cid'], $bind_pass, $bind_dn);
                 }
                 else if (!empty($bind_dn)) {
                     $this->ready = $this->bind($bind_dn, $bind_pass);
                 }
                 else {
                     $this->ready = $this->sasl_bind($bind_user, $bind_pass);
                 }
             }
 
             // connection established, we're done here
             if ($this->ready) {
                 break;
             }
 
         }  // end foreach hosts
 
         if (!is_resource($this->conn)) {
             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
                 'file' => __FILE__, 'line' => __LINE__,
                 'message' => "Could not connect to any LDAP server, last tried $host"), true);
 
             $this->ready = false;
         }
 
         return $this->ready;
     }
 
     /**
      * Fetches user data from LDAP addressbook
      */
     function get_user_record($user, $host)
     {
         $rcmail  = rcube::get_instance();
         $filter  = $rcmail->config->get('kolab_auth_filter');
         $filter  = $this->parse_vars($filter, $user, $host);
         $base_dn = $this->parse_vars($this->config['base_dn'], $user, $host);
         $scope   = $this->config['scope'];
 
         // @TODO: print error if filter is empty
 
         // get record
         if ($result = parent::search($base_dn, $filter, $scope, $this->attributes)) {
             if ($result->count() == 1) {
                 $entries = $result->entries(true);
                 $dn      = key($entries);
                 $entry   = array_pop($entries);
                 $entry   = $this->field_mapping($dn, $entry);
 
                 return $entry;
             }
         }
     }
 
     /**
      * Fetches user data from LDAP addressbook
      */
     function get_user_groups($dn, $user, $host)
     {
         if (empty($dn) || empty($this->config['groups'])) {
             return array();
         }
 
         $base_dn     = $this->parse_vars($this->config['groups']['base_dn'], $user, $host);
         $name_attr   = $this->config['groups']['name_attr'] ? $this->config['groups']['name_attr'] : 'cn';
         $member_attr = $this->get_group_member_attr();
         $filter      = "(member=$dn)(uniqueMember=$dn)";
 
         if ($member_attr != 'member' && $member_attr != 'uniqueMember')
             $filter .= "($member_attr=$dn)";
         $filter = strtr("(|$filter)", array("\\" => "\\\\"));
 
         $result = parent::search($base_dn, $filter, 'sub', array('dn', $name_attr));
 
         if (!$result) {
             return array();
         }
 
         $groups = array();
         foreach ($result as $entry) {
             $dn    = $entry['dn'];
             $entry = rcube_ldap_generic::normalize_entry($entry);
 
             $groups[$dn] = $entry[$name_attr];
         }
 
         return $groups;
     }
 
     /**
      * Get a specific LDAP record
      *
      * @param string DN
      *
      * @return array Record data
      */
     function get_record($dn)
     {
         if (!$this->ready) {
             return;
         }
 
         if ($rec = $this->get_entry($dn, $this->attributes)) {
             $rec = rcube_ldap_generic::normalize_entry($rec);
             $rec = $this->field_mapping($dn, $rec);
         }
 
         return $rec;
     }
 
     /**
      * Replace LDAP record data items
      *
      * @param string $dn    DN
      * @param array  $entry LDAP entry
      *
      * return bool True on success, False on failure
      */
     function replace($dn, $entry)
     {
         // fields mapping
         foreach ($this->fieldmap as $field => $attr) {
             if (array_key_exists($field, $entry)) {
                 $entry[$attr] = $entry[$field];
                 if ($attr != $field) {
                     unset($entry[$field]);
                 }
             }
         }
 
         return $this->mod_replace($dn, $entry);
     }
 
     /**
      * Search records (simplified version of rcube_ldap::search)
      *
      * @param mixed   $fields   The field name or array of field names to search in
      * @param string  $value    Search value
      * @param int     $mode     Matching mode:
      *                          0 - partial (*abc*),
      *                          1 - strict (=),
      *                          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 of LDAP records found
      */
     function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0)
     {
         if (empty($fields)) {
             return array();
         }
 
         $mode  = intval($mode);
 
         // try to resolve field names into ldap attributes
         $fieldmap = $this->fieldmap;
         $attrs = array_map(function($f) use ($fieldmap) {
             return array_key_exists($f, $fieldmap) ? $fieldmap[$f] : $f;
         }, (array)$fields);
 
         // compose a full-text-search-like filter
         if (count($attrs) > 1 || $mode != 1) {
             $filter = self::fulltext_search_filter($value, $attrs, $mode);
         }
         // direct search
         else {
             $field  = $attrs[0];
             $filter = "($field=" . self::quote_string($value) . ")";
         }
 
         // add required (non empty) fields filter
         $req_filter = '';
 
         foreach ((array)$required as $field) {
             $attr = array_key_exists($field, $this->fieldmap) ? $this->fieldmap[$field] : $field;
 
             // only add if required field is not already in search filter
             if (!in_array($attr, $attrs)) {
                 $req_filter .= "($attr=*)";
             }
         }
 
         if (!empty($req_filter)) {
             $filter = '(&' . $req_filter . $filter . ')';
         }
 
         // avoid double-wildcard if $value is empty
         $filter = preg_replace('/\*+/', '*', $filter);
 
         // add general filter to query
         if (!empty($this->config['filter'])) {
             $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->config['filter']) . ')' . $filter . ')';
         }
 
         $base_dn = $this->parse_vars($this->config['base_dn']);
         $scope   = $this->config['scope'];
         $attrs   = array_values($this->fieldmap);
         $list    = array();
 
         if ($result = $this->search($base_dn, $filter, $scope, $attrs)) {
             $count = $result->count();
             $i = 0;
             foreach ($result as $entry) {
                 if ($limit && $limit <= $i) {
                     break;
                 }
 
                 $dn        = $entry['dn'];
                 $entry     = rcube_ldap_generic::normalize_entry($entry);
                 $list[$dn] = $this->field_mapping($dn, $entry);
                 $i++;
             }
         }
 
         return $list;
     }
 
     /**
      * Set filter used in search()
      */
     function set_filter($filter)
     {
         $this->config['filter'] = $filter;
     }
 
     /**
      * Maps LDAP attributes to defined fields
      */
     protected function field_mapping($dn, $entry)
     {
         $entry['dn'] = $dn;
 
         // fields mapping
         foreach ($this->fieldmap as $field => $attr) {
             // $entry might be indexed by lower-case attribute names
             $attr_lc = strtolower($attr);
             if (isset($entry[$attr_lc])) {
                 $entry[$field] = $entry[$attr_lc];
             }
             else if (isset($entry[$attr])) {
                 $entry[$field] = $entry[$attr];
             }
         }
 
         // compose display name according to config
         if (empty($this->fieldmap['displayname'])) {
             $entry['displayname'] = rcube_addressbook::compose_search_name(
                 $entry,
                 $entry['email'],
                 $entry['name'],
                 $this->conf['kolab_auth_user_displayname']
             );
         }
 
         return $entry;
     }
 
     /**
      * Detects group member attribute name
      */
     private function get_group_member_attr($object_classes = array())
     {
         if (empty($object_classes)) {
             $object_classes = $this->config['groups']['object_classes'];
         }
         if (!empty($object_classes)) {
             foreach ((array)$object_classes as $oc) {
                 switch (strtolower($oc)) {
                     case 'group':
                     case 'groupofnames':
                     case 'kolabgroupofnames':
                         $member_attr = 'member';
                         break;
 
                     case 'groupofuniquenames':
                     case 'kolabgroupofuniquenames':
                         $member_attr = 'uniqueMember';
                         break;
                 }
             }
         }
 
         if (!empty($member_attr)) {
             return $member_attr;
         }
 
         if (!empty($this->config['groups']['member_attr'])) {
             return $this->config['groups']['member_attr'];
         }
 
         return 'member';
     }
 
     /**
      * Prepares filter query for LDAP search
      */
     function parse_vars($str, $user = null, $host = null)
     {
         // When authenticating user $user is always set
         // if not set it means we use this LDAP object for other
         // purposes, e.g. kolab_delegation, then username with
         // correct domain is in a session
         if (!$user) {
             $user = $_SESSION['username'];
         }
 
+        $dc = null;
         if (isset($this->icache[$user])) {
             list($user, $dc) = $this->icache[$user];
         }
         else {
             $orig_user = $user;
             $rcmail = rcube::get_instance();
 
             // get default domain
             if ($username_domain = $rcmail->config->get('username_domain')) {
                 if ($host && is_array($username_domain) && isset($username_domain[$host])) {
                     $domain = rcube_utils::parse_host($username_domain[$host], $host);
                 }
                 else if (is_string($username_domain)) {
                     $domain = rcube_utils::parse_host($username_domain, $host);
                 }
             }
 
             // realmed username (with domain)
             if (strpos($user, '@')) {
                 list($usr, $dom) = explode('@', $user);
 
                 // unrealm domain, user login can contain a domain alias
                 if ($dom != $domain && ($dc = $this->domain_root_dn($dom))) {
                     // @FIXME: we should replace domain in $user, I suppose
                 }
             }
             else if ($domain) {
                 $user .= '@' . $domain;
             }
 
             $this->icache[$orig_user] = array($user, $dc);
         }
 
         // replace variables in filter
         list($u, $d) = explode('@', $user);
 
         // hierarchal domain string
         if (empty($dc)) {
             $dc = 'dc=' . strtr($d, array('.' => ',dc='));
         }
 
         $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
 
         $this->parse_replaces = $replaces;
 
         return strtr($str, $replaces);
     }
 
     /**
      * Returns variables used for replacement in (last) parse_vars() call
      *
      * @return array Variable-value hash array
      */
     public function get_parse_vars()
     {
         return $this->parse_replaces;
     }
 
     /**
      * Register additional fields
      */
     public function extend_fieldmap($map)
     {
         foreach ((array)$map as $name => $attr) {
             if (!in_array($attr, $this->attributes)) {
                 $this->attributes[]    = $attr;
                 $this->fieldmap[$name] = $attr;
             }
         }
     }
 
     /**
      * HTML-safe DN string encoding
      *
      * @param string $str DN string
      *
      * @return string Encoded HTML identifier string
      */
     static function dn_encode($str)
     {
         return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
     }
 
     /**
      * Decodes DN string encoded with _dn_encode()
      *
      * @param string $str Encoded HTML identifier string
      *
      * @return string DN string
      */
     static function dn_decode($str)
     {
         $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
         return base64_decode($str);
     }
 }
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index 6d9db480..13db9b5b 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1,1810 +1,1810 @@
 <?php
 
 /**
  * Kolab storage class providing static methods to access groupware objects on a Kolab server.
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2012-2014, Kolab Systems AG <contact@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
 {
     const CTYPE_KEY         = '/shared/vendor/kolab/folder-type';
     const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
     const COLOR_KEY_SHARED  = '/shared/vendor/kolab/color';
     const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
     const NAME_KEY_SHARED   = '/shared/vendor/kolab/displayname';
     const NAME_KEY_PRIVATE  = '/private/vendor/kolab/displayname';
     const UID_KEY_SHARED    = '/shared/vendor/kolab/uniqueid';
     const UID_KEY_CYRUS     = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
 
     const ERROR_IMAP_CONN      = 1;
     const ERROR_CACHE_DB       = 2;
     const ERROR_NO_PERMISSION  = 3;
     const ERROR_INVALID_FOLDER = 4;
 
     public static $version = '3.0';
     public static $last_error;
     public static $encode_ids = false;
 
     private static $ready = false;
     private static $with_tempsubs = true;
     private static $subscriptions;
     private static $ldapcache = array();
     private static $ldap = array();
     private static $states;
     private static $config;
     private static $imap;
 
 
     // Default folder names
     private static $default_folders = array(
         'event'         => 'Calendar',
         'contact'       => 'Contacts',
         'task'          => 'Tasks',
         'note'          => 'Notes',
         'file'          => 'Files',
         'configuration' => 'Configuration',
         'journal'       => 'Journal',
         'mail.inbox'       => 'INBOX',
         'mail.drafts'      => 'Drafts',
         'mail.sentitems'   => 'Sent',
         'mail.wastebasket' => 'Trash',
         'mail.outbox'      => 'Outbox',
         'mail.junkemail'   => 'Junk',
     );
 
 
     /**
      * Setup the environment needed by the libs
      */
     public static function setup()
     {
         if (self::$ready)
             return true;
 
         $rcmail = rcube::get_instance();
         self::$config  = $rcmail->config;
         self::$version = strval($rcmail->config->get('kolab_format_version', self::$version));
         self::$imap    = $rcmail->get_storage();
         self::$ready   = class_exists('kolabformat') &&
             (self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
 
         if (self::$ready) {
             // do nothing
         }
         else if (!class_exists('kolabformat')) {
             rcube::raise_error(array(
                 'code' => 900, 'type' => 'php',
                 'message' => "required kolabformat module not found"
             ), true);
         }
         else if (self::$imap->get_error_code()) {
             rcube::raise_error(array(
                 'code' => 900, 'type' => 'php', 'message' => "IMAP error"
             ), true);
         }
 
         // adjust some configurable settings
         if ($event_scheduling_prop = $rcmail->config->get('kolab_event_scheduling_properties', null)) {
             kolab_format_event::$scheduling_properties = (array)$event_scheduling_prop;
         }
         // adjust some configurable settings
         if ($task_scheduling_prop = $rcmail->config->get('kolab_task_scheduling_properties', null)) {
             kolab_format_task::$scheduling_properties = (array)$task_scheduling_prop;
         }
 
         return self::$ready;
     }
 
     /**
      * Initializes LDAP object to resolve Kolab users
      *
      * @param string $name Name of the configuration option with LDAP config
      */
     public static function ldap($name = 'kolab_users_directory')
     {
         self::setup();
 
         $config = self::$config->get($name);
 
         if (empty($config)) {
             $name   = 'kolab_auth_addressbook';
             $config = self::$config->get($name);
         }
 
-        if (self::$ldap[$name]) {
+        if (self::$ldap[$name] ?? false) {
             return self::$ldap[$name];
         }
 
         if (!is_array($config)) {
             $ldap_config = (array)self::$config->get('ldap_public');
             $config = $ldap_config[$config];
         }
 
         if (empty($config)) {
             return null;
         }
 
         $ldap = new kolab_ldap($config);
 
         // overwrite filter option
         if ($filter = self::$config->get('kolab_users_filter')) {
             self::$config->set('kolab_auth_filter', $filter);
         }
 
         $user_field = $user_attrib = self::$config->get('kolab_users_id_attrib');
 
         // Fallback to kolab_auth_login, which is not attribute, but field name
         if (!$user_field && ($user_field = self::$config->get('kolab_auth_login', 'email'))) {
             $user_attrib = $config['fieldmap'][$user_field];
         }
 
         if ($user_field && $user_attrib) {
             $ldap->extend_fieldmap(array($user_field => $user_attrib));
         }
 
         self::$ldap[$name] = $ldap;
 
         return $ldap;
     }
 
     /**
      * Get a list of storage folders for the given data type
      *
      * @param string Data type to list folders for (contact,distribution-list,event,task,note)
      * @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_folders($type, $subscribed = null)
     {
         $folders = $folderdata = array();
 
         if (self::setup()) {
             foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
                 $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
             }
         }
 
         return $folders;
     }
 
     /**
      * Getter for the storage folder for the given type
      *
      * @param string Data type to list folders for (contact,distribution-list,event,task,note)
      * @return object kolab_storage_folder  The folder object
      */
     public static function get_default_folder($type)
     {
         if (self::setup()) {
             foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
                 return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
             }
         }
 
         return null;
     }
 
     /**
      * Getter for a specific storage folder
      *
      * @param string IMAP folder to access (UTF7-IMAP)
      * @param string Expected folder type
      *
      * @return object kolab_storage_folder  The folder object
      */
     public static function get_folder($folder, $type = null)
     {
         return self::setup() ? new kolab_storage_folder($folder, $type) : null;
     }
 
     /**
      * Getter for a single Kolab object, identified by its UID.
      * This will search all folders storing objects of the given type.
      *
      * @param string Object UID
      * @param string Object type (contact,event,task,journal,file,note,configuration)
      * @return array The Kolab object represented as hash array or false if not found
      */
     public static function get_object($uid, $type)
     {
         self::setup();
         $folder = null;
         foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
             if (!$folder)
                 $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
             else
                 $folder->set_folder($foldername, $type, $folderdata[$foldername]);
 
             if ($object = $folder->get_object($uid))
                 return $object;
         }
 
         return false;
     }
 
     /**
      * Execute cross-folder searches with the given query.
      *
      * @param array  Pseudo-SQL query as list of filter parameter triplets
      * @param string Folder type (contact,event,task,journal,file,note,configuration)
      * @param int    Expected number of records or limit (for performance reasons)
      *
      * @return array List of Kolab data objects (each represented as hash array)
      * @see kolab_storage_format::select()
      */
     public static function select($query, $type, $limit = null)
     {
         self::setup();
         $folder = null;
         $result = array();
 
         foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
             $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
 
             if ($limit) {
                 $folder->set_order_and_limit(null, $limit);
             }
 
             foreach ($folder->select($query) as $object) {
                 $result[] = $object;
             }
         }
 
         return $result;
     }
 
     /**
      * Returns Free-busy server URL
      */
     public static function get_freebusy_server()
     {
         $rcmail = rcube::get_instance();
 
         $url = 'https://' . $_SESSION['imap_host'] . '/freebusy';
         $url = $rcmail->config->get('kolab_freebusy_server', $url);
         $url = rcube_utils::resolve_url($url);
 
         return unslashify($url);
     }
 
     /**
      * Compose an URL to query the free/busy status for the given user
      *
      * @param string Email address of the user to get free/busy data for
      * @param object DateTime Start of the query range (optional)
      * @param object DateTime End of the query range (optional)
      *
      * @return string Fully qualified URL to query free/busy data
      */
     public static function get_freebusy_url($email, $start = null, $end = null)
     {
         $query = '';
         $param = array();
         $utc = new \DateTimeZone('UTC');
 
         // https://www.calconnect.org/pubdocs/CD0903%20Freebusy%20Read%20URL.pdf
 
         if ($start instanceof \DateTime) {
             $start->setTimezone($utc);
             $param['start'] = $param['dtstart'] = $start->format('Ymd\THis\Z');
         }
 
         if ($end instanceof \DateTime) {
             $end->setTimezone($utc);
             $param['end'] = $param['dtend'] = $end->format('Ymd\THis\Z');
         }
 
         if (!empty($param)) {
             $query = '?' . http_build_query($param);
         }
 
         $url = self::get_freebusy_server();
 
         if (strpos($url, '%u')) {
             // Expected configured full URL, just replace the %u variable
             // Note: Cyrus v3 Free-Busy service does not use .ifb extension
             $url = str_replace('%u', rawurlencode($email), $url);
         }
         else {
             $url .= '/' . $email . '.ifb';
         }
 
         return $url . $query;
     }
 
     /**
      * Creates folder ID from folder name
      *
      * @param string  $folder Folder name (UTF7-IMAP)
      * @param boolean $enc    Use lossless encoding
      * @return string Folder ID string
      */
     public static function folder_id($folder, $enc = null)
     {
         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));
     }
 
     /**
      * 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)
     {
         self::setup();
 
         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)
      *
      * @return bool True on success, false on failure
      */
     public static function folder_delete($name)
     {
         // clear cached entries first
         if ($folder = self::get_folder($name))
             $folder->cache->purge();
 
         $rcmail = rcube::get_instance();
         $plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
 
         $success = self::$imap->delete_folder($name);
         self::$last_error = self::$imap->get_error_str();
 
         return $success;
     }
 
     /**
      * Creates IMAP folder
      *
      * @param string $name       Folder name (UTF7-IMAP)
      * @param string $type       Folder type
      * @param bool   $subscribed Sets folder subscription
      * @param bool   $active     Sets folder state (client-side subscription)
      *
      * @return bool True on success, false on failure
      */
     public static function folder_create($name, $type = null, $subscribed = false, $active = false)
     {
         self::setup();
 
         $rcmail = rcube::get_instance();
         $plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
             'name' => $name,
             'subscribe' => $subscribed,
         )));
 
         if ($saved = self::$imap->create_folder($name, $subscribed)) {
             // set metadata for folder type
             if ($type) {
                 $saved = self::set_folder_type($name, $type);
 
                 // revert if metadata could not be set
                 if (!$saved) {
                     self::$imap->delete_folder($name);
                 }
                 // activate folder
                 else if ($active) {
                     self::set_state($name, true);
                 }
             }
         }
 
         if ($saved) {
             return true;
         }
 
         self::$last_error = self::$imap->get_error_str();
         return false;
     }
 
     /**
      * Renames IMAP folder
      *
      * @param string $oldname Old folder name (UTF7-IMAP)
      * @param string $newname New folder name (UTF7-IMAP)
      *
      * @return bool True on success, false on failure
      */
     public static function folder_rename($oldname, $newname)
     {
         self::setup();
 
         $rcmail = rcube::get_instance();
         $plugin = $rcmail->plugins->exec_hook('folder_rename', array(
             'oldname' => $oldname, 'newname' => $newname));
 
         $oldfolder = self::get_folder($oldname);
         $active    = self::folder_is_active($oldname);
         $success   = self::$imap->rename_folder($oldname, $newname);
         self::$last_error = self::$imap->get_error_str();
 
         // pass active state to new folder name
         if ($success && $active) {
             self::set_state($oldname, false);
             self::set_state($newname, true);
         }
 
         // assign existing cache entries to new resource uri
         if ($success && $oldfolder) {
             $oldfolder->cache->rename($newname);
         }
 
         return $success;
     }
 
     /**
      * Rename or Create a new IMAP folder.
      *
      * Does additional checks for permissions and folder name restrictions
      *
      * @param array &$prop Hash array with folder properties and metadata
      *  - name:       Folder name
      *  - oldname:    Old folder name when changed
      *  - parent:     Parent folder to create the new one in
      *  - type:       Folder type to create
      *  - subscribed: Subscribed flag (IMAP subscription)
      *  - active:     Activation flag (client-side subscription)
      *
      * @return string|false New folder name or False on failure
      *
      * @see self::set_folder_props() for list of other properties
      */
     public static function folder_update(&$prop)
     {
         self::setup();
 
         $folder    = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP');
         $oldfolder = $prop['oldname']; // UTF7
         $parent    = $prop['parent']; // UTF7
         $delimiter = self::$imap->get_hierarchy_delimiter();
 
         if (strlen($oldfolder)) {
             $options = self::$imap->folder_info($oldfolder);
         }
 
         if (!empty($options) && ($options['norename'] || $options['protected'])) {
         }
         // sanity checks (from steps/settings/save_folder.inc)
         else if (!strlen($folder)) {
             self::$last_error = 'cannotbeempty';
             return false;
         }
         else if (strlen($folder) > 128) {
             self::$last_error = 'nametoolong';
             return false;
         }
         else {
             // these characters are problematic e.g. when used in LIST/LSUB
             foreach (array($delimiter, '%', '*') as $char) {
                 if (strpos($folder, $char) !== false) {
                     self::$last_error = 'forbiddencharacter';
                     return false;
                 }
             }
         }
 
         if (!empty($options) && ($options['protected'] || $options['norename'])) {
             $folder = $oldfolder;
         }
         else if (strlen($parent)) {
             $folder = $parent . $delimiter . $folder;
         }
         else {
             // add namespace prefix (when needed)
             $folder = self::$imap->mod_folder($folder, 'in');
         }
 
         // Check access rights to the parent folder
         if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
             $parent_opts = self::$imap->folder_info($parent);
             if ($parent_opts['namespace'] != 'personal'
                 && (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
             ) {
                 self::$last_error = 'No permission to create folder';
                 return false;
             }
         }
 
         // update the folder name
         if (strlen($oldfolder)) {
             if ($oldfolder != $folder) {
                 $result = self::folder_rename($oldfolder, $folder);
             }
             else {
                 $result = true;
             }
         }
         // create new folder
         else {
             $result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']);
         }
 
         if ($result) {
             self::set_folder_props($folder, $prop);
         }
 
         return $result ? $folder : false;
     }
 
     /**
      * Getter for human-readable name of Kolab object (folder)
      * with kolab_custom_display_names support.
      * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
      *
      * @param string $folder    IMAP folder name (UTF7-IMAP)
      * @param string $folder_ns Will be set to namespace name of the folder
      *
      * @return string Name of the folder-object
      */
     public static function object_name($folder, &$folder_ns=null)
     {
         // find custom display name in folder METADATA
         if ($name = self::custom_displayname($folder)) {
             return $name;
         }
 
         return self::object_prettyname($folder, $folder_ns);
     }
 
     /**
      * Get custom display name (saved in metadata) for the given folder
      */
     public static function custom_displayname($folder)
     {
         static $_metadata;
 
         // find custom display name in folder METADATA
         if (self::$config->get('kolab_custom_display_names', true) && self::setup()) {
             if ($_metadata !== null) {
                 $metadata = $_metadata;
             }
             else {
                 // For performance reasons ask for all folders, it will be cached as one cache entry
                 $metadata = self::$imap->get_metadata("*", array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
 
                 // If cache is disabled store result in memory
                 if (!self::$config->get('imap_cache')) {
                     $_metadata = $metadata;
                 }
             }
 
             if ($data = $metadata[$folder]) {
                 if (($name = $data[self::NAME_KEY_PRIVATE]) || ($name = $data[self::NAME_KEY_SHARED])) {
                     return $name;
                 }
             }
         }
 
         return false;
     }
 
     /**
      * Getter for human-readable name of Kolab object (folder)
      * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
      *
      * @param string $folder    IMAP folder name (UTF7-IMAP)
      * @param string $folder_ns Will be set to namespace name of the folder
      *
      * @return string Name of the folder-object
      */
     public static function object_prettyname($folder, &$folder_ns=null)
     {
         self::setup();
 
         $found     = false;
         $namespace = self::$imap->get_namespace();
 
         if (!empty($namespace['shared'])) {
             foreach ($namespace['shared'] as $ns) {
                 if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
                     $prefix = '';
                     $folder = substr($folder, strlen($ns[0]));
                     $delim  = $ns[1];
                     $found  = true;
                     $folder_ns = 'shared';
                     break;
                 }
             }
         }
 
         if (!$found && !empty($namespace['other'])) {
             foreach ($namespace['other'] as $ns) {
                 if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
                     // remove namespace prefix and extract username
                     $folder = substr($folder, strlen($ns[0]));
                     $delim  = $ns[1];
 
                     // get username part and map it to user name
                     $pos = strpos($folder, $delim);
                     $fid = $pos ? substr($folder, 0, $pos) : $folder;
 
                     if ($user = self::folder_id2user($fid, true)) {
                         $fid = str_replace($delim, '', $user);
                     }
 
                     $prefix = "($fid)";
                     $folder = $pos ? substr($folder, $pos + 1) : '';
                     $found  = true;
                     $folder_ns = 'other';
                     break;
                 }
             }
         }
 
         if (!$found && !empty($namespace['personal'])) {
             foreach ($namespace['personal'] as $ns) {
                 if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
                     // remove namespace prefix
                     $folder = substr($folder, strlen($ns[0]));
                     $prefix = '';
                     $delim  = $ns[1];
                     $found  = true;
                     break;
                 }
             }
         }
 
         if (empty($delim))
             $delim = self::$imap->get_hierarchy_delimiter();
 
         $folder = rcube_charset::convert($folder, 'UTF7-IMAP');
         $folder = html::quote($folder);
         $folder = str_replace(html::quote($delim), ' &raquo; ', $folder);
 
         if ($prefix)
             $folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
 
         if (!$folder_ns)
             $folder_ns = 'personal';
 
         return $folder;
     }
 
     /**
      * Helper method to generate a truncated folder name to display.
      * Note: $origname is a string returned by self::object_name()
      */
     public static function folder_displayname($origname, &$names)
     {
         $name = $origname;
 
         // find folder prefix to truncate
         for ($i = count($names)-1; $i >= 0; $i--) {
             if (strpos($name, $names[$i] . ' &raquo; ') === 0) {
                 $length = strlen($names[$i] . ' &raquo; ');
                 $prefix = substr($name, 0, $length);
                 $count  = count(explode(' &raquo; ', $prefix));
                 $diff   = 1;
 
                 // check if prefix folder is in other users namespace
                 for ($n = count($names)-1; $n >= 0; $n--) {
                     if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
                         $diff = 0;
                         break;
                     }
                 }
 
                 $name = str_repeat('&nbsp;&nbsp;&nbsp;', $count - $diff) . '&raquo; ' . substr($name, $length);
                 break;
             }
             // other users namespace and parent folder exists
             else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
                 $length = strlen('(' . $names[$i] . ') ');
                 $prefix = substr($name, 0, $length);
                 $count  = count(explode(' &raquo; ', $prefix));
                 $name   = str_repeat('&nbsp;&nbsp;&nbsp;', $count) . '&raquo; ' . substr($name, $length);
                 break;
             }
         }
 
         $names[] = $origname;
 
         return $name;
     }
 
     /**
      * Creates a SELECT field with folders list
      *
      * @param string $type    Folder type
      * @param array  $attrs   SELECT field attributes (e.g. name)
      * @param string $current The name of current folder (to skip it)
      *
      * @return html_select SELECT object
      */
     public static function folder_selector($type, $attrs, $current = '')
     {
         // get all folders of specified type (sorted)
         $folders = self::get_folders($type, true);
 
         $delim = self::$imap->get_hierarchy_delimiter();
         $names = array();
         $len   = strlen($current);
 
         if ($len && ($rpos = strrpos($current, $delim))) {
             $parent = substr($current, 0, $rpos);
             $p_len  = strlen($parent);
         }
 
         // Filter folders list
         foreach ($folders as $c_folder) {
             $name = $c_folder->name;
 
             // skip current folder and it's subfolders
             if ($len) {
                 if ($name == $current) {
                     // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
                     if ($p_len && !isset($names[$parent])) {
                         $names[$parent] = self::object_name($parent);
                     }
                     continue;
                 }
                 if (strpos($name, $current.$delim) === 0) {
                     continue;
                 }
             }
 
             // always show the parent of current folder
             if ($p_len && $name == $parent) {
             }
             // skip folders where user have no rights to create subfolders
             else if ($c_folder->get_owner() != $_SESSION['username']) {
                 $rights = $c_folder->get_myrights();
                 if (!preg_match('/[ck]/', $rights)) {
                     continue;
                 }
             }
 
             $names[$name] = $c_folder->get_name();
         }
 
         // Build SELECT field of parent folder
         $attrs['is_escaped'] = true;
         $select = new html_select($attrs);
         $select->add('---', '');
 
         $listnames = array();
         foreach (array_keys($names) as $imap_name) {
             $name = $origname = $names[$imap_name];
 
             // find folder prefix to truncate
             for ($i = count($listnames)-1; $i >= 0; $i--) {
                 if (strpos($name, $listnames[$i].' &raquo; ') === 0) {
                     $length = strlen($listnames[$i].' &raquo; ');
                     $prefix = substr($name, 0, $length);
                     $count  = count(explode(' &raquo; ', $prefix));
                     $name   = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
                     break;
                 }
             }
 
             $listnames[] = $origname;
             $select->add($name, $imap_name);
         }
 
         return $select;
     }
 
     /**
      * Returns a list of folder names
      *
      * @param string  Optional root folder
      * @param string  Optional name pattern
      * @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)
      * @param array   Will be filled with folder-types data
      *
      * @return array List of folders
      */
     public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
     {
         if (!self::setup()) {
             return null;
         }
 
         // use IMAP subscriptions
         if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
             $subscribed = true;
         }
 
         if (!$filter) {
             // Get ALL folders list, standard way
             if ($subscribed) {
                 $folders = self::_imap_list_subscribed($root, $mbox, $filter);
             }
             else {
                 $folders = self::_imap_list_folders($root, $mbox);
             }
 
             return $folders;
         }
         $prefix = $root . $mbox;
         $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
 
         // get folders types for all folders
         $folderdata = self::folders_typedata($prefix);
 
         if (!is_array($folderdata)) {
             return array();
         }
 
         // If we only want groupware folders and don't care about the subscription state,
         // then the metadata will already contain all folder names and we can avoid the LIST below.
         if (!$subscribed && $filter != 'mail' && $prefix == '*') {
             foreach ($folderdata as $folder => $type) {
                 if (!preg_match($regexp, $type)) {
                     unset($folderdata[$folder]);
                 }
             }
 
             return self::$imap->sort_folder_list(array_keys($folderdata), true);
         }
 
         // Get folders list
         if ($subscribed) {
             $folders = self::_imap_list_subscribed($root, $mbox, $filter);
         }
         else {
             $folders = self::_imap_list_folders($root, $mbox);
         }
 
         // In case of an error, return empty list (?)
         if (!is_array($folders)) {
             return array();
         }
 
         // Filter folders list
         foreach ($folders as $idx => $folder) {
             $type = $folderdata[$folder];
 
             if ($filter == 'mail' && empty($type)) {
                 continue;
             }
             if (empty($type) || !preg_match($regexp, $type)) {
                 unset($folders[$idx]);
             }
         }
 
         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;
     }
 
     /**
      * Wrapper for rcube_imap::list_folders_subscribed()
      * with support for temporarily subscribed folders
      */
     protected static function _imap_list_subscribed($root, $mbox, $filter = null)
     {
         $folders = self::$imap->list_folders_subscribed($root, $mbox);
 
         // add temporarily subscribed folders
         if ($filter != 'mail' && self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
             $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
         }
 
         return $folders;
     }
 
     /**
      * 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();
         $query = str_replace('*', '', $query);
 
         // 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 (($query == '' || 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, $type, $folderdata[$foldername]);
             }
         }
 
         return $folders;
     }
 
     /**
      * Sort the given list of kolab folders by namespace/name
      *
      * @param array List of kolab_storage_folder objects
      * @return array Sorted list of folders
      */
     public static function sort_folders($folders)
     {
         $pad     = '  ';
         $out     = array();
         $nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
 
         foreach ($folders as $folder) {
             $_folders[$folder->name] = $folder;
             $ns = $folder->get_namespace();
             $nsnames[$ns][$folder->name] = strtolower(html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET)) . $pad;  // decode &raquo;
         }
 
         // $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);
             foreach (array_keys($nsnames[$ns]) as $utf7name) {
                 $out[] = $_folders[$utf7name];
             }
         }
 
         return $out;
     }
 
     /**
      * 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 Flat folders list
      */
     public static function folder_hierarchy($folders, &$tree = null)
     {
         if (!self::setup()) {
             return array();
         }
 
         $_folders = array();
         $delim    = self::$imap->get_hierarchy_delimiter();
         $other_ns = rtrim(self::namespace_root('other'), $delim);
         $tree     = new kolab_storage_folder_virtual('', '<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)) {
                 $tree->children[] = $folder;
             }
             else {
                 $parents = array();
                 $depth = $folder->get_namespace() == 'personal' ? 1 : 2;
 
                 while (count($path) >= $depth && ($parent = join($delim, $path))) {
                     array_pop($path);
                     $parent_parent = join($delim, $path);
 
                     if (!$refs[$parent]) {
                         if ($folder->type && self::folder_type($parent) == $folder->type) {
                             $refs[$parent] = new kolab_storage_folder($parent, $folder->type, $folder->type);
                             $refs[$parent]->parent = $parent_parent;
                         }
                         else if ($parent_parent == $other_ns) {
                             $refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
                         }
                         else {
                             $name = kolab_storage::object_name($parent);
                             $refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
                         }
                         $parents[] = $refs[$parent];
                     }
                 }
 
                 if (!empty($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]);
         }
 
         return $_folders;
     }
 
     /**
      * Returns folder types indexed by folder name
      *
      * @param string $prefix Folder prefix (Default '*' for all folders)
      *
      * @return array|bool List of folders, False on failure
      */
     public static function folders_typedata($prefix = '*')
     {
         if (!self::setup()) {
             return false;
         }
 
         $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;
                                 $opts = self::$imap->folder_attributes($folder);
                                 if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
                                     $folderdata += $data;
                                 }
                             }
                         }
                     }
                     else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
                         $folderdata += $data;
                     }
                 }
             }
         }
         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);
     }
 
     /**
      * Callback for array_map to select the correct annotation value
      */
     public static function folder_select_metadata($types)
     {
         if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
             return $types[self::CTYPE_KEY_PRIVATE];
         }
         else if (!empty($types[self::CTYPE_KEY])) {
             list($ctype, ) = explode('.', $types[self::CTYPE_KEY]);
             return $ctype;
         }
         return null;
     }
 
     /**
      * Returns type of IMAP folder
      *
      * @param string $folder Folder name (UTF7-IMAP)
      *
      * @return string Folder type
      */
     public static function folder_type($folder)
     {
         self::setup();
 
         $metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
 
         if (!is_array($metadata)) {
             return null;
         }
 
         if (!empty($metadata[$folder])) {
             return self::folder_select_metadata($metadata[$folder]);
         }
 
         return 'mail';
     }
 
     /**
      * Sets folder content-type.
      *
      * @param string $folder Folder name
      * @param string $type   Content type
      *
      * @return boolean True on success
      */
     public static function set_folder_type($folder, $type='mail')
     {
         self::setup();
 
         list($ctype, $subtype) = explode('.', $type);
 
         $success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
 
         if (!$success)  // fallback: only set private annotation
             $success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
 
         return $success;
     }
 
     /**
      * 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, $temp = false)
     {
         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) ||
             ($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
     }
 
     /**
      * 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, $temp = false)
     {
         self::setup();
 
         // 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;
         }
 
         return false;
     }
 
     /**
      * 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, $temp = false)
     {
         self::setup();
 
         // 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;
         }
 
         return false;
     }
 
     /**
      * Check activation status of this folder
      *
      * @param string $folder Folder name
      *
      * @return boolean True if active, false if not
      */
     public static function folder_is_active($folder)
     {
         $active_folders = self::get_states();
 
         return in_array($folder, $active_folders);
     }
 
     /**
      * Change activation status of this folder
      *
      * @param string $folder Folder name
      *
      * @return True on success, false on error
      */
     public static function folder_activate($folder)
     {
         // activation implies temporary subscription
         self::folder_subscribe($folder, true);
         return self::set_state($folder, true);
     }
 
     /**
      * Change activation status of this folder
      *
      * @param string $folder Folder name
      *
      * @return True on success, false on error
      */
     public static function folder_deactivate($folder)
     {
         // remove from temp subscriptions, really?
         self::folder_unsubscribe($folder, true);
 
         return self::set_state($folder, false);
     }
 
     /**
      * Return list of active folders
      */
     private static function get_states()
     {
         if (self::$states !== null) {
             return self::$states;
         }
 
         $rcube   = rcube::get_instance();
         $folders = $rcube->config->get('kolab_active_folders');
 
         if ($folders !== null) {
             self::$states = !empty($folders) ? explode('**', $folders) : array();
         }
         // for backward-compatibility copy server-side subscriptions to activation states
         else {
             self::setup();
             if (self::$subscriptions === null) {
                 self::$with_tempsubs = false;
                 self::$subscriptions = self::$imap->list_folders_subscribed();
                 self::$with_tempsubs = true;
             }
             self::$states = (array) self::$subscriptions;
             $folders = implode('**', self::$states);
             $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
         }
 
         return self::$states;
     }
 
     /**
      * Update list of active folders
      */
     private static function set_state($folder, $state)
     {
         self::get_states();
 
         // update in-memory list
         $idx = array_search($folder, self::$states);
         if ($state && $idx === false) {
             self::$states[] = $folder;
         }
         else if (!$state && $idx !== false) {
             unset(self::$states[$idx]);
         }
 
         // update user preferences
         $folders = implode('**', self::$states);
 
         return rcube::get_instance()->user->save_prefs(array('kolab_active_folders' => $folders));
     }
 
     /**
      * Creates default folder of specified type
      * To be run when none of subscribed folders (of specified type) is found
      *
      * @param string $type  Folder type
      * @param string $props Folder properties (color, etc)
      *
      * @return string Folder name
      */
     public static function create_default_folder($type, $props = array())
     {
         if (!self::setup()) {
             return;
         }
 
         $folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE));
 
         // from kolab_folders config
         $folder_type  = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default';
         $default_name = self::$config->get('kolab_folders_' . $folder_type);
         $folder_type  = str_replace('_', '.', $folder_type);
 
         // check if we have any folder in personal namespace
         // folder(s) may exist but not subscribed
         foreach ((array)$folders as $f => $data) {
             if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
                 $folder = $f;
                 break;
             }
         }
 
         if (!$folder) {
             if (!$default_name) {
                 $default_name = self::$default_folders[$type];
             }
 
             if (!$default_name) {
                 return;
             }
 
             $folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP');
             $prefix = self::$imap->get_namespace('prefix');
 
             // add personal namespace prefix if needed
             if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') {
                 $folder = $prefix . $folder;
             }
 
             if (!self::$imap->folder_exists($folder)) {
                 if (!self::$imap->create_folder($folder)) {
                     return;
                 }
             }
 
             self::set_folder_type($folder, $folder_type);
         }
 
         self::folder_subscribe($folder);
 
         if ($props['active']) {
             self::set_state($folder, true);
         }
 
         if (!empty($props)) {
             self::set_folder_props($folder, $props);
         }
 
         return $folder;
     }
 
     /**
      * Sets folder metadata properties
      *
      * @param string $folder Folder name
      * @param array  &$prop  Folder properties (color, displayname)
      */
     public static function set_folder_props($folder, &$prop)
     {
         if (!self::setup()) {
             return;
         }
 
         // TODO: also save 'showalarams' and other properties here
         $ns        = self::$imap->folder_namespace($folder);
         $supported = array(
             'color'       => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE),
             'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE),
         );
 
         foreach ($supported as $key => $metakeys) {
             if (array_key_exists($key, $prop)) {
                 $meta_saved = false;
                 if ($ns == 'personal')  // save in shared namespace for personal folders
                     $meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
                 if (!$meta_saved)    // try in private namespace
                     $meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
                 if ($meta_saved)
                     unset($prop[$key]);  // unsetting will prevent fallback to local user prefs
             }
         }
     }
 
     /**
      * Search users in Kolab LDAP 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    Maximum number of records
      * @param int     $count    Returns the number of records found
      *
      * @return array List of users
      */
     public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
     {
         $query = str_replace('*', '', $query);
 
         // requires a working LDAP setup
         if (!strlen($query) || !($ldap = self::ldap())) {
             return array();
         }
 
         $root          = self::namespace_root('other');
         $user_attrib   = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
         $search_attrib = self::$config->get('kolab_users_search_attrib', array('cn','mail','alias'));
 
         // search users using the configured attributes
         $results = $ldap->dosearch($search_attrib, $query, $mode, $required, $limit, $count);
 
         // exclude myself
         if ($_SESSION['kolab_dn']) {
             unset($results[$_SESSION['kolab_dn']]);
         }
 
         // resolve to IMAP folder name
         array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
             list($localpart, ) = explode('@', $user[$user_attrib]);
             $user['kolabtargetfolder'] = $root . $localpart;
         });
 
         return $results;
     }
 
     /**
      * 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 int     1 - subscribed folders only, 0 - all folders, 2 - all non-active
      * @param array   Will be filled with folder-types data
      *
      * @return array List of folders
      */
     public static function list_user_folders($user, $type, $subscribed = 0, &$folderdata = array())
     {
         self::setup();
 
         $folders = array();
 
         // use localpart of user attribute as root for folder listing
         $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]);
 
             $delimiter  = self::$imap->get_hierarchy_delimiter();
             $other_ns   = self::namespace_root('other');
             $prefix     = $other_ns . $mbox . $delimiter;
             $subscribed = (int) $subscribed;
             $subs       = $subscribed < 2 ? (bool) $subscribed : false;
             $folders    = self::list_folders($prefix, '*', $type, $subs, $folderdata);
 
             if ($subscribed === 2 && !empty($folders)) {
                 $active = self::get_states();
                 if (!empty($active)) {
                     $folders = array_diff($folders, $active);
                 }
             }
         }
 
         return $folders;
     }
 
     /**
      * 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($type, $subscribed)
     {
         $folders = $folderdata = array();
 
         if (self::setup()) {
             $delimiter = self::$imap->get_hierarchy_delimiter();
             $other_ns = rtrim(self::namespace_root('other'), $delimiter);
             $path_len = count(explode($delimiter, $other_ns));
 
             foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
                 if ($foldername == 'INBOX')  // skip INBOX which is added by default
                     continue;
 
                 $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]) {
                     $folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
                 }
             }
 
             // for every (subscribed) user folder, list all (unsubscribed) subfolders
             foreach ($folders as $userfolder) {
                 foreach ((array)self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) {
                     if (!$folders[$foldername]) {
                         $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
                         $userfolder->children[] = $folders[$foldername];
                     }
                 }
             }
         }
 
         return $folders;
     }
 
     /**
      * 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)
     {
         $db = rcmail::get_instance()->get_dbh();
         $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
         $db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix);
 
     }
 
     /**
      * Get folder METADATA for all supported keys
      * Do this in one go for better caching performance
      */
     public static function folder_metadata($folder)
     {
         if (self::setup()) {
             $keys = array(
                 // For better performance we skip displayname here, see (self::custom_displayname())
                 // self::NAME_KEY_PRIVATE,
                 // self::NAME_KEY_SHARED,
                 self::CTYPE_KEY,
                 self::CTYPE_KEY_PRIVATE,
                 self::COLOR_KEY_PRIVATE,
                 self::COLOR_KEY_SHARED,
                 self::UID_KEY_SHARED,
                 self::UID_KEY_CYRUS,
             );
 
             $metadata = self::$imap->get_metadata($folder, $keys);
 
             return $metadata[$folder];
         }
     }
 
     /**
      * Get user attributes for specified other user (imap) folder identifier.
      *
      * @param string $folder_id Folder name w/o path (imap user identifier)
      * @param bool   $as_string Return configured display name attribute value
      *
      * @return array User attributes
      * @see self::ldap()
      */
     public static function folder_id2user($folder_id, $as_string = false)
     {
         static $domain, $cache, $name_attr;
 
         $rcube = rcube::get_instance();
 
         if ($domain === null) {
             list(, $domain) = explode('@', $rcube->get_user_name());
         }
 
         if ($name_attr === null) {
             $name_attr = (array) ($rcube->config->get('kolab_users_name_field', $rcube->config->get('kolab_auth_name')) ?: 'name');
         }
 
         $token = $folder_id;
         if ($domain && strpos($find, '@') === false) {
             $token .= '@' . $domain;
         }
 
         if ($cache === null) {
             $cache = $rcube->get_cache_shared('kolab_users') ?: false;
         }
 
         // use value cached in memory for repeated lookups
         if (!$cache && array_key_exists($token, self::$ldapcache)) {
             $user = self::$ldapcache[$token];
         }
 
         if (empty($user) && $cache) {
             $user = $cache->get($token);
         }
 
         if (empty($user) && ($ldap = self::ldap())) {
             $user = $ldap->get_user_record($token, $_SESSION['imap_host']);
 
             if (!empty($user)) {
                 $keys = array('displayname', 'name', 'mail'); // supported keys
                 $user = array_intersect_key($user, array_flip($keys));
 
                 if (!empty($user)) {
                     if ($cache) {
                         $cache->set($token, $user);
                     }
                     else {
                         self::$ldapcache[$token] = $user;
                     }
                 }
             }
         }
 
         if (!empty($user)) {
             if ($as_string) {
                 foreach ($name_attr as $attr) {
                     if ($display = $user[$attr]) {
                         break;
                     }
                 }
 
                 if (!$display) {
                     $display = $user['displayname'] ?: $user['name'];
                 }
 
                 if ($display && $display != $folder_id) {
                     $display = "$display ($folder_id)";
                 }
 
                 return $display;
             }
 
             return $user;
         }
     }
 
     /**
      * Chwala's 'folder_mod' hook handler for mapping other users folder names
      */
     public static function folder_mod($args)
     {
         static $roots;
 
         if ($roots === null) {
             self::setup();
             $roots = self::$imap->get_namespace('other');
         }
 
         // Note: We're working with UTF7-IMAP encoding here
 
         if ($args['dir'] == 'in') {
             foreach ((array) $roots as $root) {
                 if (strpos($args['folder'], $root[0]) === 0) {
                     // remove root and explode folder
                     $delim  = $root[1];
                     $folder = explode($delim, substr($args['folder'], strlen($root[0])));
                     // compare first (user) part with a regexp, it's supposed
                     // to look like this: "Doe, Jane (uid)", so we can extract the uid
                     // and replace the folder with it
                     if (preg_match('~^[^/]+ \(([^)]+)\)$~', $folder[0], $m)) {
                         $folder[0] = $m[1];
                         $args['folder'] = $root[0] . implode($delim, $folder);
                     }
 
                     break;
                 }
             }
         }
         else { // dir == 'out'
             foreach ((array) $roots as $root) {
                 if (strpos($args['folder'], $root[0]) === 0) {
                     // remove root and explode folder
                     $delim  = $root[1];
                     $folder = explode($delim, substr($args['folder'], strlen($root[0])));
 
                     // Replace uid with "Doe, Jane (uid)"
                     if ($user = self::folder_id2user($folder[0], true)) {
                         $user      = str_replace($delim, '', $user);
                         $folder[0] = rcube_charset::convert($user, RCUBE_CHARSET, 'UTF7-IMAP');
 
                         $args['folder'] = $root[0] . implode($delim, $folder);
                     }
 
                     break;
                 }
             }
         }
 
         return $args;
     }
 }
diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 859249f1..22e9140c 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -1,1465 +1,1466 @@
 <?php
 
 /**
  * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012-2013, Kolab Systems AG <contact@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_cache
 {
     const DB_DATE_FORMAT = 'Y-m-d H:i:s';
     const MAX_RECORDS    = 500;
 
     protected $db;
     protected $imap;
     protected $folder;
     protected $uid2msg;
     protected $objects;
     protected $metadata = array();
     protected $folder_id;
     protected $resource_uri;
     protected $enabled = true;
     protected $synched = false;
     protected $synclock = false;
     protected $ready = false;
     protected $cache_table;
     protected $folders_table;
     protected $max_sql_packet;
     protected $max_sync_lock_time = 600;
     protected $extra_cols = array();
     protected $data_props = array();
     protected $order_by = null;
     protected $limit = null;
     protected $error = 0;
     protected $server_timezone;
     protected $sync_start;
+    protected $cache_bypassed = 0;
 
 
     /**
      * Factory constructor
      */
     public static function factory(kolab_storage_folder $storage_folder)
     {
         $subclass = 'kolab_storage_cache_' . $storage_folder->type;
         if (class_exists($subclass)) {
             return new $subclass($storage_folder);
         }
         else {
             rcube::raise_error(array(
                 'code' => 900,
                 'type' => 'php',
                 'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'"
             ), true);
 
             return new kolab_storage_cache($storage_folder);
         }
     }
 
 
     /**
      * Default constructor
      */
     public function __construct(kolab_storage_folder $storage_folder = null)
     {
         $rcmail = rcube::get_instance();
         $this->db = $rcmail->get_dbh();
         $this->imap = $rcmail->get_storage();
         $this->enabled = $rcmail->config->get('kolab_cache', false);
         $this->folders_table = $this->db->table_name('kolab_folders');
         $this->server_timezone = new DateTimeZone(date_default_timezone_get());
 
         if ($this->enabled) {
             // always read folder cache and lock state from DB master
             $this->db->set_table_dsn('kolab_folders', 'w');
             // remove sync-lock on script termination
             $rcmail->add_shutdown_function(array($this, '_sync_unlock'));
         }
 
         if ($storage_folder) {
             $this->set_folder($storage_folder);
         }
     }
 
     /**
      * Direct access to cache by folder_id
      * (only for internal use)
      */
     public function select_by_id($folder_id)
     {
         $query = $this->db->query("SELECT * FROM `{$this->folders_table}` WHERE `folder_id` = ?", $folder_id);
         if ($sql_arr = $this->db->fetch_assoc($query)) {
             $this->metadata = $sql_arr;
             $this->folder_id = $sql_arr['folder_id'];
             $this->folder = new StdClass;
             $this->folder->type = $sql_arr['type'];
             $this->resource_uri = $sql_arr['resource'];
             $this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']);
             $this->ready = true;
         }
     }
 
     /**
      * Connect cache with a storage folder
      *
      * @param kolab_storage_folder The storage folder instance to connect with
      */
     public function set_folder(kolab_storage_folder $storage_folder)
     {
         $this->folder = $storage_folder;
 
         if (empty($this->folder->name) || !$this->folder->valid) {
             $this->ready = false;
             return;
         }
 
         // compose fully qualified ressource uri for this instance
         $this->resource_uri = $this->folder->get_resource_uri();
         $this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
         $this->ready = $this->enabled && !empty($this->folder->type);
         $this->folder_id = null;
     }
 
     /**
      * Returns true if this cache supports query by type
      */
     public function has_type_col()
     {
         return in_array('type', $this->extra_cols);
     }
 
     /**
      * Getter for the numeric ID used in cache tables
      */
     public function get_folder_id()
     {
         $this->_read_folder_data();
         return $this->folder_id;
     }
 
     /**
      * Returns code of last error
      *
      * @return int Error code
      */
     public function get_error()
     {
         return $this->error;
     }
 
     /**
      * Synchronize local cache data with remote
      */
     public function synchronize()
     {
         // only sync once per request cycle
         if ($this->synched)
             return;
 
         if (!$this->ready) {
             // kolab cache is disabled, synchronize IMAP mailbox cache only
             $this->imap_mode(true);
             $this->imap->folder_sync($this->folder->name);
             $this->imap_mode(false);
         }
         else {
             $this->sync_start = time();
 
             // read cached folder metadata
             $this->_read_folder_data();
 
             // Read folder data from IMAP
             $ctag = $this->folder->get_ctag();
 
             // Validate current ctag
             list($uidvalidity, $highestmodseq, $uidnext) = explode('-', $ctag);
 
             if (empty($uidvalidity) || empty($highestmodseq)) {
                 rcube::raise_error(array(
                     'code' => 900,
                     'message' => "Failed to sync the kolab cache (Invalid ctag)"
                 ), true);
             }
             // check cache status ($this->metadata is set in _read_folder_data())
             else if (
                 empty($this->metadata['ctag'])
                 || empty($this->metadata['changed'])
                 || $this->metadata['ctag'] !== $ctag
             ) {
                 // lock synchronization for this folder or wait if locked
                 $this->_sync_lock();
 
                 // Run a full-sync (initial sync or continue the aborted sync)
                 if (empty($this->metadata['changed']) || empty($this->metadata['ctag'])) {
                     $result = $this->synchronize_full();
                 }
                 // Synchronize only the changes since last sync
                 else {
                     $result = $this->synchronize_update($ctag);
                 }
 
                 // update ctag value (will be written to database in _sync_unlock())
                 if ($result) {
                     $this->metadata['ctag']    = $ctag;
                     $this->metadata['changed'] = date(self::DB_DATE_FORMAT, time());
                 }
 
                 // remove lock
                 $this->_sync_unlock();
             }
         }
 
         $this->check_error();
         $this->synched = time();
     }
 
     /**
      * Perform full cache synchronization
      */
     protected function synchronize_full()
     {
         // get effective time limit we have for synchronization (~70% of the execution time)
         $time_limit = $this->_max_sync_lock_time() * 0.7;
 
         if (time() - $this->sync_start > $time_limit) {
             return false;
         }
 
         // disable messages cache if configured to do so
         $this->imap_mode(true);
 
         // synchronize IMAP mailbox cache, does nothing if messages cache is disabled
         $this->imap->folder_sync($this->folder->name);
 
         // compare IMAP index with object cache index
         $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
 
         $this->imap_mode(false);
 
         if ($imap_index->is_error()) {
             rcube::raise_error(array(
                     'code' => 900,
                     'message' => "Failed to sync the kolab cache (SEARCH failed)"
                 ), true);
             return false;
         }
 
         // determine objects to fetch or to invalidate
         $imap_index = $imap_index->get();
         $del_index  = array();
         $old_index  = $this->current_index($del_index);
 
         // Fetch objects and store in DB
         $result = $this->synchronize_fetch($imap_index, $old_index, $del_index);
 
         if ($result) {
             // Remove redundant entries from IMAP and cache
             $rem_index = array_intersect($del_index, $imap_index);
             $del_index = array_merge(array_unique($del_index), array_diff($old_index, $imap_index));
 
             $this->synchronize_delete($rem_index, $del_index);
         }
 
         return $result;
     }
 
     /**
      * Perform partial cache synchronization, based on QRESYNC
      */
     protected function synchronize_update()
     {
         if (!$this->imap->get_capability('QRESYNC')) {
             rcube::raise_error(array(
                     'code' => 900,
                     'message' => "Failed to sync the kolab cache (no QRESYNC capability)"
                 ), true);
 
             return $this->synchronize_full();
         }
 
         // Handle the previous ctag
         list($uidvalidity, $highestmodseq, $uidnext) = explode('-', $this->metadata['ctag']);
 
         if (empty($uidvalidity) || empty($highestmodseq)) {
             rcube::raise_error(array(
                     'code' => 900,
                     'message' => "Failed to sync the kolab cache (Invalid old ctag)"
                 ), true);
             return false;
         }
 
         // Enable QRESYNC
         $res = $this->imap->conn->enable('QRESYNC');
         if ($res === false) {
             rcube::raise_error(array(
                     'code' => 900,
                     'message' => "Failed to sync the kolab cache (failed to enable QRESYNC/CONDSTORE)"
                 ), true);
 
             return false;
         }
 
         $mbox_data = $this->imap->folder_data($this->folder->name);
         if (empty($mbox_data)) {
             rcube::raise_error(array(
                     'code' => 900,
                     'message' => "Failed to sync the kolab cache (failed to get folder state)"
                 ), true);
 
              return false;
         }
 
         // Check UIDVALIDITY
         if ($uidvalidity != $mbox_data['UIDVALIDITY']) {
             return $this->synchronize_full();
         }
 
         // QRESYNC not supported on specified mailbox
         if (!empty($mbox_data['NOMODSEQ']) || empty($mbox_data['HIGHESTMODSEQ'])) {
             rcube::raise_error(array(
                     'code' => 900,
                     'message' => "Failed to sync the kolab cache (QRESYNC not supported on the folder)"
                 ), true);
 
              return $this->synchronize_full();
         }
 
         // Get modified flags and vanished messages
         // UID FETCH 1:* (FLAGS) (CHANGEDSINCE 0123456789 VANISHED)
         $result = $this->imap->conn->fetch(
             $this->folder->name, '1:*', true, array('FLAGS'), $highestmodseq, true
         );
 
         $removed  = array();
         $modified = array();
         $existing = $this->current_index($removed);
 
         if (!empty($result)) {
             foreach ($result as $msg) {
                 $uid = $msg->uid;
 
                 // Message marked as deleted
                 if (!empty($msg->flags['DELETED'])) {
                     $removed[] = $uid;
                     continue;
                 }
 
                 // Flags changed or new
                 $modified[] = $uid;
             }
         }
 
         $new    = array_diff($modified, $existing, $removed);
         $result = true;
 
         if (!empty($new)) {
             $result = $this->synchronize_fetch($new, $existing, $removed);
 
             if (!$result) {
                 return false;
             }
         }
 
         // VANISHED found?
         $mbox_data = $this->imap->folder_data($this->folder->name);
 
         // Removed vanished messages from the database
-        $vanished = (array) rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED']);
+        $vanished = (array) rcube_imap_generic::uncompressMessageSet($mbox_data['VANISHED'] ?? null);
 
         // Remove redundant entries from IMAP and DB
         $vanished = array_merge($removed, array_intersect($vanished, $existing));
         $this->synchronize_delete($removed, $vanished);
 
         return $result;
     }
 
     /**
      * Fetch objects from IMAP and save into the database
      */
     protected function synchronize_fetch($new_index, &$old_index, &$del_index)
     {
         // get effective time limit we have for synchronization (~70% of the execution time)
         $time_limit = $this->_max_sync_lock_time() * 0.7;
 
         if (time() - $this->sync_start > $time_limit) {
             return false;
         }
 
         $i = 0;
         $aborted = false;
 
         // fetch new objects from imap
         foreach (array_diff($new_index, $old_index) as $msguid) {
             // Note: We'll store only objects matching the folder type
             // anything else will be silently ignored
             if ($object = $this->folder->read_object($msguid)) {
                 // Deduplication: remove older objects with the same UID
                 // Here we do not resolve conflicts, we just make sure
                 // the most recent version of the object will be used
-                if ($old_msguid = $old_index[$object['uid']]) {
+                if ($old_msguid = ($old_index[$object['uid']] ?? null)) {
                     if ($old_msguid < $msguid) {
                         $del_index[] = $old_msguid;
                     }
                     else {
                         $del_index[] = $msguid;
                         continue;
                     }
                 }
 
                 $old_index[$object['uid']] = $msguid;
 
                 $this->_extended_insert($msguid, $object);
 
                 // check time limit and abort sync if running too long
                 if (++$i % 50 == 0 && time() - $this->sync_start > $time_limit) {
                     $aborted = true;
                     break;
                 }
             }
         }
 
         $this->_extended_insert(0, null);
 
         return $aborted === false;
     }
 
     /**
      * Remove specified objects from the database and IMAP
      */
     protected function synchronize_delete($imap_delete, $db_delete)
     {
         if (!empty($imap_delete)) {
             $this->imap_mode(true);
             $this->imap->delete_message($imap_delete, $this->folder->name);
             $this->imap_mode(false);
         }
 
         if (!empty($db_delete)) {
             $quoted_ids = join(',', array_map(array($this->db, 'quote'), $db_delete));
             $this->db->query(
                 "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` IN ($quoted_ids)",
                 $this->folder_id
             );
         }
     }
 
     /**
      * Return current use->msguid index
      */
     protected function current_index(&$duplicates = array())
     {
         // read cache index
         $sql_result = $this->db->query(
             "SELECT `msguid`, `uid` FROM `{$this->cache_table}` WHERE `folder_id` = ?"
                 . " ORDER BY `msguid` DESC", $this->folder_id
         );
 
         $index = $del_index = array();
 
         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
             // Mark all duplicates for removal (note sorting order above)
             // Duplicates here should not happen, but they do sometimes
             if (isset($index[$sql_arr['uid']])) {
                 $duplicates[] = $sql_arr['msguid'];
             }
             else {
                 $index[$sql_arr['uid']] = $sql_arr['msguid'];
             }
         }
 
         return $index;
     }
 
     /**
      * Read a single entry from cache or from IMAP directly
      *
      * @param string Related IMAP message UID
      * @param string Object type to read
      * @param string IMAP folder name the entry relates to
      * @param array  Hash array with object properties or null if not found
      */
     public function get($msguid, $type = null, $foldername = null)
     {
         // delegate to another cache instance
         if ($foldername && $foldername != $this->folder->name) {
             $success = false;
             if ($targetfolder = kolab_storage::get_folder($foldername)) {
                 $success = $targetfolder->cache->get($msguid, $type);
                 $this->error = $targetfolder->cache->get_error();
             }
             return $success;
         }
 
         // load object if not in memory
         if (!isset($this->objects[$msguid])) {
             if ($this->ready) {
                 $this->_read_folder_data();
 
                 $sql_result = $this->db->query(
                     "SELECT * FROM `{$this->cache_table}` ".
                     "WHERE `folder_id` = ? AND `msguid` = ?",
                     $this->folder_id,
                     $msguid
                 );
 
                 if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                     $this->objects = array($msguid => $this->_unserialize($sql_arr));  // store only this object in memory (#2827)
                 }
             }
 
             // fetch from IMAP if not present in cache
             if (empty($this->objects[$msguid])) {
                 if ($object = $this->folder->read_object($msguid, $type ?: '*', $foldername)) {
                     $this->objects = array($msguid => $object);
                     $this->set($msguid, $object);
                 }
             }
         }
 
         $this->check_error();
         return $this->objects[$msguid];
     }
 
     /**
      * Getter for a single Kolab object identified by its UID
      *
      * @param string $uid Object UID
      *
      * @return array The Kolab object represented as hash array
      */
     public function get_by_uid($uid)
     {
         $old_order_by = $this->order_by;
         $old_limit    = $this->limit;
 
         // set order to make sure we get most recent object version
         // set limit to skip count query
         $this->order_by = '`msguid` DESC';
         $this->limit    = array(1, 0);
 
         $list = $this->select(array(array('uid', '=', $uid)));
 
         // set the order/limit back to defined value
         $this->order_by = $old_order_by;
         $this->limit    = $old_limit;
 
         if (!empty($list) && !empty($list[0])) {
             return $list[0];
         }
     }
 
     /**
      * Insert/Update a cache entry
      *
      * @param string Related IMAP message UID
      * @param mixed  Hash array with object properties to save or false to delete the cache entry
      * @param string IMAP folder name the entry relates to
      */
     public function set($msguid, $object, $foldername = null)
     {
         if (!$msguid) {
             return;
         }
 
         // delegate to another cache instance
         if ($foldername && $foldername != $this->folder->name) {
           if ($targetfolder = kolab_storage::get_folder($foldername)) {
               $targetfolder->cache->set($msguid, $object);
               $this->error = $targetfolder->cache->get_error();
           }
           return;
         }
 
         // remove old entry
         if ($this->ready) {
             $this->_read_folder_data();
             $this->db->query("DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` = ?",
                 $this->folder_id, $msguid);
         }
 
         if ($object) {
             // insert new object data...
             $this->save($msguid, $object);
         }
         else {
             // ...or set in-memory cache to false
             $this->objects[$msguid] = $object;
         }
 
         $this->check_error();
     }
 
 
     /**
      * Insert (or update) a cache entry
      *
      * @param int    Related IMAP message UID
      * @param mixed  Hash array with object properties to save or false to delete the cache entry
      * @param int    Optional old message UID (for update)
      */
     public function save($msguid, $object, $olduid = null)
     {
         // write to cache
         if ($this->ready) {
             $this->_read_folder_data();
 
             $sql_data = $this->_serialize($object);
             $sql_data['folder_id'] = $this->folder_id;
             $sql_data['msguid']    = $msguid;
             $sql_data['uid']       = $object['uid'];
 
             $args = array();
             $cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'tags', 'words');
             $cols = array_merge($cols, $this->extra_cols);
 
             foreach ($cols as $idx => $col) {
                 $cols[$idx] = $this->db->quote_identifier($col);
                 $args[]     = $sql_data[$col];
             }
 
             if ($olduid) {
                 foreach ($cols as $idx => $col) {
                     $cols[$idx] = "$col = ?";
                 }
 
                 $query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
                     . " WHERE `folder_id` = ? AND `msguid` = ?";
                 $args[] = $this->folder_id;
                 $args[] = $olduid;
             }
             else {
                 $query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
                     . ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
             }
 
             $result = $this->db->query($query, $args);
 
             if (!$this->db->affected_rows($result)) {
                 rcube::raise_error(array(
                     'code' => 900, 'type' => 'php',
                     'message' => "Failed to write to kolab cache"
                 ), true);
             }
         }
 
         // keep a copy in memory for fast access
         $this->objects = array($msguid => $object);
         $this->uid2msg = array($object['uid'] => $msguid);
 
         $this->check_error();
     }
 
 
     /**
      * Move an existing cache entry to a new resource
      *
      * @param string               Entry's IMAP message UID
      * @param string               Entry's Object UID
      * @param kolab_storage_folder Target storage folder instance
      * @param string               Target entry's IMAP message UID
      */
     public function move($msguid, $uid, $target, $new_msguid = null)
     {
         if ($this->ready && $target) {
             // clear cached uid mapping and force new lookup
             unset($target->cache->uid2msg[$uid]);
 
             // resolve new message UID in target folder
             if (!$new_msguid) {
                 $new_msguid = $target->cache->uid2msguid($uid);
             }
 
             if ($new_msguid) {
                 $this->_read_folder_data();
 
                 $this->db->query(
                     "UPDATE `{$this->cache_table}` SET `folder_id` = ?, `msguid` = ? ".
                     "WHERE `folder_id` = ? AND `msguid` = ?",
                     $target->cache->get_folder_id(),
                     $new_msguid,
                     $this->folder_id,
                     $msguid
                 );
 
                 $result = $this->db->affected_rows();
             }
         }
 
         if (empty($result)) {
             // just clear cache entry
             $this->set($msguid, false);
         }
 
         unset($this->uid2msg[$uid]);
         $this->check_error();
     }
 
 
     /**
      * Remove all objects from local cache
      */
     public function purge()
     {
         if (!$this->ready) {
             return true;
         }
 
         $this->_read_folder_data();
 
         $result = $this->db->query(
             "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ?",
             $this->folder_id
         );
 
         return $this->db->affected_rows($result);
     }
 
     /**
      * Update resource URI for existing cache entries
      *
      * @param string Target IMAP folder to move it to
      */
     public function rename($new_folder)
     {
         if (!$this->ready) {
             return;
         }
 
         if ($target = kolab_storage::get_folder($new_folder)) {
             // resolve new message UID in target folder
             $this->db->query(
                 "UPDATE `{$this->folders_table}` SET `resource` = ? ".
                 "WHERE `resource` = ?",
                 $target->get_resource_uri(),
                 $this->resource_uri
             );
 
             $this->check_error();
         }
         else {
             $this->error = kolab_storage::ERROR_IMAP_CONN;
         }
     }
 
     /**
      * Select Kolab objects filtered by the given query
      *
      * @param array Pseudo-SQL query as list of filter parameter triplets
      *   triplet: array('<colname>', '<comparator>', '<value>')
      * @param boolean Set true to only return UIDs instead of complete objects
      * @param boolean Use fast mode to fetch only minimal set of information
      *                (no xml fetching and parsing, etc.)
      *
      * @return array List of Kolab data objects (each represented as hash array) or UIDs
      */
     public function select($query = array(), $uids = false, $fast = false)
     {
         $result = $uids ? array() : new kolab_storage_dataset($this);
         $count = null;
 
         // read from local cache DB (assume it to be synchronized)
         if ($this->ready) {
             $this->_read_folder_data();
 
             // fetch full object data on one query if a small result set is expected
             $fetchall = !$uids && ($this->limit ? $this->limit[0] : ($count = $this->count($query))) < self::MAX_RECORDS;
 
             // skip SELECT if we know it will return nothing
             if ($count === 0) {
                 return $result;
             }
 
             $sql_query = "SELECT " . ($fetchall ? '*' : "`msguid` AS `_msguid`, `uid`")
                 . " FROM `{$this->cache_table}` WHERE `folder_id` = ?"
                 . $this->_sql_where($query)
                 . (!empty($this->order_by) ? " ORDER BY " . $this->order_by : '');
 
             $sql_result = $this->limit ?
                 $this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
                 $this->db->query($sql_query, $this->folder_id);
 
             if ($this->db->is_error($sql_result)) {
                 if ($uids) {
                     return null;
                 }
                 $result->set_error(true);
                 return $result;
             }
 
             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                 if ($fast) {
                     $sql_arr['fast-mode'] = true;
                 }
                 if ($uids) {
                     $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
                     $result[] = $sql_arr['uid'];
                 }
                 else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
                     $result[] = $object;
                 }
                 else if (!$fetchall) {
                     // only add msguid to dataset index
                     $result[] = $sql_arr;
                 }
             }
         }
         // use IMAP
         else {
             $filter = $this->_query2assoc($query);
 
             $this->imap_mode(true);
 
             if ($filter['type']) {
                 $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
                 $index  = $this->imap->search_once($this->folder->name, $search);
             }
             else {
                 $index = $this->imap->index($this->folder->name, null, null, true, true);
             }
 
             $this->imap_mode(false);
 
             if ($index->is_error()) {
                 $this->check_error();
                 if ($uids) {
                     return null;
                 }
                 $result->set_error(true);
                 return $result;
             }
 
             $index  = $index->get();
             $result = $uids ? $index : $this->_fetch($index, $filter['type']);
 
             // TODO: post-filter result according to query
         }
 
         // We don't want to cache big results in-memory, however
         // if we select only one object here, there's a big chance we will need it later
         if (!$uids && count($result) == 1) {
             if ($msguid = $result[0]['_msguid']) {
                 $this->uid2msg[$result[0]['uid']] = $msguid;
                 $this->objects = array($msguid => $result[0]);
             }
         }
 
         $this->check_error();
 
         return $result;
     }
 
     /**
      * Get number of objects mathing the given query
      *
      * @param array  $query Pseudo-SQL query as list of filter parameter triplets
      * @return integer The number of objects of the given type
      */
     public function count($query = array())
     {
         // read from local cache DB (assume it to be synchronized)
         if ($this->ready) {
             $this->_read_folder_data();
 
             $sql_result = $this->db->query(
                 "SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
                 "WHERE `folder_id` = ?" . $this->_sql_where($query),
                 $this->folder_id
             );
 
             if ($this->db->is_error($sql_result)) {
                 return null;
             }
 
             $sql_arr = $this->db->fetch_assoc($sql_result);
             $count   = intval($sql_arr['numrows']);
         }
         // use IMAP
         else {
             $filter = $this->_query2assoc($query);
 
             $this->imap_mode(true);
 
             if ($filter['type']) {
                 $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
                 $index  = $this->imap->search_once($this->folder->name, $search);
             }
             else {
                 $index = $this->imap->index($this->folder->name, null, null, true, true);
             }
 
             $this->imap_mode(false);
 
             if ($index->is_error()) {
                 $this->check_error();
                 return null;
             }
 
             // TODO: post-filter result according to query
 
             $count = $index->count();
         }
 
         $this->check_error();
         return $count;
     }
 
     /**
      * Define ORDER BY clause for cache queries
      */
     public function set_order_by($sortcols)
     {
         if (!empty($sortcols)) {
             $sortcols = array_map(function($v) {
                 $v = trim($v);
                 if (strpos($v, ' ')) {
                     list($column, $order) = explode(' ', $v, 2);
                     return "`{$column}` {$order}";
                 }
                 return "`{$v}`";
             }, (array) $sortcols);
 
             $this->order_by = join(', ', $sortcols);
         }
         else {
             $this->order_by = null;
         }
     }
 
     /**
      * Define LIMIT clause for cache queries
      */
     public function set_limit($length, $offset = 0)
     {
         $this->limit = array($length, $offset);
     }
 
     /**
      * Helper method to compose a valid SQL query from pseudo filter triplets
      */
     protected function _sql_where($query)
     {
         $sql_where = '';
         foreach ((array) $query as $param) {
             if (is_array($param[0])) {
                 $subq = array();
                 foreach ($param[0] as $q) {
                     $subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where(array($q)));
                 }
                 if (!empty($subq)) {
                     $sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')';
                 }
                 continue;
             }
             else if ($param[1] == '=' && is_array($param[2])) {
                 $qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
                 $param[1] = 'IN';
             }
             else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') {
                 $not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : '';
                 $param[1] = $not . 'LIKE';
                 $qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
             }
             else if ($param[1] == '~*' || $param[1] == '!~*') {
                 $not = $param[1][1] == '!' ? 'NOT ' : '';
                 $param[1] = $not . 'LIKE';
                 $qvalue = $this->db->quote(preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
             }
             else if ($param[0] == 'tags') {
                 $param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE';
                 $qvalue = $this->db->quote('% '.$param[2].' %');
             }
             else {
                 $qvalue = $this->db->quote($param[2]);
             }
 
             $sql_where .= sprintf(' AND %s %s %s',
                 $this->db->quote_identifier($param[0]),
                 $param[1],
                 $qvalue
             );
         }
 
         return $sql_where;
     }
 
     /**
      * Helper method to convert the given pseudo-query triplets into
      * an associative filter array with 'equals' values only
      */
     protected function _query2assoc($query)
     {
         // extract object type from query parameter
         $filter = array();
         foreach ($query as $param) {
             if ($param[1] == '=')
                 $filter[$param[0]] = $param[2];
         }
         return $filter;
     }
 
     /**
      * Fetch messages from IMAP
      *
      * @param array  List of message UIDs to fetch
      * @param string Requested object type or * for all
      * @param string IMAP folder to read from
      * @return array List of parsed Kolab objects
      */
     protected function _fetch($index, $type = null, $folder = null)
     {
         $results = new kolab_storage_dataset($this);
         foreach ((array)$index as $msguid) {
             if ($object = $this->folder->read_object($msguid, $type, $folder)) {
                 $results[] = $object;
                 $this->set($msguid, $object);
             }
         }
 
         return $results;
     }
 
     /**
      * Helper method to convert the given Kolab object into a dataset to be written to cache
      */
     protected function _serialize($object)
     {
         $data     = array();
         $sql_data = array('changed' => null, 'tags' => '', 'words' => '');
 
         if ($object['changed']) {
             $sql_data['changed'] = date(self::DB_DATE_FORMAT, is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
         }
 
         if ($object['_formatobj']) {
             $xml = (string) $object['_formatobj']->write(3.0);
 
             $data['_size']     = strlen($xml);
             $sql_data['tags']  = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' ';  // pad with spaces for strict/prefix search
             $sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
         }
 
         // Store only minimal set of object properties
         foreach ($this->data_props as $prop) {
             if (isset($object[$prop])) {
                 $data[$prop] = $object[$prop];
                 if ($data[$prop] instanceof DateTimeInterface) {
                     $data[$prop] = array(
                         'cl' => 'DateTime',
                         'dt' => $data[$prop]->format('Y-m-d H:i:s'),
                         'tz' => $data[$prop]->getTimezone()->getName(),
                     );
                 }
             }
         }
 
         $sql_data['data'] = json_encode(rcube_charset::clean($data));
 
         return $sql_data;
     }
 
     /**
      * Helper method to turn stored cache data into a valid storage object
      */
     protected function _unserialize($sql_arr)
     {
-        if ($sql_arr['fast-mode'] && !empty($sql_arr['data']) && ($object = json_decode($sql_arr['data'], true))) {
+        if (($sql_arr['fast-mode'] ?? false) && !empty($sql_arr['data']) && ($object = json_decode($sql_arr['data'], true))) {
             $object['uid'] = $sql_arr['uid'];
 
             foreach ($this->data_props as $prop) {
-                if (isset($object[$prop]) && is_array($object[$prop]) && $object[$prop]['cl'] == 'DateTime') {
+                if (isset($object[$prop]) && is_array($object[$prop]) && isset($object[$prop]['cl']) && $object[$prop]['cl'] == 'DateTime') {
                     $object[$prop] = new DateTime($object[$prop]['dt'], new DateTimeZone($object[$prop]['tz']));
                 }
                 else if (!isset($object[$prop]) && isset($sql_arr[$prop])) {
                     $object[$prop] = $sql_arr[$prop];
                 }
             }
 
             if ($sql_arr['created'] && empty($object['created'])) {
                 $object['created'] = new DateTime($sql_arr['created']);
             }
 
             if ($sql_arr['changed'] && empty($object['changed'])) {
                 $object['changed'] = new DateTime($sql_arr['changed']);
             }
 
-            $object['_type']     = $sql_arr['type'] ?: $this->folder->type;
+            $object['_type']     = $sql_arr['type'] ?? $this->folder->type;
             $object['_msguid']   = $sql_arr['msguid'];
             $object['_mailbox']  = $this->folder->name;
         }
         // Fetch object xml
         else {
             // FIXME: Because old cache solution allowed storing objects that
             // do not match folder type we may end up with invalid objects.
             // 2nd argument of read_object() here makes sure they are still
             // usable. However, not allowing them here might be also an intended
             // solution in future.
             $object = $this->folder->read_object($sql_arr['msguid'], '*');
         }
 
         return $object;
     }
 
     /**
      * Write records into cache using extended inserts to reduce the number of queries to be executed
      *
      * @param int  Message UID. Set 0 to commit buffered inserts
      * @param array Kolab object to cache
      */
     protected function _extended_insert($msguid, $object)
     {
         static $buffer = '';
 
         $line = '';
         $cols = array('folder_id', 'msguid', 'uid', 'created', 'changed', 'data', 'tags', 'words');
         if ($this->extra_cols) {
             $cols = array_merge($cols, $this->extra_cols);
         }
 
         if ($object) {
             $sql_data = $this->_serialize($object);
 
             // Skip multi-folder insert for all databases but MySQL
             // In Oracle we can't put long data inline, others we don't support yet
             if (strpos($this->db->db_provider, 'mysql') !== 0) {
                 $extra_args = array();
                 $params = array($this->folder_id, $msguid, $object['uid'], $sql_data['changed'],
                     $sql_data['data'], $sql_data['tags'], $sql_data['words']);
 
                 foreach ($this->extra_cols as $col) {
                     $params[] = $sql_data[$col];
                     $extra_args[] = '?';
                 }
 
                 $cols = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
                 $extra_args = count($extra_args) ? ', ' . implode(', ', $extra_args) : '';
 
                 $result = $this->db->query(
                     "INSERT INTO `{$this->cache_table}` ($cols)"
                     . " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?$extra_args)",
                     $params
                 );
 
                 if (!$this->db->affected_rows($result)) {
                     rcube::raise_error(array(
                         'code' => 900, 'message' => "Failed to write to kolab cache"
                     ), true);
                 }
 
                 return;
             }
 
             $values = array(
                 $this->db->quote($this->folder_id),
                 $this->db->quote($msguid),
                 $this->db->quote($object['uid']),
                 $this->db->now(),
                 $this->db->quote($sql_data['changed']),
                 $this->db->quote($sql_data['data']),
                 $this->db->quote($sql_data['tags']),
                 $this->db->quote($sql_data['words']),
             );
             foreach ($this->extra_cols as $col) {
                 $values[] = $this->db->quote($sql_data[$col]);
             }
             $line = '(' . join(',', $values) . ')';
         }
 
         if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
             $columns = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
             $update  = implode(', ', array_map(function($i) { return "`{$i}` = VALUES(`{$i}`)"; }, array_slice($cols, 2)));
 
             $result = $this->db->query(
                 "INSERT INTO `{$this->cache_table}` ($columns) VALUES $buffer"
                 . " ON DUPLICATE KEY UPDATE $update"
             );
 
             if (!$this->db->affected_rows($result)) {
                 rcube::raise_error(array(
                     'code' => 900, 'message' => "Failed to write to kolab cache"
                 ), true);
             }
 
             $buffer = '';
         }
 
         $buffer .= ($buffer ? ',' : '') . $line;
     }
 
     /**
      * Returns max_allowed_packet from mysql config
      */
     protected function max_sql_packet()
     {
         if (!$this->max_sql_packet) {
             // mysql limit or max 4 MB
             $value = $this->db->get_variable('max_allowed_packet', 1048500);
             $this->max_sql_packet = min($value, 4*1024*1024) - 2000;
         }
 
         return $this->max_sql_packet;
     }
 
     /**
      * Read this folder's ID and cache metadata
      */
     protected function _read_folder_data()
     {
         // already done
         if (!empty($this->folder_id) || !$this->ready)
             return;
 
         $sql_arr = $this->db->fetch_assoc($this->db->query(
                 "SELECT `folder_id`, `synclock`, `ctag`, `changed`"
                 . " FROM `{$this->folders_table}` WHERE `resource` = ?",
                 $this->resource_uri
         ));
 
         if ($sql_arr) {
             $this->metadata = $sql_arr;
             $this->folder_id = $sql_arr['folder_id'];
         }
         else {
             $this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)"
                 . " VALUES (?, ?)", $this->resource_uri, $this->folder->type);
 
             $this->folder_id = $this->db->insert_id('kolab_folders');
             $this->metadata = array();
         }
     }
 
     /**
      * Check lock record for this folder and wait if locked or set lock
      */
     protected function _sync_lock()
     {
         if (!$this->ready)
             return;
 
         $this->_read_folder_data();
 
         // abort if database is not set-up
         if ($this->db->is_error()) {
             $this->check_error();
             $this->ready = false;
             return;
         }
 
         $read_query  = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?";
         $write_query = "UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ? AND `synclock` = ?";
         $max_lock_time = $this->_max_sync_lock_time();
 
         // wait if locked (expire locks after 10 minutes) ...
         // ... or if setting lock fails (another process meanwhile set it)
         while (
             (intval($this->metadata['synclock'] ?? 0) + $max_lock_time > time()) ||
             (($res = $this->db->query($write_query, time(), $this->folder_id, intval($this->metadata['synclock'] ?? 0)))
                 && !($affected = $this->db->affected_rows($res))
             )
         ) {
             usleep(500000);
             $this->metadata = $this->db->fetch_assoc($this->db->query($read_query, $this->folder_id));
         }
 
         $this->synclock = $affected > 0;
     }
 
     /**
      * Remove lock for this folder
      */
     public function _sync_unlock()
     {
         if (!$this->ready || !$this->synclock)
             return;
 
         $this->db->query(
             "UPDATE `{$this->folders_table}` SET `synclock` = 0, `ctag` = ?, `changed` = ? WHERE `folder_id` = ?",
             $this->metadata['ctag'],
             $this->metadata['changed'],
             $this->folder_id
         );
 
         $this->synclock = false;
     }
 
     protected function _max_sync_lock_time()
     {
         $limit = get_offset_sec(ini_get('max_execution_time'));
 
         if ($limit <= 0 || $limit > $this->max_sync_lock_time) {
             $limit = $this->max_sync_lock_time;
         }
 
         return $limit;
     }
 
     /**
      * Check IMAP connection error state
      */
     protected function check_error()
     {
         if (($err_code = $this->imap->get_error_code()) < 0) {
             $this->error = kolab_storage::ERROR_IMAP_CONN;
             if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
                 $this->error = kolab_storage::ERROR_NO_PERMISSION;
             }
         }
         else if ($this->db->is_error()) {
             $this->error = kolab_storage::ERROR_CACHE_DB;
         }
     }
 
     /**
      * Resolve an object UID into an IMAP message UID
      *
      * @param string  Kolab object UID
      * @param boolean Include deleted objects
      * @return int The resolved IMAP message UID
      */
     public function uid2msguid($uid, $deleted = false)
     {
         // query local database if available
         if (!isset($this->uid2msg[$uid]) && $this->ready) {
             $this->_read_folder_data();
 
             $sql_result = $this->db->query(
                 "SELECT `msguid` FROM `{$this->cache_table}` ".
                 "WHERE `folder_id` = ? AND `uid` = ? ORDER BY `msguid` DESC",
                 $this->folder_id,
                 $uid
             );
 
             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                 $this->uid2msg[$uid] = $sql_arr['msguid'];
             }
         }
 
         if (!isset($this->uid2msg[$uid])) {
             // use IMAP SEARCH to get the right message
             $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .
                 'HEADER SUBJECT ' . rcube_imap_generic::escape($uid));
             $results = $index->get();
             $this->uid2msg[$uid] = end($results);
         }
 
         return $this->uid2msg[$uid];
     }
 
     /**
      * Getter for protected member variables
      */
     public function __get($name)
     {
         if ($name == 'folder_id') {
             $this->_read_folder_data();
         }
 
         return $this->$name;
     }
 
     /**
      * Set Roundcube storage options and bypass messages/indexes cache.
      *
      * We use skip_deleted and threading settings specific to Kolab,
      * we have to change these global settings only temporarily.
      * Roundcube cache duplicates information already stored in kolab_cache,
      * that's why we can disable it for better performance.
      *
      * @param bool $force True to start Kolab mode, False to stop it.
      */
     public function imap_mode($force = false)
     {
         // remember current IMAP settings
         if ($force) {
             $this->imap_options = array(
                 'skip_deleted' => $this->imap->get_option('skip_deleted'),
                 'threading'    => $this->imap->get_threading(),
             );
         }
 
         // re-set IMAP settings
         $this->imap->set_threading($force ? false : $this->imap_options['threading']);
         $this->imap->set_options(array(
                 'skip_deleted' => $force ? true : $this->imap_options['skip_deleted'],
         ));
 
         // if kolab cache is disabled do nothing
         if (!$this->enabled) {
             return;
         }
 
         static $messages_cache, $cache_bypass;
 
         if ($messages_cache === null) {
             $rcmail = rcube::get_instance();
             $messages_cache = (bool) $rcmail->config->get('messages_cache');
             $cache_bypass   = (int) $rcmail->config->get('kolab_messages_cache_bypass');
         }
 
         if ($messages_cache) {
             // handle recurrent (multilevel) bypass() calls
             if ($force) {
                 $this->cache_bypassed += 1;
                 if ($this->cache_bypassed > 1) {
                     return;
                 }
             }
             else {
                 $this->cache_bypassed -= 1;
                 if ($this->cache_bypassed > 0) {
                     return;
                 }
             }
 
             switch ($cache_bypass) {
                 case 2:
                     // Disable messages and index cache completely
                     $this->imap->set_messages_caching(!$force);
                     break;
 
                 case 3:
                 case 1:
                     // We'll disable messages cache, but keep index cache (1) or vice-versa (3)
                     // Default mode is both (MODE_INDEX | MODE_MESSAGE)
                     $mode = $cache_bypass == 3 ? rcube_imap_cache::MODE_MESSAGE : rcube_imap_cache::MODE_INDEX;
 
                     if (!$force) {
                         $mode |= $cache_bypass == 3 ? rcube_imap_cache::MODE_INDEX : rcube_imap_cache::MODE_MESSAGE;
                     }
 
                     $this->imap->set_messages_caching(true, $mode);
             }
         }
     }
 
     /**
      * Converts DateTime or unix timestamp into sql date format
      * using server timezone.
      */
     protected function _convert_datetime($datetime)
     {
         if (is_object($datetime)) {
             $dt = clone $datetime;
             $dt->setTimeZone($this->server_timezone);
             return $dt->format(self::DB_DATE_FORMAT);
         }
         else if ($datetime) {
             return date(self::DB_DATE_FORMAT, $datetime);
         }
     }
 }
diff --git a/plugins/libkolab/lib/kolab_storage_cache_contact.php b/plugins/libkolab/lib/kolab_storage_cache_contact.php
index 22d0a330..ab39ef7f 100644
--- a/plugins/libkolab/lib/kolab_storage_cache_contact.php
+++ b/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -1,68 +1,68 @@
 <?php
 
 /**
  * Kolab storage cache class for contact objects
  *
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2013, Kolab Systems AG <contact@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_cache_contact extends kolab_storage_cache
 {
     protected $extra_cols_max = 255;
     protected $extra_cols     = array('type', 'name', 'firstname', 'surname', 'email');
     protected $data_props     = array('type', 'name', 'firstname', 'middlename', 'prefix', 'suffix', 'surname', 'email', 'organization', 'member');
 
     /**
      * Helper method to convert the given Kolab object into a dataset to be written to cache
      *
      * @override
      */
     protected function _serialize($object)
     {
         $sql_data = parent::_serialize($object);
         $sql_data['type'] = $object['_type'];
 
         // columns for sorting
-        $sql_data['name']      = rcube_charset::clean($object['name'] . $object['prefix']);
-        $sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']);
-        $sql_data['surname']   = rcube_charset::clean($object['surname']   . $object['firstname']  . $object['middlename']);
-        $sql_data['email']     = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']);
+        $sql_data['name']      = rcube_charset::clean(($object['name'] ?? null) . ($object['prefix'] ?? null));
+        $sql_data['firstname'] = rcube_charset::clean(($object['firstname'] ?? null) . ($object['middlename'] ?? null) . ($object['surname'] ?? null));
+        $sql_data['surname']   = rcube_charset::clean(($object['surname'] ?? null)   . ($object['firstname'] ?? null)  . ($object['middlename'] ?? null));
+        $sql_data['email']     = rcube_charset::clean(is_array($object['email'] ?? null) ? $object['email'][0] : ($object['email'] ?? null));
 
-        if (is_array($sql_data['email'])) {
+        if (is_array($sql_data['email'] ?? null)) {
             $sql_data['email'] = $sql_data['email']['address'];
         }
         // avoid value being null
-        if (empty($sql_data['email'])) {
+        if (empty($sql_data['email'] ?? null)) {
             $sql_data['email'] = '';
         }
 
         // use organization if name is empty
-        if (empty($sql_data['name']) && !empty($object['organization'])) {
+        if (empty($sql_data['name'] ?? null) && !empty($object['organization'] ?? null)) {
             $sql_data['name'] = rcube_charset::clean($object['organization']);
         }
 
         // make sure some data is not longer that database limit (#5291)
         foreach ($this->extra_cols as $col) {
             if (strlen($sql_data[$col]) > $this->extra_cols_max) {
                 $sql_data[$col] = rcube_charset::clean(substr($sql_data[$col], 0,  $this->extra_cols_max));
             }
         }
 
         return $sql_data;
     }
 }
diff --git a/plugins/libkolab/lib/kolab_storage_cache_event.php b/plugins/libkolab/lib/kolab_storage_cache_event.php
index 5ad9a3a1..625b10fe 100644
--- a/plugins/libkolab/lib/kolab_storage_cache_event.php
+++ b/plugins/libkolab/lib/kolab_storage_cache_event.php
@@ -1,68 +1,68 @@
 <?php
 
 /**
  * Kolab storage cache class for calendar event objects
  *
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2013, Kolab Systems AG <contact@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_cache_event extends kolab_storage_cache
 {
     protected $extra_cols = array('dtstart','dtend');
     protected $data_props = array('categories', 'status', 'attendees'); // start, end
 
     /**
      * Helper method to convert the given Kolab object into a dataset to be written to cache
      *
      * @override
      */
     protected function _serialize($object)
     {
         $sql_data = parent::_serialize($object);
 
-        $sql_data['dtstart'] = $this->_convert_datetime($object['start']);
-        $sql_data['dtend']   = $this->_convert_datetime($object['end']);
+        $sql_data['dtstart'] = $this->_convert_datetime($object['start'] ?? null);
+        $sql_data['dtend']   = $this->_convert_datetime($object['end'] ?? null);
 
         // extend date range for recurring events
-        if ($object['recurrence']) {
+        if ($object['recurrence'] ?? null) {
             $recurrence = new kolab_date_recurrence($object['_formatobj']);
             $dtend = $recurrence->end() ?: new DateTime('now +100 years');
             $sql_data['dtend'] = $this->_convert_datetime($dtend);
         }
 
         // extend start/end dates to spawn all exceptions
-        if (is_array($object['exceptions'])) {
+        if (is_array($object['exceptions'] ?? null)) {
             foreach ($object['exceptions'] as $exception) {
-                if ($exception['start'] instanceof DateTimeInterface) {
+                if (($exception['start'] ?? null) instanceof DateTimeInterface) {
                     $exstart = $this->_convert_datetime($exception['start']);
                     if ($exstart < $sql_data['dtstart']) {
                         $sql_data['dtstart'] = $exstart;
                     }
                 }
-                if ($exception['end'] instanceof DateTimeInterface) {
+                if (($exception['end'] ?? null) instanceof DateTimeInterface) {
                     $exend = $this->_convert_datetime($exception['end']);
                     if ($exend > $sql_data['dtend']) {
                         $sql_data['dtend'] = $exend;
                     }
                 }
             }
         }
 
         return $sql_data;
     }
 }
diff --git a/plugins/libkolab/lib/kolab_storage_cache_task.php b/plugins/libkolab/lib/kolab_storage_cache_task.php
index 9c776d75..55425fcc 100644
--- a/plugins/libkolab/lib/kolab_storage_cache_task.php
+++ b/plugins/libkolab/lib/kolab_storage_cache_task.php
@@ -1,43 +1,43 @@
 <?php
 
 /**
  * Kolab storage cache class for task objects
  *
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2013, Kolab Systems AG <contact@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_cache_task extends kolab_storage_cache
 {
     protected $extra_cols = array('dtstart', 'dtend');
     protected $data_props = array('categories', 'status', 'complete', 'start', 'due');
 
     /**
      * Helper method to convert the given Kolab object into a dataset to be written to cache
      *
      * @override
      */
     protected function _serialize($object)
     {
         $sql_data = parent::_serialize($object);
 
-        $sql_data['dtstart'] = $this->_convert_datetime($object['start']);
-        $sql_data['dtend']   = $this->_convert_datetime($object['due']);
+        $sql_data['dtstart'] = $this->_convert_datetime($object['start'] ?? null);
+        $sql_data['dtend']   = $this->_convert_datetime($object['due'] ?? null);
 
         return $sql_data;
     }
 }
diff --git a/plugins/libkolab/lib/kolab_storage_config.php b/plugins/libkolab/lib/kolab_storage_config.php
index ee316145..64d305ab 100644
--- a/plugins/libkolab/lib/kolab_storage_config.php
+++ b/plugins/libkolab/lib/kolab_storage_config.php
@@ -1,1009 +1,1009 @@
 <?php
 
 /**
  * Kolab storage class providing access to configuration objects on a Kolab server.
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2012-2014, Kolab Systems AG <contact@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_config
 {
     const FOLDER_TYPE   = 'configuration';
     const MAX_RELATIONS = 499; // should be less than kolab_storage_cache::MAX_RECORDS
 
     /**
      * Singleton instace of kolab_storage_config
      *
      * @var kolab_storage_config
      */
     static protected $instance;
 
     private $folders;
     private $default;
     private $enabled;
     private $tags;
 
 
     /**
      * This implements the 'singleton' design pattern
      *
      * @return kolab_storage_config The one and only instance
      */
     static function get_instance()
     {
         if (!self::$instance) {
             self::$instance = new kolab_storage_config();
         }
 
         return self::$instance;
     }
 
     /**
      * Private constructor (finds default configuration folder as a config source)
      */
     private function _init()
     {
         if ($this->enabled !== null) {
             return $this->enabled;
         }
 
         // get all configuration folders
         $this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false);
 
         foreach ($this->folders as $folder) {
             if ($folder->default) {
                 $this->default = $folder;
                 break;
             }
         }
 
         // if no folder is set as default, choose the first one
         if (!$this->default) {
             $this->default = reset($this->folders);
         }
 
         // attempt to create a default folder if it does not exist
         if (!$this->default) {
             $folder_name = 'Configuration';
             $folder_type = self::FOLDER_TYPE . '.default';
 
             if (kolab_storage::folder_create($folder_name, $folder_type, true)) {
                 $this->default = new kolab_storage_folder($folder_name, $folder_type);
             }
         }
 
         // check if configuration folder exist
         return $this->enabled = $this->default && $this->default->name;
     }
 
     /**
      * Check wether any configuration storage (folder) exists
      *
      * @return bool
      */
     public function is_enabled()
     {
         return $this->_init();
     }
 
     /**
      * Get configuration objects
      *
      * @param array $filter  Search filter
      * @param bool  $default Enable to get objects only from default folder
      * @param int   $limit   Max. number of records (per-folder)
      *
      * @return array List of objects
      */
     public function get_objects($filter = array(), $default = false, $limit = 0)
     {
         $list = array();
 
         if (!$this->is_enabled()) {
             return $list;
         }
 
         foreach ($this->folders as $folder) {
             // we only want to read from default folder
             if ($default && !$folder->default) {
                 continue;
             }
 
             // for better performance it's good to assume max. number of records
             if ($limit) {
                 $folder->set_order_and_limit(null, $limit);
             }
 
             foreach ($folder->select($filter, true) as $object) {
                 unset($object['_formatobj']);
                 $list[] = $object;
             }
         }
 
         return $list;
     }
 
     /**
      * Get configuration object
      *
      * @param string $uid     Object UID
      * @param bool   $default Enable to get objects only from default folder
      *
      * @return array Object data
      */
     public function get_object($uid, $default = false)
     {
         if (!$this->is_enabled()) {
             return;
         }
 
         foreach ($this->folders as $folder) {
             // we only want to read from default folder
             if ($default && !$folder->default) {
                 continue;
             }
 
             if ($object = $folder->get_object($uid)) {
                 return $object;
             }
         }
     }
 
     /**
      * Create/update configuration object
      *
      * @param array  $object Object data
      * @param string $type   Object type
      *
      * @return bool True on success, False on failure
      */
     public function save(&$object, $type)
     {
         if (!$this->is_enabled()) {
             return false;
         }
 
         $folder = $this->find_folder($object);
 
         if ($type) {
             $object['type'] = $type;
         }
 
-        $status = $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']);
+        $status = $folder->save($object, self::FOLDER_TYPE . '.' . ($object['type'] ?? null), $object['uid'] ?? null);
 
         // on success, update cached tags list
-        if ($status && $object['category'] == 'tag' && is_array($this->tags)) {
+        if ($status && ($object['category'] ?? null) == 'tag' && is_array($this->tags)) {
             $found = false;
             unset($object['_formatobj']); // we don't need it anymore
 
             foreach ($this->tags as $idx => $tag) {
                 if ($tag['uid'] == $object['uid']) {
                     $found = true;
                     $this->tags[$idx] = $object;
                 }
             }
 
             if (!$found) {
                 $this->tags[] = $object;
             }
         }
 
         return !empty($status);
     }
 
     /**
      * Remove configuration object
      *
      * @param string|array $object Object array or its UID
      *
      * @return bool True on success, False on failure
      */
     public function delete($object)
     {
         if (!$this->is_enabled()) {
             return false;
         }
 
         // fetch the object to find folder
         if (!is_array($object)) {
             $object = $this->get_object($object);
         }
 
         if (!$object) {
             return false;
         }
 
         $folder = $this->find_folder($object);
         $status = $folder->delete($object);
 
         // on success, update cached tags list
         if ($status && is_array($this->tags)) {
             foreach ($this->tags as $idx => $tag) {
                 if ($tag['uid'] == $object['uid']) {
                     unset($this->tags[$idx]);
                     break;
                 }
             }
         }
 
         return $status;
     }
 
     /**
      * Find folder
      */
     public function find_folder($object = array())
     {
         if (!$this->is_enabled()) {
             return;
         }
 
         // find folder object
-        if ($object['_mailbox']) {
+        if ($object['_mailbox'] ?? false) {
             foreach ($this->folders as $folder) {
                 if ($folder->name == $object['_mailbox']) {
                     break;
                 }
             }
         }
         else {
             $folder = $this->default;
         }
 
         return $folder;
     }
 
     /**
      * Builds relation member URI
      *
      * @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date)
      *
      * @return string $url Member URI
      */
     public static function build_member_url($params)
     {
         // param is object UUID
         if (is_string($params) && !empty($params)) {
             return 'urn:uuid:' . $params;
         }
 
         if (empty($params) || !strlen($params['folder'])) {
             return null;
         }
 
         $rcube   = rcube::get_instance();
         $storage = $rcube->get_storage();
         list($username, $domain) = explode('@', $rcube->get_user_name());
 
         if (strlen($domain)) {
             $domain = '@' . $domain;
         }
 
         // modify folder spec. according to namespace
         $folder = $params['folder'];
         $ns     = $storage->folder_namespace($folder);
 
         if ($ns == 'shared') {
             // Note: this assumes there's only one shared namespace root
             if ($ns = $storage->get_namespace('shared')) {
                 if ($prefix = $ns[0][0]) {
                     $folder = substr($folder, strlen($prefix));
                 }
             }
         }
         else {
             if ($ns == 'other') {
                 // Note: this assumes there's only one other users namespace root
                 if ($ns = $storage->get_namespace('other')) {
                     if ($prefix = $ns[0][0]) {
                         list($otheruser, $path) = explode('/', substr($folder, strlen($prefix)), 2);
                         $folder = 'user/' . $otheruser . $domain . '/' . $path;
                     }
                 }
             }
             else {
                 $folder = 'user/' . $username . $domain . '/' . $folder;
             }
         }
 
         $folder = implode('/', array_map('rawurlencode', explode('/', $folder)));
 
         // build URI
         $url = 'imap:///' . $folder;
 
         // UID is optional here because sometimes we want
         // to build just a member uri prefix
         if ($params['uid']) {
             $url .= '/' . $params['uid'];
         }
 
         unset($params['folder']);
         unset($params['uid']);
 
         if (!empty($params)) {
             $url .= '?' . http_build_query($params, '', '&');
         }
 
         return $url;
     }
 
     /**
      * Parses relation member string
      *
      * @param string $url Member URI
      *
      * @return array Message folder, UID, Search headers (Message-Id, Date)
      */
     public static function parse_member_url($url)
     {
         // Look for IMAP URI:
         // imap:///(user/username@domain|shared)/<folder>/<UID>?<search_params>
         if (strpos($url, 'imap:///') === 0) {
             $rcube   = rcube::get_instance();
             $storage = $rcube->get_storage();
 
             // parse_url does not work with imap:/// prefix
             $url   = parse_url(substr($url, 8));
             $path  = explode('/', $url['path']);
             parse_str($url['query'], $params);
 
             $uid  = array_pop($path);
             $ns   = array_shift($path);
             $path = array_map('rawurldecode', $path);
 
             // resolve folder name
             if ($ns == 'user') {
                 $username = array_shift($path);
                 $folder   = implode('/', $path);
 
                 if ($username != $rcube->get_user_name()) {
                     list($user, $domain) = explode('@', $username);
 
                     // Note: this assumes there's only one other users namespace root
                     if ($ns = $storage->get_namespace('other')) {
                         if ($prefix = $ns[0][0]) {
                             $folder = $prefix . $user . '/' . $folder;
                         }
                     }
                 }
                 else if (!strlen($folder)) {
                     $folder = 'INBOX';
                 }
             }
             else {
                 $folder = $ns . '/' . implode('/', $path);
                 // Note: this assumes there's only one shared namespace root
                 if ($ns = $storage->get_namespace('shared')) {
                     if ($prefix = $ns[0][0]) {
                         $folder = $prefix . $folder;
                     }
                 }
             }
 
             return array(
                 'folder' => $folder,
                 'uid'    => $uid,
                 'params' => $params,
             );
         }
 
         return false;
     }
 
     /**
      * Build array of member URIs from set of messages
      *
      * @param string $folder   Folder name
      * @param array  $messages Array of rcube_message objects
      *
      * @return array List of members (IMAP URIs)
      */
     public static function build_members($folder, $messages)
     {
         $members = array();
 
         foreach ((array) $messages as $msg) {
             $params = array(
                 'folder' => $folder,
                 'uid'    => $msg->uid,
             );
 
             // add search parameters:
             // we don't want to build "invalid" searches e.g. that
             // will return false positives (more or wrong messages)
             if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) {
                 $params['message-id'] = $messageid;
                 $params['date']       = $date;
 
                 if ($subject = $msg->get('subject', false)) {
                     $params['subject'] = substr($subject, 0, 256);
                 }
             }
 
             $members[] = self::build_member_url($params);
         }
 
         return $members;
     }
 
     /**
      * Resolve/validate/update members (which are IMAP URIs) of relation object.
      *
      * @param array $tag   Tag object
      * @param bool  $force Force members list update
      *
      * @return array Folder/UIDs list
      */
     public static function resolve_members(&$tag, $force = true)
     {
         $result = array();
 
         foreach ((array) $tag['members'] as $member) {
             // IMAP URI members
             if ($url = self::parse_member_url($member)) {
                 $folder = $url['folder'];
 
                 if (!$force) {
                     $result[$folder][] = $url['uid'];
                 }
                 else {
                     $result[$folder]['uid'][]    = $url['uid'];
                     $result[$folder]['params'][] = $url['params'];
                     $result[$folder]['member'][] = $member;
                 }
             }
         }
 
         if (empty($result) || !$force) {
             return $result;
         }
 
         $rcube   = rcube::get_instance();
         $storage = $rcube->get_storage();
         $search  = array();
         $missing = array();
 
         // first we search messages by Folder+UID
         foreach ($result as $folder => $data) {
             // @FIXME: maybe better use index() which is cached?
             // @TODO: consider skip_deleted option
             $index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid']));
             $uids  = $index->get();
 
             // messages that were not found need to be searched by search parameters
             $not_found = array_diff($data['uid'], $uids);
             if (!empty($not_found)) {
                 foreach ($not_found as $uid) {
                     $idx = array_search($uid, $data['uid']);
 
                     if ($p = $data['params'][$idx]) {
                         $search[] = $p;
                     }
 
                     $missing[] = $result[$folder]['member'][$idx];
 
                     unset($result[$folder]['uid'][$idx]);
                     unset($result[$folder]['params'][$idx]);
                     unset($result[$folder]['member'][$idx]);
                 }
             }
 
             $result[$folder] = $uids;
         }
 
         // search in all subscribed mail folders using search parameters
         if (!empty($search)) {
             // remove not found members from the members list
             $tag['members'] = array_diff($tag['members'], $missing);
 
             // get subscribed folders
             $folders = $storage->list_folders_subscribed('', '*', 'mail', null, true);
 
             // @TODO: do this search in chunks (for e.g. 10 messages)?
             $search_str = '';
 
             foreach ($search as $p) {
                 $search_params = array();
                 foreach ($p as $key => $val) {
                     $key = strtoupper($key);
                     // don't search by subject, we don't want false-positives
                     if ($key != 'SUBJECT') {
                         $search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
                     }
                 }
 
                 $search_str .= ' (' . implode(' ', $search_params) . ')';
             }
 
             $search_str = trim(str_repeat(' OR', count($search)-1) . $search_str);
 
             // search
             $search = $storage->search_once($folders, $search_str);
 
             // handle search result
             $folders = (array) $search->get_parameters('MAILBOX');
 
             foreach ($folders as $folder) {
                 $set  = $search->get_set($folder);
                 $uids = $set->get();
 
                 if (!empty($uids)) {
                     $msgs    = $storage->fetch_headers($folder, $uids, false);
                     $members = self::build_members($folder, $msgs);
 
                     // merge new members into the tag members list
                     $tag['members'] = array_merge($tag['members'], $members);
 
                     // add UIDs into the result
                     $result[$folder] = array_unique(array_merge((array)$result[$folder], $uids));
                 }
             }
 
             // update tag object with new members list
             $tag['members'] = array_unique($tag['members']);
             kolab_storage_config::get_instance()->save($tag, 'relation', false);
         }
 
         return $result;
     }
 
     /**
      * Assign tags to kolab objects
      *
      * @param array $records   List of kolab objects
      * @param bool  $no_return Don't return anything
      *
      * @return array List of tags
      */
     public function apply_tags(&$records, $no_return = false)
     {
         if (empty($records) && $no_return) {
             return;
         }
 
         // first convert categories into tags
         foreach ($records as $i => $rec) {
             if (!empty($rec['categories'])) {
                 $folder = new kolab_storage_folder($rec['_mailbox']);
                 if ($object = $folder->get_object($rec['uid'])) {
                     $tags = $rec['categories'];
 
                     unset($object['categories']);
                     unset($records[$i]['categories']);
 
                     $this->save_tags($rec['uid'], $tags);
                     $folder->save($object, $rec['_type'], $rec['uid']);
                 }
             }
         }
 
         $tags = array();
 
         // assign tags to objects
         foreach ($this->get_tags() as $tag) {
             foreach ($records as $idx => $rec) {
                 $uid = self::build_member_url($rec['uid']);
                 if (in_array($uid, (array) $tag['members'])) {
                     $records[$idx]['tags'][] = $tag['name'];
                 }
             }
 
             $tags[] = $tag['name'];
         }
 
         $tags = $no_return ? null : array_unique($tags);
 
         return $tags;
     }
 
     /**
      * Assign links (relations) to kolab objects
      *
      * @param array $records List of kolab objects
      */
     public function apply_links(&$records)
     {
         $links = array();
         $uids  = array();
         $ids   = array();
         $limit = 25;
 
         // get list of object UIDs and UIRs map
         foreach ($records as $i => $rec) {
             $uids[] = $rec['uid'];
             // there can be many objects with the same uid (recurring events)
             $ids[self::build_member_url($rec['uid'])][] = $i;
             $records[$i]['links'] = array();
         }
 
         if (!empty($uids)) {
             $uids = array_unique($uids);
         }
 
         // The whole story here is to not do SELECT for every object.
         // We'll build one SELECT for many (limit above) objects at once
 
         while (!empty($uids)) {
             $chunk = array_splice($uids, 0, $limit);
             $chunk = array_map(function($v) { return array('member', '=', $v); }, $chunk);
 
             $filter = array(
                 array('type', '=', 'relation'),
                 array('category', '=', 'generic'),
                 array($chunk, 'OR'),
             );
 
             $relations = $this->get_objects($filter, true, self::MAX_RELATIONS);
 
             foreach ($relations as $relation) {
                 $links[$relation['uid']] = $relation;
             }
         }
 
         if (empty($links)) {
             return;
         }
 
         // assign links of related messages
         foreach ($links as $relation) {
             // make relation members up-to-date
             kolab_storage_config::resolve_members($relation);
 
             $members = array();
             foreach ((array) $relation['members'] as $member) {
                 if (strpos($member, 'imap://') === 0) {
                     $members[$member] = $member;
                 }
             }
             $members = array_values($members);
 
             // assign links to objects
             foreach ((array) $relation['members'] as $member) {
-                if (($id = $ids[$member]) !== null) {
+                if (($id = ($ids[$member] ?? null)) !== null) {
                     foreach ($id as $i) {
                         $records[$i]['links'] = array_unique(array_merge($records[$i]['links'], $members));
                     }
                 }
             }
         }
     }
 
     /**
      * Update object tags
      *
      * @param string $uid  Kolab object UID
      * @param array  $tags List of tag names
      */
     public function save_tags($uid, $tags)
     {
         $url       = self::build_member_url($uid);
         $relations = $this->get_tags();
 
         foreach ($relations as $idx => $relation) {
             $selected = !empty($tags) && in_array($relation['name'], $tags);
             $found    = !empty($relation['members']) && in_array($url, $relation['members']);
             $update   = false;
 
             // remove member from the relation
             if ($found && !$selected) {
                 $relation['members'] = array_diff($relation['members'], (array) $url);
                 $update = true;
             }
             // add member to the relation
             else if (!$found && $selected) {
                 $relation['members'][] = $url;
                 $update = true;
             }
 
             if ($update) {
                 $this->save($relation, 'relation');
             }
 
             if ($selected) {
                 $tags = array_diff($tags, array($relation['name']));
             }
         }
 
         // create new relations
         if (!empty($tags)) {
             foreach ($tags as $tag) {
                 $relation = array(
                     'name'     => $tag,
                     'members'  => (array) $url,
                     'category' => 'tag',
                 );
 
                 $this->save($relation, 'relation');
             }
         }
     }
 
     /**
      * Get tags (all or referring to specified object)
      *
      * @param string $member Optional object UID or mail message-id
      *
      * @return array List of Relation objects
      */
     public function get_tags($member = '*')
     {
         if (!isset($this->tags)) {
             $default = true;
             $filter  = array(
                 array('type', '=', 'relation'),
                 array('category', '=', 'tag')
             );
 
             // use faster method
             if ($member && $member != '*') {
                 $filter[] = array('member', '=', $member);
                 $tags = $this->get_objects($filter, $default, self::MAX_RELATIONS);
             }
             else {
                 $this->tags = $tags = $this->get_objects($filter, $default, self::MAX_RELATIONS);
             }
         }
         else {
             $tags = $this->tags;
         }
 
         if ($member === '*') {
             return $tags;
         }
 
         $result = array();
 
         if ($member[0] == '<') {
             $search_msg = urlencode($member);
         }
         else {
             $search_uid = self::build_member_url($member);
         }
 
         foreach ($tags as $tag) {
             if ($search_uid && in_array($search_uid, (array) $tag['members'])) {
                 $result[] = $tag;
             }
             else if ($search_msg) {
                 foreach ($tag['members'] as $m) {
                     if (strpos($m, $search_msg) !== false) {
                         $result[] = $tag;
                         break;
                     }
                 }
             }
         }
 
         return $result;
     }
 
     /**
      * Find objects linked with the given groupware object through a relation
      *
      * @param string Object UUID
      *
      * @return array List of related URIs
      */
     public function get_object_links($uid)
     {
         $links = array();
         $object_uri = self::build_member_url($uid);
 
         foreach ($this->get_relations_for_member($uid) as $relation) {
             if (in_array($object_uri, (array) $relation['members'])) {
                 // make relation members up-to-date
                 kolab_storage_config::resolve_members($relation);
 
                 foreach ($relation['members'] as $member) {
                     if ($member != $object_uri) {
                         $links[] = $member;
                     }
                 }
             }
         }
 
         return array_unique($links);
     }
 
     /**
      * Save relations of an object.
      * Note, that we already support only one-to-one relations.
      * So, all relations to the object that are not provided in $links
      * argument will be removed.
      *
      * @param string $uid   Object UUID
      * @param array  $links List of related-object URIs
      *
      * @return bool True on success, False on failure
      */
     public function save_object_links($uid, $links)
     {
         $object_uri = self::build_member_url($uid);
         $relations  = $this->get_relations_for_member($uid);
         $done       = false;
 
         foreach ($relations as $relation) {
             // make relation members up-to-date
             kolab_storage_config::resolve_members($relation);
 
             // remove and add links
             $members = array($object_uri);
             $members = array_unique(array_merge($members, $links));
 
             // remove relation if no other members remain
             if (count($members) <= 1) {
                 $done = $this->delete($relation);
             }
             // update relation object if members changed
             else if (count(array_diff($members, $relation['members'])) || count(array_diff($relation['members'], $members))) {
                 $relation['members'] = $members;
                 $done = $this->save($relation, 'relation');
                 $links = array();
             }
             // no changes, we're happy
             else {
                 $done  = true;
                 $links = array();
             }
         }
 
         // create a new relation
         if (!$done && !empty($links)) {
             $relation = array(
                 'members'  => array_merge($links, array($object_uri)),
                 'category' => 'generic',
             );
 
             $done = $this->save($relation, 'relation');
         }
 
         return $done;
     }
 
     /**
      * Find relation objects referring to specified note
      */
     public function get_relations_for_member($uid, $reltype = 'generic')
     {
         $default = true;
         $filter  = array(
             array('type', '=', 'relation'),
             array('category', '=', $reltype),
             array('member', '=', $uid),
         );
 
         return $this->get_objects($filter, $default, self::MAX_RELATIONS);
     }
 
     /**
      * Find kolab objects assigned to specified e-mail message
      *
      * @param rcube_message $message E-mail message
      * @param string        $folder  Folder name
      * @param string        $type    Result objects type
      *
      * @return array List of kolab objects
      */
     public function get_message_relations($message, $folder, $type)
     {
         static $_cache = array();
 
         $result  = array();
         $uids    = array();
         $default = true;
         $uri     = self::get_message_uri($message, $folder);
         $filter  = array(
             array('type', '=', 'relation'),
             array('category', '=', 'generic'),
         );
 
         // query by message-id
         $member_id = $message->get('message-id', false);
         if (empty($member_id)) {
             // derive message identifier from URI
             $member_id = md5($uri);
         }
         $filter[] = array('member', '=', $member_id);
 
         if (!isset($_cache[$uri])) {
             // get UIDs of related groupware objects
             foreach ($this->get_objects($filter, $default) as $relation) {
                 // we don't need to update members if the URI is found
                 if (!in_array($uri, $relation['members'])) {
                     // update members...
                     $messages = kolab_storage_config::resolve_members($relation);
                     // ...and check again
                     if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) {
                         continue;
                     }
                 }
 
                 // find groupware object UID(s)
                 foreach ($relation['members'] as $member) {
                     if (strpos($member, 'urn:uuid:') === 0) {
                         $uids[] = substr($member, 9);
                     }
                 }
             }
 
             // remember this lookup
             $_cache[$uri] = $uids;
         }
         else {
             $uids = $_cache[$uri];
         }
 
         // get kolab objects of specified type
         if (!empty($uids)) {
             $query  = array(array('uid', '=', array_unique($uids)));
             $result = kolab_storage::select($query, $type, count($uids));
         }
 
         return $result;
     }
 
     /**
      * Build a URI representing the given message reference
      */
     public static function get_message_uri($headers, $folder)
     {
         $params = array(
             'folder' => $headers->folder ?: $folder,
             'uid'    => $headers->uid,
         );
 
         if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) {
             $params['message-id'] = $messageid;
             $params['date']       = $date;
 
             if ($subject = $headers->get('subject')) {
                 $params['subject'] = $subject;
             }
         }
 
         return self::build_member_url($params);
     }
 
     /**
      * Resolve the email message reference from the given URI
      */
     public static function get_message_reference($uri, $rel = null)
     {
         if ($linkref = self::parse_member_url($uri)) {
             $linkref['subject'] = $linkref['params']['subject'];
             $linkref['uri']     = $uri;
 
             $rcmail = rcube::get_instance();
             if (method_exists($rcmail, 'url')) {
                 $linkref['mailurl'] = $rcmail->url(array(
                     'task'   => 'mail',
                     'action' => 'show',
                     'mbox'   => $linkref['folder'],
                     'uid'    => $linkref['uid'],
                     'rel'    => $rel,
                 ));
             }
 
             unset($linkref['params']);
         }
 
         return $linkref;
     }
 }
diff --git a/plugins/libkolab/lib/kolab_storage_dataset.php b/plugins/libkolab/lib/kolab_storage_dataset.php
index fb81eab1..3cab1092 100644
--- a/plugins/libkolab/lib/kolab_storage_dataset.php
+++ b/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -1,193 +1,193 @@
 <?php
 
 /**
  * Dataset class providing the results of a select operation on a kolab_storage_folder.
  *
  * Can be used as a normal array as well as an iterator in foreach() loops.
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2014, Kolab Systems AG <contact@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_dataset implements Iterator, ArrayAccess, Countable
 {
     const CHUNK_SIZE = 25;
 
     private $cache;  // kolab_storage_cache instance to use for fetching data
     private $memlimit = 0;
     private $buffer = false;
     private $index = [];
     private $data = [];
     private $iteratorkey = 0;
     private $error = null;
     private $chunk = [];
 
     /**
      * Default constructor
      *
      * @param object kolab_storage_cache instance to be used for fetching objects upon access
      */
     public function __construct($cache)
     {
         $this->cache = $cache;
 
         // enable in-memory buffering up until 1/5 of the available memory
         if (function_exists('memory_get_usage')) {
             $this->memlimit = parse_bytes(ini_get('memory_limit')) / 5;
             $this->buffer = true;
         }
     }
 
     /**
      * Return error state
      */
     public function is_error()
     {
         return !empty($this->error);
     }
 
     /**
      * Set error state
      */
     public function set_error($err)
     {
         $this->error = $err;
     }
 
 
     /*** Implement PHP Countable interface ***/
 
     public function count(): int
     {
         return count($this->index);
     }
 
 
     /*** Implement PHP ArrayAccess interface ***/
 
     public function offsetSet($offset, $value): void
     {
         if (is_string($value)) {
             $uid = $value;
         }
         else {
             $uid = !empty($value['_msguid']) ? $value['_msguid'] : $value['uid'];
         }
 
         if (is_null($offset)) {
             $offset = count($this->index);
         }
 
         $this->index[$offset] = $uid;
 
         // keep full payload data in memory if possible
         if ($this->memlimit && $this->buffer) {
             $this->data[$offset] = $value;
 
             // check memory usage and stop buffering
             if ($offset % 10 == 0) {
                 $this->buffer = memory_get_usage() < $this->memlimit;
             }
         }
     }
 
     public function offsetExists($offset): bool
     {
         return isset($this->index[$offset]);
     }
 
     public function offsetUnset($offset): void
     {
         unset($this->index[$offset]);
     }
 
     #[ReturnTypeWillChange]
     public function offsetGet($offset)
     {
         if (isset($this->chunk[$offset])) {
             return $this->chunk[$offset] ?: null;
         }
 
         // The item is a string (object's UID), use multiget method to pre-fetch
         // multiple objects from the server in one request
         if (isset($this->data[$offset]) && is_string($this->data[$offset]) && method_exists($this->cache, 'multiget')) {
             $idx  = $offset;
             $uids = [];
 
             while (isset($this->index[$idx]) && count($uids) < self::CHUNK_SIZE) {
                 if (isset($this->data[$idx]) && !is_string($this->data[$idx])) {
                     // skip objects that had the raw content in the cache (are not empty)
                 }
                 else {
                     $uids[$idx] = $this->index[$idx];
                 }
 
                 $idx++;
             }
 
             if (!empty($uids)) {
                 $this->chunk = $this->cache->multiget($uids);
             }
 
             if (isset($this->chunk[$offset])) {
                 return $this->chunk[$offset] ?: null;
             }
 
             return null;
         }
 
         if (isset($this->data[$offset])) {
             return $this->data[$offset];
         }
 
-        if ($uid = $this->index[$offset]) {
+        if ($uid = ($this->index[$offset] ?? null)) {
             return $this->cache->get($uid);
         }
 
         return null;
     }
 
 
     /*** Implement PHP Iterator interface ***/
 
     #[ReturnTypeWillChange]
     public function current()
     {
         return $this->offsetGet($this->iteratorkey);
     }
 
     public function key(): int
     {
         return $this->iteratorkey;
     }
 
     public function next(): void
     {
         $this->iteratorkey++;
     }
 
     public function rewind(): void
     {
         $this->iteratorkey = 0;
     }
 
     public function valid(): bool
     {
         return !empty($this->index[$this->iteratorkey]);
     }
 }
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 4ef538b8..6cf7d1b8 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -1,1171 +1,1175 @@
 <?php
 
 /**
  * The kolab_storage_folder class represents an IMAP folder on the Kolab server.
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  * @author Aleksander Machniak <machniak@kolabsys.com>
  *
  * Copyright (C) 2012-2013, Kolab Systems AG <contact@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 extends kolab_storage_folder_api
 {
     /**
      * The kolab_storage_cache instance for caching operations
      * @var object
      */
     public $cache;
 
     /**
      * Indicate validity status
      * @var boolean
      */
     public $valid = false;
 
     protected $error = 0;
     protected $resource_uri;
 
 
     /**
      * Default constructor
      *
      * @param string The folder name/path
      * @param string Expected folder type
      * @param string Optional folder type if known
      */
     function __construct($name, $type = null, $type_annotation = null)
     {
         parent::__construct($name);
         $this->set_folder($name, $type, $type_annotation);
     }
 
     /**
      * Set the IMAP folder this instance connects to
      *
      * @param string The folder name/path
      * @param string Expected folder type
      * @param string Optional folder type if known
      */
     public function set_folder($name, $type = null, $type_annotation = null)
     {
         $this->name = $name;
 
         if (empty($type_annotation)) {
             $type_annotation = $this->get_type();
         }
 
         $oldtype = $this->type;
         list($this->type, $suffix) = explode('.', $type_annotation);
         $this->default      = $suffix == 'default';
         $this->subtype      = $this->default ? '' : $suffix;
         $this->id           = kolab_storage::folder_id($name);
         $this->valid        = !empty($this->type) && $this->type != 'mail' && (!$type || $this->type == $type);
 
         if (!$this->valid) {
             $this->error = $this->imap->get_error_code() < 0 ? kolab_storage::ERROR_IMAP_CONN : kolab_storage::ERROR_INVALID_FOLDER;
         }
 
         // reset cached object properties
         $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;
 
         // get a new cache instance if folder type changed
         if (!$this->cache || $this->type != $oldtype)
             $this->cache = kolab_storage_cache::factory($this);
         else
             $this->cache->set_folder($this);
 
         $this->imap->set_folder($this->name);
     }
 
     /**
      * Returns code of last error
      *
      * @return int Error code
      */
     public function get_error()
     {
         return $this->error ?: $this->cache->get_error();
     }
 
     /**
      * Check IMAP connection error state
      */
     public function check_error()
     {
         if (($err_code = $this->imap->get_error_code()) < 0) {
             $this->error = kolab_storage::ERROR_IMAP_CONN;
             if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
                 $this->error = kolab_storage::ERROR_NO_PERMISSION;
             }
         }
 
         return $this->error;
     }
 
     /**
      * Compose a unique resource URI for this IMAP folder
      */
     public function get_resource_uri()
     {
         if (!empty($this->resource_uri)) {
             return $this->resource_uri;
         }
 
         // strip namespace prefix from folder name
         $ns     = $this->get_namespace();
         $nsdata = $this->imap->get_namespace($ns);
 
         if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
             $subpath = substr($this->name, strlen($nsdata[0][0]));
             if ($ns == 'other') {
                 list($user, $suffix) = explode($nsdata[0][1], $subpath, 2);
                 $subpath = $suffix;
             }
         }
         else {
             $subpath = $this->name;
         }
 
         // compose fully qualified ressource uri for this instance
         $this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath;
         return $this->resource_uri;
     }
 
     /**
      * Helper method to extract folder UID metadata
      *
      * @return string Folder's UID
      */
     public function get_uid()
     {
         // UID is defined in folder METADATA
         $metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_CYRUS);
         $metadata = $this->get_metadata();
 
         if ($metadata !== null) {
             foreach ($metakeys as $key) {
-                if ($uid = $metadata[$key]) {
+                if ($uid = ($metadata[$key] ?? null)) {
                     return $uid;
                 }
             }
 
             // generate a folder UID and set it to IMAP
             $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-');
             if ($this->set_uid($uid)) {
                 return $uid;
             }
         }
 
         $this->check_error();
 
         // create hash from folder name if we can't write the UID metadata
         return md5($this->name . $this->get_owner());
     }
 
     /**
      * Helper method to set an UID value to the given IMAP folder instance
      *
      * @param string Folder's UID
      * @return boolean True on succes, False on failure
      */
     public function set_uid($uid)
     {
         $success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid));
 
         $this->check_error();
         return $success;
     }
 
     /**
      * Compose a folder Etag identifier
      */
     public function get_ctag()
     {
         $fdata = $this->get_imap_data();
         $this->check_error();
-        return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']);
+        return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'] ?? null, $fdata['HIGHESTMODSEQ'] ?? null, $fdata['UIDNEXT'] ?? null);
     }
 
     /**
      * 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);
     }
 
     /**
      * Get number of objects stored in this folder
      *
      * @param mixed Pseudo-SQL query as list of filter parameter triplets
      *    or string with object type (e.g. contact, event, todo, journal, note, configuration)
      *
      * @return integer The number of objects of the given type
      * @see self::select()
      */
     public function count($query = null)
     {
         if (!$this->valid) {
             return 0;
         }
 
         // synchronize cache first
         $this->cache->synchronize();
 
         return $this->cache->count($this->_prepare_query($query));
     }
 
     /**
      * List Kolab objects matching the given query
      *
      * @param mixed Pseudo-SQL query as list of filter parameter triplets
      *    or string with object type (e.g. contact, event, todo, journal, note, configuration)
      *
      * @return array List of Kolab data objects (each represented as hash array)
      * @deprecated Use select()
      */
     public function get_objects($query = array())
     {
         return $this->select($query);
     }
 
     /**
      * Select Kolab objects matching the given query
      *
      * @param mixed   Pseudo-SQL query as list of filter parameter triplets
      *                or string with object type (e.g. contact, event, todo, journal, note, configuration)
      * @param boolean Use fast mode to fetch only minimal set of information
      *                (no xml fetching and parsing, etc.)
      *
      * @return array List of Kolab data objects (each represented as hash array)
      */
     public function select($query = array(), $fast = false)
     {
         if (!$this->valid) {
             return array();
         }
 
         // synchronize caches
         $this->cache->synchronize();
 
         // fetch objects from cache
         return $this->cache->select($this->_prepare_query($query), false, $fast);
     }
 
     /**
      * Getter for object UIDs only
      *
      * @param array Pseudo-SQL query as list of filter parameter triplets
      * @return array List of Kolab object UIDs
      */
     public function get_uids($query = array())
     {
         if (!$this->valid) {
             return array();
         }
 
         // synchronize caches
         $this->cache->synchronize();
 
         // fetch UIDs from cache
         return $this->cache->select($this->_prepare_query($query), true);
     }
 
     /**
      * Setter for ORDER BY and LIMIT parameters for cache queries
      *
      * @param array   List of columns to order by
      * @param integer Limit result set to this length
      * @param integer Offset row
      */
     public function set_order_and_limit($sortcols, $length = null, $offset = 0)
     {
         $this->cache->set_order_by($sortcols);
 
         if ($length !== null) {
             $this->cache->set_limit($length, $offset);
         }
     }
 
     /**
      * Helper method to sanitize query arguments
      */
     private function _prepare_query($query)
     {
         // string equals type query
         // FIXME: should not be called this way!
         if (is_string($query)) {
             return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array();
         }
 
         foreach ((array)$query as $i => $param) {
             if ($param[0] == 'type' && !$this->cache->has_type_col()) {
                 unset($query[$i]);
             }
             else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) {
                 if (is_object($param[2]) && $param[2] instanceof DateTimeInterface) {
                     $param[2] = $param[2]->format('U');
                 }
                 if (is_numeric($param[2])) {
                     $query[$i][2] = date('Y-m-d H:i:s', $param[2]);
                 }
             }
         }
 
         return $query;
     }
 
     /**
      * Getter for a single Kolab object identified by its UID
      *
      * @param string $uid Object UID
      *
      * @return array The Kolab object represented as hash array
      */
     public function get_object($uid)
     {
         if (!$this->valid || !$uid) {
             return false;
         }
 
         // synchronize caches
         $this->cache->synchronize();
 
         return $this->cache->get_by_uid($uid);
     }
 
     /**
      * Fetch a Kolab object attachment which is stored in a separate part
      * of the mail MIME message that represents the Kolab record.
      *
      * @param string   Object's UID
      * @param string   The attachment's mime number
      * @param string   IMAP folder where message is stored;
      *                 If set, that also implies that the given UID is an IMAP UID
      * @param bool     True to print the part content
      * @param resource File pointer to save the message part
      * @param boolean  Disables charset conversion
      *
      * @return mixed  The attachment content as binary string
      */
     public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
     {
         if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) {
             $this->imap->set_folder($mailbox ? $mailbox : $this->name);
 
             if (substr($part, 0, 2) == 'i:') {
                 // attachment data is stored in XML
                 if ($object = $this->cache->get($msguid)) {
                     // load data from XML (attachment content is not stored in cache)
                     if ($object['_formatobj'] && isset($object['_size'])) {
                         $object['_attachments'] = array();
                         $object['_formatobj']->get_attachments($object);
                     }
 
                     foreach ($object['_attachments'] as $attach) {
                         if ($attach['id'] == $part) {
                             if ($print)   echo $attach['content'];
                             else if ($fp) fwrite($fp, $attach['content']);
                             else          return $attach['content'];
                             return true;
                         }
                     }
                 }
             }
             else {
                 // return message part from IMAP directly
                 // TODO: We could improve performance if we cache part's encoding
                 //       without 3rd argument get_message_part() will request BODYSTRUCTURE from IMAP
                 return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
             }
         }
 
         return null;
     }
 
     /**
      * Fetch the mime message from the storage server and extract
      * the Kolab groupware object from it
      *
      * @param string The IMAP message UID to fetch
      * @param string The object type expected (use wildcard '*' to accept all types)
      * @param string The folder name where the message is stored
      *
      * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
      */
     public function read_object($msguid, $type = null, $folder = null)
     {
         if (!$this->valid) {
             return false;
         }
 
         if (!$type) $type = $this->type;
         if (!$folder) $folder = $this->name;
 
         $this->imap->set_folder($folder);
 
         $this->cache->imap_mode(true);
         $message = new rcube_message($msguid);
         $this->cache->imap_mode(false);
 
         // Message doesn't exist?
         if (empty($message->headers)) {
             return false;
         }
 
         // extract the X-Kolab-Type header from the XML attachment part if missing
         if (empty($message->headers->others['x-kolab-type'])) {
             foreach ((array)$message->attachments as $part) {
                 if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
                     $message->headers->others['x-kolab-type'] = $part->mimetype;
                     break;
                 }
             }
         }
         // fix buggy messages stating the X-Kolab-Type header twice
         else if (is_array($message->headers->others['x-kolab-type'])) {
             $message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']);
         }
 
         // no object type header found: abort
         if (empty($message->headers->others['x-kolab-type'])) {
             rcube::raise_error(array(
                 'code' => 600,
                 'type' => 'php',
                 'file' => __FILE__,
                 'line' => __LINE__,
                 'message' => "No X-Kolab-Type information found in message $msguid ($this->name).",
             ), true);
             return false;
         }
 
         $object_type  = kolab_format::mime2object_type($message->headers->others['x-kolab-type']);
         $content_type = kolab_format::KTYPE_PREFIX . $object_type;
 
         // check object type header and abort on mismatch
         if ($type != '*' && strpos($object_type, $type) !== 0 && !($object_type == 'distribution-list' && $type == 'contact')) {
             return false;
         }
 
         $attachments = array();
 
         // get XML part
+        $xml = null;
         foreach ((array)$message->attachments as $part) {
             if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!i', $part->mimetype))) {
                 $xml = $message->get_part_body($part->mime_id, true);
             }
             else if ($part->filename || $part->content_id) {
                 $key  = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
                 $size = null;
 
                 // Use Content-Disposition 'size' as for the Kolab Format spec.
                 if (isset($part->d_parameters['size'])) {
                     $size = $part->d_parameters['size'];
                 }
                 // we can trust part size only if it's not encoded
                 else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') {
                     $size = $part->size;
                 }
 
                 $attachments[$key] = array(
                     'id'       => $part->mime_id,
                     'name'     => $part->filename,
                     'mimetype' => $part->mimetype,
                     'size'     => $size,
                 );
             }
         }
 
         if (!$xml) {
             rcube::raise_error(array(
                 'code' => 600,
                 'type' => 'php',
                 'file' => __FILE__,
                 'line' => __LINE__,
                 'message' => "Could not find Kolab data part in message $msguid ($this->name).",
             ), true);
             return false;
         }
 
         // check kolab format version
         $format_version = $message->headers->others['x-kolab-mime-version'];
         if (empty($format_version)) {
             list($xmltype, $subtype) = explode('.', $object_type);
             $xmlhead = substr($xml, 0, 512);
 
             // detect old Kolab 2.0 format
             if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false)
                 $format_version = '2.0';
             else
                 $format_version = '3.0'; // assume 3.0
         }
 
         // get Kolab format handler for the given type
         $format = kolab_format::factory($object_type, $format_version);
 
         if (is_a($format, 'PEAR_Error'))
             return false;
 
         // load Kolab object from XML part
         $format->load($xml);
 
         if ($format->is_valid()) {
             $object = $format->to_array(array('_attachments' => $attachments));
             $object['_type']      = $object_type;
             $object['_msguid']    = $msguid;
             $object['_mailbox']   = $this->name;
             $object['_formatobj'] = $format;
             $object['_size']      = strlen($xml);
 
             return $object;
         }
         else {
             // try to extract object UID from XML block
             if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
                 $msgadd = " UID = " . trim(strip_tags($m[1]));
 
             rcube::raise_error(array(
                 'code' => 600,
                 'type' => 'php',
                 'file' => __FILE__,
                 'line' => __LINE__,
                 'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd,
             ), true);
 
             self::save_user_xml("$msguid.xml", $xml);
         }
 
         return false;
     }
 
     /**
      * Save an object in this folder.
      *
      * @param array  $object The array that holds the data of the object.
      * @param string $type   The type of the kolab object.
      * @param string $uid    The UID of the old object if it existed before
      *
      * @return mixed False on error or IMAP message UID on success
      */
     public function save(&$object, $type = null, $uid = null)
     {
         if (!$this->valid || empty($object)) {
             return false;
         }
 
         if (!$type)
             $type = $this->type;
 
         // copy attachments from old message
-        $copyfrom = $object['_copyfrom'] ?: $object['_msguid'];
+        $copyfrom = $object['_copyfrom'] ?? ($object['_msguid'] ?? null);
         if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) {
             foreach ((array)$old['_attachments'] as $key => $att) {
                 if (!isset($object['_attachments'][$key])) {
                     $object['_attachments'][$key] = $old['_attachments'][$key];
                 }
                 // unset deleted attachment entries
                 if ($object['_attachments'][$key] == false) {
                     unset($object['_attachments'][$key]);
                 }
                 // load photo.attachment from old Kolab2 format to be directly embedded in xcard block
                 else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
                     if (!isset($object['photo']))
                         $object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']);
                     unset($object['_attachments'][$key]);
                 }
             }
         }
 
         // save contact photo to attachment for Kolab2 format
         if (kolab_storage::$version == '2.0' && $object['photo']) {
             $attkey = 'kolab-picture.png';  // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp
             $object['_attachments'][$attkey] = array(
                 'mimetype'=> rcube_mime::image_content_type($object['photo']),
                 'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']),
             );
         }
 
         // process attachments
-        if (is_array($object['_attachments'])) {
+        if (is_array($object['_attachments'] ?? null)) {
             $numatt = count($object['_attachments']);
             foreach ($object['_attachments'] as $key => $attachment) {
                 // FIXME: kolab_storage and Roundcube attachment hooks use different fields!
                 if (empty($attachment['content']) && !empty($attachment['data'])) {
                     $attachment['content'] = $attachment['data'];
                     unset($attachment['data'], $object['_attachments'][$key]['data']);
                 }
 
                 // make sure size is set, so object saved in cache contains this info
                 if (!isset($attachment['size'])) {
                     if (!empty($attachment['content'])) {
                         if (is_resource($attachment['content'])) {
                             // this need to be a seekable resource, otherwise
                             // fstat() failes and we're unable to determine size
                             // here nor in rcube_imap_generic before IMAP APPEND
                             $stat = fstat($attachment['content']);
                             $attachment['size'] = $stat ? $stat['size'] : 0;
                         }
                         else {
                             $attachment['size'] = strlen($attachment['content']);
                         }
                     }
                     else if (!empty($attachment['path'])) {
                         $attachment['size'] = filesize($attachment['path']);
                     }
                     $object['_attachments'][$key] = $attachment;
                 }
 
                 // generate unique keys (used as content-id) for attachments
                 if (is_numeric($key) && $key < $numatt) {
                     // derrive content-id from attachment file name
                     $ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null;
                     $basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext));  // to 7bit ascii
                     if (!$basename) $basename = 'noname';
                     $cid = $basename . '.' . microtime(true) . $key . $ext;
 
                     $object['_attachments'][$cid] = $attachment;
                     unset($object['_attachments'][$key]);
                 }
             }
         }
 
         // save recurrence exceptions as individual objects due to lack of support in Kolab v2 format
         if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) {
             $this->save_recurrence_exceptions($object, $type);
         }
 
         // check IMAP BINARY extension support for 'file' objects
         // allow configuration to workaround bug in Cyrus < 2.4.17
         $rcmail = rcube::get_instance();
         $binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY');
 
         // generate and save object message
         if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) {
             // resolve old msguid before saving
             if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) {
                 $object['_msguid'] = $msguid;
                 $object['_mailbox'] = $this->name;
             }
 
             $result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
 
             // update cache with new UID
             if ($result) {
-                $old_uid = $object['_msguid'];
+                $old_uid = $object['_msguid'] ?? null;
 
                 $object['_msguid'] = $result;
                 $object['_mailbox'] = $this->name;
 
                 if ($old_uid) {
                     // delete old message
                     $this->cache->imap_mode(true);
                     $this->imap->delete_message($old_uid, $object['_mailbox']);
                     $this->cache->imap_mode(false);
                 }
 
                 // insert/update message in cache
                 $this->cache->save($result, $object, $old_uid);
             }
 
             // remove temp file
             if ($body_file) {
                 @unlink($body_file);
             }
         }
 
         return $result;
     }
 
     /**
      * Save recurrence exceptions as individual objects.
      * The Kolab v2 format doesn't allow us to save fully embedded exception objects.
      *
      * @param array Hash array with event properties
      * @param string Object type
      */
     private function save_recurrence_exceptions(&$object, $type = null)
     {
         if ($object['recurrence']['EXCEPTIONS']) {
             $exdates = [];
             foreach ((array) $object['recurrence']['EXDATE'] as $exdate) {
                 $key = $exdate instanceof DateTimeInterface ? $exdate->format('Y-m-d') : strval($exdate);
                 $exdates[$key] = 1;
             }
 
             // save every exception as individual object
             foreach ((array) $object['recurrence']['EXCEPTIONS'] as $exception) {
                 $exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd'));
                 $exception['sequence'] = $object['sequence'] + 1;
 
                 if ($exception['thisandfuture']) {
                     $exception['recurrence'] = $object['recurrence'];
 
                     // adjust the recurrence duration of the exception
                     if ($object['recurrence']['COUNT']) {
                         $recurrence = new kolab_date_recurrence($object['_formatobj']);
                         if ($end = $recurrence->end()) {
                             unset($exception['recurrence']['COUNT']);
                             $exception['recurrence']['UNTIL'] = $end;
                         }
                     }
 
                     // set UNTIL date if we have a thisandfuture exception
                     $untildate = clone $exception['start'];
                     $untildate->sub(new DateInterval('P1D'));
                     $object['recurrence']['UNTIL'] = $untildate;
                     unset($object['recurrence']['COUNT']);
                 }
                 else {
                     if (!$exdates[$exception['start']->format('Y-m-d')])
                         $object['recurrence']['EXDATE'][] = clone $exception['start'];
                     unset($exception['recurrence']);
                 }
 
                 unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']);
                 $this->save($exception, $type, $exception['uid']);
             }
 
             unset($object['recurrence']['EXCEPTIONS']);
         }
     }
 
     /**
      * Generate an object UID with the given recurrence-ID in a way that it is
      * unique (the original UID is not a substring) but still recoverable.
      */
     private static function recurrence_exception_uid($uid, $recurrence_id)
     {
         $offset = -2;
         return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset);
     }
 
     /**
      * Delete the specified object from this folder.
      *
      * @param  mixed   $object  The Kolab object to delete or object UID
      * @param  boolean $expunge Should the folder be expunged?
      *
      * @return boolean True if successful, false on error
      */
     public function delete($object, $expunge = true)
     {
         if (!$this->valid) {
             return false;
         }
 
         $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
         $success = false;
 
         $this->cache->imap_mode(true);
 
         if ($msguid && $expunge) {
             $success = $this->imap->delete_message($msguid, $this->name);
         }
         else if ($msguid) {
             $success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
         }
 
         $this->cache->imap_mode(false);
 
         if ($success) {
             $this->cache->set($msguid, false);
         }
 
         return $success;
     }
 
     /**
      *
      */
     public function delete_all()
     {
         if (!$this->valid) {
             return false;
         }
 
         $this->cache->purge();
         $this->cache->imap_mode(true);
         $result = $this->imap->clear_folder($this->name);
         $this->cache->imap_mode(false);
 
         return $result;
     }
 
     /**
      * Restore a previously deleted object
      *
      * @param string Object UID
      * @return mixed Message UID on success, false on error
      */
     public function undelete($uid)
     {
         if (!$this->valid) {
             return false;
         }
 
         if ($msguid = $this->cache->uid2msguid($uid, true)) {
             $this->cache->imap_mode(true);
             $result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
             $this->cache->imap_mode(false);
 
             if ($result) {
                 return $msguid;
             }
         }
 
         return false;
     }
 
     /**
      * Move a Kolab object message to another IMAP folder
      *
      * @param string Object UID
      * @param string IMAP folder to move object to
      * @return boolean True on success, false on failure
      */
     public function move($uid, $target_folder)
     {
         if (!$this->valid) {
             return false;
         }
 
         if (is_string($target_folder))
             $target_folder = kolab_storage::get_folder($target_folder);
 
         if ($msguid = $this->cache->uid2msguid($uid)) {
             $this->cache->imap_mode(true);
             $result = $this->imap->move_message($msguid, $target_folder->name, $this->name);
             $this->cache->imap_mode(false);
 
             if ($result) {
                 $new_uid = ($copyuid = $this->imap->conn->data['COPYUID']) ? $copyuid[1] : null;
                 $this->cache->move($msguid, $uid, $target_folder, $new_uid);
                 return true;
             }
             else {
                 rcube::raise_error(array(
                     'code' => 600, 'type' => 'php',
                     'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(),
                 ), true);
             }
         }
 
         return false;
     }
 
     /**
      * Creates source of the configuration object message
      *
      * @param array  $object    The array that holds the data of the object.
      * @param string $type      The type of the kolab object.
      * @param bool   $binary    Enables use of binary encoding of attachment(s)
      * @param string $body_file Reference to filename of message body
      *
      * @return mixed Message as string or array with two elements
      *               (one for message file path, second for message headers)
      */
     private function build_message(&$object, $type, $binary, &$body_file)
     {
         // load old object to preserve data we don't understand/process
-        if (is_object($object['_formatobj']))
+        $format = null;
+        if (is_object($object['_formatobj'] ?? null))
             $format = $object['_formatobj'];
-        else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'])))
-            $format = $old['_formatobj'];
+        else if ($object['_msguid'] ?? null && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'] ?? null)))
+            $format = $old['_formatobj'] ?? null;
 
         // create new kolab_format instance
         if (!$format)
             $format = kolab_format::factory($type, kolab_storage::$version);
 
         if (PEAR::isError($format))
             return false;
 
         $format->set($object);
         $xml = $format->write(kolab_storage::$version);
         $object['uid'] = $format->uid;  // read UID from format
         $object['_formatobj'] = $format;
 
         if (empty($xml) || !$format->is_valid() || empty($object['uid'])) {
             return false;
         }
 
         $mime     = new Mail_mime("\r\n");
         $rcmail   = rcube::get_instance();
         $headers  = array();
         $files    = array();
         $part_id  = 1;
         $encoding = $binary ? 'binary' : 'base64';
 
         if ($user_email = $rcmail->get_user_email()) {
             $headers['From'] = $user_email;
             $headers['To'] = $user_email;
         }
         $headers['Date'] = date('r');
         $headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
         $headers['X-Kolab-Mime-Version'] = kolab_storage::$version;
         $headers['Subject'] = $object['uid'];
 //        $headers['Message-ID'] = $rcmail->gen_message_id();
         $headers['User-Agent'] = $rcmail->config->get('useragent');
 
         // Check if we have enough memory to handle the message in it
         // It's faster than using files, so we'll do this if we only can
         if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) {
             $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
 
             foreach ($object['_attachments'] as $attachment) {
                 $memory += $attachment['size'];
             }
 
             // 1.33 is for base64, we need at least 4x more memory than the message size
             if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) {
                 $marker   = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%';
                 $is_file  = true;
                 $temp_dir = unslashify($rcmail->config->get('temp_dir'));
                 $mime->setParam('delay_file_io', true);
             }
         }
 
         $mime->headers($headers);
         $mime->setTXTBody("This is a Kolab Groupware object. "
             . "To view this object you will need an email client that understands the Kolab Groupware format. "
             . "For a list of such email clients please visit http://www.kolab.org/\n\n");
 
         $ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE;
         // Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines"
         // when APPENDing from temp file
         $xml = preg_replace('/\r?\n/', "\r\n", $xml);
 
         $mime->addAttachment($xml,  // file
             $ctype,                 // content-type
             'kolab.xml',            // filename
             false,                  // is_file
             '8bit',                 // encoding
             'attachment',           // disposition
             RCUBE_CHARSET           // charset
         );
         $part_id++;
 
+        $is_file = false;
+
         // save object attachments as separate parts
-        foreach ((array)$object['_attachments'] as $key => $att) {
+        foreach ((array)($object['_attachments'] ?? []) as $key => $att) {
             if (empty($att['content']) && !empty($att['id'])) {
                 // @TODO: use IMAP CATENATE to skip attachment fetch+push operation
                 $msguid = $object['_copyfrom'] ?: ($object['_msguid'] ?: $object['uid']);
                 if ($is_file) {
                     $att['path'] = tempnam($temp_dir, 'rcmAttmnt');
                     if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {
                         fclose($fp);
                     }
                     else {
                         return false;
                     }
                 }
                 else {
                     $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true);
                 }
             }
 
             $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable'));
             $name = !empty($att['name']) ? $att['name'] : $key;
 
             // To store binary files we can use faster method
             // without writting full message content to a temporary file but
             // directly to IMAP, see rcube_imap_generic::append().
             // I.e. use file handles where possible
             if (!empty($att['path'])) {
                 if ($is_file && $binary) {
                     $files[] = fopen($att['path'], 'r');
                     $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
                 }
                 else {
                     $mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
                 }
             }
             else {
                 if (is_resource($att['content']) && $is_file && $binary) {
                     $files[] = $att['content'];
                     $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
                 }
                 else {
                     if (is_resource($att['content'])) {
                         @rewind($att['content']);
                         $att['content'] = stream_get_contents($att['content']);
                     }
                     $mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
                 }
             }
 
             $object['_attachments'][$key]['id'] = ++$part_id;
         }
 
         if (!$is_file || !empty($files)) {
             $message = $mime->getMessage();
         }
 
         // parse message and build message array with
         // attachment file pointers in place of file markers
         if (!empty($files)) {
             $message = explode($marker, $message);
             $tmp     = array();
 
             foreach ($message as $msg_part) {
                 $tmp[] = $msg_part;
                 if ($file = array_shift($files)) {
                     $tmp[] = $file;
                 }
             }
             $message = $tmp;
         }
         // write complete message body into temp file
         else if ($is_file) {
             // use common temp dir
             $body_file = tempnam($temp_dir, 'rcmMsg');
 
             if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) {
                 rcube::raise_error(array('code' => 650, 'type' => 'php',
                     'file' => __FILE__, 'line' => __LINE__,
                     'message' => "Could not create message: ".$mime_result->getMessage()),
                     true, false);
                 return false;
             }
 
             $message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r'));
         }
 
         return $message;
     }
 
     /**
      * Triggers any required updates after changes within the
      * folder. This is currently only required for handling free/busy
      * information with Kolab.
      *
      * @return boolean|PEAR_Error True if successfull.
      */
     public function trigger()
     {
         $owner = $this->get_owner();
         $result = false;
 
         switch($this->type) {
         case 'event':
             if ($this->get_namespace() == 'personal') {
                 $result = $this->trigger_url(
                     sprintf('%s/trigger/%s/%s.pfb',
                         kolab_storage::get_freebusy_server(),
                         urlencode($owner),
                         urlencode($this->imap->mod_folder($this->name))
                     ),
                     $this->imap->options['user'],
                     $this->imap->options['password']
                 );
             }
             break;
 
         default:
             return true;
         }
 
         if ($result && is_object($result) && is_a($result, 'PEAR_Error')) {
             return PEAR::raiseError(
                 sprintf("Failed triggering folder %s. Error was: %s", $this->name, $result->getMessage())
             );
         }
 
         return $result;
     }
 
     /**
      * Triggers a URL.
      *
      * @param string $url          The URL to be triggered.
      * @param string $auth_user    Username to authenticate with
      * @param string $auth_passwd  Password for basic auth
      * @return boolean|PEAR_Error  True if successfull.
      */
     private function trigger_url($url, $auth_user = null, $auth_passwd = null)
     {
         try {
             $request = libkolab::http_request($url);
 
             // set authentication credentials
             if ($auth_user && $auth_passwd)
                 $request->setAuth($auth_user, $auth_passwd);
 
             $result = $request->send();
             // rcube::write_log('trigger', $result->getBody());
         }
         catch (Exception $e) {
             return PEAR::raiseError($e->getMessage());
         }
 
         return true;
     }
 
     /**
      * Log content to a file in per_user_loggin dir if configured
      */
     private static function save_user_xml($filename, $content)
     {
         $rcmail = rcube::get_instance();
 
         if ($rcmail->config->get('kolab_format_error_log')) {
             $log_dir   = $rcmail->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
             $user_name = $rcmail->get_user_name();
             $log_dir   = $log_dir . '/' . $user_name;
 
             if (!empty($user_name) && is_writable($log_dir)) {
                 file_put_contents("$log_dir/$filename", $content);
             }
         }
     }
 }
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
index eb5b1c10..53c72bcd 100644
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -1,392 +1,392 @@
 <?php
 
 /**
  * Kolab core library
  *
  * Plugin to setup a basic environment for the interaction with a Kolab server.
  * Other Kolab-related plugins will depend on it and can use the library classes
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012-2015, Kolab Systems AG <contact@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 libkolab extends rcube_plugin
 {
     static $http_requests = array();
     static $bonnie_api    = false;
 
     /**
      * Required startup method of a Roundcube plugin
      */
     public function init()
     {
         // load local config
         $this->load_config();
         $this->require_plugin('libcalendaring');
 
         // extend include path to load bundled lib classes
         $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
         set_include_path($include_path);
 
         $this->add_hook('storage_init', array($this, 'storage_init'));
         $this->add_hook('storage_connect', array($this, 'storage_connect'));
         $this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders'));
 
         // For Chwala
         $this->add_hook('folder_mod', array('kolab_storage', 'folder_mod'));
 
         $rcmail = rcube::get_instance();
         try {
             kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
         }
         catch (Exception $e) {
             rcube::raise_error($e, true);
             kolab_format::$timezone = new DateTimeZone('GMT');
         }
 
         $this->add_texts('localization/', false);
 
         if (!empty($rcmail->output->type) && $rcmail->output->type == 'html') {
             $rcmail->output->add_handler('libkolab.folder_search_form', array($this, 'folder_search_form'));
             $this->include_stylesheet($this->local_skin_path() . '/libkolab.css');
         }
 
         // embed scripts and templates for email message audit trail
-        if ($rcmail->task == 'mail' && self::get_bonnie_api()) {
+        if (property_exists($rcmail, 'task') && $rcmail->task == 'mail' && self::get_bonnie_api()) {
             if ($rcmail->output->type == 'html') {
                 $this->add_hook('render_page', array($this, 'bonnie_render_page'));
                 $this->include_script('libkolab.js');
 
                 // add 'Show history' item to message menu
                 $this->api->add_content(html::tag('li', array('role' => 'menuitem'),
                     $this->api->output->button(array(
                         'command'  => 'kolab-mail-history',
                         'label'    => 'libkolab.showhistory',
                         'type'     => 'link',
                         'classact' => 'icon history active',
                         'class'    => 'icon history disabled',
                         'innerclass' => 'icon history',
                     ))),
                     'messagemenu');
             }
 
             $this->register_action('plugin.message-changelog', array($this, 'message_changelog'));
         }
     }
 
     /**
      * Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
      */
     function storage_init($p)
     {
         $kolab_headers = 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION MESSAGE-ID';
 
         if (!empty($p['fetch_headers'])) {
             $p['fetch_headers'] .= ' ' . $kolab_headers;
         }
         else {
             $p['fetch_headers'] = $kolab_headers;
         }
 
         return $p;
     }
 
     /**
      * Hook into IMAP connection to replace client identity
      */
     function storage_connect($p)
     {
         $client_name = 'Roundcube/Kolab';
 
         if (empty($p['ident'])) {
             $p['ident'] = array(
                 'name'    => $client_name,
                 'version' => RCUBE_VERSION,
 /*
                 'php'     => PHP_VERSION,
                 'os'      => PHP_OS,
                 'command' => $_SERVER['REQUEST_URI'],
 */
             );
         }
         else {
             $p['ident']['name'] = $client_name;
         }
 
         return $p;
     }
 
     /**
      * Getter for a singleton instance of the Bonnie API
      *
      * @return mixed kolab_bonnie_api instance if configured, false otherwise
      */
     public static function get_bonnie_api()
     {
         // get configuration for the Bonnie API
         if (!self::$bonnie_api && ($bonnie_config = rcube::get_instance()->config->get('kolab_bonnie_api', false))) {
             self::$bonnie_api = new kolab_bonnie_api($bonnie_config);
         }
 
         return self::$bonnie_api;
     }
 
     /**
      * Hook to append the message history dialog template to the mail view
      */
     function bonnie_render_page($p)
     {
         if (($p['template'] === 'mail' || $p['template'] === 'message') && !$p['kolab-audittrail']) {
             // append a template for the audit trail dialog
             $this->api->output->add_footer(
                 html::div(array('id' => 'mailmessagehistory',  'class' => 'uidialog', 'aria-hidden' => 'true', 'style' => 'display:none'),
                     self::object_changelog_table(array('class' => 'records-table changelog-table'))
                 )
             );
             $this->api->output->set_env('kolab_audit_trail', true);
             $p['kolab-audittrail'] = true;
         }
 
         return $p;
     }
 
     /**
      * Handler for message audit trail changelog requests
      */
     public function message_changelog()
     {
         if (!self::$bonnie_api) {
             return false;
         }
 
         $rcmail = rcube::get_instance();
         $msguid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST, true);
         $mailbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
 
         $result = $msguid && $mailbox ? self::$bonnie_api->changelog('mail', null, $mailbox, $msguid) : null;
         if (is_array($result)) {
             if (is_array($result['changes'])) {
                 $dtformat = $rcmail->config->get('date_format') . ' ' . $rcmail->config->get('time_format');
                 array_walk($result['changes'], function(&$change) use ($dtformat, $rcmail) {
                   if ($change['date']) {
                       $dt = rcube_utils::anytodatetime($change['date']);
                       if ($dt instanceof DateTimeInterface) {
                           $change['date'] = $rcmail->format_date($dt, $dtformat);
                       }
                   }
                 });
             }
             $this->api->output->command('plugin.message_render_changelog', $result['changes']);
         }
         else {
             $this->api->output->command('plugin.message_render_changelog', false);
         }
 
         $this->api->output->send();
     }
 
     /**
      * Wrapper function to load and initalize the HTTP_Request2 Object
      *
      * @param string|Net_Url2 Request URL
      * @param string          Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
      * @param array           Configuration for this Request instance, that will be merged
      *                        with default configuration
      *
      * @return HTTP_Request2 Request object
      */
     public static function http_request($url = '', $method = 'GET', $config = array())
     {
         $rcube       = rcube::get_instance();
         $http_config = (array) $rcube->config->get('kolab_http_request');
 
         // deprecated configuration options
         if (empty($http_config)) {
             foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
                 $value = $rcube->config->get('kolab_' . $option, true);
                 if (is_bool($value)) {
                     $http_config[$option] = $value;
                 }
             }
         }
 
         if (!empty($config)) {
             $http_config = array_merge($http_config, $config);
         }
 
         // force CURL adapter, this allows to handle correctly
         // compressed responses with SplObserver registered (kolab_files) (#4507)
         $http_config['adapter'] = 'HTTP_Request2_Adapter_Curl';
 
         $key = md5(serialize($http_config));
 
         if (!empty(self::$http_requests[$key])) {
             $request = self::$http_requests[$key];
         }
         else {
             // load HTTP_Request2 (support both composer-installed and system-installed package)
             if (!class_exists('HTTP_Request2')) {
                 require_once 'HTTP/Request2.php';
             }
 
             try {
                 $request = new HTTP_Request2();
                 $request->setConfig($http_config);
             }
             catch (Exception $e) {
                 rcube::raise_error($e, true, true);
             }
 
             // proxy User-Agent string
             $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
 
             self::$http_requests[$key] = $request;
         }
 
         // cleanup
         try {
             $request->setBody('');
             $request->setUrl($url);
             $request->setMethod($method);
         }
         catch (Exception $e) {
             rcube::raise_error($e, true, true);
         }
 
         return $request;
     }
 
     /**
      * Table oultine for object changelog display
      */
     public static function object_changelog_table($attrib = array())
     {
         $rcube = rcube::get_instance();
         $attrib += array('domain' => 'libkolab');
 
         $table = new html_table(array('cols' => 5, 'border' => 0, 'cellspacing' => 0));
         $table->add_header('diff',      '');
         $table->add_header('revision',  $rcube->gettext('revision', $attrib['domain']));
         $table->add_header('date',      $rcube->gettext('date', $attrib['domain']));
         $table->add_header('user',      $rcube->gettext('user', $attrib['domain']));
         $table->add_header('operation', $rcube->gettext('operation', $attrib['domain']));
         $table->add_header('actions',   '&nbsp;');
 
         $rcube->output->add_label(
             'libkolab.showrevision',
             'libkolab.actionreceive',
             'libkolab.actionappend',
             'libkolab.actionmove',
             'libkolab.actiondelete',
             'libkolab.actionread',
             'libkolab.actionflagset',
             'libkolab.actionflagclear',
             'libkolab.objectchangelog',
             'libkolab.objectchangelognotavailable',
             'close'
         );
 
         return $table->show($attrib);
     }
 
     /**
      * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
      */
     public static function html_diff($from, $to, $is_html = null)
     {
         // auto-detect text/html format
         if ($is_html === null) {
             $from_html = (preg_match('/<(html|body)(\s+[a-z]|>)/', $from, $m) && strpos($from, '</'.$m[1].'>') > 0);
             $to_html   = (preg_match('/<(html|body)(\s+[a-z]|>)/', $to, $m) && strpos($to, '</'.$m[1].'>') > 0);
             $is_html   = $from_html || $to_html;
 
             // ensure both parts are of the same format
             if ($is_html && !$from_html) {
                 $converter = new rcube_text2html($from, false, array('wrap' => true));
                 $from = $converter->get_html();
             }
             if ($is_html && !$to_html) {
                 $converter = new rcube_text2html($to, false, array('wrap' => true));
                 $to = $converter->get_html();
             }
         }
 
         // compute diff from HTML
         if ($is_html) {
             include_once __dir__ . '/vendor/Caxy/HtmlDiff/Match.php';
             include_once __dir__ . '/vendor/Caxy/HtmlDiff/Operation.php';
             include_once __dir__ . '/vendor/Caxy/HtmlDiff/HtmlDiff.php';
 
             // replace data: urls with a transparent image to avoid memory problems
             $from = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $from);
             $to   = preg_replace('/src="data:image[^"]+/', 'src="data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7', $to);
 
             $diff = new Caxy\HtmlDiff\HtmlDiff($from, $to);
             $diffhtml = $diff->build();
 
             // remove empty inserts (from tables)
             return preg_replace('!<ins class="diff\w+">\s*</ins>!Uims', '', $diffhtml);
         }
         else {
             include_once __dir__ . '/vendor/finediff.php';
 
             $diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
             return $diff->renderDiffToHTML();
         }
     }
 
     /**
      * Return a date() format string to render identifiers for recurrence instances
      *
      * @param array Hash array with event properties
      * @return string Format string
      */
     public static function recurrence_id_format($event)
     {
         return $event['allday'] ? 'Ymd' : 'Ymd\THis';
     }
 
     /**
      * Returns HTML code for folder search widget
      *
      * @param array $attrib Named parameters
      *
      * @return string HTML code for the gui object
      */
     public function folder_search_form($attrib)
     {
         $rcmail = rcube::get_instance();
         $attrib += array(
             'gui-object'    => false,
             'wrapper'       => true,
             'form-name'     => 'foldersearchform',
             'command'       => 'non-extsing-command',
             'reset-command' => 'non-existing-command',
         );
 
         if ($attrib['label-domain'] && !strpos($attrib['buttontitle'], '.')) {
             $attrib['buttontitle'] = $attrib['label-domain'] . '.' . $attrib['buttontitle'];
         }
 
         if ($attrib['buttontitle']) {
             $attrib['placeholder'] = $rcmail->gettext($attrib['buttontitle']);
         }
 
         return $rcmail->output->search_form($attrib);
     }
 }
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 2a9cbc3e..b1eea3e0 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -1,1738 +1,1739 @@
 <?php
 
 /**
  * Kolab Groupware driver for the Tasklist plugin
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012-2015, Kolab Systems AG <contact@kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
  * published by the Free Software Foundation, either version 3 of the
  * License, or (at your option) any later version.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  * GNU Affero General Public License for more details.
  *
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
 class tasklist_kolab_driver extends tasklist_driver
 {
     // features supported by the backend
     public $alarms      = false;
     public $attachments = true;
     public $attendees   = true;
     public $undelete    = false; // task undelete action
     public $alarm_types = array('DISPLAY','AUDIO');
     public $search_more_results;
 
     private $rc;
     private $plugin;
     private $lists;
     private $folders = array();
     private $tasks   = array();
     private $tags    = array();
     private $bonnie_api = false;
 
 
     /**
      * Default constructor
      */
     public function __construct($plugin)
     {
         $this->rc     = $plugin->rc;
         $this->plugin = $plugin;
 
         if (kolab_storage::$version == '2.0') {
             $this->alarm_absolute = false;
         }
 
         // tasklist use fully encoded identifiers
         kolab_storage::$encode_ids = true;
 
         // get configuration for the Bonnie API
         $this->bonnie_api = libkolab::get_bonnie_api();
 
         $this->plugin->register_action('folder-acl', array($this, 'folder_acl'));
     }
 
     /**
      * Read available calendars for the current user and store them internally
      */
     private function _read_lists($force = false)
     {
         // already read sources
         if (isset($this->lists) && !$force) {
             return $this->lists;
         }
 
         // get all folders that have type "task"
         $folders = kolab_storage::sort_folders(kolab_storage::get_folders('task'));
         $this->lists = $this->folders = array();
 
         $delim = $this->rc->get_storage()->get_hierarchy_delimiter();
 
         // find default folder
         $default_index = 0;
         foreach ($folders as $i => $folder) {
             if ($folder->default && strpos($folder->name, $delim) === false)
                 $default_index = $i;
         }
 
         // put default folder (aka INBOX) on top of the list
         if ($default_index > 0) {
             $default_folder = $folders[$default_index];
             unset($folders[$default_index]);
             array_unshift($folders, $default_folder);
         }
 
         $prefs = $this->rc->config->get('kolab_tasklists', array());
 
         foreach ($folders as $folder) {
             $tasklist = $this->folder_props($folder, $prefs);
 
             $this->lists[$tasklist['id']] = $tasklist;
             $this->folders[$tasklist['id']] = $folder;
             $this->folders[$folder->name] = $folder;
         }
 
         return $this->lists;
     }
 
     /**
      * Derive list properties from the given kolab_storage_folder object
      */
     protected function folder_props($folder, $prefs)
     {
         if ($folder->get_namespace() == 'personal') {
             $norename = false;
             $editable = true;
             $rights = 'lrswikxtea';
             $alarms = true;
         }
         else {
             $alarms = false;
             $rights = 'lr';
             $editable = false;
             if ($myrights = $folder->get_myrights()) {
                 $rights = $myrights;
                 if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false)
                     $editable = strpos($rights, 'i') !== 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' => $editable,
             'rights'    => $rights,
             'norename' => $norename,
             'active' => $folder->is_active(),
             'owner' => $folder->get_owner(),
             '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(),
             'removable' => !$folder->default,
             'subtype'  => $folder->subtype,
             'group' => $folder->default ? 'default' : $folder->get_namespace(),
             'class' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')),
             'caldavuid' => $folder->get_uid(),
             'history' => !empty($this->bonnie_api),
         );
     }
 
     /**
      * Get a list of available task lists from this source
      *
      * @param integer Bitmask defining filter criterias.
      *                See FILTER_* constants for possible values.
      */
     public function get_lists($filter = 0, &$tree = null)
     {
         $this->_read_lists();
 
         // attempt to create a default list for this user
         if (empty($this->lists) && !isset($this->search_more_results)) {
             $prop = array('name' => 'Tasks', 'color' => '0000CC', 'default' => true);
             if ($this->create_list($prop))
                 $this->_read_lists(true);
         }
 
         $folders = $this->filter_folders($filter);
 
         // include virtual folders for a full folder tree
         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) {
             $list_id   = $folder->id; // kolab_storage::folder_id($folder->name);
             $imap_path = explode($delim, $folder->name);
 
             // 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 = $folder->get_foldername();
 
             // special handling for virtual folders
             if ($folder instanceof kolab_storage_folder_user) {
                 $lists[$list_id] = array(
                     'id'       => $list_id,
                     'name'     => $fullname,
                     'listname' => $listname,
                     'title'    => $folder->get_title(),
                     'virtual'  => true,
                     'editable' => false,
                     'rights'   => 'l',
                     'group'    => 'other virtual',
                     'class'    => 'user',
                     'parent'   => $parent_id,
                 );
             }
             else if ($folder->virtual) {
                 $lists[$list_id] = array(
                     'id'       => $list_id,
                     'name'     => $fullname,
                     'listname' => $listname,
                     'virtual'  => true,
                     'editable' => false,
                     'rights'   => 'l',
                     'group'    => $folder->get_namespace(),
                     'class'    => 'folder',
                     'parent'   => $parent_id,
                 );
             }
             else {
                 if (!$this->lists[$list_id]) {
                     $this->lists[$list_id] = $this->folder_props($folder, $prefs);
                     $this->folders[$list_id] = $folder;
                 }
                 $this->lists[$list_id]['parent'] = $parent_id;
                 $lists[$list_id] = $this->lists[$list_id];
             }
         }
 
         return $lists;
     }
 
     /**
      * Get list of folders according to specified filters
      *
      * @param integer Bitmask defining restrictions. See FILTER_* constants for possible values.
      *
      * @return array List of task folders
      */
     protected function filter_folders($filter)
     {
         $this->_read_lists();
 
         $folders = array();
         foreach ($this->lists as $id => $list) {
             if (!empty($this->folders[$id])) {
                 $folder = $this->folders[$id];
 
                 if ($folder->get_namespace() == 'personal') {
                     $folder->editable = true;
                 }
                 else if ($rights = $folder->get_myrights()) {
                     if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) {
                         $folder->editable = strpos($rights, 'i') !== false;
                     }
                 }
 
                 $folders[] = $folder;
             }
         }
 
         $plugin = $this->rc->plugins->exec_hook('tasklist_list_filter', array(
             'list'      => $folders,
             'filter'    => $filter,
             'tasklists' => $folders,
         ));
 
         if ($plugin['abort'] || !$filter) {
             return $plugin['tasklists'];
         }
 
         $personal = $filter & self::FILTER_PERSONAL;
         $shared   = $filter & self::FILTER_SHARED;
 
         $tasklists = array();
         foreach ($folders as $folder) {
             if (($filter & self::FILTER_WRITEABLE) && !$folder->editable) {
                 continue;
             }
 /*
             if (($filter & self::FILTER_INSERTABLE) && !$folder->insert) {
                 continue;
             }
             if (($filter & self::FILTER_ACTIVE) && !$folder->is_active()) {
                 continue;
             }
             if (($filter & self::FILTER_PRIVATE) && $folder->subtype != 'private') {
                 continue;
             }
             if (($filter & self::FILTER_CONFIDENTIAL) && $folder->subtype != 'confidential') {
                 continue;
             }
 */
             if ($personal || $shared) {
                 $ns = $folder->get_namespace();
                 if (!(($personal && $ns == 'personal') || ($shared && $ns == 'shared'))) {
                     continue;
                 }
             }
 
             $tasklists[$folder->id] = $folder;
         }
 
         return $tasklists;
     }
 
     /**
      * 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
      */
     protected function get_folder($id)
     {
         $this->_read_lists();
 
         // 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->config->get('kolab_tasklists', array()));
             }
         }
 
         return $this->folders[$id];
     }
 
 
     /**
      * Create a new list assigned to the current user
      *
      * @param array Hash array with list properties
      *        name: List name
      *       color: The color of the list
      *  showalarms: True if alarms are enabled
      * @return mixed ID of the new list on success, False on error
      */
     public function create_list(&$prop)
     {
         $prop['type'] = 'task' . ($prop['default'] ? '.default' : '');
         $prop['active'] = true; // activate folder by default
         $prop['subscribed'] = true;
         $folder = kolab_storage::folder_update($prop);
 
         if ($folder === false) {
             $this->last_error = kolab_storage::$last_error;
             return false;
         }
 
         // create ID
         $id = kolab_storage::folder_id($folder);
 
         $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array());
 
         if (isset($prop['showalarms']))
             $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
 
         if ($prefs['kolab_tasklists'][$id])
             $this->rc->user->save_prefs($prefs);
 
         // force page reload to properly render folder hierarchy
         if (!empty($prop['parent'])) {
             $prop['_reload'] = true;
         }
         else {
             $folder = kolab_storage::get_folder($folder);
             $prop += $this->folder_props($folder, array());
         }
 
         return $id;
     }
 
     /**
      * Update properties of an existing tasklist
      *
      * @param array Hash array with list properties
      *          id: List Identifier
      *        name: List name
      *       color: The color of the list
      *  showalarms: True if alarms are enabled (if supported)
      * @return boolean True on success, Fales on failure
      */
     public function edit_list(&$prop)
     {
         if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
             $prop['oldname'] = $folder->name;
             $prop['type'] = 'task';
             $newfolder = kolab_storage::folder_update($prop);
 
             if ($newfolder === false) {
                 $this->last_error = kolab_storage::$last_error;
                 return false;
             }
 
             // create ID
             $id = kolab_storage::folder_id($newfolder);
 
             // fallback to local prefs
             $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array());
             unset($prefs['kolab_tasklists'][$prop['id']]);
 
             if (isset($prop['showalarms']))
                 $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
 
             if ($prefs['kolab_tasklists'][$id])
                 $this->rc->user->save_prefs($prefs);
 
             // force page reload if folder name/hierarchy changed
             if ($newfolder != $prop['oldname'])
                 $prop['_reload'] = true;
 
             return $id;
         }
 
         return false;
     }
 
     /**
      * Set active/subscribed state of a list
      *
      * @param array Hash array with list properties
      *          id: List Identifier
      *      active: True if list is active, false if not
      *   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->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']));
 
             // apply to child folders, too
             if ($prop['recursive']) {
                 foreach ((array)kolab_storage::list_folders($folder->name, '*', 'task') as $subfolder) {
                     if (isset($prop['permanent']))
                         ($prop['permanent'] ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
                     if (isset($prop['active']))
                         ($prop['active'] ? kolab_storage::folder_activate($subfolder) : kolab_storage::folder_deactivate($subfolder));
                 }
             }
 
             return $ret;
         }
 
         return false;
     }
 
     /**
      * Delete the given list with all its contents
      *
      * @param array Hash array with list properties
      *      id: list Identifier
      * @return boolean True on success, Fales on failure
      */
     public function delete_list($prop)
     {
         if ($prop['id'] && ($folder = $this->get_folder($prop['id']))) {
             if (kolab_storage::folder_delete($folder->name)) {
                 return true;
             }
 
             $this->last_error = kolab_storage::$last_error;
         }
 
         return false;
     }
 
     /**
      * 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();
 
         // 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, 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, array());
 
                     foreach ($folders as $folder) {
                         $this->folders[$folder->id] = $folder;
                         $this->lists[$folder->id] = $this->folder_props($folder, array());
                         $count++;
                     }
                 }
 
                 if ($count >= $limit) {
                     $this->search_more_results = true;
                     break;
                 }
             }
         }
 
         return $this->get_lists();
     }
 
     /**
      * Get a list of tags to assign tasks to
      *
      * @return array List of tags
      */
     public function get_tags()
     {
         $config = kolab_storage_config::get_instance();
         $tags   = $config->get_tags();
         $backend_tags = array_map(function($v) { return $v['name']; }, $tags);
 
         return array_values(array_unique(array_merge($this->tags, $backend_tags)));
     }
 
     /**
      * Get number of tasks matching the given filter
      *
      * @param array List of lists to count tasks of
      * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate)
      */
     public function count_tasks($lists = null)
     {
         if (empty($lists)) {
             $lists = $this->_read_lists();
             $lists = array_keys($lists);
         }
         else if (is_string($lists)) {
             $lists = explode(',', $lists);
         }
 
         $today_date    = new DateTime('now', $this->plugin->timezone);
         $today         = $today_date->format('Y-m-d');
         $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone);
         $tomorrow      = $tomorrow_date->format('Y-m-d');
 
         $counts = array('all' => 0, 'today' => 0, 'tomorrow' => 0, 'later' => 0, 'overdue'  => 0);
 
         foreach ($lists as $list_id) {
             if (!$folder = $this->get_folder($list_id)) {
                 continue;
             }
 
             foreach ($folder->select(array(array('tags','!~','x-complete')), true) as $record) {
                 $rec = $this->_to_rcube_task($record, $list_id, false);
 
                 if ($this->is_complete($rec))  // don't count complete tasks
                     continue;
 
                 $counts['all']++;
                 if (empty($rec['date']))
                     $counts['later']++;
                 else if ($rec['date'] == $today)
                     $counts['today']++;
                 else if ($rec['date'] == $tomorrow)
                     $counts['tomorrow']++;
                 else if ($rec['date'] < $today)
                     $counts['overdue']++;
                 else if ($rec['date'] > $tomorrow)
                     $counts['later']++;
             }
         }
 
         // avoid session race conditions that will loose temporary subscriptions
         $this->plugin->rc->session->nowrite = true;
 
         return $counts;
     }
 
     /**
      * Get all task records matching the given filter
      *
      * @param array Hash array with filter criterias:
      *  - mask:  Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants)
      *  - from:  Date range start as string (Y-m-d)
      *  - to:    Date range end as string (Y-m-d)
      *  - search: Search query string
      *  - uid:   Task UIDs
      * @param array List of lists to get tasks from
      * @return array List of tasks records matchin the criteria
      */
     public function list_tasks($filter, $lists = null)
     {
         if (empty($lists)) {
             $lists = $this->_read_lists();
             $lists = array_keys($lists);
         }
         else if (is_string($lists)) {
             $lists = explode(',', $lists);
         }
 
         $config  = kolab_storage_config::get_instance();
         $results = array();
 
         // query Kolab storage
         $query = array();
         if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE)
             $query[] = array('tags','~','x-complete');
         else if (empty($filter['since']))
             $query[] = array('tags','!~','x-complete');
 
         // full text search (only works with cache enabled)
         if ($filter['search']) {
             $search = mb_strtolower($filter['search']);
             foreach (rcube_utils::normalize_string($search, true) as $word) {
                 $query[] = array('words', '~', $word);
             }
         }
 
-        if ($filter['since']) {
+        if ($filter['since'] ?? false) {
             $query[] = array('changed', '>=', $filter['since']);
         }
 
-        if ($filter['uid']) {
+        if ($filter['uid'] ?? false) {
             $query[] = array('uid', '=', (array) $filter['uid']);
         }
 
         foreach ($lists as $list_id) {
             if (!$folder = $this->get_folder($list_id)) {
                 continue;
             }
 
             foreach ($folder->select($query) as $record) {
                 // TODO: post-filter tasks returned from storage
                 $record['list_id'] = $list_id;
                 $results[] = $record;
             }
         }
 
         $config->apply_tags($results, true);
         $config->apply_links($results);
 
         foreach (array_keys($results) as $idx) {
             $results[$idx] = $this->_to_rcube_task($results[$idx], $results[$idx]['list_id']);
         }
 
         // avoid session race conditions that will loose temporary subscriptions
         $this->plugin->rc->session->nowrite = true;
 
         return $results;
     }
 
     /**
      * Return data of a specific task
      *
      * @param mixed   Hash array with task properties or task UID
      * @param integer Bitmask defining filter criterias for folders.
      *                See FILTER_* constants for possible values.
      *
      * @return array Hash array with task properties or false if not found
      */
     public function get_task($prop, $filter = 0)
     {
         $this->_parse_id($prop);
 
         $id      = $prop['uid'];
         $list_id = $prop['list'];
         $folders = $list_id ? array($list_id => $this->get_folder($list_id)) : $this->get_lists($filter);
 
         // find task in the available folders
         foreach ($folders as $list_id => $folder) {
             if (is_array($folder))
                 $folder = $this->folders[$list_id];
             if (is_numeric($list_id) || !$folder)
                 continue;
-            if (!$this->tasks[$id] && ($object = $folder->get_object($id))) {
+            if (!($this->tasks[$id] ?? false) && ($object = $folder->get_object($id))) {
                 $this->load_tags($object);
                 $this->tasks[$id] = $this->_to_rcube_task($object, $list_id);
                 break;
             }
         }
 
         return $this->tasks[$id];
     }
 
     /**
      * Get all decendents of the given task record
      *
      * @param mixed  Hash array with task properties or task UID
      * @param boolean True if all childrens children should be fetched
      * @return array List of all child task IDs
      */
     public function get_childs($prop, $recursive = false)
     {
         if (is_string($prop)) {
             $task = $this->get_task($prop);
             $prop = array('uid' => $task['uid'], 'list' => $task['list']);
         }
         else {
             $this->_parse_id($prop);
         }
 
         $childs = array();
         $list_id = $prop['list'];
         $task_ids = array($prop['uid']);
         $folder = $this->get_folder($list_id);
 
         // query for childs (recursively)
         while ($folder && !empty($task_ids)) {
             $query_ids = array();
             foreach ($task_ids as $task_id) {
                 $query = array(array('tags','=','x-parent:' . $task_id));
                 foreach ($folder->select($query) as $record) {
                     // don't rely on kolab_storage_folder filtering
                     if ($record['parent_id'] == $task_id) {
                         $childs[] = $list_id . ':' . $record['uid'];
                         $query_ids[] = $record['uid'];
                     }
                 }
             }
 
             if (!$recursive)
                 break;
 
             $task_ids = $query_ids;
         }
 
         return $childs;
     }
 
     /**
      * Provide a list of revisions for the given task
      *
      * @param array  $task Hash array with task properties
      * @return array List of changes, each as a hash array
      * @see tasklist_driver::get_task_changelog()
      */
     public function get_task_changelog($prop)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
 
         $result = $uid && $mailbox ? $this->bonnie_api->changelog('task', $uid, $mailbox, $msguid) : null;
         if (is_array($result) && $result['uid'] == $uid) {
             return $result['changes'];
         }
 
         return false;
     }
 
     /**
      * Return full data of a specific revision of an event
      *
      * @param mixed  $task UID string or hash array with task properties
      * @param mixed  $rev Revision number
      *
      * @return array Task object as hash array
      * @see tasklist_driver::get_task_revision()
      */
     public function get_task_revison($prop, $rev)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         $this->_parse_id($prop);
         $uid     = $prop['uid'];
         $list_id = $prop['list'];
         list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
 
         // call Bonnie API
         $result = $this->bonnie_api->get('task', $uid, $rev, $mailbox, $msguid);
         if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
             $format = kolab_format::factory('task');
             $format->load($result['xml']);
             $rec = $format->to_array();
             $format->get_attachments($rec, true);
 
             if ($format->is_valid()) {
                 $rec = self::_to_rcube_task($rec, $list_id, false);
                 $rec['rev'] = $result['rev'];
                 return $rec;
             }
         }
 
         return false;
     }
 
     /**
      * Command the backend to restore a certain revision of a task.
      * This shall replace the current object with an older version.
      *
      * @param mixed  $task UID string or hash array with task properties
      * @param mixed  $rev Revision number
      *
      * @return boolean True on success, False on failure
      * @see tasklist_driver::restore_task_revision()
      */
     public function restore_task_revision($prop, $rev)
     {
         if (empty($this->bonnie_api)) {
             return false;
         }
 
         $this->_parse_id($prop);
         $uid     = $prop['uid'];
         $list_id = $prop['list'];
         list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
 
         $folder  = $this->get_folder($list_id);
         $success = false;
 
         if ($folder && ($raw_msg = $this->bonnie_api->rawdata('task', $uid, $rev, $mailbox))) {
             $imap = $this->rc->get_storage();
 
             // insert $raw_msg as new message
             if ($imap->save_message($folder->name, $raw_msg, null, false)) {
                 $success = true;
 
                 // delete old revision from imap and cache
                 $imap->delete_message($msguid, $folder->name);
                 $folder->cache->set($msguid, false);
             }
         }
 
         return $success;
     }
 
     /**
      * Get a list of property changes beteen two revisions of a task object
      *
      * @param array  $task Hash array with task properties
      * @param mixed  $rev   Revisions: "from:to"
      *
      * @return array List of property changes, each as a hash array
      * @see tasklist_driver::get_task_diff()
      */
     public function get_task_diff($prop, $rev1, $rev2)
     {
         $this->_parse_id($prop);
         $uid     = $prop['uid'];
         $list_id = $prop['list'];
         list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($prop);
 
         // call Bonnie API
         $result = $this->bonnie_api->diff('task', $uid, $rev1, $rev2, $mailbox, $msguid, $instance_id);
         if (is_array($result) && $result['uid'] == $uid) {
             $result['rev1'] = $rev1;
             $result['rev2'] = $rev2;
 
             $keymap = array(
                 'start'    => 'start',
                 'due'      => 'date',
                 'dstamp'   => 'changed',
                 'summary'  => 'title',
                 'alarm'    => 'alarms',
                 'attendee' => 'attendees',
                 'attach'   => 'attachments',
                 'rrule'    => 'recurrence',
                 'related-to' => 'parent_id',
                 'percent-complete' => 'complete',
                 'lastmodified-date' => 'changed',
             );
             $prop_keymaps = array(
                 'attachments' => array('fmttype' => 'mimetype', 'label' => 'name'),
                 'attendees'   => array('partstat' => 'status'),
             );
             $special_changes = array();
 
             // map kolab event properties to keys the client expects
             array_walk($result['changes'], function(&$change, $i) use ($keymap, $prop_keymaps, $special_changes) {
                 if (array_key_exists($change['property'], $keymap)) {
                     $change['property'] = $keymap[$change['property']];
                 }
                 if ($change['property'] == 'priority') {
                     $change['property'] = 'flagged';
                     $change['old'] = $change['old'] == 1 ? $this->plugin->gettext('yes') : null;
                     $change['new'] = $change['new'] == 1 ? $this->plugin->gettext('yes') : null;
                 }
                 // map alarms trigger value
                 if ($change['property'] == 'alarms') {
                     if (is_array($change['old']) && is_array($change['old']['trigger']))
                         $change['old']['trigger'] = $change['old']['trigger']['value'];
                     if (is_array($change['new']) && is_array($change['new']['trigger']))
                         $change['new']['trigger'] = $change['new']['trigger']['value'];
                 }
                 // make all property keys uppercase
                 if ($change['property'] == 'recurrence') {
                     $special_changes['recurrence'] = $i;
                     foreach (array('old','new') as $m) {
                         if (is_array($change[$m])) {
                             $props = array();
                             foreach ($change[$m] as $k => $v) {
                                 $props[strtoupper($k)] = $v;
                             }
                             $change[$m] = $props;
                         }
                     }
                 }
                 // map property keys names
                 if (is_array($prop_keymaps[$change['property']])) {
                   foreach ($prop_keymaps[$change['property']] as $k => $dest) {
                     if (is_array($change['old']) && array_key_exists($k, $change['old'])) {
                         $change['old'][$dest] = $change['old'][$k];
                         unset($change['old'][$k]);
                     }
                     if (is_array($change['new']) && array_key_exists($k, $change['new'])) {
                         $change['new'][$dest] = $change['new'][$k];
                         unset($change['new'][$k]);
                     }
                   }
                 }
 
                 if ($change['property'] == 'exdate') {
                     $special_changes['exdate'] = $i;
                 }
                 else if ($change['property'] == 'rdate') {
                     $special_changes['rdate'] = $i;
                 }
             });
 
             // merge some recurrence changes
             foreach (array('exdate','rdate') as $prop) {
                 if (array_key_exists($prop, $special_changes)) {
                     $exdate = $result['changes'][$special_changes[$prop]];
                     if (array_key_exists('recurrence', $special_changes)) {
                         $recurrence = &$result['changes'][$special_changes['recurrence']];
                     }
                     else {
                         $i = count($result['changes']);
                         $result['changes'][$i] = array('property' => 'recurrence', 'old' => array(), 'new' => array());
                         $recurrence = &$result['changes'][$i]['recurrence'];
                     }
                     $key = strtoupper($prop);
                     $recurrence['old'][$key] = $exdate['old'];
                     $recurrence['new'][$key] = $exdate['new'];
                     unset($result['changes'][$special_changes[$prop]]);
                 }
             }
 
             return $result;
         }
 
         return false;
     }
 
     /**
      * Helper method to resolved the given task identifier into uid and folder
      *
      * @return array (uid,folder,msguid) tuple
      */
     private function _resolve_task_identity($prop)
     {
         $mailbox = $msguid = null;
 
         $this->_parse_id($prop);
         $uid     = $prop['uid'];
         $list_id = $prop['list'];
 
         if ($folder = $this->get_folder($list_id)) {
             $mailbox = $folder->get_mailbox_id();
 
             // get task object from storage in order to get the real object uid an msguid
             if ($rec = $folder->get_object($uid)) {
                 $msguid = $rec['_msguid'];
                 $uid = $rec['uid'];
             }
         }
 
         return array($uid, $mailbox, $msguid);
     }
 
     /**
      * Get a list of pending alarms to be displayed to the user
      *
      * @param  integer Current time (unix timestamp)
      * @param  mixed   List of list IDs to show alarms for (either as array or comma-separated string)
      * @return array   A list of alarms, each encoded as hash array with task properties
      * @see tasklist_driver::pending_alarms()
      */
     public function pending_alarms($time, $lists = null)
     {
         $interval = 300;
         $time -= $time % 60;
 
         $slot = $time;
         $slot -= $slot % $interval;
 
         $last = $time - max(60, $this->rc->config->get('refresh_interval', 0));
         $last -= $last % $interval;
 
         // only check for alerts once in 5 minutes
         if ($last == $slot)
             return array();
 
         if ($lists && is_string($lists))
             $lists = explode(',', $lists);
 
         $time = $slot + $interval;
 
         $candidates = array();
         $query      = array(
             array('tags', '=', 'x-has-alarms'),
             array('tags', '!=', 'x-complete')
         );
 
         $this->_read_lists();
 
         foreach ($this->lists as $lid => $list) {
             // skip lists with alarms disabled
             if (!$list['showalarms'] || ($lists && !in_array($lid, $lists)))
                 continue;
 
             $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;
 
                 $task = $this->_to_rcube_task($record, $lid, false);
 
                 // add to list if alarm is set
                 $alarm = libcalendaring::get_next_alarm($task, 'task');
                 if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) {
                     $id = $alarm['id'];  // use alarm-id as primary identifier
                     $candidates[$id] = array(
                         'id'       => $id,
                         'title'    => $task['title'],
                         'date'     => $task['date'],
                         'time'     => $task['time'],
                         'notifyat' => $alarm['time'],
                         'action'   => $alarm['action'],
                     );
                 }
             }
         }
 
         // get alarm information stored in local database
         if (!empty($candidates)) {
             $alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates));
             $result = $this->rc->db->query("SELECT *"
                 . " FROM " . $this->rc->db->table_name('kolab_alarms', true)
                 . " WHERE `alarm_id` IN (" . join(',', $alarm_ids) . ")"
                     . " AND `user_id` = ?",
                 $this->rc->user->ID
             );
 
             while ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
                 $dbdata[$rec['alarm_id']] = $rec;
             }
         }
 
         $alarms = array();
         foreach ($candidates as $id => $task) {
           // skip dismissed
           if ($dbdata[$id]['dismissed'])
               continue;
 
           // snooze function may have shifted alarm time
           $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat'];
           if ($notifyat <= $time)
               $alarms[] = $task;
         }
 
         return $alarms;
     }
 
     /**
      * (User) feedback after showing an alarm notification
      * This should mark the alarm as 'shown' or snooze it for the given amount of time
      *
      * @param  string  Task identifier
      * @param  integer Suspend the alarm for this number of seconds
      */
     public function dismiss_alarm($id, $snooze = 0)
     {
         // delete old alarm entry
         $this->rc->db->query(
             "DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . "
              WHERE `alarm_id` = ? AND `user_id` = ?",
             $id,
             $this->rc->user->ID
         );
 
         // set new notifyat time or unset if not snoozed
         $notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
 
         $query = $this->rc->db->query(
             "INSERT INTO " . $this->rc->db->table_name('kolab_alarms', true) . "
              (`alarm_id`, `user_id`, `dismissed`, `notifyat`)
              VALUES (?, ?, ?, ?)",
             $id,
             $this->rc->user->ID,
             $snooze > 0 ? 0 : 1,
             $notifyat
         );
 
         return $this->rc->db->affected_rows($query);
     }
 
     /**
      * Remove alarm dismissal or snooze state
      *
      * @param  string  Task identifier
      */
     public function clear_alarms($id)
     {
         // delete alarm entry
         $this->rc->db->query(
             "DELETE FROM " . $this->rc->db->table_name('kolab_alarms', true) . "
              WHERE `alarm_id` = ? AND `user_id` = ?",
             $id,
             $this->rc->user->ID
         );
 
         return true;
     }
 
     /**
      * Get task tags
      */
     private function load_tags(&$object)
     {
         // this task hasn't been migrated yet
         if (!empty($object['categories'])) {
             // OPTIONAL: call kolab_storage_config::apply_tags() to migrate the object
             $object['tags'] = (array)$object['categories'];
             if (!empty($object['tags'])) {
                 $this->tags = array_merge($this->tags, $object['tags']);
             }
         }
         else {
             $config = kolab_storage_config::get_instance();
             $tags   = $config->get_tags($object['uid']);
             $object['tags'] = array_map(function($v) { return $v['name']; }, $tags);
         }
     }
 
     /**
      * Update task tags
      */
     private function save_tags($uid, $tags)
     {
         $config = kolab_storage_config::get_instance();
         $config->save_tags($uid, $tags);
     }
 
     /**
      * Find messages linked with a task record
      */
     private function get_links($uid)
     {
         $config = kolab_storage_config::get_instance();
         return $config->get_object_links($uid);
     }
 
     /**
      *
      */
     private function save_links($uid, $links)
     {
         $config = kolab_storage_config::get_instance();
         return $config->save_object_links($uid, (array) $links);
     }
 
     /**
      * Extract uid + list identifiers from the given input
      *
      * @param mixed array or string with task identifier(s)
      */
     private function _parse_id(&$prop)
     {
         $id_ = null;
         if (is_array($prop)) {
             // 'uid' + 'list' available, nothing to be done
             if (!empty($prop['uid']) && !empty($prop['list'])) {
                 return;
             }
 
             // 'id' is given
             if (!empty($prop['id'])) {
                 if (!empty($prop['list'])) {
                     $list_id = $prop['_fromlist'] ?: $prop['list'];
                     if (strpos($prop['id'], $list_id.':') === 0) {
                         $prop['uid'] = substr($prop['id'], strlen($list_id)+1);
                     }
                     else {
                         $prop['uid'] = $prop['id'];
                     }
                 }
                 else {
                     $id_ = $prop['id'];
                 }
             }
         }
         else {
             $id_ = strval($prop);
             $prop = array();
         }
 
         // split 'id' into list + uid
         if (!empty($id_)) {
             list($list, $uid) = explode(':', $id_, 2);
             if (!empty($uid)) {
                 $prop['uid'] = $uid;
                 $prop['list'] = $list;
             }
             else {
                 $prop['uid'] = $id_;
             }
         }
     }
 
     /**
      * Convert from Kolab_Format to internal representation
      */
     private function _to_rcube_task($record, $list_id, $all = true)
     {
         $id_prefix = $list_id . ':';
         $task = array(
             'id' => $id_prefix . $record['uid'],
             'uid' => $record['uid'],
-            'title' => $record['title'],
+            'title' => $record['title'] ?? null,
 //            'location' => $record['location'],
-            'description' => $record['description'],
-            'flagged' => $record['priority'] == 1,
-            'complete' => floatval($record['complete'] / 100),
-            'status' => $record['status'],
-            'parent_id' => $record['parent_id'] ? $id_prefix . $record['parent_id'] : null,
-            'recurrence' => $record['recurrence'],
-            'attendees' => $record['attendees'],
-            'organizer' => $record['organizer'],
-            'sequence' => $record['sequence'],
-            'tags' => $record['tags'],
+            'description' => $record['description'] ?? null,
+            'flagged' => ($record['priority'] ?? null) == 1,
+            'complete' => floatval(($record['complete'] ?? null) / 100),
+            'status' => $record['status'] ?? null,
+            'parent_id' => ($record['parent_id'] ?? null) ? $id_prefix . $record['parent_id'] : null,
+            'recurrence' => $record['recurrence'] ?? null,
+            'attendees' => $record['attendees'] ?? null,
+            'organizer' => $record['organizer'] ?? null,
+            'sequence' => $record['sequence'] ?? null,
+            'tags' => $record['tags'] ?? null,
             'list' => $list_id,
-            'links' => $record['links'],
+            'links' => $record['links'] ?? null,
         );
 
         // we can sometimes skip this expensive operation
         if ($all && !array_key_exists('links', $task)) {
             $task['links'] = $this->get_links($task['uid']);
         }
 
         // convert from DateTime to internal date format
-        if ($record['due'] instanceof DateTimeInterface) {
+        if (($record['due'] ?? null) instanceof DateTimeInterface) {
             $due = $this->plugin->lib->adjust_timezone($record['due']);
             $task['date'] = $due->format('Y-m-d');
             if (empty($record['due']->_dateonly)) {
                 $task['time'] = $due->format('H:i');
             }
         }
         // convert from DateTime to internal date format
-        if ($record['start'] instanceof DateTimeInterface) {
+        if (($record['start'] ?? null) instanceof DateTimeInterface) {
             $start = $this->plugin->lib->adjust_timezone($record['start']);
             $task['startdate'] = $start->format('Y-m-d');
             if (empty($record['start']->_dateonly)) {
                 $task['starttime'] = $start->format('H:i');
             }
         }
-        if ($record['changed'] instanceof DateTimeInterface) {
+        if (($record['changed'] ?? null) instanceof DateTimeInterface) {
             $task['changed'] = $record['changed'];
         }
-        if ($record['created'] instanceof DateTimeInterface) {
+        if (($record['created'] ?? null) instanceof DateTimeInterface) {
             $task['created'] = $record['created'];
         }
 
-        if ($record['valarms']) {
+        if ($record['valarms'] ?? false) {
             $task['valarms'] = $record['valarms'];
         }
-        else if ($record['alarms']) {
+        else if ($record['alarms'] ?? false) {
             $task['alarms'] = $record['alarms'];
         }
 
         if (!empty($task['attendees'])) {
             foreach ((array)$task['attendees'] as $i => $attendee) {
                 if (isset($attendee['delegated-from']) && is_array($attendee['delegated-from'])) {
                     $task['attendees'][$i]['delegated-from'] = join(', ', $attendee['delegated-from']);
                 }
                 if (isset($attendee['delegated-to']) && is_array($attendee['delegated-to'])) {
                     $task['attendees'][$i]['delegated-to'] = join(', ', $attendee['delegated-to']);
                 }
             }
         }
 
-        if (!empty($record['_attachments'])) {
+        if (!empty($record['_attachments'] ?? [])) {
             foreach ($record['_attachments'] as $key => $attachment) {
                 if ($attachment !== false) {
                     if (empty($attachment['name'])) {
                         $attachment['name'] = $key;
                     }
                     $attachments[] = $attachment;
                 }
             }
 
             $task['attachments'] = $attachments;
         }
 
         return $task;
     }
 
     /**
      * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
      * (opposite of self::_to_rcube_event())
      */
     private function _from_rcube_task($task, $old = [])
     {
         $object    = $task;
         $id_prefix = $task['list'] . ':';
 
         $toDT = function($date) {
             // Convert DateTime into libcalendaring_datetime
             return libcalendaring_datetime::createFromFormat(
                 'Y-m-d\\TH:i:s',
                 $date->format('Y-m-d\\TH:i:s'),
                 $date->getTimezone()
             );
         };
 
         if (!empty($task['date'])) {
             $object['due'] = $toDT(rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->plugin->timezone));
             if (empty($task['time'])) {
                 $object['due']->_dateonly = true;
             }
             unset($object['date']);
         }
 
         if (!empty($task['startdate'])) {
             $object['start'] = $toDT(rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone));
             if (empty($task['starttime'])) {
                 $object['start']->_dateonly = true;
             }
             unset($object['startdate']);
         }
 
         // as per RFC (and the Kolab schema validation), start and due dates need to be of the same type (#3614)
         // this should be catched in the client already but just make sure we don't write invalid objects
         if (!empty($object['start']) && !empty($object['due']) && $object['due']->_dateonly != $object['start']->_dateonly) {
             $object['start']->_dateonly = true;
             $object['due']->_dateonly = true;
         }
 
         $object['complete'] = $task['complete'] * 100;
         if ($task['complete'] == 1.0 && empty($task['complete']))
             $object['status'] = 'COMPLETED';
 
-        if ($task['flagged'])
+        if ($task['flagged'] ?? false)
             $object['priority'] = 1;
         else
-            $object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0;
+            $object['priority'] = ($old['priority'] ?? 0) > 1 ? $old['priority'] : 0;
 
         // remove list: prefix from parent_id
         if (!empty($task['parent_id']) && strpos($task['parent_id'], $id_prefix) === 0) {
             $object['parent_id'] = substr($task['parent_id'], strlen($id_prefix));
         }
 
         // copy meta data (starting with _) from old object
         foreach ((array)$old as $key => $val) {
             if (!isset($object[$key]) && $key[0] == '_')
                 $object[$key] = $val;
         }
 
         // copy recurrence rules if the client didn't submit it (#2713)
         if (!array_key_exists('recurrence', $object) && $old['recurrence']) {
             $object['recurrence'] = $old['recurrence'];
         }
 
         unset($task['attachments']);
         kolab_format::merge_attachments($object, $old);
 
         // allow sequence increments if I'm the organizer
         if ($this->plugin->is_organizer($object) && empty($object['_method'])) {
             unset($object['sequence']);
         }
         else if (isset($old['sequence']) && empty($object['_method'])) {
             $object['sequence'] = $old['sequence'];
         }
 
         unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['created']);
         return $object;
     }
 
     /**
      * Add a single task to the database
      *
      * @param array Hash array with task properties (see header of tasklist_driver.php)
      * @return mixed New task ID on success, False on error
      */
     public function create_task($task)
     {
         return $this->edit_task($task);
     }
 
     /**
      * Update an task entry with the given data
      *
      * @param array Hash array with task properties (see header of tasklist_driver.php)
      * @return boolean True on success, False on error
      */
     public function edit_task($task)
     {
         $this->_parse_id($task);
         $list_id = $task['list'];
         if (!$list_id || !($folder = $this->get_folder($list_id))) {
             rcube::raise_error(array(
                 'code' => 600, 'type' => 'php',
                 'file' => __FILE__, 'line' => __LINE__,
                 'message' => "Invalid list identifer to save task: " . print_r($list_id, true)),
                 true, false);
             return false;
         }
 
         // email links and tags are stored separately
-        $links = $task['links'];
-        $tags  = $task['tags'];
+        $links = $task['links'] ?? null;
+        $tags  = $task['tags'] ?? null;
         unset($task['tags'], $task['links']);
 
         // moved from another folder
-        if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
+        if (($task['_fromlist'] ?? false) && ($fromfolder = $this->get_folder($task['_fromlist']))) {
             if (!$fromfolder->move($task['uid'], $folder))
                 return false;
 
             unset($task['_fromlist']);
         }
 
         // load previous version of this task to merge
-        if ($task['id']) {
+        $old = null;
+        if ($task['id'] ?? null) {
             $old = $folder->get_object($task['uid']);
             if (!$old || PEAR::isError($old))
                 return false;
 
             // merge existing properties if the update isn't complete
             if (!isset($task['title']) || !isset($task['complete']))
                 $task += $this->_to_rcube_task($old, $list_id);
         }
 
         // generate new task object from RC input
         $object = $this->_from_rcube_task($task, $old);
         $saved  = $folder->save($object, 'task', $task['uid']);
 
         if (!$saved) {
             rcube::raise_error(array(
                 'code' => 600, 'type' => 'php',
                 'file' => __FILE__, 'line' => __LINE__,
                 'message' => "Error saving task object to Kolab server"),
                 true, false);
             $saved = false;
         }
         else {
             // save links in configuration.relation object
             $this->save_links($object['uid'], $links);
             // save tags in configuration.relation object
             $this->save_tags($object['uid'], $tags);
 
             $task = $this->_to_rcube_task($object, $list_id);
             $task['tags'] = (array) $tags;
             $this->tasks[$task['uid']] = $task;
         }
 
         return $saved;
     }
 
     /**
      * Move a single task to another list
      *
      * @param array   Hash array with task properties:
      * @return boolean True on success, False on error
      * @see tasklist_driver::move_task()
      */
     public function move_task($task)
     {
         $this->_parse_id($task);
         $list_id = $task['list'];
         if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         // execute move command
         if ($task['_fromlist'] && ($fromfolder = $this->get_folder($task['_fromlist']))) {
             return $fromfolder->move($task['uid'], $folder);
         }
 
         return false;
     }
 
     /**
      * Remove a single task from the database
      *
      * @param array   Hash array with task properties:
      *      id: Task identifier
      * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend)
      * @return boolean True on success, False on error
      */
     public function delete_task($task, $force = true)
     {
         $this->_parse_id($task);
         $list_id = $task['list'];
         if (!$list_id || !($folder = $this->get_folder($list_id)))
             return false;
 
         $status = $folder->delete($task['uid']);
 
         if ($status) {
             // remove tag assignments
             // @TODO: don't do this when undelete feature will be implemented
             $this->save_tags($task['uid'], null);
         }
 
         return $status;
     }
 
     /**
      * Restores a single deleted task (if supported)
      *
      * @param array Hash array with task properties:
      *      id: Task identifier
      * @return boolean True on success, False on error
      */
     public function undelete_task($prop)
     {
         // TODO: implement this
         return false;
     }
 
 
     /**
      * Get attachment properties
      *
      * @param string $id    Attachment identifier
      * @param array  $task  Hash array with event properties:
      *         id: Task identifier
      *       list: List identifier
      *        rev: Revision (optional)
      *
      * @return array Hash array with attachment properties:
      *         id: Attachment identifier
      *       name: Attachment name
      *   mimetype: MIME content type of the attachment
      *       size: Attachment size
      */
     public function get_attachment($id, $task)
     {
         // get old revision of the object
         if ($task['rev']) {
             $task = $this->get_task_revison($task, $task['rev']);
         }
         else {
             $task = $this->get_task($task);
         }
 
         if ($task && !empty($task['attachments'])) {
             foreach ($task['attachments'] as $att) {
                 if ($att['id'] == $id)
                     return $att;
             }
         }
 
         return null;
     }
 
     /**
      * Get attachment body
      *
      * @param string $id    Attachment identifier
      * @param array  $task  Hash array with event properties:
      *         id: Task identifier
      *       list: List identifier
      *        rev: Revision (optional)
      *
      * @return string Attachment body
      */
     public function get_attachment_body($id, $task)
     {
         $this->_parse_id($task);
 
         // get old revision of event
         if ($task['rev']) {
             if (empty($this->bonnie_api)) {
                 return false;
             }
 
             $cid = substr($id, 4);
 
             // call Bonnie API and get the raw mime message
             list($uid, $mailbox, $msguid) = $this->_resolve_task_identity($task);
             if ($msg_raw = $this->bonnie_api->rawdata('task', $uid, $task['rev'], $mailbox, $msguid)) {
                 // parse the message and find the part with the matching content-id
                 $message = rcube_mime::parse_message($msg_raw);
                 foreach ((array)$message->parts as $part) {
                     if ($part->headers['content-id'] && trim($part->headers['content-id'], '<>') == $cid) {
                         return $part->body;
                     }
                 }
             }
 
             return false;
         }
 
 
         if ($storage = $this->get_folder($task['list'])) {
             return $storage->get_attachment($task['uid'], $id);
         }
 
         return false;
     }
 
     /**
      * Build a struct representing the given message reference
      *
      * @see tasklist_driver::get_message_reference()
      */
     public function get_message_reference($uri_or_headers, $folder = null)
     {
         if (is_object($uri_or_headers)) {
             $uri_or_headers = kolab_storage_config::get_message_uri($uri_or_headers, $folder);
         }
 
         if (is_string($uri_or_headers)) {
             return kolab_storage_config::get_message_reference($uri_or_headers, 'task');
         }
 
         return false;
     }
 
     /**
      * Find tasks assigned to a specified message
      *
      * @see tasklist_driver::get_message_related_tasks()
      */
     public function get_message_related_tasks($headers, $folder)
     {
         $config = kolab_storage_config::get_instance();
         $result = $config->get_message_relations($headers, $folder, 'task');
 
         foreach ($result as $idx => $rec) {
             $result[$idx] = $this->_to_rcube_task($rec, kolab_storage::folder_id($rec['_mailbox']));
         }
 
         return $result;
     }
 
     /**
      * 
      */
     public function tasklist_edit_form($action, $list, $fieldprop)
     {
         $this->_read_lists();
 
         if ($list['id'] && ($list = $this->lists[$list['id']])) {
             $folder_name = $this->get_folder($list['id'])->name; // UTF7
         }
         else {
             $folder_name = '';
         }
 
         $storage = $this->rc->get_storage();
         $delim   = $storage->get_hierarchy_delimiter();
         $form    = array();
 
         if (strlen($folder_name)) {
             $path_imap = explode($delim, $folder_name);
             array_pop($path_imap);  // pop off name part
             $path_imap = implode($delim, $path_imap);
 
             $options = $storage->folder_info($folder_name);
         }
         else {
             $path_imap = '';
         }
 
         $hidden_fields[] = array('name' => 'oldname', 'value' => $folder_name);
 
         // folder name (default field)
         $input_name = new html_inputfield(array('name' => 'name', 'id' => 'taskedit-tasklistname', 'size' => 20));
         $fieldprop['name']['value'] = $input_name->show($list['editname'], array('disabled' => ($options['norename'] || $options['protected'])));
 
         // prevent user from moving folder
         if (!empty($options) && ($options['norename'] || $options['protected'])) {
             $hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
         }
         else {
             $select = kolab_storage::folder_selector('task', array('name' => 'parent', 'id' => 'taskedit-parentfolder'), $folder_name);
             $fieldprop['parent'] = array(
                 'id'    => 'taskedit-parentfolder',
                 'label' => $this->plugin->gettext('parentfolder'),
                 'value' => $select->show($path_imap),
             );
         }
 
         // General tab
         $form['properties'] = array(
             'name' => $this->rc->gettext('properties'),
             'fields' => array(),
         );
 
         foreach (array('name','parent','showalarms') as $f) {
             $form['properties']['fields'][$f] = $fieldprop[$f];
         }
 
         return kolab_utils::folder_form($form, $folder_name, 'tasklist', $hidden_fields);
     }
 }
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 32a9caa6..6d36149c 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -1,623 +1,623 @@
 <?php
 /**
  * User Interface class for the Tasklist plugin
  *
  * @version @package_version@
  * @author Thomas Bruederli <bruederli@kolabsys.com>
  *
  * Copyright (C) 2012-2015, Kolab Systems AG <contact@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/>.
  */
 
 #[AllowDynamicProperties]
 class tasklist_ui
 {
     private $rc;
     private $plugin;
     private $ready = false;
     private $gui_objects = [];
 
     function __construct($plugin)
     {
         $this->plugin = $plugin;
         $this->rc     = $plugin->rc;
     }
 
     /**
      * Calendar UI initialization and requests handlers
      */
     public function init()
     {
         if ($this->ready) {
             return;
         }
 
         // add taskbar button
         $this->plugin->add_button(array(
             'command'    => 'tasks',
             'class'      => 'button-tasklist',
             'classsel'   => 'button-tasklist button-selected',
             'innerclass' => 'button-inner',
             'label'      => 'tasklist.navtitle',
             'type'       => 'link'
         ), 'taskbar');
 
         $this->plugin->include_stylesheet($this->plugin->local_skin_path() . '/tasklist.css');
 
         if ($this->rc->task == 'mail' || $this->rc->task == 'tasks') {
             $this->plugin->include_script('tasklist_base.js');
 
             // copy config to client
             $this->rc->output->set_env('tasklist_settings', $this->load_settings());
 
             // initialize attendees autocompletion
             $this->rc->autocomplete_init();
         }
 
         $this->ready = true;
     }
 
     /**
      *
      */
     function load_settings()
     {
         $settings = [];
 
         $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0);
         $settings['itip_notify']   = (int)$this->rc->config->get('calendar_itip_send_option', 3);
         $settings['sort_col']      = $this->rc->config->get('tasklist_sort_col', '');
         $settings['sort_order']    = $this->rc->config->get('tasklist_sort_order', 'asc');
 
         // get user identity to create default attendee
         foreach ($this->rc->user->list_emails() as $rec) {
             if (empty($identity)) {
                 $identity = $rec;
             }
 
             $identity['emails'][] = $rec['email'];
             $settings['identities'][$rec['identity_id']] = $rec['email'];
         }
 
         $identity['emails'][] = $this->rc->user->get_username();
         $settings['identity'] = array(
             'name'   => $identity['name'],
             'email'  => strtolower($identity['email']),
             'emails' => ';' . strtolower(join(';', $identity['emails']))
         );
 
         if ($list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC)) {
             $settings['selected_list'] = $list;
         }
         if ($list && ($id = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC))) {
             $settings['selected_id'] = $id;
 
             // check if the referenced task is completed
             $task = $this->plugin->driver->get_task(array('id' => $id, 'list' => $list));
             if ($task && $this->plugin->driver->is_complete($task)) {
                 $settings['selected_filter'] = 'complete';
             }
         }
         else if ($filter = rcube_utils::get_input_value('_filter', rcube_utils::INPUT_GPC)) {
             $settings['selected_filter'] = $filter;
         }
 
         return $settings;
     }
 
     /**
      * Render a HTML select box for user identity selection
      */
     function identity_select($attrib = [])
     {
         $attrib['name'] = 'identity';
         $select         = new html_select($attrib);
         $identities     = $this->rc->user->list_emails();
 
         foreach ($identities as $ident) {
             $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
         }
 
         return $select->show(null);
     }
 
     /**
     * Register handler methods for the template engine
     */
     public function init_templates()
     {
         $this->plugin->register_handler('plugin.tasklists', array($this, 'tasklists'));
         $this->plugin->register_handler('plugin.tasklist_select', array($this, 'tasklist_select'));
         $this->plugin->register_handler('plugin.status_select', array($this, 'status_select'));
         $this->plugin->register_handler('plugin.searchform', array($this->rc->output, 'search_form'));
         $this->plugin->register_handler('plugin.quickaddform', array($this, 'quickadd_form'));
         $this->plugin->register_handler('plugin.tasks', array($this, 'tasks_resultview'));
         $this->plugin->register_handler('plugin.tags_editline', array($this, 'tags_editline'));
         $this->plugin->register_handler('plugin.alarm_select', array($this, 'alarm_select'));
         $this->plugin->register_handler('plugin.recurrence_form', array($this->plugin->lib, 'recurrence_form'));
         $this->plugin->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
         $this->plugin->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
         $this->plugin->register_handler('plugin.identity_select', array($this, 'identity_select'));
         $this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
         $this->plugin->register_handler('plugin.task_rsvp_buttons', array($this->plugin->itip, 'itip_rsvp_buttons'));
         $this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
         $this->plugin->register_handler('plugin.tasks_export_form', array($this, 'tasks_export_form'));
         $this->plugin->register_handler('plugin.tasks_import_form', array($this, 'tasks_import_form'));
 
         kolab_attachments_handler::ui();
 
         $this->plugin->include_script('tasklist.js');
         $this->plugin->api->include_script('libkolab/libkolab.js');
     }
 
     /**
      *
      */
     public function tasklists($attrib = [])
     {
         $tree  = true;
         $jsenv = [];
         $lists = $this->plugin->driver->get_lists(0, $tree);
 
         if (empty($attrib['id'])) {
             $attrib['id'] = 'rcmtasklistslist';
         }
 
         // 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'] = ($attrib['class'] ?? '') . ' flat';
             $html = '';
 
             foreach ((array) $lists as $id => $prop) {
                 if (!empty($attrib['activeonly']) && empty($prop['active'])) {
                     continue;
                 }
 
                 $html .= html::tag('li', [
                         'id' => 'rcmlitasklist' . rcube_utils::html_identifier($id),
                         'class' => $prop['group'] ?? null,
                     ],
                     $this->tasklist_list_item($id, $prop, $jsenv, !empty($attrib['activeonly']))
                 );
             }
         }
 
         $this->rc->output->include_script('treelist.js');
 
         $this->rc->output->set_env('source', rcube_utils::get_input_value('source', rcube_utils::INPUT_GET));
         $this->rc->output->set_env('tasklists', $jsenv);
         $this->register_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)
     {
         $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']);
+            $content = $this->tasklist_list_item($id, $prop, $jsenv, $attrib['activeonly'] ?? null);
 
             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);
             }
         }
 
         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 (empty($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;
             $prop['attendees']   = $this->plugin->driver->attendees;
             $prop['caldavurl']   = $this->plugin->driver->tasklist_caldav_url($prop);
             $jsenv[$id] = $prop;
         }
 
         $classes = array('tasklist');
         $title   = '';
 
         if (!empty($prop['title'])) {
             $title = $prop['title'];
         }
         else if (empty($prop['listname']) || $prop['name'] != $prop['listname'] || strlen($prop['name']) > 25) {
             html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET);
         }
 
         if (!empty($prop['virtual'])) {
             $classes[] = 'virtual';
         }
         else if (empty($prop['editable'])) {
             $classes[] = 'readonly';
         }
         if (!empty($prop['subscribed'])) {
             $classes[] = 'subscribed';
         }
         if (!empty($prop['class'])) {
             $classes[] = $prop['class'];
         }
 
         if (!$activeonly || $prop['active']) {
             $label_id = 'tl:' . $id;
             $chbox = html::tag('input', array(
                     'type'    => 'checkbox',
                     'name'    => '_list[]',
                     'value'   => $id,
                     'checked' => $prop['active'],
                     'title'   => $this->plugin->gettext('activate'),
                     'aria-labelledby' => $label_id
             ));
 
             $actions = '';
             if (!empty($prop['removable'])) {
                 $actions .= html::a(['href' => '#', 'class' => 'remove', 'title' => $this->plugin->gettext('removelist')], ' ');
             }
             $actions .= html::a(['href' => '#', 'class' => 'quickview', 'title' => $this->plugin->gettext('focusview'), 'role' => 'checkbox', 'aria-checked' => 'false'], ' ');
             if (isset($prop['subscribed'])) {
-                $action .= html::a(['href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'], ' ');
+                $actions .= html::a(['href' => '#', 'class' => 'subscribed', 'title' => $this->plugin->gettext('tasklistsubscribe'), 'role' => 'checkbox', 'aria-checked' => $prop['subscribed'] ? 'true' : 'false'], ' ');
             }
 
             return html::div(join(' ', $classes),
                 html::a(['class' => 'listname', 'title' => $title, 'href' => '#', 'id' => $label_id],
                     !empty($prop['listname']) ? $prop['listname'] : $prop['name'])
                     . (!empty($prop['virtual']) ? '' : $chbox . html::span('actions', $actions))
             );
         }
 
         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
      */
     function tasklist_select($attrib = array())
     {
         if (empty($attrib['name'])) {
             $attrib['name'] = 'list';
         }
 
         $attrib['is_escaped'] = true;
         $select = new html_select($attrib);
         $default = null;
 
         if (!empty($attrib['extra'])) {
             foreach ((array) $attrib['extra'] as $id => $name) {
                 $select->add($name, $id);
             }
         }
 
         foreach ((array) $this->plugin->driver->get_lists() as $id => $prop) {
             if (!empty($prop['editable']) || strpos($prop['rights'], 'i') !== false) {
                 $select->add($prop['name'], $id);
                 if (!$default || !empty($prop['default'])) {
                     $default = $id;
                 }
             }
         }
 
         return $select->show($default);
     }
 
     function tasklist_editform($action, $list = array())
     {
         $this->action = $action;
         $this->list   = $list;
 
         $this->rc->output->set_env('pagetitle', $this->plugin->gettext('arialabeltasklistform'));
         $this->rc->output->add_handler('folderform', array($this, 'tasklistform'));
         $this->rc->output->send('libkolab.folderform');
     }
 
     function tasklistform($attrib)
     {
         $fields = array(
             'name' => array(
                 'id'    => 'taskedit-tasklistname',
                 'label' => $this->plugin->gettext('listname'),
                 'value' => html::tag('input', array('id' => 'taskedit-tasklistname', 'name' => 'name', 'type' => 'text', 'class' => 'text', 'size' => 40)),
             ),
 /*
             'color' => array(
                 'id'    => 'taskedit-color',
                 'label' => $this->plugin->gettext('color'),
                 'value' => html::tag('input', array('id' => 'taskedit-color', 'name' => 'color', 'type' => 'text', 'class' => 'text colorpicker', 'size' => 6)),
             ),
 */
             'showalarms' => array(
                 'id'    => 'taskedit-showalarms',
                 'label' => $this->plugin->gettext('showalarms'),
                 'value' => html::tag('input', array('id' => 'taskedit-showalarms', 'name' => 'showalarms', 'type' => 'checkbox', 'value' => 1)),
             ),
         );
 
         return html::tag('form', $attrib + array('action' => "#", 'method' => "post", 'id' => 'tasklisteditform'),
             $this->plugin->driver->tasklist_edit_form($this->action, $this->list, $fields)
         );
     }
 
     /**
      * Render HTML form for alarm configuration
      */
     function alarm_select($attrib = array())
     {
         $attrib['_type'] = 'task';
         return $this->plugin->lib->alarm_select($attrib, $this->plugin->driver->alarm_types, $this->plugin->driver->alarm_absolute);
     }
 
     /**
      *
      */
     function quickadd_form($attrib)
     {
         $attrib += array('action' => $this->rc->url('add'), 'method' => 'post', 'id' => 'quickaddform');
 
         $label = html::label(array('for' => 'quickaddinput', 'class' => 'voice'), $this->plugin->gettext('quickaddinput'));
         $input = new html_inputfield(array('name' => 'text', 'id' => 'quickaddinput'));
         $button = html::tag('input', array(
                 'type'  => 'submit',
                 'value' => '+',
                 'title' => $this->plugin->gettext('createtask'),
                 'class' => 'button mainaction'
         ));
 
         $this->register_gui_object('quickaddform', $attrib['id']);
         return html::tag('form', $attrib, $label . $input->show() . $button);
     }
 
     /**
      * The result view
      */
     function tasks_resultview($attrib)
     {
         $attrib += array('id' => 'rcmtaskslist');
 
         $this->register_gui_object('resultlist', $attrib['id']);
 
         unset($attrib['name']);
         return html::tag('ul', $attrib, '');
     }
 
     /**
      * Interactive UI element to add/remove tags
      */
     function tags_editline($attrib)
     {
         $attrib += array('id' => 'rcmtasktagsedit');
         $this->register_gui_object('edittagline', $attrib['id']);
 
         $input = new html_inputfield(array(
                 'name' => 'tags[]',
                 'class' => 'tag',
                 'size' => !empty($attrib['size']) ? $attrib['size'] : null,
                 'tabindex' => isset($attrib['tabindex']) ? $attrib['tabindex'] : null,
         ));
         unset($attrib['tabindex']);
         return html::div($attrib, $input->show(''));
     }
 
     /**
      *
      */
     function attendees_list($attrib = array())
     {
         // add "noreply" checkbox to attendees table only
         $invitations = strpos($attrib['id'], 'attend') !== false;
 
         $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite'));
         $table  = new html_table(array('cols' => 4 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
 
 //      $table->add_header('role', $this->plugin->gettext('role'));
         $table->add_header('name', $this->plugin->gettext($attrib['coltitle'] ?: 'attendee'));
         $table->add_header('confirmstate', $this->plugin->gettext('confirmstate'));
         if ($invitations) {
             $table->add_header(array('class' => 'invite', 'title' => $this->plugin->gettext('sendinvitations')),
                 $invite->show(1) . html::label('edit-attendees-invite', html::span('inner', $this->plugin->gettext('sendinvitations'))));
         }
         $table->add_header('options', '');
 
         // hide invite column if disabled by config
         $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', 3);
         if ($invitations && !($itip_notify & 2)) {
             $css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']);
             $this->rc->output->add_footer(html::tag('style', array('type' => 'text/css'), $css));
         }
 
         return $table->show($attrib);
     }
 
     /**
      *
      */
     function attendees_form($attrib = array())
     {
         $input = new html_inputfield(array(
                 'name' => 'participant',
                 'id' => 'edit-attendee-name',
                 'size' => !empty($attrib['size']) ? $attrib['size'] : null,
                 'class' => 'form-control'
         ));
 
         $textarea = new html_textarea(array(
                 'name' => 'comment',
                 'id' => 'edit-attendees-comment',
                 'rows' => 4,
                 'cols' => 55,
                 'title' => $this->plugin->gettext('itipcommenttitle'),
                 'class' => 'form-control'
         ));
 
         return html::div($attrib,
             html::div('form-searchbar', $input->show() . " " .
                 html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->plugin->gettext('addattendee')))
                 // . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->plugin->gettext('scheduletime').'...'))
                 ) .
             html::p('attendees-commentbox', html::label('edit-attendees-comment', $this->plugin->gettext('itipcomment')) . $textarea->show())
         );
     }
 
     /**
      *
      */
     function edit_attendees_notify($attrib = array())
     {
         $checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1, 'class' => 'pretty-checkbox'));
         return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->plugin->gettext('sendnotifications')));
     }
 
     /**
      * Form for uploading and importing tasks
      */
     function tasks_import_form($attrib = array())
     {
         if (empty($attrib['id'])) {
             $attrib['id'] = 'rcmImportForm';
         }
 
         // Get max filesize, enable upload progress bar
         $max_filesize = $this->rc->upload_init();
         $accept       = '.ics, text/calendar, text/x-vcalendar, application/ics';
         if (class_exists('ZipArchive', false)) {
             $accept .= ', .zip, application/zip';
         }
 
         $input = new html_inputfield(array(
                 'id'     => 'importfile',
                 'type'   => 'file',
                 'name'   => '_data',
                 'size'   => !empty($attrib['uploadfieldsize']) ? $attrib['uploadfieldsize'] : null,
                 'accept' => $accept
         ));
 
         $html = html::div('form-section form-group row',
             html::label(array('class' => 'col-sm-4 col-form-label', 'for' => 'importfile'), rcube::Q($this->rc->gettext('importfromfile')))
             . html::div('col-sm-8', $input->show()
                 . html::div('hint', $this->rc->gettext(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize)))))
         );
 
         $html .= html::div('form-section form-group row',
             html::label(array('for' => 'task-import-list', 'class' => 'col-sm-4 col-form-label'), $this->plugin->gettext('list'))
             . html::div('col-sm-8', $this->tasklist_select(array('name' => 'source', 'id' => 'task-import-list', 'editable' => true)))
         );
 
         $this->rc->output->add_gui_object('importform', $attrib['id']);
         $this->rc->output->add_label('import', 'importerror');
 
         return html::tag('p', null, $this->plugin->gettext('importtext'))
             .html::tag('form', array(
                 'action'  => $this->rc->url(array('task' => 'tasklist', 'action' => 'import')),
                 'method'  => 'post',
                 'enctype' => 'multipart/form-data',
                 'id'      => $attrib['id'],
             ),
             $html
         );
     }
 
     /**
      * Form to select options for exporting tasks
      */
     function tasks_export_form($attrib = array())
     {
         if (empty($attrib['id'])) {
             $attrib['id'] = 'rcmTaskExportForm';
         }
 
         $html = html::div('form-section form-group row',
             html::label(array('for' => 'task-export-list', 'class' => 'col-sm-4 col-form-label'), $this->plugin->gettext('list'))
             . html::div('col-sm-8', $this->tasklist_select(array(
                         'name'  => 'source',
                         'id'    => 'task-export-list',
                         'extra' => array('' => '- ' . $this->plugin->gettext('currentview') . ' -'),
                 )))
         );
 
         $checkbox = new html_checkbox(array('name' => 'attachments', 'id' => 'task-export-attachments', 'value' => 1, 'class' => 'form-check-input pretty-checkbox'));
         $html .= html::div('form-section row form-check',
             html::label(array('for' => 'task-export-attachments', 'class' => 'col-sm-4 col-form-label'), $this->plugin->gettext('exportattachments'))
             . html::div('col-sm-8', $checkbox->show(1))
         );
 
         $this->register_gui_object('exportform', $attrib['id']);
 
         return html::tag('form', array(
                 'action' => $this->rc->url(array('task' => 'tasklist', 'action' => 'export')),
                 'method' => 'post', 'id' => $attrib['id']
             ),
             $html
         );
     }
 
     /**
      * Wrapper for rcube_output_html::add_gui_object()
      */
     function register_gui_object($name, $id)
     {
         $this->gui_objects[$name] = $id;
         $this->rc->output->add_gui_object($name, $id);
     }
 
     /**
      * Getter for registered gui objects.
      * (for manual registration when loading the inline UI)
      */
     function get_gui_objects()
     {
         return $this->gui_objects;
     }
 }