diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 9b815bc8..2b37b799 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); } @@ -2780,6 +2819,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 */ diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 62b263bc..ae745fe5 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 722d54fc..e01ecd80 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 d802153c..b63b9305 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 b2ec3b8b..e9be25d9 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 8bf9c356..bb5a66f9 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; @@ -824,6 +840,90 @@ rcube_libcalendaring.add_from_itip_mail = function(mime_id, task, status, dom_id return false; }; +/** + * 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 = '
'; + + 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; +}; + /** * */ @@ -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 9c3507cc..2542c47c 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 89e123ff..f679abc3 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; +}