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

This commit is contained in:
Thomas Bruederli 2014-11-06 10:07:54 +01:00
parent 17f8ec0d04
commit 4a150a2139
8 changed files with 272 additions and 13 deletions

View file

@ -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
*/

View file

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

View file

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

View file

@ -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?';

View file

@ -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));

View file

@ -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 = '<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;
};
/**
*
*/
@ -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();
}
});

View file

@ -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

View file

@ -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;
}