Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117933541
D3184.1775468766.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
56 KB
Referenced Files
None
Subscribers
None
D3184.1775468766.diff
View Options
diff --git a/src/app/Console/Command.php b/src/app/Console/Command.php
--- a/src/app/Console/Command.php
+++ b/src/app/Console/Command.php
@@ -29,6 +29,31 @@
*/
protected $dangerous = false;
+
+ /**
+ * Shortcut to creating a progress bar of a particular format with a particular message.
+ *
+ * @param int $count Number of progress steps
+ * @param string $message The description
+ *
+ * @return \Symfony\Component\Console\Helper\ProgressBar
+ */
+ protected function createProgressBar($count, $message = null)
+ {
+ $bar = $this->output->createProgressBar($count);
+ $bar->setFormat(
+ '%current:7s%/%max:7s% [%bar%] %percent:3s%% %elapsed:7s%/%estimated:-7s% %message% '
+ );
+
+ if ($message) {
+ $bar->setMessage("{$message}...");
+ }
+
+ $bar->start();
+
+ return $bar;
+ }
+
/**
* Find the domain.
*
diff --git a/src/app/Console/Commands/Data/Import/IP4NetsCommand.php b/src/app/Console/Commands/Data/Import/IP4NetsCommand.php
--- a/src/app/Console/Commands/Data/Import/IP4NetsCommand.php
+++ b/src/app/Console/Commands/Data/Import/IP4NetsCommand.php
@@ -57,11 +57,7 @@
continue;
}
- $bar = \App\Utils::createProgressBar(
- $this->output,
- $numLines,
- "Importing IPv4 Networks from {$file}"
- );
+ $bar = $this->createProgressBar($numLines, "Importing IPv4 Networks from {$file}");
$fp = fopen($file, 'r');
diff --git a/src/app/Console/Commands/Data/Import/IP6NetsCommand.php b/src/app/Console/Commands/Data/Import/IP6NetsCommand.php
--- a/src/app/Console/Commands/Data/Import/IP6NetsCommand.php
+++ b/src/app/Console/Commands/Data/Import/IP6NetsCommand.php
@@ -57,11 +57,7 @@
continue;
}
- $bar = \App\Utils::createProgressBar(
- $this->output,
- $numLines,
- "Importing IPv6 Networks from {$file}"
- );
+ $bar = $this->createProgressBar($numLines, "Importing IPv6 Networks from {$file}");
$fp = fopen($file, 'r');
diff --git a/src/app/Console/Commands/Data/Import/LdifCommand.php b/src/app/Console/Commands/Data/Import/LdifCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Data/Import/LdifCommand.php
@@ -0,0 +1,1000 @@
+<?php
+
+namespace App\Console\Commands\Data\Import;
+
+use App\Console\Command;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+class LdifCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'data:import:ldif {file} {owner} {--force}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Migrate data from an LDIF file';
+
+ /** @var array Aliases email addresses of the owner */
+ protected $aliases = [];
+
+ /** @var array List of imported domains */
+ protected $domains = [];
+
+ /** @var ?string LDAP DN of the account owner */
+ protected $ownerDN;
+
+ /** @var array Packages information */
+ protected $packages = [];
+
+ /** @var ?\App\Wallet A wallet of the account owner */
+ protected $wallet;
+
+ /** @var string Temp table name */
+ protected static $table = 'tmp_ldif_import';
+
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ ini_set("memory_limit", "2048M");
+
+ // (Re-)create temporary table
+ Schema::dropIfExists(self::$table);
+ Schema::create(
+ self::$table,
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->text('dn')->index();
+ $table->string('type')->nullable()->index();
+ $table->text('data')->nullable();
+ $table->text('error')->nullable();
+ $table->text('warning')->nullable();
+ }
+ );
+
+ // Import data from the file to the temp table
+ $this->loadFromFile();
+
+ // Check for errors in the data, print them and abort (if not using --force)
+ if ($this->printErrors()) {
+ return 1;
+ }
+
+ // Prepare packages/skus information
+ $this->preparePackagesAndSkus();
+
+ // Import the account owner first
+ $this->importOwner();
+
+ // Import domains first
+ $this->importDomains();
+
+ // Import other objects
+ $this->importUsers();
+ $this->importSharedFolders();
+ $this->importResources();
+ $this->importGroups();
+
+ // Print warnings collected in the whole process
+ $this->printWarnings();
+
+ // Finally, drop the temp table
+ Schema::dropIfExists(self::$table);
+ }
+
+ /**
+ * Check if a domain exists
+ */
+ protected function domainExists($domain): bool
+ {
+ return in_array($domain, $this->domains);
+ }
+
+ /**
+ * Load data from the LDIF file into the temp table
+ */
+ protected function loadFromFile(): void
+ {
+ $file = $this->argument('file');
+
+ $numLines = \App\Utils::countLines($file);
+
+ $bar = $this->createProgressBar($numLines, "Parsing input file");
+
+ $fh = fopen($file, 'r');
+
+ $inserts = [];
+ $entry = [];
+ $lastAttr = null;
+
+ $insertFunc = function ($limit = 0) use (&$entry, &$inserts) {
+ if (!empty($entry)) {
+ if ($entry = $this->parseLDAPEntry($entry)) {
+ $inserts[] = $entry;
+ }
+ $entry = [];
+ }
+
+ if (count($inserts) > $limit) {
+ DB::table(self::$table)->insert($inserts);
+ $inserts = [];
+ }
+ };
+
+ while (!feof($fh)) {
+ $line = rtrim(fgets($fh));
+
+ $bar->advance();
+
+ if (trim($line) === '' || $line[0] === '#') {
+ continue;
+ }
+
+ if (substr($line, 0, 3) == 'dn:') {
+ $insertFunc(20);
+ $entry['dn'] = strtolower(substr($line, 4));
+ $lastAttr = 'dn';
+ } elseif (substr($line, 0, 1) == ' ') {
+ if (is_array($entry[$lastAttr])) {
+ $elemNum = count($entry[$lastAttr]) - 1;
+ $entry[$lastAttr][$elemNum] .= ltrim($line);
+ } else {
+ $entry[$lastAttr] .= ltrim($line);
+ }
+ } else {
+ list ($attr, $remainder) = explode(':', $line, 2);
+ $attr = strtolower($attr);
+
+ if ($remainder[0] === ':') {
+ $remainder = base64_decode(substr($remainder, 2));
+ } else {
+ $remainder = ltrim($remainder);
+ }
+
+ if (array_key_exists($attr, $entry)) {
+ if (!is_array($entry[$attr])) {
+ $entry[$attr] = [$entry[$attr]];
+ }
+
+ $entry[$attr][] = $remainder;
+ } else {
+ $entry[$attr] = $remainder;
+ }
+
+ $lastAttr = $attr;
+ }
+ }
+
+ $insertFunc();
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import domains from the temp table
+ */
+ protected function importDomains(): void
+ {
+ $domains = DB::table(self::$table)->where('type', 'domain')->whereNull('error')->get();
+
+ $bar = $this->createProgressBar(count($domains), "Importing domains");
+
+ foreach ($domains as $_domain) {
+ $bar->advance();
+
+ $data = json_decode($_domain->data);
+
+ $domain = \App\Domain::withTrashed()->where('namespace', $data->namespace)->first();
+
+ if ($domain) {
+ $this->setImportWarning($_domain->id, "Domain already exists");
+ continue;
+ }
+
+ $domain = \App\Domain::create([
+ 'namespace' => $data->namespace,
+ 'type' => \App\Domain::TYPE_EXTERNAL,
+ ]);
+
+ // Entitlements
+ $domain->assignPackageAndWallet($this->packages['domain'], $this->wallet);
+
+ $this->domains[] = $domain->namespace;
+
+ if (!empty($data->aliases)) {
+ foreach ($data->aliases as $alias) {
+ $alias = strtolower($alias);
+ $domain = \App\Domain::withTrashed()->where('namespace', $alias)->first();
+
+ if ($domain) {
+ $this->setImportWarning($_domain->id, "Domain already exists");
+ continue;
+ }
+
+ $domain = \App\Domain::create([
+ 'namespace' => $alias,
+ 'type' => \App\Domain::TYPE_EXTERNAL,
+ ]);
+
+ // Entitlements
+ $domain->assignPackageAndWallet($this->packages['domain'], $this->wallet);
+
+ $this->domains[] = $domain->namespace;
+ }
+ }
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import groups from the temp table
+ */
+ protected function importGroups(): void
+ {
+ $groups = DB::table(self::$table)->where('type', 'group')->whereNull('error')->get();
+
+ $bar = $this->createProgressBar(count($groups), "Importing groups");
+
+ foreach ($groups as $_group) {
+ $bar->advance();
+
+ $data = json_decode($_group->data);
+
+ // Collect group member email addresses
+ $members = $this->resolveUserDNs($data->members);
+
+ if (empty($members)) {
+ $this->setImportWarning($_group->id, "Members resolve to an empty array");
+ continue;
+ }
+
+ $group = \App\Group::withTrashed()->where('email', $data->email)->first();
+
+ if ($group) {
+ $this->setImportWarning($_group->id, "Group already exists");
+ continue;
+ }
+
+ // Make sure the domain exists
+ if (!$this->domainExists($data->domain)) {
+ $this->setImportWarning($_group->id, "Domain not found");
+ continue;
+ }
+
+ $group = \App\Group::create([
+ 'name' => $data->name,
+ 'email' => $data->email,
+ 'members' => $members,
+ ]);
+
+ $group->assignToWallet($this->wallet);
+
+ // Sender policy
+ if (!empty($data->sender_policy)) {
+ $group->setSetting('sender_policy', json_encode($data->sender_policy));
+ }
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import resources from the temp table
+ */
+ protected function importResources(): void
+ {
+ $resources = DB::table(self::$table)->where('type', 'resource')->whereNull('error')->get();
+
+ $bar = $this->createProgressBar(count($resources), "Importing resources");
+
+ foreach ($resources as $_resource) {
+ $bar->advance();
+
+ $data = json_decode($_resource->data);
+
+ $resource = \App\Resource::withTrashed()
+ ->where('name', $data->name)
+ ->where('email', 'like', '%@' . $data->domain)
+ ->first();
+
+ if ($resource) {
+ $this->setImportWarning($_resource->id, "Resource already exists");
+ continue;
+ }
+
+ // Resource invitation policy
+ if (!empty($data->invitation_policy) && $data->invitation_policy == 'manual') {
+ $members = empty($data->owner) ? [] : $this->resolveUserDNs([$data->owner]);
+
+ if (empty($members)) {
+ $this->setImportWarning($_resource->id, "Failed to resolve the resource owner");
+ $data->invitation_policy = null;
+ } else {
+ $data->invitation_policy = 'manual:' . $members[0];
+ }
+ }
+
+ // Make sure the domain exists
+ if (!$this->domainExists($data->domain)) {
+ $this->setImportWarning($_resource->id, "Domain not found");
+ continue;
+ }
+
+ $resource = new \App\Resource();
+ $resource->name = $data->name;
+ $resource->domain = $data->domain;
+ $resource->save();
+
+ $resource->assignToWallet($this->wallet);
+
+ // Invitation policy
+ if (!empty($data->invitation_policy)) {
+ $resource->setSetting('invitation_policy', $data->invitation_policy);
+ }
+
+ // Target folder
+ if (!empty($data->folder)) {
+ $resource->setSetting('folder', $data->folder);
+ }
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import shared folders from the temp table
+ */
+ protected function importSharedFolders(): void
+ {
+ $folders = DB::table(self::$table)->where('type', 'sharedFolder')->whereNull('error')->get();
+
+ $bar = $this->createProgressBar(count($folders), "Importing shared folders");
+
+ foreach ($folders as $_folder) {
+ $bar->advance();
+
+ $data = json_decode($_folder->data);
+
+ $folder = \App\SharedFolder::withTrashed()
+ ->where('name', $data->name)
+ ->where('email', 'like', '%@' . $data->domain)
+ ->first();
+
+ if ($folder) {
+ $this->setImportWarning($_folder->id, "Folder already exists");
+ continue;
+ }
+
+ // Make sure the domain exists
+ if (!$this->domainExists($data->domain)) {
+ $this->setImportWarning($_folder->id, "Domain not found");
+ continue;
+ }
+
+ $folder = new \App\SharedFolder();
+ $folder->name = $data->name;
+ $folder->type = $data->type ?? 'mail';
+ $folder->domain = $data->domain;
+ $folder->save();
+
+ $folder->assignToWallet($this->wallet);
+
+ // Invitation policy
+ if (!empty($data->acl)) {
+ $folder->setSetting('acl', json_encode($data->acl));
+ }
+
+ // Target folder
+ if (!empty($data->folder)) {
+ $folder->setSetting('folder', $data->folder);
+ }
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import users from the temp table
+ */
+ protected function importUsers(): void
+ {
+ $users = DB::table(self::$table)->where('type', 'user')->whereNull('error');
+
+ // Skip the (already imported) account owner
+ if ($this->ownerDN) {
+ $users->whereNotIn('dn', [$this->ownerDN]);
+ }
+
+ // Import aliases of the owner, we got from importOwner() call
+ if (!empty($this->aliases) && $this->wallet) {
+ $this->setUserAliases($this->wallet->owner, $this->aliases);
+ }
+
+ $bar = $this->createProgressBar($users->count(), "Importing users");
+
+ foreach ($users->cursor() as $_user) {
+ $bar->advance();
+
+ $this->importSingleUser($_user);
+ }
+
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ /**
+ * Import the account owner (or find it among the existing accounts)
+ */
+ protected function importOwner(): void
+ {
+ // The owner email not found in the import data, try existing users
+ $user = $this->getUser($this->argument('owner'));
+
+ if (!$user && $this->ownerDN) {
+ // The owner email found in the import data
+ $bar = $this->createProgressBar(1, "Importing account owner");
+
+ $user = DB::table(self::$table)->where('dn', $this->ownerDN)->first();
+ $user = $this->importSingleUser($user);
+
+ // TODO: We should probably make sure the user's domain is to be imported too
+ // and/or create it automatically.
+
+ $bar->advance();
+ $bar->finish();
+
+ $this->info("DONE");
+ }
+
+ if (!$user) {
+ $this->error("Unable to find the specified account owner");
+ exit(1);
+ }
+
+ $this->wallet = $user->wallets->first();
+ }
+
+ /**
+ * A helper that imports a single user record
+ */
+ protected function importSingleUser($ldap_user)
+ {
+ $data = json_decode($ldap_user->data);
+
+ $user = \App\User::withTrashed()->where('email', $data->email)->first();
+
+ if ($user) {
+ $this->setImportWarning($ldap_user->id, "User already exists");
+ return;
+ }
+
+ // Make sure the domain exists
+ if ($this->wallet && !$this->domainExists($data->domain)) {
+ $this->setImportWarning($ldap_user->id, "Domain not found");
+ return;
+ }
+
+ $user = \App\User::create(['email' => $data->email]);
+
+ // Entitlements
+ $user->assignPackageAndWallet($this->packages['user'], $this->wallet ?: $user->wallets()->first());
+
+ if (!empty($data->quota)) {
+ $quota = ceil($data->quota / 1024 / 1024) - $this->packages['quota'];
+ if ($quota > 0) {
+ $user->assignSku($this->packages['storage'], $quota);
+ }
+ }
+
+ // User settings
+ if (!empty($data->settings)) {
+ $settings = [];
+ foreach ($data->settings as $key => $value) {
+ $settings[] = [
+ 'user_id' => $user->id,
+ 'key' => $key,
+ 'value' => $value,
+ ];
+ }
+
+ DB::table('user_settings')->insert($settings);
+ }
+
+ // Update password
+ if ($data->password != $user->password_ldap) {
+ \App\User::where('id', $user->id)->update(['password_ldap' => $data->password]);
+ }
+
+ // Import aliases
+ if (!empty($data->aliases)) {
+ if (!$this->wallet) {
+ // This is the account owner creation, at this point we likely do not have
+ // domain records yet, save the aliases to be inserted later (in importUsers())
+ $this->aliases = $data->aliases;
+ } else {
+ $this->setUserAliases($user, $data->aliases);
+ }
+ }
+
+ return $user;
+ }
+
+ /**
+ * Convert LDAP entry into an object supported by the migration tool
+ *
+ * @param array $entry LDAP entry attributes
+ *
+ * @return array Record data for inserting to the temp table
+ */
+ protected function parseLDAPEntry(array $entry): array
+ {
+ $type = null;
+ $data = null;
+ $error = null;
+
+ $ouTypeMap = [
+ 'Shared Folders' => 'sharedfolder',
+ 'Resources' => 'resource',
+ 'Groups' => 'group',
+ 'People' => 'user',
+ 'Domains' => 'domain',
+ ];
+
+ foreach ($ouTypeMap as $ou => $_type) {
+ if (stripos($entry['dn'], ",ou={$ou}")) {
+ $type = $_type;
+ break;
+ }
+ }
+
+ if (!$type) {
+ $error = "Unknown record type";
+ }
+
+ if (empty($error)) {
+ $method = 'parseLDAP' . ucfirst($type);
+ list($data, $error) = $this->{$method}($entry);
+
+ if (empty($data['domain']) && !empty($data['email'])) {
+ $data['domain'] = explode('@', $data['email'])[1];
+ }
+ }
+
+ return [
+ 'dn' => $entry['dn'],
+ 'type' => $type,
+ 'data' => json_encode($data),
+ 'error' => $error,
+ ];
+ }
+
+ /**
+ * Convert LDAP domain data into Kolab4 "format"
+ */
+ protected function parseLDAPDomain($entry)
+ {
+ $error = null;
+ $result = [];
+
+ if (empty($entry['associateddomain'])) {
+ $error = "Missing 'associatedDomain' attribute";
+ } elseif (!empty($entry['inetdomainstatus']) && $entry['inetdomainstatus'] == 'deleted') {
+ $error = "Domain deleted";
+ } else {
+ $result['namespace'] = strtolower($this->attrStringValue($entry, 'associateddomain'));
+
+ if (is_array($entry['associateddomain']) && count($entry['associateddomain']) > 1) {
+ $result['aliases'] = array_slice($entry['associateddomain'], 1);
+ }
+
+ // TODO: inetdomainstatus = suspended ???
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Convert LDAP group data into Kolab4 "format"
+ */
+ protected function parseLDAPGroup($entry)
+ {
+ $error = null;
+ $result = [];
+
+ if (empty($entry['cn'])) {
+ $error = "Missing 'cn' attribute";
+ } elseif (empty($entry['mail'])) {
+ $error = "Missing 'mail' attribute";
+ } elseif (empty($entry['uniquemember'])) {
+ $error = "Missing 'uniqueMember' attribute";
+ } else {
+ $result['name'] = $this->attrStringValue($entry, 'cn');
+ $result['email'] = strtolower($this->attrStringValue($entry, 'mail'));
+ $result['members'] = $this->attrArrayValue($entry, 'uniquemember');
+
+ if (!empty($entry['kolaballowsmtpsender'])) {
+ $policy = $this->attrArrayValue($entry, 'kolaballowsmtpsender');
+ $result['sender_policy'] = $this->parseSenderPolicy($policy);
+ }
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Convert LDAP resource data into Kolab4 "format"
+ */
+ protected function parseLDAPResource($entry)
+ {
+ $error = null;
+ $result = [];
+
+ if (empty($entry['cn'])) {
+ $error = "Missing 'cn' attribute";
+ } elseif (empty($entry['mail'])) {
+ $error = "Missing 'mail' attribute";
+ } else {
+ $result['name'] = $this->attrStringValue($entry, 'cn');
+ $result['email'] = strtolower($this->attrStringValue($entry, 'mail'));
+
+ if (!empty($entry['kolabtargetfolder'])) {
+ $result['folder'] = $this->attrStringValue($entry, 'kolabtargetfolder');
+ }
+
+ if (!empty($entry['owner'])) {
+ $result['owner'] = $this->attrStringValue($entry, 'owner');
+ }
+
+ if (!empty($entry['kolabinvitationpolicy'])) {
+ $policy = $this->attrArrayValue($entry, 'kolabinvitationpolicy');
+ $result['invitation_policy'] = $this->parseInvitationPolicy($policy);
+ }
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Convert LDAP shared folder data into Kolab4 "format"
+ */
+ protected function parseLDAPSharedFolder($entry)
+ {
+ $error = null;
+ $result = [];
+
+ if (empty($entry['cn'])) {
+ $error = "Missing 'cn' attribute";
+ } elseif (empty($entry['mail'])) {
+ $error = "Missing 'mail' attribute";
+ } else {
+ $result['name'] = $this->attrStringValue($entry, 'cn');
+ $result['email'] = strtolower($this->attrStringValue($entry, 'mail'));
+
+ if (!empty($entry['kolabfoldertype'])) {
+ $result['type'] = $this->attrStringValue($entry, 'kolabfoldertype');
+ }
+
+ if (!empty($entry['kolabtargetfolder'])) {
+ $result['folder'] = $this->attrStringValue($entry, 'kolabtargetfolder');
+ }
+
+ if (!empty($entry['acl'])) {
+ $result['acl'] = $this->parseACL($this->attrArrayValue($entry, 'acl'));
+ }
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Convert LDAP user data into Kolab4 "format"
+ */
+ protected function parseLDAPUser($entry)
+ {
+ $error = null;
+ $result = [];
+
+ $settingAttrs = [
+ 'givenname' => 'first_name',
+ 'sn' => 'last_name',
+ 'telephonenumber' => 'phone',
+ 'mailalternateaddress' => 'external_email',
+ 'mobile' => 'phone',
+ 'o' => 'organization',
+ // 'address' => 'billing_address'
+ ];
+
+ if (empty($entry['mail'])) {
+ $error = "Missing 'mail' attribute";
+ } else {
+ $result['email'] = strtolower($this->attrStringValue($entry, 'mail'));
+ $result['settings'] = [];
+ $result['aliases'] = [];
+
+ foreach ($settingAttrs as $attr => $setting) {
+ if (!empty($entry[$attr])) {
+ $result['settings'][$setting] = $this->attrStringValue($entry, $attr);
+ }
+ }
+
+ if (!empty($entry['alias'])) {
+ $result['aliases'] = $this->attrArrayValue($entry, 'alias');
+ }
+
+ if (!empty($entry['userpassword'])) {
+ $result['password'] = $this->attrStringValue($entry, 'userpassword');
+ }
+
+ if (!empty($entry['mailquota'])) {
+ $result['quota'] = $this->attrStringValue($entry, 'mailquota');
+ }
+
+ if ($result['email'] == $this->argument('owner')) {
+ $this->ownerDN = $entry['dn'];
+ }
+ }
+
+ return [$result, $error];
+ }
+
+ /**
+ * Print import errors
+ */
+ protected function printErrors(): bool
+ {
+ if ($this->option('force')) {
+ return false;
+ }
+
+ $errors = DB::table(self::$table)->whereNotNull('error')->orderBy('id')
+ ->get()
+ ->map(function ($record) {
+ $this->error("ERROR {$record->dn}: {$record->error}");
+ return $record->id;
+ })
+ ->all();
+
+ return !empty($errors);
+ }
+
+ /**
+ * Print import warnings (for records that do not have an error specified)
+ */
+ protected function printWarnings(): void
+ {
+ DB::table(self::$table)->whereNotNull('warning')->whereNull('error')->orderBy('id')
+ ->each(function ($record) {
+ $this->warn("WARNING {$record->dn}: {$record->warning}");
+ return $record->id;
+ });
+ }
+
+ /**
+ * Convert ldap attribute value to an array
+ */
+ protected static function attrArrayValue($entry, $attribute)
+ {
+ return is_array($entry[$attribute]) ? $entry[$attribute] : [$entry[$attribute]];
+ }
+
+ /**
+ * Convert ldap attribute to a string
+ */
+ protected static function attrStringValue($entry, $attribute)
+ {
+ return is_array($entry[$attribute]) ? $entry[$attribute][0] : $entry[$attribute];
+ }
+
+ /**
+ * Resolve a list of user DNs into email addresses. Makes sure
+ * the returned addresses exist in Kolab4 database.
+ */
+ protected function resolveUserDNs($user_dns): array
+ {
+ // Get email addresses from the import data
+ $users = DB::table(self::$table)->whereIn('dn', $user_dns)
+ ->where('type', 'user')
+ ->whereNull('error')
+ ->get()
+ ->map(function ($user) {
+ $mdata = json_decode($user->data);
+ return $mdata->email;
+ })
+ // Make sure to skip these with unknown domains
+ ->filter(function ($email) {
+ return $this->domainExists(explode('@', $email)[1]);
+ })
+ ->all();
+
+ // Get email addresses for existing Kolab4 users
+ if (!empty($users)) {
+ $users = \App\User::whereIn('email', $users)->get()->pluck('email')->all();
+ }
+
+ return $users;
+ }
+
+ /**
+ * Validate/convert acl to Kolab4 format
+ */
+ protected static function parseACL(array $acl): array
+ {
+ $map = [
+ 'lrswipkxtecdn' => 'full',
+ 'lrs' => 'read-only',
+ 'read' => 'read-only',
+ 'lrswitedn' => 'read-write',
+ ];
+
+ $supportedRights = ['full', 'read-only', 'read-write'];
+
+ foreach ($acl as $idx => $entry) {
+ $parts = explode(',', $entry);
+ $entry = null;
+
+ if (count($parts) == 2) {
+ $label = trim($parts[0]);
+ $rights = trim($parts[1]);
+ $rights = $map[$rights] ?? $rights;
+
+ if (in_array($rights, $supportedRights) && ($label === 'anyone' || strpos($label, '@'))) {
+ $entry = "{$label}, {$rights}";
+ }
+
+ // TODO: Throw an error or log a warning on unsupported acl entry?
+ }
+
+ $acl[$idx] = $entry;
+ }
+
+ return array_values(array_filter($acl));
+ }
+
+ /**
+ * Validate/convert invitation policy to Kolab4 format
+ */
+ protected static function parseInvitationPolicy(array $policies): ?string
+ {
+ foreach ($policies as $policy) {
+ if ($policy == 'ACT_MANUAL') {
+ // 'owner' attribute handling in another place
+ return 'manual';
+ }
+
+ if ($policy == 'ACT_ACCEPT_AND_NOTIFY') {
+ break; // use the default 'accept' (null) policy
+ }
+
+ if ($policy == 'ACT_REJECT') {
+ return 'reject';
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Validate/convert sender policy to Kolab4 format
+ */
+ protected static function parseSenderPolicy(array $rules): array
+ {
+ foreach ($rules as $idx => $rule) {
+ $entry = trim($rule);
+ $rule = null;
+
+ // 'deny' rules aren't supported
+ if (isset($entry[0]) && $entry[0] !== '-') {
+ $rule = $entry;
+ }
+
+ $rules[$idx] = $rule;
+ }
+
+ $rules = array_values(array_filter($rules));
+
+ if (!empty($rules) && $rules[count($rules) - 1] != '-') {
+ $rules[] = '-';
+ }
+
+ return $rules;
+ }
+
+ /**
+ * Get/prepare packages/skus information
+ */
+ protected function preparePackagesAndSkus(): void
+ {
+ // Find the tenant
+ if (empty($this->ownerDN)) {
+ if ($user = $this->getUser($this->argument('owner'))) {
+ $tenant_id = $user->tenant_id;
+ }
+ }
+
+ // TODO: Tenant id could be a command option
+
+ if (empty($tenant_id)) {
+ $tenant_id = \config('app.tenant_id');
+ }
+
+ // TODO: We should probably make package titles configurable with command options
+
+ $this->packages = [
+ 'user' => \App\Package::where('title', 'kolab')->where('tenant_id', $tenant_id)->first(),
+ 'domain' => \App\Package::where('title', 'domain-hosting')->where('tenant_id', $tenant_id)->first(),
+ ];
+
+ // Count storage skus
+ $sku = $this->packages['user']->skus()->where('title', 'storage')->first();
+
+ $this->packages['quota'] = $sku ? $sku->pivot->qty : 0;
+ $this->packages['storage'] = \App\Sku::where('title', 'storage')->where('tenant_id', $tenant_id)->first();
+ }
+
+ /**
+ * Set aliases for the user
+ */
+ protected function setUserAliases(\App\User $user, array $aliases = [])
+ {
+ if (!empty($aliases)) {
+ // Some users might have alias entry with their main address, remove it
+ $aliases = array_map('strtolower', $aliases);
+ $aliases = array_diff(array_unique($aliases), [$user->email]);
+
+ // Remove aliases for domains that do not exist
+ if (!empty($aliases)) {
+ $aliases = array_filter(
+ $aliases,
+ function ($alias) {
+ return $this->domainExists(explode('@', $alias)[1]);
+ }
+ );
+ }
+
+ if (!empty($aliases)) {
+ $user->setAliases($aliases);
+ }
+ }
+ }
+
+ /**
+ * Set error message for specified import data record
+ */
+ protected static function setImportError($id, $error): void
+ {
+ DB::table(self::$table)->where('id', $id)->update(['error' => $error]);
+ }
+
+ /**
+ * Set warning message for specified import data record
+ */
+ protected static function setImportWarning($id, $warning): void
+ {
+ DB::table(self::$table)->where('id', $id)->update(['warning' => $warning]);
+ }
+}
diff --git a/src/app/Console/Commands/MigratePrices.php b/src/app/Console/Commands/MigratePrices.php
--- a/src/app/Console/Commands/MigratePrices.php
+++ b/src/app/Console/Commands/MigratePrices.php
@@ -2,7 +2,7 @@
namespace App\Console\Commands;
-use Illuminate\Console\Command;
+use App\Console\Command;
class MigratePrices extends Command
{
@@ -33,7 +33,7 @@
private function updateSKUs()
{
- $bar = \App\Utils::createProgressBar($this->output, 8, "Updating SKUs");
+ $bar = $this->createProgressBar(8, "Updating SKUs");
// 1. Set the list price for the SKU 'mailbox' to 500.
$bar->advance();
@@ -88,7 +88,7 @@
{
$users = \App\User::all();
- $bar = \App\Utils::createProgressBar($this->output, count($users), "Updating entitlements");
+ $bar = $this->createProgressBar(count($users), "Updating entitlements");
$groupware_sku = \App\Sku::where('title', 'groupware')->first();
$activesync_sku = \App\Sku::where('title', 'activesync')->first();
diff --git a/src/app/Observers/ResourceObserver.php b/src/app/Observers/ResourceObserver.php
--- a/src/app/Observers/ResourceObserver.php
+++ b/src/app/Observers/ResourceObserver.php
@@ -16,7 +16,7 @@
public function creating(Resource $resource): void
{
if (empty($resource->email)) {
- if (!isset($resource->name)) {
+ if (!isset($resource->domain)) {
throw new \Exception("Missing 'domain' property for a new resource");
}
diff --git a/src/app/Observers/SharedFolderObserver.php b/src/app/Observers/SharedFolderObserver.php
--- a/src/app/Observers/SharedFolderObserver.php
+++ b/src/app/Observers/SharedFolderObserver.php
@@ -20,7 +20,7 @@
}
if (empty($folder->email)) {
- if (!isset($folder->name)) {
+ if (!isset($folder->domain)) {
throw new \Exception("Missing 'domain' property for a new shared folder");
}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -24,6 +24,7 @@
* @property string $email
* @property int $id
* @property string $password
+ * @property string $password_ldap
* @property int $status
* @property int $tenant_id
*/
diff --git a/src/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -65,32 +65,6 @@
return self::countryForIP($ip);
}
- /**
- * Shortcut to creating a progress bar of a particular format with a particular message.
- *
- * @param \Illuminate\Console\OutputStyle $output Console output object
- * @param int $count Number of progress steps
- * @param string $message The description
- *
- * @return \Symfony\Component\Console\Helper\ProgressBar
- */
- public static function createProgressBar($output, $count, $message = null)
- {
- $bar = $output->createProgressBar($count);
-
- $bar->setFormat(
- '%current:7s%/%max:7s% [%bar%] %percent:3s%% %elapsed:7s%/%estimated:-7s% %message% '
- );
-
- if ($message) {
- $bar->setMessage($message . " ...");
- }
-
- $bar->start();
-
- return $bar;
- }
-
/**
* Return the number of days in the month prior to this one.
*
diff --git a/src/tests/Feature/Console/Data/Import/LdifTest.php b/src/tests/Feature/Console/Data/Import/LdifTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/Data/Import/LdifTest.php
@@ -0,0 +1,432 @@
+<?php
+
+namespace Tests\Feature\Console\Data\Import;
+
+use Tests\TestCase;
+
+class LdifTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('owner@kolab3.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('owner@kolab3.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test the command
+ */
+ public function testHandle(): void
+ {
+ $code = \Artisan::call("data:import:ldif tests/data/kolab3.ldif owner@kolab3.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+
+ $this->assertStringNotContainsString("Importing", $output);
+ $this->assertStringNotContainsString("WARNING", $output);
+ $this->assertStringContainsString(
+ "ERROR cn=error,ou=groups,ou=kolab3.com,dc=hosted,dc=com: Missing 'mail' attribute",
+ $output
+ );
+ $this->assertStringContainsString(
+ "ERROR cn=error,ou=resources,ou=kolab3.com,dc=hosted,dc=com: Missing 'mail' attribute",
+ $output
+ );
+
+ $code = \Artisan::call("data:import:ldif tests/data/kolab3.ldif owner@kolab3.com --force");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertStringContainsString("Importing domains... DONE", $output);
+ $this->assertStringContainsString("Importing users... DONE", $output);
+ $this->assertStringContainsString("Importing resources... DONE", $output);
+ $this->assertStringContainsString("Importing shared folders... DONE", $output);
+ $this->assertStringContainsString("Importing groups... DONE", $output);
+ $this->assertStringNotContainsString("ERROR", $output);
+ $this->assertStringContainsString(
+ "WARNING cn=unknowndomain,ou=groups,ou=kolab3.org,dc=hosted,dc=com: Domain not found",
+ $output
+ );
+
+ $owner = \App\User::where('email', 'owner@kolab3.com')->first();
+
+ $this->assertNull($owner->password);
+ $this->assertSame(
+ '{SSHA512}g74+SECTLsM1x0aYkSrTG9sOFzEp8wjCflhshr2DjE7mi1G3iNb4ClH3ljorPRlTgZ105PsQGEpNtNr+XRjigg==',
+ $owner->password_ldap
+ );
+
+ // User settings
+ $this->assertSame('Aleksander', $owner->getSetting('first_name'));
+ $this->assertSame('Machniak', $owner->getSetting('last_name'));
+ $this->assertSame('123456789', $owner->getSetting('phone'));
+ $this->assertSame('external@gmail.com', $owner->getSetting('external_email'));
+ $this->assertSame('Organization AG', $owner->getSetting('organization'));
+
+ // User aliases
+ $aliases = $owner->aliases()->orderBy('alias')->pluck('alias')->all();
+ $this->assertSame(['alias@kolab3-alias.com', 'alias@kolab3.com'], $aliases);
+
+ // Wallet, entitlements
+ $wallet = $owner->wallets->first();
+
+ $this->assertEntitlements($owner, [
+ 'groupware',
+ 'mailbox',
+ 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage', 'storage',
+ ]);
+
+ // Users
+ $this->assertSame(2, $owner->users(false)->count());
+ $user = $owner->users(false)->where('email', 'user@kolab3.com')->first();
+
+ // User settings
+ $this->assertSame('Jane', $user->getSetting('first_name'));
+ $this->assertSame('Doe', $user->getSetting('last_name'));
+ $this->assertSame('1234567890', $user->getSetting('phone'));
+ $this->assertSame('ext@gmail.com', $user->getSetting('external_email'));
+ $this->assertSame('Org AG', $user->getSetting('organization'));
+
+ // User aliases
+ $aliases = $user->aliases()->orderBy('alias')->pluck('alias')->all();
+ $this->assertSame(['alias2@kolab3.com'], $aliases);
+
+ $this->assertEntitlements($user, [
+ 'groupware',
+ 'mailbox',
+ 'storage', 'storage', 'storage', 'storage', 'storage',
+ ]);
+
+ // Domains
+ $domains = $owner->domains(false, false)->orderBy('namespace')->get();
+
+ $this->assertCount(2, $domains);
+ $this->assertSame('kolab3-alias.com', $domains[0]->namespace);
+ $this->assertSame('kolab3.com', $domains[1]->namespace);
+ $this->assertSame(\App\Domain::TYPE_EXTERNAL, $domains[0]->type);
+ $this->assertSame(\App\Domain::TYPE_EXTERNAL, $domains[1]->type);
+
+ $this->assertEntitlements($domains[0], ['domain-hosting']);
+ $this->assertEntitlements($domains[1], ['domain-hosting']);
+
+ // Shared folders
+ $folders = $owner->sharedFolders(false)->orderBy('email')->get();
+
+ $this->assertCount(2, $folders);
+ $this->assertMatchesRegularExpression('/^event-[0-9]+@kolab3\.com$/', $folders[0]->email);
+ $this->assertMatchesRegularExpression('/^mail-[0-9]+@kolab3\.com$/', $folders[1]->email);
+ $this->assertSame('Folder2', $folders[0]->name);
+ $this->assertSame('Folder1', $folders[1]->name);
+ $this->assertSame('event', $folders[0]->type);
+ $this->assertSame('mail', $folders[1]->type);
+ $this->assertSame('["anyone, read-only"]', $folders[0]->getSetting('acl'));
+ $this->assertSame('shared/Folder2@kolab3.com', $folders[0]->getSetting('folder'));
+ $this->assertSame('["anyone, read-write","owner@kolab3.com, full"]', $folders[1]->getSetting('acl'));
+ $this->assertSame('shared/Folder1@kolab3.com', $folders[1]->getSetting('folder'));
+
+ // Groups
+ $groups = $owner->groups(false)->orderBy('email')->get();
+
+ $this->assertCount(1, $groups);
+ $this->assertSame('Group', $groups[0]->name);
+ $this->assertSame('group@kolab3.com', $groups[0]->email);
+ $this->assertSame(['owner@kolab3.com', 'user@kolab3.com'], $groups[0]->members);
+ $this->assertSame('["sender@gmail.com","-"]', $groups[0]->getSetting('sender_policy'));
+
+ // Resources
+ $resources = $owner->resources(false)->orderBy('email')->get();
+
+ $this->assertCount(1, $resources);
+ $this->assertSame('Resource', $resources[0]->name);
+ $this->assertMatchesRegularExpression('/^resource-[0-9]+@kolab3\.com$/', $resources[0]->email);
+ $this->assertSame('shared/Resource@kolab3.com', $resources[0]->getSetting('folder'));
+ $this->assertSame('manual:user@kolab3.com', $resources[0]->getSetting('invitation_policy'));
+ }
+
+ /**
+ * Test parseACL() method
+ */
+ public function testParseACL(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $result = $this->invokeMethod($command, 'parseACL', [[]]);
+ $this->assertSame([], $result);
+
+ $acl = [
+ 'anyone, read-write',
+ 'read-only@kolab3.com, read-only',
+ 'read-only@kolab3.com, read',
+ 'full@kolab3.com,full',
+ 'lrswipkxtecdn@kolab3.com, lrswipkxtecdn', // full
+ 'lrs@kolab3.com, lrs', // read-only
+ 'lrswitedn@kolab3.com, lrswitedn', // read-write
+ // unsupported:
+ 'anonymous, read-only',
+ 'group:test, lrs',
+ 'test@kolab3.com, lrspkxtdn',
+ ];
+
+ $expected = [
+ 'anyone, read-write',
+ 'read-only@kolab3.com, read-only',
+ 'read-only@kolab3.com, read-only',
+ 'full@kolab3.com, full',
+ 'lrswipkxtecdn@kolab3.com, full',
+ 'lrs@kolab3.com, read-only',
+ 'lrswitedn@kolab3.com, read-write',
+ ];
+
+ $result = $this->invokeMethod($command, 'parseACL', [$acl]);
+ $this->assertSame($expected, $result);
+ }
+
+ /**
+ * Test parseInvitationPolicy() method
+ */
+ public function testParseInvitationPolicy(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [[]]);
+ $this->assertSame(null, $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['UNKNOWN']]);
+ $this->assertSame(null, $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['ACT_ACCEPT']]);
+ $this->assertSame(null, $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['ACT_MANUAL']]);
+ $this->assertSame('manual', $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['ACT_REJECT']]);
+ $this->assertSame('reject', $result);
+
+ $result = $this->invokeMethod($command, 'parseInvitationPolicy', [['ACT_ACCEPT_AND_NOTIFY', 'ACT_REJECT']]);
+ $this->assertSame(null, $result);
+ }
+
+ /**
+ * Test parseSenderPolicy() method
+ */
+ public function testParseSenderPolicy(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $result = $this->invokeMethod($command, 'parseSenderPolicy', [[]]);
+ $this->assertSame([], $result);
+
+ $result = $this->invokeMethod($command, 'parseSenderPolicy', [['test']]);
+ $this->assertSame(['test', '-'], $result);
+
+ $result = $this->invokeMethod($command, 'parseSenderPolicy', [['test', '-test2', 'test3', '']]);
+ $this->assertSame(['test', 'test3', '-'], $result);
+ }
+
+ /**
+ * Test parseLDAPDomain() method
+ */
+ public function testParseLDAPDomain(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $entry = [];
+ $result = $this->invokeMethod($command, 'parseLDAPDomain', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Missing 'associatedDomain' attribute", $result[1]);
+
+ $entry = ['associateddomain' => 'test.com'];
+ $result = $this->invokeMethod($command, 'parseLDAPDomain', [$entry]);
+ $this->assertSame(['namespace' => 'test.com'], $result[0]);
+ $this->assertSame(null, $result[1]);
+
+ $entry = ['associateddomain' => 'test.com', 'inetdomainstatus' => 'deleted'];
+ $result = $this->invokeMethod($command, 'parseLDAPDomain', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Domain deleted", $result[1]);
+ }
+
+ /**
+ * Test parseLDAPGroup() method
+ */
+ public function testParseLDAPGroup(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $entry = [];
+ $result = $this->invokeMethod($command, 'parseLDAPGroup', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Missing 'cn' attribute", $result[1]);
+
+ $entry = ['cn' => 'Test'];
+ $result = $this->invokeMethod($command, 'parseLDAPGroup', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Missing 'mail' attribute", $result[1]);
+
+ $entry = ['cn' => 'Test', 'mail' => 'test@domain.tld'];
+ $result = $this->invokeMethod($command, 'parseLDAPGroup', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Missing 'uniqueMember' attribute", $result[1]);
+
+ $entry = [
+ 'cn' => 'Test',
+ 'mail' => 'Test@domain.tld',
+ 'uniquemember' => 'uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com',
+ 'kolaballowsmtpsender' => ['sender1@gmail.com', 'sender2@gmail.com'],
+ ];
+
+ $expected = [
+ 'name' => 'Test',
+ 'email' => 'test@domain.tld',
+ 'members' => ['uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com'],
+ 'sender_policy' => ['sender1@gmail.com', 'sender2@gmail.com', '-'],
+ ];
+
+ $result = $this->invokeMethod($command, 'parseLDAPGroup', [$entry]);
+ $this->assertSame($expected, $result[0]);
+ $this->assertSame(null, $result[1]);
+ }
+
+ /**
+ * Test parseLDAPResource() method
+ */
+ public function testParseLDAPResource(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $entry = [];
+ $result = $this->invokeMethod($command, 'parseLDAPResource', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Missing 'cn' attribute", $result[1]);
+
+ $entry = ['cn' => 'Test'];
+ $result = $this->invokeMethod($command, 'parseLDAPResource', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Missing 'mail' attribute", $result[1]);
+
+ $entry = [
+ 'cn' => 'Test',
+ 'mail' => 'Test@domain.tld',
+ 'owner' => 'uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com',
+ 'kolabtargetfolder' => 'Folder',
+ 'kolabinvitationpolicy' => 'ACT_REJECT'
+ ];
+
+ $expected = [
+ 'name' => 'Test',
+ 'email' => 'test@domain.tld',
+ 'folder' => 'Folder',
+ 'owner' => 'uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com',
+ 'invitation_policy' => 'reject',
+ ];
+
+ $result = $this->invokeMethod($command, 'parseLDAPResource', [$entry]);
+ $this->assertSame($expected, $result[0]);
+ $this->assertSame(null, $result[1]);
+ }
+
+ /**
+ * Test parseLDAPSharedFolder() method
+ */
+ public function testParseLDAPSharedFolder(): void
+ {
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+
+ $entry = [];
+ $result = $this->invokeMethod($command, 'parseLDAPSharedFolder', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Missing 'cn' attribute", $result[1]);
+
+ $entry = ['cn' => 'Test'];
+ $result = $this->invokeMethod($command, 'parseLDAPSharedFolder', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Missing 'mail' attribute", $result[1]);
+
+ $entry = [
+ 'cn' => 'Test',
+ 'mail' => 'Test@domain.tld',
+ 'kolabtargetfolder' => 'Folder',
+ 'kolabfoldertype' => 'event',
+ 'acl' => 'anyone, read-write',
+ ];
+
+ $expected = [
+ 'name' => 'Test',
+ 'email' => 'test@domain.tld',
+ 'type' => 'event',
+ 'folder' => 'Folder',
+ 'acl' => ['anyone, read-write'],
+ ];
+
+ $result = $this->invokeMethod($command, 'parseLDAPSharedFolder', [$entry]);
+ $this->assertSame($expected, $result[0]);
+ $this->assertSame(null, $result[1]);
+ }
+
+ /**
+ * Test parseLDAPUser() method
+ */
+ public function testParseLDAPUser(): void
+ {
+ // Note: If we do not initialize the command input we'll get an error
+ $args = [
+ 'file' => 'test.ldif',
+ 'owner' => 'test@domain.tld',
+ ];
+
+ $command = new \App\Console\Commands\Data\Import\LdifCommand();
+ $command->setInput(new \Symfony\Component\Console\Input\ArrayInput($args, $command->getDefinition()));
+
+ $entry = ['cn' => 'Test'];
+ $result = $this->invokeMethod($command, 'parseLDAPUser', [$entry]);
+ $this->assertSame([], $result[0]);
+ $this->assertSame("Missing 'mail' attribute", $result[1]);
+
+ $entry = [
+ 'dn' => 'user dn',
+ 'givenname' => 'Given',
+ 'mail' => 'Test@domain.tld',
+ 'sn' => 'Surname',
+ 'telephonenumber' => '123',
+ 'o' => 'Org',
+ 'mailalternateaddress' => 'test@ext.com',
+ 'alias' => ['test1@domain.tld', 'test2@domain.tld'],
+ 'userpassword' => 'pass',
+ 'mailquota' => '12345678',
+ ];
+
+ $expected = [
+ 'email' => 'test@domain.tld',
+ 'settings' => [
+ 'first_name' => 'Given',
+ 'last_name' => 'Surname',
+ 'phone' => '123',
+ 'external_email' => 'test@ext.com',
+ 'organization' => 'Org',
+ ],
+ 'aliases' => ['test1@domain.tld', 'test2@domain.tld'],
+ 'password' => 'pass',
+ 'quota' => '12345678',
+ ];
+
+ $result = $this->invokeMethod($command, 'parseLDAPUser', [$entry]);
+ $this->assertSame($expected, $result[0]);
+ $this->assertSame(null, $result[1]);
+ $this->assertSame($entry['dn'], $this->getObjectProperty($command, 'ownerDN'));
+ }
+}
diff --git a/src/tests/data/kolab3.ldif b/src/tests/data/kolab3.ldif
new file mode 100644
--- /dev/null
+++ b/src/tests/data/kolab3.ldif
@@ -0,0 +1,119 @@
+dn: associateddomain=kolab3.com,ou=Domains,dc=hosted,dc=com
+objectClass: top
+objectClass: domainrelatedobject
+objectClass: inetdomain
+inetDomainBaseDN: ou=kolab3.com,dc=hosted,dc=com
+associatedDomain: kolab3.com
+associatedDomain: kolab3-alias.com
+
+dn: uid=owner@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+cn: Aleksander Machniak
+displayName: Machniak, Aleksander
+givenName: Aleksander
+sn: Machniak
+o: Organization AG
+l:: U2llbWlhbm93aWNlIMWabMSFc2tpZQ==
+mobile: 123456789
+c: PL
+objectClass: top
+objectClass: inetorgperson
+objectClass: kolabinetorgperson
+objectClass: organizationalperson
+objectClass: mailrecipient
+objectClass: country
+objectClass: person
+mail: owner@kolab3.com
+alias: alias@kolab3.com
+alias: alias@kolab3-alias.com
+mailAlternateAddress: external@gmail.com
+mailHost: imap.hosted.com
+mailQuota: 8388608
+uid: owner@kolab3.com
+userPassword:: e1NTSEE1MTJ9Zzc0K1NFQ1RMc00xeDBhWWtTclRHOXNPRnpFcDh3akNmbGhzaHIyRGpFN21pMUczaU5iNENsSDNsam9yUFJsVGdaMTA1UHNRR0VwTnROcitYUmppZ2c9PQ==
+nsUniqueID: 229dc10c-1b6a11f7-b7c1edc1-0e0f46c4
+createtimestamp: 20170407081419Z
+modifytimestamp: 20200915082359Z
+
+dn: uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+cn: Jane Doe
+displayName: Doe, Jane
+givenName: Jane
+sn: Doe
+o: Org AG
+telephoneNumber: 1234567890
+objectClass: top
+objectClass: inetorgperson
+objectClass: kolabinetorgperson
+objectClass: organizationalperson
+objectClass: mailrecipient
+objectClass: country
+objectClass: person
+mail: user@kolab3.com
+alias: alias2@kolab3.com
+mailAlternateAddress: ext@gmail.com
+mailHost: imap.hosted.com
+mailQuota: 2097152
+uid: user@kolab3.com
+userPassword:: e1NTSEE1MTJ9Zzc0K1NFQ1RMc00xeDBhWWtTclRHOXNPRnpFcDh3akNmbGhzaHIyRGpFN21pMUczaU5iNENsSDNsam9yUFJsVGdaMTA1UHNRR0VwTnROcitYUmppZ2c9PQ==
+nsUniqueID: 229dc20c-1b6a11f7-b7c1edc1-0e0f46c4
+
+dn: cn=Group,ou=Groups,ou=kolab3.com,dc=hosted,dc=com
+cn: Group
+mail: group@kolab3.com
+objectClass: top
+objectClass: groupofuniquenames
+objectClass: kolabgroupofuniquenames
+uniqueMember: uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+uniqueMember: uid=owner@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+kolabAllowSMTPRecipient: recipient@kolab.org
+kolabAllowSMTPSender: sender@gmail.com
+
+dn: cn=Error,ou=Groups,ou=kolab3.com,dc=hosted,dc=com
+cn: Error
+uniqueMember: uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+
+dn: cn=UnknownDomain,ou=Groups,ou=kolab3.org,dc=hosted,dc=com
+cn: UnknownDomain
+mail: unknowndomain@kolab3.org
+objectClass: top
+objectClass: groupofuniquenames
+objectClass: kolabgroupofuniquenames
+uniqueMember: uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+uniqueMember: uid=owner@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+
+dn: cn=Resource,ou=Resources,ou=kolab3.com,dc=hosted,dc=com
+cn: Resource
+mail: resource-car-resource@kolab3.com
+objectClass: top
+objectClass: kolabsharedfolder
+objectClass: kolabresource
+objectClass: mailrecipient
+owner: uid=user@kolab3.com,ou=People,ou=kolab3.com,dc=hosted,dc=com
+kolabAllowSMTPRecipient: recipient@kolab.org
+kolabAllowSMTPSender: sender@gmail.com
+kolabInvitationPolicy: ACT_MANUAL
+kolabTargetFolder: shared/Resource@kolab3.com
+
+dn: cn=Error,ou=Resources,ou=kolab3.com,dc=hosted,dc=com
+cn: Error
+
+dn: cn=Folder1,ou=Shared Folders,ou=kolab3.com,dc=hosted,dc=com
+cn: Folder1
+objectClass: kolabsharedfolder
+objectClass: mailrecipient
+objectClass: top
+kolabFolderType: mail
+kolabTargetFolder: shared/Folder1@kolab3.com
+mail: folder1@kolab3.com
+acl: anyone, read-write
+acl: owner@kolab3.com, full
+
+dn: cn=Folder2,ou=Shared Folders,ou=kolab3.com,dc=hosted,dc=com
+cn: Folder2
+objectClass: kolabsharedfolder
+objectClass: mailrecipient
+objectClass: top
+kolabFolderType: event
+kolabTargetFolder: shared/Folder2@kolab3.com
+mail: folder2@kolab3.com
+acl: anyone, read-only
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Mon, Apr 6, 9:46 AM (11 h, 10 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18834825
Default Alt Text
D3184.1775468766.diff (56 KB)
Attached To
Mode
D3184: Kolab3 migration tool
Attached
Detach File
Event Timeline