diff --git a/app/Console/Commands/ProbeRbl.php b/app/Console/Commands/ProbeRbl.php new file mode 100644 index 0000000..7bc016b --- /dev/null +++ b/app/Console/Commands/ProbeRbl.php @@ -0,0 +1,168 @@ +copy()->addDays($this->minIntervalDays) : null; + + if (!$this->option('force') && $nextDue && $now->lt($nextDue)) { + $this->info("Übersprungen: nächste Prüfung erst ab {$nextDue->toIso8601String()} (force mit --force)."); + return self::SUCCESS; + } + + // IPs ermitteln (Installer-ENV bevorzugt) + [$ipv4, $ipv6] = $this->resolvePublicIpsFromInstallerEnv(); + $ipv4 = $ipv4 ?: trim((string) env('SERVER_PUBLIC_IPV4', '')) ?: null; + $ipv6 = $ipv6 ?: trim((string) env('SERVER_PUBLIC_IPV6', '')) ?: null; + + // Kandidat für RBL (nur IPv4) + $ip = $this->validIPv4($ipv4) ? $ipv4 : null; + if (!$ip) { + $file = trim((string) @file_get_contents('/etc/mailwolt/public_ip')); + if ($this->validIPv4($file)) $ip = $file; + } + if (!$ip) { + $curl = trim((string) @shell_exec('curl -fsS --max-time 2 ifconfig.me 2>/dev/null')); + if ($this->validIPv4($curl)) $ip = $curl; + } + if (!$ip) $ip = '0.0.0.0'; + + // Abfragen (DNS) + [$lists, $meta] = $this->queryRblLists($ip); + + $payload = [ + 'ip' => $ip, + 'ipv4' => $ipv4, + 'ipv6' => $ipv6, + 'hits' => count($lists), + 'lists' => array_values($lists), // nur die tatsächlich gelisteten Zonen + 'meta' => $meta, // {zone:{status, txt?}} + 'checked_at' => $now->toIso8601String(), + 'valid_until' => $now->copy()->addDays($this->ttlDays)->toIso8601String(), + 'min_next' => $now->copy()->addDays($this->minIntervalDays)->toIso8601String(), + ]; + + // Persistieren (DB) + in Redis spiegeln + Setting::set('health.rbl', $payload); + Cache::put('health.rbl', $payload, now()->addDays($this->ttlDays)); + + $this->info(sprintf( + 'RBL: ip=%s hits=%d lists=[%s]', + $payload['ip'], $payload['hits'], implode(',', $payload['lists']) + )); + + return self::SUCCESS; + } + + /* ---------- Helpers ---------- */ + + private function resolvePublicIpsFromInstallerEnv(): array + { + $file = '/etc/mailwolt/installer.env'; + if (!is_readable($file)) return [null, null]; + + $ipv4 = $ipv6 = null; + foreach (@file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { + if ($line === '' || $line[0] === '#') continue; + if (!str_contains($line, '=')) continue; + [$k, $v] = array_map('trim', explode('=', $line, 2)); + $v = trim($v, " \t\n\r\0\x0B\"'"); + if ($k === 'SERVER_PUBLIC_IPV4' && $this->validIPv4($v)) $ipv4 = $v; + if ($k === 'SERVER_PUBLIC_IPV6' && $this->validIPv6($v)) $ipv6 = $v; + } + return [ $ipv4, $ipv6 ]; + } + + private function validIPv4(?string $ip): bool + { + return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + } + private function validIPv6(?string $ip): bool + { + return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); + } + + /** + * Gibt [listedZones, meta] zurück. + * meta[zone] = ['status'=>'listed|clean|blocked|nx', 'txt'=>?string] + */ + private function queryRblLists(string $ip): array + { + if (!$this->validIPv4($ip)) return [[], []]; + + $rev = implode('.', array_reverse(explode('.', $ip))); + + // Kuratierte, erreichbare Zonen (ohne kaputte Subdomains) + $zones = [ + // Spamhaus ZEN – rate-limitiert, liefert „blocked“ bei Open Resolver + 'zen.spamhaus.org', + // PSBL + 'psbl.surriel.com', + // UCEPROTECT Level 1 + 'dnsbl-1.uceprotect.net', + // s5h + 'bl.s5h.net', + ]; + + $listed = []; + $meta = []; + + foreach ($zones as $zone) { + $q = "{$rev}.{$zone}."; + + $txt = @dns_get_record($q, DNS_TXT) ?: []; + $a = @dns_get_record($q, DNS_A) ?: []; + + // Spamhaus „blocked“ Heuristik + $blocked = false; + if ($zone === 'zen.spamhaus.org') { + foreach ($a as $rec) { + if (!empty($rec['ip']) && in_array($rec['ip'], ['127.255.255.254','127.255.255.255'], true)) { + $blocked = true; break; + } + } + if (!$blocked) { + foreach ($txt as $rec) { + $t = implode('', $rec['txt'] ?? []); + if (stripos($t, 'open resolver') !== false) { $blocked = true; break; } + } + } + } + + if ($blocked) { + $meta[$zone] = ['status' => 'blocked', 'txt' => 'Spamhaus blockt – nutze privaten Resolver']; + continue; + } + + $hasA = !empty($a); + $hasTXT = !empty($txt); + if ($hasA || $hasTXT) { + $listed[] = $zone; + $meta[$zone] = ['status' => 'listed', 'txt' => $hasTXT ? ($txt[0]['txt'][0] ?? null) : null]; + } else { + // NXDOMAIN / sauber + $meta[$zone] = ['status' => 'clean', 'txt' => null]; + } + } + + return [$listed, $meta]; + } +} diff --git a/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php b/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php index 9a5c746..083a333 100644 --- a/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php +++ b/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php @@ -6,6 +6,7 @@ use App\Models\Domain; use App\Models\TlsaRecord; use App\Support\NetProbe; use Illuminate\Support\Facades\DB; +use Livewire\Attributes\On; use LivewireUI\Modal\ModalComponent; class DomainDnsModal extends ModalComponent @@ -23,54 +24,57 @@ class DomainDnsModal extends ModalComponent /** @var array> */ public array $optional = []; - public static function modalMaxWidth(): string - { - return '6xl'; - } + /** Farbklassen für den Status */ + public array $stateColors = [ + 'ok' => 'border-emerald-400/30 bg-emerald-500/10', + 'missing' => 'border-rose-400/30 bg-rose-500/10', + 'syntax' => 'border-amber-400/30 bg-amber-500/10', + 'neutral' => 'border-white/10 bg-white/5', + ]; + + /** erst nach Klick färben */ + public bool $checked = false; + + public static function modalMaxWidth(): string { return '6xl'; } public function mount(int $domainId): void { $this->recordColors = [ - 'A' => 'bg-cyan-500/20 text-cyan-300', - 'AAAA' => 'bg-blue-500/20 text-blue-300', - 'MX' => 'bg-emerald-500/20 text-emerald-300', - 'CNAME' => 'bg-indigo-500/20 text-indigo-300', - 'PTR' => 'bg-amber-500/20 text-amber-300', - 'TXT' => 'bg-violet-500/20 text-violet-300', - 'SRV' => 'bg-rose-500/20 text-rose-300', - 'TLSA' => 'bg-red-500/20 text-red-300', + 'A' => 'bg-cyan-500/20 text-cyan-300', + 'AAAA' => 'bg-blue-500/20 text-blue-300', + 'MX' => 'bg-emerald-500/20 text-emerald-300', + 'CNAME' => 'bg-indigo-500/20 text-indigo-300', + 'PTR' => 'bg-amber-500/20 text-amber-300', + 'TXT' => 'bg-violet-500/20 text-violet-300', + 'SRV' => 'bg-rose-500/20 text-rose-300', + 'TLSA' => 'bg-red-500/20 text-red-300', 'OPTIONAL' => 'bg-gray-500/20 text-gray-300', ]; $d = Domain::findOrFail($domainId); + $this->domainId = $domainId; $this->domainName = $d->domain; - $tlsa = TlsaRecord::forServer() - ->where('service', '_25._tcp') - ->latest('id') - ->first(); + $tlsa = TlsaRecord::forServer()->where('service', '_25._tcp')->latest('id')->first(); - $ips = NetProbe::resolve(); + $ips = NetProbe::resolve(); $ipv4 = $ips['ipv4']; $ipv6 = $ips['ipv6']; $this->zone = $this->extractZone($d->domain); $mta_sub = env('MTA_SUB'); - $base = env('BASE_DOMAIN'); - $mailServerFqdn = $mta_sub . '.' . $base; - $mta = $mta_sub . '.' . $this->zone; + $base = env('BASE_DOMAIN'); + $mailServerFqdn = "{$mta_sub}.{$base}"; - // --- Statische Infrastruktur (für alle Domains gleich) --- + // --- Infrastruktur (global) --- $this->static = [ - ['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4], - ['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $tlsa->host], + ['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4], + ['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $mailServerFqdn], ]; if ($ipv6) { - $this->static[] = ['type' => 'AAAA', 'name' => $mailServerFqdn, 'value' => $ipv6]; + $this->static[] = ['type' => 'AAAA','name' => $mailServerFqdn, 'value' => $ipv6]; $this->static[] = ['type' => 'PTR', 'name' => $this->ptrFromIPv6($ipv6), 'value' => $mailServerFqdn]; } - - if ($tlsa?->dns_string) { $this->static[] = [ 'type' => 'TLSA', @@ -80,37 +84,337 @@ class DomainDnsModal extends ModalComponent } // --- Domain-spezifisch --- - $spf = "v=spf1 ip4:{$ipv4} ip6:{$ipv6} mx -all"; + $spf = "v=spf1 ip4:{$ipv4}" . ($ipv6 ? " ip6:{$ipv6}" : '') . " mx -all"; $dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100"; - $dkim = DB::table('dkim_keys') - ->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first(); + $dkim = DB::table('dkim_keys')->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first(); $selector = $dkim ? $dkim->selector : 'mwl1'; $dkimHost = "{$selector}._domainkey.{$this->domainName}"; - $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=') - ? 'v=DKIM1; k=rsa; p=' . $dkim->public_key_txt - : ($dkim->public_key_txt ?? 'v=DKIM1; k=rsa; p='); + $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=') + ? 'v=DKIM1; k=rsa; p=' . trim($dkim->public_key_txt) + : (trim($dkim->public_key_txt ?? '') ?: 'v=DKIM1; k=rsa; p='); $this->dynamic = [ - ['type' => 'MX', 'name' => $this->domainName, 'value' => "10 {$mailServerFqdn}."], - - ['type' => 'CNAME', 'name' => "autoconfig.$this->domainName", 'value' => "$mailServerFqdn."], - ['type' => 'CNAME', 'name' => "autodiscover.$this->domainName", 'value' => "$mailServerFqdn."], - - // TXT Records - ['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf, 'helpLabel' => 'SPF Record Syntax', 'helpUrl' => 'http://www.open-spf.org/SPF_Record_Syntax/'], - ['type' => 'TXT', 'name' => "_dmarc.{$this->domainName}", 'value' => $dmarc, 'helpLabel' => 'DMARC Assistant', 'helpUrl' => 'https://www.kitterman.com/dmarc/assistant.html'], - ['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt, 'helpLabel' => 'DKIM Inspector', 'helpUrl' => 'https://dkimvalidator.com/'], - + ['type' => 'MX', 'name' => $this->domainName, 'value' => "10 {$mailServerFqdn}."], + ['type' => 'CNAME', 'name' => "autoconfig.{$this->domainName}", 'value' => "{$mailServerFqdn}."], + ['type' => 'CNAME', 'name' => "autodiscover.{$this->domainName}", 'value' => "{$mailServerFqdn}."], + ['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf], + ['type' => 'TXT', 'name' => "_dmarc.{$this->domainName}", 'value' => $dmarc], + ['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt], ]; +// $this->optional = [ +// ['type' => 'SRV', 'name' => "_autodiscover._tcp.{$this->domainName}", 'value' => "0 1 443 {$mailServerFqdn}."], +// ['type' => 'SRV', 'name' => "_imaps._tcp.{$this->domainName}", 'value' => "0 1 993 {$mailServerFqdn}."], +// ['type' => 'SRV', 'name' => "_pop3s._tcp.{$this->domainName}", 'value' => "0 1 995 {$mailServerFqdn}."], +// ['type' => 'SRV', 'name' => "_submission._tcp.{$this->domainName}", 'value' => "0 1 587 {$mailServerFqdn}."], +// ]; + $this->optional = [ - // SRV Records für Autodiscover und Maildienste - ['type' => 'SRV', 'name' => "_autodiscover._tcp.$this->domainName", 'value' => "0 0 443 {$mailServerFqdn}."], - ['type' => 'SRV', 'name' => "_imaps._tcp.$this->domainName", 'value' => "0 0 993 {$mailServerFqdn}."], - ['type' => 'SRV', 'name' => "_pop3s._tcp.$this->domainName", 'value' => "0 0 995 {$mailServerFqdn}."], - ['type' => 'SRV', 'name' => "_submission._tcp.$this->domainName", 'value' => "0 0 587 {$mailServerFqdn}."], + // --- Service-Erkennung --- + [ + 'type' => 'SRV', + 'name' => "_autodiscover._tcp.{$this->domainName}", + 'value' => "0 1 443 {$mailServerFqdn}.", + 'helpLabel' => 'Autodiscover SRV', + 'helpUrl' => 'https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover', + 'info' => 'Ermöglicht automatische Mailkonfiguration (v. a. für Outlook und Apple Mail).', + ], + [ + 'type' => 'SRV', + 'name' => "_imaps._tcp.{$this->domainName}", + 'value' => "0 1 993 {$mailServerFqdn}.", + 'helpLabel' => 'IMAPS SRV', + 'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc6186', + 'info' => 'Definiert den IMAP-Dienst (Port 993, SSL/TLS). Empfohlen für Clients.', + ], + [ + 'type' => 'SRV', + 'name' => "_pop3s._tcp.{$this->domainName}", + 'value' => "0 1 995 {$mailServerFqdn}.", + 'helpLabel' => 'POP3S SRV', + 'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc6186', + 'info' => 'Optional für POP3 (Port 995, SSL/TLS). Nur falls POP3 unterstützt wird.', + ], + [ + 'type' => 'SRV', + 'name' => "_submission._tcp.{$this->domainName}", + 'value' => "0 1 587 {$mailServerFqdn}.", + 'helpLabel' => 'Submission SRV', + 'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc6409', + 'info' => 'Definiert den SMTP-Submission-Dienst (Port 587, STARTTLS).', + ], + + // --- Sicherheit & Policies --- + [ + 'type' => 'TXT', + 'name' => "_mta-sts.{$this->domainName}", + 'value' => "v=STSv1; id=20250101T000000Z;", + 'helpLabel' => 'MTA-STS Policy', + 'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc8461', + 'info' => 'Sichert SMTP-Verbindungen durch verpflichtendes TLS (Mail Transport Security).', + ], + [ + 'type' => 'CNAME', + 'name' => "mta-sts.{$this->domainName}", + 'value' => "{$mailServerFqdn}.", + 'helpLabel' => 'MTA-STS Host', + 'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc8461', + 'info' => 'Zeigt auf den Server, der die MTA-STS-Policy unter /.well-known/mta-sts.txt bereitstellt.', + ], + [ + 'type' => 'TXT', + 'name' => "_smtp._tls.{$this->domainName}", + 'value' => "v=TLSRPTv1; rua=mailto:tlsrpt@{$this->domainName}", + 'helpLabel' => 'TLS-RPT (Reporting)', + 'helpUrl' => 'https://datatracker.ietf.org/doc/html/rfc8460', + 'info' => 'Ermöglicht Berichte zu fehlerhaften TLS-Verbindungen beim Mailversand (TLS Reporting).', + ], + + // --- Komfort / Web --- + [ + 'type' => 'CNAME', + 'name' => env('WEBMAIL_SUB') . ".{$this->domainName}", + 'value' => "{$mailServerFqdn}.", + 'helpLabel' => 'Webmail Alias', + 'helpUrl' => 'https://en.wikipedia.org/wiki/CNAME_record', + 'info' => 'Erlaubt Zugriff auf Webmail unter webmail.domain.tld.', + ], ]; + // wichtig: initial neutral einfärben (kein Check gelaufen) + $this->applyNeutralColors(); + } + + private function applyNeutralColors(): void + { + foreach (['static', 'dynamic', 'optional'] as $group) { + $this->{$group} = array_map(fn($r) => array_merge($r, [ + 'state' => 'neutral', + 'boxClass' => $this->stateColors['neutral'], + 'actual' => '', + ]), $this->{$group}); + } + } + + /** Klick im Footer löst diesen Check aus */ + #[On('domain:check-dns')] + public function checkDns(): void + { + // dynamische (Pflicht) Records prüfen + foreach ($this->dynamic as $i => $r) { + $actual = $this->dig($r['type'], $r['name']); + $state = $this->stateFor($r['type'], $r['value'], $actual, optional:false); + $this->dynamic[$i]['actual'] = $actual; + $this->dynamic[$i]['state'] = $state; + $this->dynamic[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral']; + } + + // statische (Pflicht) Records prüfen + foreach ($this->static as $i => $r) { + $actual = $this->dig($r['type'], $r['name']); + $state = $this->stateFor($r['type'], $r['value'], $actual, optional:false); + $this->static[$i]['actual'] = $actual; + $this->static[$i]['state'] = $state; + $this->static[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral']; + } + + // optionale Records: nie „missing“, nur neutral|syntax|ok + foreach ($this->optional as $i => $r) { + $actual = $this->dig($r['type'], $r['name']); + $state = $this->stateFor($r['type'], $r['value'], $actual, optional:true); + $this->optional[$i]['actual'] = $actual; + $this->optional[$i]['state'] = $state; + $this->optional[$i]['boxClass'] = $this->stateColors[$state] ?? $this->stateColors['neutral']; + } + + $this->checked = true; + } + + /** Prüft eine Record-Liste und gibt die Liste mit 'state' zurück */ + private function checkGroup(array $group, bool $optional = false): array + { + foreach ($group as &$r) { + $type = (string)$r['type']; + $name = (string)$r['name']; + $exp = (string)$r['value']; + + $act = $this->dig($type, $name); + + if ($act === '') { + $r['state'] = $optional ? 'missing' : 'missing'; + continue; + } + + $ok = $this->compareDns($type, $exp, $act); + $r['state'] = $ok ? 'ok' : 'mismatch'; + } + return $group; + } + + /* ---------- DNS & Bewertung ---------- */ + + private function dig(string $type, string $name): string + { + $type = strtoupper($type); + $name = rtrim($name, '.'); // intern ohne trailing dot + $out = @shell_exec('dig +timeout=2 +tries=1 +short ' + . escapeshellarg($name) . ' ' . escapeshellarg($type) . ' 2>/dev/null') ?? ''; + $out = trim($out); + + // Mehrzeiliges TXT zu einer Zeile squashen, Quotes weg + if ($type === 'TXT' && $out !== '') { + $lines = array_filter(array_map('trim', explode("\n", $out))); + $joined = implode('', array_map(fn($l)=>trim($l,'"'), $lines)); + return $joined; + } + + // Nur erste Zeile vergleichen + if ($out !== '') { + $out = trim(explode("\n", $out)[0]); + // MX/SRV/CNAME Ziele ohne trailing dot vergleichen + if (in_array($type, ['MX','CNAME','SRV'])) { + $out = rtrim($out, '.'); + } + } + return $out; + } + + private function stateFor(string $type, string $expected, string $actual, bool $optional): string + { + if ($actual === '') { + return $optional ? 'neutral' : 'missing'; + } + + $type = strtoupper($type); + $exp = $this->normExpected($type, $expected); + $act = $this->normActual($type, $actual); + + // Syntax plausibilisieren + $syntaxOk = $this->validateSyntax($type, $act); + if (!$syntaxOk) return 'syntax'; + + // TXT-Policies: nur „Startet mit v=…“ prüfen → OK, + // selbst wenn Inhalt nicht 1:1 dem Vorschlag entspricht. + if ($type === 'TXT') { + $upperExp = strtoupper($exp); + $upperAct = strtoupper($act); + if (str_starts_with($upperExp, 'V=SPF1')) return str_starts_with($upperAct, 'V=SPF1') ? 'ok' : 'syntax'; + if (str_starts_with($upperExp, 'V=DMARC1')) return str_starts_with($upperAct, 'V=DMARC1') ? 'ok' : 'syntax'; + if (str_starts_with($upperExp, 'V=DKIM1')) return str_starts_with($upperAct, 'V=DKIM1') ? 'ok' : 'syntax'; + return ($act !== '') ? 'ok' : ($optional ? 'neutral' : 'missing'); + } + + // MX: „prio host“ – wir prüfen Host grob + if ($type === 'MX') { + $parts = preg_split('/\s+/', $act); + $host = strtolower($parts[1] ?? $act); + $expHost = strtolower(preg_replace('/^\d+\s+/', '', $exp)); + return ($host === $expHost) ? 'ok' : 'syntax'; + } + + // SRV: „prio weight port host“ – Port + Host grob + if ($type === 'SRV') { + $ap = preg_split('/\s+/', $act); + $ep = preg_split('/\s+/', $exp); + if (count($ap) >= 4 && count($ep) >= 4) { + $aport = (int)$ap[2]; + $eport = (int)$ep[2]; + $ahost = strtolower(rtrim(end($ap), '.')); + $ehost = strtolower(rtrim(end($ep), '.')); + return ($aport === $eport && $ahost === $ehost) ? 'ok' : 'syntax'; + } + return 'syntax'; + } + + // CNAME/A/AAAA/PTR/TLSA: Gleichheit nach Normalisierung + return ($act === $exp) ? 'ok' : 'syntax'; + } + + private function normExpected(string $type, string $v): string + { + $v = trim($v); + $t = strtoupper($type); + if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.'); + if ($t === 'PTR') $v = strtolower(rtrim($v, '.')); + if ($t === 'TLSA') $v = preg_replace('/\s+/', ' ', $v); + return $v; + } + + private function normActual(string $type, string $v): string + { + $v = trim($v); + $t = strtoupper($type); + if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.'); + if ($t === 'PTR') $v = strtolower(rtrim($v, '.')); + if ($t === 'TLSA') $v = preg_replace('/\s+/', ' ', $v); + return $v; + } + + private function validateSyntax(string $type, string $val): bool + { + $t = strtoupper($type); + if ($val === '') return false; + + return match ($t) { + 'A' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), + 'AAAA' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), + 'CNAME' => (bool)preg_match('/^[a-z0-9._-]+$/i', $val), + 'PTR' => (bool)preg_match('/\.(in-addr|ip6)\.arpa$/i', $val), + 'MX' => (bool)preg_match('/^\d+\s+[a-z0-9._-]+$/i', $val), + 'SRV' => (bool)preg_match('/^\d+\s+\d+\s+\d+\s+[a-z0-9._-]+$/i', $val), + 'TLSA' => (bool)preg_match('/^[0-3]\s+[01]\s+[123]\s+[0-9a-f]{32,}$/i', $val), + 'TXT' => strlen($val) > 0, + default => true, + }; + } + + /** Vergleich je Typ robust normalisieren (nicht genutzt, bleibt aber da) */ + private function compareDns(string $type, string $expected, string $actual): bool + { + $type = strtoupper($type); + + $norm = function (string $s): string { + $s = trim($s); + $s = trim($s, "\"'"); + $s = preg_replace('/\s+/', ' ', $s); + $s = rtrim($s, '.'); + return $s; + }; + + if ($type === 'TXT') { + $parts = array_map( + fn($line) => $norm($line), + preg_split('/\R+/', $actual) + ); + $actualFlat = implode('', array_map(fn($p) => str_replace(['"',' '], '', $p), $parts)); + $expectedFlat = str_replace(['"',' '], '', $norm($expected)); + return stripos($actualFlat, $expectedFlat) !== false; + } + + if ($type === 'MX') { + $ax = array_filter(array_map('trim', preg_split('/\R+/', $actual))); + foreach ($ax as $line) { + $line = $norm($line); + if (stripos($line, $norm($expected)) !== false) return true; + } + return false; + } + + if (in_array($type, ['A','AAAA','CNAME','PTR','SRV','TLSA'], true)) { + $ax = array_filter(array_map('trim', preg_split('/\R+/', $actual))); + $exp = $norm($expected); + foreach ($ax as $line) { + if ($norm($line) === $exp) return true; + } + if (in_array($type, ['CNAME','SRV','TLSA'], true)) { + foreach ($ax as $line) { + if (stripos($norm($line), rtrim($exp, '.')) !== false) return true; + } + } + return false; + } + + return stripos($actual, $expected) !== false; } private function extractZone(string $fqdn): string @@ -118,24 +422,7 @@ class DomainDnsModal extends ModalComponent $fqdn = strtolower(trim($fqdn, ".")); $parts = explode('.', $fqdn); $n = count($parts); - return $n >= 2 ? $parts[$n - 2] . '.' . $parts[$n - 1] : $fqdn; // nimmt die letzten 2 Labels - } - - private function detectIPv4(): string - { - // robust & ohne env - $out = @shell_exec("ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if(\$i==\"src\"){print \$(i+1); exit}}'"); - $ip = trim((string)$out); - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return $ip; - $ip = trim($_SERVER['SERVER_ADDR'] ?? ''); - return $ip ?: '203.0.113.10'; // Fallback Demo - } - - private function detectIPv6(): ?string - { - $out = @shell_exec("ip -6 route get 2001:4860:4860::8888 2>/dev/null | awk '{for(i=1;i<=NF;i++) if(\$i==\"src\"){print \$(i+1); exit}}'"); - $ip = trim((string)$out); - return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? $ip : null; + return $n >= 2 ? $parts[$n - 2] . '.' . $parts[$n - 1] : $fqdn; } private function ptrFromIPv4(string $ip): string @@ -155,122 +442,700 @@ class DomainDnsModal extends ModalComponent { return view('livewire.ui.domain.modal.domain-dns-modal'); } - +} +//namespace App\Livewire\Ui\Domain\Modal; +// +//use App\Models\Domain; +//use App\Models\TlsaRecord; +//use App\Support\NetProbe; +//use Illuminate\Support\Facades\DB; +//use Livewire\Attributes\On; +//use LivewireUI\Modal\ModalComponent; +// +//class DomainDnsModal extends ModalComponent +//{ // public int $domainId; -// public Domain $domain; +// public string $domainName = ''; +// public string $zone = ''; +// public string $ttl = '3600'; +// public array $recordColors = []; // -// public array $records = []; +// /** @var array> */ +// public array $static = []; +// /** @var array> */ +// public array $dynamic = []; +// /** @var array> */ +// public array $optional = []; +// +// /** Farbklassen für den Status */ +// public array $stateColors = [ +// 'ok' => 'border-emerald-400/30 bg-emerald-500/10', +// 'missing' => 'border-rose-400/30 bg-rose-500/10', +// 'syntax' => 'border-amber-400/30 bg-amber-500/10', +// 'neutral' => 'border-white/10 bg-white/5', +// ]; +// +// /** erst nach Klick färben */ +// public bool $checked = false; +// +// public static function modalMaxWidth(): string { return '6xl'; } // // public function mount(int $domainId): void // { +// $this->recordColors = [ +// 'A' => 'bg-cyan-500/20 text-cyan-300', +// 'AAAA' => 'bg-blue-500/20 text-blue-300', +// 'MX' => 'bg-emerald-500/20 text-emerald-300', +// 'CNAME' => 'bg-indigo-500/20 text-indigo-300', +// 'PTR' => 'bg-amber-500/20 text-amber-300', +// 'TXT' => 'bg-violet-500/20 text-violet-300', +// 'SRV' => 'bg-rose-500/20 text-rose-300', +// 'TLSA' => 'bg-red-500/20 text-red-300', +// 'OPTIONAL' => 'bg-gray-500/20 text-gray-300', +// ]; +// +// $d = Domain::findOrFail($domainId); // $this->domainId = $domainId; -// $this->domain = Domain::findOrFail($domainId); +// $this->domainName = $d->domain; // -// // Placeholder-Werte, sofern du sie anderswo speicherst gern ersetzen: -// $serverIp = config('app.server_ip', 'DEINE.SERVER.IP'); -// $mxHost = config('mailwolt.mx_fqdn', 'mx.' . $this->domain->domain); -// $selector = optional( -// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first() -// )->selector ?? 'mwl1'; +// $tlsa = TlsaRecord::forServer()->where('service', '_25._tcp')->latest('id')->first(); // -// $dkimTxt = optional( -// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first() -// )->public_key_txt ?? 'DKIM_PUBLIC_KEY'; +// $ips = NetProbe::resolve(); +// $ipv4 = $ips['ipv4']; +// $ipv6 = $ips['ipv6']; // -// $this->records = [ -// [ -// 'type' => 'A', -// 'host' => $this->domain->domain, -// 'value' => $serverIp, -// 'ttl' => 3600, -// ], -// [ -// 'type' => 'MX', -// 'host' => $this->domain->domain, -// 'value' => "10 {$mxHost}.", -// 'ttl' => 3600, -// ], -// [ -// 'type' => 'TXT', -// 'host' => $this->domain->domain, -// 'value' => 'v=spf1 mx a -all', -// 'ttl' => 3600, -// ], -// [ -// 'type' => 'TXT', -// 'host' => "{$selector}._domainkey." . $this->domain->domain, -// // komplette, fertige TXT-Payload (aus deiner dkim_keys.public_key_txt Spalte) -// 'value' => "v=DKIM1; k=rsa; p={$dkimTxt}", -// 'ttl' => 3600, -// ], -// [ -// 'type' => 'TXT', -// 'host' => "_dmarc." . $this->domain->domain, -// 'value' => "v=DMARC1; p=none; rua=mailto:dmarc@" . $this->domain->domain . "; pct=100", -// 'ttl' => 3600, -// ], +// $this->zone = $this->extractZone($d->domain); +// $mta_sub = env('MTA_SUB'); +// $base = env('BASE_DOMAIN'); +// $mailServerFqdn = "{$mta_sub}.{$base}"; +// +// // --- Infrastruktur (global) --- +// $this->static = [ +// ['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4], +// ['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $mailServerFqdn], // ]; -// } -// -// public static function modalMaxWidth(): string -// { -// return '4xl'; // schön breit -// } -// -// public function render() -// { -// return view('livewire.ui.domain.modal.domain-dns-modal'); -// } - -// public Domain $domain; -// -// public static function modalMaxWidth(): string -// { -// return '3xl'; -// } -// -// public function mount(int $domainId): void -// { -// $this->domain = Domain::with(['dkimKeys' /* falls Relationen existieren */])->findOrFail($domainId); -// } -// -// public function render() -// { -// // Hier kannst du die Records vorbereiten (SPF/DMARC/DKIM) -// $records = [ -// [ -// 'type' => 'TXT', -// 'host' => $this->domain->domain, -// 'value'=> $this->domain->spf_record ?? 'v=spf1 mx a -all', -// 'ttl' => 3600, -// ], -// [ -// 'type' => 'TXT', -// 'host' => '_dmarc.' . $this->domain->domain, -// 'value'=> $this->domain->dmarc_record ?? 'v=DMARC1; p=none; rua=mailto:dmarc@' . $this->domain->domain . '; pct=100', -// 'ttl' => 3600, -// ], -// // DKIM (falls vorhanden) -// // Beispiel: nimm den aktiven Key oder den ersten -// ]; -// -// $dkim = optional($this->domain->dkimKeys()->where('is_active', true)->first() ?? $this->domain->dkimKeys()->first()); -// if ($dkim && $dkim->selector) { -// $records[] = [ -// 'type' => 'TXT', -// 'host' => "{$dkim->selector}._domainkey." . $this->domain->domain, -// 'value'=> "v=DKIM1; k=rsa; p={$dkim->public_key_txt}", -// 'ttl' => 3600, +// if ($ipv6) { +// $this->static[] = ['type' => 'AAAA','name' => $mailServerFqdn, 'value' => $ipv6]; +// $this->static[] = ['type' => 'PTR', 'name' => $this->ptrFromIPv6($ipv6), 'value' => $mailServerFqdn]; +// } +// if ($tlsa?->dns_string) { +// $this->static[] = [ +// 'type' => 'TLSA', +// 'name' => "{$tlsa->service}.{$tlsa->host}", +// 'value' => "{$tlsa->usage} {$tlsa->selector} {$tlsa->matching} {$tlsa->hash}", // ]; // } // -// return view('livewire.ui.domain.modal.domain-dns-modal', [ -// 'records' => $records, -// ]); +// // --- Domain-spezifisch --- +// $spf = "v=spf1 ip4:{$ipv4}" . ($ipv6 ? " ip6:{$ipv6}" : '') . " mx -all"; +// $dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100"; +// +// $dkim = DB::table('dkim_keys')->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first(); +// $selector = $dkim ? $dkim->selector : 'mwl1'; +// $dkimHost = "{$selector}._domainkey.{$this->domainName}"; +// $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=') +// ? 'v=DKIM1; k=rsa; p=' . trim($dkim->public_key_txt) +// : (trim($dkim->public_key_txt ?? '') ?: 'v=DKIM1; k=rsa; p='); +// +// $this->dynamic = [ +// ['type' => 'MX', 'name' => $this->domainName, 'value' => "10 {$mailServerFqdn}."], +// ['type' => 'CNAME', 'name' => "autoconfig.{$this->domainName}", 'value' => "{$mailServerFqdn}."], +// ['type' => 'CNAME', 'name' => "autodiscover.{$this->domainName}",'value' => "{$mailServerFqdn}."], +// ['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf], +// ['type' => 'TXT', 'name' => "_dmarc.{$this->domainName}", 'value' => $dmarc], +// ['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt], +// ]; +// +// $this->optional = [ +// ['type' => 'SRV', 'name' => "_autodiscover._tcp.{$this->domainName}", 'value' => "0 0 443 {$mailServerFqdn}."], +// ['type' => 'SRV', 'name' => "_imaps._tcp.{$this->domainName}", 'value' => "0 0 993 {$mailServerFqdn}."], +// ['type' => 'SRV', 'name' => "_pop3s._tcp.{$this->domainName}", 'value' => "0 0 995 {$mailServerFqdn}."], +// ['type' => 'SRV', 'name' => "_submission._tcp.{$this->domainName}", 'value' => "0 0 587 {$mailServerFqdn}."], +// ]; // } - +// +// private function applyNeutralColors(): void +// { +// foreach (['static', 'dynamic', 'optional'] as $group) { +// $this->{$group} = array_map(fn($r) => array_merge($r, [ +// 'state' => 'neutral', +// 'boxClass' => $this->stateColors['neutral'], +// 'actual' => '', +// ]), $this->{$group}); +// } +// } +// +// /** Klick im Footer löst diesen Check aus */ +// #[On('domain:check-dns')] +// public function checkDns(): void +// { +// // dynamische (pflicht) Records prüfen +// foreach ($this->dynamic as $i => $r) { +// $actual = $this->dig($r['type'], $r['name']); +// $state = $this->stateFor($r['type'], $r['value'], $actual, optional:false); +// $this->dynamic[$i]['actual'] = $actual; +// $this->dynamic[$i]['state'] = $state; +// } +// +// // statische (pflicht) Records prüfen (TLSA, PTR, A/AAAA …) +// foreach ($this->static as $i => $r) { +// $actual = $this->dig($r['type'], $r['name']); +// $state = $this->stateFor($r['type'], $r['value'], $actual, optional:false); +// $this->static[$i]['actual'] = $actual; +// $this->static[$i]['state'] = $state; +// } +// +// // optionale Records: nie „missing“, nur neutral|syntax|ok +// foreach ($this->optional as $i => $r) { +// $actual = $this->dig($r['type'], $r['name']); +// $state = $this->stateFor($r['type'], $r['value'], $actual, optional:true); +// $this->optional[$i]['actual'] = $actual; +// $this->optional[$i]['state'] = $state; +// } +// +// $this->checked = true; +// } +// +// /** Prüft eine Record-Liste und gibt die Liste mit 'state' zurück */ +// private function checkGroup(array $group, bool $optional = false): array +// { +// foreach ($group as &$r) { +// $type = (string)$r['type']; +// $name = (string)$r['name']; +// $exp = (string)$r['value']; +// +// $act = $this->dig($type, $name); +// +// if ($act === '') { +// $r['state'] = $optional ? 'missing' : 'missing'; +// continue; +// } +// +// $ok = $this->compareDns($type, $exp, $act); +// $r['state'] = $ok ? 'ok' : 'mismatch'; +// } +// return $group; +// } +// +// /** dig wrapper */ +// /* ---------- DNS & Bewertung ---------- */ +// +// private function dig(string $type, string $name): string +// { +// $type = strtoupper($type); +// $name = rtrim($name, '.'); // wir arbeiten intern ohne trailing dot +// $out = @shell_exec('dig +timeout=2 +tries=1 +short ' +// . escapeshellarg($name) . ' ' . escapeshellarg($type) . ' 2>/dev/null') ?? ''; +// $out = trim($out); +// +// // Mehrzeiliges TXT zu einer Zeile squashen, Quotes weg +// if ($type === 'TXT' && $out !== '') { +// $lines = array_filter(array_map('trim', explode("\n", $out))); +// $joined = implode('', array_map(fn($l)=>trim($l,'"'), $lines)); +// return $joined; +// } +// +// // Nur erste Zeile vergleichen (mehrere Antworten -> erste reicht) +// if ($out !== '') { +// $out = trim(explode("\n", $out)[0]); +// // MX/SRV/CNAME Ziele ohne trailing dot vergleichen +// if (in_array($type, ['MX','CNAME','SRV'])) { +// $out = rtrim($out, '.'); +// } +// } +// return $out; +// } +// +// private function stateFor(string $type, string $expected, string $actual, bool $optional): string +// { +// if ($actual === '') { +// return $optional ? 'neutral' : 'missing'; +// } +// +// $type = strtoupper($type); +// $exp = $this->normExpected($type, $expected); +// $act = $this->normActual($type, $actual); +// +// // Syntax plausibilisieren +// $syntaxOk = $this->validateSyntax($type, $act); +// if (!$syntaxOk) return 'syntax'; +// +// // Bei TXT/Policies: inhaltlich locker matchen (Beginn mit v=…) +// if ($type === 'TXT') { +// // SPF +// if (str_starts_with(strtoupper($exp), 'V=SPF1')) { +// return str_starts_with(strtoupper($act), 'V=SPF1') ? 'ok' : 'syntax'; +// } +// // DMARC +// if (str_starts_with(strtoupper($exp), 'V=DMARC1')) { +// return str_starts_with(strtoupper($act), 'V=DMARC1') ? 'ok' : 'syntax'; +// } +// // DKIM +// if (str_starts_with(strtoupper($exp), 'V=DKIM1')) { +// return str_starts_with(strtoupper($act), 'V=DKIM1') ? 'ok' : 'syntax'; +// } +// return ($act !== '') ? 'ok' : ($optional ? 'neutral' : 'missing'); +// } +// +// // MX: wir erwarten „prio host“ – wir prüfen nur, ob host passt +// if ($type === 'MX') { +// // $act z.B. "10 mx.example.tld" +// $parts = preg_split('/\s+/', $act); +// $host = strtolower($parts[1] ?? $act); +// $expHost = strtolower(preg_replace('/^\d+\s+/', '', $exp)); +// return ($host === $expHost) ? 'ok' : 'syntax'; +// } +// +// // SRV: „prio weight port host“ – prüfen Port + Host grob +// if ($type === 'SRV') { +// $ap = preg_split('/\s+/', $act); +// $ep = preg_split('/\s+/', $exp); +// if (count($ap) >= 4 && count($ep) >= 4) { +// $aport = (int)$ap[2]; +// $eport = (int)$ep[2]; +// $ahost = strtolower(rtrim(end($ap), '.')); +// $ehost = strtolower(rtrim(end($ep), '.')); +// return ($aport === $eport && $ahost === $ehost) ? 'ok' : 'syntax'; +// } +// return 'syntax'; +// } +// +// // CNAME/A/AAAA/PTR/TLSA: einfacher Gleichheitsvergleich (normalisiert) +// return ($act === $exp) ? 'ok' : 'syntax'; +// } +// +// private function normExpected(string $type, string $v): string +// { +// $v = trim($v); +// $t = strtoupper($type); +// if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.'); +// if ($t === 'PTR') $v = strtolower(rtrim($v, '.')); +// if ($t === 'TLSA') $v = preg_replace('/\s+/', ' ', $v); +// return $v; +// } +// +// private function normActual(string $type, string $v): string +// { +// $v = trim($v); +// $t = strtoupper($type); +// if (in_array($t, ['MX','CNAME','SRV'])) $v = rtrim($v, '.'); +// if ($t === 'PTR') $v = strtolower(rtrim($v, '.')); +// if ($t === 'TLSA') $v = preg_replace('/\s+/', ' ', $v); +// return $v; +// } +// +// private function validateSyntax(string $type, string $val): bool +// { +// $t = strtoupper($type); +// if ($val === '') return false; +// +// return match ($t) { +// 'A' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4), +// 'AAAA' => (bool)filter_var($val, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6), +// 'CNAME' => (bool)preg_match('/^[a-z0-9._-]+$/i', $val), +// 'PTR' => (bool)preg_match('/\.(in-addr|ip6)\.arpa$/i', $val), +// 'MX' => (bool)preg_match('/^\d+\s+[a-z0-9._-]+$/i', $val), +// 'SRV' => (bool)preg_match('/^\d+\s+\d+\s+\d+\s+[a-z0-9._-]+$/i', $val), +// 'TLSA' => (bool)preg_match('/^[0-3]\s+[01]\s+[123]\s+[0-9a-f]{32,}$/i', $val), +// 'TXT' => strlen($val) > 0, // SPF/DMARC/DKIM oben extra geprüft +// default => true, +// }; +// } +// +// /** Vergleich je Typ robust normalisieren */ +// private function compareDns(string $type, string $expected, string $actual): bool +// { +// $type = strtoupper($type); +// +// // Normalisierer +// $norm = function (string $s): string { +// $s = trim($s); +// $s = trim($s, "\"'"); // TXT quotes +// $s = preg_replace('/\s+/', ' ', $s); // unify spaces +// $s = rtrim($s, '.'); // trailing dot +// return $s; +// }; +// +// // TXT kann aus mehreren quoted Strings bestehen (mehrere Zeilen) +// if ($type === 'TXT') { +// // dig kann mehrere Zeilen liefern, jede in Quotes – zusammenfügen +// $parts = array_map( +// fn($line) => $norm($line), +// preg_split('/\R+/', $actual) +// ); +// $actualFlat = implode('', array_map(fn($p) => str_replace(['"',' '], '', $p), $parts)); +// $expectedFlat = str_replace(['"',' '], '', $norm($expected)); +// return stripos($actualFlat, $expectedFlat) !== false; +// } +// +// if ($type === 'MX') { +// // Erwartet i.d.R. "10 host." – wir normalisieren beide Seiten +// $ax = array_filter(array_map('trim', preg_split('/\R+/', $actual))); +// foreach ($ax as $line) { +// $line = $norm($line); +// if (stripos($line, $norm($expected)) !== false) return true; +// } +// return false; +// } +// +// if (in_array($type, ['A','AAAA','CNAME','PTR','SRV','TLSA'], true)) { +// $ax = array_filter(array_map('trim', preg_split('/\R+/', $actual))); +// $exp = $norm($expected); +// foreach ($ax as $line) { +// if ($norm($line) === $exp) return true; +// } +// // bei CNAME/SRV/TLSA reicht oft „enthält denselben Zielhost“ +// if (in_array($type, ['CNAME','SRV','TLSA'], true)) { +// foreach ($ax as $line) { +// if (stripos($norm($line), rtrim($exp, '.')) !== false) return true; +// } +// } +// return false; +// } +// +// // Fallback: substring +// return stripos($actual, $expected) !== false; +// } +// +// private function extractZone(string $fqdn): string +// { +// $fqdn = strtolower(trim($fqdn, ".")); +// $parts = explode('.', $fqdn); +// $n = count($parts); +// return $n >= 2 ? $parts[$n - 2] . '.' . $parts[$n - 1] : $fqdn; +// } +// +// private function ptrFromIPv4(string $ip): string +// { +// $p = array_reverse(explode('.', $ip)); +// return implode('.', $p) . '.in-addr.arpa'; +// } +// +// private function ptrFromIPv6(string $ip): string +// { +// $expanded = strtolower(inet_ntop(inet_pton($ip))); +// $hex = str_replace(':', '', $expanded); +// return implode('.', array_reverse(str_split($hex))) . '.ip6.arpa'; +// } +// // public function render() // { // return view('livewire.ui.domain.modal.domain-dns-modal'); // } -} +//} + +//namespace App\Livewire\Ui\Domain\Modal; +// +//use App\Models\Domain; +//use App\Models\TlsaRecord; +//use App\Support\NetProbe; +//use Illuminate\Support\Facades\DB; +//use Livewire\Attributes\On; +//use LivewireUI\Modal\ModalComponent; +// +//class DomainDnsModal extends ModalComponent +//{ +// public int $domainId; +// public string $domainName = ''; +// public string $zone = ''; +// public string $ttl = '3600'; +// public array $recordColors = []; +// +// /** @var array> */ +// public array $static = []; +// /** @var array> */ +// public array $dynamic = []; +// /** @var array> */ +// public array $optional = []; +// +// public array $stateColors = [ +// 'ok' => 'text-emerald-300 bg-emerald-500/10 border-emerald-400/30', +// 'missing' => 'text-rose-300 bg-rose-500/10 border-rose-400/30', +// 'mismatch' => 'text-amber-200 bg-amber-500/10 border-amber-400/30', +// ]; +// +// public static function modalMaxWidth(): string +// { +// return '6xl'; +// } +// +// public function mount(int $domainId): void +// { +// $this->recordColors = [ +// 'A' => 'bg-cyan-500/20 text-cyan-300', +// 'AAAA' => 'bg-blue-500/20 text-blue-300', +// 'MX' => 'bg-emerald-500/20 text-emerald-300', +// 'CNAME' => 'bg-indigo-500/20 text-indigo-300', +// 'PTR' => 'bg-amber-500/20 text-amber-300', +// 'TXT' => 'bg-violet-500/20 text-violet-300', +// 'SRV' => 'bg-rose-500/20 text-rose-300', +// 'TLSA' => 'bg-red-500/20 text-red-300', +// 'OPTIONAL' => 'bg-gray-500/20 text-gray-300', +// ]; +// +// $d = Domain::findOrFail($domainId); +// $this->domainName = $d->domain; +// +// $tlsa = TlsaRecord::forServer() +// ->where('service', '_25._tcp') +// ->latest('id') +// ->first(); +// +// $ips = NetProbe::resolve(); +// $ipv4 = $ips['ipv4']; +// $ipv6 = $ips['ipv6']; +// +// $this->zone = $this->extractZone($d->domain); +// $mta_sub = env('MTA_SUB'); +// $base = env('BASE_DOMAIN'); +// $mailServerFqdn = $mta_sub . '.' . $base; +// $mta = $mta_sub . '.' . $this->zone; +// +// // --- Statische Infrastruktur (für alle Domains gleich) --- +// $this->static = [ +// ['type' => 'A', 'name' => $mailServerFqdn, 'value' => $ipv4], +// ['type' => 'PTR', 'name' => $this->ptrFromIPv4($ipv4), 'value' => $tlsa->host], +// ]; +// if ($ipv6) { +// $this->static[] = ['type' => 'AAAA', 'name' => $mailServerFqdn, 'value' => $ipv6]; +// $this->static[] = ['type' => 'PTR', 'name' => $this->ptrFromIPv6($ipv6), 'value' => $mailServerFqdn]; +// } +// +// +// if ($tlsa?->dns_string) { +// $this->static[] = [ +// 'type' => 'TLSA', +// 'name' => "{$tlsa->service}.{$tlsa->host}", +// 'value' => "{$tlsa->usage} {$tlsa->selector} {$tlsa->matching} {$tlsa->hash}", +// ]; +// } +// +// // --- Domain-spezifisch --- +// $spf = "v=spf1 ip4:{$ipv4} ip6:{$ipv6} mx -all"; +// $dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100"; +// +// $dkim = DB::table('dkim_keys') +// ->where('domain_id', $d->id)->where('is_active', 1)->orderByDesc('id')->first(); +// $selector = $dkim ? $dkim->selector : 'mwl1'; +// $dkimHost = "{$selector}._domainkey.{$this->domainName}"; +// $dkimTxt = $dkim && !str_starts_with(trim($dkim->public_key_txt), 'v=') +// ? 'v=DKIM1; k=rsa; p=' . $dkim->public_key_txt +// : ($dkim->public_key_txt ?? 'v=DKIM1; k=rsa; p='); +// +// $this->dynamic = [ +// ['type' => 'MX', 'name' => $this->domainName, 'value' => "10 {$mailServerFqdn}."], +// +// ['type' => 'CNAME', 'name' => "autoconfig.$this->domainName", 'value' => "$mailServerFqdn."], +// ['type' => 'CNAME', 'name' => "autodiscover.$this->domainName", 'value' => "$mailServerFqdn."], +// +// // TXT Records +// ['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf, 'helpLabel' => 'SPF Record Syntax', 'helpUrl' => 'http://www.open-spf.org/SPF_Record_Syntax/'], +// ['type' => 'TXT', 'name' => "_dmarc.{$this->domainName}", 'value' => $dmarc, 'helpLabel' => 'DMARC Assistant', 'helpUrl' => 'https://www.kitterman.com/dmarc/assistant.html'], +// ['type' => 'TXT', 'name' => $dkimHost, 'value' => $dkimTxt, 'helpLabel' => 'DKIM Inspector', 'helpUrl' => 'https://dkimvalidator.com/'], +// +// ]; +// +// $this->optional = [ +// // SRV Records für Autodiscover und Maildienste +// ['type' => 'SRV', 'name' => "_autodiscover._tcp.$this->domainName", 'value' => "0 0 443 {$mailServerFqdn}."], +// ['type' => 'SRV', 'name' => "_imaps._tcp.$this->domainName", 'value' => "0 0 993 {$mailServerFqdn}."], +// ['type' => 'SRV', 'name' => "_pop3s._tcp.$this->domainName", 'value' => "0 0 995 {$mailServerFqdn}."], +// ['type' => 'SRV', 'name' => "_submission._tcp.$this->domainName", 'value' => "0 0 587 {$mailServerFqdn}."], +// ]; +// +// } +// +// #[On('domain:check-dns')] +// public function checkDns(): void +// { +// $this->validateDnsRecords(); +// } +// +// +// private function validateDnsRecords(): void +// { +// // alle Listen zusammenführen +// $records = array_merge($this->static, $this->dynamic); +// +// foreach ($records as &$record) { +// $type = $record['type']; +// $name = $record['name']; +// $expected = trim((string) $record['value']); +// +// $result = @shell_exec("dig +short {$type} {$name} 2>/dev/null"); +// $result = trim((string) $result); +// +// if ($result === '') { +// $record['state'] = 'missing'; +// } elseif (stripos($result, trim($expected, '.')) !== false) { +// $record['state'] = 'ok'; +// } else { +// $record['state'] = 'mismatch'; +// } +// } +// +// $this->static = array_values(array_filter($records, fn($r) => in_array($r['type'], ['A','AAAA','PTR','TLSA']))); +// $this->dynamic = array_values(array_filter($records, fn($r) => !in_array($r['type'], ['A','AAAA','PTR','TLSA']))); +// } +// +// private function extractZone(string $fqdn): string +// { +// $fqdn = strtolower(trim($fqdn, ".")); +// $parts = explode('.', $fqdn); +// $n = count($parts); +// return $n >= 2 ? $parts[$n - 2] . '.' . $parts[$n - 1] : $fqdn; // nimmt die letzten 2 Labels +// } +// +// private function detectIPv4(): string +// { +// // robust & ohne env +// $out = @shell_exec("ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if(\$i==\"src\"){print \$(i+1); exit}}'"); +// $ip = trim((string)$out); +// if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return $ip; +// $ip = trim($_SERVER['SERVER_ADDR'] ?? ''); +// return $ip ?: '203.0.113.10'; // Fallback Demo +// } +// +// private function detectIPv6(): ?string +// { +// $out = @shell_exec("ip -6 route get 2001:4860:4860::8888 2>/dev/null | awk '{for(i=1;i<=NF;i++) if(\$i==\"src\"){print \$(i+1); exit}}'"); +// $ip = trim((string)$out); +// return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? $ip : null; +// } +// +// private function ptrFromIPv4(string $ip): string +// { +// $p = array_reverse(explode('.', $ip)); +// return implode('.', $p) . '.in-addr.arpa'; +// } +// +// private function ptrFromIPv6(string $ip): string +// { +// $expanded = strtolower(inet_ntop(inet_pton($ip))); +// $hex = str_replace(':', '', $expanded); +// return implode('.', array_reverse(str_split($hex))) . '.ip6.arpa'; +// } +// +// public function render() +// { +// return view('livewire.ui.domain.modal.domain-dns-modal'); +// } +// +//// public int $domainId; +//// public Domain $domain; +//// +//// public array $records = []; +//// +//// public function mount(int $domainId): void +//// { +//// $this->domainId = $domainId; +//// $this->domain = Domain::findOrFail($domainId); +//// +//// // Placeholder-Werte, sofern du sie anderswo speicherst gern ersetzen: +//// $serverIp = config('app.server_ip', 'DEINE.SERVER.IP'); +//// $mxHost = config('mailwolt.mx_fqdn', 'mx.' . $this->domain->domain); +//// $selector = optional( +//// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first() +//// )->selector ?? 'mwl1'; +//// +//// $dkimTxt = optional( +//// DkimKey::where('domain_id', $this->domain->id)->where('is_active', true)->first() +//// )->public_key_txt ?? 'DKIM_PUBLIC_KEY'; +//// +//// $this->records = [ +//// [ +//// 'type' => 'A', +//// 'host' => $this->domain->domain, +//// 'value' => $serverIp, +//// 'ttl' => 3600, +//// ], +//// [ +//// 'type' => 'MX', +//// 'host' => $this->domain->domain, +//// 'value' => "10 {$mxHost}.", +//// 'ttl' => 3600, +//// ], +//// [ +//// 'type' => 'TXT', +//// 'host' => $this->domain->domain, +//// 'value' => 'v=spf1 mx a -all', +//// 'ttl' => 3600, +//// ], +//// [ +//// 'type' => 'TXT', +//// 'host' => "{$selector}._domainkey." . $this->domain->domain, +//// // komplette, fertige TXT-Payload (aus deiner dkim_keys.public_key_txt Spalte) +//// 'value' => "v=DKIM1; k=rsa; p={$dkimTxt}", +//// 'ttl' => 3600, +//// ], +//// [ +//// 'type' => 'TXT', +//// 'host' => "_dmarc." . $this->domain->domain, +//// 'value' => "v=DMARC1; p=none; rua=mailto:dmarc@" . $this->domain->domain . "; pct=100", +//// 'ttl' => 3600, +//// ], +//// ]; +//// } +//// +//// public static function modalMaxWidth(): string +//// { +//// return '4xl'; // schön breit +//// } +//// +//// public function render() +//// { +//// return view('livewire.ui.domain.modal.domain-dns-modal'); +//// } +// +//// public Domain $domain; +//// +//// public static function modalMaxWidth(): string +//// { +//// return '3xl'; +//// } +//// +//// public function mount(int $domainId): void +//// { +//// $this->domain = Domain::with(['dkimKeys' /* falls Relationen existieren */])->findOrFail($domainId); +//// } +//// +//// public function render() +//// { +//// // Hier kannst du die Records vorbereiten (SPF/DMARC/DKIM) +//// $records = [ +//// [ +//// 'type' => 'TXT', +//// 'host' => $this->domain->domain, +//// 'value'=> $this->domain->spf_record ?? 'v=spf1 mx a -all', +//// 'ttl' => 3600, +//// ], +//// [ +//// 'type' => 'TXT', +//// 'host' => '_dmarc.' . $this->domain->domain, +//// 'value'=> $this->domain->dmarc_record ?? 'v=DMARC1; p=none; rua=mailto:dmarc@' . $this->domain->domain . '; pct=100', +//// 'ttl' => 3600, +//// ], +//// // DKIM (falls vorhanden) +//// // Beispiel: nimm den aktiven Key oder den ersten +//// ]; +//// +//// $dkim = optional($this->domain->dkimKeys()->where('is_active', true)->first() ?? $this->domain->dkimKeys()->first()); +//// if ($dkim && $dkim->selector) { +//// $records[] = [ +//// 'type' => 'TXT', +//// 'host' => "{$dkim->selector}._domainkey." . $this->domain->domain, +//// 'value'=> "v=DKIM1; k=rsa; p={$dkim->public_key_txt}", +//// 'ttl' => 3600, +//// ]; +//// } +//// +//// return view('livewire.ui.domain.modal.domain-dns-modal', [ +//// 'records' => $records, +//// ]); +//// } +// +//// public function render() +//// { +//// return view('livewire.ui.domain.modal.domain-dns-modal'); +//// } +//} diff --git a/app/Livewire/Ui/Mail/DnsHealthCard.php b/app/Livewire/Ui/Mail/DnsHealthCard.php index 3d31ec7..f284583 100644 --- a/app/Livewire/Ui/Mail/DnsHealthCard.php +++ b/app/Livewire/Ui/Mail/DnsHealthCard.php @@ -2,42 +2,339 @@ namespace App\Livewire\Ui\Mail; +use Livewire\Attributes\On; use Livewire\Component; use App\Models\Domain; use Illuminate\Support\Facades\Cache; class DnsHealthCard extends Component { - public array $rows = []; // [ ['domain'=>..., 'dkim'=>bool, 'dmarc'=>bool, 'tlsa'=>bool], ... ] + public array $rows = []; // [{id,name,ok,missing:[...]}] + public string $mtaHost = ''; // z.B. mx.nexlab.at + public bool $tlsa = false; // hostweit - public function mount(): void { $this->load(); } - public function render() { return view('livewire.ui.mail.dns-health-card'); } - public function refresh(): void { $this->load(true); } - - protected function load(bool $force=false): void + public function mount(): void { - $this->rows = Cache::remember('dash.dnshealth', $force ? 1 : 600, function () { + $this->load(); + } + + public function render() + { + return view('livewire.ui.mail.dns-health-card'); + } + + public function refresh(): void + { + $this->load(true); + } + + public function openDnsModal(int $domainId): void + { + $this->dispatch('openModal', component: 'ui.domain.modal.domain-dns-modal', arguments: ['domainId' => $domainId]); + } + + protected function load(bool $force = false): void + { + [$this->mtaHost, $this->tlsa, $this->rows] = Cache::remember('dash.dnshealth.v1', $force ? 1 : 600, function () { + + $base = trim((string)env('BASE_DOMAIN', '')); + $mtaSub = trim((string)env('MTA_SUB', 'mx')); + $mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub; + $rows = []; - $domains = Domain::query()->where('is_system', false)->where('is_active', true)->get(['domain']); + $domains = Domain::query() + ->where('is_system', false) + ->where('is_active', true) + ->orderBy('domain') + ->get(['id', 'domain']); + foreach ($domains as $d) { $dom = $d->domain; - $dkim = $this->hasTxt("_domainkey.$dom"); // rough: just any dkim TXT exists - $dmarc = $this->hasTxt("_dmarc.$dom"); - $tlsa = $this->hasTlsa("_25._tcp.$dom") || $this->hasTlsa("_465._tcp.$dom") || $this->hasTlsa("_587._tcp.$dom"); - $rows[] = compact('dom','dkim','dmarc','tlsa'); + + $missing = []; + + // Pflicht-Checks + if (!$this->mxPointsTo($dom, [$mtaHost])) $missing[] = 'MX'; + if (!$this->hasSpf($dom)) $missing[] = 'SPF'; + if (!$this->hasDkim($dom)) $missing[] = 'DKIM'; + if (!$this->hasTxt("_dmarc.$dom")) $missing[] = 'DMARC'; + + $rows[] = [ + 'id' => (int)$d->id, + 'name' => $dom, + 'ok' => empty($missing), + 'missing' => $missing, + ]; } - return $rows; + + // TLSA (hostweit, nur Info) + $tlsa = $this->hasTlsa("_25._tcp.$mtaHost") || $this->hasTlsa("_465._tcp.$mtaHost") || $this->hasTlsa("_587._tcp.$mtaHost"); + + return [$mtaHost, $tlsa, $rows]; }); } + /* ── DNS Helpers (mit Timeout, damit UI nicht hängt) ───────────────── */ + + protected function digShort(string $type, string $name): string + { + $cmd = "timeout 2 dig +short " . escapeshellarg($name) . " " . escapeshellarg(strtoupper($type)) . " 2>/dev/null"; + return (string)@shell_exec($cmd) ?: ''; + } + protected function hasTxt(string $name): bool { - $out = @shell_exec("dig +short TXT ".escapeshellarg($name)." 2>/dev/null"); - return is_string($out) && trim($out) !== ''; + return trim($this->digShort('TXT', $name)) !== ''; } + protected function hasTlsa(string $name): bool { - $out = @shell_exec("dig +short TLSA ".escapeshellarg($name)." 2>/dev/null"); - return is_string($out) && trim($out) !== ''; + return trim($this->digShort('TLSA', $name)) !== ''; + } + + protected function hasSpf(string $domain): bool + { + $out = $this->digShort('TXT', $domain); + foreach (preg_split('/\R+/', trim($out)) as $line) { + if (stripos($line, 'v=spf1') !== false) return true; + } + return false; + } + + // DKIM: wenn spezifischer Selector vorhanden → prüfe den, sonst akzeptiere _domainkey-Policy als “vorhanden” + protected function hasDkim(string $domain): bool + { + $sel = trim((string)env('DKIM_SELECTOR', '')); + if ($sel !== '' && $this->hasTxt("{$sel}._domainkey.$domain")) return true; + return $this->hasTxt("_domainkey.$domain"); + } + + protected function mxPointsTo(string $domain, array $allowedHosts): bool + { + $out = $this->digShort('MX', $domain); + if ($out === '') return false; + + $targets = []; + foreach (preg_split('/\R+/', trim($out)) as $line) { + // Format: "10 mx.example.com." + if (preg_match('~\s+([A-Za-z0-9\.\-]+)\.?$~', trim($line), $m)) { + $targets[] = strtolower($m[1]); + } + } + if (!$targets) return false; + + $allowed = array_map('strtolower', $allowedHosts); + foreach ($targets as $t) { + if (in_array($t, $allowed, true)) return true; + } + return false; } } + +//namespace App\Livewire\Ui\Mail; +// +//use Livewire\Component; +//use App\Models\Domain; +//use Illuminate\Support\Facades\Cache; +// +//class DnsHealthCard extends Component +//{ +// public array $domains = []; // [['name'=>..., 'dkim'=>bool, 'dmarc'=>bool], ...] +// public string $host = ''; // z.B. mx.nexlab.at +// public bool $tlsa = false; // hostbasiert (einmalig) +// public ?string $ipv4 = null; +// public ?string $ipv6 = null; +// +// public function mount(): void +// { +// $this->load(); +// } +// +// public function render() +// { +// return view('livewire.ui.mail.dns-health-card'); +// } +// +// public function refresh(): void +// { +// $this->load(true); +// } +// +// protected function load(bool $force = false): void +// { +// [$this->host, $this->tlsa, $this->domains, $this->ipv4, $this->ipv6] = +// Cache::remember('dash.dnshealth', $force ? 1 : 900, function () { +// +// // ── ENV lesen ──────────────────────────────────────────────── +// $base = trim((string)env('BASE_DOMAIN', '')); +// $mtaSub = trim((string)env('MTA_SUB', 'mx')); +// $host = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub; +// +// $ipv4 = trim((string)env('SERVER_PUBLIC_IPV4', '')) ?: null; +// $ipv6 = trim((string)env('SERVER_PUBLIC_IPV6', '')) ?: null; +// +// // ── Domains laden (nur aktive, nicht-system) ──────────────── +// $rows = []; +// $domains = Domain::query() +// ->where('is_system', false) +// ->where('is_active', true) +// ->orderBy('domain') +// ->get(['domain']); +// +// foreach ($domains as $d) { +// $dom = $d->domain; +// $rows[] = [ +// 'name' => $dom, +// 'dkim' => $this->hasTxt("_domainkey.$dom"), +// 'dmarc' => $this->hasTxt("_dmarc.$dom"), +// ]; +// } +// +// // ── TLSA nur hostbasiert prüfen (25/465/587) ──────────────── +// $tlsa = $this->hasTlsa("_25._tcp.$host") +// || $this->hasTlsa("_465._tcp.$host") +// || $this->hasTlsa("_587._tcp.$host"); +// +// return [$host, $tlsa, $rows, $ipv4, $ipv6]; +// }); +// } +// +// /* ───────────────────────── DNS Helpers ───────────────────────── */ +// +// protected function hasTxt(string $name): bool +// { +// $out = @shell_exec("timeout 2 dig +short TXT " . escapeshellarg($name) . " 2>/dev/null"); +// return is_string($out) && trim($out) !== ''; +// } +// +// protected function hasTlsa(string $name): bool +// { +// $out = @shell_exec("timeout 2 dig +short TLSA " . escapeshellarg($name) . " 2>/dev/null"); +// return is_string($out) && trim($out) !== ''; +// } +//} + +//namespace App\Livewire\Ui\Mail; +// +//use Livewire\Component; +//use App\Models\Domain; +//use Illuminate\Support\Facades\Cache; +// +//class DnsHealthCard extends Component +//{ +// public array $rows = []; // pro Domain: ['dom','dkim','dmarc'] +// public string $host = ''; // z.B. mx.nexlab.at +// public bool $tlsa = false; // TLSA-Status für den Host +// public ?string $ipv4 = null; +// public ?string $ipv6 = null; +// +// public function mount(): void { $this->load(); } +// public function render() { return view('livewire.ui.mail.dns-health-card'); } +// public function refresh(): void { $this->load(true); } +// +// protected function load(bool $force = false): void +// { +// // Werte aus .env +// $this->ipv4 = trim((string) env('SERVER_PUBLIC_IPV4', '')) ?: null; +// $this->ipv6 = trim((string) env('SERVER_PUBLIC_IPV6', '')) ?: null; +// +// $base = trim((string) env('BASE_DOMAIN', '')); +// $mta = trim((string) env('MTA_SUB', 'mx')); +// $host = $base ? "{$mta}.{$base}" : $mta; // z.B. mx.nexlab.at +// $this->host = $host; +// +// // Neuer Cache-Key, damit altes Format keinen Crash verursacht +// [$calcHost, $calcTlsa, $calcRows] = Cache::remember( +// 'dash.dnshealth.v2', +// $force ? 1 : 900, +// function () use ($host) { +// // TLSA: nur 1× pro Host prüfen +// $tlsa = $this->hasTlsa("_25._tcp.{$host}") +// || $this->hasTlsa("_465._tcp.{$host}") +// || $this->hasTlsa("_587._tcp.{$host}"); +// +// // Domains: nur DKIM/DMARC +// $rows = []; +// $domains = Domain::query() +// ->where('is_system', false) +// ->where('is_active', true) +// ->get(['domain']); +// +// foreach ($domains as $d) { +// $dom = $d->domain; +// $dkim = $this->hasTxt("_domainkey.{$dom}"); +// $dmarc = $this->hasTxt("_dmarc.{$dom}"); +// $rows[] = compact('dom','dkim','dmarc'); +// } +// +// return [$host, $tlsa, $rows]; +// } +// ); +// +// // Defensive: falls mal falsches Datenformat im Cache war +// if (!is_string($calcHost) || !is_bool($calcTlsa) || !is_array($calcRows)) { +// Cache::forget('dash.dnshealth.v2'); +// $this->load(true); +// return; +// } +// +// $this->host = $calcHost; +// $this->tlsa = $calcTlsa; +// $this->rows = $calcRows; +// } +// +// protected function hasTxt(string $name): bool +// { +// $out = @shell_exec("dig +short TXT " . escapeshellarg($name) . " 2>/dev/null"); +// return is_string($out) && trim($out) !== ''; +// } +// +// protected function hasTlsa(string $name): bool +// { +// $out = @shell_exec("dig +short TLSA " . escapeshellarg($name) . " 2>/dev/null"); +// return is_string($out) && trim($out) !== ''; +// } +//} + +// +//namespace App\Livewire\Ui\Mail; +// +//use Livewire\Component; +//use App\Models\Domain; +//use Illuminate\Support\Facades\Cache; +// +//class DnsHealthCard extends Component +//{ +// public array $rows = []; // [ ['domain'=>..., 'dkim'=>bool, 'dmarc'=>bool, 'tlsa'=>bool], ... ] +// +// public function mount(): void { $this->load(); } +// public function render() { return view('livewire.ui.mail.dns-health-card'); } +// public function refresh(): void { $this->load(true); } +// +// protected function load(bool $force=false): void +// { +// $this->rows = Cache::remember('dash.dnshealth', $force ? 1 : 600, function () { +// $rows = []; +// $domains = Domain::query()->where('is_system', false)->where('is_active', true)->get(['domain']); +// foreach ($domains as $d) { +// $dom = $d->domain; +// $dkim = $this->hasTxt("_domainkey.$dom"); // rough: just any dkim TXT exists +// $dmarc = $this->hasTxt("_dmarc.$dom"); +// $tlsa = $this->hasTlsa("_25._tcp.$dom") || $this->hasTlsa("_465._tcp.$dom") || $this->hasTlsa("_587._tcp.$dom"); +// $rows[] = compact('dom','dkim','dmarc','tlsa'); +// } +// return $rows; +// }); +// } +// +// protected function hasTxt(string $name): bool +// { +// $out = @shell_exec("dig +short TXT ".escapeshellarg($name)." 2>/dev/null"); +// return is_string($out) && trim($out) !== ''; +// } +// protected function hasTlsa(string $name): bool +// { +// $out = @shell_exec("dig +short TLSA ".escapeshellarg($name)." 2>/dev/null"); +// return is_string($out) && trim($out) !== ''; +// } +//} diff --git a/app/Livewire/Ui/Security/RblCard.php b/app/Livewire/Ui/Security/RblCard.php index ca79921..a8ae649 100644 --- a/app/Livewire/Ui/Security/RblCard.php +++ b/app/Livewire/Ui/Security/RblCard.php @@ -1,142 +1,192 @@ load(); - } - - public function render() - { - return view('livewire.ui.security.rbl-card'); - } + public function mount(): void { $this->load(); } + public function render() { return view('livewire.ui.security.rbl-card'); } public function refresh(): void { - Cache::forget('dash.rbl'); + // Manuelles Re-Check via Command (asynchron, damit UI nicht blockiert) + @shell_exec('nohup php /var/www/mailwolt/artisan rbl:probe --force >/dev/null 2>&1 &'); + // Sofortige UI-Aktualisierung aus Settings (altes Ergebnis) … $this->load(true); + // … und kurzer Hinweis + $this->dispatch('toast', type:'info', title:'RBL-Prüfung gestartet', text:'Ergebnis wird aktualisiert, sobald verfügbar.', duration:2500); } protected function load(bool $force = false): void { - [$ip4, $ip6] = $this->resolvePublicIpsFromInstallerEnv(); + $payload = $force + ? (array) Setting::get('health.rbl', []) // direkt aus DB + : (array) (Cache::get('health.rbl') ?: Setting::get('health.rbl', [])); - $this->ipv4 = $ip4 ?: trim((string)env('SERVER_PUBLIC_IPV4', '')) ?: '–'; - $this->ipv6 = $ip6 ?: trim((string)env('SERVER_PUBLIC_IPV6', '')) ?: '–'; - - $data = Cache::remember('dash.rbl', $force ? 1 : 6 * 3600, function () { - $candidate = $this->validIPv4($this->ipv4 ?? '') ? $this->ipv4 : null; - - if (!$candidate) { - $fromFile = trim((string)@file_get_contents('/etc/mailwolt/public_ip')); - if ($this->validIPv4($fromFile)) $candidate = $fromFile; - } - if (!$candidate) { - $curl = trim((string)@shell_exec("curl -fsS --max-time 2 ifconfig.me 2>/dev/null")); - if ($this->validIPv4($curl)) $candidate = $curl; - } - - $ip = $candidate ?: '0.0.0.0'; - $lists = $this->queryRblLists($ip); - - return ['ip' => $ip, 'hits' => count($lists), 'lists' => $lists]; - }); - - foreach ($data as $k => $v) $this->$k = $v; - } - - /** bevorzugt Installer-ENV */ - private function resolvePublicIpsFromInstallerEnv(): array - { - $file = '/etc/mailwolt/installer.env'; - if (!is_readable($file)) return [null, null]; - - $ipv4 = $ipv6 = null; - $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; - foreach ($lines as $line) { - if (preg_match('/^\s*#/', $line) || !str_contains($line, '=')) continue; - [$k, $v] = array_map('trim', explode('=', $line, 2)); - $v = trim($v, " \t\n\r\0\x0B\"'"); - if ($k === 'SERVER_PUBLIC_IPV4' && $this->validIPv4($v)) $ipv4 = $v; - if ($k === 'SERVER_PUBLIC_IPV6' && $this->validIPv6($v)) $ipv6 = $v; - } - return [$ipv4, $ipv6]; - } - - private function validIPv4(?string $ip): bool - { - return (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); - } - - private function validIPv6(?string $ip): bool - { - return (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); - } - - /** - * Prüft die IP gegen gängige **öffentliche** RBLs. - * @return array gelistete RBL-Zonen - */ - private function queryRblLists(string $ip): array - { - if (!$this->validIPv4($ip)) return []; - - $rev = implode('.', array_reverse(explode('.', $ip))); - - // Öffentliche/abfragbare Zonen (keine Registrierung nötig) - $publicZones = [ - 'zen.spamhaus.org', // seriös, rate-limited - 'psbl.surriel.com', // public - 'dnsbl-1.uceprotect.net', // public (umstritten, aber abfragbar) - 'all.s5h.net', // public - ]; - - // Registrierungspflichtige – nur optional prüfen - $registeredZones = [ - 'b.barracudacentral.org', // benötigt Account - // weitere bei Bedarf - ]; - - $zones = $publicZones; - if ($this->includeRegistered) { - $zones = array_merge($zones, $registeredZones); - } - - // Vorab: Zone existiert? (NS-Record) – sonst überspringen - $zones = array_values(array_filter($zones, function ($z) { - return @checkdnsrr($z . '.', 'NS'); - })); - - $listed = []; - foreach ($zones as $zone) { - $qname = "{$rev}.{$zone}."; - // Wenn A oder TXT existiert → gelistet - if (@checkdnsrr($qname, 'A') || @checkdnsrr($qname, 'TXT')) { - $listed[] = $zone; - } - } - - return $listed; + $this->ip = (string)($payload['ip'] ?? '–'); + $this->ipv4 = $payload['ipv4'] ?? null; + $this->ipv6 = $payload['ipv6'] ?? null; + $this->hits = (int)($payload['hits'] ?? 0); + $this->lists = (array)($payload['lists'] ?? []); + $this->meta = (array)($payload['meta'] ?? []); + $this->checkedAt = $payload['checked_at'] ?? null; + $this->validUntil = $payload['valid_until'] ?? null; } } -//declare(strict_types=1); + +//namespace App\Livewire\Ui\Security; +// +//use Illuminate\Support\Facades\Cache; +//use Livewire\Component; +// +//class RblCard extends Component +//{ +// public string $ip = '–'; +// public int $hits = 0; +// public array $lists = []; +// +// public ?string $ipv4 = null; +// public ?string $ipv6 = null; +// +// // Schalte registrierungspflichtige Listen (Barracuda etc.) optional zu +// private bool $includeRegistered = false; // env('RBL_INCLUDE_REGISTERED', false) wenn du willst +// +// public function mount(): void +// { +// $this->load(); +// } +// +// public function render() +// { +// return view('livewire.ui.security.rbl-card'); +// } +// +// public function refresh(): void +// { +// Cache::forget('dash.rbl'); +// $this->load(true); +// } +// +// protected function load(bool $force = false): void +// { +// [$ip4, $ip6] = $this->resolvePublicIpsFromInstallerEnv(); +// +// $this->ipv4 = $ip4 ?: trim((string)env('SERVER_PUBLIC_IPV4', '')) ?: '–'; +// $this->ipv6 = $ip6 ?: trim((string)env('SERVER_PUBLIC_IPV6', '')) ?: '–'; +// +// $data = Cache::remember('dash.rbl', $force ? 1 : 6 * 3600, function () { +// $candidate = $this->validIPv4($this->ipv4 ?? '') ? $this->ipv4 : null; +// +// if (!$candidate) { +// $fromFile = trim((string)@file_get_contents('/etc/mailwolt/public_ip')); +// if ($this->validIPv4($fromFile)) $candidate = $fromFile; +// } +// if (!$candidate) { +// $curl = trim((string)@shell_exec("curl -fsS --max-time 2 ifconfig.me 2>/dev/null")); +// if ($this->validIPv4($curl)) $candidate = $curl; +// } +// +// $ip = $candidate ?: '0.0.0.0'; +// $lists = $this->queryRblLists($ip); +// +// return ['ip' => $ip, 'hits' => count($lists), 'lists' => $lists]; +// }); +// +// foreach ($data as $k => $v) $this->$k = $v; +// } +// +// /** bevorzugt Installer-ENV */ +// private function resolvePublicIpsFromInstallerEnv(): array +// { +// $file = '/etc/mailwolt/installer.env'; +// if (!is_readable($file)) return [null, null]; +// +// $ipv4 = $ipv6 = null; +// $lines = @file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []; +// foreach ($lines as $line) { +// if (preg_match('/^\s*#/', $line) || !str_contains($line, '=')) continue; +// [$k, $v] = array_map('trim', explode('=', $line, 2)); +// $v = trim($v, " \t\n\r\0\x0B\"'"); +// if ($k === 'SERVER_PUBLIC_IPV4' && $this->validIPv4($v)) $ipv4 = $v; +// if ($k === 'SERVER_PUBLIC_IPV6' && $this->validIPv6($v)) $ipv6 = $v; +// } +// return [$ipv4, $ipv6]; +// } +// +// private function validIPv4(?string $ip): bool +// { +// return (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); +// } +// +// private function validIPv6(?string $ip): bool +// { +// return (bool)filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); +// } +// +// /** +// * Prüft die IP gegen gängige **öffentliche** RBLs. +// * @return array gelistete RBL-Zonen +// */ +// private function queryRblLists(string $ip): array +// { +// if (!$this->validIPv4($ip)) return []; +// +// $rev = implode('.', array_reverse(explode('.', $ip))); +// +// // nur Zonen prüfen, die es wirklich gibt +// $zones = [ +// 'zen.spamhaus.org', +// 'psbl.surriel.com', +// 'dnsbl-1.uceprotect.net', +// 'all.s5h.net', +// ]; +// $zones = array_values(array_filter($zones, fn($z) => @checkdnsrr($z.'.','NS'))); +// +// $listed = []; +// foreach ($zones as $zone) { +// $q = "{$rev}.{$zone}."; +// +// $a = @dns_get_record($q, DNS_A) ?: []; +// if (!count($a)) continue; +// +// $ips = array_column($a, 'ip'); +// +// // --- WICHTIG: Spamhaus "blocked" / Ratelimit ignorieren +// if (array_intersect($ips, ['127.255.255.254','127.255.255.255'])) { +// // optional: merk dir, dass Spamhaus blockt -> UI-Hinweis +// $listed[] = ['zone'=>$zone, 'code'=>'blocked', 'txt'=>null]; +// continue; +// } +// +// $txtRecs = @dns_get_record($q, DNS_TXT) ?: []; +// $txt = $txtRecs[0]['txt'] ?? null; +// +// $listed[] = ['zone'=>$zone, 'code'=>$ips[0] ?? null, 'txt'=>$txt]; +// } +// +// // Nur echte Treffer zurückgeben; „blocked“ separat signalisieren +// $real = array_values(array_filter($listed, fn($e) => ($e['code'] ?? null) !== 'blocked')); +// +// // Falls alles nur "blocked" war, gib leere Liste zurück +// return array_map(fn($e) => $e['zone'].($e['code'] ? " ({$e['code']})" : ''), $real); +// } +//} +////declare(strict_types=1); // //namespace App\Livewire\Ui\Security; // diff --git a/app/Livewire/Ui/System/UpdateCard.php b/app/Livewire/Ui/System/UpdateCard.php index bd6d2a3..def4b92 100644 --- a/app/Livewire/Ui/System/UpdateCard.php +++ b/app/Livewire/Ui/System/UpdateCard.php @@ -114,6 +114,7 @@ class UpdateCard extends Component @shell_exec('nohup php /var/www/mailwolt/artisan health:collect >/dev/null 2>&1 &'); @shell_exec('nohup php /var/www/mailwolt/artisan settings:sync >/dev/null 2>&1 &'); @shell_exec('nohup php /var/www/mailwolt/artisan spamav:collect >/dev/null 2>&1 &'); + @shell_exec('nohup php /var/www/mailwolt/artisan rbl:probe --force >/dev/null 2>&1 &'); $this->postActionsDone = true; $ver = $this->displayCurrent ?? 'aktuelle Version'; diff --git a/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php b/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php index eb2a32a..9599897 100644 --- a/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php +++ b/resources/views/livewire/ui/domain/modal/domain-dns-modal.blade.php @@ -1,14 +1,13 @@ {{-- resources/views/livewire/domains/dns-assistant-modal.blade.php --}} @push('modal.header') -
- - TTL: {{ $ttl }} - -

DNS-Einträge

+
+ + TTL: {{ $ttl }} + +

DNS-Einträge

Setze die folgenden Records für {{ $zone }}. @@ -17,131 +16,366 @@ @endpush

-
- {{-- GRID: links (Mail-Records), rechts (Infrastruktur) --}} -
- {{-- Mail-Records --}} -
-
- Step 1 - Mail-Records - - Absenderdomain - -
+
+ {{-- Step 1: Mail-Records (domain-spezifisch) --}} +
+
+ Step 1 + Mail-Records + + Absenderdomain + +
-
- @foreach ($dynamic as $r) -
-
-
- {{ $r['type'] }} - {{ $r['name'] }} -
-
- -
+
+ @foreach ($dynamic as $r) +
+
+
+ + {{ $r['type'] }} + + {{ $r['name'] }}
-
-
{{ $r['value'] }}
- @if(!empty($r['helpUrl'])) - - {{ $r['helpLabel'] }} - - @endif +
+
- @endforeach - @foreach ($optional as $r) -
-
-
- {{ $r['type'] }} - {{ $r['name'] }} -
-
- Optional - -
-
-
-
{{ $r['value'] }}
- @if(!empty($r['helpUrl'])) - - {{ $r['helpLabel'] }} - - @endif +
+
{{ $r['value'] }}
+ + @if($checked && filled(trim($r['actual'] ?? '')) && ($r['state'] ?? '') !== 'ok') +
+ Ist: + + {{ str_replace('"', '', preg_replace('/\s+/', ' ', trim($r['actual']))) }} +
+ @endif + {{-- @if($checked && !empty($r['actual']))--}} +{{--
--}} +{{-- Ist:--}} +{{-- {{ $r['actual'] }}--}} +{{--
--}} +{{-- @endif--}} +
+
+ @endforeach +
+
+ + {{-- Step 2: Globale Infrastruktur (MTA-Host) --}} +
+
+ Step 2 + Globale Infrastruktur (MTA-Host) + gilt für alle Domains +
+ +
+ @foreach ($static as $r) +
+
+
+ + {{ $r['type'] }} + + {{ $r['name'] }}
- @endforeach -
-
- - {{-- Globale Infrastruktur --}} -
-
- Step 2 - Globale Infrastruktur (MTA-Host) - gilt für alle Domains -
- -
- @foreach ($static as $r) -
-
-
- {{ $r['type'] }} - {{ $r['name'] }} -
-
- -
-
-
-
{{ $r['value'] }}
+
+
- @endforeach -
-
-
+ +
+
{{ $r['value'] }}
+ @if($checked && filled(trim($r['actual'] ?? '')) && ($r['state'] ?? '') !== 'ok') +
+ Ist: + + {{ str_replace('"', '', preg_replace('/\s+/', ' ', trim($r['actual']))) }} + +
+ @endif +
+
+ @endforeach +
+
+ + {{-- Optional-Block unterhalb (einspaltig) --}} + @if(!empty($optional)) +
+
+ Optional + empfohlene Zusatz-Records +
+ +
+ @foreach ($optional as $r) +
+
+
+ + {{ $r['type'] }} + + {{ $r['name'] }} + + Optional + +
+
+ +
+
+ +
+
{{ $r['value'] }}
+ + @if($checked && filled(trim($r['actual'] ?? '')) && ($r['state'] ?? '') !== 'ok') +
+ Ist: + + {{ str_replace('"', '', preg_replace('/\s+/', ' ', trim($r['actual']))) }} + +
+ @endif + @if(!empty($r['helpUrl'])) +
+ {{ $r['info'] }} +
+ + {{ $r['helpLabel'] }} + + @endif +
+
+ @endforeach +
+
+ @endif
@push('modal.footer')
- - vorhanden - - - abweichend - - - fehlt - - -
- - -
-
+ + DNS prüfen + prüfe… + + +
@endpush + +{{--@push('modal.header')--}} +{{--
--}} +{{-- --}} +{{-- TTL: {{ $ttl }}--}} +{{-- --}} +{{--

DNS-Einträge

--}} + +{{--

--}} +{{-- Setze die folgenden Records für--}} +{{-- {{ $zone }}.--}} +{{--

--}} +{{--
--}} +{{--@endpush--}} + +{{--
--}} +{{--
--}} +{{-- --}}{{-- GRID: links (Mail-Records), rechts (Infrastruktur) --}} +{{--
--}} +{{-- --}}{{-- Mail-Records --}} +{{--
--}} +{{--
--}} +{{-- Step 1--}} +{{-- Mail-Records--}} +{{-- --}} +{{-- Absenderdomain--}} +{{-- --}} +{{--
--}} + +{{--
--}} +{{-- @foreach ($dynamic as $r)--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- {{ $r['type'] }}--}} +{{-- --}} +{{-- {{ $r['name'] }}--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
{{ $r['value'] }}
--}} +{{-- @if(!empty($r['helpUrl']))--}} +{{-- --}} +{{-- {{ $r['helpLabel'] }}--}} +{{-- --}} +{{-- @endif--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{-- {{ $r['type'] }}--}} +{{-- {{ $r['name'] }}--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
{{ $r['value'] }}
--}} +{{-- @if(!empty($r['helpUrl']))--}} +{{-- --}} +{{-- {{ $r['helpLabel'] }}--}} +{{-- --}} +{{-- @endif--}} +{{--
--}} +{{--
--}} +{{-- @endforeach--}} + +{{-- @foreach ($optional as $r)--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{-- {{ $r['type'] }}--}} +{{-- {{ $r['name'] }}--}} +{{--
--}} +{{--
--}} +{{-- Optional--}} +{{-- --}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
{{ $r['value'] }}
--}} +{{-- @if(!empty($r['helpUrl']))--}} +{{-- --}} +{{-- {{ $r['helpLabel'] }}--}} +{{-- --}} +{{-- @endif--}} +{{--
--}} +{{--
--}} +{{-- @endforeach--}} +{{--
--}} +{{--
--}} + +{{-- --}}{{-- Globale Infrastruktur --}} +{{--
--}} +{{--
--}} +{{-- Step 2--}} +{{-- Globale Infrastruktur (MTA-Host)--}} +{{-- gilt für alle Domains--}} +{{--
--}} + +{{--
--}} +{{-- @foreach ($static as $r)--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- {{ $r['type'] }}--}} +{{-- --}} +{{-- {{ $r['name'] }}--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
{{ $r['value'] }}
--}} +{{--
--}} +{{--
--}} + +{{--
--}} +{{--
--}} +{{--
--}} +{{-- {{ $r['type'] }}--}} +{{-- {{ $r['name'] }}--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
{{ $r['value'] }}
--}} +{{--
--}} +{{--
--}} +{{-- @endforeach--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
--}} + +{{--@push('modal.footer')--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- vorhanden--}} +{{-- --}} +{{-- --}} +{{-- abweichend--}} +{{-- --}} +{{-- --}} +{{-- fehlt--}} +{{-- --}} + +{{-- --}} +{{--
--}} +{{-- --}} +{{-- --}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{--@endpush--}} {{--@push('modal.footer')--}} {{--
--}} {{--
--}} diff --git a/resources/views/livewire/ui/mail/dns-health-card.blade.php b/resources/views/livewire/ui/mail/dns-health-card.blade.php index 2e2e510..f374ee4 100644 --- a/resources/views/livewire/ui/mail/dns-health-card.blade.php +++ b/resources/views/livewire/ui/mail/dns-health-card.blade.php @@ -1,22 +1,192 @@ -
-
+
+
- DKIM / DMARC / TLSA + Mail-DNS Health +
+
+ TLSA: + + {{ $tlsa ? 'ok' : 'fehlend' }} + + ({{ $mtaHost }})
+
@forelse($rows as $r) -
-
{{ $r['dom'] }}
-
- DKIM - DMARC - TLSA -
-
+ @empty
Keine Domains.
@endforelse
+ +{{--
--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- DNS / Mail Health--}} +{{--
--}} +{{--
--}} +{{-- IP:--}} +{{-- {{ $ipv4 ?? '–' }}--}} +{{--
--}} +{{--
--}} + +{{-- --}}{{-- Hostweite TLSA-Anzeige --}} +{{--
--}} +{{-- --}} +{{-- TLSA ({{ $host }})--}} +{{-- --}} +{{-- für 25/465/587--}} +{{--
--}} + +{{--
--}} +{{-- @forelse($domains as $dom)--}} +{{--
--}} +{{--
{{ $dom['name'] }}
--}} +{{--
--}} +{{-- --}} +{{-- DKIM--}} +{{-- --}} +{{-- --}} +{{-- DMARC--}} +{{-- --}} +{{--
--}} +{{--
--}} +{{-- @empty--}} +{{--
Keine Domains.
--}} +{{-- @endforelse--}} +{{--
--}} +{{--
--}} + +{{--
--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- DNS Overview--}} +{{--
--}} +{{--
--}} +{{-- IP: {{ $ipv4 ?? '–' }}--}} +{{--
--}} +{{--
--}} + +{{-- @foreach($domains as $dom)--}} +{{--
--}} +{{--
{{ $dom['name'] }}
--}} +{{--
--}} +{{-- @foreach($dom['checks'] as $label => $status)--}} +{{--
--}} +{{-- --}} +{{-- {{ strtoupper($label) }}--}} +{{-- --}} +{{--
--}} +{{-- @endforeach--}} +{{--
--}} +{{--
--}} +{{-- @endforeach--}} +{{--
--}} + +{{--
--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- DKIM / DMARC / TLSA--}} +{{--
--}} +{{--
--}} +{{-- IP:--}} +{{-- --}} +{{-- {{ $ipv4 ?? '–' }}{{ $ipv6 ? ' / '.$ipv6 : '' }}--}} +{{-- --}} +{{--
--}} +{{--
--}} + +{{-- --}}{{-- Host-Status (TLSA) --}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- {{ $host }}--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- TLSA--}} +{{-- --}} +{{--
--}} +{{--
--}} + +{{-- --}}{{-- Domainliste: DKIM/DMARC --}} +{{--
--}} +{{-- @forelse($rows as $r)--}} +{{--
--}} +{{--
{{ $r['dom'] }}
--}} +{{--
--}} +{{-- --}} +{{-- DKIM--}} +{{-- --}} +{{-- --}} +{{-- DMARC--}} +{{-- --}} +{{--
--}} +{{--
--}} +{{-- @empty--}} +{{--
Keine aktiven Domains gefunden.
--}} +{{-- @endforelse--}} +{{--
--}} +{{--
--}} + +{{--
--}} +{{--
--}} +{{--
--}} +{{-- --}} +{{-- DKIM / DMARC / TLSA--}} +{{--
--}} +{{--
--}} +{{--
--}} +{{-- @forelse($rows as $r)--}} +{{--
--}} +{{--
{{ $r['dom'] }}
--}} +{{--
--}} +{{-- DKIM--}} +{{-- DMARC--}} +{{-- TLSA--}} +{{--
--}} +{{--
--}} +{{-- @empty--}} +{{--
Keine Domains.
--}} +{{-- @endforelse--}} +{{--
--}} +{{--
--}} diff --git a/resources/views/livewire/ui/security/rbl-card.blade.php b/resources/views/livewire/ui/security/rbl-card.blade.php index e9aabef..9e2fab0 100644 --- a/resources/views/livewire/ui/security/rbl-card.blade.php +++ b/resources/views/livewire/ui/security/rbl-card.blade.php @@ -1,50 +1,68 @@ -
+
- - Reputation / RBL + + Reputation / RBL
-
- IP: {{ $ip }} +
+ IP: + {{ $ip }}
-
+
@if($hits === 0) -
- - Deine IP ist aktuell auf keiner der geprüften Blacklists. +
+
+ +
+
+
+ Deine IP ist auf keiner bekannten Blacklist. +
+
+ Gute Reputation – keine Auffälligkeiten gefunden. +
+
-
- Geprüfte öffentliche RBLs: Spamhaus, PSBL, UCEPROTECT-1, s5h. +
+ Geprüfte öffentliche RBLs: + Spamhaus, PSBL, UCEPROTECT-1, s5h
@else -
- - {{ $hits }} Treffer auf Blacklists: +
+
+ +
+
+
+ {{ $hits }} {{ Str::plural('Treffer', $hits) }} auf Blacklists +
+
    + @foreach($lists as $l) +
  • + + {{ $l }} +
  • + @endforeach +
+
-
    - @foreach($lists as $l) -
  • - - {{ $l }} -
  • - @endforeach -
@endif
-
+
+ {{--
--}} {{--
--}} {{--
--}} diff --git a/routes/console.php b/routes/console.php index d0f1471..e407f97 100644 --- a/routes/console.php +++ b/routes/console.php @@ -13,6 +13,8 @@ Schedule::job(RunHealthChecks::class)->everyMinute()->withoutOverlapping(); Schedule::command('spamav:collect')->everyFiveMinutes()->withoutOverlapping(); //Schedule::command('mailwolt:check-updates')->dailyAt('04:10'); Schedule::command('mailwolt:check-updates')->everytwoMinutes(); +Schedule::command('rbl:probe')->weeklyOn(0, '3:30')->withoutOverlapping(); + Schedule::command('mail:update-stats')->everyFiveMinutes()->withoutOverlapping(); Schedule::command('health:probe-disk', ['target' => '/', '--ttl' => 900])->everyTenMinutes();