More iTip implementation: send/recieve replies, make organizer role immutable (until delegation is implemented)

This commit is contained in:
Thomas 2011-08-22 19:20:46 +02:00
parent 218854bca5
commit 36c20e8628
9 changed files with 401 additions and 116 deletions

View file

@ -219,6 +219,7 @@ class calendar extends rcube_plugin
$this->register_handler('plugin.attendees_freebusy_table', array($this->ui, 'attendees_freebusy_table'));
$this->register_handler('plugin.edit_attendees_notify', array($this->ui, 'edit_attendees_notify'));
$this->register_handler('plugin.edit_recurring_warning', array($this->ui, 'recurring_event_warning'));
$this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
$this->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
$this->rc->output->add_label('low','normal','high','delete','cancel','uploading','noemailwarning');
@ -228,6 +229,13 @@ class calendar extends rcube_plugin
$this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$this->rc->output->set_env('view', $view);
if ($date = get_input_value('date', RCUBE_INPUT_GPC))
$this->rc->output->set_env('date', $date);
$this->rc->output->send("calendar.calendar");
}
@ -534,7 +542,7 @@ class calendar extends rcube_plugin
}
$reload = true;
break;
case "edit":
$this->prepare_event($event, $action);
if ($success = $this->driver->edit_event($event))
@ -592,6 +600,27 @@ class calendar extends rcube_plugin
break;
case "rsvp":
$ev = $this->driver->get_event($event);
$ev['attendees'] = $event['attendees'];
$event = $ev;
if ($success = $this->driver->edit_event($event)) {
$status = get_input_value('status', RCUBE_INPUT_GPC);
$organizer = null;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
break;
}
}
if ($organizer && $this->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
break;
case "dismiss":
foreach (explode(',', $event['id']) as $id)
$success |= $this->driver->dismiss_alarm($id, $event['snooze']);
@ -740,8 +769,12 @@ class calendar extends rcube_plugin
// get user identity to create default attendee
if ($this->ui->screen == 'calendar') {
$identity = $this->rc->user->get_identity();
$settings['event_owner'] = array('name' => $identity['name'], 'email' => $identity['email']);
foreach ($this->rc->user->list_identities() as $rec) {
if (!$identity)
$identity = $rec;
$identity['emails'][] = $rec['email'];
}
$settings['identity'] = array('name' => $identity['name'], 'email' => $identity['email'], 'emails' => ';' . join(';', $identity['emails']));
}
return $settings;
@ -1254,8 +1287,8 @@ class calendar extends rcube_plugin
// set owner as organizer if yet missing
if (!$organizer && $owner !== false) {
$event['attendees'][$i]['role'] = 'ORGANIZER';
unset($event['attendees'][$i]['rsvp']);
$event['attendees'][$owner]['role'] = 'ORGANIZER';
unset($event['attendees'][$owner]['rsvp']);
}
else if (!$organizer && $identity['email'] && $action == 'new') {
array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED'));
@ -1280,83 +1313,35 @@ class calendar extends rcube_plugin
*/
private function notify_attendees($event, $old, $action = 'edit')
{
$sent = 0;
$myself = $this->rc->user->get_identity();
$from = rcube_idn_to_ascii($myself['email']);
$sender = format_email_recipient($from, $myself['name']);
// compose multipart message using PEAR:Mail_Mime
$message = new Mail_mime("\r\n");
$message->setParam('text_encoding', 'quoted-printable');
$message->setParam('head_encoding', 'quoted-printable');
$message->setParam('head_charset', RCMAIL_CHARSET);
$message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed");
// compose common headers array
$headers = array(
'From' => $sender,
'Date' => rcmail_user_date(),
'Message-ID' => rcmail_gen_message_id(),
'X-Sender' => $from,
);
if ($agent = $this->rc->config->get('useragent'))
$headers['User-Agent'] = $agent;
if ($action == 'remove') {
$event['cancelled'] = true;
$is_cancelled = true;
}
// attach ics file for this event
$this->load_ical();
$vcal = $this->ical->export(array($event), 'REQUEST');
$message->addAttachment($vcal, 'text/calendar', 'event.ics', false, '8bit', 'attachment', RCMAIL_CHARSET);
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
$message = $this->compose_itip_message($event, $method);
// list existing attendees from $old event
$old_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
// compose a list of all event attendees
$attendees_list = array();
foreach ((array)$event['attendees'] as $attendee) {
$attendees_list[] = ($attendee['name'] && $attendee['email']) ?
$attendee['name'] . ' <' . $attendee['email'] . '>' :
($attendee['name'] ? $attendee['name'] : $attendee['email']);
}
// send to every attendee
$sent = 0;
foreach ((array)$event['attendees'] as $attendee) {
// skip myself for obvious reasons
if (!$attendee['email'] || $attendee['email'] == $myself['email'])
continue;
// which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees);
$mailto = rcube_idn_to_ascii($attendee['email']);
$headers['To'] = format_email_recipient($mailto, $attendee['name']);
$headers['Subject'] = $this->gettext(array(
'name' => $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty')),
'vars' => array('title' => $event['title']),
));
// compose message body
$body = $this->gettext(array(
'name' => $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody'),
'vars' => array(
'title' => $event['title'],
'date' => $this->event_date_text($event),
'attendees' => join(', ', $attendees_list),
'organizer' => $myself['name'],
)
));
$message->headers($headers);
$message->setTXTBody(rcube_message::format_flowed($body, 79));
$bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
$subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty'));
// finally send the message
if (rcmail_deliver_message($message, $from, $mailto, $smtp_error))
if ($this->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message))
$sent++;
else
$sent = -100;
@ -1577,21 +1562,76 @@ class calendar extends rcube_plugin
if (empty($events))
continue;
// TODO: show more iTip options like (accept, deny, etc.)
// show a box for every event in the file
foreach ($events as $idx => $event) {
$prefix = $this->ical->method == 'REPLY' ? $this->gettext('itipreply') : '';
// add box below messsage body
$html .= html::p('calendar-invitebox',
html::span('eventtitle', Q($prefix . $event['title'])) . ' ' .
html::span('eventdate', Q('(' . $this->event_date_text($event) . ')')) . ' ' .
html::tag('input', array(
// define buttons according to method
if ($this->ical->method == 'REPLY') {
$title = $this->gettext('itipreply');
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "', '" . JQ($event['title']) . "')",
'value' => $this->ical->method == 'REPLY' ? $this->gettext('updateattendeestatus') : $this->gettext('importtocalendar'),
))
);
'value' => $this->gettext('updateattendeestatus'),
));
}
else if ($this->ical->method == 'REQUEST') {
$title = $event['SEQUENCE'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
// TODO: check for a copy in my calendar in order to show the right options
if (!$event['SEQUENCE']) {
foreach (array('accepted','tentative','declined') as $method) {
$buttons .= html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "', '$method')",
'value' => $this->gettext('itip' . $method),
));
}
}
else {
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
}
}
else if ($this->ical->method == 'CANCEL') {
$title = $this->gettext('itipcancellation');
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.remove_event_from_mail('" . JQ($event['uid']) . "', '" . JQ($event['title']) . "')",
'value' => $this->gettext('importtocalendar'),
));
}
else {
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
}
// show event details in a table
$table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
$table->add('ititle', $title);
$table->add('title', Q($event['title']));
$table->add('label', $this->gettext('date'));
$table->add('location', Q($this->event_date_text($event)));
if ($event['location']) {
$table->add('label', $this->gettext('location'));
$table->add('location', Q($event['location']));
}
// add box below messsage body
$html .= html::div('calendar-invitebox', $table->show() . html::div('rsvp-buttons', $buttons));
// limit listing
if ($idx >= 3)
break;
}
}
@ -1604,6 +1644,7 @@ class calendar extends rcube_plugin
return $p;
}
/**
* Handler for POST request to import an event attached to a mail message
*/
@ -1612,6 +1653,7 @@ class calendar extends rcube_plugin
$uid = get_input_value('_uid', RCUBE_INPUT_POST);
$mbox = get_input_value('_mbox', RCUBE_INPUT_POST);
$mime_id = get_input_value('_part', RCUBE_INPUT_POST);
$status = get_input_value('_status', RCUBE_INPUT_POST);
$charset = RCMAIL_CHARSET;
// establish imap connection
@ -1646,8 +1688,25 @@ class calendar extends rcube_plugin
}
}
}
// update my attendee status according to submitted method
if (!empty($status)) {
$emails = array($this->rc->user->get_username());
foreach ($this->rc->user->list_identities() as $identity)
$emails[] = $identity['email'];
$organizer = null;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
}
else if ($attendee['email'] && in_array($attendee['email'], $emails)) {
$event['attendees'][$i]['status'] = strtoupper($status);
}
}
}
if ($calendar && !$calendar['readonly']) {
// save to calendar
if ($calendar && !$calendar['readonly'] && $status != 'declined') {
$event['id'] = $event['uid'];
$event['calendar'] = $calendar['id'];
@ -1701,6 +1760,8 @@ class calendar extends rcube_plugin
$success = $this->driver->new_event($event);
}
}
else if ($status == 'declined')
$error_msg = null;
else
$error_msg = $this->gettext('nowritecalendarfound');
}
@ -1708,14 +1769,117 @@ class calendar extends rcube_plugin
if ($success) {
$message = $this->ical->method == 'REPLY' ? 'attendeupdateesuccess' : 'importedsuccessfully';
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
$error_msg = null;
}
else
else if ($error_msg)
$this->rc->output->command('display_message', $error_msg, 'error');
// send iTip reply
if ($this->ical->method == 'REQUEST' && $organizer && !$error_msg) {
if ($this->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
$this->rc->output->send();
}
/**
* Send an iTip mail message
*
* @param array Event object to send
* @param string iTip method (REQUEST|REPLY|CANCEL)
* @param array Hash array with recipient data (name, email)
* @param string Mail subject
* @param string Mail body text label
* @param object Mail_mime object with message data
* @return boolean True on success, false on failure
*/
public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null)
{
if (!$message)
$message = $this->compose_itip_message($event, $method);
$myself = $this->rc->user->get_identity();
$mailto = rcube_idn_to_ascii($recipient['email']);
$headers = $message->headers();
$headers['To'] = format_email_recipient($mailto, $recipient['name']);
$headers['Subject'] = $this->gettext(array(
'name' => $subject,
'vars' => array('title' => $event['title'], 'name' => ($myself['name'] ? $myself['name'] : $myself['email']))
));
// compose a list of all event attendees
$attendees_list = array();
foreach ((array)$event['attendees'] as $attendee) {
$attendees_list[] = ($attendee['name'] && $attendee['email']) ?
$attendee['name'] . ' <' . $attendee['email'] . '>' :
($attendee['name'] ? $attendee['name'] : $attendee['email']);
}
$mailbody = $this->gettext(array(
'name' => $bodytext,
'vars' => array(
'title' => $event['title'],
'date' => $this->event_date_text($event),
'attendees' => join(', ', $attendees_list),
'sender' => $myself['name'] ? $myself['name'] : $myself['email'],
'organizer' => $myself['name'],
)
));
$message->headers($headers);
$message->setTXTBody(rcube_message::format_flowed($mailbody, 79));
// finally send the message
return rcmail_deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
}
/**
* Helper function to build a Mail_mime object to send an iTip message
*
* @param array Event object to send
* @param string iTip method (REQUEST|REPLY|CANCEL)
* @return object Mail_mime object with message data
*/
private function compose_itip_message($event, $method)
{
$myself = $this->rc->user->get_identity();
$from = rcube_idn_to_ascii($myself['email']);
$sender = format_email_recipient($from, $myself['name']);
// compose multipart message using PEAR:Mail_Mime
$message = new Mail_mime("\r\n");
$message->setParam('text_encoding', 'quoted-printable');
$message->setParam('head_encoding', 'quoted-printable');
$message->setParam('head_charset', RCMAIL_CHARSET);
$message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed");
// compose common headers array
$headers = array(
'From' => $sender,
'Date' => rcmail_user_date(),
'Message-ID' => rcmail_gen_message_id(),
'X-Sender' => $from,
);
if ($agent = $this->rc->config->get('useragent'))
$headers['User-Agent'] = $agent;
$message->headers($headers);
// attach ics file for this event
$this->load_ical();
$vcal = $this->ical->export(array($event), $method);
$message->addAttachment($vcal, 'text/calendar', 'event.ics', false, '8bit', 'attachment', RCMAIL_CHARSET);
return $message;
}
/**
* Checks if specified message part is a vcalendar data
*

View file

@ -159,10 +159,15 @@ function rcube_calendar(settings)
}
// static methods
rcube_calendar.add_event_from_mail = function(mime_id, title)
rcube_calendar.add_event_from_mail = function(mime_id, status)
{
var lock = rcmail.set_busy(true, 'loading');
rcmail.http_post('calendar/mailimportevent', '_uid='+rcmail.env.uid+'&_mbox='+urlencode(rcmail.env.mailbox)+'&_part='+urlencode(mime_id), lock);
rcmail.http_post('calendar/mailimportevent', {
'_uid': rcmail.env.uid,
'_mbox': rcmail.env.mailbox,
'_part': mime_id,
'_status': status
}, lock);
return false;
};

View file

@ -137,7 +137,17 @@ function rcube_calendar_ui(settings)
// check if the event has 'real' attendees, excluding the current user
var has_attendees = function(event)
{
return (event.attendees && (event.attendees.length > 1 || event.attendees[0].email != settings.event_owner.email));
return (event.attendees && (event.attendees.length > 1 || event.attendees[0].email != settings.identity.email));
};
// check if the current user is the organizer
var is_organizer = function(event)
{
for (var i=0; event.attendees && i < event.attendees.length; i++) {
if (event.attendees[i].role == 'ORGANIZER' && event.attendees[i].email && event.attendees[i].email == settings.identity.email)
return true;
}
return !event.id;
};
// create a nice human-readable string for the date/time range
@ -233,6 +243,7 @@ function rcube_calendar_ui(settings)
{
var $dialog = $("#eventshow");
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
me.selected_event = event;
$dialog.find('div.event-section, div.event-line').hide();
$('#event-title').html(Q(event.title)).show();
@ -279,27 +290,37 @@ function rcube_calendar_ui(settings)
// list event attendees
if (calendar.attendees && event.attendees) {
var data, dispname, organizer = false, html = '';
var dispname, html = '';
if (event.organizer) {
dispname = Q(data.name || data.email);
html += '<span class="attendee organizer">' + dispname + '</span> ';
}
var data, rsvp = false;
for (var j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
dispname = Q(data.name || data.email);
if (data.email)
if (data.email) {
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
html += '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '">' + dispname + '</span> ';
if (data.role == 'ORGANIZER')
organizer = true;
if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' && settings.identity.emails.indexOf(';'+data.email) >= 0)
rsvp = true;
}
html += '<span class="attendee ' + data.status.toLowerCase() + '">' + dispname + '</span> ';
// stop listing attendees
if (j == 7 && event.attendees.length >= 7) {
html += ' <em>' + rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1) + '</em>';
break;
}
}
if (html && event.attendees.length > 1 || !organizer) {
if (html) {
$('#event-attendees').show()
.children('.event-text')
.html(html)
.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
}
$('#event-rsvp')[(rsvp?'show':'hide')]();
}
var buttons = {};
@ -468,9 +489,10 @@ function rcube_calendar_ui(settings)
$('#edit-recurring-warning').hide();
// init attendees tab
var organizer = is_organizer(event);
event_attendees = [];
attendees_list = $('#edit-attendees-table > tbody').html('');
$('#edit-attendees-notify')[(notify.checked?'show':'hide')]();
$('#edit-attendees-notify')[(notify.checked && organizer ? 'show' : 'hide')]();
var load_attendees_tab = function()
{
@ -479,6 +501,7 @@ function rcube_calendar_ui(settings)
add_attendee(event.attendees[j], true);
}
$('#edit-attendees-form .attendees-invitebox')[(organizer?'show':'hide')]();
$('#edit-attendee-schedule')[(calendar.freebusy?'show':'hide')]();
};
@ -557,11 +580,11 @@ function rcube_calendar_ui(settings)
});
// don't submit attendees if only myself is added as organizer
if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && data.attendees[0].email == settings.event_owner.email)
if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && data.attendees[0].email == settings.identity.email)
data.attendees = [];
// tell server to send notifications
if (data.attendees.length && ((event.id && notify.checked) || (!event.id && invite.checked))) {
if (data.attendees.length && organizer && ((event.id && notify.checked) || (!event.id && invite.checked))) {
data.notify = 1;
}
@ -734,8 +757,8 @@ function rcube_calendar_ui(settings)
.bind('click.roleicons', function(e){
// toggle attendee status upon click on icon
if (e.target.id && e.target.id.match(/rcmlia(.+)/)) {
var attendee, domid = RegExp.$1, roles = [ 'ORGANIZER', 'REQ-PARTICIPANT', 'OPT-PARTICIPANT', 'CHAIR' ];
if ((attendee = freebusy_ui.attendees[domid])) {
var attendee, domid = RegExp.$1, roles = [ 'REQ-PARTICIPANT', 'OPT-PARTICIPANT', 'CHAIR' ];
if ((attendee = freebusy_ui.attendees[domid]) && attendee.role != 'ORGANIZER') {
var req = attendee.role != 'OPT-PARTICIPANT';
var j = $.inArray(attendee.role, roles);
j = (j+1) % roles.length;
@ -1288,13 +1311,15 @@ function rcube_calendar_ui(settings)
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
// role selection
var opts = {
'ORGANIZER': rcmail.gettext('calendar.roleorganizer'),
'REQ-PARTICIPANT': rcmail.gettext('calendar.rolerequired'),
'OPT-PARTICIPANT': rcmail.gettext('calendar.roleoptional'),
'CHAIR': rcmail.gettext('calendar.roleresource')
};
var select = '<select class="edit-attendee-role">';
var organizer = data.role == 'ORGANIZER';
var opts = {};
if (organizer)
opts.ORGANIZER = rcmail.gettext('calendar.roleorganizer');
opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired');
opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional');
opts['CHAIR'] = rcmail.gettext('calendar.roleresource');
var select = '<select class="edit-attendee-role"' + (organizer ? ' disabled="true"' : '') + '>';
for (var r in opts)
select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
select += '</select>';
@ -1310,7 +1335,7 @@ function rcube_calendar_ui(settings)
'<td class="name">' + dispname + '</td>' +
'<td class="availability"><img src="./program/blank.gif" class="availabilityicon ' + avail + '" /></td>' +
'<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '">' + Q(data.status) + '</span></td>' +
'<td class="options">' + dellink + '</td>';
'<td class="options">' + (organizer ? '' : dellink) + '</td>';
var tr = $('<tr>')
.addClass(String(data.role).toLowerCase())
@ -1372,6 +1397,24 @@ function rcube_calendar_ui(settings)
event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) });
};
// when the user accepts or declines an event invitation
var event_rsvp = function(response)
{
if (me.selected_event && me.selected_event.attendees && response) {
// 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(';'+data.email) >= 0)
data.status = response.toUpperCase();
}
event_show_dialog(me.selected_event);
// submit status change to server
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('event', { action:'rsvp', e:me.selected_event, status:response });
}
}
// post the given event data to server
var update_event = function(action, data)
{
@ -1413,14 +1456,20 @@ function rcube_calendar_ui(settings)
var update_event_confirm = function(action, event, data)
{
if (!data) data = event;
var html = '';
var notify = false, html = '';
// event has attendees, ask whether to notify them
if (has_attendees(event)) {
html += '<div class="message">' +
'<label><input class="confirm-attendees-donotify" type="checkbox" ' + (action != 'remove' ? ' checked="checked"' : '') + ' value="1" name="notify" />&nbsp;' +
rcmail.gettext((action == 'remove' ? 'sendcancellation' : 'sendnotifications'), 'calendar') +
'</label></div>';
if (is_organizer(event)) {
notify = true;
html += '<div class="message">' +
'<label><input class="confirm-attendees-donotify" type="checkbox" ' + (action != 'remove' ? ' checked="checked"' : '') + ' value="1" name="notify" />&nbsp;' +
rcmail.gettext((action == 'remove' ? 'sendcancellation' : 'sendnotifications'), 'calendar') +
'</label></div>';
}
else {
html += '<div class="message">' + rcmail.gettext('localchangeswarning', 'calendar') + '</div>';
}
}
// recurring event: user needs to select the savemode
@ -1442,7 +1491,7 @@ function rcube_calendar_ui(settings)
$dialog.find('a.button').button().click(function(e){
data.savemode = String(this.href).replace(/.+#/, '');
if ($dialog.find('input.confirm-attendees-donotify').get(0))
data.notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
data.notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
update_event(action, data);
$dialog.dialog("destroy").hide();
return false;
@ -1459,7 +1508,7 @@ function rcube_calendar_ui(settings)
buttons.push({
text: rcmail.gettext((action == 'remove' ? 'remove' : 'save'), 'calendar'),
click: function() {
data.notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
data.notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
update_event(action, data);
$(this).dialog("close");
}
@ -1814,6 +1863,9 @@ function rcube_calendar_ui(settings)
if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly)
this.selected_calendar = settings.default_calendar;
var viewdate = new Date();
if (rcmail.env.date)
viewdate.setTime(fromunixtime(rcmail.env.date));
// initalize the fullCalendar plugin
var fc = $('#calendar').fullCalendar({
@ -1823,6 +1875,9 @@ function rcube_calendar_ui(settings)
right: 'agendaDay,agendaWeek,month,table'
},
aspectRatio: 1,
date: viewdate.getDate(),
month: viewdate.getMonth(),
year: viewdate.getFullYear(),
ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
height: $('#main').height(),
eventSources: event_sources,
@ -1856,7 +1911,7 @@ function rcube_calendar_ui(settings)
listPage: 1, // advance one day in agenda view
listRange: settings['agenda_range'],
tableCols: ['handle', 'date', 'time', 'title', 'location'],
defaultView: settings['default_view'],
defaultView: rcmail.env.view || settings['default_view'],
allDayText: rcmail.gettext('all-day', 'calendar'),
buttonText: {
prev: (bw.ie6 ? '&nbsp;&lt;&lt;&nbsp;' : '&nbsp;&#9668;&nbsp;'),
@ -2104,7 +2159,7 @@ function rcube_calendar_ui(settings)
update_freebusy_status(me.selected_event);
// add current user as organizer if non added yet
if (!event_attendees.length)
add_attendee($.extend({ role:'ORGANIZER' }, settings.event_owner));
add_attendee($.extend({ role:'ORGANIZER' }, settings.identity));
}
}
});
@ -2198,6 +2253,10 @@ function rcube_calendar_ui(settings)
if (this.checked)
$('<style type="text/css" id="workinghourscss"> td.offhours { opacity:0.3; filter:alpha(opacity=30) } </style>').appendTo('head');
});
$('#event-rsvp input.button').click(function(){
event_rsvp($(this).attr('rel'))
})
// hide event dialog when clicking somewhere into document
$(document).bind('mousedown', dialog_check);

View file

@ -61,10 +61,11 @@
* 'id' => 'Attachment identifier'
* ),
* 'deleted_attachments' => array(), // array of attachment identifiers to delete when event is updated
* 'organizer' => array('name' => 'Organizer name', 'email' => ''),
* 'attendees' => array( // List of event participants
* 'name' => 'Participant name',
* 'email' => 'Participant e-mail address', // used as identifier
* 'role' => 'ORGANIZER|REQ-PARTICIPANT|OPT-PARTICIPANT|CHAIR',
* 'role' => 'REQ-PARTICIPANT|OPT-PARTICIPANT|CHAIR',
* 'status' => 'NEEDS-ACTION|UNKNOWN|ACCEPTED|TENTATIVE|DECLINED'
* 'rsvp' => true|false,
* ),

View file

@ -181,6 +181,10 @@ class calendar_ical
$event['recurrence_id'] = $this->_date2time($attr['value']);
break;
case 'SEQUENCE':
$event['sequence'] = intval($attr['value']);
break;
case 'DESCRIPTION':
case 'LOCATION':
$event[strtolower($attr['name'])] = $attr['value'];

View file

@ -627,5 +627,22 @@ class calendar_ui
return $table->show($attrib);
}
function event_rsvp_buttons($attrib = array())
{
foreach (array('accepted','tentative','declined') as $method) {
$buttons .= html::tag('input', array(
'type' => 'button',
'class' => 'button',
'rel' => $method,
'value' => $this->calendar->gettext('itip' . $method),
));
}
return html::div($attrib,
html::div('label', $this->calendar->gettext('acceptinvitation')) .
html::div('rsvp-buttons', $buttons));
}
}

View file

@ -122,9 +122,22 @@ $labels['eventcancelsubject'] = '"$title" has been canceled';
$labels['eventcancelmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nThe event has been cancelled by \$organizer.\n\nPlease find attached an iCalendar file with the updated event details.";
// invitation handling
$labels['itipreply'] = 'Reply to ';
$labels['itipinvitation'] = 'Invitation to';
$labels['itipupdate'] = 'Update of';
$labels['itipcancellation'] = 'Cancelled:';
$labels['itipreply'] = 'Reply to';
$labels['itipaccepted'] = 'Accept';
$labels['itiptentative'] = 'Maybe';
$labels['itipdeclined'] = 'Decline';
$labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
$labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
$labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
$labels['itipmailbodyaccepted'] = "\$sender has accepted the invitation to the following event:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
$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['importtocalendar'] = 'Save to my calendar';
$labels['updateattendeestatus'] = 'Update the participant\'s status';
$labels['acceptinvitation'] = 'Do you accept this invitation?';
// event dialog tabs
$labels['tabsummary'] = 'Summary';
@ -149,6 +162,9 @@ $labels['newerversionexists'] = 'A newer version of this event already exists! A
$labels['nowritecalendarfound'] = 'No calendar found to save the event';
$labels['importedsuccessfully'] = 'The event was successfully added to \'$calendar\'';
$labels['attendeupdateesuccess'] = 'Successfully updated the participant\'s status';
$labels['itipresponseerror'] = 'Failed to send the response to this event invitation';
$labels['sentresponseto'] = 'Successfully sent invitation response to $mailto';
$labels['localchangeswarning'] = 'You are about to make changes that will only be reflected on your personal calendar';
// recurrence form
$labels['repeat'] = 'Repeat';

View file

@ -527,6 +527,7 @@ td.topalign {
min-width: 5em;
}
#event-rsvp,
#edit-attendees-notify {
margin: 0.3em 0;
padding: 0.5em;
@ -1091,15 +1092,31 @@ fieldset #calendarcategories div {
/* Invitation UI in mail */
p.calendar-invitebox {
div.calendar-invitebox {
min-height: 20px;
margin: 5px 8px;
padding: 6px 6px 6px 34px;
padding: 3px 6px 6px 34px;
border: 1px solid #C2D071;
background: url('images/calendar.png') 6px 3px no-repeat #F7FDCB;
background: url('images/calendar.png') 6px 5px no-repeat #F7FDCB;
}
p.calendar-invitebox input.button {
margin-left: 1em;
font-size: 11px;
div.calendar-invitebox td.ititle {
font-weight: bold;
padding-right: 0.5em;
}
div.calendar-invitebox td.label {
color: #666;
padding-right: 1em;
}
#event-rsvp .rsvp-buttons,
div.calendar-invitebox .rsvp-buttons {
margin-top: 0.5em;
}
#event-rsvp input.button,
div.calendar-invitebox input.button {
font-size: 11px;
margin-right: 0.5em;
}

View file

@ -83,6 +83,8 @@
<label><roundcube:label name="attachments" /></label>
<div class="event-text attachments-list"></div>
</div>
<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" style="display:none" />
</div>
<div id="eventedit" class="uidialog">