parent
fdfa282af0
commit
878dae8876
|
|
@ -4,131 +4,89 @@ namespace App\Console\Commands;
|
|||
|
||||
use App\Models\MailUser;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Process\Process;
|
||||
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 spezifischen Benutzer)';
|
||||
protected $description = 'Aktualisiert Mailquota und Nachrichtenzahl für alle Mailboxen (oder einen Benutzer)';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$query = MailUser::query()->where('is_active', true);
|
||||
if ($email = $this->option('user')) {
|
||||
$query->where('email', $email);
|
||||
$q = MailUser::query()
|
||||
->where('is_active', true)
|
||||
// sichere Filter: E-Mail vorhanden und enthält genau ein "@"
|
||||
->whereNotNull('email')
|
||||
->where('email', 'like', '%@%');
|
||||
|
||||
if ($email = trim((string)$this->option('user'))) {
|
||||
$q->where('email', $email);
|
||||
}
|
||||
|
||||
$users = $query->get();
|
||||
$rows = [];
|
||||
$users = $q->get();
|
||||
if ($users->isEmpty()) {
|
||||
$this->warn('Keine passenden Mailboxen gefunden.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
foreach ($users as $u) {
|
||||
$email = (string)$u->email;
|
||||
|
||||
// E-Mail validieren
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
$this->warn("Überspringe ungültige Adresse: '{$email}'");
|
||||
$email = trim($u->email);
|
||||
if (!preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', $email)) {
|
||||
// still und leise überspringen – kein „Überspringe @“ mehr
|
||||
continue;
|
||||
}
|
||||
|
||||
[$local, $domain] = explode('@', $email, 2);
|
||||
$base = "/var/mail/vhosts/{$domain}/{$local}";
|
||||
$maildir = "/var/mail/vhosts/{$domain}/{$local}";
|
||||
|
||||
// ---- Größe (Bytes) via `du -sb`, mit Fallback ----
|
||||
// 1) Größe in Bytes (rekursiv; ohne "du")
|
||||
$usedBytes = 0;
|
||||
if (is_dir($base)) {
|
||||
$usedBytes = $this->dirSizeBytes($base);
|
||||
}
|
||||
|
||||
// ---- Nachrichten zählen: erst doveadm, sonst Maildir ----
|
||||
$messageCount = $this->countMessages($email, $base);
|
||||
|
||||
$u->update([
|
||||
'used_bytes' => $usedBytes,
|
||||
'message_count' => $messageCount,
|
||||
'stats_refreshed_at'=> now(),
|
||||
]);
|
||||
|
||||
$rows[] = sprintf(
|
||||
"%-35s %6.1f MiB %4d msgs",
|
||||
$email,
|
||||
$usedBytes / 1024 / 1024,
|
||||
$messageCount
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($rows as $line) {
|
||||
$this->info($line);
|
||||
}
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function dirSizeBytes(string $path): int
|
||||
{
|
||||
// du -sb (schnell & korrekt, braucht nur r-x für Verzeichnisse)
|
||||
$p = Process::fromShellCommandline('du -sb '.escapeshellarg($path).' | cut -f1');
|
||||
$p->run();
|
||||
if ($p->isSuccessful()) {
|
||||
$val = trim($p->getOutput());
|
||||
if ($val !== '' && ctype_digit($val)) {
|
||||
return (int)$val;
|
||||
}
|
||||
}
|
||||
// Fallback (langsamer, aber funktioniert ohne du)
|
||||
$size = 0;
|
||||
$it = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($it as $f) {
|
||||
if ($f->isFile()) {
|
||||
$size += $f->getSize();
|
||||
}
|
||||
}
|
||||
return $size;
|
||||
}
|
||||
|
||||
private function countMessages(string $email, string $base): int
|
||||
{
|
||||
// 1) Bevorzugt: doveadm
|
||||
$cmd = sprintf(
|
||||
'doveadm -S /run/dovecot/doveadm-server mailbox status -u %s messages INBOX',
|
||||
escapeshellarg($email)
|
||||
);
|
||||
$p = Process::fromShellCommandline($cmd);
|
||||
$p->run();
|
||||
|
||||
if ($p->isSuccessful()) {
|
||||
$out = trim($p->getOutput());
|
||||
if (preg_match('/messages=(\d+)/', $out, $m)) {
|
||||
return (int)$m[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Fallback: Maildir zählen (INBOX: root/cur + root/new)
|
||||
$count = 0;
|
||||
|
||||
$paths = [
|
||||
// Dovecot: INBOX ist i.d.R. das Root-Maildir
|
||||
$base.'/cur',
|
||||
$base.'/new',
|
||||
// manche Layouts haben expliziten INBOX-Ordner
|
||||
$base.'/INBOX/cur',
|
||||
$base.'/INBOX/new',
|
||||
];
|
||||
|
||||
foreach ($paths as $p) {
|
||||
if (is_dir($p)) {
|
||||
$proc = Process::fromShellCommandline('find '.escapeshellarg($p).' -type f -printf . | wc -c');
|
||||
$proc->run();
|
||||
if ($proc->isSuccessful()) {
|
||||
$val = trim($proc->getOutput());
|
||||
if ($val !== '' && ctype_digit($val)) {
|
||||
$count += (int)$val;
|
||||
if (is_dir($maildir)) {
|
||||
$it = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($maildir, \FilesystemIterator::SKIP_DOTS)
|
||||
);
|
||||
foreach ($it as $file) {
|
||||
if ($file->isFile()) {
|
||||
$usedBytes += $file->getSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Message-Count = Dateien in cur/ + new/
|
||||
$messageCount = 0;
|
||||
foreach (['cur', 'new'] as $sub) {
|
||||
$dir = "{$maildir}/{$sub}";
|
||||
if (is_dir($dir)) {
|
||||
$dh = opendir($dir);
|
||||
if ($dh) {
|
||||
while (($fn = readdir($dh)) !== false) {
|
||||
// echte Maildir-Dateien haben keinen führenden Punkt
|
||||
if ($fn !== '.' && $fn !== '..' && $fn[0] !== '.') {
|
||||
$messageCount++;
|
||||
}
|
||||
}
|
||||
closedir($dh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update DB
|
||||
$u->forceFill([
|
||||
'used_bytes' => $usedBytes,
|
||||
'message_count' => $messageCount,
|
||||
'stats_refreshed_at'=> now(),
|
||||
])->save();
|
||||
|
||||
$this->line(sprintf(
|
||||
"%-35s %7.1f MiB %5d msgs",
|
||||
$email,
|
||||
$usedBytes / 1024 / 1024,
|
||||
$messageCount
|
||||
));
|
||||
}
|
||||
|
||||
return $count;
|
||||
$this->info('Mailbox-Statistiken aktualisiert.');
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue