294 lines
11 KiB
PHP
294 lines
11 KiB
PHP
<?php
|
||
|
||
|
||
namespace App\Livewire\Ui\Mail\Modal;
|
||
|
||
use App\Models\Domain;
|
||
use App\Models\MailAlias;
|
||
use App\Models\MailUser;
|
||
use Illuminate\Database\QueryException;
|
||
use Illuminate\Support\Facades\Hash;
|
||
use Illuminate\Support\Str;
|
||
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<int,array{id:int,domain:string}> */
|
||
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;
|
||
|
||
// 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'],
|
||
];
|
||
}
|
||
|
||
/* ---------- Lifecycle ---------- */
|
||
public function mount(?int $domainId = null): void
|
||
{
|
||
// alle Nicht-System-Domains in Select
|
||
$this->domains = Domain::query()
|
||
->where('is_system', false)->where('is_server', 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->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 <b>' . e($email) . '</b> wurde erfolgreich angelegt.',
|
||
duration: 6000
|
||
);
|
||
}
|
||
|
||
public static function modalMaxWidth(): string
|
||
{
|
||
return '3xl';
|
||
}
|
||
|
||
public function render()
|
||
{
|
||
return view('livewire.ui.mail.modal.mailbox-create-modal');
|
||
}
|
||
}
|