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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user