mailwolt/app/Console/Commands/UpdateMailboxStats.php

1048 lines
37 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
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 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);
// 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}";
// 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();
}
$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);
$sumUserBytes += $usedBytes;
$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++;
$this->line(sprintf("%-35s %7.2f MiB %5d msgs",
$email, $usedBytes / 1048576, $messageCount));
$log->info('updated', ['email'=>$email]+$new);
}
}
});
// 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 "
. 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;
}
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;
}
}
//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<number>"
// 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 "INBOX<TAB>123" 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;
// }
//}