diff --git a/src/app/DataMigrator/EWS/Appointment.php b/src/app/DataMigrator/EWS/Appointment.php index 6256026f..98799ab2 100644 --- a/src/app/DataMigrator/EWS/Appointment.php +++ b/src/app/DataMigrator/EWS/Appointment.php @@ -1,92 +1,92 @@ 'calendar:UID']; return $request; } /** * Process event object */ - protected function processItem(Type &$item): bool + protected function processItem(Type $item): bool { // Decode MIME content // TODO: Maybe find less-hacky way $content = $item->getMimeContent(); $ical = base64_decode((string) $content); // Inject attachment bodies into the iCalendar content // Calendar event attachments are exported as: // ATTACH:CID:81490FBA13A3DC2BF071B894C96B44BA51BEAAED@eurprd05.prod.outlook.com if ($item->getHasAttachments()) { // FIXME: I've tried hard and no matter what ContentId property is always empty // This means we can't match the CID from iCalendar with the attachment. // That's why we'll just remove all ATTACH:CID:... occurrences // and inject attachments to the main event $ical = preg_replace('/\r\nATTACH:CID:[^\r]+\r\n(\r\n [^\r\n]*)?/', '', $ical); foreach ((array) $item->getAttachments()->getFileAttachment() as $attachment) { $_attachment = $this->getAttachment($attachment); // FIXME: This is imo inconsistence on php-ews side that MimeContent // is base64 encoded, but Content isn't // TODO: We should not do it in memory to not exceed the memory limit $body = base64_encode($_attachment->getContent()); $body = rtrim(chunk_split($body, 74, "\r\n "), ' '); $ctype = $_attachment->getContentType(); // Inject the attachment at the end of the first VEVENT block // TODO: We should not do it in memory to not exceed the memory limit $append = "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE={$ctype}:\r\n {$body}"; $pos = strpos($ical, "\r\nEND:VEVENT"); $ical = substr_replace($ical, $append, $pos + 2, 0); } } // TODO: Maybe find less-hacky way $item->getMimeContent()->_ = $ical; return true; } /** * Get Item UID (Generate a new one if needed) */ protected function getUID(Type $item): string { if ($this->uid === null) { // Only appointments have UID property $this->uid = $item->getUID(); } return $this->uid; } } diff --git a/src/app/DataMigrator/EWS/Contact.php b/src/app/DataMigrator/EWS/Contact.php index e96fe532..6225ca1a 100644 --- a/src/app/DataMigrator/EWS/Contact.php +++ b/src/app/DataMigrator/EWS/Contact.php @@ -1,49 +1,49 @@ getMimeContent()); // Inject UID to the vCard $uid = $this->getUID($item); $vcard = str_replace("BEGIN:VCARD", "BEGIN:VCARD\r\nUID:{$uid}", $vcard); // Note: Looks like PHOTO property is exported properly, so we // don't have to handle attachments as we do for calendar items // TODO: Maybe find less-hacky way $item->getMimeContent()->_ = $vcard; return true; } } diff --git a/src/app/DataMigrator/EWS/DistList.php b/src/app/DataMigrator/EWS/DistList.php index 4cbbd45f..37570127 100644 --- a/src/app/DataMigrator/EWS/DistList.php +++ b/src/app/DataMigrator/EWS/DistList.php @@ -1,64 +1,64 @@ $this->getUID($item), "KIND" => "group", "FN" => $item->getDisplayName(), "REV;VALUE=DATE-TIME" => $item->getLastModifiedTime(), ]; $vcard = "BEGIN:VCARD\r\nVERSION:4.0\r\nPRODID:Kolab EWS DataMigrator\r\n"; foreach ($data as $key => $value) { // TODO: value wrapping/escaping $vcard .= "{$key}:{$value}\r\n"; } // Process list members // Note: The fact that getMembers() returns stdClass is probably a bug in php-ews foreach ($item->getMembers()->Member as $member) { $mailbox = $member->getMailbox(); $mailto = $mailbox->getEmailAddress(); $name = $mailbox->getName(); // FIXME: Investigate if mailto: members are handled properly by Kolab // or we need to use MEMBER:urn:uuid:9bd97510-9dbb-4810-a144-6180962df5e0 syntax // But do not forget lists can have members that are not contacts if ($mailto) { if ($name && $name != $mailto) { $mailto = urlencode(sprintf('"%s" <%s>', addcslashes($name, '"'), $mailto)); } $vcard .= "MEMBER:mailto:{$mailto}\r\n"; } } $vcard .= "END:VCARD"; // TODO: Maybe find less-hacky way $item->setMimeContent((new Type\MimeContentType)->set('_', $vcard)); return true; } } diff --git a/src/app/DataMigrator/EWS/Item.php b/src/app/DataMigrator/EWS/Item.php index 41786d0c..1dec3996 100644 --- a/src/app/DataMigrator/EWS/Item.php +++ b/src/app/DataMigrator/EWS/Item.php @@ -1,134 +1,134 @@ engine = $engine; $this->folder = $folder; } /** * Factory method. * Returns object suitable to handle specified item type. */ public static function factory(EWS $engine, Type $item, array $folder) { $item_class = str_replace('IPM.', '', $item->getItemClass()); $item_class = "\App\DataMigrator\EWS\\{$item_class}"; if (class_exists($item_class)) { return new $item_class($engine, $folder); } } /** * Synchronize specified object */ public function syncItem(Type $item): void { // Fetch the item $item = $this->engine->api->getItem($item->getItemId(), $this->getItemRequest()); $uid = $this->getUID($item); $this->engine->debug("* Saving item {$uid}..."); // Apply type-specific format converters if ($this->processItem($item) === false) { return; } $uid = preg_replace('/[^a-zA-Z0-9_:@-]/', '', $uid); $location = $this->folder['location']; if (!file_exists($location)) { mkdir($location, 0740, true); } $location .= '/' . $uid . '.' . $this::FILE_EXT; file_put_contents($location, (string) $item->getMimeContent()); } /** * Item conversion code */ - abstract protected function processItem(Type &$item): bool; + abstract protected function processItem(Type $item): bool; /** * Get GetItem request parameters */ protected function getItemRequest(): array { $request = [ 'ItemShape' => [ // Reqest default set of properties 'BaseShape' => 'Default', // Additional properties, e.g. LastModifiedTime // FIXME: How to add multiple properties here? 'AdditionalProperties' => [ 'FieldURI' => ['FieldURI' => 'item:LastModifiedTime'], ] ] ]; return $request; } /** * Fetch attachment object from Exchange */ protected function getAttachment(Type\FileAttachmentType $attachment) { $request = [ 'AttachmentIds' => [ $attachment->getAttachmentId()->toXmlObject() ], 'AttachmentShape' => [ 'IncludeMimeContent' => true, ] ]; return $this->engine->api->getClient()->GetAttachment($request); } /** * Get Item UID (Generate a new one if needed) */ protected function getUID(Type $item): string { if ($this->uid === null) { // We should generate an UID for objects that do not have it // and inject it into the output file // FIXME: Should we use e.g. md5($itemId->getId()) instead? $this->uid = \App\Utils::uuidStr(); } return $this->uid; } } diff --git a/src/app/DataMigrator/EWS/Task.php b/src/app/DataMigrator/EWS/Task.php index 10a50e0d..73054061 100644 --- a/src/app/DataMigrator/EWS/Task.php +++ b/src/app/DataMigrator/EWS/Task.php @@ -1,111 +1,111 @@ $this->getUID($item), "DTSTAMP;VALUE=DATE-TIME" => $item->getLastModifiedTime(), "CREATED;VALUE=DATE-TIME" => $item->getDateTimeCreated(), "SEQUENCE" => '0', // TODO "SUMMARY" => $item->getSubject(), "DESCRIPTION" => (string) $item->getBody(), "PERCENT-COMPLETE" => intval($item->getPercentComplete()), "STATUS" => strtoupper($item->getStatus()), ]; if ($dueDate = $item->getDueDate()) { $data["DUE:VALUE=DATE-TIME"] = $dueDate; } if ($startDate = $item->getStartDate()) { $data["DTSTART:VALUE=DATE-TIME"] = $startDate; } if (($categories = $item->getCategories()) && $categories->String) { $data["CATEGORIES"] = $categories->String; } if ($sensitivity = $item->getSensitivity()) { $sensitivity_map = [ 'CONFIDENTIAL' => 'CONFIDENTIAL', 'NORMAL' => 'PUBLIC', 'PERSONAL' => 'PUBLIC', 'PRIVATE' => 'PRIVATE', ]; $data['CLASS'] = $sensitivity_map[strtoupper($sensitivity)] ?: 'PUBLIC'; } // TODO: VTIMEZONE, VALARM, ORGANIZER, ATTENDEE, RRULE, // TODO: PRIORITY (Importance) - not used by Kolab // ReminderDueBy, ReminderIsSet, IsRecurring, Owner, Recurrence $ical = "BEGIN:VCALENDAR\r\nMETHOD:PUBLISH\r\nVERSION:2.0\r\nPRODID:Kolab EWS DataMigrator\r\nBEGIN:VTODO\r\n"; foreach ($data as $key => $value) { // TODO: value wrapping/escaping $ical .= "{$key}:{$value}\r\n"; } // Attachments if ($item->getHasAttachments()) { foreach ((array) $item->getAttachments()->getFileAttachment() as $attachment) { $_attachment = $this->getAttachment($attachment); // FIXME: This is imo inconsistence on php-ews side that MimeContent // is base64 encoded, but Content isn't // TODO: We should not do it in memory to not exceed the memory limit $body = base64_encode($_attachment->getContent()); $body = rtrim(chunk_split($body, 74, "\r\n "), ' '); $ctype = $_attachment->getContentType(); // Inject the attachment at the end of the VTODO block // TODO: We should not do it in memory to not exceed the memory limit $ical .= "ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE={$ctype}:\r\n {$body}\r\n"; } } $ical .= "END:VEVENT\r\n"; $ical .= "END:VCALENDAR"; // TODO: Maybe find less-hacky way $item->setMimeContent((new Type\MimeContentType)->set('_', $ical)); return true; } }