HOME


Mini Shell 1.0
DIR:/proc/thread-self/root/usr/local/cwpsrv/var/services/roundcube/plugins/tasklist/
Upload File :
Current File : //proc/thread-self/root/usr/local/cwpsrv/var/services/roundcube/plugins/tasklist/tasklist.php
<?php

/**
 * Tasks plugin for Roundcube webmail
 *
 * @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 tasklist extends rcube_plugin
{
    const FILTER_MASK_TODAY = 1;
    const FILTER_MASK_TOMORROW = 2;
    const FILTER_MASK_WEEK = 4;
    const FILTER_MASK_LATER = 8;
    const FILTER_MASK_NODATE = 16;
    const FILTER_MASK_OVERDUE = 32;
    const FILTER_MASK_FLAGGED = 64;
    const FILTER_MASK_COMPLETE = 128;
    const FILTER_MASK_ASSIGNED = 256;
    const FILTER_MASK_MYTASKS = 512;

    const SESSION_KEY = 'tasklist_temp';

    public static $filter_masks = array(
        'today'    => self::FILTER_MASK_TODAY,
        'tomorrow' => self::FILTER_MASK_TOMORROW,
        'week'     => self::FILTER_MASK_WEEK,
        'later'    => self::FILTER_MASK_LATER,
        'nodate'   => self::FILTER_MASK_NODATE,
        'overdue'  => self::FILTER_MASK_OVERDUE,
        'flagged'  => self::FILTER_MASK_FLAGGED,
        'complete' => self::FILTER_MASK_COMPLETE,
        'assigned' => self::FILTER_MASK_ASSIGNED,
        'mytasks'  => self::FILTER_MASK_MYTASKS,
    );

    public $task = '?(?!login|logout).*';
    public $allowed_prefs = array('tasklist_sort_col','tasklist_sort_order');

    public $rc;
    public $lib;
    public $timezone;
    public $ui;
    public $home;  // declare public to be used in other classes

    // These are handled by __get()
    // public $driver;
    // public $itip;
    // public $ical;

    private $collapsed_tasks = array();
    private $message_tasks   = array();
    private $task_titles     = array();


    /**
     * Plugin initialization.
     */
    function init()
    {
        $this->require_plugin('libcalendaring');
        $this->require_plugin('libkolab');

        $this->rc  = rcube::get_instance();
        $this->lib = libcalendaring::get_instance();

        $this->register_task('tasks', 'tasklist');

        // load plugin configuration
        $this->load_config();

        $this->timezone = $this->lib->timezone;

        // proceed initialization in startup hook
        $this->add_hook('startup', array($this, 'startup'));

        $this->add_hook('user_delete', array($this, 'user_delete'));
    }

    /**
     * Startup hook
     */
    public function startup($args)
    {
        // the tasks module can be enabled/disabled by the kolab_auth plugin
        if ($this->rc->config->get('tasklist_disabled', false) || !$this->rc->config->get('tasklist_enabled', true))
            return;

        // load localizations
        $this->add_texts('localization/', $args['task'] == 'tasks' && (!$args['action'] || $args['action'] == 'print'));
        $this->rc->load_language($_SESSION['language'], array('tasks.tasks' => $this->gettext('navtitle')));  // add label for task title

        if ($args['task'] == 'tasks' && $args['action'] != 'save-pref') {
            $this->load_driver();

            // register calendar actions
            $this->register_action('index', array($this, 'tasklist_view'));
            $this->register_action('task', array($this, 'task_action'));
            $this->register_action('tasklist', array($this, 'tasklist_action'));
            $this->register_action('counts', array($this, 'fetch_counts'));
            $this->register_action('fetch', array($this, 'fetch_tasks'));
            $this->register_action('print', array($this, 'print_tasks'));
            $this->register_action('dialog-ui', array($this, 'mail_message2task'));
            $this->register_action('get-attachment', array($this, 'attachment_get'));
            $this->register_action('upload', array($this, 'attachment_upload'));
            $this->register_action('import', array($this, 'import_tasks'));
            $this->register_action('export', array($this, 'export_tasks'));
            $this->register_action('mailimportitip', array($this, 'mail_import_itip'));
            $this->register_action('mailimportattach', array($this, 'mail_import_attachment'));
            $this->register_action('itip-status', array($this, 'task_itip_status'));
            $this->register_action('itip-remove', array($this, 'task_itip_remove'));
            $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
            $this->register_action('itip-delegate', array($this, 'mail_itip_delegate'));
            $this->add_hook('refresh', array($this, 'refresh'));

            $this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
        }
        else if ($args['task'] == 'mail') {
            if ($args['action'] == 'show' || $args['action'] == 'preview') {
                if ($this->rc->config->get('tasklist_mail_embed', true)) {
                    $this->add_hook('message_load', array($this, 'mail_message_load'));
                }
                $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
            }

            // add 'Create event' item to message menu
            if ($this->api->output->type == 'html' && (empty($_GET['_rel']) || $_GET['_rel'] != 'task')) {
                $this->api->add_content(html::tag('li', array('role' => 'menuitem'),
                    $this->api->output->button(array(
                        'command'  => 'tasklist-create-from-mail',
                        'label'    => 'tasklist.createfrommail',
                        'type'     => 'link',
                        'classact' => 'icon taskaddlink active',
                        'class'    => 'icon taskaddlink disabled',
                        'innerclass' => 'icon taskadd',
                    ))),
                'messagemenu');

                $this->api->output->add_label('tasklist.createfrommail');
            }
        }

        if (!$this->rc->output->ajax_call && empty($this->rc->output->env['framed'])) {
            $this->load_ui();
            $this->ui->init();
        }

        // add hooks for alarms handling
        $this->add_hook('pending_alarms', array($this, 'pending_alarms'));
        $this->add_hook('dismiss_alarms', array($this, 'dismiss_alarms'));
    }

    /**
     *
     */
    private function load_ui()
    {
        if (!$this->ui) {
            require_once($this->home . '/tasklist_ui.php');
            $this->ui = new tasklist_ui($this);
        }
    }

    /**
     * Helper method to load the backend driver according to local config
     */
    private function load_driver()
    {
        if (!empty($this->driver)) {
            return;
        }

        $driver_name  = $this->rc->config->get('tasklist_driver', 'database');
        $driver_class = 'tasklist_' . $driver_name . '_driver';

        require_once($this->home . '/drivers/tasklist_driver.php');
        require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');

        $this->driver = new $driver_class($this);

        $this->rc->output->set_env('tasklist_driver', $driver_name);
    }

    /**
     * Dispatcher for task-related actions initiated by the client
     */
    public function task_action()
    {
        $filter = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
        $action = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
        $rec    = rcube_utils::get_input_value('t', rcube_utils::INPUT_POST, true);
        $oldrec = $rec;
        $success = $refresh = $got_msg = false;

        // force notify if hidden + active
        $itip_send_option = (int)$this->rc->config->get('calendar_itip_send_option', 3);
        if ($itip_send_option === 1 && empty($rec['_reportpartstat'])) {
            $rec['_notify'] = 1;
        }

        switch ($action) {
        case 'new':
            $oldrec = null;
            $rec = $this->prepare_task($rec);
            $rec['uid'] = $this->generate_uid();
            $temp_id = !empty($rec['tempid']) ? $rec['tempid'] : null;
            if ($success = $this->driver->create_task($rec)) {
                $refresh = $this->driver->get_task($rec);
                if ($temp_id) $refresh['tempid'] = $temp_id;
                $this->cleanup_task($rec);
            }
            break;

        case 'complete':
            $complete = intval(rcube_utils::get_input_value('complete', rcube_utils::INPUT_POST));
            if (!($rec = $this->driver->get_task($rec))) {
                break;
            }

            $oldrec = $rec;
            $rec['status'] = $complete ? 'COMPLETED' : ($rec['complete'] > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION');

            // sent itip notifications if enabled (no user interaction here)
            if (($itip_send_option & 1)) {
                if ($this->is_attendee($rec)) {
                    $rec['_reportpartstat'] = $rec['status'];
                }
                else if ($this->is_organizer($rec)) {
                    $rec['_notify'] = 1;
                }
            }

        case 'edit':
            $oldrec = $this->driver->get_task($rec);
            $rec = $this->prepare_task($rec);
            $clone = $this->handle_recurrence($rec, $this->driver->get_task($rec));
            if ($success = $this->driver->edit_task($rec)) {
                $new_task = $this->driver->get_task($rec);
                $new_task['tempid'] = $rec['id'];
                $refresh[] = $new_task;
                $this->cleanup_task($rec);

                // add clone from recurring task
                if ($clone && $this->driver->create_task($clone)) {
                    $new_clone = $this->driver->get_task($clone);
                    $new_clone['tempid'] = $clone['id'];
                    $refresh[] = $new_clone;
                    $this->driver->clear_alarms($rec['id']);
                }

                // move all childs if list assignment was changed
                if (!empty($rec['_fromlist']) && !empty($rec['list']) && $rec['_fromlist'] != $rec['list']) {
                    foreach ($this->driver->get_childs(array('id' => $rec['id'], 'list' => $rec['_fromlist']), true) as $cid) {
                        $child = array('id' => $cid, 'list' => $rec['list'], '_fromlist' => $rec['_fromlist']);
                        if ($this->driver->move_task($child)) {
                            $r = $this->driver->get_task($child);
                            if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) {
                                $r['tempid'] = $cid;
                                $refresh[] = $r;
                            }
                        }
                    }
                }
            }
            break;

          case 'move':
              foreach ((array)$rec['id'] as $id) {
                  $r = $rec;
                  $r['id'] = $id;
                  if ($this->driver->move_task($r)) {
                      $new_task = $this->driver->get_task($r);
                      $new_task['tempid'] = $id;
                      $refresh[] = $new_task;
                      $success = true;

                      // move all childs, too
                      foreach ($this->driver->get_childs(array('id' => $id, 'list' => $rec['_fromlist']), true) as $cid) {
                          $child = $rec;
                          $child['id'] = $cid;
                          if ($this->driver->move_task($child)) {
                              $r = $this->driver->get_task($child);
                              if ((bool)($filter & self::FILTER_MASK_COMPLETE) == $this->driver->is_complete($r)) {
                                  $r['tempid'] = $cid;
                                  $refresh[] = $r;
                              }
                          }
                      }
                  }
              }
              break;

        case 'delete':
            $mode  = intval(rcube_utils::get_input_value('mode', rcube_utils::INPUT_POST));
            $oldrec = $this->driver->get_task($rec);
            if ($success = $this->driver->delete_task($rec, false)) {
                // delete/modify all childs
                foreach ($this->driver->get_childs($rec, $mode) as $cid) {
                    $child = array('id' => $cid, 'list' => $rec['list']);

                    if ($mode == 1) {  // delete all childs
                        if ($this->driver->delete_task($child, false)) {
                            if ($this->driver->undelete)
                                $_SESSION['tasklist_undelete'][$rec['id']][] = $cid;
                        }
                        else
                            $success = false;
                    }
                    else {
                        $child['parent_id'] = strval($oldrec['parent_id']);
                        $this->driver->edit_task($child);
                    }
                }
                // update parent task to adjust list of children
                if (!empty($oldrec['parent_id'])) {
                    $parent = array('id' => $oldrec['parent_id'], 'list' => $rec['list']);
                    if ($parent = $this->driver->get_task()) {
                        $refresh[] = $parent;
                    }
                }
            }

            if (!$success)
                $this->rc->output->command('plugin.reload_data');
            break;

        case 'undelete':
            if ($success = $this->driver->undelete_task($rec)) {
                $refresh[] = $this->driver->get_task($rec);
                foreach ((array)$_SESSION['tasklist_undelete'][$rec['id']] as $cid) {
                    if ($this->driver->undelete_task($rec)) {
                        $refresh[] = $this->driver->get_task($rec);
                    }
                }
            }
            break;

        case 'collapse':
            foreach (explode(',', $rec['id']) as $rec_id) {
                if (intval(rcube_utils::get_input_value('collapsed', rcube_utils::INPUT_GPC))) {
                    $this->collapsed_tasks[] = $rec_id;
                }
                else {
                    $i = array_search($rec_id, $this->collapsed_tasks);
                    if ($i !== false)
                        unset($this->collapsed_tasks[$i]);
                }
            }

            $this->rc->user->save_prefs(array('tasklist_collapsed_tasks' => join(',', array_unique($this->collapsed_tasks))));
            return;  // avoid further actions

        case 'rsvp':
            $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_GPC);
            $noreply = intval(rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC)) || $status == 'needs-action';
            $task = $this->driver->get_task($rec);
            $task['attendees'] = $rec['attendees'];
            $task['_type'] = 'task';

            // send invitation to delegatee + add it as attendee
            if ($status == 'delegated' && $rec['to']) {
                $itip = $this->load_itip();
                if ($itip->delegate_to($task, $rec['to'], (bool)$rec['rsvp'])) {
                    $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
                    $refresh[] = $task;
                    $noreply = false;
                }
            }

            $rec = $task;

            if ($success = $this->driver->edit_task($rec)) {
                if (!$noreply) {
                    // let the reply clause further down send the iTip message
                    $rec['_reportpartstat'] = $status;
                }
            }
            break;

        case 'changelog':
            $data = $this->driver->get_task_changelog($rec);
            if (is_array($data) && !empty($data)) {
                $lib = $this->lib;
                $dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
                array_walk($data, function(&$change) use ($lib, $dtformat) {
                  if ($change['date']) {
                      $dt = $lib->adjust_timezone($change['date']);
                      if ($dt instanceof DateTime) {
                          $change['date'] = $this->rc->format_date($dt, $dtformat, false);
                      }
                  }
                });
                $this->rc->output->command('plugin.task_render_changelog', $data);
            }
            else {
                $this->rc->output->command('plugin.task_render_changelog', false);
            }
            $got_msg = true;
            break;

        case 'diff':
            $data = $this->driver->get_task_diff($rec, $rec['rev1'], $rec['rev2']);
            if (is_array($data)) {
                // convert some properties, similar to self::_client_event()
                $lib = $this->lib;
                $date_format = $this->rc->config->get('date_format', 'Y-m-d');
                $time_format = $this->rc->config->get('time_format', 'H:i');
                array_walk($data['changes'], function(&$change, $i) use ($lib, $date_format, $time_format) {
                    // convert date cols
                    if (in_array($change['property'], array('date','start','created','changed'))) {
                        if (!empty($change['old'])) {
                            $dtformat = strlen($change['old']) == 10 ? $date_format : $date_format . ' ' . $time_format;
                            $change['old_'] = $lib->adjust_timezone($change['old'], strlen($change['old']) == 10)->format($dtformat);
                        }
                        if (!empty($change['new'])) {
                            $dtformat = strlen($change['new']) == 10 ? $date_format : $date_format . ' ' . $time_format;
                            $change['new_'] = $lib->adjust_timezone($change['new'], strlen($change['new']) == 10)->format($dtformat);
                        }
                    }
                    // create textual representation for alarms and recurrence
                    if ($change['property'] == 'alarms') {
                        if (is_array($change['old']))
                            $change['old_'] = libcalendaring::alarm_text($change['old']);
                        if (is_array($change['new']))
                            $change['new_'] = libcalendaring::alarm_text(array_merge((array)$change['old'], $change['new']));
                    }
                    if ($change['property'] == 'recurrence') {
                        if (is_array($change['old']))
                            $change['old_'] = $lib->recurrence_text($change['old']);
                        if (is_array($change['new']))
                            $change['new_'] = $lib->recurrence_text(array_merge((array)$change['old'], $change['new']));
                    }
                    if ($change['property'] == 'complete') {
                        $change['old_'] = intval($change['old']) . '%';
                        $change['new_'] = intval($change['new']) . '%';
                    }
                    if ($change['property'] == 'attachments') {
                        if (is_array($change['old']))
                            $change['old']['classname'] = rcube_utils::file2class($change['old']['mimetype'], $change['old']['name']);
                        if (is_array($change['new'])) {
                            $change['new'] = array_merge((array)$change['old'], $change['new']);
                            $change['new']['classname'] = rcube_utils::file2class($change['new']['mimetype'], $change['new']['name']);
                        }
                    }
                    // resolve parent_id to the refered task title for display
                    if ($change['property'] == 'parent_id') {
                        $change['property'] = 'parent-title';
                        if (!empty($change['old']) && ($old_parent = $this->driver->get_task(array('id' => $change['old'], 'list' => $rec['list'])))) {
                            $change['old_'] = $old_parent['title'];
                        }
                        if (!empty($change['new']) && ($new_parent = $this->driver->get_task(array('id' => $change['new'], 'list' => $rec['list'])))) {
                            $change['new_'] = $new_parent['title'];
                        }
                    }
                    // compute a nice diff of description texts
                    if ($change['property'] == 'description') {
                        $change['diff_'] = libkolab::html_diff($change['old'], $change['new']);
                    }
                });
                $this->rc->output->command('plugin.task_show_diff', $data);
            }
            else {
                $this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
            }
            $got_msg = true;
            break;

        case 'show':
            if ($rec = $this->driver->get_task_revison($rec, $rec['rev'])) {
                $this->encode_task($rec);
                $rec['readonly'] = 1;
                $this->rc->output->command('plugin.task_show_revision', $rec);
            }
            else {
                $this->rc->output->command('display_message', $this->gettext('objectnotfound'), 'error');
            }
            $got_msg = true;
            break;

        case 'restore':
            if ($success = $this->driver->restore_task_revision($rec, $rec['rev'])) {
                $refresh = $this->driver->get_task($rec);
                $this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rec['rev']))), 'confirmation');
                $this->rc->output->command('plugin.close_history_dialog');
            }
            else {
                $this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
            }
            $got_msg = true;
            break;

        }

        if ($success) {
            $this->rc->output->show_message('successfullysaved', 'confirmation');
            $this->update_counts($oldrec, $refresh);
        }
        else if (!$got_msg) {
            $this->rc->output->show_message('tasklist.errorsaving', 'error');
        }

        // send out notifications
        if ($success && !empty($rec['_notify']) && ($rec['attendees'] || $oldrec['attendees'])) {
            // make sure we have the complete record
            $task = $action == 'delete' ? $oldrec : $this->driver->get_task($rec);

            // only notify if data really changed (TODO: do diff check on client already)
            if (!$oldrec || $action == 'delete' || self::task_diff($task, $oldrec)) {
                $sent = $this->notify_attendees($task, $oldrec, $action, $rec['_comment']);
                if ($sent > 0)
                    $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
                else if ($sent < 0)
                    $this->rc->output->show_message('tasklist.errornotifying', 'error');
            }
        }

        if ($success && !empty($rec['_reportpartstat']) && $rec['_reportpartstat'] != 'NEEDS-ACTION') {
            // get the full record after update
            if (!$task) {
                $task = $this->driver->get_task($rec);
            }

            // send iTip REPLY with the updated partstat
            if ($task['organizer'] && ($idx = $this->is_attendee($task)) !== false) {
                $sender = $task['attendees'][$idx];
                $status = strtolower($sender['status']);

                if (!empty($_POST['comment']))
                    $task['comment'] = rcube_utils::get_input_value('comment', rcube_utils::INPUT_POST);

                $itip = $this->load_itip();
                $itip->set_sender_email($sender['email']);

                if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $task['organizer'], 'itipsubject' . $status, 'itipmailbody' . $status))
                    $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $task['organizer']['name'] ?: $task['organizer']['email']))), 'confirmation');
                else
                    $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
            }
        }

        // unlock client
        $this->rc->output->command('plugin.unlock_saving', $success);

        if ($refresh) {
            if (!empty($refresh['id'])) {
                $this->encode_task($refresh);
            }
            else if (is_array($refresh)) {
                foreach ($refresh as $i => $r)
                    $this->encode_task($refresh[$i]);
            }
            $this->rc->output->command('plugin.update_task', $refresh);
        }
        else if ($success && ($action == 'delete' || $action == 'undelete')) {
            $this->rc->output->command('plugin.refresh_tagcloud');
        }
    }

    /**
     * Load iTIP functions
     */
    private function load_itip()
    {
        if (empty($this->itip)) {
            require_once __DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php';
            $this->itip = new libcalendaring_itip($this, 'tasklist');
            $this->itip->set_rsvp_actions(array('accepted','declined','delegated'));
            $this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
        }

        return $this->itip;
    }

    /**
     * repares new/edited task properties before save
     */
    private function prepare_task($rec)
    {
        // try to be smart and extract date from raw input
        if (!empty($rec['raw'])) {
            foreach (array('today','tomorrow','sunday','monday','tuesday','wednesday','thursday','friday','saturday','sun','mon','tue','wed','thu','fri','sat') as $word) {
                $locwords[] = '/^' . preg_quote(mb_strtolower($this->gettext($word))) . '\b/i';
                $normwords[] = $word;
                $datewords[] = $word;
            }
            foreach (array('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','now','dec') as $month) {
                $locwords[] = '/(' . preg_quote(mb_strtolower($this->gettext('long'.$month))) . '|' . preg_quote(mb_strtolower($this->gettext($month))) . ')\b/i';
                $normwords[] = $month;
                $datewords[] = $month;
            }
            foreach (array('on','this','next','at') as $word) {
                $fillwords[] = preg_quote(mb_strtolower($this->gettext($word)));
                $fillwords[] = $word;
            }

            $raw = trim($rec['raw']);
            $date_str = '';

            // translate localized keywords
            $raw = preg_replace('/^(' . join('|', $fillwords) . ')\s*/i', '', $raw);
            $raw = preg_replace($locwords, $normwords, $raw);

            // find date pattern
            $date_pattern = '!^(\d+[./-]\s*)?((?:\d+[./-])|' . join('|', $datewords) . ')\.?(\s+\d{4})?[:;,]?\s+!i';
            if (preg_match($date_pattern, $raw, $m)) {
                $date_str .= $m[1] . $m[2] . $m[3];
                $raw = preg_replace(array($date_pattern, '/^(' . join('|', $fillwords) . ')\s*/i'), '', $raw);
                // add year to date string
                if ($m[1] && !$m[3])
                    $date_str .= date('Y');
            }

            // find time pattern
            $time_pattern = '/^(\d+([:.]\d+)?(\s*[hapm.]+)?),?\s+/i';
            if (preg_match($time_pattern, $raw, $m)) {
                $has_time = true;
                $date_str .= ($date_str ? ' ' : 'today ') . $m[1];
                $raw = preg_replace($time_pattern, '', $raw);
            }

            // yes, raw input matched a (valid) date
            if (strlen($date_str) && strtotime($date_str) && ($date = new DateTime($date_str, $this->timezone))) {
                $rec['date'] = $date->format('Y-m-d');
                if ($has_time)
                    $rec['time'] = $date->format('H:i');
                $rec['title'] = $raw;
            }
            else
                $rec['title'] = $rec['raw'];
        }

        // normalize input from client
        if (isset($rec['complete'])) {
            $rec['complete'] = floatval($rec['complete']);
            if ($rec['complete'] > 1)
                $rec['complete'] /= 100;
        }
        if (isset($rec['flagged']))
            $rec['flagged'] = intval($rec['flagged']);

        // fix for garbage input
        if ($rec['description'] == 'null')
            $rec['description'] = '';

        foreach ($rec as $key => $val) {
            if ($val === 'null')
                $rec[$key] = null;
        }

        if (!empty($rec['date'])) {
            $this->normalize_dates($rec, 'date', 'time');
        }

        if (!empty($rec['startdate'])) {
            $this->normalize_dates($rec, 'startdate', 'starttime');
        }

        // convert tags to array, filter out empty entries
        if (isset($rec['tags']) && !is_array($rec['tags'])) {
            $rec['tags'] = array_filter((array)$rec['tags']);
        }

        // convert the submitted alarm values
        if (!empty($rec['valarms'])) {
            $valarms = array();
            foreach (libcalendaring::from_client_alarms($rec['valarms']) as $alarm) {
                // alarms can only work with a date (either task start, due or absolute alarm date)
                if (is_a($alarm['trigger'], 'DateTime') || $rec['date'] || $rec['startdate'])
                    $valarms[] = $alarm;
            }
            $rec['valarms'] = $valarms;
        }

        // convert the submitted recurrence settings
        if (is_array($rec['recurrence'])) {
            $refdate = null;
            if (!empty($rec['date'])) {
                $refdate = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
            }
            else if (!empty($rec['startdate'])) {
                $refdate = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
            }

            if ($refdate) {
                $rec['recurrence'] = $this->lib->from_client_recurrence($rec['recurrence'], $refdate);

                // translate count into an absolute end date.
                // why? because when shifting completed tasks to the next recurrence,
                // the initial start date to count from gets lost.
                if (!empty($rec['recurrence']['COUNT'])) {
                    $engine = libcalendaring::get_recurrence();
                    $engine->init($rec['recurrence'], $refdate);
                    if ($until = $engine->end()) {
                        $rec['recurrence']['UNTIL'] = $until;
                        unset($rec['recurrence']['COUNT']);
                    }
                }
            }
            else {  // recurrence requires a reference date
                $rec['recurrence'] = '';
            }
        }

        $attachments = array();
        $taskid = $rec['id'];
        if (!empty($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $taskid) {
            if (!empty($_SESSION[self::SESSION_KEY]['attachments'])) {
                foreach ($_SESSION[self::SESSION_KEY]['attachments'] as $id => $attachment) {
                    if (is_array($rec['attachments']) && in_array($id, $rec['attachments'])) {
                        $attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
                        unset($attachments[$id]['abort'], $attachments[$id]['group']);
                    }
                }
            }
        }

        $rec['attachments'] = $attachments;

        // convert link references into simple URIs
        if (array_key_exists('links', $rec)) {
            $rec['links'] = array_map(function($link) { return is_array($link) ? $link['uri'] : strval($link); }, (array)$rec['links']);
        }

        // convert invalid data
        if (isset($rec['attendees']) && !is_array($rec['attendees'])) {
            $rec['attendees'] = array();
        }

        if (!empty($rec['attendees'])) {
            foreach ((array) $rec['attendees'] as $i => $attendee) {
                if (is_string($attendee['rsvp'])) {
                    $rec['attendees'][$i]['rsvp'] = $attendee['rsvp'] == 'true' || $attendee['rsvp'] == '1';
                }
            }
        }

        // copy the task status to my attendee partstat
        if (!empty($rec['_reportpartstat'])) {
            if (($idx = $this->is_attendee($rec)) !== false) {
                if (!($rec['_reportpartstat'] == 'NEEDS-ACTION' && $rec['attendees'][$idx]['status'] == 'ACCEPTED'))
                    $rec['attendees'][$idx]['status'] = $rec['_reportpartstat'];
                else
                    unset($rec['_reportpartstat']);
            }
        }

        // set organizer from identity selector
        if ((isset($rec['_identity']) || (!empty($rec['attendees']) && empty($rec['organizer']))) &&
                ($identity = $this->rc->user->get_identity($rec['_identity']))) {
            $rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']);
        }

        if (is_numeric($rec['id']) && $rec['id'] < 0)
            unset($rec['id']);

        return $rec;
    }

    /**
     * Utility method to convert a tasks date/time values into a normalized format
     */
    private function normalize_dates(&$rec, $date_key, $time_key)
    {
        try {
            // parse date from user format (#2801)
            $date_format = $this->rc->config->get(empty($rec[$time_key]) ? 'date_format' : 'date_long', 'Y-m-d');
            $date = DateTime::createFromFormat($date_format, trim($rec[$date_key] . ' ' . $rec[$time_key]), $this->timezone);

            // fall back to default strtotime logic
            if (empty($date)) {
                $date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
            }

            $rec[$date_key] = $date->format('Y-m-d');
            if (!empty($rec[$time_key]))
                $rec[$time_key] = $date->format('H:i');

            return true;
        }
        catch (Exception $e) {
            $rec[$date_key] = $rec[$time_key] = null;
        }

        return false;
    }

    /**
     * Releases some resources after successful save
     */
    private function cleanup_task(&$rec)
    {
        // remove temp. attachment files
        if (!empty($_SESSION[self::SESSION_KEY]) && ($taskid = $_SESSION[self::SESSION_KEY]['id'])) {
            $this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $taskid));
            $this->rc->session->remove(self::SESSION_KEY);
        }
    }

    /**
     * When flagging a recurring task as complete,
     * clone it and shift dates to the next occurrence
     */
    private function handle_recurrence(&$rec, $old)
    {
        $clone = null;
        if ($this->driver->is_complete($rec) && $old && !$this->driver->is_complete($old) && is_array($rec['recurrence'])) {
            $engine = libcalendaring::get_recurrence();
            $rrule = $rec['recurrence'];
            $updates = array();

            // compute the next occurrence of date attributes
            foreach (array('date'=>'time', 'startdate'=>'starttime') as $date_key => $time_key) {
                if (empty($rec[$date_key]))
                    continue;

                $date = new DateTime($rec[$date_key] . ' ' . $rec[$time_key], $this->timezone);
                $engine->init($rrule, $date);
                if ($next = $engine->next()) {
                    $updates[$date_key] = $next->format('Y-m-d');
                    if (!empty($rec[$time_key]))
                        $updates[$time_key] = $next->format('H:i');
                }
            }

            // shift absolute alarm dates
            if (!empty($updates) && is_array($rec['valarms'])) {
                $updates['valarms'] = array();
                unset($rrule['UNTIL'], $rrule['COUNT']);  // make recurrence rule unlimited

                foreach ($rec['valarms'] as $i => $alarm) {
                    if ($alarm['trigger'] instanceof DateTime) {
                        $engine->init($rrule, $alarm['trigger']);
                        if ($next = $engine->next()) {
                            $alarm['trigger'] = $next;
                        }
                    }
                    $updates['valarms'][$i] = $alarm;
                }
            }

            if (!empty($updates)) {
                // clone task to save a completed copy
                $clone = $rec;
                $clone['uid'] = $this->generate_uid();
                $clone['parent_id'] = $rec['id'];
                unset($clone['id'], $clone['recurrence'], $clone['attachments']);

                // update the task but unset completed flag
                $rec = array_merge($rec, $updates);
                $rec['complete'] = $old['complete'];
                $rec['status'] = $old['status'];
            }
        }

        return $clone;
    }

    /**
     * Send out an invitation/notification to all task attendees
     */
    private function notify_attendees($task, $old, $action = 'edit', $comment = null)
    {
        if ($action == 'delete' || ($task['status'] == 'CANCELLED' && $old['status'] != $task['status'])) {
            $task['cancelled'] = true;
            $is_cancelled      = true;
        }

        $itip   = $this->load_itip();
        $emails = $this->lib->get_user_emails();
        $itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', 3);

        // add comment to the iTip attachment
        $task['comment'] = $comment;

        // needed to generate VTODO instead of VEVENT entry
        $task['_type'] = 'task';

        // compose multipart message using PEAR:Mail_Mime
        $method  = $action == 'delete' ? 'CANCEL' : 'REQUEST';
        $object = $this->to_libcal($task);
        $message = $itip->compose_itip_message($object, $method, $task['sequence'] > $old['sequence']);

        // list existing attendees from the $old task
        $old_attendees = array();
        foreach ((array)$old['attendees'] as $attendee) {
            $old_attendees[] = $attendee['email'];
        }

        // send to every attendee
        $sent = 0; $current = array();
        foreach ((array)$task['attendees'] as $attendee) {
            $current[] = strtolower($attendee['email']);

            // skip myself for obvious reasons
            if (!$attendee['email'] || in_array(strtolower($attendee['email']), $emails)) {
                continue;
            }

            // skip if notification is disabled for this attendee
            if ($attendee['noreply'] && $itip_notify & 2) {
                continue;
            }

            // skip if this attendee has delegated and set RSVP=FALSE
            if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) {
                continue;
            }

            // which template to use for mail text
            $is_new   = !in_array($attendee['email'], $old_attendees);
            $is_rsvp  = $is_new || $task['sequence'] > $old['sequence'];
            $bodytext = $is_cancelled ? 'itipcancelmailbody' : ($is_new ? 'invitationmailbody' : 'itipupdatemailbody');
            $subject  = $is_cancelled ? 'itipcancelsubject'  : ($is_new ? 'invitationsubject' : ($task['title'] ? 'itipupdatesubject' : 'itipupdatesubjectempty'));

            // finally send the message
            if ($itip->send_itip_message($object, $method, $attendee, $subject, $bodytext, $message, $is_rsvp))
                $sent++;
            else
                $sent = -100;
        }

        // send CANCEL message to removed attendees
        foreach ((array)$old['attendees'] as $attendee) {
            if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
                continue;
            }

            $vtodo = $this->to_libcal($old);
            $vtodo['cancelled'] = $is_cancelled;
            $vtodo['attendees'] = array($attendee);
            $vtodo['comment']   = $comment;

            if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody'))
                $sent++;
            else
                $sent = -100;
        }

        return $sent;
    }

    /**
     * Compare two task objects and return differing properties
     *
     * @param array Event A
     * @param array Event B
     * @return array List of differing task properties
     */
    public static function task_diff($a, $b)
    {
        $diff   = array();
        $ignore = array('changed' => 1, 'attachments' => 1);

        foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
            if (!$ignore[$key] && $a[$key] != $b[$key])
                $diff[] = $key;
        }

        // only compare number of attachments
        if (count($a['attachments']) != count($b['attachments']))
            $diff[] = 'attachments';

        return $diff;
    }

    /**
     * Dispatcher for tasklist actions initiated by the client
     */
    public function tasklist_action()
    {
        $action  = rcube_utils::get_input_value('action', rcube_utils::INPUT_GPC);
        $list    = rcube_utils::get_input_value('l', rcube_utils::INPUT_GPC, true);
        $success = false;

        unset($list['_token']);

        if (isset($list['showalarms'])) {
            $list['showalarms'] = intval($list['showalarms']);
        }

        switch ($action) {
        case 'form-new':
        case 'form-edit':
            $this->load_ui();
            echo $this->ui->tasklist_editform($action, $list);
            exit;

        case 'new':
            $list += array('showalarms' => true, 'active' => true, 'editable' => true);
            if ($insert_id = $this->driver->create_list($list)) {
                $list['id'] = $insert_id;
                if (empty($list['_reload'])) {
                    $this->load_ui();
                    $list['html'] = $this->ui->tasklist_list_item($insert_id, $list, $jsenv);
                    $list += (array)$jsenv[$insert_id];
                }
                $this->rc->output->command('plugin.insert_tasklist', $list);
                $success = true;
            }
            break;

        case 'edit':
            if ($newid = $this->driver->edit_list($list)) {
                $list['oldid'] = $list['id'];
                $list['id'] = $newid;
                $this->rc->output->command('plugin.update_tasklist', $list);
                $success = true;
            }
            break;

        case 'subscribe':
            $success = $this->driver->subscribe_list($list);
            break;

        case 'delete':
            if (($success = $this->driver->delete_list($list)))
                $this->rc->output->command('plugin.destroy_tasklist', $list);
            break;

        case 'search':
            $this->load_ui();
            $results = array();
            $query   = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
            $source  = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);

            foreach ((array)$this->driver->search_lists($query, $source) as $id => $prop) {
                $editname = $prop['editname'];
                unset($prop['editname']);  // force full name to be displayed
                $prop['active'] = false;

                // let the UI generate HTML and CSS representation for this calendar
                $html = $this->ui->tasklist_list_item($id, $prop, $jsenv);
                $prop += (array)$jsenv[$id];
                $prop['editname'] = $editname;
                $prop['html'] = $html;

                $results[] = $prop;
            }
            // report more results available
            if (!empty($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;
        }

        if ($success) {
            $this->rc->output->show_message('successfullysaved', 'confirmation');
        }
        else {
            $this->rc->output->show_message('tasklist.errorsaving', 'error');
        }

        $this->rc->output->command('plugin.unlock_saving');
    }

    /**
     * Get counts for active tasks divided into different selectors
     */
    public function fetch_counts()
    {
        if (isset($_REQUEST['lists'])) {
            $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
        }
        else {
            foreach ($this->driver->get_lists() as $list) {
                if (!empty($list['active'])) {
                    $lists[] = $list['id'];
                }
            }
        }
        $counts = $this->driver->count_tasks($lists);
        $this->rc->output->command('plugin.update_counts', $counts);
    }

    /**
     * Adjust the cached counts after changing a task
     */
    public function update_counts($oldrec, $newrec)
    {
        // rebuild counts until this function is finally implemented
        $this->fetch_counts();

        // $this->rc->output->command('plugin.update_counts', $counts);
    }

    /**
     *
     */
    public function fetch_tasks()
    {
        $mask   = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
        $search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
        $lists  = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
        $filter = array('mask' => $mask, 'search' => $search);

        $data = $this->tasks_data($this->driver->list_tasks($filter, $lists));

        $this->rc->output->command('plugin.data_ready', array(
                'filter' => $mask,
                'lists'  => $lists,
                'search' => $search,
                'data'   => $data,
                'tags'   => $this->driver->get_tags(),
        ));
    }

    /**
     * Handler for printing calendars
     */
    public function print_tasks()
    {
        // Add CSS stylesheets to the page header
        $skin_path = $this->local_skin_path();

        $this->include_stylesheet($skin_path . '/print.css');
        $this->include_script('tasklist.js');

        $this->rc->output->add_handlers(array(
            'plugin.tasklist_print' => array($this, 'print_tasks_list'),
        ));

        $this->rc->output->set_pagetitle($this->gettext('print'));
        $this->rc->output->send('tasklist.print');
    }

    /**
     * Handler for printing calendars
     */
    public function print_tasks_list($attrib)
    {
        $mask   = intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC));
        $search = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
        $lists  = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);
        $filter = array('mask' => $mask, 'search' => $search);

        $data = $this->tasks_data($this->driver->list_tasks($filter, $lists));

        // we'll build the tasks table in javascript on page load
        // where we have sorting methods, etc.
        $this->rc->output->set_env('tasks', $data);
        $this->rc->output->set_env('filtermask', $mask);

        return $this->ui->tasks_resultview($attrib);
    }

    /**
     * Prepare and sort the given task records to be sent to the client
     */
    private function tasks_data($records)
    {
        $data = $this->task_tree = $this->task_titles = array();

        foreach ($records as $rec) {
            if (!empty($rec['parent_id'])) {
                $this->task_tree[$rec['id']] = $rec['parent_id'];
            }

            $this->encode_task($rec);

            $data[] = $rec;
        }

        // assign hierarchy level indicators for later sorting
        array_walk($data, array($this, 'task_walk_tree'));

        return $data;
    }

    /**
     * Prepare the given task record before sending it to the client
     */
    private function encode_task(&$rec)
    {
        $rec['mask'] = $this->filter_mask($rec);
        $rec['flagged'] = intval($rec['flagged']);
        $rec['complete'] = floatval($rec['complete']);

        if (is_object($rec['created'])) {
            $rec['created_'] = $this->rc->format_date($rec['created']);
            $rec['created'] = $rec['created']->format('U');
        }
        if (is_object($rec['changed'])) {
            $rec['changed_'] = $this->rc->format_date($rec['changed']);
            $rec['changed'] = $rec['changed']->format('U');
        }
        else {
            $rec['changed'] = null;
        }

        if ($rec['date']) {
            try {
                $date = new DateTime($rec['date'] . ' ' . $rec['time'], $this->timezone);
                $rec['datetime'] = intval($date->format('U'));
                $rec['date'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
                $rec['_hasdate'] = 1;
            }
            catch (Exception $e) {
                $rec['date'] = $rec['datetime'] = null;
            }
        }
        else {
            $rec['date'] = $rec['datetime'] = null;
            $rec['_hasdate'] = 0;
        }

        if ($rec['startdate']) {
            try {
                $date = new DateTime($rec['startdate'] . ' ' . $rec['starttime'], $this->timezone);
                $rec['startdatetime'] = intval($date->format('U'));
                $rec['startdate'] = $date->format($this->rc->config->get('date_format', 'Y-m-d'));
            }
            catch (Exception $e) {
                $rec['startdate'] = $rec['startdatetime'] = null;
            }
        }

        if (!empty($rec['valarms'])) {
            $rec['alarms_text'] = libcalendaring::alarms_text($rec['valarms']);
            $rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']);
        }

        if (!empty($rec['recurrence'])) {
            $rec['recurrence_text'] = $this->lib->recurrence_text($rec['recurrence']);
            $rec['recurrence'] = $this->lib->to_client_recurrence($rec['recurrence'], $rec['time'] || $rec['starttime']);
        }

        if (!empty($rec['attachments'])) {
            foreach ((array) $rec['attachments'] as $k => $attachment) {
                $rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);
            }
        }

        // convert link URIs references into structs
        if (array_key_exists('links', $rec)) {
            foreach ((array) $rec['links'] as $i => $link) {
                if (strpos($link, 'imap://') === 0 && ($msgref = $this->driver->get_message_reference($link, 'task'))) {
                    $rec['links'][$i] = $msgref;
                }
            }
        }

        // Convert HTML description into plain text
        if ($this->is_html($rec)) {
            $h2t = new rcube_html2text($rec['description'], false, true, 0);
            $rec['description'] = $h2t->get_text();
        }

        if (!is_array($rec['tags']))
            $rec['tags'] = (array)$rec['tags'];
        sort($rec['tags'], SORT_LOCALE_STRING);

        if (in_array($rec['id'], $this->collapsed_tasks))
          $rec['collapsed'] = true;

        if (empty($rec['parent_id']))
            $rec['parent_id'] = null;

        $this->task_titles[$rec['id']] = $rec['title'];
    }

    /**
     * Determine whether the given task description is HTML formatted
     */
    private function is_html($task)
    {
        // check for opening and closing <html> or <body> tags
        return (preg_match('/<(html|body)(\s+[a-z]|>)/', $task['description'], $m) && strpos($task['description'], '</'.$m[1].'>') > 0);
    }

    /**
     * Callback function for array_walk over all tasks.
     * Sets tree depth and parent titles
     */
    private function task_walk_tree(&$rec)
    {
        $rec['_depth'] = 0;
        $parent_titles = array();
        $parent_id = isset($this->task_tree[$rec['id']]) ? $this->task_tree[$rec['id']] : null;
        while ($parent_id) {
            $rec['_depth']++;
            if (isset($this->task_titles[$parent_id])) {
                array_unshift($parent_titles, $this->task_titles[$parent_id]);
            }
            $parent_id = isset($this->task_tree[$parent_id]) ? $this->task_tree[$parent_id] : null;
        }

        if (count($parent_titles)) {
            $rec['parent_title'] = join(' ยป ', array_filter($parent_titles));
        }
    }

    /**
     * Compute the filter mask of the given task
     *
     * @param array Hash array with Task record properties
     * @return int Filter mask
     */
    public function filter_mask($rec)
    {
        static $today, $today_date, $tomorrow, $weeklimit;

        if (!$today) {
            $today_date    = new DateTime('now', $this->timezone);
            $today         = $today_date->format('Y-m-d');
            $tomorrow_date = new DateTime('now + 1 day', $this->timezone);
            $tomorrow      = $tomorrow_date->format('Y-m-d');

            // In Kolab-mode we hide "Next 7 days" filter, which means
            // "Later" should catch tasks with date after tomorrow (#5353)
            if ($this->rc->output->get_env('tasklist_driver') == 'kolab') {
                $weeklimit = $tomorrow;
            }
            else {
                $week_date = new DateTime('now + 7 days', $this->timezone);
                $weeklimit = $week_date->format('Y-m-d');
            }
        }

        $mask    = 0;
        $start   = $rec['startdate'] ?: '1900-00-00';
        $duedate = $rec['date'] ?: '3000-00-00';

        if ($rec['flagged'])
            $mask |= self::FILTER_MASK_FLAGGED;
        if ($this->driver->is_complete($rec))
            $mask |= self::FILTER_MASK_COMPLETE;

        if (empty($rec['date']))
            $mask |= self::FILTER_MASK_NODATE;
        else if ($rec['date'] < $today)
            $mask |= self::FILTER_MASK_OVERDUE;

        if (empty($rec['recurrence']) || $duedate < $today || $start > $weeklimit) {
            if ($duedate <= $today || ($rec['startdate'] && $start <= $today))
                $mask |= self::FILTER_MASK_TODAY;
            else if (($start > $today && $start <= $tomorrow) || ($duedate > $today && $duedate <= $tomorrow))
                $mask |= self::FILTER_MASK_TOMORROW;
            else if (($start > $tomorrow && $start <= $weeklimit) || ($duedate > $tomorrow && $duedate <= $weeklimit))
                $mask |= self::FILTER_MASK_WEEK;
            else if ($start > $weeklimit || $duedate > $weeklimit)
                $mask |= self::FILTER_MASK_LATER;
        }
        else if ($rec['startdate'] || $rec['date']) {
            $date = new DateTime($rec['startdate'] ?: $rec['date'], $this->timezone);

            // set safe recurrence start
            while ($date->format('Y-m-d') >= $today) {
                switch ($rec['recurrence']['FREQ']) {
                    case 'DAILY':
                        $date = clone $today_date;
                        $date->sub(new DateInterval('P1D'));
                        break;
                    case 'WEEKLY': $date->sub(new DateInterval('P7D')); break;
                    case 'MONTHLY': $date->sub(new DateInterval('P1M')); break;
                    case 'YEARLY': $date->sub(new DateInterval('P1Y')); break;
                    default; break 2;
                }
            }

            $date->_dateonly = true;

            $engine = libcalendaring::get_recurrence();
            $engine->init($rec['recurrence'], $date);

            // check task occurrences (stop next week)
            // FIXME: is there a faster way of doing this?
            while ($date = $engine->next()) {
                $date = $date->format('Y-m-d');

                // break iteration asap
                if ($date > $duedate || ($mask & self::FILTER_MASK_LATER)) {
                    break;
                }

                if ($date == $today) {
                    $mask |= self::FILTER_MASK_TODAY;
                }
                else if ($date == $tomorrow) {
                    $mask |= self::FILTER_MASK_TOMORROW;
                }
                else if ($date > $tomorrow && $date <= $weeklimit) {
                    $mask |= self::FILTER_MASK_WEEK;
                }
                else if ($date > $weeklimit) {
                    $mask |= self::FILTER_MASK_LATER;
                    break;
                }
            }
        }

        // add masks for assigned tasks
        if ($this->is_organizer($rec) && !empty($rec['attendees']) && $this->is_attendee($rec) === false)
            $mask |= self::FILTER_MASK_ASSIGNED;
        else if (/*empty($rec['attendees']) ||*/ $this->is_attendee($rec) !== false)
            $mask |= self::FILTER_MASK_MYTASKS;

        return $mask;
    }

    /**
     * Determine whether the current user is an attendee of the given task
     */
    public function is_attendee($task)
    {
        $emails = $this->lib->get_user_emails();
        foreach ((array)$task['attendees'] as $i => $attendee) {
            if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
                return $i;
            }
        }

        return false;
    }

    /**
     * Determine whether the current user is the organizer of the given task
     */
    public function is_organizer($task)
    {
        $emails = $this->lib->get_user_emails();
        return (empty($task['organizer']) || in_array(strtolower($task['organizer']['email']), $emails));
    }


    /*******  UI functions  ********/

    /**
     * Render main view of the tasklist task
     */
    public function tasklist_view()
    {
        $this->ui->init();
        $this->ui->init_templates();

        // set autocompletion env
        $this->rc->output->set_env('autocomplete_threads', (int)$this->rc->config->get('autocomplete_threads', 0));
        $this->rc->output->set_env('autocomplete_max', (int)$this->rc->config->get('autocomplete_max', 15));
        $this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length'));
        $this->rc->output->add_label('autocompletechars', 'autocompletemore', 'delete', 'close');

        $this->rc->output->set_pagetitle($this->gettext('navtitle'));
        $this->rc->output->send('tasklist.mainview');
    }

    /**
     * Handler for keep-alive requests
     * This will check for updated data in active lists and sync them to the client
     */
    public function refresh($attr)
    {
        // refresh the entire list every 10th time to also sync deleted items
        if (rand(0,10) == 10) {
            $this->rc->output->command('plugin.reload_data');
            return;
        }

        $filter = array(
            'since'  => $attr['last'],
            'search' => rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC),
            'mask'   => intval(rcube_utils::get_input_value('filter', rcube_utils::INPUT_GPC)) & self::FILTER_MASK_COMPLETE,
        );
        $lists = rcube_utils::get_input_value('lists', rcube_utils::INPUT_GPC);;

        $updates = $this->driver->list_tasks($filter, $lists);
        if (!empty($updates)) {
            $this->rc->output->command('plugin.refresh_tasks', $this->tasks_data($updates), true);

            // update counts
            $counts = $this->driver->count_tasks($lists);
            $this->rc->output->command('plugin.update_counts', $counts);
        }
    }

    /**
     * Handler for pending_alarms plugin hook triggered by the calendar module on keep-alive requests.
     * This will check for pending notifications and pass them to the client
     */
    public function pending_alarms($p)
    {
        $this->load_driver();
        if ($alarms = $this->driver->pending_alarms($p['time'] ?: time())) {
            foreach ($alarms as $alarm) {
                // encode alarm object to suit the expectations of the calendaring code
                if ($alarm['date'])
                    $alarm['start'] = new DateTime($alarm['date'].' '.$alarm['time'], $this->timezone);

                $alarm['id'] = 'task:' . $alarm['id'];  // prefix ID with task:
                $alarm['allday'] = empty($alarm['time']) ? 1 : 0;
                $p['alarms'][] = $alarm;
            }
        }

        return $p;
    }

    /**
     * Handler for alarm dismiss hook triggered by the calendar module
     */
    public function dismiss_alarms($p)
    {
        $this->load_driver();
        foreach ((array)$p['ids'] as $id) {
            if (strpos($id, 'task:') === 0)
                $p['success'] |= $this->driver->dismiss_alarm(substr($id, 5), $p['snooze']);
        }

        return $p;
    }

    /**
     * Handler for importing .ics files
     */
    function import_tasks()
    {
        // Upload progress update
        if (!empty($_GET['_progress'])) {
            $this->rc->upload_progress();
        }

        @set_time_limit(0);

        // process uploaded file if there is no error
        $err = $_FILES['_data']['error'];

        if (!$err && $_FILES['_data']['tmp_name']) {
            $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
            $lists  = $this->driver->get_lists();
            $list   = $lists[$source] ?: $this->get_default_tasklist();
            $source = $list['id'];

            // extract zip file
            if ($_FILES['_data']['type'] == 'application/zip') {
                $count = 0;
                if (class_exists('ZipArchive', false)) {
                    $zip = new ZipArchive();
                    if ($zip->open($_FILES['_data']['tmp_name'])) {
                        $randname = uniqid('zip-' . session_id(), true);
                        $tmpdir   = slashify($this->rc->config->get('temp_dir', sys_get_temp_dir())) . $randname;
                        mkdir($tmpdir, 0700);

                        // extract each ical file from the archive and import it
                        for ($i = 0; $i < $zip->numFiles; $i++) { 
                            $filename = $zip->getNameIndex($i);
                            if (preg_match('/\.ics$/i', $filename)) {
                                $tmpfile = $tmpdir . '/' . basename($filename);
                                if (copy('zip://' . $_FILES['_data']['tmp_name'] . '#'.$filename, $tmpfile)) {
                                    $count += $this->import_from_file($tmpfile, $source, $errors);
                                    unlink($tmpfile);
                                }
                            }
                        }

                        rmdir($tmpdir);
                        $zip->close();
                    }
                    else {
                        $errors = 1;
                        $msg = 'Failed to open zip file.';
                    }
                }
                else {
                    $errors = 1;
                    $msg = 'Zip files are not supported for import.';
                }
            }
            else {
                // attempt to import the uploaded file directly
                $count = $this->import_from_file($_FILES['_data']['tmp_name'], $source, $errors);
            }

            if ($count) {
                $this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
                $this->rc->output->command('plugin.import_success', array('source' => $source, 'refetch' => true));
            }
            else if (!$errors) {
                $this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
                $this->rc->output->command('plugin.import_success', array('source' => $source));
            }
            else {
                $this->rc->output->command('plugin.import_error', array('message' => $this->gettext('importerror') . ($msg ? ': ' . $msg : '')));
            }
        }
        else {
            if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
                $msg = $this->rc->gettext(array('name' => 'filesizeerror', 'vars' => array(
                    'size' => $this->rc->show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
            }
            else {
                $msg = $this->rc->gettext('fileuploaderror');
            }

            $this->rc->output->command('plugin.import_error', array('message' => $msg));
        }

        $this->rc->output->send('iframe');
    }

    /**
     * Helper function to parse and import a single .ics file
     */
    private function import_from_file($filepath, $source, &$errors)
    {
        $user_email = $this->rc->user->get_username();
        $ical       = $this->get_ical();
        $errors     = !$ical->fopen($filepath);
        $count      = $i = 0;

        foreach ($ical as $task) {
            // keep the browser connection alive on long import jobs
            if (++$i > 100 && $i % 100 == 0) {
                echo "<!-- -->";
                ob_flush();
            }

            if ($task['_type'] == 'task') {
                $task['list'] = $source;

                if ($this->driver->create_task($task)) {
                    $count++;
                }
                else {
                    $errors++;
                }
            }
        }

        return $count;
    }

    /**
     * Construct the ics file for exporting tasks to iCalendar format
     */
    function export_tasks()
    {
        $source      = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
        $task_id     = rcube_utils::get_input_value('id', rcube_utils::INPUT_GPC);
        $attachments = (bool) rcube_utils::get_input_value('attachments', rcube_utils::INPUT_GPC);

        $this->load_driver();

        $browser = new rcube_browser;
        $lists   = $this->driver->get_lists();
        $tasks   = array();
        $filter  = array();

        // get message UIDs for filter
        if ($source && ($list = $lists[$source])) {
            $filename = html_entity_decode($list['name']) ?: $sorce;
            $filter   = array($source => true);
        }
        else if ($task_id) {
            $filename = 'tasks';
            foreach (explode(',', $task_id) as $id) {
                list($list_id, $task_id) = explode(':', $id, 2);
                if ($list_id && $task_id) {
                    $filter[$list_id][] = $task_id;
                }
            }
        }

        // Get tasks
        foreach ($filter as $list_id => $uids) {
            $_filter = is_array($uids) ? array('uid' => $uids) : null;
            $_tasks  = $this->driver->list_tasks($_filter, $list_id);
            if (!empty($_tasks)) {
                $tasks = array_merge($tasks, $_tasks);
            }
        }

        // Set file name
        if ($source && count($tasks) == 1) {
            $filename = $tasks[0]['title'] ?: 'task';
        }
        $filename .= '.ics';
        $filename = $browser->ie ? rawurlencode($filename) : addcslashes($filename, '"');

        $tasks = array_map(array($this, 'to_libcal'), $tasks);

        // Give plugins a possibility to implement other output formats or modify the result
        $plugin = $this->rc->plugins->exec_hook('tasks_export', array(
                'result'      => $tasks,
                'attachments' => $attachments,
                'filename'    => $filename,
                'plugin'      => $this,
        ));

        if ($plugin['abort']) {
            exit;
        }

        $this->rc->output->nocacheing_headers();

        // don't kill the connection if download takes more than 30 sec.
        @set_time_limit(0);
        header("Content-Type: text/calendar");
        header("Content-Disposition: inline; filename=\"". $plugin['filename'] ."\"");

        $this->get_ical()->export($plugin['result'], '', true,
            !empty($plugin['attachments']) ? array($this->driver, 'get_attachment_body') : null);
        exit;
    }


    /******* Attachment handling  *******/

    /**
     * Handler for attachments upload
    */
    public function attachment_upload()
    {
        $handler = new kolab_attachments_handler();
        $handler->attachment_upload(self::SESSION_KEY);
    }

    /**
     * Handler for attachments download/displaying
     */
    public function attachment_get()
    {
        $handler = new kolab_attachments_handler();

        // show loading page
        if (!empty($_GET['_preload'])) {
            return $handler->attachment_loading_page();
        }

        $task = rcube_utils::get_input_value('_t', rcube_utils::INPUT_GPC);
        $list = rcube_utils::get_input_value('_list', rcube_utils::INPUT_GPC);
        $id   = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC);
        $rev  = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);

        $task       = array('id' => $task, 'list' => $list, 'rev' => $rev);
        $attachment = $this->driver->get_attachment($id, $task);

        // show part page
        if (!empty($_GET['_frame'])) {
            $handler->attachment_page($attachment);
        }
        // deliver attachment content
        else if ($attachment) {
            $attachment['body'] = $this->driver->get_attachment_body($id, $task);
            $handler->attachment_get($attachment);
        }

        // if we arrive here, the requested part was not found
        header('HTTP/1.1 404 Not Found');
        exit;
    }


    /*******  Email related function *******/

    public function mail_message2task()
    {
        $this->load_ui();
        $this->ui->init();
        $this->ui->init_templates();
        $this->ui->tasklists();

        $uid  = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GET);
        $mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GET);
        $task = array();

        $imap    = $this->rc->get_storage();
        $message = new rcube_message($uid, $mbox);

        if ($message->headers) {
            $task['title']       = trim($message->subject);
            $task['description'] = trim($message->first_text_part());
            $task['id']          = -$uid;

            $this->load_driver();

            // add a reference to the email message
            if ($msgref = $this->driver->get_message_reference($message->headers, $mbox)) {
                $task['links'] = array($msgref);
            }
            // copy mail attachments to task
            else if ($message->attachments && $this->driver->attachments) {
                if (!is_array($_SESSION[self::SESSION_KEY]) || $_SESSION[self::SESSION_KEY]['id'] != $task['id']) {
                    $_SESSION[self::SESSION_KEY] = array(
                        'id'          => $task['id'],
                        'attachments' => array(),
                    );
                }

                foreach ((array)$message->attachments as $part) {
                    $attachment = array(
                        'data'     => $imap->get_message_part($uid, $part->mime_id, $part),
                        'size'     => $part->size,
                        'name'     => $part->filename,
                        'mimetype' => $part->mimetype,
                        'group'    => $task['id'],
                    );

                    $attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);

                    if ($attachment['status'] && !$attachment['abort']) {
                        $id = $attachment['id'];
                        $attachment['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);

                        // store new attachment in session
                        unset($attachment['status'], $attachment['abort'], $attachment['data']);
                        $_SESSION[self::SESSION_KEY]['attachments'][$id] = $attachment;

                        $attachment['id'] = 'rcmfile' . $attachment['id'];  // add prefix to consider it 'new'
                        $task['attachments'][] = $attachment;
                    }
                }
            }

            $this->rc->output->set_env('task_prop', $task);
        }
        else {
            $this->rc->output->command('display_message', $this->gettext('messageopenerror'), 'error');
        }

        $this->rc->output->send('tasklist.dialog');
    }

    /**
     * Add UI element to copy task invitations or updates to the tasklist
     */
    public function mail_messagebody_html($p)
    {
        // load iCalendar functions (if necessary)
        if (!empty($this->lib->ical_parts)) {
            $this->get_ical();
            $this->load_itip();
        }

        $html = '';
        $has_tasks = false;
        $ical_objects = $this->lib->get_mail_ical_objects();

        // show a box for every task in the file
        foreach ($ical_objects as $idx => $task) {
            if ($task['_type'] != 'task') {
                continue;
            }

            $has_tasks = true;

            // get prepared inline UI for this event object
            if ($ical_objects->method) {
                $html .= html::div('tasklist-invitebox invitebox boxinformation',
                    $this->itip->mail_itip_inline_ui(
                        $task,
                        $ical_objects->method,
                        $ical_objects->mime_id . ':' . $idx,
                        'tasks',
                        rcube_utils::anytodatetime($ical_objects->message_date)
                    )
                );
            }

            // limit listing
            if ($idx >= 3) {
                break;
            }
        }

        // list linked tasks
        $links = array();
        foreach ($this->message_tasks as $task) {
            $checkbox = new html_checkbox(array(
                'name' => 'completed',
                'class' => 'complete pretty-checkbox',
                'title' => $this->gettext('complete'),
                'data-list' => $task['list'],
            ));
            $complete = $this->driver->is_complete($task);
            $links[] = html::tag('li', 'messagetaskref' . ($complete ? ' complete' : ''),
                $checkbox->show($complete ? $task['id'] : null, array('value' => $task['id'])) . ' ' .
                html::a(array(
                    'href' => $this->rc->url(array(
                        'task' => 'tasks',
                        'list' => $task['list'],
                        'id' => $task['id'],
                    )),
                    'class' => 'messagetasklink',
                    'rel' => $task['id'] . '@' . $task['list'],
                    'target' => '_blank',
                ), rcube::Q($task['title']))
            );
        }
        if (count($links)) {
            $html .= html::div('messagetasklinks boxinformation', html::tag('ul', 'tasklist', join("\n", $links)));
        }

        // prepend iTip/relation boxes to message body
        if ($html) {
            $this->load_ui();
            $this->ui->init();

            $p['content'] = $html . $p['content'];

            $this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm');
        }

        // add "Save to tasks" button into attachment menu
        if ($has_tasks) {
            $this->add_button(array(
                'id'         => 'attachmentsavetask',
                'name'       => 'attachmentsavetask',
                'type'       => 'link',
                'wrapper'    => 'li',
                'command'    => 'attachment-save-task',
                'class'      => 'icon tasklistlink disabled',
                'classact'   => 'icon tasklistlink active',
                'innerclass' => 'icon taskadd',
                'label'      => 'tasklist.savetotasklist',
            ), 'attachmentmenu');
        }

        return $p;
    }

    /**
     * Lookup backend storage and find notes associated with the given message
     */
    public function mail_message_load($p)
    {
        if (empty($p['object']->headers->others['x-kolab-type'])) {
            $this->load_driver();
            $this->message_tasks = $this->driver->get_message_related_tasks($p['object']->headers, $p['object']->folder);

            // sort message tasks by completeness and due date
            $driver = $this->driver;
            array_walk($this->message_tasks, array($this, 'encode_task'));
            usort($this->message_tasks, function($a, $b) use ($driver) {
                $a_complete = intval($driver->is_complete($a));
                $b_complete = intval($driver->is_complete($b));
                $d = $a_complete - $b_complete;
                if (!$d) $d = $b['_hasdate'] - $a['_hasdate'];
                if (!$d) $d = $a['datetime'] - $b['datetime'];
                return $d;
            });
        }
    }

    /**
     * Load iCalendar functions
     */
    public function get_ical()
    {
        if (empty($this->ical)) {
            $this->ical = libcalendaring::get_ical();
        }

        return $this->ical;
    }

    /**
     * Get properties of the tasklist this user has specified as default
     */
    public function get_default_tasklist($sensitivity = null, $lists = null)
    {
        if ($lists === null) {
            $lists = $this->driver->get_lists(tasklist_driver::FILTER_PERSONAL | tasklist_driver::FILTER_WRITEABLE);
        }

        $list = null;

        foreach ($lists as $l) {
            if ($sensitivity && $l['subtype'] == $sensitivity) {
                $list = $l;
                break;
            }
            if ($l['default']) {
                $list = $l;
            }

            if ($l['editable']) {
                $first = $l;
            }
        }

        return $list ?: $first;
    }

    /**
     * Import the full payload from a mail message attachment
     */
    public function mail_import_attachment()
    {
        $uid     = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
        $mbox    = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
        $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
        $charset = RCUBE_CHARSET;

        // establish imap connection
        $imap = $this->rc->get_storage();
        $imap->set_folder($mbox);

        if ($uid && $mime_id) {
            $part    = $imap->get_message_part($uid, $mime_id);
//            $headers = $imap->get_message_headers($uid);

            if ($part->ctype_parameters['charset']) {
                $charset = $part->ctype_parameters['charset'];
            }

            if ($part) {
                $tasks = $this->get_ical()->import($part, $charset);
            }
        }

        $success = $existing = 0;

        if (!empty($tasks)) {
            // find writeable tasklist to store task
            $cal_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null;
            $lists  = $this->driver->get_lists();

            foreach ($tasks as $task) {
                // save to tasklist
                $list   = $lists[$cal_id] ?: $this->get_default_tasklist($task['sensitivity']);
                if ($list && $list['editable'] && $task['_type'] == 'task') {
                    $task = $this->from_ical($task);
                    $task['list'] = $list['id'];

                    if (!$this->driver->get_task($task['uid'])) {
                        $success += (bool) $this->driver->create_task($task);
                    }
                    else {
                        $existing++;
                    }
                }
            }
        }

        if ($success) {
            $this->rc->output->command('display_message', $this->gettext(array(
                'name' => 'importsuccess',
                'vars' => array('nr' => $success),
            )), 'confirmation');
        }
        else if ($existing) {
            $this->rc->output->command('display_message', $this->gettext('importwarningexists'), 'warning');
        }
        else {
            $this->rc->output->command('display_message', $this->gettext('errorimportingtask'), 'error');
        }
    }

    /**
     * Handler for POST request to import an event attached to a mail message
     */
    public function mail_import_itip()
    {
        $uid     = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
        $mbox    = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_POST);
        $mime_id = rcube_utils::get_input_value('_part', rcube_utils::INPUT_POST);
        $status  = rcube_utils::get_input_value('_status', rcube_utils::INPUT_POST);
        $comment = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
        $delete  = intval(rcube_utils::get_input_value('_del', rcube_utils::INPUT_POST));
        $noreply = intval(rcube_utils::get_input_value('_noreply', rcube_utils::INPUT_POST)) || $status == 'needs-action';

        $error_msg = $this->gettext('errorimportingtask');
        $success   = false;

        if ($status == 'delegated') {
            $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false);
            $delegate  = reset($delegates);

            if (empty($delegate) || empty($delegate['mailto'])) {
                $this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error');
                return;
            }
        }

        // successfully parsed tasks?
        if ($task = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'task')) {
            $task = $this->from_ical($task);

            // forward iTip request to delegatee
            if ($delegate) {
                $rsvpme = rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST);
                $itip   = $this->load_itip();

                $task['comment'] = $comment;

                if ($itip->delegate_to($task, $delegate, !empty($rsvpme))) {
                    $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
                }
                else {
                    $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
                }

                unset($task['comment']);
            }

            $mode = tasklist_driver::FILTER_PERSONAL
                | tasklist_driver::FILTER_SHARED
                | tasklist_driver::FILTER_WRITEABLE;

            // find writeable list to store the task
            $list_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null;
            $lists   = $this->driver->get_lists($mode);
            $list    = $lists[$list_id];
            $dontsave = ($_REQUEST['_folder'] === '' && $task['_method'] == 'REQUEST');

            // select default list except user explicitly selected 'none'
            if (!$list && !$dontsave) {
                $list = $this->get_default_tasklist($task['sensitivity'], $lists);
            }

            $metadata = array(
                'uid'      => $task['uid'],
                'changed'  => is_object($task['changed']) ? $task['changed']->format('U') : 0,
                'sequence' => intval($task['sequence']),
                'fallback' => strtoupper($status),
                'method'   => $task['_method'],
                'task'     => 'tasks',
            );

            // update my attendee status according to submitted method
            if (!empty($status)) {
                $organizer = $task['organizer'];
                $emails    = $this->lib->get_user_emails();

                foreach ($task['attendees'] as $i => $attendee) {
                    if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
                        $metadata['attendee'] = $attendee['email'];
                        $metadata['rsvp']     = $attendee['role'] != 'NON-PARTICIPANT';
                        $reply_sender         = $attendee['email'];

                        $task['attendees'][$i]['status'] = strtoupper($status);
                        if (!in_array($task['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) {
                            $task['attendees'][$i]['rsvp'] = false;  // unset RSVP attribute
                        }
                    }
                }

                // add attendee with this user's default identity if not listed
                if (!$reply_sender) {
                    $sender_identity = $this->rc->user->list_emails(true);
                    $task['attendees'][] = array(
                        'name'   => $sender_identity['name'],
                        'email'  => $sender_identity['email'],
                        'role'   => 'OPT-PARTICIPANT',
                        'status' => strtoupper($status),
                    );
                    $metadata['attendee'] = $sender_identity['email'];
                }
            }

            // save to tasklist
            if ($list && $list['editable']) {
                $task['list'] = $list['id'];

                // check for existing task with the same UID
                $existing = $this->find_task($task['uid'], $mode);

                if ($existing) {
                    // only update attendee status
                    if ($task['_method'] == 'REPLY') {
                        // try to identify the attendee using the email sender address
                        $existing_attendee = -1;
                        $existing_attendee_emails = array();
                        foreach ($existing['attendees'] as $i => $attendee) {
                            $existing_attendee_emails[] = $attendee['email'];
                            if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
                                $existing_attendee = $i;
                            }
                        }

                        $task_attendee = null;
                        foreach ($task['attendees'] as $attendee) {
                            if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
                                $task_attendee        = $attendee;
                                $metadata['fallback'] = $attendee['status'];
                                $metadata['attendee'] = $attendee['email'];
                                $metadata['rsvp']     = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
                                if ($attendee['status'] != 'DELEGATED') {
                                    break;
                                }
                            }
                            // also copy delegate attendee
                            else if (!empty($attendee['delegated-from']) &&
                                     (stripos($attendee['delegated-from'], $task['_sender']) !== false || stripos($attendee['delegated-from'], $task['_sender_utf']) !== false) &&
                                     (!in_array($attendee['email'], $existing_attendee_emails))) {
                                $existing['attendees'][] = $attendee;
                            }
                        }

                        // if delegatee has declined, set delegator's RSVP=True
                        if ($task_attendee && $task_attendee['status'] == 'DECLINED' && $task_attendee['delegated-from']) {
                            foreach ($existing['attendees'] as $i => $attendee) {
                                if ($attendee['email'] == $task_attendee['delegated-from']) {
                                    $existing['attendees'][$i]['rsvp'] = true;
                                    break;
                                }
                            }
                        }

                        // found matching attendee entry in both existing and new events
                        if ($existing_attendee >= 0 && $task_attendee) {
                            $existing['attendees'][$existing_attendee] = $task_attendee;
                            $success = $this->driver->edit_task($existing);
                        }
                        // update the entire attendees block
                        else if (($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) && $task_attendee) {
                            $existing['attendees'][] = $task_attendee;
                            $success = $this->driver->edit_task($existing);
                        }
                        else {
                            $error_msg = $this->gettext('newerversionexists');
                        }
                    }
                    // delete the task when declined
                    else if ($status == 'declined' && $delete) {
                        $deleted = $this->driver->delete_task($existing, true);
                        $success = true;
                    }
                    // import the (newer) task
                    else if ($task['sequence'] >= $existing['sequence'] || $task['changed'] >= $existing['changed']) {
                        $task['id']   = $existing['id'];
                        $task['list'] = $existing['list'];

                        // preserve my participant status for regular updates
                        if (empty($status)) {
                            $this->lib->merge_attendees($task, $existing);
                        }

                        // set status=CANCELLED on CANCEL messages
                        if ($task['_method'] == 'CANCEL') {
                            $task['status'] = 'CANCELLED';
                        }

                        // update attachments list, allow attachments update only on REQUEST (#5342)
                        if ($task['_method'] == 'REQUEST') {
                            $task['deleted_attachments'] = true;
                        }
                        else {
                            unset($task['attachments']);
                        }

                        // show me as free when declined (#1670)
                        if ($status == 'declined' || $task['status'] == 'CANCELLED') {
                            $task['free_busy'] = 'free';
                        }

                        $success = $this->driver->edit_task($task);
                    }
                    else if (!empty($status)) {
                        $existing['attendees'] = $task['attendees'];
                        if ($status == 'declined') { // show me as free when declined (#1670)
                            $existing['free_busy'] = 'free';
                        }

                        $success = $this->driver->edit_event($existing);
                    }
                    else {
                        $error_msg = $this->gettext('newerversionexists');
                    }
                }
                else if (!$existing && ($status != 'declined' || $this->rc->config->get('kolab_invitation_tasklists'))) {
                    $success = $this->driver->create_task($task);
                }
                else if ($status == 'declined') {
                    $error_msg = null;
                }
            }
            else if ($status == 'declined' || $dontsave) {
                $error_msg = null;
            }
            else {
                $error_msg = $this->gettext('nowritetasklistfound');
            }
        }

        if ($success || $dontsave) {
            if ($success) {
                $message = $task['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
                $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
            }

            $metadata['rsvp']         = intval($metadata['rsvp']);
            $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);

            $this->rc->output->command('plugin.itip_message_processed', $metadata);
            $error_msg = null;
        }
        else if ($error_msg) {
            $this->rc->output->command('display_message', $error_msg, 'error');
        }

        // send iTip reply
        if ($task['_method'] == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
            $task['comment'] = $comment;
            $itip = $this->load_itip();
            $itip->set_sender_email($reply_sender);

            if ($itip->send_itip_message($this->to_libcal($task), 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
                $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ?: $organizer['email']))), 'confirmation');
            else
                $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
        }

        $this->rc->output->send();
    }


    /****  Task invitation plugin hooks ****/

    /**
     * Handler for task/itip-delegate requests
     */
    function mail_itip_delegate()
    {
        // forward request to mail_import_itip() with the right status
        $_POST['_status'] = $_REQUEST['_status'] = 'delegated';
        $this->mail_import_itip();
    }

    /**
     * Find a task in user tasklists
     */
    protected function find_task($task, &$mode)
    {
        $this->load_driver();

        // We search for writeable folders in personal namespace by default
        $mode   = tasklist_driver::FILTER_WRITEABLE | tasklist_driver::FILTER_PERSONAL;
        $result = $this->driver->get_task($task, $mode);

        // ... now check shared folders if not found
        if (!$result) {
            $result = $this->driver->get_task($task, tasklist_driver::FILTER_WRITEABLE | tasklist_driver::FILTER_SHARED);
            if ($result) {
                $mode |= tasklist_driver::FILTER_SHARED;
            }
        }

        return $result;
    }

    /**
     * Handler for task/itip-status requests
     */
    public function task_itip_status()
    {
        $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);

        // find local copy of the referenced task
        $existing  = $this->find_task($data, $mode);
        $is_shared = $mode & tasklist_driver::FILTER_SHARED;
        $itip      = $this->load_itip();
        $response  = $itip->get_itip_status($data, $existing);

        // get a list of writeable lists to save new tasks to
        if ((!$existing || $is_shared) && $response['action'] == 'rsvp' || $response['action'] == 'import') {
            $lists  = $this->driver->get_lists($mode);
            $select = new html_select(array('name' => 'tasklist', 'id' => 'itip-saveto', 'is_escaped' => true, 'class' => 'form-control'));
            $select->add('--', '');

            foreach ($lists as $list) {
                if ($list['editable']) {
                    $select->add($list['name'], $list['id']);
                }
            }
        }

        if ($select) {
            $default_list = $this->get_default_tasklist($data['sensitivity'], $lists);
            $response['select'] = html::span('folder-select', $this->gettext('saveintasklist') . '&nbsp;' .
                $select->show($is_shared ? $existing['list'] : $default_list['id']));
        }

        $this->rc->output->command('plugin.update_itip_object_status', $response);
    }

    /**
     * Handler for task/itip-remove requests
     */
    public function task_itip_remove()
    {
        $success = false;
        $uid     = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);

        // search for event if only UID is given
        if ($task = $this->driver->get_task($uid)) {
            $success = $this->driver->delete_task($task, true);
        }

        if ($success) {
            $this->rc->output->show_message('tasklist.successremoval', 'confirmation');
        }
        else {
            $this->rc->output->show_message('tasklist.errorsaving', 'error');
        }
    }


    /*******  Utility functions  *******/

    /**
     * Generate a unique identifier for an event
     */
    public function generate_uid()
    {
      return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
    }

    /**
     * Map task properties for ical exprort using libcalendaring
     */
    public function to_libcal($task)
    {
        $object = $task;
        $object['_type'] = 'task';
        $object['categories'] = (array)$task['tags'];

        // convert to datetime objects
        if (!empty($task['date'])) {
            $object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->timezone);
            if (empty($task['time']))
                $object['due']->_dateonly = true;
            unset($object['date']);
        }

        if (!empty($task['startdate'])) {
            $object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->timezone);
            if (empty($task['starttime']))
                $object['start']->_dateonly = true;
            unset($object['startdate']);
        }

        $object['complete'] = $task['complete'] * 100;
        if ($task['complete'] == 1.0 && empty($task['complete'])) {
            $object['status'] = 'COMPLETED';
        }

        if ($task['flagged']) {
            $object['priority'] = 1;
        }
        else if (empty($task['priority'])) {
            $object['priority'] = 0;
        }

        return $object;
    }

    /**
     * Convert task properties from ical parser to the internal format
     */
    public function from_ical($vtodo)
    {
        $task = $vtodo;

        $task['tags'] = array_filter((array)$vtodo['categories']);
        $task['flagged'] = $vtodo['priority'] == 1;
        $task['complete'] = floatval($vtodo['complete'] / 100);

        // convert from DateTime to internal date format
        if (is_a($vtodo['due'], 'DateTime')) {
            $due = $this->lib->adjust_timezone($vtodo['due']);
            $task['date'] = $due->format('Y-m-d');
            if (!$vtodo['due']->_dateonly)
                $task['time'] = $due->format('H:i');
        }
        // convert from DateTime to internal date format
        if (is_a($vtodo['start'], 'DateTime')) {
            $start = $this->lib->adjust_timezone($vtodo['start']);
            $task['startdate'] = $start->format('Y-m-d');
            if (!$vtodo['start']->_dateonly)
                $task['starttime'] = $start->format('H:i');
        }
        if (is_a($vtodo['dtstamp'], 'DateTime')) {
            $task['changed'] = $vtodo['dtstamp'];
        }

        unset($task['categories'], $task['due'], $task['start'], $task['dtstamp']);

        return $task;
    }

    /**
     * Handler for user_delete plugin hook
     */
    public function user_delete($args)
    {
       $this->load_driver();
       return $this->driver->user_delete($args);
    }


    /**
     * Magic getter for public access to protected members
     */
    public function __get($name)
    {
        switch ($name) {
            case 'ical':
                return $this->get_ical();

            case 'itip':
                return $this->load_itip();

            case 'driver':
                $this->load_driver();
                return $this->driver;
        }

        return null;
    }
}