generated from Nodarx/template
feat: initial version
Signed-off-by: Sebastian Krupinski <root@LAPTOP-7DVOR6NC>
This commit was merged in pull request #1.
This commit is contained in:
246
lib/Providers/Provider.php
Normal file
246
lib/Providers/Provider.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Providers;
|
||||
|
||||
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\ProviderImapMail\Service\Discovery;
|
||||
use KTXM\ProviderImapMail\Service\Remote\RemoteService;
|
||||
use KTXM\ProviderImapMail\Stores\ServiceStore;
|
||||
|
||||
/**
|
||||
* IMAP Mail Provider
|
||||
*
|
||||
* Registers IMAP as a mail provider and handles service lifecycle:
|
||||
* list / fetch / create / modify / destroy / discover / test.
|
||||
*/
|
||||
class Provider implements ProviderServiceMutateInterface, ProviderServiceDiscoverInterface, ProviderServiceTestInterface
|
||||
{
|
||||
public const JSON_TYPE = ProviderBaseInterface::JSON_TYPE;
|
||||
|
||||
protected const PROVIDER_IDENTIFIER = 'imap';
|
||||
protected const PROVIDER_LABEL = 'IMAP Mail Provider';
|
||||
protected const PROVIDER_DESCRIPTION = 'Provides mail services via the IMAP protocol';
|
||||
protected const PROVIDER_ICON = 'mdi-email';
|
||||
|
||||
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,
|
||||
) {}
|
||||
|
||||
// ── ProviderBaseInterface ─────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ── ProviderServiceMutateInterface ────────────────────────────────────────
|
||||
|
||||
public function serviceList(string $tenantId, string $userId, array $filter = []): array
|
||||
{
|
||||
$list = $this->serviceStore->list($tenantId, $userId, $filter);
|
||||
$result = [];
|
||||
foreach ($list as $entry) {
|
||||
$service = new Service();
|
||||
$service->fromStore($entry);
|
||||
$result[$service->identifier()] = $service;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
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 serviceExtant(string $tenantId, string $userId, string|int ...$identifiers): array
|
||||
{
|
||||
return $this->serviceStore->extant($tenantId, $userId, $identifiers);
|
||||
}
|
||||
|
||||
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 an instance of IMAP 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 an instance of IMAP 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());
|
||||
}
|
||||
|
||||
// ── ProviderServiceDiscoverInterface ──────────────────────────────────────
|
||||
|
||||
public function serviceDiscover(
|
||||
string $tenantId,
|
||||
string $userId,
|
||||
string $identity,
|
||||
?string $location = null,
|
||||
?string $secret = null,
|
||||
): ?ResourceServiceLocationInterface {
|
||||
$discovery = new Discovery();
|
||||
// TODO: Make SSL verification configurable per-tenant
|
||||
$verifySSL = true;
|
||||
return $discovery->discover($identity, $location, $secret, $verifySSL);
|
||||
}
|
||||
|
||||
// ── ProviderServiceTestInterface ──────────────────────────────────────────
|
||||
|
||||
public function serviceTest(ServiceBaseInterface $service, array $options = []): array
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
if (!($service instanceof Service)) {
|
||||
throw new \InvalidArgumentException('Service must be an instance of IMAP Service');
|
||||
}
|
||||
|
||||
// Attempt to authenticate and list mailboxes as a connectivity check
|
||||
$wrapper = RemoteService::freshClient($service);
|
||||
$mailboxes = $wrapper->mailboxes();
|
||||
|
||||
$latency = (int) round((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'IMAP connection successful'
|
||||
. ' (Mailboxes: ' . count($mailboxes) . ')'
|
||||
. ' (Latency: ' . $latency . ' ms)',
|
||||
];
|
||||
} catch (\Throwable $e) {
|
||||
$latency = (int) round((microtime(true) - $startTime) * 1000);
|
||||
|
||||
$location = ($service instanceof Service) ? $service->getLocation() : null;
|
||||
$target = $location
|
||||
? $location->getEncryption() . '://' . $location->getHost() . ':' . $location->getPort()
|
||||
: 'unknown host';
|
||||
|
||||
// stream_socket_client errors are suppressed with @ in gricob — recover them
|
||||
$phpError = error_get_last();
|
||||
$detail = $e->getMessage() !== '' ? $e->getMessage() : ($phpError['message'] ?? '');
|
||||
|
||||
if ($detail === '' && $location !== null) {
|
||||
$host = $location->getHost();
|
||||
if ($host !== '' && gethostbyname($host) === $host) {
|
||||
$detail = "hostname '{$host}' could not be resolved";
|
||||
} else {
|
||||
$detail = 'connection refused or timed out — check port and encryption settings';
|
||||
}
|
||||
} elseif ($detail === '') {
|
||||
$detail = 'no details — check host, port, and encryption settings';
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => sprintf(
|
||||
'Connection to %s failed (%s): %s',
|
||||
$target,
|
||||
(new \ReflectionClass($e))->getShortName(),
|
||||
$detail,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user