*/ public $domainMailUsers; // -------------------- Lifecycle -------------------- public function mount(?int $aliasId = null, ?int $domainId = null): void { $this->aliasId = $aliasId; $this->domainId = $domainId; if ($this->aliasId) { $alias = MailAlias::with(['domain', 'recipients.mailUser'])->findOrFail($this->aliasId); if ($alias->domain->is_system) abort(403, 'System-Domains sind für Aliasse gesperrt.'); $this->domain = $alias->domain; $this->domainId = $alias->domain_id; $this->fill([ 'local' => $alias->local, 'type' => $alias->type, 'group_name' => $alias->group_name, 'is_active' => $alias->is_active, 'notes' => $alias->notes, ]); // vorhandene Empfänger -> mit stabiler id versehen $this->recipients = $alias->recipients ->map(fn($r) => [ 'id' => (string)Str::uuid(), 'mail_user_id' => $r->mail_user_id, 'email' => $r->email, ]) ->all(); } else { $this->domain = $this->domainId ? Domain::where('is_system', false)->findOrFail($this->domainId) : Domain::where('is_system', false)->orderBy('domain')->first(); $this->domainId = $this->domain->id ?? null; $this->recipients = [[ 'id' => (string)Str::uuid(), 'mail_user_id' => null, 'email' => null, ]]; } $this->domainMailUsers = $this->domain ? MailUser::where('domain_id', $this->domain->id)->orderBy('localpart')->get() : collect(); $this->ensureAtLeastOneRow(); $this->recomputeUi(); $this->validateRecipientDuplicates(); } // -------------------- UI-Reaktionen -------------------- public function updated($name, $value): void { // XOR je Zeile if (preg_match('/^recipients\.(\d+)\.(mail_user_id|email)$/', $name, $m)) { $i = (int)$m[1]; if ($m[2] === 'mail_user_id' && !empty($value)) { $this->recipients[$i]['email'] = null; } elseif ($m[2] === 'email') { $this->recipients[$i]['mail_user_id'] = null; $this->recipients[$i]['email'] = Str::lower(trim((string)$this->recipients[$i]['email'])); } } // Self-Loop Live-Feedback (extern == alias) if (preg_match('/^recipients\.(\d+)\.email$/', $name, $m)) { $i = (int)$m[1]; $self = $this->currentAliasAddress(); $this->resetErrorBag("recipients.$i.email"); if ($self && Str::lower(trim((string)$this->recipients[$i]['email'])) === $self) { $this->addError("recipients.$i.email", 'Alias darf nicht an sich selbst weiterleiten.'); } } if ($name === 'type' && $this->type === 'single') { $first = $this->recipients[0] ?? ['id' => (string)Str::uuid(), 'mail_user_id' => null, 'email' => null]; $first['id'] = $first['id'] ?? (string)Str::uuid(); $this->recipients = [$first]; } $this->ensureAtLeastOneRow(); $this->recomputeUi(); $this->validateRecipientDuplicates(); } public function updatedType(string $value): void { if ($value === 'single') { $first = $this->recipients[0] ?? ['id' => (string)Str::uuid(), 'mail_user_id' => null, 'email' => null]; $first['id'] = $first['id'] ?? (string)Str::uuid(); $this->recipients = [$first]; $this->recomputeUi(); $this->validateRecipientDuplicates(); } } public function updatedDomainId(): void { $this->domain = $this->domainId ? Domain::where('is_system', false)->find($this->domainId) : null; $this->domainMailUsers = $this->domain ? MailUser::where('domain_id', $this->domain->id)->orderBy('localpart')->get() : collect(); // Zeilen resetten (neue Domain) $this->recipients = [[ 'id' => (string)Str::uuid(), 'mail_user_id' => null, 'email' => null, ]]; $this->ensureAtLeastOneRow(); $this->recomputeUi(); $this->validateRecipientDuplicates(); } public function addRecipientRow(): void { if ($this->type === 'single') return; if (count($this->recipients) >= $this->maxGroupRecipients) return; $this->recipients[] = ['id' => (string)Str::uuid(), 'mail_user_id' => null, 'email' => null]; $this->recomputeUi(); } public function removeRecipientRow(int $index): void { unset($this->recipients[$index]); $this->recipients = array_values($this->recipients); $this->ensureAtLeastOneRow(); $this->recomputeUi(); $this->validateRecipientDuplicates(); } // -------------------- Helpers -------------------- private function currentAliasAddress(): ?string { if (!$this->domainId || $this->local === '') return null; $domainName = $this->domain?->domain ?? Domain::find($this->domainId)?->domain; if (!$domainName) return null; return Str::lower(trim($this->local)) . '@' . Str::lower($domainName); } private function ensureAtLeastOneRow(): void { if (empty($this->recipients)) { $this->recipients = [[ 'id' => (string)Str::uuid(), 'mail_user_id' => null, 'email' => null, ]]; } } private function recomputeUi(): void { $count = count($this->recipients); $this->canAddRecipient = $this->type === 'group' && $count < $this->maxGroupRecipients; $usedMailboxIds = []; $usedExternal = []; foreach ($this->recipients as $r) { if (!empty($r['mail_user_id'])) $usedMailboxIds[] = (int)$r['mail_user_id']; if (!empty($r['email'])) $usedExternal[] = Str::lower(trim((string)$r['email'])); } $this->disabledMailboxIdsByRow = []; $this->rowState = []; $this->rowDuplicateError = []; $aliasAddr = $this->currentAliasAddress(); for ($i = 0; $i < $count; $i++) { $myId = (int)($this->recipients[$i]['mail_user_id'] ?? 0); $hasInternal = $myId > 0; $hasExternal = !empty($this->recipients[$i]['email']); $myExternal = Str::lower(trim((string)($this->recipients[$i]['email'] ?? ''))); // andere interne IDs sperren $disabled = array_values(array_unique(array_filter( $usedMailboxIds, fn($id) => $id && $id !== $myId ))); // interne IDs sperren, deren Adresse in externen Feldern vorkommt if ($this->domain) { foreach ($this->domainMailUsers as $mu) { $addr = Str::lower($mu->localpart . '@' . $this->domain->domain); if (in_array($addr, $usedExternal, true) && $mu->id !== $myId) { $disabled[] = (int)$mu->id; } } } $this->disabledMailboxIdsByRow[$i] = array_values(array_unique($disabled)); $this->rowState[$i] = [ 'disable_internal' => $hasExternal || ($aliasAddr && $aliasAddr === $myExternal), 'disable_external' => $hasInternal, 'can_remove' => !($this->type === 'single' && $count === 1), ]; } } private function validateRecipientDuplicates(): bool { $this->resetErrorBag(); $values = []; $byAddr = []; foreach ($this->recipients as $i => $r) { $addr = null; if (!empty($r['mail_user_id']) && $this->domain) { $u = MailUser::find((int)$r['mail_user_id']); if ($u) $addr = Str::lower($u->localpart . '@' . $this->domain->domain); } elseif (!empty($r['email'])) { $addr = Str::lower(trim((string)$r['email'])); } if ($addr) { $values[$i] = $addr; $byAddr[$addr] = $byAddr[$addr] ?? []; $byAddr[$addr][] = $i; } } $hasDupes = false; foreach ($byAddr as $addr => $idxs) { if (count($idxs) <= 1) continue; $hasDupes = true; foreach ($idxs as $i) { if (!empty($this->recipients[$i]['mail_user_id'])) { $this->addError("recipients.$i.mail_user_id", 'Dieser Empfänger ist bereits hinzugefügt.'); } else { $this->addError("recipients.$i.email", 'Dieser Empfänger ist bereits hinzugefügt.'); } } } return $hasDupes; } private function aliasConflictsWithMailbox(): bool { if (!$this->domainId || $this->local === '') return false; $local = Str::lower(trim($this->local)); return MailUser::query() ->where('domain_id', $this->domainId) ->whereRaw('LOWER(localpart) = ?', [$local]) ->exists(); } // -------------------- Save -------------------- public function save(): void { $this->validate($this->rules(), $this->messages()); foreach ($this->recipients as $i => $r) { if (empty($r['mail_user_id']) && empty($r['email'])) { $this->addError("recipients.$i.email", 'Wähle internes Postfach oder externe E-Mail.'); return; } } if ($this->validateRecipientDuplicates()) return; if ($aliasAddr = $this->currentAliasAddress()) { foreach ($this->recipients as $i => $r) { if (!empty($r['email']) && Str::lower(trim((string)$r['email'])) === $aliasAddr) { $this->addError("recipients.$i.email", 'Alias darf nicht an sich selbst weiterleiten.'); return; } if (!empty($r['mail_user_id'])) { $u = MailUser::find((int)$r['mail_user_id']); if ($u && $this->domain && Str::lower($u->localpart . '@' . $this->domain->domain) === $aliasAddr) { $this->addError("recipients.$i.mail_user_id", 'Alias darf nicht an sich selbst weiterleiten.'); return; } } } } if ($this->aliasConflictsWithMailbox()) { $this->addError('local', 'Diese Adresse ist bereits als Postfach vergeben.'); return; } $rows = collect($this->recipients) ->map(fn($r) => [ 'mail_user_id' => $r['mail_user_id'] ?: null, 'email' => isset($r['email']) && $r['email'] !== '' ? Str::lower(trim($r['email'])) : null, ]) ->filter(fn($r) => $r['mail_user_id'] || $r['email']) ->values(); $isUpdate = (bool) $this->aliasId; DB::transaction(function () use ($rows) { /** @var MailAlias $alias */ $alias = MailAlias::updateOrCreate( ['id' => $this->aliasId], [ 'domain_id' => $this->domainId, 'local' => $this->local, 'type' => $this->type, 'group_name' => $this->type === 'group' ? ($this->group_name ?: null) : null, 'is_active' => $this->is_active, 'notes' => $this->notes, ] ); $alias->recipients()->delete(); foreach ($rows as $i => $r) { $alias->recipients()->create([ 'mail_user_id' => $r['mail_user_id'], 'email' => $r['email'], 'position' => $i, ]); } $this->aliasId = $alias->id; }); $email = $this->currentAliasAddress(); if (!$email) { $domainName = $this->domain?->domain ?? optional(\App\Models\Domain::find($this->domainId))->domain; $email = trim(strtolower($this->local)) . '@' . trim(strtolower((string) $domainName)); } $this->dispatch('alias:' . ($this->aliasId ? 'updated' : 'created')); $this->dispatch('closeModal'); $typeLabel = $this->type === 'single' ? 'Single-Alias' : 'Gruppen-Alias'; $action = $this->aliasId ? 'aktualisiert' : 'erstellt'; $this->dispatch('toast', type: 'done', badge: 'Alias', title: "$typeLabel $action", text: "Der $typeLabel " . e($email) . " wurde erfolgreich $action.", duration: 6000 ); } // -------------------- Validation -------------------- protected function rules(): array { $occupied = $this->domainId ? MailUser::query() ->where('domain_id', $this->domainId) ->pluck('localpart') ->map(fn($l) => Str::lower($l)) ->all() : []; return [ 'domainId' => ['required', 'exists:domains,id'], 'local' => [ 'required', 'regex:/^[A-Za-z0-9._%+-]+$/', Rule::unique('mail_aliases', 'local') ->ignore($this->aliasId) ->where(fn($q) => $q->where('domain_id', $this->domainId)), Rule::notIn($occupied), ], 'type' => ['required', 'in:single,group'], 'group_name' => ['nullable','string','max:80','required_if:type,group'], 'recipients' => ['array', 'min:1', 'max:' . ($this->type === 'single' ? 1 : $this->maxGroupRecipients)], 'recipients.*.mail_user_id' => ['nullable', 'exists:mail_users,id'], 'recipients.*.email' => ['nullable', 'email:rfc'], ]; } protected function messages(): array { return [ 'local.required' => 'Alias-Adresse ist erforderlich.', 'local.regex' => 'Erlaubt sind Buchstaben, Zahlen und ._%+-', 'local.unique' => 'Dieser Alias existiert in dieser Domain bereits.', 'local.not_in' => 'Diese Adresse ist bereits als Postfach vergeben.', 'recipients.min' => 'Mindestens ein Empfänger ist erforderlich.', 'recipients.max' => $this->type === 'single' ? 'Bei „Single“ ist nur ein Empfänger zulässig.' : 'Maximal ' . $this->maxGroupRecipients . ' Empfänger erlaubt.', ]; } public function render() { return view('livewire.ui.mail.modal.alias-form-modal', [ 'domains' => Domain::where('is_system', false)->where('is_server', false)->orderBy('domain')->get(['id', 'domain']), ]); } }