Fix: Mailbox Stats über Dovecot mit config/mailpool.php

main v1.0.96
boban 2025-10-29 04:10:49 +01:00
parent 0cb7212d4b
commit 3c1093311c
3 changed files with 233 additions and 49 deletions

View File

@ -103,13 +103,6 @@ class DomainDnsModal extends ModalComponent
['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 = [
// --- Service-Erkennung ---
[
@ -587,6 +580,8 @@ class DomainDnsModal extends ModalComponent
return view('livewire.ui.domain.modal.domain-dns-modal');
}
}
//namespace App\Livewire\Ui\Domain\Modal;
//
//use App\Models\Domain;

View File

@ -2,16 +2,16 @@
namespace App\Livewire\Ui\Mail;
use Livewire\Attributes\On;
use Livewire\Component;
use App\Models\Domain;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class DnsHealthCard extends Component
{
public array $rows = []; // [{id,name,ok,missing:[...]}]
public string $mtaHost = ''; // z.B. mx.nexlab.at
public bool $tlsa = false; // hostweit
public string $mtaHost = '';
public bool $tlsa = false;
public function mount(): void
{
@ -35,51 +35,66 @@ class DnsHealthCard extends Component
protected function load(bool $force = false): void
{
[$this->mtaHost, $this->tlsa, $this->rows] = Cache::remember('dash.dnshealth.v1', $force ? 1 : 600, function () {
[$this->mtaHost, $this->tlsa, $this->rows] = Cache::remember('dash.dnshealth.v2', $force ? 1 : 600, function () {
$base = trim((string)env('BASE_DOMAIN', ''));
$mtaSub = trim((string)env('MTA_SUB', 'mx'));
$mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub;
$base = trim((string) env('BASE_DOMAIN', ''));
$mtaSub = trim((string) env('MTA_SUB', 'mx'));
$mtaHost = $base !== '' ? "{$mtaSub}.{$base}" : $mtaSub; // z.B. mx.nexlab.at
$rows = [];
// ▼ gewünschter Filter:
$domains = Domain::query()
->where('is_system', false)
->where('is_active', true)
->where(function ($q) {
$q->where('is_system', false)
->orWhere(function ($q2) {
$q2->where('is_system', true)->where('is_server', false);
});
})
->orderBy('domain')
->get(['id', 'domain']);
$rows = [];
foreach ($domains as $d) {
$dom = $d->domain;
// DKIM-Selector ermitteln: .env > DB > Fallback null
$selector = trim((string) env('DKIM_SELECTOR', ''));
if ($selector === '') {
$selector = (string) DB::table('dkim_keys')
->where('domain_id', $d->id)
->where('is_active', 1)
->orderByDesc('id')
->value('selector') ?? '';
}
$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';
if (!$this->hasSpf($dom)) $missing[] = 'SPF';
if (!$this->hasDkim($dom, $selector)) $missing[] = 'DKIM';
if (!$this->hasTxt("_dmarc.$dom")) $missing[] = 'DMARC';
$rows[] = [
'id' => (int)$d->id,
'name' => $dom,
'ok' => empty($missing),
'id' => (int) $d->id,
'name' => $dom,
'ok' => empty($missing),
'missing' => $missing,
];
}
// TLSA (hostweit, nur Info)
// Hostweites TLSA (nur Hinweis)
$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) ───────────────── */
/* ── DNS Helpers ───────────────────────────────────────────────────── */
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) ?: '';
$cmd = "timeout 2 dig +short " . escapeshellarg($name) . ' ' . escapeshellarg(strtoupper($type)) . " 2>/dev/null";
return (string) @shell_exec($cmd) ?: '';
}
protected function hasTxt(string $name): bool
@ -101,11 +116,13 @@ class DnsHealthCard extends Component
return false;
}
// DKIM: wenn spezifischer Selector vorhanden → prüfe den, sonst akzeptiere _domainkey-Policy als “vorhanden”
protected function hasDkim(string $domain): bool
// ▼ DKIM: bevorzugt konkreten Selector prüfen; wenn leer, versuche Policy (_domainkey)
protected function hasDkim(string $domain, string $selector = ''): bool
{
$sel = trim((string)env('DKIM_SELECTOR', ''));
if ($sel !== '' && $this->hasTxt("{$sel}._domainkey.$domain")) return true;
if ($selector !== '') {
return $this->hasTxt("{$selector}._domainkey.$domain");
}
// Manche Betreiber veröffentlichen eine Policy auf _domainkey.<dom>
return $this->hasTxt("_domainkey.$domain");
}
@ -116,21 +133,153 @@ class DnsHealthCard extends Component
$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);
$allowed = array_map(fn ($h) => strtolower(rtrim($h, '.')), $allowedHosts);
foreach ($targets as $t) {
$t = rtrim($t, '.');
if (in_array($t, $allowed, true)) return true;
}
return false;
}
}
//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 = []; // [{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);
// }
//
// 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)
// ->orderBy('domain')
// ->get(['id', 'domain']);
//
// foreach ($domains as $d) {
// $dom = $d->domain;
//
// $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,
// ];
// }
//
// // 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
// {
// return trim($this->digShort('TXT', $name)) !== '';
// }
//
// protected function hasTlsa(string $name): bool
// {
// 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;

View File

@ -1,15 +1,17 @@
<div wire:poll.60s="refresh" class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">
<div class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">
<div class="flex items-center justify-between mb-4">
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
<i class="ph ph-globe-stand text-white/70 text-[13px]"></i>
<span class="text-[11px] uppercase text-white/70 tracking-wide">Mail-DNS Health</span>
</div>
<div class="text-xs text-white/60">
<span class="opacity-70">TLSA:</span>
<span class="{{ $tlsa ? 'text-emerald-300' : 'text-rose-300' }}">
{{ $tlsa ? 'ok' : 'fehlend' }}
</span>
<span class="opacity-50">({{ $mtaHost }})</span>
<div class="flex items-center gap-3">
<button wire:click="refresh"
class="inline-flex items-center gap-1.5 rounded-full text-[12px] px-3 py-1.5
text-white/80 bg-white/10 border border-white/15
hover:bg-white/15 hover:text-white transition">
<i class="ph ph-arrows-clockwise text-[13px]"></i>
Neu prüfen
</button>
</div>
</div>
@ -17,7 +19,7 @@
@forelse($rows as $r)
<button type="button"
wire:click="openDnsModal({{ $r['id'] }})"
class="w-full text-left py-3 flex items-center justify-between hover:bg-white/5/20 rounded-lg px-2 hover:bg-white/5">
class="w-full text-left py-3 flex items-center justify-between rounded-lg px-2 hover:bg-white/5">
<div class="text-white/85">{{ $r['name'] }}</div>
@if($r['ok'])
@ -25,14 +27,9 @@
OK
</span>
@else
<div class="flex items-center gap-2">
<span class="px-2 py-0.5 rounded-full border text-amber-200 border-amber-400/30 bg-amber-500/10 text-xs">
Fertig konfigurieren
</span>
{{-- <span class="text-[11px] text-white/45">--}}
{{-- fehlt: {{ implode(', ', $r['missing']) }}--}}
{{-- </span>--}}
</div>
<span class="px-2 py-0.5 rounded-full border text-amber-200 border-amber-400/30 bg-amber-500/10 text-xs">
Fertig konfigurieren
</span>
@endif
</button>
@empty
@ -41,6 +38,49 @@
</div>
</div>
{{--<div wire:poll.60s="refresh" class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">--}}
{{-- <div class="flex items-center justify-between mb-4">--}}
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
{{-- <i class="ph ph-globe-stand text-white/70 text-[13px]"></i>--}}
{{-- <span class="text-[11px] uppercase text-white/70 tracking-wide">Mail-DNS Health</span>--}}
{{-- </div>--}}
{{-- <div class="text-xs text-white/60">--}}
{{-- <span class="opacity-70">TLSA:</span>--}}
{{-- <span class="{{ $tlsa ? 'text-emerald-300' : 'text-rose-300' }}">--}}
{{-- {{ $tlsa ? 'ok' : 'fehlend' }}--}}
{{-- </span>--}}
{{-- <span class="opacity-50">({{ $mtaHost }})</span>--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- <div class="divide-y divide-white/5">--}}
{{-- @forelse($rows as $r)--}}
{{-- <button type="button"--}}
{{-- wire:click="openDnsModal({{ $r['id'] }})"--}}
{{-- class="w-full text-left py-3 flex items-center justify-between hover:bg-white/5/20 rounded-lg px-2 hover:bg-white/5">--}}
{{-- <div class="text-white/85">{{ $r['name'] }}</div>--}}
{{-- @if($r['ok'])--}}
{{-- <span class="px-2 py-0.5 rounded-full border text-emerald-300 border-emerald-400/30 bg-emerald-500/10 text-xs">--}}
{{-- OK--}}
{{-- </span>--}}
{{-- @else--}}
{{-- <div class="flex items-center gap-2">--}}
{{-- <span class="px-2 py-0.5 rounded-full border text-amber-200 border-amber-400/30 bg-amber-500/10 text-xs">--}}
{{-- Fertig konfigurieren--}}
{{-- </span>--}}
{{-- <span class="text-[11px] text-white/45">--}}
{{-- fehlt: {{ implode(', ', $r['missing']) }}--}}
{{-- </span>--}}
{{-- </div>--}}
{{-- @endif--}}
{{-- </button>--}}
{{-- @empty--}}
{{-- <div class="py-4 text-sm text-white/60">Keine Domains.</div>--}}
{{-- @endforelse--}}
{{-- </div>--}}
{{--</div>--}}
{{--<div wire:poll.60s="refresh" class="glass-card p-5 rounded-2xl border border-white/10 bg-white/5">--}}
{{-- <div class="flex items-center justify-between mb-4">--}}
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}