Files
provider_imap/lib/Console/HarmonizeCommand.php
T
2026-10-07 21:49:31 -04:00

187 lines
6.8 KiB
PHP

<?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\ServiceBase;
use KTXM\ProviderImap\Service\Cache\HarmonizationService;
use KTXM\ProviderImap\Service\Cache\MessageDeltaService;
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 ServiceBase) {
$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;
}
$state = $this->mailboxStore->state($serviceId, (string) $name);
$rows[] = [
$name,
$outcome['status'],
$outcome['added'],
$outcome['updated'],
$outcome['removed'],
$outcome['complete'] ? 'yes' : 'no',
$state['uidValidity'] !== null
? MessageDeltaService::signature((int) $state['uidValidity'], (int) $state['changeSeq'])
: '',
$outcome['reset'] ? 'UIDVALIDITY changed, cache reset' : '',
];
}
$io->table(['Mailbox', 'Status', 'Added', 'Updated', 'Removed', 'Complete', 'Signature', '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);
}
}