parent
f69ea2ddcb
commit
57b01654cc
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\MailUser;
|
||||
|
|
@ -12,78 +13,116 @@ use RecursiveIteratorIterator;
|
|||
class UpdateMailboxStats extends Command
|
||||
{
|
||||
protected $signature = 'mail:update-stats {--user=}';
|
||||
protected $description = 'Aktualisiert Quota & Nachrichtenzahl (Settings/Redis; DB-frei)';
|
||||
protected $description = 'Aktualisiert Quota & Nachrichtenzahl (Settings/Redis; ohne MailUser-DB-Felder).';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
Log::info('=== mail:update-stats START ===', ['userOpt' => $this->option('user')]);
|
||||
$log = Log::channel('mailstats');
|
||||
|
||||
$onlyUser = trim((string)$this->option('user')) ?: null;
|
||||
$started = microtime(true);
|
||||
|
||||
$q = MailUser::query()
|
||||
->with('domain:id,domain')
|
||||
->active();
|
||||
->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 ($only = trim((string)$this->option('user'))) {
|
||||
$q->byEmail($only);
|
||||
if ($onlyUser) {
|
||||
$q->where('email', $onlyUser);
|
||||
}
|
||||
|
||||
$users = $q->get();
|
||||
|
||||
$log->info('mail:update-stats START', [
|
||||
'only' => $onlyUser,
|
||||
'users' => $users->count(),
|
||||
]);
|
||||
|
||||
if ($users->isEmpty()) {
|
||||
Log::warning('Keine passenden Mailboxen gefunden.');
|
||||
$this->warn('Keine passenden Mailboxen gefunden.');
|
||||
$log->info('mail:update-stats DONE (no users)', ['ms' => (int)((microtime(true) - $started) * 1000)]);
|
||||
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 ?? '');
|
||||
$changed = 0;
|
||||
$checked = 0;
|
||||
|
||||
if (!preg_match('/^[^@\s]+@[^@\s]+\.[^@\s]+$/', (string)$email)) {
|
||||
Log::warning('Ungültige effektive Adresse – skip', [
|
||||
'user_id' => $u->id, 'raw' => $raw, 'computed' => $email
|
||||
]);
|
||||
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}";
|
||||
|
||||
// 2) Größe (Dateisystem)
|
||||
// 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();
|
||||
if ($f->isFile()) {
|
||||
$usedBytes += $f->getSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Message-Count – prefer doveadm als vmail; Fallback: Files
|
||||
// Nachrichten zählen
|
||||
$messageCount = $this->countViaDoveadm($email);
|
||||
if ($messageCount === null) {
|
||||
$messageCount = $this->countViaFilesystem($maildir);
|
||||
}
|
||||
|
||||
// 4) In Settings (→ Redis + DB-Backup) persistieren
|
||||
Setting::set("mailbox.{$email}", [
|
||||
// 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(),
|
||||
]);
|
||||
];
|
||||
|
||||
$this->line(sprintf("%-35s %7.1f MiB %5d msgs",
|
||||
$email, $usedBytes / 1024 / 1024, $messageCount));
|
||||
if (
|
||||
!is_array($prev) ||
|
||||
($prev['used_bytes'] ?? null) !== $new['used_bytes'] ||
|
||||
($prev['message_count'] ?? null) !== $new['message_count']
|
||||
) {
|
||||
Setting::set($key, $new);
|
||||
$changed++;
|
||||
|
||||
Log::info('Mailbox-Stat OK', [
|
||||
'email' => $email,
|
||||
'maildir' => $maildir,
|
||||
'used_bytes' => $usedBytes,
|
||||
'message_count' => $messageCount,
|
||||
]);
|
||||
// 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]);
|
||||
}
|
||||
}
|
||||
|
||||
Log::info('=== mail:update-stats DONE ===');
|
||||
$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;
|
||||
}
|
||||
|
|
@ -95,6 +134,7 @@ class UpdateMailboxStats extends Command
|
|||
$out = [];
|
||||
$rc = 0;
|
||||
exec($cmd, $out, $rc);
|
||||
|
||||
if ($rc !== 0) return null;
|
||||
|
||||
foreach ($out as $line) {
|
||||
|
|
@ -123,6 +163,129 @@ class UpdateMailboxStats extends Command
|
|||
}
|
||||
}
|
||||
|
||||
//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;
|
||||
|
|
|
|||
|
|
@ -8,67 +8,96 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|||
class MailUser extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'domain_id', 'localpart', 'email', 'display_name', 'password_hash',
|
||||
'is_active', 'quota_mb', 'is_system'
|
||||
'domain_id',
|
||||
'localpart',
|
||||
'email',
|
||||
'display_name',
|
||||
'password_hash',
|
||||
'is_system',
|
||||
'is_active',
|
||||
'can_login',
|
||||
'quota_mb',
|
||||
'rate_limit_per_hour',
|
||||
// -> KEINE used_bytes / message_count hier, die cachen wir separat
|
||||
];
|
||||
|
||||
protected $hidden = ['password_hash'];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'bool',
|
||||
'is_system' => 'bool',
|
||||
'quota_mb' => 'int',
|
||||
'is_system' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'can_login' => 'boolean',
|
||||
'quota_mb' => 'integer',
|
||||
'rate_limit_per_hour' => 'integer',
|
||||
'last_login_at' => 'datetime',
|
||||
'stats_refreshed_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Beziehungen
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function domain(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Domain::class);
|
||||
}
|
||||
|
||||
// optional: virtueller Setter
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Accessors / Helper
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bevorzugt den DB-Wert aus 'email'.
|
||||
* Fehlt dieser, wird aus localpart + domain.domain gebaut.
|
||||
* Gibt niemals "@" zurück, sondern null, wenn unbestimmbar.
|
||||
*/
|
||||
public function getEmailAttribute($value): ?string
|
||||
{
|
||||
if (!empty($value)) {
|
||||
return $value; // DB-Wert hat Vorrang
|
||||
}
|
||||
|
||||
$local = (string)($this->attributes['localpart'] ?? '');
|
||||
// Domainname – wenn Relation geladen, daraus; sonst nichts (kein teurer Query hier).
|
||||
$dom = $this->relationLoaded('domain')
|
||||
? (string)($this->domain->domain ?? '')
|
||||
: (string)($this->attributes['domain'] ?? ''); // nur falls bei Joins alias 'domain' selektiert wäre
|
||||
|
||||
if ($local !== '' && $dom !== '') {
|
||||
return "{$local}@{$dom}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* „Adresse“ als bequemer Fallback (identisch zu email(), fällt aber am Ende auf '@dom' NICHT zurück).
|
||||
*/
|
||||
public function getAddressAttribute(): string
|
||||
{
|
||||
return (string)($this->getRawOriginal('email') ?: ($this->email ?? ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionaler Setter für Passwort über virtuelles Attribut "password".
|
||||
*/
|
||||
public function setPasswordAttribute(string $plain): void
|
||||
{
|
||||
$this->attributes['password_hash'] = password_hash($plain, PASSWORD_BCRYPT);
|
||||
}
|
||||
|
||||
/**
|
||||
* KORREKT: DB-Wert hat Vorrang; falls leer → Fallback localpart@domain.domain
|
||||
*/
|
||||
public function getEmailAttribute($value): ?string
|
||||
{
|
||||
if (!empty($value)) {
|
||||
return $value;
|
||||
}
|
||||
$local = (string)($this->attributes['localpart'] ?? $this->localpart ?? '');
|
||||
$dom = $this->relationLoaded('domain')
|
||||
? (string)($this->domain->domain ?? '')
|
||||
: (string)($this->attributes['domain'] ?? '');
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Scopes
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
if ($local !== '' && $dom !== '') {
|
||||
return "{$local}@{$dom}";
|
||||
}
|
||||
// nichts zusammenfummeln → nicht "@"
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adresse ohne Accessor/DB-Fallback erzwingen (z.B. in Queries vor-joined)
|
||||
*/
|
||||
public function getAddressAttribute(): string
|
||||
{
|
||||
$raw = (string)($this->attributes['email'] ?? '');
|
||||
if ($raw !== '') return $raw;
|
||||
|
||||
$local = (string)($this->attributes['localpart'] ?? $this->localpart ?? '');
|
||||
$dom = $this->relationLoaded('domain')
|
||||
? (string)($this->domain->domain ?? '')
|
||||
: (string)($this->attributes['domain'] ?? '');
|
||||
|
||||
return ($local !== '' && $dom !== '') ? "{$local}@{$dom}" : '';
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopeActive($q)
|
||||
{
|
||||
return $q->where('is_active', true);
|
||||
|
|
@ -86,6 +115,87 @@ class MailUser extends Model
|
|||
}
|
||||
|
||||
|
||||
//class MailUser extends Model
|
||||
//{
|
||||
// protected $fillable = [
|
||||
// 'domain_id', 'localpart', 'email', 'display_name', 'password_hash',
|
||||
// 'is_active', 'quota_mb', 'is_system'
|
||||
// ];
|
||||
//
|
||||
// protected $hidden = ['password_hash'];
|
||||
//
|
||||
// protected $casts = [
|
||||
// 'is_active' => 'bool',
|
||||
// 'is_system' => 'bool',
|
||||
// 'quota_mb' => 'int',
|
||||
// 'last_login_at' => 'datetime',
|
||||
// ];
|
||||
//
|
||||
// public function domain(): BelongsTo
|
||||
// {
|
||||
// return $this->belongsTo(Domain::class);
|
||||
// }
|
||||
//
|
||||
// // optional: virtueller Setter
|
||||
// public function setPasswordAttribute(string $plain): void
|
||||
// {
|
||||
// $this->attributes['password_hash'] = password_hash($plain, PASSWORD_BCRYPT);
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * KORREKT: DB-Wert hat Vorrang; falls leer → Fallback localpart@domain.domain
|
||||
// */
|
||||
// public function getEmailAttribute($value): ?string
|
||||
// {
|
||||
// if (!empty($value)) {
|
||||
// return $value;
|
||||
// }
|
||||
// $local = (string)($this->attributes['localpart'] ?? $this->localpart ?? '');
|
||||
// $dom = $this->relationLoaded('domain')
|
||||
// ? (string)($this->domain->domain ?? '')
|
||||
// : (string)($this->attributes['domain'] ?? '');
|
||||
//
|
||||
// if ($local !== '' && $dom !== '') {
|
||||
// return "{$local}@{$dom}";
|
||||
// }
|
||||
// // nichts zusammenfummeln → nicht "@"
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// /**
|
||||
// * Adresse ohne Accessor/DB-Fallback erzwingen (z.B. in Queries vor-joined)
|
||||
// */
|
||||
// public function getAddressAttribute(): string
|
||||
// {
|
||||
// $raw = (string)($this->attributes['email'] ?? '');
|
||||
// if ($raw !== '') return $raw;
|
||||
//
|
||||
// $local = (string)($this->attributes['localpart'] ?? $this->localpart ?? '');
|
||||
// $dom = $this->relationLoaded('domain')
|
||||
// ? (string)($this->domain->domain ?? '')
|
||||
// : (string)($this->attributes['domain'] ?? '');
|
||||
//
|
||||
// return ($local !== '' && $dom !== '') ? "{$local}@{$dom}" : '';
|
||||
// }
|
||||
//
|
||||
// // Scopes
|
||||
// public function scopeActive($q)
|
||||
// {
|
||||
// return $q->where('is_active', true);
|
||||
// }
|
||||
//
|
||||
// public function scopeSystem($q)
|
||||
// {
|
||||
// return $q->where('is_system', true);
|
||||
// }
|
||||
//
|
||||
// public function scopeByEmail($q, string $email)
|
||||
// {
|
||||
// return $q->where('email', $email);
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
//class MailUser extends Model
|
||||
//{
|
||||
//// protected $table = 'mail_users';
|
||||
|
|
|
|||
|
|
@ -54,8 +54,9 @@ return [
|
|||
'mailstats' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/mailstats.log'),
|
||||
'level' => 'debug',
|
||||
'days' => 14,
|
||||
'level' => 'info',
|
||||
'days' => 7,
|
||||
'bubble' => false,
|
||||
],
|
||||
|
||||
'stack' => [
|
||||
|
|
|
|||
|
|
@ -11,9 +11,6 @@ Artisan::command('inspire', function () {
|
|||
|
||||
Schedule::job(RunHealthChecks::class)->everytenSeconds()->withoutOverlapping();
|
||||
//Schedule::command('mailwolt:check-updates')->dailyAt('04:10');
|
||||
Schedule::command('mailwolt:check-updates')->everyFiveMinutes();
|
||||
Schedule::command('mailwolt:check-updates')->everytwoMinutes();
|
||||
|
||||
Schedule::command('mail:update-stats')
|
||||
->everyFiveMinutes()
|
||||
->withoutOverlapping()
|
||||
->appendOutputTo(storage_path('logs/mail_stats.log'));
|
||||
Schedule::command('mail:update-stats')->everyFiveMinutes()->withoutOverlapping();
|
||||
|
|
|
|||
Loading…
Reference in New Issue