Page MenuHomePhorge

D4570.1775190822.diff
No OneTemporary

Authored By
Unknown
Size
15 KB
Referenced Files
None
Subscribers
None

D4570.1775190822.diff

diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php
--- a/lib/kolab_sync_data_calendar.php
+++ b/lib/kolab_sync_data_calendar.php
@@ -116,6 +116,14 @@
const SENSITIVITY_PRIVATE = 2;
const SENSITIVITY_CONFIDENTIAL = 3;
+ /**
+ * Internal iTip states
+ */
+ const ITIP_ACCEPTED = 'ACCEPTED';
+ const ITIP_DECLINED = 'DECLINED';
+ const ITIP_TENTATIVE = 'TENTATIVE';
+ const ITIP_CANCELLED = 'CANCELLED';
+
const KEY_DTSTAMP = 'x-custom.X-ACTIVESYNC-DTSTAMP';
const KEY_REPLYTIME = 'x-custom.X-ACTIVESYNC-REPLYTIME';
@@ -743,6 +751,147 @@
return empty($status) ? null : $this->serverId($event['uid'], $folder);
}
+ /**
+ * Process an event from an iTip message - update the event in the recipient's calendar
+ *
+ * @param array $event Event data from the iTip
+ *
+ * @return string|null Attendee status from the iTip (self::ITIP_* constant value)
+ */
+ public function processItipReply($event)
+ {
+ // FIXME: This does not prevent from spoofing, i.e. an iTip message
+ // could be sent by anyone impersonating an organizer or attendee
+
+ // FIXME: This will not work with Kolab delegation, as we do look
+ // for the event instance in personal folders only (for now)
+ // We also do not use SENT-BY,DELEGATED-TO,DELEGATED-FROM here at all.
+
+ // FIXME: This is potential performance problem - we update an event
+ // whenever we sync an email message. User can have multiple AC clients
+ // or many iTip messages in INBOX. Should we remember which email was
+ // already processed?
+
+ // FIXME: Should we check SEQUENCE or something else to prevent
+ // overwriting the attendee status with outdated status (on REPLY)?
+
+ // Here we're handling CANCEL message, find the event (or occurrence) and remove it
+ if ($event['_method'] == 'CANCEL') {
+ // TODO: Performance: When we're going to delete the event we don't have to fetch it,
+ // we just need to find that it exists and in which folder.
+
+ if ($existing = $this->find_event_by_uid($event['uid'])) {
+ // Note: Normally we'd just set the event status to canceled, but
+ // ActiveSync clients do not understand that, we have to delete it
+
+ if (!empty($event['recurrence_date'])) {
+ // A single recurring event occurrence
+ $rec_day = $event['recurrence_date']->format('Ymd');
+ // Remove the matching RDATE entry
+ if (!empty($existing['recurrence']['RDATE'])) {
+ foreach ($existing['recurrence']['RDATE'] as $j => $rdate) {
+ if ($rdate->format('Ymd') == $rec_day) {
+ unset($existing['recurrence']['RDATE'][$j]);
+ break;
+ }
+ }
+ }
+
+ // Check EXDATE list, maybe already cancelled
+ if (!empty($existing['recurrence']['EXDATE'])) {
+ foreach ($existing['recurrence']['EXDATE'] as $j => $exdate) {
+ if ($exdate->format('Ymd') == $rec_day) {
+ return self::ITIP_CANCELLED; // skip update
+ }
+ }
+ } else {
+ $existing['recurrence']['EXDATE'] = [];
+ }
+
+ if (!isset($existing['exceptions'])) {
+ $existing['exceptions'] = [];
+ }
+
+ if (!empty($existing['exceptions'])) {
+ foreach ($existing['exceptions'] as $i => $exception) {
+ if (libcalendaring::is_recurrence_exception($event, $exception)) {
+ unset($existing['exceptions'][$i]);
+ }
+ }
+ }
+
+ // Add an exception to the master event
+ $existing['recurrence']['EXDATE'][] = $event['recurrence_date'];
+
+ // TODO: Handle errors
+ $this->save_event($existing, null);
+ }
+ else {
+ $folder = $this->getFolderObject($existing['_mailbox']);
+ if ($folder && $folder->valid) {
+ // TODO: Handle errors
+ $folder->delete($event['uid']);
+ }
+ }
+ }
+
+ return self::ITIP_CANCELLED;
+ }
+
+ // Here we're handling REPLY message
+ if (empty($event['attendees']) || $event['_method'] != 'REPLY') {
+ return null;
+ }
+
+ $attendeeStatus = null;
+ $attendeeEmail = null;
+
+ // Get the attendee/status
+ foreach ($event['attendees'] as $attendee) {
+ if (empty($attendee['role']) || $attendee['role'] != 'ORGANIZER') {
+ if (!empty($attendee['email']) && !empty($attendee['status'])) {
+ // Per iTip spec. there should be only one (non-organizer) attendee here
+ // FIXME: Verify is it realy the case with e.g. Kolab webmail, If not, we should
+ // probably use the message sender from the From: header
+ $attendeeStatus = strtoupper($attendee['status']);
+ $attendeeEmail = $attendee['email'];
+ break;
+ }
+ }
+ }
+
+ // Find the event (or occurrence) and update it
+ if ($attendeeStatus && ($existing = $this->find_event_by_uid($event['uid']))) {
+ // TODO: We should probably check the SEQUENCE to not reset status to an outdated value
+
+ if (!empty($event['recurrence_date'])) {
+ // A single recurring event occurrence
+ // Find the exception entry, it should exist, if not ignore
+ if (!empty($existing['exceptions'])) {
+ foreach ($existing['exceptions'] as $i => $exception) {
+ if (!empty($exception['attendees']) && libcalendaring::is_recurrence_exception($event, $exception)) {
+ $attendees = &$existing['exceptions'][$i]['attendees'];
+ break;
+ }
+ }
+ }
+ }
+ else if (!empty($existing['attendees'])) {
+ $attendees = &$existing['attendees'];
+ }
+
+ if (isset($attendees)) {
+ $found = $this->find_and_update_attendee_status($attendees, $attendeeStatus, [$attendeeEmail], $changed);
+ if ($found && $changed) {
+ // TODO: error handling
+ $this->save_event($existing, null);
+ }
+ }
+ }
+
+ return $attendeeStatus;
+ }
+
/**
* Get an event from the invitation email or calendar folder
*/
@@ -803,21 +952,60 @@
throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingResponse::INVALID_REQUEST);
}
- if ($event['free_busy']) {
- $old['free_busy'] = $event['free_busy'];
+ // A single recurring event occurrence
+ if (!empty($event['recurrence_date'])) {
+ $event['recurrence'] = [];
+
+ if ($status) {
+ $this->update_attendee_status($event, $status);
+ $status = null;
+ }
+
+ if (!isset($old['exceptions'])) {
+ $old['exceptions'] = [];
+ }
+
+ $existing = false;
+ foreach ($old['exceptions'] as $i => $exception) {
+ if (libcalendaring::is_recurrence_exception($event, $exception)) {
+ $old['exceptions'][$i] = $event;
+ $existing = true;
+ }
+ }
+
+ // TODO: In case organizer first cancelled an occurrence and then invited
+ // an attendee to the same date, and attendee accepts, we should remove EXDATE entry.
+ // FIXME: We have to check with ActiveSync clients whether it is better
+ // to have an exception with DECLINED attendee status, or an EXDATE entry
+
+ if (!$existing) {
+ $old['exceptions'][] = $event;
+ }
+ }
+ // A main event update
+ else if (isset($event['sequence']) && $event['sequence'] > $old['sequence']) {
+ // FIXME: Can we be smarter here? Should we update everything? What about e.g. new attendees?
+ // And do we need to check the sequence?
+ $props = ['start', 'end', 'title', 'description', 'location', 'free_busy'];
+
+ foreach ($props as $prop) {
+ if (isset($event[$prop])) {
+ $old[$prop] = $event[$prop];
+ }
+ }
+
+ // Copy new custom properties
+ if (!empty($event['x-custom'])) {
+ foreach ($event['x-custom'] as $key => $val) {
+ $old['x-custom'][$key] = $val;
+ }
+ }
}
// Updating an existing event is most-likely a response
// to an iTip request with bumped SEQUENCE
$old['sequence'] += 1;
- // Copy new custom properties
- if (!empty($event['x-custom'])) {
- foreach ($event['x-custom'] as $key => $val) {
- $old['x-custom'][$key] = $val;
- }
- }
-
// Update the event
return $this->save_event($old, $status);
}
@@ -875,7 +1063,7 @@
/**
* Update the attendee status of the user matching $emails
*/
- protected function find_and_update_attendee_status(&$attendees, $status, $emails)
+ protected function find_and_update_attendee_status(&$attendees, $status, $emails, &$changed = false)
{
$found = false;
foreach ((array) $attendees as $i => $attendee) {
@@ -883,6 +1071,7 @@
&& (empty($attendee['role']) || $attendee['role'] != 'ORGANIZER')
&& in_array_nocase($attendee['email'], $emails)
) {
+ $changed = $changed || ($status != ($attendee['status'] ?? ''));
$attendees[$i]['status'] = $status;
$attendees[$i]['rsvp'] = false;
$this->logger->debug('Updating existing attendee: ' . $attendee['email'] . ' status: ' . $status);
diff --git a/lib/kolab_sync_data_email.php b/lib/kolab_sync_data_email.php
--- a/lib/kolab_sync_data_email.php
+++ b/lib/kolab_sync_data_email.php
@@ -398,6 +398,8 @@
$result['messageClass'] = 'IPM.Note.SMIME';
}
else if ($event = $this->get_invitation_event_from_message($message)) {
+ // Note: Depending on MessageClass a client will display a proper set of buttons
+ // Either Accept/Maybe/Decline (REQUEST), or "Remove from Calendar" (CANCEL) or none (REPLY).
$result['messageClass'] = 'IPM.Schedule.Meeting.Request';
$result['contentClass'] = 'urn:content-classes:calendarmessage';
@@ -425,7 +427,7 @@
// Organizer
if (!empty($event['attendees'])) {
- foreach ($event['attendees'] as $idx => $attendee) {
+ foreach ($event['attendees'] as $attendee) {
if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER' && !empty($attendee['email'])) {
$meeting['organizer'] = $attendee['email'];
break;
@@ -447,7 +449,6 @@
'now' => $fileTime,
]);
- // TODO handle other methods
if ($event['_method'] == 'REQUEST') {
$meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_REQUEST;
@@ -456,15 +457,59 @@
// parameter value of "NEEDS-ACTION" in the request". I think it is safe to do this for all requests.
// Note: This does not have impact on the existence of Accept/Decline buttons in the client.
$meeting['responseRequested'] = 1;
- } else {
+ } else { // REPLY or CANCEL
$meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_NORMAL;
+
+ $itip_processing = kolab_sync::get_instance()->config->get('activesync_itip_processing');
+ if ($itip_processing && empty($headers->flags['SEEN'])) {
+ // Optionally process the message and update the event in recipient's calendar
+ // TODO: Use this only for development purposes, for now better to use wallace
+ $calendar_class = new kolab_sync_data_calendar($this->device, $this->syncTimeStamp);
+ $attendeeStatus = $calendar_class->processItipReply($event);
+ }
+ else if ($event['_method'] == 'CANCEL') {
+ $attendeeStatus = kolab_sync_data_calendar::ITIP_CANCELLED;
+ }
+ else if (!empty($event['attendees'])) {
+ // Get the attendee/status in the REPLY
+ foreach ($event['attendees'] as $attendee) {
+ if (empty($attendee['role']) || $attendee['role'] != 'ORGANIZER') {
+ if (!empty($attendee['email']) && !empty($attendee['status'])) {
+ // Per iTip spec. there should be only one (non-organizer) attendee here
+ // FIXME: Verify is it realy the case with e.g. Kolab webmail, If not, we should
+ // probably use the message sender from the From: header
+ $attendeeStatus = strtoupper($attendee['status']);
+ break;
+ }
+ }
+ }
+ }
+
+ switch ($attendeeStatus) {
+ case kolab_sync_data_calendar::ITIP_CANCELLED:
+ $result['messageClass'] = 'IPM.Schedule.Meeting.Canceled';
+ break;
+ case kolab_sync_data_calendar::ITIP_DECLINED:
+ $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Neg';
+ break;
+ case kolab_sync_data_calendar::ITIP_TENTATIVE:
+ $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Tent';
+ break;
+ case kolab_sync_data_calendar::ITIP_ACCEPTED:
+ $result['messageClass'] = 'IPM.Schedule.Meeting.Resp.Pos';
+ break;
+ default:
+ $skipRequest = true;
+ }
}
// New time proposals aren't supported by Kolab.
// This disables the UI elements related to this on the client side
$meeting['disallowNewTimeProposal'] = 1;
- $result['meetingRequest'] = new Syncroton_Model_EmailMeetingRequest($meeting);
+ if (empty($skipRequest)) {
+ $result['meetingRequest'] = new Syncroton_Model_EmailMeetingRequest($meeting);
+ }
}
// Categories (Tags)
@@ -1810,6 +1855,11 @@
$libcal->mail_message_load(array('object' => $message));
$ical_objects = $libcal->get_mail_ical_objects();
+ // Skip methods we do not support here
+ if (!in_array($ical_objects->method, ['REQUEST', 'CANCEL', 'REPLY'])) {
+ return null;
+ }
+
// We support only one event in the iTip
foreach ($ical_objects as $mime_id => $event) {
if ($event['_type'] == 'event') {

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 4:33 AM (3 h, 5 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822584
Default Alt Text
D4570.1775190822.diff (15 KB)

Event Timeline