Branch 'dev/task-attendees' - plugins/tasklist

Aleksander Machniak machniak at kolabsys.com
Tue Jul 29 15:11:40 CEST 2014


 plugins/tasklist/localization/en_US.inc                 |   16 
 plugins/tasklist/skins/larry/images/ical-attachment.png |binary
 plugins/tasklist/skins/larry/images/loading_blue.gif    |binary
 plugins/tasklist/skins/larry/images/tasklist.png        |binary
 plugins/tasklist/skins/larry/tasklist.css               |   95 ++
 plugins/tasklist/tasklist.php                           |  538 +++++++++++++++-
 plugins/tasklist/tasklist_base.js                       |   33 
 7 files changed, 672 insertions(+), 10 deletions(-)

New commits:
commit 445edd09b76fc5e029e5f94e70a64a89907ae965
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Tue Jul 29 15:11:05 2014 +0200

    Added iTip handling in tasklist plugin (code copied from Calendar)

diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 0b71377..f43fbb9 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -77,7 +77,7 @@ $labels['at'] = 'at';
 $labels['this'] = 'this';
 $labels['next'] = 'next';
 
-// mesages
+// messages
 $labels['savingdata'] = 'Saving data...';
 $labels['errorsaving'] = 'Failed to save data.';
 $labels['notasksfound'] = 'No tasks found for the given criteria';
@@ -138,3 +138,17 @@ $labels['errornotifying'] = 'Failed to send notifications to task participants';
 $labels['andnmore'] = '$nr more...';
 $labels['delegatedto'] = 'Delegated to: ';
 $labels['delegatedfrom'] = 'Delegated from: ';
+$labels['savetotasklist'] = 'Save to tasks';
+$labels['comment'] = 'Comment';
+$labels['errorimportingtask'] = 'Failed to import task(s)';
+$labels['importwarningexists'] = 'A copy of this task already exists in your tasklist.';
+$labels['importsuccess'] = 'Successfully imported $nr tasks';
+$labels['newerversionexists'] = 'A newer version of this task already exists! Aborted.';
+$labels['nowritetasklistfound'] = 'No tasklist found to save the task';
+$labels['importedsuccessfully'] = 'The task was successfully added to \'$list\'';
+$labels['updatedsuccessfully'] = 'The task was successfully updated in \'$list\'';
+$labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status';
+$labels['itipresponseerror'] = 'Failed to send the response to this task invitation';
+$labels['itipinvalidrequest'] = 'This invitation is no longer valid';
+$labels['sentresponseto'] = 'Successfully sent invitation response to $mailto';
+$labels['successremoval'] = 'The task has been deleted successfully.';
diff --git a/plugins/tasklist/skins/larry/images/ical-attachment.png b/plugins/tasklist/skins/larry/images/ical-attachment.png
new file mode 100644
index 0000000..8fa486a
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/ical-attachment.png differ
diff --git a/plugins/tasklist/skins/larry/images/loading_blue.gif b/plugins/tasklist/skins/larry/images/loading_blue.gif
new file mode 100644
index 0000000..2ea6b19
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/loading_blue.gif differ
diff --git a/plugins/tasklist/skins/larry/images/tasklist.png b/plugins/tasklist/skins/larry/images/tasklist.png
new file mode 100644
index 0000000..50ed630
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/tasklist.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 7bdfe57..bcdf29a 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -20,7 +20,8 @@
 	background-position: 0 -26px;
 }
 
-ul.toolbarmenu li span.icon.taskadd {
+ul.toolbarmenu li span.icon.taskadd,
+#attachmentmenu li a.tasklistlink span.icon.taskadd {
 	background-image: url(buttons.png);
 	background-position: -4px -90px;
 }
@@ -1085,6 +1086,98 @@ label.block {
 	margin-bottom: 0.5em;
 }
 
+/* Invitation UI in mail */
+
+.messagelist tbody .attachment span.ical {
+	display: inline-block;
+	vertical-align: middle;
+	height: 18px;
+	width: 20px;
+	padding: 0;
+	background: url(images/ical-attachment.png) 2px 1px no-repeat;
+}
+
+div.tasklist-invitebox {
+	min-height: 20px;
+	margin: 5px 8px;
+	padding: 3px 6px 6px 34px;
+	border: 1px solid #ffdf0e;
+	background: url(images/tasklist.png) 6px 5px no-repeat #fef893;
+}
+
+div.tasklist-invitebox td.ititle {
+	font-weight: bold;
+	padding-right: 0.5em;
+}
+
+div.tasklist-invitebox td.label {
+	color: #666;
+	padding-right: 1em;
+}
+
+#event-rsvp .rsvp-buttons,
+div.tasklist-invitebox .itip-buttons div {
+	margin-top: 0.5em;
+}
+
+#event-rsvp input.button,
+div.tasklist-invitebox input.button {
+	font-weight: bold;
+	margin-right: 0.5em;
+}
+
+div.tasklist-invitebox .folder-select {
+	font-weight: 10px;
+	margin-left: 1em;
+}
+
+div.tasklist-invitebox .rsvp-status {
+	padding-left: 2px;
+}
+
+div.tasklist-invitebox .rsvp-status.loading {
+	color: #666;
+	padding: 1px 0 2px 24px;
+	background: url(images/loading_blue.gif) top left no-repeat;
+}
+
+div.tasklist-invitebox .rsvp-status.hint {
+	color: #666;
+	text-shadow: none;
+	font-style: italic;
+}
+
+#event-partstat .changersvp,
+div.tasklist-invitebox .rsvp-status.declined,
+div.tasklist-invitebox .rsvp-status.tentative,
+div.tasklist-invitebox .rsvp-status.accepted,
+div.tasklist-invitebox .rsvp-status.delegated,
+div.tasklist-invitebox .rsvp-status.needs-action  {
+	padding: 0 0 1px 22px;
+	background: url(images/attendee-status.png) 2px -20px no-repeat;
+}
+
+#event-partstat .changersvp.declined,
+div.tasklist-invitebox .rsvp-status.declined {
+	background-position: 2px -40px;
+}
+
+#event-partstat .changersvp.tentative,
+div.tasklist-invitebox .rsvp-status.tentative {
+	background-position: 2px -60px;
+}
+
+#event-partstat .changersvp.delegated,
+div.tasklist-invitebox .rsvp-status.delegated {
+	background-position: 2px -180px;
+}
+
+#event-partstat .changersvp.needs-action,
+div.tasklist-invitebox .rsvp-status.needs-action {
+	background-position: 2px 0;
+}
+
+
 /** Special hacks for IE7 **/
 /** They need to be in this file to also affect the task-create dialog embedded in mail view **/
 
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 34b1ffb..76ef251 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -56,6 +56,7 @@ class tasklist extends rcube_plugin
 
     private $collapsed_tasks = array();
     private $itip;
+    private $ical;
 
 
     /**
@@ -107,15 +108,19 @@ class tasklist extends rcube_plugin
             $this->register_action('mail2task', array($this, 'mail_message2task'));
             $this->register_action('get-attachment', array($this, 'attachment_get'));
             $this->register_action('upload', array($this, 'attachment_upload'));
+            $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->add_hook('refresh', array($this, 'refresh'));
 
             $this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
         }
         else if ($args['task'] == 'mail') {
-            // TODO: register hooks to catch ical/vtodo email attachments
             if ($args['action'] == 'show' || $args['action'] == 'preview') {
-                // $this->add_hook('message_load', array($this, 'mail_message_load'));
-                // $this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
+                $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
@@ -1255,6 +1260,532 @@ class tasklist extends rcube_plugin
         $this->rc->output->send();
     }
 
+    /**
+     * Check mail message structure of there are .ics files attached
+     *
+     * @todo move to libcalendaring
+     */
+    public function mail_message_load($p)
+    {
+        $this->message = $p['object'];
+        $itip_part     = null;
+
+        // check all message parts for .ics files
+        foreach ((array)$this->message->mime_parts as $part) {
+            if ($this->is_vcalendar($part)) {
+                if ($part->ctype_parameters['method'])
+                    $itip_part = $part->mime_id;
+                else
+                    $this->ics_parts[] = $part->mime_id;
+            }
+        }
+
+        // priorize part with method parameter
+        if ($itip_part) {
+            $this->ics_parts = array($itip_part);
+        }
+    }
+
+    /**
+     * Add UI element to copy event invitations or updates to the calendar
+     *
+     * @todo move to libcalendaring
+     */
+    public function mail_messagebody_html($p)
+    {
+        // load iCalendar functions (if necessary)
+        if (!empty($this->ics_parts)) {
+            $this->get_ical();
+            $this->load_itip();
+        }
+
+        // @todo: Calendar plugin does the same, which means the
+        // attachment body is fetched twice, this is not optimal
+        $html = '';
+        foreach ($this->ics_parts as $mime_id) {
+            $part    = $this->message->mime_parts[$mime_id];
+            $charset = $part->ctype_parameters['charset'] ? $part->ctype_parameters['charset'] : RCMAIL_CHARSET;
+            $objects = $this->ical->import($this->message->get_part_content($mime_id), $charset);
+            $title   = $this->gettext('title');
+
+            // successfully parsed events?
+            if (empty($objects)) {
+                continue;
+            }
+
+            // show a box for every task in the file
+            foreach ($objects as $idx => $task) {
+                if ($task['_type'] != 'task') {
+                    continue;
+                }
+
+                // get prepared inline UI for this event object
+                $html .= html::div('tasklist-invitebox',
+                    $this->itip->mail_itip_inline_ui(
+                        $task,
+                        $this->ical->method,
+                        $mime_id . ':' . $idx,
+                        'tasks',
+                        rcube_utils::anytodatetime($this->message->headers->date)
+                    )
+                );
+
+                // limit listing
+                if ($idx >= 3) {
+                    break;
+                }
+            }
+        }
+
+        // prepend event 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 calendar" button into attachment menu
+            $this->add_button(array(
+                'id'         => 'attachmentsavetask',
+                'name'       => 'attachmentsavetask',
+                'type'       => 'link',
+                'wrapper'    => 'li',
+                'command'    => 'attachment-save-task',
+                'class'      => 'icon tasklistlink',
+                'classact'   => 'icon tasklistlink active',
+                'innerclass' => 'icon taskadd',
+                'label'      => 'tasklist.savetotasklist',
+            ), 'attachmentmenu');
+        }
+
+        return $p;
+    }
+
+    /**
+     * Read the given mime message from IMAP and parse ical data
+     *
+     * @todo move to libcalendaring
+     */
+    private function mail_get_itip_task($mbox, $uid, $mime_id)
+    {
+        $charset = RCMAIL_CHARSET;
+
+        // establish imap connection
+        $imap = $this->rc->get_storage();
+        $imap->set_mailbox($mbox);
+
+        if ($uid && $mime_id) {
+            list($mime_id, $index) = explode(':', $mime_id);
+
+            $part    = $imap->get_message_part($uid, $mime_id);
+            $headers = $imap->get_message_headers($uid);
+
+            if ($part->ctype_parameters['charset']) {
+                $charset = $part->ctype_parameters['charset'];
+            }
+
+            if ($part) {
+                $tasks = $this->get_ical()->import($part, $charset);
+            }
+        }
+
+        // successfully parsed events?
+        if (!empty($tasks) && ($task = $tasks[$index])) {
+            // store the message's sender address for comparisons
+            $task['_sender'] = preg_match('/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/', $headers->from, $m) ? $m[1] : '';
+            $askt['_sender_utf'] = rcube_idn_to_utf8($task['_sender']);
+
+            return $task;
+        }
+
+        return null;
+    }
+
+    /**
+     * Checks if specified message part is a vcalendar data
+     *
+     * @param rcube_message_part Part object
+     *
+     * @return boolean True if part is of type vcard
+     *
+     * @todo move to libcalendaring
+     */
+    private function is_vcalendar($part)
+    {
+        return (
+            in_array($part->mimetype, array('text/calendar', 'text/x-vcalendar', 'application/ics')) ||
+            // Apple sends files as application/x-any (!?)
+            ($part->mimetype == 'application/x-any' && $part->filename && preg_match('/\.ics$/i', $part->filename))
+        );
+    }
+
+    /**
+     * Load iCalendar functions
+     */
+    public function get_ical()
+    {
+        if (!$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($writeable = false)
+    {
+//        $default_id = $this->rc->config->get('tasklist_default_list');
+        $lists = $this->driver->get_lists();
+//        $list  = $calendars[$default_id] ?: null;
+
+        if (!$list || ($writeable && !$list['editable'])) {
+            foreach ($lists as $l) {
+                if ($l['default']) {
+                    $list = $l;
+                    break;
+                }
+
+                if (!$writeable || $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 = RCMAIL_CHARSET;
+
+        // establish imap connection
+        $imap = $this->rc->get_storage();
+        $imap->set_mailbox($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();
+            $list   = $lists[$cal_id] ?: $this->get_default_tasklist(true);
+
+            foreach ($tasks as $task) {
+                // save to tasklist
+                if ($list && $list['editable'] && $task['_type'] == '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);
+        $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;
+
+        // successfully parsed tasks?
+        if ($task = $this->mail_get_itip_task($mbox, $uid, $mime_id)) {
+            // find writeable list to store the task
+            $list_id = !empty($_REQUEST['_list']) ? rcube_utils::get_input_value('_list', rcube_utils::INPUT_POST) : null;
+            $lists   = $this->driver->get_lists();
+            $list    = $lists[$list_id] ?: $this->get_default_tasklist(true);
+
+            $metadata = array(
+                'uid'      => $task['uid'],
+                'changed'  => is_object($task['changed']) ? $task['changed']->format('U') : 0,
+                'sequence' => intval($task['sequence']),
+                'fallback' => strtoupper($status),
+                'method'   => $this->ical->method,
+                'task'     => 'tasks',
+            );
+
+            // update my attendee status according to submitted method
+            if (!empty($status)) {
+                $organizer = null;
+                $emails    = $this->lib->get_user_emails();
+
+                foreach ($task['attendees'] as $i => $attendee) {
+                    if ($attendee['role'] == 'ORGANIZER') {
+                        $organizer = $attendee;
+                    }
+                    else 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 ($task['attendees'][$i]['status'] != 'NEEDS-ACTION') {
+                            unset($task['attendees'][$i]['rsvp']);  // remove RSVP attribute
+                        }
+                    }
+                }
+
+                // add attendee with this user's default identity if not listed
+                if (!$reply_sender) {
+                    $sender_identity = $this->rc->user->get_identity();
+                    $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->driver->get_task($task['uid']);
+
+                if ($existing) {
+                    // only update attendee status
+                    if ($this->ical->method == 'REPLY') {
+                        // try to identify the attendee using the email sender address
+                        $existing_attendee = -1;
+                        foreach ($existing['attendees'] as $i => $attendee) {
+                            if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
+                                $existing_attendee = $i;
+                                break;
+                            }
+                        }
+
+                        $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';
+                                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)) {
+                            $emails = $this->lib->get_user_emails();
+                            foreach ($task['attendees'] as $i => $attendee) {
+                                if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+                                    foreach ($existing['attendees'] as $j => $_attendee) {
+                                        if ($attendee['email'] == $_attendee['email']) {
+                                            $task['attendees'][$i] = $existing['attendees'][$j];
+                                            break;
+                                        }
+                                    }
+                                }
+                            }
+                        }
+
+                        // set status=CANCELLED on CANCEL messages
+                        if ($this->ical->method == 'CANCEL') {
+                            $task['status'] = 'CANCELLED';
+                        }
+                        // 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') {
+                $error_msg = null;
+            }
+            else {
+                $error_msg = $this->gettext('nowritetasklistfound');
+            }
+        }
+
+        if ($success) {
+            $message = $this->ical->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('tasklist_itip_after_action');
+
+            $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 ($this->ical->method == 'REQUEST' && $organizer && !$noreply && !in_array(strtolower($organizer['email']), $emails) && !$error_msg) {
+            $task['comment'] = rcube_utils::get_input_value('_comment', rcube_utils::INPUT_POST);
+            $itip = $this->load_itip();
+            $itip->set_sender_email($reply_sender);
+
+            if ($itip->send_itip_message($task, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
+                $this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $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 calendar/itip-status requests
+     */
+    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->driver->get_task($data);
+        $itip     = $this->load_itip();
+        $response = $itip->get_itip_status($data, $existing);
+
+        // get a list of writeable lists to save new tasks to
+        if (!$existing && $response['action'] == 'rsvp' || $response['action'] == 'import') {
+            $lists  = $this->driver->get_lists();
+            $select = new html_select(array('name' => 'tasklist', 'id' => 'itip-saveto', 'is_escaped' => true));
+            $num    = 0;
+
+            foreach ($lists as $list) {
+                if ($list['editable']) {
+                    $select->add($list['name'], $list['id']);
+                    $num++;
+                }
+            }
+
+            if ($num <= 1) {
+                $select = null;
+            }
+        }
+
+        if ($select) {
+            $default_list = $this->get_default_tasklist(true);
+            $response['select'] = html::span('folder-select', $this->gettext('saveintasklist') . ' ' .
+                $select->show($this->rc->config->get('tasklist_default_list', $default_list['id'])));
+        }
+
+        $this->rc->output->command('plugin.update_itip_object_status', $response);
+    }
+
+    /**
+     * Handler for calendar/itip-remove requests
+     */
+    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  *******/
 
@@ -1275,4 +1806,3 @@ class tasklist extends rcube_plugin
        return $this->driver->user_delete($args);
     }
 }
-
diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js
index 6e93b15..81e27f0 100644
--- a/plugins/tasklist/tasklist_base.js
+++ b/plugins/tasklist/tasklist_base.js
@@ -37,6 +37,7 @@ function rcube_tasklist(settings)
     /* public methods */
     this.create_from_mail = create_from_mail;
     this.mail2taskdialog = mail2task_dialog;
+    this.save_to_tasklist = save_to_tasklist;
 
 
     /**
@@ -80,21 +81,46 @@ function rcube_tasklist(settings)
         this.ui.edit_task(null, 'new', prop);
     }
 
+    // handler for attachment-save-tasklist commands
+    function save_to_tasklist()
+    {
+      // TODO: show dialog to select the tasklist for importing
+      if (this.selected_attachment && window.rcube_libcalendaring) {
+        rcmail.http_post('tasks/mailimportattach', {
+            _uid: rcmail.env.uid,
+            _mbox: rcmail.env.mailbox,
+            _part: this.selected_attachment,
+            // _list: $('#tasklist-attachment-saveto').val(),
+          }, rcmail.set_busy(true, 'itip.savingdata'));
+      }
+    }
+
 }
 
 /* tasklist plugin initialization (for email task) */
 window.rcmail && rcmail.env.task == 'mail' && rcmail.addEventListener('init', function(evt) {
     var tasks = new rcube_tasklist(rcmail.env.libcal_settings);
 
-    rcmail.register_command('tasklist-create-from-mail', function() { tasks.create_from_mail() });
-    rcmail.addEventListener('plugin.mail2taskdialog', function(p){ tasks.mail2taskdialog(p) });
-    rcmail.addEventListener('plugin.unlock_saving', function(p){ tasks.ui && tasks.ui.unlock_saving(); });
+    rcmail.register_command('tasklist-create-from-mail', function() { tasks.create_from_mail(); });
+    rcmail.register_command('attachment-save-task', function() { tasks.save_to_tasklist(); });
+    rcmail.addEventListener('plugin.mail2taskdialog', function(p) { tasks.mail2taskdialog(p); });
+    rcmail.addEventListener('plugin.unlock_saving', function(p) { tasks.ui && tasks.ui.unlock_saving(); });
 
     if (rcmail.env.action != 'show')
         rcmail.env.message_commands.push('tasklist-create-from-mail');
     else
         rcmail.enable_command('tasklist-create-from-mail', true);
 
+    rcmail.addEventListener('beforemenu-open', function(p) {
+        if (p.menu == 'attachmentmenu') {
+            tasks.selected_attachment = p.id;
+            var mimetype = rcmail.env.attachments[p.id],
+                is_ics = mimetype == 'text/calendar' || mimetype == 'text/x-vcalendar' || mimetype == 'application/ics';
+
+            rcmail.enable_command('attachment-save-task', is_ics);
+        }
+    });
+
     // add contextmenu item
     if (window.rcm_contextmenu_register_command) {
         rcm_contextmenu_register_command(
@@ -104,4 +130,3 @@ window.rcmail && rcmail.env.task == 'mail' && rcmail.addEventListener('init', fu
             'moveto');
         }
 });
-




More information about the commits mailing list