diff --git a/app/Console/Commands/UpdateMailboxStats.php b/app/Console/Commands/UpdateMailboxStats.php new file mode 100644 index 0000000..bbe4660 --- /dev/null +++ b/app/Console/Commands/UpdateMailboxStats.php @@ -0,0 +1,52 @@ +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; + } +} diff --git a/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php b/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php index 8c4f07a..8fb175b 100644 --- a/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php +++ b/app/Livewire/Ui/Domain/Modal/DomainDnsModal.php @@ -78,7 +78,7 @@ class DomainDnsModal extends ModalComponent } // --- 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"; $dkim = DB::table('dkim_keys') @@ -90,14 +90,14 @@ class DomainDnsModal extends ModalComponent : ($dkim->public_key_txt ?? 'v=DKIM1; k=rsa; p='); $this->dynamic = [ - ['type' => 'CNAME', 'name' => "autoconfig.$this->domainName", 'value' => "$this->domainName."], - ['type' => 'CNAME', 'name' => "autodiscover.$this->domainName", 'value' => "$this->domainName."], + ['type' => 'CNAME', 'name' => "autoconfig.$this->domainName", 'value' => "$mailServerFqdn."], + ['type' => 'CNAME', 'name' => "autodiscover.$this->domainName", 'value' => "$mailServerFqdn."], // SRV Records für Autodiscover und Maildienste - ['type' => 'SRV', 'name' => "_autodiscover._tcp.$this->domainName", 'value' => "0 0 443 {$this->domainName}."], - ['type' => 'SRV', 'name' => "_imaps._tcp.$this->domainName", 'value' => "0 0 993 {$this->domainName}."], - ['type' => 'SRV', 'name' => "_pop3s._tcp.$this->domainName", 'value' => "0 0 995 {$this->domainName}."], - ['type' => 'SRV', 'name' => "_submission._tcp.$this->domainName", 'value' => "0 0 587 {$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 {$mailServerFqdn}."], + ['type' => 'SRV', 'name' => "_pop3s._tcp.$this->domainName", 'value' => "0 0 995 {$mailServerFqdn}."], + ['type' => 'SRV', 'name' => "_submission._tcp.$this->domainName", 'value' => "0 0 587 {$mailServerFqdn}."], // TXT Records ['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 { $fqdn = strtolower(trim($fqdn, ".")); diff --git a/app/Livewire/Ui/Mail/MailboxList.php b/app/Livewire/Ui/Mail/MailboxList.php index a0193ef..eedaa22 100644 --- a/app/Livewire/Ui/Mail/MailboxList.php +++ b/app/Livewire/Ui/Mail/MailboxList.php @@ -4,6 +4,7 @@ namespace App\Livewire\Ui\Mail; use App\Models\Domain; +use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Str; use Livewire\Attributes\On; 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() { @@ -107,9 +121,14 @@ class MailboxList extends Component $domainActive = (bool)($d->is_active ?? true); foreach ($d->mailUsers as $u) { - $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; + $usedMB = (int) round(($u->used_bytes ?? 0) / 1024 / 1024); + $quota = (int)($u->quota_mb ?? 0); + $usage = $quota > 0 ? min(100, (int) round($usedMB / 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); $effective = $domainActive && $mailboxActive; @@ -124,9 +143,11 @@ class MailboxList extends Component 'id' => $u->id, 'localpart' => (string)$u->localpart, 'quota_mb' => $quota, - 'used_mb' => $used, '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_effective_active' => $effective, 'inactive_reason' => $reason, diff --git a/database/migrations/2025_09_27_153311_create_mail_users_table.php b/database/migrations/2025_09_27_153311_create_mail_users_table.php index e2d75a0..759c0c7 100644 --- a/database/migrations/2025_09_27_153311_create_mail_users_table.php +++ b/database/migrations/2025_09_27_153311_create_mail_users_table.php @@ -28,6 +28,9 @@ return new class extends Migration $table->unsignedInteger('quota_mb')->default(0); // 0 = unlimited $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->timestamps(); diff --git a/database/migrations/2025_10_17_152819_add_destination_to_mail_aliases_table.php b/database/migrations/2025_10_17_152819_add_destination_to_mail_aliases_table.php deleted file mode 100644 index 236eb0b..0000000 --- a/database/migrations/2025_10_17_152819_add_destination_to_mail_aliases_table.php +++ /dev/null @@ -1,28 +0,0 @@ -string('destination', 191)->nullable(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('mail_aliases', function (Blueprint $table) { - // - }); - } -}; diff --git a/resources/views/livewire/ui/mail/mailbox-list.blade.php b/resources/views/livewire/ui/mail/mailbox-list.blade.php index 32e49cf..f5ca474 100644 --- a/resources/views/livewire/ui/mail/mailbox-list.blade.php +++ b/resources/views/livewire/ui/mail/mailbox-list.blade.php @@ -13,7 +13,7 @@ class="md:max-w-96 w-full justify-end flex-1 rounded-xl bg-white/5 border border-white/10 px-4 py-2.5 text-white/90 placeholder:text-white/50" placeholder="Suchen (Domain oder Postfach) …"> - @if($domains->count()) + @if($domains->count())
@foreach($domains as $domain) @@ -27,10 +27,10 @@ {{ $domain->domain }}
-{{-- --}} -{{-- {{ ($domain->is_active ?? true) ? 'aktiv' : 'inaktiv' }}--}} -{{-- --}} + {{-- --}} + {{-- {{ ($domain->is_active ?? true) ? 'aktiv' : 'inaktiv' }}--}} + {{-- --}} @@ -47,68 +47,68 @@ {{-- Postfächer --}} -{{--
--}} -{{--
--}} -{{-- @forelse($domain->prepared_mailboxes as $u)--}} + {{--
--}} + {{--
--}} + {{-- @forelse($domain->prepared_mailboxes as $u)--}} -{{--
--}} -{{--
--}} -{{--
--}} -{{-- {{ $u['localpart'] !== '' ? ($u['localpart'].'@'.$domain->domain) : '—' }}--}} -{{--
--}} + {{--
--}} + {{--
--}} + {{--
--}} + {{-- {{ $u['localpart'] !== '' ? ($u['localpart'].'@'.$domain->domain) : '—' }}--}} + {{--
--}} -{{--
--}} -{{-- --}} -{{-- {{ $u['is_effective_active'] ? 'aktiv' : 'inaktiv' }}--}} -{{-- --}} -{{-- @if(!$u['is_effective_active'] && !empty($u['inactive_reason']))--}} -{{-- --}} -{{-- {{ $u['inactive_reason'] }}--}} -{{-- --}} -{{-- @endif--}} + {{--
--}} + {{-- --}} + {{-- {{ $u['is_effective_active'] ? 'aktiv' : 'inaktiv' }}--}} + {{-- --}} + {{-- @if(!$u['is_effective_active'] && !empty($u['inactive_reason']))--}} + {{-- --}} + {{-- {{ $u['inactive_reason'] }}--}} + {{-- --}} + {{-- @endif--}} -{{-- --}} -{{-- Max {{ $u['quota_mb'] }} MiB--}} -{{-- --}} -{{-- --}} -{{-- Verbraucht: {{ $u['used_mb'] }} MiB ({{ $u['usage_percent'] }} %)--}} -{{-- --}} -{{-- --}} -{{-- {{ $u['message_count'] }} E-Mails--}} -{{-- --}} -{{--
--}} -{{--
--}} -{{--
--}} -{{--
--}} -{{--
--}} + {{-- --}} + {{-- Max {{ $u['quota_mb'] }} MiB--}} + {{-- --}} + {{-- --}} + {{-- Verbraucht: {{ $u['used_mb'] }} MiB ({{ $u['usage_percent'] }} %)--}} + {{-- --}} + {{-- --}} + {{-- {{ $u['message_count'] }} E-Mails--}} + {{-- --}} + {{--
--}} + {{--
--}} + {{--
--}} + {{--
--}} + {{--
--}} -{{--
--}} + {{--
--}} -{{-- Aktionen--}} -{{--
--}} -{{-- Bearbeiten--}} -{{-- --}} -{{-- Bearbeiten--}} -{{-- --}} + {{-- Aktionen--}} + {{--
--}} + {{-- Bearbeiten--}} + {{-- --}} + {{-- Bearbeiten--}} + {{-- --}} -{{-- Löschen--}} -{{-- --}} -{{-- Löschen--}} -{{-- --}} -{{--
--}} -{{--
--}} + {{-- Löschen--}} + {{-- --}} + {{-- Löschen--}} + {{-- --}} + {{--
--}} + {{--
--}} -{{-- @empty--}} -{{--
Keine Postfächer
--}} -{{-- @endforelse--}} -{{--
--}} -{{--
--}} + {{-- @empty--}} + {{--
Keine Postfächer
--}} + {{-- @endforelse--}} + {{--
--}} + {{--
--}} {{-- Postfächer (responsive: Cards < md, Table ≥ md) --}}
@@ -150,6 +150,12 @@
+
+ +