From 878dae8876812ded4ac72ba5e9a98731707a803e Mon Sep 17 00:00:00 2001 From: boban Date: Tue, 21 Oct 2025 02:19:40 +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 | 166 ++++++++------------ 1 file changed, 62 insertions(+), 104 deletions(-) diff --git a/app/Console/Commands/UpdateMailboxStats.php b/app/Console/Commands/UpdateMailboxStats.php index 5fa2685..efb3a9d 100644 --- a/app/Console/Commands/UpdateMailboxStats.php +++ b/app/Console/Commands/UpdateMailboxStats.php @@ -4,131 +4,89 @@ namespace App\Console\Commands; use App\Models\MailUser; use Illuminate\Console\Command; -use Symfony\Component\Process\Process; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; class UpdateMailboxStats extends Command { protected $signature = 'mail:update-stats {--user=}'; - protected $description = 'Aktualisiert Mailquota und Nachrichtenzahl für alle Mailboxen (oder einen spezifischen Benutzer)'; + protected $description = 'Aktualisiert Mailquota und Nachrichtenzahl für alle Mailboxen (oder einen Benutzer)'; public function handle(): int { - $query = MailUser::query()->where('is_active', true); - if ($email = $this->option('user')) { - $query->where('email', $email); + $q = MailUser::query() + ->where('is_active', true) + // sichere Filter: E-Mail vorhanden und enthält genau ein "@" + ->whereNotNull('email') + ->where('email', 'like', '%@%'); + + if ($email = trim((string)$this->option('user'))) { + $q->where('email', $email); } - $users = $query->get(); - $rows = []; + $users = $q->get(); + if ($users->isEmpty()) { + $this->warn('Keine passenden Mailboxen gefunden.'); + return self::SUCCESS; + } foreach ($users as $u) { - $email = (string)$u->email; - - // E-Mail validieren - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - $this->warn("Überspringe ungültige Adresse: '{$email}'"); + $email = trim($u->email); + if (!preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { + // still und leise überspringen – kein „Überspringe @“ mehr continue; } [$local, $domain] = explode('@', $email, 2); - $base = "/var/mail/vhosts/{$domain}/{$local}"; + $maildir = "/var/mail/vhosts/{$domain}/{$local}"; - // ---- Größe (Bytes) via `du -sb`, mit Fallback ---- + // 1) Größe in Bytes (rekursiv; ohne "du") $usedBytes = 0; - if (is_dir($base)) { - $usedBytes = $this->dirSizeBytes($base); - } - - // ---- Nachrichten zählen: erst doveadm, sonst Maildir ---- - $messageCount = $this->countMessages($email, $base); - - $u->update([ - 'used_bytes' => $usedBytes, - 'message_count' => $messageCount, - 'stats_refreshed_at'=> now(), - ]); - - $rows[] = sprintf( - "%-35s %6.1f MiB %4d msgs", - $email, - $usedBytes / 1024 / 1024, - $messageCount - ); - } - - foreach ($rows as $line) { - $this->info($line); - } - - return self::SUCCESS; - } - - private function dirSizeBytes(string $path): int - { - // du -sb (schnell & korrekt, braucht nur r-x für Verzeichnisse) - $p = Process::fromShellCommandline('du -sb '.escapeshellarg($path).' | cut -f1'); - $p->run(); - if ($p->isSuccessful()) { - $val = trim($p->getOutput()); - if ($val !== '' && ctype_digit($val)) { - return (int)$val; - } - } - // Fallback (langsamer, aber funktioniert ohne du) - $size = 0; - $it = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS) - ); - foreach ($it as $f) { - if ($f->isFile()) { - $size += $f->getSize(); - } - } - return $size; - } - - private function countMessages(string $email, string $base): int - { - // 1) Bevorzugt: doveadm - $cmd = sprintf( - 'doveadm -S /run/dovecot/doveadm-server mailbox status -u %s messages INBOX', - escapeshellarg($email) - ); - $p = Process::fromShellCommandline($cmd); - $p->run(); - - if ($p->isSuccessful()) { - $out = trim($p->getOutput()); - if (preg_match('/messages=(\d+)/', $out, $m)) { - return (int)$m[1]; - } - } - - // 2) Fallback: Maildir zählen (INBOX: root/cur + root/new) - $count = 0; - - $paths = [ - // Dovecot: INBOX ist i.d.R. das Root-Maildir - $base.'/cur', - $base.'/new', - // manche Layouts haben expliziten INBOX-Ordner - $base.'/INBOX/cur', - $base.'/INBOX/new', - ]; - - foreach ($paths as $p) { - if (is_dir($p)) { - $proc = Process::fromShellCommandline('find '.escapeshellarg($p).' -type f -printf . | wc -c'); - $proc->run(); - if ($proc->isSuccessful()) { - $val = trim($proc->getOutput()); - if ($val !== '' && ctype_digit($val)) { - $count += (int)$val; + if (is_dir($maildir)) { + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS) + ); + foreach ($it as $file) { + if ($file->isFile()) { + $usedBytes += $file->getSize(); } } } + + // 2) Message-Count = Dateien in cur/ + new/ + $messageCount = 0; + foreach (['cur', 'new'] as $sub) { + $dir = "{$maildir}/{$sub}"; + if (is_dir($dir)) { + $dh = opendir($dir); + if ($dh) { + while (($fn = readdir($dh)) !== false) { + // echte Maildir-Dateien haben keinen führenden Punkt + if ($fn !== '.' && $fn !== '..' && $fn[0] !== '.') { + $messageCount++; + } + } + closedir($dh); + } + } + } + + // Update DB + $u->forceFill([ + 'used_bytes' => $usedBytes, + 'message_count' => $messageCount, + 'stats_refreshed_at'=> now(), + ])->save(); + + $this->line(sprintf( + "%-35s %7.1f MiB %5d msgs", + $email, + $usedBytes / 1024 / 1024, + $messageCount + )); } - return $count; + $this->info('Mailbox-Statistiken aktualisiert.'); + return self::SUCCESS; } }