generated from Nodarx/template
feat: console commands
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
362
lib/Console/TestCommand.php
Normal file
362
lib/Console/TestCommand.php
Normal file
@@ -0,0 +1,362 @@
|
||||
<?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 KTXC\SessionTenant;
|
||||
use KTXM\ProviderImap\Client\Command\SelectCommand;
|
||||
use KTXM\ProviderImap\Client\FetchOptions;
|
||||
use KTXM\ProviderImap\Client\FetchTarget;
|
||||
use KTXM\ProviderImap\Client\Message;
|
||||
use KTXM\ProviderImap\Client\SequenceSet;
|
||||
use KTXM\ProviderImap\Providers\Provider;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Service\Remote\RemoteService;
|
||||
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 service diagnostic.
|
||||
*
|
||||
* Loads an existing stored IMAP service, connects with its saved credentials,
|
||||
* shows a bounded list of mailboxes, then prints the most recent messages
|
||||
* from a selected mailbox.
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'provider_imap_mail:service:test',
|
||||
description: 'Test an existing IMAP service and inspect mailboxes and messages',
|
||||
)]
|
||||
class TestCommand 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, 'Stored service ID to test')
|
||||
->addOption('tenant', 't', InputOption::VALUE_REQUIRED, 'Tenant ID')
|
||||
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User ID')
|
||||
->addOption('mailbox', 'm', InputOption::VALUE_REQUIRED, 'Specific mailbox to inspect (for example: INBOX)')
|
||||
->addOption('mailbox-limit', null, InputOption::VALUE_REQUIRED, 'Maximum number of mailboxes to inspect (default: 5, 0 = all)', '5')
|
||||
->addOption('message-limit', null, InputOption::VALUE_REQUIRED, 'Maximum number of recent messages to show (default: 5, 0 = none)', '5')
|
||||
->setHelp(<<<'HELP'
|
||||
The <info>provider_imap_mail:service:test</info> command performs a live
|
||||
diagnostic against an already stored IMAP service.
|
||||
|
||||
It will:
|
||||
1. Load the saved service for a tenant/user pair
|
||||
2. Connect and authenticate with the stored credentials
|
||||
3. Show a bounded list of selectable mailboxes
|
||||
4. Show the most recent messages from the selected mailbox
|
||||
|
||||
Examples:
|
||||
|
||||
Interactive service selection:
|
||||
<info>provider_imap_mail:service:test --tenant=t1 --user=u1</info>
|
||||
|
||||
Test a specific stored service:
|
||||
<info>provider_imap_mail:service:test abc123 --tenant=t1 --user=u1</info>
|
||||
|
||||
Show recent messages from INBOX:
|
||||
<info>provider_imap_mail:service:test abc123 --tenant=t1 --user=u1 --mailbox=INBOX</info>
|
||||
|
||||
Show 10 mailboxes and 3 recent messages from Drafts:
|
||||
<info>provider_imap_mail:service:test abc123 --tenant=t1 --user=u1 --mailbox=Drafts --mailbox-limit=10 --message-limit=3</info>
|
||||
HELP);
|
||||
}
|
||||
|
||||
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') ?? '');
|
||||
$mailboxName = trim((string) ($input->getOption('mailbox') ?? 'INBOX'));
|
||||
$mailboxLimit = $this->normalizeLimit($input->getOption('mailbox-limit'), 5);
|
||||
$messageLimit = $this->normalizeLimit($input->getOption('message-limit'), 5);
|
||||
|
||||
$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 ($errors !== []) {
|
||||
$io->error($errors);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->sessionTenant->configureById($tenantId);
|
||||
$service = $this->provider->serviceFetch($tenantId, $userId, $serviceId);
|
||||
|
||||
if ($service === null) {
|
||||
$io->error(sprintf("Service '%s' not found.", $serviceId));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$startedAt = microtime(true);
|
||||
|
||||
try {
|
||||
$client = RemoteService::freshClient($service);
|
||||
$mailService = RemoteService::mailService($service, $client);
|
||||
$mailboxes = $mailService->collectionList();
|
||||
} catch (\Throwable $e) {
|
||||
$io->error('IMAP diagnostic failed: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$mailboxNames = array_keys($mailboxes);
|
||||
sort($mailboxNames);
|
||||
|
||||
if (!in_array($mailboxName, $mailboxNames, true)) {
|
||||
$io->error(sprintf("Mailbox '%s' not found on this service.", $mailboxName));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$visibleMailboxNames = $mailboxLimit === 0
|
||||
? $mailboxNames
|
||||
: array_slice($mailboxNames, 0, $mailboxLimit);
|
||||
|
||||
$io->section('Service');
|
||||
$io->definitionList(
|
||||
['ID' => $serviceId],
|
||||
['Label' => $service->getLabel() ?? $serviceId],
|
||||
['Target' => $this->formatTarget($service)],
|
||||
['Username' => $service->getIdentity()?->getIdentity() ?? '-'],
|
||||
['Mailbox target' => $mailboxName],
|
||||
['Mailbox list size' => $mailboxLimit === 0 ? 'all' : (string) $mailboxLimit],
|
||||
['Recent message count' => (string) $messageLimit],
|
||||
);
|
||||
|
||||
if ($mailboxNames === []) {
|
||||
$io->warning('Authenticated successfully, but no selectable mailboxes were returned.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->section('Mailboxes');
|
||||
$mailboxRows = [];
|
||||
foreach ($visibleMailboxNames as $name) {
|
||||
$mailbox = $mailboxes[$name];
|
||||
$properties = $mailbox->getProperties()->jsonSerialize();
|
||||
$mailboxRows[] = [
|
||||
$name,
|
||||
(string) ($properties['role'] ?? 'custom'),
|
||||
(string) ($mailbox->collection() ?? '-'),
|
||||
(string) ($properties['delimiter'] ?? '/'),
|
||||
$this->formatMailboxAttributes($properties['attributes'] ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
$io->table(
|
||||
['Mailbox', 'Role', 'Parent', 'Delimiter', 'Attributes'],
|
||||
$mailboxRows,
|
||||
);
|
||||
|
||||
if ($mailboxLimit !== 0 && count($visibleMailboxNames) < count($mailboxNames)) {
|
||||
$io->note(sprintf(
|
||||
'Mailbox list truncated to the first %d selectable mailboxes. Increase --mailbox-limit or set it to 0 to inspect all mailboxes.',
|
||||
count($visibleMailboxNames),
|
||||
));
|
||||
}
|
||||
|
||||
$io->section(sprintf('Recent Messages: %s', $mailboxName));
|
||||
|
||||
try {
|
||||
$mailboxClient = RemoteService::freshClient($service);
|
||||
$mailboxService = RemoteService::mailService($service, $mailboxClient);
|
||||
$selectedMailbox = $mailboxClient->perform(new SelectCommand($mailboxName, true));
|
||||
} catch (\Throwable $e) {
|
||||
$io->error('Mailbox inspection failed: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$totalMessages = $selectedMailbox->messages();
|
||||
$io->text(sprintf('Total messages: <info>%d</info>', $totalMessages));
|
||||
|
||||
if ($totalMessages === 0) {
|
||||
$io->note('No messages found in this mailbox.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if ($messageLimit === 0) {
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$sampleCount = min($totalMessages, $messageLimit);
|
||||
$startSequence = max(1, $totalMessages - $sampleCount + 1);
|
||||
$messageRows = [];
|
||||
|
||||
try {
|
||||
foreach ($mailboxService->messageList(
|
||||
$mailboxName,
|
||||
FetchTarget::sequence(SequenceSet::range($startSequence, $totalMessages)),
|
||||
FetchOptions::message(),
|
||||
) as $message) {
|
||||
$messageRows[] = [
|
||||
(string) $message->uid(),
|
||||
$this->formatSubject($message),
|
||||
$this->formatSender($message),
|
||||
$message->internalDate() ?? '-',
|
||||
$this->formatFlags($message),
|
||||
$this->formatBytes($message->size()),
|
||||
];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$io->error('Message listing failed: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$messageRows = array_reverse($messageRows);
|
||||
|
||||
$io->table(
|
||||
['UID', 'Subject', 'From', 'Date', 'Flags', 'Size'],
|
||||
$messageRows,
|
||||
);
|
||||
|
||||
$elapsed = (int) round((microtime(true) - $startedAt) * 1000);
|
||||
$io->text(sprintf('Elapsed: <info>%d ms</info>', $elapsed));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('IMAP Service Diagnostic');
|
||||
|
||||
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')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tenantId = (string) ($input->getOption('tenant') ?? '');
|
||||
$userId = (string) ($input->getOption('user') ?? '');
|
||||
|
||||
if ($tenantId === '' || $userId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sessionTenant->configureById($tenantId);
|
||||
$services = $this->provider->serviceList($tenantId, $userId);
|
||||
|
||||
if ($services === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$choices = [];
|
||||
foreach ($services as $id => $service) {
|
||||
$label = $service->getLabel() ?? (string) $id;
|
||||
$choices[$id] = sprintf('%s [%s] %s', $label, $id, $this->formatTarget($service));
|
||||
}
|
||||
|
||||
$chosen = $io->choice('Select service to test', array_values($choices));
|
||||
$serviceId = (string) array_search($chosen, $choices, true);
|
||||
if ($serviceId !== '') {
|
||||
$input->setArgument('service-id', $serviceId);
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeLimit(mixed $value, int $default): int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$limit = (int) $value;
|
||||
return $limit >= 0 ? $limit : $default;
|
||||
}
|
||||
|
||||
private function formatTarget(Service $service): string
|
||||
{
|
||||
$location = $service->getLocation();
|
||||
if ($location === null) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return sprintf('%s://%s:%d', $location->getEncryption(), $location->getHost(), $location->getPort());
|
||||
}
|
||||
|
||||
private function formatSender(Message $message): string
|
||||
{
|
||||
$sender = $message->from()[0] ?? $message->sender()[0] ?? null;
|
||||
if ($sender === null) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return $sender->name() ?: ($sender->email() ?? '-');
|
||||
}
|
||||
|
||||
private function formatSubject(Message $message): string
|
||||
{
|
||||
$subject = trim((string) ($message->subject() ?? ''));
|
||||
if ($subject === '') {
|
||||
return '(no subject)';
|
||||
}
|
||||
|
||||
return mb_strimwidth($subject, 0, 60, '...');
|
||||
}
|
||||
|
||||
private function formatFlags(Message $message): string
|
||||
{
|
||||
$flags = $message->flags();
|
||||
return $flags === [] ? '-' : implode(', ', $flags);
|
||||
}
|
||||
|
||||
private function formatMailboxAttributes(array $attributes): string
|
||||
{
|
||||
if ($attributes === []) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return implode(', ', $attributes);
|
||||
}
|
||||
|
||||
private function formatBytes(int $bytes): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
$size = (float) max(0, $bytes);
|
||||
$unitIndex = 0;
|
||||
|
||||
while ($size >= 1024 && $unitIndex < count($units) - 1) {
|
||||
$size /= 1024;
|
||||
$unitIndex++;
|
||||
}
|
||||
|
||||
return sprintf($unitIndex === 0 ? '%.0f %s' : '%.1f %s', $size, $units[$unitIndex]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user