kein Limit public bool $rate_limit_override = false; // Mailbox darf Domain-Limit überschreiben public bool $active = true; // DKIM public string $dkim_selector = 'dkim'; public int $dkim_bits = 2048; // 1024/2048/3072/4096 // Anzeige public int $available_mib = 0; public array $tagPalette = ['#22c55e', '#06b6d4', '#a855f7', '#f59e0b', '#ef4444', '#3b82f6']; public function mount(MailStorage $pool): void { // Default $this->max_aliases = (int)config('mailpool.defaults.max_aliases', 400); $this->max_mailboxes = (int)config('mailpool.defaults.max_mailboxes', 10); $this->default_quota_mb = (int)config('mailpool.defaults.default_quota_mb', 3072); $this->max_quota_per_mailbox_mb = config('mailpool.defaults.max_quota_per_mailbox_mb', 3072); $this->total_quota_mb = (int)config('mailpool.defaults.total_quota_mb', 10240); $this->dkim_selector = (string)config('mailpool.defaults.dkim_selector', 'dkim'); $this->dkim_bits = (int)config('mailpool.defaults.dkim_bits', 2048); // Speicherpool-Grenze $this->available_mib = (int)$pool->remainingPoolMb(); $this->total_quota_mb = min($this->total_quota_mb, $this->available_mib); $this->tags = [['label' => '', 'color' => $this->tagPalette[0]]]; } protected function rules(): array { return [ 'domain' => [ 'required','string','lowercase','max:255', 'regex:/^(?!-)(?:[a-z0-9-]+\.)+[a-z]{2,}$/', Rule::unique('domains','domain'), function ($attr, $value, $fail) { $value = \Illuminate\Support\Str::lower($value); $isReserved = \App\Models\Domain::query() ->where('is_system', true) ->whereRaw('LOWER(domain) = ?', [$value]) ->exists(); if ($isReserved) { $fail(__('validation.domain_reserved', ['domain' => $value])); $fail("Die Domain {$value} ist reserviert und kann nicht verwendet werden."); } }, ], 'description' => ['nullable', 'string', 'max:500'], 'tags' => ['array', 'max:50'], 'tags.*.label' => ['nullable', 'string', 'max:40'], 'tags.*.color' => ['nullable', 'regex:/^#[0-9a-fA-F]{6}$/'], 'max_aliases' => ['required', 'integer', 'min:0', 'max:100000'], 'max_mailboxes' => ['required', 'integer', 'min:0', 'max:100000'], 'default_quota_mb' => ['required', 'integer', 'min:0', 'max:2000000'], 'max_quota_per_mailbox_mb' => ['nullable', 'integer', 'min:1', 'max:2000000'], 'total_quota_mb' => ['required', 'integer', 'min:1', 'max:2000000'], 'rate_limit_per_hour' => ['nullable', 'integer', 'min:1', 'max:2000000'], 'rate_limit_override' => ['boolean'], 'active' => ['boolean'], 'dkim_selector' => ['required', 'string', 'max:50', 'regex:/^[a-z0-9\-]+$/i'], 'dkim_bits' => ['required', Rule::in([1024, 2048, 3072, 4096])], ]; } protected function messages(): array { return [ 'domain.required' => 'Bitte gib eine Domain an.', 'domain.string' => 'Die Domain muss als Text eingegeben werden.', 'domain.lowercase' => 'Die Domain darf keine Großbuchstaben enthalten.', 'domain.max' => 'Die Domain darf maximal 255 Zeichen lang sein.', 'domain.regex' => 'Die Domain ist ungültig. Beispiel: mail.example.com', 'domain.unique' => 'Diese Domain ist bereits vorhanden.', 'domain_reserved' => 'Die Domain :domain ist reserviert und kann nicht verwendet werden.', 'description.max' => 'Die Beschreibung darf maximal 500 Zeichen haben.', 'tags.array' => 'Tags müssen als Array übergeben werden.', 'tags.max' => 'Es dürfen maximal 50 Tags verwendet werden.', 'tags.*.label.max' => 'Ein Tag-Label darf maximal 40 Zeichen haben.', 'tags.*.color.regex' => 'Die Farbe eines Tags muss als Hexwert angegeben werden.', 'max_aliases.required' => 'Bitte gib ein Alias-Limit an.', 'max_aliases.integer' => 'Das Alias-Limit muss eine Zahl sein.', 'max_aliases.max' => 'Das Alias-Limit ist zu hoch.', 'max_mailboxes.required' => 'Bitte gib ein Postfach-Limit an.', 'max_mailboxes.integer' => 'Das Postfach-Limit muss eine Zahl sein.', 'max_mailboxes.max' => 'Das Postfach-Limit ist zu hoch.', 'default_quota_mb.required' => 'Bitte gib eine Standard-Quota an.', 'default_quota_mb.integer' => 'Die Standard-Quota muss eine Zahl sein.', 'default_quota_mb.max' => 'Die Standard-Quota ist zu hoch.', 'max_quota_per_mailbox_mb.integer' => 'Die maximale Quota pro Postfach muss eine Zahl sein.', 'max_quota_per_mailbox_mb.max' => 'Die maximale Quota pro Postfach ist zu hoch.', 'total_quota_mb.required' => 'Bitte gib die Gesamt-Quota an.', 'total_quota_mb.integer' => 'Die Gesamt-Quota muss eine Zahl sein.', 'total_quota_mb.max' => 'Die Gesamt-Quota ist zu hoch.', 'rate_limit_per_hour.integer' => 'Das Rate-Limit muss eine Zahl sein.', 'rate_limit_per_hour.max' => 'Das Rate-Limit ist zu hoch.', 'dkim_selector.required' => 'Bitte gib einen DKIM-Selector an.', 'dkim_selector.regex' => 'Der DKIM-Selector darf nur Buchstaben, Zahlen und Bindestriche enthalten.', 'dkim_bits.required' => 'Bitte wähle eine Schlüssellänge.', 'dkim_bits.in' => 'Ungültige DKIM-Schlüssellänge.', ]; } public function addTag(): void { $this->tags[] = ['label' => '', 'color' => $this->tagPalette[0]]; } public function removeTag(int $i): void { unset($this->tags[$i]); $this->tags = array_values($this->tags); } public function pickTagColor(int $i, string $hex): void { if (!isset($this->tags[$i])) return; $hex = $this->normalizeHex($hex); if ($hex) $this->tags[$i]['color'] = $hex; } private function normalizeHex(?string $hex): ?string { $hex = trim((string)$hex); if ($hex === '') return null; if ($hex[0] !== '#') $hex = "#{$hex}"; return preg_match('/^#[0-9a-fA-F]{6}$/', $hex) ? strtolower($hex) : null; } private function assertNotReserved(string $fqdn): void { $zone = config('mailpool.platform_zone'); // z.B. sysmail.your-saas.tld $blocked = [ 'system.'.$fqdn, // falls du es doch schützen willst 'bounce.'.$fqdn, 'mx.'.$fqdn, // nur wenn du's global blocken willst ]; foreach ($blocked as $bad) { if (Str::lower($fqdn) === Str::lower($bad)) { throw ValidationException::withMessages([ 'domain' => 'Diese Domain/Subdomain ist reserviert und kann nicht verwendet werden.', ]); } } // Plattform-Zone darf generell nicht als Benutzer-Domain eingetragen werden: if (Str::endsWith(Str::lower($fqdn), '.'.Str::lower($zone)) || Str::lower($fqdn) === Str::lower($zone)) { throw ValidationException::withMessages([ 'domain' => 'Domains innerhalb der System-Zone sind reserviert.', ]); } } #[On('domain:create')] public function save(MailStorage $pool): void { $this->domain = Str::lower(trim($this->domain)); $this->validate(); // $this->assertNotReserved($this->domain); // Konsistenz-Checks der Eingaben (Create) if ($this->max_quota_per_mailbox_mb !== null && $this->default_quota_mb > $this->max_quota_per_mailbox_mb) { throw ValidationException::withMessages([ 'default_quota_mb' => 'Die Standard-Quota darf die maximale Mailbox-Quota nicht überschreiten.', ]); } if ($this->default_quota_mb > $this->total_quota_mb) { throw ValidationException::withMessages([ 'default_quota_mb' => 'Die Standard-Quota darf die Domain-Gesamtquota nicht überschreiten.', ]); } // Pool-Kapazität $remaining = (int)$pool->remainingPoolMb(); if ($this->total_quota_mb > $remaining) { throw ValidationException::withMessages([ 'total_quota_mb' => 'Nicht genügend Speicher im Pool. Maximal möglich: ' . number_format($remaining) . ' MiB.', ]); } // Tags normalisieren $tagsOut = []; foreach ($this->tags as $t) { $label = trim((string)($t['label'] ?? '')); if ($label === '') continue; $color = $this->normalizeHex($t['color'] ?? '') ?? $this->tagPalette[0]; $tagsOut[] = ['label' => $label, 'color' => $color]; } // Persist $domain = Domain::create([ 'domain' => $this->domain, 'description' => $this->description, 'tags' => $tagsOut, 'is_active' => $this->active, 'is_system' => false, 'max_aliases' => $this->max_aliases, 'max_mailboxes' => $this->max_mailboxes, 'default_quota_mb' => $this->default_quota_mb, 'max_quota_per_mailbox_mb' => $this->max_quota_per_mailbox_mb ?? 0, 'total_quota_mb' => $this->total_quota_mb, 'rate_limit_per_hour' => $this->rate_limit_per_hour, 'rate_limit_override' => $this->rate_limit_override, ]); app(DnsRecordService::class)->provision( $domain, $dkim['selector'] ?? null, $dkim['dns_txt'] ?? null, [ 'spf_tail' => Setting::get('mailpool.spf_tail', '~all'), 'spf_extra' => Setting::get('mailpool.spf_extra', []), 'dmarc_policy' => Setting::get('mailpool.dmarc_policy', 'none'), 'rua' => Setting::get('mailpool.rua', null), ] ); // app(TlsaService::class)->createForDomain($domain); // UI $this->dispatch('domain-created'); $this->dispatch('closeModal'); $this->dispatch('toast', type: 'done', badge: 'Domain', title: 'Domain angelegt', text: 'Die Domain ' . e($this->domain) . ' wurde erfolgreich erstellt. DKIM, SPF und DMARC sind vorbereitet.', duration: 6000, ); } public static function modalMaxWidth(): string { return '3xl'; } public function render() { return view('livewire.ui.domain.modal.domain-create-modal'); } }