parent
8a85735397
commit
ead03b073c
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\MailUser;
|
use App\Models\MailUser;
|
||||||
|
|
@ -8,7 +9,6 @@ use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use RecursiveDirectoryIterator;
|
use RecursiveDirectoryIterator;
|
||||||
use RecursiveIteratorIterator;
|
use RecursiveIteratorIterator;
|
||||||
use Symfony\Component\Process\Process;
|
|
||||||
|
|
||||||
class UpdateMailboxStats extends Command
|
class UpdateMailboxStats extends Command
|
||||||
{
|
{
|
||||||
|
|
@ -17,142 +17,269 @@ class UpdateMailboxStats extends Command
|
||||||
|
|
||||||
public function handle(): int
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$log = Log::channel('mailstats');
|
Log::channel('mailstats')->info('=== mail:update-stats START ===', [
|
||||||
$log->info('=== mail:update-stats gestartet', [
|
'userOpt' => $this->option('user')
|
||||||
'user_opt' => $this->option('user'),
|
|
||||||
'php_user' => get_current_user(),
|
|
||||||
'uid' => getmyuid(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$q = MailUser::query()
|
$q = MailUser::query()
|
||||||
->where('is_active', true)
|
->where('is_active', true)
|
||||||
->with(['domain:id,domain']);
|
->whereNotNull('email')
|
||||||
|
->where('email', 'like', '%@%');
|
||||||
|
|
||||||
if ($one = trim((string)$this->option('user'))) {
|
if ($email = trim((string)$this->option('user'))) {
|
||||||
if (str_contains($one, '@')) {
|
$q->where('email', $email);
|
||||||
[$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();
|
$users = $q->get();
|
||||||
$log->info('Gefundene Mailboxen', ['count' => $users->count(), 'ids' => $users->pluck('id')]);
|
|
||||||
|
|
||||||
if ($users->isEmpty()) {
|
if ($users->isEmpty()) {
|
||||||
|
Log::channel('mailstats')->warning('Keine passenden Mailboxen gefunden.');
|
||||||
$this->warn('Keine passenden Mailboxen gefunden.');
|
$this->warn('Keine passenden Mailboxen gefunden.');
|
||||||
$log->warning('Abbruch: keine Mailboxen gefunden');
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($users as $u) {
|
foreach ($users as $u) {
|
||||||
$local = trim((string)$u->localpart);
|
$email = trim((string)$u->email);
|
||||||
$domain = optional($u->domain)->domain;
|
|
||||||
$email = trim((string)($u->email ?? ''));
|
|
||||||
|
|
||||||
if ($email === '' && $local !== '' && $domain) {
|
if (!preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
|
||||||
$email = "{$local}@{$domain}";
|
Log::channel('mailstats')->warning('Überspringe ungültige Adresse', ['email' => $email]);
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[$local, $domain] = explode('@', $email, 2);
|
||||||
$maildir = "/var/mail/vhosts/{$domain}/{$local}";
|
$maildir = "/var/mail/vhosts/{$domain}/{$local}";
|
||||||
$log->info('Verarbeite Mailbox', ['id' => $u->id, 'email' => $email, 'maildir' => $maildir]);
|
|
||||||
|
|
||||||
// 1) Größe berechnen
|
// 1) Größe berechnen (Dateisystem)
|
||||||
$usedBytes = 0;
|
$usedBytes = 0;
|
||||||
if (is_dir($maildir)) {
|
if (is_dir($maildir)) {
|
||||||
$it = new RecursiveIteratorIterator(
|
$it = new RecursiveIteratorIterator(
|
||||||
new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
|
new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
|
||||||
);
|
);
|
||||||
foreach ($it as $file) {
|
foreach ($it as $f) {
|
||||||
if ($file->isFile()) {
|
if ($f->isFile()) $usedBytes += $f->getSize();
|
||||||
$usedBytes += $file->getSize();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
$log->warning('Maildir nicht gefunden', ['path' => $maildir]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) Nachrichtenzahl
|
// 2) Nachrichten zählen – bevorzugt via doveadm als vmail
|
||||||
$messageCount = $this->countViaDoveadm($email, $log);
|
$messageCount = $this->countViaDoveadm($email);
|
||||||
if ($messageCount === null) {
|
if ($messageCount === null) {
|
||||||
$messageCount = $this->countViaFilesystem($maildir, $log);
|
$messageCount = $this->countViaFilesystem($maildir);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache (Settings/Redis)
|
// optional: in Settings/Redis cachen (für UI)
|
||||||
Setting::set("mailbox.{$email}", [
|
Setting::set("mailbox.{$email}", [
|
||||||
'used_bytes' => (int)$usedBytes,
|
'used_bytes' => (int)$usedBytes,
|
||||||
'message_count' => (int)$messageCount,
|
'message_count' => (int)$messageCount,
|
||||||
'ts' => now()->toISOString(),
|
'updated_at' => now()->toDateTimeString(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->line(sprintf("%-40s %7.1f MiB %5d msgs", $email, $usedBytes / 1024 / 1024, $messageCount));
|
$line = sprintf("%-35s %7.1f MiB %5d msgs", $email, $usedBytes / 1024 / 1024, $messageCount);
|
||||||
$log->info('Aktualisiert', [
|
$this->line($line);
|
||||||
'email' => $email,
|
Log::channel('mailstats')->info('OK', [
|
||||||
'used_bytes' => $usedBytes,
|
'email' => $email,
|
||||||
|
'maildir' => $maildir,
|
||||||
|
'used_bytes' => $usedBytes,
|
||||||
'message_count' => $messageCount,
|
'message_count' => $messageCount,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$log->info('=== mail:update-stats fertig ===');
|
Log::channel('mailstats')->info('=== mail:update-stats DONE ===');
|
||||||
$this->info('Mailbox-Statistiken aktualisiert.');
|
$this->info('Mailbox-Statistiken aktualisiert.');
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function countViaDoveadm(string $email, $log): ?int
|
protected function countViaDoveadm(string $email): ?int
|
||||||
{
|
{
|
||||||
$cmd = ['sudo', '-n', '-u', 'vmail', '/usr/bin/doveadm', '-f', 'tab', 'mailbox', 'status', '-u', $email, 'messages', 'INBOX'];
|
$cmd = "sudo -n -u vmail /usr/bin/doveadm -f tab mailbox status -u " . escapeshellarg($email) . " messages INBOX 2>&1";
|
||||||
$p = new Process($cmd, null, ['LANG' => 'C']);
|
$out = [];
|
||||||
$p->setTimeout(10);
|
$rc = 0;
|
||||||
|
exec($cmd, $out, $rc);
|
||||||
|
|
||||||
try {
|
Log::channel('mailstats')->debug('doveadm exec', ['cmd' => $cmd, 'rc' => $rc, 'out' => $out]);
|
||||||
$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;
|
if ($rc !== 0) return null;
|
||||||
|
|
||||||
$lines = preg_split('/\R+/', $out);
|
// Erwartet: "mailbox\tmessages\nINBOX\t<number>"
|
||||||
$last = end($lines);
|
foreach ($out as $line) {
|
||||||
if (preg_match('/\t(\d+)$/', (string)$last, $m)) {
|
if (preg_match('/^\s*INBOX\s+(\d+)\s*$/i', trim($line), $m)) {
|
||||||
return (int)$m[1];
|
return (int)$m[1];
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
$log->error('doveadm Exception', ['msg' => $e->getMessage()]);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function countViaFilesystem(string $maildir, $log): int
|
protected function countViaFilesystem(string $maildir): int
|
||||||
{
|
{
|
||||||
$count = 0;
|
$n = 0;
|
||||||
foreach (['cur', 'new'] as $sub) {
|
foreach (['cur', 'new'] as $sub) {
|
||||||
$dir = "{$maildir}/{$sub}";
|
$dir = "{$maildir}/{$sub}";
|
||||||
if (is_dir($dir) && ($dh = @opendir($dir))) {
|
if (!is_dir($dir)) continue;
|
||||||
while (false !== ($fn = readdir($dh))) {
|
$dh = opendir($dir);
|
||||||
if ($fn !== '.' && $fn !== '..' && $fn[0] !== '.') {
|
if (!$dh) continue;
|
||||||
$count++;
|
while (($fn = readdir($dh)) !== false) {
|
||||||
}
|
if ($fn === '.' || $fn === '..' || $fn[0] === '.') continue;
|
||||||
}
|
$n++;
|
||||||
closedir($dh);
|
|
||||||
} else {
|
|
||||||
$log->debug('Ordner fehlt/leer', ['path' => $dir]);
|
|
||||||
}
|
}
|
||||||
|
closedir($dh);
|
||||||
}
|
}
|
||||||
return $count;
|
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;
|
//namespace App\Console\Commands;
|
||||||
//
|
//
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue