parent
40340c709a
commit
7f4f2adbe5
|
|
@ -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 "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
|
||||
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 "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;
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -129,4 +129,12 @@ return [
|
|||
|
||||
],
|
||||
|
||||
|
||||
'mailstats' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/mailstats.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 14,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@
|
|||
|
||||
<td class="px-3 py-2 rounded-r-xl">
|
||||
<div class="flex items-center gap-2 justify-end">
|
||||
<button wire:click="updateMailboxStatsOne('{{ $u['localpart'].'@'.$domain->domain }}')">Jetzt aktualisieren</button>
|
||||
{{-- <button wire:click="updateMailboxStatsOne('{{ $u['localpart'].'@'.$domain->domain }}')">Jetzt aktualisieren</button>--}}
|
||||
|
||||
<button wire:click="updateMailboxStats"
|
||||
class="px-3 py-1 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-700 transition">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ Artisan::command('inspire', function () {
|
|||
})->purpose('Display an inspiring quote');
|
||||
|
||||
Schedule::job(RunHealthChecks::class)->everytenSeconds()->withoutOverlapping();
|
||||
Schedule::command('mailwolt:check-updates')->dailyAt('04:10');
|
||||
//Schedule::command('mailwolt:check-updates')->dailyAt('04:10');
|
||||
Schedule::command('mailwolt:check-updates')->everyFiveMinutes();
|
||||
|
||||
Schedule::command('mail:update-stats')
|
||||
->everyFiveMinutes()
|
||||
|
|
|
|||
Loading…
Reference in New Issue