From 7ac0a436b1cfd44f74ca45b80cdf37ae85b66b9d Mon Sep 17 00:00:00 2001 From: idevakk <219866223+idevakk@users.noreply.github.com> Date: Mon, 17 Nov 2025 05:27:19 -0800 Subject: [PATCH] feat: add IMAP connection testing and website settings optimization - Add dynamic IMAP connection testing for multiple account types (public, premium) - Implement testIMAPConnection method using ZEmail::connectMailBox for reliable testing - Add comprehensive error handling and user-friendly notifications - Support easy extension for future IMAP configurations (vip, etc.) - Add queued artisan command execution in WebsiteSettings (optimize, optimize:clear) - Enhance website settings with performance optimization controls - Add validation for IMAP extension availability and helpful error messages --- app/Filament/Pages/ImapSettings.php | 229 +++++++++++++++++++++++++ app/Filament/Pages/WebsiteSettings.php | 56 ++++++ 2 files changed, 285 insertions(+) diff --git a/app/Filament/Pages/ImapSettings.php b/app/Filament/Pages/ImapSettings.php index e7a45a9..b412138 100644 --- a/app/Filament/Pages/ImapSettings.php +++ b/app/Filament/Pages/ImapSettings.php @@ -2,14 +2,18 @@ namespace App\Filament\Pages; +use App\Models\ZEmail; use BackedEnum; +use Filament\Actions\Action; use Filament\Forms\Components\Checkbox; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; +use Filament\Notifications\Notification; use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; use Inerba\DbConfig\AbstractPageSettings; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; class ImapSettings extends AbstractPageSettings { @@ -48,6 +52,19 @@ class ImapSettings extends AbstractPageSettings ]; } + public function getHeaderActions(): array + { + return [ + Action::make('test_connection') + ->label('Test Connection') + ->color('gray') + ->icon('heroicon-o-paper-airplane') + ->action(fn () => $this->testIMAPConnection()), + + ...parent::getHeaderActions(), + ]; + } + public function form(Schema $schema): Schema { return $schema @@ -92,4 +109,216 @@ class ImapSettings extends AbstractPageSettings ]) ->statePath('data'); } + + private function testIMAPConnection(): void + { + $settings = $this->data; + $results = []; + $hasSuccess = false; + $hasFailure = false; + + // Define IMAP configuration sections - easy to extend + $imapSections = ['public', 'premium']; + + foreach ($imapSections as $sectionName) { + $sectionConfig = $this->getImapSectionConfig($settings, $sectionName); + $result = $this->testSingleImapConnection($sectionName, $sectionConfig); + $results[] = $result; + + if ($result['success']) { + $hasSuccess = true; + } else { + $hasFailure = true; + } + } + + // Send appropriate notification based on results + $this->sendImapTestNotification($results, $hasSuccess, $hasFailure); + } + + /** + * Get IMAP section configuration for a specific parent key. + */ + private function getImapSectionConfig(array $settings, string $sectionName): array + { + $config = []; + $fields = ['host', 'port', 'username', 'password', 'encryption', 'validate_cert', 'protocol']; + + foreach ($fields as $field) { + $key = $sectionName . '.' . $field; + + // Try different data structure approaches + $value = null; + + // 1. Direct key access (flat structure with dots) + if (isset($settings[$key])) { + $value = $settings[$key]; + } + // 2. Nested structure access + elseif (isset($settings[$sectionName][$field])) { + $value = $settings[$sectionName][$field]; + } + // 3. Alternative nested structure + elseif (isset($settings[$sectionName]) && is_array($settings[$sectionName])) { + $nested = $settings[$sectionName]; + if (isset($nested[$field])) { + $value = $nested[$field]; + } + } + + $config[$field] = $value; + } + + return $config; + } + + /** + * Test a single IMAP connection configuration. + */ + private function testSingleImapConnection(string $sectionName, array $config): array + { + $requiredFields = ['host', 'port', 'username', 'password']; + + // Check for missing required fields + $missingFields = collect($requiredFields)->filter(fn ($field): bool => empty($config[$field])); + + if ($missingFields->isNotEmpty()) { + return [ + 'section' => ucfirst($sectionName), + 'success' => false, + 'message' => "Missing required fields: " . $missingFields->join(', '), + 'details' => null + ]; + } + + try { + // First check if IMAP extension is available + if (! function_exists('imap_open')) { + return [ + 'section' => ucfirst($sectionName), + 'success' => false, + 'message' => 'IMAP extension is not loaded in your web server. Please check your Herd PHP configuration or restart your server.', + 'details' => null + ]; + } + + // Build IMAP configuration array in the format ZEmail expects + $imapConfig = [ + 'host' => $config['host'], + 'port' => (int) $config['port'], + 'username' => $config['username'], + 'password' => $config['password'], + 'encryption' => $config['encryption'] ?? 'none', + 'validate_cert' => $config['validate_cert'] ?? false, + 'protocol' => $config['protocol'] ?? 'imap' + ]; + + // Test connection using the existing ZEmail::connectMailBox method + ZEmail::connectMailBox($imapConfig); + + return [ + 'section' => ucfirst($sectionName), + 'success' => true, + 'message' => 'Connection successful', + 'details' => [ + 'host' => $config['host'], + 'port' => $config['port'], + 'encryption' => $config['encryption'] ?? 'none', + 'protocol' => $config['protocol'] ?? 'imap' + ] + ]; + + } catch (\Exception $e) { + $errorMessage = $e->getMessage(); + + // Provide more helpful error messages + if (str_contains($errorMessage, 'IMAP extension must be enabled')) { + $errorMessage = 'IMAP extension is not properly loaded in the web server. Try restarting Herd or check your PHP configuration.'; + } + + return [ + 'section' => ucfirst($sectionName), + 'success' => false, + 'message' => $errorMessage, + 'details' => [ + 'host' => $config['host'] ?? null, + 'port' => $config['port'] ?? null, + 'encryption' => $config['encryption'] ?? 'none', + 'protocol' => $config['protocol'] ?? 'imap' + ] + ]; + } + } + + + /** + * Send appropriate notification based on test results. + */ + private function sendImapTestNotification(array $results, bool $hasSuccess, bool $hasFailure): void + { + $totalTests = count($results); + $successCount = count(array_filter($results, fn ($r): bool => $r['success'])); + + if ($hasSuccess && ! $hasFailure) { + // All successful + Notification::make() + ->title("All IMAP connections successful! ({$successCount}/{$totalTests})") + ->success() + ->body($this->formatSuccessNotification($results)) + ->send(); + } elseif (! $hasSuccess && $hasFailure) { + // All failed + Notification::make() + ->title("All IMAP connections failed (0/{$totalTests})") + ->danger() + ->body($this->formatFailureNotification($results)) + ->send(); + } else { + // Mixed results + Notification::make() + ->title("IMAP connection test completed ({$successCount}/{$totalTests} successful)") + ->warning() + ->body($this->formatMixedNotification($results)) + ->send(); + } + } + + /** + * Format success notification details. + */ + private function formatSuccessNotification(array $results): string + { + $details = []; + foreach ($results as $result) { + if ($result['success'] && isset($result['details']['messages'])) { + $details[] = "{$result['section']}: {$result['details']['messages']} messages"; + } + } + return implode(' | ', $details); + } + + /** + * Format failure notification details. + */ + private function formatFailureNotification(array $results): string + { + $details = []; + foreach ($results as $result) { + $details[] = "{$result['section']}: {$result['message']}"; + } + return implode(' | ', $details); + } + + /** + * Format mixed results notification details. + */ + private function formatMixedNotification(array $results): string + { + $details = []; + foreach ($results as $result) { + $status = $result['success'] ? '✅' : '❌'; + $details[] = "{$status} {$result['section']}"; + } + return implode(' | ', $details); + } } diff --git a/app/Filament/Pages/WebsiteSettings.php b/app/Filament/Pages/WebsiteSettings.php index 715509e..68e4a53 100644 --- a/app/Filament/Pages/WebsiteSettings.php +++ b/app/Filament/Pages/WebsiteSettings.php @@ -3,13 +3,16 @@ namespace App\Filament\Pages; use BackedEnum; +use Filament\Actions\Action; use Filament\Forms\Components\KeyValue; use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; +use Filament\Notifications\Notification; use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; use Filament\Support\Icons\Heroicon; +use Illuminate\Support\Facades\Artisan; use Inerba\DbConfig\AbstractPageSettings; class WebsiteSettings extends AbstractPageSettings @@ -44,6 +47,25 @@ class WebsiteSettings extends AbstractPageSettings return []; } + public function getHeaderActions(): array + { + return [ + Action::make('filament-optimize') + ->label('Optimize Application') + ->color('gray') + ->icon('heroicon-o-paper-airplane') + ->action(fn () => $this->appOptimize()), + + Action::make('filament-optimize-clear') + ->label('Clear Optimized Files') + ->color('danger') + ->icon('heroicon-o-trash') + ->action(fn () => $this->appOptimizeClear()), + + ...parent::getHeaderActions(), + ]; + } + public function form(Schema $schema): Schema { return $schema @@ -105,4 +127,38 @@ class WebsiteSettings extends AbstractPageSettings ]) ->statePath('data'); } + + private function appOptimize(): void + { + try { + \Artisan::queue('optimize'); + Notification::make() + ->title('App optimization successful!') + ->success() + ->send(); + } catch (\Exception $e) { + \Log::error('App optimization failed', ['exception' => $e->getMessage()]); + Notification::make() + ->title('App optimization failed: '.$e->getMessage()) + ->danger() + ->send(); + } + } + + private function appOptimizeClear(): void + { + try { + Artisan::queue('optimize:clear'); + Notification::make() + ->title('Cache files clear successful!') + ->success() + ->send(); + } catch (\Exception $e) { + \Log::error('App Optimize clear failed', ['exception' => $e->getMessage()]); + Notification::make() + ->title('Failed to clear cache files: '.$e->getMessage()) + ->danger() + ->send(); + } + } }