12 commits - plugins/calendar plugins/libcalendaring plugins/libkolab plugins/tasklist

Thomas Brüderli bruederli at kolabsys.com
Thu Jul 31 13:50:21 CEST 2014


 plugins/calendar/calendar.php                            |   25 
 plugins/libcalendaring/lib/libcalendaring_itip.php       |   37 
 plugins/libcalendaring/libcalendaring.php                |   18 
 plugins/libcalendaring/libvcalendar.php                  |    2 
 plugins/libcalendaring/localization/en_US.inc            |    8 
 plugins/libkolab/lib/kolab_format_event.php              |    2 
 plugins/libkolab/lib/kolab_format_task.php               |    2 
 plugins/libkolab/lib/kolab_format_xcal.php               |    5 
 plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php |   26 
 plugins/tasklist/drivers/tasklist_driver.php             |    1 
 plugins/tasklist/localization/en_US.inc                  |   73 +
 plugins/tasklist/skins/larry/images/attendee-status.png  |binary
 plugins/tasklist/skins/larry/images/badge_cancelled.png  |binary
 plugins/tasklist/skins/larry/images/loading_blue.gif     |binary
 plugins/tasklist/skins/larry/images/sendinvitation.png   |binary
 plugins/tasklist/skins/larry/images/tasklist.png         |binary
 plugins/tasklist/skins/larry/tasklist.css                |  277 ++++-
 plugins/tasklist/skins/larry/templates/mainview.html     |   17 
 plugins/tasklist/skins/larry/templates/taskedit.html     |   14 
 plugins/tasklist/tasklist.js                             |  608 +++++++++--
 plugins/tasklist/tasklist.php                            |  828 ++++++++++++++-
 plugins/tasklist/tasklist_base.js                        |   35 
 plugins/tasklist/tasklist_ui.php                         |  111 +-
 23 files changed, 1969 insertions(+), 120 deletions(-)

New commits:
commit 65989e7783942506873a0f554cf82cbb6c28188b
Merge: 93ce0d8 f5e9318
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 13:49:47 2014 +0200

    Merge branch 'dev/task-attendees'



commit f5e93184e3ef08d2e4f9d57f3ade9a41390fe610
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 12:49:27 2014 +0200

    Fix iTip replies to organizer (don't increment sequence if I'm not the owner)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index ef8f31d..bcc5c06 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -902,9 +902,15 @@ class tasklist_kolab_driver extends tasklist_driver
             unset($object['attachments']);
         }
 
-        $object['_owner'] = $identity['email'];
+        // allow sequence increments if I'm the organizer
+        if ($this->plugin->is_organizer($object)) {
+            unset($object['sequence']);
+        }
+        else if (isset($old['sequence'])) {
+            $object['sequence'] = $old['sequence'];
+        }
 
-        unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['sequence']);
+        unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
         return $object;
     }
 
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 3953977..96b5e82 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -949,10 +949,11 @@ a.morelink:hover {
 	display: block;
 	color: #333;
 	font-weight: bold;
-	padding: 8px 4px 3px 30px;
+	padding: 3px 4px 3px 30px;
 	text-shadow: 0px 1px 1px #fff;
 	text-decoration: none;
 	white-space: nowrap;
+	line-height: 20px;
 }
 
 #taskedit-attachments ul li a.file {
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 9da7a88..a0ff22f 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -992,16 +992,14 @@ function rcube_tasklist_ui(settings)
           notify = false, partstat = false, html = '';
 
         // task has attendees, ask whether to notify them
-        if (has_attendees(rec)) {
-            if (is_organizer(rec)) {
-                notify = true;
-                html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
-            }
-            // ask whether to change my partstat and notify organizer
-            else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
-              partstat = true;
-              html = rcmail.gettext('partstatupdatenotification', 'tasklist');
-            }
+        if (has_attendees(rec) && is_organizer(rec)) {
+            notify = true;
+            html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
+        }
+        // ask whether to change my partstat and notify organizer
+        else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
+          partstat = true;
+          html = rcmail.gettext('partstatupdatenotification', 'tasklist');
         }
 
         // remove to avoid endless recursion
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index ae76f99..728c4a3 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1097,6 +1097,16 @@ class tasklist extends rcube_plugin
         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  ********/
 
     /**


commit f8b6706074b394244b52d479e90dacd99f6c6cce
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 12:43:55 2014 +0200

    Fix 'create task from mail' function

diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 9ac70bc..ae76f99 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1128,11 +1128,17 @@ class tasklist extends rcube_plugin
 
         $texts['tasklist.newtask'] = $this->gettext('createfrommail');
 
+        // collect env variables
+        $env = array(
+            'tasklists' => array(),
+            'tasklist_settings' => $this->ui->load_settings(),
+        );
+
         $this->ui->init_templates();
         echo $this->api->output->parse('tasklist.taskedit', false, false);
         echo html::tag('link', array('rel' => 'stylesheet', 'type' => 'text/css', 'href' => $this->url($this->local_skin_path() . '/tagedit.css'), 'nl' => true));
         echo html::tag('script', array('type' => 'text/javascript'),
-            "rcmail.set_env('tasklists', " . json_encode($this->api->output->env['tasklists']) . ");\n".
+            "rcmail.set_env(" . json_encode($env) . ");\n".
             "rcmail.add_label(" . json_encode($texts) . ");\n"
         );
         exit;
diff --git a/plugins/tasklist/tasklist_base.js b/plugins/tasklist/tasklist_base.js
index 81e27f0..5448418 100644
--- a/plugins/tasklist/tasklist_base.js
+++ b/plugins/tasklist/tasklist_base.js
@@ -57,7 +57,7 @@ function rcube_tasklist(settings)
                     // rcmail.gui_object('attachmentlist', 'attachmentlist');
 
                     ui_loaded = true;
-                    me.ui = new rcube_tasklist_ui(settings);
+                    me.ui = new rcube_tasklist_ui($.extend(rcmail.env.tasklist_settings, settings));
                     create_from_mail(uid);  // start over
                 });
                 return;


commit 0d8b6912ae2864113a520b5f81a0d63447ecf8ff
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 12:22:07 2014 +0200

    Fix auto-incrementing the sequence value of xcal objects

diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
index 596d0da..c233f44 100644
--- a/plugins/libkolab/lib/kolab_format_event.php
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -26,7 +26,7 @@ class kolab_format_event extends kolab_format_xcal
 {
     public $CTYPEv2 = 'application/x-vnd.kolab.event';
 
-    public static $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+    public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
 
     protected $objclass = 'Event';
     protected $read_func = 'readEvent';
diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php
index ee0ca6a..52744d4 100644
--- a/plugins/libkolab/lib/kolab_format_task.php
+++ b/plugins/libkolab/lib/kolab_format_task.php
@@ -26,7 +26,7 @@ class kolab_format_task extends kolab_format_xcal
 {
     public $CTYPEv2 = 'application/x-vnd.kolab.task';
 
-    public static $scheduling_properties = array('start', 'due', 'summary', 'status');
+    public $scheduling_properties = array('start', 'due', 'summary', 'status');
 
     protected $objclass = 'Todo';
     protected $read_func = 'readTodo';
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 6624b02..7d077b7 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -29,7 +29,8 @@ abstract class kolab_format_xcal extends kolab_format
     public $CTYPE = 'application/calendar+xml';
 
     public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
-    public static $scheduling_properties = array('start', 'end', 'location');
+
+    public $scheduling_properties = array('start', 'end', 'location');
 
     protected $sensitivity_map = array(
         'public'       => kolabformat::ClassPublic,
@@ -315,7 +316,7 @@ abstract class kolab_format_xcal extends kolab_format
                 // increment sequence when updating properties relevant for scheduling.
                 // RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
                 // TODO: make the list of properties considered 'significant' for scheduling configurable
-                foreach (self::$scheduling_properties as $prop) {
+                foreach ($this->scheduling_properties as $prop) {
                     $a = $old[$prop];
                     $b = $object[$prop];
                     if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 6884083..ef8f31d 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -904,7 +904,7 @@ class tasklist_kolab_driver extends tasklist_driver
 
         $object['_owner'] = $identity['email'];
 
-        unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
+        unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags'], $object['sequence']);
         return $object;
     }
 


commit 457195102e070c098899da7972b2fe6a25f572aa
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 11:40:39 2014 +0200

    Complete iTip communication on task status changes: ask to notify the organizer on update or deletion + add icons for task-specific partstats and cancelled tasks

diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index b37faa8..a7ec1ea 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -51,6 +51,7 @@ $labels['newtask'] = 'New Task';
 $labels['edittask'] = 'Edit Task';
 $labels['save'] = 'Save';
 $labels['cancel'] = 'Cancel';
+$labels['saveandnotify'] = 'Save and Notify';
 $labels['addsubtask'] = 'Add subtask';
 $labels['deletetask'] = 'Delete task';
 $labels['deletethisonly'] = 'Delete this task only';
@@ -88,6 +89,9 @@ $labels['deleteparenttasktconfirm'] = 'Do you really want to delete this task an
 $labels['deletelistconfirm'] = 'Do you really want to delete this list with all its tasks?';
 $labels['deletelistconfirmrecursive'] = 'Do you really want to delete this list with all its sub-lists and tasks?';
 $labels['aclnorights'] = 'You do not have administrator rights on this task list.';
+$labels['changetaskconfirm'] = 'Update task';
+$labels['changeconfirmnotifications'] = 'Do you want to notify the attendees about the modification?';
+$labels['partstatupdatenotification'] = 'Do you want to notify the organizer about the status change?';
 
 // (hidden) titles and labels for accessibility annotations
 $labels['quickaddinput'] = 'New task date and title';
@@ -124,17 +128,27 @@ $labels['saveintasklist'] = 'save in ';
 // invitation handling (overrides labels from libcalendaring)
 $labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.';
 
-$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nAssignees: \$attendees";
 $labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nDue: \$date";
+$labels['itipmailbodyin-process'] = "\$sender has set the status of the following task to in-process:\n\n*\$title*\n\nDue: \$date";
+$labels['itipmailbodycompleted'] = "\$sender has completed the following task:\n\n*\$title*\n\nDue: \$date";
 
-$labels['itipdeclineevent'] = 'Do you want to decline your assignment to this task?';
+$labels['attendeeaccepted'] = 'Assignee has accepted';
+$labels['attendeetentative'] = 'Assignee has tentatively accepted';
+$labels['attendeedeclined'] = 'Assignee has declined';
+$labels['attendeedelegated'] = 'Assignee has delegated to $delegatedto';
+$labels['attendeein-process'] = 'Assignee is in-process';
+$labels['attendeecompleted'] = 'Assignee has completed';
+
+$labels['itipdeclinetask'] = 'Decline your assignment to this task to the organizer';
 $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';
 $labels['itipcomment'] = 'Invitation/notification comment';
-$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to participants';
-$labels['itipsendsuccess'] = 'Invitation sent to participants.';
-$labels['errornotifying'] = 'Failed to send notifications to task participants';
+$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to assignees';
+$labels['itipsendsuccess'] = 'Invitation sent to assignees';
+$labels['errornotifying'] = 'Failed to send notifications to task assignees';
+$labels['removefromcalendar'] = 'Remove from my tasks';
 
 $labels['andnmore'] = '$nr more...';
 $labels['delegatedto'] = 'Delegated to: ';
@@ -149,7 +163,7 @@ $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['itipresponseerror'] = 'Failed to send the response to this task assignment';
 $labels['itipinvalidrequest'] = 'This invitation is no longer valid';
-$labels['sentresponseto'] = 'Successfully sent invitation response to $mailto';
+$labels['sentresponseto'] = 'Successfully sent assignment response to $mailto';
 $labels['successremoval'] = 'The task has been deleted successfully.';
diff --git a/plugins/tasklist/skins/larry/images/attendee-status.png b/plugins/tasklist/skins/larry/images/attendee-status.png
index 59b4493..5343e60 100644
Binary files a/plugins/tasklist/skins/larry/images/attendee-status.png and b/plugins/tasklist/skins/larry/images/attendee-status.png differ
diff --git a/plugins/tasklist/skins/larry/images/badge_cancelled.png b/plugins/tasklist/skins/larry/images/badge_cancelled.png
new file mode 100644
index 0000000..b89029e
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/badge_cancelled.png differ
diff --git a/plugins/tasklist/skins/larry/images/ical-attachment.png b/plugins/tasklist/skins/larry/images/ical-attachment.png
deleted file mode 100644
index 8fa486a..0000000
Binary files a/plugins/tasklist/skins/larry/images/ical-attachment.png and /dev/null differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 1d891ed..3953977 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -815,6 +815,10 @@ ul.toolbarmenu li span.delete {
 	color: #999;
 }
 
+#taskshow.status-cancelled {
+	background: url(images/badge_cancelled.png) top right no-repeat;
+}
+
 #task-parent-title {
 	position: relative;
 	top: -0.6em;
@@ -1043,7 +1047,7 @@ label.block {
 	text-decoration: underline;
 }
 
-.task-attendees span.accepted {
+.task-attendees span.completed {
 	background-position: right -20px;
 }
 
@@ -1059,6 +1063,14 @@ label.block {
 	background-position: right -180px;
 }
 
+.task-attendees span.in-process {
+	background-position: right -200px;
+}
+
+.task-attendees span.accepted {
+	background-position: right -220px;
+}
+
 .task-attendees span.organizer {
 	background-position: right 100px;
 }
@@ -1082,12 +1094,7 @@ label.block {
 	width: 20em;
 }
 
-.ui-dialog .task-update-confirm {
-	padding: 0 0.5em 0.5em 0.5em;
-}
-
-.task-dialog-message,
-.task-update-confirm .message {
+.task-dialog-message {
 	margin-top: 0.5em;
 	padding: 0.8em;
 	border: 1px solid #ffdf0e;
@@ -1161,35 +1168,54 @@ div.tasklist-invitebox .rsvp-status.hint {
 }
 
 #event-partstat .changersvp,
+.edit-attendees-table td.confirmstate span,
 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  {
+div.tasklist-invitebox .rsvp-status.in-process,
+div.tasklist-invitebox .rsvp-status.completed,
+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 {
+div.tasklist-invitebox .rsvp-status.declined,
+.edit-attendees-table td.confirmstate span.declined {
 	background-position: 2px -40px;
 }
 
 #event-partstat .changersvp.tentative,
-div.tasklist-invitebox .rsvp-status.tentative {
+div.tasklist-invitebox .rsvp-status.tentative,
+.edit-attendees-table td.confirmstate span.tentative {
 	background-position: 2px -60px;
 }
 
 #event-partstat .changersvp.delegated,
-div.tasklist-invitebox .rsvp-status.delegated {
+div.tasklist-invitebox .rsvp-status.delegated,
+.edit-attendees-table td.confirmstate span.delegated {
 	background-position: 2px -180px;
 }
 
 #event-partstat .changersvp.needs-action,
-div.tasklist-invitebox .rsvp-status.needs-action {
+div.tasklist-invitebox .rsvp-status.needs-action,
+.edit-attendees-table td.confirmstate span.needs-action {
 	background-position: 2px 0;
 }
 
+#event-partstat .changersvp.in-process,
+div.tasklist-invitebox .rsvp-status.in-process,
+.edit-attendees-table td.confirmstate span.in-process {
+	background-position: 2px -200px;
+}
+
+#event-partstat .changersvp.accepted,
+div.tasklist-invitebox .rsvp-status.accepted,
+.edit-attendees-table td.confirmstate span.accepted {
+	background-position: 2px -220px;
+}
+
 
 /** 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.js b/plugins/tasklist/tasklist.js
index 1b10773..9da7a88 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -353,9 +353,8 @@ function rcube_tasklist_ui(settings)
                     if (rcmail.busy)
                         return false;
 
-                    rec.status = e.target.checked ? 'COMPLETED' : (rec.complete == 1 ? 'NEEDS-ACTION' : '');
-                    li.toggleClass('complete');
-                    save_task(rec, 'edit');
+                    save_task_confirm(rec, 'edit', { _status_before:rec.status + '', status:e.target.checked ? 'COMPLETED' : (rec.complete > 0 ? 'IN-PROCESS' : 'NEEDS-ACTION') });
+                    item.toggleClass('complete');
                     return true;
 
                 case 'flagged':
@@ -363,7 +362,7 @@ function rcube_tasklist_ui(settings)
                         return false;
 
                     rec.flagged = rec.flagged ? 0 : 1;
-                    li.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
+                    item.toggleClass('flagged').find('.flagged:first').attr('aria-checked', (rec.flagged ? 'true' : 'false'));
                     save_task(rec, 'edit');
                     break;
 
@@ -377,8 +376,7 @@ function rcube_tasklist_ui(settings)
                     input.datepicker($.extend({
                         onClose: function(dateText, inst) {
                             if (dateText != (rec.date || '')) {
-                                rec.date = dateText;
-                                save_task(rec, 'edit');
+                                save_task_confirm(rec, 'edit', { date:dateText });
                             }
                             input.datepicker('destroy').remove();
                             link.html(dateText || rcmail.gettext('nodate','tasklist'));
@@ -971,6 +969,10 @@ function rcube_tasklist_ui(settings)
      */
     function save_task(rec, action)
     {
+        // show confirmation dialog when status of an assigned task has changed
+        if (rec._status_before !== undefined && is_attendee(rec))
+            return save_task_confirm(rec, action);
+
         if (!rcmail.busy) {
             saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
             rcmail.http_post('tasks/task', { action:action, t:rec, filter:filtermask });
@@ -982,6 +984,84 @@ function rcube_tasklist_ui(settings)
     }
 
     /**
+     * Display confirm dialog when modifying/deleting a task record
+     */
+    var save_task_confirm = function(rec, action, updates)
+    {
+        var data = $.extend({}, rec, updates || {}),
+          notify = false, partstat = false, html = '';
+
+        // task has attendees, ask whether to notify them
+        if (has_attendees(rec)) {
+            if (is_organizer(rec)) {
+                notify = true;
+                html = rcmail.gettext('changeconfirmnotifications', 'tasklist');
+            }
+            // ask whether to change my partstat and notify organizer
+            else if (data._status_before !== undefined && data.status && data._status_before != data.status && is_attendee(rec)) {
+              partstat = true;
+              html = rcmail.gettext('partstatupdatenotification', 'tasklist');
+            }
+        }
+
+        // remove to avoid endless recursion
+        delete data._status_before;
+
+        // show dialog
+        if (html) {
+            var $dialog = $('<div>').html(html);
+
+            var buttons = [];
+            buttons.push({
+                text: rcmail.gettext('saveandnotify', 'tasklist'),
+                click: function() {
+                    if (notify)   data._notify = 1;
+                    if (partstat) data._reportpartstat = data.status == 'CANCELLED' ? 'DECLINED' : data.status;
+                    save_task(data, action);
+                    $(this).dialog('close');
+                }
+            });
+            buttons.push({
+                text: rcmail.gettext('save', 'tasklist'),
+                click: function() {
+                    save_task(data, action);
+                    $(this).dialog('close');
+                }
+            });
+            buttons.push({
+                text: rcmail.gettext('cancel', 'tasklist'),
+                click: function() {
+                    $(this).dialog('close');
+                    if (updates)
+                      render_task(rec, rec.id);  // restore previous state
+                }
+            });
+
+            $dialog.dialog({
+                modal: true,
+                width: 460,
+                closeOnEscapeType: false,
+                dialogClass: 'warning no-close',
+                title: rcmail.gettext('changetaskconfirm', 'tasklist'),
+                buttons: buttons,
+                open: function() {
+                    setTimeout(function(){
+                      $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+                    }, 5);
+                },
+                close: function(){
+                    $dialog.dialog('destroy').remove();
+                }
+            }).addClass('task-update-confirm').show();
+
+            return true;
+        }
+
+        // do update
+        return save_task(data, action);
+    }
+
+    /**
      * Remove saving lock and free the UI for new input
      */
     function unlock_saving()
@@ -1488,6 +1568,12 @@ function rcube_tasklist_ui(settings)
         if ($dialog.is(':ui-dialog'))
           $dialog.dialog('close');
 
+        // remove status-* classes
+        $dialog.removeClass(function(i, oldclass) {
+            var oldies = String(oldclass).split(' ');
+            return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0 }).join(' ');
+        });
+
         if (!(rec = listdata[id]) || clear_popups({}))
             return;
 
@@ -1528,6 +1614,10 @@ function rcube_tasklist_ui(settings)
             });
         }
 
+        if (rec.status) {
+          $dialog.addClass('status-' + String(rec.status).toLowerCase());
+        }
+
         if (rec.recurrence && rec.recurrence_text) {
             $('#task-recurrence').show().children('.task-text').html(Q(rec.recurrence_text));
         }
@@ -1817,6 +1907,7 @@ function rcube_tasklist_ui(settings)
         var buttons = {};
         buttons[rcmail.gettext('save', 'tasklist')] = function() {
             var data = me.selected_task;
+            data._status_before = me.selected_task.status + '';
 
             // copy form field contents into task object to save
             $.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus, list:tasklist }, function(key,input){
@@ -1867,6 +1958,8 @@ function rcube_tasklist_ui(settings)
             data.complete = complete.val() / 100;
             if (isNaN(data.complete))
                 data.complete = null;
+            else if (data.complete == 1.0 && rec.status === '')
+                data.status = 'COMPLETED';
 
             if (!data.list && list.id)
                 data.list = list.id;
@@ -1879,11 +1972,6 @@ function rcube_tasklist_ui(settings)
                 delete data.organizer;
             }
 
-            // don't submit attendees if only myself is added as organizer
-            if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && String(data.attendees[0].email).toLowerCase() == settings.identity.email) {
-                data.attendees = [];
-            }
-
             // per-attendee notification suppression
             var need_invitation = false;
             if (allow_invitations) {
@@ -2050,7 +2138,34 @@ function rcube_tasklist_ui(settings)
         if (!rec || rec.readonly || rcmail.busy)
             return false;
 
-        var html, buttons = [];
+        var html, buttons = [], $dialog = $('<div>');
+
+        // Subfunction to submit the delete command after confirm
+        var _delete_task = function(id, mode) {
+            var rec = listdata[id],
+                li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide(),
+                decline = $dialog.find('input.confirm-attendees-decline:checked').length,
+                notify = $dialog.find('input.confirm-attendees-notify:checked').length;
+
+            saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
+            rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list, _decline:decline, _notify:notify }, mode:mode, filter:filtermask });
+
+            // move childs to parent/root
+            if (mode != 1 && rec.children !== undefined) {
+                var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
+                if (!parent_node || !parent_node.length)
+                    parent_node = rcmail.gui_objects.resultlist;
+
+                $.each(rec.children, function(i,cid) {
+                    var child = listdata[cid];
+                    child.parent_id = rec.parent_id;
+                    resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
+                });
+            }
+
+            li.remove();
+            delete listdata[id];
+        }
 
         if (rec.children && rec.children.length) {
             html = rcmail.gettext('deleteparenttasktconfirm','tasklist');
@@ -2080,6 +2195,19 @@ function rcube_tasklist_ui(settings)
             });
         }
 
+        if (is_attendee(rec)) {
+            html += '<div class="task-dialog-message">' +
+                '<label><input class="confirm-attendees-decline" type="checkbox" checked="checked" value="1" name="_decline" /> ' +
+                    rcmail.gettext('itipdeclinetask', 'tasklist') + 
+                '</label></div>';
+        }
+        else if (has_attendees(rec) && is_organizer(rec)) {
+            html += '<div class="task-dialog-message">' +
+                '<label><input class="confirm-attendees-notify" type="checkbox" checked="checked" value="1" name="_notify" /> ' +
+                    rcmail.gettext('sendcancellation', 'tasklist') + 
+                '</label></div>';
+        }
+
         buttons.push({
             text: rcmail.gettext('cancel', 'tasklist'),
             click: function() {
@@ -2087,11 +2215,11 @@ function rcube_tasklist_ui(settings)
             }
         });
 
-        var $dialog = $('<div>').html(html);
+        $dialog.html(html);
         $dialog.dialog({
           modal: true,
           width: 520,
-          dialogClass: 'warning',
+          dialogClass: 'warning no-close',
           title: rcmail.gettext('deletetask', 'tasklist'),
           buttons: buttons,
           close: function(){
@@ -2103,34 +2231,6 @@ function rcube_tasklist_ui(settings)
     }
 
     /**
-     * Subfunction to submit the delete command after confirm
-     */
-    function _delete_task(id, mode)
-    {
-        var rec = listdata[id],
-            li = $('li[rel="'+id+'"]', rcmail.gui_objects.resultlist).hide();
-
-        saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
-        rcmail.http_post('task', { action:'delete', t:{ id:rec.id, list:rec.list }, mode:mode, filter:filtermask });
-
-        // move childs to parent/root
-        if (mode != 1 && rec.children !== undefined) {
-            var parent_node = rec.parent_id ? $('li[rel="'+rec.parent_id+'"] > .childtasks', rcmail.gui_objects.resultlist) : null;
-            if (!parent_node || !parent_node.length)
-                parent_node = rcmail.gui_objects.resultlist;
-
-            $.each(rec.children, function(i,cid) {
-                var child = listdata[cid];
-                child.parent_id = rec.parent_id;
-                resort_task(child, $('li[rel="'+cid+'"]').appendTo(parent_node), true);
-            });
-        }
-
-        li.remove();
-        delete listdata[id];
-    }
-
-    /**
      * Check if the given task matches the current filtermask and tag selection
      */
     function match_filter(rec, cache, recursive)
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 62ddea0..9ac70bc 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -342,6 +342,24 @@ class tasklist extends rcube_plugin
                     $this->rc->output->show_message('tasklist.errornotifying', 'error');
             }
         }
+        else if ($success && $rec['_reportpartstat']) {
+            // get the full record after update
+            $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']);
+
+                $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');
@@ -367,6 +385,7 @@ class tasklist extends rcube_plugin
             require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
             $this->itip = new libcalendaring_itip($this, 'tasklist');
             $this->itip->set_rsvp_actions(array('accepted','declined'));
+            $this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
         }
 
         return $this->itip;
@@ -517,6 +536,20 @@ class tasklist extends rcube_plugin
 
         $rec['attachments'] = $attachments;
 
+        // convert invalid data
+        if (isset($rec['attendees']) && !is_array($rec['attendees']))
+            $rec['attendees'] = array();
+
+        // 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']) && ($identity = $this->rc->user->get_identity($rec['_identity']))) {
             $rec['organizer'] = array('name' => $identity['name'], 'email' => $identity['email']);
@@ -1044,9 +1077,25 @@ class tasklist extends rcube_plugin
         else if ($start > $weeklimit || ($rec['date'] && $duedate > $weeklimit))
             $mask |= self::FILTER_MASK_LATER;
 
+        // TODO: add mask for "assigned to me"
+
         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;
+    }
 
     /*******  UI functions  ********/
 
@@ -1709,7 +1758,7 @@ class tasklist extends rcube_plugin
             $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');
+            $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);
 
             $this->rc->output->command('plugin.itip_message_processed', $metadata);
             $error_msg = null;
@@ -1725,7 +1774,7 @@ class tasklist extends rcube_plugin
             $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['name'] : $organizer['email']))), 'confirmation');
+                $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');
         }
@@ -1813,6 +1862,7 @@ class tasklist extends rcube_plugin
     public function to_libcal($task)
     {
         $object = $task;
+        $object['_type'] = 'task';
         $object['categories'] = (array)$task['tags'];
 
         // convert to datetime objects


commit e46cc9499efb8539be4a8701155c32fabbd81d28
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Jul 31 11:36:18 2014 +0200

    Add support for task-specific participant status values

diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index ece0e48..93bdfee 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -60,6 +60,11 @@ class libcalendaring_itip
         $this->rsvp_status = array_merge($this->rsvp_actions, array('delegated'));
     }
 
+    public function set_rsvp_status($status)
+    {
+        $this->rsvp_status = $status;
+    }
+
     /**
      * Wrapper for rcube_plugin::gettext()
      * Checking for a label in different domains
@@ -246,7 +251,8 @@ class libcalendaring_itip
         // attach ics file for this event
         $ical = libcalendaring::get_ical();
         $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
-        $message->addAttachment($ics, 'text/calendar', 'event.ics', false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
+        $filename = $event['_type'] == 'task' ? 'todo.ics' : 'event.ics';
+        $message->addAttachment($ics, 'text/calendar', $filename, false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
 
         return $message;
     }
@@ -321,9 +327,10 @@ class libcalendaring_itip
           $listed = false;
           foreach ($existing['attendees'] as $attendee) {
             if ($attendee['role'] != 'ORGANIZER' && strcasecmp($attendee['email'], $event['attendee']) == 0) {
-              if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED','DELEGATED'))) {
-                $html = html::div('rsvp-status ' . strtolower($status), $this->gettext(array(
-                    'name' => 'attendee'.strtolower($status),
+              $status_lc = strtolower($status);
+              if (in_array($status_lc, $this->rsvp_status)) {
+                $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
+                    'name' => 'attendee' . $status_lc,
                     'vars' => array(
                         'delegatedto' => Q($attendee['delegated-to'] ?: '?'),
                     )
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 7e8c717..f3a506b 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -86,6 +86,8 @@ $labels['itipobjectnotfound'] = 'The object referred by this message was not fou
 $labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
 $labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
 $labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
+$labels['itipsubjectin-process'] = '"$title" is in-process by $name';
+$labels['itipsubjectcompleted'] = '"$title" was completed by $name';
 $labels['itipsubjectcancel'] = 'Your participation in "$title" has been cancelled';
 
 $labels['itipnewattendee'] = 'This is a reply from a new participant';
@@ -99,18 +101,24 @@ $labels['youhaveaccepted'] = 'You have accepted this invitation';
 $labels['youhavetentative'] = 'You have tentatively accepted this invitation';
 $labels['youhavedeclined'] = 'You have declined this invitation';
 $labels['youhavedelegated'] = 'You have delegated this invitation';
+$labels['youhavein-process'] = 'You are working on this assignment';
+$labels['youhavecompleted'] = 'You have completed this assignment';
 $labels['youhaveneeds-action'] = 'Your response to this invitation is still pending';
 
 $labels['youhavepreviouslyaccepted'] = 'You have previously accepted this invitation';
 $labels['youhavepreviouslytentative'] = 'You have previously accepted this invitation tentatively';
 $labels['youhavepreviouslydeclined'] = 'You have previously declined this invitation';
 $labels['youhavepreviouslydelegated'] = 'You have previously delegated this invitation';
+$labels['youhavepreviouslyin-process'] = 'You have previously reported to work on this assignment';
+$labels['youhavepreviouslycompleted'] = 'You have previously completed this assignment';
 $labels['youhavepreviouslyneeds-action'] = 'Your response to this invitation is still pending';
 
 $labels['attendeeaccepted'] = 'Participant has accepted';
 $labels['attendeetentative'] = 'Participant has tentatively accepted';
 $labels['attendeedeclined'] = 'Participant has declined';
 $labels['attendeedelegated'] = 'Participant has delegated to $delegatedto';
+$labels['attendeein-process'] = 'Participant is in-process';
+$labels['attendeecompleted'] = 'Participant has completed';
 $labels['notanattendee'] = 'You\'re not listed as an attendee of this object';
 $labels['outdatedinvitation'] = 'This invitation has been replaced by a newer version';
 


commit 228a1b2438e507a002e106f18638c9bd44e77fea
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 30 17:51:54 2014 +0200

    Hide 'Save to calendar' option for VTODO attachments (#3227)

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 343b3e5..4768048 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2240,6 +2240,7 @@ class calendar extends rcube_plugin
     }
 
     $html = '';
+    $has_events = false;
     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;
@@ -2255,16 +2256,20 @@ class calendar extends rcube_plugin
         if ($event['_type'] != 'event')  // skip non-event objects (#2928)
           continue;
 
+        $has_events = true;
+
         // get prepared inline UI for this event object
-        $html .= html::div('calendar-invitebox',
-          $this->itip->mail_itip_inline_ui(
-            $event,
-            $this->ical->method,
-            $mime_id.':'.$idx,
-            'calendar',
-            rcube_utils::anytodatetime($this->message->headers->date)
-          )
-        );
+        if ($this->ical->method) {
+          $html .= html::div('calendar-invitebox',
+            $this->itip->mail_itip_inline_ui(
+              $event,
+              $this->ical->method,
+              $mime_id.':'.$idx,
+              'calendar',
+              rcube_utils::anytodatetime($this->message->headers->date)
+            )
+          );
+        }
 
         // limit listing
         if ($idx >= 3)
@@ -2280,7 +2285,7 @@ class calendar extends rcube_plugin
     }
 
     // add "Save to calendar" button into attachment menu
-    if (!empty($this->ics_parts)) {
+    if ($has_events) {
       $this->add_button(array(
         'id'         => 'attachmentsavecal',
         'name'       => 'attachmentsavecal',
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 0757c63..62ddea0 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1305,6 +1305,7 @@ class tasklist extends rcube_plugin
         // @todo: Calendar plugin does the same, which means the
         // attachment body is fetched twice, this is not optimal
         $html = '';
+        $has_tasks = false;
         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;
@@ -1322,16 +1323,20 @@ class tasklist extends rcube_plugin
                     continue;
                 }
 
+                $has_tasks = true;
+
                 // 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)
-                    )
-                );
+                if ($this->ical->method) {
+                    $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) {
@@ -1348,8 +1353,10 @@ class tasklist extends rcube_plugin
             $p['content'] = $html . $p['content'];
 
             $this->rc->output->add_label('tasklist.savingdata','tasklist.deletetaskconfirm','tasklist.declinedeleteconfirm');
+        }
 
-            // add "Save to calendar" button into attachment menu
+        // add "Save to tasks" button into attachment menu
+        if ($has_tasks) {
             $this->add_button(array(
                 'id'         => 'attachmentsavetask',
                 'name'       => 'attachmentsavetask',


commit b3c5acd66a088248c5634bdb14e5faa3e2011cf4
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Jul 30 17:40:53 2014 +0200

    - Fix task attendees and organizer setting and display
    - Make basic iTip exchange for task assignments work
    - Improve wording for task assignments

diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index a6fca9c..ece0e48 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -213,6 +213,14 @@ class libcalendaring_itip
                 $event['attendees'] = $reply_attendees;
             }
         }
+        // set RSVP=TRUE for every attendee if not set
+        else if ($method == 'REQUEST') {
+            foreach ($event['attendees'] as $i => $attendee) {
+                if (!isset($attendee['rsvp'])) {
+                    $event['attendees'][$i]['rsvp']= true;
+                }
+            }
+        }
 
         // compose multipart message using PEAR:Mail_Mime
         $message = new Mail_mime("\r\n");
@@ -532,15 +540,21 @@ class libcalendaring_itip
     }
 
     /**
-     * Render event details in a table
+     * Render event/task details in a table
      */
     function itip_object_details_table($event, $title)
     {
         $table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
         $table->add('ititle', $title);
         $table->add('title', Q($event['title']));
-        $table->add('label', $this->plugin->gettext('date'), $this->domain);
-        $table->add('date', Q($this->lib->event_date_text($event)));
+        if ($event['start'] && $event['end']) {
+            $table->add('label', $this->plugin->gettext('date'), $this->domain);
+            $table->add('date', Q($this->lib->event_date_text($event)));
+        }
+        else if ($event['due'] && $event['_type'] == 'task') {
+            $table->add('label', $this->plugin->gettext('date'), $this->domain);
+            $table->add('date', Q($this->lib->event_date_text($event)));
+        }
         if ($event['location']) {
             $table->add('label', $this->plugin->gettext('location'), $this->domain);
             $table->add('location', Q($event['location']));
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 5a1a8b0..36fc287 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -247,11 +247,25 @@ class libcalendaring extends rcube_plugin
      */
     public function event_date_text($event, $tzinfo = false)
     {
-        $fromto = '';
+        $fromto = '--';
+
+        // handle task objects
+        if ($event['_type'] == 'task' && is_object($event['due'])) {
+            $date_format = $event['due']->_dateonly ? self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])) : null;
+            $fromto = $this->rc->format_date($event['due'], $date_format, false);
+
+            // add timezone information
+            if ($fromto && $tzinfo && ($tzname = $this->timezone->getName())) {
+                $fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
+            }
+
+            return $fromto;
+        }
 
         // abort if no valid event dates are given
-        if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime'))
+        if (!is_object($event['start']) || !is_a($event['start'], 'DateTime') || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) {
             return $fromto;
+        }
 
         $duration = $event['start']->diff($event['end'])->format('s');
 
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 855e074..a89cec2 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -567,7 +567,7 @@ class libvcalendar implements Iterator
         }
 
         // make organizer part of the attendees list for compatibility reasons
-        if (!empty($event['organizer']) && is_array($event['attendees'])) {
+        if (!empty($event['organizer']) && is_array($event['attendees']) && $event['_type'] == 'event') {
             array_unshift($event['attendees'], $event['organizer']);
         }
 
diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 624bdd5..6884083 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -774,6 +774,8 @@ class tasklist_kolab_driver extends tasklist_driver
             'parent_id' => $record['parent_id'],
             'recurrence' => $record['recurrence'],
             'attendees' => $record['attendees'],
+            'organizer' => $record['organizer'],
+            'sequence' => $record['sequence'],
         );
 
         // convert from DateTime to internal date format
@@ -817,8 +819,8 @@ class tasklist_kolab_driver extends tasklist_driver
     }
 
     /**
-    * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
-    * (opposite of self::_to_rcube_event())
+     * Convert the given task record into a data structure that can be passed to kolab_storage backend for saving
+     * (opposite of self::_to_rcube_event())
      */
     private function _from_rcube_task($task, $old = array())
     {
@@ -826,14 +828,14 @@ class tasklist_kolab_driver extends tasklist_driver
         $object['categories'] = (array)$task['tags'];
 
         if (!empty($task['date'])) {
-            $object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone);
+            $object['due'] = rcube_utils::anytodatetime($task['date'].' '.$task['time'], $this->plugin->timezone);
             if (empty($task['time']))
                 $object['due']->_dateonly = true;
             unset($object['date']);
         }
 
         if (!empty($task['startdate'])) {
-            $object['start'] = new DateTime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone);
+            $object['start'] = rcube_utils::anytodatetime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone);
             if (empty($task['starttime']))
                 $object['start']->_dateonly = true;
             unset($object['startdate']);
@@ -900,12 +902,6 @@ class tasklist_kolab_driver extends tasklist_driver
             unset($object['attachments']);
         }
 
-        // set current user as ORGANIZER
-        $identity = $this->rc->user->get_identity();
-        if (empty($object['attendees']) && $identity['email']) {
-            $object['attendees'] = array(array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']));
-        }
-
         $object['_owner'] = $identity['email'];
 
         unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index f43fbb9..b37faa8 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -33,6 +33,7 @@ $labels['status-needs-action'] = 'Needs action';
 $labels['status-in-process'] = 'In process';
 $labels['status-completed'] = 'Completed';
 $labels['status-cancelled'] = 'Cancelled';
+$labels['assignedto'] = 'Assigned to';
 
 $labels['all'] = 'All';
 $labels['flagged'] = 'Flagged';
@@ -98,35 +99,35 @@ $labels['arialabeltaskselector'] = 'List mode';
 $labels['arialabeltasklisting'] = 'Tasks listing';
 
 // attendees
-$labels['attendee'] = 'Participant';
+$labels['attendee'] = 'Assignee';
 $labels['role'] = 'Role';
 $labels['availability'] = 'Avail.';
 $labels['confirmstate'] = 'Status';
-$labels['addattendee'] = 'Add participant';
+$labels['addattendee'] = 'Add assignee';
 $labels['roleorganizer'] = 'Organizer';
 $labels['rolerequired'] = 'Required';
 $labels['roleoptional'] = 'Optional';
 $labels['rolechair'] = 'Chair';
-$labels['rolenonparticipant'] = 'Absent';
+$labels['rolenonparticipant'] = 'Observer';
 $labels['sendinvitations'] = 'Send invitations';
-$labels['sendnotifications'] = 'Notify participants about modifications';
-$labels['sendcancellation'] = 'Notify participants about task cancellation';
-$labels['invitationsubject'] = 'You\'ve been invited to "$title"';
-$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the task details which you can import to your tasks application.";
-$labels['invitationattendlinks'] = "In case your email client doesn't support iTip requests you can use the following link to either accept or decline this invitation:\n\$url";
-$labels['eventupdatesubject'] = '"$title" has been updated';
-$labels['eventupdatesubjectempty'] = 'A task that concerns you has been updated';
-$labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with the updated task details which you can import to your tasks application.";
-$labels['eventcancelsubject'] = '"$title" has been canceled';
-$labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details.";
+$labels['sendnotifications'] = 'Notify assignees about modifications';
+$labels['sendcancellation'] = 'Notify assignees about task cancellation';
+$labels['invitationsubject'] = 'You\'ve been assigned to "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nPlease find attached an iCalendar file with all the task details which you can import to your tasks application.";
+$labels['itipupdatesubject'] = '"$title" has been updated';
+$labels['itipupdatesubjectempty'] = 'A task that concerns you has been updated';
+$labels['itipupdatemailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nPlease find attached an iCalendar file with the updated task details which you can import to your tasks application.";
+$labels['itipcancelsubject'] = '"$title" has been canceled';
+$labels['itipcancelmailbody'] = "*\$title*\n\nDue: \$date\n\nAssignees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details.";
+$labels['saveintasklist'] = 'save in ';
 
 // invitation handling (overrides labels from libcalendaring)
 $labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.';
 
-$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
-$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nDue: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nDue: \$date";
 
 $labels['itipdeclineevent'] = 'Do you want to decline your assignment to this task?';
 $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index bcdf29a..1d891ed 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -855,6 +855,14 @@ a.morelink:hover {
 	margin-top: 0.5em;
 }
 
+.edit-attendees-table tbody td {
+	padding: 4px 7px;
+}
+
+.edit-attendees-table tbody tr:last-child td {
+	border-bottom: 0;
+}
+
 .edit-attendees-table th.role,
 .edit-attendees-table td.role {
 	width: 9em;
@@ -864,18 +872,19 @@ a.morelink:hover {
 .edit-attendees-table td.availability,
 .edit-attendees-table th.confirmstate,
 .edit-attendees-table td.confirmstate {
-	width: 4em;
+	width: 6em;
 }
 
 .edit-attendees-table th.options,
 .edit-attendees-table td.options {
-	width: 16px;
+	width: 24px;
 	padding: 2px 4px;
+	text-align: right;
 }
 
 .edit-attendees-table th.sendmail,
 .edit-attendees-table td.sendmail {
-	width: 44px;
+	width: 48px;
 	padding: 2px;
 }
 
@@ -955,7 +964,7 @@ a.morelink:hover {
 div.form-section {
 	position: relative;
 	margin-top: 0.2em;
-	margin-bottom: 0.8em;
+	margin-bottom: 0.5em;
 }
 
 .form-section label {
@@ -970,6 +979,10 @@ label.block {
 	margin-bottom: 0.3em;
 }
 
+#task-description {
+	margin-bottom: 1em;
+}
+
 #taskedit-completeness-slider {
 	display: inline-block;
 	margin-left: 2em;
@@ -1047,7 +1060,7 @@ label.block {
 }
 
 .task-attendees span.organizer {
-	background-position: right -80px;
+	background-position: right 100px;
 }
 
 #all-task-attendees span.attendee {
diff --git a/plugins/tasklist/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index 1881c76..727d31f 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -156,12 +156,16 @@
 		<label><roundcube:label name="tasklist.alarms" /></label>
 		<span class="task-text"></span>
 	</div>
-	<div class="form-section task-attendees" id="task-attendees">
-		<h5 class="label"><roundcube:label name="tasklist.tabassignments" /></h5>
-		<div class="task-text"></div>
+	<div id="task-attendees" class="form-section task-attendees">
+		<label><roundcube:label name="tasklist.assignedto" /></label>
+		<span class="task-text"></span>
+	</div>
+	<div id="task-organizer" class="form-section task-attendees">
+		<label><roundcube:label name="tasklist.roleorganizer" /></label>
+		<span class="task-text"></span>
 	</div>
 <!--
-	<div class="form-section" id="task-partstat">
+	<div id="task-partstat" class="form-section">
 		<label><roundcube:label name="tasklist.mystatus" /></label>
 		<span class="changersvp" role="button" tabindex="0" title="<roundcube:label name='tasklist.changepartstat' />">
 			<span class="task-text"></span>
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index 554028b..25c5796 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -81,6 +81,10 @@
 		</div>
 		<!-- attendees list (assignments) -->
 		<div id="taskedit-panel-attendees">
+			<div class="form-section" id="taskedit-organizer">
+				<label for="edit-identities-list"><roundcube:label name="tasklist.roleorganizer" /></label>
+				<roundcube:object name="plugin.identity_select" id="edit-identities-list" />
+			</div>
 			<h3 id="aria-label-attendeestable" class="voice"><roundcube:label name="tasklist.arialabeleventassignments" /></h3>
 			<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" aria-labelledby="aria-label-attendeestable" />
 			<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 03aed93..1b10773 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -84,7 +84,6 @@ function rcube_tasklist_ui(settings)
     var focused_subclass;
     var task_attendees = [];
     var attendees_list;
-//    var resources_list;
     var me = this;
 
     // general datepicker settings
@@ -1349,7 +1348,7 @@ function rcube_tasklist_ui(settings)
     };
 
     // check if the current user is an attendee of this task
-    var is_attendee = function(task, role, email)
+    var is_attendee = function(task, email, role)
     {
         var i, attendee, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
 
@@ -1366,7 +1365,10 @@ function rcube_tasklist_ui(settings)
     // check if the current user is the organizer
     var is_organizer = function(task, email)
     {
-        return is_attendee(task, 'ORGANIZER', email) || !task.id;
+        if (!email) email = task.organizer ? task.organizer.email : null;
+        if (email)
+            return settings.identity.emails.indexOf(';'+email) >= 0;
+        return true;
     };
 
     // add the given list of participants
@@ -1421,34 +1423,10 @@ function rcube_tasklist_ui(settings)
         if (exists)
             return false;
 
-//      var list = me.selected_task && me.tasklists[me.selected_task.list] ? me.tasklists[me.selected_task.list] : me.tasklists[me.selected_list];
-
         var dispname = Q(data.name || data.email);
         if (data.email)
             dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
 
-        // role selection
-        var opts = {}, organizer = data.role == 'ORGANIZER';
-        if (organizer)
-            opts.ORGANIZER = rcmail.gettext('tasklist.roleorganizer');
-        opts['REQ-PARTICIPANT'] = rcmail.gettext('tasklist.rolerequired');
-        opts['OPT-PARTICIPANT'] = rcmail.gettext('tasklist.roleoptional');
-        opts['NON-PARTICIPANT'] = rcmail.gettext('tasklist.rolenonparticipant');
-
-        if (data.cutype != 'RESOURCE')
-            opts['CHAIR'] =  rcmail.gettext('tasklist.rolechair');
-
-        if (organizer && !readonly)
-            dispname = rcmail.env['identities-selector'];
-
-        var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','tasklist') + '">';
-        for (var r in opts)
-            select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
-        select += '</select>';
-
-        // availability
-        var avail = data.email ? 'loading' : 'unknown';
-
         // delete icon
         var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
         var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
@@ -1463,18 +1441,15 @@ function rcube_tasklist_ui(settings)
         else if (data['delegated-from'])
             tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
 
-        var html = '<td class="role">' + select + '</td>' +
-            '<td class="name">' + dispname + '</td>' +
-//            '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
+        var html = '<td class="name">' + dispname + '</td>' +
             '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
-            (data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (organizer || readonly || !invbox ? '' : invbox) + '</td>' : '') +
-            '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
+            (data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (readonly || !invbox ? '' : invbox) + '</td>' : '') +
+            '<td class="options">' + (readonly ? '' : dellink) + '</td>';
 
-        var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
         var tr = $('<tr>')
             .addClass(String(data.role).toLowerCase())
             .html(html)
-            .appendTo(table);
+            .appendTo(attendees_list);
 
         tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
         tr.find('a.mailtolink').click(task_attendee_click);
@@ -1483,15 +1458,6 @@ function rcube_tasklist_ui(settings)
             $('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
         });
 
-        // select organizer identity
-        if (data.identity_id)
-            $('#edit-identities-list').val(data.identity_id);
-
-      // check free-busy status
-//      if (avail == 'loading') {
-//        check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_task);
-//      }
-
         task_attendees.push(data);
         return true;
     };
@@ -1499,15 +1465,8 @@ function rcube_tasklist_ui(settings)
     // event handler for clicks on an attendee link
     var task_attendee_click = function(e)
     {
-        var cutype = $(this).attr('data-cutype'),
-            mailto = this.href.substr(7);
-
-        if (rcmail.env.tasklist_resources && cutype == 'RESOURCE') {
-            task_resources_dialog(mailto);
-        }
-        else {
-            rcmail.redirect(rcmail.url('mail/compose', {_to: mailto}));
-        }
+        var mailto = this.href.substr(7);
+        rcmail.command('compose', mailto);
 
         return false;
     };
@@ -1547,7 +1506,7 @@ function rcube_tasklist_ui(settings)
         $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%');
         $('#task-status')[(rec.status ? 'show' : 'hide')]().children('.task-text').html(rcmail.gettext('status-'+String(rec.status).toLowerCase(),'tasklist'));
         $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
-        $('#task-attendees').hide();
+        $('#task-attendees, #task-organizer').hide();
 
         var itags = get_inherited_tags(rec);
         var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty();
@@ -1587,6 +1546,7 @@ function rcube_tasklist_ui(settings)
 
         // list task attendees
         if (list.attendees && rec.attendees) {
+            console.log(rec.attendees)
 /*
             // sort resources to the end
             rec.attendees.sort(function(a,b) {
@@ -1595,30 +1555,19 @@ function rcube_tasklist_ui(settings)
                 return (j - k);
             });
 */
-            var j, data, dispname, tooltip, organizer = false, rsvp = false, mystatus = null, line, morelink, html = '', overflow = '';
+            var j, data, rsvp = false, mystatus = null, line, morelink, html = '', overflow = '',
+                organizer = is_organizer(rec);
+
             for (j=0; j < rec.attendees.length; j++) {
                 data = rec.attendees[j];
-                dispname = Q(data.name || data.email);
-                tooltip = '';
-
-                if (data.email) {
-                    tooltip = data.email;
-                    dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
-                    if (data.role == 'ORGANIZER')
-                        organizer = true;
-                    else if (settings.identity.emails.indexOf(';'+data.email) >= 0) {
-                        mystatus = data.status.toLowerCase();
-                        if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
-                            rsvp = mystatus;
-                    }
-                }
 
-                if (data['delegated-to'])
-                    tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
-                else if (data['delegated-from'])
-                    tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
+                if (data.email && settings.identity.emails.indexOf(';'+data.email) >= 0) {
+                    mystatus = data.status.toLowerCase();
+                    if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
+                        rsvp = mystatus;
+                }
 
-                line = '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
+                line = task_attendee_html(data);
 
                 if (morelink)
                     overflow += line;
@@ -1631,7 +1580,7 @@ function rcube_tasklist_ui(settings)
                 }
             }
 
-            if (html && (rec.attendees.length > 1 || !organizer)) {
+            if (html) {
                 $('#task-attendees').show()
                     .children('.task-text')
                     .html(html)
@@ -1667,6 +1616,10 @@ function rcube_tasklist_ui(settings)
             $('#task-rsvp a.reply-comment-toggle').show();
             $('#task-rsvp .itip-reply-comment textarea').hide().val('');
 */
+
+            if (rec.organizer && !organizer) {
+                $('#task-organizer').show().children('.task-text').html(task_attendee_html(rec.organizer));
+            }
         }
 
         // define dialog buttons
@@ -1697,7 +1650,7 @@ function rcube_tasklist_ui(settings)
           closeOnEscape: true,
           title: rcmail.gettext('taskdetails', 'tasklist'),
           open: function() {
-            $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
+              $dialog.parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().focus();
           },
           close: function() {
               $dialog.dialog('destroy').appendTo(document.body);
@@ -1711,6 +1664,24 @@ function rcube_tasklist_ui(settings)
         me.dialog_resize($dialog.get(0), $dialog.height(), 580);
     }
 
+    // render HTML code for displaying an attendee record
+    function task_attendee_html(data)
+    {
+        var dispname = Q(data.name || data.email), tooltip = '';
+
+        if (data.email) {
+          tooltip = data.email;
+          dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+        }
+
+        if (data['delegated-to'])
+          tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
+        else if (data['delegated-from'])
+          tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
+
+        return '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
+    }
+
     /**
      * Opens the dialog to edit a task
      */
@@ -1786,18 +1757,17 @@ function rcube_tasklist_ui(settings)
 
         task_attendees = [];
         attendees_list = $('#edit-attendees-table > tbody').html('');
-        //resources_list = $('#edit-resources-table > tbody').html('');
         $('#edit-attendees-notify')[(notify.checked && allow_invitations ? 'show' : 'hide')]();
         $('#edit-localchanges-warning')[(has_attendees(rec) && !(allow_invitations || (rec.owner && is_organizer(rec, rec.owner))) ? 'show' : 'hide')]();
 
-        var load_attendees_tab = function()
-        {
+        // attendees (aka assignees)
+        if (list.attendees) {
             var j, data, reply_selected = 0;
             if (rec.attendees) {
                 for (j=0; j < rec.attendees.length; j++) {
                     data = rec.attendees[j];
                     add_attendee(data, !allow_invitations);
-                    if (allow_invitations && data.role != 'ORGANIZER' && !data.noreply) {
+                    if (allow_invitations && !data.noreply) {
                         reply_selected++;
                     }
                 }
@@ -1812,16 +1782,16 @@ function rcube_tasklist_ui(settings)
             // select the correct organizer identity
             var identity_id = 0;
             $.each(settings.identities, function(i,v) {
-                if (organizer && v == organizer.email) {
+                if (rec.organizer && v == rec.organizer.email) {
                     identity_id = i;
                     return false;
                 }
             });
 
-            $('#edit-identities-list').val(identity_id);
             $('#edit-attendees-form')[(allow_invitations?'show':'hide')]();
-//            $('#edit-attendee-schedule')[(tasklist.freebusy?'show':'hide')]();
-        };
+            $('#edit-identities-list').val(identity_id);
+            $('#taskedit-organizer')[(organizer ? 'show' : 'hide')]();
+        }
 
         // attachments
         rcmail.enable_command('remove-attachment', list.editable);
@@ -1904,15 +1874,9 @@ function rcube_tasklist_ui(settings)
             if (!data.tags.length)
                 data.tags = '';
 
-            // read attendee roles
-            $('select.edit-attendee-role').each(function(i, elem) {
-                if (data.attendees[i]) {
-                    data.attendees[i].role = $(elem).val();
-                }
-            });
-
             if (organizer) {
                 data._identity = $('#edit-identities-list option:selected').val();
+                delete data.organizer;
             }
 
             // don't submit attendees if only myself is added as organizer
@@ -1977,9 +1941,6 @@ function rcube_tasklist_ui(settings)
 
         // set dialog size according to content
         me.dialog_resize($dialog.get(0), $dialog.height(), 580);
-
-        if (list.attendees)
-            window.setTimeout(load_attendees_tab, 1);
     }
 
     /**
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 76ef251..0757c63 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -334,7 +334,7 @@ class tasklist extends rcube_plugin
             $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($event, $old)) {
+            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');
@@ -366,10 +366,7 @@ class tasklist extends rcube_plugin
         if (!$this->itip) {
             require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
             $this->itip = new libcalendaring_itip($this, 'tasklist');
-
-//            if ($this->rc->config->get('kolab_invitation_tasklists')) {
-//                $this->itip->set_rsvp_actions(array('accepted','tentative','declined','needs-action'));
-//            }
+            $this->itip->set_rsvp_actions(array('accepted','declined'));
         }
 
         return $this->itip;
@@ -520,6 +517,11 @@ class tasklist extends rcube_plugin
 
         $rec['attachments'] = $attachments;
 
+        // set organizer from identity selector
+        if (isset($rec['_identity']) && ($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']);
 
@@ -646,7 +648,8 @@ class tasklist extends rcube_plugin
 
         // compose multipart message using PEAR:Mail_Mime
         $method  = $action == 'delete' ? 'CANCEL' : 'REQUEST';
-        $message = $itip->compose_itip_message($task, $method);
+        $object = $this->to_libcal($task);
+        $message = $itip->compose_itip_message($object, $method);
 
         // list existing attendees from the $old task
         $old_attendees = array();
@@ -671,11 +674,11 @@ class tasklist extends rcube_plugin
 
             // which template to use for mail text
             $is_new   = !in_array($attendee['email'], $old_attendees);
-            $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
-            $subject  = $is_cancelled ? 'eventcancelsubject'  : ($is_new ? 'invitationsubject' : ($task['title'] ? 'eventupdatesubject' : 'eventupdatesubjectempty'));
+            $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($task, $method, $attendee, $subject, $bodytext, $message))
+            if ($itip->send_itip_message($object, $method, $attendee, $subject, $bodytext, $message))
                 $sent++;
             else
                 $sent = -100;
@@ -683,16 +686,16 @@ class tasklist extends rcube_plugin
 
         // send CANCEL message to removed attendees
         foreach ((array)$old['attendees'] as $attendee) {
-            if ($attendee['ROLE'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
+            if (!$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
                 continue;
             }
 
-            $vevent = $old;
-            $vevent['cancelled'] = $is_cancelled;
-            $vevent['attendees'] = array($attendee);
-            $vevent['comment']   = $comment;
+            $vtodo = $this->to_libcal($old);
+            $vtodo['cancelled'] = $is_cancelled;
+            $vtodo['attendees'] = array($attendee);
+            $vtodo['comment']   = $comment;
 
-            if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody'))
+            if ($itip->send_itip_message($vtodo, 'CANCEL', $attendee, 'itipcancelsubject', 'itipcancelmailbody'))
                 $sent++;
             else
                 $sent = -100;
@@ -1393,6 +1396,8 @@ class tasklist extends rcube_plugin
 
         // successfully parsed events?
         if (!empty($tasks) && ($task = $tasks[$index])) {
+            $task = $this->from_ical($task);
+
             // 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']);
@@ -1496,6 +1501,7 @@ class tasklist extends rcube_plugin
             foreach ($tasks as $task) {
                 // save to tasklist
                 if ($list && $list['editable'] && $task['_type'] == 'task') {
+                    $task = $this->from_ical($task);
                     $task['list'] = $list['id'];
 
                     if (!$this->driver->get_task($task['uid'])) {
@@ -1555,14 +1561,11 @@ class tasklist extends rcube_plugin
 
             // update my attendee status according to submitted method
             if (!empty($status)) {
-                $organizer = null;
+                $organizer = $task['organizer'];
                 $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)) {
+                    if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
                         $metadata['attendee'] = $attendee['email'];
                         $metadata['rsvp']     = $attendee['role'] != 'NON-PARTICIPANT';
                         $reply_sender         = $attendee['email'];
@@ -1714,7 +1717,7 @@ class tasklist extends rcube_plugin
             $itip = $this->load_itip();
             $itip->set_sender_email($reply_sender);
 
-            if ($itip->send_itip_message($task, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
+            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['name'] : $organizer['email']))), 'confirmation');
             else
                 $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
@@ -1729,7 +1732,7 @@ class tasklist extends rcube_plugin
     /**
      * Handler for calendar/itip-status requests
      */
-    function task_itip_status()
+    public function task_itip_status()
     {
         $data = rcube_utils::get_input_value('data', rcube_utils::INPUT_POST, true);
 
@@ -1768,7 +1771,7 @@ class tasklist extends rcube_plugin
     /**
      * Handler for calendar/itip-remove requests
      */
-    function task_itip_remove()
+    public function task_itip_remove()
     {
         $success = false;
         $uid     = rcube_utils::get_input_value('uid', rcube_utils::INPUT_POST);
@@ -1798,6 +1801,78 @@ class tasklist extends rcube_plugin
     }
 
     /**
+     * Map task properties for ical exprort using libcalendaring
+     */
+    public function to_libcal($task)
+    {
+        $object = $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 (!$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)
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 5fc0a20..21b322e 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -56,7 +56,6 @@ class tasklist_ui
 
         // copy config to client
         $this->rc->output->set_env('tasklist_settings', $this->load_settings());
-        $this->rc->output->set_env('identities-selector', $this->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->plugin->gettext('roleorganizer'))));
 
         // initialize attendees autocompletion
         $this->rc->autocomplete_init();
@@ -71,7 +70,7 @@ class tasklist_ui
     {
         $settings = array();
 
-        //$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
+        $settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', 0);
 
         // get user identity to create default attendee
         foreach ($this->rc->user->list_identities() as $rec) {
@@ -128,6 +127,7 @@ class tasklist_ui
         $this->plugin->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));
         $this->plugin->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
         $this->plugin->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
+        $this->plugin->register_handler('plugin.identity_select', array($this, 'identity_select'));
         $this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
 
         $this->plugin->include_script('jquery.tagedit.js');
@@ -438,9 +438,8 @@ class tasklist_ui
         $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite'));
         $table  = new html_table(array('cols' => 4 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
 
-        $table->add_header('role', $this->plugin->gettext('role'));
+//      $table->add_header('role', $this->plugin->gettext('role'));
         $table->add_header('name', $this->plugin->gettext($attrib['coltitle'] ?: 'attendee'));
-//        $table->add_header('availability', $this->plugin->gettext('availability'));
         $table->add_header('confirmstate', $this->plugin->gettext('confirmstate'));
         if ($invitations) {
             $table->add_header(array('class' => 'sendmail', 'title' => $this->plugin->gettext('sendinvitations')),


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');
         }
 });
-


commit f2c6a3851d85a31d43057b5179c8b3216c23cb59
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Fri Jul 25 14:08:41 2014 +0200

    More changes for Assignments tab (#1165)

diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
index 1999a8e..624bdd5 100644
--- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
+++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php
@@ -25,16 +25,17 @@
 class tasklist_kolab_driver extends tasklist_driver
 {
     // features supported by the backend
-    public $alarms = false;
+    public $alarms      = false;
     public $attachments = true;
-    public $undelete = false; // task undelete action
+    public $attendees   = true;
+    public $undelete    = false; // task undelete action
     public $alarm_types = array('DISPLAY','AUDIO');
 
     private $rc;
     private $plugin;
     private $lists;
     private $folders = array();
-    private $tasks = array();
+    private $tasks   = array();
 
 
     /**
@@ -772,6 +773,7 @@ class tasklist_kolab_driver extends tasklist_driver
             'status' => $record['status'],
             'parent_id' => $record['parent_id'],
             'recurrence' => $record['recurrence'],
+            'attendees' => $record['attendees'],
         );
 
         // convert from DateTime to internal date format
@@ -898,6 +900,14 @@ class tasklist_kolab_driver extends tasklist_driver
             unset($object['attachments']);
         }
 
+        // set current user as ORGANIZER
+        $identity = $this->rc->user->get_identity();
+        if (empty($object['attendees']) && $identity['email']) {
+            $object['attendees'] = array(array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']));
+        }
+
+        $object['_owner'] = $identity['email'];
+
         unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']);
         return $object;
     }
diff --git a/plugins/tasklist/drivers/tasklist_driver.php b/plugins/tasklist/drivers/tasklist_driver.php
index eb7663e..dd2e415 100644
--- a/plugins/tasklist/drivers/tasklist_driver.php
+++ b/plugins/tasklist/drivers/tasklist_driver.php
@@ -72,6 +72,7 @@ abstract class tasklist_driver
     // features supported by the backend
     public $alarms = false;
     public $attachments = false;
+    public $attendees = false;
     public $undelete = false; // task undelete action
     public $sortable = false;
     public $alarm_types = array('DISPLAY');
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 7920c94..0b71377 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -132,3 +132,9 @@ $labels['itipdeclineevent'] = 'Do you want to decline your assignment to this ta
 $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';
 $labels['itipcomment'] = 'Invitation/notification comment';
 $labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to participants';
+$labels['itipsendsuccess'] = 'Invitation sent to participants.';
+$labels['errornotifying'] = 'Failed to send notifications to task participants';
+
+$labels['andnmore'] = '$nr more...';
+$labels['delegatedto'] = 'Delegated to: ';
+$labels['delegatedfrom'] = 'Delegated from: ';
diff --git a/plugins/tasklist/skins/larry/images/attendee-status.png b/plugins/tasklist/skins/larry/images/attendee-status.png
new file mode 100644
index 0000000..59b4493
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/attendee-status.png differ
diff --git a/plugins/tasklist/skins/larry/images/sendinvitation.png b/plugins/tasklist/skins/larry/images/sendinvitation.png
new file mode 100644
index 0000000..ecdaa09
Binary files /dev/null and b/plugins/tasklist/skins/larry/images/sendinvitation.png differ
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index 1e847b3..7bdfe57 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -1013,6 +1013,49 @@ label.block {
 	outline: none;
 }
 
+.task-attendees span.attendee {
+	padding-right: 18px;
+	margin-right: 0.5em;
+	background: url(images/attendee-status.png) right 0 no-repeat;
+}
+
+.task-attendees span.attendee a.mailtolink {
+	text-decoration: none;
+	white-space: nowrap;
+	outline: none;
+}
+
+.task-attendees span.attendee a.mailtolink:hover {
+	text-decoration: underline;
+}
+
+.task-attendees span.accepted {
+	background-position: right -20px;
+}
+
+.task-attendees span.declined {
+	background-position: right -40px;
+}
+
+.task-attendees span.tentative {
+	background-position: right -60px;
+}
+
+.task-attendees span.delegated {
+	background-position: right -180px;
+}
+
+.task-attendees span.organizer {
+	background-position: right -80px;
+}
+
+#all-task-attendees span.attendee {
+	display: block;
+	margin-bottom: 0.4em;
+	padding-bottom: 0.3em;
+	border-bottom: 1px solid #ddd;
+}
+
 .tasklistview .uidialog .tabbed {
 	min-width: 600px;
 }
@@ -1025,6 +1068,22 @@ label.block {
 	width: 20em;
 }
 
+.ui-dialog .task-update-confirm {
+	padding: 0 0.5em 0.5em 0.5em;
+}
+
+.task-dialog-message,
+.task-update-confirm .message {
+	margin-top: 0.5em;
+	padding: 0.8em;
+	border: 1px solid #ffdf0e;
+	background-color: #fef893;
+}
+
+.task-dialog-message .message,
+.task-update-confirm .message {
+	margin-bottom: 0.5em;
+}
 
 /** 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/skins/larry/templates/mainview.html b/plugins/tasklist/skins/larry/templates/mainview.html
index 3003067..1881c76 100644
--- a/plugins/tasklist/skins/larry/templates/mainview.html
+++ b/plugins/tasklist/skins/larry/templates/mainview.html
@@ -156,6 +156,19 @@
 		<label><roundcube:label name="tasklist.alarms" /></label>
 		<span class="task-text"></span>
 	</div>
+	<div class="form-section task-attendees" id="task-attendees">
+		<h5 class="label"><roundcube:label name="tasklist.tabassignments" /></h5>
+		<div class="task-text"></div>
+	</div>
+<!--
+	<div class="form-section" id="task-partstat">
+		<label><roundcube:label name="tasklist.mystatus" /></label>
+		<span class="changersvp" role="button" tabindex="0" title="<roundcube:label name='tasklist.changepartstat' />">
+			<span class="task-text"></span>
+			<a class="iconbutton edit"><roundcube:label name='tasklist.changepartstat' /></a>
+		</span>
+	</div>
+-->
 	<div id="task-list" class="form-section">
 		<label><roundcube:label name="tasklist.list" /></label>
 		<span class="task-text"></span>
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index 599974e..554028b 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -98,5 +98,5 @@
 			<roundcube:object name="plugin.filedroparea" id="taskedit-tab-2" />
 		</div>
 	</form>
-	<roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="taskedit-dialog-message" style="display:none" />
+	<roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="task-dialog-message" style="display:none" />
 </div>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index b0c95ba..03aed93 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -572,6 +572,19 @@ function rcube_tasklist_ui(settings)
           input.val('');
         }
       });
+
+      // handle change of "send invitations" checkbox
+      $('#edit-attendees-invite').change(function() {
+        $('#edit-attendees-donotify,input.edit-attendee-reply').prop('checked', this.checked);
+        // hide/show comment field
+        $('.attendees-commentbox')[this.checked ? 'show' : 'hide']();
+      });
+
+      // delegate change task to "send invitations" checkbox
+      $('#edit-attendees-donotify').change(function() {
+        $('#edit-attendees-invite').click();
+        return false;
+      });
     }
 
     /**
@@ -1338,11 +1351,13 @@ function rcube_tasklist_ui(settings)
     // check if the current user is an attendee of this task
     var is_attendee = function(task, role, email)
     {
-        var i, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
+        var i, attendee, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
 
         for (i=0; task.attendees && i < task.attendees.length; i++) {
-            if ((!role || task.attendees[i].role == role) && task.attendees[i].email && emails.indexOf(';'+task.attendees[i].email.toLowerCase()) >= 0)
-                return task.attendees[i];
+            attendee = task.attendees[i];
+            if ((!role || attendee.role == role) && attendee.email && emails.indexOf(';'+attendee.email.toLowerCase()) >= 0) {
+                return attendee;
+            }
         }
 
         return false;
@@ -1357,140 +1372,144 @@ function rcube_tasklist_ui(settings)
     // add the given list of participants
     var add_attendees = function(names, params)
     {
-      names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
-
-      // parse name/email pairs
-      var item, email, name, success = false;
-      for (var i=0; i < names.length; i++) {
-        email = name = '';
-        item = $.trim(names[i]);
-
-        if (!item.length) {
-          continue;
-        } // address in brackets without name (do nothing)
-        else if (item.match(/^<[^@]+@[^>]+>$/)) {
-          email = item.replace(/[<>]/g, '');
-        } // address without brackets and without name (add brackets)
-        else if (rcube_check_email(item)) {
-          email = item;
-        } // address with name
-        else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
-          email = RegExp.$1;
-          name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
-        }
-        if (email) {
-          add_attendee($.extend({ email:email, name:name }, params));
-          success = true;
-        }
-        else {
-          alert(rcmail.gettext('noemailwarning'));
+        names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
+
+        // parse name/email pairs
+        var i, item, email, name, success = false;
+        for (i=0; i < names.length; i++) {
+            email = name = '';
+            item = $.trim(names[i]);
+
+            if (!item.length) {
+                continue;
+            }
+            // address in brackets without name (do nothing)
+            else if (item.match(/^<[^@]+@[^>]+>$/)) {
+                email = item.replace(/[<>]/g, '');
+            }
+            // address without brackets and without name (add brackets)
+            else if (rcube_check_email(item)) {
+                email = item;
+            }
+            // address with name
+            else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
+                email = RegExp.$1;
+                name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
+            }
+
+            if (email) {
+                add_attendee($.extend({ email:email, name:name }, params));
+                success = true;
+            }
+            else {
+                alert(rcmail.gettext('noemailwarning'));
+            }
         }
-      }
 
-      return success;
+        return success;
     };
 
     // add the given attendee to the list
     var add_attendee = function(data, readonly)
     {
-      if (!me.selected_task)
-        return false;
+        if (!me.selected_task)
+            return false;
 
-      // check for dupes...
-      var exists = false;
-      $.each(task_attendees, function(i, v) { exists |= (v.email == data.email); });
-      if (exists)
-        return false;
+        // check for dupes...
+        var exists = false;
+        $.each(task_attendees, function(i, v) { exists |= (v.email == data.email); });
+        if (exists)
+            return false;
 
-      var list = me.selected_task && me.tasklists[me.selected_task.list] ? me.tasklists[me.selected_task.list] : me.tasklists[me.selected_list];
-
-      var dispname = Q(data.name || data.email);
-      if (data.email)
-        dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
-
-      // role selection
-      var organizer = data.role == 'ORGANIZER';
-      var opts = {};
-      if (organizer)
-        opts.ORGANIZER = rcmail.gettext('.roleorganizer');
-      opts['REQ-PARTICIPANT'] = rcmail.gettext('tasklist.rolerequired');
-      opts['OPT-PARTICIPANT'] = rcmail.gettext('tasklist.roleoptional');
-      opts['NON-PARTICIPANT'] = rcmail.gettext('tasklist.rolenonparticipant');
-
-      if (data.cutype != 'RESOURCE')
-        opts['CHAIR'] =  rcmail.gettext('tasklist.rolechair');
-
-      if (organizer && !readonly)
-          dispname = rcmail.env['identities-selector'];
-
-      var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','tasklist') + '">';
-      for (var r in opts)
-        select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
-      select += '</select>';
-
-      // availability
-      var avail = data.email ? 'loading' : 'unknown';
-
-      // delete icon
-      var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
-      var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
-      var tooltip = data.status || '';
-
-      // send invitation checkbox
-      var invbox = '<input type="checkbox" class="edit-attendee-reply" value="' + Q(data.email) +'" title="' + Q(rcmail.gettext('tasklist.sendinvitations')) + '" '
-        + (!data.noreply ? 'checked="checked" ' : '') + '/>';
-
-      if (data['delegated-to'])
-        tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
-      else if (data['delegated-from'])
-        tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
-
-      var html = '<td class="role">' + select + '</td>' +
-        '<td class="name">' + dispname + '</td>' +
-//        '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
-        '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
-        (data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (organizer || readonly || !invbox ? '' : invbox) + '</td>' : '') +
-        '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
-
-      var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
-      var tr = $('<tr>')
-        .addClass(String(data.role).toLowerCase())
-        .html(html)
-        .appendTo(table);
-
-      tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
-      tr.find('a.mailtolink').click(task_attendee_click);
-      tr.find('input.edit-attendee-reply').click(function() {
-        var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
-        $('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
-      });
+//      var list = me.selected_task && me.tasklists[me.selected_task.list] ? me.tasklists[me.selected_task.list] : me.tasklists[me.selected_list];
+
+        var dispname = Q(data.name || data.email);
+        if (data.email)
+            dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+
+        // role selection
+        var opts = {}, organizer = data.role == 'ORGANIZER';
+        if (organizer)
+            opts.ORGANIZER = rcmail.gettext('tasklist.roleorganizer');
+        opts['REQ-PARTICIPANT'] = rcmail.gettext('tasklist.rolerequired');
+        opts['OPT-PARTICIPANT'] = rcmail.gettext('tasklist.roleoptional');
+        opts['NON-PARTICIPANT'] = rcmail.gettext('tasklist.rolenonparticipant');
+
+        if (data.cutype != 'RESOURCE')
+            opts['CHAIR'] =  rcmail.gettext('tasklist.rolechair');
+
+        if (organizer && !readonly)
+            dispname = rcmail.env['identities-selector'];
+
+        var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','tasklist') + '">';
+        for (var r in opts)
+            select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
+        select += '</select>';
+
+        // availability
+        var avail = data.email ? 'loading' : 'unknown';
+
+        // delete icon
+        var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
+        var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
+        var tooltip = data.status || '';
+
+        // send invitation checkbox
+        var invbox = '<input type="checkbox" class="edit-attendee-reply" value="' + Q(data.email) +'" title="' + Q(rcmail.gettext('tasklist.sendinvitations')) + '" '
+            + (!data.noreply ? 'checked="checked" ' : '') + '/>';
+
+        if (data['delegated-to'])
+            tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
+        else if (data['delegated-from'])
+            tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
+
+        var html = '<td class="role">' + select + '</td>' +
+            '<td class="name">' + dispname + '</td>' +
+//            '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
+            '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
+            (data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (organizer || readonly || !invbox ? '' : invbox) + '</td>' : '') +
+            '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
+
+        var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
+        var tr = $('<tr>')
+            .addClass(String(data.role).toLowerCase())
+            .html(html)
+            .appendTo(table);
+
+        tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
+        tr.find('a.mailtolink').click(task_attendee_click);
+        tr.find('input.edit-attendee-reply').click(function() {
+            var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
+            $('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
+        });
 
-      // select organizer identity
-      if (data.identity_id)
-        $('#edit-identities-list').val(data.identity_id);
+        // select organizer identity
+        if (data.identity_id)
+            $('#edit-identities-list').val(data.identity_id);
 
       // check free-busy status
 //      if (avail == 'loading') {
 //        check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_task);
 //      }
 
-      task_attendees.push(data);
-      return true;
+        task_attendees.push(data);
+        return true;
     };
 
     // event handler for clicks on an attendee link
     var task_attendee_click = function(e)
     {
-      var cutype = $(this).attr('data-cutype'),
-        mailto = this.href.substr(7);
+        var cutype = $(this).attr('data-cutype'),
+            mailto = this.href.substr(7);
 
-      if (rcmail.env.calendar_resources && cutype == 'RESOURCE') {
-        event_resources_dialog(mailto);
-      }
-      else {
-        rcmail.redirect(rcmail.url('mail/compose', { _to:mailto }));
-      }
-      return false;
+        if (rcmail.env.tasklist_resources && cutype == 'RESOURCE') {
+            task_resources_dialog(mailto);
+        }
+        else {
+            rcmail.redirect(rcmail.url('mail/compose', {_to: mailto}));
+        }
+
+        return false;
     };
 
     // remove an attendee from the list
@@ -1528,6 +1547,7 @@ function rcube_tasklist_ui(settings)
         $('#task-completeness .task-text').html(((rec.complete || 0) * 100) + '%');
         $('#task-status')[(rec.status ? 'show' : 'hide')]().children('.task-text').html(rcmail.gettext('status-'+String(rec.status).toLowerCase(),'tasklist'));
         $('#task-list .task-text').html(Q(me.tasklists[rec.list] ? me.tasklists[rec.list].name : ''));
+        $('#task-attendees').hide();
 
         var itags = get_inherited_tags(rec);
         var taglist = $('#task-tags')[(rec.tags && rec.tags.length || itags.length ? 'show' : 'hide')]().children('.task-text').empty();
@@ -1565,6 +1585,90 @@ function rcube_tasklist_ui(settings)
             }
         }
 
+        // list task attendees
+        if (list.attendees && rec.attendees) {
+/*
+            // sort resources to the end
+            rec.attendees.sort(function(a,b) {
+                var j = a.cutype == 'RESOURCE' ? 1 : 0,
+                    k = b.cutype == 'RESOURCE' ? 1 : 0;
+                return (j - k);
+            });
+*/
+            var j, data, dispname, tooltip, organizer = false, rsvp = false, mystatus = null, line, morelink, html = '', overflow = '';
+            for (j=0; j < rec.attendees.length; j++) {
+                data = rec.attendees[j];
+                dispname = Q(data.name || data.email);
+                tooltip = '';
+
+                if (data.email) {
+                    tooltip = data.email;
+                    dispname = '<a href="mailto:' + data.email + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+                    if (data.role == 'ORGANIZER')
+                        organizer = true;
+                    else if (settings.identity.emails.indexOf(';'+data.email) >= 0) {
+                        mystatus = data.status.toLowerCase();
+                        if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
+                            rsvp = mystatus;
+                    }
+                }
+
+                if (data['delegated-to'])
+                    tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
+                else if (data['delegated-from'])
+                    tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
+
+                line = '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + dispname + '</span> ';
+
+                if (morelink)
+                    overflow += line;
+                else
+                    html += line;
+
+                // stop listing attendees
+                if (j == 7 && rec.attendees.length >= 7) {
+                    morelink = $('<a href="#more" class="morelink"></a>').html(rcmail.gettext('andnmore', 'tasklist').replace('$nr', rec.attendees.length - j - 1));
+                }
+            }
+
+            if (html && (rec.attendees.length > 1 || !organizer)) {
+                $('#task-attendees').show()
+                    .children('.task-text')
+                    .html(html)
+                    .find('a.mailtolink').click(task_attendee_click);
+
+                // display all attendees in a popup when clicking the "more" link
+                if (morelink) {
+                    $('#task-attendees .task-text').append(morelink);
+                    morelink.click(function(e) {
+                        rcmail.show_popup_dialog(
+                            '<div id="all-task-attendees" class="task-attendees">' + html + overflow + '</div>',
+                            rcmail.gettext('tabattendees', 'tasklist'),
+                            null,
+                            {width: 450, modal: false}
+                        );
+                        $('#all-task-attendees a.mailtolink').click(task_attendee_click);
+                        return false;
+                    });
+                }
+            }
+/*
+            if (mystatus && !rsvp) {
+                $('#task-partstat').show().children('.changersvp')
+                    .removeClass('accepted tentative declined delegated needs-action')
+                    .addClass(mystatus)
+                    .children('.task-text')
+                    .html(Q(rcmail.gettext('itip' + mystatus, 'libcalendaring')));
+            }
+
+            $('#task-rsvp')[(rsvp && !is_organizer(event) && rec.status != 'CANCELLED' ? 'show' : 'hide')]();
+            $('#task-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+mystatus+']').prop('disabled', true);
+
+            $('#task-rsvp a.reply-comment-toggle').show();
+            $('#task-rsvp .itip-reply-comment textarea').hide().val('');
+*/
+        }
+
         // define dialog buttons
         var buttons = [];
         if (list.editable && !rec.readonly) {
@@ -1874,7 +1978,7 @@ function rcube_tasklist_ui(settings)
         // set dialog size according to content
         me.dialog_resize($dialog.get(0), $dialog.height(), 580);
 
-        if (tasklist.attendees)
+        if (list.attendees)
             window.setTimeout(load_attendees_tab, 1);
     }
 
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index d227f54..34b1ffb 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -55,6 +55,7 @@ class tasklist extends rcube_plugin
     public $home;  // declare public to be used in other classes
 
     private $collapsed_tasks = array();
+    private $itip;
 
 
     /**
@@ -64,7 +65,7 @@ class tasklist extends rcube_plugin
     {
         $this->require_plugin('libcalendaring');
 
-        $this->rc = rcube::get_instance();
+        $this->rc  = rcube::get_instance();
         $this->lib = libcalendaring::get_instance();
 
         $this->register_task('tasks', 'tasklist');
@@ -188,7 +189,7 @@ class tasklist extends rcube_plugin
     {
         $filter = intval(get_input_value('filter', RCUBE_INPUT_GPC));
         $action = get_input_value('action', RCUBE_INPUT_GPC);
-        $rec  = get_input_value('t', RCUBE_INPUT_POST, true);
+        $rec    = get_input_value('t', RCUBE_INPUT_POST, true);
         $oldrec = $rec;
         $success = $refresh = false;
 
@@ -318,8 +319,24 @@ class tasklist extends rcube_plugin
             $this->rc->output->show_message('successfullysaved', 'confirmation');
             $this->update_counts($oldrec, $refresh);
         }
-        else
+        else {
             $this->rc->output->show_message('tasklist.errorsaving', 'error');
+        }
+
+        // send out notifications
+        if ($success && $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($event, $old)) {
+                $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');
+            }
+        }
 
         // unlock client
         $this->rc->output->command('plugin.unlock_saving');
@@ -337,6 +354,23 @@ class tasklist extends rcube_plugin
     }
 
     /**
+     * Load iTIP functions
+     */
+    private function load_itip()
+    {
+        if (!$this->itip) {
+            require_once realpath(__DIR__ . '/../libcalendaring/lib/libcalendaring_itip.php');
+            $this->itip = new libcalendaring_itip($this, 'tasklist');
+
+//            if ($this->rc->config->get('kolab_invitation_tasklists')) {
+//                $this->itip->set_rsvp_actions(array('accepted','tentative','declined','needs-action'));
+//            }
+        }
+
+        return $this->itip;
+    }
+
+    /**
      * repares new/edited task properties before save
      */
     private function prepare_task($rec)
@@ -587,6 +621,106 @@ class tasklist extends rcube_plugin
     }
 
     /**
+     * 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();
+
+        // 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';
+        $message = $itip->compose_itip_message($task, $method);
+
+        // 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']) {
+                continue;
+            }
+
+            // which template to use for mail text
+            $is_new   = !in_array($attendee['email'], $old_attendees);
+            $bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
+            $subject  = $is_cancelled ? 'eventcancelsubject'  : ($is_new ? 'invitationsubject' : ($task['title'] ? 'eventupdatesubject' : 'eventupdatesubjectempty'));
+
+            // finally send the message
+            if ($itip->send_itip_message($task, $method, $attendee, $subject, $bodytext, $message))
+                $sent++;
+            else
+                $sent = -100;
+        }
+
+        // send CANCEL message to removed attendees
+        foreach ((array)$old['attendees'] as $attendee) {
+            if ($attendee['ROLE'] == 'ORGANIZER' || !$attendee['email'] || in_array(strtolower($attendee['email']), $current)) {
+                continue;
+            }
+
+            $vevent = $old;
+            $vevent['cancelled'] = $is_cancelled;
+            $vevent['attendees'] = array($attendee);
+            $vevent['comment']   = $comment;
+
+            if ($itip->send_itip_message($vevent, 'CANCEL', $attendee, 'eventcancelsubject', 'eventcancelmailbody'))
+                $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()
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index 4dd7b82..5fc0a20 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -56,6 +56,7 @@ class tasklist_ui
 
         // copy config to client
         $this->rc->output->set_env('tasklist_settings', $this->load_settings());
+        $this->rc->output->set_env('identities-selector', $this->identity_select(array('id' => 'edit-identities-list', 'aria-label' => $this->plugin->gettext('roleorganizer'))));
 
         // initialize attendees autocompletion
         $this->rc->autocomplete_init();
@@ -92,6 +93,22 @@ class tasklist_ui
     }
 
     /**
+     * Render a HTML select box for user identity selection
+     */
+    function identity_select($attrib = array())
+    {
+        $attrib['name'] = 'identity';
+        $select         = new html_select($attrib);
+        $identities     = $this->rc->user->list_identities();
+
+        foreach ($identities as $ident) {
+            $select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
+        }
+
+        return $select->show(null);
+    }
+
+    /**
     * Register handler methods for the template engine
     */
     public function init_templates()
@@ -200,10 +217,11 @@ class tasklist_ui
         // enrich list properties with settings from the driver
         if (!$prop['virtual']) {
             unset($prop['user_id']);
-            $prop['alarms'] = $this->plugin->driver->alarms;
-            $prop['undelete'] = $this->plugin->driver->undelete;
-            $prop['sortable'] = $this->plugin->driver->sortable;
+            $prop['alarms']      = $this->plugin->driver->alarms;
+            $prop['undelete']    = $this->plugin->driver->undelete;
+            $prop['sortable']    = $this->plugin->driver->sortable;
             $prop['attachments'] = $this->plugin->driver->attachments;
+            $prop['attendees']   = $this->plugin->driver->attendees;
             $jsenv[$id] = $prop;
         }
 


commit 6fad5ede559746d4697c083d047dedf26058f00a
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Fri Jul 25 14:01:13 2014 +0200

    Don't depend on get_ical() method existence

diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 074f87f..a6fca9c 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -236,7 +236,7 @@ class libcalendaring_itip
         $message->headers($headers);
 
         // attach ics file for this event
-        $ical = $this->plugin->get_ical();
+        $ical = libcalendaring::get_ical();
         $ics = $ical->export(array($event), $method, false, $method == 'REQUEST' && $this->plugin->driver ? array($this->plugin->driver, 'get_attachment_body') : false);
         $message->addAttachment($ics, 'text/calendar', 'event.ics', false, '8bit', '', RCMAIL_CHARSET . "; method=" . $method);
 


commit c0da6448fb6f1a0955f909f9bbca0d5fd80133d4
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Wed Jul 23 14:35:52 2014 +0200

    Start implementing Assignments (Attendees) tab for tasks (#1165)

diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 9fd0e3e..7920c94 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -58,6 +58,7 @@ $labels['taskactions'] = 'Task options...';
 
 $labels['tabsummary'] = 'Summary';
 $labels['tabrecurrence'] = 'Recurrence';
+$labels['tabassignments'] = 'Assignments';
 $labels['tabattachments'] = 'Attachments';
 $labels['tabsharing'] = 'Sharing';
 
@@ -96,3 +97,38 @@ $labels['arialabellistsearchform'] = 'Tasklists search form';
 $labels['arialabeltaskselector'] = 'List mode';
 $labels['arialabeltasklisting'] = 'Tasks listing';
 
+// attendees
+$labels['attendee'] = 'Participant';
+$labels['role'] = 'Role';
+$labels['availability'] = 'Avail.';
+$labels['confirmstate'] = 'Status';
+$labels['addattendee'] = 'Add participant';
+$labels['roleorganizer'] = 'Organizer';
+$labels['rolerequired'] = 'Required';
+$labels['roleoptional'] = 'Optional';
+$labels['rolechair'] = 'Chair';
+$labels['rolenonparticipant'] = 'Absent';
+$labels['sendinvitations'] = 'Send invitations';
+$labels['sendnotifications'] = 'Notify participants about modifications';
+$labels['sendcancellation'] = 'Notify participants about task cancellation';
+$labels['invitationsubject'] = 'You\'ve been invited to "$title"';
+$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the task details which you can import to your tasks application.";
+$labels['invitationattendlinks'] = "In case your email client doesn't support iTip requests you can use the following link to either accept or decline this invitation:\n\$url";
+$labels['eventupdatesubject'] = '"$title" has been updated';
+$labels['eventupdatesubjectempty'] = 'A task that concerns you has been updated';
+$labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with the updated task details which you can import to your tasks application.";
+$labels['eventcancelsubject'] = '"$title" has been canceled';
+$labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe task has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated task details.";
+
+// invitation handling (overrides labels from libcalendaring)
+$labels['itipobjectnotfound'] = 'The task referred by this message was not found in your tasks list.';
+
+$labels['itipmailbodyaccepted'] = "\$sender has accepted the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodytentative'] = "\$sender has tentatively accepted the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the following task:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
+$labels['itipmailbodycancel'] = "\$sender has rejected your assignment to the following task:\n\n*\$title*\n\nWhen: \$date";
+
+$labels['itipdeclineevent'] = 'Do you want to decline your assignment to this task?';
+$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined task from your tasks list?';
+$labels['itipcomment'] = 'Invitation/notification comment';
+$labels['itipcommenttitle'] = 'This comment will be attached to the invitation/notification message sent to participants';
diff --git a/plugins/tasklist/skins/larry/tasklist.css b/plugins/tasklist/skins/larry/tasklist.css
index e93a311..1e847b3 100644
--- a/plugins/tasklist/skins/larry/tasklist.css
+++ b/plugins/tasklist/skins/larry/tasklist.css
@@ -849,6 +849,84 @@ a.morelink:hover {
 	border-bottom: 2px solid #fafafa;
 }
 
+.edit-attendees-table {
+	width: 100%;
+	margin-top: 0.5em;
+}
+
+.edit-attendees-table th.role,
+.edit-attendees-table td.role {
+	width: 9em;
+}
+
+.edit-attendees-table th.availability,
+.edit-attendees-table td.availability,
+.edit-attendees-table th.confirmstate,
+.edit-attendees-table td.confirmstate {
+	width: 4em;
+}
+
+.edit-attendees-table th.options,
+.edit-attendees-table td.options {
+	width: 16px;
+	padding: 2px 4px;
+}
+
+.edit-attendees-table th.sendmail,
+.edit-attendees-table td.sendmail {
+	width: 44px;
+	padding: 2px;
+}
+
+.edit-attendees-table th.sendmail label {
+	display: inline-block;
+	position: relative;
+	top: 4px;
+	width: 24px;
+	height: 18px;
+	min-width: 24px;
+	padding: 0;
+	overflow: hidden;
+	text-indent: -5000px;
+	white-space: nowrap;
+	background: url(images/sendinvitation.png) 1px 0 no-repeat;
+}
+
+.edit-attendees-table th.name,
+.edit-attendees-table td.name {
+	width: auto;
+	white-space: nowrap;
+	overflow: hidden;
+	text-overflow: ellipsis;
+}
+
+.edit-attendees-table td.name select {
+	width: 100%;
+}
+
+.edit-attendees-table a.deletelink {
+	display: inline-block;
+	width: 17px;
+	height: 17px;
+	padding: 0;
+	overflow: hidden;
+	text-indent: 1000px;
+}
+
+#edit-attendees-form {
+	position: relative;
+	margin-top: 15px;
+}
+
+#edit-attendees-form .attendees-invitebox {
+	text-align: right;
+	margin: 0;
+}
+
+#edit-attendees-form .attendees-invitebox label {
+	padding-right: 3px;
+}
+
 #taskedit-attachments {
 	margin: 0.6em 0;
 }
@@ -954,4 +1032,3 @@ label.block {
 html.ie7 #taskedit-completeness-slider {
 	display: inline;
 }
-
diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html
index c4b3c13..599974e 100644
--- a/plugins/tasklist/skins/larry/templates/taskedit.html
+++ b/plugins/tasklist/skins/larry/templates/taskedit.html
@@ -1,7 +1,7 @@
 <div id="taskedit" class="uidialog uidialog-tabbed">
 	<form id="taskeditform" action="#" method="post" enctype="multipart/form-data">
 		<ul>
-			<li><a href="#taskedit-panel-main"><roundcube:label name="tasklist.tabsummary" /></a></li><li><a href="#taskedit-panel-recurrence"><roundcube:label name="tasklist.tabrecurrence" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-panel-attachments"><roundcube:label name="tasklist.tabattachments" /></a></li>
+			<li><a href="#taskedit-panel-main"><roundcube:label name="tasklist.tabsummary" /></a></li><li><a href="#taskedit-panel-recurrence"><roundcube:label name="tasklist.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#taskedit-panel-attendees"><roundcube:label name="tasklist.tabassignments" /></a></li><li id="taskedit-tab-attachments"><a href="#taskedit-panel-attachments"><roundcube:label name="tasklist.tabattachments" /></a></li>
 		</ul>
 		<!-- basic info -->
 		<div id="taskedit-panel-main">
@@ -79,6 +79,13 @@
 				<roundcube:object name="plugin.recurrence_form" part="rdate" class="form-section" />
 			</div>
 		</div>
+		<!-- attendees list (assignments) -->
+		<div id="taskedit-panel-attendees">
+			<h3 id="aria-label-attendeestable" class="voice"><roundcube:label name="tasklist.arialabeleventassignments" /></h3>
+			<roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table edit-attendees-table" coltitle="attendee" aria-labelledby="aria-label-attendeestable" />
+			<roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
+			<roundcube:include file="/templates/freebusylegend.html" />
+		</div>
 		<!-- attachments list (with upload form) -->
 		<div id="taskedit-panel-attachments">
 			<div id="taskedit-attachments">
@@ -91,4 +98,5 @@
 			<roundcube:object name="plugin.filedroparea" id="taskedit-tab-2" />
 		</div>
 	</form>
+	<roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="taskedit-dialog-message" style="display:none" />
 </div>
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 0852e12..b0c95ba 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -82,6 +82,9 @@ function rcube_tasklist_ui(settings)
     var tasklists_widget;
     var focused_task;
     var focused_subclass;
+    var task_attendees = [];
+    var attendees_list;
+//    var resources_list;
     var me = this;
 
     // general datepicker settings
@@ -541,6 +544,34 @@ function rcube_tasklist_ui(settings)
             if (sel) $(sel).val('');
             return false;
         });
+
+      // init attendees autocompletion
+      var ac_props;
+      // parallel autocompletion
+      if (rcmail.env.autocomplete_threads > 0) {
+        ac_props = {
+          threads: rcmail.env.autocomplete_threads,
+          sources: rcmail.env.autocomplete_sources
+        };
+      }
+      rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
+      rcmail.addEventListener('autocomplete_insert', function(e){
+        if (e.field.name == 'participant') {
+          $('#edit-attendee-add').click();
+        }
+//        else if (e.field.name == 'resource' && e.data && e.data.email) {
+//          add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }));
+//          e.field.value = '';
+//        }
+      });
+
+      $('#edit-attendee-add').click(function(){
+        var input = $('#edit-attendee-name');
+        rcmail.ksearch_blur();
+        if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) {
+          input.val('');
+        }
+      });
     }
 
     /**
@@ -1298,6 +1329,177 @@ function rcube_tasklist_ui(settings)
             scroll_timer = window.setTimeout(function(){ tasklist_drag_scroll(container, dir); }, scroll_speed);
     }
 
+    // check if the task has 'real' attendees, excluding the current user
+    var has_attendees = function(task)
+    {
+        return !!(task.attendees && task.attendees.length && (task.attendees.length > 1 || String(task.attendees[0].email).toLowerCase() != settings.identity.email));
+    };
+
+    // check if the current user is an attendee of this task
+    var is_attendee = function(task, role, email)
+    {
+        var i, emails = email ? ';' + email.toLowerCase() : settings.identity.emails;
+
+        for (i=0; task.attendees && i < task.attendees.length; i++) {
+            if ((!role || task.attendees[i].role == role) && task.attendees[i].email && emails.indexOf(';'+task.attendees[i].email.toLowerCase()) >= 0)
+                return task.attendees[i];
+        }
+
+        return false;
+    };
+
+    // check if the current user is the organizer
+    var is_organizer = function(task, email)
+    {
+        return is_attendee(task, 'ORGANIZER', email) || !task.id;
+    };
+
+    // add the given list of participants
+    var add_attendees = function(names, params)
+    {
+      names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
+
+      // parse name/email pairs
+      var item, email, name, success = false;
+      for (var i=0; i < names.length; i++) {
+        email = name = '';
+        item = $.trim(names[i]);
+
+        if (!item.length) {
+          continue;
+        } // address in brackets without name (do nothing)
+        else if (item.match(/^<[^@]+@[^>]+>$/)) {
+          email = item.replace(/[<>]/g, '');
+        } // address without brackets and without name (add brackets)
+        else if (rcube_check_email(item)) {
+          email = item;
+        } // address with name
+        else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
+          email = RegExp.$1;
+          name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
+        }
+        if (email) {
+          add_attendee($.extend({ email:email, name:name }, params));
+          success = true;
+        }
+        else {
+          alert(rcmail.gettext('noemailwarning'));
+        }
+      }
+
+      return success;
+    };
+
+    // add the given attendee to the list
+    var add_attendee = function(data, readonly)
+    {
+      if (!me.selected_task)
+        return false;
+
+      // check for dupes...
+      var exists = false;
+      $.each(task_attendees, function(i, v) { exists |= (v.email == data.email); });
+      if (exists)
+        return false;
+
+      var list = me.selected_task && me.tasklists[me.selected_task.list] ? me.tasklists[me.selected_task.list] : me.tasklists[me.selected_list];
+
+      var dispname = Q(data.name || data.email);
+      if (data.email)
+        dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink" data-cutype="' + data.cutype + '">' + dispname + '</a>';
+
+      // role selection
+      var organizer = data.role == 'ORGANIZER';
+      var opts = {};
+      if (organizer)
+        opts.ORGANIZER = rcmail.gettext('.roleorganizer');
+      opts['REQ-PARTICIPANT'] = rcmail.gettext('tasklist.rolerequired');
+      opts['OPT-PARTICIPANT'] = rcmail.gettext('tasklist.roleoptional');
+      opts['NON-PARTICIPANT'] = rcmail.gettext('tasklist.rolenonparticipant');
+
+      if (data.cutype != 'RESOURCE')
+        opts['CHAIR'] =  rcmail.gettext('tasklist.rolechair');
+
+      if (organizer && !readonly)
+          dispname = rcmail.env['identities-selector'];
+
+      var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + ' aria-label="' + rcmail.gettext('role','tasklist') + '">';
+      for (var r in opts)
+        select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
+      select += '</select>';
+
+      // availability
+      var avail = data.email ? 'loading' : 'unknown';
+
+      // delete icon
+      var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
+      var dellink = '<a href="#delete" class="iconlink delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
+      var tooltip = data.status || '';
+
+      // send invitation checkbox
+      var invbox = '<input type="checkbox" class="edit-attendee-reply" value="' + Q(data.email) +'" title="' + Q(rcmail.gettext('tasklist.sendinvitations')) + '" '
+        + (!data.noreply ? 'checked="checked" ' : '') + '/>';
+
+      if (data['delegated-to'])
+        tooltip = rcmail.gettext('delegatedto', 'tasklist') + data['delegated-to'];
+      else if (data['delegated-from'])
+        tooltip = rcmail.gettext('delegatedfrom', 'tasklist') + data['delegated-from'];
+
+      var html = '<td class="role">' + select + '</td>' +
+        '<td class="name">' + dispname + '</td>' +
+//        '<td class="availability"><img src="./program/resources/blank.gif" class="availabilityicon ' + avail + '" data-email="' + data.email + '" alt="" /></td>' +
+        '<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '" title="' + Q(tooltip) + '">' + Q(data.status || '') + '</span></td>' +
+        (data.cutype != 'RESOURCE' ? '<td class="sendmail">' + (organizer || readonly || !invbox ? '' : invbox) + '</td>' : '') +
+        '<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
+
+      var table = rcmail.env.tasklist_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
+      var tr = $('<tr>')
+        .addClass(String(data.role).toLowerCase())
+        .html(html)
+        .appendTo(table);
+
+      tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
+      tr.find('a.mailtolink').click(task_attendee_click);
+      tr.find('input.edit-attendee-reply').click(function() {
+        var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
+        $('p.attendees-commentbox')[enabled ? 'show' : 'hide']();
+      });
+
+      // select organizer identity
+      if (data.identity_id)
+        $('#edit-identities-list').val(data.identity_id);
+
+      // check free-busy status
+//      if (avail == 'loading') {
+//        check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_task);
+//      }
+
+      task_attendees.push(data);
+      return true;
+    };
+
+    // event handler for clicks on an attendee link
+    var task_attendee_click = function(e)
+    {
+      var cutype = $(this).attr('data-cutype'),
+        mailto = this.href.substr(7);
+
+      if (rcmail.env.calendar_resources && cutype == 'RESOURCE') {
+        event_resources_dialog(mailto);
+      }
+      else {
+        rcmail.redirect(rcmail.url('mail/compose', { _to:mailto }));
+      }
+      return false;
+    };
+
+    // remove an attendee from the list
+    var remove_attendee = function(elem, id)
+    {
+      $(elem).closest('tr').remove();
+      task_attendees = $.grep(task_attendees, function(data) { return (data.name != id && data.email != id) });
+    };
+
     /**
      * Show task details in a dialog
      */
@@ -1422,7 +1624,7 @@ function rcube_tasklist_ui(settings)
             return false;
 
         me.selected_task = $.extend({ valarms:[] }, rec);  // clone task object
-        rec =  me.selected_task;
+        rec = me.selected_task;
 
         // assign temporary id
         if (!me.selected_task.id)
@@ -1442,6 +1644,12 @@ function rcube_tasklist_ui(settings)
         completeness_slider.slider('value', complete.val());
         var taskstatus = $('#taskedit-status').val(rec.status || '');
         var tasklist = $('#taskedit-tasklist').val(rec.list || me.selected_list).prop('disabled', rec.parent_id ? true : false);
+        var notify = $('#edit-attendees-donotify').get(0);
+        var invite = $('#edit-attendees-invite').get(0);
+        var comment = $('#edit-attendees-comment');
+
+        notify.checked = has_attendees(rec);
+        invite.checked = true;
 
         // tag-edit line
         var tagline = $(rcmail.gui_objects.edittagline).empty();
@@ -1468,6 +1676,49 @@ function rcube_tasklist_ui(settings)
         // set recurrence
         me.set_recurrence_edit(rec);
 
+        // init attendees tab
+        var organizer = !rec.attendees || is_organizer(rec),
+            allow_invitations = organizer || (rec.owner && rec.owner == 'anonymous') || settings.invite_shared;
+
+        task_attendees = [];
+        attendees_list = $('#edit-attendees-table > tbody').html('');
+        //resources_list = $('#edit-resources-table > tbody').html('');
+        $('#edit-attendees-notify')[(notify.checked && allow_invitations ? 'show' : 'hide')]();
+        $('#edit-localchanges-warning')[(has_attendees(rec) && !(allow_invitations || (rec.owner && is_organizer(rec, rec.owner))) ? 'show' : 'hide')]();
+
+        var load_attendees_tab = function()
+        {
+            var j, data, reply_selected = 0;
+            if (rec.attendees) {
+                for (j=0; j < rec.attendees.length; j++) {
+                    data = rec.attendees[j];
+                    add_attendee(data, !allow_invitations);
+                    if (allow_invitations && data.role != 'ORGANIZER' && !data.noreply) {
+                        reply_selected++;
+                    }
+                }
+            }
+
+            // make sure comment box is visible if at least one attendee has reply enabled
+            // or global "send invitations" checkbox is checked
+            if (reply_selected || $('#edit-attendees-invite:checked').length) {
+                $('p.attendees-commentbox').show();
+            }
+
+            // select the correct organizer identity
+            var identity_id = 0;
+            $.each(settings.identities, function(i,v) {
+                if (organizer && v == organizer.email) {
+                    identity_id = i;
+                    return false;
+                }
+            });
+
+            $('#edit-identities-list').val(identity_id);
+            $('#edit-attendees-form')[(allow_invitations?'show':'hide')]();
+//            $('#edit-attendee-schedule')[(tasklist.freebusy?'show':'hide')]();
+        };
+
         // attachments
         rcmail.enable_command('remove-attachment', list.editable);
         me.selected_task.deleted_attachments = [];
@@ -1491,23 +1742,26 @@ function rcube_tasklist_ui(settings)
         // define dialog buttons
         var buttons = {};
         buttons[rcmail.gettext('save', 'tasklist')] = function() {
+            var data = me.selected_task;
+
             // copy form field contents into task object to save
             $.each({ title:title, description:description, date:recdate, time:rectime, startdate:recstartdate, starttime:recstarttime, status:taskstatus, list:tasklist }, function(key,input){
-                me.selected_task[key] = input.val();
+                data[key] = input.val();
             });
-            me.selected_task.tags = [];
-            me.selected_task.attachments = [];
-            me.selected_task.valarms = me.serialize_alarms('#taskedit-alarms');
-            me.selected_task.recurrence = me.serialize_recurrence(rectime.val());
+            data.tags = [];
+            data.attachments = [];
+            data.attendees = task_attendees;
+            data.valarms = me.serialize_alarms('#taskedit-alarms');
+            data.recurrence = me.serialize_recurrence(rectime.val());
 
             // do some basic input validation
-            if (!me.selected_task.title || !me.selected_task.title.length) {
+            if (!data.title || !data.title.length) {
                 title.focus();
                 return false;
             }
-            else if (me.selected_task.startdate && me.selected_task.date) {
-                var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.startdate, datepicker_settings);
-                var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, me.selected_task.date, datepicker_settings);
+            else if (data.startdate && data.date) {
+                var startdate = $.datepicker.parseDate(datepicker_settings.dateFormat, data.startdate, datepicker_settings);
+                var duedate = $.datepicker.parseDate(datepicker_settings.dateFormat, data.date, datepicker_settings);
                 if (startdate > duedate) {
                     alert(rcmail.gettext('invalidstartduedates', 'tasklist'));
                     return false;
@@ -1515,38 +1769,76 @@ function rcube_tasklist_ui(settings)
             }
 
             // collect tags
-            $('input[type="hidden"]', rcmail.gui_objects.edittagline).each(function(i,elem){
+            $('input[type="hidden"]', rcmail.gui_objects.edittagline).each(function(i,elem) {
                 if (elem.value)
-                    me.selected_task.tags.push(elem.value);
+                    data.tags.push(elem.value);
             });
             // including the "pending" one in the text box
             var newtag = $('#tagedit-input').val();
             if (newtag != '') {
-                me.selected_task.tags.push(newtag);
+                data.tags.push(newtag);
             }
 
             // uploaded attachments list
             for (var i in rcmail.env.attachments) {
                 if (i.match(/^rcmfile(.+)/))
-                    me.selected_task.attachments.push(RegExp.$1);
+                    data.attachments.push(RegExp.$1);
             }
 
             // task assigned to a new list
-            if (me.selected_task.list && listdata[id] && me.selected_task.list != listdata[id].list) {
-                me.selected_task._fromlist = list.id;
+            if (data.list && listdata[id] && data.list != listdata[id].list) {
+                data._fromlist = list.id;
+            }
+
+            data.complete = complete.val() / 100;
+            if (isNaN(data.complete))
+                data.complete = null;
+
+            if (!data.list && list.id)
+                data.list = list.id;
+
+            if (!data.tags.length)
+                data.tags = '';
+
+            // read attendee roles
+            $('select.edit-attendee-role').each(function(i, elem) {
+                if (data.attendees[i]) {
+                    data.attendees[i].role = $(elem).val();
+                }
+            });
+
+            if (organizer) {
+                data._identity = $('#edit-identities-list option:selected').val();
             }
 
-            me.selected_task.complete = complete.val() / 100;
-            if (isNaN(me.selected_task.complete))
-                me.selected_task.complete = null;
+            // don't submit attendees if only myself is added as organizer
+            if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && String(data.attendees[0].email).toLowerCase() == settings.identity.email) {
+                data.attendees = [];
+            }
 
-            if (!me.selected_task.list && list.id)
-                me.selected_task.list = list.id;
+            // per-attendee notification suppression
+            var need_invitation = false;
+            if (allow_invitations) {
+                $.each(data.attendees, function (i, v) {
+                    if (v.role != 'ORGANIZER') {
+                        if ($('input.edit-attendee-reply[value="' + v.email + '"]').prop('checked')) {
+                            need_invitation = true;
+                            delete data.attendees[i]['noreply'];
+                        }
+                        else {
+                            data.attendees[i].noreply = 1;
+                        }
+                    }
+                });
+            }
 
-            if (!me.selected_task.tags.length)
-                me.selected_task.tags = '';
+            // tell server to send notifications
+            if ((data.attendees.length || (rec.id && rec.attendees.length)) && allow_invitations && (notify.checked || invite.checked || need_invitation)) {
+                data._notify = 1;
+                data._comment = comment.val();
+            }
 
-            if (save_task(me.selected_task, action))
+            if (save_task(data, action))
                 $dialog.dialog('close');
         };
 
@@ -1581,8 +1873,10 @@ function rcube_tasklist_ui(settings)
 
         // set dialog size according to content
         me.dialog_resize($dialog.get(0), $dialog.height(), 580);
-    }
 
+        if (tasklist.attendees)
+            window.setTimeout(load_attendees_tab, 1);
+    }
 
     /**
      * Open a task attachment either in a browser window for inline view or download it
@@ -2060,6 +2354,29 @@ function rcube_tasklist_ui(settings)
 
     /**** Utility functions ****/
 
+    // same as str.split(delimiter) but it ignores delimiters within quoted strings
+    var explode_quoted_string = function(str, delimiter)
+    {
+      var result = [],
+        strlen = str.length,
+        q, p, i, char, last;
+
+      for (q = p = i = 0; i < strlen; i++) {
+        char = str.charAt(i);
+        if (char == '"' && last != '\\') {
+          q = !q;
+        }
+        else if (!q && char == delimiter) {
+          result.push(str.substring(p, i));
+          p = i + 1;
+        }
+        last = char;
+      }
+
+      result.push(str.substr(p));
+      return result;
+    };
+
     /**
      * Clear any text selection
      * (text is probably selected when double-clicking somewhere)
@@ -2205,7 +2522,7 @@ jQuery.unqiqueStrings = (function() {
 var rctasks;
 window.rcmail && rcmail.addEventListener('init', function(evt) {
 
-  rctasks = new rcube_tasklist_ui(rcmail.env.libcal_settings);
+  rctasks = new rcube_tasklist_ui($.extend(rcmail.env.tasklist_settings, rcmail.env.libcal_settings));
 
   // register button commands
   rcmail.register_command('newtask', function(){ rctasks.edit_task(null, 'new', {}); }, true);
diff --git a/plugins/tasklist/tasklist_ui.php b/plugins/tasklist/tasklist_ui.php
index fc7320e..4dd7b82 100644
--- a/plugins/tasklist/tasklist_ui.php
+++ b/plugins/tasklist/tasklist_ui.php
@@ -55,10 +55,41 @@ class tasklist_ui
         $this->plugin->include_script('tasklist_base.js');
 
         // copy config to client
-        // $this->rc->output->set_env('tasklist_settings', $settings);
+        $this->rc->output->set_env('tasklist_settings', $this->load_settings());
+
+        // initialize attendees autocompletion
+        $this->rc->autocomplete_init();
 
         $this->ready = true;
-  }
+    }
+
+    /**
+     *
+     */
+    function load_settings()
+    {
+        $settings = array();
+
+        //$settings['invite_shared'] = (int)$this->rc->config->get('calendar_allow_invite_shared', $this->defaults['calendar_allow_invite_shared']);
+
+        // get user identity to create default attendee
+        foreach ($this->rc->user->list_identities() as $rec) {
+            if (!$identity)
+                $identity = $rec;
+
+            $identity['emails'][] = $rec['email'];
+            $settings['identities'][$rec['identity_id']] = $rec['email'];
+        }
+
+        $identity['emails'][] = $this->rc->user->get_username();
+        $settings['identity'] = array(
+            'name'   => $identity['name'],
+            'email'  => strtolower($identity['email']),
+            'emails' => ';' . strtolower(join(';', $identity['emails']))
+        );
+
+        return $settings;
+    }
 
     /**
     * Register handler methods for the template engine
@@ -78,6 +109,9 @@ class tasklist_ui
         $this->plugin->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
         $this->plugin->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
         $this->plugin->register_handler('plugin.filedroparea', array($this, 'file_drop_area'));
+        $this->plugin->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
+        $this->plugin->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
+        $this->plugin->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
 
         $this->plugin->include_script('jquery.tagedit.js');
         $this->plugin->include_script('tasklist.js');
@@ -375,4 +409,54 @@ class tasklist_ui
         }
     }
 
+    /**
+     *
+     */
+    function attendees_list($attrib = array())
+    {
+        // add "noreply" checkbox to attendees table only
+        $invitations = strpos($attrib['id'], 'attend') !== false;
+
+        $invite = new html_checkbox(array('value' => 1, 'id' => 'edit-attendees-invite'));
+        $table  = new html_table(array('cols' => 4 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
+
+        $table->add_header('role', $this->plugin->gettext('role'));
+        $table->add_header('name', $this->plugin->gettext($attrib['coltitle'] ?: 'attendee'));
+//        $table->add_header('availability', $this->plugin->gettext('availability'));
+        $table->add_header('confirmstate', $this->plugin->gettext('confirmstate'));
+        if ($invitations) {
+            $table->add_header(array('class' => 'sendmail', 'title' => $this->plugin->gettext('sendinvitations')),
+                $invite->show(1) . html::label('edit-attendees-invite', $this->plugin->gettext('sendinvitations')));
+        }
+        $table->add_header('options', '');
+
+        return $table->show($attrib);
+    }
+
+    /**
+     *
+     */
+    function attendees_form($attrib = array())
+    {
+        $input    = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30));
+        $textarea = new html_textarea(array('name' => 'comment', 'id' => 'edit-attendees-comment',
+            'rows' => 4, 'cols' => 55, 'title' => $this->plugin->gettext('itipcommenttitle')));
+
+        return html::div($attrib,
+            html::div(null, $input->show() . " " .
+                html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->plugin->gettext('addattendee')))
+                // . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->plugin->gettext('scheduletime').'...'))
+                ) .
+            html::p('attendees-commentbox', html::label(null, $this->plugin->gettext('itipcomment') . $textarea->show()))
+        );
+    }
+
+    /**
+     *
+     */
+    function edit_attendees_notify($attrib = array())
+    {
+        $checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1));
+        return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->plugin->gettext('sendnotifications')));
+    }
 }





More information about the commits mailing list