Page MenuHomePhorge

D3325.1775274540.diff
No OneTemporary

Authored By
Unknown
Size
101 KB
Referenced Files
None
Subscribers
None

D3325.1775274540.diff

diff --git a/.arclint b/.arclint
--- a/.arclint
+++ b/.arclint
@@ -11,7 +11,7 @@
},
"phpstan": {
"type": "phpstan",
- "include": "(\\.php$)",
+ "include": "(^src/(app|config|resources|tests)/.*\\.php$)",
"exclude": "(^.arc/)",
"config": "src/phpstan.neon",
"bin": "src/vendor/bin/phpstan",
diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php
--- a/src/app/Backends/LDAP.php
+++ b/src/app/Backends/LDAP.php
@@ -1031,6 +1031,7 @@
$entry['kolabfoldertype'] = $folder->type;
$entry['kolabtargetfolder'] = $settings['folder'] ?? '';
$entry['acl'] = !empty($settings['acl']) ? json_decode($settings['acl'], true) : '';
+ $entry['alias'] = $folder->aliases()->pluck('alias')->all();
}
/**
@@ -1073,7 +1074,7 @@
$entry['inetuserstatus'] = $user->status;
$entry['o'] = $settings['organization'];
$entry['mailquota'] = 0;
- $entry['alias'] = $user->aliases->pluck('alias')->toArray();
+ $entry['alias'] = $user->aliases()->pluck('alias')->all();
$roles = [];
@@ -1193,7 +1194,7 @@
$domainName = explode('@', $email, 2)[1];
$base_dn = self::baseDN($ldap, $domainName, 'Shared Folders');
- $attrs = ['dn', 'cn', 'mail', 'objectclass', 'kolabtargetfolder', 'kolabfoldertype', 'acl'];
+ $attrs = ['dn', 'cn', 'mail', 'objectclass', 'kolabtargetfolder', 'kolabfoldertype', 'acl', 'alias'];
// For shared folders we're using search() instead of get_entry() because
// a folder name is not constant, so e.g. on update we might have
diff --git a/src/app/Console/Commands/Data/Import/LdifCommand.php b/src/app/Console/Commands/Data/Import/LdifCommand.php
--- a/src/app/Console/Commands/Data/Import/LdifCommand.php
+++ b/src/app/Console/Commands/Data/Import/LdifCommand.php
@@ -342,7 +342,7 @@
$resource = new \App\Resource();
$resource->name = $data->name;
- $resource->domain = $data->domain;
+ $resource->domainName = $data->domain;
$resource->save();
$resource->assignToWallet($this->wallet);
@@ -396,7 +396,7 @@
$folder = new \App\SharedFolder();
$folder->name = $data->name;
$folder->type = $data->type ?? 'mail';
- $folder->domain = $data->domain;
+ $folder->domainName = $data->domain;
$folder->save();
$folder->assignToWallet($this->wallet);
@@ -410,6 +410,11 @@
if (!empty($data->folder)) {
$folder->setSetting('folder', $data->folder);
}
+
+ // Import aliases
+ if (!empty($data->aliases)) {
+ $this->setObjectAliases($folder, $data->aliases);
+ }
}
$bar->finish();
@@ -431,7 +436,7 @@
// Import aliases of the owner, we got from importOwner() call
if (!empty($this->aliases) && $this->wallet) {
- $this->setUserAliases($this->wallet->owner, $this->aliases);
+ $this->setObjectAliases($this->wallet->owner, $this->aliases);
}
$bar = $this->createProgressBar($users->count(), "Importing users");
@@ -537,7 +542,7 @@
// domain records yet, save the aliases to be inserted later (in importUsers())
$this->aliases = $data->aliases;
} else {
- $this->setUserAliases($user, $data->aliases);
+ $this->setObjectAliases($user, $data->aliases);
}
}
@@ -706,6 +711,10 @@
if (!empty($entry['acl'])) {
$result['acl'] = $this->parseACL($this->attrArrayValue($entry, 'acl'));
}
+
+ if (!empty($entry['alias'])) {
+ $result['aliases'] = $this->attrArrayValue($entry, 'alias');
+ }
}
return [$result, $error];
@@ -957,14 +966,14 @@
}
/**
- * Set aliases for the user
+ * Set aliases for for an object
*/
- protected function setUserAliases(\App\User $user, array $aliases = [])
+ protected function setObjectAliases($object, 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]);
+ $aliases = array_diff(array_unique($aliases), [$object->email]);
// Remove aliases for domains that do not exist
if (!empty($aliases)) {
@@ -977,7 +986,7 @@
}
if (!empty($aliases)) {
- $user->setAliases($aliases);
+ $object->setAliases($aliases);
}
}
}
diff --git a/src/app/Console/Commands/SharedFolder/AddAliasCommand.php b/src/app/Console/Commands/SharedFolder/AddAliasCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/SharedFolder/AddAliasCommand.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Console\Commands\SharedFolder;
+
+use App\Console\Command;
+use App\Http\Controllers\API\V4\UsersController;
+
+class AddAliasCommand extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'sharedfolder:add-alias {--force} {folder} {alias}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Add an email alias to a shared folder (forcefully)";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $folder = $this->getSharedFolder($this->argument('folder'));
+
+ if (!$folder) {
+ $this->error("Folder not found.");
+ return 1;
+ }
+
+ $alias = \strtolower($this->argument('alias'));
+
+ // Check if the alias already exists
+ if ($folder->aliases()->where('alias', $alias)->first()) {
+ $this->error("Address is already assigned to the folder.");
+ return 1;
+ }
+
+ // Validate the alias
+ $error = UsersController::validateAlias($alias, $folder->walletOwner());
+
+ if ($error) {
+ if (!$this->option('force')) {
+ $this->error($error);
+ return 1;
+ }
+ }
+
+ $folder->aliases()->create(['alias' => $alias]);
+ }
+}
diff --git a/src/app/Console/Commands/User/AliasesCommand.php b/src/app/Console/Commands/SharedFolder/AliasesCommand.php
copy from src/app/Console/Commands/User/AliasesCommand.php
copy to src/app/Console/Commands/SharedFolder/AliasesCommand.php
--- a/src/app/Console/Commands/User/AliasesCommand.php
+++ b/src/app/Console/Commands/SharedFolder/AliasesCommand.php
@@ -1,6 +1,6 @@
<?php
-namespace App\Console\Commands\User;
+namespace App\Console\Commands\SharedFolder;
use App\Console\Command;
@@ -11,14 +11,14 @@
*
* @var string
*/
- protected $signature = 'user:aliases {user}';
+ protected $signature = 'sharedfolder:aliases {folder}';
/**
* The console command description.
*
* @var string
*/
- protected $description = "List a user's aliases";
+ protected $description = "List shared folder's aliases";
/**
* Execute the console command.
@@ -27,15 +27,15 @@
*/
public function handle()
{
- $user = $this->getUser($this->argument('user'));
+ $folder = $this->getSharedFolder($this->argument('folder'));
- if (!$user) {
- $this->error("User not found.");
+ if (!$folder) {
+ $this->error("Folder not found.");
return 1;
}
- foreach ($user->aliases as $alias) {
- $this->info("{$alias->alias}");
+ foreach ($folder->aliases()->pluck('alias')->all() as $alias) {
+ $this->info($alias);
}
}
}
diff --git a/src/app/Console/Commands/SharedFolder/CreateCommand.php b/src/app/Console/Commands/SharedFolder/CreateCommand.php
--- a/src/app/Console/Commands/SharedFolder/CreateCommand.php
+++ b/src/app/Console/Commands/SharedFolder/CreateCommand.php
@@ -79,7 +79,7 @@
$folder = new SharedFolder();
$folder->name = $name;
$folder->type = $type;
- $folder->domain = $domainName;
+ $folder->domainName = $domainName;
$folder->save();
$folder->assignToWallet($owner->wallets->first());
diff --git a/src/app/Console/Commands/SharedFolderAliasesCommand.php b/src/app/Console/Commands/SharedFolderAliasesCommand.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/SharedFolderAliasesCommand.php
@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Console\ObjectListCommand;
+
+class SharedFolderAliasesCommand extends ObjectListCommand
+{
+ protected $objectClass = \App\SharedFolderAlias::class;
+ protected $objectName = 'shared-folder-alias';
+ protected $objectNamePlural = 'shared-folder-aliases';
+ protected $objectTitle = 'alias';
+}
diff --git a/src/app/Console/Commands/User/AliasesCommand.php b/src/app/Console/Commands/User/AliasesCommand.php
--- a/src/app/Console/Commands/User/AliasesCommand.php
+++ b/src/app/Console/Commands/User/AliasesCommand.php
@@ -34,8 +34,8 @@
return 1;
}
- foreach ($user->aliases as $alias) {
- $this->info("{$alias->alias}");
+ foreach ($user->aliases()->pluck('alias')->all() as $alias) {
+ $this->info($alias);
}
}
}
diff --git a/src/app/Group.php b/src/app/Group.php
--- a/src/app/Group.php
+++ b/src/app/Group.php
@@ -4,6 +4,7 @@
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
+use App\Traits\EmailPropertyTrait;
use App\Traits\GroupConfigTrait;
use App\Traits\SettingsTrait;
use App\Traits\StatusPropertyTrait;
@@ -26,6 +27,7 @@
{
use BelongsToTenantTrait;
use EntitleableTrait;
+ use EmailPropertyTrait;
use GroupConfigTrait;
use SettingsTrait;
use SoftDeletes;
@@ -51,43 +53,6 @@
];
- /**
- * Returns group domain.
- *
- * @return ?\App\Domain The domain group belongs to, NULL if it does not exist
- */
- public function domain(): ?Domain
- {
- list($local, $domainName) = explode('@', $this->email);
-
- return Domain::where('namespace', $domainName)->first();
- }
-
- /**
- * Find whether an email address exists as a group (including deleted groups).
- *
- * @param string $email Email address
- * @param bool $return_group Return Group instance instead of boolean
- *
- * @return \App\Group|bool True or Group model object if found, False otherwise
- */
- public static function emailExists(string $email, bool $return_group = false)
- {
- if (strpos($email, '@') === false) {
- return false;
- }
-
- $email = \strtolower($email);
-
- $group = self::withTrashed()->where('email', $email)->first();
-
- if ($group) {
- return $return_group ? $group : true;
- }
-
- return false;
- }
-
/**
* Group members propert accessor. Converts internal comma-separated list into an array
*
@@ -100,16 +65,6 @@
return $members ? explode(',', $members) : [];
}
- /**
- * Ensure the email is appropriately cased.
- *
- * @param string $email Group email address
- */
- public function setEmailAttribute(string $email)
- {
- $this->attributes['email'] = strtolower($email);
- }
-
/**
* Ensure the members are appropriately formatted.
*
diff --git a/src/app/Http/Controllers/API/V4/Admin/UsersController.php b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
--- a/src/app/Http/Controllers/API/V4/Admin/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/UsersController.php
@@ -58,13 +58,15 @@
$user_ids = $user_ids->merge($ext_user_ids)->unique();
- // Search by a distribution list or resource email
+ // Search by an email of a group, resource, shared folder, etc.
if ($group = \App\Group::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$group->wallet()->user_id])->unique();
} elseif ($resource = \App\Resource::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$resource->wallet()->user_id])->unique();
} elseif ($folder = \App\SharedFolder::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$folder->wallet()->user_id])->unique();
+ } elseif ($alias = \App\SharedFolderAlias::where('alias', $search)->first()) {
+ $user_ids = $user_ids->merge([$alias->sharedFolder->wallet()->user_id])->unique();
}
if (!$user_ids->isEmpty()) {
diff --git a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php
--- a/src/app/Http/Controllers/API/V4/Reseller/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/Reseller/UsersController.php
@@ -50,13 +50,15 @@
$user_ids = $user_ids->merge($ext_user_ids)->unique();
- // Search by a distribution list email
+ // Search by an email of a group, resource, shared folder, etc.
if ($group = Group::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$group->wallet()->user_id])->unique();
} elseif ($resource = \App\Resource::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$resource->wallet()->user_id])->unique();
} elseif ($folder = \App\SharedFolder::withTrashed()->where('email', $search)->first()) {
$user_ids = $user_ids->merge([$folder->wallet()->user_id])->unique();
+ } elseif ($alias = \App\SharedFolderAlias::where('alias', $search)->first()) {
+ $user_ids = $user_ids->merge([$alias->sharedFolder->wallet()->user_id])->unique();
}
if (!$user_ids->isEmpty()) {
diff --git a/src/app/Http/Controllers/API/V4/ResourcesController.php b/src/app/Http/Controllers/API/V4/ResourcesController.php
--- a/src/app/Http/Controllers/API/V4/ResourcesController.php
+++ b/src/app/Http/Controllers/API/V4/ResourcesController.php
@@ -74,7 +74,7 @@
// Create the resource
$resource = new Resource();
$resource->name = request()->input('name');
- $resource->domain = $domain;
+ $resource->domainName = $domain;
$resource->save();
$resource->assignToWallet($owner->wallets->first());
diff --git a/src/app/Http/Controllers/API/V4/SharedFoldersController.php b/src/app/Http/Controllers/API/V4/SharedFoldersController.php
--- a/src/app/Http/Controllers/API/V4/SharedFoldersController.php
+++ b/src/app/Http/Controllers/API/V4/SharedFoldersController.php
@@ -9,6 +9,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
+use Illuminate\Support\Str;
class SharedFoldersController extends RelationController
{
@@ -54,34 +55,29 @@
public function store(Request $request)
{
$current_user = $this->guard()->user();
- $owner = $current_user->wallet()->owner;
+ $owner = $current_user->walletOwner();
- if ($owner->id != $current_user->id) {
+ if (empty($owner) || $owner->id != $current_user->id) {
return $this->errorResponse(403);
}
- $domain = request()->input('domain');
-
- $rules = [
- 'name' => ['required', 'string', new SharedFolderName($owner, $domain)],
- 'type' => ['required', 'string', new SharedFolderType()]
- ];
-
- $v = Validator::make($request->all(), $rules);
-
- if ($v->fails()) {
- return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ if ($error_response = $this->validateFolderRequest($request, null, $owner)) {
+ return $error_response;
}
DB::beginTransaction();
// Create the shared folder
$folder = new SharedFolder();
- $folder->name = request()->input('name');
- $folder->type = request()->input('type');
- $folder->domain = $domain;
+ $folder->name = $request->input('name');
+ $folder->type = $request->input('type');
+ $folder->domainName = $request->input('domain');
$folder->save();
+ if (!empty($request->aliases) && $folder->type === 'mail') {
+ $folder->setAliases($request->aliases);
+ }
+
$folder->assignToWallet($owner->wallets->first());
DB::commit();
@@ -114,30 +110,25 @@
return $this->errorResponse(403);
}
- $owner = $folder->wallet()->owner;
+ if ($error_response = $this->validateFolderRequest($request, $folder, $folder->walletOwner())) {
+ return $error_response;
+ }
$name = $request->input('name');
- $errors = [];
- // Validate the folder name
- if ($name !== null && $name != $folder->name) {
- $domainName = explode('@', $folder->email, 2)[1];
- $rules = ['name' => ['required', 'string', new SharedFolderName($owner, $domainName)]];
-
- $v = Validator::make($request->all(), $rules);
+ DB::beginTransaction();
- if ($v->fails()) {
- $errors = $v->errors()->toArray();
- } else {
- $folder->name = $name;
- }
+ if ($name && $name != $folder->name) {
+ $folder->name = $name;
}
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ $folder->save();
+
+ if (isset($request->aliases) && $folder->type === 'mail') {
+ $folder->setAliases($request->aliases);
}
- $folder->save();
+ DB::commit();
return response()->json([
'status' => 'success',
@@ -194,4 +185,78 @@
return false;
}
+
+ /**
+ * Validate shared folder input
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ * @param \App\SharedFolder|null $folder Shared folder
+ * @param \App\User|null $owner Account owner
+ *
+ * @return \Illuminate\Http\JsonResponse|null The error response on error
+ */
+ protected function validateFolderRequest(Request $request, $folder, $owner)
+ {
+ $errors = [];
+
+ if (empty($folder)) {
+ $domain = $request->input('domain');
+ $rules = [
+ 'name' => ['required', 'string', new SharedFolderName($owner, $domain)],
+ 'type' => ['required', 'string', new SharedFolderType()],
+ ];
+ } else {
+ // On update validate the folder name (if changed)
+ $name = $request->input('name');
+ if ($name !== null && $name != $folder->name) {
+ $domain = explode('@', $folder->email, 2)[1];
+ $rules = ['name' => ['required', 'string', new SharedFolderName($owner, $domain)]];
+ }
+ }
+
+ if (!empty($rules)) {
+ $v = Validator::make($request->all(), $rules);
+
+ if ($v->fails()) {
+ $errors = $v->errors()->toArray();
+ }
+ }
+
+ // Validate aliases input
+ if (isset($request->aliases)) {
+ $aliases = [];
+ $existing_aliases = $owner->aliases()->get()->pluck('alias')->toArray();
+
+ foreach ($request->aliases as $idx => $alias) {
+ if (is_string($alias) && !empty($alias)) {
+ // Alias cannot be the same as the email address
+ if (!empty($folder) && Str::lower($alias) == Str::lower($folder->email)) {
+ continue;
+ }
+
+ // validate new aliases
+ if (
+ !in_array($alias, $existing_aliases)
+ && ($error = UsersController::validateAlias($alias, $owner))
+ ) {
+ if (!isset($errors['aliases'])) {
+ $errors['aliases'] = [];
+ }
+ $errors['aliases'][$idx] = $error;
+ continue;
+ }
+
+ $aliases[] = $alias;
+ }
+ }
+
+ $request->aliases = $aliases;
+ }
+
+ if (!empty($errors)) {
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ return null;
+ }
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -4,7 +4,6 @@
use App\Http\Controllers\RelationController;
use App\Domain;
-use App\Group;
use App\Rules\Password;
use App\Rules\UserEmailDomain;
use App\Rules\UserEmailLocal;
@@ -133,6 +132,7 @@
$response['skus'] = \App\Entitlement::objectEntitlementsSummary($user);
$response['config'] = $user->getConfig();
+ $response['aliases'] = $user->aliases()->pluck('alias')->all();
$code = $user->verificationcodes()->where('active', true)
->where('expires_at', '>', \Carbon\Carbon::now())
@@ -207,7 +207,7 @@
public function store(Request $request)
{
$current_user = $this->guard()->user();
- $owner = $current_user->wallet()->owner;
+ $owner = $current_user->walletOwner();
if ($owner->id != $current_user->id) {
return $this->errorResponse(403);
@@ -394,12 +394,6 @@
$response['settings'][$item->key] = $item->value;
}
- // Aliases
- $response['aliases'] = [];
- foreach ($user->aliases as $item) {
- $response['aliases'][] = $item->alias;
- }
-
// Status info
$response['statusInfo'] = self::statusInfo($user);
@@ -652,33 +646,19 @@
}
// Check if it is one of domains available to the user
- if (!$user->domains()->where('namespace', $domain->namespace)->exists()) {
+ if (!$domain->isPublic() && $user->id != $domain->walletOwner()->id) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
- // Check if a user with specified address already exists
- if ($existing_user = User::emailExists($email, true)) {
- // If this is a deleted user in the same custom domain
- // we'll force delete him before
- if (!$domain->isPublic() && $existing_user->trashed()) {
- $deleted = $existing_user;
- } else {
- return \trans('validation.entryexists', ['attribute' => 'email']);
- }
- }
-
- // Check if an alias with specified address already exists.
- if (User::aliasExists($email)) {
- return \trans('validation.entryexists', ['attribute' => 'email']);
- }
-
- // Check if a group or resource with specified address already exists
+ // Check if a user/group/resource/shared folder with specified address already exists
if (
- ($existing = Group::emailExists($email, true))
+ ($existing = User::emailExists($email, true))
+ || ($existing = \App\Group::emailExists($email, true))
|| ($existing = \App\Resource::emailExists($email, true))
+ || ($existing = \App\SharedFolder::emailExists($email, true))
) {
- // If this is a deleted group/resource in the same custom domain
- // we'll force delete it before
+ // If this is a deleted user/group/resource/folder in the same custom domain
+ // we'll force delete it before creating the target user
if (!$domain->isPublic() && $existing->trashed()) {
$deleted = $existing;
} else {
@@ -686,6 +666,11 @@
}
}
+ // Check if an alias with specified address already exists.
+ if (User::aliasExists($email) || \App\SharedFolder::aliasExists($email)) {
+ return \trans('validation.entryexists', ['attribute' => 'email']);
+ }
+
return null;
}
@@ -727,7 +712,7 @@
}
// Check if it is one of domains available to the user
- if (!$user->domains()->where('namespace', $domain->namespace)->exists()) {
+ if (!$domain->isPublic() && $user->id != $domain->walletOwner()->id) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
@@ -739,8 +724,17 @@
}
}
+ // Check if a group/resource/shared folder with specified address already exists
+ if (
+ \App\Group::emailExists($email)
+ || \App\Resource::emailExists($email)
+ || \App\SharedFolder::emailExists($email)
+ ) {
+ return \trans('validation.entryexists', ['attribute' => 'alias']);
+ }
+
// Check if an alias with specified address already exists
- if (User::aliasExists($email)) {
+ if (User::aliasExists($email) || \App\SharedFolder::aliasExists($email)) {
// Allow assigning the same alias to a user in the same group account,
// but only for non-public domains
if ($domain->isPublic()) {
@@ -748,11 +742,6 @@
}
}
- // Check if a group with specified address already exists
- if (Group::emailExists($email)) {
- return \trans('validation.entryexists', ['attribute' => 'alias']);
- }
-
return null;
}
diff --git a/src/app/Http/Controllers/RelationController.php b/src/app/Http/Controllers/RelationController.php
--- a/src/app/Http/Controllers/RelationController.php
+++ b/src/app/Http/Controllers/RelationController.php
@@ -308,6 +308,10 @@
$response['config'] = $resource->getConfig();
}
+ if (method_exists($resource, 'aliases')) {
+ $response['aliases'] = $resource->aliases()->pluck('alias')->all();
+ }
+
return response()->json($response);
}
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
@@ -15,18 +15,6 @@
*/
public function creating(Resource $resource): void
{
- if (empty($resource->email)) {
- if (!isset($resource->domain)) {
- throw new \Exception("Missing 'domain' property for a new resource");
- }
-
- $domainName = \strtolower($resource->domain);
-
- $resource->email = "resource-{$resource->id}@{$domainName}";
- } else {
- $resource->email = \strtolower($resource->email);
- }
-
$resource->status |= Resource::STATUS_NEW | Resource::STATUS_ACTIVE;
}
diff --git a/src/app/Observers/SharedFolderAliasObserver.php b/src/app/Observers/SharedFolderAliasObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/SharedFolderAliasObserver.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace App\Observers;
+
+use App\Domain;
+use App\SharedFolder;
+use App\SharedFolderAlias;
+
+class SharedFolderAliasObserver
+{
+ /**
+ * Handle the "creating" event on an alias
+ *
+ * @param \App\SharedFolderAlias $alias The shared folder email alias
+ *
+ * @return bool
+ */
+ public function creating(SharedFolderAlias $alias): bool
+ {
+ $alias->alias = \strtolower($alias->alias);
+
+ $domainName = explode('@', $alias->alias)[1];
+
+ $domain = Domain::where('namespace', $domainName)->first();
+
+ if (!$domain) {
+ \Log::error("Failed creating alias {$alias->alias}. Domain does not exist.");
+ return false;
+ }
+
+ if ($alias->sharedFolder) {
+ if ($alias->sharedFolder->tenant_id != $domain->tenant_id) {
+ \Log::error("Reseller for folder '{$alias->sharedFolder->email}' and domain '{$domainName}' differ.");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Handle the shared folder alias "created" event.
+ *
+ * @param \App\SharedFolderAlias $alias Shared folder email alias
+ *
+ * @return void
+ */
+ public function created(SharedFolderAlias $alias)
+ {
+ if ($alias->sharedFolder) {
+ \App\Jobs\SharedFolder\UpdateJob::dispatch($alias->shared_folder_id);
+ }
+ }
+
+ /**
+ * Handle the shared folder alias "updated" event.
+ *
+ * @param \App\SharedFolderAlias $alias Shared folder email alias
+ *
+ * @return void
+ */
+ public function updated(SharedFolderAlias $alias)
+ {
+ if ($alias->sharedFolder) {
+ \App\Jobs\SharedFolder\UpdateJob::dispatch($alias->shared_folder_id);
+ }
+ }
+
+ /**
+ * Handle the shared folder alias "deleted" event.
+ *
+ * @param \App\SharedFolderAlias $alias Shared folder email alias
+ *
+ * @return void
+ */
+ public function deleted(SharedFolderAlias $alias)
+ {
+ if ($alias->sharedFolder) {
+ \App\Jobs\SharedFolder\UpdateJob::dispatch($alias->shared_folder_id);
+ }
+ }
+}
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
@@ -19,18 +19,6 @@
$folder->type = 'mail';
}
- if (empty($folder->email)) {
- if (!isset($folder->domain)) {
- throw new \Exception("Missing 'domain' property for a new shared folder");
- }
-
- $domainName = \strtolower($folder->domain);
-
- $folder->email = "{$folder->type}-{$folder->id}@{$domainName}";
- } else {
- $folder->email = \strtolower($folder->email);
- }
-
$folder->status |= SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE;
}
diff --git a/src/app/Observers/UserAliasObserver.php b/src/app/Observers/UserAliasObserver.php
--- a/src/app/Observers/UserAliasObserver.php
+++ b/src/app/Observers/UserAliasObserver.php
@@ -12,8 +12,6 @@
/**
* Handle the "creating" event on an alias
*
- * Ensures that there's no user with specified email.
- *
* @param \App\UserAlias $alias The user email alias
*
* @return bool
@@ -60,7 +58,7 @@
}
/**
- * Handle the user setting "updated" event.
+ * Handle the user alias "updated" event.
*
* @param \App\UserAlias $alias User email alias
*
@@ -74,7 +72,7 @@
}
/**
- * Handle the user setting "deleted" event.
+ * Handle the user alias "deleted" event.
*
* @param \App\UserAlias $alias User email alias
*
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -54,6 +54,7 @@
\App\Resource::observe(\App\Observers\ResourceObserver::class);
\App\ResourceSetting::observe(\App\Observers\ResourceSettingObserver::class);
\App\SharedFolder::observe(\App\Observers\SharedFolderObserver::class);
+ \App\SharedFolderAlias::observe(\App\Observers\SharedFolderAliasObserver::class);
\App\SharedFolderSetting::observe(\App\Observers\SharedFolderSettingObserver::class);
\App\SignupCode::observe(\App\Observers\SignupCodeObserver::class);
\App\SignupInvitation::observe(\App\Observers\SignupInvitationObserver::class);
diff --git a/src/app/Resource.php b/src/app/Resource.php
--- a/src/app/Resource.php
+++ b/src/app/Resource.php
@@ -4,6 +4,7 @@
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
+use App\Traits\EmailPropertyTrait;
use App\Traits\ResourceConfigTrait;
use App\Traits\SettingsTrait;
use App\Traits\StatusPropertyTrait;
@@ -30,6 +31,7 @@
use SoftDeletes;
use StatusPropertyTrait;
use UuidIntKeyTrait;
+ use EmailPropertyTrait; // must be first after UuidIntKeyTrait
// we've simply never heard of this resource
public const STATUS_NEW = 1 << 0;
@@ -44,54 +46,12 @@
// resource has been created in IMAP
public const STATUS_IMAP_READY = 1 << 8;
+ // A template for the email attribute on a resource creation
+ public const EMAIL_TEMPLATE = 'resource-{id}@{domainName}';
+
protected $fillable = [
'email',
'name',
'status',
];
-
- /** @var ?string Domain name for a resource to be created */
- public $domain;
-
-
- /**
- * Returns the resource domain.
- *
- * @return ?\App\Domain The domain to which the resource belongs to, NULL if it does not exist
- */
- public function domain(): ?Domain
- {
- if (isset($this->domain)) {
- $domainName = $this->domain;
- } else {
- list($local, $domainName) = explode('@', $this->email);
- }
-
- return Domain::where('namespace', $domainName)->first();
- }
-
- /**
- * Find whether an email address exists as a resource (including deleted resources).
- *
- * @param string $email Email address
- * @param bool $return_resource Return Resource instance instead of boolean
- *
- * @return \App\Resource|bool True or Resource model object if found, False otherwise
- */
- public static function emailExists(string $email, bool $return_resource = false)
- {
- if (strpos($email, '@') === false) {
- return false;
- }
-
- $email = \strtolower($email);
-
- $resource = self::withTrashed()->where('email', $email)->first();
-
- if ($resource) {
- return $return_resource ? $resource : true;
- }
-
- return false;
- }
}
diff --git a/src/app/SharedFolder.php b/src/app/SharedFolder.php
--- a/src/app/SharedFolder.php
+++ b/src/app/SharedFolder.php
@@ -2,13 +2,14 @@
namespace App;
+use App\Traits\AliasesTrait;
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
+use App\Traits\EmailPropertyTrait;
use App\Traits\SharedFolderConfigTrait;
use App\Traits\SettingsTrait;
use App\Traits\StatusPropertyTrait;
use App\Traits\UuidIntKeyTrait;
-use App\Wallet;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -24,6 +25,7 @@
*/
class SharedFolder extends Model
{
+ use AliasesTrait;
use BelongsToTenantTrait;
use EntitleableTrait;
use SharedFolderConfigTrait;
@@ -31,6 +33,7 @@
use SoftDeletes;
use StatusPropertyTrait;
use UuidIntKeyTrait;
+ use EmailPropertyTrait; // must be first after UuidIntKeyTrait
// we've simply never heard of this folder
public const STATUS_NEW = 1 << 0;
@@ -48,6 +51,9 @@
/** @const array Supported folder type labels */
public const SUPPORTED_TYPES = ['mail', 'event', 'contact', 'task', 'note'];
+ /** @const string A template for the email attribute on a folder creation */
+ public const EMAIL_TEMPLATE = '{type}-{id}@{domainName}';
+
/** @var array Mass-assignable properties */
protected $fillable = [
'email',
@@ -56,51 +62,6 @@
'type',
];
- /** @var ?string Domain name for a shared folder to be created */
- public $domain;
-
-
- /**
- * Returns the shared folder domain.
- *
- * @return ?\App\Domain The domain to which the folder belongs to, NULL if it does not exist
- */
- public function domain(): ?Domain
- {
- if (isset($this->domain)) {
- $domainName = $this->domain;
- } else {
- list($local, $domainName) = explode('@', $this->email);
- }
-
- return Domain::where('namespace', $domainName)->first();
- }
-
- /**
- * Find whether an email address exists as a shared folder (including deleted folders).
- *
- * @param string $email Email address
- * @param bool $return_folder Return SharedFolder instance instead of boolean
- *
- * @return \App\SharedFolder|bool True or Resource model object if found, False otherwise
- */
- public static function emailExists(string $email, bool $return_folder = false)
- {
- if (strpos($email, '@') === false) {
- return false;
- }
-
- $email = \strtolower($email);
-
- $folder = self::withTrashed()->where('email', $email)->first();
-
- if ($folder) {
- return $return_folder ? $folder : true;
- }
-
- return false;
- }
-
/**
* Folder type mutator
*
diff --git a/src/app/SharedFolderAlias.php b/src/app/SharedFolderAlias.php
new file mode 100644
--- /dev/null
+++ b/src/app/SharedFolderAlias.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace App;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * An email address alias for a SharedFolder.
+ *
+ * @property string $alias
+ * @property int $id
+ * @property int $shared_folder_id
+ */
+class SharedFolderAlias extends Model
+{
+ protected $fillable = [
+ 'shared_folder_id', 'alias'
+ ];
+
+ /**
+ * Ensure the email address is appropriately cased.
+ *
+ * @param string $alias Email address
+ */
+ public function setAliasAttribute(string $alias)
+ {
+ $this->attributes['alias'] = \strtolower($alias);
+ }
+
+ /**
+ * The shared folder to which this alias belongs.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
+ */
+ public function sharedFolder()
+ {
+ return $this->belongsTo('\App\SharedFolder', 'shared_folder_id', 'id');
+ }
+}
diff --git a/src/app/Traits/UserAliasesTrait.php b/src/app/Traits/AliasesTrait.php
rename from src/app/Traits/UserAliasesTrait.php
rename to src/app/Traits/AliasesTrait.php
--- a/src/app/Traits/UserAliasesTrait.php
+++ b/src/app/Traits/AliasesTrait.php
@@ -2,11 +2,22 @@
namespace App\Traits;
-trait UserAliasesTrait
+use Illuminate\Support\Str;
+
+trait AliasesTrait
{
+ /**
+ * Email aliases of this object.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function aliases()
+ {
+ return $this->hasMany(static::class . 'Alias');
+ }
+
/**
* Find whether an email address exists as an alias
- * (including aliases of deleted users).
*
* @param string $email Email address
*
@@ -19,14 +30,13 @@
}
$email = \strtolower($email);
+ $class = static::class . 'Alias';
- $count = \App\UserAlias::where('alias', $email)->count();
-
- return $count > 0;
+ return $class::where('alias', $email)->count() > 0;
}
/**
- * A helper to update user aliases list.
+ * A helper to update object's aliases list.
*
* Example Usage:
*
@@ -47,6 +57,7 @@
$existing_aliases = [];
foreach ($this->aliases()->get() as $alias) {
+ /** @var \App\UserAlias|\App\SharedFolderAlias $alias */
if (!in_array($alias->alias, $aliases)) {
$alias->delete();
} else {
diff --git a/src/app/Traits/EmailPropertyTrait.php b/src/app/Traits/EmailPropertyTrait.php
new file mode 100644
--- /dev/null
+++ b/src/app/Traits/EmailPropertyTrait.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace App\Traits;
+
+trait EmailPropertyTrait
+{
+ /** @var ?string Domain name for the to-be-created object */
+ public $domainName;
+
+
+ /**
+ * Boot function from Laravel.
+ */
+ protected static function bootEmailPropertyTrait()
+ {
+ static::creating(function ($model) {
+ if (empty($model->email) && defined('static::EMAIL_TEMPLATE')) {
+ $template = static::EMAIL_TEMPLATE; // @phpstan-ignore-line
+ $defaults = [
+ 'type' => 'mail',
+ ];
+
+ foreach (['id', 'domainName', 'type'] as $prop) {
+ if (strpos($template, "{{$prop}}") === false) {
+ continue;
+ }
+
+ $value = $model->{$prop} ?? ($defaults[$prop] ?? '');
+
+ if ($value === '' || $value === null) {
+ throw new \Exception("Missing '{$prop}' property for " . static::class);
+ }
+
+ $template = str_replace("{{$prop}}", $value, $template);
+ }
+
+ $model->email = strtolower($template);
+ }
+ });
+ }
+
+ /**
+ * Returns the object's domain (including soft-deleted).
+ *
+ * @return ?\App\Domain The domain to which the object belongs to, NULL if it does not exist
+ */
+ public function domain(): ?\App\Domain
+ {
+ if (empty($this->email) && isset($this->domainName)) {
+ $domainName = $this->domainName;
+ } else {
+ list($local, $domainName) = explode('@', $this->email);
+ }
+
+ return \App\Domain::withTrashed()->where('namespace', $domainName)->first();
+ }
+
+ /**
+ * Find whether an email address exists as a model object (including soft-deleted).
+ *
+ * @param string $email Email address
+ * @param bool $return_object Return model instance instead of a boolean
+ *
+ * @return object|bool True or Model object if found, False otherwise
+ */
+ public static function emailExists(string $email, bool $return_object = false)
+ {
+ if (strpos($email, '@') === false) {
+ return false;
+ }
+
+ $email = \strtolower($email);
+
+ $object = static::withTrashed()->where('email', $email)->first();
+
+ if ($object) {
+ return $return_object ? $object : true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Ensure the email is appropriately cased.
+ *
+ * @param string $email Email address
+ */
+ public function setEmailAttribute(string $email): void
+ {
+ $this->attributes['email'] = strtolower($email);
+ }
+}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -2,15 +2,14 @@
namespace App;
-use App\UserAlias;
+use App\Traits\AliasesTrait;
use App\Traits\BelongsToTenantTrait;
use App\Traits\EntitleableTrait;
-use App\Traits\UserAliasesTrait;
+use App\Traits\EmailPropertyTrait;
use App\Traits\UserConfigTrait;
use App\Traits\UuidIntKeyTrait;
use App\Traits\SettingsTrait;
use App\Traits\StatusPropertyTrait;
-use App\Wallet;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
@@ -31,12 +30,13 @@
*/
class User extends Authenticatable
{
+ use AliasesTrait;
use BelongsToTenantTrait;
use EntitleableTrait;
+ use EmailPropertyTrait;
use HasApiTokens;
use NullableFields;
use UserConfigTrait;
- use UserAliasesTrait;
use UuidIntKeyTrait;
use SettingsTrait;
use SoftDeletes;
@@ -103,16 +103,6 @@
);
}
- /**
- * Email aliases of this user.
- *
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
- */
- public function aliases()
- {
- return $this->hasMany('App\UserAlias', 'user_id');
- }
-
/**
* Assign a package to a user. The user should not have any existing entitlements.
*
@@ -263,20 +253,6 @@
$this->save();
}
- /**
- * Return the \App\Domain for this user.
- *
- * @return \App\Domain|null
- */
- public function domain()
- {
- list($local, $domainName) = explode('@', $this->email);
-
- $domain = \App\Domain::withTrashed()->where('namespace', $domainName)->first();
-
- return $domain;
- }
-
/**
* List the domains to which this user is entitled.
*
@@ -306,31 +282,6 @@
return $domains;
}
- /**
- * Find whether an email address exists as a user (including deleted users).
- *
- * @param string $email Email address
- * @param bool $return_user Return User instance instead of boolean
- *
- * @return \App\User|bool True or User model object if found, False otherwise
- */
- public static function emailExists(string $email, bool $return_user = false)
- {
- if (strpos($email, '@') === false) {
- return false;
- }
-
- $email = \strtolower($email);
-
- $user = self::withTrashed()->where('email', $email)->first();
-
- if ($user) {
- return $return_user ? $user : true;
- }
-
- return false;
- }
-
/**
* Return entitleable objects of a specified type controlled by the current user.
*
@@ -386,7 +337,7 @@
return $user;
}
- $aliases = UserAlias::where('alias', $email)->get();
+ $aliases = \App\UserAlias::where('alias', $email)->get();
if (count($aliases) == 1) {
return $aliases->first()->user;
diff --git a/src/app/UserAlias.php b/src/app/UserAlias.php
--- a/src/app/UserAlias.php
+++ b/src/app/UserAlias.php
@@ -17,6 +17,16 @@
'user_id', 'alias'
];
+ /**
+ * Ensure the email address is appropriately cased.
+ *
+ * @param string $alias Email address
+ */
+ public function setAliasAttribute(string $alias)
+ {
+ $this->attributes['alias'] = \strtolower($alias);
+ }
+
/**
* The user to which this alias belongs.
*
diff --git a/src/database/migrations/2022_01_25_100000_create_shared_folder_aliases_table.php b/src/database/migrations/2022_01_25_100000_create_shared_folder_aliases_table.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2022_01_25_100000_create_shared_folder_aliases_table.php
@@ -0,0 +1,43 @@
+<?php
+
+use Illuminate\Support\Facades\Schema;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Database\Migrations\Migration;
+
+// phpcs:ignore
+class CreateSharedFolderAliasesTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::create(
+ 'shared_folder_aliases',
+ function (Blueprint $table) {
+ $table->bigIncrements('id');
+ $table->unsignedBigInteger('shared_folder_id');
+ $table->string('alias');
+ $table->timestamp('created_at')->useCurrent();
+ $table->timestamp('updated_at')->useCurrent();
+
+ $table->unique(['alias', 'shared_folder_id']);
+
+ $table->foreign('shared_folder_id')->references('id')->on('shared_folders')
+ ->onDelete('cascade')->onUpdate('cascade');
+ }
+ );
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::dropIfExists('shared_folder_aliases');
+ }
+}
diff --git a/src/phpstan.neon b/src/phpstan.neon
--- a/src/phpstan.neon
+++ b/src/phpstan.neon
@@ -14,4 +14,5 @@
paths:
- app/
- config/
+ - resources/lang/
- tests/
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -489,7 +489,7 @@
}
})
- form.find('.is-invalid:not(.listinput-widget)').first().focus()
+ form.find('.is-invalid:not(.list-input)').first().focus()
})
}
else if (data.status == 'error') {
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -124,6 +124,7 @@
'disabled' => "disabled",
'domain' => "Domain",
'email' => "Email Address",
+ 'emails' => "Email Addresses",
'enabled' => "enabled",
'firstname' => "First Name",
'general' => "General",
@@ -317,6 +318,7 @@
],
'shf' => [
+ 'aliases-none' => "This shared folder has no email aliases.",
'create' => "Create folder",
'delete' => "Delete folder",
'acl-text' => "Defines user permissions to access the shared folder.",
diff --git a/src/resources/lang/fr/ui.php b/src/resources/lang/fr/ui.php
--- a/src/resources/lang/fr/ui.php
+++ b/src/resources/lang/fr/ui.php
@@ -175,7 +175,7 @@
'security' => "sécurité de chambre",
'security-text' => "Renforcez la sécurité de la salle en définissant un mot de passe que les participants devront connaître."
. " avant de pouvoir entrer, ou verrouiller la porte afin que les participants doivent frapper, et un modérateur peut accepter ou refuser ces demandes.",
- 'qa' => "Lever la main (Q&A)",
+ 'qa-title' => "Lever la main (Q&A)",
'qa-text' => "Les membres du public silencieux peuvent lever la main pour animer une séance de questions-réponses avec les membres du panel.",
'moderation' => "Délégation des Modérateurs",
'moderation-text' => "Déléguer l'autorité du modérateur pour la séance, afin qu'un orateur ne soit pas inutilement"
diff --git a/src/resources/vue/Admin/SharedFolder.vue b/src/resources/vue/Admin/SharedFolder.vue
--- a/src/resources/vue/Admin/SharedFolder.vue
+++ b/src/resources/vue/Admin/SharedFolder.vue
@@ -43,6 +43,11 @@
{{ $t('form.settings') }}
</a>
</li>
+ <li class="nav-item">
+ <a class="nav-link" id="tab-aliases" href="#folder-aliases" role="tab" aria-controls="folder-aliases" aria-selected="false" @click="$root.tab">
+ {{ $t('user.aliases-email') }} ({{ folder.aliases.length }})
+ </a>
+ </li>
</ul>
<div class="tab-content">
<div class="tab-pane show active" id="folder-settings" role="tabpanel" aria-labelledby="tab-settings">
@@ -66,6 +71,29 @@
</div>
</div>
</div>
+ <div class="tab-pane" id="folder-aliases" role="tabpanel" aria-labelledby="tab-aliases">
+ <div class="card-body">
+ <div class="card-text">
+ <table class="table table-sm table-hover mb-0">
+ <thead>
+ <tr>
+ <th scope="col">{{ $t('form.email') }}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(alias, index) in folder.aliases" :id="'alias' + index" :key="index">
+ <td>{{ alias }}</td>
+ </tr>
+ </tbody>
+ <tfoot class="table-fake-body">
+ <tr>
+ <td>{{ $t('shf.aliases-none') }}</td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ </div>
+ </div>
</div>
</div>
</template>
@@ -74,7 +102,7 @@
export default {
data() {
return {
- folder: { config: {} }
+ folder: { config: {}, aliases: [] }
}
},
created() {
diff --git a/src/resources/vue/SharedFolder/Info.vue b/src/resources/vue/SharedFolder/Info.vue
--- a/src/resources/vue/SharedFolder/Info.vue
+++ b/src/resources/vue/SharedFolder/Info.vue
@@ -55,10 +55,10 @@
</select>
</div>
</div>
- <div v-if="folder.email" class="row mb-3">
- <label for="email" class="col-sm-4 col-form-label">{{ $t('form.email') }}</label>
+ <div class="row mb-3" v-if="folder.type == 'mail'">
+ <label for="aliases-input" class="col-sm-4 col-form-label">{{ $t('form.emails') }}</label>
<div class="col-sm-8">
- <input type="text" class="form-control" id="email" disabled v-model="folder.email">
+ <list-input id="aliases" :list="folder.aliases"></list-input>
</div>
</div>
<button class="btn btn-primary" type="submit"><svg-icon icon="check"></svg-icon> {{ $t('btn.submit') }}</button>
@@ -87,18 +87,20 @@
<script>
import AclInput from '../Widgets/AclInput'
+ import ListInput from '../Widgets/ListInput'
import StatusComponent from '../Widgets/Status'
export default {
components: {
AclInput,
+ ListInput,
StatusComponent
},
data() {
return {
domains: [],
folder_id: null,
- folder: { type: 'mail', config: {} },
+ folder: { type: 'mail', config: {}, aliases: [] },
status: {},
types: [ 'mail', 'event', 'task', 'contact', 'note', 'file' ]
}
@@ -155,7 +157,11 @@
location += '/' + this.folder_id
}
- const post = this.$root.pick(this.folder, ['id', 'name', 'domain', 'type'])
+ const post = this.$root.pick(this.folder, ['id', 'name', 'domain', 'type', 'aliases'])
+
+ if (post.type != 'mail') {
+ delete post.aliases
+ }
axios[method](location, post)
.then(response => {
diff --git a/src/resources/vue/SharedFolder/List.vue b/src/resources/vue/SharedFolder/List.vue
--- a/src/resources/vue/SharedFolder/List.vue
+++ b/src/resources/vue/SharedFolder/List.vue
@@ -15,7 +15,6 @@
<tr>
<th scope="col">{{ $t('form.name') }}</th>
<th scope="col">{{ $t('form.type') }}</th>
- <th scope="col">{{ $t('form.email') }}</th>
</tr>
</thead>
<tbody>
@@ -25,12 +24,11 @@
<router-link :to="{ path: 'shared-folder/' + folder.id }">{{ folder.name }}</router-link>
</td>
<td>{{ $t('shf.type-' + folder.type) }}</td>
- <td><router-link :to="{ path: 'shared-folder/' + folder.id }">{{ folder.email }}</router-link></td>
</tr>
</tbody>
<tfoot class="table-fake-body">
<tr>
- <td colspan="3">{{ $t('shf.list-empty') }}</td>
+ <td colspan="2">{{ $t('shf.list-empty') }}</td>
</tr>
</tfoot>
</table>
diff --git a/src/tests/Browser/Admin/SharedFolderTest.php b/src/tests/Browser/Admin/SharedFolderTest.php
--- a/src/tests/Browser/Admin/SharedFolderTest.php
+++ b/src/tests/Browser/Admin/SharedFolderTest.php
@@ -56,6 +56,7 @@
$user = $this->getTestUser('john@kolab.org');
$folder = $this->getTestSharedFolder('folder-event@kolab.org');
$folder->setConfig(['acl' => ['anyone, read-only', 'jack@kolab.org, read-write']]);
+ $folder->setAliases(['folder-alias1@kolab.org', 'folder-alias2@kolab.org']);
$folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE
| SharedFolder::STATUS_LDAP_READY | SharedFolder::STATUS_IMAP_READY;
$folder->save();
@@ -85,13 +86,20 @@
->assertSeeIn('.row:nth-child(4) label', 'Type')
->assertSeeIn('.row:nth-child(4) #type', 'Calendar');
})
- ->assertElementsCount('ul.nav-tabs', 1)
- ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings')
+ ->assertElementsCount('ul.nav-tabs .nav-item', 2)
+ ->assertSeeIn('ul.nav-tabs .nav-item:nth-child(1) .nav-link', 'Settings')
->with('@folder-settings form', function (Browser $browser) {
$browser->assertElementsCount('.row', 1)
->assertSeeIn('.row:nth-child(1) label', 'Access rights')
->assertSeeIn('.row:nth-child(1) #acl', 'anyone: read-only')
->assertSeeIn('.row:nth-child(1) #acl', 'jack@kolab.org: read-write');
+ })
+ ->assertSeeIn('ul.nav-tabs .nav-item:nth-child(2) .nav-link', 'Email Aliases (2)')
+ ->click('ul.nav-tabs .nav-item:nth-child(2) .nav-link')
+ ->with('@folder-aliases table', function (Browser $browser) {
+ $browser->assertElementsCount('tbody tr', 2)
+ ->assertSeeIn('tbody tr:nth-child(1) td', 'folder-alias1@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(2) td', 'folder-alias2@kolab.org');
});
// Test invalid shared folder identifier
diff --git a/src/tests/Browser/Pages/Admin/SharedFolder.php b/src/tests/Browser/Pages/Admin/SharedFolder.php
--- a/src/tests/Browser/Pages/Admin/SharedFolder.php
+++ b/src/tests/Browser/Pages/Admin/SharedFolder.php
@@ -52,6 +52,7 @@
'@app' => '#app',
'@folder-info' => '#folder-info',
'@folder-settings' => '#folder-settings',
+ '@folder-aliases' => '#folder-aliases',
];
}
}
diff --git a/src/tests/Browser/Reseller/SharedFolderTest.php b/src/tests/Browser/Reseller/SharedFolderTest.php
--- a/src/tests/Browser/Reseller/SharedFolderTest.php
+++ b/src/tests/Browser/Reseller/SharedFolderTest.php
@@ -56,6 +56,7 @@
$user = $this->getTestUser('john@kolab.org');
$folder = $this->getTestSharedFolder('folder-event@kolab.org');
$folder->setConfig(['acl' => ['anyone, read-only', 'jack@kolab.org, read-write']]);
+ $folder->setAliases(['folder-alias1@kolab.org', 'folder-alias2@kolab.org']);
$folder->status = SharedFolder::STATUS_NEW | SharedFolder::STATUS_ACTIVE
| SharedFolder::STATUS_LDAP_READY | SharedFolder::STATUS_IMAP_READY;
$folder->save();
@@ -85,13 +86,20 @@
->assertSeeIn('.row:nth-child(4) label', 'Type')
->assertSeeIn('.row:nth-child(4) #type', 'Calendar');
})
- ->assertElementsCount('ul.nav-tabs', 1)
- ->assertSeeIn('ul.nav-tabs .nav-link', 'Settings')
+ ->assertElementsCount('ul.nav-tabs .nav-item', 2)
+ ->assertSeeIn('ul.nav-tabs .nav-item:nth-child(1) .nav-link', 'Settings')
->with('@folder-settings form', function (Browser $browser) {
$browser->assertElementsCount('.row', 1)
->assertSeeIn('.row:nth-child(1) label', 'Access rights')
->assertSeeIn('.row:nth-child(1) #acl', 'anyone: read-only')
->assertSeeIn('.row:nth-child(1) #acl', 'jack@kolab.org: read-write');
+ })
+ ->assertSeeIn('ul.nav-tabs .nav-item:nth-child(2) .nav-link', 'Email Aliases (2)')
+ ->click('ul.nav-tabs .nav-item:nth-child(2) .nav-link')
+ ->with('@folder-aliases table', function (Browser $browser) {
+ $browser->assertElementsCount('tbody tr', 2)
+ ->assertSeeIn('tbody tr:nth-child(1) td', 'folder-alias1@kolab.org')
+ ->assertSeeIn('tbody tr:nth-child(2) td', 'folder-alias2@kolab.org');
});
// Test invalid shared folder identifier
diff --git a/src/tests/Browser/SharedFolderTest.php b/src/tests/Browser/SharedFolderTest.php
--- a/src/tests/Browser/SharedFolderTest.php
+++ b/src/tests/Browser/SharedFolderTest.php
@@ -5,6 +5,7 @@
use App\SharedFolder;
use Tests\Browser;
use Tests\Browser\Components\AclInput;
+use Tests\Browser\Components\ListInput;
use Tests\Browser\Components\Status;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
@@ -95,17 +96,15 @@
->on(new SharedFolderList())
->whenAvailable('@table', function (Browser $browser) {
$browser->waitFor('tbody tr')
+ ->assertElementsCount('thead th', 2)
->assertSeeIn('thead tr th:nth-child(1)', 'Name')
->assertSeeIn('thead tr th:nth-child(2)', 'Type')
- ->assertSeeIn('thead tr th:nth-child(3)', 'Email Address')
->assertElementsCount('tbody tr', 2)
->assertSeeIn('tbody tr:nth-child(1) td:nth-child(1) a', 'Calendar')
->assertSeeIn('tbody tr:nth-child(1) td:nth-child(2)', 'Calendar')
- ->assertSeeIn('tbody tr:nth-child(1) td:nth-child(3) a', 'folder-event@kolab.org')
->assertText('tbody tr:nth-child(1) td:nth-child(1) svg.text-success title', 'Active')
->assertSeeIn('tbody tr:nth-child(2) td:nth-child(1) a', 'Contacts')
->assertSeeIn('tbody tr:nth-child(2) td:nth-child(2)', 'Address Book')
- ->assertSeeIn('tbody tr:nth-child(2) td:nth-child(3) a', 'folder-contact@kolab.org')
->assertMissing('tfoot');
});
});
@@ -152,23 +151,45 @@
->assertSeeIn('div.row:nth-child(3) label', 'Domain')
->assertSelectHasOptions('div.row:nth-child(3) select', ['kolab.org'])
->assertValue('div.row:nth-child(3) select', 'kolab.org')
+ ->assertSeeIn('div.row:nth-child(4) label', 'Email Addresses')
+ ->with(new ListInput('#aliases'), function (Browser $browser) {
+ $browser->assertListInputValue([])
+ ->assertValue('@input', '');
+ })
->assertSeeIn('button[type=submit]', 'Submit');
})
// Test error conditions
->type('#name', str_repeat('A', 192))
+ ->select('#type', 'event')
+ ->assertMissing('#aliases')
->click('@general button[type=submit]')
->waitFor('#name + .invalid-feedback')
->assertSeeIn('#name + .invalid-feedback', 'The name may not be greater than 191 characters.')
->assertFocused('#name')
->assertToast(Toast::TYPE_ERROR, 'Form validation error')
- // Test successful folder creation
+ // Test error handling on aliases input
->type('#name', 'Test Folder')
+ ->select('#type', 'mail')
+ ->with(new ListInput('#aliases'), function (Browser $browser) {
+ $browser->addListEntry('folder-alias@unknown');
+ })
+ ->click('@general button[type=submit]')
+ ->assertMissing('#name + .invalid-feedback')
+ ->waitFor('#aliases + .invalid-feedback')
+ ->with(new ListInput('#aliases'), function (Browser $browser) {
+ $browser->assertFormError(1, "The specified domain is invalid.", true);
+ })
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ // Test successful folder creation
->select('#type', 'event')
->click('@general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'Shared folder created successfully.')
->on(new SharedFolderList())
->assertElementsCount('@table tbody tr', 3);
+ $this->assertSame(1, SharedFolder::where('name', 'Test Folder')->count());
+ $this->assertSame(0, SharedFolder::where('name', 'Test Folder')->first()->aliases()->count());
+
// Test folder update
$browser->click('@table tr:nth-child(3) td:first-child a')
->on(new SharedFolderInfo())
@@ -182,12 +203,7 @@
->assertValue('div.row:nth-child(2) input[type=text]', 'Test Folder')
->assertSeeIn('div.row:nth-child(3) label', 'Type')
->assertSelected('div.row:nth-child(3) select:disabled', 'event')
- ->assertSeeIn('div.row:nth-child(4) label', 'Email Address')
- ->assertAttributeRegExp(
- 'div.row:nth-child(4) input[type=text]:disabled',
- 'value',
- '/^event-[0-9]+@kolab\.org$/'
- )
+ ->assertMissing('#aliases')
->assertSeeIn('button[type=submit]', 'Submit');
})
// Test error handling
@@ -219,6 +235,54 @@
$this->assertNull(SharedFolder::where('name', 'Test Folder Update')->first());
});
+
+ // Test creation/updating a mail folder with mail aliases
+ $this->browse(function (Browser $browser) {
+ $browser->on(new SharedFolderList())
+ ->click('button.create-folder')
+ ->on(new SharedFolderInfo())
+ ->type('#name', 'Test Folder2')
+ ->with(new ListInput('#aliases'), function (Browser $browser) {
+ $browser->addListEntry('folder-alias1@kolab.org')
+ ->addListEntry('folder-alias2@kolab.org');
+ })
+ ->click('@general button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Shared folder created successfully.')
+ ->on(new SharedFolderList())
+ ->assertElementsCount('@table tbody tr', 3);
+
+ $folder = SharedFolder::where('name', 'Test Folder2')->first();
+
+ $this->assertSame(
+ ['folder-alias1@kolab.org', 'folder-alias2@kolab.org'],
+ $folder->aliases()->pluck('alias')->all()
+ );
+
+ // Test folder update
+ $browser->click('@table tr:nth-child(3) td:first-child a')
+ ->on(new SharedFolderInfo())
+ ->with('@general', function (Browser $browser) {
+ // Assert form content
+ $browser->assertFocused('#name')
+ ->assertValue('div.row:nth-child(2) input[type=text]', 'Test Folder2')
+ ->assertSelected('div.row:nth-child(3) select:disabled', 'mail')
+ ->with(new ListInput('#aliases'), function (Browser $browser) {
+ $browser->assertListInputValue(['folder-alias1@kolab.org', 'folder-alias2@kolab.org'])
+ ->assertValue('@input', '');
+ });
+ })
+ // change folder name, and remove one alias
+ ->type('#name', 'Test Folder Update2')
+ ->with(new ListInput('#aliases'), function (Browser $browser) {
+ $browser->removeListEntry(2);
+ })
+ ->click('@general button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, 'Shared folder updated successfully.');
+
+ $folder->refresh();
+ $this->assertSame('Test Folder Update2', $folder->name);
+ $this->assertSame(['folder-alias1@kolab.org'], $folder->aliases()->pluck('alias')->all());
+ });
}
/**
diff --git a/src/tests/Feature/Backends/LDAPTest.php b/src/tests/Feature/Backends/LDAPTest.php
--- a/src/tests/Feature/Backends/LDAPTest.php
+++ b/src/tests/Feature/Backends/LDAPTest.php
@@ -331,6 +331,7 @@
'kolabfoldertype' => 'event',
'kolabtargetfolder' => 'shared/test-folder@kolab.org',
'acl' => null,
+ 'alias' => null,
];
foreach ($expected as $attr => $value) {
@@ -342,6 +343,8 @@
$folder->name = 'Te(=ść)1';
$folder->save();
$folder->setSetting('acl', '["john@kolab.org, read-write","anyone, read-only"]');
+ $aliases = ['t1-' . $folder->email, 't2-' . $folder->email];
+ $folder->setAliases($aliases);
LDAP::updateSharedFolder($folder);
@@ -349,6 +352,7 @@
$expected['acl'] = ['john@kolab.org, read-write', 'anyone, read-only'];
$expected['dn'] = 'cn=Te(\\3dść)1,ou=Shared Folders,ou=kolab.org,' . $root_dn;
$expected['cn'] = 'Te(=ść)1';
+ $expected['alias'] = $aliases;
$ldap_folder = LDAP::getSharedFolder($folder->email);
diff --git a/src/tests/Feature/Console/Data/Import/LdifTest.php b/src/tests/Feature/Console/Data/Import/LdifTest.php
--- a/src/tests/Feature/Console/Data/Import/LdifTest.php
+++ b/src/tests/Feature/Console/Data/Import/LdifTest.php
@@ -137,6 +137,11 @@
$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'));
+ $this->assertSame([], $folders[0]->aliases()->orderBy('alias')->pluck('alias')->all());
+ $this->assertSame(
+ ['folder-alias1@kolab3.com', 'folder-alias2@kolab3.com'],
+ $folders[1]->aliases()->orderBy('alias')->pluck('alias')->all()
+ );
// Groups
$groups = $owner->groups(false)->orderBy('email')->get();
@@ -363,6 +368,7 @@
'kolabtargetfolder' => 'Folder',
'kolabfoldertype' => 'event',
'acl' => 'anyone, read-write',
+ 'alias' => ['test1@domain.tld', 'test2@domain.tld'],
];
$expected = [
@@ -371,6 +377,7 @@
'type' => 'event',
'folder' => 'Folder',
'acl' => ['anyone, read-write'],
+ 'aliases' => ['test1@domain.tld', 'test2@domain.tld'],
];
$result = $this->invokeMethod($command, 'parseLDAPSharedFolder', [$entry]);
diff --git a/src/tests/Feature/Console/SharedFolder/AddAliasTest.php b/src/tests/Feature/Console/SharedFolder/AddAliasTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/SharedFolder/AddAliasTest.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Tests\Feature\Console\SharedFolder;
+
+use Tests\TestCase;
+
+class AddAliasTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestSharedFolder('folder-test@force-delete.com');
+ $this->deleteTestUser('user@force-delete.com');
+ $this->deleteTestDomain('force-delete.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestSharedFolder('folder-test@force-delete.com');
+ $this->deleteTestUser('user@force-delete.com');
+ $this->deleteTestDomain('force-delete.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test the command
+ */
+ public function testHandle(): void
+ {
+ // Non-existing folder
+ $code = \Artisan::call("sharedfolder:add-alias unknown unknown");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Folder not found.", $output);
+
+ $user = $this->getTestUser('user@force-delete.com');
+ $domain = $this->getTestDomain('force-delete.com', [
+ 'status' => \App\Domain::STATUS_NEW,
+ 'type' => \App\Domain::TYPE_EXTERNAL,
+ ]);
+ $package_kolab = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first();
+ $package_domain = \App\Package::withEnvTenantContext()->where('title', 'domain-hosting')->first();
+ $user->assignPackage($package_kolab);
+ $domain->assignPackage($package_domain, $user);
+ $folder = $this->getTestSharedFolder('folder-test@force-delete.com');
+ $folder->assignToWallet($user->wallets->first());
+
+ // Invalid alias
+ $code = \Artisan::call("sharedfolder:add-alias {$folder->email} invalid");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("The specified alias is invalid.", $output);
+ $this->assertSame([], $folder->aliases()->pluck('alias')->all());
+
+ // A domain of another user
+ $code = \Artisan::call("sharedfolder:add-alias {$folder->email} test@kolab.org");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("The specified domain is not available.", $output);
+ $this->assertSame([], $folder->aliases()->pluck('alias')->all());
+
+ // Test success
+ $code = \Artisan::call("sharedfolder:add-alias {$folder->email} test@force-delete.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(0, $code);
+ $this->assertSame("", $output);
+ $this->assertSame(['test@force-delete.com'], $folder->aliases()->pluck('alias')->all());
+
+ // Alias already exists
+ $code = \Artisan::call("sharedfolder:add-alias {$folder->email} test@force-delete.com");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("Address is already assigned to the folder.", $output);
+
+ // TODO: test --force option
+ }
+}
diff --git a/src/tests/Feature/Controller/Admin/UsersTest.php b/src/tests/Feature/Controller/Admin/UsersTest.php
--- a/src/tests/Feature/Controller/Admin/UsersTest.php
+++ b/src/tests/Feature/Controller/Admin/UsersTest.php
@@ -38,6 +38,8 @@
$jack = $this->getTestUser('jack@kolab.org');
$jack->setSetting('external_email', null);
+ \App\SharedFolderAlias::truncate();
+
parent::tearDown();
}
@@ -205,6 +207,19 @@
$this->assertSame($user->id, $json['list'][0]['id']);
$this->assertSame($user->email, $json['list'][0]['email']);
+ // Search by shared folder alias
+ $folder = $this->getTestSharedFolder('folder-event@kolab.org');
+ $folder->setAliases(['folder-alias@kolab.org']);
+ $response = $this->actingAs($admin)->get("api/v4/users?search=folder-alias@kolab.org");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($user->id, $json['list'][0]['id']);
+ $this->assertSame($user->email, $json['list'][0]['email']);
+
// Deleted users/domains
$domain = $this->getTestDomain('testsearch.com', ['type' => \App\Domain::TYPE_EXTERNAL]);
$user = $this->getTestUser('test@testsearch.com');
diff --git a/src/tests/Feature/Controller/AuthTest.php b/src/tests/Feature/Controller/AuthTest.php
--- a/src/tests/Feature/Controller/AuthTest.php
+++ b/src/tests/Feature/Controller/AuthTest.php
@@ -75,7 +75,6 @@
$this->assertEquals(User::STATUS_NEW | User::STATUS_ACTIVE, $json['status']);
$this->assertTrue(is_array($json['statusInfo']));
$this->assertTrue(is_array($json['settings']));
- $this->assertTrue(is_array($json['aliases']));
$this->assertTrue(!isset($json['access_token']));
// Note: Details of the content are tested in testUserResponse()
@@ -95,7 +94,6 @@
$this->assertEquals('john@kolab.org', $json['email']);
$this->assertTrue(is_array($json['statusInfo']));
$this->assertTrue(is_array($json['settings']));
- $this->assertTrue(is_array($json['aliases']));
$this->assertTrue(!empty($json['access_token']));
$this->assertTrue(!empty($json['expires_in']));
}
@@ -142,7 +140,6 @@
$this->assertEquals($user->email, $json['email']);
$this->assertTrue(is_array($json['statusInfo']));
$this->assertTrue(is_array($json['settings']));
- $this->assertTrue(is_array($json['aliases']));
// Valid user+password (upper-case)
$post = ['email' => 'John@Kolab.org', 'password' => 'simple123'];
diff --git a/src/tests/Feature/Controller/Reseller/UsersTest.php b/src/tests/Feature/Controller/Reseller/UsersTest.php
--- a/src/tests/Feature/Controller/Reseller/UsersTest.php
+++ b/src/tests/Feature/Controller/Reseller/UsersTest.php
@@ -32,6 +32,8 @@
$this->deleteTestUser('test@testsearch.com');
$this->deleteTestDomain('testsearch.com');
+ \App\SharedFolderAlias::truncate();
+
parent::tearDown();
}
@@ -130,6 +132,30 @@
$this->assertSame(0, $json['count']);
$this->assertSame([], $json['list']);
+ // Search by shared folder email
+ $response = $this->actingAs($reseller1)->get("api/v4/users?search=folder-event@kolab.org");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($user->id, $json['list'][0]['id']);
+ $this->assertSame($user->email, $json['list'][0]['email']);
+
+ // Search by shared folder alias
+ $folder = $this->getTestSharedFolder('folder-event@kolab.org');
+ $folder->setAliases(['folder-alias@kolab.org']);
+ $response = $this->actingAs($reseller1)->get("api/v4/users?search=folder-alias@kolab.org");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame($user->id, $json['list'][0]['id']);
+ $this->assertSame($user->email, $json['list'][0]['email']);
+
// Create a domain with some users in the Sample Tenant so we have anything to search for
$domain = $this->getTestDomain('testsearch.com', ['type' => \App\Domain::TYPE_EXTERNAL]);
$domain->tenant_id = $reseller2->tenant_id;
diff --git a/src/tests/Feature/Controller/SharedFoldersTest.php b/src/tests/Feature/Controller/SharedFoldersTest.php
--- a/src/tests/Feature/Controller/SharedFoldersTest.php
+++ b/src/tests/Feature/Controller/SharedFoldersTest.php
@@ -208,6 +208,7 @@
$folder = $this->getTestSharedFolder('folder-test@kolab.org');
$folder->assignToWallet($john->wallets->first());
$folder->setSetting('acl', '["anyone, full"]');
+ $folder->setAliases(['folder-alias@kolab.org']);
// Test unauthenticated access
$response = $this->get("/api/v4/shared-folders/{$folder->id}");
@@ -231,6 +232,7 @@
$this->assertSame($folder->email, $json['email']);
$this->assertSame($folder->name, $json['name']);
$this->assertSame($folder->type, $json['type']);
+ $this->assertSame(['folder-alias@kolab.org'], $json['aliases']);
$this->assertTrue(!empty($json['statusInfo']));
$this->assertArrayHasKey('isDeleted', $json);
$this->assertArrayHasKey('isActive', $json);
@@ -404,8 +406,14 @@
$this->assertCount(2, $json);
$this->assertCount(2, $json['errors']);
- // Test too long name
- $post = ['domain' => 'kolab.org', 'name' => str_repeat('A', 192), 'type' => 'unknown'];
+ // Test too long name, invalid alias domain
+ $post = [
+ 'domain' => 'kolab.org',
+ 'name' => str_repeat('A', 192),
+ 'type' => 'unknown',
+ 'aliases' => ['folder-alias@unknown.org'],
+ ];
+
$response = $this->actingAs($john)->post("/api/v4/shared-folders", $post);
$response->assertStatus(422);
@@ -415,11 +423,14 @@
$this->assertCount(2, $json);
$this->assertSame(["The name may not be greater than 191 characters."], $json['errors']['name']);
$this->assertSame(["The specified type is invalid."], $json['errors']['type']);
- $this->assertCount(2, $json['errors']);
+ $this->assertSame(["The specified domain is invalid."], $json['errors']['aliases']);
+ $this->assertCount(3, $json['errors']);
// Test successful folder creation
$post['name'] = 'Test Folder';
$post['type'] = 'event';
+ $post['aliases'] = ['folder-alias@kolab.org']; // expected to be ignored
+
$response = $this->actingAs($john)->post("/api/v4/shared-folders", $post);
$json = $response->json();
@@ -433,6 +444,7 @@
$this->assertInstanceOf(SharedFolder::class, $folder);
$this->assertSame($post['type'], $folder->type);
$this->assertTrue($john->sharedFolders()->get()->contains($folder));
+ $this->assertSame([], $folder->aliases()->pluck('alias')->all());
// Shared folder name must be unique within a domain
$post['type'] = 'mail';
@@ -444,6 +456,20 @@
$this->assertCount(2, $json);
$this->assertCount(1, $json['errors']);
$this->assertSame("The specified name is not available.", $json['errors']['name'][0]);
+
+ $folder->forceDelete();
+
+ // Test successful folder creation with aliases
+ $post['name'] = 'Test Folder';
+ $post['type'] = 'mail';
+ $post['aliases'] = ['folder-alias@kolab.org'];
+ $response = $this->actingAs($john)->post("/api/v4/shared-folders", $post);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ $folder = SharedFolder::where('name', $post['name'])->first();
+ $this->assertSame(['folder-alias@kolab.org'], $folder->aliases()->pluck('alias')->all());
}
/**
@@ -484,5 +510,41 @@
$folder->refresh();
$this->assertSame($post['name'], $folder->name);
+
+ // Aliases with error
+ $post['aliases'] = ['folder-alias1@kolab.org', 'folder-alias2@unknown.com'];
+
+ $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post);
+ $json = $response->json();
+
+ $response->assertStatus(422);
+
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json);
+ $this->assertCount(1, $json['errors']);
+ $this->assertCount(1, $json['errors']['aliases']);
+ $this->assertSame("The specified domain is invalid.", $json['errors']['aliases'][1]);
+ $this->assertSame([], $folder->aliases()->pluck('alias')->all());
+
+ // Aliases with success expected
+ $post['aliases'] = ['folder-alias1@kolab.org', 'folder-alias2@kolab.org'];
+
+ $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post);
+ $json = $response->json();
+
+ $response->assertStatus(200);
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Shared folder updated successfully.", $json['message']);
+ $this->assertCount(2, $json);
+ $this->assertSame($post['aliases'], $folder->aliases()->pluck('alias')->all());
+
+ // All aliases removal
+ $post['aliases'] = [];
+
+ $response = $this->actingAs($john)->put("/api/v4/shared-folders/{$folder->id}", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame($post['aliases'], $folder->aliases()->pluck('alias')->all());
}
}
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -34,6 +34,8 @@
$this->deleteTestDomain('userscontroller.com');
$this->deleteTestGroup('group-test@kolabnow.com');
$this->deleteTestGroup('group-test@kolab.org');
+ $this->deleteTestSharedFolder('folder-test@kolabnow.com');
+ $this->deleteTestResource('resource-test@kolabnow.com');
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
@@ -60,6 +62,8 @@
$this->deleteTestDomain('userscontroller.com');
$this->deleteTestGroup('group-test@kolabnow.com');
$this->deleteTestGroup('group-test@kolab.org');
+ $this->deleteTestSharedFolder('folder-test@kolabnow.com');
+ $this->deleteTestResource('resource-test@kolabnow.com');
$user = $this->getTestUser('john@kolab.org');
$wallet = $user->wallets()->first();
@@ -287,9 +291,9 @@
$this->assertEquals($userA->email, $json['email']);
$this->assertTrue(is_array($json['statusInfo']));
$this->assertTrue(is_array($json['settings']));
- $this->assertTrue(is_array($json['aliases']));
$this->assertTrue($json['config']['greylist_enabled']);
$this->assertSame([], $json['skus']);
+ $this->assertSame([], $json['aliases']);
// Values below are tested by Unit tests
$this->assertArrayHasKey('isDeleted', $json);
$this->assertArrayHasKey('isDegraded', $json);
@@ -311,6 +315,10 @@
// Ned: Additional account controller
$response = $this->actingAs($ned)->get("/api/v4/users/{$john->id}");
$response->assertStatus(200);
+
+ $json = $response->json();
+ $this->assertSame(['john.doe@kolab.org'], $json['aliases']);
+
$response = $this->actingAs($ned)->get("/api/v4/users/{$jack->id}");
$response->assertStatus(200);
@@ -337,6 +345,8 @@
$this->assertSame([500], $json['skus'][$mailbox_sku->id]['costs']);
$this->assertSame(1, $json['skus'][$secondfactor_sku->id]['count']);
$this->assertSame([0], $json['skus'][$secondfactor_sku->id]['costs']);
+
+ $this->assertSame([], $json['aliases']);
}
/**
@@ -1189,10 +1199,6 @@
$this->assertEquals($user->status, $result['status']);
$this->assertTrue(is_array($result['statusInfo']));
- $this->assertTrue(is_array($result['aliases']));
- $this->assertCount(1, $result['aliases']);
- $this->assertSame('john.doe@kolab.org', $result['aliases'][0]);
-
$this->assertTrue(is_array($result['settings']));
$this->assertSame('US', $result['settings']['country']);
$this->assertSame('USD', $result['settings']['currency']);
@@ -1262,21 +1268,39 @@
}
/**
- * List of email address validation cases for testValidateEmail()
+ * User email address validation.
*
- * @return array Arguments for testValidateEmail()
+ * Note: Technically these include unit tests, but let's keep it here for now.
+ * FIXME: Shall we do a http request for each case?
*/
- public function dataValidateEmail(): array
+ public function testValidateEmail(): void
{
- $this->refreshApplication();
+ Queue::fake();
+
$public_domains = Domain::getPublicDomains();
$domain = reset($public_domains);
$john = $this->getTestUser('john@kolab.org');
$jack = $this->getTestUser('jack@kolab.org');
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $folder = $this->getTestSharedFolder('folder-event@kolab.org');
+ $folder->setAliases(['folder-alias1@kolab.org']);
+ $folder_del = $this->getTestSharedFolder('folder-test@kolabnow.com');
+ $folder_del->setAliases(['folder-alias2@kolabnow.com']);
+ $folder_del->delete();
+ $pub_group = $this->getTestGroup('group-test@kolabnow.com');
+ $pub_group->delete();
+ $priv_group = $this->getTestGroup('group-test@kolab.org');
+ $resource = $this->getTestResource('resource-test@kolabnow.com');
+ $resource->delete();
+
+ $cases = [
+ // valid (user domain)
+ ["admin@kolab.org", $john, null],
+
+ // valid (public domain)
+ ["test.test@$domain", $john, null],
- return [
// Invalid format
["$domain", $john, 'The specified email is invalid.'],
[".@$domain", $john, 'The specified email is invalid.'],
@@ -1293,29 +1317,39 @@
// forbidden (other user's domain)
["testtest@kolab.org", $user, 'The specified domain is not available.'],
- // existing alias of other user, to be a user email
+ // existing alias of other user
["jack.daniels@kolab.org", $john, 'The specified email is not available.'],
- // valid (user domain)
- ["admin@kolab.org", $john, null],
+ // An existing shared folder or folder alias
+ ["folder-event@kolab.org", $john, 'The specified email is not available.'],
+ ["folder-alias1@kolab.org", $john, 'The specified email is not available.'],
- // valid (public domain)
- ["test.test@$domain", $john, null],
+ // A soft-deleted shared folder or folder alias
+ ["folder-test@kolabnow.com", $john, 'The specified email is not available.'],
+ ["folder-alias2@kolabnow.com", $john, 'The specified email is not available.'],
+
+ // A group
+ ["group-test@kolab.org", $john, 'The specified email is not available.'],
+
+ // A soft-deleted group
+ ["group-test@kolabnow.com", $john, 'The specified email is not available.'],
+
+ // A resource
+ ["resource-test1@kolab.org", $john, 'The specified email is not available.'],
+
+ // A soft-deleted resource
+ ["resource-test@kolabnow.com", $john, 'The specified email is not available.'],
];
- }
- /**
- * User email address validation.
- *
- * Note: Technically these include unit tests, but let's keep it here for now.
- * FIXME: Shall we do a http request for each case?
- *
- * @dataProvider dataValidateEmail
- */
- public function testValidateEmail($email, $user, $expected_result): void
- {
- $result = UsersController::validateEmail($email, $user);
- $this->assertSame($expected_result, $result);
+ foreach ($cases as $idx => $case) {
+ list($email, $user, $expected) = $case;
+
+ $deleted = null;
+ $result = UsersController::validateEmail($email, $user, $deleted);
+
+ $this->assertSame($expected, $result, "Case {$email}");
+ $this->assertNull($deleted, "Case {$email}");
+ }
}
/**
@@ -1345,19 +1379,7 @@
$result = UsersController::validateEmail('jack@kolab.org', $john, $deleted);
$this->assertSame('The specified email is not available.', $result);
$this->assertSame(null, $deleted);
- }
-
- /**
- * User email validation - tests for an address being a group email address
- *
- * Note: Technically these include unit tests, but let's keep it here for now.
- * FIXME: Shall we do a http request for each case?
- */
- public function testValidateEmailGroup(): void
- {
- Queue::fake();
- $john = $this->getTestUser('john@kolab.org');
$pub_group = $this->getTestGroup('group-test@kolabnow.com');
$priv_group = $this->getTestGroup('group-test@kolab.org');
@@ -1384,23 +1406,43 @@
$result = UsersController::validateEmail($priv_group->email, $john, $deleted);
$this->assertSame(null, $result);
$this->assertSame($priv_group->id, $deleted->id);
+
+ // TODO: Test the same with a resource and shared folder
}
/**
- * List of alias validation cases for testValidateAlias()
+ * User email alias validation.
*
- * @return array Arguments for testValidateAlias()
+ * Note: Technically these include unit tests, but let's keep it here for now.
+ * FIXME: Shall we do a http request for each case?
*/
- public function dataValidateAlias(): array
+ public function testValidateAlias(): void
{
- $this->refreshApplication();
+ Queue::fake();
+
$public_domains = Domain::getPublicDomains();
$domain = reset($public_domains);
$john = $this->getTestUser('john@kolab.org');
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
+ $deleted_priv = $this->getTestUser('deleted@kolab.org');
+ $deleted_priv->setAliases(['deleted-alias@kolab.org']);
+ $deleted_priv->delete();
+ $deleted_pub = $this->getTestUser('deleted@kolabnow.com');
+ $deleted_pub->setAliases(['deleted-alias@kolabnow.com']);
+ $deleted_pub->delete();
+ $folder = $this->getTestSharedFolder('folder-event@kolab.org');
+ $folder->setAliases(['folder-alias1@kolab.org']);
+ $folder_del = $this->getTestSharedFolder('folder-test@kolabnow.com');
+ $folder_del->setAliases(['folder-alias2@kolabnow.com']);
+ $folder_del->delete();
+ $group_priv = $this->getTestGroup('group-test@kolab.org');
+ $group = $this->getTestGroup('group-test@kolabnow.com');
+ $group->delete();
+ $resource = $this->getTestResource('resource-test@kolabnow.com');
+ $resource->delete();
- return [
+ $cases = [
// Invalid format
["$domain", $john, 'The specified alias is invalid.'],
[".@$domain", $john, 'The specified alias is invalid.'],
@@ -1428,59 +1470,38 @@
// valid (public domain)
["test.test@$domain", $john, null],
- ];
- }
- /**
- * User email alias validation.
- *
- * Note: Technically these include unit tests, but let's keep it here for now.
- * FIXME: Shall we do a http request for each case?
- *
- * @dataProvider dataValidateAlias
- */
- public function testValidateAlias($alias, $user, $expected_result): void
- {
- $result = UsersController::validateAlias($alias, $user);
- $this->assertSame($expected_result, $result);
- }
+ // An alias that was a user email before is allowed, but only for custom domains
+ ["deleted@kolab.org", $john, null],
+ ["deleted-alias@kolab.org", $john, null],
+ ["deleted@kolabnow.com", $john, 'The specified alias is not available.'],
+ ["deleted-alias@kolabnow.com", $john, 'The specified alias is not available.'],
- /**
- * User alias validation - more cases.
- *
- * Note: Technically these include unit tests, but let's keep it here for now.
- * FIXME: Shall we do a http request for each case?
- */
- public function testValidateAlias2(): void
- {
- Queue::fake();
+ // An existing shared folder or folder alias
+ ["folder-event@kolab.org", $john, 'The specified alias is not available.'],
+ ["folder-alias1@kolab.org", $john, null],
- $john = $this->getTestUser('john@kolab.org');
- $jack = $this->getTestUser('jack@kolab.org');
- $user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
- $deleted_priv = $this->getTestUser('deleted@kolab.org');
- $deleted_priv->setAliases(['deleted-alias@kolab.org']);
- $deleted_priv->delete();
- $deleted_pub = $this->getTestUser('deleted@kolabnow.com');
- $deleted_pub->setAliases(['deleted-alias@kolabnow.com']);
- $deleted_pub->delete();
- $group = $this->getTestGroup('group-test@kolabnow.com');
+ // A soft-deleted shared folder or folder alias
+ ["folder-test@kolabnow.com", $john, 'The specified alias is not available.'],
+ ["folder-alias2@kolabnow.com", $john, 'The specified alias is not available.'],
- // An alias that was a user email before is allowed, but only for custom domains
- $result = UsersController::validateAlias('deleted@kolab.org', $john);
- $this->assertSame(null, $result);
+ // A group with the same email address exists
+ ["group-test@kolab.org", $john, 'The specified alias is not available.'],
- $result = UsersController::validateAlias('deleted-alias@kolab.org', $john);
- $this->assertSame(null, $result);
+ // A soft-deleted group
+ ["group-test@kolabnow.com", $john, 'The specified alias is not available.'],
- $result = UsersController::validateAlias('deleted@kolabnow.com', $john);
- $this->assertSame('The specified alias is not available.', $result);
+ // A resource
+ ["resource-test1@kolab.org", $john, 'The specified alias is not available.'],
- $result = UsersController::validateAlias('deleted-alias@kolabnow.com', $john);
- $this->assertSame('The specified alias is not available.', $result);
+ // A soft-deleted resource
+ ["resource-test@kolabnow.com", $john, 'The specified alias is not available.'],
+ ];
- // A grpoup with the same email address exists
- $result = UsersController::validateAlias($group->email, $john);
- $this->assertSame('The specified alias is not available.', $result);
+ foreach ($cases as $idx => $case) {
+ list($alias, $user, $expected) = $case;
+ $result = UsersController::validateAlias($alias, $user);
+ $this->assertSame($expected, $result, "Case {$alias}");
+ }
}
}
diff --git a/src/tests/Feature/ResourceTest.php b/src/tests/Feature/ResourceTest.php
--- a/src/tests/Feature/ResourceTest.php
+++ b/src/tests/Feature/ResourceTest.php
@@ -106,7 +106,7 @@
$resource = new Resource();
$resource->name = 'Reśo';
- $resource->domain = 'kolabnow.com';
+ $resource->domainName = 'kolabnow.com';
$resource->save();
$this->assertMatchesRegularExpression('/^[0-9]{1,20}$/', $resource->id);
diff --git a/src/tests/Feature/SharedFolderTest.php b/src/tests/Feature/SharedFolderTest.php
--- a/src/tests/Feature/SharedFolderTest.php
+++ b/src/tests/Feature/SharedFolderTest.php
@@ -28,6 +28,60 @@
parent::tearDown();
}
+ /**
+ * Tests for AliasesTrait methods
+ */
+ public function testAliases(): void
+ {
+ Queue::fake();
+ Queue::assertNothingPushed();
+
+ $folder = $this->getTestSharedFolder('folder-test@kolabnow.com');
+
+ $this->assertCount(0, $folder->aliases->all());
+
+ // Add an alias
+ $folder->setAliases(['FolderAlias1@kolabnow.com']);
+
+ Queue::assertPushed(\App\Jobs\SharedFolder\UpdateJob::class, 1);
+
+ $aliases = $folder->aliases()->get();
+
+ $this->assertCount(1, $aliases);
+ $this->assertSame('folderalias1@kolabnow.com', $aliases[0]->alias);
+ $this->assertTrue(SharedFolder::aliasExists('folderalias1@kolabnow.com'));
+
+ // Add another alias
+ $folder->setAliases(['FolderAlias1@kolabnow.com', 'FolderAlias2@kolabnow.com']);
+
+ Queue::assertPushed(\App\Jobs\SharedFolder\UpdateJob::class, 2);
+
+ $aliases = $folder->aliases()->orderBy('alias')->get();
+ $this->assertCount(2, $aliases);
+ $this->assertSame('folderalias1@kolabnow.com', $aliases[0]->alias);
+ $this->assertSame('folderalias2@kolabnow.com', $aliases[1]->alias);
+
+ // Remove an alias
+ $folder->setAliases(['FolderAlias1@kolabnow.com']);
+
+ Queue::assertPushed(\App\Jobs\SharedFolder\UpdateJob::class, 3);
+
+ $aliases = $folder->aliases()->get();
+
+ $this->assertCount(1, $aliases);
+ $this->assertSame('folderalias1@kolabnow.com', $aliases[0]->alias);
+ $this->assertFalse(SharedFolder::aliasExists('folderalias2@kolabnow.com'));
+
+ // Remove all aliases
+ $folder->setAliases([]);
+
+ Queue::assertPushed(\App\Jobs\SharedFolder\UpdateJob::class, 4);
+
+ $this->assertCount(0, $folder->aliases()->get());
+ $this->assertFalse(SharedFolder::aliasExists('folderalias1@kolabnow.com'));
+ $this->assertFalse(SharedFolder::aliasExists('folderalias2@kolabnow.com'));
+ }
+
/**
* Tests for SharedFolder::assignToWallet()
*/
@@ -116,7 +170,7 @@
$folder = new SharedFolder();
$folder->name = 'Reśo';
- $folder->domain = 'kolabnow.com';
+ $folder->domainName = 'kolabnow.com';
$folder->save();
$this->assertMatchesRegularExpression('/^[0-9]{1,20}$/', $folder->id);
diff --git a/src/tests/Feature/Stories/GreylistTest.php b/src/tests/Feature/Stories/GreylistTest.php
--- a/src/tests/Feature/Stories/GreylistTest.php
+++ b/src/tests/Feature/Stories/GreylistTest.php
@@ -254,11 +254,11 @@
$this->assertFalse($request->shouldDefer());
// Ensure we also find the setting by alias
- $aliases = $this->domainOwner->aliases()->orderBy('alias')->get();
+ $aliases = $this->domainOwner->aliases()->orderBy('alias')->pluck('alias')->all();
$request = new Greylist\Request(
[
'sender' => 'someone@sender.domain',
- 'recipient' => $aliases[0]->alias,
+ 'recipient' => $aliases[0],
'client_address' => $this->clientAddress
]
);
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -1023,7 +1023,7 @@
}
/**
- * Tests for UserAliasesTrait::setAliases()
+ * Tests for AliasesTrait::setAliases()
*/
public function testSetAliases(): void
{
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -417,7 +417,7 @@
$resource = new Resource();
$resource->email = $email;
- $resource->domain = $domain;
+ $resource->domainName = $domain;
if (!isset($attrib['name'])) {
$resource->name = $local;
@@ -449,7 +449,7 @@
$folder = new SharedFolder();
$folder->email = $email;
- $folder->domain = $domain;
+ $folder->domainName = $domain;
if (!isset($attrib['name'])) {
$folder->name = $local;
diff --git a/src/tests/data/kolab3.ldif b/src/tests/data/kolab3.ldif
--- a/src/tests/data/kolab3.ldif
+++ b/src/tests/data/kolab3.ldif
@@ -105,6 +105,8 @@
kolabFolderType: mail
kolabTargetFolder: shared/Folder1@kolab3.com
mail: folder1@kolab3.com
+alias: folder-alias1@kolab3.com
+alias: folder-alias2@kolab3.com
acl: anyone, read-write
acl: owner@kolab3.com, full

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 3:49 AM (9 h, 34 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18827969
Default Alt Text
D3325.1775274540.diff (101 KB)

Event Timeline