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); // 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']) ->with(['domain:id,domain,is_system']) ->where('is_active', true); 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,&$sumUserBytes,&$sumSystemBytes,$log) { foreach ($users as $u) { $checked++; $raw = (string)($u->getRawOriginal('email') ?? ''); $email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? null); if (!is_string($email) || !preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { continue; } [$local, $domain] = explode('@', $email, 2); $maildir = "/var/mail/vhosts/{$domain}/{$local}"; $isSystemDomain = (bool)($u->domain->is_system ?? false); if ($isSystemDomain || $u->is_system) { $sumSystemBytes += $this->sizeViaDoveadm($email) ?? $this->sizeViaFilesystem($maildir) ?? 0; continue; } // Alle Ordner zählen (nicht nur INBOX) — doveadm ist primäre Quelle $messageCount = $this->countAllFoldersViaDoveadm($email); $usedBytes = $this->sizeViaDoveadm($email); if ($messageCount === null) { // Filesystem-Fallback nur wenn das Verzeichnis lesbar ist $fsCount = $this->countViaFilesystem($maildir); $fsSize = $this->sizeViaFilesystem($maildir); if ($fsCount === 0 && $fsSize === null) { $log->debug('skip (no access)', ['email' => $email]); continue; } $messageCount = $fsCount; $usedBytes = $fsSize ?? 0; } else { $usedBytes = $usedBytes ?? $this->sizeViaFilesystem($maildir) ?? 0; } $sumUserBytes += $usedBytes; // Immer schreiben wenn doveadm erfolgreich war — keine Change-Detection Setting::set("mailbox.{$email}", [ 'used_bytes' => (int)$usedBytes, 'message_count' => (int)$messageCount, 'updated_at' => now()->toDateTimeString(), ]); \Illuminate\Support\Facades\DB::table('mail_users') ->where('id', $u->id) ->update([ 'used_bytes' => (int)$usedBytes, 'message_count' => (int)$messageCount, 'stats_refreshed_at' => now(), ]); $changed++; $this->line(sprintf("%-35s %7.2f MiB %5d msgs", $email, $usedBytes / 1048576, $messageCount)); $log->info('updated', ['email' => $email, 'used_bytes' => $usedBytes, 'message_count' => $messageCount]); } }); // 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 doveadmStatus(string $email, string $fields): array { $cmd = "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u " . escapeshellarg($email) . " {$fields} INBOX 2>/dev/null"; $out = []; $rc = 0; exec($cmd, $out, $rc); if ($rc !== 0) return []; // header: "mailbox\tmessages" data: "INBOX\t4" $header = null; foreach ($out as $line) { $parts = explode("\t", trim($line)); if ($header === null) { $header = $parts; continue; } if (count($parts) !== count($header)) continue; return array_combine($header, $parts); } return []; } private function countAllFoldersViaDoveadm(string $email): ?int { // Entwürfe, Papierkorb und Spam/Junk nicht mitzählen static $exclude = ['Drafts', 'Trash', 'Junk', 'Spam']; $cmd = "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u " . escapeshellarg($email) . " messages '*' 2>/dev/null"; $out = []; $rc = 0; exec($cmd, $out, $rc); if ($rc !== 0 || count($out) < 2) return null; $total = 0; $header = null; foreach ($out as $line) { $parts = explode("\t", trim($line)); if ($header === null) { $header = $parts; continue; } if (count($parts) !== count($header)) continue; $row = array_combine($header, $parts); if (in_array($row['mailbox'] ?? '', $exclude, true)) continue; $total += (int)($row['messages'] ?? 0); } return $total; } private function sizeViaDoveadm(string $email): ?int { $row = $this->doveadmStatus($email, 'vsize'); if (isset($row['vsize'])) return (int)$row['vsize']; return null; } private 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; } private function sizeViaFilesystem(string $maildir): ?int { if (!is_dir($maildir) || !is_readable($maildir)) return null; try { $bytes = 0; $it = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS) ); foreach ($it as $f) { if ($f->isFile()) $bytes += $f->getSize(); } return $bytes; } catch (\Throwable) { return null; } } } //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; //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; DB-frei)'; // // public function handle(): int // { // Log::info('=== mail:update-stats START ===', ['userOpt' => $this->option('user')]); // // $q = MailUser::query() // ->with('domain:id,domain') // ->active(); // // if ($only = trim((string)$this->option('user'))) { // $q->byEmail($only); // } // // $users = $q->get(); // if ($users->isEmpty()) { // Log::warning('Keine passenden Mailboxen gefunden.'); // $this->warn('Keine passenden Mailboxen gefunden.'); // return self::SUCCESS; // } // // foreach ($users as $u) { // // 1) sichere E-Mail ermitteln: roher DB-Wert → Accessor → zusammengesetzt // $raw = (string)($u->getRawOriginal('email') ?? ''); // $email = $raw !== '' ? $raw : ($u->email ?? $u->address ?? ''); // // if (!preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', (string)$email)) { // Log::warning('Ungültige effektive Adresse – skip', [ // 'user_id' => $u->id, 'raw' => $raw, 'computed' => $email // ]); // continue; // } // // [$local, $domain] = explode('@', $email, 2); // $maildir = "/var/mail/vhosts/{$domain}/{$local}"; // // // 2) 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(); // } // } // // // 3) Message-Count – prefer doveadm als vmail; Fallback: Files // $messageCount = $this->countViaDoveadm($email); // if ($messageCount === null) { // $messageCount = $this->countViaFilesystem($maildir); // } // // // 4) In Settings (→ Redis + DB-Backup) persistieren // Setting::set("mailbox.{$email}", [ // 'used_bytes' => (int)$usedBytes, // 'message_count' => (int)$messageCount, // 'updated_at' => now()->toDateTimeString(), // ]); // // $this->line(sprintf("%-35s %7.1f MiB %5d msgs", // $email, $usedBytes / 1024 / 1024, $messageCount)); // // Log::info('Mailbox-Stat OK', [ // 'email' => $email, // 'maildir' => $maildir, // 'used_bytes' => $usedBytes, // 'message_count' => $messageCount, // ]); // } // // Log::info('=== mail:update-stats DONE ==='); // $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 Illuminate\Console\Command; //use Illuminate\Support\Facades\Log; //use App\Models\MailUser; //use App\Models\Setting; //use RecursiveDirectoryIterator; //use RecursiveIteratorIterator; //class UpdateMailboxStats extends Command //{ // protected $signature = 'mail:update-stats {--user=}'; // protected $description = 'Aktualisiert Mailquota und Nachrichtenzahl'; // // public function handle(): int // { // Log::info('=== mail:update-stats START ===', ['userOpt' => $this->option('user')]); // // $q = MailUser::query() // ->with('domain:id,domain') // <— Domain fürs Fallback laden // ->where('is_active', true); // // if ($only = trim((string)$this->option('user'))) { // $q->where('email', $only); // } else { // $q->whereNotNull('email')->where('email', 'like', '%@%'); // } // // $users = $q->get(); // if ($users->isEmpty()) { // Log::warning('Keine passenden Mailboxen gefunden.'); // $this->warn('Keine passenden Mailboxen gefunden.'); // return self::SUCCESS; // } // // foreach ($users as $u) { // // 1) rohen DB-Wert nehmen (ohne Accessor/Casts) // $rawEmail = (string) $u->getRawOriginal('email'); // // // 2) Fallback aus localpart + domain, falls roher Wert leer // $fallback = ($u->localpart ?? '') . '@' . ($u->domain->domain ?? ''); // $email = trim($rawEmail !== '' ? $rawEmail : $fallback); // // if (!preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { // Log::warning('Ungültige effektive Adresse – skip', [ // 'raw' => $rawEmail, // 'fallback' => $fallback, // 'got' => $email, // 'user_id' => $u->id, // ]); // 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(); // } // } // // // Zähler via doveadm (als vmail), Fallback: Files zählen // $messageCount = $this->countViaDoveadm($email); // if ($messageCount === null) { // $messageCount = $this->countViaFilesystem($maildir); // } // // // Optional zusätzlich cachen (UI liest das bereits) // Setting::set("mailbox.{$email}", [ // 'used_bytes' => (int)$usedBytes, // 'message_count' => (int)$messageCount, // 'updated_at' => now()->toDateTimeString(), // ]); // // $this->line(sprintf("%-35s %7.1f MiB %5d msgs", $email, $usedBytes/1024/1024, $messageCount)); // Log::info('Mailbox-Stat OK', compact('email','maildir','usedBytes','messageCount')); // } // // Log::info('=== mail:update-stats DONE ==='); // $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; //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 Mailquota und Nachrichtenzahl für alle Mailboxen (oder einen Benutzer)'; // // public function handle(): int // { // Log::channel('mailstats')->info('=== mail:update-stats START ===', [ // 'userOpt' => $this->option('user') // ]); // // $q = MailUser::query() // ->where('is_active', true) // ->whereNotNull('email') // ->where('email', 'like', '%@%'); // // if ($email = trim((string)$this->option('user'))) { // $q->where('email', $email); // } // // $users = $q->get(); // // if ($users->isEmpty()) { // Log::channel('mailstats')->warning('Keine passenden Mailboxen gefunden.'); // $this->warn('Keine passenden Mailboxen gefunden.'); // return self::SUCCESS; // } // // foreach ($users as $u) { // $email = trim((string)$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}"; // // // 1) Größe berechnen (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(); // } // } // // // 2) Nachrichten zählen – bevorzugt via doveadm als vmail // $messageCount = $this->countViaDoveadm($email); // if ($messageCount === null) { // $messageCount = $this->countViaFilesystem($maildir); // } // // // optional: in Settings/Redis cachen (für UI) // Setting::set("mailbox.{$email}", [ // 'used_bytes' => (int)$usedBytes, // 'message_count' => (int)$messageCount, // 'updated_at' => now()->toDateTimeString(), // ]); // // $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::channel('mailstats')->info('=== mail:update-stats DONE ==='); // $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); // // Log::channel('mailstats')->debug('doveadm exec', ['cmd' => $cmd, 'rc' => $rc, 'out' => $out]); // // if ($rc !== 0) return null; // // // 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; // } // // protected function countViaFilesystem(string $maildir): int // { // $n = 0; // foreach (['cur', 'new'] as $sub) { // $dir = "{$maildir}/{$sub}"; // 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); // } // 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; // //use App\Models\MailUser; //use App\Models\Setting; //use Illuminate\Console\Command; //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 Benutzer)'; // // public function handle(): int // { // $q = MailUser::query() // ->where('is_active', true) // ->whereNotNull('email') // ->where('email', 'like', '%@%'); // // if ($email = trim((string)$this->option('user'))) { // $q->where('email', $email); // } // // $users = $q->get(); // if ($users->isEmpty()) { // $this->warn('Keine passenden Mailboxen gefunden.'); // return self::SUCCESS; // } // // foreach ($users as $u) { // $email = trim($u->email); // if (!preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { // // ungültig → überspringen // continue; // } // // [$local, $domain] = explode('@', $email, 2); // $maildir = "/var/mail/vhosts/{$domain}/{$local}"; // // // 1) Größe (rekursiv, ohne `du`) // $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(); // } // } // } // // // 2) Nachrichtenzahl – bevorzugt via doveadm (sudo -u vmail), Fallback: Dateien zählen // $messageCount = $this->messageCountViaDoveadm($email); // if ($messageCount === null) { // $messageCount = $this->messageCountViaFilesystem($maildir); // } // // Setting::set("mailbox.{$email}", [ // 'used_bytes' => $usedBytes, // 'message_count' => $messageCount, // 'updated_at' => now()->toDateTimeString(), // ]); // // $this->line(sprintf( // "%-35s %7.1f MiB %5d Messages", // $email, // $usedBytes / 1024 / 1024, // $messageCount // )); // } // // $this->info('Mailbox-Statistiken aktualisiert.'); // return self::SUCCESS; // } // // /** // * Holt die Anzahl der Nachrichten in INBOX über doveadm als vmail (sudo). // * Gibt int bei Erfolg, oder null bei Fehlern zurück. // */ // private function messageCountViaDoveadm(string $email): ?int // { // // bevorzugtes Format: tabellarisch → "INBOX\t123" // $cmd = sprintf( // "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u %s messages INBOX 2>/dev/null", // escapeshellarg($email) // ); // $out = trim((string) shell_exec($cmd)); // // if ($out === '') { // // Fallback auf normales Format: "messages=123" // $cmd2 = sprintf( // "sudo -n -u vmail /usr/bin/doveadm mailbox status -u %s messages INBOX 2>/dev/null", // escapeshellarg($email) // ); // $out = trim((string) shell_exec($cmd2)); // } // // if ($out === '') { // return null; // } // // // Match "INBOX123" oder "messages=123" // if (preg_match('/\t(\d+)\s*$/', $out, $m) || preg_match('/messages=(\d+)/', $out, $m)) { // return (int) $m[1]; // } // // return null; // } // // /** // * Fallback: Zählt Dateien in cur/ + new/ (Maildir). // */ // private function messageCountViaFilesystem(string $maildir): int // { // $count = 0; // foreach (['cur', 'new'] as $sub) { // $dir = "{$maildir}/{$sub}"; // if (is_dir($dir) && is_readable($dir)) { // $dh = opendir($dir); // if ($dh) { // while (($fn = readdir($dh)) !== false) { // if ($fn !== '.' && $fn !== '..' && $fn[0] !== '.') { // $count++; // } // } // closedir($dh); // } // } // } // return $count; // } //}