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

Thomas Brüderli bruederli at kolabsys.com
Thu Nov 6 17:10:20 CET 2014


 plugins/calendar/calendar.php                      |   97 +++++++++++++++--
 plugins/calendar/calendar_ui.js                    |   33 +++++
 plugins/calendar/lib/calendar_ui.php               |    6 -
 plugins/calendar/localization/ca_ES.inc            |    4 
 plugins/calendar/localization/da_DK.inc            |    4 
 plugins/calendar/localization/de_DE.inc            |    4 
 plugins/calendar/localization/en_US.inc            |    6 -
 plugins/calendar/localization/es_AR.inc            |    4 
 plugins/calendar/localization/fi_FI.inc            |    4 
 plugins/calendar/localization/fr_FR.inc            |    4 
 plugins/calendar/localization/hu_HU.inc            |    4 
 plugins/calendar/localization/nl_NL.inc            |    4 
 plugins/calendar/localization/pl_PL.inc            |    4 
 plugins/calendar/localization/pt_BR.inc            |    4 
 plugins/calendar/localization/ru_RU.inc            |    4 
 plugins/libcalendaring/lib/libcalendaring_itip.php |  117 ++++++++++++++++++---
 plugins/libcalendaring/libcalendaring.js           |  103 ++++++++++++++++++
 plugins/libcalendaring/libcalendaring.php          |   14 ++
 plugins/libcalendaring/libvcalendar.php            |    3 
 plugins/libcalendaring/localization/en_US.inc      |    7 +
 plugins/libcalendaring/skins/larry/libcal.css      |    8 +
 plugins/libkolab/lib/kolab_format_xcal.php         |    9 +
 plugins/tasklist/localization/en_US.inc            |    3 
 plugins/tasklist/tasklist.js                       |   41 ++++++-
 plugins/tasklist/tasklist.php                      |  117 +++++++++++++++++----
 25 files changed, 515 insertions(+), 93 deletions(-)

New commits:
commit c6f5a8233bf1a52841a7bfd2123e1d3f838413d8
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Nov 6 17:09:49 2014 +0100

    Fix handling of RSVP flags

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 910affb..e359712 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1642,7 +1642,9 @@ class calendar extends rcube_plugin
     foreach ((array)$event['attendees'] as $i => $attendee) {
       if ($attendee['role'] == 'ORGANIZER') {
         $organizer = $attendee;
-        break;
+      }
+      if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] == false) {
+        $event['attendees'][$i]['noreply'] = true;
       }
     }
 
@@ -1895,7 +1897,7 @@ class calendar extends rcube_plugin
 
     // compose multipart message using PEAR:Mail_Mime
     $method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
-    $message = $itip->compose_itip_message($event, $method);
+    $message = $itip->compose_itip_message($event, $method, $event['sequence'] > $old['sequence']);
 
     // list existing attendees from $old event
     $old_attendees = array();
@@ -1915,7 +1917,11 @@ class calendar extends rcube_plugin
       // skip if notification is disabled for this attendee
       if ($attendee['noreply'] && $itip_notify & 2)
         continue;
-      
+
+      // skip if this attendee has delegated and set RSVP=FALSE
+      if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false)
+        continue;
+
       // which template to use for mail text
       $is_new = !in_array($attendee['email'], $old_attendees);
       $is_rsvp = $is_new || $event['sequence'] > $old['sequence'];
@@ -2639,7 +2645,8 @@ class calendar extends rcube_plugin
           else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
             $event['attendees'][$i]['status'] = strtoupper($status);
             if (!in_array($event['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED')))
-              unset($event['attendees'][$i]['rsvp']);  // remove RSVP attribute
+              $event['attendees'][$i]['rsvp'] = false;  // unset RSVP attribute
+
             $metadata['attendee'] = $attendee['email'];
             $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
             $reply_sender = $attendee['email'];
@@ -2696,7 +2703,17 @@ class calendar extends rcube_plugin
                 $existing['attendees'][] = $attendee;
               }
             }
-            
+
+            // if delegatee has declined, set delegator's RSVP=True
+            if ($event_attendee && $event_attendee['status'] == 'DECLINED' && $event_attendee['delegated-from']) {
+              foreach ($existing['attendees'] as $i => $attendee) {
+                if ($attendee['email'] == $event_attendee['delegated-from']) {
+                  $existing['attendees'][$i]['rsvp'] = true;
+                  break;
+                }
+              }
+            }
+
             // found matching attendee entry in both existing and new events
             if ($existing_attendee >= 0 && $event_attendee) {
               $existing['attendees'][$existing_attendee] = $event_attendee;
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 9524249..bd83f16 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -90,6 +90,7 @@ class libcalendaring_itip
      * @param string  Mail subject
      * @param string  Mail body text label
      * @param object  Mail_mime object with message data
+     * @param boolean Request RSVP
      * @return boolean True on success, false on failure
      */
     public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null, $rsvp = true)
@@ -98,7 +99,7 @@ class libcalendaring_itip
             $this->sender['name'] = $this->sender['email'];
 
         if (!$message)
-            $message = $this->compose_itip_message($event, $method);
+            $message = $this->compose_itip_message($event, $method, $rsvp);
 
         $mailto = rcube_idn_to_ascii($recipient['email']);
 
@@ -192,9 +193,10 @@ class libcalendaring_itip
      *
      * @param array   Event object to send
      * @param string  iTip method (REQUEST|REPLY|CANCEL)
+     * @param boolean Request RSVP
      * @return object Mail_mime object with message data
      */
-    public function compose_itip_message($event, $method)
+    public function compose_itip_message($event, $method, $rsvp = true)
     {
         $from = rcube_idn_to_ascii($this->sender['email']);
         $from_utf = rcube_utils::idn_to_utf8($from);
@@ -228,11 +230,11 @@ class libcalendaring_itip
                 $event['attendees'] = $reply_attendees;
             }
         }
-        // set RSVP=TRUE for every attendee if not set
+        // set RSVP for every attendee
         else if ($method == 'REQUEST') {
             foreach ($event['attendees'] as $i => $attendee) {
-                if (!isset($attendee['rsvp'])) {
-                    $event['attendees'][$i]['rsvp']= true;
+                if ($attendee['status'] != 'DELEGATED') {
+                    $event['attendees'][$i]['rsvp']= $rsvp ? true : null;
                 }
             }
         }
diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php
index 1e10ddc..062ee7e 100644
--- a/plugins/libcalendaring/libvcalendar.php
+++ b/plugins/libcalendaring/libvcalendar.php
@@ -1072,7 +1072,8 @@ class libvcalendar implements Iterator
                     $event['organizer'] = $attendee;
             }
             else if (!empty($attendee['email'])) {
-                $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : null;
+                if (isset($attendee['rsvp']))
+                    $attendee['rsvp'] = $attendee['rsvp'] ? 'TRUE' : 'FALSE';
                 $ve->add('ATTENDEE', 'mailto:' . $attendee['email'], array_filter(self::map_keys($attendee, $this->attendee_keymap)));
             }
         }
diff --git a/plugins/libkolab/lib/kolab_format_xcal.php b/plugins/libkolab/lib/kolab_format_xcal.php
index 08f27d0..ad54505 100644
--- a/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/plugins/libkolab/lib/kolab_format_xcal.php
@@ -364,14 +364,17 @@ abstract class kolab_format_xcal extends kolab_format
                 $cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
                 $cr->setName($attendee['name']);
 
+                // set attendee RSVP if missing
+                if (!isset($attendee['rsvp'])) {
+                    $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] = true;
+                }
+
                 $att = new Attendee;
                 $att->setContact($cr);
                 $att->setPartStat($this->part_status_map[$attendee['status']]);
                 $att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
                 $att->setCutype($this->cutype_map[$attendee['cutype']] ? $this->cutype_map[$attendee['cutype']] : kolabformat::CutypeIndividual);
-                $att->setRSVP((bool)$attendee['rsvp'] || $reschedule);
-
-                $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] || $reschedule;
+                $att->setRSVP((bool)$attendee['rsvp']);
 
                 if (!empty($attendee['delegated-from'])) {
                     $vdelegators = new vectorcontactref;
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 71e6431..3f23c59 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -757,7 +757,7 @@ class tasklist extends rcube_plugin
         // compose multipart message using PEAR:Mail_Mime
         $method  = $action == 'delete' ? 'CANCEL' : 'REQUEST';
         $object = $this->to_libcal($task);
-        $message = $itip->compose_itip_message($object, $method);
+        $message = $itip->compose_itip_message($object, $method, $task['sequence'] > $old['sequence']);
 
         // list existing attendees from the $old task
         $old_attendees = array();
@@ -779,6 +779,12 @@ class tasklist extends rcube_plugin
             if ($attendee['noreply'] && $itip_notify & 2) {
                 continue;
             }
+
+            // skip if this attendee has delegated and set RSVP=FALSE
+            if ($attendee['status'] == 'DELEGATED' && $attendee['rsvp'] === false) {
+                continue;
+            }
+
             // which template to use for mail text
             $is_new   = !in_array($attendee['email'], $old_attendees);
             $is_rsvp  = $is_new || $task['sequence'] > $old['sequence'];
@@ -1776,7 +1782,7 @@ class tasklist extends rcube_plugin
 
                         $task['attendees'][$i]['status'] = strtoupper($status);
                         if (!in_array($task['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) {
-                            unset($task['attendees'][$i]['rsvp']);  // remove RSVP attribute
+                            $task['attendees'][$i]['rsvp'] = false;  // unset RSVP attribute
                         }
                     }
                 }
@@ -1833,6 +1839,16 @@ class tasklist extends rcube_plugin
                             }
                         }
 
+                        // if delegatee has declined, set delegator's RSVP=True
+                        if ($task_attendee && $task_attendee['status'] == 'DECLINED' && $task_attendee['delegated-from']) {
+                            foreach ($existing['attendees'] as $i => $attendee) {
+                                if ($attendee['email'] == $task_attendee['delegated-from']) {
+                                    $existing['attendees'][$i]['rsvp'] = true;
+                                    break;
+                                }
+                            }
+                        }
+
                         // found matching attendee entry in both existing and new events
                         if ($existing_attendee >= 0 && $task_attendee) {
                             $existing['attendees'][$existing_attendee] = $task_attendee;


commit 7294ef8be0594ec32476ba49a34282675919f38b
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Nov 6 15:14:44 2014 +0100

    Send delegate attendee in iTip reply (as suggested in RFC 5546) + add it to organizers calendar/tasklist

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 88a4351..910affb 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2671,10 +2671,11 @@ class calendar extends rcube_plugin
           if ($event['_method'] == 'REPLY') {
             // try to identify the attendee using the email sender address
             $existing_attendee = -1;
+            $existing_attendee_emails = array();
             foreach ($existing['attendees'] as $i => $attendee) {
+              $existing_attendee_emails[] = $attendee['email'];
               if ($event['_sender'] && ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf'])) {
                 $existing_attendee = $i;
-                break;
               }
             }
             $event_attendee = null;
@@ -2684,7 +2685,15 @@ class calendar extends rcube_plugin
                 $metadata['fallback'] = $attendee['status'];
                 $metadata['attendee'] = $attendee['email'];
                 $metadata['rsvp'] = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
-                break;
+                if ($attendee['status'] != 'DELEGATED') {
+                  break;
+                }
+              }
+              // also copy delegate attendee
+              else if (!empty($attendee['delegated-from']) &&
+                       (stripos($attendee['delegated-from'], $event['_sender']) !== false || stripos($attendee['delegated-from'], $event['_sender_utf']) !== false) &&
+                       (!in_array($attendee['email'], $existing_attendee_emails))) {
+                $existing['attendees'][] = $attendee;
               }
             }
             
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index 9403f46..9524249 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -197,24 +197,34 @@ class libcalendaring_itip
     public function compose_itip_message($event, $method)
     {
         $from = rcube_idn_to_ascii($this->sender['email']);
-        $from_utf = rcube_idn_to_utf8($from);
+        $from_utf = rcube_utils::idn_to_utf8($from);
         $sender = format_email_recipient($from, $this->sender['name']);
 
         // truncate list attendees down to the recipient of the iTip Reply.
         // constraints for a METHOD:REPLY according to RFC 5546
         if ($method == 'REPLY') {
-            $replying_attendee = null; $reply_attendees = array();
+            $replying_attendee = null;
+            $reply_attendees = array();
             foreach ($event['attendees'] as $attendee) {
                 if ($attendee['role'] == 'ORGANIZER') {
                     $reply_attendees[] = $attendee;
                 }
-                else if (strcasecmp($attedee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) {
+                else if (strcasecmp($attendee['email'], $from) == 0 || strcasecmp($attendee['email'], $from_utf) == 0) {
                     $replying_attendee = $attendee;
-                    unset($replying_attendee['rsvp']);  // unset the RSVP attribute
+                    if ($attendee['status'] != 'DELEGATED') {
+                        unset($replying_attendee['rsvp']);  // unset the RSVP attribute
+                    }
+                }
+                // include attendees relevant for delegation (RFC 5546, Section 4.2.5)
+                else if ((!empty($attendee['delegated-to']) &&
+                            (strcasecmp($attendee['delegated-to'], $from) == 0 || strcasecmp($attendee['delegated-to'], $from_utf) == 0)) ||
+                         (!empty($attendee['delegated-from']) &&
+                            (strcasecmp($attendee['delegated-from'], $from) == 0 || strcasecmp($attendee['delegated-from'], $from_utf) == 0))) {
+                    $reply_attendees[] = $attendee;
                 }
             }
             if ($replying_attendee) {
-                $reply_attendees[] = $replying_attendee;
+                array_unshift($reply_attendees, $replying_attendee);
                 $event['attendees'] = $reply_attendees;
             }
         }
@@ -454,7 +464,8 @@ class libcalendaring_itip
             $title = $this->gettext('itipreply');
 
             foreach ($event['attendees'] as $attendee) {
-                if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER') {
+                if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER' &&
+                      (empty($event['_sender']) || ($attendee['email'] == $event['_sender'] || $attendee['email'] == $event['_sender_utf']))) {
                     $metadata['attendee'] = $attendee['email'];
                     $rsvp_status = strtoupper($attendee['status']);
                     if ($attendee['delegated-to'])
diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php
index 6e61add..78eaa24 100644
--- a/plugins/libcalendaring/libcalendaring.php
+++ b/plugins/libcalendaring/libcalendaring.php
@@ -59,6 +59,7 @@ class libcalendaring extends rcube_plugin
     );
 
     private static $instance;
+    private static $email_regex = '/([a-z0-9][a-z0-9\-\.\+\_]*@[^&@"\'.][^@&"\']*\\.([^\\x00-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-z0-9]{2,}))/';
 
     private $mail_ical_parser;
 
@@ -1287,6 +1288,15 @@ class libcalendaring extends rcube_plugin
                 if (count($this->mail_ical_parser->objects) && $this->mail_ical_parser->method) {
                     $this->mail_ical_parser->message_date = $this->ical_message->headers->date;
                     $this->mail_ical_parser->mime_id = $mime_id;
+
+                    // store the message's sender address for comparisons
+                    $this->mail_ical_parser->sender = preg_match(self::$email_regex, $this->ical_message->headers->from, $m) ? $m[1] : '';
+                    if (!empty($this->mail_ical_parser->sender)) {
+                        foreach ($this->mail_ical_parser->objects as $i => $object) {
+                            $this->mail_ical_parser->objects[$i]['_sender'] = $this->mail_ical_parser->sender;
+                            $this->mail_ical_parser->objects[$i]['_sender_utf'] = rcube_utils::idn_to_utf8($this->mail_ical_parser->sender);
+                        }
+                    }
                     break;
                 }
             }
@@ -1335,8 +1345,8 @@ class libcalendaring extends rcube_plugin
                 $object['_method'] = $parser->method;
 
             // store the message's sender address for comparisons
-            $object['_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] : '';
-            $object['_sender_utf'] = rcube_idn_to_utf8($object['_sender']);
+            $object['_sender'] = preg_match(self::$email_regex, $headers->from, $m) ? $m[1] : '';
+            $object['_sender_utf'] = rcube_utils::idn_to_utf8($object['_sender']);
 
             return $object;
         }
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 6874121..60ca18c 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -171,6 +171,7 @@ $labels['itipcommenttitle'] = 'This comment will be attached to the invitation/n
 $labels['itipsendsuccess'] = 'Notification sent to assignees';
 $labels['errornotifying'] = 'Failed to send notifications to task assignees';
 $labels['removefromcalendar'] = 'Remove from my tasks';
+$labels['delegateinvitation'] = 'Delegate assignment';
 
 $labels['andnmore'] = '$nr more...';
 $labels['delegatedto'] = 'Delegated to: ';
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 0ddba7d..71e6431 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -1806,10 +1806,11 @@ class tasklist extends rcube_plugin
                     if ($task['_method'] == 'REPLY') {
                         // try to identify the attendee using the email sender address
                         $existing_attendee = -1;
+                        $existing_attendee_emails = array();
                         foreach ($existing['attendees'] as $i => $attendee) {
+                            $existing_attendee_emails[] = $attendee['email'];
                             if ($task['_sender'] && ($attendee['email'] == $task['_sender'] || $attendee['email'] == $task['_sender_utf'])) {
                                 $existing_attendee = $i;
-                                break;
                             }
                         }
 
@@ -1820,7 +1821,15 @@ class tasklist extends rcube_plugin
                                 $metadata['fallback'] = $attendee['status'];
                                 $metadata['attendee'] = $attendee['email'];
                                 $metadata['rsvp']     = $attendee['rsvp'] || $attendee['role'] != 'NON-PARTICIPANT';
-                                break;
+                                if ($attendee['status'] != 'DELEGATED') {
+                                    break;
+                                }
+                            }
+                            // also copy delegate attendee
+                            else if (!empty($attendee['delegated-from']) &&
+                                     (stripos($attendee['delegated-from'], $task['_sender']) !== false || stripos($attendee['delegated-from'], $task['_sender_utf']) !== false) &&
+                                     (!in_array($attendee['email'], $existing_attendee_emails))) {
+                                $existing['attendees'][] = $attendee;
                             }
                         }
 


commit f01a600af44057dc2310c8df3982da35ec1d8252
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Nov 6 12:28:58 2014 +0100

    Enable iTip delegation for tasks (#3860)

diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index f06b55a..d04a024 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -863,7 +863,7 @@ rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
                 rcmail.gettext('itip.itipcomment') + '"></textarea>' + 
         '</div>' +
         '<div class="form-section">' +
-            (selector ? selector.html() : '') +
+            (selector && selector.length ? selector.html() : '') +
         '</div>' +
     '</form>';
 
diff --git a/plugins/tasklist/localization/en_US.inc b/plugins/tasklist/localization/en_US.inc
index 8593fe5..6874121 100644
--- a/plugins/tasklist/localization/en_US.inc
+++ b/plugins/tasklist/localization/en_US.inc
@@ -153,6 +153,8 @@ $labels['itipmailbodydeclined'] = "\$sender has declined the assignment to the f
 $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['itipmailbodydelegated'] = "\$sender has delegated the following task:\n\n*\$title*\n\nDue: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender has delegated the following task to you:\n\n*\$title*\n\nDue: \$date";
 
 $labels['attendeeaccepted'] = 'Assignee has accepted';
 $labels['attendeetentative'] = 'Assignee has tentatively accepted';
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
index 7683cc5..f107a39 100644
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -533,17 +533,41 @@ function rcube_tasklist_ui(settings)
             }
         });
 
-        // init RSVP widget
-        $('#task-rsvp input.button').click(function(e) {
-            var response = $(this).attr('rel');
-
+        /**
+         *
+         */
+        function task_rsvp(response, delegate)
+        {
             if (me.selected_task && me.selected_task.attendees && response) {
+                // bring up delegation dialog
+                if (response == 'delegated' && !delegate) {
+                    rcube_libcalendaring.itip_delegate_dialog(function(data) {
+                        $('#reply-comment-task-rsvp').val(data.comment);
+                        data.rsvp = data.rsvp ? 1 : '';
+                        task_rsvp('delegated', data);
+                    });
+                    return;
+                }
+
                 // update attendee status
                 for (var data, i=0; i < me.selected_task.attendees.length; i++) {
                     data = me.selected_task.attendees[i];
                     if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) {
                         data.status = response.toUpperCase();
-                        delete data.rsvp;  // unset RSVP flag
+
+                        if (data.status == 'DELEGATED') {
+                              data['delegated-to'] = delegate.to;
+                        }
+                        else {
+                            delete data.rsvp;  // unset RSVP flag
+
+                            if (data['delegated-to']) {
+                              delete data['delegated-to'];
+                              if (data.role == 'NON-PARTICIPANT' && data.status != 'DECLINED') {
+                                  data.role = 'REQ-PARTICIPANT';
+                              }
+                            }
+                        }
                     }
                 }
 
@@ -551,7 +575,7 @@ function rcube_tasklist_ui(settings)
                 saving_lock = rcmail.set_busy(true, 'tasklist.savingdata');
                 rcmail.http_post('tasks/task', {
                     action: 'rsvp',
-                    t: me.selected_task,
+                    t: $.extend({}, me.selected_task, (delegate || {})),
                     filter: filtermask,
                     status: response,
                     noreply: $('#noreply-task-rsvp:checked').length ? 1 : 0,
@@ -560,6 +584,11 @@ function rcube_tasklist_ui(settings)
 
                 task_show_dialog(me.selected_task.id);
             }
+        }
+
+        // init RSVP widget
+        $('#task-rsvp input.button').click(function(e) {
+            task_rsvp($(this).attr('rel'))
         });
 
         // register click handler for message links
diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php
index 5f34e86..0ddba7d 100644
--- a/plugins/tasklist/tasklist.php
+++ b/plugins/tasklist/tasklist.php
@@ -120,6 +120,7 @@ class tasklist extends rcube_plugin
             $this->register_action('itip-status', array($this, 'task_itip_status'));
             $this->register_action('itip-remove', array($this, 'task_itip_remove'));
             $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
+            $this->register_action('itip-delegate', array($this, 'mail_itip_delegate'));
             $this->add_hook('refresh', array($this, 'refresh'));
 
             $this->collapsed_tasks = array_filter(explode(',', $this->rc->config->get('tasklist_collapsed_tasks', '')));
@@ -245,6 +246,7 @@ class tasklist extends rcube_plugin
             }
 
         case 'edit':
+            $oldrec = $this->driver->get_task($rec);
             $rec = $this->prepare_task($rec);
             $clone = $this->handle_recurrence($rec, $this->driver->get_task($rec));
             if ($success = $this->driver->edit_task($rec)) {
@@ -357,13 +359,24 @@ class tasklist extends rcube_plugin
 
         case 'rsvp':
             $status = rcube_utils::get_input_value('status', rcube_utils::INPUT_GPC);
+            $noreply = intval(rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC)) || $status == 'needs-action';
             $task = $this->driver->get_task($rec);
             $task['attendees'] = $rec['attendees'];
+            $task['_type'] = 'task';
+
+            // send invitation to delegatee + add it as attendee
+            if ($status == 'delegated' && $rec['to']) {
+                $itip = $this->load_itip();
+                if ($itip->delegate_to($task, $rec['to'], (bool)$rec['rsvp'])) {
+                    $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
+                    $refresh[] = $task;
+                    $noreply = false;
+                }
+            }
+
             $rec = $task;
 
             if ($success = $this->driver->edit_task($rec)) {
-                $noreply = intval(rcube_utils::get_input_value('noreply', rcube_utils::INPUT_GPC)) || $status == 'needs-action';
-
                 if (!$noreply) {
                     // let the reply clause further down send the iTip message
                     $rec['_reportpartstat'] = $status;
@@ -439,7 +452,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');
-            $this->itip->set_rsvp_actions(array('accepted','declined'));
+            $this->itip->set_rsvp_actions(array('accepted','declined','delegated'));
             $this->itip->set_rsvp_status(array('accepted','tentative','declined','delegated','in-process','completed'));
         }
 
@@ -1701,15 +1714,45 @@ class tasklist extends rcube_plugin
 
         $error_msg = $this->gettext('errorimportingtask');
         $success   = false;
+        $delegate = null;
+
+        if ($status == 'delegated') {
+            $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false);
+            $delegate  = reset($delegates);
+
+            if (empty($delegate) || empty($delegate['mailto'])) {
+                $this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error');
+                return;
+            }
+        }
 
         // successfully parsed tasks?
         if ($task = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'task')) {
             $task = $this->from_ical($task);
 
+            // forward iTip request to delegatee
+            if ($delegate) {
+                $rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST));
+
+                $itip = $this->load_itip();
+                if ($itip->delegate_to($task, $delegate, $rsvpme ? true : false)) {
+                    $this->rc->output->show_message('tasklist.itipsendsuccess', 'confirmation');
+                }
+                else {
+                    $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+                }
+            }
+
             // find writeable list to store the task
             $list_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null;
             $lists   = $this->driver->get_lists();
-            $list    = $lists[$list_id] ?: $this->get_default_tasklist(true, $task['sensitivity'] == 'confidential');
+            $list    = $lists[$list_id];
+            $dontsave = ($_REQUEST['_folder'] === '' && $task['_method'] == 'REQUEST');
+
+            // select default list except user explicitly selected 'none'
+            if (!$list && !$dontsave) {
+                $list = $this->get_default_tasklist(true, $task['sensitivity'] == 'confidential');
+            }
 
             $metadata = array(
                 'uid'      => $task['uid'],
@@ -1732,7 +1775,7 @@ class tasklist extends rcube_plugin
                         $reply_sender         = $attendee['email'];
 
                         $task['attendees'][$i]['status'] = strtoupper($status);
-                        if ($task['attendees'][$i]['status'] != 'NEEDS-ACTION') {
+                        if (!in_array($task['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED'))) {
                             unset($task['attendees'][$i]['rsvp']);  // remove RSVP attribute
                         }
                     }
@@ -1850,7 +1893,7 @@ class tasklist extends rcube_plugin
                     $error_msg = null;
                 }
             }
-            else if ($status == 'declined') {
+            else if ($status == 'declined' || $dontsave) {
                 $error_msg = null;
             }
             else {
@@ -1858,9 +1901,11 @@ class tasklist extends rcube_plugin
             }
         }
 
-        if ($success) {
-            $message = $task['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
-            $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
+        if ($success || $dontsave) {
+            if ($success) {
+                $message = $task['_method'] == 'REPLY' ? 'attendeupdateesuccess' : ($deleted ? 'successremoval' : ($existing ? 'updatedsuccessfully' : 'importedsuccessfully'));
+                $this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('list' => $list['name']))), 'confirmation');
+            }
 
             $metadata['rsvp']         = intval($metadata['rsvp']);
             $metadata['after_action'] = $this->rc->config->get('calendar_itip_after_action', 0);
@@ -1891,7 +1936,17 @@ class tasklist extends rcube_plugin
     /****  Task invitation plugin hooks ****/
 
     /**
-     * Handler for calendar/itip-status requests
+     * Handler for task/itip-delegate requests
+     */
+    function mail_itip_delegate()
+    {
+        // forward request to mail_import_itip() with the right status
+        $_POST['_status'] = $_REQUEST['_status'] = 'delegated';
+        $this->mail_import_itip();
+    }
+
+    /**
+     * Handler for task/itip-status requests
      */
     public function task_itip_status()
     {
@@ -1906,18 +1961,13 @@ class tasklist extends rcube_plugin
         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;
+            $select->add('--', '');
 
             foreach ($lists as $list) {
                 if ($list['editable']) {
                     $select->add($list['name'], $list['id']);
-                    $num++;
                 }
             }
-
-            if ($num <= 1) {
-                $select = null;
-            }
         }
 
         if ($select) {
@@ -1930,7 +1980,7 @@ class tasklist extends rcube_plugin
     }
 
     /**
-     * Handler for calendar/itip-remove requests
+     * Handler for task/itip-remove requests
      */
     public function task_itip_remove()
     {


commit c26f9d25cba791ec5e4735e4c7a14ee1c1b30469
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Nov 6 12:06:51 2014 +0100

    Some fixes to the iTip delegation functions

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 2b37b79..88a4351 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -2638,7 +2638,7 @@ class calendar extends rcube_plugin
           }
           else if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
             $event['attendees'][$i]['status'] = strtoupper($status);
-            if ($event['attendees'][$i]['status'] != 'NEEDS-ACTION')
+            if (!in_array($event['attendees'][$i]['status'], array('NEEDS-ACTION','DELEGATED')))
               unset($event['attendees'][$i]['rsvp']);  // remove RSVP attribute
             $metadata['attendee'] = $attendee['email'];
             $metadata['rsvp'] = $attendee['role'] != 'NON-PARTICIPANT';
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index ae745fe..32d4773 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2349,7 +2349,7 @@ function rcube_calendar_ui(settings)
 
               if (data['delegated-to']) {
                 delete data['delegated-to'];
-                if (data.role == 'NON-PARTICIPANT' && status != 'DECLINED')
+                if (data.role == 'NON-PARTICIPANT' && data.status != 'DECLINED')
                   data.role = 'REQ-PARTICIPANT';
               }
             }
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index e9be25d..9403f46 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -262,6 +262,7 @@ class libcalendaring_itip
      *
      * @param array Event object to delegate
      * @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
+     * @param boolean The delegator's RSVP flag
      * @return boolean True on success, False on failure
      */
     public function delegate_to(&$event, $delegate, $rsvp = false)
@@ -301,6 +302,7 @@ class libcalendaring_itip
               $delegate_index = $i;
               break;
           }
+          // TODO: remove previous delegatee (i.e. attendee that has DELEGATED-FROM == $me)
         }
 
         // set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter
@@ -635,7 +637,7 @@ class libcalendaring_itip
         // add localized texts for the delegation dialog
         if (in_array('delegated', $actions)) {
             foreach (array('itipdelegated','itipcomment','delegateinvitation',
-                  'delegateto','delegatersvpme','delegateinvalidaddress') as $label) {
+                  'delegateto','delegatersvpme','delegateinvalidaddress','cancel') as $label) {
                 $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
             }
         }
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index bb5a66f..f06b55a 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -916,7 +916,8 @@ rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
             rcm.env.recipients_delimiter = '';
         },
         close: function(event, ui) {
-            rcmail.ksearch_blur();
+            rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
+            rcm.ksearch_blur();
             $(this).remove();
         }
     });


commit 4a150a21390f367f2d68fa8e638b1185fa5c8614
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Nov 6 10:07:54 2014 +0100

    Implement iTip delegation functionality for calendar/mail view (#3860)

diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 9b815bc..2b37b79 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -150,6 +150,7 @@ class calendar extends rcube_plugin
       $this->register_action('itip-status', array($this, 'event_itip_status'));
       $this->register_action('itip-remove', array($this, 'event_itip_remove'));
       $this->register_action('itip-decline-reply', array($this, 'mail_itip_decline_reply'));
+      $this->register_action('itip-delegate', array($this, 'mail_itip_delegate'));
       $this->register_action('resources-list', array($this, 'resources_list'));
       $this->register_action('resources-owner', array($this, 'resources_owner'));
       $this->register_action('resources-calendar', array($this, 'resources_calendar'));
@@ -239,7 +240,7 @@ class calendar extends rcube_plugin
       $this->itip = new calendar_itip($this);
       
       if ($this->rc->config->get('kolab_invitation_calendars'))
-        $this->itip->set_rsvp_actions(array('accepted','tentative','declined','needs-action'));
+        $this->itip->set_rsvp_actions(array('accepted','tentative','declined','delegated','needs-action'));
     }
 
     return $this->itip;
@@ -957,6 +958,16 @@ class calendar extends rcube_plugin
 
         $ev = $this->driver->get_event($event);
         $ev['attendees'] = $event['attendees'];
+
+        // send invitation to delegatee + add it as attendee
+        if ($status == 'delegated' && $event['to']) {
+          $itip = $this->load_itip();
+          if ($itip->delegate_to($ev, $event['to'], (bool)$event['rsvp'])) {
+            $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
+            $noreply = false;
+          }
+        }
+
         $event = $ev;
 
         if ($success = $this->driver->edit_rsvp($event, $status)) {
@@ -974,6 +985,7 @@ class calendar extends rcube_plugin
               $reply_sender = $attendee['email'];
             }
           }
+
           if (!$noreply) {
             $itip = $this->load_itip();
             $itip->set_sender_email($reply_sender);
@@ -2567,9 +2579,36 @@ class calendar extends rcube_plugin
 
     $error_msg = $this->gettext('errorimportingevent');
     $success = false;
+    $delegate = null;
+
+    if ($status == 'delegated') {
+      $delegates = rcube_mime::decode_address_list(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true), 1, false);
+      $delegate  = reset($delegates);
+
+      if (empty($delegate) || empty($delegate['mailto'])) {
+        $this->rc->output->command('display_message', $this->gettext('libcalendaring.delegateinvalidaddress'), 'error');
+        return;
+      }
+    }
 
     // successfully parsed events?
     if ($event = $this->lib->mail_get_itip_object($mbox, $uid, $mime_id, 'event')) {
+      // forward iTip request to delegatee
+      if ($delegate) {
+        $rsvpme = intval(rcube_utils::get_input_value('_rsvp', rcube_utils::INPUT_POST));
+
+        $itip = $this->load_itip();
+        if ($itip->delegate_to($event, $delegate, $rsvpme ? true : false)) {
+          $this->rc->output->show_message('calendar.itipsendsuccess', 'confirmation');
+        }
+        else {
+          $this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
+        }
+
+        // the delegator is set to non-participant, thus save as non-blocking
+        $event['free_busy'] = 'free';
+      }
+
       // find writeable calendar to store event
       $cal_id = !empty($_REQUEST['_folder']) ? rcube_utils::get_input_value('_folder', rcube_utils::INPUT_POST) : null;
       $dontsave = ($_REQUEST['_folder'] === '' && $event['_method'] == 'REQUEST');
@@ -2692,14 +2731,14 @@ class calendar extends rcube_plugin
             if ($event['_method'] == 'CANCEL')
               $event['status'] = 'CANCELLED';
             // show me as free when declined (#1670)
-            if ($status == 'declined' || $event['status'] == 'CANCELLED')
+            if ($status == 'declined' || $event['status'] == 'CANCELLED' || $event_attendee['role'] == 'NON-PARTICIPANT')
               $event['free_busy'] = 'free';
 
             $success = $this->driver->edit_event($event);
           }
           else if (!empty($status)) {
             $existing['attendees'] = $event['attendees'];
-            if ($status == 'declined')  // show me as free when declined (#1670)
+            if ($status == 'declined' || $event_attendee['role'] == 'NON-PARTICIPANT')  // show me as free when declined (#1670)
               $existing['free_busy'] = 'free';
             $success = $this->driver->edit_event($existing);
           }
@@ -2781,6 +2820,16 @@ class calendar extends rcube_plugin
   }
 
   /**
+   * Handler for calendar/itip-delegate requests
+   */
+  function mail_itip_delegate()
+  {
+    // forward request to mail_import_itip() with the right status
+    $_POST['_status'] = $_REQUEST['_status'] = 'delegated';
+    $this->mail_import_itip();
+  }
+
+  /**
    * Import the full payload from a mail message attachment
    */
   public function mail_import_attachment()
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 62b263b..ae745fe 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -2323,20 +2323,41 @@ function rcube_calendar_ui(settings)
     }
 
     // when the user accepts or declines an event invitation
-    var event_rsvp = function(response)
+    var event_rsvp = function(response, delegate)
     {
       if (me.selected_event && me.selected_event.attendees && response) {
+        // bring up delegation dialog
+        if (response == 'delegated' && !delegate) {
+          rcube_libcalendaring.itip_delegate_dialog(function(data) {
+            data.rsvp = data.rsvp ? 1 : '';
+            event_rsvp('delegated', data);
+          });
+          return;
+        }
+
         // update attendee status
         for (var data, i=0; i < me.selected_event.attendees.length; i++) {
           data = me.selected_event.attendees[i];
           if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) {
             data.status = response.toUpperCase();
-            delete data.rsvp;  // unset RSVP flag
+
+            if (data.status == 'DELEGATED') {
+              data['delegated-to'] = delegate.to;
+            }
+            else {
+              delete data.rsvp;  // unset RSVP flag
+
+              if (data['delegated-to']) {
+                delete data['delegated-to'];
+                if (data.role == 'NON-PARTICIPANT' && status != 'DECLINED')
+                  data.role = 'REQ-PARTICIPANT';
+              }
+            }
           }
         }
 
         // submit status change to server
-        var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val() }),
+        var submit_data = $.extend({}, me.selected_event, { source:null, comment:$('#reply-comment-event-rsvp').val() }, (delegate || {})),
           noreply = $('#noreply-event-rsvp:checked').length ? 1 : 0;
 
         // import event from mail (temporary iTip event)
@@ -2347,6 +2368,8 @@ function rcube_calendar_ui(settings)
             _uid:  submit_data._uid,
             _part: submit_data._part,
             _status:  response,
+            _to: (delegate ? delegate.to : null),
+            _rsvp: (delegate && delegate.rsvp) ? 1 : 0,
             _noreply: noreply,
             _comment: submit_data.comment
           });
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 722d54f..e01ecd8 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -886,7 +886,7 @@ class calendar_ui
 
   function event_rsvp_buttons($attrib = array())
   {
-    return $this->cal->itip->itip_rsvp_buttons($attrib, array('accepted','tentative','declined'));
+    return $this->cal->itip->itip_rsvp_buttons($attrib, array('accepted','tentative','declined','delegated'));
   }
 
 }
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index d802153..b63b930 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -190,6 +190,8 @@ $labels['itipmailbodyaccepted'] = "\$sender has accepted the invitation to the f
 $labels['itipmailbodytentative'] = "\$sender has tentatively accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
 $labels['itipmailbodydeclined'] = "\$sender has declined the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
 $labels['itipmailbodycancel'] = "\$sender has rejected your participation in the following event:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodydelegated'] = "\$sender has delegated the participation in the following event:\n\n*\$title*\n\nWhen: \$date";
+$labels['itipmailbodydelegatedto'] = "\$sender has delegated the participation in the following event to you:\n\n*\$title*\n\nWhen: \$date";
 
 $labels['itipdeclineevent'] = 'Do you want to decline your invitation to this event?';
 $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?';
diff --git a/plugins/libcalendaring/lib/libcalendaring_itip.php b/plugins/libcalendaring/lib/libcalendaring_itip.php
index b2ec3b8..e9be25d 100644
--- a/plugins/libcalendaring/lib/libcalendaring_itip.php
+++ b/plugins/libcalendaring/lib/libcalendaring_itip.php
@@ -30,7 +30,7 @@ class libcalendaring_itip
     protected $sender;
     protected $domain;
     protected $itip_send = false;
-    protected $rsvp_actions = array('accepted','tentative','declined');
+    protected $rsvp_actions = array('accepted','tentative','declined','delegated');
     protected $rsvp_status  = array('accepted','tentative','declined','delegated');
 
     function __construct($plugin, $domain = 'libcalendaring')
@@ -257,6 +257,61 @@ class libcalendaring_itip
         return $message;
     }
 
+    /**
+     * Forward the given iTip event as delegation to another person
+     *
+     * @param array Event object to delegate
+     * @param mixed Delegatee as string or hash array with keys 'name' and 'mailto'
+     * @return boolean True on success, False on failure
+     */
+    public function delegate_to(&$event, $delegate, $rsvp = false)
+    {
+        if (is_string($delegate)) {
+            $delegates = rcube_mime::decode_address_list($delegate, 1, false);
+            if (count($delegates) > 0) {
+                $delegate = reset($delegates);
+            }
+        }
+
+        $emails = $this->lib->get_user_emails();
+        $me = $this->rc->user->get_identity();
+
+        // find/create the delegate attendee
+        $delegate_attendee = array(
+            'email' => $delegate['mailto'],
+            'name'  => $delegate['name'],
+            'role'  => 'REQ-PARTICIPANT',
+        );
+        $delegate_index = count($event['attendees']);
+
+        foreach ($event['attendees'] as $i => $attendee) {
+          // set myself the DELEGATED-TO parameter
+          if ($attendee['email'] && in_array(strtolower($attendee['email']), $emails)) {
+              $event['attendees'][$i]['delegated-to'] = $delegate['mailto'];
+              $event['attendees'][$i]['status'] = 'DELEGATED';
+              $event['attendees'][$i]['role'] = 'NON-PARTICIPANT';
+              $event['attendees'][$i]['rsvp'] = $rsvp;
+
+              $me['email'] = $attendee['email'];
+              $delegate_attendee['role'] = $attendee['role'];
+          }
+          // the disired delegatee is already listed as an attendee
+          else if (stripos($delegate['mailto'], $attendee['email']) !== false && $attendee['role'] != 'ORGANIZER') {
+              $delegate_attendee = $attendee;
+              $delegate_index = $i;
+              break;
+          }
+        }
+
+        // set/add delegate attendee with RSVP=TRUE and DELEGATED-FROM parameter
+        $delegate_attendee['rsvp'] = true;
+        $delegate_attendee['status'] = 'NEEDS-ACTION';
+        $delegate_attendee['delegated-from'] = $me['email'];
+        $event['attendees'][$delegate_index] = $delegate_attendee;
+
+        $this->set_sender_email($me['email']);
+        return $this->send_itip_message($event, 'REQUEST', $delegate_attendee, 'itipsubjectdelegatedto', 'itipmailbodydelegatedto');
+    }
 
     /**
      * Handler for calendar/itip-status requests
@@ -332,7 +387,7 @@ class libcalendaring_itip
                 $html = html::div('rsvp-status ' . $status_lc, $this->gettext(array(
                     'name' => 'attendee' . $status_lc,
                     'vars' => array(
-                        'delegatedto' => Q($attendee['delegated-to'] ?: '?'),
+                        'delegatedto' => Q($event['delegated-to'] ?: ($attendee['delegated-to'] ?: '?')),
                     )
                 )));
               }
@@ -400,6 +455,8 @@ class libcalendaring_itip
                 if (!empty($attendee['email']) && $attendee['role'] != 'ORGANIZER') {
                     $metadata['attendee'] = $attendee['email'];
                     $rsvp_status = strtoupper($attendee['status']);
+                    if ($attendee['delegated-to'])
+                        $metadata['delegated-to'] = $attendee['delegated-to'];
                     break;
                 }
             }
@@ -498,6 +555,11 @@ class libcalendaring_itip
 
             $buttons[] = html::div(array('id' => 'rsvp-'.$dom_id, 'class' => 'rsvp-buttons', 'style' => 'display:none'), $rsvp_buttons);
             $buttons[] = html::div(array('id' => 'update-'.$dom_id, 'style' => 'display:none'), $update_button);
+
+            // prepare autocompletion for delegation dialog
+            if (in_array('delegated', $this->rsvp_actions)) {
+                $this->rc->autocomplete_init();
+            }
         }
         // for CANCEL messages, we can:
         else if ($method == 'CANCEL') {
@@ -530,8 +592,6 @@ class libcalendaring_itip
             $buttons[] = html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
         }
 
-        // TODO: add option/checkbox to delete this message after update
-
         // pass some metadata about the event and trigger the asynchronous status check
         $metadata['fallback'] = $rsvp_status;
         $metadata['rsvp'] = intval($metadata['rsvp']);
@@ -539,7 +599,9 @@ class libcalendaring_itip
         $this->rc->output->add_script("rcube_libcalendaring.fetch_itip_object_status(" . json_serialize($metadata) . ")", 'docready');
 
         // get localized texts from the right domain
-        foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee','declineattendeeconfirm','cancel') as $label) {
+        foreach (array('savingdata','deleteobjectconfirm','declinedeleteconfirm','declineattendee',
+            'cancel','itipdelegated','declineattendeeconfirm','itipcomment','delegateinvitation',
+            'delegateto','delegatersvpme','delegateinvalidaddress') as $label) {
           $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
         }
 
@@ -570,6 +632,14 @@ class libcalendaring_itip
 
         $buttons .= html::div('itip-reply-controls', $this->itip_rsvp_options_ui($attrib['id']));
 
+        // add localized texts for the delegation dialog
+        if (in_array('delegated', $actions)) {
+            foreach (array('itipdelegated','itipcomment','delegateinvitation',
+                  'delegateto','delegatersvpme','delegateinvalidaddress') as $label) {
+                $this->rc->output->command('add_label', "itip.$label", $this->gettext($label));
+            }
+        }
+
         return html::div($attrib,
             html::div('label', $this->gettext('acceptinvitation')) .
             html::div('rsvp-buttons', $buttons));
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js
index 8bf9c35..bb5a66f 100644
--- a/plugins/libcalendaring/libcalendaring.js
+++ b/plugins/libcalendaring/libcalendaring.js
@@ -803,6 +803,22 @@ rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id
         del = confirm(rcmail.gettext('itip.declinedeleteconfirm'));
     }
 
+    // open dialog for iTip delegation
+    if (status == 'delegated') {
+        rcube_libcalendaring.itip_delegate_dialog(function(data) {
+            rcmail.http_post(task + '/itip-delegate', {
+                _uid: rcmail.env.uid,
+                _mbox: rcmail.env.mailbox,
+                _part: mime_id,
+                _to: data.to,
+                _rsvp: data.rsvp ? 1 : 0,
+                _comment: data.comment,
+                _folder: data.target
+            }, rcmail.set_busy(true, 'itip.savingdata'));
+        }, $('#rsvp-'+dom_id+' .folder-select'));
+        return false;
+    }
+
     var noreply = 0, comment = '';
     if (dom_id) {
       noreply = $('#noreply-'+dom_id+':checked').length ? 1 : 0;
@@ -825,6 +841,90 @@ rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id
 };
 
 /**
+ * Helper function to render the iTip delegation dialog
+ * and trigger a callback function when submitted.
+ */
+rcube_libcalendaring.itip_delegate_dialog = function(callback, selector)
+{
+    // show dialog for entering the delegatee address and comment
+    var html = '<form class="itip-dialog-form" action="javascript:void()">' +
+        '<div class="form-section">' +
+            '<label for="itip-delegate-to">' + rcmail.gettext('itip.delegateto') + '</label><br/>' +
+            '<input type="text" id="itip-delegate-to" class="text" size="40" value="" />' +
+        '</div>' +
+        '<div class="form-section">' +
+            '<label for="itip-delegate-rsvp">' +
+                '<input type="checkbox" id="itip-delegate-rsvp" class="checkbox" size="40" value="" />' +
+                rcmail.gettext('itip.delegatersvpme') +
+            '</label>' +
+        '</div>' +
+        '<div class="form-section">' +
+            '<textarea id="itip-delegate-comment" class="itip-comment" cols="40" rows="8" placeholder="' +
+                rcmail.gettext('itip.itipcomment') + '"></textarea>' + 
+        '</div>' +
+        '<div class="form-section">' +
+            (selector ? selector.html() : '') +
+        '</div>' +
+    '</form>';
+
+    var dialog, buttons = [];
+    buttons.push({
+        text: rcmail.gettext('itipdelegated', 'itip'),
+        click: function() {
+            var doc = window.parent.document,
+                delegatee = String($('#itip-delegate-to', doc).val()).replace(/(^\s+)|(\s+$)/, '');
+
+            if (delegatee != '' && rcube_check_email(delegatee, true)) {
+                callback({
+                    to: delegatee,
+                    rsvp: $('#itip-delegate-rsvp', doc).prop('checked'),
+                    comment: $('#itip-delegate-comment', doc).val(),
+                    target: $('#itip-saveto', doc).val()
+                });
+
+                setTimeout(function() { dialog.dialog("close"); }, 500);
+            }
+            else {
+                alert(rcmail.gettext('itip.delegateinvalidaddress'));
+                $('#itip-delegate-to', doc).focus();
+            }
+        }
+    });
+
+    buttons.push({
+        text: rcmail.gettext('cancel', 'itip'),
+        click: function() {
+            dialog.dialog('close');
+        }
+    });
+
+    dialog = rcmail.show_popup_dialog(html, rcmail.gettext('delegateinvitation', 'itip'), buttons, {
+        width: 460,
+        open: function(event, ui) {
+            $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
+            $(this).find('#itip-saveto').val('');
+
+            // initialize autocompletion
+            var ac_props, rcm = rcmail.is_framed() ? parent.rcmail : rcmail;
+            if (rcmail.env.autocomplete_threads > 0) {
+                ac_props = {
+                    threads: rcmail.env.autocomplete_threads,
+                    sources: rcmail.env.autocomplete_sources
+                };
+            }
+            rcm.init_address_input_events($(this).find('#itip-delegate-to').focus(), ac_props);
+            rcm.env.recipients_delimiter = '';
+        },
+        close: function(event, ui) {
+            rcmail.ksearch_blur();
+            $(this).remove();
+        }
+    });
+
+    return dialog;
+};
+
+/**
  *
  */
 rcube_libcalendaring.remove_from_itip = function(uid, task, title)
@@ -870,7 +970,7 @@ rcube_libcalendaring.decline_attendee_reply = function(mime_id, task)
     dialog = rcmail.show_popup_dialog(html, rcmail.gettext('declineattendee', 'itip'), buttons, {
         width: 460,
         open: function() {
-            $(this).parent().find('.ui-button').first().addClass('mainaction');
+            $(this).parent().find('.ui-button:not(.ui-dialog-titlebar-close)').first().addClass('mainaction');
             $('#itip-decline-comment').focus();
         }
     });
diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc
index 9c3507c..2542c47 100644
--- a/plugins/libcalendaring/localization/en_US.inc
+++ b/plugins/libcalendaring/localization/en_US.inc
@@ -98,6 +98,8 @@ $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['itipsubjectdelegated'] = '"$title" has been delegated by $name';
+$labels['itipsubjectdelegatedto'] = '"$title" has been delegated to you by $name';
 
 $labels['itipnewattendee'] = 'This is a reply from a new participant';
 $labels['updateattendeestatus'] = 'Update the participant\'s status';
@@ -139,6 +141,11 @@ $labels['openpreview'] = 'Open Preview';
 $labels['deleteobjectconfirm'] = 'Do you really want to delete this object?';
 $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined object from your account?';
 
+$labels['delegateinvitation'] = 'Delegate Invitation';
+$labels['delegateto'] = 'Delegate to';
+$labels['delegatersvpme'] = 'Keep me informed about updates of this incidence';
+$labels['delegateinvalidaddress'] = 'Please enter a valid email address for the delegate';
+
 $labels['savingdata'] = 'Saving data...';
 
 // attendees labels
diff --git a/plugins/libcalendaring/skins/larry/libcal.css b/plugins/libcalendaring/skins/larry/libcal.css
index 89e123f..f679abc 100644
--- a/plugins/libcalendaring/skins/larry/libcal.css
+++ b/plugins/libcalendaring/skins/larry/libcal.css
@@ -156,3 +156,11 @@ label.noreply-toggle + a.reply-comment-toggle {
 	margin-top: -1.4em;
 }
 
+.itip-dialog-form input.text {
+	width: 98%;
+}
+
+.itip-dialog-form label > input.checkbox {
+	margin-left: 0;
+	margin-right: 10px;
+}


commit 17f8ec0d04fe4d22c51f12ac8c9600a344efa16c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Wed Nov 5 15:17:43 2014 +0100

    Align calendar labels with libcalendaring and tasklist plugins

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 912f6ce..62b263b 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -445,7 +445,7 @@ function rcube_calendar_ui(settings)
 
       if (event.status) {
         var status_lc = String(event.status).toLowerCase();
-        $('#event-status').show().children('.event-text').html(Q(rcmail.gettext(status_lc,'calendar')));
+        $('#event-status').show().children('.event-text').html(Q(rcmail.gettext('status-'+status_lc,'calendar')));
         $dialog.addClass('status-'+status_lc);
       }
       if (event.sensitivity && event.sensitivity != 'public') {
@@ -868,7 +868,7 @@ function rcube_calendar_ui(settings)
       };
 
       if (event.id) {
-        buttons[rcmail.gettext('remove', 'calendar')] = function() {
+        buttons[rcmail.gettext('delete', 'calendar')] = function() {
           me.delete_event(event);
           $dialog.dialog('close');
         };
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 612bda5..722d54f 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -407,8 +407,8 @@ class calendar_ui
     $attrib['name'] = 'status';
     $select = new html_select($attrib);
     $select->add('---', '');
-    $select->add($this->cal->gettext('confirmed'), 'CONFIRMED');
-    $select->add($this->cal->gettext('cancelled'), 'CANCELLED');
+    $select->add($this->cal->gettext('status-confirmed'), 'CONFIRMED');
+    $select->add($this->cal->gettext('status-cancelled'), 'CANCELLED');
     //$select->add($this->cal->gettext('tentative'), 'TENTATIVE');
     return $select->show(null);
   }
diff --git a/plugins/calendar/localization/ca_ES.inc b/plugins/calendar/localization/ca_ES.inc
index 53c02ec..a49f56a 100644
--- a/plugins/calendar/localization/ca_ES.inc
+++ b/plugins/calendar/localization/ca_ES.inc
@@ -73,8 +73,8 @@ $labels['outofoffice'] = 'Fora de l\'oficina';
 $labels['tentative'] = 'Provisional';
 $labels['mystatus'] = 'El meu estat';
 $labels['status'] = 'Estat';
-$labels['confirmed'] = 'Confirmat';
-$labels['cancelled'] = 'Cancel·lat';
+$labels['status-confirmed'] = 'Confirmat';
+$labels['status-cancelled'] = 'Cancel·lat';
 $labels['priority'] = 'Prioritat';
 $labels['sensitivity'] = 'Privadesa';
 $labels['public'] = 'públic';
diff --git a/plugins/calendar/localization/da_DK.inc b/plugins/calendar/localization/da_DK.inc
index 006d49f..f76df92 100644
--- a/plugins/calendar/localization/da_DK.inc
+++ b/plugins/calendar/localization/da_DK.inc
@@ -62,8 +62,8 @@ $labels['busy'] = 'Optaget';
 $labels['outofoffice'] = 'Ikke på kontoret';
 $labels['tentative'] = 'Forsøgsvis';
 $labels['status'] = 'Status';
-$labels['confirmed'] = 'Bekræftet';
-$labels['cancelled'] = 'Annulleret';
+$labels['status-confirmed'] = 'Bekræftet';
+$labels['status-cancelled'] = 'Annulleret';
 $labels['priority'] = 'Prioritet';
 $labels['sensitivity'] = 'Privatliv';
 $labels['public'] = 'offentlig';
diff --git a/plugins/calendar/localization/de_DE.inc b/plugins/calendar/localization/de_DE.inc
index e68d8b2..9ec5ba4 100644
--- a/plugins/calendar/localization/de_DE.inc
+++ b/plugins/calendar/localization/de_DE.inc
@@ -80,8 +80,8 @@ $labels['outofoffice'] = 'Abwesend';
 $labels['tentative'] = 'Mit Vorbehalt';
 $labels['mystatus'] = 'Mein Status';
 $labels['status'] = 'Status';
-$labels['confirmed'] = 'Bestätigt';
-$labels['cancelled'] = 'Gekündigt';
+$labels['status-confirmed'] = 'Bestätigt';
+$labels['status-cancelled'] = 'Gekündigt';
 $labels['priority'] = 'Priorität';
 $labels['sensitivity'] = 'Sichtbarkeit';
 $labels['public'] = 'öffentlich';
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index 630a47b..d802153 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -80,8 +80,8 @@ $labels['outofoffice'] = 'Out of Office';
 $labels['tentative'] = 'Tentative';
 $labels['mystatus'] = 'My status';
 $labels['status'] = 'Status';
-$labels['confirmed'] = 'Confirmed';
-$labels['cancelled'] = 'Cancelled';
+$labels['status-confirmed'] = 'Confirmed';
+$labels['status-cancelled'] = 'Cancelled';
 $labels['priority'] = 'Priority';
 $labels['sensitivity'] = 'Privacy';
 $labels['public'] = 'public';
diff --git a/plugins/calendar/localization/es_AR.inc b/plugins/calendar/localization/es_AR.inc
index 353e867..a0d4420 100644
--- a/plugins/calendar/localization/es_AR.inc
+++ b/plugins/calendar/localization/es_AR.inc
@@ -73,8 +73,8 @@ $labels['outofoffice'] = 'Fuera de la oficina';
 $labels['tentative'] = 'Tentativo';
 $labels['mystatus'] = 'Mi estado';
 $labels['status'] = 'Estado';
-$labels['confirmed'] = 'Confirmado';
-$labels['cancelled'] = 'Cancelado';
+$labels['status-confirmed'] = 'Confirmado';
+$labels['status-cancelled'] = 'Cancelado';
 $labels['priority'] = 'Prioridad';
 $labels['sensitivity'] = 'Privacidad';
 $labels['public'] = 'público';
diff --git a/plugins/calendar/localization/fi_FI.inc b/plugins/calendar/localization/fi_FI.inc
index 71eeaad..e473619 100644
--- a/plugins/calendar/localization/fi_FI.inc
+++ b/plugins/calendar/localization/fi_FI.inc
@@ -55,8 +55,8 @@ $labels['freebusy'] = 'Aseta tilakseni';
 $labels['free'] = 'Vapaa';
 $labels['busy'] = 'Varattu';
 $labels['status'] = 'Tila';
-$labels['confirmed'] = 'Vahvistettu';
-$labels['cancelled'] = 'Peruttu';
+$labels['status-confirmed'] = 'Vahvistettu';
+$labels['status-cancelled'] = 'Peruttu';
 $labels['priority'] = 'Tärkeys';
 $labels['sensitivity'] = 'Yksityisyys';
 $labels['public'] = 'julkinen';
diff --git a/plugins/calendar/localization/fr_FR.inc b/plugins/calendar/localization/fr_FR.inc
index 31fed82..c096375 100644
--- a/plugins/calendar/localization/fr_FR.inc
+++ b/plugins/calendar/localization/fr_FR.inc
@@ -61,8 +61,8 @@ $labels['busy'] = 'Occupé';
 $labels['outofoffice'] = 'Absent';
 $labels['tentative'] = 'Provisoire';
 $labels['status'] = 'Statut';
-$labels['confirmed'] = 'Confirmé';
-$labels['cancelled'] = 'Annulé';
+$labels['status-confirmed'] = 'Confirmé';
+$labels['status-cancelled'] = 'Annulé';
 $labels['priority'] = 'Priorité';
 $labels['sensitivity'] = 'Diffusion';
 $labels['public'] = 'publique';
diff --git a/plugins/calendar/localization/hu_HU.inc b/plugins/calendar/localization/hu_HU.inc
index 7496362..7ba7c0b 100644
--- a/plugins/calendar/localization/hu_HU.inc
+++ b/plugins/calendar/localization/hu_HU.inc
@@ -63,8 +63,8 @@ $labels['busy'] = 'Foglalt';
 $labels['outofoffice'] = 'Házon kívűl';
 $labels['tentative'] = 'Feltételes';
 $labels['status'] = 'Stát.';
-$labels['confirmed'] = 'Confirmed';
-$labels['cancelled'] = 'Cancelled';
+$labels['status-confirmed'] = 'Confirmed';
+$labels['status-cancelled'] = 'Cancelled';
 $labels['priority'] = 'Prioritás';
 $labels['sensitivity'] = 'Manánszféra';
 $labels['public'] = 'publikus';
diff --git a/plugins/calendar/localization/nl_NL.inc b/plugins/calendar/localization/nl_NL.inc
index 4f18e21..9c0c331 100644
--- a/plugins/calendar/localization/nl_NL.inc
+++ b/plugins/calendar/localization/nl_NL.inc
@@ -63,8 +63,8 @@ $labels['busy'] = 'Bezet';
 $labels['outofoffice'] = 'Niet Aanwezig';
 $labels['tentative'] = 'Misschien';
 $labels['status'] = 'Status';
-$labels['confirmed'] = 'Bevestigd';
-$labels['cancelled'] = 'Afgelast';
+$labels['status-confirmed'] = 'Bevestigd';
+$labels['status-cancelled'] = 'Afgelast';
 $labels['priority'] = 'Prioriteit';
 $labels['sensitivity'] = 'Zichtbaarheid';
 $labels['public'] = 'publiek';
diff --git a/plugins/calendar/localization/pl_PL.inc b/plugins/calendar/localization/pl_PL.inc
index 6df3d20..8bae25a 100644
--- a/plugins/calendar/localization/pl_PL.inc
+++ b/plugins/calendar/localization/pl_PL.inc
@@ -73,8 +73,8 @@ $labels['outofoffice'] = 'Poza biurem';
 $labels['tentative'] = 'Niepewny';
 $labels['mystatus'] = 'Mój status';
 $labels['status'] = 'Status';
-$labels['confirmed'] = 'Potwierdzony';
-$labels['cancelled'] = 'Anulowany';
+$labels['status-confirmed'] = 'Potwierdzony';
+$labels['status-cancelled'] = 'Anulowany';
 $labels['priority'] = 'Priorytet';
 $labels['sensitivity'] = 'Poufność';
 $labels['public'] = 'publiczny';
diff --git a/plugins/calendar/localization/pt_BR.inc b/plugins/calendar/localization/pt_BR.inc
index 78650fb..a959a72 100644
--- a/plugins/calendar/localization/pt_BR.inc
+++ b/plugins/calendar/localization/pt_BR.inc
@@ -63,8 +63,8 @@ $labels['busy'] = 'Ocupado';
 $labels['outofoffice'] = 'Fora de escritório';
 $labels['tentative'] = 'Tentativa';
 $labels['status'] = 'Situação';
-$labels['confirmed'] = 'Confirmado';
-$labels['cancelled'] = 'Cancalado';
+$labels['status-confirmed'] = 'Confirmado';
+$labels['status-cancelled'] = 'Cancalado';
 $labels['priority'] = 'Prioridade';
 $labels['sensitivity'] = 'Privacidade';
 $labels['public'] = 'público';
diff --git a/plugins/calendar/localization/ru_RU.inc b/plugins/calendar/localization/ru_RU.inc
index 84ae137..b15f32e 100644
--- a/plugins/calendar/localization/ru_RU.inc
+++ b/plugins/calendar/localization/ru_RU.inc
@@ -73,8 +73,8 @@ $labels['outofoffice'] = 'Вне офиса';
 $labels['tentative'] = 'Неопределённо';
 $labels['mystatus'] = 'Мой статус';
 $labels['status'] = 'Статус';
-$labels['confirmed'] = 'Подтвеждённый';
-$labels['cancelled'] = 'Отмененный';
+$labels['status-confirmed'] = 'Подтвеждённый';
+$labels['status-cancelled'] = 'Отмененный';
 $labels['priority'] = 'Приоритет';
 $labels['sensitivity'] = 'Секретность';
 $labels['public'] = 'общедоступная';




More information about the commits mailing list