Domain Create Modal anpassen Fehler auf Null

main
boban 2025-10-17 18:40:12 +02:00
parent c85dc952d6
commit d99913df06
7 changed files with 169 additions and 104 deletions

View File

@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use App\Models\MailUser;
use Illuminate\Console\Command;
class UpdateMailboxStats extends Command
{
protected $signature = 'mail:update-stats {--user=}';
protected $description = 'Aktualisiert Mailquota und Nachrichtenzahl für alle Mailboxen (oder einen spezifischen Benutzer)';
public function handle(): int
{
$query = MailUser::query()->where('is_active', true);
if ($email = $this->option('user')) {
$query->where('email', $email);
}
$users = $query->get();
foreach ($users as $u) {
$email = $u->email;
$domain = explode('@', $email)[1];
$local = explode('@', $email)[0];
$path = "/var/mail/vhosts/{$domain}/{$local}";
$usedBytes = 0;
$messageCount = 0;
if (is_dir($path)) {
$usedBytes = (int) trim(shell_exec('du -sb '.escapeshellarg($path).' 2>/dev/null | cut -f1'));
}
$out = trim(shell_exec('doveadm mailbox status -u '.escapeshellarg($email).' messages INBOX 2>/dev/null'));
if (preg_match('/messages=(\d+)/', $out, $m)) {
$messageCount = (int) $m[1];
}
$u->update([
'used_bytes' => $usedBytes,
'message_count' => $messageCount,
'stats_refreshed_at' => now(),
]);
$this->info(sprintf("%-35s %6.1f MiB %4d Nachrichten", $email, $usedBytes / 1024 / 1024, $messageCount));
}
return Command::SUCCESS;
}
}

View File

@ -78,7 +78,7 @@ class DomainDnsModal extends ModalComponent
} }
// --- Domain-spezifisch --- // --- Domain-spezifisch ---
$spf = 'v=spf1 mx a -all'; $spf = "v=spf1 a mx ip4:{$ipv4} ip6:{$ipv6} ~all";
$dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100"; $dmarc = "v=DMARC1; p=none; rua=mailto:dmarc@{$this->domainName}; pct=100";
$dkim = DB::table('dkim_keys') $dkim = DB::table('dkim_keys')
@ -90,14 +90,14 @@ class DomainDnsModal extends ModalComponent
: ($dkim->public_key_txt ?? 'v=DKIM1; k=rsa; p='); : ($dkim->public_key_txt ?? 'v=DKIM1; k=rsa; p=');
$this->dynamic = [ $this->dynamic = [
['type' => 'CNAME', 'name' => "autoconfig.$this->domainName", 'value' => "$this->domainName."], ['type' => 'CNAME', 'name' => "autoconfig.$this->domainName", 'value' => "$mailServerFqdn."],
['type' => 'CNAME', 'name' => "autodiscover.$this->domainName", 'value' => "$this->domainName."], ['type' => 'CNAME', 'name' => "autodiscover.$this->domainName", 'value' => "$mailServerFqdn."],
// SRV Records für Autodiscover und Maildienste // SRV Records für Autodiscover und Maildienste
['type' => 'SRV', 'name' => "_autodiscover._tcp.$this->domainName", 'value' => "0 0 443 {$this->domainName}."], ['type' => 'SRV', 'name' => "_autodiscover._tcp.$this->domainName", 'value' => "0 0 443 {$mailServerFqdn}."],
['type' => 'SRV', 'name' => "_imaps._tcp.$this->domainName", 'value' => "0 0 993 {$this->domainName}."], ['type' => 'SRV', 'name' => "_imaps._tcp.$this->domainName", 'value' => "0 0 993 {$mailServerFqdn}."],
['type' => 'SRV', 'name' => "_pop3s._tcp.$this->domainName", 'value' => "0 0 995 {$this->domainName}."], ['type' => 'SRV', 'name' => "_pop3s._tcp.$this->domainName", 'value' => "0 0 995 {$mailServerFqdn}."],
['type' => 'SRV', 'name' => "_submission._tcp.$this->domainName", 'value' => "0 0 587 {$this->domainName}."], ['type' => 'SRV', 'name' => "_submission._tcp.$this->domainName", 'value' => "0 0 587 {$mailServerFqdn}."],
// TXT Records // TXT Records
['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf, 'helpLabel' => 'SPF Record Syntax', 'helpUrl' => 'http://www.open-spf.org/SPF_Record_Syntax/'], ['type' => 'TXT', 'name' => $this->domainName, 'value' => $spf, 'helpLabel' => 'SPF Record Syntax', 'helpUrl' => 'http://www.open-spf.org/SPF_Record_Syntax/'],
@ -106,7 +106,6 @@ class DomainDnsModal extends ModalComponent
]; ];
} }
private function extractZone(string $fqdn): string private function extractZone(string $fqdn): string
{ {
$fqdn = strtolower(trim($fqdn, ".")); $fqdn = strtolower(trim($fqdn, "."));

View File

@ -4,6 +4,7 @@
namespace App\Livewire\Ui\Mail; namespace App\Livewire\Ui\Mail;
use App\Models\Domain; use App\Models\Domain;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Livewire\Attributes\On; use Livewire\Attributes\On;
use Livewire\Component; use Livewire\Component;
@ -57,6 +58,19 @@ class MailboxList extends Component
]); ]);
} }
public function updateMailboxStats()
{
// führe Artisan-Command direkt aus
Artisan::call('mail:update-stats');
$this->dispatch('$refresh');
$this->dispatch('toast',
type: 'done',
badge: 'Mailbox',
title: 'Mailbox aktualisiert',
text: 'Die Mailbox-Statistiken wurden aktualisiert.',
duration: 6000,
);
}
public function render() public function render()
{ {
@ -107,9 +121,14 @@ class MailboxList extends Component
$domainActive = (bool)($d->is_active ?? true); $domainActive = (bool)($d->is_active ?? true);
foreach ($d->mailUsers as $u) { foreach ($d->mailUsers as $u) {
$usedMB = (int) round(($u->used_bytes ?? 0) / 1024 / 1024);
$quota = (int)($u->quota_mb ?? 0); $quota = (int)($u->quota_mb ?? 0);
$used = (int)($u->used_mb ?? 0); $usage = $quota > 0 ? min(100, (int) round($usedMB / max(1,$quota) * 100)) : 0;
$usage = $quota > 0 ? min(100, (int)round($used / max(1, $quota) * 100)) : 0;
// $quota = (int)($u->quota_mb ?? 0);
// $used = (int)($u->used_mb ?? 0);
// $usage = $quota > 0 ? min(100, (int)round($used / max(1, $quota) * 100)) : 0;
$mailboxActive = (bool)($u->is_active ?? true); $mailboxActive = (bool)($u->is_active ?? true);
$effective = $domainActive && $mailboxActive; $effective = $domainActive && $mailboxActive;
@ -124,9 +143,11 @@ class MailboxList extends Component
'id' => $u->id, 'id' => $u->id,
'localpart' => (string)$u->localpart, 'localpart' => (string)$u->localpart,
'quota_mb' => $quota, 'quota_mb' => $quota,
'used_mb' => $used,
'usage_percent' => $usage, 'usage_percent' => $usage,
'message_count' => (int)($u->message_count ?? $u->mails_count ?? 0), 'used_mb' => $usedMB,
'message_count' => (int) ($u->message_count ?? 0),
// 'used_mb' => $used,
// 'message_count' => (int)($u->message_count ?? $u->mails_count ?? 0),
'is_active' => $mailboxActive, 'is_active' => $mailboxActive,
'is_effective_active' => $effective, 'is_effective_active' => $effective,
'inactive_reason' => $reason, 'inactive_reason' => $reason,

View File

@ -28,6 +28,9 @@ return new class extends Migration
$table->unsignedInteger('quota_mb')->default(0); // 0 = unlimited $table->unsignedInteger('quota_mb')->default(0); // 0 = unlimited
$table->unsignedInteger('rate_limit_per_hour')->nullable(); $table->unsignedInteger('rate_limit_per_hour')->nullable();
$table->unsignedInteger('used_bytes')->default(0);
$table->unsignedInteger('message_count')->default(0);
$table->timestamp('stats_refreshed_at')->nullable();
$table->timestamp('last_login_at')->nullable(); $table->timestamp('last_login_at')->nullable();
$table->timestamps(); $table->timestamps();

View File

@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('mail_aliases', function (Blueprint $table) {
$table->string('destination', 191)->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('mail_aliases', function (Blueprint $table) {
//
});
}
};

View File

@ -150,6 +150,12 @@
</div> </div>
</div> </div>
<div class="shrink-0 flex flex-col gap-2"> <div class="shrink-0 flex flex-col gap-2">
<div class="flex justify-end mb-2">
<button wire:click="updateMailboxStats"
class="px-3 py-1 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-700 transition">
Jetzt aktualisieren
</button>
</div>
<button <button
class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-white/80 hover:text-white hover:border-white/20" class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-white/80 hover:text-white hover:border-white/20"
wire:click="openMailboxEdit({{ $u['id'] }})"> wire:click="openMailboxEdit({{ $u['id'] }})">
@ -269,11 +275,13 @@
<td class="px-3 py-2"> <td class="px-3 py-2">
@if(!$u['is_effective_active'] && !empty($u['inactive_reason'])) @if(!$u['is_effective_active'] && !empty($u['inactive_reason']))
<span class="#ml-2 px-2 py-0.5 rounded-full text-xs border border-amber-400/30 text-amber-300 bg-amber-500/10"> <span
class="#ml-2 px-2 py-0.5 rounded-full text-xs border border-amber-400/30 text-amber-300 bg-amber-500/10">
{{ $u['inactive_reason'] }} {{ $u['inactive_reason'] }}
</span> </span>
@else @else
<span class="px-2 py-0.5 rounded-full text-xs border border-emerald-400/30 text-emerald-300 bg-emerald-500/10"> <span
class="px-2 py-0.5 rounded-full text-xs border border-emerald-400/30 text-emerald-300 bg-emerald-500/10">
Aktiv Aktiv
</span> </span>
@endif @endif
@ -296,6 +304,10 @@
<td class="px-3 py-2 rounded-r-xl"> <td class="px-3 py-2 rounded-r-xl">
<div class="flex items-center gap-2 justify-end"> <div class="flex items-center gap-2 justify-end">
<button wire:click="updateMailboxStats"
class="px-3 py-1 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-700 transition">
Jetzt aktualisieren
</button>
<button wire:click="openMailboxEdit({{ $u['id'] }})" <button wire:click="openMailboxEdit({{ $u['id'] }})"
class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-white/80 hover:text-white hover:border-white/20"> class="inline-flex items-center gap-1.5 rounded-lg border border-white/10 bg-white/5 px-3 py-1.5 text-xs text-white/80 hover:text-white hover:border-white/20">
<i class="ph ph-pencil-simple text-[12px]"></i> Bearbeiten <i class="ph ph-pencil-simple text-[12px]"></i> Bearbeiten

View File

@ -2,9 +2,15 @@
use Illuminate\Foundation\Inspiring; use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () { Artisan::command('inspire', function () {
$this->comment(Inspiring::quote()); $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote'); })->purpose('Display an inspiring quote');
\Illuminate\Support\Facades\Schedule::job(\App\Jobs\RunHealthChecks::class)->everytenSeconds()->withoutOverlapping(); Schedule::job(\App\Jobs\RunHealthChecks::class)->everytenSeconds()->withoutOverlapping();
Schedule::command('mail:update-stats')
->everyFiveMinutes()
->withoutOverlapping()
->appendOutputTo(storage_path('logs/mail_stats.log'));