From 95ab70e2510e6125b2a56257519c95f180019195 Mon Sep 17 00:00:00 2001 From: boban Date: Tue, 21 Oct 2025 19:53:09 +0200 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/UpdateMailboxStats.php | 281 ++++++++++++++------ config/logging.php | 2 +- 2 files changed, 205 insertions(+), 78 deletions(-) diff --git a/app/Console/Commands/UpdateMailboxStats.php b/app/Console/Commands/UpdateMailboxStats.php index 63e9998..d68751e 100644 --- a/app/Console/Commands/UpdateMailboxStats.php +++ b/app/Console/Commands/UpdateMailboxStats.php @@ -1,5 +1,6 @@ info('=== mail:update-stats gestartet', [ - 'user_opt' => $this->option('user'), - 'php_user' => get_current_user(), - 'uid' => getmyuid(), + Log::channel('mailstats')->info('=== mail:update-stats START ===', [ + 'userOpt' => $this->option('user') ]); $q = MailUser::query() ->where('is_active', true) - ->with(['domain:id,domain']); + ->whereNotNull('email') + ->where('email', 'like', '%@%'); - if ($one = trim((string)$this->option('user'))) { - if (str_contains($one, '@')) { - [$local, $dom] = explode('@', $one, 2); - $q->where('localpart', $local) - ->whereHas('domain', fn($d) => $d->where('domain', $dom)); - } else { - $q->where('localpart', $one); - } + if ($email = trim((string)$this->option('user'))) { + $q->where('email', $email); } $users = $q->get(); - $log->info('Gefundene Mailboxen', ['count' => $users->count(), 'ids' => $users->pluck('id')]); if ($users->isEmpty()) { + Log::channel('mailstats')->warning('Keine passenden Mailboxen gefunden.'); $this->warn('Keine passenden Mailboxen gefunden.'); - $log->warning('Abbruch: keine Mailboxen gefunden'); return self::SUCCESS; } foreach ($users as $u) { - $local = trim((string)$u->localpart); - $domain = optional($u->domain)->domain; - $email = trim((string)($u->email ?? '')); + $email = trim((string)$u->email); - if ($email === '' && $local !== '' && $domain) { - $email = "{$local}@{$domain}"; - } - - if ($email === '' || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { - $log->warning('Überspringe: ungültige Adresse', ['id' => $u->id, 'local' => $local, 'domain' => $domain, 'email' => $u->email]); + if (!preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { + Log::channel('mailstats')->warning('Überspringe ungültige Adresse', ['email' => $email]); continue; } + [$local, $domain] = explode('@', $email, 2); $maildir = "/var/mail/vhosts/{$domain}/{$local}"; - $log->info('Verarbeite Mailbox', ['id' => $u->id, 'email' => $email, 'maildir' => $maildir]); - // 1) Größe berechnen + // 1) Größe berechnen (Dateisystem) $usedBytes = 0; if (is_dir($maildir)) { $it = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS) ); - foreach ($it as $file) { - if ($file->isFile()) { - $usedBytes += $file->getSize(); - } + foreach ($it as $f) { + if ($f->isFile()) $usedBytes += $f->getSize(); } - } else { - $log->warning('Maildir nicht gefunden', ['path' => $maildir]); } - // 2) Nachrichtenzahl - $messageCount = $this->countViaDoveadm($email, $log); + // 2) Nachrichten zählen – bevorzugt via doveadm als vmail + $messageCount = $this->countViaDoveadm($email); if ($messageCount === null) { - $messageCount = $this->countViaFilesystem($maildir, $log); + $messageCount = $this->countViaFilesystem($maildir); } - - // Cache (Settings/Redis) + + // optional: in Settings/Redis cachen (für UI) Setting::set("mailbox.{$email}", [ - 'used_bytes' => (int)$usedBytes, + 'used_bytes' => (int)$usedBytes, 'message_count' => (int)$messageCount, - 'ts' => now()->toISOString(), + 'updated_at' => now()->toDateTimeString(), ]); - $this->line(sprintf("%-40s %7.1f MiB %5d msgs", $email, $usedBytes / 1024 / 1024, $messageCount)); - $log->info('Aktualisiert', [ - 'email' => $email, - 'used_bytes' => $usedBytes, + $line = sprintf("%-35s %7.1f MiB %5d msgs", $email, $usedBytes / 1024 / 1024, $messageCount); + $this->line($line); + Log::channel('mailstats')->info('OK', [ + 'email' => $email, + 'maildir' => $maildir, + 'used_bytes' => $usedBytes, 'message_count' => $messageCount, ]); } - $log->info('=== mail:update-stats fertig ==='); + Log::channel('mailstats')->info('=== mail:update-stats DONE ==='); $this->info('Mailbox-Statistiken aktualisiert.'); return self::SUCCESS; } - protected function countViaDoveadm(string $email, $log): ?int + protected function countViaDoveadm(string $email): ?int { - $cmd = ['sudo', '-n', '-u', 'vmail', '/usr/bin/doveadm', '-f', 'tab', 'mailbox', 'status', '-u', $email, 'messages', 'INBOX']; - $p = new Process($cmd, null, ['LANG' => 'C']); - $p->setTimeout(10); + $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); - try { - $log->debug('Starte doveadm', ['cmd' => implode(' ', $cmd)]); - $p->run(); - $rc = $p->getExitCode(); - $out = trim($p->getOutput()); - $err = trim($p->getErrorOutput()); - $log->debug('doveadm Ergebnis', ['rc' => $rc, 'out' => $out, 'err' => $err]); + Log::channel('mailstats')->debug('doveadm exec', ['cmd' => $cmd, 'rc' => $rc, 'out' => $out]); - if ($rc !== 0) return null; + if ($rc !== 0) return null; - $lines = preg_split('/\R+/', $out); - $last = end($lines); - if (preg_match('/\t(\d+)$/', (string)$last, $m)) { + // Erwartet: "mailbox\tmessages\nINBOX\t" + foreach ($out as $line) { + if (preg_match('/^\s*INBOX\s+(\d+)\s*$/i', trim($line), $m)) { return (int)$m[1]; } - return null; - } catch (\Throwable $e) { - $log->error('doveadm Exception', ['msg' => $e->getMessage()]); - return null; } + return null; } - protected function countViaFilesystem(string $maildir, $log): int + protected function countViaFilesystem(string $maildir): int { - $count = 0; + $n = 0; foreach (['cur', 'new'] as $sub) { $dir = "{$maildir}/{$sub}"; - if (is_dir($dir) && ($dh = @opendir($dir))) { - while (false !== ($fn = readdir($dh))) { - if ($fn !== '.' && $fn !== '..' && $fn[0] !== '.') { - $count++; - } - } - closedir($dh); - } else { - $log->debug('Ordner fehlt/leer', ['path' => $dir]); + if (!is_dir($dir)) continue; + $dh = opendir($dir); + if (!$dh) continue; + while (($fn = readdir($dh)) !== false) { + if ($fn === '.' || $fn === '..' || $fn[0] === '.') continue; + $n++; } + closedir($dh); } - return $count; + Log::channel('mailstats')->debug('fs count', ['maildir' => $maildir, 'count' => $n]); + return $n; } } +//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; +//use Symfony\Component\Process\Process; +// +//class UpdateMailboxStats extends Command +//{ +// protected $signature = 'mail:update-stats {--user=}'; +// protected $description = 'Aktualisiert Mailquota und Nachrichtenzahl für alle Mailboxen (oder einen Benutzer)'; +// +// public function handle(): int +// { +// $log = Log::channel('mailstats'); +// $log->info('=== mail:update-stats gestartet', [ +// 'user_opt' => $this->option('user'), +// 'php_user' => get_current_user(), +// 'uid' => getmyuid(), +// ]); +// +// $q = MailUser::query() +// ->where('is_active', true) +// ->with(['domain:id,domain']); +// +// if ($one = trim((string)$this->option('user'))) { +// if (str_contains($one, '@')) { +// [$local, $dom] = explode('@', $one, 2); +// $q->where('localpart', $local) +// ->whereHas('domain', fn($d) => $d->where('domain', $dom)); +// } else { +// $q->where('localpart', $one); +// } +// } +// +// $users = $q->get(); +// $log->info('Gefundene Mailboxen', ['count' => $users->count(), 'ids' => $users->pluck('id')]); +// +// if ($users->isEmpty()) { +// $this->warn('Keine passenden Mailboxen gefunden.'); +// $log->warning('Abbruch: keine Mailboxen gefunden'); +// return self::SUCCESS; +// } +// +// foreach ($users as $u) { +// $local = trim((string)$u->localpart); +// $domain = optional($u->domain)->domain; +// $email = trim((string)($u->email ?? '')); +// +// if ($email === '' && $local !== '' && $domain) { +// $email = "{$local}@{$domain}"; +// } +// +// if ($email === '' || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { +// $log->warning('Überspringe: ungültige Adresse', ['id' => $u->id, 'local' => $local, 'domain' => $domain, 'email' => $u->email]); +// continue; +// } +// +// $maildir = "/var/mail/vhosts/{$domain}/{$local}"; +// $log->info('Verarbeite Mailbox', ['id' => $u->id, 'email' => $email, 'maildir' => $maildir]); +// +// // 1) Größe berechnen +// $usedBytes = 0; +// if (is_dir($maildir)) { +// $it = new RecursiveIteratorIterator( +// new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS) +// ); +// foreach ($it as $file) { +// if ($file->isFile()) { +// $usedBytes += $file->getSize(); +// } +// } +// } else { +// $log->warning('Maildir nicht gefunden', ['path' => $maildir]); +// } +// +// // 2) Nachrichtenzahl +// $messageCount = $this->countViaDoveadm($email, $log); +// if ($messageCount === null) { +// $messageCount = $this->countViaFilesystem($maildir, $log); +// } +// +// // Cache (Settings/Redis) +// Setting::set("mailbox.{$email}", [ +// 'used_bytes' => (int)$usedBytes, +// 'message_count' => (int)$messageCount, +// 'ts' => now()->toISOString(), +// ]); +// +// $this->line(sprintf("%-40s %7.1f MiB %5d msgs", $email, $usedBytes / 1024 / 1024, $messageCount)); +// $log->info('Aktualisiert', [ +// 'email' => $email, +// 'used_bytes' => $usedBytes, +// 'message_count' => $messageCount, +// ]); +// } +// +// $log->info('=== mail:update-stats fertig ==='); +// $this->info('Mailbox-Statistiken aktualisiert.'); +// return self::SUCCESS; +// } +// +// protected function countViaDoveadm(string $email, $log): ?int +// { +// $cmd = ['sudo', '-n', '-u', 'vmail', '/usr/bin/doveadm', '-f', 'tab', 'mailbox', 'status', '-u', $email, 'messages', 'INBOX']; +// $p = new Process($cmd, null, ['LANG' => 'C']); +// $p->setTimeout(10); +// +// try { +// $log->debug('Starte doveadm', ['cmd' => implode(' ', $cmd)]); +// $p->run(); +// $rc = $p->getExitCode(); +// $out = trim($p->getOutput()); +// $err = trim($p->getErrorOutput()); +// $log->debug('doveadm Ergebnis', ['rc' => $rc, 'out' => $out, 'err' => $err]); +// +// if ($rc !== 0) return null; +// +// $lines = preg_split('/\R+/', $out); +// $last = end($lines); +// if (preg_match('/\t(\d+)$/', (string)$last, $m)) { +// return (int)$m[1]; +// } +// return null; +// } catch (\Throwable $e) { +// $log->error('doveadm Exception', ['msg' => $e->getMessage()]); +// return null; +// } +// } +// +// protected function countViaFilesystem(string $maildir, $log): int +// { +// $count = 0; +// foreach (['cur', 'new'] as $sub) { +// $dir = "{$maildir}/{$sub}"; +// if (is_dir($dir) && ($dh = @opendir($dir))) { +// while (false !== ($fn = readdir($dh))) { +// if ($fn !== '.' && $fn !== '..' && $fn[0] !== '.') { +// $count++; +// } +// } +// closedir($dh); +// } else { +// $log->debug('Ordner fehlt/leer', ['path' => $dir]); +// } +// } +// return $count; +// } +//} + // //namespace App\Console\Commands; // diff --git a/config/logging.php b/config/logging.php index 601d9c0..d7a85c5 100644 --- a/config/logging.php +++ b/config/logging.php @@ -57,7 +57,7 @@ return [ 'level' => 'debug', 'days' => 14, ], - + 'stack' => [ 'driver' => 'stack', 'channels' => explode(',', (string) env('LOG_STACK', 'single')),