mailwolt/app/Livewire/Ui/Mail/Modal/AliasFormModal.php

446 lines
16 KiB
PHP

<?php
namespace App\Livewire\Ui\Mail\Modal;
use App\Models\Domain;
use App\Models\MailAlias;
use App\Models\MailUser;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use LivewireUI\Modal\ModalComponent;
class AliasFormModal extends ModalComponent
{
// Eingabe / State
public ?int $aliasId = null;
public ?int $domainId = null;
public string $local = '';
public string $type = 'single'; // single|group
public ?string $group_name = null;
public bool $is_active = true;
public ?string $notes = null;
public array $recipients = [];
// UI
public int $maxGroupRecipients = 20;
public bool $canAddRecipient = false;
public array $rowState = [];
public array $disabledMailboxIdsByRow = [];
public array $rowDuplicateError = [];
// Lookup
public ?Domain $domain = null;
/** @var \Illuminate\Support\Collection<int,\App\Models\MailUser> */
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 <b>" . e($email) . "</b> 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']),
]);
}
}