refactor: use custom imap client

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-05-08 00:16:43 -04:00
parent a728aeb11c
commit a8764747fd
179 changed files with 6782 additions and 5907 deletions

View File

@@ -1,202 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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 ServiceConnectCommand 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 <info>provider_imap_mail:service:connect</info> 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:
<info>bin/console provider_imap_mail:service:connect</info>
Pre-fill common options:
<info>bin/console provider_imap_mail:service:connect \
--host=mail.example.com --username=user@example.com \
--tenant=t1 --user=u1</info>
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 <info>' . $host . ':' . $port . '</info>…');
$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: <info>{$id}</info>");
return Command::SUCCESS;
}
}

View File

@@ -1,177 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImap\Console;
use KTXM\ProviderImap\Providers\Provider;
use KTXM\ProviderImap\Providers\Service;
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;
/**
* Remove a stored IMAP service connection.
*
* Looks up the service by its ID (or prompts for one), confirms with the
* operator, then permanently deletes the service document from the store.
*
* Usage:
* bin/console provider_imap_mail:service:disconnect --tenant=t1 --user=u1 <service-id>
*/
#[AsCommand(
name: 'provider_imap_mail:service:disconnect',
description: 'Remove a stored IMAP service connection',
)]
class ServiceDisconnectCommand 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 remove')
->addOption('tenant', 't', InputOption::VALUE_REQUIRED, 'Tenant ID')
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User ID')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Skip confirmation prompt')
->setHelp(<<<'HELP'
The <info>provider_imap_mail:service:disconnect</info> command permanently removes
a stored IMAP service configuration from the store.
Examples:
Interactive (lists services and prompts for ID):
<info>bin/console provider_imap_mail:service:disconnect --tenant=t1 --user=u1</info>
Direct, with confirmation:
<info>bin/console provider_imap_mail:service:disconnect abc123 --tenant=t1 --user=u1</info>
Skip confirmation:
<info>bin/console provider_imap_mail:service:disconnect abc123 --tenant=t1 --user=u1 --force</info>
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 no service ID given, list available services and let the operator pick
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)) {
// nothing to select — let execute() handle the error
return;
}
$choices = [];
foreach ($services as $id => $service) {
$label = $service instanceof Service ? ($service->getLabel() ?? $id) : $id;
$choices[$id] = "{$label} [{$id}]";
}
$chosen = $io->choice('Select service to disconnect', array_values($choices));
// resolve the chosen label back to its key
$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') ?? '');
$force = (bool) $input->getOption('force');
$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 service for display ────────────────────────────────────────
$this->sessionTenant->configureById($tenantId);
$service = $this->provider->serviceFetch($tenantId, $userId, $serviceId);
if ($service === null) {
$io->error("Service '{$serviceId}' not found.");
return Command::FAILURE;
}
$label = $service->getLabel() ?? $serviceId;
$host = $service->getLocation()?->getHost() ?? 'unknown';
$io->title('Disconnect IMAP Service');
$io->definitionList(
['ID' => $serviceId],
['Label' => $label],
['Host' => $host],
);
// ── Confirmation ─────────────────────────────────────────────────────
if (!$force) {
$confirm = $io->confirm(
"Permanently remove service <comment>{$label}</comment> ({$serviceId})?",
false
);
if (!$confirm) {
$io->note('Aborted.');
return Command::SUCCESS;
}
}
// ── Destroy ──────────────────────────────────────────────────────────
$deleted = $this->provider->serviceDestroy($tenantId, $userId, $service);
if (!$deleted) {
$io->error("Failed to remove service '{$serviceId}'.");
return Command::FAILURE;
}
$io->success("Service '{$label}' ({$serviceId}) has been removed.");
return Command::SUCCESS;
}
}

View File

@@ -1,226 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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 KTXM\ProviderImap\Service\Discovery;
use KTXM\ProviderImap\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;
/**
* Automated IMAP service discovery.
*
* Probes DNS SRV records and common hostnames to determine the correct IMAP
* server settings for a given e-mail address, then optionally persists the
* resulting service to the store.
*
* Usage:
* bin/console provider_imap_mail:service:discover user@example.com
* bin/console provider_imap_mail:service:discover user@example.com --tenant=t1 --user=u1 --save
*/
#[AsCommand(
name: 'provider_imap_mail:service:discover',
description: 'Auto-discover IMAP server settings from an e-mail address',
)]
class ServiceDiscoverCommand extends Command
{
public function __construct(
private readonly Provider $provider,
private readonly Discovery $discovery,
private readonly SessionTenant $sessionTenant,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('address', InputArgument::OPTIONAL, 'E-mail address to discover settings for')
->addOption('tenant', 't', InputOption::VALUE_REQUIRED, 'Tenant ID (required when --save is set)')
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User ID (required when --save is set)')
->addOption('host', null, InputOption::VALUE_REQUIRED, 'Explicit hostname to probe instead of DNS lookup')
->addOption('no-verify', null, InputOption::VALUE_NONE, 'Disable TLS certificate verification')
->addOption('save', null, InputOption::VALUE_NONE, 'Persist the discovered service after a successful test')
->setHelp(<<<'HELP'
The <info>provider_imap_mail:service:discover</info> command auto-discovers IMAP
server settings by probing DNS SRV records (_imaps._tcp / _imap._tcp) and
common hostname conventions (mail.<domain>, imap.<domain>, ).
Examples:
Dry-run discovery (no persistence):
<info>bin/console provider_imap_mail:service:discover user@example.com</info>
Discover and save under a specific tenant/user pair:
<info>bin/console provider_imap_mail:service:discover user@example.com --tenant=t1 --user=u1 --save</info>
Probe an explicit host:
<info>bin/console provider_imap_mail:service:discover user@example.com --host=mail.example.com</info>
HELP);
}
protected function interact(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
if (!$input->getArgument('address')) {
$address = $io->ask('E-mail address');
if ($address) {
$input->setArgument('address', $address);
}
}
if ($input->getOption('save')) {
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);
}
}
}
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$address = (string) $input->getArgument('address');
$host = $input->getOption('host');
$noVerify = (bool) $input->getOption('no-verify');
$save = (bool) $input->getOption('save');
$tenantId = (string) ($input->getOption('tenant') ?? '');
$userId = (string) ($input->getOption('user') ?? '');
if ($address === '') {
$io->error('An e-mail address is required.');
return Command::FAILURE;
}
if ($save && ($tenantId === '' || $userId === '')) {
$io->error('--tenant and --user are required when --save is set.');
return Command::FAILURE;
}
$io->title('IMAP Service Discovery');
$io->text("Discovering settings for <info>{$address}</info>…");
// ── Discovery ────────────────────────────────────────────────────────
$candidates = $this->discovery->discoverAll(
identity: $address,
location: $host,
verifySSL: !$noVerify,
);
if ($candidates === []) {
$io->error('Discovery failed — no reachable IMAP server found.');
return Command::FAILURE;
}
// ── Build labelled choice list ────────────────────────────────────────
$encLabel = static fn (string $e): string => match ($e) {
'ssl' => 'SSL/TLS',
'starttls' => 'STARTTLS',
default => 'None (plain)',
};
/** @var array<string, ServiceLocation> $choiceMap */
$choiceMap = [];
foreach ($candidates as $c) {
$label = sprintf('%s : %d [%s]', $c->getHost(), $c->getPort(), $encLabel($c->getEncryption()));
$choiceMap[$label] = $c;
}
$io->text(sprintf('<info>%d</info> location(s) found:', count($candidates)));
$io->newLine();
if (count($candidates) === 1) {
$chosenLabel = array_key_first($choiceMap);
} else {
$chosenLabel = $io->choice('Select which server to use', array_keys($choiceMap), array_key_first($choiceMap));
}
$location = $choiceMap[$chosenLabel];
// ── Display chosen result ─────────────────────────────────────────────
$io->success('Server selected:');
$io->definitionList(
['Host' => $location->getHost()],
['Port' => (string) $location->getPort()],
['Encryption' => $encLabel($location->getEncryption())],
);
if (!$save) {
$io->note('Run with --save --tenant=<id> --user=<id> to persist this service.');
return Command::SUCCESS;
}
// ── Interactive credential prompt ────────────────────────────────────
$username = $io->ask('Username', $address);
$password = $io->askHidden('Password');
if (!$username || !$password) {
$io->error('Username and password are required to save the service.');
return Command::FAILURE;
}
// ── Test before saving ───────────────────────────────────────────────
$service = new Service();
$service->setLocation($location);
$service->setIdentity((new ServiceIdentityBasic())->jsonDeserialize([
'identity' => $username,
'secret' => $password,
]));
$io->text('Testing connection…');
$result = $this->provider->serviceTest($service);
if (!$result['success']) {
$io->error('Connection test failed: ' . $result['message']);
return Command::FAILURE;
}
$io->success($result['message']);
// ── Persist ──────────────────────────────────────────────────────────
$this->sessionTenant->configureById($tenantId);
$label = $io->ask('Service label', $address);
if ($label) {
$service->setLabel($label);
}
$id = $this->provider->serviceCreate($tenantId, $userId, $service);
$io->success("Service saved with ID: <info>{$id}</info>");
return Command::SUCCESS;
}
}

View File

@@ -1,203 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* 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\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 <service-id> --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 <info>provider_imap_mail:service:test</info> 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:
<info>bin/console provider_imap_mail:service:test abc123 --tenant=t1 --user=u1</info>
Interactive (lists services, lets you choose one):
<info>bin/console provider_imap_mail:service:test --tenant=t1 --user=u1</info>
Show all mailboxes (not just top-level):
<info>bin/console provider_imap_mail:service:test abc123 --tenant=t1 --user=u1 --all</info>
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() ? '<fg=green>✓</>' : '<fg=yellow></>';
$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('<info>%d</info> %s listed. Latency: <comment>%d ms</comment>.', count($rows), $noun, $latency));
}
} catch (\Throwable $e) {
$io->warning('Could not list mailboxes: ' . $e->getMessage());
}
return Command::SUCCESS;
}
}