diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 757bcf9c..035fdc0f 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -193,6 +193,7 @@ class calendar extends rcube_plugin $this->register_handler('plugin.attendees_list', array($this->ui, 'attendees_list')); $this->register_handler('plugin.attendees_form', array($this->ui, 'attendees_form')); $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.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template @@ -485,6 +486,10 @@ class calendar extends rcube_plugin $action = get_input_value('action', RCUBE_INPUT_GPC); $event = get_input_value('e', RCUBE_INPUT_POST); $success = $reload = $got_msg = false; + + // read old event data in order to find changes + if ($event['_notify']) + $old = $this->driver->get_event($event); switch ($action) { case "new": @@ -558,6 +563,18 @@ class calendar extends rcube_plugin $success |= $this->driver->dismiss_alarm($id, $event['snooze']); break; } + + // send out notifications + if ($success && $event['_notify'] && $event['attendees']) { + // make sure we have the complete record + $event = $this->driver->get_event($event); + + // only notify if data really changed (TODO: do diff check on client already) + if (self::event_diff($event, $old)) { + if ($this->notify_attendees($event, $old) < 0) + $this->rc->output->show_message('calendar.errornotifying', 'error'); + } + } // show confirmation/error message if (!$got_msg) { @@ -1217,7 +1234,117 @@ class calendar extends rcube_plugin unset($_SESSION['event_session']); } } + + /** + * Send out an invitation/notification to all event attendees + */ + private function notify_attendees($event, $old) + { + $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); + + // 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; + + + // attach ics file for this event + $vcal = $this->ical->export(array($event), 'REQUEST'); + $message->addAttachment($vcal, 'text/calendar', 'event.ics', false, '8bit', 'attachment', RCMAIL_CHARSET); + + // 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 + foreach ((array)$event['attendees'] as $attendee) { + // skip myself for obvious reasons + if (!$attendee['email'] || $attendee['email'] == $myself['email']) + continue; + + $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_new ? 'invitationsubject' : 'eventupdatesubject', + 'vars' => array('title' => $event['title']), + )); + + // compose message body + $body = $this->gettext(array( + 'name' => $is_new ? 'invitationmailbody' : 'eventupdatemailbody', + 'vars' => array( + 'title' => $event['title'], + 'date' => $this->event_date_text($event), + 'attendees' => join(', ', $attendees_list), + ) + )); + + $message->headers($headers); + $message->setTXTBody(rc_wordwrap($body, 75, "\r\n")); + + // finally send the message + if (rcmail_deliver_message($message, $from, $mailto, $smtp_error)) + $sent++; + else + $sent = -100; + } + + return $sent; + } + /** + * Compose a date string for the given event + */ + public function event_date_text($event) + { + $fromto = ''; + $duration = $event['end'] - $event['start']; + $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format')); + $time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format')); + + if ($event['allday']) { + $fromto = format_date($event['start'], $date_format) . + ($duration > 86400 || date('d', $event['start']) != date('d', $event['end']) ? ' - ' . format_date($event['end'], $date_format) : ''); + } + else if ($duration < 86400 && date('d', $event['start']) == date('d', $event['end'])) { + $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) . + ' - ' . format_date($event['end'], $time_format); + } + else { + $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) . + ' - ' . format_date($event['end'], $date_format) . ' ' . format_date($event['end'], $time_format); + } + + return $fromto; + } + /** * Echo simple free/busy status text for the given user and time range */ @@ -1333,4 +1460,27 @@ class calendar extends rcube_plugin $this->rc->output->send("calendar.print"); } + /** + * Compare two event objects and return differing properties + * + * @param array Event A + * @param array Event B + * @return array List of differing event properties + */ + public static function event_diff($a, $b) + { + $diff = array(); + $ignore = array('attachments' => 1); + foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) { + if (!$ignore[$key] && $a[$key] != $b[$key]) + $diff[] = $key; + } + + // only compare number of attachments + if (count($a['attachments']) != count($b['attachments'])) + $diff[] = 'attachments'; + + return $diff; + } + } diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 59cb4883..f29ea27d 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -369,6 +369,9 @@ function rcube_calendar_ui(settings) var enddate = $('#edit-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format'])); var endtime = $('#edit-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show(); var allday = $('#edit-allday').get(0); + var notify = $('#edit-attendees-donotify').get(0); + var invite = $('#edit-attendees-invite').get(0); + notify.checked = invite.checked = true; // enable notification by default if (event.allDay) { starttime.val("00:00").hide(); @@ -461,7 +464,11 @@ function rcube_calendar_ui(settings) if (calendar.attendees && event.attendees) { for (var j=0; j < event.attendees.length; j++) add_attendee(event.attendees[j], true); + $('#edit-attendees-notify').show(); } + else + $('#edit-attendees-notify').hide(); + $('#edit-attendee-schedule')[(calendar.freebusy?'show':'hide')](); // attachments @@ -535,6 +542,11 @@ function rcube_calendar_ui(settings) if (data.attendees[i]) data.attendees[i].role = $(elem).val(); }); + + // tell server to send notifications + if (data.attendees.length && ((event.id && notify.checked) || (!event.id && invite.checked))) { + data._notify = 1; + } // gather recurrence settings var freq; @@ -624,6 +636,7 @@ function rcube_calendar_ui(settings) title.select(); }; + // open a dialog to display detailed free-busy information and to find free slots var event_freebusy_dialog = function() { var $dialog = $('#eventfreebusy').dialog('close'); @@ -1056,7 +1069,7 @@ function rcube_calendar_ui(settings) // parse name/email pairs var item, email, name, success = false; for (var i=0; i < names.length; i++) { - email = name = null; + email = name = ''; item = $.trim(names[i]); if (!item.length) { @@ -1525,12 +1538,17 @@ function rcube_calendar_ui(settings) .data('id', id); } - if (!cal.readonly && !this.selected_calendar && (!settings.default_calendar || settings.default_calendar == id)) { + if (!cal.readonly && !this.selected_calendar) { this.selected_calendar = id; rcmail.enable_command('addevent', true); } } - + + // select default calendar + if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly) + this.selected_calendar = settings.default_calendar; + + // initalize the fullCalendar plugin var fc = $('#calendar').fullCalendar({ header: { @@ -1904,6 +1922,11 @@ function rcube_calendar_ui(settings) input.val(''); }); + // keep these two checkboxes in sync + $('#edit-attendees-donotify, #edit-attendees-invite').click(function(){ + $('#edit-attendees-donotify, #edit-attendees-invite').prop('checked', this.checked); + }); + $('#edit-attendee-schedule').click(function(){ event_freebusy_dialog(); }); diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index db3ad904..0940e861 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -180,6 +180,16 @@ abstract class calendar_driver return false; } + /** + * Return data of a single event + * + * @param array Hash array with event properties: + * id: Event identifier + * calendar: Calendar identifier + * @return array Event object as hash array + */ + abstract function get_event($event); + /** * Get events from source. * diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php index 3bd9a870..69898a8a 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -173,7 +173,7 @@ class database_driver extends calendar_driver * Add a single event to the database * * @param array Hash array with event properties - * @see Driver:new_event() + * @see calendar_driver::new_event() */ public function new_event($event) { @@ -233,7 +233,7 @@ class database_driver extends calendar_driver * Update an event entry with the given data * * @param array Hash array with event properties - * @see Driver:edit_event() + * @see calendar_driver::edit_event() */ public function edit_event($event) { @@ -502,7 +502,7 @@ class database_driver extends calendar_driver * Move a single event * * @param array Hash array with event properties - * @see Driver:move_event() + * @see calendar_driver::move_event() */ public function move_event($event) { @@ -514,7 +514,7 @@ class database_driver extends calendar_driver * Resize a single event * * @param array Hash array with event properties - * @see Driver:resize_event() + * @see calendar_driver::resize_event() */ public function resize_event($event) { @@ -528,7 +528,7 @@ class database_driver extends calendar_driver * @param array Hash array with event properties * @param boolean Remove record irreversible (@TODO) * - * @see Driver:remove_event() + * @see calendar_driver::remove_event() */ public function remove_event($event, $force = true) { @@ -601,13 +601,15 @@ class database_driver extends calendar_driver /** * Return data of a specific event - * @param string Event ID + * @param mixed Hash array with event properties or event ID * @return array Hash array with event properties */ - public function get_event($id) + public function get_event($event) { static $cache = array(); + $id = is_array($event) ? $event['id'] : $event; + if ($cache[$id]) return $cache[$id]; @@ -630,7 +632,7 @@ class database_driver extends calendar_driver /** * Get event data * - * @see Driver:load_events() + * @see calendar_driver::load_events() */ public function load_events($start, $end, $query = null, $calendars = null) { @@ -726,7 +728,7 @@ class database_driver extends calendar_driver /** * Get a list of pending alarms to be displayed to the user * - * @see Driver:pending_alarms() + * @see calendar_driver::pending_alarms() */ public function pending_alarms($time, $calendars = null) { @@ -759,7 +761,7 @@ class database_driver extends calendar_driver /** * Feedback after showing/sending an alarm notification * - * @see Driver:dismiss_alarm() + * @see calendar_driver::dismiss_alarm() */ public function dismiss_alarm($event_id, $snooze = 0) { diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index 50b06ed0..59ebb609 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -242,7 +242,7 @@ class kolab_calendar /** * Create a new event record * - * @see Driver:new_event() + * @see calendar_driver::new_event() * * @return mixed The created record ID on success, False on error */ @@ -263,6 +263,9 @@ class kolab_calendar true, false); $saved = false; } + else { + $this->events[$event['uid']] = $event; + } return $saved; } @@ -270,7 +273,7 @@ class kolab_calendar /** * Update a specific event record * - * @see Driver:new_event() + * @see calendar_driver::new_event() * @return boolean True on success, False on error */ @@ -289,6 +292,7 @@ class kolab_calendar } else { $updated = true; + $this->events[$event['id']] = $this->_to_rcube_event($object); } return $updated; @@ -297,7 +301,7 @@ class kolab_calendar /** * Delete an event record * - * @see Driver:remove_event() + * @see calendar_driver::remove_event() * @return boolean True on success, False on error */ public function delete_event($event, $force = true) @@ -332,7 +336,7 @@ class kolab_calendar /** * Restore deleted event record * - * @see Driver:undelete_event() + * @see calendar_driver::undelete_event() * @return boolean True on success, False on error */ public function restore_event($event) diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index a50edf85..62a55e03 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -274,10 +274,24 @@ class kolab_driver extends calendar_driver } + /** + * Move a single event + * + * @see calendar_driver::get_event() + * @return array Hash array with event properties, false if not found + */ + public function get_event($event) + { + if ($storage = $this->calendars[$event['calendar']]) + return $storage->get_event($event['id']); + + return false; + } + /** * Add a single event to the database * - * @see Driver:new_event() + * @see calendar_driver::new_event() */ public function new_event($event) { @@ -307,7 +321,7 @@ class kolab_driver extends calendar_driver /** * Update an event entry with the given data * - * @see Driver:new_event() + * @see calendar_driver::new_event() * @return boolean True on success, False on error */ public function edit_event($event) @@ -318,7 +332,7 @@ class kolab_driver extends calendar_driver /** * Move a single event * - * @see Driver:move_event() + * @see calendar_driver::move_event() * @return boolean True on success, False on error */ public function move_event($event) @@ -332,7 +346,7 @@ class kolab_driver extends calendar_driver /** * Resize a single event * - * @see Driver:resize_event() + * @see calendar_driver::resize_event() * @return boolean True on success, False on error */ public function resize_event($event) @@ -578,7 +592,7 @@ class kolab_driver extends calendar_driver /** * Get a list of pending alarms to be displayed to the user * - * @see Driver:pending_alarms() + * @see calendar_driver::pending_alarms() */ public function pending_alarms($time, $calendars = null) { @@ -649,7 +663,7 @@ class kolab_driver extends calendar_driver /** * Feedback after showing/sending an alarm notification * - * @see Driver:dismiss_alarm() + * @see calendar_driver::dismiss_alarm() */ public function dismiss_alarm($event_id, $snooze = 0) { diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php index d2e06cb5..704f772f 100644 --- a/plugins/calendar/lib/calendar_ical.php +++ b/plugins/calendar/lib/calendar_ical.php @@ -25,6 +25,8 @@ class calendar_ical { + const EOL = "\r\n"; + private $rc; private $driver; @@ -53,35 +55,37 @@ class calendar_ical * @param array Events as array * @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545) */ - public function export($events) + public function export($events, $method = null) { if (!empty($this->rc->user->ID)) { - $ical = "BEGIN:VCALENDAR\r\n"; - $ical .= "VERSION:2.0\r\n"; - $ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN\r\n"; - $ical .= "CALSCALE:GREGORIAN\r\n"; + $ical = "BEGIN:VCALENDAR" . self::EOL; + $ical .= "VERSION:2.0" . self::EOL; + $ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL; + $ical .= "CALSCALE:GREGORIAN" . self::EOL; + + if ($method) + $ical .= "METHOD:" . strtoupper($method) . self::EOL; foreach ($events as $event) { - $ical .= "BEGIN:VEVENT\r\n"; - $ical .= "UID:" . self::escpape($event['uid']) . "\r\n"; - $ical .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . "\r\n"; - $ical .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . "\r\n"; - $ical .= "SUMMARY:" . self::escpape($event['title']) . "\r\n"; - $ical .= "DESCRIPTION:" . wordwrap(self::escpape($event['description']),75,"\r\n ") . "\r\n"; - - if (!empty($event['attendees'])){ - - $ical .= $this->_get_attendees($event['attendees']); - } - + $ical .= "BEGIN:VEVENT" . self::EOL; + $ical .= "UID:" . self::escpape($event['uid']) . self::EOL; + $ical .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . self::EOL; + $ical .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . self::EOL; + $ical .= "SUMMARY:" . self::escpape($event['title']) . self::EOL; + $ical .= "DESCRIPTION:" . self::escpape($event['description']) . self::EOL; + + if (!empty($event['attendees'])){ + $ical .= $this->_get_attendees($event['attendees']); + } + if (!empty($event['location'])) { - $ical .= "LOCATION:" . self::escpape($event['location']) . "\r\n"; + $ical .= "LOCATION:" . self::escpape($event['location']) . self::EOL; } if ($event['recurrence']) { - $ical .= "RRULE:" . calendar::to_rrule($event['recurrence']) . "\r\n"; + $ical .= "RRULE:" . calendar::to_rrule($event['recurrence']) . self::EOL; } if(!empty($event['categories'])) { - $ical .= "CATEGORIES:" . self::escpape(strtoupper($event['categories'])) . "\r\n"; + $ical .= "CATEGORIES:" . self::escpape(strtoupper($event['categories'])) . self::EOL; } if ($event['sensitivity'] > 0) { $ical .= "X-CALENDARSERVER-ACCESS:CONFIDENTIAL"; @@ -91,21 +95,22 @@ class calendar_ical $val = calendar::parse_alaram_value($trigger); $ical .= "BEGIN:VALARM\n"; - if ($val[1]) $ical .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . "\r\n"; - else $ical .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . "\r\n"; - if ($action) $ical .= "ACTION:" . self::escpape(strtoupper($action)) . "\r\n"; + if ($val[1]) $ical .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL; + else $ical .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL; + if ($action) $ical .= "ACTION:" . self::escpape(strtoupper($action)) . self::EOL; $ical .= "END:VALARM\n"; } - $ical .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . "\r\n"; + $ical .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL; // TODO: export attachments - $ical .= "END:VEVENT\r\n"; + $ical .= "END:VEVENT" . self::EOL; } - $ical .= "END:VCALENDAR"; + $ical .= "END:VCALENDAR" . self::EOL; - return $ical; + // fold lines to 75 chars + return rcube_vcard::rfc2425_fold($ical); } } @@ -114,44 +119,33 @@ class calendar_ical return preg_replace('/(? 'notify', 'id' => 'edit-attendees-donotify', 'value' => 1)); + return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->calendar->gettext('sendnotifications'))); + } /** * Generate the form for recurrence settings @@ -581,13 +590,13 @@ class calendar_ui function attendees_form($attrib = array()) { $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30)); - $checkbox = new html_checkbox(array('name' => 'notify', 'id' => 'edit-attendees-notify', 'value' => 1, 'disabled' => true)); // disabled for now + $checkbox = new html_checkbox(array('name' => 'invite', 'id' => 'edit-attendees-invite', 'value' => 1)); return html::div($attrib, html::div(null, $input->show() . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->calendar->gettext('addattendee'))) . " " . html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->calendar->gettext('scheduletime').'...'))) . - html::p('attendees-notifybox', html::label(null, $checkbox->show(1) . $this->calendar->gettext('sendnotifications'))) + html::p('attendees-invitebox', html::label(null, $checkbox->show(1) . $this->calendar->gettext('sendinvitations'))) ); } diff --git a/plugins/calendar/localization/de_DE.inc b/plugins/calendar/localization/de_DE.inc index c1a6e0d4..08c9af43 100644 --- a/plugins/calendar/localization/de_DE.inc +++ b/plugins/calendar/localization/de_DE.inc @@ -34,7 +34,7 @@ $labels['edit'] = 'Bearbeiten'; $labels['title'] = 'Titel'; $labels['description'] = 'Beschreibung'; $labels['all-day'] = 'ganztägig'; -$labels['export'] = 'Als ICS exportieren'; +$labels['export'] = 'Als iCalendar exportieren'; $labels['category'] = 'Kategorie'; $labels['location'] = 'Ort'; $labels['date'] = 'Datum'; diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 277f5a2e..7599c010 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -39,7 +39,7 @@ $labels['print'] = 'Print calendars'; $labels['title'] = 'Summary'; $labels['description'] = 'Description'; $labels['all-day'] = 'all-day'; -$labels['export'] = 'Export to ICS'; +$labels['export'] = 'Export to iCalendar'; $labels['location'] = 'Location'; $labels['date'] = 'Date'; $labels['start'] = 'Start'; @@ -103,11 +103,16 @@ $labels['availunknown'] = 'Unknown'; $labels['availtentative'] = 'Tentative'; $labels['availoutofoffice'] = 'Out of Office'; $labels['scheduletime'] = 'Find availability'; -$labels['sendnotifications'] = 'Send notifications'; +$labels['sendinvitations'] = 'Send invitations'; +$labels['sendnotifications'] = 'Notify attendees about modifications'; $labels['onlyworkinghours'] = 'Find availability within my working hours'; $labels['prevslot'] = 'Previous Slot'; $labels['nextslot'] = 'Next Slot'; $labels['noslotfound'] = 'Unable to find a free time slot'; +$labels['invitationsubject'] = 'You\'ve been invited to "$title"'; +$labels['invitationmailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with all the event details which you can import to your calendar application."; +$labels['eventupdatesubject'] = '"$title" has been updated'; +$labels['eventupdatemailbody'] = "*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees\n\nPlease find attached an iCalendar file with the updated event details which you can import to your calendar application."; // event dialog tabs $labels['tabsummary'] = 'Summary'; @@ -126,6 +131,7 @@ $labels['invalidcalendarproperties'] = 'Invalid calendar properties! Please set $labels['searchnoresults'] = 'No events found in the selected calendars.'; $labels['successremoval'] = 'The event has been deleted successfully.'; $labels['successrestore'] = 'The event has been restored successfully.'; +$labels['errornotifying'] = 'Failed to send notifications to event participants'; // recurrence form $labels['repeat'] = 'Repeat'; diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 6529bf84..56bd9034 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -502,6 +502,13 @@ td.topalign { min-width: 5em; } +#edit-attendees-notify { + margin: 0.3em 0; + padding: 0.5em; + background-color: #F7FDCB; + border: 1px solid #C2D071; +} + #edit-attendees-table { width: 100%; display: table; diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html index de0419cc..d6d48b72 100644 --- a/plugins/calendar/skins/default/templates/calendar.html +++ b/plugins/calendar/skins/default/templates/calendar.html @@ -181,6 +181,7 @@ +