generated from Nodarx/template
feat: add cache harmonize command
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<?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\Context\TenantContext;
|
||||
use KTXM\ProviderImap\Providers\Provider;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Service\Cache\HarmonizationService;
|
||||
use KTXM\ProviderImap\Stores\MailboxStore;
|
||||
use KTXM\ProviderImap\Stores\MessageStore;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Harmonize the cache of a stored IMAP service with its server.
|
||||
*
|
||||
* Read-only towards the server (EXAMINE, BODY.PEEK); safe to run repeatedly.
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'provider_imap_mail:cache:harmonize',
|
||||
description: 'Harmonize the message cache of an IMAP service with its server',
|
||||
)]
|
||||
class HarmonizeCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Provider $provider,
|
||||
private readonly TenantContext $tenantContext,
|
||||
private readonly HarmonizationService $harmonizer,
|
||||
private readonly MailboxStore $mailboxStore,
|
||||
private readonly MessageStore $messageStore,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addArgument('service-id', InputArgument::REQUIRED, 'Stored service ID')
|
||||
->addOption('tenant', 't', InputOption::VALUE_REQUIRED, 'Tenant ID')
|
||||
->addOption('user', 'u', InputOption::VALUE_REQUIRED, 'User ID')
|
||||
->addOption('mailbox', 'm', InputOption::VALUE_REQUIRED, 'Mailbox to harmonize (default: INBOX)')
|
||||
->addOption('all', 'a', InputOption::VALUE_NONE, 'Harmonize every selectable mailbox')
|
||||
->setHelp(<<<'HELP'
|
||||
The <info>provider_imap_mail:cache:harmonize</info> command brings the cache of a
|
||||
stored IMAP service in line with its server.
|
||||
|
||||
It will:
|
||||
1. Create the cache indexes (idempotent)
|
||||
2. Harmonize the mailbox list
|
||||
3. Harmonize the messages of one mailbox, or of every selectable mailbox with --all
|
||||
|
||||
Server access is read-only: nothing is marked as read.
|
||||
|
||||
Examples:
|
||||
|
||||
Harmonize INBOX:
|
||||
<info>provider_imap_mail:cache:harmonize abc123 --tenant=t1 --user=u1</info>
|
||||
|
||||
Harmonize a specific mailbox:
|
||||
<info>provider_imap_mail:cache:harmonize abc123 --tenant=t1 --user=u1 --mailbox=Sent</info>
|
||||
|
||||
Harmonize every mailbox:
|
||||
<info>provider_imap_mail:cache:harmonize abc123 --tenant=t1 --user=u1 --all</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');
|
||||
$mailbox = trim((string) ($input->getOption('mailbox') ?? 'INBOX'));
|
||||
$all = (bool) $input->getOption('all');
|
||||
|
||||
$errors = [];
|
||||
if ($tenantId === '') {
|
||||
$errors[] = 'Tenant ID is required (--tenant).';
|
||||
}
|
||||
if ($userId === '') {
|
||||
$errors[] = 'User ID is required (--user).';
|
||||
}
|
||||
if ($all && $input->getOption('mailbox') !== null) {
|
||||
$errors[] = 'Use either --mailbox or --all, not both.';
|
||||
}
|
||||
if ($errors !== []) {
|
||||
$io->error($errors);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->tenantContext->resolveIdentifier($tenantId);
|
||||
$service = $this->provider->serviceFetch($tenantId, $userId, $serviceId);
|
||||
if (!$service instanceof Service) {
|
||||
$io->error(sprintf("Service '%s' not found.", $serviceId));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->mailboxStore->ensureIndexes();
|
||||
$this->messageStore->ensureIndexes();
|
||||
|
||||
$harmonizer = $this->harmonizer->for($service);
|
||||
$startedAt = microtime(true);
|
||||
|
||||
try {
|
||||
if ($all) {
|
||||
$result = $harmonizer->harmonizeAll();
|
||||
$mailboxes = $result['mailboxes'];
|
||||
$messages = $result['messages'];
|
||||
} else {
|
||||
$mailboxes = $harmonizer->harmonizeMailboxes();
|
||||
$messages = [$mailbox => $harmonizer->harmonizeMessages($mailbox)];
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$io->error('Harmonization failed: ' . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->section('Mailboxes');
|
||||
$io->definitionList(
|
||||
['Added' => self::names($mailboxes['added'])],
|
||||
['Updated' => count($mailboxes['updated'])],
|
||||
['Removed' => self::names($mailboxes['removed'])],
|
||||
);
|
||||
|
||||
$io->section('Messages');
|
||||
$rows = [];
|
||||
$failed = 0;
|
||||
foreach ($messages as $name => $outcome) {
|
||||
if (is_string($outcome)) {
|
||||
$failed++;
|
||||
$rows[] = [$name, '<error>failed</error>', '', '', '', '', $outcome];
|
||||
continue;
|
||||
}
|
||||
$rows[] = [
|
||||
$name,
|
||||
$outcome['status'],
|
||||
$outcome['added'],
|
||||
$outcome['updated'],
|
||||
$outcome['removed'],
|
||||
$outcome['complete'] ? 'yes' : 'no',
|
||||
$outcome['reset'] ? 'UIDVALIDITY changed, cache reset' : '',
|
||||
];
|
||||
}
|
||||
$io->table(['Mailbox', 'Status', 'Added', 'Updated', 'Removed', 'Complete', 'Note'], $rows);
|
||||
|
||||
$io->writeln(sprintf(
|
||||
'Cache: <info>storage/%s/provider_imap/%s</info>, MongoDB <info>provider_imap_mail_mailboxes</info> / <info>provider_imap_mail_messages</info>',
|
||||
$tenantId,
|
||||
$serviceId,
|
||||
));
|
||||
$io->writeln(sprintf('Completed in <info>%.2fs</info>', microtime(true) - $startedAt));
|
||||
|
||||
if ($failed > 0) {
|
||||
$io->warning(sprintf('%d mailbox(es) failed.', $failed));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $names
|
||||
*/
|
||||
private static function names(array $names): string
|
||||
{
|
||||
return $names === [] ? '-' : implode(', ', $names);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ use KTXF\Resource\Provider\ProviderInterface;
|
||||
use KTXM\ProviderImap\Console\ConnectCommand;
|
||||
use KTXM\ProviderImap\Console\DiscoverCommand;
|
||||
use KTXM\ProviderImap\Console\DisconnectCommand;
|
||||
use KTXM\ProviderImap\Console\HarmonizeCommand;
|
||||
use KTXM\ProviderImap\Console\TestCommand;
|
||||
use KTXM\ProviderImap\Listeners\UserEventListener;
|
||||
use KTXM\ProviderImap\Providers\Provider as MailProvider;
|
||||
@@ -109,6 +110,7 @@ class Module extends ModuleInstanceAbstract
|
||||
ConnectCommand::class,
|
||||
DisconnectCommand::class,
|
||||
TestCommand::class,
|
||||
HarmonizeCommand::class,
|
||||
] as $command) {
|
||||
$context->registerCommand($command);
|
||||
}
|
||||
|
||||
@@ -98,6 +98,33 @@ class HarmonizationService
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Harmonize the mailbox list, then the messages of every selectable mailbox.
|
||||
*
|
||||
* A failing mailbox does not stop the others; its error is reported instead.
|
||||
*
|
||||
* @return array{mailboxes: array{added: string[], updated: string[], removed: string[]}, messages: array<string, array|string>}
|
||||
*/
|
||||
public function harmonizeAll(): array
|
||||
{
|
||||
[$service] = $this->bound();
|
||||
$mailboxes = $this->harmonizeMailboxes();
|
||||
|
||||
$messages = [];
|
||||
foreach ($this->mailboxStore->list((string) $service->identifier()) as $name => $document) {
|
||||
if (!self::isSelectable($document)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$messages[$name] = $this->harmonizeMessages((string) $name);
|
||||
} catch (\Throwable $e) {
|
||||
$messages[$name] = $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
return ['mailboxes' => $mailboxes, 'messages' => $messages];
|
||||
}
|
||||
|
||||
/**
|
||||
* Harmonize the messages of one mailbox with the server.
|
||||
*
|
||||
@@ -252,6 +279,20 @@ class HarmonizationService
|
||||
return [$this->service, $this->live];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a cached mailbox document describes a mailbox that can be selected.
|
||||
*/
|
||||
private static function isSelectable(array $document): bool
|
||||
{
|
||||
foreach ((array) ($document['properties']['attributes'] ?? []) as $attribute) {
|
||||
if (in_array(strtolower((string) $attribute), ['\\noselect', '\\nonexistent'], true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $names
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user