2 Commits

Author SHA1 Message Date
Sebastian f901f13953 feat: listen to user life cycle events
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-08-09 22:36:41 -04:00
Sebastian f39dd75ae5 fix: folder deletion while in trash
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-08-07 20:45:29 -04:00
5 changed files with 87 additions and 23 deletions
Generated
+14 -14
View File
@@ -8,21 +8,21 @@
"packages": [
{
"name": "guzzlehttp/guzzle",
"version": "7.15.3",
"version": "7.15.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc"
"reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
"reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/61443dfb33c62f308ee8add20f45b4d6e4bf8d2f",
"reference": "61443dfb33c62f308ee8add20f45b4d6e4bf8d2f",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^2.5.2",
"guzzlehttp/promises": "^2.5.1",
"guzzlehttp/psr7": "^2.13",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
@@ -116,7 +116,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.15.3"
"source": "https://github.com/guzzle/guzzle/tree/7.15.1"
},
"funding": [
{
@@ -132,20 +132,20 @@
"type": "tidelift"
}
],
"time": "2026-08-05T19:48:21+00:00"
"time": "2026-07-18T11:23:11+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.5.2",
"version": "2.5.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce"
"reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
"reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce",
"url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29",
"reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29",
"shasum": ""
},
"require": {
@@ -200,7 +200,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.5.2"
"source": "https://github.com/guzzle/promises/tree/2.5.1"
},
"funding": [
{
@@ -216,7 +216,7 @@
"type": "tidelift"
}
],
"time": "2026-08-05T19:30:54+00:00"
"time": "2026-07-08T15:48:39+00:00"
},
{
"name": "guzzlehttp/psr7",
@@ -2556,5 +2556,5 @@
"platform-overrides": {
"php": "8.3"
},
"plugin-api-version": "2.9.0"
"plugin-api-version": "2.6.0"
}
+27
View File
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace KTXM\ProviderJmapc\Listeners;
use KTXC\User\Event\UserCreatedEvent;
use KTXC\User\Event\UserDeletingEvent;
use KTXM\ProviderJmapc\Stores\ServiceStore;
final class UserEventListener
{
public function __construct(
private readonly ServiceStore $serviceStore,
) {
}
public function onUserCreated(UserCreatedEvent $event): void
{
// TODO: implement provisioning configuration
}
public function onUserDeleting(UserDeletingEvent $event): void
{
$this->serviceStore->deleteByUser($event->tenantIdentifier(), $event->userIdentifier());
}
}
+9
View File
@@ -10,9 +10,14 @@ declare(strict_types=1);
namespace KTXM\ProviderJmapc;
use KTXC\Resource\ProviderManager;
use KTXC\User\Event\UserCreatedEvent;
use KTXC\User\Event\UserDeletingEvent;
use KTXF\Event\DeliveryMode;
use KTXF\Event\EventListenerRegistrarInterface;
use KTXF\Module\ModuleBrowserInterface;
use KTXF\Module\ModuleInstanceAbstract;
use KTXF\Resource\Provider\ProviderInterface;
use KTXM\ProviderJmapc\Listeners\UserEventListener;
use KTXM\ProviderJmapc\Providers\Mail\Provider as MailProvider;
use KTXM\ProviderJmapc\Providers\Chrono\Provider as ChronoProvider;
use KTXM\ProviderJmapc\Providers\People\Provider as PeopleProvider;
@@ -27,6 +32,7 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
{
public function __construct(
private readonly ProviderManager $providerManager,
private readonly EventListenerRegistrarInterface $events,
) {}
public function handle(): string
@@ -67,6 +73,9 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
public function boot(): void
{
// Register listeners
$this->events->listen($this->handle(), UserCreatedEvent::class, UserEventListener::class, 'onUserCreated', DeliveryMode::Deferred);
$this->events->listen($this->handle(), UserDeletingEvent::class, UserEventListener::class, 'onUserDeleting', DeliveryMode::Deferred);
// Register JMAP providers - all three share the same service store
$this->providerManager->register(ProviderInterface::TYPE_MAIL, 'jmap', MailProvider::class);
//$this->providerManager->register(ProviderInterface::TYPE_CHRONO, 'jmap', ChronoProvider::class);
+26 -9
View File
@@ -484,17 +484,34 @@ class Service implements ServiceBaseInterface, ServiceMutableInterface, ServiceC
}
}
// we need to determine if the folder being deleted is already in the trash
if (str_starts_with((string) $target->collection(), (string) $deleteTargetIdentifier?->collection())) {
// if so, we should hard delete instead of moving to avoid duplicates in the trash
$deleteMode = 'hard';
if ($deleteMode === 'soft') {
$targetMailbox = $this->mailService->collectionFetch((string) $target->collection());
if ($targetMailbox === null) {
throw new \RuntimeException('Collection not found for delete operation');
}
$targetIdentifier = (string) $target->collection();
$trashIdentifier = (string) $deleteTargetIdentifier->collection();
$parentIdentifier = isset($targetMailbox['parentId'])
? (string) $targetMailbox['parentId']
: null;
// JMAP mailbox IDs are opaque, so containment must be determined
// from parentId rather than by comparing ID prefixes.
if ($targetIdentifier === $trashIdentifier || $parentIdentifier === $trashIdentifier) {
$deleteMode = 'hard';
}
}
$result = match ($deleteMode) {
'soft' => $this->collectionMove($deleteTargetIdentifier, $target),
'hard' => $this->mailService->collectionDestroy($target->collection(), $force),
};
return $result;
if ($deleteMode === 'soft') {
return $this->collectionMove($deleteTargetIdentifier, $target);
}
if ($this->mailService->collectionDestroy((string) $target->collection(), $force) === null) {
throw new \RuntimeException("Failed to delete collection: {$target->collection()}");
}
return true;
}
public function collectionMove(CollectionIdentifier $target, CollectionIdentifier $source): CollectionBaseInterface
+11
View File
@@ -176,4 +176,15 @@ class ServiceStore
return $result->getDeletedCount() > 0;
}
/**
* Delete every service owned by a user.
*/
public function deleteByUser(string $tenantId, string $userId): void
{
$this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteMany([
'tid' => $tenantId,
'uid' => $userId,
]);
}
}