* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderImap\Console; use KTXM\ProviderImap\Providers\Provider; use KTXM\ProviderImap\Providers\Service; use KTXM\ProviderImap\Providers\ServiceIdentityBasic; use KTXM\ProviderImap\Providers\ServiceLocation; 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; /** * Manual IMAP service connection wizard. * * Interactively prompts for all connection details (host, port, encryption, * username, password), runs a live connection test, then optionally persists * the service to the store. * * Usage: * bin/console provider_imap_mail:service:connect * bin/console provider_imap_mail:service:connect --tenant=t1 --user=u1 */ #[AsCommand( name: 'provider_imap_mail:service:connect', description: 'Manually configure and connect an IMAP service', )] class ConnectCommand extends Command { public function __construct( private readonly Provider $provider, private readonly SessionTenant $sessionTenant, ) { parent::__construct(); } protected function configure(): void { $this ->addOption('tenant', 't', InputOption::VALUE_REQUIRED, 'Tenant ID') ->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User ID') ->addOption('host', null, InputOption::VALUE_REQUIRED, 'IMAP server hostname') ->addOption('port', null, InputOption::VALUE_REQUIRED, 'IMAP port (default: 993)') ->addOption('encryption', null, InputOption::VALUE_REQUIRED, 'Encryption: ssl | starttls | none (default: ssl)') ->addOption('username', null, InputOption::VALUE_REQUIRED, 'IMAP username / e-mail') ->addOption('no-verify', null, InputOption::VALUE_NONE, 'Disable TLS certificate verification') ->addOption('no-save', null, InputOption::VALUE_NONE, 'Test connection only; do not persist') ->setHelp(<<<'HELP' The provider_imap_mail:service:connect command walks you through manually configuring an IMAP account. All prompts can be pre-filled via options to support non-interactive / scripted usage. Examples: Fully interactive: bin/console provider_imap_mail:service:connect Pre-fill common options: bin/console provider_imap_mail:service:connect \ --host=mail.example.com --username=user@example.com \ --tenant=t1 --user=u1 HELP); } protected function interact(InputInterface $input, OutputInterface $output): void { $io = new SymfonyStyle($input, $output); $io->title('IMAP Service — Manual Configuration'); 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->getOption('host')) { $host = $io->ask('IMAP server hostname'); if ($host) $input->setOption('host', $host); } if (!$input->getOption('port')) { $port = $io->ask('Port', '993'); if ($port) $input->setOption('port', $port); } if (!$input->getOption('encryption')) { $enc = $io->choice('Encryption', ['ssl', 'starttls', 'none'], 'ssl'); $input->setOption('encryption', $enc); } if (!$input->getOption('username')) { $username = $io->ask('Username / e-mail address'); if ($username) $input->setOption('username', $username); } } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $tenantId = (string) ($input->getOption('tenant') ?? ''); $userId = (string) ($input->getOption('user') ?? ''); $host = (string) ($input->getOption('host') ?? ''); $port = (int) ($input->getOption('port') ?? 993); $encryption = (string) ($input->getOption('encryption') ?? 'ssl'); $username = (string) ($input->getOption('username') ?? ''); $noVerify = (bool) $input->getOption('no-verify'); $noSave = (bool) $input->getOption('no-save'); // ── Validate required fields ───────────────────────────────────────── $errors = []; if ($host === '') $errors[] = 'Hostname is required.'; if ($username === '') $errors[] = 'Username is required.'; if (!$noSave) { if ($tenantId === '') $errors[] = 'Tenant ID is required (or pass --no-save to test only).'; if ($userId === '') $errors[] = 'User ID is required (or pass --no-save to test only).'; } if (!empty($errors)) { $io->error($errors); return Command::FAILURE; } // ── Prompt for password (always interactive — never passed via option) ─ $password = $io->askHidden('Password'); if (!$password) { $io->error('Password is required.'); return Command::FAILURE; } // ── Build service object ──────────────────────────────────────────── $location = new ServiceLocation( host: $host, port: $port > 0 ? $port : 993, encryption: $encryption, verifyPeer: !$noVerify, verifyPeerName: !$noVerify, allowSelfSigned: $noVerify, ); $identity = (new ServiceIdentityBasic())->jsonDeserialize([ 'identity' => $username, 'secret' => $password, ]); $service = new Service(); $service->setLocation($location); $service->setIdentity($identity); // ── Test connection ────────────────────────────────────────────────── $io->text('Testing connection to ' . $host . ':' . $port . '…'); $result = $this->provider->serviceTest($service); if (!$result['success']) { $io->error('Connection test failed: ' . $result['message']); return Command::FAILURE; } $io->success($result['message']); if ($noSave) { $io->note('Connection test passed. Service not saved (--no-save).'); return Command::SUCCESS; } // ── Persist ────────────────────────────────────────────────────────── $this->sessionTenant->configureById($tenantId); $label = $io->ask('Service label', $username); if ($label) { $service->setLabel($label); } $id = $this->provider->serviceCreate($tenantId, $userId, $service); $io->success("Service saved with ID: {$id}"); return Command::SUCCESS; } }