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 @@ -231,8 +231,8 @@ $config = self::getConfig('admin'); $ldap = self::initLDAP($config); - list($cn, $domainName) = explode('@', $group->email); - + list(, $domainName) = explode('@', $group->email); +/* $domain = $group->domain(); if (empty($domain)) { @@ -241,24 +241,20 @@ "Failed to create group {$group->email} in LDAP (" . __LINE__ . ")" ); } - +*/ $hostedRootDN = \config('ldap.hosted.root_dn'); - - $domainBaseDN = "ou={$domain->namespace},{$hostedRootDN}"; - + $domainBaseDN = "ou={$domainName},{$hostedRootDN}"; $groupBaseDN = "ou=Groups,{$domainBaseDN}"; - + $cn = $ldap->quote_string($group->name); $dn = "cn={$cn},{$groupBaseDN}"; $entry = [ - 'cn' => $cn, 'mail' => $group->email, 'objectclass' => [ 'top', 'groupofuniquenames', 'kolabgroupofuniquenames' ], - 'uniquemember' => [] ]; self::setGroupAttributes($ldap, $group, $entry); @@ -353,8 +349,6 @@ $ldap = self::initLDAP($config); $hostedRootDN = \config('ldap.hosted.root_dn'); - $mgmtRootDN = \config('ldap.admin.root_dn'); - $domainBaseDN = "ou={$domain->namespace},{$hostedRootDN}"; if ($ldap->get_entry($domainBaseDN)) { @@ -568,41 +562,18 @@ $config = self::getConfig('admin'); $ldap = self::initLDAP($config); - list($cn, $domainName) = explode('@', $group->email); - - $domain = $group->domain(); + $newEntry = $oldEntry = self::getGroupEntry($ldap, $group->email, $dn); - if (empty($domain)) { + if (empty($oldEntry)) { self::throwException( $ldap, "Failed to update group {$group->email} in LDAP (group not found)" ); } - $hostedRootDN = \config('ldap.hosted.root_dn'); - - $domainBaseDN = "ou={$domain->namespace},{$hostedRootDN}"; - - $groupBaseDN = "ou=Groups,{$domainBaseDN}"; - - $dn = "cn={$cn},{$groupBaseDN}"; - - $entry = [ - 'cn' => $cn, - 'mail' => $group->email, - 'objectclass' => [ - 'top', - 'groupofuniquenames', - 'kolabgroupofuniquenames' - ], - 'uniquemember' => [] - ]; - - $oldEntry = $ldap->get_entry($dn); - - self::setGroupAttributes($ldap, $group, $entry); + self::setGroupAttributes($ldap, $group, $newEntry); - $result = $ldap->modify_entry($dn, $oldEntry, $entry); + $result = $ldap->modify_entry($dn, $oldEntry, $newEntry); if (!is_array($result)) { self::throwException( @@ -712,6 +683,8 @@ $settings = $group->getSettings(['sender_policy']); $entry['kolaballowsmtpsender'] = json_decode($settings['sender_policy'] ?: '[]', true); + $entry['cn'] = $group->name; + $entry['uniquemember'] = []; $validMembers = []; @@ -868,20 +841,33 @@ */ private static function getGroupEntry($ldap, $email, &$dn = null) { - list($_local, $_domain) = explode('@', $email, 2); + list(, $domainName) = explode('@', $email, 2); - $domain = $ldap->find_domain($_domain); + $domain = $ldap->find_domain($domainName); if (!$domain) { return $domain; } - $base_dn = $ldap->domain_root_dn($_domain); - $dn = "cn={$_local},ou=Groups,{$base_dn}"; + $base_dn = $ldap->domain_root_dn($domainName); - $entry = $ldap->get_entry($dn); + $attrs = ['dn', 'cn', 'mail', 'uniquemember', 'objectclass', 'kolaballowsmtpsender']; - return $entry ?: null; + // For groups we're using search() instead of get_entry() because + // a group name is not constant, so e.g. on update we might have + // the new name, but not the old one. Email address is constant. + $result = $ldap->search("ou=Groups,{$base_dn}", "(mail=$email)", "sub", $attrs); + + if ($result && $result->count() == 1) { + $entries = $result->entries(true); + $dn = key($entries); + $entry = $entries[$dn]; + $entry['dn'] = $dn; + + return $entry; + } + + return null; } /** diff --git a/src/app/Console/Commands/GroupsCommand.php b/src/app/Console/Commands/GroupsCommand.php new file mode 100644 --- /dev/null +++ b/src/app/Console/Commands/GroupsCommand.php @@ -0,0 +1,12 @@ +sortBy('namespace')->values(); + $result = $result->sortBy('name')->values(); } } elseif (!empty($search)) { if ($group = Group::where('email', $search)->first()) { @@ -41,6 +41,7 @@ $data = [ 'id' => $group->id, 'email' => $group->email, + 'name' => $group->name, ]; $data = array_merge($data, self::groupStatuses($group)); diff --git a/src/app/Http/Controllers/API/V4/GroupsController.php b/src/app/Http/Controllers/API/V4/GroupsController.php --- a/src/app/Http/Controllers/API/V4/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/GroupsController.php @@ -5,6 +5,7 @@ use App\Http\Controllers\Controller; use App\Domain; use App\Group; +use App\Rules\GroupName; use App\User; use Carbon\Carbon; use Illuminate\Http\Request; @@ -73,11 +74,12 @@ { $user = $this->guard()->user(); - $result = $user->groups()->orderBy('email')->get() + $result = $user->groups()->orderBy('name')->orderBy('email')->get() ->map(function (Group $group) { $data = [ 'id' => $group->id, 'email' => $group->email, + 'name' => $group->name, ]; $data = array_merge($data, self::groupStatuses($group)); @@ -284,13 +286,26 @@ return $this->errorResponse(403); } - $email = request()->input('email'); - $members = request()->input('members'); + $email = $request->input('email'); + $members = $request->input('members'); $errors = []; + $rules = [ + 'name' => 'required|string|max:191', + ]; // Validate group address if ($error = GroupsController::validateGroupEmail($email, $owner)) { $errors['email'] = $error; + } else { + list(, $domainName) = explode('@', $email); + $rules['name'] = ['required', 'string', new GroupName($owner, $domainName)]; + } + + // Validate the group name + $v = Validator::make($request->all(), $rules); + + if ($v->fails()) { + $errors = array_merge($errors, $v->errors()->toArray()); } // Validate members' email addresses @@ -318,6 +333,7 @@ // Create the group $group = new Group(); + $group->name = $request->input('name'); $group->email = $email; $group->members = $members; $group->save(); @@ -355,11 +371,24 @@ } $owner = $group->wallet()->owner; - - // It is possible to update members property only for now - $members = request()->input('members'); + $name = $request->input('name'); + $members = $request->input('members'); $errors = []; + // Validate the group name + if ($name !== null && $name != $group->name) { + list(, $domainName) = explode('@', $group->email); + $rules = ['name' => ['required', 'string', new GroupName($owner, $domainName)]]; + + $v = Validator::make($request->all(), $rules); + + if ($v->fails()) { + $errors = array_merge($errors, $v->errors()->toArray()); + } else { + $group->name = $name; + } + } + // Validate members' email addresses if (empty($members) || !is_array($members)) { $errors['members'] = \trans('validation.listmembersrequired'); diff --git a/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php b/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php --- a/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php +++ b/src/app/Http/Controllers/API/V4/Reseller/GroupsController.php @@ -27,7 +27,7 @@ }); } - $result = $result->sortBy('namespace')->values(); + $result = $result->sortBy('name')->values(); } } elseif (!empty($search)) { if ($group = Group::withSubjectTenantContext()->where('email', $search)->first()) { @@ -40,6 +40,7 @@ $data = [ 'id' => $group->id, 'email' => $group->email, + 'name' => $group->name, ]; $data = array_merge($data, self::groupStatuses($group)); diff --git a/src/app/Observers/GroupObserver.php b/src/app/Observers/GroupObserver.php --- a/src/app/Observers/GroupObserver.php +++ b/src/app/Observers/GroupObserver.php @@ -17,6 +17,10 @@ public function creating(Group $group): void { $group->status |= Group::STATUS_NEW | Group::STATUS_ACTIVE; + + if (!isset($group->name) && isset($group->email)) { + $group->name = explode('@', $group->email)[0]; + } } /** diff --git a/src/app/Rules/GroupName.php b/src/app/Rules/GroupName.php new file mode 100644 --- /dev/null +++ b/src/app/Rules/GroupName.php @@ -0,0 +1,72 @@ +owner = $owner; + $this->domain = Str::lower($domain); + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute Attribute name + * @param mixed $name The value to validate + * + * @return bool + */ + public function passes($attribute, $name): bool + { + if (empty($name) || !is_string($name)) { + $this->message = \trans('validation.nameinvalid'); + return false; + } + + // Check the max length, according to the database column length + if (strlen($name) > 191) { + $this->message = \trans('validation.nametoolong'); + return false; + } + + // Check if the name is unique in the domain + // FIXME: Maybe just using the whole groups table would be faster than groups()? + $exists = $this->owner->groups() + ->where('groups.name', $name) + ->where('groups.email', 'like', '%@' . $this->domain) + ->exists(); + + if ($exists) { + $this->message = \trans('validation.nameexists'); + return false; + } + + return true; + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message(): ?string + { + return $this->message; + } +} diff --git a/src/database/migrations/2021_11_10_100000_add_group_name_column.php b/src/database/migrations/2021_11_10_100000_add_group_name_column.php new file mode 100644 --- /dev/null +++ b/src/database/migrations/2021_11_10_100000_add_group_name_column.php @@ -0,0 +1,52 @@ +string('name')->nullable()->after('email'); + } + ); + + // Fill the name with the local part of the email address + DB::table('groups')->update([ + 'name' => DB::raw("SUBSTRING_INDEX(`email`, '@', 1)") + ]); + + Schema::table( + 'groups', + function (Blueprint $table) { + $table->string('name')->nullable(false)->change(); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table( + 'groups', + function (Blueprint $table) { + $table->dropColumn('name'); + } + ); + } +} 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 @@ -56,6 +56,7 @@ 'delete' => "Delete list", 'email' => "Email", 'list-empty' => "There are no distribution lists in this account.", + 'name' => "Name", 'new' => "New distribution list", 'recipients' => "Recipients", 'sender-policy' => "Sender Access List", diff --git a/src/resources/lang/en/validation.php b/src/resources/lang/en/validation.php --- a/src/resources/lang/en/validation.php +++ b/src/resources/lang/en/validation.php @@ -141,6 +141,9 @@ 'spf-entry-invalid' => 'The entry format is invalid. Expected a domain name starting with a dot.', 'sp-entry-invalid' => 'The entry format is invalid. Expected an email, domain, or part of it.', 'invalid-config-parameter' => 'The requested configuration parameter is not supported.', + 'nameexists' => 'The specified name is not available.', + 'nameinvalid' => 'The specified name is invalid.', + 'nametoolong' => 'The specified name is too long.', /* |-------------------------------------------------------------------------- diff --git a/src/resources/vue/Admin/Distlist.vue b/src/resources/vue/Admin/Distlist.vue --- a/src/resources/vue/Admin/Distlist.vue +++ b/src/resources/vue/Admin/Distlist.vue @@ -21,6 +21,12 @@ {{ $root.distlistStatusText(list) }} +
{{ $t('distlist.name') }} | {{ $t('form.email') }} | |
---|---|---|
|
+
|
|
{{ $t('user.distlists-none') }} | +{{ $t('user.distlists-none') }} |