Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117752724
D4570.1775190822.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
15 KB
Referenced Files
None
Subscribers
None
D4570.1775190822.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D4570: MeetingResponse and MeetingRequest fixes
Attached
Detach File
Event Timeline