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

main v1.0.15
boban 2025-10-21 20:54:00 +02:00
parent 57b01654cc
commit 300928851a
2 changed files with 607 additions and 317 deletions

View File

@ -13,128 +13,98 @@ use RecursiveIteratorIterator;
class UpdateMailboxStats extends Command
{
protected $signature = 'mail:update-stats {--user=}';
protected $description = 'Aktualisiert Quota & Nachrichtenzahl (Settings/Redis; ohne MailUser-DB-Felder).';
protected $description = 'Aktualisiert Quota & Nachrichtenzahl (Settings/Redis; ohne DB-Spalten).';
public function handle(): int
{
$log = Log::channel('mailstats');
$onlyUser = trim((string)$this->option('user')) ?: null;
$started = microtime(true);
$t0 = microtime(true);
$q = MailUser::query()
->with('domain:id,domain,is_system')
// Basis-Query: nur aktive, keine System-Mailboxen und keine System-Domains
$base = MailUser::query()
->select(['id', 'domain_id', 'localpart', 'email', 'is_active', 'is_system'])
->with(['domain:id,domain,is_system'])
->where('is_active', true)
->where('is_system', false) // keine System-Mailboxen
->whereHas('domain', fn($d) => $d->where('is_system', false)); // keine Systemdomains
->where('is_system', false)
->whereHas('domain', fn($d) => $d->where('is_system', false));
if ($onlyUser) {
$q->where('email', $onlyUser);
$base->where('email', $onlyUser);
}
$users = $q->get();
$log->info('mail:update-stats START', [
'only' => $onlyUser,
'users' => $users->count(),
]);
if ($users->isEmpty()) {
$this->warn('Keine passenden Mailboxen gefunden.');
$log->info('mail:update-stats DONE (no users)', ['ms' => (int)((microtime(true) - $started) * 1000)]);
return self::SUCCESS;
}
$changed = 0;
$checked = 0;
$changed = 0;
foreach ($users as $u) {
$checked++;
$log->info('mail:update-stats START', ['only' => $onlyUser]);
// Email robust ermitteln
$raw = (string)($u->getRawOriginal('email') ?? '');
$email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null);
$base->orderBy('id')->chunkById(200, function ($users) use (&$checked, &$changed, $log) {
foreach ($users as $u) {
$checked++;
if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
// nur debug vermeidet Spam
$log->debug('skip invalid email', ['user_id' => $u->id, 'raw' => $raw, 'computed' => $email]);
continue;
}
// Email robust bestimmen (raw -> accessor -> zusammengesetzt)
$raw = (string)($u->getRawOriginal('email') ?? '');
$email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null);
[$local, $domain] = explode('@', $email, 2);
$maildir = "/var/mail/vhosts/{$domain}/{$local}";
if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
// still kein Log-Spam
continue;
}
// Größe (Dateisystem)
$usedBytes = 0;
if (is_dir($maildir)) {
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
);
foreach ($it as $f) {
if ($f->isFile()) {
$usedBytes += $f->getSize();
[$local, $domain] = explode('@', $email, 2);
$maildir = "/var/mail/vhosts/{$domain}/{$local}";
// Größe in Bytes (rekursiv)
$usedBytes = 0;
if (is_dir($maildir)) {
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
);
foreach ($it as $f) {
if ($f->isFile()) $usedBytes += $f->getSize();
}
}
// Message-Count
$messageCount = $this->countViaDoveadm($email);
if ($messageCount === null) {
$messageCount = $this->countViaFilesystem($maildir);
}
$key = "mailbox.{$email}";
$prev = (array)(Setting::get($key, []) ?: []);
$new = [
'used_bytes' => (int)$usedBytes,
'message_count' => (int)$messageCount,
'updated_at' => now()->toDateTimeString(),
];
if (($prev['used_bytes'] ?? null) !== $new['used_bytes']
|| ($prev['message_count'] ?? null) !== $new['message_count']) {
Setting::set($key, $new);
$changed++;
// kurze Ausgabe & Info-Log NUR bei Änderung
$this->line(sprintf("%-35s %7.1f MiB %5d msgs",
$email, $usedBytes / 1048576, $messageCount));
$log->info('updated', ['email' => $email, 'used_bytes' => $new['used_bytes'], 'message_count' => $new['message_count']]);
}
}
});
// Nachrichten zählen
$messageCount = $this->countViaDoveadm($email);
if ($messageCount === null) {
$messageCount = $this->countViaFilesystem($maildir);
}
// Nur bei Änderungen persistieren + loggen
$key = "mailbox.{$email}";
$prev = Setting::get($key, null) ?: [];
$new = [
'used_bytes' => (int)$usedBytes,
'message_count' => (int)$messageCount,
'updated_at' => now()->toDateTimeString(),
];
if (
!is_array($prev) ||
($prev['used_bytes'] ?? null) !== $new['used_bytes'] ||
($prev['message_count'] ?? null) !== $new['message_count']
) {
Setting::set($key, $new);
$changed++;
// kurze, nützliche Info nur bei Änderung
$this->line(sprintf("%-35s %7.1f MiB %5d msgs",
$email, $usedBytes / 1024 / 1024, $messageCount));
$log->info('updated', [
'email' => $email,
'used_bytes' => $new['used_bytes'],
'message_count' => $new['message_count'],
]);
} else {
// keine Änderung → kein Info-Log
$log->debug('unchanged', ['email' => $email]);
}
}
$ms = (int)((microtime(true) - $started) * 1000);
$log->info('mail:update-stats DONE', [
'checked' => $checked,
'changed' => $changed,
'ms' => $ms,
]);
$ms = (int)((microtime(true) - $t0) * 1000);
$log->info('mail:update-stats DONE', compact('checked', 'changed', 'ms'));
$this->info('Mailbox-Statistiken aktualisiert.');
return self::SUCCESS;
}
protected function countViaDoveadm(string $email): ?int
private function countViaDoveadm(string $email): ?int
{
$cmd = "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u "
. escapeshellarg($email) . " messages INBOX 2>&1";
$out = [];
$rc = 0;
exec($cmd, $out, $rc);
if ($rc !== 0) return null;
foreach ($out as $line) {
@ -145,7 +115,7 @@ class UpdateMailboxStats extends Command
return null;
}
protected function countViaFilesystem(string $maildir): int
private function countViaFilesystem(string $maildir): int
{
$n = 0;
foreach (['cur', 'new'] as $sub) {
@ -163,6 +133,168 @@ class UpdateMailboxStats extends Command
}
}
//namespace App\Console\Commands;
//
//use App\Models\MailUser;
//use App\Models\Setting;
//use Illuminate\Console\Command;
//use Illuminate\Support\Facades\Log;
//use RecursiveDirectoryIterator;
//use RecursiveIteratorIterator;
//
//class UpdateMailboxStats extends Command
//{
// protected $signature = 'mail:update-stats {--user=}';
// protected $description = 'Aktualisiert Quota & Nachrichtenzahl (Settings/Redis; ohne MailUser-DB-Felder).';
//
// public function handle(): int
// {
// $log = Log::channel('mailstats');
//
// $onlyUser = trim((string)$this->option('user')) ?: null;
// $started = microtime(true);
//
// $q = MailUser::query()
// ->with('domain:id,domain,is_system')
// ->where('is_active', true)
// ->where('is_system', false) // keine System-Mailboxen
// ->whereHas('domain', fn($d) => $d->where('is_system', false)); // keine Systemdomains
//
// if ($onlyUser) {
// $q->where('email', $onlyUser);
// }
//
// $users = $q->get();
//
// $log->info('mail:update-stats START', [
// 'only' => $onlyUser,
// 'users' => $users->count(),
// ]);
//
// if ($users->isEmpty()) {
// $this->warn('Keine passenden Mailboxen gefunden.');
// $log->info('mail:update-stats DONE (no users)', ['ms' => (int)((microtime(true) - $started) * 1000)]);
// return self::SUCCESS;
// }
//
// $changed = 0;
// $checked = 0;
//
// foreach ($users as $u) {
// $checked++;
//
// // Email robust ermitteln
// $raw = (string)($u->getRawOriginal('email') ?? '');
// $email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null);
//
// if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
// // nur debug vermeidet Spam
// $log->debug('skip invalid email', ['user_id' => $u->id, 'raw' => $raw, 'computed' => $email]);
// continue;
// }
//
// [$local, $domain] = explode('@', $email, 2);
// $maildir = "/var/mail/vhosts/{$domain}/{$local}";
//
// // Größe (Dateisystem)
// $usedBytes = 0;
// if (is_dir($maildir)) {
// $it = new RecursiveIteratorIterator(
// new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
// );
// foreach ($it as $f) {
// if ($f->isFile()) {
// $usedBytes += $f->getSize();
// }
// }
// }
//
// // Nachrichten zählen
// $messageCount = $this->countViaDoveadm($email);
// if ($messageCount === null) {
// $messageCount = $this->countViaFilesystem($maildir);
// }
//
// // Nur bei Änderungen persistieren + loggen
// $key = "mailbox.{$email}";
// $prev = Setting::get($key, null) ?: [];
//
// $new = [
// 'used_bytes' => (int)$usedBytes,
// 'message_count' => (int)$messageCount,
// 'updated_at' => now()->toDateTimeString(),
// ];
//
// if (
// !is_array($prev) ||
// ($prev['used_bytes'] ?? null) !== $new['used_bytes'] ||
// ($prev['message_count'] ?? null) !== $new['message_count']
// ) {
// Setting::set($key, $new);
// $changed++;
//
// // kurze, nützliche Info nur bei Änderung
// $this->line(sprintf("%-35s %7.1f MiB %5d msgs",
// $email, $usedBytes / 1024 / 1024, $messageCount));
//
// $log->info('updated', [
// 'email' => $email,
// 'used_bytes' => $new['used_bytes'],
// 'message_count' => $new['message_count'],
// ]);
// } else {
// // keine Änderung → kein Info-Log
// $log->debug('unchanged', ['email' => $email]);
// }
// }
//
// $ms = (int)((microtime(true) - $started) * 1000);
// $log->info('mail:update-stats DONE', [
// 'checked' => $checked,
// 'changed' => $changed,
// 'ms' => $ms,
// ]);
//
// $this->info('Mailbox-Statistiken aktualisiert.');
// return self::SUCCESS;
// }
//
// protected function countViaDoveadm(string $email): ?int
// {
// $cmd = "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u "
// . escapeshellarg($email) . " messages INBOX 2>&1";
// $out = [];
// $rc = 0;
// exec($cmd, $out, $rc);
//
// if ($rc !== 0) return null;
//
// foreach ($out as $line) {
// if (preg_match('/^\s*INBOX\s+(\d+)\s*$/i', trim($line), $m)) {
// return (int)$m[1];
// }
// }
// return null;
// }
//
// protected function countViaFilesystem(string $maildir): int
// {
// $n = 0;
// foreach (['cur', 'new'] as $sub) {
// $dir = "{$maildir}/{$sub}";
// if (!is_dir($dir)) continue;
// $h = opendir($dir);
// if (!$h) continue;
// while (($fn = readdir($h)) !== false) {
// if ($fn === '.' || $fn === '..' || $fn[0] === '.') continue;
// $n++;
// }
// closedir($h);
// }
// return $n;
// }
//}
//namespace App\Console\Commands;
//
//use App\Models\MailUser;

View File

@ -6,7 +6,6 @@ namespace App\Livewire\Ui\Mail;
use App\Models\Domain;
use App\Models\Setting;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Livewire\Attributes\On;
use Livewire\Component;
@ -27,15 +26,13 @@ class MailboxList extends Component
#[On('focus:domain')]
public function focusDomain(int $id): void
{
// z. B. Domain nach oben holen / scrollen / highlighten
// oder direkt den "+ Postfach" Dialog:
// $this->openMailboxCreate($id);
// optional: Domain hervorheben
}
#[On('focus:user')]
public function focusUser(int $id): void
{
// später: Benutzerseite / Filter setzen ...
// optional
}
public function openMailboxCreate(int $domainId): void
@ -49,126 +46,89 @@ class MailboxList extends Component
{
// $domainId == mailbox_id
$this->dispatch('openModal', component: 'ui.mail.modal.mailbox-edit-modal', arguments: [
$domainId, // <— nur der Wert, kein Key!
$domainId,
]);
}
public function openMailboxDelete(int $domainId): void
{
$this->dispatch('openModal', component: 'ui.mail.modal.mailbox-delete-modal', arguments: [
$domainId, // <— nur der Wert, kein Key!
$domainId,
]);
}
public function updateMailboxStats()
public function updateMailboxStats(): void
{
$started = microtime(true);
Log::channel('mailstats')->info('UI: updateMailboxStats() geklickt', [
'actor' => 'web',
'ip' => request()->ip() ?? null,
]);
// Command ausführen
$rc = Artisan::call('mail:update-stats');
$output = Artisan::output();
Log::channel('mailstats')->info('UI: Command beendet', [
'rc' => $rc,
'ms' => (int)((microtime(true) - $started) * 1000),
'output' => trim($output),
]);
// UI auffrischen
$this->dispatch('$refresh');
// Ergebnis toaster
$this->dispatch('toast',
type: $rc === 0 ? 'done' : 'warn',
badge: 'Mailbox',
title: $rc === 0 ? 'Mailbox aktualisiert' : 'Aktualisierung fehlgeschlagen',
text: $rc === 0 ? 'Statistiken wurden aktualisiert.' : 'Siehe logs/mailstats.log',
duration: 6000,
);
}
public function updateMailboxStatsOne(string $email)
{
Artisan::call('mail:update-stats', ['--user' => $email]);
Artisan::call('mail:update-stats');
$this->dispatch('$refresh');
$this->dispatch('toast',
type: 'done',
badge: 'Mailbox',
title: 'Mailbox aktualisiert',
text: 'Die Mailbox-Statistiken wurden aktualisiert.',
duration: 6000
duration: 6000,
);
}
public function render()
{
$system = Domain::query()->where('is_system', true)->first();
$term = trim($this->search);
$hasTerm = $term !== '';
$needle = '%'.str_replace(['%','_'], ['\%','\_'], $term).'%'; // LIKE-sicher
$system = Domain::query()->where('is_system', true)->first();
$term = trim($this->search);
$hasTerm = $term !== '';
$needle = '%' . str_replace(['%', '_'], ['\%', '\_'], $term) . '%';
$domains = Domain::query()
->when($system, fn ($q) => $q->whereKeyNot($system->id))
// Domain selbst ODER MailUser müssen matchen
->when($system, fn($q) => $q->whereKeyNot($system->id))
->when($hasTerm, function ($q) use ($needle) {
$q->where(function ($w) use ($needle) {
$w->where('domain', 'like', $needle)
->orWhereHas('mailUsers', fn($u) => $u->where('localpart', 'like', $needle));
});
})
->withCount(['mailUsers'])
// Relationen zunächst ggf. gefiltert laden
->with([
'mailUsers' => function ($q) use ($hasTerm, $needle) {
if ($hasTerm) $q->where('localpart', 'like', $needle);
if ($hasTerm) {
$q->where('localpart', 'like', $needle);
}
$q->orderBy('localpart');
},
])
->orderBy('domain')
->get();
// Wenn der Domainname selbst matched → alle Mailboxen/Aliasse vollständig nachladen
if ($hasTerm) {
$lower = Str::lower($term);
foreach ($domains as $d) {
if (Str::contains(Str::lower($d->domain), $lower)) {
$d->setRelation('mailUsers', $d->mailUsers()->orderBy('localpart')->get());
$d->setRelation('mailUsers', $d->mailUsers()->orderBy('localpart')->get());
$d->setRelation('mailAliases', $d->mailAliases()->orderBy('local')->get());
}
}
}
// Vorbereitung für Blade
// ---- Anzeige vorbereiten (mit korrekter Prozent-Berechnung) ----
foreach ($domains as $d) {
$prepared = [];
$prepared = [];
$domainActive = (bool)($d->is_active ?? true);
foreach ($d->mailUsers as $u) {
$email = trim($u->email ?? '') !== ''
? $u->email
: ($u->localpart !== '' ? ($u->localpart.'@'.$d->domain) : null);
// Stats aus Settings (Redis → DB Fallback)
$stats = Setting::get("mailbox.{$u->email}", []);
$usedBytes = (int)($stats['used_bytes'] ?? ($u->used_bytes ?? 0));
$messageCount = (int)($stats['message_count'] ?? ($u->message_count ?? 0));
$stats = $email ? Setting::get("mailbox.$email") : null;
// Anzeige in MiB (nur fürs UI runden)
$usedMiB = (float)round($usedBytes / 1048576, 2);
$quotaMiB = (int)($u->quota_mb ?? 0);
$usedBytes = is_array($stats) && isset($stats['used_bytes']) ? (int)$stats['used_bytes'] : (int)($u->used_bytes ?? 0);
$messageCount = is_array($stats) && isset($stats['message_count']) ? (int)$stats['message_count'] : (int)($u->message_count ?? 0);
$usedMB = (int) round($usedBytes / 1024 / 1024);
$quota = (int)($u->quota_mb ?? 0);
$usage = $quota > 0 ? min(100, (int) round($usedMB / max(1, $quota) * 100)) : 0;
// Prozent aus Bytes/Quota (ohne Vorab-Rundung auf MiB!)
$usage = $quotaMiB > 0
? min(100, (int)round($usedBytes / ($quotaMiB * 1048576) * 100))
: 0;
$mailboxActive = (bool)($u->is_active ?? true);
$effective = $domainActive && $mailboxActive;
$effective = $domainActive && $mailboxActive;
$reason = null;
if (!$effective) {
@ -177,15 +137,15 @@ class MailboxList extends Component
}
$prepared[] = [
'id' => $u->id,
'localpart' => (string)$u->localpart,
'quota_mb' => $quota,
'usage_percent' => $usage,
'used_mb' => $usedMB, // MiB fürs UI
'message_count' => $messageCount,
'is_active' => $mailboxActive,
'id' => $u->id,
'localpart' => (string)$u->localpart,
'quota_mb' => $quotaMiB,
'used_mb' => $usedMiB,
'usage_percent' => $usage,
'message_count' => $messageCount,
'is_active' => $mailboxActive,
'is_effective_active' => $effective,
'inactive_reason' => $reason,
'inactive_reason' => $reason,
];
}
@ -194,9 +154,116 @@ class MailboxList extends Component
return view('livewire.ui.mail.mailbox-list', [
'domains' => $domains,
'system' => $this->showSystemCard ? $system : null,
'system' => $this->showSystemCard ? $system : null,
]);
}
}
//namespace App\Livewire\Ui\Mail;
//
//use App\Models\Domain;
//use App\Models\Setting;
//use Illuminate\Support\Facades\Artisan;
//use Illuminate\Support\Facades\Log;
//use Illuminate\Support\Str;
//use Livewire\Attributes\On;
//use Livewire\Component;
//
//class MailboxList extends Component
//{
// public string $search = '';
// public bool $showSystemCard = false;
//
// #[On('mailbox:updated')]
// #[On('mailbox:deleted')]
// #[On('mailbox:created')]
// public function refreshMailboxList(): void
// {
// $this->dispatch('$refresh');
// }
//
// #[On('focus:domain')]
// public function focusDomain(int $id): void
// {
// // z. B. Domain nach oben holen / scrollen / highlighten
// // oder direkt den "+ Postfach" Dialog:
// // $this->openMailboxCreate($id);
// }
//
// #[On('focus:user')]
// public function focusUser(int $id): void
// {
// // später: Benutzerseite / Filter setzen ...
// }
//
// public function openMailboxCreate(int $domainId): void
// {
// $this->dispatch('openModal', component: 'ui.mail.modal.mailbox-create-modal', arguments: [
// 'domainId' => $domainId,
// ]);
// }
//
// public function openMailboxEdit(int $domainId): void
// {
// // $domainId == mailbox_id
// $this->dispatch('openModal', component: 'ui.mail.modal.mailbox-edit-modal', arguments: [
// $domainId, // <— nur der Wert, kein Key!
// ]);
// }
//
// public function openMailboxDelete(int $domainId): void
// {
// $this->dispatch('openModal', component: 'ui.mail.modal.mailbox-delete-modal', arguments: [
// $domainId, // <— nur der Wert, kein Key!
// ]);
// }
//
// public function updateMailboxStats()
// {
// $started = microtime(true);
//
// Log::channel('mailstats')->info('UI: updateMailboxStats() geklickt', [
// 'actor' => 'web',
// 'ip' => request()->ip() ?? null,
// ]);
//
// // Command ausführen
// $rc = Artisan::call('mail:update-stats');
// $output = Artisan::output();
//
// Log::channel('mailstats')->info('UI: Command beendet', [
// 'rc' => $rc,
// 'ms' => (int)((microtime(true) - $started) * 1000),
// 'output' => trim($output),
// ]);
//
// // UI auffrischen
// $this->dispatch('$refresh');
//
// // Ergebnis toaster
// $this->dispatch('toast',
// type: $rc === 0 ? 'done' : 'warn',
// badge: 'Mailbox',
// title: $rc === 0 ? 'Mailbox aktualisiert' : 'Aktualisierung fehlgeschlagen',
// text: $rc === 0 ? 'Statistiken wurden aktualisiert.' : 'Siehe logs/mailstats.log',
// duration: 6000,
// );
// }
//
//
// public function updateMailboxStatsOne(string $email)
// {
// Artisan::call('mail:update-stats', ['--user' => $email]);
// $this->dispatch('$refresh');
// $this->dispatch('toast',
// type: 'done',
// badge: 'Mailbox',
// title: 'Mailbox aktualisiert',
// text: 'Die Mailbox-Statistiken wurden aktualisiert.',
// duration: 6000
// );
// }
//
// public function render()
// {
// $system = Domain::query()->where('is_system', true)->first();
@ -207,17 +274,17 @@ class MailboxList extends Component
// $domains = Domain::query()
// ->when($system, fn ($q) => $q->whereKeyNot($system->id))
//
// // Domain selbst ODER MailUser/ Aliasse müssen matchen
// // Domain selbst ODER MailUser müssen matchen
// ->when($hasTerm, function ($q) use ($needle) {
// $q->where(function ($w) use ($needle) {
// $w->where('domain', 'like', $needle)
// ->orWhereHas('mailUsers', fn($u) => $u->where('localpart', 'like', $needle));
// ->orWhereHas('mailUsers', fn($u) => $u->where('localpart', 'like', $needle));
// });
// })
//
// ->withCount(['mailUsers'])
//
// // Beziehungen zunächst gefiltert laden (damit "test" nur passende Mailboxen zeigt)
// // Relationen zunächst ggf. gefiltert laden
// ->with([
// 'mailUsers' => function ($q) use ($hasTerm, $needle) {
// if ($hasTerm) $q->where('localpart', 'like', $needle);
@ -228,35 +295,35 @@ class MailboxList extends Component
// ->orderBy('domain')
// ->get();
//
// // Domains, deren NAME den Suchbegriff trifft → ALLE Mailboxen/Aliasse zeigen
// // Wenn der Domainname selbst matched → alle Mailboxen/Aliasse vollständig nachladen
// if ($hasTerm) {
// $lower = Str::lower($term);
// foreach ($domains as $d) {
// if (Str::contains(Str::lower($d->domain), $lower)) {
// // volle Relationen nachladen (überschreibt die gefilterten)
// $d->setRelation('mailUsers', $d->mailUsers()->orderBy('localpart')->get());
// $d->setRelation('mailAliases', $d->mailAliases()->orderBy('local')->get());
// }
// }
// }
//
// // Vorbereitung für Blade (unverändert, arbeitet auf den ggf. gefilterten Relationen)
// // Vorbereitung für Blade
// foreach ($domains as $d) {
// $prepared = [];
// $domainActive = (bool)($d->is_active ?? true);
//
// foreach ($d->mailUsers as $u) {
// $stats = Setting::get("mailbox.{$u->email}");
// $usedBytes = $stats['used_bytes'] ?? ($u->used_bytes ?? 0);
// $messageCount = $stats['message_count'] ?? ($u->message_count ?? 0);
// $usedMB = (int) round(($usedBytes) / 1024 / 1024);
// $email = trim($u->email ?? '') !== ''
// ? $u->email
// : ($u->localpart !== '' ? ($u->localpart.'@'.$d->domain) : null);
//
// $stats = $email ? Setting::get("mailbox.$email") : null;
//
// $usedBytes = is_array($stats) && isset($stats['used_bytes']) ? (int)$stats['used_bytes'] : (int)($u->used_bytes ?? 0);
// $messageCount = is_array($stats) && isset($stats['message_count']) ? (int)$stats['message_count'] : (int)($u->message_count ?? 0);
//
// $usedMB = (int) round($usedBytes / 1024 / 1024);
// $quota = (int)($u->quota_mb ?? 0);
// $usage = $quota > 0 ? min(100, (int) round($usedMB / max(1,$quota) * 100)) : 0;
//
//
//// $quota = (int)($u->quota_mb ?? 0);
//// $used = (int)($u->used_mb ?? 0);
//// $usage = $quota > 0 ? min(100, (int)round($used / max(1, $quota) * 100)) : 0;
// $usage = $quota > 0 ? min(100, (int) round($usedMB / max(1, $quota) * 100)) : 0;
//
// $mailboxActive = (bool)($u->is_active ?? true);
// $effective = $domainActive && $mailboxActive;
@ -272,7 +339,7 @@ class MailboxList extends Component
// 'localpart' => (string)$u->localpart,
// 'quota_mb' => $quota,
// 'usage_percent' => $usage,
// 'used_mb' => $usedMB,
// 'used_mb' => $usedMB, // MiB fürs UI
// 'message_count' => $messageCount,
// 'is_active' => $mailboxActive,
// 'is_effective_active' => $effective,
@ -280,7 +347,6 @@ class MailboxList extends Component
// ];
// }
//
// // für Blade
// $d->prepared_mailboxes = $prepared;
// }
//
@ -289,136 +355,228 @@ class MailboxList extends Component
// 'system' => $this->showSystemCard ? $system : null,
// ]);
// }
// public function render()
// {
// $system = Domain::query()->where('is_system', true)->first();
// $term = trim($this->search);
//// public function render()
//// {
//// $system = Domain::query()->where('is_system', true)->first();
//// $term = trim($this->search);
//// $hasTerm = $term !== '';
//// $needle = '%'.str_replace(['%','_'], ['\%','\_'], $term).'%'; // LIKE-sicher
////
//// $domains = Domain::query()
//// ->when($system, fn ($q) => $q->whereKeyNot($system->id))
////
//// // Domain selbst ODER MailUser/ Aliasse müssen matchen
//// ->when($hasTerm, function ($q) use ($needle) {
//// $q->where(function ($w) use ($needle) {
//// $w->where('domain', 'like', $needle)
//// ->orWhereHas('mailUsers', fn($u) => $u->where('localpart', 'like', $needle));
//// });
//// })
////
//// ->withCount(['mailUsers'])
////
//// // Beziehungen zunächst gefiltert laden (damit "test" nur passende Mailboxen zeigt)
//// ->with([
//// 'mailUsers' => function ($q) use ($hasTerm, $needle) {
//// if ($hasTerm) $q->where('localpart', 'like', $needle);
//// $q->orderBy('localpart');
//// },
//// ])
////
//// ->orderBy('domain')
//// ->get();
////
//// // Domains, deren NAME den Suchbegriff trifft → ALLE Mailboxen/Aliasse zeigen
//// if ($hasTerm) {
//// $lower = Str::lower($term);
//// foreach ($domains as $d) {
//// if (Str::contains(Str::lower($d->domain), $lower)) {
//// // volle Relationen nachladen (überschreibt die gefilterten)
//// $d->setRelation('mailUsers', $d->mailUsers()->orderBy('localpart')->get());
//// $d->setRelation('mailAliases', $d->mailAliases()->orderBy('local')->get());
//// }
//// }
//// }
////
//// // Vorbereitung für Blade (unverändert, arbeitet auf den ggf. gefilterten Relationen)
//// foreach ($domains as $d) {
//// $prepared = [];
//// $domainActive = (bool)($d->is_active ?? true);
////
//// foreach ($d->mailUsers as $u) {
//// $stats = Setting::get("mailbox.{$u->email}");
//// $usedBytes = $stats['used_bytes'] ?? ($u->used_bytes ?? 0);
//// $messageCount = $stats['message_count'] ?? ($u->message_count ?? 0);
//// $usedMB = (int) round(($usedBytes) / 1024 / 1024);
//// $quota = (int)($u->quota_mb ?? 0);
//// $usage = $quota > 0 ? min(100, (int) round($usedMB / max(1,$quota) * 100)) : 0;
////
////
////// $quota = (int)($u->quota_mb ?? 0);
////// $used = (int)($u->used_mb ?? 0);
////// $usage = $quota > 0 ? min(100, (int)round($used / max(1, $quota) * 100)) : 0;
////
//// $mailboxActive = (bool)($u->is_active ?? true);
//// $effective = $domainActive && $mailboxActive;
////
//// $reason = null;
//// if (!$effective) {
//// $reason = !$domainActive ? 'Domain inaktiv'
//// : (!$mailboxActive ? 'Postfach inaktiv' : null);
//// }
////
//// $prepared[] = [
//// 'id' => $u->id,
//// 'localpart' => (string)$u->localpart,
//// 'quota_mb' => $quota,
//// 'usage_percent' => $usage,
//// 'used_mb' => $usedMB,
//// 'message_count' => $messageCount,
//// 'is_active' => $mailboxActive,
//// 'is_effective_active' => $effective,
//// 'inactive_reason' => $reason,
//// ];
//// }
////
//// // für Blade
//// $d->prepared_mailboxes = $prepared;
//// }
////
//// return view('livewire.ui.mail.mailbox-list', [
//// 'domains' => $domains,
//// 'system' => $this->showSystemCard ? $system : null,
//// ]);
//// }
//
// $domains = Domain::query()
// ->when($system, fn ($q) => $q->where('id', '!=', $system->id))
// ->withCount(['mailUsers','mailAliases'])
// ->with([
// 'mailUsers' => fn ($q) => $q->orderBy('localpart'),
// 'mailAliases' => fn ($q) => $q->orderBy('local'),
// ])
// ->when($term !== '', function ($q) use ($term) {
// $q->where(function ($w) use ($term) {
// $w->where('domain', 'like', "%{$term}%")
// ->orWhereHas('mailUsers', fn($u) =>
// $u->where('localpart', 'like', "%{$term}%")
// );
// });
// })
// ->orderBy('domain')
// ->get();
//// public function render()
//// {
//// $system = Domain::query()->where('is_system', true)->first();
//// $term = trim($this->search);
////
//// $domains = Domain::query()
//// ->when($system, fn ($q) => $q->where('id', '!=', $system->id))
//// ->withCount(['mailUsers','mailAliases'])
//// ->with([
//// 'mailUsers' => fn ($q) => $q->orderBy('localpart'),
//// 'mailAliases' => fn ($q) => $q->orderBy('local'),
//// ])
//// ->when($term !== '', function ($q) use ($term) {
//// $q->where(function ($w) use ($term) {
//// $w->where('domain', 'like', "%{$term}%")
//// ->orWhereHas('mailUsers', fn($u) =>
//// $u->where('localpart', 'like', "%{$term}%")
//// );
//// });
//// })
//// ->orderBy('domain')
//// ->get();
////
//// // Vorbereitung für Blade (unverändert)
//// foreach ($domains as $d) {
//// $prepared = [];
//// $domainActive = (bool)($d->is_active ?? true);
////
//// foreach ($d->mailUsers as $u) {
//// $quota = (int) ($u->quota_mb ?? 0);
//// $used = (int) ($u->used_mb ?? 0);
//// $usage = $quota > 0 ? min(100, (int) round($used / max(1,$quota) * 100)) : 0;
////
//// $mailboxActive = (bool)($u->is_active ?? true);
//// $effective = $domainActive && $mailboxActive;
////
//// $reason = null;
//// if (!$effective) {
//// $reason = !$domainActive ? 'Domain inaktiv'
//// : (!$mailboxActive ? 'Postfach inaktiv' : null);
//// }
////
//// $prepared[] = [
//// 'id' => $u->id,
//// 'localpart' => (string) $u->localpart,
//// 'quota_mb' => $quota,
//// 'used_mb' => $used,
//// 'usage_percent' => $usage,
//// 'message_count' => (int) ($u->message_count ?? $u->mails_count ?? 0),
//// 'is_active' => $mailboxActive,
//// 'is_effective_active' => $effective,
//// 'inactive_reason' => $reason,
//// ];
//// }
////
//// $d->prepared_mailboxes = $prepared;
//// }
////
//// return view('livewire.ui.mail.mailbox-list', [
//// 'domains' => $domains,
//// 'system' => $this->showSystemCard ? $system : null,
//// ]);
//// }
//
// // Vorbereitung für Blade (unverändert)
// foreach ($domains as $d) {
// $prepared = [];
// $domainActive = (bool)($d->is_active ?? true);
//
// foreach ($d->mailUsers as $u) {
// $quota = (int) ($u->quota_mb ?? 0);
// $used = (int) ($u->used_mb ?? 0);
// $usage = $quota > 0 ? min(100, (int) round($used / max(1,$quota) * 100)) : 0;
//
// $mailboxActive = (bool)($u->is_active ?? true);
// $effective = $domainActive && $mailboxActive;
//
// $reason = null;
// if (!$effective) {
// $reason = !$domainActive ? 'Domain inaktiv'
// : (!$mailboxActive ? 'Postfach inaktiv' : null);
// }
//
// $prepared[] = [
// 'id' => $u->id,
// 'localpart' => (string) $u->localpart,
// 'quota_mb' => $quota,
// 'used_mb' => $used,
// 'usage_percent' => $usage,
// 'message_count' => (int) ($u->message_count ?? $u->mails_count ?? 0),
// 'is_active' => $mailboxActive,
// 'is_effective_active' => $effective,
// 'inactive_reason' => $reason,
// ];
// }
//
// $d->prepared_mailboxes = $prepared;
// }
//
// return view('livewire.ui.mail.mailbox-list', [
// 'domains' => $domains,
// 'system' => $this->showSystemCard ? $system : null,
// ]);
// }
// public function render()
// {
// $system = Domain::query()->where('is_system', true)->first();
//
// $term = trim($this->search);
//
// $domains = Domain::query()
// ->when($system, fn ($q) => $q->where('id', '!=', $system->id))
// ->withCount(['mailUsers','mailAliases'])
// ->with([
// 'mailUsers' => fn ($q) => $q->orderBy('localpart'),
// 'mailAliases' => fn ($q) => $q->orderBy('source'),
// ])
// ->when($term !== '', function ($q) use ($term) {
// $q->where(function ($w) use ($term) {
// $w->where('domain', 'like', "%{$term}%")
// ->orWhereHas('mailUsers', fn($u) =>
// $u->where('localpart', 'like', "%{$term}%")
// );
// });
// })
// ->orderBy('domain')
// ->get();
//
// // Für das Blade vorbereiten (ohne Relations zu mutieren)
// foreach ($domains as $d) {
// $prepared = [];
// $domainActive = (bool)($d->is_active ?? true);
//
// foreach ($d->mailUsers as $u) {
// $quota = (int) ($u->quota_mb ?? 0);
// $used = (int) ($u->used_mb ?? 0);
// $usage = $quota > 0 ? min(100, (int) round($used / max(1,$quota) * 100)) : 0;
//
// $mailboxActive = (bool)($u->is_active ?? true);
// $effective = $domainActive && $mailboxActive;
//
// $reason = null;
// if (!$effective) {
// $reason = !$domainActive ? 'Domain inaktiv'
// : (!$mailboxActive ? 'Postfach inaktiv' : null);
// }
//
// $prepared[] = [
// 'id' => $u->id,
// 'localpart' => (string) $u->localpart,
// 'quota_mb' => $quota,
// 'used_mb' => $used,
// 'usage_percent' => $usage,
// 'message_count' => (int) ($u->message_count ?? $u->mails_count ?? 0),
// 'is_active' => $mailboxActive, // ursprünglicher Flag (falls du ihn brauchst)
// 'is_effective_active' => $effective, // ← NEU: Domain & Mailbox aktiv?
// 'inactive_reason' => $reason, // ← NEU: warum gesperrt
// ];
// }
//
// $d->prepared_mailboxes = $prepared;
// }
//
// return view('livewire.ui.mail.mailbox-list', [
// 'domains' => $domains,
// 'system' => $this->showSystemCard ? $system : null,
// ]);
// }
}
//// public function render()
//// {
//// $system = Domain::query()->where('is_system', true)->first();
////
//// $term = trim($this->search);
////
//// $domains = Domain::query()
//// ->when($system, fn ($q) => $q->where('id', '!=', $system->id))
//// ->withCount(['mailUsers','mailAliases'])
//// ->with([
//// 'mailUsers' => fn ($q) => $q->orderBy('localpart'),
//// 'mailAliases' => fn ($q) => $q->orderBy('source'),
//// ])
//// ->when($term !== '', function ($q) use ($term) {
//// $q->where(function ($w) use ($term) {
//// $w->where('domain', 'like', "%{$term}%")
//// ->orWhereHas('mailUsers', fn($u) =>
//// $u->where('localpart', 'like', "%{$term}%")
//// );
//// });
//// })
//// ->orderBy('domain')
//// ->get();
////
//// // Für das Blade vorbereiten (ohne Relations zu mutieren)
//// foreach ($domains as $d) {
//// $prepared = [];
//// $domainActive = (bool)($d->is_active ?? true);
////
//// foreach ($d->mailUsers as $u) {
//// $quota = (int) ($u->quota_mb ?? 0);
//// $used = (int) ($u->used_mb ?? 0);
//// $usage = $quota > 0 ? min(100, (int) round($used / max(1,$quota) * 100)) : 0;
////
//// $mailboxActive = (bool)($u->is_active ?? true);
//// $effective = $domainActive && $mailboxActive;
////
//// $reason = null;
//// if (!$effective) {
//// $reason = !$domainActive ? 'Domain inaktiv'
//// : (!$mailboxActive ? 'Postfach inaktiv' : null);
//// }
////
//// $prepared[] = [
//// 'id' => $u->id,
//// 'localpart' => (string) $u->localpart,
//// 'quota_mb' => $quota,
//// 'used_mb' => $used,
//// 'usage_percent' => $usage,
//// 'message_count' => (int) ($u->message_count ?? $u->mails_count ?? 0),
//// 'is_active' => $mailboxActive, // ursprünglicher Flag (falls du ihn brauchst)
//// 'is_effective_active' => $effective, // ← NEU: Domain & Mailbox aktiv?
//// 'inactive_reason' => $reason, // ← NEU: warum gesperrt
//// ];
//// }
////
//// $d->prepared_mailboxes = $prepared;
//// }
////
//// return view('livewire.ui.mail.mailbox-list', [
//// 'domains' => $domains,
//// 'system' => $this->showSystemCard ? $system : null,
//// ]);
//// }
//}