generated from Nodarx/template
227 lines
8.8 KiB
PHP
227 lines
8.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 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;
|
|
}
|
|
}
|