feat: implement entity mutable interface

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-05-09 17:03:58 -04:00
parent d9bc526126
commit 086d7a1366
3 changed files with 134 additions and 38 deletions

View File

@@ -112,20 +112,33 @@ class MessageProperties extends MessagePropertiesMutableAbstract {
return $this; return $this;
} }
private static function normalizeFlag(string $flag): string public function toImap(): array
{ {
$flag = ltrim($flag, '\\'); $message = [];
return match (strtolower($flag)) { if (isset($this->data['flags'])) {
'seen' => 'read', $message['flags'] = $this->data['flags'];
'flagged' => 'flagged', }
'answered' => 'answered',
'draft' => 'draft', return $message;
'deleted' => 'deleted',
default => strtolower($flag),
};
} }
/**
* Hydrate from store array
*/
public function fromStore(array $data): static {
$this->data = $data;
return $this;
}
/**
* Serialize to store array
*/
public function toStore(): array {
return $this->data;
}
/** /**
* Convert a string to UTF-8 from the given charset. * Convert a string to UTF-8 from the given charset.
* *
@@ -181,18 +194,4 @@ class MessageProperties extends MessagePropertiesMutableAbstract {
$attachments[] = $part->toArray(); $attachments[] = $part->toArray();
} }
/**
* Serialize to store array
*/
public function toStore(): array {
return $this->data;
}
/**
* Hydrate from store array
*/
public function fromStore(array $data): static {
$this->data = $data;
return $this;
}
} }

View File

@@ -12,10 +12,13 @@ namespace KTXM\ProviderImap\Providers;
use Generator; use Generator;
use KTXF\Mail\Collection\CollectionBaseInterface; use KTXF\Mail\Collection\CollectionBaseInterface;
use KTXF\Mail\Collection\CollectionMutableInterface; use KTXF\Mail\Collection\CollectionMutableInterface;
use KTXF\Mail\Entity\EntityBaseInterface;
use KTXF\Mail\Entity\EntityMutableInterface;
use KTXF\Mail\Object\Address; use KTXF\Mail\Object\Address;
use KTXF\Mail\Object\AddressInterface; 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\ServiceConfigurableInterface; use KTXF\Mail\Service\ServiceConfigurableInterface;
use KTXF\Mail\Service\ServiceMutableInterface; use KTXF\Mail\Service\ServiceMutableInterface;
use KTXF\Resource\Provider\ResourceServiceIdentityInterface; use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
@@ -37,12 +40,13 @@ use KTXM\ProviderImap\Service\Remote\RemoteMailService;
use KTXM\ProviderImap\Service\Remote\RemoteService; 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 KTXM\ProviderImap\Providers\EntityResource; use KTXM\ProviderImap\Providers\EntityResource;
/** /**
* IMAP Mail Service * IMAP Mail Service
*/ */
class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceConfigurableInterface, ServiceCollectionMutableInterface, ServiceEntityMutableInterface
{ {
public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE; public const JSON_TYPE = ServiceBaseInterface::JSON_TYPE;
@@ -354,6 +358,8 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
{ {
$this->initialize(); $this->initialize();
$list = [];
foreach ($this->mailService->collectionList($location, $filter, $sort) as $mailbox) { foreach ($this->mailService->collectionList($location, $filter, $sort) as $mailbox) {
$resource = $this->collectionFresh(); $resource = $this->collectionFresh();
$resource->fromImap($mailbox); $resource->fromImap($mailbox);
@@ -377,10 +383,10 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
{ {
$this->initialize(); $this->initialize();
$mailboxes = $this->collectionList(); $mailboxes = $this->collectionList(null);
$extant = []; $extant = [];
foreach ($identifiers as $id) { foreach ($identifiers as $id) {
$extant[(string) $id] = isset($existing[(string) $id]); $extant[(string) $id] = isset($mailboxes[(string) $id]);
} }
return $extant; return $extant;
} }
@@ -400,12 +406,12 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
return $collection; return $collection;
} }
public function collectionFresh(): CollectionMutableInterface public function collectionFresh(): CollectionResource
{ {
return new CollectionResource($this->provider(), $this->identifier()); return new CollectionResource($this->provider(), $this->identifier());
} }
public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionBaseInterface public function collectionCreate(string|int|null $location, CollectionMutableInterface $collection, array $options = []): CollectionBaseInterface
{ {
$this->initialize(); $this->initialize();
@@ -540,7 +546,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
}; };
} }
public function entityFetch(string|int $collection, string|int ...$identifiers): array public function entityFetch(string|int $collection, string|int ...$identifiers): array
{ {
$this->initialize(); $this->initialize();
@@ -571,10 +577,20 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
return new EntityResource($this->provider(), $this->identifier()); return new EntityResource($this->provider(), $this->identifier());
} }
public function entityDelete(EntityIdentifier ...$identifiers): array public function entityCreate(CollectionIdentifier $target, MessagePropertiesMutableInterface $properties, array $options = []): EntityResource
{
return $this->entityFresh();
}
public function entityModify(EntityIdentifier $target, MessagePropertiesMutableInterface $properties): EntityResource
{
return $this->entityFresh();
}
public function entityDelete(EntityIdentifier ...$targets): array
{ {
// validate identifiers and group by collection // validate identifiers and group by collection
$identifiers = $this->groupEntitiesByCollection(...$identifiers); $targets = $this->groupEntitiesByCollection(...$targets);
// determine delete mode and target collection (e.g. Trash) if applicable // determine delete mode and target collection (e.g. Trash) if applicable
$deleteMode = $this->auxiliary['deleteMode'] ?? 'soft'; $deleteMode = $this->auxiliary['deleteMode'] ?? 'soft';
@@ -606,7 +622,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
// entities need to be moved or deleted by collection // entities need to be moved or deleted by collection
$list = []; $list = [];
foreach ($identifiers as $sourceCollection => $sourceEntities) { foreach ($targets as $sourceCollection => $sourceEntities) {
if ($deleteMode === 'soft' && $sourceCollection === $deleteTargetNative) { if ($deleteMode === 'soft' && $sourceCollection === $deleteTargetNative) {
continue; continue;
} }
@@ -631,7 +647,30 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
return $results; return $results;
} }
public function entityMove(CollectionIdentifier $target, EntityIdentifier ...$identifiers): array public function entityPatch(MessagePropertiesMutableInterface $properties, EntityIdentifier ...$targets): array
{
// validate identifiers and group by collection
$targets = $this->groupEntitiesByCollection(...$targets);
// move entities on remote store and construct result map
$this->initialize();
$list = [];
foreach ($targets as $targetCollection => $targetIdentifiers) {
$uids = array_keys($targetIdentifiers);
$mutations = $this->mailService->entityPatch($targetCollection, $properties, ...$uids);
foreach ($uids as $uid) {
$list[(string)$targetIdentifiers[$uid]] = ['disposition' => 'patched'];
}
}
return $list;
}
public function entityCopy(CollectionIdentifier $target, EntityIdentifier ...$sources): array
{ {
// validate target belongs to this service // validate target belongs to this service
if ($target->provider() !== $this->provider() || $target->service() !== $this->identifier()) { if ($target->provider() !== $this->provider() || $target->service() !== $this->identifier()) {
@@ -639,12 +678,47 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
} }
// validate identifiers and group by collection // validate identifiers and group by collection
$identifiers = $this->groupEntitiesByCollection(...$identifiers); $sources = $this->groupEntitiesByCollection(...$sources);
// copy entities on remote store and construct result map
$this->initialize();
$list = [];
foreach ($sources as $sourceCollection => $sourceEntities) {
$uids = array_keys($sourceEntities);
$mutations = $this->mailService->entityCopy($target->collection(), $sourceCollection, ...$uids);
foreach ($uids as $uid) {
$mutatedUid = $mutations[$uid] ?? null;
$list[(string)$sourceEntities[$uid]] = [
'disposition' => $mutatedUid !== null ? 'copied' : 'error',
'destination' => $target,
'mutation' => $mutatedUid !== null ? new EntityIdentifier($this->provider(), $this->identifier(), $target->collection(), $mutatedUid) : null,
];
}
}
return $list;
}
public function entityMove(CollectionIdentifier $target, EntityIdentifier ...$sources): array
{
// validate target belongs to this service
if ($target->provider() !== $this->provider() || $target->service() !== $this->identifier()) {
throw new \InvalidArgumentException('Target collection does not belong to this service: ' . $target);
}
// validate identifiers and group by collection
$sources = $this->groupEntitiesByCollection(...$sources);
// move entities on remote store and construct result map // move entities on remote store and construct result map
$this->initialize(); $this->initialize();
$list = []; $list = [];
foreach ($identifiers as $sourceCollection => $sourceEntities) {
foreach ($sources as $sourceCollection => $sourceEntities) {
$uids = array_keys($sourceEntities); $uids = array_keys($sourceEntities);
$mutations = $this->mailService->entityMove($target->collection(), $sourceCollection, ...$uids); $mutations = $this->mailService->entityMove($target->collection(), $sourceCollection, ...$uids);

View File

@@ -59,8 +59,6 @@ class RemoteMailService
public function __construct( public function __construct(
private readonly Client $client, private readonly Client $client,
private readonly string $provider,
private readonly string|int $service,
) {} ) {}
/** /**
@@ -330,6 +328,31 @@ class RemoteMailService
return array_fill_keys($uids, true); return array_fill_keys($uids, true);
} }
public function entityPatch(string $collection, array $flagsToAdd = [], array $flagsToRemove = [], int ...$uids): void
{
if (empty($uids)) {
return;
}
$this->client->perform(new SelectCommand($collection, false));
if (!empty($flagsToAdd)) {
$this->client->perform(new StoreCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$flagsToAdd,
'+',
));
}
if (!empty($flagsToRemove)) {
$this->client->perform(new StoreCommand(
FetchTarget::uid(SequenceSet::items(...array_values($uids))),
$flagsToRemove,
'-',
));
}
}
public function entityMove(string $targetCollection, string $sourceCollection, int ...$uids): array public function entityMove(string $targetCollection, string $sourceCollection, int ...$uids): array
{ {
if (empty($uids)) { if (empty($uids)) {