* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderJmapc\Providers\Mail; use KTXF\Mail\Provider\ProviderBaseInterface; use KTXF\Mail\Provider\ProviderServiceDiscoverInterface; use KTXF\Mail\Provider\ProviderServiceMutateInterface; use KTXF\Mail\Provider\ProviderServiceTestInterface; use KTXF\Mail\Service\ServiceBaseInterface; use KTXF\Resource\Provider\ResourceServiceLocationInterface; use KTXF\Resource\Provider\ResourceServiceMutateInterface; use KTXM\ProviderJmapc\Service\Discovery; use KTXM\ProviderJmapc\Service\Remote\RemoteService; use KTXM\ProviderJmapc\Stores\ServiceStore; /** * JMAP Mail Provider * * Provides Mail services via JMAP protocol. * Filters services by urn:ietf:params:jmap:mail capability. */ class Provider implements ProviderServiceMutateInterface, ProviderServiceDiscoverInterface, ProviderServiceTestInterface { public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE; protected const PROVIDER_IDENTIFIER = 'jmap'; protected const PROVIDER_LABEL = 'JMAP Mail Provider'; protected const PROVIDER_DESCRIPTION = 'Provides mail services via JMAP protocol (RFC 8620)'; protected const PROVIDER_ICON = 'mdi-email-sync'; protected array $providerAbilities = [ self::CAPABILITY_SERVICE_LIST => true, self::CAPABILITY_SERVICE_FETCH => true, self::CAPABILITY_SERVICE_EXTANT => true, self::CAPABILITY_SERVICE_CREATE => true, self::CAPABILITY_SERVICE_MODIFY => true, self::CAPABILITY_SERVICE_DESTROY => true, self::CAPABILITY_SERVICE_TEST => true, ]; public function __construct( private readonly ServiceStore $serviceStore, ) {} public function jsonSerialize(): array { return [ self::JSON_PROPERTY_TYPE => self::JSON_TYPE, self::JSON_PROPERTY_IDENTIFIER => self::PROVIDER_IDENTIFIER, self::JSON_PROPERTY_LABEL => self::PROVIDER_LABEL, self::JSON_PROPERTY_CAPABILITIES => $this->providerAbilities, ]; } public function jsonDeserialize(array|string $data): static { return $this; } public function type(): string { return self::TYPE_MAIL; } public function identifier(): string { return self::PROVIDER_IDENTIFIER; } public function label(): string { return self::PROVIDER_LABEL; } public function description(): string { return self::PROVIDER_DESCRIPTION; } public function icon(): string { return self::PROVIDER_ICON; } public function capable(string $value): bool { return !empty($this->providerAbilities[$value]); } public function capabilities(): array { return $this->providerAbilities; } public function serviceList(string $tenantId, string $userId, array $filter = []): array { $list = $this->serviceStore->list($tenantId, $userId, $filter); foreach ($list as $entry) { $service = new Service(); $service->fromStore($entry); $list[$service->identifier()] = $service; } return $list; } public function serviceExtant(string $tenantId, string $userId, string|int ...$identifiers): array { return $this->serviceStore->extant($tenantId, $userId, $identifiers); } public function serviceFetch(string $tenantId, string $userId, string|int $identifier): ?Service { return $this->serviceStore->fetch($tenantId, $userId, $identifier); } public function serviceFindByAddress(string $tenantId, string $userId, string $address): ?Service { /** @var Service[] $services */ $services = $this->serviceList($tenantId, $userId); foreach ($services as $service) { if ($service->hasAddress($address)) { return $service; } } return null; } public function serviceFresh(): ResourceServiceMutateInterface { return new Service(); } public function serviceCreate(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string { if (!($service instanceof Service)) { throw new \InvalidArgumentException('Service must be instance of JMAP Service'); } $created = $this->serviceStore->create($tenantId, $userId, $service); return (string) $created->identifier(); } public function serviceModify(string $tenantId, string $userId, ResourceServiceMutateInterface $service): string { if (!($service instanceof Service)) { throw new \InvalidArgumentException('Service must be instance of JMAP Service'); } $updated = $this->serviceStore->modify($tenantId, $userId, $service); return (string) $updated->identifier(); } public function serviceDestroy(string $tenantId, string $userId, ResourceServiceMutateInterface $service): bool { if (!($service instanceof Service)) { return false; } return $this->serviceStore->delete($tenantId, $userId, $service->identifier()); } public function serviceDiscover( string $tenantId, string $userId, string $identity, ?string $location = null, ?string $secret = null ): ResourceServiceLocationInterface|null { $discovery = new Discovery(); // TODO: Make SSL verification configurable based on tenant/user settings $verifySSL = true; return $discovery->discover($identity, $location, $secret, $verifySSL); } public function serviceTest(ServiceBaseInterface $service, array $options = []): array { $startTime = microtime(true); try { if (!($service instanceof Service)) { throw new \InvalidArgumentException('Service must be instance of JMAP Service'); } $client = RemoteService::freshClient($service); $session = $client->connect(); $latency = round((microtime(true) - $startTime) * 1000); // ms4 return [ 'success' => true, 'message' => 'JMAP connection successful' . ' (Account ID: ' . ($session->username() ?? 'N/A') . ')' . ' (Latency: ' . $latency . ' ms)', ]; } catch (\Exception $e) { $latency = round((microtime(true) - $startTime) * 1000); return [ 'success' => false, 'message' => 'Test failed: ' . $e->getMessage(), ]; } } }