env('SERVER_PUBLIC_IPV4'), 'ipv6' => env('SERVER_PUBLIC_IPV6'), 'spf_tail' => config('mailpool.spf_tail', '~all'), 'spf_extra' => [], 'dmarc_policy' => config('mailpool.dmarc_policy', 'none'), 'rua' => "mailto:dmarc@{$domain->domain}", ], $opts); // --- DKIM aus DB ziehen falls nicht übergeben --- if (!$dkimSelector || !$dkimTxt) { /** @var DkimKey|null $dk */ $dk = $domain->dkimKeys()->where('is_active', true)->latest()->first(); if ($dk) { $dkimSelector = $dk->selector; $dkimTxt = "v=DKIM1; k=rsa; p={$dk->public_key_txt}"; } } // --- SPF/DMARC in DB persistieren (oder aktualisieren) --- $spfTxt = $this->buildSpfTxt($domain, $opts); $dmarcTxt = $this->buildDmarcTxt($domain, $opts['dmarc_policy'], $opts['rua']); // spf_records if (method_exists($domain, 'spf')) { $domain->spf()->updateOrCreate( ['is_active' => true], ['record_txt' => $spfTxt] ); } // dmarc_records if (method_exists($domain, 'dmarc')) { $domain->dmarc()->updateOrCreate( ['policy' => $opts['dmarc_policy']], [ 'rua' => $opts['rua'], 'pct' => 100, 'record_txt' => $dmarcTxt, 'is_active' => true, ] ); } // --- DNS-Empfehlungen berechnen --- $records = $this->buildForDomain($domain, array_merge($opts, [ 'dkim_selector' => $dkimSelector, 'dkim_txt' => $dkimTxt, ])); // Optional in eine eigene dnsRecords()-Relation persistieren $this->persist($domain, $records); return $records; } /** SPF-String zusammenbauen (mx a [+extra] + tail) */ public function buildSpfTxt(Domain $domain, array $opts = []): string { $tail = $opts['spf_tail'] ?? '~all'; $extra = $opts['spf_extra'] ?? []; $parts = ['v=spf1', 'mx', 'a']; // Falls Server-IP explizit – praktisch fürs On-Prem if (!empty($opts['ipv4'])) $parts[] = 'ip4:' . $opts['ipv4']; if (!empty($opts['ipv6'])) $parts[] = 'ip6:' . $opts['ipv6']; foreach ($extra as $m) { $m = trim((string)$m); if ($m !== '') $parts[] = $m; } $parts[] = $tail; // "~all" oder "-all" return implode(' ', $parts); } /** DMARC-String bauen (p=policy; rua=mailto:..; pct=100) */ public function buildDmarcTxt(Domain $domain, string $policy = 'none', string $rua = null): string { $rua = $rua ?: "mailto:dmarc@{$domain->domain}"; return "v=DMARC1; p={$policy}; rua={$rua}; pct=100"; } // ----------------------------------------------------------- // Ab hier deine vorhandenen Helfer (leicht erweitert): // ----------------------------------------------------------- public function buildForDomain(Domain $domain, array $opts = []): array { $baseDomain = env('BASE_DOMAIN', 'example.com'); $uiSub = env('UI_SUB', 'ui'); $webSub = env('WEBMAIL_SUB', 'webmail'); $mxSub = env('MTA_SUB', 'mx'); $uiHost = $uiSub ? "{$uiSub}.{$baseDomain}" : $baseDomain; $webmail = $webSub ? "{$webSub}.{$baseDomain}" : $baseDomain; $mtaHost = $mxSub ? "{$mxSub}.{$baseDomain}" : $baseDomain; $ipv4 = $opts['ipv4'] ?? null; $ipv6 = $opts['ipv6'] ?? null; $spfTxt = $opts['spf_txt'] ?? $this->buildSpfTxt($domain, $opts); $dmarcTxt = $opts['dmarc_txt'] ?? $this->buildDmarcTxt($domain, $opts['dmarc_policy'] ?? 'none', $opts['rua'] ?? null); $dkimSelector = $opts['dkim_selector'] ?? null; $dkimTxt = $opts['dkim_txt'] ?? null; $R = fn($type,$name,$value,$ttl=3600) => compact('type','name','value','ttl'); if ($dkimSelector && is_string($dkimTxt)) { $dkimTxt = trim($dkimTxt); // Falls nur Base64 geliefert: auf DKIM-Format heben if ($dkimTxt !== '' && !str_starts_with($dkimTxt, 'v=DKIM1')) { if (!preg_match('/^[A-Za-z0-9+\/=]+$/', $dkimTxt)) { Log::warning('DKIM TXT invalid chars', ['len'=>strlen($dkimTxt)]); $dkimTxt = ''; // hart ablehnen statt kaputt speichern } else { $dkimTxt = "v=DKIM1; k=rsa; p={$dkimTxt}"; } } if ($dkimTxt !== '') { $required[] = $R('TXT', "{$dkimSelector}._domainkey.{$domain->domain}", $dkimTxt); } } $required = [ $R('MX', $domain->domain, "10 {$mtaHost}."), $R('TXT', $domain->domain, $spfTxt), $R('TXT', "_dmarc.{$domain->domain}", $dmarcTxt), ]; if ($dkimSelector && $dkimTxt) { $required[] = $R('TXT', "{$dkimSelector}._domainkey.{$domain->domain}", $dkimTxt); } $optional = [ $R('CAA', $domain->domain, '0 issue "letsencrypt.org"'), $R('CNAME', "webmail.{$domain->domain}", "{$webmail}."), $R('CNAME', "ui.{$domain->domain}", "{$uiHost}."), $R('SRV', "_submission._tcp.{$domain->domain}", "0 1 587 {$mtaHost}."), $R('SRV', "_imaps._tcp.{$domain->domain}", "0 1 993 {$mtaHost}."), $R('SRV', "_pop3s._tcp.{$domain->domain}", "0 1 995 {$mtaHost}."), $R('CNAME', "autoconfig.{$domain->domain}", "{$uiHost}."), $R('CNAME', "autodiscover.{$domain->domain}", "{$uiHost}."), ]; if (method_exists($domain, 'tlsaRecords')) { $tlsa = $domain->tlsaRecords() ->where('service', '_25._tcp') ->latest() ->first(); if ($tlsa) { $optional[] = $R( 'TLSA', "{$tlsa->service}.{$tlsa->host}", "{$tlsa->usage} {$tlsa->selector} {$tlsa->matching} {$tlsa->hash}" ); } } if ($ipv4) $optional[] = $R('A', $domain->domain, $ipv4); if ($ipv6) $optional[] = $R('AAAA', $domain->domain, $ipv6); return ['required'=>$required,'optional'=>$optional]; } public function persist(Domain $domain, array $records): void { if (!method_exists($domain, 'dnsRecords')) return; $upsert = function(array $rec) use ($domain) { $domain->dnsRecords()->updateOrCreate( ['type'=>$rec['type'], 'name'=>$rec['name']], ['value'=>$rec['value'], 'ttl'=>$rec['ttl'], 'is_managed'=>true] ); }; foreach ($records['required'] ?? [] as $r) $upsert($r); foreach ($records['optional'] ?? [] as $r) $upsert($r); } // public function buildForDomain(Domain $domain, array $opts = []): array // { // // ---- Aus ENV lesen (deine Installer-Variablen) ---- // $baseDomain = env('BASE_DOMAIN', 'example.com'); // $uiSub = env('UI_SUB', 'ui'); // $webSub = env('WEBMAIL_SUB', 'webmail'); // $mxSub = env('MTA_SUB', 'mx'); // // // Ziel-Hosts (wohin die Kundendomain zeigen soll) // $uiHost = $uiSub ? "{$uiSub}.{$baseDomain}" : $baseDomain; // $webmail = $webSub ? "{$webSub}.{$baseDomain}" : $baseDomain; // $mtaHost = $mxSub ? "{$mxSub}.{$baseDomain}" : $baseDomain; // // // Public IPs (falls gesetzt; sonst leer -> nur Anzeige) // $ipv4 = $opts['ipv4'] ?? env('SERVER_PUBLIC_IPV4'); // z.B. vom Installer in .env geschrieben // $ipv6 = $opts['ipv6'] ?? env('SERVER_PUBLIC_IPV6'); // // // Policies // $dmarcPolicy = $opts['dmarc_policy'] ?? 'none'; // none | quarantine | reject // $spfTail = $opts['spf_tail'] ?? '~all'; // ~all | -all (streng) // // // DKIM (neuester aktiver Key) // /** @var DkimKey|null $dkim */ // $dkim = $domain->dkimKeys()->where('is_active', true)->latest()->first(); // $dkimSelector = $dkim?->selector ?: 'dkim'; // $dkimTxt = $dkim ? "v=DKIM1; k=rsa; p={$dkim->public_key_txt}" : null; // // // Helper // $R = fn(string $type, string $name, string $value, int $ttl = 3600) => [ // 'type' => $type, 'name' => $name, 'value' => $value, 'ttl' => $ttl, // ]; // // // ========== REQUIRED ========== // $required = []; // // // MX (zeigt auf dein globales MX-Host) // $required[] = $R('MX', $domain->domain, "10 {$mtaHost}."); // // // SPF: „mx“ + optional a (du hattest vorher -all; hier konfigurierbar) // $spf = trim("v=spf1 mx a {$spfTail}"); // $required[] = $R('TXT', $domain->domain, $spf); // // // DKIM (nur wenn Key vorhanden) // if ($dkimTxt) { // $required[] = $R('TXT', "{$dkimSelector}._domainkey.{$domain->domain}", $dkimTxt); // } // // // DMARC (Default p=$dmarcPolicy + RUA) // $required[] = $R('TXT', "_dmarc.{$domain->domain}", "v=DMARC1; p={$dmarcPolicy}; rua=mailto:dmarc@{$domain->domain}; pct=100"); // // // ========== OPTIONAL (empfohlen) ========== // $optional = []; // // // A/AAAA für Root – NUR falls du Root direkt terminierst (sonst weglassen) // if ($ipv4) $optional[] = $R('A', $domain->domain, $ipv4); // if ($ipv6) $optional[] = $R('AAAA', $domain->domain, $ipv6); // // // CAA für ACME/Let’s Encrypt // // 0 issue "letsencrypt.org" | optional: 0 iodef "mailto:admin@domain" // $optional[] = $R('CAA', $domain->domain, '0 issue "letsencrypt.org"'); // // // CNAMEs für UI/Webmail in der Kundenzone -> zeigen auf deine globalen Hosts // $optional[] = $R('CNAME', "webmail.{$domain->domain}", "{$webmail}."); // $optional[] = $R('CNAME', "ui.{$domain->domain}", "{$uiHost}."); // // // SRV (nutzerfreundliche Autokonfigs) // // _submission._tcp STARTTLS Port 587 // $optional[] = $R('SRV', "_submission._tcp.{$domain->domain}", "0 1 587 {$mtaHost}."); // // _imaps._tcp / _pop3s._tcp (falls aktiv) // $optional[] = $R('SRV', "_imaps._tcp.{$domain->domain}", "0 1 993 {$mtaHost}."); // $optional[] = $R('SRV', "_pop3s._tcp.{$domain->domain}", "0 1 995 {$mtaHost}."); // // // Autoconfig / Autodiscover (wenn du sie anbieten willst) // // CNAMEs auf deine UI/Webmail (oder A/AAAA, wenn du echte Subdomains je Kunde willst) // $optional[] = $R('CNAME', "autoconfig.{$domain->domain}", "{$uiHost}."); // $optional[] = $R('CNAME', "autodiscover.{$domain->domain}", "{$uiHost}."); // // return [ // 'required' => $required, // 'optional' => $optional, // 'meta' => [ // 'mx_target' => $mtaHost, // 'ui_target' => $uiHost, // 'webmail_target'=> $webmail, // 'dkim_selector' => $dkimSelector, // 'has_dkim' => (bool) $dkimTxt, // 'tips' => [ // 'rDNS' => 'Reverse DNS der Server-IP sollte auf den MX-Host zeigen.', // ], // ], // ]; // } // // /** // * Optional: Empfohlene/benötigte Records in deiner DB speichern. // * Nutzt $domain->dnsRecords() falls vorhanden. Andernfalls einfach nicht verwenden. // */ // public function persist(Domain $domain, array $records): void // { // if (!method_exists($domain, 'dnsRecords')) { // return; // } // // $upsert = function(array $rec) use ($domain) { // $domain->dnsRecords()->updateOrCreate( // ['type' => $rec['type'], 'name' => $rec['name']], // ['value' => $rec['value'], 'ttl' => $rec['ttl'], 'is_managed' => true] // ); // }; // // foreach (($records['required'] ?? []) as $r) $upsert($r); // foreach (($records['optional'] ?? []) as $r) $upsert($r); // } // // /** // * Erzeugt empfohlene DNS-Records für eine neue Domain // * und speichert sie (falls Domain->dnsRecords() existiert). // */ // public function createRecommendedRecords( // Domain $domain, // ?string $dkimSelector = null, // ?string $dkimTxt = null, // array $opts = [] // ): array { // // Fallback, falls kein DKIM-Schlüssel übergeben wurde // if (!$dkimSelector || !$dkimTxt) { // $dkim = $domain->dkimKeys()->where('is_active', true)->latest()->first(); // $dkimSelector ??= $dkim?->selector ?? 'dkim'; // $dkimTxt ??= $dkim ? "v=DKIM1; k=rsa; p={$dkim->public_key_txt}" : null; // } // // // DNS-Empfehlungen generieren // $records = $this->buildForDomain($domain, array_merge($opts, [ // 'dkim_selector' => $dkimSelector, // 'dkim_txt' => $dkimTxt, // ])); // // // Falls möglich -> in Datenbank persistieren // $this->persist($domain, $records); // // return $records; // } }