Send invitations and update notifications to event attendees

This commit is contained in:
Thomas Bruederli 2011-07-27 18:55:51 +02:00
parent 85af82794b
commit b614298c93
8 changed files with 201 additions and 10 deletions

View file

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

View file

@ -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) {
@ -1904,6 +1917,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();
});

View file

@ -133,15 +133,15 @@ class calendar_ical
//I am an orginizer
$organizer .= "ORGANIZER;";
if (!empty($at['name']))
$organizer .= 'CN="' . $at['name'] . '":';
$organizer .= "mailto:". $at['email'] . self::EOL;
$organizer .= 'CN="' . $at['name'] . '"';
$organizer .= ":mailto:". $at['email'] . self::EOL;
}
else {
//I am an attendee
$attendees .= "ATTENDEE;ROLE=" . $at['role'] . ";PARTSTAT=" . $at['status'];
if (!empty($at['name']))
$attendees .= ';CN="' . $at['name'] . '":';
$attendees .= "mailto:" . $at['email'] . self::EOL;
$attendees .= ';CN="' . $at['name'] . '"';
$attendees .= ":mailto:" . $at['email'] . self::EOL;
}
}

View file

@ -284,6 +284,15 @@ class calendar_ui
return html::tag('ul', $attrib, join("\n", $items), html::$common_attrib);
}
/**
*
*/
function edit_attendees_notify($attrib = array())
{
$checkbox = new html_checkbox(array('name' => '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')))
);
}

View file

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

View file

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

View file

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

View file

@ -181,6 +181,7 @@
</div>
</form>
<roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" style="display:none" />
<roundcube:object name="plugin.edit_recurring_warning" class="edit-recurring-warning" style="display:none" />
</div>