generated from Nodarx/template
refactor: split service into ServiceBase and LiveService
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -10,7 +10,7 @@ declare(strict_types=1);
|
||||
namespace KTXM\ProviderImap\Console;
|
||||
|
||||
use KTXM\ProviderImap\Providers\Provider;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\LiveService;
|
||||
use KTXM\ProviderImap\Providers\ServiceIdentityBasic;
|
||||
use KTXM\ProviderImap\Providers\ServiceLocation;
|
||||
use KTXC\Context\TenantContext;
|
||||
@@ -162,7 +162,7 @@ class ConnectCommand extends Command
|
||||
'secret' => $password,
|
||||
]);
|
||||
|
||||
$service = new Service();
|
||||
$service = new LiveService();
|
||||
$service->setLocation($location);
|
||||
$service->setIdentity($identity);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ declare(strict_types=1);
|
||||
namespace KTXM\ProviderImap\Console;
|
||||
|
||||
use KTXM\ProviderImap\Providers\Provider;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXC\Context\TenantContext;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -96,7 +96,7 @@ class DisconnectCommand extends Command
|
||||
|
||||
$choices = [];
|
||||
foreach ($services as $id => $service) {
|
||||
$label = $service instanceof Service ? ($service->getLabel() ?? $id) : $id;
|
||||
$label = $service instanceof ServiceBase ? ($service->getLabel() ?? $id) : $id;
|
||||
$choices[$id] = "{$label} [{$id}]";
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ declare(strict_types=1);
|
||||
namespace KTXM\ProviderImap\Console;
|
||||
|
||||
use KTXM\ProviderImap\Providers\Provider;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\LiveService;
|
||||
use KTXM\ProviderImap\Providers\ServiceIdentityBasic;
|
||||
use KTXM\ProviderImap\Providers\ServiceLocation;
|
||||
use KTXM\ProviderImap\Service\Discovery;
|
||||
@@ -191,7 +191,7 @@ class DiscoverCommand extends Command
|
||||
|
||||
// ── Test before saving ───────────────────────────────────────────────
|
||||
|
||||
$service = new Service();
|
||||
$service = new LiveService();
|
||||
$service->setLocation($location);
|
||||
$service->setIdentity((new ServiceIdentityBasic())->jsonDeserialize([
|
||||
'identity' => $username,
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace KTXM\ProviderImap\Console;
|
||||
|
||||
use KTXC\Context\TenantContext;
|
||||
use KTXM\ProviderImap\Providers\Provider;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXM\ProviderImap\Service\Cache\HarmonizationService;
|
||||
use KTXM\ProviderImap\Service\Cache\MessageDeltaService;
|
||||
use KTXM\ProviderImap\Stores\MailboxStore;
|
||||
@@ -104,7 +104,7 @@ class HarmonizeCommand extends Command
|
||||
|
||||
$this->tenantContext->resolveIdentifier($tenantId);
|
||||
$service = $this->provider->serviceFetch($tenantId, $userId, $serviceId);
|
||||
if (!$service instanceof Service) {
|
||||
if (!$service instanceof ServiceBase) {
|
||||
$io->error(sprintf("Service '%s' not found.", $serviceId));
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use KTXM\ProviderImap\Client\Protocol\Command\Argument\MessageTarget;
|
||||
use KTXM\ProviderImap\Client\Message;
|
||||
use KTXM\ProviderImap\Client\Protocol\SequenceSet;
|
||||
use KTXM\ProviderImap\Providers\Provider;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXM\ProviderImap\Service\Live\LiveMailService;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -299,7 +299,7 @@ class TestCommand extends Command
|
||||
return $limit >= 0 ? $limit : $default;
|
||||
}
|
||||
|
||||
private function formatTarget(Service $service): string
|
||||
private function formatTarget(ServiceBase $service): string
|
||||
{
|
||||
$location = $service->getLocation();
|
||||
if ($location === null) {
|
||||
|
||||
@@ -44,352 +44,22 @@ use KTXF\Mail\Collection\CollectionRoles;
|
||||
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
||||
use KTXF\Resource\Identifier\EntityIdentifierInterface;
|
||||
use KTXM\ProviderImap\Providers\EntityResource;
|
||||
use KTXM\ProviderImap\Client\Mailbox;
|
||||
|
||||
class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface, ServiceEntitySubmitInterface
|
||||
/**
|
||||
* IMAP mail service that talks to the server directly.
|
||||
*
|
||||
* Holds the mail API rules (validation, delete modes, submission, move / copy
|
||||
* result mapping); CachedService delegates writes here.
|
||||
*/
|
||||
class LiveService extends ServiceBase
|
||||
{
|
||||
private const PROVIDER_IDENTIFIER = 'imap';
|
||||
|
||||
private ?string $serviceTenantId = null;
|
||||
private ?string $serviceUserId = null;
|
||||
private ?string $serviceIdentifier = null;
|
||||
private ?string $serviceLabel = null;
|
||||
private bool $serviceEnabled = false;
|
||||
private array $primaryAddress = [];
|
||||
private array $secondaryAddresses = [];
|
||||
private ?ServiceLocation $location = null;
|
||||
private ?ServiceIdentityBasic $identity = null;
|
||||
private array $auxiliary = [];
|
||||
|
||||
private array $serviceAbilities = [
|
||||
self::CAPABILITY_COLLECTION_LIST => true,
|
||||
self::CAPABILITY_COLLECTION_LIST_FILTER => [
|
||||
self::CAPABILITY_COLLECTION_FILTER_LABEL => 's:128:256:256',
|
||||
self::CAPABILITY_COLLECTION_FILTER_ROLE => 's:32:1:1',
|
||||
self::CAPABILITY_COLLECTION_FILTER_SUBSCRIBED => 'b:0:1:1',
|
||||
],
|
||||
self::CAPABILITY_COLLECTION_LIST_SORT => [
|
||||
self::CAPABILITY_COLLECTION_SORT_LABEL,
|
||||
self::CAPABILITY_COLLECTION_SORT_RANK,
|
||||
],
|
||||
self::CAPABILITY_COLLECTION_FETCH => true,
|
||||
self::CAPABILITY_COLLECTION_EXTANT => true,
|
||||
self::CAPABILITY_COLLECTION_CREATE => true,
|
||||
self::CAPABILITY_COLLECTION_UPDATE => true,
|
||||
self::CAPABILITY_COLLECTION_DELETE => true,
|
||||
self::CAPABILITY_COLLECTION_MOVE => true,
|
||||
self::CAPABILITY_ENTITY_LIST => true,
|
||||
self::CAPABILITY_ENTITY_LIST_FILTER => [
|
||||
self::CAPABILITY_ENTITY_FILTER_FROM => 's:100:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_TO => 's:100:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_SUBJECT => 's:200:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_BODY => 's:200:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_DATE_BEFORE => 's:32:1:1',
|
||||
self::CAPABILITY_ENTITY_FILTER_DATE_AFTER => 's:32:1:1',
|
||||
self::CAPABILITY_ENTITY_FILTER_SIZE_MIN => 'i:0:16:16',
|
||||
self::CAPABILITY_ENTITY_FILTER_SIZE_MAX => 'i:0:32:32',
|
||||
],
|
||||
self::CAPABILITY_ENTITY_LIST_SORT => [
|
||||
self::CAPABILITY_ENTITY_SORT_FROM,
|
||||
self::CAPABILITY_ENTITY_SORT_TO,
|
||||
self::CAPABILITY_ENTITY_SORT_SUBJECT,
|
||||
self::CAPABILITY_ENTITY_SORT_DATE_RECEIVED,
|
||||
self::CAPABILITY_ENTITY_SORT_DATE_SENT,
|
||||
self::CAPABILITY_ENTITY_SORT_SIZE,
|
||||
],
|
||||
self::CAPABILITY_ENTITY_LIST_RANGE => [
|
||||
'tally' => ['absolute', 'relative']
|
||||
],
|
||||
self::CAPABILITY_ENTITY_FETCH => true,
|
||||
self::CAPABILITY_ENTITY_EXTANT => true,
|
||||
self::CAPABILITY_ENTITY_CREATE => true,
|
||||
self::CAPABILITY_ENTITY_MODIFY => true,
|
||||
self::CAPABILITY_ENTITY_PATCH => true,
|
||||
self::CAPABILITY_ENTITY_DELETE => true,
|
||||
self::CAPABILITY_ENTITY_MOVE => true,
|
||||
self::CAPABILITY_ENTITY_COPY => false,
|
||||
'EntityTransmit' => true,
|
||||
];
|
||||
|
||||
private LiveMailService $liveService;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
private function initialize(): void
|
||||
{
|
||||
if (!isset($this->liveService)) {
|
||||
$this->liveService = new LiveMailService($this);
|
||||
}
|
||||
}
|
||||
|
||||
public function toStore(): array
|
||||
{
|
||||
return array_filter([
|
||||
'tid' => $this->serviceTenantId,
|
||||
'uid' => $this->serviceUserId,
|
||||
'sid' => $this->serviceIdentifier,
|
||||
'enabled' => $this->serviceEnabled,
|
||||
'label' => $this->serviceLabel,
|
||||
'primaryAddress' => $this->primaryAddress,
|
||||
'secondaryAddresses'=> $this->secondaryAddresses,
|
||||
'location' => $this->location?->toStore(),
|
||||
'identity' => $this->identity?->toStore(),
|
||||
'auxiliary' => $this->auxiliary,
|
||||
], fn($v) => $v !== null);
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
{
|
||||
$this->serviceTenantId = $data['tid'] ?? null;
|
||||
$this->serviceUserId = $data['uid'] ?? null;
|
||||
$this->serviceIdentifier = $data['sid'] ?? null;
|
||||
$this->serviceLabel = $data['label'] ?? '';
|
||||
$this->serviceEnabled = $data['enabled'] ?? false;
|
||||
|
||||
if (isset($data['primaryAddress'])) {
|
||||
$this->primaryAddress = $data['primaryAddress'];
|
||||
}
|
||||
if (isset($data['secondaryAddresses']) && is_array($data['secondaryAddresses'])) {
|
||||
$this->secondaryAddresses = $data['secondaryAddresses'];
|
||||
}
|
||||
if (isset($data['location'])) {
|
||||
$this->location = (new ServiceLocation())->fromStore($data['location']);
|
||||
}
|
||||
if (isset($data['identity'])) {
|
||||
$this->identity = (new ServiceIdentityBasic())->fromStore($data['identity']);
|
||||
}
|
||||
if (isset($data['auxiliary']) && is_array($data['auxiliary'])) {
|
||||
$this->auxiliary = $data['auxiliary'];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_filter([
|
||||
self::PROPERTY_TYPE => self::JSON_TYPE,
|
||||
self::PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER,
|
||||
self::PROPERTY_IDENTIFIER => $this->serviceIdentifier,
|
||||
self::PROPERTY_LABEL => $this->serviceLabel,
|
||||
self::PROPERTY_ENABLED => $this->serviceEnabled,
|
||||
self::PROPERTY_CAPABILITIES => $this->serviceAbilities,
|
||||
self::PROPERTY_PRIMARY_ADDRESS => $this->primaryAddress,
|
||||
self::PROPERTY_SECONDARY_ADDRESSES => $this->secondaryAddresses,
|
||||
self::PROPERTY_LOCATION => $this->location?->jsonSerialize(),
|
||||
self::PROPERTY_IDENTITY => $this->identity?->jsonSerialize(),
|
||||
self::PROPERTY_AUXILIARY => $this->auxiliary,
|
||||
], fn($v) => $v !== null);
|
||||
}
|
||||
|
||||
public function jsonDeserialize(array|string $data, bool $delta = false): static
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
if (isset($data[self::PROPERTY_ENABLED])) {
|
||||
$this->setEnabled($data[self::PROPERTY_ENABLED]);
|
||||
}
|
||||
if (isset($data[self::PROPERTY_LABEL])) {
|
||||
$this->setLabel($data[self::PROPERTY_LABEL]);
|
||||
}
|
||||
if (isset($data[self::PROPERTY_LOCATION])) {
|
||||
$this->setLocation($this->freshLocation(null, $data[self::PROPERTY_LOCATION]));
|
||||
}
|
||||
if (isset($data[self::PROPERTY_IDENTITY])) {
|
||||
$this->setIdentity($this->freshIdentity(null, $data[self::PROPERTY_IDENTITY]));
|
||||
}
|
||||
if (isset($data[self::PROPERTY_PRIMARY_ADDRESS])) {
|
||||
$value = $data[self::PROPERTY_PRIMARY_ADDRESS];
|
||||
$this->setPrimaryAddress(is_array($value) ? Address::fromArray($value) : new Address((string)$value));
|
||||
}
|
||||
if (isset($data[self::PROPERTY_SECONDARY_ADDRESSES]) && is_array($data[self::PROPERTY_SECONDARY_ADDRESSES])) {
|
||||
$this->setSecondaryAddresses(array_map(
|
||||
fn($addr) => new Address(is_array($addr) ? ($addr['address'] ?? $addr) : $addr),
|
||||
$data[self::PROPERTY_SECONDARY_ADDRESSES]
|
||||
));
|
||||
}
|
||||
if (isset($data[self::PROPERTY_AUXILIARY]) && is_array($data[self::PROPERTY_AUXILIARY])) {
|
||||
$this->setAuxiliary($data[self::PROPERTY_AUXILIARY]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function capable(string $value): bool
|
||||
{
|
||||
return isset($this->serviceAbilities[$value]);
|
||||
}
|
||||
|
||||
public function capabilities(): array
|
||||
{
|
||||
return $this->serviceAbilities;
|
||||
}
|
||||
|
||||
public function provider(): string
|
||||
{
|
||||
return self::PROVIDER_IDENTIFIER;
|
||||
}
|
||||
|
||||
public function identifier(): string|int
|
||||
{
|
||||
return $this->serviceIdentifier;
|
||||
}
|
||||
|
||||
public function tenantIdentifier(): ?string
|
||||
{
|
||||
return $this->serviceTenantId;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->serviceLabel;
|
||||
}
|
||||
|
||||
public function setLabel(string $label): static
|
||||
{
|
||||
$this->serviceLabel = $label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnabled(): bool
|
||||
{
|
||||
return $this->serviceEnabled;
|
||||
}
|
||||
|
||||
public function setEnabled(bool $enabled): static
|
||||
{
|
||||
$this->serviceEnabled = $enabled;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrimaryAddress(): AddressInterface
|
||||
{
|
||||
return Address::fromArray($this->primaryAddress);
|
||||
}
|
||||
|
||||
public function setPrimaryAddress(AddressInterface $value): static
|
||||
{
|
||||
$this->primaryAddress = $value->toArray();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSecondaryAddresses(): array
|
||||
{
|
||||
return array_map(
|
||||
fn($addr) => $addr instanceof AddressInterface ? $addr : Address::fromArray(is_array($addr) ? $addr : ['address' => (string) $addr])
|
||||
, $this->secondaryAddresses);
|
||||
}
|
||||
|
||||
public function setSecondaryAddresses(array $addresses): static
|
||||
{
|
||||
$this->secondaryAddresses = array_map(
|
||||
fn($addr) => $addr instanceof AddressInterface ? $addr : Address::fromArray(is_array($addr) ? $addr : ['address' => (string) $addr]),
|
||||
$addresses
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasAddress(string $address): bool
|
||||
{
|
||||
$address = strtolower(trim($address));
|
||||
|
||||
if ($this->primaryAddress && strtolower($this->primaryAddress['address'] ?? '') === $address) {
|
||||
return true;
|
||||
}
|
||||
foreach ($this->secondaryAddresses as $secondary) {
|
||||
$secondaryAddr = $secondary instanceof AddressInterface ? $secondary->getAddress() : (string) $secondary;
|
||||
if (strtolower($secondaryAddr) === $address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public function getLocation(): ServiceLocation
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function setLocation(ResourceServiceLocationInterface $location): static
|
||||
{
|
||||
$this->location = $location;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function freshLocation(?string $type = null, array $data = []): ServiceLocation
|
||||
{
|
||||
$loc = new ServiceLocation();
|
||||
$loc->jsonDeserialize($data);
|
||||
return $loc;
|
||||
}
|
||||
|
||||
public function getIdentity(): ServiceIdentityBasic
|
||||
{
|
||||
return $this->identity;
|
||||
}
|
||||
|
||||
public function setIdentity(ResourceServiceIdentityInterface $identity): static
|
||||
{
|
||||
$this->identity = $identity;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function freshIdentity(?string $type = null, array $data = []): ServiceIdentityBasic
|
||||
{
|
||||
$id = new ServiceIdentityBasic();
|
||||
$id->jsonDeserialize($data);
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function getDebug(): bool
|
||||
{
|
||||
return ($this->auxiliary['debug'] ?? false) === true;
|
||||
}
|
||||
|
||||
public function setDebug(bool $debug): static
|
||||
{
|
||||
$this->auxiliary['debug'] = $debug;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuxiliary(): array
|
||||
{
|
||||
return $this->auxiliary;
|
||||
}
|
||||
|
||||
public function setAuxiliary(array $auxiliary): static
|
||||
{
|
||||
$this->auxiliary = $auxiliary;
|
||||
return $this;
|
||||
}
|
||||
private LiveMailService $mailService;
|
||||
|
||||
public function collectionList(string|int|null $location, ?IFilter $filter = null, ?ISort $sort = null): array
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($this->liveService->collectionList($location, $filter, $sort) as $mailbox) {
|
||||
$resource = $this->collectionFresh();
|
||||
$resource->fromImap($mailbox);
|
||||
$list[$mailbox->name()] = $resource;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function collectionListFilter(): Filter
|
||||
{
|
||||
return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []);
|
||||
}
|
||||
|
||||
public function collectionListSort(): Sort
|
||||
{
|
||||
return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []);
|
||||
return $this->mailService->collectionList($location === null ? null : (string) $location, $filter, $sort);
|
||||
}
|
||||
|
||||
public function collectionExtant(string|int ...$identifiers): array
|
||||
@@ -400,9 +70,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
|
||||
foreach ($identifiers as $identifier) {
|
||||
$key = (string) $identifier;
|
||||
$result = $this->liveService->collectionFetch($key);
|
||||
|
||||
$list[$key] = $result !== false;
|
||||
$list[$key] = $this->mailService->collectionFetch($key) !== null;
|
||||
}
|
||||
|
||||
return $list;
|
||||
@@ -412,20 +80,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$mailbox = $this->liveService->collectionFetch((string) $identifier);
|
||||
if ($mailbox === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$collection = $this->collectionFresh();
|
||||
$collection->fromImap($mailbox);
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
public function collectionFresh(): CollectionResource
|
||||
{
|
||||
return new CollectionResource($this->provider(), $this->identifier());
|
||||
return $this->mailService->collectionFetch((string) $identifier);
|
||||
}
|
||||
|
||||
public function collectionCreate(CollectionIdentifier|null $target, CollectionPropertiesBaseInterface $properties, array $options = []): CollectionBaseInterface
|
||||
@@ -441,18 +96,11 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
if ($target !== null) {
|
||||
$path = $target->collection();
|
||||
// Determine the hierarchy delimiter from an existing mailbox, default to '/'
|
||||
$mailboxes = iterator_to_array($this->liveService->collectionList(null, null, null, ''));
|
||||
$rootMailbox = $mailboxes === [] ? null : reset($mailboxes);
|
||||
$delimiter = $rootMailbox === false ? '/' : ($rootMailbox?->delimiter() ?? '/');
|
||||
$delimiter = $this->mailService->collectionDelimiter();
|
||||
$label = rtrim((string) $path, $delimiter) . $delimiter . ltrim($label, $delimiter);
|
||||
}
|
||||
|
||||
$mailbox = $this->liveService->collectionCreate($label);
|
||||
|
||||
$collection = $this->collectionFresh();
|
||||
$collection->fromImap($mailbox, ['delimiter' => $delimiter ?? null]);
|
||||
|
||||
return $collection;
|
||||
return $this->mailService->collectionCreate($label, $delimiter ?? null);
|
||||
}
|
||||
|
||||
public function collectionUpdate(CollectionIdentifier $target, CollectionPropertiesBaseInterface $properties): CollectionBaseInterface
|
||||
@@ -467,11 +115,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
// In IMAP, "update" = rename to the new label
|
||||
$oldPath = (string) $target->collection();
|
||||
$newName = $properties->getLabel();
|
||||
$mailbox = $this->liveService->collectionRename($oldPath, $newName);
|
||||
|
||||
$collection = $this->collectionFresh();
|
||||
$collection->fromImap($mailbox);
|
||||
return $collection;
|
||||
return $this->mailService->collectionRename($oldPath, $newName);
|
||||
}
|
||||
|
||||
public function collectionDelete(CollectionIdentifier $target, bool $force = false): CollectionBaseInterface | true
|
||||
@@ -494,7 +138,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
|
||||
$result = match ($deleteMode) {
|
||||
'soft' => $this->collectionMove(new CollectionIdentifier($target->provider(), $target->service(), $deleteTarget), $target),
|
||||
'hard' => $this->liveService->collectionDestroy((string) $target->collection()),
|
||||
'hard' => $this->mailService->collectionDestroy((string) $target->collection()),
|
||||
};
|
||||
return $result;
|
||||
}
|
||||
@@ -503,8 +147,8 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
$sourceMailbox = $this->liveService->collectionFetch((string) $source->collection());
|
||||
$targetMailbox = $this->liveService->collectionFetch((string) $target->collection());
|
||||
$sourceMailbox = $this->mailService->collectionFetch((string) $source->collection());
|
||||
$targetMailbox = $this->mailService->collectionFetch((string) $target->collection());
|
||||
if ($sourceMailbox === null) {
|
||||
throw new \RuntimeException('Source collection not found for move operation');
|
||||
}
|
||||
@@ -512,19 +156,15 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
throw new \RuntimeException('Target collection not found for move operation');
|
||||
}
|
||||
|
||||
$sourceDelimiter = $sourceMailbox->delimiter() ?? '/';
|
||||
$targetDelimiter = $targetMailbox->delimiter() ?? '/';
|
||||
$sourceDelimiter = $sourceMailbox->getProperties()->getDelimiter() ?: '/';
|
||||
$targetDelimiter = $targetMailbox->getProperties()->getDelimiter() ?: '/';
|
||||
|
||||
$extantPath = $sourceMailbox->name();
|
||||
$extantPath = (string) $sourceMailbox->identifier();
|
||||
$extantPathLeafs = explode($sourceDelimiter, rtrim($extantPath, $sourceDelimiter));
|
||||
|
||||
$freshPath = rtrim($targetMailbox->name(), $targetDelimiter) . $targetDelimiter . end($extantPathLeafs);
|
||||
$freshPath = rtrim((string) $targetMailbox->identifier(), $targetDelimiter) . $targetDelimiter . end($extantPathLeafs);
|
||||
|
||||
$mutatedMailbox = $this->liveService->collectionRename($extantPath, $freshPath);
|
||||
|
||||
$collection = $this->collectionFresh();
|
||||
$collection->fromImap($mutatedMailbox, ['delimiter' => $targetDelimiter]);
|
||||
return $collection;
|
||||
return $this->mailService->collectionRename($extantPath, $freshPath, $targetDelimiter);
|
||||
}
|
||||
|
||||
public function entityListBulk(string|int $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null, ?array $properties = null): array
|
||||
@@ -536,31 +176,11 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
{
|
||||
$this->initialize();
|
||||
|
||||
foreach ($this->liveService->entityList((string) $collection, $filter, $sort, $range) as $identifier => $message) {
|
||||
$resource = $this->entityFresh();
|
||||
$resource->fromImap($message, $collection);
|
||||
foreach ($this->mailService->entityList((string) $collection, $filter, $sort, $range) as $resource) {
|
||||
yield $resource->urn() => $resource;
|
||||
}
|
||||
}
|
||||
|
||||
public function entityListFilter(): Filter
|
||||
{
|
||||
return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []);
|
||||
}
|
||||
|
||||
public function entityListSort(): Sort
|
||||
{
|
||||
return new Sort($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_SORT] ?? []);
|
||||
}
|
||||
|
||||
public function entityListRange(RangeType $type): IRange
|
||||
{
|
||||
return match ($type) {
|
||||
RangeType::TALLY => new RangeTally(),
|
||||
default => new Range(),
|
||||
};
|
||||
}
|
||||
|
||||
public function entityFetchBulk(EntityIdentifierInterface ...$identifiers): array
|
||||
{
|
||||
return iterator_to_array($this->entityFetchStream(...$identifiers), true);
|
||||
@@ -574,9 +194,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
|
||||
foreach ($identifiers as $collection => $entities) {
|
||||
$uids = array_keys($entities);
|
||||
foreach ($this->liveService->entityFetch((string) $collection, null, ...$uids) as $uid => $message) {
|
||||
$resource = $this->entityFresh();
|
||||
$resource->fromImap($message, $collection);
|
||||
foreach ($this->mailService->entityFetch((string) $collection, ...$uids) as $resource) {
|
||||
yield $resource->urn() => $resource;
|
||||
}
|
||||
}
|
||||
@@ -589,7 +207,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
$uid = (int) $target->entity();
|
||||
$partId = isset($part['partId']) ? (string) $part['partId'] : null;
|
||||
|
||||
return $this->liveService->entityDownload($collection, $uid, $partId);
|
||||
return $this->mailService->entityDownload($collection, $uid, $partId);
|
||||
}
|
||||
|
||||
public function entityDelta(string|int $collection, string $signature, string $detail = 'ids'): Delta
|
||||
@@ -607,7 +225,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
static fn (int $uid): bool => $uid > 0,
|
||||
));
|
||||
|
||||
$existing = array_flip($this->liveService->entityExtant((string) $collection, ...$uids));
|
||||
$existing = array_flip($this->mailService->entityExtant((string) $collection, ...$uids));
|
||||
|
||||
$extant = [];
|
||||
foreach ($identifiers as $id) {
|
||||
@@ -616,11 +234,6 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
return $extant;
|
||||
}
|
||||
|
||||
public function entityFresh(): EntityResource
|
||||
{
|
||||
return new EntityResource($this->provider(), $this->identifier());
|
||||
}
|
||||
|
||||
public function entitySubmit(AddressInterface $sender, EntityIdentifierInterface|null $source = null, MessagePropertiesMutableInterface|null $message = null): EntitySubmitResult
|
||||
{
|
||||
if ($message === null) {
|
||||
@@ -640,7 +253,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
}
|
||||
|
||||
$this->initialize();
|
||||
$smtp = $this->liveService->smtpClient();
|
||||
$smtp = $this->mailService->smtpClient();
|
||||
try {
|
||||
$queueId = $smtp->send(trim($sender->getAddress()), $recipients, $raw);
|
||||
} finally {
|
||||
@@ -661,7 +274,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
$this->initialize();
|
||||
$sentCollection = $this->resolveSentCollection();
|
||||
if ($sentCollection !== null) {
|
||||
$uid = $this->liveService->entityCreate($sentCollection, $raw, ['\\Seen']);
|
||||
$uid = $this->mailService->entityCreate($sentCollection, $raw, ['\\Seen']);
|
||||
if ($uid !== null && $uid > 0) {
|
||||
$sentEntity = new EntityIdentifier($this->provider(), $this->identifier(), $sentCollection, (string) $uid);
|
||||
}
|
||||
@@ -680,7 +293,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
&& $source->service() === $this->identifier()) {
|
||||
try {
|
||||
$this->initialize();
|
||||
$this->liveService->entityDestroy($source->collection(), (int) $source->entity());
|
||||
$this->mailService->entityDestroy($source->collection(), (int) $source->entity());
|
||||
} catch (\Throwable) {
|
||||
// ignore — the message was already delivered
|
||||
}
|
||||
@@ -703,7 +316,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
$this->initialize();
|
||||
|
||||
[$nativeMessage, $nativeFlags] = $this->messagePayload($properties, $options);
|
||||
$created = $this->liveService->entityCreate(
|
||||
$created = $this->mailService->entityCreate(
|
||||
(string)$target->collection(),
|
||||
$nativeMessage,
|
||||
$nativeFlags,
|
||||
@@ -724,7 +337,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
$this->initialize();
|
||||
|
||||
[$nativeMessage, $nativeFlags] = $this->messagePayload($properties);
|
||||
$modified = $this->liveService->entityReplace(
|
||||
$modified = $this->mailService->entityReplace(
|
||||
(string)$target->collection(),
|
||||
(int)$target->entity(),
|
||||
$nativeMessage,
|
||||
@@ -760,7 +373,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
}
|
||||
}
|
||||
|
||||
$mutations = $this->liveService->entityPatch($targetCollection, $flagsAdd, $flagsRemove, ...$uids);
|
||||
$mutations = $this->mailService->entityPatch($targetCollection, $flagsAdd, $flagsRemove, ...$uids);
|
||||
|
||||
foreach ($uids as $uid) {
|
||||
$list[(string)$targetIdentifiers[$uid]] = ['disposition' => 'patched'];
|
||||
@@ -807,8 +420,8 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
$uids = array_keys($sourceEntities);
|
||||
|
||||
$mutations = match ($deleteMode) {
|
||||
'soft' => $this->liveService->entityMove($deleteTargetNative, $sourceCollection, ...$uids),
|
||||
'hard' => $this->liveService->entityDestroy($sourceCollection, ...$uids),
|
||||
'soft' => $this->mailService->entityMove($deleteTargetNative, $sourceCollection, ...$uids),
|
||||
'hard' => $this->mailService->entityDestroy($sourceCollection, ...$uids),
|
||||
};
|
||||
|
||||
foreach ($uids as $uid) {
|
||||
@@ -842,7 +455,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
foreach ($sources as $sourceCollection => $sourceEntities) {
|
||||
$uids = array_keys($sourceEntities);
|
||||
|
||||
$mutations = $this->liveService->entityMove($target->collection(), $sourceCollection, ...$uids);
|
||||
$mutations = $this->mailService->entityMove($target->collection(), $sourceCollection, ...$uids);
|
||||
|
||||
foreach ($uids as $uid) {
|
||||
$mutatedUid = $mutations[$uid] ?? null;
|
||||
@@ -876,7 +489,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
foreach ($sources as $sourceCollection => $sourceEntities) {
|
||||
$uids = array_keys($sourceEntities);
|
||||
|
||||
$mutations = $this->liveService->entityCopy($target->collection(), $sourceCollection, ...$uids);
|
||||
$mutations = $this->mailService->entityCopy($target->collection(), $sourceCollection, ...$uids);
|
||||
|
||||
foreach ($uids as $uid) {
|
||||
$mutatedUid = $mutations[$uid] ?? null;
|
||||
@@ -899,13 +512,9 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
$filter = $this->collectionListFilter();
|
||||
$filter->condition('role', $role->value);
|
||||
|
||||
/** @var Mailbox[] $mailboxes */
|
||||
$mailboxes = iterator_to_array($this->liveService->collectionList(null, $filter, null));
|
||||
if ($mailboxes === []) {
|
||||
return null;
|
||||
}
|
||||
$mailbox = reset($mailboxes);
|
||||
return $mailbox === false ? null : $mailbox->name();
|
||||
$collections = $this->mailService->collectionList(null, $filter, null);
|
||||
|
||||
return $collections === [] ? null : (string) array_key_first($collections);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -988,4 +597,11 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
|
||||
|
||||
return [(new MessageBuilder())->build($properties), $flags];
|
||||
}
|
||||
|
||||
protected function initialize(): void
|
||||
{
|
||||
if (!isset($this->mailService)) {
|
||||
$this->mailService = new LiveMailService($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-10
@@ -106,7 +106,7 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function serviceFetch(string $tenantId, string $userId, string|int $identifier): ?Service
|
||||
public function serviceFetch(string $tenantId, string $userId, string|int $identifier): ?ServiceBase
|
||||
{
|
||||
$serviceData = $this->serviceStore->fetch($tenantId, $userId, $identifier);
|
||||
if ($serviceData === null) {
|
||||
@@ -116,9 +116,9 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
|
||||
return $serviceInstance;
|
||||
}
|
||||
|
||||
public function serviceFindByAddress(string $tenantId, string $userId, string $address): ?Service
|
||||
public function serviceFindByAddress(string $tenantId, string $userId, string $address): ?ServiceBase
|
||||
{
|
||||
/** @var Service[] $services */
|
||||
/** @var ServiceBase[] $services */
|
||||
$services = $this->serviceList($tenantId, $userId);
|
||||
foreach ($services as $service) {
|
||||
if ($service->hasAddress($address)) {
|
||||
@@ -133,14 +133,14 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
|
||||
return $this->serviceStore->extant($tenantId, $userId, $identifiers);
|
||||
}
|
||||
|
||||
public function serviceFresh(): Service
|
||||
public function serviceFresh(): ServiceBase
|
||||
{
|
||||
return new Service();
|
||||
return new LiveService();
|
||||
}
|
||||
|
||||
public function serviceCreate(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string
|
||||
{
|
||||
if (!($service instanceof Service)) {
|
||||
if (!($service instanceof ServiceBase)) {
|
||||
throw new \InvalidArgumentException('Service must be instance of IMAP Service');
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
|
||||
|
||||
public function serviceModify(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string
|
||||
{
|
||||
if (!($service instanceof Service)) {
|
||||
if (!($service instanceof ServiceBase)) {
|
||||
throw new \InvalidArgumentException('Service must be instance of IMAP Service');
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
|
||||
|
||||
public function serviceDestroy(string $tenantId, string $userId, ResourceServiceMutateInterface $service): bool
|
||||
{
|
||||
if (!($service instanceof Service)) {
|
||||
if (!($service instanceof ServiceBase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
if (!($service instanceof Service)) {
|
||||
if (!($service instanceof ServiceBase)) {
|
||||
throw new \InvalidArgumentException('Service must be an instance of IMAP Service');
|
||||
}
|
||||
// augment the service with any provided test options (e.g. override location or credentials)
|
||||
@@ -193,7 +193,7 @@ class Provider implements ProviderBaseInterface, ProviderServiceMutateInterface,
|
||||
|
||||
// Attempt to authenticate and list mailboxes as a connectivity check
|
||||
$live = new LiveMailService($service);
|
||||
$mailboxes = iterator_to_array($live->collectionList());
|
||||
$mailboxes = $live->collectionList();
|
||||
|
||||
$latency = (int) round((microtime(true) - $startTime) * 1000);
|
||||
|
||||
|
||||
@@ -0,0 +1,404 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImap\Providers;
|
||||
|
||||
use Generator;
|
||||
use KTXF\Mail\Collection\CollectionBaseInterface;
|
||||
use KTXF\Mail\Collection\CollectionPropertiesBaseInterface;
|
||||
use KTXF\Mail\Object\Address;
|
||||
use KTXF\Mail\Object\AddressInterface;
|
||||
use KTXF\Mail\Service\ServiceBaseInterface;
|
||||
use KTXF\Mail\Service\ServiceCollectionMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceEntityMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceEntitySubmitInterface;
|
||||
use KTXF\Mail\Service\ServiceConfigurableInterface;
|
||||
use KTXF\Mail\Service\ServiceMutableInterface;
|
||||
use KTXF\Mail\Submission\EntitySubmitResult;
|
||||
use KTXF\Resource\BinaryResource;
|
||||
use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
|
||||
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
||||
use KTXF\Resource\Delta\Delta;
|
||||
use KTXF\Resource\Filter\Filter;
|
||||
use KTXF\Resource\Filter\IFilter;
|
||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||
use KTXF\Resource\Range\IRange;
|
||||
use KTXF\Resource\Range\Range;
|
||||
use KTXF\Resource\Range\RangeTally;
|
||||
use KTXF\Resource\Range\RangeType;
|
||||
use KTXF\Resource\Sort\ISort;
|
||||
use KTXF\Resource\Sort\Sort;
|
||||
use KTXM\ProviderImap\Providers\ServiceIdentityBasic;
|
||||
use KTXM\ProviderImap\Providers\ServiceLocation;
|
||||
use KTXM\ProviderImap\Mime\MessageBuilder;
|
||||
use KTXM\ProviderImap\Providers\CollectionResource;
|
||||
use KTXF\Mail\Collection\CollectionRoles;
|
||||
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
||||
use KTXF\Resource\Identifier\EntityIdentifierInterface;
|
||||
use KTXM\ProviderImap\Providers\EntityResource;
|
||||
|
||||
/**
|
||||
* IMAP mail service: account configuration and serialization.
|
||||
*
|
||||
* The mail API (collection / entity methods, declared by the implemented
|
||||
* interfaces) is left to the concrete classes: LiveService talks to the IMAP
|
||||
* server, CachedService serves reads from the cache. The Provider picks the
|
||||
* class by mode when loading a service.
|
||||
*/
|
||||
abstract class ServiceBase implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface, ServiceEntitySubmitInterface
|
||||
{
|
||||
protected const PROVIDER_IDENTIFIER = 'imap';
|
||||
|
||||
protected ?string $serviceTenantId = null;
|
||||
protected ?string $serviceUserId = null;
|
||||
protected ?string $serviceIdentifier = null;
|
||||
protected ?string $serviceLabel = null;
|
||||
protected bool $serviceEnabled = false;
|
||||
protected array $primaryAddress = [];
|
||||
protected array $secondaryAddresses = [];
|
||||
protected ?ServiceLocation $location = null;
|
||||
protected ?ServiceIdentityBasic $identity = null;
|
||||
protected array $auxiliary = [];
|
||||
|
||||
protected array $serviceAbilities = [
|
||||
self::CAPABILITY_COLLECTION_LIST => true,
|
||||
self::CAPABILITY_COLLECTION_LIST_FILTER => [
|
||||
self::CAPABILITY_COLLECTION_FILTER_LABEL => 's:128:256:256',
|
||||
self::CAPABILITY_COLLECTION_FILTER_ROLE => 's:32:1:1',
|
||||
self::CAPABILITY_COLLECTION_FILTER_SUBSCRIBED => 'b:0:1:1',
|
||||
],
|
||||
self::CAPABILITY_COLLECTION_LIST_SORT => [
|
||||
self::CAPABILITY_COLLECTION_SORT_LABEL,
|
||||
self::CAPABILITY_COLLECTION_SORT_RANK,
|
||||
],
|
||||
self::CAPABILITY_COLLECTION_FETCH => true,
|
||||
self::CAPABILITY_COLLECTION_EXTANT => true,
|
||||
self::CAPABILITY_COLLECTION_CREATE => true,
|
||||
self::CAPABILITY_COLLECTION_UPDATE => true,
|
||||
self::CAPABILITY_COLLECTION_DELETE => true,
|
||||
self::CAPABILITY_COLLECTION_MOVE => true,
|
||||
self::CAPABILITY_ENTITY_LIST => true,
|
||||
self::CAPABILITY_ENTITY_LIST_FILTER => [
|
||||
self::CAPABILITY_ENTITY_FILTER_FROM => 's:100:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_TO => 's:100:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_SUBJECT => 's:200:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_BODY => 's:200:256:256',
|
||||
self::CAPABILITY_ENTITY_FILTER_DATE_BEFORE => 's:32:1:1',
|
||||
self::CAPABILITY_ENTITY_FILTER_DATE_AFTER => 's:32:1:1',
|
||||
self::CAPABILITY_ENTITY_FILTER_SIZE_MIN => 'i:0:16:16',
|
||||
self::CAPABILITY_ENTITY_FILTER_SIZE_MAX => 'i:0:32:32',
|
||||
],
|
||||
self::CAPABILITY_ENTITY_LIST_SORT => [
|
||||
self::CAPABILITY_ENTITY_SORT_FROM,
|
||||
self::CAPABILITY_ENTITY_SORT_TO,
|
||||
self::CAPABILITY_ENTITY_SORT_SUBJECT,
|
||||
self::CAPABILITY_ENTITY_SORT_DATE_RECEIVED,
|
||||
self::CAPABILITY_ENTITY_SORT_DATE_SENT,
|
||||
self::CAPABILITY_ENTITY_SORT_SIZE,
|
||||
],
|
||||
self::CAPABILITY_ENTITY_LIST_RANGE => [
|
||||
'tally' => ['absolute', 'relative']
|
||||
],
|
||||
self::CAPABILITY_ENTITY_FETCH => true,
|
||||
self::CAPABILITY_ENTITY_EXTANT => true,
|
||||
self::CAPABILITY_ENTITY_CREATE => true,
|
||||
self::CAPABILITY_ENTITY_MODIFY => true,
|
||||
self::CAPABILITY_ENTITY_PATCH => true,
|
||||
self::CAPABILITY_ENTITY_DELETE => true,
|
||||
self::CAPABILITY_ENTITY_MOVE => true,
|
||||
self::CAPABILITY_ENTITY_COPY => false,
|
||||
'EntityTransmit' => true,
|
||||
];
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function toStore(): array
|
||||
{
|
||||
return array_filter([
|
||||
'tid' => $this->serviceTenantId,
|
||||
'uid' => $this->serviceUserId,
|
||||
'sid' => $this->serviceIdentifier,
|
||||
'enabled' => $this->serviceEnabled,
|
||||
'label' => $this->serviceLabel,
|
||||
'primaryAddress' => $this->primaryAddress,
|
||||
'secondaryAddresses'=> $this->secondaryAddresses,
|
||||
'location' => $this->location?->toStore(),
|
||||
'identity' => $this->identity?->toStore(),
|
||||
'auxiliary' => $this->auxiliary,
|
||||
], fn($v) => $v !== null);
|
||||
}
|
||||
|
||||
public function fromStore(array $data): static
|
||||
{
|
||||
$this->serviceTenantId = $data['tid'] ?? null;
|
||||
$this->serviceUserId = $data['uid'] ?? null;
|
||||
$this->serviceIdentifier = $data['sid'] ?? null;
|
||||
$this->serviceLabel = $data['label'] ?? '';
|
||||
$this->serviceEnabled = $data['enabled'] ?? false;
|
||||
|
||||
if (isset($data['primaryAddress'])) {
|
||||
$this->primaryAddress = $data['primaryAddress'];
|
||||
}
|
||||
if (isset($data['secondaryAddresses']) && is_array($data['secondaryAddresses'])) {
|
||||
$this->secondaryAddresses = $data['secondaryAddresses'];
|
||||
}
|
||||
if (isset($data['location'])) {
|
||||
$this->location = (new ServiceLocation())->fromStore($data['location']);
|
||||
}
|
||||
if (isset($data['identity'])) {
|
||||
$this->identity = (new ServiceIdentityBasic())->fromStore($data['identity']);
|
||||
}
|
||||
if (isset($data['auxiliary']) && is_array($data['auxiliary'])) {
|
||||
$this->auxiliary = $data['auxiliary'];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return array_filter([
|
||||
self::PROPERTY_TYPE => self::JSON_TYPE,
|
||||
self::PROPERTY_PROVIDER => self::PROVIDER_IDENTIFIER,
|
||||
self::PROPERTY_IDENTIFIER => $this->serviceIdentifier,
|
||||
self::PROPERTY_LABEL => $this->serviceLabel,
|
||||
self::PROPERTY_ENABLED => $this->serviceEnabled,
|
||||
self::PROPERTY_CAPABILITIES => $this->serviceAbilities,
|
||||
self::PROPERTY_PRIMARY_ADDRESS => $this->primaryAddress,
|
||||
self::PROPERTY_SECONDARY_ADDRESSES => $this->secondaryAddresses,
|
||||
self::PROPERTY_LOCATION => $this->location?->jsonSerialize(),
|
||||
self::PROPERTY_IDENTITY => $this->identity?->jsonSerialize(),
|
||||
self::PROPERTY_AUXILIARY => $this->auxiliary,
|
||||
], fn($v) => $v !== null);
|
||||
}
|
||||
|
||||
public function jsonDeserialize(array|string $data, bool $delta = false): static
|
||||
{
|
||||
if (is_string($data)) {
|
||||
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
if (isset($data[self::PROPERTY_ENABLED])) {
|
||||
$this->setEnabled($data[self::PROPERTY_ENABLED]);
|
||||
}
|
||||
if (isset($data[self::PROPERTY_LABEL])) {
|
||||
$this->setLabel($data[self::PROPERTY_LABEL]);
|
||||
}
|
||||
if (isset($data[self::PROPERTY_LOCATION])) {
|
||||
$this->setLocation($this->freshLocation(null, $data[self::PROPERTY_LOCATION]));
|
||||
}
|
||||
if (isset($data[self::PROPERTY_IDENTITY])) {
|
||||
$this->setIdentity($this->freshIdentity(null, $data[self::PROPERTY_IDENTITY]));
|
||||
}
|
||||
if (isset($data[self::PROPERTY_PRIMARY_ADDRESS])) {
|
||||
$value = $data[self::PROPERTY_PRIMARY_ADDRESS];
|
||||
$this->setPrimaryAddress(is_array($value) ? Address::fromArray($value) : new Address((string)$value));
|
||||
}
|
||||
if (isset($data[self::PROPERTY_SECONDARY_ADDRESSES]) && is_array($data[self::PROPERTY_SECONDARY_ADDRESSES])) {
|
||||
$this->setSecondaryAddresses(array_map(
|
||||
fn($addr) => new Address(is_array($addr) ? ($addr['address'] ?? $addr) : $addr),
|
||||
$data[self::PROPERTY_SECONDARY_ADDRESSES]
|
||||
));
|
||||
}
|
||||
if (isset($data[self::PROPERTY_AUXILIARY]) && is_array($data[self::PROPERTY_AUXILIARY])) {
|
||||
$this->setAuxiliary($data[self::PROPERTY_AUXILIARY]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function capable(string $value): bool
|
||||
{
|
||||
return isset($this->serviceAbilities[$value]);
|
||||
}
|
||||
|
||||
public function capabilities(): array
|
||||
{
|
||||
return $this->serviceAbilities;
|
||||
}
|
||||
|
||||
public function provider(): string
|
||||
{
|
||||
return self::PROVIDER_IDENTIFIER;
|
||||
}
|
||||
|
||||
public function identifier(): string|int
|
||||
{
|
||||
return $this->serviceIdentifier;
|
||||
}
|
||||
|
||||
public function tenantIdentifier(): ?string
|
||||
{
|
||||
return $this->serviceTenantId;
|
||||
}
|
||||
|
||||
public function getLabel(): ?string
|
||||
{
|
||||
return $this->serviceLabel;
|
||||
}
|
||||
|
||||
public function setLabel(string $label): static
|
||||
{
|
||||
$this->serviceLabel = $label;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEnabled(): bool
|
||||
{
|
||||
return $this->serviceEnabled;
|
||||
}
|
||||
|
||||
public function setEnabled(bool $enabled): static
|
||||
{
|
||||
$this->serviceEnabled = $enabled;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrimaryAddress(): AddressInterface
|
||||
{
|
||||
return Address::fromArray($this->primaryAddress);
|
||||
}
|
||||
|
||||
public function setPrimaryAddress(AddressInterface $value): static
|
||||
{
|
||||
$this->primaryAddress = $value->toArray();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSecondaryAddresses(): array
|
||||
{
|
||||
return array_map(
|
||||
fn($addr) => $addr instanceof AddressInterface ? $addr : Address::fromArray(is_array($addr) ? $addr : ['address' => (string) $addr])
|
||||
, $this->secondaryAddresses);
|
||||
}
|
||||
|
||||
public function setSecondaryAddresses(array $addresses): static
|
||||
{
|
||||
$this->secondaryAddresses = array_map(
|
||||
fn($addr) => $addr instanceof AddressInterface ? $addr : Address::fromArray(is_array($addr) ? $addr : ['address' => (string) $addr]),
|
||||
$addresses
|
||||
);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasAddress(string $address): bool
|
||||
{
|
||||
$address = strtolower(trim($address));
|
||||
|
||||
if ($this->primaryAddress && strtolower($this->primaryAddress['address'] ?? '') === $address) {
|
||||
return true;
|
||||
}
|
||||
foreach ($this->secondaryAddresses as $secondary) {
|
||||
$secondaryAddr = $secondary instanceof AddressInterface ? $secondary->getAddress() : (string) $secondary;
|
||||
if (strtolower($secondaryAddr) === $address) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
public function getLocation(): ServiceLocation
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function setLocation(ResourceServiceLocationInterface $location): static
|
||||
{
|
||||
$this->location = $location;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function freshLocation(?string $type = null, array $data = []): ServiceLocation
|
||||
{
|
||||
$loc = new ServiceLocation();
|
||||
$loc->jsonDeserialize($data);
|
||||
return $loc;
|
||||
}
|
||||
|
||||
public function getIdentity(): ServiceIdentityBasic
|
||||
{
|
||||
return $this->identity;
|
||||
}
|
||||
|
||||
public function setIdentity(ResourceServiceIdentityInterface $identity): static
|
||||
{
|
||||
$this->identity = $identity;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function freshIdentity(?string $type = null, array $data = []): ServiceIdentityBasic
|
||||
{
|
||||
$id = new ServiceIdentityBasic();
|
||||
$id->jsonDeserialize($data);
|
||||
return $id;
|
||||
}
|
||||
|
||||
public function getDebug(): bool
|
||||
{
|
||||
return ($this->auxiliary['debug'] ?? false) === true;
|
||||
}
|
||||
|
||||
public function setDebug(bool $debug): static
|
||||
{
|
||||
$this->auxiliary['debug'] = $debug;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAuxiliary(): array
|
||||
{
|
||||
return $this->auxiliary;
|
||||
}
|
||||
|
||||
public function setAuxiliary(array $auxiliary): static
|
||||
{
|
||||
$this->auxiliary = $auxiliary;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function collectionListFilter(): Filter
|
||||
{
|
||||
return new Filter($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_FILTER] ?? []);
|
||||
}
|
||||
|
||||
public function collectionListSort(): Sort
|
||||
{
|
||||
return new Sort($this->serviceAbilities[self::CAPABILITY_COLLECTION_LIST_SORT] ?? []);
|
||||
}
|
||||
|
||||
public function collectionFresh(): CollectionResource
|
||||
{
|
||||
return new CollectionResource($this->provider(), $this->identifier());
|
||||
}
|
||||
|
||||
public function entityListFilter(): Filter
|
||||
{
|
||||
return new Filter($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_FILTER] ?? []);
|
||||
}
|
||||
|
||||
public function entityListSort(): Sort
|
||||
{
|
||||
return new Sort($this->serviceAbilities[self::CAPABILITY_ENTITY_LIST_SORT] ?? []);
|
||||
}
|
||||
|
||||
public function entityListRange(RangeType $type): IRange
|
||||
{
|
||||
return match ($type) {
|
||||
RangeType::TALLY => new RangeTally(),
|
||||
default => new Range(),
|
||||
};
|
||||
}
|
||||
|
||||
public function entityFresh(): EntityResource
|
||||
{
|
||||
return new EntityResource($this->provider(), $this->identifier());
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,9 @@ declare(strict_types=1);
|
||||
namespace KTXM\ProviderImap\Service\Cache;
|
||||
|
||||
use KTXM\ProviderImap\Client\Protocol\Command\Argument\FetchOptions;
|
||||
use KTXM\ProviderImap\Providers\CollectionResource;
|
||||
use KTXM\ProviderImap\Providers\EntityResource;
|
||||
use KTXM\ProviderImap\Providers\MessageProperties;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXM\ProviderImap\Service\Live\LiveMailService;
|
||||
use KTXM\ProviderImap\Stores\MailboxStore;
|
||||
use KTXM\ProviderImap\Stores\MessageFileStore;
|
||||
@@ -35,7 +34,7 @@ class HarmonizationService
|
||||
/** Lifetime of a mailbox harmonization lock in seconds; long enough for a large first run */
|
||||
public const LOCK_TTL = 900;
|
||||
|
||||
private ?Service $service = null;
|
||||
private ?ServiceBase $service = null;
|
||||
private ?LiveMailService $live = null;
|
||||
|
||||
public function __construct(
|
||||
@@ -48,7 +47,7 @@ class HarmonizationService
|
||||
/**
|
||||
* Bind a service; returns a new instance so the shared one stays unbound.
|
||||
*/
|
||||
public function for(Service $service, ?LiveMailService $live = null): static
|
||||
public function for(ServiceBase $service, ?LiveMailService $live = null): static
|
||||
{
|
||||
$bound = clone $this;
|
||||
$bound->service = $service;
|
||||
@@ -72,10 +71,7 @@ class HarmonizationService
|
||||
$tenantId = (string) $service->tenantIdentifier();
|
||||
$serviceId = (string) $service->identifier();
|
||||
|
||||
$remote = [];
|
||||
foreach ($live->collectionList() as $name => $mailbox) {
|
||||
$remote[(string) $name] = (new CollectionResource($service->provider(), $serviceId))->fromImap($mailbox);
|
||||
}
|
||||
$remote = $live->collectionList();
|
||||
$cached = $this->mailboxStore->list($serviceId);
|
||||
|
||||
$result = ['added' => [], 'updated' => [], 'removed' => []];
|
||||
@@ -158,7 +154,7 @@ class HarmonizationService
|
||||
|
||||
try {
|
||||
$state = $this->mailboxStore->state($serviceId, $mailbox);
|
||||
$selected = $live->collectionFetch($mailbox)
|
||||
$selected = $live->mailboxFetch($mailbox)
|
||||
?? throw new RuntimeException("Mailbox not found on server: {$mailbox}");
|
||||
$uidValidity = $selected->uidValidity()
|
||||
?? throw new RuntimeException("Server did not report UIDVALIDITY for mailbox: {$mailbox}");
|
||||
@@ -239,7 +235,7 @@ class HarmonizationService
|
||||
$options = FetchOptions::message()->withBodyText(MessageIngestor::BODY_TEXT_LIMIT);
|
||||
|
||||
$ingested = [];
|
||||
foreach ($live->entityFetch($mailbox, $options, ...$uids) as $message) {
|
||||
foreach ($live->messageFetch($mailbox, $options, ...$uids) as $message) {
|
||||
$entity = (new EntityResource($service->provider(), $serviceId))->fromImap($message, $mailbox);
|
||||
array_push($ingested, ...$this->ingestor->ingest($tenantId, $serviceId, $uidValidity, $entity));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ use KTXM\ProviderImap\Client\Protocol\IdentifierMode;
|
||||
use KTXM\ProviderImap\Client\ImapException;
|
||||
use KTXM\ProviderImap\Client\Protocol\Command\Argument\ListReturnOptions;
|
||||
use KTXM\ProviderImap\Client\Mailbox;
|
||||
use KTXM\ProviderImap\Providers\CollectionResource;
|
||||
use KTXM\ProviderImap\Providers\EntityResource;
|
||||
use KTXM\ProviderImap\Client\Message;
|
||||
use KTXM\ProviderImap\Client\MessageAddress;
|
||||
use KTXM\ProviderImap\Client\MessagePart;
|
||||
@@ -49,7 +51,7 @@ use KTXF\Resource\Range\RangeAnchorType;
|
||||
use KTXF\Resource\Range\RangeTally;
|
||||
use KTXF\Resource\Sort\ISort;
|
||||
use KTXF\Resource\BinaryResource;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXM\ProviderImap\Smtp\Client as SmtpClient;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -69,7 +71,7 @@ class LiveMailService
|
||||
private ?Mailbox $selected = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly Service $service,
|
||||
private readonly ServiceBase $service,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -140,11 +142,48 @@ class LiveMailService
|
||||
}
|
||||
|
||||
/**
|
||||
* list of collections in remote storage
|
||||
*
|
||||
* @since Release 1.0.0
|
||||
*/
|
||||
public function collectionList(?string $location = null, IFilter|null $filter = null, ISort|null $sort = null, string $depth = '*'): Generator
|
||||
* List the collections of the account, keyed by mailbox name.
|
||||
*
|
||||
* @return array<string, CollectionResource>
|
||||
*/
|
||||
public function collectionList(?string $location = null, ?IFilter $filter = null, ?ISort $sort = null): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->mailboxList($location, $filter, $sort) as $name => $mailbox) {
|
||||
$list[(string) $name] = $this->collectionResource($mailbox);
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single collection by its mailbox name; null when it does not exist.
|
||||
*/
|
||||
public function collectionFetch(string $identifier): ?CollectionResource
|
||||
{
|
||||
$mailbox = $this->mailboxFetch($identifier);
|
||||
|
||||
return $mailbox === null ? null : $this->collectionResource($mailbox);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hierarchy delimiter of the account, from `LIST "" ""` (no mailboxes are listed); "/" when unknown.
|
||||
*/
|
||||
public function collectionDelimiter(): string
|
||||
{
|
||||
foreach ($this->imapClient()->perform(new ListCommand('', '')) as $mailbox) {
|
||||
return $mailbox->delimiter() ?: '/';
|
||||
}
|
||||
|
||||
return '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* List the IMAP mailboxes of the account (with status), keyed by name.
|
||||
*
|
||||
* @return Generator<string, Mailbox>
|
||||
*/
|
||||
public function mailboxList(?string $location = null, IFilter|null $filter = null, ISort|null $sort = null, string $depth = '*'): Generator
|
||||
{
|
||||
// Prepare location filter
|
||||
if (!empty($location)) {
|
||||
@@ -196,11 +235,11 @@ class LiveMailService
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single mailbox by its full name.
|
||||
* Fetch a single IMAP mailbox (with status) by its full name.
|
||||
*
|
||||
* Returns null when no mailbox matching $identifier is found.
|
||||
*/
|
||||
public function collectionFetch(string $identifier): ?Mailbox
|
||||
public function mailboxFetch(string $identifier): ?Mailbox
|
||||
{
|
||||
// LIST-STATUS (RFC 5819) returns the status with the LIST response in one round trip
|
||||
$listStatus = $this->imapClient()->hasCapability('LIST-STATUS');
|
||||
@@ -225,38 +264,38 @@ class LiveMailService
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new IMAP mailbox and return it.
|
||||
* Create a new IMAP mailbox and return it as a collection resource.
|
||||
*
|
||||
* If the server-side LIST cannot confirm the new mailbox (e.g., immediate
|
||||
* consistency), a lightweight stub resource is returned instead.
|
||||
*/
|
||||
public function collectionCreate(string $name): Mailbox
|
||||
public function collectionCreate(string $name, ?string $delimiter = null): CollectionResource
|
||||
{
|
||||
$this->imapClient()->perform(new CreateCommand($name));
|
||||
|
||||
// Attempt to refetch the new mailbox from the server
|
||||
$mailbox = $this->collectionFetch($name);
|
||||
$mailbox = $this->mailboxFetch($name);
|
||||
if ($mailbox === null) {
|
||||
throw new ImapException('Failed to create mailbox: ' . $name);
|
||||
}
|
||||
|
||||
return $mailbox;
|
||||
return $this->collectionResource($mailbox, $delimiter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename a mailbox and return the updated resource.
|
||||
*/
|
||||
public function collectionRename(string $oldName, string $newName): Mailbox
|
||||
public function collectionRename(string $oldName, string $newName, ?string $delimiter = null): CollectionResource
|
||||
{
|
||||
$this->imapClient()->perform(new RenameCommand($oldName, $newName));
|
||||
|
||||
$mailbox = $this->collectionFetch($newName);
|
||||
$mailbox = $this->mailboxFetch($newName);
|
||||
|
||||
if ($mailbox === null) {
|
||||
throw new ImapException('Failed to rename mailbox: ' . $oldName . ' to ' . $newName);
|
||||
}
|
||||
|
||||
return $mailbox;
|
||||
return $this->collectionResource($mailbox, $delimiter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,11 +393,36 @@ class LiveMailService
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a list of messages in a mailbox matching the given filter, sorted and paginated as requested.
|
||||
* List the entities of a mailbox matching the given filter, sorted and paginated as requested.
|
||||
*
|
||||
* @return Message[] list of messages matching the filter, sorted and paginated as requested
|
||||
* @return Generator<int, EntityResource> keyed by UID
|
||||
*/
|
||||
public function entityList(string $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): Generator
|
||||
{
|
||||
foreach ($this->messageList($collection, $filter, $sort, $range) as $message) {
|
||||
$entity = $this->entityResource($message, $collection);
|
||||
yield (int) $entity->identifier() => $entity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch entities by UID (full TEXT).
|
||||
*
|
||||
* @return Generator<int, EntityResource> keyed by UID
|
||||
*/
|
||||
public function entityFetch(string $collection, int ...$uids): Generator
|
||||
{
|
||||
foreach ($this->messageFetch($collection, null, ...$uids) as $uid => $message) {
|
||||
yield $uid => $this->entityResource($message, $collection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List IMAP messages of a mailbox matching the given filter, sorted and paginated as requested.
|
||||
*
|
||||
* @return Generator<Message>
|
||||
*/
|
||||
public function messageList(string $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): Generator
|
||||
{
|
||||
// text parts normally precede attachments, so capping TEXT avoids transferring attachment bytes
|
||||
$options = FetchOptions::message()->withBodyText(self::LIST_BODY_TEXT_LIMIT);
|
||||
@@ -387,16 +451,16 @@ class LiveMailService
|
||||
return [];
|
||||
}
|
||||
|
||||
yield from $this->entityFetch($collection, $options, ...$uids);
|
||||
yield from $this->messageFetch($collection, $options, ...$uids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one or more messages by UID and return EntityResource objects.
|
||||
* Fetch one or more messages by UID as IMAP messages.
|
||||
*
|
||||
* @param int ...$uids
|
||||
* @return Message[] keyed by UID
|
||||
* @return Generator<int, Message> keyed by UID
|
||||
*/
|
||||
public function entityFetch(string $collection, ?FetchOptions $options = null, int ...$uids): Generator
|
||||
public function messageFetch(string $collection, ?FetchOptions $options = null, int ...$uids): Generator
|
||||
{
|
||||
if (empty($uids)) {
|
||||
return [];
|
||||
@@ -1181,6 +1245,18 @@ class LiveMailService
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
private function collectionResource(Mailbox $mailbox, ?string $delimiter = null): CollectionResource
|
||||
{
|
||||
return (new CollectionResource($this->service->provider(), $this->service->identifier()))
|
||||
->fromImap($mailbox, ['delimiter' => $delimiter]);
|
||||
}
|
||||
|
||||
private function entityResource(Message $message, string $collection): EntityResource
|
||||
{
|
||||
return (new EntityResource($this->service->provider(), $this->service->identifier()))
|
||||
->fromImap($message, $collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a mailbox, reusing the current selection when possible.
|
||||
*
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace KTXM\ProviderImap\Stores;
|
||||
use KTXC\Db\DataStore;
|
||||
use KTXF\Security\Crypto;
|
||||
use KTXF\Utile\UUID;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
|
||||
/**
|
||||
* IMAP Service Store
|
||||
@@ -114,7 +114,7 @@ class ServiceStore
|
||||
/**
|
||||
* Create a new service
|
||||
*/
|
||||
public function create(string $tenantId, string $userId, Service $service): array
|
||||
public function create(string $tenantId, string $userId, ServiceBase $service): array
|
||||
{
|
||||
$document = $service->toStore();
|
||||
|
||||
@@ -136,7 +136,7 @@ class ServiceStore
|
||||
/**
|
||||
* Modify an existing service
|
||||
*/
|
||||
public function modify(string $tenantId, string $userId, Service $service): array
|
||||
public function modify(string $tenantId, string $userId, ServiceBase $service): array
|
||||
{
|
||||
$serviceId = $service->identifier();
|
||||
if (empty($serviceId)) {
|
||||
|
||||
@@ -6,7 +6,9 @@ namespace KTXT\ProviderImap\Tests\Unit;
|
||||
|
||||
use KTXM\ProviderImap\Client\{Client, ConnectionConfig};
|
||||
use KTXM\ProviderImap\Client\Transport\{ConnectionInterface, ConnectionFactoryInterface};
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\CollectionResource;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXM\ProviderImap\Providers\LiveService;
|
||||
use KTXM\ProviderImap\Service\Live\LiveMailService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -16,7 +18,7 @@ final class CollectionFetchTest extends TestCase
|
||||
|
||||
public function testWithListStatusUsesOneRoundTrip(): void
|
||||
{
|
||||
$mailbox = $this->service(listStatus: true)->collectionFetch('INBOX');
|
||||
$mailbox = $this->service(listStatus: true)->mailboxFetch('INBOX');
|
||||
|
||||
$this->assertCount(1, $this->commands);
|
||||
$this->assertStringContainsString('RETURN (STATUS', $this->commands[0]);
|
||||
@@ -27,7 +29,7 @@ final class CollectionFetchTest extends TestCase
|
||||
|
||||
public function testWithoutListStatusFallsBackToStatus(): void
|
||||
{
|
||||
$mailbox = $this->service(listStatus: false)->collectionFetch('INBOX');
|
||||
$mailbox = $this->service(listStatus: false)->mailboxFetch('INBOX');
|
||||
|
||||
$this->assertCount(2, $this->commands);
|
||||
$this->assertStringNotContainsString('RETURN', $this->commands[0]);
|
||||
@@ -39,7 +41,36 @@ final class CollectionFetchTest extends TestCase
|
||||
|
||||
public function testUnknownMailboxIsNull(): void
|
||||
{
|
||||
$this->assertNull($this->service(listStatus: false, exists: false)->collectionFetch('Missing'));
|
||||
$this->assertNull($this->service(listStatus: false, exists: false)->mailboxFetch('Missing'));
|
||||
}
|
||||
|
||||
public function testCollectionsAreResourcesKeyedByName(): void
|
||||
{
|
||||
$service = $this->service(listStatus: true);
|
||||
|
||||
$list = $service->collectionList();
|
||||
$collection = $service->collectionFetch('INBOX');
|
||||
|
||||
$this->assertSame(['INBOX'], array_keys($list));
|
||||
$this->assertInstanceOf(CollectionResource::class, $list['INBOX']);
|
||||
$this->assertInstanceOf(CollectionResource::class, $collection);
|
||||
$this->assertSame('INBOX', $collection?->identifier());
|
||||
$this->assertNull($this->service(listStatus: true, exists: false)->collectionFetch('Missing'));
|
||||
}
|
||||
|
||||
public function testDelimiterIsReadWithoutListingMailboxes(): void
|
||||
{
|
||||
$this->assertSame('/', $this->service(listStatus: false)->collectionDelimiter());
|
||||
$this->assertSame(['LIST "" ""'], $this->commands);
|
||||
}
|
||||
|
||||
public function testCollectionExtantReportsMissingMailboxes(): void
|
||||
{
|
||||
$service = new LiveService();
|
||||
$live = $this->service(listStatus: true, exists: false);
|
||||
(new \ReflectionProperty(LiveService::class, 'mailService'))->setValue($service, $live);
|
||||
|
||||
$this->assertSame(['Missing' => false], $service->collectionExtant('Missing'));
|
||||
}
|
||||
|
||||
private function service(bool $listStatus, bool $exists = true): LiveMailService
|
||||
@@ -57,6 +88,11 @@ final class CollectionFetchTest extends TestCase
|
||||
} else {
|
||||
$this->commands[] = $command;
|
||||
}
|
||||
if ($operation === 'LIST' && $command === 'LIST "" ""') {
|
||||
$lines[] = "* LIST (\\Noselect) \"/\" \"\"\r\n";
|
||||
$lines[] = "$tag OK Completed\r\n";
|
||||
return;
|
||||
}
|
||||
if ($operation === 'LIST') {
|
||||
if (!$listStatus && str_contains($command, 'RETURN')) {
|
||||
$lines[] = "$tag BAD Unknown RETURN option\r\n";
|
||||
@@ -80,8 +116,8 @@ final class CollectionFetchTest extends TestCase
|
||||
$client = new Client($factory);
|
||||
$client->connect(new ConnectionConfig('localhost'));
|
||||
|
||||
return new class($this->createStub(Service::class), $client) extends LiveMailService {
|
||||
public function __construct(Service $service, private readonly Client $client)
|
||||
return new class($this->createStub(ServiceBase::class), $client) extends LiveMailService {
|
||||
public function __construct(ServiceBase $service, private readonly Client $client)
|
||||
{
|
||||
parent::__construct($service);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use KTXM\ProviderImap\Client\{Client, ConnectionConfig, ImapException};
|
||||
use KTXM\ProviderImap\Client\Protocol\Command\CreateCommand;
|
||||
use KTXM\ProviderImap\Client\Result\CommandCompletion;
|
||||
use KTXM\ProviderImap\Client\Transport\{ConnectionInterface, ConnectionFactoryInterface};
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXM\ProviderImap\Service\Live\LiveMailService;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -48,8 +48,8 @@ final class CommandCompletionTest extends TestCase
|
||||
|
||||
private function service(Client $client): LiveMailService
|
||||
{
|
||||
return new class($this->createStub(Service::class), $client) extends LiveMailService {
|
||||
public function __construct(Service $service, private readonly Client $client)
|
||||
return new class($this->createStub(ServiceBase::class), $client) extends LiveMailService {
|
||||
public function __construct(ServiceBase $service, private readonly Client $client)
|
||||
{
|
||||
parent::__construct($service);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use KTXF\Resource\Filter\IFilter;
|
||||
use KTXF\Resource\Sort\ISort;
|
||||
use KTXM\ProviderImap\Client\Mailbox;
|
||||
use KTXM\ProviderImap\Providers\CollectionResource;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\LiveService;
|
||||
use KTXM\ProviderImap\Service\Cache\HarmonizationService;
|
||||
use KTXM\ProviderImap\Service\Cache\MessageIngestor;
|
||||
use KTXM\ProviderImap\Service\Live\LiveMailService;
|
||||
@@ -146,7 +146,7 @@ final class HarmonizationServiceTest extends TestCase
|
||||
$this->calls[] = ['files.deleteByMailbox', $tid, $sid, $name];
|
||||
});
|
||||
|
||||
$service = (new Service())->fromStore(['tid' => 'tenant', 'sid' => 'svc']);
|
||||
$service = (new LiveService())->fromStore(['tid' => 'tenant', 'sid' => 'svc']);
|
||||
$live = new HarmonizationServiceTestLiveStub($service);
|
||||
$live->mailboxes = array_map(static fn (string $name): Mailbox => new Mailbox($name, '/', []), $remote);
|
||||
|
||||
@@ -166,10 +166,12 @@ final class HarmonizationServiceTestLiveStub extends LiveMailService
|
||||
/** @var Mailbox[] */
|
||||
public array $mailboxes = [];
|
||||
|
||||
public function collectionList(?string $location = null, IFilter|null $filter = null, ISort|null $sort = null, string $depth = '*'): Generator
|
||||
public function collectionList(?string $location = null, ?IFilter $filter = null, ?ISort $sort = null): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->mailboxes as $mailbox) {
|
||||
yield $mailbox->name() => $mailbox;
|
||||
$list[$mailbox->name()] = (new CollectionResource('imap', 'svc'))->fromImap($mailbox);
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace KTXT\ProviderImap\Tests\Unit;
|
||||
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXM\ProviderImap\Service\Live\LiveMailService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
@@ -12,7 +12,7 @@ use RuntimeException;
|
||||
final class LiveMailServiceTest extends TestCase {
|
||||
|
||||
public function testEntityReplaceReturnsAuthoritativeReplacementUid(): void {
|
||||
$service = new LiveMailServiceStub($this->createStub(Service::class));
|
||||
$service = new LiveMailServiceStub($this->createStub(ServiceBase::class));
|
||||
$service->appendUid = 202;
|
||||
$service->destroyResults = [[101 => true]];
|
||||
|
||||
@@ -28,7 +28,7 @@ final class LiveMailServiceTest extends TestCase {
|
||||
}
|
||||
|
||||
public function testEntityReplaceRemovesReplacementWhenOldUidCannotBeDeleted(): void {
|
||||
$service = new LiveMailServiceStub($this->createStub(Service::class));
|
||||
$service = new LiveMailServiceStub($this->createStub(ServiceBase::class));
|
||||
$service->appendUid = 202;
|
||||
$service->destroyResults = [
|
||||
[101 => false],
|
||||
|
||||
@@ -4,19 +4,20 @@ declare(strict_types=1);
|
||||
|
||||
namespace KTXT\ProviderImap\Tests\Unit;
|
||||
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXM\ProviderImap\Providers\LiveService;
|
||||
use KTXM\ProviderImap\Service\Live\LiveMailService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionProperty;
|
||||
|
||||
final class ServiceTest extends TestCase
|
||||
final class LiveServiceTest extends TestCase
|
||||
{
|
||||
public function testEntityExtantReportsExistenceOfEachIdentifier(): void
|
||||
{
|
||||
$service = new Service();
|
||||
$live = new ServiceTestLiveStub($this->createStub(Service::class));
|
||||
$service = new LiveService();
|
||||
$live = new LiveServiceTestLiveStub($this->createStub(ServiceBase::class));
|
||||
$live->existing = [5];
|
||||
(new ReflectionProperty(Service::class, 'liveService'))->setValue($service, $live);
|
||||
(new ReflectionProperty(LiveService::class, 'mailService'))->setValue($service, $live);
|
||||
|
||||
$result = $service->entityExtant('INBOX', 5, '6', 'abc', 0);
|
||||
|
||||
@@ -25,7 +26,7 @@ final class ServiceTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
final class ServiceTestLiveStub extends LiveMailService
|
||||
final class LiveServiceTestLiveStub extends LiveMailService
|
||||
{
|
||||
/** @var int[] */
|
||||
public array $existing = [];
|
||||
@@ -6,7 +6,7 @@ namespace KTXT\ProviderImap\Tests\Unit;
|
||||
|
||||
use KTXM\ProviderImap\Client\{Client, ConnectionConfig, ImapException};
|
||||
use KTXM\ProviderImap\Client\Transport\{ConnectionInterface, ConnectionFactoryInterface};
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\ServiceBase;
|
||||
use KTXM\ProviderImap\Service\Live\LiveMailService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
@@ -101,8 +101,8 @@ final class MailboxSelectionTest extends TestCase
|
||||
$client = new Client($factory);
|
||||
$client->connect(new ConnectionConfig('localhost'));
|
||||
|
||||
return new class($this->createStub(Service::class), $client) extends LiveMailService {
|
||||
public function __construct(Service $service, private readonly Client $client)
|
||||
return new class($this->createStub(ServiceBase::class), $client) extends LiveMailService {
|
||||
public function __construct(ServiceBase $service, private readonly Client $client)
|
||||
{
|
||||
parent::__construct($service);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use KTXM\ProviderImap\Client\Protocol\Command\Argument\FetchOptions;
|
||||
use KTXM\ProviderImap\Client\Protocol\Parser\FetchMessageParser;
|
||||
use KTXM\ProviderImap\Providers\CollectionResource;
|
||||
use KTXM\ProviderImap\Providers\EntityResource;
|
||||
use KTXM\ProviderImap\Providers\Service;
|
||||
use KTXM\ProviderImap\Providers\LiveService;
|
||||
use KTXM\ProviderImap\Service\Cache\HarmonizationService;
|
||||
use KTXM\ProviderImap\Service\Cache\MessageDeltaService;
|
||||
use KTXM\ProviderImap\Service\Cache\MessageIngestor;
|
||||
@@ -40,7 +40,7 @@ final class MessageHarmonizationTest extends TestCase
|
||||
$this->messages = new FakeMessageStore($dataStore);
|
||||
$this->files = new FakeMessageFileStore('/nonexistent');
|
||||
|
||||
$service = (new Service())->fromStore(['tid' => 'tenant', 'sid' => 'svc']);
|
||||
$service = (new LiveService())->fromStore(['tid' => 'tenant', 'sid' => 'svc']);
|
||||
$this->live = new MessageHarmonizationLiveStub($service);
|
||||
$this->live->uidValidity = 7;
|
||||
$this->live->uidNext = 100;
|
||||
@@ -286,14 +286,16 @@ final class MessageHarmonizationLiveStub extends LiveMailService
|
||||
/** @var string[] */
|
||||
public array $failing = [];
|
||||
|
||||
public function collectionList(?string $location = null, IFilter|null $filter = null, ISort|null $sort = null, string $depth = '*'): Generator
|
||||
public function collectionList(?string $location = null, ?IFilter $filter = null, ?ISort $sort = null): array
|
||||
{
|
||||
$list = [];
|
||||
foreach ($this->mailboxList as $mailbox) {
|
||||
yield $mailbox->name() => $mailbox;
|
||||
$list[$mailbox->name()] = (new CollectionResource('imap', 'svc'))->fromImap($mailbox);
|
||||
}
|
||||
return $list;
|
||||
}
|
||||
|
||||
public function collectionFetch(string $identifier): ?Mailbox
|
||||
public function mailboxFetch(string $identifier): ?Mailbox
|
||||
{
|
||||
if (in_array($identifier, $this->failing, true)) {
|
||||
return null;
|
||||
@@ -307,7 +309,7 @@ final class MessageHarmonizationLiveStub extends LiveMailService
|
||||
yield from $this->flags;
|
||||
}
|
||||
|
||||
public function entityFetch(string $collection, ?FetchOptions $options = null, int ...$uids): Generator
|
||||
public function messageFetch(string $collection, ?FetchOptions $options = null, int ...$uids): Generator
|
||||
{
|
||||
$this->fetchBatches[] = $uids;
|
||||
if (preg_match('/BODY\.PEEK\[TEXT\]<0\.(\d+)>/', (string) $options?->toCommand(), $matches) === 1) {
|
||||
|
||||
Reference in New Issue
Block a user