From 45ec38452c53c6edd8e1986e1a5fb070699f4abd Mon Sep 17 00:00:00 2001 From: boban Date: Tue, 21 Oct 2025 01:51:12 +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 | 183 ++++++++++---------- 1 file changed, 87 insertions(+), 96 deletions(-) diff --git a/app/Console/Commands/UpdateMailboxStats.php b/app/Console/Commands/UpdateMailboxStats.php index 627f1eb..752da57 100644 --- a/app/Console/Commands/UpdateMailboxStats.php +++ b/app/Console/Commands/UpdateMailboxStats.php @@ -1,6 +1,5 @@ get(); + $rows = []; foreach ($users as $u) { - $email = $u->email; + $email = (string)$u->email; - [$messages, $vsize] = $this->fetchViaDoveadm($email); - - if ($messages === null || $vsize === null) { - if (config('mailpool.fallback_du')) { - [$messages, $vsize] = $this->fallbackDu($email); - } + // E-Mail validieren + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $this->warn("Überspringe ungültige Adresse: '{$email}'"); + continue; } + [$local, $domain] = explode('@', $email, 2); + $base = "/var/mail/vhosts/{$domain}/{$local}"; + + // ---- Größe (Bytes) via `du -sb`, mit Fallback ---- + $usedBytes = 0; + if (is_dir($base)) { + $usedBytes = $this->dirSizeBytes($base); + } + + // ---- Nachrichten zählen: erst doveadm, sonst Maildir ---- + $messageCount = $this->countMessages($email, $base); + $u->update([ - 'message_count' => (int)($messages ?? 0), - 'used_bytes' => (int)($vsize ?? 0), - 'stats_refreshed_at' => now(), + 'used_bytes' => $usedBytes, + 'message_count' => $messageCount, + 'stats_refreshed_at'=> now(), ]); - $this->line(sprintf( - '%-35s %6.1f MiB %5d msgs', + $rows[] = sprintf( + "%-35s %6.1f MiB %4d msgs", $email, - ($vsize ?? 0) / 1024 / 1024, - $messages ?? 0 - )); + $usedBytes / 1024 / 1024, + $messageCount + ); } - $this->info('Mailbox-Statistiken aktualisiert.'); - return Command::SUCCESS; + foreach ($rows as $line) { + $this->info($line); + } + + return self::SUCCESS; } - /** @return array{0:int|null,1:int|null} [messages, vsize] */ - private function fetchViaDoveadm(string $email): array + private function dirSizeBytes(string $path): int { - $bin = config('mailpool.doveadm'); - $sudo = config('mailpool.doveadm_sudo'); + // 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; + } - $cmd = [$bin, 'mailbox', 'status', '-u', $email, 'messages', 'vsize', 'INBOX']; - if ($sudo) array_unshift($cmd, 'sudo', '-n'); - - $p = new Process($cmd); - $p->setTimeout(10); + private function countMessages(string $email, string $base): int + { + // 1) Bevorzugt: doveadm + $cmd = 'doveadm mailbox status -u '.escapeshellarg($email).' messages INBOX'; + $p = Process::fromShellCommandline($cmd); $p->run(); - if (!$p->isSuccessful()) { - return [null, null]; + if ($p->isSuccessful()) { + $out = trim($p->getOutput()); + if (preg_match('/messages=(\d+)/', $out, $m)) { + return (int)$m[1]; + } } - $out = trim($p->getOutput()); - $messages = preg_match('/\bmessages=(\d+)/', $out, $m) ? (int)$m[1] : null; - $vsize = preg_match('/\bvsize=(\d+)/', $out, $m) ? (int)$m[1] : null; + // 2) Fallback: Maildir zählen (INBOX: root/cur + root/new) + $count = 0; - return [$messages, $vsize]; - } + $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', + ]; - /** Fallback mit du (nur Größe, keine Message-Zählung) */ - private function fallbackDu(string $email): array - { - [$local, $domain] = explode('@', $email, 2); - $path = rtrim(config('mailpool.mailroot'), '/') . "/{$domain}/{$local}"; + 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; + } + } + } + } - $p = new Process(['bash', '-lc', 'du -sb ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1']); - $p->setTimeout(10)->run(); - - $bytes = $p->isSuccessful() ? (int)trim($p->getOutput()) : 0; - return [null, $bytes]; + return $count; } } -// -//namespace App\Console\Commands; -// -//use App\Models\MailUser; -//use Illuminate\Console\Command; -// -//class UpdateMailboxStats extends Command -//{ -// protected $signature = 'mail:update-stats {--user=}'; -// protected $description = 'Aktualisiert Mailquota und Nachrichtenzahl für alle Mailboxen (oder einen spezifischen Benutzer)'; -// -// public function handle(): int -// { -// $query = MailUser::query()->where('is_active', true); -// -// if ($email = $this->option('user')) { -// $query->where('email', $email); -// } -// -// $users = $query->get(); -// -// foreach ($users as $u) { -// $email = $u->email; -// $domain = explode('@', $email)[1]; -// $local = explode('@', $email)[0]; -// $path = "/var/mail/vhosts/{$domain}/{$local}"; -// -// $usedBytes = 0; -// $messageCount = 0; -// -// if (is_dir($path)) { -// $usedBytes = (int) trim(shell_exec('du -sb '.escapeshellarg($path).' 2>/dev/null | cut -f1')); -// } -// -// $out = trim(shell_exec('doveadm mailbox status -u '.escapeshellarg($email).' messages INBOX 2>/dev/null')); -// if (preg_match('/messages=(\d+)/', $out, $m)) { -// $messageCount = (int) $m[1]; -// } -// -// $u->update([ -// 'used_bytes' => $usedBytes, -// 'message_count' => $messageCount, -// 'stats_refreshed_at' => now(), -// ]); -// -// $this->info(sprintf("%-35s %6.1f MiB %4d Nachrichten", $email, $usedBytes / 1024 / 1024, $messageCount)); -// } -// -// return Command::SUCCESS; -// } -//}