diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 92459738..4545d02b 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -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 * diff --git a/plugins/calendar/calendar_base.js b/plugins/calendar/calendar_base.js index 056bc5b1..f22e4689 100644 --- a/plugins/calendar/calendar_base.js +++ b/plugins/calendar/calendar_base.js @@ -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; }; diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 07e3e64c..34b35693 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -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 += '' + dispname + ' '; + } + + 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 = '' + dispname + ''; - html += '' + dispname + ' '; - if (data.role == 'ORGANIZER') - organizer = true; + if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' && settings.identity.emails.indexOf(';'+data.email) >= 0) + rsvp = true; + } + html += '' + dispname + ' '; + // stop listing attendees if (j == 7 && event.attendees.length >= 7) { html += ' ' + rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1) + ''; 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 = '' + dispname + ''; // 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 = ''; for (var r in opts) select += ''; select += ''; @@ -1310,7 +1335,7 @@ function rcube_calendar_ui(settings) '' + dispname + '' + '' + '' + Q(data.status) + '' + - '' + dellink + ''; + '' + (organizer ? '' : dellink) + ''; var 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 += '
' + - '
'; + if (is_organizer(event)) { + notify = true; + html += '
' + + '
'; + } + else { + html += '
' + rcmail.gettext('localchangeswarning', 'calendar') + '
'; + } } // 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 ? ' << ' : ' ◄ '), @@ -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) $('').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); diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index b1cd580c..77501fd3 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -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, * ), diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php index e31d4ae3..372958d7 100644 --- a/plugins/calendar/lib/calendar_ical.php +++ b/plugins/calendar/lib/calendar_ical.php @@ -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']; diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 25c48daa..c0015f45 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -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)); + } } diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 26f7949c..c31fb6b0 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -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'; diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 3de7a7f8..210936c7 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -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; } diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html index 4dfb01be..87253b72 100644 --- a/plugins/calendar/skins/default/templates/calendar.html +++ b/plugins/calendar/skins/default/templates/calendar.html @@ -83,6 +83,8 @@
+ +