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 @@ -117,6 +117,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 * @@ -339,20 +381,54 @@ $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' ) { $result['messageClass'] = 'IPM.Note.SMIME.MultipartSigned'; + $result['contentClass'] = 'urn:content-classes:message'; } else if ($headers->ctype == 'application/pkcs7-mime' || $headers->ctype == 'application/x-pkcs7-mime') { $result['messageClass'] = 'IPM.Note.SMIME'; - } - else { - $result['messageClass'] = 'IPM.Note'; - } + $result['contentClass'] = 'urn:content-classes:message'; + } + //TODO detect the correct type + else if ($headers->ctype == 'multipart/alternative') { + if ($event = $this->get_invitation_event_from_message($message)) { + $result['messageClass'] = 'IPM.Schedule.Meeting.Request'; + $result['contentClass'] = 'urn:content-classes:calendarmessage'; + + $meeting = array(); + + //TODO fill in the details + $meeting['allDayEvent'] = 0; + $meeting['startTime'] = $event['start']; + $meeting['dtStamp'] = $event['created']; + $meeting['endTime'] = $event['end']; + //TODO set recurring or not + $meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_NORMAL; + // 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'))) { + $meeting['timeZone'] = $tz_name; + } + } + } + $meeting['globalObjId'] = self::encodeGlobalObjId(['uid' => $event['uid']]); + $meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_NORMAL; - $result['contentClass'] = 'urn:content-classes:message'; + $result['meetingRequest'] = new Syncroton_Model_EmailMeetingRequest($meeting); + } + } // Categories (Tags) if (isset($this->tag_categories) && $this->tag_categories) { @@ -1672,6 +1748,25 @@ return $out; } + /** + * 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') { + return $event; + } + } + return null; + } + /** * Returns calendar event data from the iTip invitation attached to a mail message */ @@ -1679,18 +1774,9 @@ { // 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/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); + } +}