From 4645b168f770b7f2e42dea45bba2e75cb53cd536 Mon Sep 17 00:00:00 2001 From: boban Date: Tue, 28 Oct 2025 20:20:30 +0100 Subject: [PATCH] =?UTF-8?q?Fix:=20Mailbox=20Stats=20=C3=BCber=20Dovecot=20?= =?UTF-8?q?mit=20config/mailpool.php?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/StorageProbe.php | 372 +++++++++++++------- app/Console/Commands/UpdateMailboxStats.php | 152 ++++++-- 2 files changed, 373 insertions(+), 151 deletions(-) diff --git a/app/Console/Commands/StorageProbe.php b/app/Console/Commands/StorageProbe.php index 52decf2..5c68118 100644 --- a/app/Console/Commands/StorageProbe.php +++ b/app/Console/Commands/StorageProbe.php @@ -1,5 +1,6 @@ argument('target') ?: '/'; @@ -18,14 +24,17 @@ class StorageProbe extends Command Setting::set('health.disk', $data); Setting::set('health.disk_updated_at', now()->toIso8601String()); + // hübsche Konsole $hb = function (int $bytes): string { $b = max(0, $bytes); - if ($b >= 1024**3) return number_format($b / 1024**3, 1).' GB'; - if ($b >= 1024**2) return number_format($b / 1024**2, 2).' MiB'; - if ($b >= 1024) return number_format($b / 1024, 0).' KiB'; - return $b.' B'; + if ($b >= 1024 ** 3) return number_format($b / 1024 ** 3, 1) . ' GB'; + if ($b >= 1024 ** 2) return number_format($b / 1024 ** 2, 2) . ' MiB'; + if ($b >= 1024) return number_format($b / 1024, 0) . ' KiB'; + return $b . ' B'; }; + $bd = $data['breakdown_bytes'] ?? ['system' => 0, 'mails' => 0, 'backup' => 0]; + $this->info(sprintf( 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%s mails=%s backups=%s', $data['mount'], @@ -34,107 +43,19 @@ class StorageProbe extends Command $data['free_gb'], $data['free_plus_reserve_gb'], $data['percent_used_total'], - $hb((int)$data['breakdown_bytes']['system']), - $hb((int)$data['breakdown_bytes']['mails']), - $hb((int)$data['breakdown_bytes']['backup']), + $hb((int)$bd['system']), $hb((int)$bd['mails']), $hb((int)$bd['backup']) )); -// $this->info(sprintf( -// 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%.1fGB mails=%.1fGB backups=%.1fGB', -// $data['mount'], -// $data['total_gb'], -// $data['used_gb'], -// $data['free_gb'], -// $data['free_plus_reserve_gb'], -// $data['percent_used_total'], -// $data['breakdown_bytes']['system'], -// $data['breakdown_bytes']['mails'], -// $data['breakdown_bytes']['backup'], -// )); - return self::SUCCESS; } - private function detectMailRoot(): ?string - { - // Dovecot - $ml = trim((string) @shell_exec('doveconf -n 2>/dev/null | awk -F= \'/^mail_location/ {print $2}\'')); - if ($ml !== '') { - if (preg_match('~^(?:maildir|mdbox|sdbox):([^%]+)~i', $ml, $m)) { - $root = rtrim($m[1]); - foreach (['/Maildir', '/mdbox', '/sdbox'] as $suffix) { - if (str_ends_with($root, $suffix)) { $root = dirname($root); break; } - } - if (is_dir($root)) return $root; - } - } - // Postfix - $vmb = trim((string) @shell_exec('postconf -n 2>/dev/null | awk -F= \'/^virtual_mailbox_base/ {print $2}\'')); - if ($vmb !== '' && is_dir($vmb)) return $vmb; - - // Fallbacks - foreach (['/var/vmail', '/var/mail/vhosts', '/srv/mail/vhosts', '/home/vmail'] as $cand) { - if (is_dir($cand)) return $cand; - } - return null; - } - - private function duBytesDir(string $path): int - { - if (!is_dir($path)) return 0; - $out = @shell_exec('LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1'); - return max(0, (int) trim((string) $out)); - } - - // … oberhalb deiner probe()-Methode einfügen: -// private function detectMailRoot(): ?string -// { -// // 1) Dovecot: mail_location = maildir:/var/vmail/%d/%n/Maildir -// $ml = trim((string) @shell_exec('doveconf -n 2>/dev/null | awk -F= \'/^mail_location/ {print $2}\'')); -// if ($ml !== '') { -// $ml = trim($ml); -// // maildir:/path/to/root/%d/%n/Maildir oder mdbox:/path/…/mdbox -// if (preg_match('~^(?:maildir|mdbox|sdbox):([^%]+)~i', $ml, $m)) { -// $root = rtrim($m[1]); -// // häufig endet es auf /Maildir oder /mdbox → ein Level hoch als Root nehmen -// foreach (['/Maildir', '/mdbox', '/sdbox'] as $suffix) { -// if (str_ends_with($root, $suffix)) { -// $root = dirname($root); -// break; -// } -// } -// if (is_dir($root)) return $root; -// } -// } -// -// // 2) Postfix: virtual_mailbox_base = /var/vmail -// $vmb = trim((string) @shell_exec('postconf -n 2>/dev/null | awk -F= \'/^virtual_mailbox_base/ {print $2}\'')); -// $vmb = $vmb !== '' ? trim($vmb) : ''; -// if ($vmb !== '' && is_dir($vmb)) return $vmb; -// -// // 3) Fallbacks – nimm den ersten existierenden -// foreach (['/var/vmail', '/var/mail/vhosts', '/srv/mail/vhosts', '/home/vmail'] as $cand) { -// if (is_dir($cand)) return $cand; -// } -// return null; -// } -// -// private function duBytesDir(string $path): int -// { -// if (!is_dir($path)) return 0; -// // apparent-size zählt logisch (kleine Dateien werden nicht „weggerundet“) -// $out = @shell_exec('LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1'); -// return max(0, (int) trim((string) $out)); -// } - protected function probe(string $target): array { - // --- df: Gesamtdaten des Filesystems (inkl. Reserve) ----------------- + // ── 1) df lesen (Gesamt/Frei inkl. Reserve) ────────────────────────── $line = trim((string)@shell_exec('LC_ALL=C df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1')); $device = $mount = ''; $totalKb = $usedKb = $availKb = 0; - if ($line !== '') { $p = preg_split('/\s+/', $line); if (count($p) >= 6) { @@ -146,56 +67,265 @@ class StorageProbe extends Command } } - $toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // ganzzahlig (UI: Gesamt/Genutzt/Frei) - $toGiB_f = static fn($kb) => round(max(0, (int)$kb) / (1024 * 1024), 1); // eine Nachkommastelle (Breakdown/Legende) - + $toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // Ganzzahl für Kopfzahlen $totalGb = $toGiB_i($totalKb); - $freeGb = $toGiB_i($availKb); // user-verfügbar - $usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve - $res5Gb = (int)round($totalGb * 0.05); // 5% von Gesamt + $freeGb = $toGiB_i($availKb); + $usedGb = max(0, $totalGb - $freeGb); + $res5Gb = (int)round($totalGb * 0.05); $freePlusReserveGb = min($totalGb, $freeGb + $res5Gb); $percentUsed = $totalGb > 0 ? (int)round($usedGb * 100 / $totalGb) : 0; - $duBytes = function (string $path): int { - if (!is_dir($path)) return 0; - $b = (int) trim((string) @shell_exec( - 'LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1' - )); - return max(0, $b); - }; + // Bytes für Breakdown rechnen + $totalBytes = (int)$totalKb * 1024; + $freeBytes = (int)$availKb * 1024; + $usedBytes = max(0, $totalBytes - $freeBytes); - $mailRoot = $this->detectMailRoot(); - $bytesMails = $mailRoot ? $this->duBytesDir($mailRoot) : 0; - $bytesBackup = $duBytes('/var/backups/mailwolt'); + // ── 2) Mails: bevorzugt aus Cache (mail:update-stats) ──────────────── + [$mailUsersBytes, $mailSystemBytes] = $this->readMailTotals(); - $totalBytes = (int) $totalKb * 1024; - $freeBytes = (int) $availKb * 1024; - $usedBytes = max(0, $totalBytes - $freeBytes); + // ── 3) Backups schnell messen ──────────────────────────────────────── + $bytesBackup = $this->duBytesDir('/var/backups/mailwolt'); + // ── 4) Breakdown auflösen und konsistent machen ───────────────────── + // alles, was nicht Mails/Backups ist, dem System zuordnen + $bytesMails = max(0, (int)$mailUsersBytes); // nur user_mail in "Mails" $bytesSystem = max(0, $usedBytes - ($bytesMails + $bytesBackup)); + // Wenn Vorberechnung system_mail existiert, zähle sie explizit zum System. + $bytesSystem += max(0, (int)$mailSystemBytes); + + // negativ verhindern (Messrauschen) + if ($bytesSystem < 0) { + $bytesSystem = 0; + } + return [ 'device' => $device ?: 'unknown', 'mount' => $mount ?: $target, 'total_gb' => $totalGb, - 'used_gb' => $usedGb, // inkl. Reserve - 'free_gb' => $freeGb, // User-sicht - 'reserve5_gb' => $res5Gb, // Info - 'free_plus_reserve_gb' => $freePlusReserveGb, // Anzeige „Frei“ + 'used_gb' => $usedGb, + 'free_gb' => $freeGb, + 'reserve5_gb' => $res5Gb, + 'free_plus_reserve_gb' => $freePlusReserveGb, 'percent_used_total' => $percentUsed, - // Reale Breakdown-Werte 'breakdown_bytes' => [ - 'system' => $bytesSystem, - 'mails' => $bytesMails, - 'backup' => $bytesBackup, + 'system' => $bytesSystem, + 'mails' => $bytesMails, + 'backup' => $bytesBackup, ], ]; } + + /** + * Liest vorberechnete Mail-Summen aus settings: mail.totals. + * Fallback: wenn nicht vorhanden/zu alt, ermittelt Mails per du (nur dann). + * + * @return array{0:int,1:int} [users_bytes, system_bytes] + */ + private function readMailTotals(): array + { + $totals = (array)(Setting::get(self::MAILBOX_TOTALS_KEY, []) ?: []); + $ts = isset($totals['updated_at']) ? strtotime((string)$totals['updated_at']) : null; + $fresh = $ts && (time() - $ts) <= self::MAILBOX_TOTALS_STALE; + + if ($fresh) { + $users = (int)($totals['users_bytes'] ?? 0); + $system = (int)($totals['system_bytes'] ?? 0); + return [max(0, $users), max(0, $system)]; + } + + // Fallback EINMAL: grob mails via detectMailRoot() messen + $root = $this->detectMailRoot(); + $usersBytes = $root ? $this->duBytesDir($root) : 0; + + return [max(0, $usersBytes), 0]; + } + + /** + * Versucht, das Wurzelverzeichnis der Maildaten zu finden (Dovecot/Postfix), + * ohne auf konkrete Setups festgenagelt zu sein. + */ + private function detectMailRoot(): ?string + { + // Dovecot bevorzugt + $ml = trim((string)@shell_exec('doveconf -n 2>/dev/null | awk -F= \'/^mail_location/ {print $2}\'')); + if ($ml !== '') { + if (preg_match('~^(?:maildir|mdbox|sdbox):([^%]+)~i', $ml, $m)) { + $root = rtrim($m[1]); + foreach (['/Maildir', '/mdbox', '/sdbox'] as $suffix) { + if (str_ends_with($root, $suffix)) { + $root = dirname($root); + break; + } + } + if (is_dir($root)) return $root; + } + } + + // Postfix-Konfiguration + $vmb = trim((string)@shell_exec('postconf -n 2>/dev/null | awk -F= \'/^virtual_mailbox_base/ {print $2}\'')); + if ($vmb !== '' && is_dir($vmb)) return $vmb; + + // Fallbacks + foreach (['/var/vmail', '/var/mail/vhosts', '/srv/mail/vhosts', '/home/vmail'] as $cand) { + if (is_dir($cand)) return $cand; + } + return null; + } + + /** Summe in Bytes für ein Verzeichnisbaum; robust & schnell genug. */ + private function duBytesDir(string $path): int + { + if (!is_dir($path)) return 0; + $out = @shell_exec('LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1'); + return max(0, (int)trim((string)$out)); + } } +//namespace App\Console\Commands; +// +//use Illuminate\Console\Command; +//use App\Models\Setting; +// +//class StorageProbe extends Command +//{ +// protected $signature = 'health:probe-disk {target=/}'; +// protected $description = 'Speichert Storage-Werte (inkl. Breakdown) in settings:health.disk'; +// +// public function handle(): int +// { +// $target = $this->argument('target') ?: '/'; +// $data = $this->probe($target); +// +// Setting::set('health.disk', $data); +// Setting::set('health.disk_updated_at', now()->toIso8601String()); +// +// $hb = function (int $bytes): string { +// $b = max(0, $bytes); +// if ($b >= 1024**3) return number_format($b / 1024**3, 1).' GB'; +// if ($b >= 1024**2) return number_format($b / 1024**2, 2).' MiB'; +// if ($b >= 1024) return number_format($b / 1024, 0).' KiB'; +// return $b.' B'; +// }; +// +// $this->info(sprintf( +// 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%s mails=%s backups=%s', +// $data['mount'], +// $data['total_gb'], +// $data['used_gb'], +// $data['free_gb'], +// $data['free_plus_reserve_gb'], +// $data['percent_used_total'], +// $hb((int)$data['breakdown_bytes']['system']), +// $hb((int)$data['breakdown_bytes']['mails']), +// $hb((int)$data['breakdown_bytes']['backup']), +// )); +// return self::SUCCESS; +// } +// +// private function detectMailRoot(): ?string +// { +// // Dovecot +// $ml = trim((string) @shell_exec('doveconf -n 2>/dev/null | awk -F= \'/^mail_location/ {print $2}\'')); +// if ($ml !== '') { +// if (preg_match('~^(?:maildir|mdbox|sdbox):([^%]+)~i', $ml, $m)) { +// $root = rtrim($m[1]); +// foreach (['/Maildir', '/mdbox', '/sdbox'] as $suffix) { +// if (str_ends_with($root, $suffix)) { $root = dirname($root); break; } +// } +// if (is_dir($root)) return $root; +// } +// } +// // Postfix +// $vmb = trim((string) @shell_exec('postconf -n 2>/dev/null | awk -F= \'/^virtual_mailbox_base/ {print $2}\'')); +// if ($vmb !== '' && is_dir($vmb)) return $vmb; +// +// // Fallbacks +// foreach (['/var/vmail', '/var/mail/vhosts', '/srv/mail/vhosts', '/home/vmail'] as $cand) { +// if (is_dir($cand)) return $cand; +// } +// return null; +// } +// +// private function duBytesDir(string $path): int +// { +// if (!is_dir($path)) return 0; +// $out = @shell_exec('LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1'); +// return max(0, (int) trim((string) $out)); +// } +// +// protected function probe(string $target): array +// { +// // --- df: Gesamtdaten des Filesystems (inkl. Reserve) ----------------- +// $line = trim((string)@shell_exec('LC_ALL=C df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1')); +// +// $device = $mount = ''; +// $totalKb = $usedKb = $availKb = 0; +// +// if ($line !== '') { +// $p = preg_split('/\s+/', $line); +// if (count($p) >= 6) { +// $device = $p[0]; +// $totalKb = (int)$p[1]; // TOTAL (inkl. Reserve) +// $usedKb = (int)$p[2]; // Used +// $availKb = (int)$p[3]; // Avail (User-sicht) +// $mount = $p[5]; +// } +// } +// +// $toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // ganzzahlig (UI: Gesamt/Genutzt/Frei) +// $toGiB_f = static fn($kb) => round(max(0, (int)$kb) / (1024 * 1024), 1); // eine Nachkommastelle (Breakdown/Legende) +// +// $totalGb = $toGiB_i($totalKb); +// $freeGb = $toGiB_i($availKb); // user-verfügbar +// $usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve +// $res5Gb = (int)round($totalGb * 0.05); // 5% von Gesamt +// $freePlusReserveGb = min($totalGb, $freeGb + $res5Gb); +// $percentUsed = $totalGb > 0 ? (int)round($usedGb * 100 / $totalGb) : 0; +// +// $duBytes = function (string $path): int { +// if (!is_dir($path)) return 0; +// $b = (int) trim((string) @shell_exec( +// 'LC_ALL=C du -sb --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1' +// )); +// return max(0, $b); +// }; +// +// $mailRoot = $this->detectMailRoot(); +// $bytesMails = $mailRoot ? $this->duBytesDir($mailRoot) : 0; +// $bytesBackup = $duBytes('/var/backups/mailwolt'); +// +// $totalBytes = (int) $totalKb * 1024; +// $freeBytes = (int) $availKb * 1024; +// $usedBytes = max(0, $totalBytes - $freeBytes); +// +// $bytesSystem = max(0, $usedBytes - ($bytesMails + $bytesBackup)); +// +// return [ +// 'device' => $device ?: 'unknown', +// 'mount' => $mount ?: $target, +// +// 'total_gb' => $totalGb, +// 'used_gb' => $usedGb, // inkl. Reserve +// 'free_gb' => $freeGb, // User-sicht +// 'reserve5_gb' => $res5Gb, // Info +// 'free_plus_reserve_gb' => $freePlusReserveGb, // Anzeige „Frei“ +// +// 'percent_used_total' => $percentUsed, +// +// // Reale Breakdown-Werte +// 'breakdown_bytes' => [ +// 'system' => $bytesSystem, +// 'mails' => $bytesMails, +// 'backup' => $bytesBackup, +// ], +// ]; +// } +//} + // diff --git a/app/Console/Commands/UpdateMailboxStats.php b/app/Console/Commands/UpdateMailboxStats.php index 048b261..60a90bb 100644 --- a/app/Console/Commands/UpdateMailboxStats.php +++ b/app/Console/Commands/UpdateMailboxStats.php @@ -15,39 +15,119 @@ class UpdateMailboxStats extends Command protected $signature = 'mail:update-stats {--user=}'; 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; +// $t0 = microtime(true); +// +// // 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) +// ->whereHas('domain', fn($d) => $d->where('is_system', false)); +// +// if ($onlyUser) { +// $base->where('email', $onlyUser); +// } +// +// $checked = 0; +// $changed = 0; +// +// $log->info('mail:update-stats START', ['only' => $onlyUser]); +// +// $base->orderBy('id')->chunkById(200, function ($users) use (&$checked, &$changed, $log) { +// foreach ($users as $u) { +// $checked++; +// +// // Email robust bestimmen (raw -> accessor -> zusammengesetzt) +// $raw = (string)($u->getRawOriginal('email') ?? ''); +// $email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null); +// +// if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { +// // still – kein Log-Spam +// continue; +// } +// +// [$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']]); +// } +// } +// }); +// +// $ms = (int)((microtime(true) - $t0) * 1000); +// $log->info('mail:update-stats DONE', compact('checked', 'changed', 'ms')); +// $this->info('Mailbox-Statistiken aktualisiert.'); +// return self::SUCCESS; +// } + public function handle(): int { $log = Log::channel('mailstats'); $onlyUser = trim((string)$this->option('user')) ?: null; $t0 = microtime(true); - // Basis-Query: nur aktive, keine System-Mailboxen und keine System-Domains + // Summen + $sumUserBytes = 0; + $sumSystemBytes = 0; + + // aktiver Benutzerbestand (inkl. Domains, um system/non-system zu unterscheiden) $base = MailUser::query() - ->select(['id', 'domain_id', 'localpart', 'email', 'is_active', 'is_system']) + ->select(['id','domain_id','localpart','email','is_active','is_system']) ->with(['domain:id,domain,is_system']) - ->where('is_active', true) - ->where('is_system', false) - ->whereHas('domain', fn($d) => $d->where('is_system', false)); + ->where('is_active', true); if ($onlyUser) { $base->where('email', $onlyUser); } - $checked = 0; - $changed = 0; - + $checked = 0; $changed = 0; $log->info('mail:update-stats START', ['only' => $onlyUser]); - $base->orderBy('id')->chunkById(200, function ($users) use (&$checked, &$changed, $log) { + $base->orderBy('id')->chunkById(200, function ($users) use (&$checked,&$changed,&$sumUserBytes,&$sumSystemBytes,$log) { foreach ($users as $u) { $checked++; - // Email robust bestimmen (raw -> accessor -> zusammengesetzt) $raw = (string)($u->getRawOriginal('email') ?? ''); $email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null); - if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { - // still – kein Log-Spam continue; } @@ -57,47 +137,59 @@ class UpdateMailboxStats extends Command // Größe in Bytes (rekursiv) $usedBytes = 0; if (is_dir($maildir)) { - $it = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS) + $it = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS) ); - foreach ($it as $f) { - if ($f->isFile()) $usedBytes += $f->getSize(); - } + foreach ($it as $f) if ($f->isFile()) $usedBytes += $f->getSize(); + } + + $isSystemDomain = (bool)($u->domain->is_system ?? false); + + if ($isSystemDomain || $u->is_system) { + // systemische Mailboxen fließen nur in die System-Summe ein + $sumSystemBytes += $usedBytes; + continue; // KEIN per-user Setting } // Message-Count $messageCount = $this->countViaDoveadm($email); - if ($messageCount === null) { - $messageCount = $this->countViaFilesystem($maildir); - } + if ($messageCount === null) $messageCount = $this->countViaFilesystem($maildir); - $key = "mailbox.{$email}"; + $sumUserBytes += $usedBytes; + + $key = "mailbox.{$email}"; $prev = (array)(Setting::get($key, []) ?: []); - $new = [ - 'used_bytes' => (int)$usedBytes, + $new = [ + 'used_bytes' => (int)$usedBytes, 'message_count' => (int)$messageCount, - 'updated_at' => now()->toDateTimeString(), + '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", + $this->line(sprintf("%-35s %7.2f MiB %5d msgs", $email, $usedBytes / 1048576, $messageCount)); - $log->info('updated', ['email' => $email, 'used_bytes' => $new['used_bytes'], 'message_count' => $new['message_count']]); + $log->info('updated', ['email'=>$email]+$new); } } }); - $ms = (int)((microtime(true) - $t0) * 1000); - $log->info('mail:update-stats DONE', compact('checked', 'changed', 'ms')); + // Totals persistieren (Nutzen wir später im StorageProbe) + Setting::set('mailbox.totals', [ + 'users_bytes' => (int)$sumUserBytes, // alle nicht-systemischen Mailboxen + 'system_bytes' => (int)$sumSystemBytes, // systemische Mailboxen + 'updated_at' => now()->toIso8601String(), + ]); + + $ms = (int)((microtime(true)-$t0)*1000); + $log->info('mail:update-stats DONE', compact('checked','changed','ms','sumUserBytes','sumSystemBytes')); $this->info('Mailbox-Statistiken aktualisiert.'); return self::SUCCESS; } + private function countViaDoveadm(string $email): ?int { $cmd = "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u "