refactor: remove remote service

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-06-23 16:26:53 -04:00
parent 3dd9c2f983
commit d94b1ea211
6 changed files with 200 additions and 116 deletions
-1
View File
@@ -14,7 +14,6 @@ use KTXM\ProviderImap\Providers\Service;
use KTXM\ProviderImap\Providers\ServiceIdentityBasic; use KTXM\ProviderImap\Providers\ServiceIdentityBasic;
use KTXM\ProviderImap\Providers\ServiceLocation; use KTXM\ProviderImap\Providers\ServiceLocation;
use KTXM\ProviderImap\Service\Discovery; use KTXM\ProviderImap\Service\Discovery;
use KTXM\ProviderImap\Service\Remote\RemoteService;
use KTXC\SessionTenant; use KTXC\SessionTenant;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
+4 -6
View File
@@ -17,7 +17,7 @@ use KTXM\ProviderImap\Client\Message;
use KTXM\ProviderImap\Client\SequenceSet; use KTXM\ProviderImap\Client\SequenceSet;
use KTXM\ProviderImap\Providers\Provider; use KTXM\ProviderImap\Providers\Provider;
use KTXM\ProviderImap\Providers\Service; use KTXM\ProviderImap\Providers\Service;
use KTXM\ProviderImap\Service\Remote\RemoteService; use KTXM\ProviderImap\Service\Remote\RemoteMailService;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@@ -119,8 +119,7 @@ class TestCommand extends Command
$startedAt = microtime(true); $startedAt = microtime(true);
try { try {
$client = RemoteService::freshClient($service); $mailService = new RemoteMailService($service);
$mailService = RemoteService::mailService($service, $client);
$mailboxes = $mailService->collectionList(); $mailboxes = $mailService->collectionList();
} catch (\Throwable $e) { } catch (\Throwable $e) {
$io->error('IMAP diagnostic failed: ' . $e->getMessage()); $io->error('IMAP diagnostic failed: ' . $e->getMessage());
@@ -184,9 +183,8 @@ class TestCommand extends Command
$io->section(sprintf('Recent Messages: %s', $mailboxName)); $io->section(sprintf('Recent Messages: %s', $mailboxName));
try { try {
$mailboxClient = RemoteService::freshClient($service); $mailboxService = new RemoteMailService($service);
$mailboxService = RemoteService::mailService($service, $mailboxClient); $selectedMailbox = $mailboxService->imapClient()->perform(new SelectCommand($mailboxName, true));
$selectedMailbox = $mailboxClient->perform(new SelectCommand($mailboxName, true));
} catch (\Throwable $e) { } catch (\Throwable $e) {
$io->error('Mailbox inspection failed: ' . $e->getMessage()); $io->error('Mailbox inspection failed: ' . $e->getMessage());
return Command::FAILURE; return Command::FAILURE;
+3 -4
View File
@@ -18,7 +18,7 @@ use KTXF\Mail\Service\ServiceMutableInterface;
use KTXF\Resource\Provider\ResourceServiceLocationInterface; use KTXF\Resource\Provider\ResourceServiceLocationInterface;
use KTXF\Resource\Provider\ResourceServiceMutateInterface; use KTXF\Resource\Provider\ResourceServiceMutateInterface;
use KTXM\ProviderImap\Service\Discovery; use KTXM\ProviderImap\Service\Discovery;
use KTXM\ProviderImap\Service\Remote\RemoteService; use KTXM\ProviderImap\Service\Remote\RemoteMailService;
use KTXM\ProviderImap\Stores\ServiceStore; use KTXM\ProviderImap\Stores\ServiceStore;
/** /**
@@ -192,9 +192,8 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
$service->fromStore(['sid' => 'test']); $service->fromStore(['sid' => 'test']);
// Attempt to authenticate and list mailboxes as a connectivity check // Attempt to authenticate and list mailboxes as a connectivity check
$client = RemoteService::freshClient($service); $remote = new RemoteMailService($service);
$service = RemoteService::mailService($service, $client); $mailboxes = iterator_to_array($remote->collectionList());
$mailboxes = iterator_to_array($service->collectionList());
$latency = (int) round((microtime(true) - $startTime) * 1000); $latency = (int) round((microtime(true) - $startTime) * 1000);
+78 -4
View File
@@ -17,8 +17,10 @@ use KTXF\Mail\Object\AddressInterface;
use KTXF\Mail\Service\ServiceBaseInterface; use KTXF\Mail\Service\ServiceBaseInterface;
use KTXF\Mail\Service\ServiceCollectionMutableInterface; use KTXF\Mail\Service\ServiceCollectionMutableInterface;
use KTXF\Mail\Service\ServiceEntityMutableInterface; use KTXF\Mail\Service\ServiceEntityMutableInterface;
use KTXF\Mail\Service\ServiceEntitySubmitInterface;
use KTXF\Mail\Service\ServiceConfigurableInterface; use KTXF\Mail\Service\ServiceConfigurableInterface;
use KTXF\Mail\Service\ServiceMutableInterface; use KTXF\Mail\Service\ServiceMutableInterface;
use KTXF\Mail\Submission\EntitySubmitResult;
use KTXF\Resource\BinaryResource; use KTXF\Resource\BinaryResource;
use KTXF\Resource\Provider\ResourceServiceIdentityInterface; use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
use KTXF\Resource\Provider\ResourceServiceLocationInterface; use KTXF\Resource\Provider\ResourceServiceLocationInterface;
@@ -35,8 +37,8 @@ use KTXF\Resource\Sort\ISort;
use KTXF\Resource\Sort\Sort; use KTXF\Resource\Sort\Sort;
use KTXM\ProviderImap\Providers\ServiceIdentityBasic; use KTXM\ProviderImap\Providers\ServiceIdentityBasic;
use KTXM\ProviderImap\Providers\ServiceLocation; use KTXM\ProviderImap\Providers\ServiceLocation;
use KTXM\ProviderImap\Mime\MessageBuilder;
use KTXM\ProviderImap\Service\Remote\RemoteMailService; use KTXM\ProviderImap\Service\Remote\RemoteMailService;
use KTXM\ProviderImap\Service\Remote\RemoteService;
use KTXM\ProviderImap\Providers\CollectionResource; use KTXM\ProviderImap\Providers\CollectionResource;
use KTXF\Mail\Collection\CollectionRoles; use KTXF\Mail\Collection\CollectionRoles;
use KTXF\Mail\Object\MessagePropertiesMutableInterface; use KTXF\Mail\Object\MessagePropertiesMutableInterface;
@@ -44,7 +46,7 @@ use KTXF\Resource\Identifier\EntityIdentifierInterface;
use KTXM\ProviderImap\Providers\EntityResource; use KTXM\ProviderImap\Providers\EntityResource;
use KTXM\ProviderImap\Client\Mailbox; use KTXM\ProviderImap\Client\Mailbox;
class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface, ServiceEntitySubmitInterface
{ {
private const PROVIDER_IDENTIFIER = 'imap'; private const PROVIDER_IDENTIFIER = 'imap';
@@ -116,8 +118,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
private function initialize(): void private function initialize(): void
{ {
if (!isset($this->remoteService)) { if (!isset($this->remoteService)) {
$wrapper = RemoteService::freshClient($this); $this->remoteService = new RemoteMailService($this);
$this->remoteService = RemoteService::mailService($this, $wrapper);
} }
} }
@@ -624,6 +625,79 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
return new EntityResource($this->provider(), $this->identifier()); return new EntityResource($this->provider(), $this->identifier());
} }
public function entitySubmit(AddressInterface $sender, EntityIdentifierInterface|null $source = null, MessagePropertiesMutableInterface|null $message = null): EntitySubmitResult
{
if ($message === null) {
return new EntitySubmitResult(
EntitySubmitResult::DISPOSITION_ERROR,
errorCode: 'invalid_message',
errorMessage: 'No message properties were provided for submission.',
);
}
// Outbound submission via SMTP — independent of the IMAP connection.
try {
$raw = (new MessageBuilder())->build($message);
$recipients = MessageBuilder::recipients($message);
if ($recipients === []) {
throw new \RuntimeException('Message has no recipients.');
}
$this->initialize();
$smtp = $this->remoteService->submitClient();
try {
$queueId = $smtp->send(trim($sender->getAddress()), $recipients, $raw);
} finally {
$smtp->quit();
}
} catch (\Throwable $e) {
return new EntitySubmitResult(
EntitySubmitResult::DISPOSITION_ERROR,
errorCode: 'submission_failed',
errorMessage: $e->getMessage(),
);
}
// Best-effort: store a copy in the Sent collection. A failure here must
// not turn a successful delivery into an error.
$sentEntity = null;
try {
$this->initialize();
$sentCollection = $this->resolveSentCollection();
if ($sentCollection !== null) {
$uid = $this->remoteService->entityCreate($sentCollection, $raw, ['\\Seen']);
if ($uid !== null && $uid > 0) {
$sentEntity = new EntityIdentifier($this->provider(), $this->identifier(), $sentCollection, (string) $uid);
}
}
} catch (\Throwable) {
// ignore — the message was already delivered
}
return new EntitySubmitResult(
disposition: EntitySubmitResult::DISPOSITION_SENT,
transportId: $queueId !== '' ? $queueId : null,
sentEntity: $sentEntity,
);
}
/**
* Resolve the native name of the collection flagged with the Sent role.
*/
private function resolveSentCollection(): ?string
{
$filter = $this->collectionListFilter();
$filter->condition('role', CollectionRoles::Sent->value);
/** @var Mailbox[] $mailboxes */
$mailboxes = iterator_to_array($this->remoteService->collectionList(null, $filter, null));
if ($mailboxes === []) {
return null;
}
$mailbox = reset($mailboxes);
return $mailbox === false ? null : $mailbox->name();
}
public function entityCreate(CollectionIdentifier $target, MessagePropertiesMutableInterface $properties, array $options = []): EntityResource public function entityCreate(CollectionIdentifier $target, MessagePropertiesMutableInterface $properties, array $options = []): EntityResource
{ {
throw new \RuntimeException('Entity creation is not supported in this service'); throw new \RuntimeException('Entity creation is not supported in this service');
+115 -40
View File
@@ -11,7 +11,9 @@ namespace KTXM\ProviderImap\Service\Remote;
use DateTimeImmutable; use DateTimeImmutable;
use Generator; use Generator;
use KTXM\ProviderImap\Client\Client; use KTXC\Server;
use KTXC\Logger\PlainFileLogger;
use KTXM\ProviderImap\Client\Client as ImapClient;
use KTXM\ProviderImap\Client\Command\AppendCommand; use KTXM\ProviderImap\Client\Command\AppendCommand;
use KTXM\ProviderImap\Client\Command\FetchManyCommand; use KTXM\ProviderImap\Client\Command\FetchManyCommand;
use KTXM\ProviderImap\Client\Command\FetchOneCommand; use KTXM\ProviderImap\Client\Command\FetchOneCommand;
@@ -48,6 +50,9 @@ use KTXF\Resource\Range\RangeAnchorType;
use KTXF\Resource\Range\RangeTally; use KTXF\Resource\Range\RangeTally;
use KTXF\Resource\Sort\ISort; use KTXF\Resource\Sort\ISort;
use KTXF\Resource\BinaryResource; use KTXF\Resource\BinaryResource;
use KTXM\ProviderImap\Providers\Service;
use KTXM\ProviderImap\Smtp\Client as SmtpClient;
use RuntimeException;
/** /**
* IMAP Remote Mail Service * IMAP Remote Mail Service
@@ -58,10 +63,80 @@ class RemoteMailService
private const COLLECTION_FILTER_OPTIONS = ['name', 'role', 'subscription']; private const COLLECTION_FILTER_OPTIONS = ['name', 'role', 'subscription'];
private const DEFAULT_MAILBOX_STATUS_ITEMS = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY']; private const DEFAULT_MAILBOX_STATUS_ITEMS = ['MESSAGES', 'UNSEEN', 'RECENT', 'UIDNEXT', 'UIDVALIDITY'];
private ?ImapClient $imapClient = null;
private ?SmtpClient $smtpClient = null;
public function __construct( public function __construct(
private readonly Client $client, private readonly Service $service,
) {} ) {}
/**
* The connected IMAP client, established on first use.
*/
public function imapClient(): ImapClient
{
if ($this->imapClient instanceof ImapClient) {
return $this->imapClient;
}
$location = $this->service->getLocation();
if ($location === null || $location->getInboundHost() === '') {
throw new RuntimeException('No IMAP (inbound) host is configured for this service.');
}
$identity = $this->service->getIdentity();
$config = $location->toConnectionConfig(
$identity?->getIdentity(),
$identity?->getSecret(),
);
$client = new ImapClient(logger: $this->logger('imap'));
$client->connect($config);
return $this->imapClient = $client;
}
/**
* The connected SMTP client, established on first use.
*/
public function smtpClient(): SmtpClient
{
if ($this->smtpClient instanceof SmtpClient) {
return $this->smtpClient;
}
$location = $this->service->getLocation();
if ($location === null || $location->getOutboundHost() === '') {
throw new RuntimeException('No SMTP submission (outbound) host is configured for this service.');
}
$identity = $this->service->getIdentity();
$config = $location->toSmtpConnectionConfig(
$identity?->getIdentity(),
$identity?->getSecret(),
);
$client = new SmtpClient(logger: $this->logger('smtp'));
$client->connect($config);
return $this->smtpClient = $client;
}
/**
* Build a per-protocol file logger when the service has debug enabled.
*/
private function logger(string $channel): ?PlainFileLogger
{
if (!$this->service->getDebug()) {
return null;
}
$logDir = Server::getInstance()?->logDir() ?? __DIR__ . '/../../../../../var/log';
return new PlainFileLogger($logDir . '/' . $channel, $this->service->identifier());
}
/** /**
* list of collections in remote storage * list of collections in remote storage
* *
@@ -79,7 +154,7 @@ class RemoteMailService
$location = ''; $location = '';
} }
// construct the most efficient LIST command based on server capabilities // construct the most efficient LIST command based on server capabilities
if ($this->client->hasCapability('LIST-STATUS') && !empty($depth)) { if ($this->imapClient()->hasCapability('LIST-STATUS') && !empty($depth)) {
$command = new ListCommand($location, $depth, null, ListReturnOptions::status(...self::DEFAULT_MAILBOX_STATUS_ITEMS)); $command = new ListCommand($location, $depth, null, ListReturnOptions::status(...self::DEFAULT_MAILBOX_STATUS_ITEMS));
$rfc5258 = true; $rfc5258 = true;
} else { } else {
@@ -88,7 +163,7 @@ class RemoteMailService
} }
// retrieve list of mailboxes from remote // retrieve list of mailboxes from remote
$mailboxes = []; $mailboxes = [];
foreach ($this->client->perform($command) as $mailbox) { foreach ($this->imapClient()->perform($command) as $mailbox) {
// apply filter // apply filter
if ($filter === null || $this->mailboxFilter($mailbox, $filter)) { if ($filter === null || $this->mailboxFilter($mailbox, $filter)) {
if ($rfc5258) { if ($rfc5258) {
@@ -106,7 +181,7 @@ class RemoteMailService
continue; continue;
} }
try { try {
$status = $this->client->perform(new StatusCommand($mailbox->name(), self::DEFAULT_MAILBOX_STATUS_ITEMS)); $status = $this->imapClient()->perform(new StatusCommand($mailbox->name(), self::DEFAULT_MAILBOX_STATUS_ITEMS));
$mailbox = $mailbox->fromStatus($status); $mailbox = $mailbox->fromStatus($status);
} catch (ImapException) { } catch (ImapException) {
// do nothing // do nothing
@@ -126,13 +201,13 @@ class RemoteMailService
public function collectionFetch(string $identifier): ?Mailbox public function collectionFetch(string $identifier): ?Mailbox
{ {
// retrieve mailbox from remote // retrieve mailbox from remote
$mailbox = iterator_to_array($this->client->perform(new ListCommand('', $identifier, null, ListReturnOptions::status(...self::DEFAULT_MAILBOX_STATUS_ITEMS)))); $mailbox = iterator_to_array($this->imapClient()->perform(new ListCommand('', $identifier, null, ListReturnOptions::status(...self::DEFAULT_MAILBOX_STATUS_ITEMS))));
if (empty($mailbox)) { if (empty($mailbox)) {
return null; return null;
} }
$mailbox = reset($mailbox); $mailbox = reset($mailbox);
// enrich with STATUS // enrich with STATUS
$status = $this->client->perform(new StatusCommand($mailbox->name(), self::DEFAULT_MAILBOX_STATUS_ITEMS)); $status = $this->imapClient()->perform(new StatusCommand($mailbox->name(), self::DEFAULT_MAILBOX_STATUS_ITEMS));
$mailbox = $mailbox->fromStatus($status); $mailbox = $mailbox->fromStatus($status);
return $mailbox; return $mailbox;
@@ -146,7 +221,7 @@ class RemoteMailService
*/ */
public function collectionCreate(string $name): Mailbox public function collectionCreate(string $name): Mailbox
{ {
$result = $this->client->perform(new CreateCommand($name)); $result = $this->imapClient()->perform(new CreateCommand($name));
if (!$result->isOk()) { if (!$result->isOk()) {
throw new ImapException('Failed to create mailbox: ' . $name); throw new ImapException('Failed to create mailbox: ' . $name);
@@ -166,7 +241,7 @@ class RemoteMailService
*/ */
public function collectionRename(string $oldName, string $newName): Mailbox public function collectionRename(string $oldName, string $newName): Mailbox
{ {
$result = $this->client->perform(new RenameCommand($oldName, $newName)); $result = $this->imapClient()->perform(new RenameCommand($oldName, $newName));
if (!$result->isOk()) { if (!$result->isOk()) {
throw new ImapException('Failed to rename mailbox: ' . $oldName . ' to ' . $newName); throw new ImapException('Failed to rename mailbox: ' . $oldName . ' to ' . $newName);
@@ -186,7 +261,7 @@ class RemoteMailService
*/ */
public function collectionDestroy(string $name): bool public function collectionDestroy(string $name): bool
{ {
$result = $this->client->perform(new DeleteCommand($name)); $result = $this->imapClient()->perform(new DeleteCommand($name));
if (!$result->isOk()) { if (!$result->isOk()) {
throw new ImapException('Failed to delete mailbox: ' . $name); throw new ImapException('Failed to delete mailbox: ' . $name);
@@ -207,18 +282,18 @@ class RemoteMailService
$nativeFilter = $this->buildEntitySearchCriteria($filter); $nativeFilter = $this->buildEntitySearchCriteria($filter);
$nativeSort = $sort !== null ? $this->entitySortCriteria($sort) : []; $nativeSort = $sort !== null ? $this->entitySortCriteria($sort) : [];
$this->client->perform(new SelectCommand($collection, true)); $this->imapClient()->perform(new SelectCommand($collection, true));
$rfc5258 = $this->client->hasCapability('SORT'); $rfc5258 = $this->imapClient()->hasCapability('SORT');
$uids = []; $uids = [];
if ($nativeSort !== [] && $rfc5258) { if ($nativeSort !== [] && $rfc5258) {
$uids = $this->client->perform(new SortCommand( $uids = $this->imapClient()->perform(new SortCommand(
$nativeSort, $nativeSort,
$nativeFilter, $nativeFilter,
IdentifierMode::Uid, IdentifierMode::Uid,
))->matches(); ))->matches();
} else { } else {
$uids = $this->client->perform(new SearchCommand( $uids = $this->imapClient()->perform(new SearchCommand(
$nativeFilter, $nativeFilter,
IdentifierMode::Uid, IdentifierMode::Uid,
))->matches(); ))->matches();
@@ -246,13 +321,13 @@ class RemoteMailService
// fast path: fetch all messages without filtering, sorting or pagination // fast path: fetch all messages without filtering, sorting or pagination
if ($filter === null && $sort === null && $range === null) { if ($filter === null && $sort === null && $range === null) {
$mailbox = $this->client->perform(new SelectCommand($collection, true)); $mailbox = $this->imapClient()->perform(new SelectCommand($collection, true));
if ($mailbox === null) { if ($mailbox === null) {
return []; return [];
} }
yield from $this->client->perform(new FetchManyCommand( yield from $this->imapClient()->perform(new FetchManyCommand(
FetchTarget::all(), FetchTarget::all(),
$options, $options,
)); ));
@@ -283,14 +358,14 @@ class RemoteMailService
} }
$options ??= FetchOptions::message()->withBodyText(); $options ??= FetchOptions::message()->withBodyText();
$this->client->perform(new SelectCommand($collection, true)); $this->imapClient()->perform(new SelectCommand($collection, true));
$request = new FetchManyCommand( $request = new FetchManyCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$options, $options,
); );
foreach ($this->client->perform($request) as $message) { foreach ($this->imapClient()->perform($request) as $message) {
$uid = $message->uid() ?: $message->sequence(); $uid = $message->uid() ?: $message->sequence();
yield $uid => $message; yield $uid => $message;
} }
@@ -308,7 +383,7 @@ class RemoteMailService
*/ */
public function entityDownload(string $collection, int $uid, ?string $partId = null): BinaryResource public function entityDownload(string $collection, int $uid, ?string $partId = null): BinaryResource
{ {
$this->client->perform(new SelectCommand($collection, true)); $this->imapClient()->perform(new SelectCommand($collection, true));
$encoding = null; $encoding = null;
@@ -317,7 +392,7 @@ class RemoteMailService
$mimeType = 'message/rfc822'; $mimeType = 'message/rfc822';
} else { } else {
// Fetch BODYSTRUCTURE first to determine metadata (no body bytes transferred) // Fetch BODYSTRUCTURE first to determine metadata (no body bytes transferred)
$message = $this->client->perform(new FetchOneCommand( $message = $this->imapClient()->perform(new FetchOneCommand(
FetchTarget::uid(SequenceSet::items($uid)), FetchTarget::uid(SequenceSet::items($uid)),
FetchOptions::of('BODYSTRUCTURE'), FetchOptions::of('BODYSTRUCTURE'),
)); ));
@@ -333,7 +408,7 @@ class RemoteMailService
// Start download stream // Start download stream
$stream = $this->decodeStream( $stream = $this->decodeStream(
$this->client->download(FetchTarget::uid(SequenceSet::items($uid)), $partId ?? ''), $this->imapClient()->download(FetchTarget::uid(SequenceSet::items($uid)), $partId ?? ''),
$encoding $encoding
); );
@@ -413,7 +488,7 @@ class RemoteMailService
*/ */
public function entityCreate(string $collection, string $rawMessage, array $flags = []): ?int public function entityCreate(string $collection, string $rawMessage, array $flags = []): ?int
{ {
return $this->client->perform(new AppendCommand($collection, $rawMessage, $flags)); return $this->imapClient()->perform(new AppendCommand($collection, $rawMessage, $flags));
} }
/** /**
@@ -429,8 +504,8 @@ class RemoteMailService
return; return;
} }
$this->client->perform(new SelectCommand($collection, false)); $this->imapClient()->perform(new SelectCommand($collection, false));
$this->client->perform(new StoreCommand( $this->imapClient()->perform(new StoreCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$flags, $flags,
$action, $action,
@@ -448,9 +523,9 @@ class RemoteMailService
$target = FetchTarget::uid(SequenceSet::items(...array_values($uids))); $target = FetchTarget::uid(SequenceSet::items(...array_values($uids)));
$this->client->perform(new SelectCommand($collection, false)); $this->imapClient()->perform(new SelectCommand($collection, false));
$this->client->perform(new StoreCommand($target, ['\\Deleted'], '+')); $this->imapClient()->perform(new StoreCommand($target, ['\\Deleted'], '+'));
$this->client->perform(new ExpungeCommand($target)); $this->imapClient()->perform(new ExpungeCommand($target));
// TODO: find a way to determine which actual UID's were deleted // TODO: find a way to determine which actual UID's were deleted
return array_fill_keys($uids, true); return array_fill_keys($uids, true);
@@ -462,13 +537,13 @@ class RemoteMailService
return; return;
} }
$this->client->perform(new SelectCommand($collection, false)); $this->imapClient()->perform(new SelectCommand($collection, false));
$flagsToAdd = $this->normalizeFlags($flagsToAdd); $flagsToAdd = $this->normalizeFlags($flagsToAdd);
$flagsToRemove = $this->normalizeFlags($flagsToRemove); $flagsToRemove = $this->normalizeFlags($flagsToRemove);
if (!empty($flagsToAdd)) { if (!empty($flagsToAdd)) {
$this->client->perform(new StoreCommand( $this->imapClient()->perform(new StoreCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$flagsToAdd, $flagsToAdd,
'+', '+',
@@ -476,7 +551,7 @@ class RemoteMailService
} }
if (!empty($flagsToRemove)) { if (!empty($flagsToRemove)) {
$this->client->perform(new StoreCommand( $this->imapClient()->perform(new StoreCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$flagsToRemove, $flagsToRemove,
'-', '-',
@@ -490,28 +565,28 @@ class RemoteMailService
return []; return [];
} }
$rfc6851 = $this->client->hasCapability('MOVE'); $rfc6851 = $this->imapClient()->hasCapability('MOVE');
// if MOVE is supported, use it; otherwise, fall back to COPY + EXPUNGE // if MOVE is supported, use it; otherwise, fall back to COPY + EXPUNGE
if ($rfc6851) { if ($rfc6851) {
$this->client->perform(new SelectCommand($sourceCollection, false)); $this->imapClient()->perform(new SelectCommand($sourceCollection, false));
$response = $this->client->perform(new MoveCommand( $response = $this->imapClient()->perform(new MoveCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$targetCollection, $targetCollection,
)); ));
} else { } else {
$this->client->perform(new SelectCommand($sourceCollection, false)); $this->imapClient()->perform(new SelectCommand($sourceCollection, false));
$response = $this->client->perform(new CopyCommand( $response = $this->imapClient()->perform(new CopyCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$targetCollection, $targetCollection,
)); ));
if ($response->isOk()) { if ($response->isOk()) {
$this->client->perform(new StoreCommand( $this->imapClient()->perform(new StoreCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
['\\Deleted'], ['\\Deleted'],
'+', '+',
)); ));
$this->client->perform(new ExpungeCommand( $this->imapClient()->perform(new ExpungeCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
)); ));
} }
@@ -542,8 +617,8 @@ class RemoteMailService
return []; return [];
} }
$this->client->perform(new SelectCommand($sourceCollection, false)); $this->imapClient()->perform(new SelectCommand($sourceCollection, false));
$response = $this->client->perform(new CopyCommand( $response = $this->imapClient()->perform(new CopyCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$targetCollection, $targetCollection,
)); ));
@@ -768,7 +843,7 @@ class RemoteMailService
} }
} }
$messages = iterator_to_array($this->client->perform(new FetchManyCommand( $messages = iterator_to_array($this->imapClient()->perform(new FetchManyCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))), FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$options, $options,
))); )));
-61
View File
@@ -1,61 +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\Service\Remote;
use KTXC\Server;
use KTXC\Logger\PlainFileLogger;
use KTXM\ProviderImap\Client\Client;
use KTXM\ProviderImap\Providers\Service;
/**
* Static factory for IMAP remote service objects.
*
* - freshClient() → builds a gricob Client from service config
* - mailService() → constructs a RemoteMailService from the client
*/
class RemoteService
{
/**
* Build and bootstrap a fully-configured IMAP client from a Service.
*/
public static function freshClient(Service $service): Client
{
$location = $service->getLocation();
$identity = $service->getIdentity();
// Build a file logger when debug mode is enabled, otherwise pass null
$logger = null;
if ($service->getDebug()) {
$logDir = Server::getInstance()?->logDir() ?? __DIR__ . '/../../../../../var/log';
$logger = new PlainFileLogger($logDir . '/imap', $service->identifier());
}
$config = $location->toConnectionConfig(
$identity?->getIdentity(),
$identity?->getSecret(),
);
$client = new Client(logger: $logger);
$client->connect($config);
return $client;
}
/**
* Build a RemoteMailService from a Service and a pre-authenticated client.
*
* The provider identifier and service ID are taken directly from the Service
* object so the caller does not have to repeat them.
*/
public static function mailService(Service $service, Client $client): RemoteMailService
{
return new RemoteMailService($client, $service->provider(), $service->identifier());
}
}