diff --git a/lib/Providers/MessageProperties.php b/lib/Providers/MessageProperties.php index 35da2e1..09a2fa5 100644 --- a/lib/Providers/MessageProperties.php +++ b/lib/Providers/MessageProperties.php @@ -112,20 +112,33 @@ class MessageProperties extends MessagePropertiesMutableAbstract { return $this; } - private static function normalizeFlag(string $flag): string + public function toImap(): array { - $flag = ltrim($flag, '\\'); + $message = []; - return match (strtolower($flag)) { - 'seen' => 'read', - 'flagged' => 'flagged', - 'answered' => 'answered', - 'draft' => 'draft', - 'deleted' => 'deleted', - default => strtolower($flag), - }; + if (isset($this->data['flags'])) { + $message['flags'] = $this->data['flags']; + } + + return $message; } + /** + * 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. * @@ -181,18 +194,4 @@ class MessageProperties extends MessagePropertiesMutableAbstract { $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; - } } diff --git a/lib/Providers/Service.php b/lib/Providers/Service.php index 8cf2993..6ee4e03 100644 --- a/lib/Providers/Service.php +++ b/lib/Providers/Service.php @@ -12,10 +12,13 @@ namespace KTXM\ProviderImap\Providers; use Generator; use KTXF\Mail\Collection\CollectionBaseInterface; use KTXF\Mail\Collection\CollectionMutableInterface; +use KTXF\Mail\Entity\EntityBaseInterface; +use KTXF\Mail\Entity\EntityMutableInterface; 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\ServiceConfigurableInterface; use KTXF\Mail\Service\ServiceMutableInterface; use KTXF\Resource\Provider\ResourceServiceIdentityInterface; @@ -37,12 +40,13 @@ use KTXM\ProviderImap\Service\Remote\RemoteMailService; use KTXM\ProviderImap\Service\Remote\RemoteService; use KTXM\ProviderImap\Providers\CollectionResource; use KTXF\Mail\Collection\CollectionRoles; +use KTXF\Mail\Object\MessagePropertiesMutableInterface; use KTXM\ProviderImap\Providers\EntityResource; /** * 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; @@ -354,6 +358,8 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC { $this->initialize(); + $list = []; + foreach ($this->mailService->collectionList($location, $filter, $sort) as $mailbox) { $resource = $this->collectionFresh(); $resource->fromImap($mailbox); @@ -377,10 +383,10 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC { $this->initialize(); - $mailboxes = $this->collectionList(); + $mailboxes = $this->collectionList(null); $extant = []; foreach ($identifiers as $id) { - $extant[(string) $id] = isset($existing[(string) $id]); + $extant[(string) $id] = isset($mailboxes[(string) $id]); } return $extant; } @@ -400,12 +406,12 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC return $collection; } - public function collectionFresh(): CollectionMutableInterface + public function collectionFresh(): CollectionResource { 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(); @@ -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(); @@ -571,10 +577,20 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC 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 - $identifiers = $this->groupEntitiesByCollection(...$identifiers); + $targets = $this->groupEntitiesByCollection(...$targets); // determine delete mode and target collection (e.g. Trash) if applicable $deleteMode = $this->auxiliary['deleteMode'] ?? 'soft'; @@ -606,7 +622,7 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC // entities need to be moved or deleted by collection $list = []; - foreach ($identifiers as $sourceCollection => $sourceEntities) { + foreach ($targets as $sourceCollection => $sourceEntities) { if ($deleteMode === 'soft' && $sourceCollection === $deleteTargetNative) { continue; } @@ -631,7 +647,30 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC 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 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 - $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 $this->initialize(); + $list = []; - foreach ($identifiers as $sourceCollection => $sourceEntities) { + + foreach ($sources as $sourceCollection => $sourceEntities) { $uids = array_keys($sourceEntities); $mutations = $this->mailService->entityMove($target->collection(), $sourceCollection, ...$uids); diff --git a/lib/Service/Remote/RemoteMailService.php b/lib/Service/Remote/RemoteMailService.php index 398b669..6aade5e 100644 --- a/lib/Service/Remote/RemoteMailService.php +++ b/lib/Service/Remote/RemoteMailService.php @@ -59,8 +59,6 @@ class RemoteMailService public function __construct( 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); } + 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 { if (empty($uids)) {