From acc42d09ee45194e51f4f694d2cc235c244421ee Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Thu, 23 Apr 2026 22:04:36 -0400 Subject: [PATCH] refactor: bunch of improvements Signed-off-by: Sebastian Krupinski --- lib/Providers/Mail/Service.php | 74 ++++- lib/Service/Remote/RemoteMailService.php | 43 ++- lib/Service/Remote/RemoteService.php | 6 +- src/components/JmapAuthPanel.vue | 212 ++++++++---- ...pConfigPanel.vue => JmapProtocolPanel.vue} | 307 ++++++++---------- src/integrations.ts | 9 +- src/models/JmapServiceObject.ts | 8 + src/types/auxiliary.ts | 6 + 8 files changed, 406 insertions(+), 259 deletions(-) rename src/components/{JmapConfigPanel.vue => JmapProtocolPanel.vue} (64%) diff --git a/lib/Providers/Mail/Service.php b/lib/Providers/Mail/Service.php index 39db91f..5d5a683 100644 --- a/lib/Providers/Mail/Service.php +++ b/lib/Providers/Mail/Service.php @@ -11,6 +11,7 @@ namespace KTXM\ProviderJmapc\Providers\Mail; use Generator; use KTXF\Mail\Collection\CollectionBaseInterface; +use KTXF\Mail\Collection\CollectionRoles; use KTXF\Mail\Collection\CollectionMutableInterface; use KTXF\Mail\Object\Address; use KTXF\Mail\Object\AddressInterface; @@ -24,6 +25,8 @@ 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\RangeType; @@ -95,6 +98,8 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC self::CAPABILITY_ENTITY_DELTA => true, self::CAPABILITY_ENTITY_EXTANT => true, self::CAPABILITY_ENTITY_FETCH => true, + //self::CAPABILITY_ENTITY_DELETE => true, + //self::CAPABILITY_ENTITY_MOVE => true, ]; private readonly RemoteMailService $mailService; @@ -535,12 +540,75 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC return $entities; } - public function entityMove(string|int $target, array $sources): array + public function entityDelete(EntityIdentifier ...$identifiers): array { + // validate identifiers and construct ID list + $ids = []; + foreach ($identifiers as $identifier) { + if ($identifier->provider() !== $this->provider() || $identifier->service() !== (string)$this->identifier()) { + throw new \InvalidArgumentException('Entity identifier does not belong to this service: ' . (string)$identifier); + } + $ids[] = $identifier->entity(); + } + $this->initialize(); - $result = $this->mailService->entityMove($target, $sources); + $deleteMode = strtolower(trim((string) ($this->getAuxiliary()['deleteMode'] ?? 'soft'))); + if ($deleteMode === 'soft') { + $deleteDestination = $this->getAuxiliary()['deleteDestination'] ?? null; + if (is_string($deleteDestination) || is_int($deleteDestination)) { + $deleteDestination = trim((string) $deleteDestination); + if ($deleteDestination === '') { + $deleteDestination = null; + } + } else { + $deleteDestination = null; + } - return $result; + if ($deleteDestination === null) { + $filter = $this->collectionListFilter(); + $filter->condition(self::CAPABILITY_COLLECTION_FILTER_ROLE, CollectionRoles::Trash->value); + + foreach ($this->collectionList(null, $filter) as $collection) { + if (!$collection instanceof CollectionBaseInterface) { + continue; + } + + if ($collection->getProperties()->getRole() === CollectionRoles::Trash) { + $deleteDestination = (string) $collection->identifier(); + break; + } + } + } + + if ($deleteDestination === null) { + throw new \RuntimeException('Soft delete is enabled but no trash collection could be resolved.'); + } + + return $this->mailService->entityMove($deleteDestination, ...$ids); + } + + return $this->mailService->entityDelete(...$ids); + } + + public function entityMove(CollectionIdentifier $target, EntityIdentifier ...$identifiers): array + { + // validate target belongs to this service + if ($target->provider() !== $this->provider() || $target->service() !== (string)$this->identifier()) { + throw new \InvalidArgumentException('Target collection does not belong to this service'); + } + + // validate identifiers and construct ID list + $ids = []; + foreach ($identifiers as $identifier) { + if ($identifier->provider() !== $this->provider() || $identifier->service() !== (string)$this->identifier()) { + throw new \InvalidArgumentException('Entity identifier does not belong to this service: ' . (string)$identifier); + } + $ids[] = $identifier->entity(); + } + + $this->initialize(); + + return $this->mailService->entityMove($target->collection(), ...$ids); } } diff --git a/lib/Service/Remote/RemoteMailService.php b/lib/Service/Remote/RemoteMailService.php index 51e396c..e2d30a1 100644 --- a/lib/Service/Remote/RemoteMailService.php +++ b/lib/Service/Remote/RemoteMailService.php @@ -725,24 +725,40 @@ class RemoteMailService { } /** - * delete entity from remote storage + * delete entities from remote storage * * @since Release 1.0.0 */ - public function entityDelete(string $id): ?string { + public function entityDelete(string ...$identifiers): array { // construct set request $r0 = new MailSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); - // construct object - $r0->delete($id); + foreach ($identifiers as $id) { + $r0->delete($id); + } // transceive $bundle = $this->dataStore->perform([$r0]); // extract response $response = $bundle->response(0); - // determine if command succeeded - if (array_search($id, $response->deleted()) !== false) { - return $response->stateNew(); + // check for command error + if ($response instanceof ResponseException) { + if ($response->type() === 'unknownMethod') { + throw new JmapUnknownMethod($response->description(), 1); + } else { + throw new Exception($response->type() . ': ' . $response->description(), 1); + } } - return null; + + $results = []; + // check for success + foreach ($response->deleteSuccesses() as $id) { + $results[$id] = true; + } + // check for failure + foreach ($response->deleteFailures() as $id => $data) { + $results[$id] = $data['type'] ?? 'unknownError'; + } + + return $results; } /** @@ -751,7 +767,7 @@ class RemoteMailService { * @since Release 1.0.0 * */ - public function entityCopy(string $target, array $sources): array { + public function entityCopy(string $target, string ...$identifiers): array { return []; } @@ -759,15 +775,8 @@ class RemoteMailService { * move entity in remote storage * * @since Release 1.0.0 - * */ - public function entityMove(string $target, array $sources): array { - - // extract identifiers from sources - $identifiers = []; - foreach ($sources as $source) { - $identifiers = array_merge($identifiers, $source); - } + public function entityMove(string $target, string ...$identifiers): array { // construct request $r0 = new MailSet($this->dataAccount, null, $this->resourceNamespace, $this->resourceEntityLabel); foreach ($identifiers as $id) { diff --git a/lib/Service/Remote/RemoteService.php b/lib/Service/Remote/RemoteService.php index 48d1360..f762e8a 100644 --- a/lib/Service/Remote/RemoteService.php +++ b/lib/Service/Remote/RemoteService.php @@ -69,10 +69,10 @@ class RemoteService { } // debugging if ($service->getDebug()) { + $logDir = Server::getInstance()?->logDir(); + $logDir .= '/jmap/' . $service->identifier() . '.json'; $client->configureTransportLogState(true); - $client->configureTransportLogLocation( - sys_get_temp_dir() . '/' . $location->getHost() . '-' . $identity->getIdentity() . '.log' - ); + $client->configureTransportLogLocation($logDir); } // return return $client; diff --git a/src/components/JmapAuthPanel.vue b/src/components/JmapAuthPanel.vue index e15546d..742c898 100644 --- a/src/components/JmapAuthPanel.vue +++ b/src/components/JmapAuthPanel.vue @@ -1,33 +1,31 @@