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
This commit is contained in:
@@ -2,14 +2,18 @@
|
|||||||
|
|
||||||
namespace App\Filament\Pages;
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use App\Models\ZEmail;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Checkbox;
|
use Filament\Forms\Components\Checkbox;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
use Inerba\DbConfig\AbstractPageSettings;
|
use Inerba\DbConfig\AbstractPageSettings;
|
||||||
|
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||||
|
|
||||||
class ImapSettings extends AbstractPageSettings
|
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
|
public function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
@@ -92,4 +109,216 @@ class ImapSettings extends AbstractPageSettings
|
|||||||
])
|
])
|
||||||
->statePath('data');
|
->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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,16 @@
|
|||||||
namespace App\Filament\Pages;
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\KeyValue;
|
use Filament\Forms\Components\KeyValue;
|
||||||
use Filament\Forms\Components\Repeater;
|
use Filament\Forms\Components\Repeater;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Schemas\Components\Section;
|
use Filament\Schemas\Components\Section;
|
||||||
use Filament\Schemas\Schema;
|
use Filament\Schemas\Schema;
|
||||||
use Filament\Support\Icons\Heroicon;
|
use Filament\Support\Icons\Heroicon;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
use Inerba\DbConfig\AbstractPageSettings;
|
use Inerba\DbConfig\AbstractPageSettings;
|
||||||
|
|
||||||
class WebsiteSettings extends AbstractPageSettings
|
class WebsiteSettings extends AbstractPageSettings
|
||||||
@@ -44,6 +47,25 @@ class WebsiteSettings extends AbstractPageSettings
|
|||||||
return [];
|
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
|
public function form(Schema $schema): Schema
|
||||||
{
|
{
|
||||||
return $schema
|
return $schema
|
||||||
@@ -105,4 +127,38 @@ class WebsiteSettings extends AbstractPageSettings
|
|||||||
])
|
])
|
||||||
->statePath('data');
|
->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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user