*/
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');
// }
//}