diff --git a/src/app/Backends/DAV.php b/src/app/Backends/DAV.php
index f0f86f33..f5ac4ab6 100644
--- a/src/app/Backends/DAV.php
+++ b/src/app/Backends/DAV.php
@@ -1,569 +1,587 @@
'urn:ietf:params:xml:ns:caldav',
self::TYPE_VTODO => 'urn:ietf:params:xml:ns:caldav',
self::TYPE_VCARD => 'urn:ietf:params:xml:ns:carddav',
];
protected $url;
protected $user;
protected $password;
protected $responseHeaders = [];
protected $homes;
/**
* Object constructor
*/
public function __construct($user, $password, $url = null)
{
$this->url = $url ?: \config('services.dav.uri');
$this->user = $user;
$this->password = $password;
}
/**
* Discover DAV home (root) collection of a specified type.
*
* @return array|false Home locations or False on error
*/
public function discover()
{
if (is_array($this->homes)) {
return $this->homes;
}
$path = parse_url($this->url, PHP_URL_PATH);
$body = ''
. ''
. ''
. ''
. ''
. '';
// Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it)
$response = $this->request('/', 'PROPFIND', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
if (empty($response)) {
\Log::error("Failed to get current-user-principal from the DAV server.");
return false;
}
$elements = $response->getElementsByTagName('response');
$principal_href = '';
foreach ($elements as $element) {
foreach ($element->getElementsByTagName('current-user-principal') as $prop) {
$principal_href = $prop->nodeValue;
break;
}
}
if ($path && str_starts_with($principal_href, $path)) {
$principal_href = substr($principal_href, strlen($path));
}
+ $ns = [
+ 'xmlns:d="DAV:"',
+ 'xmlns:cal="urn:ietf:params:xml:ns:caldav"',
+ 'xmlns:card="urn:ietf:params:xml:ns:carddav"',
+ ];
+
$body = ''
- . ''
+ . ''
. ''
. ''
. ''
. ''
. ''
. '';
$response = $this->request($principal_href, 'PROPFIND', $body);
if (empty($response)) {
\Log::error("Failed to get home collections from the DAV server.");
return false;
}
$elements = $response->getElementsByTagName('response');
$homes = [];
if ($element = $response->getElementsByTagName('response')->item(0)) {
if ($prop = $element->getElementsByTagName('prop')->item(0)) {
foreach ($prop->childNodes as $home) {
if ($home->firstChild && $home->firstChild->localName == 'href') {
$href = $home->firstChild->nodeValue;
if ($path && str_starts_with($href, $path)) {
$href = substr($href, strlen($path));
}
$homes[$home->localName] = $href;
}
}
}
}
return $this->homes = $homes;
}
/**
* Get user home folder of specified type
*
* @param string $type Home type or component name
*
* @return string|null Folder location href
*/
public function getHome($type)
{
$options = [
self::TYPE_VEVENT => 'calendar-home-set',
self::TYPE_VTODO => 'calendar-home-set',
self::TYPE_VCARD => 'addressbook-home-set',
self::TYPE_NOTIFICATION => 'notification-URL',
];
$homes = $this->discover();
if (is_array($homes) && isset($options[$type])) {
return $homes[$options[$type]] ?? null;
}
return null;
}
/**
* Check if we can connect to the DAV server
*
* @return bool True on success, False otherwise
*/
public static function healthcheck(): bool
{
// TODO
return true;
}
/**
* Get list of folders of specified type.
*
* @param string $component Component to filter by (VEVENT, VTODO, VCARD)
*
* @return false|array List of folders' metadata or False on error
*/
public function listFolders(string $component)
{
$root_href = $this->getHome($component);
if ($root_href === null) {
return false;
}
$ns = 'xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"';
$props = '';
if ($component != self::TYPE_VCARD) {
$ns .= ' xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:a="http://apple.com/ns/ical/" xmlns:k="Kolab:"';
$props = ''
. ''
. '';
}
$body = ''
. ''
. ''
. ''
. ''
. ''
. $props
. ''
. '';
// Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it)
$headers = ['Depth' => 1, 'Prefer' => 'return-minimal'];
$response = $this->request($root_href, 'PROPFIND', $body, $headers);
if (empty($response)) {
\Log::error("Failed to get folders list from the DAV server.");
return false;
}
$folders = [];
foreach ($response->getElementsByTagName('response') as $element) {
$folder = DAV\Folder::fromDomElement($element);
// Note: Addressbooks don't have 'type' specified
if (
($component == self::TYPE_VCARD && in_array('addressbook', $folder->types))
|| in_array($component, $folder->components)
) {
$folders[] = $folder;
}
}
return $folders;
}
/**
* Create a DAV object in a folder
*
* @param DAV\CommonObject $object Object
*
* @return false|DAV\CommonObject Object on success, False on error
*/
public function create(DAV\CommonObject $object)
{
$headers = ['Content-Type' => $object->contentType];
- $response = $this->request($object->href, 'PUT', $object, $headers);
+ $content = (string) $object;
+
+ if (!strlen($content)) {
+ throw new \Exception("Cannot PUT an empty DAV object");
+ }
+
+ $response = $this->request($object->href, 'PUT', $content, $headers);
if ($response !== false) {
if (!empty($this->responseHeaders['ETag'])) {
$etag = $this->responseHeaders['ETag'][0];
if (preg_match('|^".*"$|', $etag)) {
$etag = substr($etag, 1, -1);
}
$object->etag = $etag;
}
return $object;
}
return false;
}
/**
* Update a DAV object in a folder
*
* @param DAV\CommonObject $object Object
*
* @return false|DAV\CommonObject Object on success, False on error
*/
public function update(DAV\CommonObject $object)
{
return $this->create($object);
}
/**
* Delete a DAV object from a folder
*
* @param string $location Object location
*
* @return bool True on success, False on error
*/
public function delete(string $location)
{
$response = $this->request($location, 'DELETE', '', ['Depth' => 1, 'Prefer' => 'return-minimal']);
return $response !== false;
}
/**
* Create a DAV folder (collection)
*
* @param DAV\Folder $folder Folder object
*
* @return bool True on success, False on error
*/
public function folderCreate(DAV\Folder $folder)
{
$response = $this->request($folder->href, 'MKCOL', $folder->toXML('mkcol'));
return $response !== false;
}
/**
* Delete a DAV folder (collection)
*
* @param string $location Folder location
*
* @return bool True on success, False on error
*/
public function folderDelete($location)
{
$response = $this->request($location, 'DELETE');
return $response !== false;
}
/**
* Get all properties of a folder.
*
* @param string $location Object location
*
* @return false|DAV\Folder Folder metadata or False on error
*/
public function folderInfo(string $location)
{
$body = DAV\Folder::propfindXML();
// Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it)
$response = $this->request($location, 'PROPFIND', $body, ['Depth' => 0, 'Prefer' => 'return-minimal']);
if (!empty($response) && ($element = $response->getElementsByTagName('response')->item(0))) {
return DAV\Folder::fromDomElement($element);
}
return false;
}
/**
* Update a DAV folder (collection)
*
* @param DAV\Folder $folder Folder object
*
* @return bool True on success, False on error
*/
public function folderUpdate(DAV\Folder $folder)
{
// Note: Changing resourcetype property is forbidden (at least by Cyrus)
$response = $this->request($folder->href, 'PROPPATCH', $folder->toXML('propertyupdate'));
return $response !== false;
}
/**
* Check server options (and authentication)
*
* @return false|array DAV capabilities on success, False on error
*/
public function options()
{
$response = $this->request('', 'OPTIONS');
if ($response !== false) {
return preg_split('/,\s+/', implode(',', $this->responseHeaders['DAV'] ?? []));
}
return false;
}
/**
* Search DAV objects in a folder.
*
* @param string $location Folder location
* @param DAV\Search $search Search request parameters
- * @param ?callable $callback Callback to execute on every object
+ * @param callable $callback A callback to execute on every item
*
- * @return false|array Objects metadata on success, False on error
+ * @return false|array List of objects on success, False on error
*/
public function search(string $location, DAV\Search $search, $callback = null)
{
$headers = ['Depth' => $search->depth, 'Prefer' => 'return-minimal'];
$response = $this->request($location, 'REPORT', $search, $headers);
if (empty($response)) {
\Log::error("Failed to get objects from the DAV server.");
return false;
}
$objects = [];
foreach ($response->getElementsByTagName('response') as $element) {
$object = $this->objectFromElement($element, $search->component);
if ($callback) {
$object = $callback($object);
}
- $objects[] = $object;
+ if ($object) {
+ if (is_array($object)) {
+ $objects[$object[0]] = $object[1];
+ } else {
+ $objects[] = $object;
+ }
+ }
}
return $objects;
}
/**
* Fetch DAV objects data from a folder
*
* @param string $location Folder location
* @param string $component Object type (VEVENT, VTODO, VCARD)
- * @param array $hrefs List of objects' locations to fetch (empty for all objects)
+ * @param array $hrefs List of objects' locations to fetch
*
* @return false|array Objects metadata on success, False on error
*/
public function getObjects(string $location, string $component, array $hrefs = [])
{
if (empty($hrefs)) {
return [];
}
$body = '';
foreach ($hrefs as $href) {
$body .= '' . $href . '';
}
$queries = [
self::TYPE_VEVENT => 'calendar-multiget',
self::TYPE_VTODO => 'calendar-multiget',
self::TYPE_VCARD => 'addressbook-multiget',
];
$types = [
self::TYPE_VEVENT => 'calendar-data',
self::TYPE_VTODO => 'calendar-data',
self::TYPE_VCARD => 'address-data',
];
$body = ''
. ' '
. ''
. ''
. ''
. ''
. $body
. '';
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
if (empty($response)) {
\Log::error("Failed to get objects from the DAV server.");
return false;
}
$objects = [];
foreach ($response->getElementsByTagName('response') as $element) {
$objects[] = $this->objectFromElement($element, $component);
}
return $objects;
}
/**
* Parse XML content
*/
protected function parseXML($xml)
{
$doc = new \DOMDocument('1.0', 'UTF-8');
if (str_starts_with($xml, 'loadXML($xml)) {
throw new \Exception("Failed to parse XML");
}
$doc->formatOutput = true;
}
return $doc;
}
/**
* Parse request/response body for debug purposes
*/
protected function debugBody($body, $headers)
{
$head = '';
foreach ($headers as $header_name => $header_value) {
if (is_array($header_value)) {
$header_value = implode("\n\t", $header_value);
}
$head .= "{$header_name}: {$header_value}\n";
}
if ($body instanceof DAV\CommonObject) {
$body = (string) $body;
}
if (str_starts_with($body, 'formatOutput = true;
$doc->preserveWhiteSpace = false;
if (!$doc->loadXML($body)) {
throw new \Exception("Failed to parse XML");
}
$body = $doc->saveXML();
}
return $head . (is_string($body) && strlen($body) > 0 ? "\n{$body}" : '');
}
/**
* Create DAV\CommonObject from a DOMElement
*/
protected function objectFromElement($element, $component)
{
switch ($component) {
case self::TYPE_VEVENT:
$object = DAV\Vevent::fromDomElement($element);
break;
case self::TYPE_VTODO:
$object = DAV\Vtodo::fromDomElement($element);
break;
case self::TYPE_VCARD:
$object = DAV\Vcard::fromDomElement($element);
break;
default:
throw new \Exception("Unknown component: {$component}");
}
return $object;
}
/**
* Execute HTTP request to a DAV server
*/
protected function request($path, $method, $body = '', $headers = [])
{
$debug = \config('app.debug');
$url = $this->url;
$this->responseHeaders = [];
if ($path && ($rootPath = parse_url($url, PHP_URL_PATH)) && str_starts_with($path, $rootPath)) {
$path = substr($path, strlen($rootPath));
}
$url .= $path;
$client = Http::withBasicAuth($this->user, $this->password);
// $client = Http::withToken($token); // Bearer token
if ($body) {
if (!isset($headers['Content-Type'])) {
$headers['Content-Type'] = 'application/xml; charset=utf-8';
}
$client->withBody($body, $headers['Content-Type']);
}
if (!empty($headers)) {
$client->withHeaders($headers);
}
if ($debug) {
$body = $this->debugBody($body, $headers);
\Log::debug("C: {$method}: {$url}" . (strlen($body) > 0 ? "\n$body" : ''));
}
$response = $client->send($method, $url);
$body = $response->body();
$code = $response->status();
if ($debug) {
\Log::debug("S: [{$code}]\n" . $this->debugBody($body, $response->headers()));
}
// Throw an exception if a client or server error occurred...
$response->throw();
$this->responseHeaders = $response->headers();
return $this->parseXML($body);
}
}
diff --git a/src/app/Backends/DAV/Opaque.php b/src/app/Backends/DAV/Opaque.php
index 65a88b87..40744d0e 100644
--- a/src/app/Backends/DAV/Opaque.php
+++ b/src/app/Backends/DAV/Opaque.php
@@ -1,23 +1,27 @@
content = file_get_contents($filename);
+ if ($is_file) {
+ $this->content = file_get_contents($content);
+ } else {
+ $this->content = $content;
+ }
}
/**
* Create string representation of the DAV object
*
* @return string
*/
public function __toString()
{
return $this->content;
}
}
diff --git a/src/app/Backends/DAV/Search.php b/src/app/Backends/DAV/Search.php
index 3c9bc7be..9291a7af 100644
--- a/src/app/Backends/DAV/Search.php
+++ b/src/app/Backends/DAV/Search.php
@@ -1,79 +1,90 @@
component = $component;
+ $this->withContent = $withContent;
}
/**
* Create string representation of the search
*
* @return string
*/
public function __toString()
{
$ns = implode(' ', [
'xmlns:d="DAV:"',
'xmlns:c="' . DAV::NAMESPACES[$this->component] . '"',
]);
// Return properties
$props = [];
foreach ($this->properties as $prop) {
$props[] = '<' . $prop . '/>';
}
// Warning: Looks like some servers (iRony) ignore address-data/calendar-data
// and return full VCARD/VCALENDAR. Which leads to unwanted loads of data in a response.
if (!empty($this->dataProperties)) {
$more_props = [];
foreach ($this->dataProperties as $prop) {
$more_props[] = '';
}
if ($this->component == DAV::TYPE_VCARD) {
$props[] = '' . implode('', $more_props) . '';
} else {
$props[] = ''
. ''
. ''
. '' . implode('', $more_props) . ''
. '';
}
+ } elseif ($this->withContent) {
+ if ($this->component == DAV::TYPE_VCARD) {
+ $props[] = '';
+ } else {
+ $props[] = '';
+ }
}
// Search filter
$filter = '';
if ($this->component == DAV::TYPE_VCARD) {
$query = 'addressbook-query';
} else {
$query = 'calendar-query';
- $filter = '';
- $filter = "{$filter}";
+ $filter = ''
+ . ''
+ . ''
+ . '';
}
if (empty($props)) {
$props = '';
} else {
$props = '' . implode('', $props) . '';
}
return ''
. "" . $props . $filter . "";
}
}
diff --git a/src/app/Backends/DAV/Vcard.php b/src/app/Backends/DAV/Vcard.php
index b71edb39..d846e1dc 100644
--- a/src/app/Backends/DAV/Vcard.php
+++ b/src/app/Backends/DAV/Vcard.php
@@ -1,90 +1,136 @@
getElementsByTagName('address-data')->item(0)) {
$object->fromVcard($data->nodeValue);
}
return $object;
}
/**
* Set object properties from a vcard
*
* @param string $vcard vCard string
*/
protected function fromVcard(string $vcard): void
{
$vobject = Reader::read($vcard, Reader::OPTION_FORGIVING | Reader::OPTION_IGNORE_INVALID_LINES);
if ($vobject->name != 'VCARD') {
// FIXME: throw an exception?
return;
}
+ $this->vobject = $vobject;
+ $this->members = [];
+
$string_properties = [
+ 'CLASS',
'FN',
+ 'KIND',
+ 'NOTE',
+ 'PRODID',
'REV',
'UID',
+ 'VERSION',
];
foreach ($vobject->children() as $prop) {
if (!($prop instanceof Property)) {
continue;
}
switch ($prop->name) {
// TODO: Map all vCard properties to class properties
+ case 'EMAIL':
+ $props = [];
+ foreach ($prop->parameters() as $name => $value) {
+ $key = Str::camel(strtolower($name));
+ $props[$key] = (string) $value;
+ }
+
+ $props['email'] = (string) $prop;
+ $this->email[] = $props;
+ break;
+
+ case 'MEMBER':
+ foreach ($prop as $member) {
+ $value = (string) $member;
+ if (preg_match('/^mailto:/', $value)) {
+ $this->members[] = $value;
+ }
+ }
+ break;
+
default:
// map string properties
if (in_array($prop->name, $string_properties)) {
$key = Str::camel(strtolower($prop->name));
$this->{$key} = (string) $prop;
}
// custom properties
if (\str_starts_with($prop->name, 'X-')) {
$this->custom[$prop->name] = (string) $prop;
}
}
}
+
+ if (!empty($this->custom['X-ADDRESSBOOKSERVER-KIND']) && empty($this->kind)) {
+ $this->kind = strtolower($this->custom['X-ADDRESSBOOKSERVER-KIND']);
+ }
}
/**
* Create string representation of the DAV object (vcard)
*
* @return string
*/
public function __toString()
{
- // TODO: This will be needed when we want to create/update objects
- return '';
+ if (!$this->vobject) {
+ // TODO we currently can only serialize a message back that we just read
+ throw new \Exception("Writing from properties is not implemented");
+ }
+
+ return Writer::write($this->vobject);
}
}
diff --git a/src/app/Backends/DAV/Vevent.php b/src/app/Backends/DAV/Vevent.php
index a5c40091..4d11812b 100644
--- a/src/app/Backends/DAV/Vevent.php
+++ b/src/app/Backends/DAV/Vevent.php
@@ -1,289 +1,294 @@
getElementsByTagName('calendar-data')->item(0)) {
$object->fromIcal($data->nodeValue);
}
return $object;
}
/**
* Set object properties from an iCalendar
*
* @param string $ical iCalendar string
*/
protected function fromIcal(string $ical): void
{
- $options = VObject\Reader::OPTION_FORGIVING | VObject\Reader::OPTION_IGNORE_INVALID_LINES;
- $this->vobject = VObject\Reader::read($ical, $options);
+ $this->vobject = Reader::read($ical, Reader::OPTION_FORGIVING | Reader::OPTION_IGNORE_INVALID_LINES);
if ($this->vobject->name != 'VCALENDAR') {
return;
}
$selfType = strtoupper(class_basename(get_class($this)));
+ if (!empty($this->vobject->PRODID)) {
+ $this->prodid = (string) $this->vobject->PRODID;
+ }
+
foreach ($this->vobject->getComponents() as $component) {
if ($component->name == $selfType) {
$this->fromVObject($component);
return;
}
}
}
/**
* Set object properties from a Sabre/VObject component object
*
- * @param VObject\Component $vobject Sabre/VObject component
+ * @param Component $vobject Sabre/VObject component
*/
- protected function fromVObject(VObject\Component $vobject): void
+ protected function fromVObject(Component $vobject): void
{
$string_properties = [
+ 'CLASS',
'COMMENT',
'DESCRIPTION',
'LOCATION',
+ 'PRIORITY',
'SEQUENCE',
'STATUS',
'SUMMARY',
'TRANSP',
'UID',
'URL',
];
// map string properties
foreach ($string_properties as $prop) {
if (isset($vobject->{$prop})) {
$key = Str::camel(strtolower($prop));
$this->{$key} = (string) $vobject->{$prop};
}
}
// map other properties
foreach ($vobject->children() as $prop) {
- if (!($prop instanceof VObject\Property)) {
+ if (!($prop instanceof Property)) {
continue;
}
switch ($prop->name) {
case 'DTSTART':
case 'DTEND':
- case 'DUE':
case 'CREATED':
case 'LAST-MODIFIED':
case 'DTSTAMP':
$key = Str::camel(strtolower($prop->name));
// These are of type Sabre\VObject\Property\ICalendar\DateTime
$this->{$key} = $prop;
break;
case 'RRULE':
- $params = !empty($this->recurrence) ? $this->recurrence : [];
+ $params = [];
foreach ($prop->getParts() as $k => $v) {
$params[Str::camel(strtolower($k))] = is_array($v) ? implode(',', $v) : $v;
}
if (!empty($params['until'])) {
$params['until'] = new \DateTime($params['until']);
}
if (empty($params['interval'])) {
$params['interval'] = 1;
}
- $this->recurrence = array_filter($params);
+ $this->rrule = array_filter($params);
break;
case 'EXDATE':
case 'RDATE':
$key = strtolower($prop->name);
- $dates = []; // TODO
-
- if (!empty($this->recurrence[$key])) {
- $this->recurrence[$key] = array_merge($this->recurrence[$key], $dates);
- } else {
- $this->recurrence[$key] = $dates;
- }
+ // TODO
break;
case 'ATTENDEE':
case 'ORGANIZER':
$attendee = [
'rsvp' => false,
'email' => preg_replace('!^mailto:!i', '', (string) $prop),
];
$attendeeProps = ['CN', 'PARTSTAT', 'ROLE', 'CUTYPE', 'RSVP', 'DELEGATED-FROM', 'DELEGATED-TO',
'SCHEDULE-STATUS', 'SCHEDULE-AGENT', 'SENT-BY'];
foreach ($prop->parameters() as $name => $value) {
$key = Str::camel(strtolower($name));
switch ($name) {
case 'RSVP':
- $params[$key] = strtolower($value) == 'true';
+ $attendee[$key] = strtolower($value) == 'true';
break;
case 'CN':
- $params[$key] = str_replace('\,', ',', strval($value));
+ $attendee[$key] = str_replace('\,', ',', strval($value));
break;
default:
if (in_array($name, $attendeeProps)) {
- $params[$key] = strval($value);
+ $attendee[$key] = strval($value);
}
break;
}
}
if ($prop->name == 'ORGANIZER') {
$attendee['role'] = 'ORGANIZER';
$attendee['partstat'] = 'ACCEPTED';
$this->organizer = $attendee;
} elseif (empty($this->organizer) || $attendee['email'] != $this->organizer['email']) {
$this->attendees[] = $attendee;
}
break;
default:
if (\str_starts_with($prop->name, 'X-')) {
$this->custom[$prop->name] = (string) $prop;
}
}
}
// Check DURATION property if no end date is set
/*
if (empty($this->dtend) && !empty($this->dtstart) && !empty($vobject->DURATION)) {
try {
$duration = new \DateInterval((string) $vobject->DURATION);
$end = clone $this->dtstart;
$end->add($duration);
$this->dtend = $end;
}
catch (\Exception $e) {
// TODO: Error?
}
}
*/
// Find alarms
foreach ($vobject->select('VALARM') as $valarm) {
$action = 'DISPLAY';
$trigger = null;
$alarm = [];
foreach ($valarm->children() as $prop) {
$value = strval($prop);
switch ($prop->name) {
case 'TRIGGER':
foreach ($prop->parameters as $param) {
if ($param->name == 'VALUE' && $param->getValue() == 'DATE-TIME') {
$trigger = '@' . $prop->getDateTime()->format('U');
$alarm['trigger'] = $prop->getDateTime();
} elseif ($param->name == 'RELATED') {
$alarm['related'] = $param->getValue();
}
}
/*
if (!$trigger && ($values = libcalendaring::parse_alarm_value($value))) {
$trigger = $values[2];
}
*/
if (empty($alarm['trigger'])) {
$alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $value), 'T');
// if all 0-values have been stripped, assume 'at time'
if ($alarm['trigger'] == 'P') {
$alarm['trigger'] = 'PT0S';
}
}
break;
case 'ACTION':
$action = $alarm['action'] = strtoupper($value);
break;
case 'SUMMARY':
case 'DESCRIPTION':
case 'DURATION':
$alarm[strtolower($prop->name)] = $value;
break;
case 'REPEAT':
$alarm['repeat'] = (int) $value;
break;
case 'ATTENDEE':
$alarm['attendees'][] = preg_replace('!^mailto:!i', '', $value);
break;
}
}
if ($action != 'NONE') {
if (!empty($alarm['trigger'])) {
$this->valarms[] = $alarm;
}
}
}
}
/**
* Create string representation of the DAV object (iCalendar)
*
* @return string
*/
public function __toString()
{
if (!$this->vobject) {
- //TODO we currently can only serialize a message back that we just read
+ // TODO we currently can only serialize a message back that we just read
throw new \Exception("Writing from properties is not implemented");
}
- return VObject\Writer::write($this->vobject);
+ return Writer::write($this->vobject);
}
}
diff --git a/src/app/Backends/DAV/Vtodo.php b/src/app/Backends/DAV/Vtodo.php
index 4584e0ce..9ae1bfc6 100644
--- a/src/app/Backends/DAV/Vtodo.php
+++ b/src/app/Backends/DAV/Vtodo.php
@@ -1,7 +1,53 @@
{$prop})) {
+ $key = Str::camel(strtolower($prop));
+ $this->{$key} = (string) $vobject->{$prop};
+ }
+ }
+
+ // map other properties
+ foreach ($vobject->children() as $prop) {
+ if (!($prop instanceof Property)) {
+ continue;
+ }
+
+ switch ($prop->name) {
+ case 'DUE':
+ // This is of type Sabre\VObject\Property\ICalendar\DateTime
+ $this->due = $prop;
+ break;
+
+ case 'PERCENT-COMPLETE':
+ $this->percentComplete = $prop->getValue();
+ break;
+ }
+ }
+ }
}
diff --git a/src/app/Console/Commands/Data/MigrateCommand.php b/src/app/Console/Commands/Data/MigrateCommand.php
index c64f8ab1..245bb99a 100644
--- a/src/app/Console/Commands/Data/MigrateCommand.php
+++ b/src/app/Console/Commands/Data/MigrateCommand.php
@@ -1,64 +1,63 @@
argument('src'));
$dst = new DataMigrator\Account($this->argument('dst'));
$options = [
'type' => $this->option('type'),
'force' => $this->option('force'),
'sync' => $this->option('sync'),
'stdout' => true,
];
$migrator = new DataMigrator\Engine();
$migrator->migrate($src, $dst, $options);
}
}
diff --git a/src/app/DataMigrator/Account.php b/src/app/DataMigrator/Account.php
index 530a5f23..b51141a3 100644
--- a/src/app/DataMigrator/Account.php
+++ b/src/app/DataMigrator/Account.php
@@ -1,109 +1,112 @@
:".
* For proxy authentication use: "**" as username.
*
- * @param string $input Account specification
+ * @param string $input Account specification (URI)
*/
public function __construct(string $input)
{
+ if (!preg_match('|^[a-z]+://.*|', $input)) {
+ throw new \Exception("Invalid URI specified");
+ }
+
$url = parse_url($input);
- // Not valid URI, try the other form of input
- if ($url === false || !array_key_exists('scheme', $url)) {
- list($user, $password) = explode(':', $input, 2);
- $url = ['user' => $user, 'pass' => $password];
+ // Not valid URI
+ if (!is_array($url) || empty($url)) {
+ throw new \Exception("Invalid URI specified");
}
if (isset($url['user'])) {
$this->username = urldecode($url['user']);
if (strpos($this->username, '**')) {
list($this->username, $this->loginas) = explode('**', $this->username, 2);
}
}
if (isset($url['pass'])) {
$this->password = urldecode($url['pass']);
}
if (isset($url['scheme'])) {
$this->scheme = strtolower($url['scheme']);
}
if (isset($url['port'])) {
$this->port = $url['port'];
}
if (isset($url['host'])) {
$this->host = $url['host'];
$this->uri = $this->scheme . '://' . $url['host']
. ($this->port ? ":{$this->port}" : null)
. ($url['path'] ?? '');
}
if (!empty($url['query'])) {
parse_str($url['query'], $this->params);
}
if (strpos($this->loginas, '@')) {
$this->email = $this->loginas;
} elseif (strpos($this->username, '@')) {
$this->email = $this->username;
}
$this->input = $input;
}
/**
* Returns string representation of the object.
* You can use the result as an input to the object constructor.
*
* @return string Account string representation
*/
public function __toString(): string
{
return $this->input;
}
}
diff --git a/src/app/DataMigrator/DAV.php b/src/app/DataMigrator/DAV.php
index f4e3bb39..bdfd290b 100644
--- a/src/app/DataMigrator/DAV.php
+++ b/src/app/DataMigrator/DAV.php
@@ -1,367 +1,411 @@
username . ($account->loginas ? "**{$account->loginas}" : '');
$baseUri = rtrim($account->uri, '/');
$baseUri = preg_replace('|^dav|', 'http', $baseUri);
$this->client = new DAVClient($username, $account->password, $baseUri);
$this->engine = $engine;
$this->account = $account;
}
/**
* Check user credentials.
*
* @throws \Exception
*/
public function authenticate(): void
{
try {
$this->client->options();
} catch (\Exception $e) {
throw new \Exception("Invalid DAV credentials or server.");
}
}
/**
* Create an item in a folder.
*
* @param Item $item Item to import
*
* @throws \Exception
*/
public function createItem(Item $item): void
{
- // TODO: For now we do DELETE + PUT. It's because the UID might have changed (which
- // is the case with e.g. contacts from EWS) causing a discrepancy between UID and href.
- // This is not necessarily a problem and would not happen to calendar events.
- // So, maybe we could improve that so DELETE is not needed.
if ($item->existing) {
- try {
- $this->client->delete($item->existing);
- } catch (\Illuminate\Http\Client\RequestException $e) {
- // ignore 404 response, item removed in meantime?
- if ($e->getCode() != 404) {
- throw $e;
- }
- }
+ $href = $item->existing;
+ } else {
+ $href = $this->getFolderPath($item->folder) . '/' . pathinfo($item->filename, PATHINFO_BASENAME);
}
- $href = $this->getFolderPath($item->folder) . '/' . pathinfo($item->filename, PATHINFO_BASENAME);
-
- $object = new DAVOpaque($item->filename);
+ $object = new DAVOpaque($item->filename, true);
$object->href = $href;
switch ($item->folder->type) {
case Engine::TYPE_EVENT:
case Engine::TYPE_TASK:
$object->contentType = 'text/calendar; charset=utf-8';
break;
case Engine::TYPE_CONTACT:
$object->contentType = 'text/vcard; charset=utf-8';
break;
}
if ($this->client->create($object) === false) {
throw new \Exception("Failed to save DAV object at {$href}");
}
}
/**
* Create a folder.
*
* @param Folder $folder Folder data
*
* @throws \Exception on error
*/
public function createFolder(Folder $folder): void
{
$dav_type = $this->type2DAV($folder->type);
$folders = $this->client->listFolders($dav_type);
if ($folders === false) {
throw new \Exception("Failed to list folders on the DAV server");
}
// Note: iRony flattens the list by modifying the folder name
// This is not going to work with Cyrus DAV, but anyway folder
// hierarchies support is not full in Kolab 4.
foreach ($folders as $dav_folder) {
if (str_replace(' » ', '/', $dav_folder->name) === $folder->fullname) {
// do nothing, folder already exists
return;
}
}
$home = $this->client->getHome($dav_type);
$folder_id = Utils::uuidStr();
$collection_type = $dav_type == DAVClient::TYPE_VCARD ? 'addressbook' : 'calendar';
// We create all folders on the top-level
$dav_folder = new DAVFolder();
$dav_folder->name = $folder->fullname;
$dav_folder->href = rtrim($home, '/') . '/' . $folder_id;
$dav_folder->components = [$dav_type];
$dav_folder->types = ['collection', $collection_type];
if ($this->client->folderCreate($dav_folder) === false) {
throw new \Exception("Failed to create a DAV folder {$dav_folder->href}");
}
}
/**
* Fetching an item
*/
public function fetchItem(Item $item): void
{
// Save the item content to a file
$location = $item->folder->location;
if (!file_exists($location)) {
mkdir($location, 0740, true);
}
$location .= '/' . basename($item->id);
$result = $this->client->getObjects(dirname($item->id), $this->type2DAV($item->folder->type), [$item->id]);
if ($result === false) {
throw new \Exception("Failed to fetch DAV item for {$item->id}");
}
// TODO: Do any content changes, e.g. organizer/attendee email migration
if (file_put_contents($location, (string) $result[0]) === false) {
throw new \Exception("Failed to write to {$location}");
}
$item->filename = $location;
}
/**
* Fetch a list of folder items
*/
public function fetchItemList(Folder $folder, $callback, ImporterInterface $importer): void
{
// Get existing messages' headers from the destination mailbox
$existing = $importer->getItems($folder);
- $set = new ItemSet();
-
$dav_type = $this->type2DAV($folder->type);
$location = $this->getFolderPath($folder);
- $search = new DAVSearch($dav_type);
+ $search = new DAVSearch($dav_type, false);
// TODO: We request only properties relevant to incremental migration,
// i.e. to find that something exists and its last update time.
// Some servers (iRony) do ignore that and return full VCARD/VEVENT/VTODO
// content, if there's many objects we'll have a memory limit issue.
// Also, this list should be controlled by the exporter.
- $search->dataProperties = ['UID', 'REV'];
+ $search->dataProperties = ['UID', 'REV', 'DTSTAMP'];
+
+ $set = new ItemSet();
$result = $this->client->search(
$location,
$search,
- function ($item) use (&$set, $folder, $callback) {
- // TODO: Skip an item that exists and did not change
- $exists = false;
+ function ($item) use (&$set, $dav_type, $folder, $existing, $callback) {
+ // Skip an item that exists and did not change
+ $exists = null;
+ if (!empty($existing[$item->uid])) {
+ $exists = $existing[$item->uid]['href'];
+ switch ($dav_type) {
+ case DAVClient::TYPE_VCARD:
+ if ($existing[$item->uid]['rev'] == $item->rev) {
+ return null;
+ }
+ break;
+ case DAVClient::TYPE_VEVENT:
+ case DAVClient::TYPE_VTODO:
+ if ($existing[$item->uid]['dtstamp'] == (string) $item->dtstamp) {
+ return null;
+ }
+ break;
+ }
+ }
$set->items[] = Item::fromArray([
'id' => $item->href,
'folder' => $folder,
'existing' => $exists,
]);
if (count($set->items) == self::CHUNK_SIZE) {
$callback($set);
$set = new ItemSet();
}
+
+ return null;
}
);
- if ($result === false) {
- throw new \Exception("Failed to get items from a DAV folder {$location}");
- }
-
if (count($set->items)) {
$callback($set);
}
+ if ($result === false) {
+ throw new \Exception("Failed to get items from a DAV folder {$location}");
+ }
+
// TODO: Delete items that do not exist anymore?
}
/**
- * Get a list of folder items, limited to their essential propeties
+ * Get a list of items, limited to their essential propeties
* used in incremental migration.
*
* @param Folder $folder Folder data
*
* @throws \Exception on error
*/
public function getItems(Folder $folder): array
{
$dav_type = $this->type2DAV($folder->type);
$location = $this->getFolderPath($folder);
$search = new DAVSearch($dav_type);
// TODO: We request only properties relevant to incremental migration,
// i.e. to find that something exists and its last update time.
// Some servers (iRony) do ignore that and return full VCARD/VEVENT/VTODO
// content, if there's many objects we'll have a memory limit issue.
// Also, this list should be controlled by the exporter.
- $search->dataProperties = ['UID', 'X-MS-ID', 'REV'];
+ $search->dataProperties = ['UID', 'X-MS-ID', 'REV', 'DTSTAMP'];
$items = $this->client->search(
$location,
$search,
- // @phpstan-ignore-next-line
function ($item) use ($dav_type) {
// Slim down the result to properties we might need
- $result = [
+ $object = [
'href' => $item->href,
'uid' => $item->uid,
- 'x-ms-id' => $item->custom['X-MS-ID'] ?? null,
];
- /*
+
+ if (!empty($item->custom['X-MS-ID'])) {
+ $object['x-ms-id'] = $item->custom['X-MS-ID'];
+ }
+
switch ($dav_type) {
case DAVClient::TYPE_VCARD:
- $result['rev'] = $item->rev;
+ $object['rev'] = $item->rev;
+ break;
+ case DAVClient::TYPE_VEVENT:
+ case DAVClient::TYPE_VTODO:
+ $object['dtstamp'] = (string) $item->dtstamp;
break;
}
- */
- return $result;
+ return [$item->uid, $object];
}
);
if ($items === false) {
throw new \Exception("Failed to get items from a DAV folder {$location}");
}
return $items;
}
/**
* Get folders hierarchy
*/
public function getFolders($types = []): array
{
$result = [];
foreach (['VEVENT', 'VTODO', 'VCARD'] as $component) {
$type = $this->typeFromDAV($component);
// Skip folder types we do not support (need)
if (!empty($types) && !in_array($type, $types)) {
continue;
}
- // TODO: Skip other users folders
-
$folders = $this->client->listFolders($component);
foreach ($folders as $folder) {
+ // Skip other users/shared folders
+ if ($this->shouldSkip($folder)) {
+ continue;
+ }
+
$result[$folder->href] = Folder::fromArray([
- 'fullname' => $folder->name,
+ 'fullname' => str_replace(' » ', '/', $folder->name),
'href' => $folder->href,
'type' => $type,
]);
}
}
return $result;
}
/**
* Get folder relative URI
*/
protected function getFolderPath(Folder $folder): string
{
+ $cache_key = $folder->type . '!' . $folder->fullname;
+ if (isset($this->folderPaths[$cache_key])) {
+ return $this->folderPaths[$cache_key];
+ }
+
$folders = $this->client->listFolders($this->type2DAV($folder->type));
if ($folders === false) {
throw new \Exception("Failed to list folders on the DAV server");
}
// Note: iRony flattens the list by modifying the folder name
// This is not going to work with Cyrus DAV, but anyway folder
// hierarchies support is not full in Kolab 4.
foreach ($folders as $dav_folder) {
if (str_replace(' » ', '/', $dav_folder->name) === $folder->fullname) {
- return rtrim($dav_folder->href, '/');
+ return $this->folderPaths[$cache_key] = rtrim($dav_folder->href, '/');
}
}
throw new \Exception("Folder not found: {$folder->fullname}");
}
/**
* Map Kolab type into DAV object type
*/
protected static function type2DAV(string $type): string
{
switch ($type) {
case Engine::TYPE_EVENT:
return DAVClient::TYPE_VEVENT;
case Engine::TYPE_TASK:
return DAVClient::TYPE_VTODO;
case Engine::TYPE_CONTACT:
case Engine::TYPE_GROUP:
return DAVClient::TYPE_VCARD;
default:
throw new \Exception("Cannot map type '{$type}' to DAV");
}
}
/**
* Map DAV object type into Kolab type
*/
protected static function typeFromDAV(string $type): string
{
switch ($type) {
case DAVClient::TYPE_VEVENT:
return Engine::TYPE_EVENT;
case DAVClient::TYPE_VTODO:
return Engine::TYPE_TASK;
case DAVClient::TYPE_VCARD:
// TODO what about groups
return Engine::TYPE_CONTACT;
default:
throw new \Exception("Cannot map type '{$type}' from DAV");
}
}
+
+ /**
+ * Check if the folder should not be migrated
+ */
+ private function shouldSkip($folder): bool
+ {
+ // When dealing with iRony DAV other user folders names have distinct names
+ // there's no other way to recognize them than by the name pattern.
+ // ;et's hope that users do not have personal folders with names starting with a bracket.
+
+ if (preg_match('~\(.*\) .*~', $folder->name)) {
+ return true;
+ }
+
+ if (str_starts_with($folder->name, 'shared » ')) {
+ return true;
+ }
+
+ // TODO: Cyrus DAV shared folders
+
+ return false;
+ }
}
diff --git a/src/app/DataMigrator/EWS.php b/src/app/DataMigrator/EWS.php
index e71177ba..a4a41f4b 100644
--- a/src/app/DataMigrator/EWS.php
+++ b/src/app/DataMigrator/EWS.php
@@ -1,464 +1,470 @@
Engine::TYPE_EVENT,
EWS\Contact::FOLDER_TYPE => Engine::TYPE_CONTACT,
EWS\Task::FOLDER_TYPE => Engine::TYPE_TASK,
];
/** @var Account Account to operate on */
protected $account;
/** @var Engine Data migrator engine */
protected $engine;
/**
* Object constructor
*/
public function __construct(Account $account, Engine $engine)
{
$this->account = $account;
$this->engine = $engine;
}
/**
* Server autodiscovery
*/
public static function autodiscover(string $user, string $password): ?string
{
// You should never run the Autodiscover more than once.
// It can make between 1 and 5 calls before giving up, or before finding your server,
// depending on how many different attempts it needs to make.
// TODO: Autodiscovery may fail with an exception thrown. Handle this nicely.
// TODO: Looks like this autodiscovery also does not work w/Basic Auth?
$api = API\ExchangeAutodiscover::getAPI($user, $password);
$server = $api->getClient()->getServer();
$version = $api->getClient()->getVersion();
return sprintf('ews://%s:%s@%s', urlencode($user), urlencode($password), $server);
}
/**
* Authenticate to EWS (initialize the EWS client)
*/
public function authenticate(): void
{
if (!empty($this->account->params['client_id'])) {
$this->api = $this->authenticateWithOAuth2(
$this->account->host,
$this->account->username,
$this->account->params['client_id'],
$this->account->params['client_secret'],
$this->account->params['tenant_id']
);
} else {
// Note: This initializes the client, but not yet connects to the server
// TODO: To know that the credentials work we'll have to do some API call.
$this->api = $this->authenticateWithPassword(
$this->account->host,
$this->account->username,
$this->account->password,
$this->account->loginas
);
}
}
/**
* Autodiscover the server and authenticate the user
*/
protected function authenticateWithPassword(string $server, string $user, string $password, string $loginas = null)
{
// Note: Since 2023-01-01 EWS at Office365 requires OAuth2, no way back to basic auth.
\Log::debug("[EWS] Using basic authentication on $server...");
$options = [];
if ($loginas) {
$options['impersonation'] = $loginas;
}
$this->engine->setOption('ews', [
'options' => $options,
'server' => $server,
]);
return API::withUsernameAndPassword($server, $user, $password, $this->apiOptions($options));
}
/**
* Authenticate with a token (Office365)
*/
protected function authenticateWithToken(string $server, string $user, string $token, $expires_at = null)
{
\Log::debug("[EWS] Using token authentication on $server...");
$options = ['impersonation' => $user];
$this->engine->setOption('ews', [
'options' => $options,
'server' => $server,
'token' => $token,
'expires_at' => $expires_at,
]);
return API::withCallbackToken($server, $token, $this->apiOptions($options));
}
/**
* Authenticate with OAuth2 (Office365) - get the token
*/
- protected function authenticateWithOAuth2(string $server,
- string $user, string $client_id, string $client_secret, string $tenant_id)
- {
+ protected function authenticateWithOAuth2(
+ string $server,
+ string $user,
+ string $client_id,
+ string $client_secret,
+ string $tenant_id
+ ) {
// See https://github.com/Garethp/php-ews/blob/master/examples/basic/authenticatingWithOAuth.php
// See https://github.com/Garethp/php-ews/issues/236#issuecomment-1292521527
// To register OAuth2 app goto https://entra.microsoft.com > Applications > App registrations
\Log::debug("[EWS] Fetching OAuth2 token from $server...");
$scope = 'https://outlook.office365.com/.default';
$token_uri = "https://login.microsoftonline.com/{$tenant_id}/oauth2/v2.0/token";
// $authUri = "https://login.microsoftonline.com/{$tenant_id}/oauth2/authorize";
$response = Http::asForm()
->timeout(5)
->post($token_uri, [
'client_id' => $client_id,
'client_secret' => $client_secret,
'scope' => $scope,
'grant_type' => 'client_credentials',
])
->throwUnlessStatus(200);
$token = $response->json('access_token');
// Note: Office365 default token expiration time is ~1h,
$expires_in = $response->json('expires_in');
$expires_at = now()->addSeconds($expires_in)->toDateTimeString();
return $this->authenticateWithToken($server, $user, $token, $expires_at);
}
/**
* Get folders hierarchy
*/
public function getFolders($types = []): array
{
// Get full folders hierarchy
$options = [
'Traversal' => 'Deep',
];
$folders = $this->api->getChildrenFolders('root', $options);
$result = [];
foreach ($folders as $folder) {
$class = $folder->getFolderClass();
$type = $this->type_map[$class] ?? null;
// Skip folder types we do not support (need)
if (empty($type) || (!empty($types) && !in_array($type, $types))) {
continue;
}
// Note: Folder names are localized
$name = $fullname = $folder->getDisplayName();
$id = $folder->getFolderId()->getId();
$parentId = $folder->getParentFolderId()->getId();
// Create folder name with full path
if ($parentId && !empty($result[$parentId])) {
$fullname = $result[$parentId]->fullname . '/' . $name;
}
// Top-level folder, check if it's a special folder we should ignore
// FIXME: Is there a better way to distinguish user folders from system ones?
if (
in_array($fullname, $this->folder_exceptions)
|| strpos($fullname, 'OwaFV15.1All') === 0
) {
continue;
}
$result[$id] = Folder::fromArray([
'id' => $folder->getFolderId()->toArray(true),
'total' => $folder->getTotalCount(),
'class' => $class,
'type' => $this->type_map[$class] ?? null,
'name' => $name,
'fullname' => $fullname,
]);
}
return $result;
}
/**
* Fetch a list of folder items
*/
public function fetchItemList(Folder $folder, $callback, Interface\ImporterInterface $importer): void
{
// Job processing - initialize environment
$this->initEnv($this->engine->queue);
// The folder is empty, we can stop here
if (empty($folder->total)) {
// TODO: Delete all existing items?
return;
}
// Get items already imported
// TODO: This might be slow and/or memory expensive, we should consider
// whether storing list of imported items in some cache wouldn't be a better
// solution. Of course, cache would not get changes in the destination account.
$existing = $importer->getItems($folder);
// Create X-MS-ID index for easier search in existing items
// Note: For some objects we could use UID (events), but for some we don't have UID in Exchange.
// Also because fetching extra properties here is problematic, we use X-MS-ID.
$existingIndex = [];
array_walk(
$existing,
function (&$item, $idx) use (&$existingIndex) {
if (!empty($item['x-ms-id'])) {
[$id, $changeKey] = explode('!', $item['x-ms-id']);
$item['changeKey'] = $changeKey;
$existingIndex[$id] = $idx;
unset($item['x-ms-id']);
}
}
);
$request = [
// Exchange's maximum is 1000
'IndexedPageItemView' => ['MaxEntriesReturned' => 100, 'Offset' => 0, 'BasePoint' => 'Beginning'],
'ParentFolderIds' => $folder->id,
'Traversal' => 'Shallow',
'ItemShape' => [
'BaseShape' => 'IdOnly',
'AdditionalProperties' => [
'FieldURI' => ['FieldURI' => 'item:ItemClass'],
],
],
];
$request = Type::buildFromArray($request);
// Note: It is not possible to get mimeContent with FindItem request
// That's why we first get the list of object identifiers and
// then call GetItem on each separately.
// TODO: It might be feasible to get all properties for object types
// for which we don't use MimeContent, for better performance.
// Request first page
$response = $this->api->getClient()->FindItem($request);
// @phpstan-ignore-next-line
foreach ($response as $item) {
if ($item = $this->toItem($item, $folder, $existing, $existingIndex)) {
$callback($item);
}
}
// Request other pages until we got all
while (!$response->isIncludesLastItemInRange()) {
// @phpstan-ignore-next-line
$response = $this->api->getNextPage($response);
foreach ($response as $item) {
if ($item = $this->toItem($item, $folder, $existing, $existingIndex)) {
$callback($item);
}
}
}
// TODO: Delete items that do not exist anymore?
}
/**
* Fetching an item
*/
public function fetchItem(Item $item): void
{
// Job processing - initialize environment
$this->initEnv($this->engine->queue);
if ($driver = EWS\Item::factory($this, $item)) {
$item->filename = $driver->fetchItem($item);
}
if (empty($item->filename)) {
throw new \Exception("Failed to fetch an item from EWS");
}
}
/**
* Get the source account
*/
public function getSourceAccount(): Account
{
return $this->engine->source;
}
/**
* Get the destination account
*/
public function getDestinationAccount(): Account
{
return $this->engine->destination;
}
/**
* Synchronize specified object
*/
protected function toItem(Type $item, Folder $folder, $existing, $existingIndex): ?Item
{
$id = $item->getItemId()->toArray();
- $exists = false;
+ $exists = null;
// Detect an existing item, skip if nothing changed
if (isset($existingIndex[$id['Id']])) {
$idx = $existingIndex[$id['Id']];
if ($existing[$idx]['changeKey'] == $id['ChangeKey']) {
return null;
}
$exists = $existing[$idx]['href'];
}
$item = Item::fromArray([
'id' => $id,
'class' => $item->getItemClass(),
'folder' => $folder,
'existing' => $exists,
]);
// TODO: We don't need to instantiate Item at this point, instead
// implement EWS\Item::validateClass() method
if ($driver = EWS\Item::factory($this, $item)) {
return $item;
}
return null;
}
/**
* Set common API options
*/
protected function apiOptions(array $options): array
{
if (empty($options['version'])) {
$options['version'] = API\ExchangeWebServices::VERSION_2013;
}
- // If you want to inject your own GuzzleClient for the requests
- // $options['httpClient]' = $client;
-
// In debug mode record all responses
- /*
if (\config('app.debug')) {
$options['httpPlayback'] = [
'mode' => 'record',
'recordLocation' => \storage_path('ews'),
];
}
- */
+
+ // Options for testing
+ foreach (['httpClient', 'httpPlayback'] as $opt) {
+ if (($val = $this->engine->getOption($opt)) !== null) {
+ $options[$opt] = $val;
+ }
+ }
return $options;
}
/**
* Initialize environment for job execution
*
* @param Queue $queue Queue
*/
protected function initEnv(Queue $queue): void
{
$ews = $queue->data['options']['ews'];
if (!empty($ews['token'])) {
// TODO: Refresh the token if needed
$this->api = API::withCallbackToken(
$ews['server'],
$ews['token'],
$this->apiOptions($ews['options'])
);
} else {
$this->api = API::withUsernameAndPassword(
$ews['server'],
$this->account->username,
$this->account->password,
$this->apiOptions($ews['options'])
);
}
}
}
diff --git a/src/app/DataMigrator/EWS/Appointment.php b/src/app/DataMigrator/EWS/Appointment.php
index 8816177c..e7602e91 100644
--- a/src/app/DataMigrator/EWS/Appointment.php
+++ b/src/app/DataMigrator/EWS/Appointment.php
@@ -1,100 +1,102 @@
'calendar:UID'];
return $request;
}
/**
* Process event object
*/
protected function processItem(Type $item)
{
+ // Initialize $this->itemId (for some unit tests)
+ $this->getUID($item);
+
// Decode MIME content
$ical = base64_decode((string) $item->getMimeContent());
$itemId = implode("\r\n ", str_split($this->itemId, 75 - strlen('X-MS-ID:')));
$ical = str_replace("\r\nBEGIN:VEVENT\r\n", "\r\nBEGIN:VEVENT\r\nX-MS-ID:{$itemId}\r\n", $ical);
// TODO: replace source email with destination email address in ORGANIZER/ATTENDEE
// 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);
$ctype = $_attachment->getContentType();
$body = $_attachment->getContent();
// It looks like Exchange may have an issue with plain text files.
// We'll skip empty files
if (!strlen($body)) {
continue;
}
// 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($body);
$body = rtrim(chunk_split($body, 74, "\r\n "), ' ');
// 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);
}
}
return $ical;
}
/**
* 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();
- }
+ // Only appointments have UID property
+ $this->uid = $item->getUID();
- return $this->uid;
+ // This also sets $this->itemId;
+ return parent::getUID($item);
}
}
diff --git a/src/app/DataMigrator/EWS/Contact.php b/src/app/DataMigrator/EWS/Contact.php
index 9a1b7346..2df6fe8c 100644
--- a/src/app/DataMigrator/EWS/Contact.php
+++ b/src/app/DataMigrator/EWS/Contact.php
@@ -1,51 +1,86 @@
getMimeContent());
// Remove empty properties that EWS is exporting
$vcard = preg_replace('|\n[^:]+:;*\r|', '', $vcard);
// Inject UID (and Exchange item ID) to the vCard
$uid = $this->getUID($item);
$itemId = implode("\r\n ", str_split($this->itemId, 75 - strlen('X-MS-ID:')));
+ // TODO: Use DAV\Vcard instead of string matching and replacement
+
$vcard = str_replace("BEGIN:VCARD", "BEGIN:VCARD\r\nUID:{$uid}\r\nX-MS-ID:{$itemId}", $vcard);
// Note: Looks like PHOTO property is exported properly, so we
// don't have to handle attachments as we do for calendar items
+ // TODO: Use vCard v4 for anniversary and spouse? Roundcube works with what's below
+
+ // Spouse: X-MS-SPOUSE;TYPE=N:Partner Name
+ if (preg_match('/(X-MS-SPOUSE[;:][^\r\n]+)/', $vcard, $matches)) {
+ $spouse = preg_replace('/^[^:]+:/', '', $matches[1]);
+ $vcard = str_replace($matches[1], "X-SPOUSE:{$spouse}", $vcard);
+ }
+
+ // TODO: X-MS-ANNIVERSARY;VALUE=DATE:2020-11-12
+ if (preg_match('/(X-MS-ANNIVERSARY[;:][^\r\n]+)/', $vcard, $matches)) {
+ $date = preg_replace('/^[^:]+:/', '', $matches[1]);
+ $vcard = str_replace($matches[1], "X-ANNIVERSARY:{$date}", $vcard);
+ }
+
+ // Exchange 2010 for some reason do not include email addresses in the vCard
+ if (!preg_match('/\nEMAIL[^:]*:[^\r\n]+/', $vcard) && ($emailEntries = $item->getEmailAddresses())) {
+ $emails = [];
+
+ // Note that the Entry property is either an array (multiple addresses)
+ // or an object (single address). Not a great API design.
+ if (!is_array($emailEntries->Entry)) {
+ $emailEntries->Entry = [$emailEntries->Entry];
+ }
+
+ foreach ($emailEntries->Entry as $email) {
+ $emails[] = 'EMAIL;TYPE=internet:' . strval($email);
+ }
+
+ if ($emails) {
+ $vcard = str_replace("BEGIN:VCARD\r\n", "BEGIN:VCARD\r\n" . implode("\r\n", $emails) . "\r\n", $vcard);
+ }
+ }
+
return $vcard;
}
}
diff --git a/src/app/DataMigrator/EWS/DistList.php b/src/app/DataMigrator/EWS/DistList.php
index 5cab31dd..c569d7b8 100644
--- a/src/app/DataMigrator/EWS/DistList.php
+++ b/src/app/DataMigrator/EWS/DistList.php
@@ -1,61 +1,72 @@
[$this->getUID($item)],
'KIND' => ['group'],
'FN' => [$item->getDisplayName()],
'REV' => [$item->getLastModifiedTime(), ['VALUE' => 'DATE-TIME']],
'X-MS-ID' => [$this->itemId],
];
$vcard = "BEGIN:VCARD\r\nVERSION:4.0\r\nPRODID:Kolab EWS Data Migrator\r\n";
foreach ($data as $key => $prop) {
$vcard .= $this->formatProp($key, $prop[0], isset($prop[1]) ? $prop[1] : []);
}
+ // TODO: The group description property in Exchange is not available via EWS XML,
+ // at least not at outlook.office.com (Exchange 2010). It is available in the
+ // MimeContent which is in email message format.
+
+ // TODO: mailto: members are not supported by Kolab Webclient
+ // 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
+
// 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));
- }
+ if ($members = $item->getMembers()) {
+ // The Member property is either array (multiple members) or Type\MemberType
+ // object (a group with just a one member).
+ if (!is_array($members->Member)) {
+ $members->Member = [$members->Member];
+ }
- $vcard .= $this->formatProp('MEMBER', "mailto:{$mailto}");
+ foreach ($members->Member as $member) {
+ $mailbox = $member->getMailbox();
+ $mailto = $mailbox->getEmailAddress();
+ $name = $mailbox->getName();
+
+ if ($mailto) {
+ if ($name && $name != $mailto) {
+ $mailto = urlencode(sprintf('"%s" <%s>', addcslashes($name, '"'), $mailto));
+ }
+
+ $vcard .= $this->formatProp('MEMBER', "mailto:{$mailto}");
+ }
}
}
$vcard .= "END:VCARD\r\n";
return $vcard;
}
}
diff --git a/src/app/DataMigrator/EWS/Item.php b/src/app/DataMigrator/EWS/Item.php
index a9691438..d523dcf5 100644
--- a/src/app/DataMigrator/EWS/Item.php
+++ b/src/app/DataMigrator/EWS/Item.php
@@ -1,174 +1,175 @@
driver = $driver;
$this->folder = $folder;
}
/**
* Factory method.
* Returns object suitable to handle specified item type.
*/
public static function factory(EWS $driver, ItemInterface $item)
{
$item_class = str_replace('IPM.', '', $item->class);
$item_class = "\App\DataMigrator\EWS\\{$item_class}";
if (class_exists($item_class)) {
return new $item_class($driver, $item->folder);
}
}
/**
* Fetch the specified object and put into a file
*/
public function fetchItem(ItemInterface $item)
{
$itemId = $item->id;
// Fetch the item
- $item = $this->driver->api->getItem($itemId, $this->getItemRequest());
+ $ewsItem = $this->driver->api->getItem($itemId, $this->getItemRequest());
- $this->itemId = implode('!', $itemId);
-
- $uid = $this->getUID($item);
+ $uid = $this->getUID($ewsItem);
\Log::debug("[EWS] Saving item {$uid}...");
// Apply type-specific format converters
- $content = $this->processItem($item);
+ $content = $this->processItem($ewsItem);
if (!is_string($content)) {
return;
}
$uid = preg_replace('/[^a-zA-Z0-9_:@-]/', '', $uid);
$location = $this->folder->location;
if (!file_exists($location)) {
mkdir($location, 0740, true);
}
$location .= '/' . $uid . '.' . $this->fileExtension();
file_put_contents($location, $content);
return $location;
}
/**
* Item conversion code
*/
abstract protected function processItem(Type $item);
/**
* 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->driver->api->getClient()->GetAttachment($request);
}
/**
* Get Item UID (Generate a new one if needed)
*/
protected function getUID(Type $item): string
{
+ $itemId = $item->getItemId()->toArray();
+
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?
- // It looks that ItemId on EWS consists of three parts separated with a slash,
- // maybe using the last part as UID would be a solution
- $this->uid = \App\Utils::uuidStr();
+ // Tasks, contacts, distlists do not have an UID. We have to generate one
+ // and inject it into the output file.
+ // We'll use the ItemId (excluding the ChangeKey part) as a base for the UID.
+ $this->uid = sha1($itemId['Id']);
+ // $this->uid = \App\Utils::uuidStr();
}
+ $this->itemId = implode('!', $itemId);
+
return $this->uid;
}
/**
* Filename extension for cached file in-processing
*/
protected function fileExtension(): string
{
return constant(static::class . '::FILE_EXT') ?: 'txt';
}
/**
* VCard/iCal property formatting
*/
protected function formatProp($name, $value, array $params = []): string
{
$cal = new \Sabre\VObject\Component\VCalendar();
$prop = new \Sabre\VObject\Property\Text($cal, $name, $value, $params);
$value = $prop->serialize();
// Revert escaping for some props
if ($name == 'RRULE') {
$value = str_replace("\\", '', $value);
}
return $value;
}
}
diff --git a/src/app/DataMigrator/EWS/Task.php b/src/app/DataMigrator/EWS/Task.php
index ca06b895..33b2a7a6 100644
--- a/src/app/DataMigrator/EWS/Task.php
+++ b/src/app/DataMigrator/EWS/Task.php
@@ -1,305 +1,319 @@
[$this->getUID($item)],
'DTSTAMP' => [$this->formatDate($item->getLastModifiedTime()), ['VALUE' => 'DATE-TIME']],
'CREATED' => [$this->formatDate($item->getDateTimeCreated()), ['VALUE' => 'DATE-TIME']],
'SEQUENCE' => [intval($item->getChangeCount())],
'SUMMARY' => [$item->getSubject()],
'DESCRIPTION' => [(string) $item->getBody()],
'PERCENT-COMPLETE' => [intval($item->getPercentComplete())],
- 'STATUS' => [strtoupper($item->getStatus())],
'X-MS-ID' => [$this->itemId],
];
if ($dueDate = $item->getDueDate()) {
$data['DUE'] = [$this->formatDate($dueDate), ['VALUE' => 'DATE-TIME']];
}
if ($startDate = $item->getStartDate()) {
$data['DTSTART'] = [$this->formatDate($startDate), ['VALUE' => 'DATE-TIME']];
}
+ if ($status = $item->getStatus()) {
+ $status = strtoupper($status);
+ $status_map = [
+ 'COMPLETED' => 'COMPLETED',
+ 'INPROGRESS' => 'IN-PROGRESS',
+ 'DEFERRED' => 'X-DEFERRED',
+ 'NOTSTARTED' => 'X-NOTSTARTED',
+ 'WAITINGONOTHERS' => 'X-WAITINGFOROTHERS',
+ ];
+
+ if (isset($status_map[$status])) {
+ $data['STATUS'] = [$status_map[$status]];
+ }
+ }
+
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'];
}
if ($importance = $item->getImportance()) {
$importance_map = [
'HIGH' => '9',
'NORMAL' => '5',
'LOW' => '1',
];
$data['PRIORITY'] = [$importance_map[strtoupper($importance)] ?? '0'];
}
$this->setTaskOrganizer($data, $item);
$this->setTaskRecurrence($data, $item);
$ical = "BEGIN:VCALENDAR\r\nMETHOD:PUBLISH\r\nVERSION:2.0\r\nPRODID:Kolab EWS Data Migrator\r\nBEGIN:VTODO\r\n";
foreach ($data as $key => $prop) {
$ical .= $this->formatProp($key, $prop[0], isset($prop[1]) ? $prop[1] : []);
}
// Attachments
if ($item->getHasAttachments()) {
foreach ((array) $item->getAttachments()->getFileAttachment() as $attachment) {
$_attachment = $this->getAttachment($attachment);
$ctype = $_attachment->getContentType();
$body = $_attachment->getContent();
// It looks like Exchange may have an issue with plain text files.
// We'll skip empty files
if (!strlen($body)) {
continue;
}
// 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($body);
$body = rtrim(chunk_split($body, 74, "\r\n "), ' ');
// 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}";
}
}
$ical .= $this->getVAlarm($item);
$ical .= "END:VTODO\r\n";
$ical .= "END:VCALENDAR\r\n";
return $ical;
}
/**
* Set task organizer/attendee
*/
protected function setTaskOrganizer(array &$data, Type $task)
{
// FIXME: Looks like the owner might be an email address or just a full user name
$owner = $task->getOwner();
$source = $this->driver->getSourceAccount();
$destination = $this->driver->getDestinationAccount();
if (strpos($owner, '@') && $owner != $source->email) {
// Task owned by another person
$data['ORGANIZER'] = ["mailto:{$owner}"];
// FIXME: Because attendees are not specified in EWS, assume the user is an attendee
if ($destination->email) {
$params = ['ROLE' => 'REQ-PARTICIPANT', 'CUTYPE' => 'INDIVIDUAL'];
$data['ATTENDEE'] = ["mailto:{$destination->email}", $params];
}
return;
}
// Otherwise it must be owned by the user
if ($destination->email) {
$data['ORGANIZER'] = ["mailto:{$destination->email}"];
}
}
/**
* Set task recurrence rule
*/
protected function setTaskRecurrence(array &$data, Type $task)
{
if (empty($task->getIsRecurring()) || empty($task->getRecurrence())) {
return;
}
$r = $task->getRecurrence();
$rrule = [];
if ($recurrence = $r->getDailyRecurrence()) {
$rrule['FREQ'] = 'DAILY';
$rrule['INTERVAL'] = $recurrence->getInterval() ?: 1;
} elseif ($recurrence = $r->getWeeklyRecurrence()) {
$rrule['FREQ'] = 'WEEKLY';
$rrule['INTERVAL'] = $recurrence->getInterval() ?: 1;
$rrule['BYDAY'] = $this->mapDays($recurrence->getDaysOfWeek());
$rrule['WKST'] = $this->mapDays($recurrence->getFirstDayOfWeek());
} elseif ($recurrence = $r->getAbsoluteMonthlyRecurrence()) {
$rrule['FREQ'] = 'MONTHLY';
$rrule['INTERVAL'] = $recurrence->getInterval() ?: 1;
$rrule['BYMONTHDAY'] = $recurrence->getDayOfMonth();
} elseif ($recurrence = $r->getRelativeMonthlyRecurrence()) {
$rrule['FREQ'] = 'MONTHLY';
$rrule['INTERVAL'] = $recurrence->getInterval() ?: 1;
$rrule['BYDAY'] = $this->mapDays($recurrence->getDaysOfWeek(), $recurrence->getDayOfWeekIndex());
} elseif ($recurrence = $r->getAbsoluteYearlyRecurrence()) {
$rrule['FREQ'] = 'YEARLY';
$rrule['BYMONTH'] = $this->mapMonths($recurrence->getMonth());
$rrule['BYMONTHDAY'] = $recurrence->getDayOfMonth();
} elseif ($recurrence = $r->getRelativeYearlyRecurrence()) {
$rrule['FREQ'] = 'YEARLY';
$rrule['BYMONTH'] = $this->mapMonths($recurrence->getMonth());
$rrule['BYDAY'] = $this->mapDays($recurrence->getDaysOfWeek(), $recurrence->getDayOfWeekIndex());
} else {
// There might be *Regeneration rules that we don't support
\Log::debug("[EWS] Unsupported Recurrence property value. Ignored.");
}
if (!empty($rrule)) {
if ($recurrence = $r->getNumberedRecurrence()) {
$rrule['COUNT'] = $recurrence->getNumberOfOccurrences();
} elseif ($recurrence = $r->getEndDateRecurrence()) {
$rrule['UNTIL'] = $this->formatDate($recurrence->getEndDate());
}
$rrule = array_filter($rrule);
$rrule = trim(array_reduce(
array_keys($rrule),
function ($carry, $key) use ($rrule) {
return $carry . ';' . $key . '=' . $rrule[$key];
}
), ';');
$data['RRULE'] = [$rrule];
}
}
/**
* Get VALARM block for the task Reminder
*/
protected function getVAlarm(Type $task): string
{
// FIXME: To me it looks like ReminderMinutesBeforeStart property is not used
$date = $this->formatDate($task->getReminderDueBy());
if (empty($task->getReminderIsSet()) || empty($date)) {
return '';
}
return "BEGIN:VALARM\r\nACTION:DISPLAY\r\n"
. "TRIGGER;VALUE=DATE-TIME:{$date}\r\n"
. "END:VALARM\r\n";
}
/**
* Convert EWS representation of recurrence days to iCal
*/
protected function mapDays(string $days, string $index = ''): string
{
if (preg_match('/(Day|Weekday|WeekendDay)/', $days)) {
// not supported
return '';
}
$days_map = [
'Sunday' => 'SU',
'Monday' => 'MO',
'Tuesday' => 'TU',
'Wednesday' => 'WE',
'Thursday' => 'TH',
'Friday' => 'FR',
'Saturday' => 'SA',
];
$index_map = [
'First' => 1,
'Second' => 2,
'Third' => 3,
'Fourth' => 4,
'Last' => -1,
];
$days = explode(' ', $days);
$days = array_map(
function ($day) use ($days_map, $index_map, $index) {
return ($index ? $index_map[$index] : '') . $days_map[$day];
},
$days
);
return implode(',', $days);
}
/**
* Convert EWS representation of recurrence month to iCal
*/
protected function mapMonths(string $months): string
{
$months_map = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'];
$months = explode(' ', $months);
$months = array_map(
function ($month) use ($months_map) {
return array_search($month, $months_map) + 1;
},
$months
);
return implode(',', $months);
}
/**
* Format EWS date-time into a iCalendar date-time
*/
protected function formatDate($datetime)
{
if (empty($datetime)) {
return null;
}
return str_replace(['Z', '-', ':'], '', $datetime);
}
}
diff --git a/src/app/DataMigrator/Engine.php b/src/app/DataMigrator/Engine.php
index 651d077c..c238d307 100644
--- a/src/app/DataMigrator/Engine.php
+++ b/src/app/DataMigrator/Engine.php
@@ -1,340 +1,347 @@
source = $source;
$this->destination = $destination;
$this->options = $options;
// Create a unique identifier for the migration request
- $queue_id = md5(strval($source) . strval($destination) . $options['type']);
+ $queue_id = md5(strval($source) . strval($destination) . ($options['type'] ?? ''));
// TODO: When running in 'sync' mode we shouldn't create a queue at all
// If queue exists, we'll display the progress only
if ($queue = Queue::find($queue_id)) {
// If queue contains no jobs, assume invalid
// TODO: An better API to manage (reset) queues
if (!$queue->jobs_started || !empty($options['force'])) {
$queue->delete();
} else {
while (true) {
$this->debug(sprintf("Progress [%d of %d]\n", $queue->jobs_finished, $queue->jobs_started));
if ($queue->jobs_started == $queue->jobs_finished) {
break;
}
sleep(1);
$queue->refresh();
}
return;
}
}
// Initialize the source
$this->exporter = $this->initDriver($source, ExporterInterface::class);
$this->exporter->authenticate();
// Initialize the destination
$this->importer = $this->initDriver($destination, ImporterInterface::class);
$this->importer->authenticate();
- // $this->debug("Source/destination user credentials verified.");
- $this->debug("Fetching folders hierarchy...");
-
// Create a queue
$this->createQueue($queue_id);
// We'll store output in storage/ tree
$location = storage_path('export/') . $source->email;
if (!file_exists($location)) {
mkdir($location, 0740, true);
}
- $types = preg_split('/\s*,\s*/', strtolower($this->options['type'] ?? ''));
+ $types = preg_split('/\s*,\s*/', strtolower($options['type'] ?? ''));
+
+ $this->debug("Fetching folders hierarchy...");
$folders = $this->exporter->getFolders($types);
$count = 0;
$async = empty($options['sync']);
foreach ($folders as $folder) {
$this->debug("Processing folder {$folder->fullname}...");
$folder->queueId = $queue_id;
$folder->location = $location;
if ($async) {
// Dispatch the job (for async execution)
Jobs\FolderJob::dispatch($folder);
$count++;
} else {
$this->processFolder($folder);
}
}
if ($count) {
$this->queue->bumpJobsStarted($count);
}
if ($async) {
$this->debug(sprintf('Done. %d %s created in queue: %s.', $count, Str::plural('job', $count), $queue_id));
} else {
$this->debug(sprintf('Done (queue: %s).', $queue_id));
}
}
/**
* Processing of a folder synchronization
*/
public function processFolder(Folder $folder): void
{
// Job processing - initialize environment
if (!$this->queue) {
$this->envFromQueue($folder->queueId);
}
// Create the folder on the destination server
$this->importer->createFolder($folder);
$count = 0;
$async = empty($this->options['sync']);
// Fetch items from the source
$this->exporter->fetchItemList(
$folder,
function ($item_or_set) use (&$count, $async) {
if ($async) {
// Dispatch the job (for async execution)
if ($item_or_set instanceof ItemSet) {
Jobs\ItemSetJob::dispatch($item_or_set);
} else {
Jobs\ItemJob::dispatch($item_or_set);
}
$count++;
} else {
if ($item_or_set instanceof ItemSet) {
$this->processItemSet($item_or_set);
} else {
$this->processItem($item_or_set);
}
}
},
$this->importer
);
if ($count) {
$this->queue->bumpJobsStarted($count);
}
if ($async) {
$this->queue->bumpJobsFinished();
}
}
/**
* Processing of item synchronization
*/
public function processItem(Item $item): void
{
// Job processing - initialize environment
if (!$this->queue) {
$this->envFromQueue($item->folder->queueId);
}
$this->exporter->fetchItem($item);
$this->importer->createItem($item);
if (!empty($item->filename)) {
unlink($item->filename);
}
if (empty($this->options['sync'])) {
$this->queue->bumpJobsFinished();
}
}
/**
* Processing of item-set synchronization
*/
public function processItemSet(ItemSet $set): void
{
// Job processing - initialize environment
if (!$this->queue) {
$this->envFromQueue($set->items[0]->folder->queueId);
}
// TODO: Some exporters, e.g. DAV, might optimize fetching multiple items in one go,
// we'll need a new API to do that
foreach ($set->items as $item) {
$this->exporter->fetchItem($item);
$this->importer->createItem($item);
if (!empty($item->filename)) {
unlink($item->filename);
}
}
// TODO: We should probably also track number of items migrated
if (empty($this->options['sync'])) {
$this->queue->bumpJobsFinished();
}
}
/**
* Print progress/debug information
*/
public function debug($line)
{
if (!empty($this->options['stdout'])) {
$output = new \Symfony\Component\Console\Output\ConsoleOutput();
$output->writeln("$line");
} else {
\Log::debug("[DataMigrator] $line");
}
}
+ /**
+ * Get migration option value.
+ */
+ public function getOption(string $name)
+ {
+ return $this->options[$name] ?? null;
+ }
+
/**
* Set migration queue option. Use this if you need to pass
* some data between queue processes.
*/
public function setOption(string $name, $value): void
{
$this->options[$name] = $value;
if ($this->queue) {
$this->queue->data = $this->queueData();
$this->queue->save();
}
}
/**
* Create a queue for the request
*
* @param string $queue_id Unique queue identifier
*/
protected function createQueue(string $queue_id): void
{
$this->queue = new Queue();
$this->queue->id = $queue_id;
$this->queue->data = $this->queueData();
$this->queue->save();
}
/**
* Prepare queue data
*/
protected function queueData()
{
$options = $this->options;
unset($options['stdout']); // jobs aren't in stdout anymore
// TODO: data should be encrypted
return [
'source' => (string) $this->source,
'destination' => (string) $this->destination,
'options' => $options,
];
}
/**
* Initialize environment for job execution
*
* @param string $queueId Queue identifier
*/
protected function envFromQueue(string $queueId): void
{
$this->queue = Queue::findOrFail($queueId);
$this->source = new Account($this->queue->data['source']);
$this->destination = new Account($this->queue->data['destination']);
$this->options = $this->queue->data['options'];
$this->importer = $this->initDriver($this->destination, ImporterInterface::class);
$this->exporter = $this->initDriver($this->source, ExporterInterface::class);
}
/**
* Initialize (and select) migration driver
*/
protected function initDriver(Account $account, string $interface)
{
switch ($account->scheme) {
case 'ews':
$driver = new EWS($account, $this);
break;
case 'dav':
case 'davs':
$driver = new DAV($account, $this);
break;
case 'imap':
case 'imaps':
case 'tls':
case 'ssl':
$driver = new IMAP($account, $this);
break;
default:
throw new \Exception("Failed to init driver for '{$account->scheme}'");
}
// Make sure driver is used in the direction it supports
if (!is_a($driver, $interface)) {
throw new \Exception(sprintf(
"'%s' driver does not implement %s",
class_basename($driver),
class_basename($interface)
));
}
return $driver;
}
}
diff --git a/src/app/DataMigrator/IMAP.php b/src/app/DataMigrator/IMAP.php
index 0623cb61..14d105b2 100644
--- a/src/app/DataMigrator/IMAP.php
+++ b/src/app/DataMigrator/IMAP.php
@@ -1,377 +1,437 @@
account = $account;
$this->engine = $engine;
// TODO: Move this to self::authenticate()?
$config = self::getConfig($account->username, $account->password, $account->uri);
$this->imap = self::initIMAP($config);
}
/**
* Authenticate
*/
public function authenticate(): void
{
}
/**
* Create a folder.
*
* @param Folder $folder Folder data
*
* @throws \Exception on error
*/
public function createFolder(Folder $folder): void
{
if ($folder->type != 'mail') {
throw new \Exception("IMAP does not support folder of type {$folder->type}");
}
if ($folder->fullname == 'INBOX') {
// INBOX always exists
return;
}
if (!$this->imap->createFolder($folder->fullname)) {
\Log::warning("Failed to create the folder: {$this->imap->error}");
if (str_contains($this->imap->error, "Mailbox already exists")) {
// Not an error
} else {
throw new \Exception("Failed to create an IMAP folder {$folder->fullname}");
}
}
+
+ // TODO: Migrate folder subscription state
}
/**
* Create an item in a folder.
*
* @param Item $item Item to import
*
* @throws \Exception
*/
public function createItem(Item $item): void
{
$mailbox = $item->folder->fullname;
- // TODO: When updating an email we have to just update flags
-
if ($item->filename) {
$result = $this->imap->appendFromFile(
- $mailbox, $item->filename, null, $item->data['flags'], $item->data['internaldate'], true
+ $mailbox,
+ $item->filename,
+ null,
+ $item->data['flags'],
+ $item->data['internaldate'],
+ true
);
if ($result === false) {
throw new \Exception("Failed to append IMAP message into {$mailbox}");
}
}
+
+ // When updating an existing email message we have to...
+ if ($item->existing) {
+ if (!empty($result)) {
+ // Remove the old one
+ $this->imap->flag($mailbox, $item->existing['uid'], 'DELETED');
+ $this->imap->expunge($mailbox, $item->existing['uid']);
+ } else {
+ // Update flags
+ foreach ($item->existing['flags'] as $flag) {
+ if (!in_array($flag, $item->data['flags'])) {
+ $this->imap->unflag($mailbox, $item->existing['uid'], $flag);
+ }
+ }
+ foreach ($item->data['flags'] as $flag) {
+ if (!in_array($flag, $item->existing['flags'])) {
+ $this->imap->flag($mailbox, $item->existing['uid'], $flag);
+ }
+ }
+ }
+ }
}
/**
* Fetching an item
*/
public function fetchItem(Item $item): void
{
[$uid, $messageId] = explode(':', $item->id, 2);
$mailbox = $item->folder->fullname;
// Get message flags
$header = $this->imap->fetchHeader($mailbox, (int) $uid, true, false, ['FLAGS']);
if ($header === false) {
throw new \Exception("Failed to get IMAP message headers for {$mailbox}/{$uid}");
}
// Remove flags that we can't append (e.g. RECENT)
$flags = $this->filterImapFlags(array_keys($header->flags));
- // TODO: If message already exists in the destination account we should update flags
+ // If message already exists in the destination account we should update only flags
// and be done with it. On the other hand for Drafts it's not unusual to get completely
// different body for the same Message-ID. Same can happen not only in Drafts, I suppose.
+ // So, we compare size and INTERNALDATE timestamp.
+ if (
+ !$item->existing
+ || $header->timestamp != $item->existing['timestamp']
+ || $header->size != $item->existing['size']
+ ) {
+ // Save the message content to a file
+ $location = $item->folder->location;
+
+ if (!file_exists($location)) {
+ mkdir($location, 0740, true);
+ }
- // Save the message content to a file
- $location = $item->folder->location;
+ // TODO: What if parent folder not yet exists?
+ $location .= '/' . $uid . '.eml';
- if (!file_exists($location)) {
- mkdir($location, 0740, true);
- }
+ // TODO: We should consider streaming the message, it should be possible
+ // with append() and handlePartBody(), but I don't know if anyone tried that.
- // TODO: What if parent folder not yet exists?
- $location .= '/' . $uid . '.eml';
+ $fp = fopen($location, 'w');
- // TODO: We should consider streaming the message, it should be possible
- // with append() and handlePartBody(), but I don't know if anyone tried that.
+ if (!$fp) {
+ throw new \Exception("Failed to write to {$location}");
+ }
- $fp = fopen($location, 'w');
+ $result = $this->imap->handlePartBody($mailbox, $uid, true, '', null, null, $fp);
- if (!$fp) {
- throw new \Exception("Failed to write to {$location}");
- }
+ if ($result === false) {
+ fclose($fp);
+ throw new \Exception("Failed to fetch IMAP message for {$mailbox}/{$uid}");
+ }
- $result = $this->imap->handlePartBody($mailbox, $uid, true, '', null, null, $fp);
+ $item->filename = $location;
- if ($result === false) {
fclose($fp);
- throw new \Exception("Failed to fetch IMAP message for {$mailbox}/{$uid}");
}
- $item->filename = $location;
$item->data = [
'flags' => $flags,
'internaldate' => $header->internaldate,
];
-
- fclose($fp);
}
/**
* Fetch a list of folder items
*/
public function fetchItemList(Folder $folder, $callback, ImporterInterface $importer): void
{
// Get existing messages' headers from the destination mailbox
$existing = $importer->getItems($folder);
$mailbox = $folder->fullname;
// TODO: We should probably first use SEARCH/SORT to skip messages marked as \Deleted
+ // It would also allow us to get headers in chunks 200 messages at a time, or so.
// TODO: fetchHeaders() fetches too many headers, we should slim-down, here we need
// only UID FLAGS INTERNALDATE BODY.PEEK[HEADER.FIELDS (DATE FROM MESSAGE-ID)]
$messages = $this->imap->fetchHeaders($mailbox, '1:*', true, false, ['Message-Id']);
if ($messages === false) {
throw new \Exception("Failed to get all IMAP message headers for {$mailbox}");
}
if (empty($messages)) {
\Log::debug("Nothing to migrate for {$mailbox}");
return;
}
$set = new ItemSet();
foreach ($messages as $message) {
- // TODO: If Message-Id header does not exist create it based on internaldate/From/Date
+ // If Message-Id header does not exist create it based on internaldate/From/Date
+ $id = $this->getMessageId($message, $mailbox);
// Skip message that exists and did not change
- $exists = false;
- if (isset($existing[$message->messageID])) {
- // TODO: Compare flags (compare message size, internaldate?)
- continue;
+ $exists = null;
+ if (isset($existing[$id])) {
+ $flags = $this->filterImapFlags(array_keys($message->flags));
+ if (
+ $flags == $existing[$id]['flags']
+ && $message->timestamp == $existing[$id]['timestamp']
+ && $message->size == $existing[$id]['size']
+ ) {
+ continue;
+ }
+
+ $exists = $existing[$id];
}
$set->items[] = Item::fromArray([
- 'id' => $message->uid . ':' . $message->messageID,
+ 'id' => $message->uid . ':' . $id,
'folder' => $folder,
'existing' => $exists,
]);
if (count($set->items) == self::CHUNK_SIZE) {
$callback($set);
$set = new ItemSet();
}
}
if (count($set->items)) {
$callback($set);
}
// TODO: Delete messages that do not exist anymore?
}
/**
* Get folders hierarchy
*/
public function getFolders($types = []): array
{
$folders = $this->imap->listMailboxes('', '');
if ($folders === false) {
throw new \Exception("Failed to get list of IMAP folders");
}
+ // TODO: Migrate folder subscription state
+
$result = [];
foreach ($folders as $folder) {
if ($this->shouldSkip($folder)) {
\Log::debug("Skipping folder {$folder}.");
continue;
}
$result[] = Folder::fromArray([
'fullname' => $folder,
'type' => 'mail'
]);
}
return $result;
}
/**
* Get a list of folder items, limited to their essential propeties
* used in incremental migration to skip unchanged items.
*/
public function getItems(Folder $folder): array
{
$mailbox = $folder->fullname;
// TODO: We should probably first use SEARCH/SORT to skip messages marked as \Deleted
// TODO: fetchHeaders() fetches too many headers, we should slim-down, here we need
// only UID FLAGS INTERNALDATE BODY.PEEK[HEADER.FIELDS (DATE FROM MESSAGE-ID)]
$messages = $this->imap->fetchHeaders($mailbox, '1:*', true, false, ['Message-Id']);
if ($messages === false) {
throw new \Exception("Failed to get IMAP message headers in {$mailbox}");
}
$result = [];
foreach ($messages as $message) {
// Remove flags that we can't append (e.g. RECENT)
$flags = $this->filterImapFlags(array_keys($message->flags));
- // TODO: Generate message ID if the header does not exist
- $result[$message->messageID] = [
+ // Generate message ID if the header does not exist
+ $id = $this->getMessageId($message, $mailbox);
+
+ $result[$id] = [
'uid' => $message->uid,
'flags' => $flags,
+ 'size' => $message->size,
+ 'timestamp' => $message->timestamp,
];
}
return $result;
}
/**
* Initialize IMAP connection and authenticate the user
*/
private static function initIMAP(array $config, string $login_as = null): \rcube_imap_generic
{
$imap = new \rcube_imap_generic();
if (\config('app.debug')) {
$imap->setDebug(true, 'App\Backends\IMAP::logDebug');
}
if ($login_as) {
$config['options']['auth_cid'] = $config['user'];
$config['options']['auth_pw'] = $config['password'];
$config['options']['auth_type'] = 'PLAIN';
$config['user'] = $login_as;
}
$imap->connect($config['host'], $config['user'], $config['password'], $config['options']);
if (!$imap->connected()) {
$message = sprintf("Login failed for %s against %s. %s", $config['user'], $config['host'], $imap->error);
\Log::error($message);
throw new \Exception("Connection to IMAP failed");
}
return $imap;
}
/**
* Get IMAP configuration
*/
private static function getConfig($user, $password, $uri): array
{
$uri = \parse_url($uri);
$default_port = 143;
$ssl_mode = null;
if (isset($uri['scheme'])) {
if (preg_match('/^(ssl|imaps)/', $uri['scheme'])) {
$default_port = 993;
$ssl_mode = 'ssl';
} elseif ($uri['scheme'] === 'tls') {
$ssl_mode = 'tls';
}
}
$config = [
'host' => $uri['host'],
'user' => $user,
'password' => $password,
'options' => [
'port' => !empty($uri['port']) ? $uri['port'] : $default_port,
'ssl_mode' => $ssl_mode,
'socket_options' => [
'ssl' => [
// TODO: These configuration options make sense for "local" Kolab IMAP,
// but when connecting to external one we might want to just disable
// cert validation, or make it optional via Account URI parameters
'verify_peer' => \config('imap.verify_peer'),
'verify_peer_name' => \config('imap.verify_peer'),
'verify_host' => \config('imap.verify_host')
],
],
],
];
return $config;
}
/**
* Limit IMAP flags to these that can be migrated
*/
private function filterImapFlags($flags)
{
// TODO: Support custom flags migration
return array_filter(
$flags,
function ($flag) {
- return in_array($flag, $this->imap->flags);
+ return isset($this->imap->flags[$flag]);
}
);
}
/**
* Check if the folder should not be migrated
*/
private function shouldSkip($folder): bool
{
// TODO: This should probably use NAMESPACE information
- // TODO: This should also skip other user folders
- if (preg_match("/Shared Folders\/.*/", $folder)) {
+ if (preg_match('~(Shared Folders|Other Users)/.*~', $folder)) {
return true;
}
return false;
}
+
+ /**
+ * Return Message-Id, generate unique identifier if Message-Id does not exist
+ */
+ private function getMessageId($message, $folder): string
+ {
+ if (!empty($message->messageID)) {
+ return $message->messageID;
+ }
+
+ return md5($folder . $message->from . ($message->date ?: $message->timestamp));
+ }
}
diff --git a/src/app/DataMigrator/Interface/Item.php b/src/app/DataMigrator/Interface/Item.php
index d242eb76..061c494a 100644
--- a/src/app/DataMigrator/Interface/Item.php
+++ b/src/app/DataMigrator/Interface/Item.php
@@ -1,39 +1,44 @@
$value) {
$obj->{$key} = $value;
}
return $obj;
}
}
diff --git a/src/bootstrap/app.php b/src/bootstrap/app.php
index a70ba683..0cbb38c5 100644
--- a/src/bootstrap/app.php
+++ b/src/bootstrap/app.php
@@ -1,83 +1,83 @@
singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;
diff --git a/src/tests/BackendsTrait.php b/src/tests/BackendsTrait.php
new file mode 100644
index 00000000..277a8209
--- /dev/null
+++ b/src/tests/BackendsTrait.php
@@ -0,0 +1,395 @@
+ DAV::TYPE_VEVENT,
+ Engine::TYPE_TASK => DAV::TYPE_VTODO,
+ Engine::TYPE_CONTACT => DAV::TYPE_VCARD,
+ Engine::TYPE_GROUP => DAV::TYPE_VCARD,
+ ];
+
+
+ /**
+ * Append an DAV object to a DAV folder
+ */
+ protected function davAppend(Account $account, $foldername, $filenames, $type): void
+ {
+ $dav = $this->getDavClient($account);
+
+ $folder = $this->davFindFolder($account, $foldername, $type);
+
+ if (empty($folder)) {
+ throw new \Exception("Failed to find folder {$account}/{$foldername}");
+ }
+
+ foreach ((array) $filenames as $filename) {
+ $path = __DIR__ . '/data/' . $filename;
+
+ if (!file_exists($path)) {
+ throw new \Exception("File does not exist: {$path}");
+ }
+
+ $content = file_get_contents($path);
+ $uid = preg_match('/\nUID:(?:urn:uuid:)?([a-z0-9-]+)/', $content, $m) ? $m[1] : null;
+
+ if (empty($uid)) {
+ throw new \Exception("Filed to find UID in {$path}");
+ }
+
+ $location = rtrim($folder->href, '/') . '/' . $uid . '.' . pathinfo($filename, \PATHINFO_EXTENSION);
+
+ $content = new DAV\Opaque($content);
+ $content->href = $location;
+ $content->contentType = $type == Engine::TYPE_CONTACT
+ ? 'text/vcard; charset=utf-8'
+ : 'text/calendar; charset=utf-8';
+
+ if ($dav->create($content) === false) {
+ throw new \Exception("Failed to append object into {$account}/{$location}");
+ }
+ }
+ }
+
+ /**
+ * Delete a DAV folder
+ */
+ protected function davCreateFolder(Account $account, $foldername, $type): void
+ {
+ if ($this->davFindFolder($account, $foldername, $type)) {
+ return;
+ }
+
+ $dav = $this->getDavClient($account);
+
+ $dav_type = $this->davTypes[$type];
+ $home = $dav->getHome($dav_type);
+ $folder_id = Utils::uuidStr();
+ $collection_type = $dav_type == DAV::TYPE_VCARD ? 'addressbook' : 'calendar';
+
+ // We create all folders on the top-level
+ $folder = new DAV\Folder();
+ $folder->name = $foldername;
+ $folder->href = rtrim($home, '/') . '/' . $folder_id;
+ $folder->components = [$dav_type];
+ $folder->types = ['collection', $collection_type];
+
+ if ($dav->folderCreate($folder) === false) {
+ throw new \Exception("Failed to create folder {$account}/{$folder->href}");
+ }
+ }
+
+ /**
+ * Create a DAV folder
+ */
+ protected function davDeleteFolder(Account $account, $foldername, $type): void
+ {
+ $folder = $this->davFindFolder($account, $foldername, $type);
+
+ if (empty($folder)) {
+ return;
+ }
+
+ $dav = $this->getDavClient($account);
+
+ if ($dav->folderDelete($folder->href) === false) {
+ throw new \Exception("Failed to delete folder {$account}/{$foldername}");
+ }
+ }
+
+ /**
+ * Remove all objects from a DAV folder
+ */
+ protected function davEmptyFolder(Account $account, $foldername, $type): void
+ {
+ $dav = $this->getDavClient($account);
+
+ foreach ($this->davList($account, $foldername, $type) as $object) {
+ if ($dav->delete($object->href) === false) {
+ throw new \Exception("Failed to delete {$account}/{object->href}");
+ }
+ }
+ }
+
+ /**
+ * Find a DAV folder
+ */
+ protected function davFindFolder(Account $account, $foldername, $type)
+ {
+ $dav = $this->getDavClient($account);
+
+ $list = $dav->listFolders($this->davTypes[$type]);
+
+ if ($list === false) {
+ throw new \Exception("Failed to list '{$type}' folders on {$account}");
+ }
+
+ foreach ($list as $folder) {
+ if (str_replace(' » ', '/', $folder->name) === $foldername) {
+ return $folder;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * List objects in a DAV folder
+ */
+ protected function davList(Account $account, $foldername, $type): array
+ {
+ $folder = $this->davFindFolder($account, $foldername, $type);
+
+ if (empty($folder)) {
+ throw new \Exception("Failed to find folder {$account}/{$foldername}");
+ }
+
+ $dav = $this->getDavClient($account);
+
+ $search = new DAV\Search($this->davTypes[$type], true);
+
+ $searchResult = $dav->search($folder->href, $search);
+
+ if ($searchResult === false) {
+ throw new \Exception("Failed to get items from a DAV folder {$account}/{$folder->href}");
+ }
+
+ $result = [];
+ foreach ($searchResult as $item) {
+ $result[] = $item;
+ }
+
+ return $result;
+ }
+
+ /**
+ * List DAV folders
+ */
+ protected function davListFolders(Account $account, $type): array
+ {
+ $dav = $this->getDavClient($account);
+
+ $list = $dav->listFolders($this->davTypes[$type]);
+
+ if ($list === false) {
+ throw new \Exception("Failed to list '{$type}' folders on {$account}");
+ }
+
+ $result = [];
+
+ foreach ($list as $folder) {
+ // skip shared folders (iRony)
+ if (str_starts_with($folder->name, 'shared » ') || $folder->name[0] == '(') {
+ continue;
+ }
+
+ $result[$folder->href] = str_replace(' » ', '/', $folder->name);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get configured/initialized DAV client
+ */
+ protected function getDavClient(Account $account): DAV
+ {
+ $clientId = (string) $account;
+
+ if (empty($this->clients[$clientId])) {
+ $uri = preg_replace('/^dav/', 'http', $account->uri);
+ $this->clients[$clientId] = new DAV($account->username, $account->password, $uri);
+ }
+
+ return $this->clients[$clientId];
+ }
+
+ /**
+ * Get configured/initialized IMAP client
+ */
+ protected function getImapClient(Account $account): \rcube_imap_generic
+ {
+ $clientId = (string) $account;
+
+ if (empty($this->clients[$clientId])) {
+ $class = new \ReflectionClass(IMAP::class);
+
+ $initIMAP = $class->getMethod('initIMAP');
+ $getConfig = $class->getMethod('getConfig');
+ $initIMAP->setAccessible(true);
+ $getConfig->setAccessible(true);
+
+ $config = [
+ 'user' => $account->username,
+ 'password' => $account->password,
+ ];
+
+ $config = array_merge($getConfig->invoke(null), $config);
+
+ $this->clients[$clientId] = $initIMAP->invokeArgs(null, [$config]);
+ }
+
+ return $this->clients[$clientId];
+ }
+
+ /**
+ * Initialize an account
+ */
+ protected function initAccount(Account $account): void
+ {
+ // Remove all objects from all (personal) folders
+ if ($account->scheme == 'dav' || $account->scheme == 'davs') {
+ foreach (['event', 'task', 'contact'] as $type) {
+ foreach ($this->davListFolders($account, $type) as $folder) {
+ $this->davEmptyFolder($account, $folder, $type);
+ }
+ }
+ } else {
+ // TODO: Delete all folders except the default ones?
+ foreach ($this->imapListFolders($account) as $folder) {
+ $this->imapEmptyFolder($account, $folder);
+ }
+ }
+ }
+
+ /**
+ * Append an email message to the IMAP folder
+ */
+ protected function imapAppend(Account $account, $folder, $filename, $flags = [], $date = null): string
+ {
+ $imap = $this->getImapClient($account);
+
+ $source = __DIR__ . '/data/' . $filename;
+
+ if (!file_exists($source)) {
+ throw new \Exception("File does not exist: {$source}");
+ }
+
+ $uid = $imap->appendFromFile($folder, $source, '', $flags, $date, true);
+
+ if ($uid === false) {
+ throw new \Exception("Failed to append mail into {$account}/{$folder}");
+ }
+
+ return $uid;
+ }
+
+ /**
+ * Create an IMAP folder
+ */
+ protected function imapCreateFolder(Account $account, $folder): void
+ {
+ $imap = $this->getImapClient($account);
+
+ if (!$imap->createFolder($folder)) {
+ if (str_contains($imap->error, "Mailbox already exists")) {
+ // Not an error
+ } else {
+ throw new \Exception("Failed to create an IMAP folder {$account}/{$folder}");
+ }
+ }
+ }
+
+ /**
+ * Delete an IMAP folder
+ */
+ protected function imapDeleteFolder(Account $account, $folder): void
+ {
+ $imap = $this->getImapClient($account);
+
+ if (!$imap->deleteFolder($folder)) {
+ if (str_contains($imap->error, "Mailbox does not exist")) {
+ // Ignore
+ } else {
+ throw new \Exception("Failed to delete an IMAP folder {$account}/{$folder}");
+ }
+ }
+
+ $imap->unsubscribe($folder);
+ }
+
+ /**
+ * Remove all objects from a folder
+ */
+ protected function imapEmptyFolder(Account $account, $folder): void
+ {
+ $imap = $this->getImapClient($account);
+
+ $deleted = $imap->flag($folder, '1:*', 'DELETED');
+
+ if (!$deleted) {
+ throw new \Exception("Failed to empty an IMAP folder {$account}/{$folder}");
+ }
+
+ // send expunge command in order to have the deleted message really deleted from the folder
+ $imap->expunge($folder, '1:*');
+ }
+
+ /**
+ * List emails over IMAP
+ */
+ protected function imapList(Account $account, $folder): array
+ {
+ $imap = $this->getImapClient($account);
+
+ $messages = $imap->fetchHeaders($folder, '1:*', true, false, ['Message-Id']);
+
+ if ($messages === false) {
+ throw new \Exception("Failed to get all IMAP message headers for {$account}/{$folder}");
+ }
+
+ return $messages;
+ }
+
+ /**
+ * List IMAP folders
+ */
+ protected function imapListFolders(Account $account): array
+ {
+ $imap = $this->getImapClient($account);
+
+ $folders = $imap->listMailboxes('', '');
+
+ if ($folders === false) {
+ throw new \Exception("Failed to list IMAP folders for {$account}");
+ }
+
+ $folders = array_filter(
+ $folders,
+ function ($folder) {
+ return !preg_match('~(Shared Folders|Other Users)/.*~', $folder);
+ }
+ );
+
+ return $folders;
+ }
+
+ /**
+ * Mark an email message as read over IMAP
+ */
+ protected function imapFlagAs(Account $account, $folder, $uids, $flags): void
+ {
+ $imap = $this->getImapClient($account);
+
+ foreach ($flags as $flag) {
+ if (strpos($flag, 'UN') === 0) {
+ $flagged = $imap->unflag($folder, $uids, substr($flag, 2));
+ } else {
+ $flagged = $imap->flag($folder, $uids, $flag);
+ }
+
+ if (!$flagged) {
+ throw new \Exception("Failed to flag an IMAP messages as SEEN in {$account}/{$folder}");
+ }
+ }
+ }
+}
diff --git a/src/tests/Feature/DataMigrator/DAVTest.php b/src/tests/Feature/DataMigrator/DAVTest.php
new file mode 100644
index 00000000..a174251c
--- /dev/null
+++ b/src/tests/Feature/DataMigrator/DAVTest.php
@@ -0,0 +1,119 @@
+initAccount($src);
+ $this->initAccount($dst);
+
+ // Add some items to the source account
+ $this->davAppend($src, 'Calendar', ['event/1.ics', 'event/2.ics'], Engine::TYPE_EVENT);
+ $this->davCreateFolder($src, 'DavDataMigrator', Engine::TYPE_CONTACT);
+ $this->davCreateFolder($src, 'DavDataMigrator/Test', Engine::TYPE_CONTACT);
+ $this->davAppend($src, 'DavDataMigrator/Test', ['contact/1.vcf', 'contact/2.vcf'], Engine::TYPE_CONTACT);
+
+ // Clean up the destination folders structure
+ $this->davDeleteFolder($dst, 'DavDataMigrator', Engine::TYPE_CONTACT);
+ $this->davDeleteFolder($dst, 'DavDataMigrator/Test', Engine::TYPE_CONTACT);
+
+ // Run the migration
+ $migrator = new Engine();
+ $migrator->migrate($src, $dst, ['force' => true, 'sync' => true, 'type' => 'event,contact']);
+
+ // Assert the destination account
+ $dstFolders = $this->davListFolders($dst, Engine::TYPE_CONTACT);
+ $this->assertContains('DavDataMigrator', $dstFolders);
+ $this->assertContains('DavDataMigrator/Test', $dstFolders);
+
+ // Assert the migrated events
+ $dstObjects = $this->davList($dst, 'Calendar', Engine::TYPE_EVENT);
+ $events = \collect($dstObjects)->keyBy('uid')->all();
+ $this->assertCount(2, $events);
+ $this->assertSame('Party', $events['abcdef']->summary);
+ $this->assertSame('Meeting', $events['123456']->summary);
+
+ // Assert the migrated contacts and contact folders
+ $dstObjects = $this->davList($dst, 'DavDataMigrator/Test', Engine::TYPE_CONTACT);
+ $contacts = \collect($dstObjects)->keyBy('uid')->all();
+ $this->assertCount(2, $contacts);
+ $this->assertSame('Jane Doe', $contacts['uid1']->fn);
+ $this->assertSame('Jack Strong', $contacts['uid2']->fn);
+ }
+
+ /**
+ * Test DAV to DAV incremental migration run
+ *
+ * @group dav
+ * @depends testInitialMigration
+ */
+ public function testIncrementalMigration(): void
+ {
+ $uri = \config('services.dav.uri');
+
+ $src = new Account(preg_replace('|^[a-z]+://|', 'dav://john%40kolab.org:simple123@', $uri));
+ $dst = new Account(preg_replace('|^[a-z]+://|', 'dav://jack%40kolab.org:simple123@', $uri));
+
+ // Add an event and modify another one
+ $srcEvents = $this->davList($src, 'Calendar', Engine::TYPE_EVENT);
+ $this->davAppend($src, 'Calendar', ['event/3.ics', 'event/1.1.ics'], Engine::TYPE_EVENT);
+
+ // Run the migration
+ $migrator = new Engine();
+ $migrator->migrate($src, $dst, ['force' => true, 'sync' => true, 'type' => Engine::TYPE_EVENT]);
+
+ // Assert the migrated events
+ $dstObjects = $this->davList($dst, 'Calendar', Engine::TYPE_EVENT);
+ $events = \collect($dstObjects)->keyBy('uid')->all();
+ $this->assertCount(3, $events);
+ $this->assertSame('Party Update', $events['abcdef']->summary);
+ $this->assertSame('Meeting', $events['123456']->summary);
+ $this->assertSame('Test Summary', $events['aaa-aaa']->summary);
+
+ // TODO: Assert that unmodified objects aren't migrated again
+ }
+}
diff --git a/src/tests/Feature/DataMigrator/EWSTest.php b/src/tests/Feature/DataMigrator/EWSTest.php
new file mode 100644
index 00000000..b6173f97
--- /dev/null
+++ b/src/tests/Feature/DataMigrator/EWSTest.php
@@ -0,0 +1,208 @@
+davEmptyFolder($dst, 'Calendar', Engine::TYPE_EVENT);
+ $this->davEmptyFolder($dst, 'Tasks', Engine::TYPE_TASK);
+ $this->davDeleteFolder($dst, 'Kontakty', Engine::TYPE_CONTACT);
+
+ $options = [
+ 'force' => true,
+ 'sync' => true,
+ 'type' => 'event,contact,task',
+ // Mocking, use HTTP responses from the playback file
+ 'httpPlayback' => [
+ 'mode' => 'playback',
+ 'recordLocation' => $this->buildPlaybackFile('initial'),
+ ],
+ ];
+
+ // Run the migration
+ $migrator = new Engine();
+ $migrator->migrate($src, $dst, $options);
+
+ // Assert the migrated events
+ $dstObjects = $this->davList($dst, 'Calendar', Engine::TYPE_EVENT);
+ $events = \collect($dstObjects)->keyBy('uid')->all();
+ $this->assertCount(2, $events);
+ $this->assertSame('test to ms', $events[self::EVENT1]->summary);
+ $this->assertSame('test ms3', $events[self::EVENT2]->summary);
+
+ // Assert the migrated tasks
+ // Note: Tasks do not have UID in Exchange so it's generated
+ $tasks = $this->davList($dst, 'Tasks', Engine::TYPE_TASK);
+ $this->assertCount(1, $tasks);
+ $this->assertSame('Nowe zadanie', $tasks[0]->summary);
+
+ // Assert the migrated contacts and contact folders
+ // Note: Contacts do not have UID in Exchange so it's generated
+ $dstObjects = $this->davList($dst, 'Kontakty', Engine::TYPE_CONTACT);
+ $contacts = \collect($dstObjects)->keyBy('fn')->all();
+
+ $this->assertCount(3, $contacts);
+ $this->assertSame(null, $contacts['Nowy Kontakt']->kind);
+ $this->assertSame(null, $contacts['Test Surname']->kind);
+ $this->assertSame('group', $contacts['nowa lista']->kind);
+ }
+
+ /**
+ * Test EWS to DAV incremental migration run
+ *
+ * @group ews dav
+ * @depends testInitialMigration
+ */
+ public function testIncrementalMigration(): void
+ {
+ $uri = \config('services.dav.uri');
+
+ // TODO: Test OAuth2 authentication
+
+ $src = new Account('ews://user%40outlook.com:pass@office.outlook.com');
+ $dst = new Account(preg_replace('|^[a-z]+://|', 'dav://jack%40kolab.org:simple123@', $uri));
+
+ $options = [
+ 'force' => true,
+ 'sync' => true,
+ 'type' => 'event,contact',
+ // Mocking, use HTTP responses from the playback file
+ 'httpPlayback' => [
+ 'mode' => 'playback',
+ 'recordLocation' => $this->buildPlaybackFile('incremental'),
+ ],
+ ];
+
+ // We added a new contact and modified another one
+ // Run the migration
+ $migrator = new Engine();
+ $migrator->migrate($src, $dst, $options);
+
+ $dstObjects = $this->davList($dst, 'Calendar', Engine::TYPE_EVENT);
+ $events = \collect($dstObjects)->keyBy('uid')->all();
+ $this->assertCount(2, $events);
+ $this->assertSame('test to ms', $events[self::EVENT1]->summary);
+ $this->assertSame('test ms3', $events[self::EVENT2]->summary);
+
+ // Assert the migrated contacts
+ // Note: Contacts do not have UID in Exchange so it's generated
+ $dstObjects = $this->davList($dst, 'Kontakty', Engine::TYPE_CONTACT);
+ $contacts = \collect($dstObjects)->keyBy('fn')->all();
+
+ $this->assertCount(4, $contacts);
+ $this->assertSame(null, $contacts['Nowy Kontakt']->kind);
+ $this->assertSame(null, $contacts['Test Surname 1']->kind);
+ $this->assertSame(null, $contacts['Test New']->kind);
+ $this->assertSame('group', $contacts['nowa lista']->kind);
+
+ // TODO: Assert that unmodified objects aren't migrated again,
+ // although our httpPlayback makes that it would fail otherwise.
+
+ // TODO: Test migrating a task/event with attachments
+ }
+
+ /**
+ * Test OAuth2 use
+ *
+ * @group ews dav
+ * @depends testIncrementalMigration
+ */
+ public function testOAuth2(): void
+ {
+ // TODO: Test OAuth2 authentication with HTTP client fake
+
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Build a playback file for EWS client to mock communication with Exchange.
+ */
+ private function buildPlaybackFile($type)
+ {
+ /* The .json and .xml files were produced with the follwoing script
+ having saveState.json produced by httpPlayback in 'record' mode.
+ Separated and formatted for better understanding and modification ability.
+
+ $file = file_get_contents('saveState.json');
+
+ $data = json_decode($file, true);
+
+ foreach ($data as $idx => $record) {
+ file_put_contents("a{$idx}.xml", $record['body']);
+ unset($record['body']);
+ file_put_contents("$idx.json", json_encode($record, JSON_PRETTY_PRINT));
+ exec("xmllint --format a{$idx}.xml >> $idx.xml");
+ unlink("a{$idx}.xml");
+ }
+ */
+
+ $dir = __DIR__ . '/../../data/ews/' . $type;
+ $playback = [];
+
+ foreach (glob("{$dir}/[0-9].json") as $file) {
+ $id = basename($file, '.json');
+
+ $response = json_decode(file_get_contents($file), true);
+ $response['body'] = file_get_contents(str_replace('.json', '.xml', $file));
+
+ $playback[(int) $id] = $response;
+ }
+
+ ksort($playback);
+
+ file_put_contents($dir . '/' . 'saveState.json', json_encode($playback));
+
+ return $dir;
+ }
+}
diff --git a/src/tests/Feature/DataMigrator/EngineTest.php b/src/tests/Feature/DataMigrator/EngineTest.php
new file mode 100644
index 00000000..1b8440a2
--- /dev/null
+++ b/src/tests/Feature/DataMigrator/EngineTest.php
@@ -0,0 +1,47 @@
+markTestIncomplete();
+ }
+
+ /**
+ * Test synchronous migration
+ */
+ public function testSyncMigration(): void
+ {
+ $this->markTestIncomplete();
+ }
+}
diff --git a/src/tests/Feature/DataMigrator/IMAPTest.php b/src/tests/Feature/DataMigrator/IMAPTest.php
new file mode 100644
index 00000000..b0f67820
--- /dev/null
+++ b/src/tests/Feature/DataMigrator/IMAPTest.php
@@ -0,0 +1,143 @@
+initAccount($src);
+ $this->initAccount($dst);
+
+ // Add some mail to the source account
+ $this->imapAppend($src, 'INBOX', 'mail/1.eml');
+ $this->imapAppend($src, 'INBOX', 'mail/2.eml', ['SEEN']);
+ $this->imapCreateFolder($src, 'ImapDataMigrator');
+ $this->imapCreateFolder($src, 'ImapDataMigrator/Test');
+ $this->imapAppend($src, 'ImapDataMigrator/Test', 'mail/1.eml');
+ $this->imapAppend($src, 'ImapDataMigrator/Test', 'mail/2.eml');
+
+ // Clean up the destination folders structure
+ $this->imapDeleteFolder($dst, 'ImapDataMigrator');
+ $this->imapDeleteFolder($dst, 'ImapDataMigrator/Test');
+
+ // Run the migration
+ $migrator = new Engine();
+ $migrator->migrate($src, $dst, ['force' => true, 'sync' => true]);
+
+ // Assert the destination mailbox
+ $dstFolders = $this->imapListFolders($dst);
+ $this->assertContains('ImapDataMigrator', $dstFolders);
+ $this->assertContains('ImapDataMigrator/Test', $dstFolders);
+
+ // Assert the migrated messages
+ $dstMessages = $this->imapList($dst, 'INBOX');
+ $this->assertCount(2, $dstMessages);
+ $msg = array_shift($dstMessages);
+ $this->assertSame('', $msg->messageID);
+ $this->assertSame([], $msg->flags);
+ $msg = array_shift($dstMessages);
+ $this->assertSame('', $msg->messageID);
+ $this->assertSame(['SEEN'], array_keys($msg->flags));
+
+ $dstMessages = $this->imapList($dst, 'ImapDataMigrator/Test');
+ $this->assertCount(2, $dstMessages);
+ $msg = array_shift($dstMessages);
+ $this->assertSame('', $msg->messageID);
+ $this->assertSame([], $msg->flags);
+ $msg = array_shift($dstMessages);
+ $this->assertSame('', $msg->messageID);
+ $this->assertSame([], $msg->flags);
+
+ // TODO: Test INTERNALDATE migration
+ }
+
+ /**
+ * Test IMAP to IMAP incremental migration run
+ *
+ * @group imap
+ * @depends testInitialMigration
+ */
+ public function testIncrementalMigration(): void
+ {
+ $uri = \config('imap.uri');
+
+ $src = new Account(str_replace('://', '://john%40kolab.org:simple123@', $uri));
+ $dst = new Account(str_replace('://', '://jack%40kolab.org:simple123@', $uri));
+
+ // Add some mails to the source account
+ $srcMessages = $this->imapList($src, 'INBOX');
+ $msg1 = array_shift($srcMessages);
+ $msg2 = array_shift($srcMessages);
+ $this->imapAppend($src, 'INBOX', 'mail/3.eml');
+ $this->imapAppend($src, 'INBOX', 'mail/4.eml');
+ $this->imapFlagAs($src, 'INBOX', $msg1->uid, ['SEEN']);
+ $this->imapFlagAs($src, 'INBOX', $msg2->uid, ['UNSEEN', 'FLAGGED']);
+
+ // Run the migration
+ $migrator = new Engine();
+ $migrator->migrate($src, $dst, ['force' => true, 'sync' => true]);
+
+ // In INBOX two new messages and two old ones with changed flags
+ // The order of messages tells us that there was no redundant APPEND+DELETE
+ $dstMessages = $this->imapList($dst, 'INBOX');
+ $this->assertCount(4, $dstMessages);
+ $msg = array_shift($dstMessages);
+ $this->assertSame('', $msg->messageID);
+ $this->assertSame(['SEEN'], array_keys($msg->flags));
+ $msg = array_shift($dstMessages);
+ $this->assertSame('', $msg->messageID);
+ $this->assertSame(['FLAGGED'], array_keys($msg->flags));
+ $ids = array_map(fn ($msg) => $msg->messageID, $dstMessages);
+ $this->assertSame(['',''], $ids);
+
+ // Nothing changed in the other folder
+ $dstMessages = $this->imapList($dst, 'ImapDataMigrator/Test');
+ $this->assertCount(2, $dstMessages);
+ $msg = array_shift($dstMessages);
+ $this->assertSame('', $msg->messageID);
+ $this->assertSame([], $msg->flags);
+ $msg = array_shift($dstMessages);
+ $this->assertSame('', $msg->messageID);
+ $this->assertSame([], $msg->flags);
+ }
+}
diff --git a/src/tests/Unit/DataMigrator/AccountTest.php b/src/tests/Unit/DataMigrator/AccountTest.php
new file mode 100644
index 00000000..827527da
--- /dev/null
+++ b/src/tests/Unit/DataMigrator/AccountTest.php
@@ -0,0 +1,33 @@
+assertSame($uri, (string) $account);
+ $this->assertSame('user', $account->username);
+ $this->assertSame('pass@word', $account->password);
+ $this->assertSame('host.tld', $account->host);
+ $this->assertSame('imap', $account->scheme);
+ $this->assertSame(143, $account->port);
+ $this->assertSame('imap://host.tld:143', $account->uri);
+ $this->assertSame(['client_id' => '123', 'client_secret' => '456'], $account->params);
+ $this->assertNull($account->email);
+ $this->assertNull($account->loginas);
+
+ // Invalid input
+ $this->expectException(\Exception::class);
+ $account = new Account(str_replace('imap://', '', $uri));
+ }
+}
diff --git a/src/tests/Unit/DataMigrator/EWS/AppointmentTest.php b/src/tests/Unit/DataMigrator/EWS/AppointmentTest.php
new file mode 100644
index 00000000..4ee95f26
--- /dev/null
+++ b/src/tests/Unit/DataMigrator/EWS/AppointmentTest.php
@@ -0,0 +1,112 @@
+ 'test']);
+ $appointment = new EWS\Appointment($ews, $folder);
+
+ $ical = file_get_contents(__DIR__ . '/../../../data/ews/event/1.ics');
+
+ // FIXME: I haven't found a way to convert xml content into a Type instance
+ // therefore we create it "manually", but it would be better to have both
+ // vcard and xml in a single data file that we could just get content from.
+
+ $item = Type::buildFromArray([
+ 'MimeContent' => base64_encode($ical),
+ 'ItemId' => new Type\ItemIdType(
+ 'AAMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NGQ0OABGAAAAAAC9tlDYSlG2TaxWBr'
+ . 'A1OzWtBwAs2ajhknXlRYN/pbC8JqblAAAAAAEOAAAs2ajhknXlRYN/pbC8JqblAAJnrWkBAAA=',
+ 'EQAAABYAAAAs2ajhknXlRYN/pbC8JqblAAJnqlKm',
+ ),
+ 'UID' => '1F3C13D7E99642A75ABE23D50487B454-8FE68B2E68E1B348',
+ 'Subject' => 'test subject',
+ 'HasAttachments' => false,
+ 'IsAssociated' => false,
+ 'Start' => '2023-11-21T11:00:00Z',
+ 'End' => '2023-11-21T11:30:00Z',
+ 'LegacyFreeBusyStatus' => 'Tentative',
+ 'CalendarItemType' => 'Single',
+ 'Organizer' => [
+ 'Mailbox' => [
+ 'Name' => 'Aleksander Machniak',
+ 'EmailAddress' => 'test@kolab.org',
+ 'RoutingType' => 'SMTP',
+ 'MailboxType' => 'Contact',
+ ],
+ ],
+ 'RequiredAttendees' => (object) [
+ 'Attendee' => [
+ Type\AttendeeType::buildFromArray([
+ 'Mailbox' => [
+ 'Name' => 'Aleksander Machniak',
+ 'EmailAddress' => 'test@kolab.org',
+ 'RoutingType' => 'SMTP',
+ 'MailboxType' => 'Contact',
+ ],
+ 'ResponseType' => 'Unknown',
+ ]),
+ Type\AttendeeType::buildFromArray([
+ 'Mailbox' => [
+ 'Name' => 'Alec Machniak',
+ 'EmailAddress' => 'test@outlook.com',
+ 'RoutingType' => 'SMTP',
+ 'MailboxType' => 'Mailbox',
+ ],
+ 'ResponseType' => 'Unknown',
+ ]),
+ ],
+ ],
+ ]);
+
+ // Convert the Exchange item into iCalendar
+ $ical = $this->invokeMethod($appointment, 'processItem', [$item]);
+
+ // Parse the iCalendar output
+ $event = new Vevent();
+ $this->invokeMethod($event, 'fromIcal', [$ical]);
+
+ $msId = implode('!', $item->getItemId()->toArray());
+ $this->assertSame($msId, $event->custom['X-MS-ID']);
+ $this->assertSame($item->getUID(), $event->uid);
+ $this->assertSame('test description', $event->description);
+ $this->assertSame('test subject', $event->summary);
+ $this->assertSame('CONFIRMED', $event->status);
+ $this->assertSame('PUBLIC', $event->class);
+ $this->assertSame('Microsoft Exchange Server 2010', $event->prodid);
+ $this->assertSame('2023-11-20T14:50:05+00:00', $event->dtstamp->getDateTime()->format('c'));
+ $this->assertSame('2023-11-21T12:00:00+01:00', $event->dtstart->getDateTime()->format('c'));
+ $this->assertSame('2023-11-21T12:30:00+01:00', $event->dtend->getDateTime()->format('c'));
+
+ // Organizer/attendees
+ $this->assertSame('test@kolab.org', $event->organizer['email']);
+ $this->assertSame('Aleksander Machniak', $event->organizer['cn']);
+ $this->assertSame('ORGANIZER', $event->organizer['role']);
+ $this->assertSame('ACCEPTED', $event->organizer['partstat']);
+ $this->assertSame(false, $event->organizer['rsvp']);
+
+ $this->assertCount(1, $event->attendees);
+ $this->assertSame('alec@outlook.com', $event->attendees[0]['email']);
+ $this->assertSame('Alec Machniak', $event->attendees[0]['cn']);
+ $this->assertSame('REQ-PARTICIPANT', $event->attendees[0]['role']);
+ $this->assertSame('NEEDS-ACTION', $event->attendees[0]['partstat']);
+ $this->assertSame(true, $event->attendees[0]['rsvp']);
+ }
+}
diff --git a/src/tests/Unit/DataMigrator/EWS/ContactTest.php b/src/tests/Unit/DataMigrator/EWS/ContactTest.php
new file mode 100644
index 00000000..2aaa9a01
--- /dev/null
+++ b/src/tests/Unit/DataMigrator/EWS/ContactTest.php
@@ -0,0 +1,140 @@
+ 'test']);
+ $contact = new EWS\Contact($ews, $folder);
+
+ $vcard = file_get_contents(__DIR__ . '/../../../data/ews/contact/1.vcf');
+
+ // FIXME: I haven't found a way to convert xml content into a Type instance
+ // therefore we create it "manually", but it would be better to have both
+ // vcard and xml in a single data file that we could just get content from.
+
+ $item = Type::buildFromArray([
+ 'MimeContent' => base64_encode($vcard),
+ 'ItemId' => new Type\ItemIdType(
+ 'AAMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NGQ0OABGAAAAAAC9tlDYSlG2TaxWBr'
+ . 'A1OzWtBwAs2ajhknXlRYN/pbC8JqblAAAAAAEOAAAs2ajhknXlRYN/pbC8JqblAAJnrWkBAAA=',
+ 'EQAAABYAAAAs2ajhknXlRYN/pbC8JqblAAJnqlKm',
+ ),
+ 'HasAttachments' => false,
+ 'LastModifiedTime' => '2024-07-15T11:17:39,701Z',
+ 'DisplayName' => 'Nowy Nazwisko',
+ 'GivenName' => 'Nowy',
+ 'Surname' => 'Nazwisko',
+ 'EmailAddresses' => (object) [
+ 'Entry' => [
+ Type\EmailAddressDictionaryEntryType::buildFromArray([
+ 'Key' => 'EmailAddress1',
+ 'Name' => 'test1@outlook.com',
+ 'RoutingType' => 'SMTP',
+ 'MailboxType' => 'Contact',
+ '_value' => 'christian1@outlook.com',
+ ]),
+ Type\EmailAddressDictionaryEntryType::buildFromArray([
+ 'Key' => 'EmailAddress2',
+ 'Name' => 'test2@outlook.com',
+ 'RoutingType' => 'SMTP',
+ 'MailboxType' => 'Contact',
+ '_value' => 'test2@outlook.com',
+ ]),
+ ],
+ ],
+ /*
+
+
+
+ ContactPicture.jpg
+ image/jpeg
+ 2081
+ 2024-07-15T11:17:38
+ false
+ true
+
+
+
+ category
+
+ en-US
+
+
+ Mr
+ Nowy
+ Bartosz
+ Nazwisko
+ Jr.
+ Nowy Nazwisko
+ alec
+
+ Company
+
+
+ Testowa
+ Warsaw
+ mazowickie
+ Poland
+ 00-001
+
+
+
+
+ home123456
+ 1234556679200
+
+
+
+ 2014-10-11T11:59:00Z
+ IT
+ Developer
+ Office Location
+ 2020-11-12T11:59:00Z
+ true
+ */
+ ]);
+
+ // Convert the Exchange item into vCard
+ $vcard = $this->invokeMethod($contact, 'processItem', [$item]);
+
+ // Parse the vCard
+ $contact = new Vcard();
+ $this->invokeMethod($contact, 'fromVcard', [$vcard]);
+
+ $this->assertMatchesRegularExpression('/^[a-f0-9]{40}$/', $contact->uid);
+ $this->assertSame('PUBLIC', $contact->class);
+ $this->assertSame('Nowy Nazwisko', $contact->fn);
+ $this->assertSame(null, $contact->kind);
+ $this->assertSame('Microsoft Exchange', $contact->prodid);
+ $this->assertSame('2024-07-15T11:17:39,701Z', $contact->rev);
+ $this->assertSame('Notatki do kontaktu', $contact->note);
+
+ // EWS Properties with special handling
+ $msId = implode('!', $item->getItemId()->toArray());
+ $this->assertSame($msId, $contact->custom['X-MS-ID']);
+ $this->assertSame('Partner Name', $contact->custom['X-SPOUSE']);
+ $this->assertSame('2020-11-12', $contact->custom['X-ANNIVERSARY']);
+ $this->assertCount(2, $contact->email);
+ $this->assertSame('internet', $contact->email[0]['type']);
+ $this->assertSame('christian1@outlook.com', $contact->email[0]['email']);
+ $this->assertSame('internet', $contact->email[1]['type']);
+ $this->assertSame('test2@outlook.com', $contact->email[1]['email']);
+ }
+}
diff --git a/src/tests/Unit/DataMigrator/EWS/DistListTest.php b/src/tests/Unit/DataMigrator/EWS/DistListTest.php
new file mode 100644
index 00000000..77366f11
--- /dev/null
+++ b/src/tests/Unit/DataMigrator/EWS/DistListTest.php
@@ -0,0 +1,89 @@
+ 'test']);
+ $distlist = new EWS\DistList($ews, $folder);
+
+ // FIXME: I haven't found a way to convert xml content into a Type instance
+ // therefore we create it "manually", but it would be better to have both
+ // vcard and xml in a single data file that we could just get content from.
+
+ $item = Type::buildFromArray([
+ 'ItemId' => new Type\ItemIdType(
+ 'AAMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NGQ0OABGAAAAAAC9tlDYSlG2TaxWBr'
+ . 'A1OzWtBwAs2ajhknXlRYN/pbC8JqblAAAAAAEOAAAs2ajhknXlRYN/pbC8JqblAAJnrWkBAAA=',
+ 'EQAAABYAAAAs2ajhknXlRYN/pbC8JqblAAJnqlKm',
+ ),
+ 'Subject' => 'subject list',
+ 'LastModifiedTime' => '2024-06-27T13:44:32Z',
+ 'DisplayName' => 'Lista',
+ 'FileAs' => 'lista',
+ 'Members' => (object) [
+ 'Member' => [
+ Type\MemberType::buildFromArray([
+ 'Key' => 'AAAAAIErH6S+oxAZnW4A3QEPVAIAAAGAYQBsAGUAYwBAAGEAbABlAGMALgBw'
+ . 'AGwAAABTAE0AVABQAAAAYQBsAGUAYwBAAGEAbABlAGMALgBwAGwAAAA=',
+ 'Mailbox' => [
+ 'Name' => 'Alec',
+ 'EmailAddress' => 'alec@kolab.org',
+ 'RoutingType' => 'SMTP',
+ 'MailboxType' => 'OneOff',
+ ],
+ 'Status' => 'Normal',
+ ]),
+ Type\MemberType::buildFromArray([
+ 'Key' => 'AAAAAIErH6S+oxAZnW4A3QEPVAIAAAGAYQBsAGUAYwBAAGEAbABlAGMALgBw'
+ . 'AGwAAABTAE0AVABQAAAAYQBsAGUAYwBAAGEAbABlAGMALgBwAGwAAAB=',
+ 'Mailbox' => [
+ 'Name' => 'Christian',
+ 'EmailAddress' => 'christian@kolab.org',
+ 'RoutingType' => 'SMTP',
+ 'MailboxType' => 'OneOff',
+ ],
+ 'Status' => 'Normal',
+ ]),
+ ],
+ ],
+ ]);
+
+ // Convert the Exchange item into vCard
+ $vcard = $this->invokeMethod($distlist, 'processItem', [$item]);
+
+ // Parse the vCard
+ $distlist = new Vcard();
+ $this->invokeMethod($distlist, 'fromVcard', [$vcard]);
+
+ $msId = implode('!', $item->getItemId()->toArray());
+ $this->assertSame(['X-MS-ID' => $msId], $distlist->custom);
+ $this->assertMatchesRegularExpression('/^[a-f0-9]{40}$/', $distlist->uid);
+ $this->assertSame('group', $distlist->kind);
+ $this->assertSame('Lista', $distlist->fn);
+ $this->assertSame('Kolab EWS Data Migrator', $distlist->prodid);
+ $this->assertSame('2024-06-27T13:44:32Z', $distlist->rev);
+
+ $members = [
+ 'mailto:%22Alec%22+%3Calec%40kolab.org%3E',
+ 'mailto:%22Christian%22+%3Cchristian%40kolab.org%3E',
+ ];
+ $this->assertSame($members, $distlist->members);
+ }
+}
diff --git a/src/tests/Unit/DataMigrator/EWS/TaskTest.php b/src/tests/Unit/DataMigrator/EWS/TaskTest.php
new file mode 100644
index 00000000..e7f118a7
--- /dev/null
+++ b/src/tests/Unit/DataMigrator/EWS/TaskTest.php
@@ -0,0 +1,158 @@
+source = $source;
+ $engine->destination = $destination;
+ $ews = new EWS($source, $engine);
+ $folder = Folder::fromArray(['id' => 'test']);
+ $task = new EWS\Task($ews, $folder);
+
+ // FIXME: I haven't found a way to convert xml content into a Type instance
+ // therefore we create it "manually", but it would be better to have it in XML.
+
+ $html = ''
+ . 'task notes
';
+
+ $item = Type\TaskType::buildFromArray([
+ 'ItemId' => new Type\ItemIdType(
+ 'AAMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NGQ0OABGAAAAAAC9tlDYSlG2TaxWBr'
+ . 'A1OzWtBwAs2ajhknXlRYN/pbC8JqblAAAAAAEOAAAs2ajhknXlRYN/pbC8JqblAAJnrWkBAAA=',
+ 'EQAAABYAAAAs2ajhknXlRYN/pbC8JqblAAJnqlKm',
+ ),
+ 'ItemClass' => 'IPM.Task',
+ 'Subject' => 'Nowe zadanie',
+ 'LastModifiedTime' => '2024-06-27T13:44:32Z',
+ 'Sensitivity' => 'Private',
+ // TODO: Looks like EWS has Body->IsTruncated property, but is it relevant?
+ 'Body' => new Type\BodyType($html, 'HTML'),
+ 'Importance' => 'High',
+ 'DateTimeCreated' => '2024-06-27T08:58:05Z',
+ 'ReminderDueBy' => '2024-07-17T07:00:00Z',
+ 'ReminderIsSet' => true,
+ 'ReminderNextTime' => '2024-07-17T07:00:00Z',
+ 'ReminderMinutesBeforeStart' => '0',
+ 'DueDate' => '2024-06-26T22:00:00Z',
+ 'IsComplete' => false,
+ 'IsRecurring' => true,
+ 'Owner' => 'Alec Machniak',
+ 'PercentComplete' => '10',
+ 'Recurrence' => Type\TaskRecurrenceType::buildFromArray([
+ 'WeeklyRecurrence' => [
+ 'Interval' => '1',
+ 'DaysOfWeek' => 'Thursday',
+ 'FirstDayOfWeek' => 'Sunday',
+ ],
+ 'NoEndRecurrence' => [
+ 'StartDate' => '2024-06-27Z',
+ ],
+ ]),
+ 'Status' => 'NotStarted',
+ 'ChangeCount' => '2',
+ /*
+
+
+
+ testdisk.log
+ application/octet-stream
+ 299368b3-06e4-42df-959e-d428046f55e6
+ 249
+ 2024-07-16T12:13:58
+ false
+ false
+
+
+ 2024-06-27T08:58:05Z
+ 3041
+
+ Kategoria Niebieski
+
+ false
+ false
+ false
+ false
+ false
+ 2024-06-27T08:58:05Z
+
+ true
+ en-US
+
+ false
+ false
+ false
+ true
+ true
+ true
+ true
+
+ Alec Machniak
+ 2024-07-16T12:14:38Z
+ false
+
+
+ NotFlagged
+
+ AQEAAAAAAAESAQAAAmjiC08AAAAA
+
+ 1
+ Not Started
+ */
+ ]);
+
+ // Convert the Exchange item into iCalendar
+ $ical = $this->invokeMethod($task, 'processItem', [$item]);
+
+ // Parse the iCalendar output
+ $task = new Vtodo();
+ $this->invokeMethod($task, 'fromIcal', [$ical]);
+
+ $msId = implode('!', $item->getItemId()->toArray());
+ $this->assertSame($msId, $task->custom['X-MS-ID']);
+ $this->assertMatchesRegularExpression('/^[a-f0-9]{40}$/', $task->uid);
+ $this->assertSame('Nowe zadanie', $task->summary);
+ $this->assertSame($html, $task->description);
+ $this->assertSame('Kolab EWS Data Migrator', $task->prodid);
+ $this->assertSame('2', $task->sequence);
+ $this->assertSame('9', $task->priority);
+ $this->assertSame('PRIVATE', $task->class);
+ $this->assertSame(10, $task->percentComplete);
+ $this->assertSame('X-NOTSTARTED', $task->status);
+ $this->assertSame('2024-06-27T13:44:32+00:00', $task->dtstamp->getDateTime()->format('c'));
+ $this->assertSame('2024-06-27T08:58:05+00:00', $task->created->getDateTime()->format('c'));
+ $this->assertSame('2024-06-26T22:00:00+00:00', $task->due->getDateTime()->format('c'));
+ $this->assertSame('test@kolab.org', $task->organizer['email']);
+
+ $this->assertSame('WEEKLY', $task->rrule['freq']);
+ $this->assertSame('1', $task->rrule['interval']);
+ $this->assertSame('TH', $task->rrule['byday']);
+ $this->assertSame('SU', $task->rrule['wkst']);
+
+ // TODO: Reminder
+ }
+
+ /**
+ * Test processing Recurrence property
+ */
+ public function testProcessItemRecurrence(): void
+ {
+ $this->markTestIncomplete();
+ }
+}
diff --git a/src/tests/data/contact/1.vcf b/src/tests/data/contact/1.vcf
new file mode 100644
index 00000000..e0469c11
--- /dev/null
+++ b/src/tests/data/contact/1.vcf
@@ -0,0 +1,6 @@
+BEGIN:VCARD
+VERSION:3.0
+UID:uid1
+FN:Jane Doe
+N:Doe;Jane;J.;;
+END:VCARD
diff --git a/src/tests/data/contact/2.vcf b/src/tests/data/contact/2.vcf
new file mode 100644
index 00000000..c67a2f80
--- /dev/null
+++ b/src/tests/data/contact/2.vcf
@@ -0,0 +1,6 @@
+BEGIN:VCARD
+VERSION:3.0
+UID:uid2
+FN:Jack Strong
+N:Strong;Jack;;;
+END:VCARD
diff --git a/src/tests/data/event/1.1.ics b/src/tests/data/event/1.1.ics
new file mode 100644
index 00000000..0f6fc72b
--- /dev/null
+++ b/src/tests/data/event/1.1.ics
@@ -0,0 +1,12 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//test/test//NONSGML v1.0//EN
+BEGIN:VEVENT
+UID:abcdef
+DTSTAMP:19970714T180000Z
+ORGANIZER;CN=John Doe:MAILTO:john@kolab.org
+DTSTART:20240714T170000Z
+DTEND:20240714T180000Z
+SUMMARY:Party Update
+END:VEVENT
+END:VCALENDAR
diff --git a/src/tests/data/event/1.ics b/src/tests/data/event/1.ics
new file mode 100644
index 00000000..5b814375
--- /dev/null
+++ b/src/tests/data/event/1.ics
@@ -0,0 +1,12 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//test/test//NONSGML v1.0//EN
+BEGIN:VEVENT
+UID:abcdef
+DTSTAMP:19970714T170000Z
+ORGANIZER;CN=John Doe:MAILTO:john@kolab.org
+DTSTART:20240714T170000Z
+DTEND:20240714T180000Z
+SUMMARY:Party
+END:VEVENT
+END:VCALENDAR
diff --git a/src/tests/data/event/2.ics b/src/tests/data/event/2.ics
new file mode 100644
index 00000000..1914dbb9
--- /dev/null
+++ b/src/tests/data/event/2.ics
@@ -0,0 +1,12 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//test/test//NONSGML v1.0//EN
+BEGIN:VEVENT
+UID:123456
+DTSTAMP:19970714T170000Z
+ORGANIZER;CN=John Doe:MAILTO:john@kolab.org
+DTSTART:20240715T170000Z
+DTEND:20240715T180000Z
+SUMMARY:Meeting
+END:VEVENT
+END:VCALENDAR
diff --git a/src/tests/data/event/3.ics b/src/tests/data/event/3.ics
new file mode 100644
index 00000000..24959b2c
--- /dev/null
+++ b/src/tests/data/event/3.ics
@@ -0,0 +1,12 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//test/test//NONSGML v1.0//EN
+BEGIN:VEVENT
+UID:aaa-aaa
+DTSTAMP:19970714T170000Z
+ORGANIZER;CN=John Doe:MAILTO:john@kolab.org
+DTSTART:20240715T170000Z
+DTEND:20240715T180000Z
+SUMMARY:Test Summary
+END:VEVENT
+END:VCALENDAR
diff --git a/src/tests/data/ews/contact/1.vcf b/src/tests/data/ews/contact/1.vcf
new file mode 100644
index 00000000..c749a006
--- /dev/null
+++ b/src/tests/data/ews/contact/1.vcf
@@ -0,0 +1,59 @@
+BEGIN:VCARD
+PROFILE:VCARD
+VERSION:3.0
+MAILER:Microsoft Exchange
+PRODID:Microsoft Exchange
+FN:Nowy Nazwisko
+N:Nazwisko;Nowy;Bartosz;Mr;Jr.
+PHOTO;TYPE=JPEG;ENCODING=B:/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgID
+ AwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2w
+ BDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ
+ EBAQEBAQEBAQEBD/wAARCAA6ADoDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAw
+ QFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0Kx
+ wRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3
+ R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW
+ 19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQ
+ oL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAV
+ YnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eH
+ l6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna
+ 4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD87K+4PlwoAKACgAoAKACgAoAKACgD2D
+ 9mj9l/4iftQ+M5PC/gpIbSx09Fm1bV7oH7PYRMSFzjl5GwwSMctgnhQzDmxOKhhY80/kjahQlX
+ laJ992v/AARx+F66WIr34w+KZdR2YM8Vlbxw7vXyjubHtv8AxryHnFS+kVY9H+zoW+I+Kf2s/w
+ BjD4hfsqata3Gq3sWveFdVlaHTtctoTGplAJ8meMk+VLtBYDJVgCVJwwX1MJjYYpaaNdDhxGGl
+ QeuqPnquw5goAKACgAoA/ar/AIJa+ENF0D9kzRfEGnwRi98T6lqN9fyAfOzxXL2yKT6COBSB/t
+ E9zXzGaTcsQ0+lj3MDFKimup9eV5x2Hkn7Vnwhk+OXwD8X/Dqxsbe51O/sTLpQmcRhb6JhJAd5
+ +5ll2k/3WYdCa6MLW9hWjN7GNen7Wm4n4g/HX9nr4h/s665p/hr4lJpNvqepWzXkVrZ6jHdOkO
+ 7aHcJ9wMQwXPXY3pX1NDEQxCcobHhVaMqLtI8yrcyCgAoAKAP0U/4Jh/tg+E/AFhc/AL4n63Bp
+ Njd3rXvh7UruQJbxyyY821kc8RhmAdGOFLM4JBK58bM8HKo/bU1fuelgcQoL2c/kfqfFLHPGs0
+ MiujgMrKwIYHoQR1rwD1jE8deM/D/w78H6x448VXy2mkaHZS315MeqxxqSQB3Y9FHUkgDrVwhK
+ pJQjuyZSUIuT2R/Pn8dPi74g+OvxV8Q/FDxGStxrV0ZIbfduW1tlG2GBfZI1Vc9yCTyTX2FCjG
+ hTVOPQ+dq1HVm5s4OtTMKACgAoA6/4R/DPxD8Y/iT4e+GfhaLdqOv3qWqOVysKdZJnA/gjjDu3
+ sprOrVjRg6ktkXTg6klBdT+hP4c+BNA+GHgTQvh/4YgaLSvD9hFYWoY5ZkRcbmPdmOWJ7ljXx1
+ So6s3OW7Po4QUIqK6H54f8FaP2kMDTv2bfC9/y3lat4maNu33rW1b9JmB/6Yn1r2cpw29eXojz
+ cwrf8ul8z8zK9w8sKACgAoAKAP0F/wCCYl3+z/8ACqHXPjB8Vvir4R0bxHf50nSLC/1KKOe1tA
+ QZpmQnKmRgqrnB2xt2evHzNVqtqdOLa3Z6OBdOnec2rn298Qf26f2Z/B/gnWvE+lfFvwv4gvtN
+ spJ7XStP1KOW4vZgP3cKKuT8zFQT0AJJ4FeXTwNeclFxaO+eKpRi2nc/Dnx1408Q/EbxjrPjvx
+ Xem71fXb2W+vJT0MjsSQo7KOAo6BQAOlfUwhGnFQjsjwZyc5OT3ZhVZIUAFABQAUAFABQAUAFA
+ BQAUAfRn/CK+GP8AoXNL/wDAOP8Awr7/AOpYb/n3H7kfkn9pY3/n9L/wJ/5h/wAIr4Y/6FzS/w
+ DwDj/wo+pYb/n3H7kH9pY3/n9L/wACf+Yf8Ir4Y/6FzS//AADj/wAKPqWG/wCfcfuQf2ljf+f0
+ v/An/mH/AAivhj/oXNL/APAOP/Cj6lhv+fcfuQf2ljf+f0v/AAJ/5h/wivhj/oXNL/8AAOP/AA
+ o+pYb/AJ9x+5B/aWN/5/S/8Cf+Yf8ACK+GP+hc0v8A8A4/8KPqWG/59x+5B/aWN/5/S/8AAn/m
+ H/CK+GP+hc0v/wAA4/8ACj6lhv8An3H7kH9pY3/n9L/wJ/5h/wAIr4Y/6FzS/wDwDj/wo+pYb/
+ n3H7kH9pY3/n9L/wACf+Z//9k=
+NOTE:Notatki do kontaktu
+ORG:Company;IT
+CLASS:PUBLIC
+ADR;TYPE=WORK:;Office Location;;;;;
+ADR;TYPE=HOME:;;Testowa;Warsaw;mazowickie;00-001;Poland
+LABEL;TYPE=HOME:Testowa\n00-001 Warsaw mazowickie\nPoland
+TEL;TYPE=HOME:home123456
+TEL;TYPE=CELL:1234556679200
+X-MS-SPOUSE;TYPE=N:Partner Name
+NICKNAME:alec
+TITLE:Developer
+URL;TYPE=HOME:www.alec.pl
+CATEGORIES:category
+BDAY;VALUE=DATE:2014-10-11
+REV;VALUE=DATE-TIME:2024-07-15T11:17:39,701Z
+X-MS-ANNIVERSARY;VALUE=DATE:2020-11-12
+END:VCARD
diff --git a/src/tests/data/ews/event/1.ics b/src/tests/data/ews/event/1.ics
new file mode 100644
index 00000000..35472b22
--- /dev/null
+++ b/src/tests/data/ews/event/1.ics
@@ -0,0 +1,49 @@
+BEGIN:VCALENDAR
+METHOD:PUBLISH
+PRODID:Microsoft Exchange Server 2010
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Central European Standard Time
+BEGIN:STANDARD
+DTSTART:16010101T030000
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010101T020000
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+ORGANIZER;CN=Aleksander Machniak:mailto:test@kolab.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Aleksander
+ Machniak:mailto:test@kolab.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=Alec Machn
+ iak:mailto:alec@outlook.com
+DESCRIPTION:test description
+UID:1F3C13D7E99642A75ABE23D50487B454-8FE68B2E68E1B348
+SUMMARY:test subject
+DTSTART;TZID=Central European Standard Time:20231121T120000
+DTEND;TZID=Central European Standard Time:20231121T123000
+CLASS:PUBLIC
+PRIORITY:5
+DTSTAMP:20231120T145005Z
+TRANSP:OPAQUE
+STATUS:CONFIRMED
+SEQUENCE:0
+X-MICROSOFT-CDO-APPT-SEQUENCE:0
+X-MICROSOFT-CDO-OWNERAPPTID:0
+X-MICROSOFT-CDO-BUSYSTATUS:TENTATIVE
+X-MICROSOFT-CDO-INTENDEDSTATUS:BUSY
+X-MICROSOFT-CDO-ALLDAYEVENT:FALSE
+X-MICROSOFT-CDO-IMPORTANCE:1
+X-MICROSOFT-CDO-INSTTYPE:0
+X-MICROSOFT-DONOTFORWARDMEETING:FALSE
+X-MICROSOFT-DISALLOW-COUNTER:FALSE
+X-MICROSOFT-REQUESTEDATTENDANCEMODE:DEFAULT
+X-MICROSOFT-ISRESPONSEREQUESTED:TRUE
+END:VEVENT
+END:VCALENDAR
diff --git a/src/tests/data/ews/incremental/0.json b/src/tests/data/ews/incremental/0.json
new file mode 100644
index 00000000..0da82daa
--- /dev/null
+++ b/src/tests/data/ews/incremental/0.json
@@ -0,0 +1,96 @@
+{
+ "error": false,
+ "statusCode": 200,
+ "headers": {
+ "Cache-Control": [
+ "private"
+ ],
+ "Transfer-Encoding": [
+ "chunked"
+ ],
+ "Content-Type": [
+ "text\/xml; charset=utf-8"
+ ],
+ "Server": [
+ "Microsoft-HTTPAPI\/2.0"
+ ],
+ "X-NanoProxy": [
+ "1,1"
+ ],
+ "X-AspNet-Version": [
+ "4.0.30319"
+ ],
+ "Request-Id": [
+ "a4ce6838-81ac-65a7-4759-3d9294738779"
+ ],
+ "X-CalculatedFETarget": [
+ "GV0P278CU004.internal.outlook.com"
+ ],
+ "Alt-Svc": [
+ "h3=\":443\";ma=2592000,h3-29=\":443\";ma=2592000"
+ ],
+ "MS-CV": [
+ "OGjOpKyBp2VHWT2SlHOHeQ.1.1"
+ ],
+ "Restrict-Access-Confirm": [
+ "1"
+ ],
+ "X-BackEndHttpStatus": [
+ "200,200"
+ ],
+ "X-BEServer": [
+ "GVAP278MB0615"
+ ],
+ "X-BeSku": [
+ "WCS6"
+ ],
+ "X-CalculatedBETarget": [
+ "GVAP278MB0615.CHEP278.PROD.OUTLOOK.COM"
+ ],
+ "X-DiagInfo": [
+ "GVAP278MB0615"
+ ],
+ "x-EwsHandler": [
+ "FindFolder"
+ ],
+ "X-FEEFZInfo": [
+ "GVA"
+ ],
+ "X-FEProxyInfo": [
+ "GV0P278CA0060"
+ ],
+ "X-FEServer": [
+ "WA0P291CA0004"
+ ],
+ "x-ms-appId": [
+ "6ce1e52b-cd1d-4663-abc3-368fba0bfae7"
+ ],
+ "X-Proxy-BackendServerStatus": [
+ "200"
+ ],
+ "X-Proxy-RoutingCorrectness": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedPath": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedDbCopy": [
+ "1"
+ ],
+ "X-RUM-Validated": [
+ "1"
+ ],
+ "X-FirstHopCafeEFZ": [
+ "WAW"
+ ],
+ "Strict-Transport-Security": [
+ "max-age=31536000; includeSubDomains"
+ ],
+ "Set-Cookie": [
+ "exchangecookie=faf9d40d8bc14d53a013ecd1336e35b5; expires=Fri, 11-Jul-2025 12:51:36 GMT; path=\/; secure; HttpOnly"
+ ],
+ "Date": [
+ "Thu, 11 Jul 2024 12:51:36 GMT"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/tests/data/ews/incremental/0.xml b/src/tests/data/ews/incremental/0.xml
new file mode 100644
index 00000000..35296240
--- /dev/null
+++ b/src/tests/data/ews/incremental/0.xml
@@ -0,0 +1,7139 @@
+
+
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+
+ IPF.Note
+ AllCategorizedItems
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ AllContacts
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ AllContactsExtended
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF
+ AllItems
+ 230
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 110
+
+
+
+
+ IPF.Note
+ AllPersonMetadata
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Task
+ AllTodoTasks
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ApplicationDataRoot
+ 0
+ 56
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 00000002-0000-0ff1-ce00-000000000000
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ HxNotificationSettings
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 00000006-0000-0ff1-ce00-000000000000
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GlobalSearchHistory
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 13937bba-652e-4c46-b222-3003f4d1ff97
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ContextDataFast
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ContextMetadata
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserInteractionsFast
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 1caee58f-eb14-4a6b-9339-1fe2ddf6692b
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Recent
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Settings
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 1e70cd27-4707-4589-8ec5-9bd20c472a46
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 224fffa9-a547-4c78-ab95-3aacd66c3e99
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FeaturesManagementCollection_1
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 32d4b5e5-7d33-4e7f-b073-f8cffbbb47a1
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ outlookfavorites
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 33b68b23-a6c2-4684-99a0-fa3832792226
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AggregationV2BackfillStateCollection
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ RelevanceDailyAggregation
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 34421fbe-f100-4e5b-9c46-2fea25aa7b88
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Contacts
+ 6
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 35d54a08-36c9-4847-9018-93934c62740c
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PeoplePredictions.FeedbackEntries
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PeoplePredictions.RequestEntries
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PeoplePredictions.UserProfile
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 3873c4f1-2162-4ddb-96e6-232e4eddeedc
+ 0
+ 4
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SEED_PeopleIntegrityCheckerStateCollection_v2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SEED_ScdIntegrityCheckerStateCollection_v2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 394866fc-eedb-4f01-8536-3ff84b16be2a
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AssociationBackfillStatus
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Favorites
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ InsightInstancesActions
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 3b2e5a14-128d-48aa-b581-482aac616d32
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 3c896ded-22c5-450f-91f6-3d1ef0848f6e
+ 0
+ 42
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActionExecutions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActivitiesDaily
+ 58
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActivitiesWeekly
+ 8
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AfterHoursEmailImpact
+ 50
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AutomaticRepliesHistory
+ 105
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ChatsInterruptionStatistics
+ 49
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ComputeLogs
+ 38
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CumulativeNetworkSnapshot
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CumulativeOutOfOfficeClustering
+ 106
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DailyActivityTimeBins
+ 56
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DailyAppointments
+ 85
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DailyInteractions
+ 50
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DailyNetworkSnapshot
+ 51
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DelayedEmail
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DeliverCardEvents
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DetailedMeetings
+ 51
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EmailActionStatistics
+ 50
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EmailGrazerData
+ 50
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EmfResponses
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FoodForThoughtRanking
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ HeterogeneousItems
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ImportantContact
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ InsightsEngagement
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ MailActivityCompactSignalBatch
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ManagementOperationExecutionRecords
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ MeetingActionStatistics
+ 9
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ MeetingResponseActivity
+ 6
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutcomeStates
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutOfOffice
+ 44
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ProgramMetrics
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SegmentsStateRecord
+ 18
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SentMailLogs
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SigsChatsAndCallsCompactSignalBatch
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UnifiedCardEngagements
+ 30
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserConfiguration
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserProgramInstances
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyEmbeddingBasedUserTimePrediction
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyInteractions
+ 27
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyOutOfOfficeAndWorkingDay
+ 28
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyOutOfRoutineMeetingScorePDF
+ 30
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyOutOfRoutineMeetingTimes
+ 61
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyWorkplaceActivityEmbedding
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 3e3c8645-20a6-4c6e-8355-c34cbbe04585
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PintSigsMsGraphState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 42c2c67f-2760-4f42-b0c4-ef5c3854a0c2
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CollabObjects
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 43375d74-c6a5-4d4e-a0a3-de139860ea75
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ElcArchiveProcessorStatsSdsCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OverallArchivingStatsSdsCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 441509e5-a165-4363-8ee7-bcf0b7d26739
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GenericWorkflowProcessor.SessionManager.Data
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Idf
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ IdfMeeting
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 4787c7ff-7cea-43db-8d0d-919f15c6354b
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ VivaSales
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 48af08dc-f6d2-435f-b2a7-069abd99c086
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ InsightsProvidersSettings
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 49499048-0129-47f5-b95e-f9d315b861a6
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutlookAccountCloudSettings
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutlookCloudSettings
+ 5
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 497effe9-df71-4043-a8bb-14cf78c4b63b
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EACUserPreferences
+ 5
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 4e445925-163e-42ca-b801-9073bfa46d17
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ NewsSubscriptionSourcesv2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 59cc317d-5453-4fcd-ab98-60552b158926
+ 0
+ 6
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AlgorithmRunStatus
+ 9
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CDDS_SuggestionCheckpoint
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ContactFeedback
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ContactMergeSuggestions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ HiddenDuplicates
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ HideSettings
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 644c1b11-f63f-45fa-826b-a9d2801db711
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ _PolicyContainer
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ YWxlY0BhcGhlbGVpYWl0Lm9ubWljcm9zb2Z0LmNvbQ==_LabelFile
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ YWxlY0BhcGhlbGVpYWl0Lm9ubWljcm9zb2Z0LmNvbQ==_PolicyContainer
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 66a88757-258c-4c72-893c-3e8bed4d6899
+ 0
+ 31
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ScopeEntities
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.Apps
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.CalendarEvents
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.CollaborationNetworkEntities
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.ContentDomain
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EmailEntities
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EmailTokens
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe.Bookmarks
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe.Links
+ 4
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe.Locations
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe.Qnas
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.FreshHistory
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.FreshSharePointFiles
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.FreshSharePointSites
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.GroupsRoomsMiscIndex
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.Notes
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.People
+ 11
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.PeopleCache
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.PeopleIndex
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.RecentEmailItems
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.RecommendationsService
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.RecommendationsServiceProcessor
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.Scope
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.SearchHistory.Main
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.SearchHistoryState
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.SharePointDocuments
+ 5
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.SkillEntities
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.TeamsAndChannels
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.TeamsEntities
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.Topics
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 6cd5135b-7a07-43ec-acc2-b5e27dc628cb
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ currentuser
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ neighbors
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OfficeFeedSdsCacheCollection
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 766ef332-38e5-4cb4-920c-baa478e39fd9
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CrawlerExecutionInfoCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 7a5fbd1c-3e6d-461a-9075-83049393b3a7
+ 0
+ 7
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AcronymAggregationState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AcronymService.SessionManager.Data
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ConceptAggregationSessionManager.Data
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ NicknamesAllInbox
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SimpleAcronymsIndex
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SimpleAcronymsIndexElysium
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SimpleUserContextIndex
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 7aadb841-2368-4d24-a373-94d0ba1f077a
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ outlookspaces
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 7ae974c5-1af7-4923-af3a-fb1fd14dcb7e
+ 0
+ 9
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AppBarTiles
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FloodgateSurvey
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GetStartedStore
+ 20
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ lightning
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ LightningSharedStore
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ LightningStore
+ 67
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutlookEndpointDataCollection
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutlookOptionsV3
+ 7
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WhatsNewStore
+ 97
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 7e02887d-df1d-45a4-af52-dcee2e585eee
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserEnablementStatus
+ 8
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 80723a00-368e-4d64-8281-210e49e593a8
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActivityFeed_201905
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 82d8ab62-be52-a567-14ea-1616c4ee06c4
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActorAnalyticsProcessorState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 8a18aa92-0a1e-4e06-abd0-e118fa4787b1
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GraphMetaColl
+ 26
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 8c22b648-ee54-4ece-a4ca-3015b6d24f8e
+ 0
+ 20
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DIF
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Images
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_contactsync
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_meetinginsightssetting
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_officegraphvisibilitysetting
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceaad
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceconnector
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceemailaddressdisplaynames
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcemanagerchain
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcemsgraph
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcenamepronunciation
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcenone
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceorganizationheadcount
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcephotoetag
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceupa
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceuserdefined
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_userdefinedsettings
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SQI_FI
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ view_profilev2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ view_settingsv2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 94c63fef-13a3-47bc-8074-75af8c65887a
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DelveCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 97cb1f73-50df-47d1-8fb0-0271f2728514
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CleanupProcessorState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ a3883eba-fbe9-48bd-9ed3-dca3e0e84250
+ 0
+ 4
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AggregatedLinks
+ 9
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EmailsIndexV1
+ 65
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ LinkFiltersMetadataIndex
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ LinksIndex
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ a6061a71-f529-4ca2-bc9d-c8085ecbf3ca
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EdgeSettings
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ adcd8967-0799-4ec0-bfc8-dcf85fd84767
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CssProcessingState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ae8e128e-080f-4086-b0e3-4c19301ada69
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Scheduling
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ b669c6ea-1adf-453f-b8bc-6d526592b419
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FocusedInboxMailboxData
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ NudgeCandidates
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ScenarioTracking
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ b6e65498-8dce-4d57-8de9-1269fcfcf6ce
+ 0
+ 6
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ KeyphraseRelationshipsSessionManager.Data
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserDocKpeStats
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserDocWithKpes
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserKpes
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserKpeState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserStatistics
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ c9224372-5534-42cb-a48b-8db4f4a3892e
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ TeamsCalendar.SSMeetingPropsSync
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ c94eb245-f5ab-4fea-a534-841091469193
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ic3usersettings
+ 13
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ d297b9ff-5d5b-4bce-8ed4-eedb1431bf61
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ NucleusProperties
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ d396de1f-10d4-4023-aae2-5bb3d724ba9a
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FolderGroups-DefaultCollection
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ settings
+ 6
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ TasksDataMarker_v9
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ d71dfe16-1070-48f3-bd3a-c3ec919d34e7
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ TxpAutoblocking
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ TxpUserSettings
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ d8cbf452-c2df-4756-95a5-8aa23fedc3ce
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserMigrationDetails
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ e69932cd-f814-4087-8ab1-5ab3f1ad18eb
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ e7762c80-392b-4278-80fa-a8fda80b129c
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CMUState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ e7f049f2-9dc5-4301-88a2-121b268ad42f
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Common
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SharingSuggestion
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ f08262d6-bf02-4e1e-bdef-e95e2f0a940f
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GlowCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ f164ee43-46e5-4f7c-b5ff-2d1abcd4ce7e
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PersonalProfileVector_AnalyzerStates
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ f3f64fec-ecee-4e9a-9eed-a36a5e421300
+ 0
+ 10
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ IdfEmailPoi
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ IdfInboxPoi
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ IdfMeetingPoi
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ InferredLanguage
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SlpJobs
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechNGrams
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechTrimmedLms
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechUserDocumentNGrams
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechUserDocumentSentences
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechUserDocumentsHistory
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ BrokerSubscriptions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ BulkActions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ CalendarSharingCacheCollection
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Common Views
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ComplianceMetadata
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Connectors
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ConnectorConfigurations
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.CrawlerData
+ CrawlerData
+ 35
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Deferred Action
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.DlpPolicyEvaluation
+ DlpPolicyEvaluation
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Document Centric Conversations
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ExchangeODataSyncData
+ 199
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ExchangeSyncData
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ AirSync-iPhone-J97G80P7O916504POMJIHAJE70
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ AirSync-TestActiveSyncConnectivity-430847304
+ 5
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Hx-Outlook-B57C063B9A6C46ECAD4C159C1E161B14
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ OutlookServiceActionQueue
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Policy
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Finder
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ OwaFV15.1AllFocusedAQMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NABkNDgALgAAA722UNhKUbZNrFYGsDU7Na0BACzZqOGSdeVFg3+lsLwmpuUAAAIBDAAAAA==
+ 101
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 95
+
+
+
+
+ IPF.Note
+ OwaFV15.1AllOtherAQMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NABkNDgALgAAA722UNhKUbZNrFYGsDU7Na0BACzZqOGSdeVFg3+lsLwmpuUAAAIBDAAAAA==
+ 15
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 15
+
+
+
+
+ IPF.Task
+ Folder Memberships
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Task
+ Flagged Emails
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Folder nadrzędny magazynu informacji
+ 0
+ 19
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Archiwum
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Configuration
+ Conversation Action Settings
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Journal
+ Dziennik
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Elementy usunięte
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Elementy wysłane
+ 5
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.EventCheckPoints
+ EventCheckPoints
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Contact
+ ExternalContacts
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF
+ Folder główny usługi Yammer
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF
+ Feeds
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF
+ Inbound
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF
+ Outbound
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Historia konwersacji
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.SkypeTeams.Message
+ Czat zespołu
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Appointment
+ Calendar
+ 2
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Appointment
+ United States holidays
+ 97
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Appointment.Birthday
+ Urodziny
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Contact
+ Kontakty
+ 4
+ 7
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Contact.MOC.QuickContacts
+ {06967759-274D-40B2-A3EB-D7F9E73727D7}
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Contact.MOC.ImContactList
+ {A9E2BC46-B3A0-4243-B315-60D991004455}
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Contact.Company
+ Firmy
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Contact.GalContacts
+ GAL Contacts
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Contact.OrganizationalContacts
+ Organizational Contacts
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Contact.PeopleCentricConversationBuddies
+ PeopleCentricConversation Buddies
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Contact.RecipientCache
+ Recipient Cache
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.Contact.MeContact
+ MeContact
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+
+
+
+
+ IPF.StickyNote
+ Notatki
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Files
+ Pliki
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Skrzynka nadawcza
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Skrzynka odbiorcza
+ 116
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 110
+
+
+
+
+ IPF.Note
+ Wersje robocze
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Wiadomości-śmieci
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 3dbb5f39-dc76-4916-9db3-fa9191760a55
+ 0
+
+
+
+
+ IPF.Task
+ Tasks
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Freebusy Data
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.FreeBusyCache
+ FreeBusyLocalCache
+ 2
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ FreeBusyLocalCacheSubscriptions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ GraphFilesAndWorkingSetSearchFolder
+ 64
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ GraphStore
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ GraphRelations
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Inference
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ MailboxAssociations
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ MailboxMoveHistory
+ 4
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ MailboxReplicationService Index
+ 4
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ MergedViewFolderCollection
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ MessageIngestion
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Yammer
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Moje kontakty
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ O365 Suite Notifications
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF
+ OneDriveRoot
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ OneNotePagePreviews
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Task
+ Orion Notes
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ OutlookExtensions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ PACE
+ 31
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ People I Know
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ PeopleConnect
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ PeoplePublicData
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Outlook.Reminder
+ Przypomnienia
+ 21
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ QuarantinedEmail
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ cce0d6e6-b69b-410a-907d-06dc2071ab58
+ 0
+
+
+
+
+ QuarantinedEmailDefaultCategory
+ 0
+ 4
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ cce0d6e6-b69b-410a-907d-06dc2071ab58
+ 0
+
+
+
+
+ QedcDefaultRetention
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ cce0d6e6-b69b-410a-907d-06dc2071ab58
+ 0
+
+
+
+
+ QedcLongRetention
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ cce0d6e6-b69b-410a-907d-06dc2071ab58
+ 0
+
+
+
+
+ QedcMediumRetention
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ cce0d6e6-b69b-410a-907d-06dc2071ab58
+ 0
+
+
+
+
+ QedcShortRetention
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ cce0d6e6-b69b-410a-907d-06dc2071ab58
+ 0
+
+
+
+
+ Recoverable Items
+ 0
+ 6
+
+ true
+ true
+ false
+ false
+ false
+
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Audits
+ 12
+ 0
+
+ true
+ true
+ false
+ false
+ false
+
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Calendar Logging
+ 0
+ 0
+
+ true
+ true
+ false
+ false
+ false
+
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Deletions
+ 0
+ 0
+
+ true
+ true
+ false
+ false
+ false
+
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Purges
+ 2
+ 0
+
+ true
+ true
+ false
+ false
+ false
+
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ SubstrateHolds
+ 0
+ 0
+
+ true
+ true
+ false
+ false
+ false
+
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Versions
+ 0
+ 0
+
+ true
+ true
+ false
+ false
+ false
+
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.RecoveryPoints
+ RecoveryPoints
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ RelevantContacts
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Schedule
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ SearchFoldersView
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.ShadowItems
+ ShadowItems
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ SharedFilesSearchFolder
+ 63
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ SharePointNotifications
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Sharing
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Shortcuts
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ShortNotes
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ SkypeSpacesData
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ SkypeMessages
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ TeamsMeetings
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ TeamsMessages
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Spooler Queue
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ SpoolsPresentSharedItemsSearchFolder
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ SpoolsSearchFolder
+ 8
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF
+ SubstrateFiles
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ClassicAttachments
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.Signal
+ GraphWorkingSet
+ 64
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ SPOOLS
+ 8
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ SuggestedUserGroupAssociations
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.SwssItems
+ SwssItems
+ 0
+ 10
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF
+ TeamChatHistory
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF
+ TeamsMessagesData
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ TemporarySaves
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Task
+ To-Do Search
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Ulubione
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ UserCuratedContacts
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ UserSocialActivityNotifications
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Views
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ XrmActivityStream
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ XrmActivityStreamSearch
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ XrmCompanySearch
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ XrmDealSearch
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ XrmInsights
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ XrmProjects
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ XrmSearch
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ YammerData
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+
+
+
+
diff --git a/src/tests/data/ews/incremental/1.json b/src/tests/data/ews/incremental/1.json
new file mode 100644
index 00000000..46b779e1
--- /dev/null
+++ b/src/tests/data/ews/incremental/1.json
@@ -0,0 +1,96 @@
+{
+ "error": false,
+ "statusCode": 200,
+ "headers": {
+ "Cache-Control": [
+ "private"
+ ],
+ "Transfer-Encoding": [
+ "chunked"
+ ],
+ "Content-Type": [
+ "text\/xml; charset=utf-8"
+ ],
+ "Server": [
+ "Microsoft-HTTPAPI\/2.0"
+ ],
+ "X-NanoProxy": [
+ "1,1"
+ ],
+ "X-AspNet-Version": [
+ "4.0.30319"
+ ],
+ "Request-Id": [
+ "0de708d9-ea99-409a-9a67-9c8506607510"
+ ],
+ "X-CalculatedFETarget": [
+ "GV0P278CU003.internal.outlook.com"
+ ],
+ "Alt-Svc": [
+ "h3=\":443\";ma=2592000,h3-29=\":443\";ma=2592000"
+ ],
+ "MS-CV": [
+ "2QjnDZnqmkCaZ5yFBmB1EA.1.1"
+ ],
+ "Restrict-Access-Confirm": [
+ "1"
+ ],
+ "X-BackEndHttpStatus": [
+ "200,200"
+ ],
+ "X-BEServer": [
+ "GV0P278MB1046"
+ ],
+ "X-BeSku": [
+ "WCS7"
+ ],
+ "X-CalculatedBETarget": [
+ "GV0P278MB1046.CHEP278.PROD.OUTLOOK.COM"
+ ],
+ "X-DiagInfo": [
+ "GV0P278MB1046"
+ ],
+ "x-EwsHandler": [
+ "FindItem"
+ ],
+ "X-FEEFZInfo": [
+ "GVA"
+ ],
+ "X-FEProxyInfo": [
+ "GV0P278CA0041"
+ ],
+ "X-FEServer": [
+ "WA0P291CA0004"
+ ],
+ "x-ms-appId": [
+ "6ce1e52b-cd1d-4663-abc3-368fba0bfae7"
+ ],
+ "X-Proxy-BackendServerStatus": [
+ "200"
+ ],
+ "X-Proxy-RoutingCorrectness": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedPath": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedDbCopy": [
+ "1"
+ ],
+ "X-RUM-Validated": [
+ "1"
+ ],
+ "X-FirstHopCafeEFZ": [
+ "WAW"
+ ],
+ "Strict-Transport-Security": [
+ "max-age=31536000; includeSubDomains"
+ ],
+ "Set-Cookie": [
+ "exchangecookie=69bae057f3b74f0fb7163f66ddd11493; expires=Fri, 11-Jul-2025 12:51:40 GMT; path=\/; secure; HttpOnly"
+ ],
+ "Date": [
+ "Thu, 11 Jul 2024 12:51:39 GMT"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/tests/data/ews/incremental/1.xml b/src/tests/data/ews/incremental/1.xml
new file mode 100644
index 00000000..ad5c7e0c
--- /dev/null
+++ b/src/tests/data/ews/incremental/1.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Appointment
+
+
+
+ IPM.Appointment
+
+
+
+
+
+
+
+
diff --git a/src/tests/data/ews/incremental/4.json b/src/tests/data/ews/incremental/4.json
new file mode 100644
index 00000000..905f098f
--- /dev/null
+++ b/src/tests/data/ews/incremental/4.json
@@ -0,0 +1,96 @@
+{
+ "error": false,
+ "statusCode": 200,
+ "headers": {
+ "Cache-Control": [
+ "private"
+ ],
+ "Transfer-Encoding": [
+ "chunked"
+ ],
+ "Content-Type": [
+ "text\/xml; charset=utf-8"
+ ],
+ "Server": [
+ "Microsoft-HTTPAPI\/2.0"
+ ],
+ "X-NanoProxy": [
+ "1,1"
+ ],
+ "X-AspNet-Version": [
+ "4.0.30319"
+ ],
+ "Request-Id": [
+ "65d9caeb-2f07-ca8d-bd20-851f8dc232ed"
+ ],
+ "X-CalculatedFETarget": [
+ "GV0P278CU002.internal.outlook.com"
+ ],
+ "Alt-Svc": [
+ "h3=\":443\";ma=2592000,h3-29=\":443\";ma=2592000"
+ ],
+ "MS-CV": [
+ "68rZZQcvjcq9IIUfjcIy7Q.1.1"
+ ],
+ "Restrict-Access-Confirm": [
+ "1"
+ ],
+ "X-BackEndHttpStatus": [
+ "200,200"
+ ],
+ "X-BEServer": [
+ "GVAP278MB0389"
+ ],
+ "X-BeSku": [
+ "WCS6"
+ ],
+ "X-CalculatedBETarget": [
+ "GVAP278MB0389.CHEP278.PROD.OUTLOOK.COM"
+ ],
+ "X-DiagInfo": [
+ "GVAP278MB0389"
+ ],
+ "x-EwsHandler": [
+ "FindItem"
+ ],
+ "X-FEEFZInfo": [
+ "GVA"
+ ],
+ "X-FEProxyInfo": [
+ "GV0P278CA0020"
+ ],
+ "X-FEServer": [
+ "WA0P291CA0004"
+ ],
+ "x-ms-appId": [
+ "6ce1e52b-cd1d-4663-abc3-368fba0bfae7"
+ ],
+ "X-Proxy-BackendServerStatus": [
+ "200"
+ ],
+ "X-Proxy-RoutingCorrectness": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedPath": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedDbCopy": [
+ "1"
+ ],
+ "X-RUM-Validated": [
+ "1"
+ ],
+ "X-FirstHopCafeEFZ": [
+ "WAW"
+ ],
+ "Strict-Transport-Security": [
+ "max-age=31536000; includeSubDomains"
+ ],
+ "Set-Cookie": [
+ "exchangecookie=5ad29dedadde423c99430af280c2ae09; expires=Fri, 11-Jul-2025 12:51:49 GMT; path=\/; secure; HttpOnly"
+ ],
+ "Date": [
+ "Thu, 11 Jul 2024 12:51:49 GMT"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/tests/data/ews/incremental/4.xml b/src/tests/data/ews/incremental/4.xml
new file mode 100644
index 00000000..ea487095
--- /dev/null
+++ b/src/tests/data/ews/incremental/4.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+ IPM.Contact
+
+
+
+ IPM.DistList
+
+
+
+ IPM.Contact
+
+
+
+ IPM.Contact
+
+
+
+
+
+
+
+
diff --git a/src/tests/data/ews/incremental/5.json b/src/tests/data/ews/incremental/5.json
new file mode 100644
index 00000000..80355d07
--- /dev/null
+++ b/src/tests/data/ews/incremental/5.json
@@ -0,0 +1,96 @@
+{
+ "error": false,
+ "statusCode": 200,
+ "headers": {
+ "Cache-Control": [
+ "private"
+ ],
+ "Transfer-Encoding": [
+ "chunked"
+ ],
+ "Content-Type": [
+ "text\/xml; charset=utf-8"
+ ],
+ "Server": [
+ "Microsoft-HTTPAPI\/2.0"
+ ],
+ "X-NanoProxy": [
+ "1,1"
+ ],
+ "X-AspNet-Version": [
+ "4.0.30319"
+ ],
+ "Request-Id": [
+ "75aec142-54d8-bfc0-c0e7-adc4e3f2ba14"
+ ],
+ "X-CalculatedFETarget": [
+ "GV0P278CU005.internal.outlook.com"
+ ],
+ "Alt-Svc": [
+ "h3=\":443\";ma=2592000,h3-29=\":443\";ma=2592000"
+ ],
+ "MS-CV": [
+ "QsGuddhUwL\/A563E4\/K6FA.1.1"
+ ],
+ "Restrict-Access-Confirm": [
+ "1"
+ ],
+ "X-BackEndHttpStatus": [
+ "200,200"
+ ],
+ "X-BEServer": [
+ "GV0P278MB0353"
+ ],
+ "X-BeSku": [
+ "WCS6"
+ ],
+ "X-CalculatedBETarget": [
+ "GV0P278MB0353.CHEP278.PROD.OUTLOOK.COM"
+ ],
+ "X-DiagInfo": [
+ "GV0P278MB0353"
+ ],
+ "x-EwsHandler": [
+ "GetItem"
+ ],
+ "X-FEEFZInfo": [
+ "GVA"
+ ],
+ "X-FEProxyInfo": [
+ "GV0P278CA0084"
+ ],
+ "X-FEServer": [
+ "WA0P291CA0004"
+ ],
+ "x-ms-appId": [
+ "6ce1e52b-cd1d-4663-abc3-368fba0bfae7"
+ ],
+ "X-Proxy-BackendServerStatus": [
+ "200"
+ ],
+ "X-Proxy-RoutingCorrectness": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedPath": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedDbCopy": [
+ "1"
+ ],
+ "X-RUM-Validated": [
+ "1"
+ ],
+ "X-FirstHopCafeEFZ": [
+ "WAW"
+ ],
+ "Strict-Transport-Security": [
+ "max-age=31536000; includeSubDomains"
+ ],
+ "Set-Cookie": [
+ "exchangecookie=08d3bd1184da4e9f8016984e26367d56; expires=Fri, 11-Jul-2025 12:51:50 GMT; path=\/; secure; HttpOnly"
+ ],
+ "Date": [
+ "Thu, 11 Jul 2024 12:51:50 GMT"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/tests/data/ews/incremental/5.xml b/src/tests/data/ews/incremental/5.xml
new file mode 100644
index 00000000..d89bc525
--- /dev/null
+++ b/src/tests/data/ews/incremental/5.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+ NoError
+
+
+ QkVHSU46VkNBUkQNClBST0ZJTEU6VkNBUkQNClZFUlNJT046My4wDQpNQUlMRVI6TWljcm9zb2Z0IEV4Y2hhbmdlDQpQUk9ESUQ6TWljcm9zb2Z0IEV4Y2hhbmdlDQpGTjpUZXN0IFN1cm5hbWUgMQ0KTjpTdXJuYW1lIDE7VGVzdDs7Ow0KTk9URToNCk9SRzo7DQpDTEFTUzpQVUJMSUMNCkFEUjtUWVBFPVdPUks6Ozs7Ozs7DQpBRFI7VFlQRT1IT01FOjs7Ozs7Ow0KQURSO1RZUEU9UE9TVEFMOjs7Ozs7Ow0KVEVMO1RZUEU9Q0VMTDoNClRFTDtUWVBFPVBBR0VSOg0KVEVMO1RZUEU9Q0FSOg0KVEVMO1RZUEU9Vk9JQ0U6DQpYLU1TLVNQT1VTRTtUWVBFPU46DQpYLU1TLU1BTkFHRVI7VFlQRT1OOg0KWC1NUy1BU1NJU1RBTlQ7VFlQRT1OOg0KVElUTEU6DQpVUkw7VFlQRT1IT01FOg0KVVJMO1RZUEU9V09SSzoNClJFVjtWQUxVRT1EQVRFLVRJTUU6MjAyNC0wNi0yNVQxMDo1NDo1NCwyMTRaDQpFTkQ6VkNBUkQNCg==
+
+ false
+ en-US
+ 2024-06-25T09:54:54Z
+
+ Test Surname 1
+ Test
+
+ Test
+ Surname 1
+ Test Surname 1
+
+
+ christian@outlook.com
+
+
+
+
+
+
+
+
+
+
+ Surname 1
+ Q1ne1H54kk35i44UboHGPFRRPI8=
+ 2790877950c24ec1a3a9b41ce75aec8a
+ EXCH
+
+
+
+
+
+
+
+
diff --git a/src/tests/data/ews/incremental/6.json b/src/tests/data/ews/incremental/6.json
new file mode 100644
index 00000000..80355d07
--- /dev/null
+++ b/src/tests/data/ews/incremental/6.json
@@ -0,0 +1,96 @@
+{
+ "error": false,
+ "statusCode": 200,
+ "headers": {
+ "Cache-Control": [
+ "private"
+ ],
+ "Transfer-Encoding": [
+ "chunked"
+ ],
+ "Content-Type": [
+ "text\/xml; charset=utf-8"
+ ],
+ "Server": [
+ "Microsoft-HTTPAPI\/2.0"
+ ],
+ "X-NanoProxy": [
+ "1,1"
+ ],
+ "X-AspNet-Version": [
+ "4.0.30319"
+ ],
+ "Request-Id": [
+ "75aec142-54d8-bfc0-c0e7-adc4e3f2ba14"
+ ],
+ "X-CalculatedFETarget": [
+ "GV0P278CU005.internal.outlook.com"
+ ],
+ "Alt-Svc": [
+ "h3=\":443\";ma=2592000,h3-29=\":443\";ma=2592000"
+ ],
+ "MS-CV": [
+ "QsGuddhUwL\/A563E4\/K6FA.1.1"
+ ],
+ "Restrict-Access-Confirm": [
+ "1"
+ ],
+ "X-BackEndHttpStatus": [
+ "200,200"
+ ],
+ "X-BEServer": [
+ "GV0P278MB0353"
+ ],
+ "X-BeSku": [
+ "WCS6"
+ ],
+ "X-CalculatedBETarget": [
+ "GV0P278MB0353.CHEP278.PROD.OUTLOOK.COM"
+ ],
+ "X-DiagInfo": [
+ "GV0P278MB0353"
+ ],
+ "x-EwsHandler": [
+ "GetItem"
+ ],
+ "X-FEEFZInfo": [
+ "GVA"
+ ],
+ "X-FEProxyInfo": [
+ "GV0P278CA0084"
+ ],
+ "X-FEServer": [
+ "WA0P291CA0004"
+ ],
+ "x-ms-appId": [
+ "6ce1e52b-cd1d-4663-abc3-368fba0bfae7"
+ ],
+ "X-Proxy-BackendServerStatus": [
+ "200"
+ ],
+ "X-Proxy-RoutingCorrectness": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedPath": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedDbCopy": [
+ "1"
+ ],
+ "X-RUM-Validated": [
+ "1"
+ ],
+ "X-FirstHopCafeEFZ": [
+ "WAW"
+ ],
+ "Strict-Transport-Security": [
+ "max-age=31536000; includeSubDomains"
+ ],
+ "Set-Cookie": [
+ "exchangecookie=08d3bd1184da4e9f8016984e26367d56; expires=Fri, 11-Jul-2025 12:51:50 GMT; path=\/; secure; HttpOnly"
+ ],
+ "Date": [
+ "Thu, 11 Jul 2024 12:51:50 GMT"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/tests/data/ews/incremental/6.xml b/src/tests/data/ews/incremental/6.xml
new file mode 100644
index 00000000..b1c6deb6
--- /dev/null
+++ b/src/tests/data/ews/incremental/6.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+ NoError
+
+
+ QkVHSU46VkNBUkQNClBST0ZJTEU6VkNBUkQNClZFUlNJT046My4wDQpNQUlMRVI6TWljcm9zb2Z0IEV4Y2hhbmdlDQpQUk9ESUQ6TWljcm9zb2Z0IEV4Y2hhbmdlDQpGTjpUZXN0IE5ldw0KTjpOZXc7VGVzdDs7Ow0KTk9URToNCk9SRzo7DQpDTEFTUzpQVUJMSUMNCkFEUjtUWVBFPVdPUks6Ozs7Ozs7DQpBRFI7VFlQRT1IT01FOjs7Ozs7Ow0KQURSO1RZUEU9UE9TVEFMOjs7Ozs7Ow0KVEVMO1RZUEU9Q0VMTDoNClRFTDtUWVBFPVBBR0VSOg0KVEVMO1RZUEU9Q0FSOg0KVEVMO1RZUEU9Vk9JQ0U6DQpYLU1TLVNQT1VTRTtUWVBFPU46DQpYLU1TLU1BTkFHRVI7VFlQRT1OOg0KWC1NUy1BU1NJU1RBTlQ7VFlQRT1OOg0KVElUTEU6DQpVUkw7VFlQRT1IT01FOg0KVVJMO1RZUEU9V09SSzoNClJFVjtWQUxVRT1EQVRFLVRJTUU6MjAyNC0wNi0yNVQxMDo1NDo1NCwyMTRaDQpFTkQ6VkNBUkQNCg==
+
+ false
+ en-US
+ 2024-06-25T09:54:54Z
+
+ Test New
+ Test
+
+ Test
+ New
+ Test New
+
+
+ new@outlook.com
+
+
+
+
+
+
+
+
+
+
+ New
+ Q1ne1H54kk35i44UboHGPFRRPI8=
+ 2790877950c24ec1a3a9b41ce75aec8a
+ EXCH
+
+
+
+
+
+
+
+
diff --git a/src/tests/data/ews/initial/0.json b/src/tests/data/ews/initial/0.json
new file mode 100644
index 00000000..0da82daa
--- /dev/null
+++ b/src/tests/data/ews/initial/0.json
@@ -0,0 +1,96 @@
+{
+ "error": false,
+ "statusCode": 200,
+ "headers": {
+ "Cache-Control": [
+ "private"
+ ],
+ "Transfer-Encoding": [
+ "chunked"
+ ],
+ "Content-Type": [
+ "text\/xml; charset=utf-8"
+ ],
+ "Server": [
+ "Microsoft-HTTPAPI\/2.0"
+ ],
+ "X-NanoProxy": [
+ "1,1"
+ ],
+ "X-AspNet-Version": [
+ "4.0.30319"
+ ],
+ "Request-Id": [
+ "a4ce6838-81ac-65a7-4759-3d9294738779"
+ ],
+ "X-CalculatedFETarget": [
+ "GV0P278CU004.internal.outlook.com"
+ ],
+ "Alt-Svc": [
+ "h3=\":443\";ma=2592000,h3-29=\":443\";ma=2592000"
+ ],
+ "MS-CV": [
+ "OGjOpKyBp2VHWT2SlHOHeQ.1.1"
+ ],
+ "Restrict-Access-Confirm": [
+ "1"
+ ],
+ "X-BackEndHttpStatus": [
+ "200,200"
+ ],
+ "X-BEServer": [
+ "GVAP278MB0615"
+ ],
+ "X-BeSku": [
+ "WCS6"
+ ],
+ "X-CalculatedBETarget": [
+ "GVAP278MB0615.CHEP278.PROD.OUTLOOK.COM"
+ ],
+ "X-DiagInfo": [
+ "GVAP278MB0615"
+ ],
+ "x-EwsHandler": [
+ "FindFolder"
+ ],
+ "X-FEEFZInfo": [
+ "GVA"
+ ],
+ "X-FEProxyInfo": [
+ "GV0P278CA0060"
+ ],
+ "X-FEServer": [
+ "WA0P291CA0004"
+ ],
+ "x-ms-appId": [
+ "6ce1e52b-cd1d-4663-abc3-368fba0bfae7"
+ ],
+ "X-Proxy-BackendServerStatus": [
+ "200"
+ ],
+ "X-Proxy-RoutingCorrectness": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedPath": [
+ "1"
+ ],
+ "X-RUM-NotUpdateQueriedDbCopy": [
+ "1"
+ ],
+ "X-RUM-Validated": [
+ "1"
+ ],
+ "X-FirstHopCafeEFZ": [
+ "WAW"
+ ],
+ "Strict-Transport-Security": [
+ "max-age=31536000; includeSubDomains"
+ ],
+ "Set-Cookie": [
+ "exchangecookie=faf9d40d8bc14d53a013ecd1336e35b5; expires=Fri, 11-Jul-2025 12:51:36 GMT; path=\/; secure; HttpOnly"
+ ],
+ "Date": [
+ "Thu, 11 Jul 2024 12:51:36 GMT"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/src/tests/data/ews/initial/0.xml b/src/tests/data/ews/initial/0.xml
new file mode 100644
index 00000000..34fda8ad
--- /dev/null
+++ b/src/tests/data/ews/initial/0.xml
@@ -0,0 +1,7139 @@
+
+
+
+
+
+
+
+
+
+ NoError
+
+
+
+
+
+ IPF.Note
+ AllCategorizedItems
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ AllContacts
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ AllContactsExtended
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF
+ AllItems
+ 230
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 110
+
+
+
+
+ IPF.Note
+ AllPersonMetadata
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Task
+ AllTodoTasks
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ApplicationDataRoot
+ 0
+ 56
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 00000002-0000-0ff1-ce00-000000000000
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ HxNotificationSettings
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 00000006-0000-0ff1-ce00-000000000000
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GlobalSearchHistory
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 13937bba-652e-4c46-b222-3003f4d1ff97
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ContextDataFast
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ContextMetadata
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserInteractionsFast
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 1caee58f-eb14-4a6b-9339-1fe2ddf6692b
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Recent
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Settings
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 1e70cd27-4707-4589-8ec5-9bd20c472a46
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 224fffa9-a547-4c78-ab95-3aacd66c3e99
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FeaturesManagementCollection_1
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 32d4b5e5-7d33-4e7f-b073-f8cffbbb47a1
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ outlookfavorites
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 33b68b23-a6c2-4684-99a0-fa3832792226
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AggregationV2BackfillStateCollection
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ RelevanceDailyAggregation
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 34421fbe-f100-4e5b-9c46-2fea25aa7b88
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Contacts
+ 6
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 35d54a08-36c9-4847-9018-93934c62740c
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PeoplePredictions.FeedbackEntries
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PeoplePredictions.RequestEntries
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PeoplePredictions.UserProfile
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 3873c4f1-2162-4ddb-96e6-232e4eddeedc
+ 0
+ 4
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SEED_PeopleIntegrityCheckerStateCollection_v2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SEED_ScdIntegrityCheckerStateCollection_v2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 394866fc-eedb-4f01-8536-3ff84b16be2a
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AssociationBackfillStatus
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Favorites
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ InsightInstancesActions
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 3b2e5a14-128d-48aa-b581-482aac616d32
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 3c896ded-22c5-450f-91f6-3d1ef0848f6e
+ 0
+ 42
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActionExecutions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActivitiesDaily
+ 58
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActivitiesWeekly
+ 8
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AfterHoursEmailImpact
+ 50
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AutomaticRepliesHistory
+ 105
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ChatsInterruptionStatistics
+ 49
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ComputeLogs
+ 38
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CumulativeNetworkSnapshot
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CumulativeOutOfOfficeClustering
+ 106
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DailyActivityTimeBins
+ 56
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DailyAppointments
+ 85
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DailyInteractions
+ 50
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DailyNetworkSnapshot
+ 51
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DelayedEmail
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DeliverCardEvents
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DetailedMeetings
+ 51
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EmailActionStatistics
+ 50
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EmailGrazerData
+ 50
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EmfResponses
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FoodForThoughtRanking
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ HeterogeneousItems
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ImportantContact
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ InsightsEngagement
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ MailActivityCompactSignalBatch
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ManagementOperationExecutionRecords
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ MeetingActionStatistics
+ 9
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ MeetingResponseActivity
+ 6
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutcomeStates
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutOfOffice
+ 44
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ProgramMetrics
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SegmentsStateRecord
+ 18
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SentMailLogs
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SigsChatsAndCallsCompactSignalBatch
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UnifiedCardEngagements
+ 30
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserConfiguration
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserProgramInstances
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyEmbeddingBasedUserTimePrediction
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyInteractions
+ 27
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyOutOfOfficeAndWorkingDay
+ 28
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyOutOfRoutineMeetingScorePDF
+ 30
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyOutOfRoutineMeetingTimes
+ 61
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WeeklyWorkplaceActivityEmbedding
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 3e3c8645-20a6-4c6e-8355-c34cbbe04585
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PintSigsMsGraphState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 42c2c67f-2760-4f42-b0c4-ef5c3854a0c2
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CollabObjects
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 43375d74-c6a5-4d4e-a0a3-de139860ea75
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ElcArchiveProcessorStatsSdsCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OverallArchivingStatsSdsCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 441509e5-a165-4363-8ee7-bcf0b7d26739
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GenericWorkflowProcessor.SessionManager.Data
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Idf
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ IdfMeeting
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 4787c7ff-7cea-43db-8d0d-919f15c6354b
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ VivaSales
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 48af08dc-f6d2-435f-b2a7-069abd99c086
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ InsightsProvidersSettings
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 49499048-0129-47f5-b95e-f9d315b861a6
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutlookAccountCloudSettings
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutlookCloudSettings
+ 5
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 497effe9-df71-4043-a8bb-14cf78c4b63b
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EACUserPreferences
+ 5
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 4e445925-163e-42ca-b801-9073bfa46d17
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ NewsSubscriptionSourcesv2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 59cc317d-5453-4fcd-ab98-60552b158926
+ 0
+ 6
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AlgorithmRunStatus
+ 9
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CDDS_SuggestionCheckpoint
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ContactFeedback
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ContactMergeSuggestions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ HiddenDuplicates
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ HideSettings
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 644c1b11-f63f-45fa-826b-a9d2801db711
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ _PolicyContainer
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ YWxlY0BhcGhlbGVpYWl0Lm9ubWljcm9zb2Z0LmNvbQ==_LabelFile
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ YWxlY0BhcGhlbGVpYWl0Lm9ubWljcm9zb2Z0LmNvbQ==_PolicyContainer
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 66a88757-258c-4c72-893c-3e8bed4d6899
+ 0
+ 31
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ScopeEntities
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.Apps
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.CalendarEvents
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.CollaborationNetworkEntities
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.ContentDomain
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EmailEntities
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EmailTokens
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe.Bookmarks
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe.Links
+ 4
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe.Locations
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.EntityServe.Qnas
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.FreshHistory
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.FreshSharePointFiles
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.FreshSharePointSites
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.GroupsRoomsMiscIndex
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.Notes
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.People
+ 11
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.PeopleCache
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.PeopleIndex
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.RecentEmailItems
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.RecommendationsService
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.RecommendationsServiceProcessor
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.Scope
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.SearchHistory.Main
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.SearchHistoryState
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.SharePointDocuments
+ 5
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.SkillEntities
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.TeamsAndChannels
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.TeamsEntities
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SubstrateSearch.Topics
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 6cd5135b-7a07-43ec-acc2-b5e27dc628cb
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ currentuser
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ neighbors
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OfficeFeedSdsCacheCollection
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 766ef332-38e5-4cb4-920c-baa478e39fd9
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CrawlerExecutionInfoCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 7a5fbd1c-3e6d-461a-9075-83049393b3a7
+ 0
+ 7
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AcronymAggregationState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AcronymService.SessionManager.Data
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ConceptAggregationSessionManager.Data
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ NicknamesAllInbox
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SimpleAcronymsIndex
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SimpleAcronymsIndexElysium
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SimpleUserContextIndex
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 7aadb841-2368-4d24-a373-94d0ba1f077a
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ outlookspaces
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 7ae974c5-1af7-4923-af3a-fb1fd14dcb7e
+ 0
+ 9
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AppBarTiles
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FloodgateSurvey
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GetStartedStore
+ 20
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ lightning
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ LightningSharedStore
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ LightningStore
+ 67
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutlookEndpointDataCollection
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ OutlookOptionsV3
+ 7
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ WhatsNewStore
+ 97
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 7e02887d-df1d-45a4-af52-dcee2e585eee
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserEnablementStatus
+ 8
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 80723a00-368e-4d64-8281-210e49e593a8
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActivityFeed_201905
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 82d8ab62-be52-a567-14ea-1616c4ee06c4
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ActorAnalyticsProcessorState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 8a18aa92-0a1e-4e06-abd0-e118fa4787b1
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GraphMetaColl
+ 26
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 8c22b648-ee54-4ece-a4ca-3015b6d24f8e
+ 0
+ 20
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DIF
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Images
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_contactsync
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_meetinginsightssetting
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_officegraphvisibilitysetting
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceaad
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceconnector
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceemailaddressdisplaynames
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcemanagerchain
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcemsgraph
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcenamepronunciation
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcenone
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceorganizationheadcount
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourcephotoetag
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceupa
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_sourceuserdefined
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ source_userdefinedsettings
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SQI_FI
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ view_profilev2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ view_settingsv2
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 94c63fef-13a3-47bc-8074-75af8c65887a
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ DelveCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ 97cb1f73-50df-47d1-8fb0-0271f2728514
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CleanupProcessorState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ a3883eba-fbe9-48bd-9ed3-dca3e0e84250
+ 0
+ 4
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ AggregatedLinks
+ 9
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EmailsIndexV1
+ 65
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ LinkFiltersMetadataIndex
+ 3
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ LinksIndex
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ a6061a71-f529-4ca2-bc9d-c8085ecbf3ca
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ EdgeSettings
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ adcd8967-0799-4ec0-bfc8-dcf85fd84767
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CssProcessingState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ae8e128e-080f-4086-b0e3-4c19301ada69
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Scheduling
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ b669c6ea-1adf-453f-b8bc-6d526592b419
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FocusedInboxMailboxData
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ NudgeCandidates
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ScenarioTracking
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ b6e65498-8dce-4d57-8de9-1269fcfcf6ce
+ 0
+ 6
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ KeyphraseRelationshipsSessionManager.Data
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserDocKpeStats
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserDocWithKpes
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserKpes
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserKpeState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserStatistics
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ c9224372-5534-42cb-a48b-8db4f4a3892e
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ TeamsCalendar.SSMeetingPropsSync
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ c94eb245-f5ab-4fea-a534-841091469193
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ ic3usersettings
+ 13
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ d297b9ff-5d5b-4bce-8ed4-eedb1431bf61
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ NucleusProperties
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ d396de1f-10d4-4023-aae2-5bb3d724ba9a
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ FolderGroups-DefaultCollection
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ settings
+ 6
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ TasksDataMarker_v9
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ d71dfe16-1070-48f3-bd3a-c3ec919d34e7
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ TxpAutoblocking
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ TxpUserSettings
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ d8cbf452-c2df-4756-95a5-8aa23fedc3ce
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ UserMigrationDetails
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ e69932cd-f814-4087-8ab1-5ab3f1ad18eb
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ e7762c80-392b-4278-80fa-a8fda80b129c
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ CMUState
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ e7f049f2-9dc5-4301-88a2-121b268ad42f
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ Common
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SharingSuggestion
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ f08262d6-bf02-4e1e-bdef-e95e2f0a940f
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ GlowCollection
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ f164ee43-46e5-4f7c-b5ff-2d1abcd4ce7e
+ 0
+ 1
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ PersonalProfileVector_AnalyzerStates
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ f3f64fec-ecee-4e9a-9eed-a36a5e421300
+ 0
+ 10
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ IdfEmailPoi
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ IdfInboxPoi
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ IdfMeetingPoi
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ InferredLanguage
+ 2
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SlpJobs
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechNGrams
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechTrimmedLms
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechUserDocumentNGrams
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechUserDocumentSentences
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPM.ApplicationData
+ SpeechUserDocumentsHistory
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ BrokerSubscriptions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ BulkActions
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ CalendarSharingCacheCollection
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Common Views
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ComplianceMetadata
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Connectors
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ConnectorConfigurations
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.CrawlerData
+ CrawlerData
+ 35
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Deferred Action
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.StoreItem.DlpPolicyEvaluation
+ DlpPolicyEvaluation
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ Document Centric Conversations
+ 0
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ExchangeODataSyncData
+ 199
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ ExchangeSyncData
+ 0
+ 3
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ AirSync-iPhone-J97G80P7O916504POMJIHAJE70
+ 10
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ AirSync-TestActiveSyncConnectivity-430847304
+ 5
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Hx-Outlook-B57C063B9A6C46ECAD4C159C1E161B14
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ OutlookServiceActionQueue
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Policy
+ 1
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ Finder
+ 0
+ 2
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 0
+
+
+
+
+ IPF.Note
+ OwaFV15.1AllFocusedAQMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NABkNDgALgAAA722UNhKUbZNrFYGsDU7Na0BACzZqOGSdeVFg3+lsLwmpuUAAAIBDAAAAA==
+ 101
+ 0
+
+ true
+ true
+ true
+ true
+ true
+ true
+ true
+
+ 95
+
+
+
+
+ IPF.Note
+ OwaFV15.1AllOtherAQMkAGEzOGRlODRiLTBkN2ItNDgwZS04ZDJmLTM5NDEyY2Q0NABkNDgALgAAA722UNhKUbZNrFYGsDU7Na0BACzZqOGSdeVFg3+lsLwmpuUAAAIBDAAAAA==
+ 15
+ 0
+