diff --git a/src/app/Console/Commands/UserMigrate.php b/src/app/Console/Commands/UserMigrate.php index be277415..af32ad7f 100644 --- a/src/app/Console/Commands/UserMigrate.php +++ b/src/app/Console/Commands/UserMigrate.php @@ -1,47 +1,49 @@ argument('user'); - $pass = $this->argument('password'); + $src = new DataMigrator\Account($this->argument('src')); + $dst = new DataMigrator\Account($this->argument('dst')); - DataMigrator::migrate($user, $pass); + DataMigrator::migrate($src, $dst); } } diff --git a/src/app/DataMigrator.php b/src/app/DataMigrator.php index 15a90d29..b53e7a6c 100644 --- a/src/app/DataMigrator.php +++ b/src/app/DataMigrator.php @@ -1,23 +1,25 @@ migrate($user, $password); + $driver->migrate($source, $destination); } } diff --git a/src/app/DataMigrator/Account.php b/src/app/DataMigrator/Account.php new file mode 100644 index 00000000..cb0e977e --- /dev/null +++ b/src/app/DataMigrator/Account.php @@ -0,0 +1,55 @@ +:" + $url = parse_url($input); + + if ($url === false || !array_key_exists('user', $url)) { + list($user, $password) = explode(':', $input, 2); + $url = ['user' => $user, 'pass' => $password]; + } + + if (isset($url['user'])) { + $this->username = $url['user']; + } + + if (isset($url['pass'])) { + $this->password = $url['pass']; + } + + if (isset($url['host'])) { + $this->location = $input; + } + + if (strpos($this->username, '@')) { + $this->email = $this->username; + } + } +} diff --git a/src/app/DataMigrator/EWS.php b/src/app/DataMigrator/EWS.php index 21e8af21..b22ea722 100644 --- a/src/app/DataMigrator/EWS.php +++ b/src/app/DataMigrator/EWS.php @@ -1,232 +1,258 @@ writeln($line); + } + + /** + * Return destination account + */ + public function getDestination() + { + return $this->destination; + } + + /** + * Return source account + */ + public function getSource() + { + return $this->source; + } /** * Execute migration for the specified user */ - public function migrate(string $user, string $password): void + public function migrate(Account $source, Account $destination): void { + $this->source = $source; + $this->destination = $destination; + // Autodiscover and authenticate the user - $this->authenticate($user, $password); + $this->authenticate($source->username, $source->password); // We'll store output in storage/ tree - $this->location = storage_path('export/') . $user; + $this->location = storage_path('export/') . $source->email; if (!file_exists($this->location)) { mkdir($this->location, 0740, true); } $this->debug("Logged in. Fetching folders hierarchy..."); $folders = $this->getFolders(); foreach ($folders as $folder) { $this->debug("Syncing folder {$folder['fullname']}..."); if ($folder['total'] > 0) { $this->syncItems($folder); } } $this->debug("Done."); } /** * Autodiscover the server and authenticate the user */ protected function authenticate(string $user, string $password): void { // 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. $api = API\ExchangeAutodiscover::getAPI($user, $password); $server = $api->getClient()->getServer(); $version = $api->getClient()->getVersion(); $this->debug("Connected to $server ($version). Authenticating..."); $this->api = API::withUsernameAndPassword($server, $user, $password, [ 'version' => $version ]); } /** * Get folders hierarchy */ protected function getFolders(): array { // Get full folders hierarchy $options = [ 'Traversal' => 'Deep', ]; $folders = $this->api->getChildrenFolders('root', $options); $result = []; foreach ($folders as $folder) { $class = $folder->getFolderClass(); // Skip folder types we do not support if (!in_array($class, $this->folder_classes)) { continue; } $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] = [ 'id' => $folder->getFolderId(), 'total' => $folder->getTotalCount(), 'class' => $class, 'name' => $name, 'fullname' => $fullname, 'location' => $this->location . '/' . $fullname, ]; } return $result; } /** * Synchronize specified folder */ protected function syncItems(array $folder): void { $request = [ // Exchange's maximum is 1000 'IndexedPageItemView' => ['MaxEntriesReturned' => 100, 'Offset' => 0, 'BasePoint' => 'Beginning'], 'ParentFolderIds' => $folder['id']->toArray(true), '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); foreach ($response as $item) { $this->syncItem($item, $folder); } // Request other pages until we got all while (!$response->isIncludesLastItemInRange()) { $response = $this->api->getNextPage($response); foreach ($response as $item) { $this->syncItem($item, $folder); } } } /** * Synchronize specified object */ protected function syncItem(Type $item, array $folder): void { if ($driver = EWS\Item::factory($this, $item, $folder)) { $driver->syncItem($item); return; } // TODO IPM.Note (email) and IPM.StickyNote // Note: iTip messages in mail folders may have different class assigned // https://docs.microsoft.com/en-us/office/vba/outlook/Concepts/Forms/item-types-and-message-classes $this->debug("Unsupported object type: {$item->getItemClass()}. Skiped."); } - - /** - * Print progress/debug information - */ - public function debug($line) - { - // TODO: When not in console mode we should - // not write to stdout, but to log - $output = new \Symfony\Component\Console\Output\ConsoleOutput; - $output->writeln($line); - } }