Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117884955
D3325.1775353548.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
93 KB
Referenced Files
None
Subscribers
None
D3325.1775353548.diff
View Options
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())
@@ -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,7 +646,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->wallet()->owner->id) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
@@ -672,12 +666,13 @@
return \trans('validation.entryexists', ['attribute' => 'email']);
}
- // Check if a group or resource with specified address already exists
+ // Check if a group, resource or shared folder with specified address already exists
if (
- ($existing = Group::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
+ // If this is a deleted group/resource/folder in the same custom domain
// we'll force delete it before
if (!$domain->isPublic() && $existing->trashed()) {
$deleted = $existing;
@@ -727,7 +722,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->wallet()->owner->id) {
return \trans('validation.entryexists', ['attribute' => 'domain']);
}
@@ -739,8 +734,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 +752,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')) {
+ $result['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;
@@ -23,13 +24,14 @@
*/
class Resource extends Model
{
+ use UuidIntKeyTrait; // must be first
use BelongsToTenantTrait;
use EntitleableTrait;
+ use EmailPropertyTrait;
use ResourceConfigTrait;
use SettingsTrait;
use SoftDeletes;
use StatusPropertyTrait;
- use 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,13 +25,15 @@
*/
class SharedFolder extends Model
{
+ use UuidIntKeyTrait; // must be first
+ use AliasesTrait;
use BelongsToTenantTrait;
use EntitleableTrait;
+ use EmailPropertyTrait;
use SharedFolderConfigTrait;
use SettingsTrait;
use SoftDeletes;
use StatusPropertyTrait;
- use 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,25 @@
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()
+ {
+ $class = static::class . 'Alias';
+ $column = Str::snake(\class_basename(static::class)) . '_id';
+
+ return $this->hasMany($class, $column);
+ }
+
/**
* Find whether an email address exists as an alias
- * (including aliases of deleted users).
*
* @param string $email Email address
*
@@ -19,14 +33,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 +60,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.
+ *
+ * @return ?\App\Domain The domain to which the object belongs to, NULL if it does not exist
+ */
+ public function domain(): ?\App\Domain
+ {
+ if (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/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/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
@@ -287,9 +287,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 +311,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 +341,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 +1195,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,13 +1264,15 @@
}
/**
- * 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);
@@ -1276,7 +1280,7 @@
$jack = $this->getTestUser('jack@kolab.org');
$user = $this->getTestUser('UsersControllerTest1@userscontroller.com');
- return [
+ $cases = [
// Invalid format
["$domain", $john, 'The specified email is invalid.'],
[".@$domain", $john, 'The specified email is invalid.'],
@@ -1302,20 +1306,12 @@
// valid (public domain)
["test.test@$domain", $john, null],
];
- }
- /**
- * 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;
+ $result = UsersController::validateEmail($email, $user);
+ $this->assertSame($expected, $result, "Case #{$idx}");
+ }
}
/**
@@ -1387,20 +1383,30 @@
}
/**
- * 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');
- return [
+ $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');
+
+ $cases = [
// Invalid format
["$domain", $john, 'The specified alias is invalid.'],
[".@$domain", $john, 'The specified alias is invalid.'],
@@ -1428,59 +1434,21 @@
// 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();
-
- $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');
-
- // 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);
-
- $result = UsersController::validateAlias('deleted-alias@kolab.org', $john);
- $this->assertSame(null, $result);
-
- $result = UsersController::validateAlias('deleted@kolabnow.com', $john);
- $this->assertSame('The specified alias is not available.', $result);
-
- $result = UsersController::validateAlias('deleted-alias@kolabnow.com', $john);
- $this->assertSame('The specified alias is not available.', $result);
+ // A group with the same email address exists
+ [$group->email, $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 #{$idx}");
+ }
}
}
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
Details
Attached
Mime Type
text/plain
Expires
Sun, Apr 5, 1:45 AM (6 h, 44 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18831871
Default Alt Text
D3325.1775353548.diff (93 KB)
Attached To
Mode
D3325: Shared folder aliases
Attached
Detach File
Event Timeline