*/ public array $domains = []; public string $email_preview = ''; // Formular public string $localpart = ''; public ?string $display_name = null; public ?string $password = null; public int $quota_mb = 0; public ?int $rate_limit_per_hour = null; public bool $is_active = true; public bool $must_change_pw = true; // Limits / Status public ?int $limit_max_mailboxes = null; public ?int $limit_default_quota_mb = null; public ?int $limit_max_quota_per_mb = null; public ?int $limit_total_quota_mb = null; // 0 = unlimitiert public ?int $limit_domain_rate_per_hour = null; public bool $allow_rate_limit_override = false; public int $mailbox_count_used = 0; public int $domain_storage_used_mb = 0; // Hints/Flags public string $quota_hint = ''; public bool $rate_limit_readonly = false; public bool $no_mailbox_slots = false; public bool $no_storage_left = false; public bool $can_create = true; public string $block_reason = ''; /* ---------- Validation ---------- */ protected function rules(): array { $maxPerMailbox = $this->limit_max_quota_per_mb ?? PHP_INT_MAX; $remainingByTotal = (is_null($this->limit_total_quota_mb) || (int)$this->limit_total_quota_mb === 0) ? PHP_INT_MAX : max(0, (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb); $cap = min($maxPerMailbox, $remainingByTotal); // alle Alias-Localparts der Domain (kleingeschrieben) – verhindert Kollision Mailbox vs. Alias $aliasLocals = $this->domain_id ? MailAlias::query() ->where('domain_id', $this->domain_id) ->pluck('local') ->map(fn($l) => Str::lower($l)) ->all() : []; return [ 'domain_id' => ['required', Rule::exists('domains', 'id')], 'localpart' => [ 'required', 'max:191', 'regex:/^[A-Za-z0-9._%+-]+$/', // darf nicht als Alias existieren (gleiches Domain-Scope) Rule::notIn($aliasLocals), // und auch kein bestehendes Postfach mit gleichem Localpart Rule::unique('mail_users', 'localpart') ->where(fn($q) => $q->where('domain_id', $this->domain_id)), ], 'display_name' => ['nullable', 'max:191'], 'password' => ['nullable', 'min:8'], 'quota_mb' => ['required', 'integer', 'min:0', 'max:' . $cap], 'rate_limit_per_hour' => ['nullable', 'integer', 'min:1'], 'is_active' => ['boolean'], 'must_change_pw' => ['boolean'], ]; } /* ---------- Lifecycle ---------- */ public function mount(?int $domainId = null): void { // alle Nicht-System-Domains in Select $this->domains = Domain::query() ->where('is_system', false) ->orderBy('domain')->get(['id', 'domain'])->toArray(); // vorselektieren falls mitgegeben, sonst 1. Domain (falls vorhanden) $this->domain_id = $domainId ?: ($this->domains[0]['id'] ?? null); // Limits + Anzeige laden $this->syncDomainContext(); } public function updatedDomainId(): void { $this->resetErrorBag(); // scoped unique etc. $this->syncDomainContext(); // bei Domainwechsel Alias-Kollision neu prüfen $this->checkAliasCollisionLive(); } public function updatedLocalpart(): void { $this->localpart = strtolower(trim($this->localpart)); $this->rebuildEmailPreview(); $this->checkAliasCollisionLive(); } public function updatedQuotaMb(): void { $this->recomputeQuotaHints(); $this->recomputeBlockers(); } /* ---------- Helpers ---------- */ private function syncDomainContext(): void { if (!$this->domain_id) return; $d = Domain::query() ->withCount('mailUsers') ->withSum('mailUsers as used_storage_mb', 'quota_mb') ->findOrFail($this->domain_id); $this->domain_name = $d->domain; $this->limit_max_mailboxes = (int)$d->max_mailboxes; $this->limit_default_quota_mb = (int)$d->default_quota_mb; $this->limit_max_quota_per_mb = $d->max_quota_per_mailbox_mb !== null ? (int)$d->max_quota_per_mailbox_mb : null; $this->limit_total_quota_mb = (int)$d->total_quota_mb; // 0 = unlimitiert $this->limit_domain_rate_per_hour = $d->rate_limit_per_hour !== null ? (int)$d->rate_limit_per_hour : null; $this->allow_rate_limit_override = (bool)$d->rate_limit_override; $this->mailbox_count_used = (int)$d->mail_users_count; $this->domain_storage_used_mb = (int)($d->used_storage_mb ?? 0); // Defaults $this->quota_mb = $this->limit_default_quota_mb ?? 0; if (!$this->allow_rate_limit_override) { $this->rate_limit_per_hour = $this->limit_domain_rate_per_hour; $this->rate_limit_readonly = true; } else { $this->rate_limit_per_hour = $this->limit_domain_rate_per_hour; $this->rate_limit_readonly = false; } $this->rebuildEmailPreview(); $this->recomputeQuotaHints(); $this->recomputeBlockers(); } private function rebuildEmailPreview(): void { $this->email_preview = $this->localpart && $this->domain_name ? ($this->localpart . '@' . $this->domain_name) : ''; } private function recomputeQuotaHints(): void { $parts = []; if (!is_null($this->limit_total_quota_mb) && (int)$this->limit_total_quota_mb > 0) { $remainingNow = max(0, (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb); $remainingAfter = max(0, $remainingNow - max(0, (int)$this->quota_mb)); $parts[] = "Verbleibend jetzt: {$remainingNow} MiB"; $parts[] = "nach Speichern: {$remainingAfter} MiB"; } if (!is_null($this->limit_max_quota_per_mb)) $parts[] = "Max {$this->limit_max_quota_per_mb} MiB pro Postfach"; if (!is_null($this->limit_default_quota_mb)) $parts[] = "Standard: {$this->limit_default_quota_mb} MiB"; $this->quota_hint = implode(' · ', $parts); } private function recomputeBlockers(): void { // Slots $this->no_mailbox_slots = false; if (!is_null($this->limit_max_mailboxes)) { $free = (int)$this->limit_max_mailboxes - (int)$this->mailbox_count_used; if ($free <= 0) $this->no_mailbox_slots = true; } // Speicher $this->no_storage_left = false; if (!is_null($this->limit_total_quota_mb) && (int)$this->limit_total_quota_mb > 0) { $remaining = (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb; if ($remaining <= 0) $this->no_storage_left = true; } $reasons = []; if ($this->no_mailbox_slots) $reasons[] = 'Keine freien Postfach-Slots in dieser Domain.'; if ($this->no_storage_left) $reasons[] = 'Kein Domain-Speicher mehr verfügbar.'; $this->block_reason = implode(' ', $reasons); $this->can_create = !($this->no_mailbox_slots || $this->no_storage_left); } /** Prüft live, ob bereits ein Alias gleichen Localparts existiert, und setzt eine Feldfehlermeldung. */ private function checkAliasCollisionLive(): void { $this->resetErrorBag('localpart'); if ($this->aliasExistsForLocalpart($this->domain_id, $this->localpart)) { $this->addError('localpart', 'Dieser Name ist bereits als Alias in dieser Domain vergeben.'); } } /** true, wenn in der Domain ein Alias mit gleichem Localpart existiert (case-insensitiv) */ private function aliasExistsForLocalpart(?int $domainId, ?string $local): bool { $local = Str::lower(trim((string)$local)); if (!$domainId || $local === '') return false; return MailAlias::query() ->where('domain_id', $domainId) ->whereRaw('LOWER(local) = ?', [$local]) ->exists(); } /* ---------- Save ---------- */ #[On('mailbox:create')] public function save(): void { $this->recomputeBlockers(); if (!$this->can_create) { $this->addError('domain_id', $this->block_reason ?: 'Erstellung aktuell nicht möglich.'); return; } // Vorab-Hard-Check gegen Alias-Kollision (zusätzlich zur Validation) if ($this->aliasExistsForLocalpart($this->domain_id, $this->localpart)) { $this->addError('localpart', 'Diese Adresse ist bereits als Alias vorhanden.'); return; } $data = $this->validate(); $email = $data['localpart'] . '@' . $this->domain_name; try { $u = new MailUser(); $u->domain_id = $data['domain_id']; $u->localpart = $data['localpart']; $u->email = $email; $u->display_name = $this->display_name ?: null; $u->password_hash = $this->password ? Hash::make($this->password) : null; $u->is_system = false; $u->is_active = (bool)$data['is_active']; $u->must_change_pw = (bool)$data['must_change_pw']; $u->quota_mb = (int)$data['quota_mb']; $u->rate_limit_per_hour = $data['rate_limit_per_hour']; $u->save(); } catch (QueryException $e) { $msg = strtolower($e->getMessage()); if (str_contains($msg, 'mail_users_domain_localpart_unique')) { $this->addError('localpart', 'Dieses Postfach existiert in dieser Domain bereits.'); return; } if (str_contains($msg, 'mail_users_email_unique')) { $this->addError('localpart', 'Diese E-Mail-Adresse ist bereits vergeben.'); return; } throw $e; } $this->dispatch('mailbox:created'); $this->dispatch('closeModal'); $this->dispatch('toast', type: 'done', badge: 'Postfach', title: 'Postfach angelegt', text: 'Das Postfach ' . e($email) . ' wurde erfolgreich angelegt.', duration: 6000 ); } public static function modalMaxWidth(): string { return '3xl'; } public function render() { return view('livewire.ui.mail.modal.mailbox-create-modal'); } } //namespace App\Livewire\Ui\Mail\Modal; // //use App\Models\Domain; //use App\Models\MailUser; //use Illuminate\Database\QueryException; //use Illuminate\Support\Facades\Hash; //use Illuminate\Validation\Rule; //use Livewire\Attributes\On; //use LivewireUI\Modal\ModalComponent; // //class MailboxCreateModal extends ModalComponent //{ // // optional vorselektierte Domain // public ?int $domain_id = null; // // // Anzeige // public string $domain_name = ''; // /** @var array */ // public array $domains = []; // public string $email_preview = ''; // // public string $localpart = ''; // public ?string $display_name = null; // public ?string $password = null; // public int $quota_mb = 0; // public ?int $rate_limit_per_hour = null; // public bool $is_active = true; // public bool $must_change_pw = true; // // // Limits / Status // public ?int $limit_max_mailboxes = null; // public ?int $limit_default_quota_mb = null; // public ?int $limit_max_quota_per_mb = null; // public ?int $limit_total_quota_mb = null; // 0 = unlimitiert // public ?int $limit_domain_rate_per_hour = null; // public bool $allow_rate_limit_override = false; // // public int $mailbox_count_used = 0; // public int $domain_storage_used_mb = 0; // // // Hints/Flags // public string $quota_hint = ''; // public bool $rate_limit_readonly = false; // public bool $no_mailbox_slots = false; // public bool $no_storage_left = false; // public bool $can_create = true; // public string $block_reason = ''; // // /* ---------- Validation ---------- */ // protected function rules(): array // { // $maxPerMailbox = $this->limit_max_quota_per_mb ?? PHP_INT_MAX; // $remainingByTotal = (is_null($this->limit_total_quota_mb) || (int)$this->limit_total_quota_mb === 0) // ? PHP_INT_MAX // : max(0, (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb); // $cap = min($maxPerMailbox, $remainingByTotal); // // return [ // 'domain_id' => ['required', Rule::exists('domains', 'id')], // 'localpart' => [ // 'required', 'max:191', 'regex:/^[A-Za-z0-9._%+-]+$/', // Rule::unique('mail_users', 'localpart')->where(fn($q) => $q->where('domain_id', $this->domain_id)), // ], // 'display_name' => ['nullable', 'max:191'], // 'password' => ['nullable', 'min:8'], // 'quota_mb' => ['required', 'integer', 'min:0', 'max:' . $cap], // 'rate_limit_per_hour' => ['nullable', 'integer', 'min:1'], // 'is_active' => ['boolean'], // 'must_change_pw' => ['boolean'], // ]; // } // // /* ---------- Lifecycle ---------- */ // public function mount(?int $domainId = null): void // { // // alle Nicht-System-Domains in Select // $this->domains = Domain::query() // ->where('is_system', false) // ->orderBy('domain')->get(['id', 'domain'])->toArray(); // // // vorselektieren falls mitgegeben, sonst 1. Domain (falls vorhanden) // $this->domain_id = $domainId ?: ($this->domains[0]['id'] ?? null); // // // Limits + Anzeige laden // $this->syncDomainContext(); // } // // public function updatedDomainId(): void // { // $this->resetErrorBag(); // scoped unique etc. // $this->syncDomainContext(); // } // // public function updatedLocalpart(): void // { // $this->localpart = strtolower(trim($this->localpart)); // $this->rebuildEmailPreview(); // } // // public function updatedQuotaMb(): void // { // $this->recomputeQuotaHints(); // $this->recomputeBlockers(); // } // // /* ---------- Helpers ---------- */ // private function syncDomainContext(): void // { // if (!$this->domain_id) return; // // $d = Domain::query() // ->withCount('mailUsers') // ->withSum('mailUsers as used_storage_mb', 'quota_mb') // ->findOrFail($this->domain_id); // // $this->domain_name = $d->domain; // $this->limit_max_mailboxes = (int)$d->max_mailboxes; // $this->limit_default_quota_mb = (int)$d->default_quota_mb; // $this->limit_max_quota_per_mb = $d->max_quota_per_mailbox_mb !== null ? (int)$d->max_quota_per_mailbox_mb : null; // $this->limit_total_quota_mb = (int)$d->total_quota_mb; // 0 = unlimitiert // $this->limit_domain_rate_per_hour = $d->rate_limit_per_hour !== null ? (int)$d->rate_limit_per_hour : null; // $this->allow_rate_limit_override = (bool)$d->rate_limit_override; // // $this->mailbox_count_used = (int)$d->mail_users_count; // $this->domain_storage_used_mb = (int)($d->used_storage_mb ?? 0); // // // Defaults // $this->quota_mb = $this->limit_default_quota_mb ?? 0; // if (!$this->allow_rate_limit_override) { // $this->rate_limit_per_hour = $this->limit_domain_rate_per_hour; // $this->rate_limit_readonly = true; // } else { // $this->rate_limit_per_hour = $this->limit_domain_rate_per_hour; // $this->rate_limit_readonly = false; // } // // $this->rebuildEmailPreview(); // $this->recomputeQuotaHints(); // $this->recomputeBlockers(); // } // // private function rebuildEmailPreview(): void // { // $this->email_preview = $this->localpart && $this->domain_name // ? ($this->localpart . '@' . $this->domain_name) : ''; // } // // private function recomputeQuotaHints(): void // { // $parts = []; // // if (!is_null($this->limit_total_quota_mb) && (int)$this->limit_total_quota_mb > 0) { // $remainingNow = max(0, (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb); // $remainingAfter = max(0, $remainingNow - max(0, (int)$this->quota_mb)); // $parts[] = "Verbleibend jetzt: {$remainingNow} MiB"; // $parts[] = "nach Speichern: {$remainingAfter} MiB"; // } // if (!is_null($this->limit_max_quota_per_mb)) $parts[] = "Max {$this->limit_max_quota_per_mb} MiB pro Postfach"; // if (!is_null($this->limit_default_quota_mb)) $parts[] = "Standard: {$this->limit_default_quota_mb} MiB"; // // $this->quota_hint = implode(' · ', $parts); // } // // private function recomputeBlockers(): void // { // // Slots // $this->no_mailbox_slots = false; // if (!is_null($this->limit_max_mailboxes)) { // $free = (int)$this->limit_max_mailboxes - (int)$this->mailbox_count_used; // if ($free <= 0) $this->no_mailbox_slots = true; // } // // // Speicher // $this->no_storage_left = false; // if (!is_null($this->limit_total_quota_mb) && (int)$this->limit_total_quota_mb > 0) { // $remaining = (int)$this->limit_total_quota_mb - (int)$this->domain_storage_used_mb; // if ($remaining <= 0) $this->no_storage_left = true; // } // // $reasons = []; // if ($this->no_mailbox_slots) $reasons[] = 'Keine freien Postfach-Slots in dieser Domain.'; // if ($this->no_storage_left) $reasons[] = 'Kein Domain-Speicher mehr verfügbar.'; // $this->block_reason = implode(' ', $reasons); // $this->can_create = !($this->no_mailbox_slots || $this->no_storage_left); // } // // /* ---------- Save ---------- */ // #[On('mailbox:create')] // public function save(): void // { // $this->recomputeBlockers(); // if (!$this->can_create) { // $this->addError('domain_id', $this->block_reason ?: 'Erstellung aktuell nicht möglich.'); // return; // } // // $data = $this->validate(); // $email = $data['localpart'] . '@' . $this->domain_name; // // try { // $u = new MailUser(); // $u->domain_id = $data['domain_id']; // $u->localpart = $data['localpart']; // $u->email = $email; // $u->display_name = $this->display_name ?: null; // $u->password_hash = $this->password ? Hash::make($this->password) : null; // $u->is_system = false; // $u->is_active = (bool)$data['is_active']; // $u->must_change_pw = (bool)$data['must_change_pw']; // $u->quota_mb = (int)$data['quota_mb']; // $u->rate_limit_per_hour = $data['rate_limit_per_hour']; // $u->save(); // } catch (QueryException $e) { // $msg = strtolower($e->getMessage()); // if (str_contains($msg, 'mail_users_domain_localpart_unique')) { // $this->addError('localpart', 'Dieses Postfach existiert in dieser Domain bereits.'); // return; // } // if (str_contains($msg, 'mail_users_email_unique')) { // $this->addError('localpart', 'Diese E-Mail-Adresse ist bereits vergeben.'); // return; // } // throw $e; // } // // $this->dispatch('mailbox:created'); // $this->dispatch('closeModal'); // $this->dispatch('toast', // type: 'done', // badge: 'Postfach', // title: 'Postfach angelegt', // text: 'Das Postfach ' . e($email) . ' wurde erfolgreich angelegt.', // duration: 6000 // ); // // } // // public static function modalMaxWidth(): string // { // return '3xl'; // } // // public function render() // { // return view('livewire.ui.mail.modal.mailbox-create-modal'); // } //}