Page MenuHomePhorge

D4570.1775367436.diff
No OneTemporary

Authored By
Unknown
Size
13 KB
Referenced Files
None
Subscribers
None

D4570.1775367436.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,140 @@
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') {
+ 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 (!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 $idx => $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)) {
+ // TODO: The iTip might have been already processed, skip event update if status didn't change
+ // TODO: error handling
+ if ($this->find_and_update_attendee_status($attendees, $attendeeStatus, [$attendeeEmail])) {
+ $this->save_event($existing, null);
+ }
+ }
+ }
+
+ return $attendeeStatus;
+ }
+
/**
* Get an event from the invitation email or calendar folder
*/
@@ -803,21 +945,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'])) {
+ if (isset($old['recurrence']['EXCEPTIONS'])) {
+ $old['exceptions'] = &$old['recurrence']['EXCEPTIONS'];
+ }
+ else {
+ $old['exceptions'] = [];
+ }
+ }
+
+ $existing = false;
+ foreach ($old['exceptions'] as $i => $exception) {
+ if (libcalendaring::is_recurrence_exception($event, $exception)) {
+ $old['exceptions'][$i] = $event;
+ $existing = true;
+ }
+ }
+
+ 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);
}
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
@@ -397,7 +397,11 @@
else if ($headers->ctype == 'application/pkcs7-mime' || $headers->ctype == 'application/x-pkcs7-mime') {
$result['messageClass'] = 'IPM.Note.SMIME';
}
+ // FIXME: Should we skip this for folders like Trash, Junk or Sent? Should we process INBOX only?
+ // FIXME: What about delegation and shared/other user folders?
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 +429,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 +451,6 @@
'now' => $fileTime,
]);
- // TODO handle other methods
if ($event['_method'] == 'REQUEST') {
$meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_REQUEST;
@@ -456,15 +459,38 @@
// 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;
+
+ // Process the message and update the event in recipient's calendar
+ $calendar_class = new kolab_sync_data_calendar($this->device, $this->syncTimeStamp);
+ $attendeeStatus = $calendar_class->processItipReply($event);
+
+ 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 +1836,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
Sun, Apr 5, 5:37 AM (20 h, 16 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18823713
Default Alt Text
D4570.1775367436.diff (13 KB)

Event Timeline