diff --git a/app/Console/Commands/UpdateMailboxStats.php b/app/Console/Commands/UpdateMailboxStats.php index 465de2e..63e9998 100644 --- a/app/Console/Commands/UpdateMailboxStats.php +++ b/app/Console/Commands/UpdateMailboxStats.php @@ -5,8 +5,10 @@ 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 { @@ -15,32 +17,54 @@ class UpdateMailboxStats extends Command 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) - ->whereNotNull('email') - ->where('email', 'like', '%@%'); + ->with(['domain:id,domain']); - if ($email = trim((string)$this->option('user'))) { - $q->where('email', $email); + 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) { - $email = trim($u->email); - if (!preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) { - // ungültig → überspringen + $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; } - [$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 (rekursiv, ohne `du`) + // 1) Größe berechnen $usedBytes = 0; if (is_dir($maildir)) { $it = new RecursiveIteratorIterator( @@ -51,86 +75,216 @@ class UpdateMailboxStats extends Command $usedBytes += $file->getSize(); } } + } else { + $log->warning('Maildir nicht gefunden', ['path' => $maildir]); } - // 2) Nachrichtenzahl – bevorzugt via doveadm (sudo -u vmail), Fallback: Dateien zählen - $messageCount = $this->messageCountViaDoveadm($email); + // 2) Nachrichtenzahl + $messageCount = $this->countViaDoveadm($email, $log); if ($messageCount === null) { - $messageCount = $this->messageCountViaFilesystem($maildir); + $messageCount = $this->countViaFilesystem($maildir, $log); } + // Cache (Settings/Redis) Setting::set("mailbox.{$email}", [ - 'used_bytes' => $usedBytes, - 'message_count' => $messageCount, - 'updated_at' => now()->toDateTimeString(), + 'used_bytes' => (int)$usedBytes, + 'message_count' => (int)$messageCount, + 'ts' => now()->toISOString(), ]); - $this->line(sprintf( - "%-35s %7.1f MiB %5d msgs", - $email, - $usedBytes / 1024 / 1024, - $messageCount - )); + $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; } - /** - * 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 + protected function countViaDoveadm(string $email, $log): ?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)); + $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); - 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)); - } + 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 ($out === '') { + 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; } - - // 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 + protected function countViaFilesystem(string $maildir, $log): 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++; - } + if (is_dir($dir) && ($dh = @opendir($dir))) { + while (false !== ($fn = readdir($dh))) { + if ($fn !== '.' && $fn !== '..' && $fn[0] !== '.') { + $count++; } - closedir($dh); } + 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; +// } +//} diff --git a/app/Livewire/Ui/Mail/MailboxList.php b/app/Livewire/Ui/Mail/MailboxList.php index 489429e..19dcc63 100644 --- a/app/Livewire/Ui/Mail/MailboxList.php +++ b/app/Livewire/Ui/Mail/MailboxList.php @@ -6,6 +6,7 @@ namespace App\Livewire\Ui\Mail; use App\Models\Domain; use App\Models\Setting; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Livewire\Attributes\On; use Livewire\Component; @@ -61,18 +62,37 @@ class MailboxList extends Component public function updateMailboxStats() { - // führe Artisan-Command direkt aus - Artisan::call('mail:update-stats'); + $started = microtime(true); + + Log::channel('mailstats')->info('UI: updateMailboxStats() geklickt', [ + 'actor' => 'web', + 'ip' => request()->ip() ?? null, + ]); + + // Command ausführen + $rc = Artisan::call('mail:update-stats'); + $output = Artisan::output(); + + Log::channel('mailstats')->info('UI: Command beendet', [ + 'rc' => $rc, + 'ms' => (int)((microtime(true) - $started) * 1000), + 'output' => trim($output), + ]); + + // UI auffrischen $this->dispatch('$refresh'); + + // Ergebnis toaster $this->dispatch('toast', - type: 'done', + type: $rc === 0 ? 'done' : 'warn', badge: 'Mailbox', - title: 'Mailbox aktualisiert', - text: 'Die Mailbox-Statistiken wurden aktualisiert.', + title: $rc === 0 ? 'Mailbox aktualisiert' : 'Aktualisierung fehlgeschlagen', + text: $rc === 0 ? 'Statistiken wurden aktualisiert.' : 'Siehe logs/mailstats.log', duration: 6000, ); } + public function updateMailboxStatsOne(string $email) { Artisan::call('mail:update-stats', ['--user' => $email]); diff --git a/config/logging.php b/config/logging.php index 9e998a4..37482f9 100644 --- a/config/logging.php +++ b/config/logging.php @@ -129,4 +129,12 @@ return [ ], + + 'mailstats' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/mailstats.log'), + 'level' => 'debug', + 'days' => 14, + ], + ]; diff --git a/resources/views/livewire/ui/mail/mailbox-list.blade.php b/resources/views/livewire/ui/mail/mailbox-list.blade.php index 619c739..51ed77b 100644 --- a/resources/views/livewire/ui/mail/mailbox-list.blade.php +++ b/resources/views/livewire/ui/mail/mailbox-list.blade.php @@ -304,7 +304,7 @@
- +{{-- --}}