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 @@ -185,20 +185,9 @@ $config = $this->getFolderConfig($event['_mailbox']); $result = array(); - // Timezone // Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows // only one timezone per-event. We'll use timezone of the start date - if ($event['start'] instanceof DateTime) { - $timezone = $event['start']->getTimezone(); - - if ($timezone && ($tz_name = $timezone->getName()) != 'UTC') { - $tzc = kolab_sync_timezone_converter::getInstance(); - - if ($tz_name = $tzc->encodeTimezone($tz_name, $event['start']->format('Y-m-d'))) { - $result['timezone'] = $tz_name; - } - } - } + $result['timezone'] = kolab_sync_timezone_converter::encodeTimezoneFromDate($event['start']); // Calendar namespace fields foreach ($this->mapping as $key => $name) { 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 @@ -118,6 +118,48 @@ } /** + * Encode a globalObjId according to https://interoperability.blob.core.windows.net/files/MS-ASEMAIL/%5bMS-ASEMAIL%5d-150526.pdf 2.2.2.3 + * + * @param array $data An array with the data to encode + * + * @return string the encoded globalObjId + */ + public static function encodeGlobalObjId(array $data): string + { + $classid = "040000008200e00074c5b7101a82e008"; + $uid = $data['uid']; + $vcalid = "vCal-Uid\1\0\0\0{$uid}\0"; + + $packed = pack( + "H32nCCPx8Va*", + $classid, + $data['year'] ?? 0, + $data['month'] ?? 0, + $data['day'] ?? 0, + $data['now'] ?? 0, + strlen($vcalid), + $vcalid + ); + + return base64_encode($packed); + } + + /** + * Decode a globalObjId according to https://interoperability.blob.core.windows.net/files/MS-ASEMAIL/%5bMS-ASEMAIL%5d-150526.pdf 2.2.2.3 + * + * @param string the encoded globalObjId + * + * @return array An array with the decoded data + */ + public static function decodeGlobalObjId(string $globalObjId): array + { + $unpackString = 'H32classid/nyear/Cmonth/Cday/Pnow/x8/Vbytecount/a*data'; + $decoded = unpack($unpackString, base64_decode($globalObjId)); + $decoded['uid'] = substr($decoded['data'], strlen("vCal-Uid\1\0\0\0"), -1); + return $decoded; + } + + /** * Creates model object * * @param Syncroton_Model_SyncCollection $collection Collection data @@ -339,6 +381,9 @@ $result['nativeBodyType'] = $message->has_html_part() ? 2 : 1; // Message class + $result['messageClass'] = 'IPM.Note'; + $result['contentClass'] = 'urn:content-classes:message'; + if ($headers->ctype == 'multipart/signed' && !empty($message->parts[1]) && $message->parts[1]->mimetype == 'application/pkcs7-signature' @@ -348,11 +393,61 @@ else if ($headers->ctype == 'application/pkcs7-mime' || $headers->ctype == 'application/x-pkcs7-mime') { $result['messageClass'] = 'IPM.Note.SMIME'; } - else { - $result['messageClass'] = 'IPM.Note'; - } + else if ($event = $this->get_invitation_event_from_message($message)) { + $result['messageClass'] = 'IPM.Schedule.Meeting.Request'; + $result['contentClass'] = 'urn:content-classes:calendarmessage'; + + $meeting = array(); + + $meeting['allDayEvent'] = $event['allday'] ?? null ? 1 : 0; + $meeting['startTime'] = $event['start']; + $meeting['dtStamp'] = $event['created'] ?? null; + $meeting['endTime'] = $event['end'] ?? null; + + //TODO implement recurrences. We can't detect exceptions like this (don't know how), and the recurrences structure is different from event, + //so that also doesn't work like this. + // if (isset($event['recurrence']['EXCEPTIONS'])) { + // $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_EXCEPTION; + // $this->recurrence_from_kolab($collection, $event, $meeting); + // // } else if (isset($event['recurrence'])) { + // // $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_SINGLE; + // // $meeting['recurrenceId'] = set the date; + // } else if (isset($event['recurrence'])) { + // $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_MASTER; + // $this->recurrence_from_kolab($collection, $event, $meeting); + // } else { + // $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_NORMAL; + // } + $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_NORMAL; + + $meeting['location'] = $event['location'] ?? null; + + // Organizer + if (!empty($event['attendees'])) { + foreach ($event['attendees'] as $idx => $attendee) { + if ($attendee['role'] == 'ORGANIZER') { + if ($email = $attendee['email']) { + $meeting['organizer'] = $email; + } + break; + } + } + } - $result['contentClass'] = 'urn:content-classes:message'; + // Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows + // only one timezone per-event. We'll use timezone of the start date + $meeting['timeZone'] = kolab_sync_timezone_converter::encodeTimezoneFromDate($event['start']); + $meeting['globalObjId'] = self::encodeGlobalObjId(['uid' => $event['uid']]); + + //TODO handle other methods + if ($event['_method'] == 'REQUEST') { + $meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_REQUEST; + } else { + $meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_NORMAL; + } + + $result['meetingRequest'] = new Syncroton_Model_EmailMeetingRequest($meeting); + } // Categories (Tags) if (isset($this->tag_categories) && $this->tag_categories) { @@ -1675,22 +1770,33 @@ /** * Returns calendar event data from the iTip invitation attached to a mail message */ + public function get_invitation_event_from_message($message) + { + // Parse the message and find iTip attachments + $libcal = libcalendaring::get_instance(); + $libcal->mail_message_load(array('object' => $message)); + $ical_objects = $libcal->get_mail_ical_objects(); + + // We support only one event in the iTip + foreach ($ical_objects as $mime_id => $event) { + if ($event['_type'] == 'event') { + $event['_method'] = $ical_objects->method; + return $event; + } + } + return null; + } + + /** + * Returns calendar event data from the iTip invitation attached to a mail message + */ public function get_invitation_event($messageId) { // Get the mail message object if ($message = $this->getObject($messageId)) { - // Parse the message and find iTip attachments - $libcal = libcalendaring::get_instance(); - $libcal->mail_message_load(array('object' => $message)); - $ical_objects = $libcal->get_mail_ical_objects(); - - // We support only one event in the iTip - foreach ($ical_objects as $mime_id => $event) { - if ($event['_type'] == 'event') { - return $event; - } - } + return $this->get_invitation_event_from_message($message); } + return null; } diff --git a/lib/kolab_sync_timezone_converter.php b/lib/kolab_sync_timezone_converter.php --- a/lib/kolab_sync_timezone_converter.php +++ b/lib/kolab_sync_timezone_converter.php @@ -217,6 +217,29 @@ return $this->_packTimezoneInfo($offsets); } + + /** + * Returns an encoded timezone representation from $date + * + * @param DateTime $date The date with the timezone to encode + * + * @return string encoded timezone + */ + public static function encodeTimezoneFromDate($date) + { + if ($date instanceof DateTime) { + $timezone = $date->getTimezone(); + + if ($timezone && ($tz_name = $timezone->getName()) != 'UTC') { + $tzc = self::getInstance(); + if ($tz_name = $tzc->encodeTimezone($tz_name, $date->format('Y-m-d'))) { + return $tz_name; + } + } + } + return null; + } + /** * Get offsets for given timezone * diff --git a/tests/globalid_converter.php b/tests/globalid_converter.php new file mode 100644 --- /dev/null +++ b/tests/globalid_converter.php @@ -0,0 +1,32 @@ +assertSame(51, $output['bytecount']); + $this->assertSame('{81412D3C-2A24-4E9D-B20E-11F7BBE92799}', $output['uid']); + + $encoded = kolab_sync_data_email::encodeGlobalObjId($output); + $this->assertSame($encoded, $input); + + + $input = 'BAAAAIIA4AB0xbcQGoLgCAfUCRDgQMnBJoXEAQAAAAAAAAAAEAAAAAvw7UtuTulOnjnjhns3jvM='; + $output = kolab_sync_data_email::decodeGlobalObjId($input); + + $this->assertSame(16, $output['bytecount']); + $this->assertSame(2004, $output['year']); + $this->assertSame(9, $output['month']); + $this->assertSame(16, $output['day']); + //FIXME we don't currently implement non ical uids + // $encoded = kolab_sync_data_email::encodeGlobalObjId($output); + // $this->assertSame($encoded, $input); + } +}