* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderImapMail\Console; use KTXM\ProviderImapMail\Providers\Provider; use KTXM\ProviderImapMail\Providers\Service; use KTXM\ProviderImapMail\Service\Remote\RemoteService; use KTXC\SessionTenant; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** * Live IMAP connection test. * * Connects to the server using a stored service's credentials, authenticates, * and lists the root-level mailboxes with their unread message counts as a * quick end-to-end sanity check. * * Usage: * bin/console provider_imap_mail:service:test --tenant=t1 --user=u1 */ #[AsCommand( name: 'provider_imap_mail:service:test', description: 'Test an IMAP service connection and list root mailboxes', )] class ServiceTestCommand extends Command { public function __construct( private readonly Provider $provider, private readonly SessionTenant $sessionTenant, ) { parent::__construct(); } protected function configure(): void { $this ->addArgument('service-id', InputArgument::OPTIONAL, 'Service ID to test') ->addOption('tenant', 't', InputOption::VALUE_REQUIRED, 'Tenant ID') ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User ID') ->addOption('all', 'a', InputOption::VALUE_NONE, 'List all mailboxes, not just the root level') ->setHelp(<<<'HELP' The provider_imap_mail:service:test command opens a live IMAP connection for a stored service, authenticates, and lists the available mailboxes together with their message counts. Examples: Test a specific service: bin/console provider_imap_mail:service:test abc123 --tenant=t1 --user=u1 Interactive (lists services, lets you choose one): bin/console provider_imap_mail:service:test --tenant=t1 --user=u1 Show all mailboxes (not just top-level): bin/console provider_imap_mail:service:test abc123 --tenant=t1 --user=u1 --all HELP); } protected function interact(InputInterface $input, OutputInterface $output): void { $io = new SymfonyStyle($input, $output); if (!$input->getOption('tenant')) { $tenant = $io->ask('Tenant ID'); if ($tenant) $input->setOption('tenant', $tenant); } if (!$input->getOption('user')) { $user = $io->ask('User ID'); if ($user) $input->setOption('user', $user); } if (!$input->getArgument('service-id')) { $tenantId = (string) ($input->getOption('tenant') ?? ''); $userId = (string) ($input->getOption('user') ?? ''); if ($tenantId !== '' && $userId !== '') { $this->sessionTenant->configureById($tenantId); $services = $this->provider->serviceList($tenantId, $userId); if (!empty($services)) { $choices = []; foreach ($services as $id => $service) { $label = $service instanceof Service ? ($service->getLabel() ?? $id) : $id; $choices[$id] = "{$label} [{$id}]"; } $chosen = $io->choice('Select service to test', array_values($choices)); $serviceId = (string) array_search($chosen, $choices, true); if ($serviceId !== '') { $input->setArgument('service-id', $serviceId); } } } } } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $tenantId = (string) ($input->getOption('tenant') ?? ''); $userId = (string) ($input->getOption('user') ?? ''); $serviceId = (string) ($input->getArgument('service-id') ?? ''); $showAll = (bool) $input->getOption('all'); $errors = []; if ($tenantId === '') $errors[] = 'Tenant ID is required (--tenant).'; if ($userId === '') $errors[] = 'User ID is required (--user).'; if ($serviceId === '') $errors[] = 'Service ID is required.'; if (!empty($errors)) { $io->error($errors); return Command::FAILURE; } // ── Fetch stored service ───────────────────────────────────────────── $this->sessionTenant->configureById($tenantId); $service = $this->provider->serviceFetch($tenantId, $userId, $serviceId); if (!($service instanceof Service)) { $io->error("Service '{$serviceId}' not found."); return Command::FAILURE; } $host = $service->getLocation()?->getHost() ?? 'unknown'; $port = $service->getLocation()?->getPort() ?? 993; $enc = $service->getLocation()?->getEncryption() ?? 'ssl'; $io->title('IMAP Connection Test'); $io->definitionList( ['Service' => $service->getLabel() ?? $serviceId], ['Host' => $host], ['Port' => (string) $port], ['Encryption' => $enc], ['Username' => $service->getIdentity()?->getIdentity() ?? '–'], ); // ── Quick single-call test via Provider ────────────────────────────── $io->text('Authenticating…'); $startTime = microtime(true); $testResult = $this->provider->serviceTest($service); $latency = (int) round((microtime(true) - $startTime) * 1000); if (!$testResult['success']) { $io->error($testResult['message']); return Command::FAILURE; } $io->success($testResult['message']); // ── Mailbox listing ────────────────────────────────────────────────── $io->text('Fetching mailbox list…'); try { $wrapper = RemoteService::freshClient($service); $mailboxes = $wrapper->mailboxes(); $rows = []; foreach ($mailboxes as $mailbox) { // Filter to root-level only unless --all if (!$showAll && substr_count($mailbox->name, $mailbox->hierarchyDelimiter ?: '/') > 0) { continue; } $selectable = $mailbox->isSelectable() ? '✓' : '–'; $rows[] = [ $mailbox->name, $selectable, ]; } if (empty($rows)) { $io->note('No mailboxes found' . ($showAll ? '.' : ' at the root level. Use --all to see all mailboxes.')); } else { $io->table(['Mailbox', 'Selectable'], $rows); $noun = count($rows) === 1 ? 'mailbox' : 'mailboxes'; $io->text(sprintf('%d %s listed. Latency: %d ms.', count($rows), $noun, $latency)); } } catch (\Throwable $e) { $io->warning('Could not list mailboxes: ' . $e->getMessage()); } return Command::SUCCESS; } }