Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27ce930a12 |
@@ -1,59 +0,0 @@
|
|||||||
name: PHP Integration Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
name: Integration Tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
services:
|
|
||||||
mongo:
|
|
||||||
image: mongo:8
|
|
||||||
options: >-
|
|
||||||
--health-cmd "mongosh --quiet --eval \"db.adminCommand('ping')\""
|
|
||||||
--health-interval 5s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 12
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Retrieve Server Install Action
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: Nodarx/action-server-install
|
|
||||||
ref: main
|
|
||||||
path: action-server-install
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install server
|
|
||||||
uses: ./action-server-install
|
|
||||||
with:
|
|
||||||
install-php: 'true'
|
|
||||||
php-version: '8.5'
|
|
||||||
server-path: './server'
|
|
||||||
database-uri: 'mongodb://mongo:27017/?tls=false'
|
|
||||||
database-name: 'ktrix_ci'
|
|
||||||
app-environment: 'test'
|
|
||||||
|
|
||||||
- name: Checkout module under test
|
|
||||||
uses: actions/checkout@v6.0.2
|
|
||||||
with:
|
|
||||||
repository: ${{ github.repository }}
|
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
path: server/modules/mail_manager
|
|
||||||
github-server-url: https://git.ktrix.dev
|
|
||||||
|
|
||||||
- name: Install module dependencies
|
|
||||||
run: composer install --prefer-dist --no-progress
|
|
||||||
working-directory: server/modules/mail_manager
|
|
||||||
|
|
||||||
- name: Install and enable module
|
|
||||||
working-directory: server
|
|
||||||
run: |
|
|
||||||
php bin/console module:install mail_manager
|
|
||||||
php bin/console module:enable mail_manager
|
|
||||||
|
|
||||||
- name: Run integration tests
|
|
||||||
working-directory: server/modules/mail_manager
|
|
||||||
run: composer test:integration
|
|
||||||
@@ -25,17 +25,11 @@ jobs:
|
|||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
|
|
||||||
- name: Install Renovate
|
- name: Install Renovate
|
||||||
run: |
|
run: npm install -g renovate
|
||||||
npm install --global --no-audit --fund=false \
|
|
||||||
--prefix "${{ runner.temp }}/renovate-npm" \
|
|
||||||
--cache "${{ runner.temp }}/renovate-npm-cache" \
|
|
||||||
renovate
|
|
||||||
"${{ runner.temp }}/renovate-npm/bin/renovate" --version
|
|
||||||
|
|
||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
env:
|
env:
|
||||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
RENOVATE_PLATFORM: gitea
|
RENOVATE_PLATFORM: gitea
|
||||||
RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1
|
RENOVATE_ENDPOINT: https://git.ktrix.dev/api/v1
|
||||||
run: |
|
run: renovate ${{ gitea.repository }}
|
||||||
"${{ runner.temp }}/renovate-npm/bin/renovate" ${{ gitea.repository }}
|
|
||||||
|
|||||||
+5
-1
@@ -14,7 +14,11 @@ node_modules/
|
|||||||
# Backend development
|
# Backend development
|
||||||
/lib/vendor/
|
/lib/vendor/
|
||||||
coverage/
|
coverage/
|
||||||
*.cache
|
phpunit.xml.cache
|
||||||
|
.phpunit.cache
|
||||||
|
.phpunit.result.cache
|
||||||
|
.php-cs-fixer.cache
|
||||||
|
.phpstan.cache
|
||||||
.phpactor/
|
.phpactor/
|
||||||
|
|
||||||
# Editors
|
# Editors
|
||||||
|
|||||||
+5
-6
@@ -10,16 +10,16 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"optimize-autoloader": true,
|
"optimize-autoloader": true,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "8.3"
|
"php": "8.2"
|
||||||
},
|
},
|
||||||
"autoloader-suffix": "MailManager",
|
"autoloader-suffix": "MailManager",
|
||||||
"vendor-dir": "lib/vendor"
|
"vendor-dir": "lib/vendor"
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.3 <=8.5"
|
"php": ">=8.2 <=8.5"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^12.0"
|
"phpunit/phpunit": "^11.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
@@ -36,8 +36,7 @@
|
|||||||
],
|
],
|
||||||
"post-update-cmd": [
|
"post-update-cmd": [
|
||||||
],
|
],
|
||||||
"test:unit": "phpunit --configuration tests/php/phpunit.xml --testsuite \"Unit Tests\" --colors=always --testdox",
|
"test:unit": "phpunit --configuration tests/php/phpunit.unit.xml --colors=always --testdox",
|
||||||
"test:integration": "phpunit --configuration tests/php/phpunit.xml --testsuite \"Integration Tests\" --colors=always --testdox",
|
"test:coverage": "XDEBUG_MODE=coverage phpunit --configuration tests/php/phpunit.unit.xml --coverage-html .phpunit.coverage --coverage-text"
|
||||||
"test:coverage": "XDEBUG_MODE=coverage phpunit --configuration tests/php/phpunit.xml --testsuite \"Unit Tests\" --coverage-html .phpunit.coverage --coverage-text"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+356
-256
File diff suppressed because it is too large
Load Diff
@@ -14,8 +14,8 @@ use KTXC\Http\Response\JsonResponse;
|
|||||||
use KTXC\Http\Response\Response;
|
use KTXC\Http\Response\Response;
|
||||||
use KTXC\Http\Response\StreamedNdJsonResponse;
|
use KTXC\Http\Response\StreamedNdJsonResponse;
|
||||||
use KTXC\Http\Response\StreamedResponse;
|
use KTXC\Http\Response\StreamedResponse;
|
||||||
use KTXC\Context\IdentityContextInterface;
|
use KTXC\SessionIdentity;
|
||||||
use KTXC\Context\TenantContextInterface;
|
use KTXC\SessionTenant;
|
||||||
use KTXF\Controller\ControllerAbstract;
|
use KTXF\Controller\ControllerAbstract;
|
||||||
use KTXF\Json\JsonSerializable;
|
use KTXF\Json\JsonSerializable;
|
||||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||||
@@ -23,8 +23,10 @@ use KTXF\Resource\Identifier\EntityIdentifier;
|
|||||||
use KTXF\Resource\Identifier\ResourceIdentifier;
|
use KTXF\Resource\Identifier\ResourceIdentifier;
|
||||||
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
||||||
use KTXF\Resource\Identifier\ServiceIdentifier;
|
use KTXF\Resource\Identifier\ServiceIdentifier;
|
||||||
use KTXF\Mail\Provider\ProviderBaseInterface;
|
|
||||||
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
||||||
|
use KTXF\Resource\Selector\CollectionSelector;
|
||||||
|
use KTXF\Resource\Selector\ServiceSelector;
|
||||||
|
use KTXF\Resource\Selector\SourceSelector;
|
||||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||||
use KTXM\MailManager\Manager;
|
use KTXM\MailManager\Manager;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -39,7 +41,6 @@ class DefaultController extends ControllerAbstract {
|
|||||||
private const ERR_MISSING_SOURCES = 'Missing parameter: sources';
|
private const ERR_MISSING_SOURCES = 'Missing parameter: sources';
|
||||||
private const ERR_MISSING_TARGET = 'Missing parameter: target';
|
private const ERR_MISSING_TARGET = 'Missing parameter: target';
|
||||||
private const ERR_MISSING_TARGETS = 'Missing parameter: targets';
|
private const ERR_MISSING_TARGETS = 'Missing parameter: targets';
|
||||||
private const ERR_MISSING_SENDER = 'Missing parameter: sender';
|
|
||||||
private const ERR_INVALID_OPERATION = 'Invalid operation: ';
|
private const ERR_INVALID_OPERATION = 'Invalid operation: ';
|
||||||
private const ERR_INVALID_PROVIDER = 'Invalid parameter: provider must be a string';
|
private const ERR_INVALID_PROVIDER = 'Invalid parameter: provider must be a string';
|
||||||
private const ERR_INVALID_SERVICE = 'Invalid parameter: service must be a string';
|
private const ERR_INVALID_SERVICE = 'Invalid parameter: service must be a string';
|
||||||
@@ -47,13 +48,14 @@ class DefaultController extends ControllerAbstract {
|
|||||||
private const ERR_INVALID_SOURCES = 'Invalid parameter: sources must be an array';
|
private const ERR_INVALID_SOURCES = 'Invalid parameter: sources must be an array';
|
||||||
private const ERR_INVALID_TARGET = 'Invalid parameter: target must be an array';
|
private const ERR_INVALID_TARGET = 'Invalid parameter: target must be an array';
|
||||||
private const ERR_INVALID_TARGETS = 'Invalid parameter: targets must be an array';
|
private const ERR_INVALID_TARGETS = 'Invalid parameter: targets must be an array';
|
||||||
private const ERR_INVALID_SENDER = 'Invalid parameter: sender must be a string';
|
|
||||||
private const ERR_INVALID_DATA = 'Invalid parameter: data must be an array';
|
private const ERR_INVALID_DATA = 'Invalid parameter: data must be an array';
|
||||||
|
|
||||||
|
private const STREAM_FLUSH_INTERVAL = 1;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TenantContextInterface $tenantContext,
|
private readonly SessionTenant $tenantIdentity,
|
||||||
private readonly IdentityContextInterface $identityContext,
|
private readonly SessionIdentity $userIdentity,
|
||||||
private Manager $manager,
|
private Manager $mailManager,
|
||||||
private readonly LoggerInterface $logger
|
private readonly LoggerInterface $logger
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -80,26 +82,8 @@ class DefaultController extends ControllerAbstract {
|
|||||||
): Response {
|
): Response {
|
||||||
|
|
||||||
// authorize request
|
// authorize request
|
||||||
$tenantId = $this->tenantContext->identifier();
|
$tenantId = $this->tenantIdentity->identifier();
|
||||||
$userId = $this->identityContext->identifier();
|
$userId = $this->userIdentity->identifier();
|
||||||
|
|
||||||
// acting-user override: only the reserved system context is permitted,
|
|
||||||
// gated on the system mail management permission
|
|
||||||
if ($user !== null && $user !== $userId) {
|
|
||||||
if ($user !== ProviderBaseInterface::USER_SYSTEM || !$this->identityContext->hasPermission('mail_manager.system')) {
|
|
||||||
return new JsonResponse([
|
|
||||||
'version' => $version,
|
|
||||||
'transaction' => $transaction,
|
|
||||||
'operation' => $operation,
|
|
||||||
'status' => 'error',
|
|
||||||
'data' => [
|
|
||||||
'code' => JsonResponse::HTTP_FORBIDDEN,
|
|
||||||
'message' => 'Not permitted to act as user: ' . $user
|
|
||||||
]
|
|
||||||
], JsonResponse::HTTP_FORBIDDEN);
|
|
||||||
}
|
|
||||||
$userId = $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -178,9 +162,8 @@ class DefaultController extends ControllerAbstract {
|
|||||||
'entity.patch' => $this->entityPatch($tenantId, $userId, $data),
|
'entity.patch' => $this->entityPatch($tenantId, $userId, $data),
|
||||||
'entity.move' => $this->entityMove($tenantId, $userId, $data),
|
'entity.move' => $this->entityMove($tenantId, $userId, $data),
|
||||||
'entity.copy' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
'entity.copy' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||||
'entity.submit' => $this->entitySubmit($tenantId, $userId, $data),
|
'entity.transmit' => $this->entityTransmit($tenantId, $userId, $data),
|
||||||
'entity.download' => $this->entityDownload($tenantId, $userId, $data),
|
'entity.download' => $this->entityDownload($tenantId, $userId, $data),
|
||||||
'entity.blobs' => $this->entityBlobs($tenantId, $userId, $data),
|
|
||||||
|
|
||||||
default => throw new InvalidArgumentException(self::ERR_INVALID_OPERATION . $operation)
|
default => throw new InvalidArgumentException(self::ERR_INVALID_OPERATION . $operation)
|
||||||
};
|
};
|
||||||
@@ -190,48 +173,40 @@ class DefaultController extends ControllerAbstract {
|
|||||||
|
|
||||||
private function providerList(string $tenantId, string $userId, array $data): mixed {
|
private function providerList(string $tenantId, string $userId, array $data): mixed {
|
||||||
|
|
||||||
if (isset($data['targets'])) {
|
$sources = null;
|
||||||
if (!is_array($data['targets'])) {
|
if (isset($data['sources']) && is_array($data['sources'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
$sources = new SourceSelector();
|
||||||
}
|
$sources->jsonDeserialize($data['sources']);
|
||||||
|
}
|
||||||
foreach ($data['targets'] as $target) {
|
|
||||||
if (!is_string($target)) {
|
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->manager->providerList($tenantId, $userId, $data['targets'] ?? []);
|
return $this->mailManager->providerList($tenantId, $userId, $sources);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function providerFetch(string $tenantId, string $userId, array $data): mixed {
|
private function providerFetch(string $tenantId, string $userId, array $data): mixed {
|
||||||
|
|
||||||
if (!isset($data['target'])) {
|
if (!isset($data['identifier'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGET);
|
throw new InvalidArgumentException(self::ERR_MISSING_IDENTIFIER);
|
||||||
}
|
}
|
||||||
if (!is_string($data['target'])) {
|
if (!is_string($data['identifier'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGET);
|
throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->providerFetch($tenantId, $userId, $data['target']);
|
return $this->mailManager->providerFetch($tenantId, $userId, $data['identifier']);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function providerExtant(string $tenantId, string $userId, array $data): mixed {
|
private function providerExtant(string $tenantId, string $userId, array $data): mixed {
|
||||||
|
|
||||||
if (!isset($data['targets'])) {
|
if (!isset($data['sources'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
|
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
|
||||||
}
|
}
|
||||||
|
if (!is_array($data['sources'])) {
|
||||||
foreach ($data['targets'] as $target) {
|
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
|
||||||
if (!is_string($target)) {
|
}
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
$sources = new SourceSelector();
|
||||||
}
|
$sources->jsonDeserialize($data['sources']);
|
||||||
}
|
|
||||||
|
|
||||||
return $this->manager->providerExtant($tenantId, $userId, $data['targets']);
|
return $this->mailManager->providerExtant($tenantId, $userId, $sources);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,17 +214,13 @@ class DefaultController extends ControllerAbstract {
|
|||||||
|
|
||||||
private function serviceList(string $tenantId, string $userId, array $data): mixed {
|
private function serviceList(string $tenantId, string $userId, array $data): mixed {
|
||||||
|
|
||||||
$targets = null;
|
$sources = null;
|
||||||
if (isset($data['targets']) && is_array($data['targets'])) {
|
if (isset($data['sources']) && is_array($data['sources'])) {
|
||||||
$targets = ResourceIdentifiers::fromArray($data['targets']);
|
$sources = new SourceSelector();
|
||||||
foreach ($targets as $target) {
|
$sources->jsonDeserialize($data['sources']);
|
||||||
if (!$target instanceof CollectionIdentifier && !$target instanceof ServiceIdentifier) {
|
|
||||||
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->serviceList($tenantId, $userId, $targets);
|
return $this->mailManager->serviceList($tenantId, $userId, $sources);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,25 +239,21 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER);
|
throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->serviceFetch($tenantId, $userId, $data['provider'], $data['identifier']);
|
return $this->mailManager->serviceFetch($tenantId, $userId, $data['provider'], $data['identifier']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function serviceExtant(string $tenantId, string $userId, array $data): mixed {
|
private function serviceExtant(string $tenantId, string $userId, array $data): mixed {
|
||||||
|
|
||||||
if (!isset($data['targets'])) {
|
if (!isset($data['sources'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
|
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
|
||||||
}
|
}
|
||||||
if (!is_array($data['targets'])) {
|
if (!is_array($data['sources'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
|
||||||
}
|
}
|
||||||
$targets = ResourceIdentifiers::fromArray($data['targets']);
|
$sources = new SourceSelector();
|
||||||
foreach ($targets as $target) {
|
$sources->jsonDeserialize($data['sources']);
|
||||||
if (!$target instanceof ServiceIdentifier) {
|
|
||||||
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service identifiers');
|
return $this->mailManager->serviceExtant($tenantId, $userId, $sources);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->manager->serviceExtant($tenantId, $userId, $targets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function serviceCreate(string $tenantId, string $userId, array $data): mixed {
|
private function serviceCreate(string $tenantId, string $userId, array $data): mixed {
|
||||||
@@ -303,7 +270,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException(self::ERR_INVALID_DATA);
|
throw new InvalidArgumentException(self::ERR_INVALID_DATA);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->serviceCreate(
|
return $this->mailManager->serviceCreate(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
$data['provider'],
|
$data['provider'],
|
||||||
@@ -334,7 +301,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException('Invalid parameter: delta must be a boolean');
|
throw new InvalidArgumentException('Invalid parameter: delta must be a boolean');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->serviceUpdate(
|
return $this->mailManager->serviceUpdate(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
$data['provider'],
|
$data['provider'],
|
||||||
@@ -358,7 +325,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER);
|
throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->serviceDelete(
|
return $this->mailManager->serviceDelete(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
$data['provider'],
|
$data['provider'],
|
||||||
@@ -379,7 +346,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException('Either a service identifier or location and identity must be provided for service test');
|
throw new InvalidArgumentException('Either a service identifier or location and identity must be provided for service test');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->serviceTest(
|
return $this->mailManager->serviceTest(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
$data['provider'],
|
$data['provider'],
|
||||||
@@ -400,7 +367,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
$location = $data['location'] ?? null;
|
$location = $data['location'] ?? null;
|
||||||
$secret = $data['secret'] ?? null;
|
$secret = $data['secret'] ?? null;
|
||||||
|
|
||||||
$discoverGenerator = $this->manager->serviceDiscover($tenantId, $userId, $provider, $identity, $location, $secret);
|
$discoverGenerator = $this->mailManager->serviceDiscover($tenantId, $userId, $provider, $identity, $location, $secret);
|
||||||
$logger = $this->logger;
|
$logger = $this->logger;
|
||||||
|
|
||||||
$response = (function () use ($discoverGenerator, $version, $transaction, $logger): \Generator {
|
$response = (function () use ($discoverGenerator, $version, $transaction, $logger): \Generator {
|
||||||
@@ -438,18 +405,20 @@ class DefaultController extends ControllerAbstract {
|
|||||||
private function collectionList(string $tenantId, string $userId, array $data): mixed {
|
private function collectionList(string $tenantId, string $userId, array $data): mixed {
|
||||||
$sources = null;
|
$sources = null;
|
||||||
if (isset($data['sources']) && is_array($data['sources'])) {
|
if (isset($data['sources']) && is_array($data['sources'])) {
|
||||||
|
// TODO: Refactor to use identifiers directly
|
||||||
$sources = ResourceIdentifiers::fromArray($data['sources']);
|
$sources = ResourceIdentifiers::fromArray($data['sources']);
|
||||||
foreach ($sources as $source) {
|
foreach ($sources as $source) {
|
||||||
if (!$source instanceof CollectionIdentifier && !$source instanceof ServiceIdentifier) {
|
if (!$source instanceof CollectionIdentifier && !$source instanceof ServiceIdentifier) {
|
||||||
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
|
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$sources = $this->createSourceSelectorFromIdentifiers($sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
$filter = $data['filter'] ?? null;
|
$filter = $data['filter'] ?? null;
|
||||||
$sort = $data['sort'] ?? null;
|
$sort = $data['sort'] ?? null;
|
||||||
|
|
||||||
return $this->manager->collectionList($tenantId, $userId, $sources, $filter, $sort);
|
return $this->mailManager->collectionList($tenantId, $userId, $sources, $filter, $sort);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function collectionFetch(string $tenantId, string $userId, array $data): mixed {
|
private function collectionFetch(string $tenantId, string $userId, array $data): mixed {
|
||||||
@@ -460,6 +429,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor to use identifiers directly
|
||||||
$targetIdentifiers = ResourceIdentifiers::fromArray($data['targets']);
|
$targetIdentifiers = ResourceIdentifiers::fromArray($data['targets']);
|
||||||
foreach ($targetIdentifiers as $targetIdentifier) {
|
foreach ($targetIdentifiers as $targetIdentifier) {
|
||||||
if (!$targetIdentifier instanceof CollectionIdentifier) {
|
if (!$targetIdentifier instanceof CollectionIdentifier) {
|
||||||
@@ -467,11 +437,16 @@ class DefaultController extends ControllerAbstract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$list = $this->manager->collectionFetch(
|
$list = [];
|
||||||
$tenantId,
|
foreach ($targetIdentifiers as $targetIdentifier) {
|
||||||
$userId,
|
$list[(string)$targetIdentifier] = $this->mailManager->collectionFetch(
|
||||||
$targetIdentifier
|
$tenantId,
|
||||||
);
|
$userId,
|
||||||
|
$targetIdentifier->provider(),
|
||||||
|
$targetIdentifier->service(),
|
||||||
|
$targetIdentifier->collection()
|
||||||
|
);
|
||||||
|
}
|
||||||
return $list;
|
return $list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,14 +458,16 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Refactor to use identifiers directly
|
||||||
$sources = ResourceIdentifiers::fromArray($data['targets']);
|
$sources = ResourceIdentifiers::fromArray($data['targets']);
|
||||||
foreach ($sources as $source) {
|
foreach ($sources as $source) {
|
||||||
if (!$source instanceof CollectionIdentifier) {
|
if (!$source instanceof CollectionIdentifier) {
|
||||||
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
|
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$sources = $this->createSourceSelectorFromIdentifiers($sources);
|
||||||
|
|
||||||
return $this->manager->collectionExtant($tenantId, $userId, $sources);
|
return $this->mailManager->collectionExtant($tenantId, $userId, $sources);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function collectionCreate(string $tenantId, string $userId, array $data): mixed {
|
private function collectionCreate(string $tenantId, string $userId, array $data): mixed {
|
||||||
@@ -523,7 +500,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->collectionCreate(
|
return $this->mailManager->collectionCreate(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
$data['provider'],
|
$data['provider'],
|
||||||
@@ -552,7 +529,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException('Invalid parameter: target must be provider:service:collection');
|
throw new InvalidArgumentException('Invalid parameter: target must be provider:service:collection');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->collectionUpdate(
|
return $this->mailManager->collectionUpdate(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
$targetIdentifier,
|
$targetIdentifier,
|
||||||
@@ -573,7 +550,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException('Invalid parameter: target must be provider:service:collection');
|
throw new InvalidArgumentException('Invalid parameter: target must be provider:service:collection');
|
||||||
}
|
}
|
||||||
|
|
||||||
$result = $this->manager->collectionDelete($tenantId, $userId, $targetIdentifier, $data['options'] ?? [] );
|
$result = $this->mailManager->collectionDelete($tenantId, $userId, $targetIdentifier, $data['options'] ?? [] );
|
||||||
|
|
||||||
if (is_bool($result)) {
|
if (is_bool($result)) {
|
||||||
return [
|
return [
|
||||||
@@ -616,7 +593,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->collectionMove($tenantId, $userId, $target, $source);
|
return $this->mailManager->collectionMove($tenantId, $userId, $target, $source);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Entity Operations ====================
|
// ==================== Entity Operations ====================
|
||||||
@@ -635,12 +612,14 @@ class DefaultController extends ControllerAbstract {
|
|||||||
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection:entity identifiers');
|
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection:entity identifiers');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$sources = $this->createSourceSelectorFromIdentifiers($sources);
|
||||||
|
|
||||||
$filter = $data['filter'] ?? null;
|
$filter = $data['filter'] ?? null;
|
||||||
$sort = $data['sort'] ?? null;
|
$sort = $data['sort'] ?? null;
|
||||||
$range = $data['range'] ?? null;
|
$range = $data['range'] ?? null;
|
||||||
|
|
||||||
return $this->manager->entityListBulk($tenantId, $userId, $sources, $filter, $sort, $range);
|
return $this->mailManager->entityListBulk($tenantId, $userId, $sources, $filter, $sort, $range);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,11 +638,13 @@ class DefaultController extends ControllerAbstract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$sources = $this->createSourceSelectorFromIdentifiers($sources);
|
||||||
|
|
||||||
$filter = $data['filter'] ?? null;
|
$filter = $data['filter'] ?? null;
|
||||||
$sort = $data['sort'] ?? null;
|
$sort = $data['sort'] ?? null;
|
||||||
$range = $data['range'] ?? null;
|
$range = $data['range'] ?? null;
|
||||||
|
|
||||||
$entityGenerator = $this->manager->entityListStream($tenantId, $userId, $sources, $filter, $sort, $range);
|
$entityGenerator = $this->mailManager->entityListStream($tenantId, $userId, $sources, $filter, $sort, $range);
|
||||||
$logger = $this->logger;
|
$logger = $this->logger;
|
||||||
|
|
||||||
$responseGenerator = (function () use ($entityGenerator, $version, $transaction, $logger): \Generator {
|
$responseGenerator = (function () use ($entityGenerator, $version, $transaction, $logger): \Generator {
|
||||||
@@ -693,6 +674,50 @@ class DefaultController extends ControllerAbstract {
|
|||||||
return new StreamedNdJsonResponse($responseGenerator, 1, 200, ['Content-Type' => 'application/json']);
|
return new StreamedNdJsonResponse($responseGenerator, 1, 200, ['Content-Type' => 'application/json']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createSourceSelectorFromIdentifiers(ResourceIdentifiers $identifiers): SourceSelector {
|
||||||
|
$sources = new SourceSelector();
|
||||||
|
|
||||||
|
foreach ($identifiers as $identifier) {
|
||||||
|
if (!$identifier instanceof ServiceIdentifier) {
|
||||||
|
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service, provider:service:collection, or provider:service:collection:entity identifiers');
|
||||||
|
}
|
||||||
|
|
||||||
|
$provider = $identifier->provider();
|
||||||
|
$service = $identifier->service();
|
||||||
|
|
||||||
|
if (!isset($sources[$provider])) {
|
||||||
|
$sources[$provider] = new ServiceSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
$serviceSelector = $sources[$provider];
|
||||||
|
if (!$serviceSelector instanceof ServiceSelector) {
|
||||||
|
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection selectors');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($identifier instanceof ServiceIdentifier && !$identifier instanceof CollectionIdentifier) {
|
||||||
|
$serviceSelector[$service] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($serviceSelector[$service]) && $serviceSelector[$service] === true) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($serviceSelector[$service])) {
|
||||||
|
$serviceSelector[$service] = new CollectionSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
$collectionSelector = $serviceSelector[$service];
|
||||||
|
if (!$collectionSelector instanceof CollectionSelector) {
|
||||||
|
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection selectors');
|
||||||
|
}
|
||||||
|
|
||||||
|
$collectionSelector[$identifier->collection()] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sources;
|
||||||
|
}
|
||||||
|
|
||||||
private function entityFetch(string $tenantId, string $userId, array $data): mixed {
|
private function entityFetch(string $tenantId, string $userId, array $data): mixed {
|
||||||
if (!isset($data['targets'])) {
|
if (!isset($data['targets'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
|
throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
|
||||||
@@ -708,7 +733,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->entityFetchBulk(
|
return $this->mailManager->entityFetchBulk(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
...$targets->all()
|
...$targets->all()
|
||||||
@@ -716,39 +741,49 @@ class DefaultController extends ControllerAbstract {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function entityExtant(string $tenantId, string $userId, array $data): mixed {
|
private function entityExtant(string $tenantId, string $userId, array $data): mixed {
|
||||||
if (!isset($data['targets'])) {
|
if (!isset($data['sources'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
|
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
|
||||||
}
|
}
|
||||||
if (!is_array($data['targets'])) {
|
if (!is_array($data['sources'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
|
||||||
}
|
}
|
||||||
|
|
||||||
$targets = ResourceIdentifiers::fromArray($data['targets']);
|
$sources = new SourceSelector();
|
||||||
foreach ($targets as $target) {
|
$sources->jsonDeserialize($data['sources']);
|
||||||
if (!$target instanceof CollectionIdentifier && !$target instanceof EntityIdentifier) {
|
|
||||||
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service:collection or provider:service:collection:entity identifiers');
|
return $this->mailManager->entityExtant($tenantId, $userId, $sources);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->manager->entityExtant($tenantId, $userId, $targets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function entityDelta(string $tenantId, string $userId, array $data): mixed {
|
private function entityDelta(string $tenantId, string $userId, array $data): mixed {
|
||||||
|
if (!isset($data['sources'])) {
|
||||||
|
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
|
||||||
|
}
|
||||||
|
if (!is_array($data['sources'])) {
|
||||||
|
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sources = new SourceSelector();
|
||||||
|
$sources->jsonDeserialize($data['sources']);
|
||||||
|
|
||||||
|
return $this->mailManager->entityDelta($tenantId, $userId, $sources);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function entityDelete(string $tenantId, string $userId, array $data): mixed {
|
||||||
if (!isset($data['targets'])) {
|
if (!isset($data['targets'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
|
throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
|
||||||
}
|
}
|
||||||
if (!is_array($data['targets'])) {
|
if (!is_array($data['targets'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
||||||
}
|
}
|
||||||
|
|
||||||
$targets = ResourceIdentifiers::fromArray($data['targets']);
|
$targets = ResourceIdentifiers::fromArray($data['targets']);
|
||||||
foreach ($targets as $target) {
|
foreach ($targets as $target) {
|
||||||
if (!$target instanceof CollectionIdentifier && !$target instanceof EntityIdentifier) {
|
if (!$target instanceof EntityIdentifier) {
|
||||||
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service:collection or provider:service:collection:signature identifiers');
|
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service:collection:entity identifiers');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->entityDelta($tenantId, $userId, $targets);
|
return $this->mailManager->entityDelete($tenantId, $userId, ...$targets->all());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function entityPatch(string $tenantId, string $userId, array $data): mixed {
|
private function entityPatch(string $tenantId, string $userId, array $data): mixed {
|
||||||
@@ -772,25 +807,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->entityPatch($tenantId, $userId, $data['properties'], ...$targets->all());
|
return $this->mailManager->entityPatch($tenantId, $userId, $data['properties'], ...$targets->all());
|
||||||
}
|
|
||||||
|
|
||||||
private function entityDelete(string $tenantId, string $userId, array $data): mixed {
|
|
||||||
if (!isset($data['targets'])) {
|
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
|
|
||||||
}
|
|
||||||
if (!is_array($data['targets'])) {
|
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
|
||||||
}
|
|
||||||
|
|
||||||
$targets = ResourceIdentifiers::fromArray($data['targets']);
|
|
||||||
foreach ($targets as $target) {
|
|
||||||
if (!$target instanceof EntityIdentifier) {
|
|
||||||
throw new InvalidArgumentException('Invalid parameter: targets must contain provider:service:collection:entity identifiers');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->manager->entityDelete($tenantId, $userId, ...$targets->all());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function entityMove(string $tenantId, string $userId, array $data): mixed {
|
private function entityMove(string $tenantId, string $userId, array $data): mixed {
|
||||||
@@ -819,24 +836,32 @@ class DefaultController extends ControllerAbstract {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->manager->entityMove($tenantId, $userId, $target, ...$sources->all());
|
return $this->mailManager->entityMove($tenantId, $userId, $target, ...$sources->all());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function entitySubmit(string $tenantId, string $userId, array $data): mixed {
|
private function entityTransmit(string $tenantId, string $userId, array $data): mixed {
|
||||||
if (!isset($data['sender'])) {
|
if (!isset($data['provider'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_SENDER);
|
throw new InvalidArgumentException(self::ERR_MISSING_PROVIDER);
|
||||||
}
|
}
|
||||||
if (!is_string($data['sender'])) {
|
if (!is_string($data['provider'])) {
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_SENDER);
|
throw new InvalidArgumentException(self::ERR_INVALID_PROVIDER);
|
||||||
}
|
}
|
||||||
|
if (!isset($data['service'])) {
|
||||||
return $this->manager->entitySubmit(
|
throw new InvalidArgumentException(self::ERR_MISSING_SERVICE);
|
||||||
|
}
|
||||||
|
if (!is_string($data['service'])) {
|
||||||
|
throw new InvalidArgumentException(self::ERR_INVALID_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
$jobId = $this->mailManager->entityTransmit(
|
||||||
$tenantId,
|
$tenantId,
|
||||||
$userId,
|
$userId,
|
||||||
$data['sender'],
|
$data['provider'],
|
||||||
$data['source'] ?? null,
|
$data['service'],
|
||||||
$data['message'] ?? null,
|
$data['data']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return ['jobId' => $jobId];
|
||||||
}
|
}
|
||||||
|
|
||||||
private function entityDownload(string $tenantId, string $userId, array $data): mixed {
|
private function entityDownload(string $tenantId, string $userId, array $data): mixed {
|
||||||
@@ -853,7 +878,7 @@ class DefaultController extends ControllerAbstract {
|
|||||||
$target = ResourceIdentifier::fromString($data['target']);
|
$target = ResourceIdentifier::fromString($data['target']);
|
||||||
$logger = $this->logger;
|
$logger = $this->logger;
|
||||||
|
|
||||||
$result = $this->manager->entityDownload($tenantId, $userId, $target, $part);
|
$result = $this->mailManager->entityDownload($tenantId, $userId, $target, $part);
|
||||||
|
|
||||||
$filename = $result->filename();
|
$filename = $result->filename();
|
||||||
$asciiFilename = preg_replace('/[^\x20-\x7E]|[\\\\"]/', '_', $filename);
|
$asciiFilename = preg_replace('/[^\x20-\x7E]|[\\\\"]/', '_', $filename);
|
||||||
@@ -880,43 +905,5 @@ class DefaultController extends ControllerAbstract {
|
|||||||
'Cache-Control' => 'no-store',
|
'Cache-Control' => 'no-store',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function entityBlobs(string $tenantId, string $userId, array $data): mixed {
|
|
||||||
if (!isset($data['target'])) {
|
|
||||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGET);
|
|
||||||
}
|
|
||||||
if (!is_string($data['target'])) {
|
|
||||||
throw new InvalidArgumentException(self::ERR_INVALID_IDENTIFIER);
|
|
||||||
}
|
|
||||||
if (!isset($data['parts']) || !is_array($data['parts']) || $data['parts'] === []) {
|
|
||||||
throw new InvalidArgumentException('At least one part selector is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
$target = ResourceIdentifier::fromString($data['target']);
|
|
||||||
|
|
||||||
$results = [];
|
|
||||||
foreach ($data['parts'] as $part) {
|
|
||||||
if (!is_array($part)) {
|
|
||||||
throw new InvalidArgumentException('Invalid part selector');
|
|
||||||
}
|
|
||||||
|
|
||||||
$resource = $this->manager->entityDownload($tenantId, $userId, $target, $part);
|
|
||||||
|
|
||||||
$bytes = '';
|
|
||||||
foreach ($resource->stream() as $chunk) {
|
|
||||||
$bytes .= $chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
$results[] = [
|
|
||||||
'source' => $data['target'],
|
|
||||||
'part' => $part,
|
|
||||||
'mime' => $resource->mimeType(),
|
|
||||||
'filename' => $resource->filename(),
|
|
||||||
'bytes' => base64_encode($bytes),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $results;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+221
-213
@@ -10,8 +10,6 @@ use KTXF\Mail\Collection\CollectionBaseInterface;
|
|||||||
use KTXF\Mail\Collection\CollectionPropertiesMutableInterface;
|
use KTXF\Mail\Collection\CollectionPropertiesMutableInterface;
|
||||||
use KTXF\Mail\Collection\ICollectionBase;
|
use KTXF\Mail\Collection\ICollectionBase;
|
||||||
use KTXF\Mail\Entity\IMessageBase;
|
use KTXF\Mail\Entity\IMessageBase;
|
||||||
use KTXF\Mail\Object\Address;
|
|
||||||
use KTXF\Mail\Object\AddressInterface;
|
|
||||||
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
||||||
use KTXF\Mail\Provider\ProviderBaseInterface;
|
use KTXF\Mail\Provider\ProviderBaseInterface;
|
||||||
use KTXF\Mail\Provider\ProviderServiceDiscoverInterface;
|
use KTXF\Mail\Provider\ProviderServiceDiscoverInterface;
|
||||||
@@ -21,18 +19,20 @@ use KTXF\Mail\Service\ServiceBaseInterface;
|
|||||||
use KTXF\Mail\Service\ServiceCollectionMutableInterface;
|
use KTXF\Mail\Service\ServiceCollectionMutableInterface;
|
||||||
use KTXF\Mail\Service\ServiceConfigurableInterface;
|
use KTXF\Mail\Service\ServiceConfigurableInterface;
|
||||||
use KTXF\Mail\Service\ServiceEntityMutableInterface;
|
use KTXF\Mail\Service\ServiceEntityMutableInterface;
|
||||||
use KTXF\Mail\Service\ServiceEntitySubmitInterface;
|
|
||||||
use KTXF\Mail\Service\ServiceMutableInterface;
|
use KTXF\Mail\Service\ServiceMutableInterface;
|
||||||
use KTXF\Mail\Submission\EntitySubmitResult;
|
|
||||||
use KTXF\Resource\BinaryResource;
|
use KTXF\Resource\BinaryResource;
|
||||||
use KTXF\Resource\Filter\IFilter;
|
use KTXF\Resource\Filter\IFilter;
|
||||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||||
use KTXF\Resource\Identifier\EntityIdentifierInterface;
|
|
||||||
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
use KTXF\Resource\Identifier\ResourceIdentifiers;
|
||||||
use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
|
use KTXF\Resource\Provider\ResourceServiceIdentityInterface;
|
||||||
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
use KTXF\Resource\Provider\ResourceServiceLocationInterface;
|
||||||
|
use KTXF\Resource\Range\RangeAnchorType;
|
||||||
use KTXF\Resource\Range\RangeType;
|
use KTXF\Resource\Range\RangeType;
|
||||||
|
use KTXF\Resource\Selector\CollectionSelector;
|
||||||
|
use KTXF\Resource\Selector\EntitySelector;
|
||||||
|
use KTXF\Resource\Selector\ServiceSelector;
|
||||||
|
use KTXF\Resource\Selector\SourceSelector;
|
||||||
use KTXF\Resource\Sort\ISort;
|
use KTXF\Resource\Sort\ISort;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
@@ -51,13 +51,15 @@ class Manager {
|
|||||||
/**
|
/**
|
||||||
* Retrieve available providers
|
* Retrieve available providers
|
||||||
*
|
*
|
||||||
* @param ResourceIdentifiers|null $sources collection of provider identifiers
|
* @param SourceSelector|null $sources collection of provider identifiers
|
||||||
*
|
*
|
||||||
* @return array<string,ProviderBaseInterface> collection of available providers e.g. ['provider1' => IProvider, 'provider2' => IProvider]
|
* @return array<string,ProviderBaseInterface> collection of available providers e.g. ['provider1' => IProvider, 'provider2' => IProvider]
|
||||||
*/
|
*/
|
||||||
public function providerList(string $tenantId, string $userId, array|null $targets = []): array {
|
public function providerList(string $tenantId, string $userId, ?SourceSelector $sources = null): array {
|
||||||
|
// determine filter from sources
|
||||||
|
$filter = ($sources !== null && $sources->identifiers() !== []) ? $sources->identifiers() : null;
|
||||||
// retrieve providers from provider manager
|
// retrieve providers from provider manager
|
||||||
return $this->providerManager->providers(ProviderBaseInterface::TYPE_MAIL, $targets ?: null);
|
return $this->providerManager->providers(ProviderBaseInterface::TYPE_MAIL, $filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,27 +72,27 @@ class Manager {
|
|||||||
* @return ProviderBaseInterface
|
* @return ProviderBaseInterface
|
||||||
* @throws InvalidArgumentException
|
* @throws InvalidArgumentException
|
||||||
*/
|
*/
|
||||||
public function providerFetch(string $tenantId, string $userId, string $target): ProviderBaseInterface {
|
public function providerFetch(string $tenantId, string $userId, string $provider): ProviderBaseInterface {
|
||||||
// retrieve provider
|
// retrieve provider
|
||||||
$providers = $this->providerList($tenantId, $userId, [$target]);
|
$providers = $this->providerList($tenantId, $userId, new SourceSelector([$provider => true]));
|
||||||
if (!isset($providers[$target])) {
|
if (!isset($providers[$provider])) {
|
||||||
throw new InvalidArgumentException("Provider '$target' not found");
|
throw new InvalidArgumentException("Provider '$provider' not found");
|
||||||
}
|
}
|
||||||
return $providers[$target];
|
return $providers[$provider];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm which providers are available
|
* Confirm which providers are available
|
||||||
*
|
*
|
||||||
* @param array<string> $targets collection of provider identifiers to confirm
|
* @param SourceSelector|null $sources collection of provider identifiers to confirm
|
||||||
*
|
*
|
||||||
* @return array<string,bool> collection of providers and their availability status e.g. ['provider1' => true, 'provider2' => false]
|
* @return array<string,bool> collection of providers and their availability status e.g. ['provider1' => true, 'provider2' => false]
|
||||||
*/
|
*/
|
||||||
public function providerExtant(string $tenantId, string $userId, array $targets): array {
|
public function providerExtant(string $tenantId, string $userId, SourceSelector $sources): array {
|
||||||
// determine which providers are available
|
// determine which providers are available
|
||||||
$providersResolved = $this->providerList($tenantId, $userId, $targets);
|
$providersResolved = $this->providerList($tenantId, $userId, $sources);
|
||||||
$providersAvailable = array_keys($providersResolved);
|
$providersAvailable = array_keys($providersResolved);
|
||||||
$providersUnavailable = array_diff($targets, $providersAvailable);
|
$providersUnavailable = array_diff($sources->identifiers(), $providersAvailable);
|
||||||
// construct response data
|
// construct response data
|
||||||
$responseData = array_merge(
|
$responseData = array_merge(
|
||||||
array_fill_keys($providersAvailable, true),
|
array_fill_keys($providersAvailable, true),
|
||||||
@@ -104,24 +106,18 @@ class Manager {
|
|||||||
*
|
*
|
||||||
* @param string $tenantId tenant identifier
|
* @param string $tenantId tenant identifier
|
||||||
* @param string $userId user identifier
|
* @param string $userId user identifier
|
||||||
* @param ResourceIdentifiers|null $targets list of provider and service identifiers
|
* @param SourceSelector|null $sources list of provider and service identifiers
|
||||||
*
|
*
|
||||||
* @return array<string,<string,ServiceBaseInterface>> collections of available services e.g. ['provider1' => ['service1' => IServiceBase], 'provider2' => ['service2' => IServiceBase]]
|
* @return array<string,<string,ServiceBaseInterface>> collections of available services e.g. ['provider1' => ['service1' => IServiceBase], 'provider2' => ['service2' => IServiceBase]]
|
||||||
*/
|
*/
|
||||||
public function serviceList(string $tenantId, string $userId, ?ResourceIdentifiers $targets = null): array {
|
public function serviceList(string $tenantId, string $userId, ?SourceSelector $sources = null): array {
|
||||||
// retrieve providers
|
// retrieve providers
|
||||||
$providerFilter = $targets !== null ? $targets->providers() : null;
|
$providers = $this->providerList($tenantId, $userId, $sources);
|
||||||
$providers = $this->providerList($tenantId, $userId, $providerFilter);
|
|
||||||
// retrieve services for each provider
|
// retrieve services for each provider
|
||||||
$responseData = [];
|
$responseData = [];
|
||||||
foreach ($providers as $provider) {
|
foreach ($providers as $provider) {
|
||||||
if ($targets !== null) {
|
$serviceFilter = $sources[$provider->identifier()] instanceof ServiceSelector ? $sources[$provider->identifier()]->identifiers() : [];
|
||||||
$servicesSelected = $targets?->byProvider($provider->identifier()) ?: [];
|
$services = $provider->serviceList($tenantId, $userId, $serviceFilter);
|
||||||
$servicesFilter = $servicesSelected->services();
|
|
||||||
} else {
|
|
||||||
$servicesFilter = [];
|
|
||||||
}
|
|
||||||
$services = $provider->serviceList($tenantId, $userId, $servicesFilter);
|
|
||||||
$responseData[$provider->identifier()] = $services;
|
$responseData[$provider->identifier()] = $services;
|
||||||
}
|
}
|
||||||
return $responseData;
|
return $responseData;
|
||||||
@@ -153,23 +149,24 @@ class Manager {
|
|||||||
*
|
*
|
||||||
* @param string $tenantId tenant identifier
|
* @param string $tenantId tenant identifier
|
||||||
* @param string $userId user identifier
|
* @param string $userId user identifier
|
||||||
* @param ResourceIdentifiers $targets collection of provider and service identifiers to confirm
|
* @param SourceSelector|null $sources collection of provider and service identifiers to confirm
|
||||||
*
|
*
|
||||||
* @return array<string,bool> collection of providers and their availability status e.g. ['provider1' => ['service1' => false], 'provider2' => ['service2' => true, 'service3' => true]]
|
* @return array<string,bool> collection of providers and their availability status e.g. ['provider1' => ['service1' => false], 'provider2' => ['service2' => true, 'service3' => true]]
|
||||||
*/
|
*/
|
||||||
public function serviceExtant(string $tenantId, string $userId, ResourceIdentifiers $targets): array {
|
public function serviceExtant(string $tenantId, string $userId, SourceSelector $sources): array {
|
||||||
// retrieve available providers
|
// retrieve providers
|
||||||
$providersRequested = $targets->providers();
|
$providers = $this->providerList($tenantId, $userId, $sources);
|
||||||
$providers = $this->providerList($tenantId, $userId, $providersRequested);
|
$providersRequested = $sources->identifiers();
|
||||||
$providersUnavailable = array_diff($providersRequested, array_keys($providers));
|
$providersUnavailable = array_diff($providersRequested, array_keys($providers));
|
||||||
|
|
||||||
// initialize response with unavailable providers
|
// initialize response with unavailable providers
|
||||||
$responseData = array_fill_keys($providersUnavailable, false);
|
$responseData = array_fill_keys($providersUnavailable, false);
|
||||||
|
|
||||||
// retrieve services for each available provider
|
// retrieve services for each available provider
|
||||||
foreach ($providers as $providerId => $provider) {
|
foreach ($providers as $provider) {
|
||||||
$servicesRequested = $targets->byProvider($providerId)->services();
|
$serviceSelector = $sources[$provider->identifier()];
|
||||||
$responseData[$providerId] = $provider->serviceExtant($tenantId, $userId, ...$servicesRequested);
|
$serviceAvailability = $provider->serviceExtant($tenantId, $userId, ...$serviceSelector->identifiers());
|
||||||
|
$responseData[$provider->identifier()] = $serviceAvailability;
|
||||||
}
|
}
|
||||||
return $responseData;
|
return $responseData;
|
||||||
}
|
}
|
||||||
@@ -236,14 +233,7 @@ class Manager {
|
|||||||
$serviceId = $provider->serviceCreate($tenantId, $userId, $service);
|
$serviceId = $provider->serviceCreate($tenantId, $userId, $service);
|
||||||
|
|
||||||
// Fetch and return the created service
|
// Fetch and return the created service
|
||||||
$createdService = $provider->serviceFetch($tenantId, $userId, $serviceId);
|
return $provider->serviceFetch($tenantId, $userId, $serviceId);
|
||||||
if ($createdService === null) {
|
|
||||||
throw new \RuntimeException(
|
|
||||||
"Provider '$providerId' created service '$serviceId', but it could not be fetched"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $createdService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -347,7 +337,7 @@ class Manager {
|
|||||||
string|null $location = null,
|
string|null $location = null,
|
||||||
string|null $secret = null
|
string|null $secret = null
|
||||||
): \Generator {
|
): \Generator {
|
||||||
$providers = $this->providerList($tenantId, $userId, $providerId !== null ? [$providerId] : null);
|
$providers = $this->providerList($tenantId, $userId, $providerId !== null ? new SourceSelector([$providerId => true]) : null);
|
||||||
|
|
||||||
foreach ($providers as $currentProviderId => $provider) {
|
foreach ($providers as $currentProviderId => $provider) {
|
||||||
if ($provider instanceof ProviderServiceDiscoverInterface === false) {
|
if ($provider instanceof ProviderServiceDiscoverInterface === false) {
|
||||||
@@ -464,23 +454,26 @@ class Manager {
|
|||||||
*
|
*
|
||||||
* @param string $tenantId Tenant identifier
|
* @param string $tenantId Tenant identifier
|
||||||
* @param string|null $userId User identifier for context
|
* @param string|null $userId User identifier for context
|
||||||
* @param ResourceIdentifiers|null $targets Provider/service sources
|
* @param SourceSelector|null $sources Provider/service sources
|
||||||
* @param IFilter|null $filter Collection filter
|
* @param IFilter|null $filter Collection filter
|
||||||
* @param ISort|null $sort Collection sort
|
* @param ISort|null $sort Collection sort
|
||||||
*
|
*
|
||||||
* @return array<string, array<string|int, array<string|int, ICollectionBase>>> Collections grouped by provider/service
|
* @return array<string, array<string|int, array<string|int, ICollectionBase>>> Collections grouped by provider/service
|
||||||
*/
|
*/
|
||||||
public function collectionList(string $tenantId, ?string $userId, ?ResourceIdentifiers $targets = null, ?IFilter $filter = null, ?ISort $sort = null): array {
|
public function collectionList(string $tenantId, ?string $userId, ?SourceSelector $sources = null, ?IFilter $filter = null, ?ISort $sort = null): array {
|
||||||
// confirm that sources are provided
|
// confirm that sources are provided
|
||||||
if ($targets === null) {
|
if ($sources === null) {
|
||||||
$targets = new ResourceIdentifiers([]);
|
$sources = new SourceSelector([]);
|
||||||
}
|
}
|
||||||
// retrieve providers
|
// retrieve providers
|
||||||
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
|
$providers = $this->providerList($tenantId, $userId, $sources);
|
||||||
// retrieve services for each provider
|
// retrieve services for each provider
|
||||||
$responseData = [];
|
$responseData = [];
|
||||||
// retrieve collections for each service
|
foreach ($providers as $provider) {
|
||||||
foreach ($aggregateServices as $services) {
|
$serviceFilter = $sources[$provider->identifier()] instanceof ServiceSelector ? $sources[$provider->identifier()]->identifiers() : [];
|
||||||
|
/** @var ServiceBaseInterface[] $services */
|
||||||
|
$services = $provider->serviceList($tenantId, $userId, $serviceFilter);
|
||||||
|
// retrieve collections for each service
|
||||||
foreach ($services as $service) {
|
foreach ($services as $service) {
|
||||||
if ($service->getEnabled() === false) {
|
if ($service->getEnabled() === false) {
|
||||||
continue;
|
continue;
|
||||||
@@ -501,23 +494,72 @@ class Manager {
|
|||||||
$collectionSort->condition($attribute, $direction);
|
$collectionSort->condition($attribute, $direction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
$collections = $service->collectionList('', $collectionFilter, $collectionSort);
|
||||||
$collectionIdentifiers = $targets->byProvider($service->provider())->byService($service->identifier())->collections();
|
|
||||||
if ($collectionIdentifiers !== []) {
|
|
||||||
foreach ($collectionIdentifiers as $collectionIdentifier) {
|
|
||||||
$collections = array_merge($collections ?? [], $service->collectionList($collectionIdentifier, $collectionFilter, $collectionSort));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$collections = $service->collectionList('', $collectionFilter, $collectionSort);
|
|
||||||
}
|
|
||||||
if ($collections !== []) {
|
if ($collections !== []) {
|
||||||
$responseData[$service->provider()][$service->identifier()] = $collections;
|
$responseData[$provider->identifier()][$service->identifier()] = $collections;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return $responseData;
|
return $responseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if collections exist
|
||||||
|
*
|
||||||
|
* @since 2025.05.01
|
||||||
|
*
|
||||||
|
* @param string $tenantId Tenant identifier
|
||||||
|
* @param string|null $userId User identifier for context
|
||||||
|
* @param SourceSelector $sources Collection sources with identifiers
|
||||||
|
*
|
||||||
|
* @return array<string, array<string|int, array<string|int, bool>>> Existence map grouped by provider/service
|
||||||
|
*/
|
||||||
|
public function collectionExtant(string $tenantId, ?string $userId, SourceSelector $sources): array {
|
||||||
|
// retrieve available providers
|
||||||
|
$providers = $this->providerList($tenantId, $userId, $sources);
|
||||||
|
$providersRequested = $sources->identifiers();
|
||||||
|
$providersUnavailable = array_diff($providersRequested, array_keys($providers));
|
||||||
|
|
||||||
|
// initialize response with unavailable providers
|
||||||
|
$responseData = array_fill_keys($providersUnavailable, false);
|
||||||
|
|
||||||
|
// check services and collections for each available provider
|
||||||
|
foreach ($providers as $provider) {
|
||||||
|
// extract services for this provider
|
||||||
|
$serviceSelector = $sources[$provider->identifier()];
|
||||||
|
$servicesRequested = $serviceSelector->identifiers();
|
||||||
|
/** @var ServiceBaseInterface[] $servicesAvailable */
|
||||||
|
$servicesAvailable = $provider->serviceList($tenantId, $userId, $servicesRequested);
|
||||||
|
$servicesUnavailable = array_diff($servicesRequested, array_keys($servicesAvailable));
|
||||||
|
// mark unavailable services as false
|
||||||
|
if ($servicesUnavailable !== []) {
|
||||||
|
$responseData[$provider->identifier()] = array_fill_keys($servicesUnavailable, false);
|
||||||
|
}
|
||||||
|
// confirm collections for each available service
|
||||||
|
foreach ($servicesAvailable as $service) {
|
||||||
|
// omit disabled services
|
||||||
|
if ($service->getEnabled() === false) {
|
||||||
|
$responseData[$provider->identifier()][$service->identifier()] = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// extract collections requested for this service
|
||||||
|
$collectionSelector = $serviceSelector[$service->identifier()];
|
||||||
|
$collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
|
||||||
|
if ($collectionsRequested === []) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// check each requested collection
|
||||||
|
$collectionsAvailable = $service->collectionExtant(...$collectionsRequested);
|
||||||
|
$collectionsUnavailable = array_diff($collectionsRequested, array_keys($collectionsAvailable));
|
||||||
|
$responseData[$provider->identifier()][$service->identifier()] = array_merge(
|
||||||
|
$collectionsAvailable,
|
||||||
|
array_fill_keys($collectionsUnavailable, false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $responseData;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch a specific collection
|
* Fetch a specific collection
|
||||||
*
|
*
|
||||||
@@ -531,62 +573,14 @@ class Manager {
|
|||||||
*
|
*
|
||||||
* @return CollectionBaseInterface|null
|
* @return CollectionBaseInterface|null
|
||||||
*/
|
*/
|
||||||
public function collectionFetch(string $tenantId, ?string $userId, CollectionIdentifier $target): ?CollectionBaseInterface {
|
public function collectionFetch(string $tenantId, ?string $userId, string $providerId, string|int $serviceId, string|int $collectionId): ?CollectionBaseInterface {
|
||||||
// retrieve service
|
// retrieve service
|
||||||
$service = $this->serviceFetch($tenantId, $userId, $target->provider(), $target->service());
|
$service = $this->serviceFetch($tenantId, $userId, $providerId, $serviceId);
|
||||||
// retrieve collection
|
if ($service === null || $service->getEnabled() === false) {
|
||||||
return $service->collectionFetch($target->collection());
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if collections exist
|
|
||||||
*
|
|
||||||
* @since 2025.05.01
|
|
||||||
*
|
|
||||||
* @param string $tenantId Tenant identifier
|
|
||||||
* @param string|null $userId User identifier for context
|
|
||||||
* @param ResourceIdentifiers $targets Collection sources with identifiers
|
|
||||||
*
|
|
||||||
* @return array<string, array<string|int, array<string|int, bool>>> Existence map grouped by provider/service
|
|
||||||
*/
|
|
||||||
public function collectionExtant(string $tenantId, ?string $userId, ResourceIdentifiers $targets): array {
|
|
||||||
// retrieve available services grouped by provider
|
|
||||||
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
|
|
||||||
// initialize response with unavailable providers marked as false
|
|
||||||
$providersRequested = $targets->providers();
|
|
||||||
$providersUnavailable = array_diff($providersRequested, array_keys($aggregateServices));
|
|
||||||
$responseData = array_fill_keys($providersUnavailable, false);
|
|
||||||
|
|
||||||
// check services and collections for each available provider
|
|
||||||
foreach ($aggregateServices as $providerId => $services) {
|
|
||||||
// mark unavailable services as false
|
|
||||||
$servicesRequested = $targets->byProvider($providerId)->services();
|
|
||||||
$servicesUnavailable = array_diff($servicesRequested, array_keys($services));
|
|
||||||
if ($servicesUnavailable !== []) {
|
|
||||||
$responseData[$providerId] = array_fill_keys($servicesUnavailable, false);
|
|
||||||
}
|
|
||||||
// confirm collections for each available service
|
|
||||||
foreach ($services as $service) {
|
|
||||||
// omit disabled services
|
|
||||||
if ($service->getEnabled() === false) {
|
|
||||||
$responseData[$providerId][$service->identifier()] = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// extract collections requested for this service
|
|
||||||
$collectionsRequested = $targets->byProvider($providerId)->byService($service->identifier())->collections();
|
|
||||||
if ($collectionsRequested === []) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// check each requested collection
|
|
||||||
$collectionsAvailable = $service->collectionExtant(...$collectionsRequested);
|
|
||||||
$collectionsUnavailable = array_diff($collectionsRequested, array_keys($collectionsAvailable));
|
|
||||||
$responseData[$providerId][$service->identifier()] = array_merge(
|
|
||||||
$collectionsAvailable,
|
|
||||||
array_fill_keys($collectionsUnavailable, false)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return $responseData;
|
// retrieve collection
|
||||||
|
return $service->collectionFetch($collectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -731,31 +725,31 @@ class Manager {
|
|||||||
*
|
*
|
||||||
* @param string $tenantId Tenant identifier
|
* @param string $tenantId Tenant identifier
|
||||||
* @param string $userId User identifier
|
* @param string $userId User identifier
|
||||||
* @param ResourceIdentifiers|null $targets Message sources with collection identifiers
|
* @param SourceSelector $sources Message sources with collection identifiers
|
||||||
* @param array|null $filter Message filter
|
* @param array|null $filter Message filter
|
||||||
* @param array|null $sort Message sort
|
* @param array|null $sort Message sort
|
||||||
* @param array|null $range Message range/pagination
|
* @param array|null $range Message range/pagination
|
||||||
*
|
*
|
||||||
* @return array<string, array<string|int, array<string|int, array<string|int, IMessageBase>>>> Messages grouped by provider/service/collection
|
* @return array<string, array<string|int, array<string|int, array<string|int, IMessageBase>>>> Messages grouped by provider/service/collection
|
||||||
*/
|
*/
|
||||||
public function entityListBulk(string $tenantId, string $userId, ?ResourceIdentifiers $targets = null, array|null $filter = null, array|null $sort = null, array|null $range = null): array {
|
public function entityListBulk(string $tenantId, string $userId, SourceSelector $sources, array|null $filter = null, array|null $sort = null, array|null $range = null): array {
|
||||||
// confirm that sources are provided
|
// retrieve providers
|
||||||
if ($targets === null) {
|
$providers = $this->providerList($tenantId, $userId, $sources);
|
||||||
$targets = new ResourceIdentifiers([]);
|
|
||||||
}
|
|
||||||
// retrieve services for each provider
|
// retrieve services for each provider
|
||||||
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
|
|
||||||
// retrieve entities for each service
|
|
||||||
$responseData = [];
|
$responseData = [];
|
||||||
foreach ($aggregateServices as $services) {
|
foreach ($providers as $provider) {
|
||||||
|
// retrieve services for each provider
|
||||||
|
$serviceSelector = $sources[$provider->identifier()];
|
||||||
|
$servicesSelected = $provider->serviceList($tenantId,$userId, $serviceSelector->identifiers());
|
||||||
/** @var ServiceBaseInterface $service */
|
/** @var ServiceBaseInterface $service */
|
||||||
foreach ($services as $service) {
|
foreach ($servicesSelected as $service) {
|
||||||
// omit disabled services
|
// omit disabled services
|
||||||
if ($service->getEnabled() === false) {
|
if ($service->getEnabled() === false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// retrieve collections for each service
|
// retrieve collections for each service
|
||||||
$collectionSelected = $targets->byProvider($service->provider())->byService($service->identifier())->collections();
|
$collectionSelector = $serviceSelector[$service->identifier()];
|
||||||
|
$collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
|
||||||
if ($collectionSelected === []) {
|
if ($collectionSelected === []) {
|
||||||
$collections = $service->collectionList('');
|
$collections = $service->collectionList('');
|
||||||
$collectionSelected = array_map(
|
$collectionSelected = array_map(
|
||||||
@@ -785,16 +779,29 @@ class Manager {
|
|||||||
// construct range for entities
|
// construct range for entities
|
||||||
$entityRange = null;
|
$entityRange = null;
|
||||||
if ($range !== null && $range !== [] && isset($range['type'])) {
|
if ($range !== null && $range !== [] && isset($range['type'])) {
|
||||||
$entityRange = $service->entityListRange(RangeType::from($range['type']))->jsonDeserialize($range);
|
$entityRange = $service->entityListRange(RangeType::from($range['type']));
|
||||||
|
// Cast to IRangeTally if the range type is TALLY
|
||||||
|
if ($entityRange->type() === RangeType::TALLY) {
|
||||||
|
/** @var IRangeTally $entityRange */
|
||||||
|
if (isset($range['anchor'])) {
|
||||||
|
$entityRange->setAnchor(RangeAnchorType::from($range['anchor']));
|
||||||
|
}
|
||||||
|
if (isset($range['position'])) {
|
||||||
|
$entityRange->setPosition($range['position']);
|
||||||
|
}
|
||||||
|
if (isset($range['tally'])) {
|
||||||
|
$entityRange->setTally($range['tally']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// retrieve entities for each collection
|
// retrieve entities for each collection
|
||||||
foreach ($collectionSelected as $collectionId) {
|
foreach ($collectionSelected as $collectionId) {
|
||||||
$entities = $service->entityListBulk($collectionId, $entityFilter, $entitySort, $entityRange, null);
|
$entities = $service->entityList($collectionId, $entityFilter, $entitySort, $entityRange, null);
|
||||||
// skip collections with no entities
|
// skip collections with no entities
|
||||||
if ($entities === []) {
|
if ($entities === []) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$responseData[$service->provider()][$service->identifier()][$collectionId] = $entities;
|
$responseData[$provider->identifier()][$service->identifier()][$collectionId] = $entities;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -809,29 +816,29 @@ class Manager {
|
|||||||
*
|
*
|
||||||
* @param string $tenantId Tenant identifier
|
* @param string $tenantId Tenant identifier
|
||||||
* @param string $userId User identifier
|
* @param string $userId User identifier
|
||||||
* @param ResourceIdentifiers|null $targets Message sources with collection identifiers
|
* @param SourceSelector $sources Message sources with collection identifiers
|
||||||
* @param array|null $filter Message filter
|
* @param array|null $filter Message filter
|
||||||
* @param array|null $sort Message sort
|
* @param array|null $sort Message sort
|
||||||
* @param array|null $range Message range/pagination
|
* @param array|null $range Message range/pagination
|
||||||
*
|
*
|
||||||
* @return \Generator<EntityBaseInterface> Yields each entity as it is retrieved
|
* @return \Generator<EntityBaseInterface> Yields each entity as it is retrieved
|
||||||
*/
|
*/
|
||||||
public function entityListStream(string $tenantId, string $userId, ?ResourceIdentifiers $targets = null, array|null $filter = null, array|null $sort = null, array|null $range = null): \Generator {
|
public function entityListStream(string $tenantId, string $userId, SourceSelector $sources, array|null $filter = null, array|null $sort = null, array|null $range = null): \Generator {
|
||||||
// confirm that sources are provided
|
// retrieve providers
|
||||||
if ($targets === null) {
|
$providers = $this->providerList($tenantId, $userId, $sources);
|
||||||
$targets = new ResourceIdentifiers([]);
|
|
||||||
}
|
|
||||||
// retrieve services for each provider
|
// retrieve services for each provider
|
||||||
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
|
foreach ($providers as $provider) {
|
||||||
foreach ($aggregateServices as $services) {
|
$serviceSelector = $sources[$provider->identifier()];
|
||||||
|
$servicesSelected = $provider->serviceList($tenantId, $userId, $serviceSelector->identifiers());
|
||||||
/** @var ServiceBaseInterface $service */
|
/** @var ServiceBaseInterface $service */
|
||||||
foreach ($services as $service) {
|
foreach ($servicesSelected as $service) {
|
||||||
// omit disabled services
|
// omit disabled services
|
||||||
if ($service->getEnabled() === false) {
|
if ($service->getEnabled() === false) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// retrieve collections for each service
|
// retrieve collections for each service
|
||||||
$collectionSelected = $targets->byProvider($service->provider())->byService($service->identifier())->collections();
|
$collectionSelector = $serviceSelector[$service->identifier()];
|
||||||
|
$collectionSelected = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
|
||||||
if ($collectionSelected === []) {
|
if ($collectionSelected === []) {
|
||||||
$collections = $service->collectionList('');
|
$collections = $service->collectionList('');
|
||||||
$collectionSelected = array_map(
|
$collectionSelected = array_map(
|
||||||
@@ -861,7 +868,19 @@ class Manager {
|
|||||||
// construct range for entities
|
// construct range for entities
|
||||||
$entityRange = null;
|
$entityRange = null;
|
||||||
if ($range !== null && $range !== [] && isset($range['type'])) {
|
if ($range !== null && $range !== [] && isset($range['type'])) {
|
||||||
$entityRange = $service->entityListRange(RangeType::from($range['type']))->jsonDeserialize($range);
|
$entityRange = $service->entityListRange(RangeType::from($range['type']));
|
||||||
|
if ($entityRange->type() === RangeType::TALLY) {
|
||||||
|
/** @var IRangeTally $entityRange */
|
||||||
|
if (isset($range['anchor'])) {
|
||||||
|
$entityRange->setAnchor(RangeAnchorType::from($range['anchor']));
|
||||||
|
}
|
||||||
|
if (isset($range['position'])) {
|
||||||
|
$entityRange->setPosition($range['position']);
|
||||||
|
}
|
||||||
|
if (isset($range['tally'])) {
|
||||||
|
$entityRange->setTally($range['tally']);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// yield entities for each collection individually
|
// yield entities for each collection individually
|
||||||
foreach ($collectionSelected as $collectionId) {
|
foreach ($collectionSelected as $collectionId) {
|
||||||
@@ -886,7 +905,7 @@ class Manager {
|
|||||||
// group identifiers by provider/service
|
// group identifiers by provider/service
|
||||||
$groupedIdentifiers = [];
|
$groupedIdentifiers = [];
|
||||||
foreach ($identifiers as $identifier) {
|
foreach ($identifiers as $identifier) {
|
||||||
$groupedIdentifiers[$identifier->provider()][$identifier->service()][] = $identifier;
|
$groupedIdentifiers[$identifier->provider()][$identifier->service()][] = $identifier->entity();
|
||||||
}
|
}
|
||||||
// retrieve each service and fetch entities
|
// retrieve each service and fetch entities
|
||||||
$list = [];
|
$list = [];
|
||||||
@@ -947,36 +966,43 @@ class Manager {
|
|||||||
*
|
*
|
||||||
* @param string $tenantId Tenant identifier
|
* @param string $tenantId Tenant identifier
|
||||||
* @param string|null $userId User identifier for context
|
* @param string|null $userId User identifier for context
|
||||||
* @param ResourceIdentifiers $targets Message sources with identifiers
|
* @param SourceSelector $sources Message sources with identifiers
|
||||||
*
|
*
|
||||||
* @return array<string, array<string|int, array<string|int, array<string|int, bool>>>> Existence map grouped by provider/service/collection
|
* @return array<string, array<string|int, array<string|int, array<string|int, bool>>>> Existence map grouped by provider/service/collection
|
||||||
*/
|
*/
|
||||||
public function entityExtant(string $tenantId, string $userId, ResourceIdentifiers $targets): array {
|
public function entityExtant(string $tenantId, string $userId, SourceSelector $sources): array {
|
||||||
// retrieve available services grouped by provider
|
// confirm that sources are provided
|
||||||
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
|
if ($sources === null) {
|
||||||
// initialize response with unavailable providers marked as false
|
$sources = new SourceSelector([]);
|
||||||
$providersRequested = $targets->providers();
|
}
|
||||||
$providersUnavailable = array_diff($providersRequested, array_keys($aggregateServices));
|
// retrieve available providers
|
||||||
|
$providers = $this->providerList($tenantId, $userId, $sources);
|
||||||
|
$providersRequested = $sources->identifiers();
|
||||||
|
$providersUnavailable = array_diff($providersRequested, array_keys($providers));
|
||||||
|
// initialize response with unavailable providers
|
||||||
$responseData = array_fill_keys($providersUnavailable, false);
|
$responseData = array_fill_keys($providersUnavailable, false);
|
||||||
|
|
||||||
// check services, collections, and entities for each available provider
|
// check services, collections, and entities for each available provider
|
||||||
foreach ($aggregateServices as $providerId => $services) {
|
foreach ($providers as $provider) {
|
||||||
|
// extract services requested for this provider
|
||||||
|
$serviceSelector = $sources[$provider->identifier()];
|
||||||
|
$servicesRequested = $serviceSelector->identifiers();
|
||||||
|
/** @var ServiceBaseInterface[] $servicesAvailable */
|
||||||
|
$servicesAvailable = $provider->serviceList($tenantId, $userId, $servicesRequested);
|
||||||
|
$servicesUnavailable = array_diff($servicesRequested, array_keys($servicesAvailable));
|
||||||
// mark unavailable services as false
|
// mark unavailable services as false
|
||||||
$servicesRequested = $targets->byProvider($providerId)->services();
|
|
||||||
$servicesUnavailable = array_diff($servicesRequested, array_keys($services));
|
|
||||||
if ($servicesUnavailable !== []) {
|
if ($servicesUnavailable !== []) {
|
||||||
$responseData[$providerId] = array_fill_keys($servicesUnavailable, false);
|
$responseData[$provider->identifier()] = array_fill_keys($servicesUnavailable, false);
|
||||||
}
|
}
|
||||||
// check collections and entities for each available service
|
// check collections and entities for each available service
|
||||||
foreach ($services as $service) {
|
foreach ($servicesAvailable as $service) {
|
||||||
// omit disabled services
|
// omit disabled services
|
||||||
if ($service->getEnabled() === false) {
|
if ($service->getEnabled() === false) {
|
||||||
$responseData[$providerId][$service->identifier()] = false;
|
$responseData[$provider->identifier()][$service->identifier()] = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// extract collections requested for this service
|
// extract collections requested for this service
|
||||||
$serviceTargets = $targets->byProvider($providerId)->byService($service->identifier());
|
$collectionSelector = $serviceSelector[$service->identifier()];
|
||||||
$collectionsRequested = $serviceTargets->collections();
|
$collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
|
||||||
if ($collectionsRequested === []) {
|
if ($collectionsRequested === []) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -986,18 +1012,19 @@ class Manager {
|
|||||||
$collectionExists = $service->collectionExtant((string)$collectionId);
|
$collectionExists = $service->collectionExtant((string)$collectionId);
|
||||||
if (!$collectionExists) {
|
if (!$collectionExists) {
|
||||||
// collection doesn't exist, mark as false
|
// collection doesn't exist, mark as false
|
||||||
$responseData[$providerId][$service->identifier()][$collectionId] = false;
|
$responseData[$provider->identifier()][$service->identifier()][$collectionId] = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// extract entity identifiers requested for this collection
|
// extract entity identifiers from collection selector
|
||||||
$entitiesRequested = $serviceTargets->byCollection($collectionId)->entities();
|
$entitySelector = $collectionSelector[$collectionId];
|
||||||
if ($entitiesRequested === []) {
|
// handle both array of entity IDs and boolean true (meaning check if collection exists)
|
||||||
|
if ($entitySelector instanceof EntitySelector) {
|
||||||
|
// check specific entities within the collection
|
||||||
|
$responseData[$provider->identifier()][$service->identifier()][$collectionId] = $service->entityExtant($collectionId, ...$entitySelector->identifiers());
|
||||||
|
} elseif ($entitySelector === true) {
|
||||||
// just checking if collection exists (already confirmed above)
|
// just checking if collection exists (already confirmed above)
|
||||||
$responseData[$providerId][$service->identifier()][$collectionId] = true;
|
$responseData[$provider->identifier()][$service->identifier()][$collectionId] = true;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
// check specific entities within the collection
|
|
||||||
$responseData[$providerId][$service->identifier()][$collectionId] = $service->entityExtant($collectionId, ...$entitiesRequested);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1011,45 +1038,50 @@ class Manager {
|
|||||||
*
|
*
|
||||||
* @param string $tenantId Tenant identifier
|
* @param string $tenantId Tenant identifier
|
||||||
* @param string|null $userId User identifier for context
|
* @param string|null $userId User identifier for context
|
||||||
* @param ResourceIdentifiers $targets Message sources with signatures
|
* @param SourceSelector $sources Message sources with signatures
|
||||||
*
|
*
|
||||||
* @return array<string, array<string|int, array<string|int, array>>> Delta grouped by provider/service/collection
|
* @return array<string, array<string|int, array<string|int, array>>> Delta grouped by provider/service/collection
|
||||||
*/
|
*/
|
||||||
public function entityDelta(string $tenantId, string $userId, ResourceIdentifiers $targets): array {
|
public function entityDelta(string $tenantId, string $userId, SourceSelector $sources): array {
|
||||||
// retrieve available services grouped by provider
|
// confirm that sources are provided
|
||||||
$aggregateServices = $this->serviceList($tenantId, $userId, $targets);
|
if ($sources === null) {
|
||||||
// initialize response with unavailable providers marked as false
|
$sources = new SourceSelector([]);
|
||||||
$providersRequested = $targets->providers();
|
}
|
||||||
$providersUnavailable = array_diff($providersRequested, array_keys($aggregateServices));
|
// retrieve providers
|
||||||
|
$providers = $this->providerList($tenantId, $userId, $sources);
|
||||||
|
$providersRequested = $sources->identifiers();
|
||||||
|
$providersUnavailable = array_diff($providersRequested, array_keys($providers));
|
||||||
|
// initialize response with unavailable providers
|
||||||
$responseData = array_fill_keys($providersUnavailable, false);
|
$responseData = array_fill_keys($providersUnavailable, false);
|
||||||
|
|
||||||
// iterate through available providers
|
// iterate through available providers
|
||||||
foreach ($aggregateServices as $providerId => $services) {
|
foreach ($providers as $provider) {
|
||||||
// mark unavailable services as false
|
// extract services requested for this provider
|
||||||
$servicesRequested = $targets->byProvider($providerId)->services();
|
$serviceSelector = $sources[$provider->identifier()];
|
||||||
|
$servicesRequested = $serviceSelector instanceof ServiceSelector ? $serviceSelector->identifiers() : [];
|
||||||
|
/** @var ServiceBaseInterface[] $services */
|
||||||
|
$services = $provider->serviceList($tenantId, $userId, $servicesRequested);
|
||||||
$servicesUnavailable = array_diff($servicesRequested, array_keys($services));
|
$servicesUnavailable = array_diff($servicesRequested, array_keys($services));
|
||||||
if ($servicesUnavailable !== []) {
|
if ($servicesUnavailable !== []) {
|
||||||
$responseData[$providerId] = array_fill_keys($servicesUnavailable, false);
|
$responseData[$provider->identifier()] = array_fill_keys($servicesUnavailable, false);
|
||||||
}
|
}
|
||||||
// iterate through available services
|
// iterate through available services
|
||||||
foreach ($services as $service) {
|
foreach ($services as $service) {
|
||||||
// omit disabled services
|
// omit disabled services
|
||||||
if ($service->getEnabled() === false) {
|
if ($service->getEnabled() === false) {
|
||||||
$responseData[$providerId][$service->identifier()] = false;
|
$responseData[$provider->identifier()][$service->identifier()] = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// extract collections requested for this service
|
// extract collections requested for this service
|
||||||
$serviceTargets = $targets->byProvider($providerId)->byService($service->identifier());
|
$collectionSelector = $serviceSelector[$service->identifier()];
|
||||||
$collectionsRequested = $serviceTargets->collections();
|
$collectionsRequested = $collectionSelector instanceof CollectionSelector ? $collectionSelector->identifiers() : [];
|
||||||
if ($collectionsRequested === []) {
|
if ($collectionsRequested === []) {
|
||||||
$responseData[$providerId][$service->identifier()] = false;
|
$responseData[$provider->identifier()][$service->identifier()] = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// check delta for each requested collection
|
// check delta for each requested collection
|
||||||
foreach ($collectionsRequested as $collection) {
|
foreach ($collectionsRequested as $collection) {
|
||||||
// signature for the collection is carried in the entity slot of the identifier
|
$entitySelector = $collectionSelector[$collection] ?? null;
|
||||||
$signature = $serviceTargets->byCollection($collection)->entities()[0] ?? '';
|
$responseData[$provider->identifier()][$service->identifier()][$collection] = $service->entityDelta($collection, $entitySelector);
|
||||||
$responseData[$providerId][$service->identifier()][$collection] = $service->entityDelta($collection, $signature);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1270,28 +1302,4 @@ class Manager {
|
|||||||
return $operationOutcome;
|
return $operationOutcome;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function entitySubmit(string $tenantId, string $userId, AddressInterface|string $sender, EntityIdentifierInterface|null $source = null, MessagePropertiesMutableInterface|array|null $message = null): EntitySubmitResult {
|
|
||||||
if ($sender instanceof AddressInterface === false) {
|
|
||||||
$sender = new Address($sender);
|
|
||||||
}
|
|
||||||
|
|
||||||
$service = $this->serviceFindByAddress($tenantId, $userId, $sender->getAddress());
|
|
||||||
if ($service === null || $service->getEnabled() === false) {
|
|
||||||
throw new InvalidArgumentException("Service not found for sender '{$sender->getAddress()}' or service is disabled");
|
|
||||||
}
|
|
||||||
if ($service instanceof ServiceEntitySubmitInterface === false) {
|
|
||||||
throw new InvalidArgumentException("Service '{$service->identifier()}' does not support entity submission");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($source === null && $message === null) {
|
|
||||||
throw new InvalidArgumentException("At least one of source or message must be provided for entity submission");
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($message !== null && $message instanceof MessagePropertiesMutableInterface === false) {
|
|
||||||
$message = $service->entityFresh()->getProperties()->jsonDeserialize($message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $service->entitySubmit($sender, $source, $message);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,11 +50,6 @@ class Module extends ModuleInstanceAbstract implements ModuleBrowserInterface
|
|||||||
'description' => 'View and access the mail manager module',
|
'description' => 'View and access the mail manager module',
|
||||||
'group' => 'Mail Management'
|
'group' => 'Mail Management'
|
||||||
],
|
],
|
||||||
'mail_manager.system' => [
|
|
||||||
'label' => 'Manage System Mail',
|
|
||||||
'description' => 'Manage system mail accounts and routing rules (act in the reserved system user context)',
|
|
||||||
'group' => 'Mail Management'
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+605
-1318
File diff suppressed because it is too large
Load Diff
+2
-5
@@ -18,21 +18,18 @@
|
|||||||
"test:coverage": "vitest run --coverage --config tests/js/vitest.config.ts"
|
"test:coverage": "vitest run --coverage --config tests/js/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pinia": "^4.0.0",
|
"pinia": "^3.0.0",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^5.2.0",
|
"vue-router": "^5.0.0",
|
||||||
"vuetify": "^4.0.0"
|
"vuetify": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
"@vitest/coverage-v8": "^4.0.18",
|
"@vitest/coverage-v8": "^4.0.18",
|
||||||
"@vitest/ui": "^4.0.18",
|
"@vitest/ui": "^4.0.18",
|
||||||
"@vue/test-utils": "^2.4.10",
|
|
||||||
"@vue/tsconfig": "^0.9.0",
|
"@vue/tsconfig": "^0.9.0",
|
||||||
"jsdom": "^29.1.1",
|
|
||||||
"typescript": "~6.0.0",
|
"typescript": "~6.0.0",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"vitest": "^4.0.18",
|
|
||||||
"vue-tsc": "^3.0.5"
|
"vue-tsc": "^3.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useIntegrationStore } from '@KTXC/stores/integrationStore'
|
|||||||
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
import { useServicesStore } from '@MailManager/stores/servicesStore'
|
||||||
import { useProvidersStore } from '@MailManager/stores/providersStore'
|
import { useProvidersStore } from '@MailManager/stores/providersStore'
|
||||||
import { ServiceObject, type ProviderObject } from '@MailManager/models'
|
import { ServiceObject, type ProviderObject } from '@MailManager/models'
|
||||||
import type { ProviderDiscoveryStatus, ServiceAddressInterface, ServiceInterface, ServiceLocation } from '@MailManager/types'
|
import type { ProviderDiscoveryStatus, ServiceInterface, ServiceLocation } from '@MailManager/types'
|
||||||
import DiscoveryEntryPanel from '@MailManager/components/steps/DiscoveryEntryPanel.vue'
|
import DiscoveryEntryPanel from '@MailManager/components/steps/DiscoveryEntryPanel.vue'
|
||||||
import DiscoveryStatusPanel from '@MailManager/components/steps/DiscoveryStatusPanel.vue'
|
import DiscoveryStatusPanel from '@MailManager/components/steps/DiscoveryStatusPanel.vue'
|
||||||
import ProviderSelectionPanel from '@MailManager/components/steps/ProviderSelectionPanel.vue'
|
import ProviderSelectionPanel from '@MailManager/components/steps/ProviderSelectionPanel.vue'
|
||||||
@@ -32,7 +32,6 @@ const MANUAL_STEPS = {
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
user?: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -58,11 +57,6 @@ const discoverAddress = ref<string>('')
|
|||||||
const discoverSecret = ref<string | null>(null)
|
const discoverSecret = ref<string | null>(null)
|
||||||
const discoverHostname = ref<string | null>(null)
|
const discoverHostname = ref<string | null>(null)
|
||||||
|
|
||||||
// Address entered during discovery, as a service address object
|
|
||||||
const discoverServiceAddress = computed<ServiceAddressInterface | null>(() =>
|
|
||||||
discoverAddress.value ? { address: discoverAddress.value } : null
|
|
||||||
)
|
|
||||||
|
|
||||||
// Step 2: Discovery Status / Provider Selection
|
// Step 2: Discovery Status / Provider Selection
|
||||||
const selectedProvider = shallowRef<ProviderObject | null>(null)
|
const selectedProvider = shallowRef<ProviderObject | null>(null)
|
||||||
const selectedService = shallowRef<ServiceObject | null>(null)
|
const selectedService = shallowRef<ServiceObject | null>(null)
|
||||||
@@ -160,7 +154,7 @@ function createServiceObject(
|
|||||||
identifier: null,
|
identifier: null,
|
||||||
label: data.label ?? null,
|
label: data.label ?? null,
|
||||||
enabled: data.enabled ?? true,
|
enabled: data.enabled ?? true,
|
||||||
primaryAddress: data.primaryAddress ?? discoverServiceAddress.value,
|
primaryAddress: data.primaryAddress ?? (discoverAddress.value || null),
|
||||||
secondaryAddresses: data.secondaryAddresses ?? null,
|
secondaryAddresses: data.secondaryAddresses ?? null,
|
||||||
location: data.location ?? null,
|
location: data.location ?? null,
|
||||||
identity: data.identity ?? null,
|
identity: data.identity ?? null,
|
||||||
@@ -243,8 +237,7 @@ async function handleDiscover() {
|
|||||||
discoverSecret.value || undefined,
|
discoverSecret.value || undefined,
|
||||||
discoverHostname.value || undefined,
|
discoverHostname.value || undefined,
|
||||||
identifier,
|
identifier,
|
||||||
(service) => { discoveredService = service },
|
(service) => { discoveredService = service }
|
||||||
props.user
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Success - check if we got results for this provider
|
// Success - check if we got results for this provider
|
||||||
@@ -307,7 +300,7 @@ async function handleProviderSelect(identifier: string) {
|
|||||||
...discoveredJson,
|
...discoveredJson,
|
||||||
label: discoveredJson.label || discoverAddress.value,
|
label: discoveredJson.label || discoverAddress.value,
|
||||||
enabled: discoveredJson.enabled ?? true,
|
enabled: discoveredJson.enabled ?? true,
|
||||||
primaryAddress: discoveredJson.primaryAddress ?? discoverServiceAddress.value,
|
primaryAddress: discoveredJson.primaryAddress || discoverAddress.value,
|
||||||
location: discoveredJson.location
|
location: discoveredJson.location
|
||||||
})
|
})
|
||||||
setSelectedProviderAndService(identifier, service)
|
setSelectedProviderAndService(identifier, service)
|
||||||
@@ -324,7 +317,7 @@ function handleProviderAdvanced(identifier: string) {
|
|||||||
...discoveredJson,
|
...discoveredJson,
|
||||||
label: discoveredJson?.label || discoverAddress.value,
|
label: discoveredJson?.label || discoverAddress.value,
|
||||||
enabled: discoveredJson?.enabled ?? true,
|
enabled: discoveredJson?.enabled ?? true,
|
||||||
primaryAddress: discoveredJson?.primaryAddress ?? discoverServiceAddress.value,
|
primaryAddress: discoveredJson?.primaryAddress || discoverAddress.value,
|
||||||
location: discoveredJson?.location ?? null
|
location: discoveredJson?.location ?? null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -348,7 +341,7 @@ function handleProviderManualSelect(identifier: string) {
|
|||||||
const service = createServiceObject(identifier, {
|
const service = createServiceObject(identifier, {
|
||||||
label: discoverAddress.value,
|
label: discoverAddress.value,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
primaryAddress: discoverServiceAddress.value,
|
primaryAddress: discoverAddress.value,
|
||||||
location: null,
|
location: null,
|
||||||
identity: null
|
identity: null
|
||||||
})
|
})
|
||||||
@@ -386,8 +379,7 @@ async function testConnection() {
|
|||||||
selectedProvider.value.identifier,
|
selectedProvider.value.identifier,
|
||||||
null,
|
null,
|
||||||
selectedService.value.location,
|
selectedService.value.location,
|
||||||
selectedService.value.identity,
|
selectedService.value.identity
|
||||||
props.user
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return testResult
|
return testResult
|
||||||
@@ -404,7 +396,7 @@ async function saveAccount() {
|
|||||||
try {
|
try {
|
||||||
const accountData = {
|
const accountData = {
|
||||||
label: serviceData.label || discoverAddress.value,
|
label: serviceData.label || discoverAddress.value,
|
||||||
primaryAddress: serviceData.primaryAddress ?? discoverServiceAddress.value,
|
primaryAddress: serviceData.primaryAddress || discoverAddress.value,
|
||||||
enabled: serviceData.enabled,
|
enabled: serviceData.enabled,
|
||||||
location: serviceData.location,
|
location: serviceData.location,
|
||||||
identity: serviceData.identity,
|
identity: serviceData.identity,
|
||||||
@@ -413,8 +405,7 @@ async function saveAccount() {
|
|||||||
|
|
||||||
await servicesStore.create(
|
await servicesStore.create(
|
||||||
selectedProvider.value.identifier,
|
selectedProvider.value.identifier,
|
||||||
accountData,
|
accountData
|
||||||
props.user
|
|
||||||
)
|
)
|
||||||
|
|
||||||
emit('saved')
|
emit('saved')
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const props = defineProps<{
|
|||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
serviceProvider: string
|
serviceProvider: string
|
||||||
serviceIdentifier: string | number
|
serviceIdentifier: string | number
|
||||||
user?: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -104,10 +103,7 @@ async function load() {
|
|||||||
try {
|
try {
|
||||||
const [provider, service] = await Promise.all([
|
const [provider, service] = await Promise.all([
|
||||||
providersStore.provider(props.serviceProvider) ?? providersStore.fetch(props.serviceProvider),
|
providersStore.provider(props.serviceProvider) ?? providersStore.fetch(props.serviceProvider),
|
||||||
// acting-user context always fetches fresh, bypassing the shared cache
|
servicesStore.service(props.serviceProvider, props.serviceIdentifier) ?? servicesStore.fetch(props.serviceProvider, props.serviceIdentifier)
|
||||||
props.user
|
|
||||||
? servicesStore.fetch(props.serviceProvider, props.serviceIdentifier, props.user)
|
|
||||||
: servicesStore.service(props.serviceProvider, props.serviceIdentifier) ?? servicesStore.fetch(props.serviceProvider, props.serviceIdentifier)
|
|
||||||
])
|
])
|
||||||
|
|
||||||
localProvider.value = provider.clone()
|
localProvider.value = provider.clone()
|
||||||
@@ -165,16 +161,12 @@ async function testConnection() {
|
|||||||
localService.value.provider,
|
localService.value.provider,
|
||||||
null,
|
null,
|
||||||
localService.value.location,
|
localService.value.location,
|
||||||
localService.value.identity,
|
localService.value.identity
|
||||||
props.user
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
testResult = await servicesStore.test(
|
testResult = await servicesStore.test(
|
||||||
localService.value.provider,
|
localService.value.provider,
|
||||||
localService.value.identifier,
|
localService.value.identifier
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
props.user
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,10 +182,6 @@ async function testConnection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function saveAccount() {
|
async function saveAccount() {
|
||||||
if (!localService.value) {
|
|
||||||
console.error('[Mail Manager][Edit Account Dialog] - No service data to save')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// No changes made, just close the dialog
|
// No changes made, just close the dialog
|
||||||
if (!localService.value.mutated() && !localService.value.location?.mutated() && !localService.value.identity?.mutated()) {
|
if (!localService.value.mutated() && !localService.value.location?.mutated() && !localService.value.identity?.mutated()) {
|
||||||
close()
|
close()
|
||||||
@@ -207,8 +195,7 @@ async function saveAccount() {
|
|||||||
localService.value.provider,
|
localService.value.provider,
|
||||||
localService.value.identifier as string | number,
|
localService.value.identifier as string | number,
|
||||||
true, // delta update
|
true, // delta update
|
||||||
localService.value,
|
localService.value
|
||||||
props.user
|
|
||||||
)
|
)
|
||||||
|
|
||||||
emit('saved')
|
emit('saved')
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ watch(
|
|||||||
<v-icon>mdi-label</v-icon>
|
<v-icon>mdi-label</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Account Name</v-list-item-title>
|
<v-list-item-title>Account Name</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ localService.label || localService.primaryAddress?.address || 'New Account' }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ localService.label || localService.primaryAddress || 'New Account' }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<!-- Email Address -->
|
<!-- Email Address -->
|
||||||
@@ -143,7 +143,7 @@ watch(
|
|||||||
<v-icon>mdi-email</v-icon>
|
<v-icon>mdi-email</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>Email Address</v-list-item-title>
|
<v-list-item-title>Email Address</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ localService.primaryAddress?.address }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ localService.primaryAddress }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<!-- Provider -->
|
<!-- Provider -->
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Background mail synchronization composable
|
* Background mail synchronization composable
|
||||||
*
|
*
|
||||||
* Periodically checks for changes in mailboxes using the delta method
|
* Periodically checks for changes in mailboxes using the delta method
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -8,10 +8,12 @@ import { ref, onMounted, onUnmounted } from 'vue';
|
|||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import { useEntitiesStore } from '../stores/entitiesStore';
|
import { useEntitiesStore } from '../stores/entitiesStore';
|
||||||
import { useCollectionsStore } from '../stores/collectionsStore';
|
import { useCollectionsStore } from '../stores/collectionsStore';
|
||||||
import type { CollectionIdentifier, EntityIdentifier } from '../types/common';
|
|
||||||
|
|
||||||
/** A sync source is a collection identifier (provider:service:collection) to monitor */
|
export interface SyncSource {
|
||||||
export type SyncSource = CollectionIdentifier;
|
provider: string;
|
||||||
|
service: string | number;
|
||||||
|
collections: (string | number)[];
|
||||||
|
}
|
||||||
|
|
||||||
interface SyncOptions {
|
interface SyncOptions {
|
||||||
/** Polling interval in milliseconds (default: 30000 = 30 seconds) */
|
/** Polling interval in milliseconds (default: 30000 = 30 seconds) */
|
||||||
@@ -26,9 +28,9 @@ export interface MailSyncController {
|
|||||||
isRunning: Ref<boolean>;
|
isRunning: Ref<boolean>;
|
||||||
lastSync: Ref<Date | null>;
|
lastSync: Ref<Date | null>;
|
||||||
error: Ref<string | null>;
|
error: Ref<string | null>;
|
||||||
sources: Ref<CollectionIdentifier[]>;
|
sources: Ref<SyncSource[]>;
|
||||||
addSource: (source: CollectionIdentifier) => void;
|
addSource: (source: SyncSource) => void;
|
||||||
removeSource: (source: CollectionIdentifier) => void;
|
removeSource: (source: SyncSource) => void;
|
||||||
clearSources: () => void;
|
clearSources: () => void;
|
||||||
sync: () => Promise<void>;
|
sync: () => Promise<void>;
|
||||||
start: () => void;
|
start: () => void;
|
||||||
@@ -45,21 +47,26 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController {
|
|||||||
|
|
||||||
const entitiesStore = useEntitiesStore();
|
const entitiesStore = useEntitiesStore();
|
||||||
const collectionsStore = useCollectionsStore();
|
const collectionsStore = useCollectionsStore();
|
||||||
|
|
||||||
const isRunning = ref(false);
|
const isRunning = ref(false);
|
||||||
const lastSync = ref<Date | null>(null);
|
const lastSync = ref<Date | null>(null);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const sources = ref<CollectionIdentifier[]>([]);
|
const sources = ref<SyncSource[]>([]);
|
||||||
// Last known signature per collection identifier (updated by delta)
|
const signatures = ref<Record<string, Record<string, Record<string, string>>>>({});
|
||||||
const signatures = ref<Record<string, string>>({});
|
|
||||||
|
|
||||||
let syncInterval: ReturnType<typeof setInterval> | null = null;
|
let syncInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a source to sync (mailbox to monitor)
|
* Add a source to sync (mailbox to monitor)
|
||||||
*/
|
*/
|
||||||
function addSource(source: CollectionIdentifier) {
|
function addSource(source: SyncSource) {
|
||||||
if (!sources.value.includes(source)) {
|
const exists = sources.value.some(
|
||||||
|
s => s.provider === source.provider
|
||||||
|
&& s.service === source.service
|
||||||
|
&& JSON.stringify(s.collections) === JSON.stringify(source.collections)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
sources.value.push(source);
|
sources.value.push(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,8 +74,13 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController {
|
|||||||
/**
|
/**
|
||||||
* Remove a source from sync
|
* Remove a source from sync
|
||||||
*/
|
*/
|
||||||
function removeSource(source: CollectionIdentifier) {
|
function removeSource(source: SyncSource) {
|
||||||
const index = sources.value.indexOf(source);
|
const index = sources.value.findIndex(
|
||||||
|
s => s.provider === source.provider
|
||||||
|
&& s.service === source.service
|
||||||
|
&& JSON.stringify(s.collections) === JSON.stringify(source.collections)
|
||||||
|
);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
sources.value.splice(index, 1);
|
sources.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -92,74 +104,91 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController {
|
|||||||
try {
|
try {
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
// Build flat identifier list for the delta request, embedding the last known
|
// Build sources structure for delta request
|
||||||
// signature for each collection as the entity slot (provider:service:collection:signature)
|
const deltaSources: any = {};
|
||||||
const requestSignatures: Record<string, string> = {};
|
|
||||||
const targets: (CollectionIdentifier | EntityIdentifier)[] = [];
|
sources.value.forEach(source => {
|
||||||
|
if (!deltaSources[source.provider]) {
|
||||||
sources.value.forEach(collectionId => {
|
deltaSources[source.provider] = {};
|
||||||
// Look up signature from local tracking (updated by delta), falling back to the
|
|
||||||
// collection's own signature if it has not been synced yet
|
|
||||||
let signature = signatures.value[collectionId];
|
|
||||||
if (!signature) {
|
|
||||||
const collectionData = collectionsStore.collection(collectionId);
|
|
||||||
signature = collectionData?.signature || '';
|
|
||||||
}
|
}
|
||||||
|
if (!deltaSources[source.provider][source.service]) {
|
||||||
|
deltaSources[source.provider][source.service] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add collections to check with their signatures
|
||||||
|
source.collections.forEach(collection => {
|
||||||
|
// Look up signature from local tracking (updated by delta)
|
||||||
|
let signature = signatures.value[source.provider]?.[String(source.service)]?.[String(collection)];
|
||||||
|
|
||||||
|
// Fallback to collection signature if not yet synced
|
||||||
|
if (!signature) {
|
||||||
|
const collectionData = collectionsStore.collection(source.provider, source.service, collection);
|
||||||
|
signature = collectionData?.signature || '';
|
||||||
|
}
|
||||||
|
|
||||||
requestSignatures[collectionId] = signature;
|
console.log(`[Sync] Collection ${source.provider}/${source.service}/${collection} signature: "${signature}"`);
|
||||||
targets.push(signature ? `${collectionId}:${signature}` as EntityIdentifier : collectionId);
|
|
||||||
|
// Map collection identifier to signature string
|
||||||
|
deltaSources[source.provider][source.service][collection] = signature || '';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get delta changes
|
// Get delta changes
|
||||||
const deltaResponse = await entitiesStore.delta(targets);
|
const deltaResponse = await entitiesStore.delta(deltaSources);
|
||||||
|
|
||||||
// If fetchDetails is enabled, fetch full entity data for additions and modifications
|
// If fetchDetails is enabled, fetch full entity data for additions and modifications
|
||||||
if (fetchDetails) {
|
if (fetchDetails) {
|
||||||
const fetchPromises: Promise<any>[] = [];
|
const fetchPromises: Promise<any>[] = [];
|
||||||
|
|
||||||
Object.entries(deltaResponse).forEach(([provider, providerData]: [string, any]) => {
|
Object.entries(deltaResponse).forEach(([provider, providerData]: [string, any]) => {
|
||||||
if (providerData === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Object.entries(providerData).forEach(([service, serviceData]: [string, any]) => {
|
Object.entries(providerData).forEach(([service, serviceData]: [string, any]) => {
|
||||||
if (serviceData === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Object.entries(serviceData).forEach(([collection, collectionData]: [string, any]) => {
|
Object.entries(serviceData).forEach(([collection, collectionData]: [string, any]) => {
|
||||||
// Skip if no changes (server returns false or a bare signature string)
|
// Skip if no changes (server returns false or string signature)
|
||||||
if (collectionData === false || typeof collectionData === 'string') {
|
if (collectionData === false || typeof collectionData === 'string') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionId = `${provider}:${service}:${collection}` as CollectionIdentifier;
|
|
||||||
|
|
||||||
// Update signature tracking
|
// Update signature tracking
|
||||||
if (collectionData.signature) {
|
if (collectionData.signature) {
|
||||||
signatures.value[collectionId] = collectionData.signature;
|
if (!signatures.value[provider]) {
|
||||||
|
signatures.value[provider] = {};
|
||||||
|
}
|
||||||
|
if (!signatures.value[provider][service]) {
|
||||||
|
signatures.value[provider][service] = {};
|
||||||
|
}
|
||||||
|
signatures.value[provider][service][collection] = collectionData.signature;
|
||||||
|
console.log(`[Sync] Updated signature for ${provider}/${service}/${collection}: "${collectionData.signature}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip fetching when the signature did not actually change
|
// Check if signature actually changed (if not, skip fetching)
|
||||||
const oldSignature = requestSignatures[collectionId];
|
const oldSignature = deltaSources[provider]?.[service]?.[collection];
|
||||||
const newSignature = collectionData.signature;
|
const newSignature = collectionData.signature;
|
||||||
|
|
||||||
if (oldSignature && newSignature && oldSignature === newSignature) {
|
if (oldSignature && newSignature && oldSignature === newSignature) {
|
||||||
|
// Signature unchanged - server bug returning additions anyway, skip fetch
|
||||||
|
console.log(`[Sync] Skipping fetch for ${provider}/${service}/${collection} - signature unchanged (${newSignature})`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const changedIds = [
|
const identifiersToFetch = [
|
||||||
...(collectionData.additions || []),
|
...(collectionData.additions || []),
|
||||||
...(collectionData.modifications || []),
|
...(collectionData.modifications || []),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (changedIds.length > 0) {
|
if (identifiersToFetch.length > 0) {
|
||||||
const entityTargets = changedIds.map(
|
console.log(`[Sync] Fetching ${identifiersToFetch.length} entities for ${provider}/${service}/${collection}`);
|
||||||
(id: string | number) => `${collectionId}:${id}` as EntityIdentifier
|
fetchPromises.push(
|
||||||
|
entitiesStore.fetch(
|
||||||
|
provider,
|
||||||
|
service,
|
||||||
|
collection,
|
||||||
|
identifiersToFetch
|
||||||
|
)
|
||||||
);
|
);
|
||||||
fetchPromises.push(entitiesStore.fetch(entityTargets));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch all in parallel
|
// Fetch all in parallel
|
||||||
await Promise.allSettled(fetchPromises);
|
await Promise.allSettled(fetchPromises);
|
||||||
}
|
}
|
||||||
@@ -180,10 +209,10 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isRunning.value = true;
|
isRunning.value = true;
|
||||||
|
|
||||||
// Do initial sync
|
// Do initial sync
|
||||||
sync();
|
sync();
|
||||||
|
|
||||||
// Set up periodic sync
|
// Set up periodic sync
|
||||||
syncInterval = setInterval(() => {
|
syncInterval = setInterval(() => {
|
||||||
sync();
|
sync();
|
||||||
@@ -199,7 +228,7 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isRunning.value = false;
|
isRunning.value = false;
|
||||||
|
|
||||||
if (syncInterval) {
|
if (syncInterval) {
|
||||||
clearInterval(syncInterval);
|
clearInterval(syncInterval);
|
||||||
syncInterval = null;
|
syncInterval = null;
|
||||||
@@ -231,7 +260,7 @@ export function useMailSync(options: SyncOptions = {}): MailSyncController {
|
|||||||
lastSync,
|
lastSync,
|
||||||
error,
|
error,
|
||||||
sources,
|
sources,
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
addSource,
|
addSource,
|
||||||
removeSource,
|
removeSource,
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* Address implementation class for Mail Manager services
|
|
||||||
*
|
|
||||||
* Plain value object (no mutation tracking). ServiceObject getters return
|
|
||||||
* fresh instances built from raw data; mutating a getter result does not
|
|
||||||
* write back — assign through the setter to persist changes.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ServiceAddressInterface } from '@/types/service';
|
|
||||||
|
|
||||||
export class ServiceAddressObject implements ServiceAddressInterface {
|
|
||||||
|
|
||||||
_address: string;
|
|
||||||
_label: string | null;
|
|
||||||
|
|
||||||
constructor(address: string = '', label: string | null = null) {
|
|
||||||
this._address = address;
|
|
||||||
this._label = label;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJson(data: ServiceAddressInterface): ServiceAddressObject {
|
|
||||||
return new ServiceAddressObject(data.address, data.label ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
toJson(): ServiceAddressInterface {
|
|
||||||
const label = this._label?.trim() ?? '';
|
|
||||||
return {
|
|
||||||
address: this._address.trim(),
|
|
||||||
label: label.length > 0 ? label : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): ServiceAddressInterface {
|
|
||||||
return this.toJson();
|
|
||||||
}
|
|
||||||
|
|
||||||
clone(): ServiceAddressObject {
|
|
||||||
return new ServiceAddressObject(this._address, this._label);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Case-insensitive address comparison, ignoring labels */
|
|
||||||
matches(address: string): boolean {
|
|
||||||
return this._address.trim().toLowerCase() === address.trim().toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
equals(other: ServiceAddressObject | null | undefined): boolean {
|
|
||||||
if (!other) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const current = this.toJson();
|
|
||||||
const next = other.toJson();
|
|
||||||
return current.address === next.address && (current.label ?? null) === (next.label ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Display form: "Label <address>" or bare address */
|
|
||||||
format(): string {
|
|
||||||
const { address, label } = this.toJson();
|
|
||||||
return label ? `${label} <${address}>` : address;
|
|
||||||
}
|
|
||||||
|
|
||||||
get empty(): boolean {
|
|
||||||
return this._address.trim().length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Properties (raw values for editing; normalization happens in toJson) */
|
|
||||||
|
|
||||||
get address(): string {
|
|
||||||
return this._address;
|
|
||||||
}
|
|
||||||
|
|
||||||
set address(value: string) {
|
|
||||||
this._address = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
get label(): string | null {
|
|
||||||
return this._label;
|
|
||||||
}
|
|
||||||
|
|
||||||
set label(value: string | null) {
|
|
||||||
this._label = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -2,17 +2,9 @@
|
|||||||
* Class model for Collection Interface
|
* Class model for Collection Interface
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {
|
import type { CollectionInterface, CollectionModelInterface, CollectionPropertiesInterface, CollectionPropertiesModelInterface } from "@/types/collection";
|
||||||
CollectionInterface,
|
|
||||||
CollectionModelInterface,
|
|
||||||
CollectionPropertiesInterface,
|
|
||||||
CollectionPropertiesModelInterface
|
|
||||||
} from "@/types/collection";
|
|
||||||
import { clonePlain } from './clone-plain';
|
import { clonePlain } from './clone-plain';
|
||||||
import type {
|
import type { CollectionIdentifier, ServiceIdentifier } from "@/services";
|
||||||
CollectionIdentifier,
|
|
||||||
ServiceIdentifier
|
|
||||||
} from "@/services";
|
|
||||||
|
|
||||||
export class CollectionObject implements CollectionModelInterface {
|
export class CollectionObject implements CollectionModelInterface {
|
||||||
|
|
||||||
@@ -24,9 +16,9 @@ export class CollectionObject implements CollectionModelInterface {
|
|||||||
'@type': 'mail:collection',
|
'@type': 'mail:collection',
|
||||||
version: 1,
|
version: 1,
|
||||||
provider: '',
|
provider: '',
|
||||||
service: '' as ServiceIdentifier,
|
service: '',
|
||||||
collection: null as CollectionIdentifier | null,
|
collection: null,
|
||||||
identifier: '' as CollectionIdentifier,
|
identifier: '',
|
||||||
properties: {'@type': 'mail:folder', label: ''},
|
properties: {'@type': 'mail:folder', label: ''},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export class EntityObject implements EntityModelInterface {
|
|||||||
version: 1,
|
version: 1,
|
||||||
provider: '',
|
provider: '',
|
||||||
service: '',
|
service: '',
|
||||||
collection: '' as CollectionIdentifier,
|
collection: null,
|
||||||
identifier: '' as EntityIdentifier,
|
identifier: null,
|
||||||
signature: null,
|
signature: null,
|
||||||
created: null,
|
created: null,
|
||||||
modified: null,
|
modified: null,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export { ProviderObject } from './provider';
|
export { ProviderObject } from './provider';
|
||||||
export { ServiceObject } from './service';
|
export { ServiceObject } from './service';
|
||||||
export { ServiceAddressObject } from './address';
|
|
||||||
export {
|
export {
|
||||||
CollectionObject,
|
CollectionObject,
|
||||||
CollectionPropertiesObject
|
CollectionPropertiesObject
|
||||||
@@ -8,7 +7,6 @@ export {
|
|||||||
export { EntityObject } from './entity';
|
export { EntityObject } from './entity';
|
||||||
export {
|
export {
|
||||||
MessageObject,
|
MessageObject,
|
||||||
MessageAddressObject,
|
|
||||||
MessagePartObject
|
MessagePartObject
|
||||||
} from './message';
|
} from './message';
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -119,19 +119,19 @@ export class MessageObject implements MessageModelInterface {
|
|||||||
return this._data.attachments ? this._data.attachments.map(att => new MessagePartObject(att)) : [];
|
return this._data.attachments ? this._data.attachments.map(att => new MessagePartObject(att)) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get flags(): { seen?: boolean; flagged?: boolean; answered?: boolean; draft?: boolean } | {} {
|
get flags(): { read?: boolean; flagged?: boolean; answered?: boolean; draft?: boolean } | {} {
|
||||||
return clonePlain(this._data.flags ?? {});
|
return clonePlain(this._data.flags ?? {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// this should be moved to a mutable object, but for now we can allow it here for convenience
|
// this should be moved to a mutable object, but for now we can allow it here for convenience
|
||||||
set flags(value: { seen?: boolean; flagged?: boolean; answered?: boolean; draft?: boolean }) {
|
set flags(value: { read?: boolean; flagged?: boolean; answered?: boolean; draft?: boolean }) {
|
||||||
this._data.flags = clonePlain(value);
|
this._data.flags = clonePlain(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper methods */
|
/** Helper methods */
|
||||||
|
|
||||||
get isRead(): boolean {
|
get isRead(): boolean {
|
||||||
return this._data.flags?.seen ?? false;
|
return this._data.flags?.read ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFlagged(): boolean {
|
get isFlagged(): boolean {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import type {
|
|||||||
ServiceLocation,
|
ServiceLocation,
|
||||||
ServiceModelInterface
|
ServiceModelInterface
|
||||||
} from "@/types/service";
|
} from "@/types/service";
|
||||||
import { ServiceAddressObject } from './address';
|
|
||||||
import { Identity } from './identity';
|
import { Identity } from './identity';
|
||||||
import { Location } from './location';
|
import { Location } from './location';
|
||||||
import { MutationProxy } from './mutation-proxy';
|
import { MutationProxy } from './mutation-proxy';
|
||||||
@@ -119,20 +118,20 @@ export class ServiceObject implements ServiceModelInterface {
|
|||||||
return this._data.capabilities ?? {};
|
return this._data.capabilities ?? {};
|
||||||
}
|
}
|
||||||
|
|
||||||
get primaryAddress(): ServiceAddressObject | null {
|
get primaryAddress(): string | null {
|
||||||
return this._data.primaryAddress ? ServiceAddressObject.fromJson(this._data.primaryAddress) : null;
|
return this._data.primaryAddress ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
set primaryAddress(value: ServiceAddressObject | null) {
|
set primaryAddress(value: string | null) {
|
||||||
this._data.primaryAddress = value ? value.toJson() : null;
|
this._data.primaryAddress = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
get secondaryAddresses(): ServiceAddressObject[] {
|
get secondaryAddresses(): string[] {
|
||||||
return (this._data.secondaryAddresses ?? []).map(entry => ServiceAddressObject.fromJson(entry));
|
return this._data.secondaryAddresses ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
set secondaryAddresses(value: ServiceAddressObject[] | null) {
|
set secondaryAddresses(value: string[] | null) {
|
||||||
this._data.secondaryAddresses = value ? value.map(entry => entry.toJson()) : null;
|
this._data.secondaryAddresses = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Mutable Properties */
|
/** Mutable Properties */
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ async function handleAccountSaved() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="text-h6">{{ service.label }}</h3>
|
<h3 class="text-h6">{{ service.label }}</h3>
|
||||||
<p class="text-caption text-medium-emphasis">
|
<p class="text-caption text-medium-emphasis">
|
||||||
{{ service.primaryAddress?.address || (service.identity?.type === 'BA' ? service.identity.identity : 'No email configured') }}
|
{{ service.primaryAddress || (service.identity?.type === 'BA' ? service.identity.identity : 'No email configured') }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
+1
-1
@@ -2,7 +2,7 @@ const routes = [
|
|||||||
{
|
{
|
||||||
name: 'mail-accounts',
|
name: 'mail-accounts',
|
||||||
path: '/accounts',
|
path: '/accounts',
|
||||||
component: () => import('@/pages/Main.vue'),
|
component: () => import('@/pages/AccountsPage.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
title: 'Mail Accounts',
|
title: 'Mail Accounts',
|
||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ import type {
|
|||||||
EntityPatchResponse,
|
EntityPatchResponse,
|
||||||
EntityPatchRequest,
|
EntityPatchRequest,
|
||||||
EntityDownloadRequest,
|
EntityDownloadRequest,
|
||||||
EntityBlobsRequest,
|
|
||||||
EntityBlobsResponse,
|
|
||||||
EntityBlobsWireResponse,
|
|
||||||
} from '../types/entity';
|
} from '../types/entity';
|
||||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||||
import { EntityObject } from '../models';
|
import { EntityObject } from '../models';
|
||||||
@@ -230,36 +227,6 @@ export const entityService = {
|
|||||||
download(request: EntityDownloadRequest): { transaction: string } {
|
download(request: EntityDownloadRequest): { transaction: string } {
|
||||||
return transceiveDownload<EntityDownloadRequest>('entity.download', request);
|
return transceiveDownload<EntityDownloadRequest>('entity.download', request);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch one or more message parts (attachments) inline for rendering.
|
|
||||||
*
|
|
||||||
* Returns JSON with base64-encoded bytes; each result is decoded into a Blob
|
|
||||||
* so callers can create object URLs for preview.
|
|
||||||
*/
|
|
||||||
async blobs(request: EntityBlobsRequest): Promise<EntityBlobsResponse> {
|
|
||||||
const wire = await transceivePost<EntityBlobsRequest, EntityBlobsWireResponse>(
|
|
||||||
'entity.blobs',
|
|
||||||
request,
|
|
||||||
);
|
|
||||||
return wire.map((result) => ({
|
|
||||||
source: result.source,
|
|
||||||
part: result.part,
|
|
||||||
mime: result.mime,
|
|
||||||
filename: result.filename,
|
|
||||||
blob: base64ToBlob(result.bytes, result.mime),
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Decode base64 content into a Blob of the given MIME type. */
|
|
||||||
function base64ToBlob(base64: string, mime: string): Blob {
|
|
||||||
const binary = atob(base64);
|
|
||||||
const bytes = new Uint8Array(binary.length);
|
|
||||||
for (let i = 0; i < binary.length; i++) {
|
|
||||||
bytes[i] = binary.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return new Blob([bytes], { type: mime });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default entityService;
|
export default entityService;
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ function createServiceObject(data: ServiceInterface): ServiceObject {
|
|||||||
export const serviceService = {
|
export const serviceService = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve list of services, optionally filtered by provider/service identifiers
|
* Retrieve list of services, optionally filtered by source selector
|
||||||
*
|
*
|
||||||
* @param request - list request parameters
|
* @param request - list request parameters
|
||||||
*
|
*
|
||||||
* @returns Promise with service object list grouped by provider and keyed by service identifier
|
* @returns Promise with service object list grouped by provider and keyed by service identifier
|
||||||
*/
|
*/
|
||||||
async list(request: ServiceListRequest = {}, user?: string): Promise<Record<string, Record<string, ServiceObject>>> {
|
async list(request: ServiceListRequest = {}): Promise<Record<string, Record<string, ServiceObject>>> {
|
||||||
const response = await transceivePost<ServiceListRequest, ServiceListResponse>('service.list', request, user);
|
const response = await transceivePost<ServiceListRequest, ServiceListResponse>('service.list', request);
|
||||||
|
|
||||||
// Convert nested response to ServiceObject instances
|
// Convert nested response to ServiceObject instances
|
||||||
const providerList: Record<string, Record<string, ServiceObject>> = {};
|
const providerList: Record<string, Record<string, ServiceObject>> = {};
|
||||||
@@ -70,20 +70,20 @@ export const serviceService = {
|
|||||||
*
|
*
|
||||||
* @returns Promise with service object
|
* @returns Promise with service object
|
||||||
*/
|
*/
|
||||||
async fetch(request: ServiceFetchRequest, user?: string): Promise<ServiceObject> {
|
async fetch(request: ServiceFetchRequest): Promise<ServiceObject> {
|
||||||
const response = await transceivePost<ServiceFetchRequest, ServiceFetchResponse>('service.fetch', request, user);
|
const response = await transceivePost<ServiceFetchRequest, ServiceFetchResponse>('service.fetch', request);
|
||||||
return createServiceObject(response);
|
return createServiceObject(response);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve service availability status for the given service identifiers
|
* Retrieve service availability status for a given source selector
|
||||||
*
|
*
|
||||||
* @param request - extant request parameters
|
* @param request - extant request parameters
|
||||||
*
|
*
|
||||||
* @returns Promise with service availability status
|
* @returns Promise with service availability status
|
||||||
*/
|
*/
|
||||||
async extant(request: ServiceExtantRequest, user?: string): Promise<ServiceExtantResponse> {
|
async extant(request: ServiceExtantRequest): Promise<ServiceExtantResponse> {
|
||||||
return await transceivePost<ServiceExtantRequest, ServiceExtantResponse>('service.extant', request, user);
|
return await transceivePost<ServiceExtantRequest, ServiceExtantResponse>('service.extant', request);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,8 +96,7 @@ export const serviceService = {
|
|||||||
*/
|
*/
|
||||||
async discover(
|
async discover(
|
||||||
request: ServiceDiscoverRequest,
|
request: ServiceDiscoverRequest,
|
||||||
onService: (service: ServiceObject) => void,
|
onService: (service: ServiceObject) => void
|
||||||
user?: string
|
|
||||||
): Promise<{ total: number }> {
|
): Promise<{ total: number }> {
|
||||||
return await transceiveStream<ServiceDiscoverRequest, ServiceDiscoverResponse>(
|
return await transceiveStream<ServiceDiscoverRequest, ServiceDiscoverResponse>(
|
||||||
'service.discover',
|
'service.discover',
|
||||||
@@ -108,12 +107,11 @@ export const serviceService = {
|
|||||||
provider: service.provider,
|
provider: service.provider,
|
||||||
identifier: null,
|
identifier: null,
|
||||||
label: null,
|
label: null,
|
||||||
enabled: true,
|
enabled: false,
|
||||||
location: service.location,
|
location: service.location,
|
||||||
};
|
};
|
||||||
onService(createServiceObject(serviceData));
|
onService(createServiceObject(serviceData));
|
||||||
},
|
}
|
||||||
user
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -123,8 +121,8 @@ export const serviceService = {
|
|||||||
* @param request - Service test request
|
* @param request - Service test request
|
||||||
* @returns Promise with test results
|
* @returns Promise with test results
|
||||||
*/
|
*/
|
||||||
async test(request: ServiceTestRequest, user?: string): Promise<ServiceTestResponse> {
|
async test(request: ServiceTestRequest): Promise<ServiceTestResponse> {
|
||||||
return await transceivePost<ServiceTestRequest, ServiceTestResponse>('service.test', request, user);
|
return await transceivePost<ServiceTestRequest, ServiceTestResponse>('service.test', request);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,8 +132,8 @@ export const serviceService = {
|
|||||||
*
|
*
|
||||||
* @returns Promise with created service object
|
* @returns Promise with created service object
|
||||||
*/
|
*/
|
||||||
async create(request: ServiceCreateRequest, user?: string): Promise<ServiceObject> {
|
async create(request: ServiceCreateRequest): Promise<ServiceObject> {
|
||||||
const response = await transceivePost<ServiceCreateRequest, ServiceCreateResponse>('service.create', request, user);
|
const response = await transceivePost<ServiceCreateRequest, ServiceCreateResponse>('service.create', request);
|
||||||
return createServiceObject(response);
|
return createServiceObject(response);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -146,8 +144,8 @@ export const serviceService = {
|
|||||||
*
|
*
|
||||||
* @returns Promise with updated service object
|
* @returns Promise with updated service object
|
||||||
*/
|
*/
|
||||||
async update(request: ServiceUpdateRequest, user?: string): Promise<ServiceObject> {
|
async update(request: ServiceUpdateRequest): Promise<ServiceObject> {
|
||||||
const response = await transceivePost<ServiceUpdateRequest, ServiceUpdateResponse>('service.update', request, user);
|
const response = await transceivePost<ServiceUpdateRequest, ServiceUpdateResponse>('service.update', request);
|
||||||
return createServiceObject(response);
|
return createServiceObject(response);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -158,8 +156,8 @@ export const serviceService = {
|
|||||||
*
|
*
|
||||||
* @returns Promise with deletion result
|
* @returns Promise with deletion result
|
||||||
*/
|
*/
|
||||||
async delete(request: { provider: string; identifier: string | number }, user?: string): Promise<any> {
|
async delete(request: { provider: string; identifier: string | number }): Promise<any> {
|
||||||
return await transceivePost<ServiceDeleteRequest, ServiceDeleteResponse>('service.delete', request, user);
|
return await transceivePost<ServiceDeleteRequest, ServiceDeleteResponse>('service.delete', request);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+24
-35
@@ -8,7 +8,6 @@ import { entityService } from '../services'
|
|||||||
import { EntityObject, MessageObject } from '../models'
|
import { EntityObject, MessageObject } from '../models'
|
||||||
import type {
|
import type {
|
||||||
EntityBlobSelector,
|
EntityBlobSelector,
|
||||||
EntityBlobsResponse,
|
|
||||||
EntityDownloadRequest,
|
EntityDownloadRequest,
|
||||||
EntityTransmitRequest,
|
EntityTransmitRequest,
|
||||||
EntityTransmitResponse,
|
EntityTransmitResponse,
|
||||||
@@ -44,10 +43,13 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specific entity from store, with optional retrieval
|
* Get a specific entity from store, with optional retrieval
|
||||||
*
|
*
|
||||||
* @param target - entity identifier
|
* @param provider - provider identifier
|
||||||
|
* @param service - service identifier
|
||||||
|
* @param collection - collection identifier
|
||||||
|
* @param identifier - entity identifier
|
||||||
* @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only
|
* @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only
|
||||||
*
|
*
|
||||||
* @returns Entity object or null
|
* @returns Entity object or null
|
||||||
*/
|
*/
|
||||||
function entity(target: EntityIdentifier, retrieve: boolean = false): EntityObject | null {
|
function entity(target: EntityIdentifier, retrieve: boolean = false): EntityObject | null {
|
||||||
@@ -61,10 +63,12 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all entities for a specific collection
|
* Get all entities for a specific collection
|
||||||
*
|
*
|
||||||
* @param target - collection identifier
|
* @param provider - provider identifier
|
||||||
|
* @param service - service identifier
|
||||||
|
* @param collection - collection identifier
|
||||||
* @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only
|
* @param retrieve - Retrieve behavior: true = fetch if missing or refresh, false = cache only
|
||||||
*
|
*
|
||||||
* @returns Array of entity objects
|
* @returns Array of entity objects
|
||||||
*/
|
*/
|
||||||
function entitiesForCollection(target: CollectionIdentifier, retrieve: boolean = false): EntityObject[] {
|
function entitiesForCollection(target: CollectionIdentifier, retrieve: boolean = false): EntityObject[] {
|
||||||
@@ -112,11 +116,14 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve specific entities by their identifiers
|
* Retrieve specific entities by provider, service, collection, and identifiers
|
||||||
*
|
*
|
||||||
* @param targets - array of entity identifiers to fetch
|
* @param provider - provider identifier
|
||||||
*
|
* @param service - service identifier
|
||||||
|
* @param collection - collection identifier
|
||||||
|
* @param identifiers - array of entity identifiers to fetch
|
||||||
|
*
|
||||||
* @returns Promise with entity objects keyed by identifier
|
* @returns Promise with entity objects keyed by identifier
|
||||||
*/
|
*/
|
||||||
async function fetch(targets: EntityIdentifier[]): Promise<Record<string, EntityObject>> {
|
async function fetch(targets: EntityIdentifier[]): Promise<Record<string, EntityObject>> {
|
||||||
@@ -165,19 +172,17 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
/**
|
/**
|
||||||
* Retrieve delta changes for entities
|
* Retrieve delta changes for entities
|
||||||
*
|
*
|
||||||
* @param targets - collection identifiers (provider:service:collection), optionally
|
* @param sources - source selector for delta check
|
||||||
* suffixed with a known signature (provider:service:collection:signature)
|
*
|
||||||
* to request a delta relative to that signature
|
|
||||||
*
|
|
||||||
* @returns Promise with delta changes (additions, modifications, deletions)
|
* @returns Promise with delta changes (additions, modifications, deletions)
|
||||||
*
|
*
|
||||||
* Note: Delta returns only identifiers, not full entities.
|
* Note: Delta returns only identifiers, not full entities.
|
||||||
* Caller should fetch full entities for additions/modifications separately.
|
* Caller should fetch full entities for additions/modifications separately.
|
||||||
*/
|
*/
|
||||||
async function delta(targets: (CollectionIdentifier | EntityIdentifier)[]) {
|
async function delta(sources: CollectionIdentifier[]) {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await entityService.delta({ targets })
|
const response = await entityService.delta({ sources })
|
||||||
|
|
||||||
// Process delta and update store
|
// Process delta and update store
|
||||||
Object.entries(response).forEach(([, providerData]) => {
|
Object.entries(response).forEach(([, providerData]) => {
|
||||||
@@ -503,21 +508,6 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch one or more message parts (attachments) inline as Blobs, for preview.
|
|
||||||
*/
|
|
||||||
async function blobs(
|
|
||||||
target: EntityIdentifier,
|
|
||||||
parts: EntityBlobSelector[],
|
|
||||||
): Promise<EntityBlobsResponse> {
|
|
||||||
try {
|
|
||||||
return await entityService.blobs({ target, parts })
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('[Mail Manager][Store] - Failed to fetch attachment blobs:', error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return public API
|
// Return public API
|
||||||
return {
|
return {
|
||||||
// State (readonly)
|
// State (readonly)
|
||||||
@@ -540,6 +530,5 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
|||||||
move,
|
move,
|
||||||
transmit,
|
transmit,
|
||||||
download,
|
download,
|
||||||
blobs,
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { ref, computed, readonly } from 'vue'
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { providerService } from '../services'
|
import { providerService } from '../services'
|
||||||
import { ProviderObject } from '../models/provider'
|
import { ProviderObject } from '../models/provider'
|
||||||
import type { ProviderIdentifier } from '../types'
|
import type { SourceSelector } from '../types'
|
||||||
|
|
||||||
export const useProvidersStore = defineStore('mailProvidersStore', () => {
|
export const useProvidersStore = defineStore('mailProvidersStore', () => {
|
||||||
// State
|
// State
|
||||||
@@ -50,14 +50,14 @@ export const useProvidersStore = defineStore('mailProvidersStore', () => {
|
|||||||
/**
|
/**
|
||||||
* Retrieve all or specific providers, optionally filtered by source selector
|
* Retrieve all or specific providers, optionally filtered by source selector
|
||||||
*
|
*
|
||||||
* @param targets - list request parameters
|
* @param request - list request parameters
|
||||||
*
|
*
|
||||||
* @returns Promise with provider object list keyed by provider identifier
|
* @returns Promise with provider object list keyed by provider identifier
|
||||||
*/
|
*/
|
||||||
async function list(targets?: ProviderIdentifier[]): Promise<Record<string, ProviderObject>> {
|
async function list(sources?: SourceSelector): Promise<Record<string, ProviderObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const providers = await providerService.list({ targets })
|
const providers = await providerService.list({ sources })
|
||||||
|
|
||||||
// Merge retrieved providers into state
|
// Merge retrieved providers into state
|
||||||
_providers.value = { ..._providers.value, ...providers }
|
_providers.value = { ..._providers.value, ...providers }
|
||||||
@@ -75,14 +75,14 @@ export const useProvidersStore = defineStore('mailProvidersStore', () => {
|
|||||||
/**
|
/**
|
||||||
* Retrieve a specific provider by identifier
|
* Retrieve a specific provider by identifier
|
||||||
*
|
*
|
||||||
* @param target - fetch target identifier
|
* @param identifier - provider identifier
|
||||||
*
|
*
|
||||||
* @returns Promise with provider object
|
* @returns Promise with provider object
|
||||||
*/
|
*/
|
||||||
async function fetch(target: string): Promise<ProviderObject> {
|
async function fetch(identifier: string): Promise<ProviderObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const provider = await providerService.fetch({ target })
|
const provider = await providerService.fetch({ identifier })
|
||||||
|
|
||||||
// Merge fetched provider into state
|
// Merge fetched provider into state
|
||||||
_providers.value[provider.identifier] = provider
|
_providers.value[provider.identifier] = provider
|
||||||
@@ -100,14 +100,14 @@ export const useProvidersStore = defineStore('mailProvidersStore', () => {
|
|||||||
/**
|
/**
|
||||||
* Retrieve provider availability status for a given source selector
|
* Retrieve provider availability status for a given source selector
|
||||||
*
|
*
|
||||||
* @param targets - list of provider identifiers to check availability for
|
* @param sources - source selector to check availability for
|
||||||
*
|
*
|
||||||
* @returns Promise with provider availability status
|
* @returns Promise with provider availability status
|
||||||
*/
|
*/
|
||||||
async function extant(targets: ProviderIdentifier[]) {
|
async function extant(sources: SourceSelector) {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await providerService.extant({ targets })
|
const response = await providerService.extant({ sources })
|
||||||
|
|
||||||
Object.entries(response).forEach(([providerId, providerStatus]) => {
|
Object.entries(response).forEach(([providerId, providerStatus]) => {
|
||||||
if (providerStatus === false) {
|
if (providerStatus === false) {
|
||||||
@@ -115,7 +115,7 @@ export const useProvidersStore = defineStore('mailProvidersStore', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully checked', targets ? targets.length : 0, 'providers')
|
console.debug('[Mail Manager][Store] - Successfully checked', sources ? Object.keys(sources).length : 0, 'providers')
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Mail Manager][Store] - Failed to check providers:', error)
|
console.error('[Mail Manager][Store] - Failed to check providers:', error)
|
||||||
|
|||||||
+35
-51
@@ -7,9 +7,9 @@ import { defineStore } from 'pinia'
|
|||||||
import { serviceService } from '../services'
|
import { serviceService } from '../services'
|
||||||
import { ServiceObject } from '../models/service'
|
import { ServiceObject } from '../models/service'
|
||||||
import type {
|
import type {
|
||||||
CollectionIdentifier,
|
|
||||||
ServiceIdentifier,
|
ServiceIdentifier,
|
||||||
ServiceLocation,
|
ServiceLocation,
|
||||||
|
SourceSelector,
|
||||||
ServiceIdentity,
|
ServiceIdentity,
|
||||||
ServiceInterface,
|
ServiceInterface,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
@@ -101,10 +101,7 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
* @returns Service object or null
|
* @returns Service object or null
|
||||||
*/
|
*/
|
||||||
function serviceForAddress(address: string, retrieve: boolean = false): ServiceObject | null {
|
function serviceForAddress(address: string, retrieve: boolean = false): ServiceObject | null {
|
||||||
const service = Object.values(_services.value).find(s =>
|
const service = Object.values(_services.value).find(s => s.primaryAddress === address || s.secondaryAddresses?.includes(address))
|
||||||
s.primaryAddress?.matches(address) ||
|
|
||||||
s.secondaryAddresses.some(candidate => candidate.matches(address)),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (retrieve === true && !service) {
|
if (retrieve === true && !service) {
|
||||||
console.debug(`[Mail Manager][Store] - No service found for address "${address}", discovery may be needed`)
|
console.debug(`[Mail Manager][Store] - No service found for address "${address}", discovery may be needed`)
|
||||||
@@ -125,16 +122,16 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all or specific services, optionally filtered by provider/service identifiers
|
* Retrieve all or specific services, optionally filtered by source selector
|
||||||
*
|
*
|
||||||
* @param targets - optional array of provider:service (or provider:service:collection) identifiers
|
* @param sources - optional source selector
|
||||||
*
|
*
|
||||||
* @returns Promise with service object list keyed by provider and service identifier
|
* @returns Promise with service object list keyed by provider and service identifier
|
||||||
*/
|
*/
|
||||||
async function list(targets?: ServiceIdentifier[] | CollectionIdentifier[], user?: string): Promise<Record<string, ServiceObject>> {
|
async function list(sources?: SourceSelector): Promise<Record<string, ServiceObject>> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await serviceService.list({ targets }, user)
|
const response = await serviceService.list({ sources })
|
||||||
|
|
||||||
// Flatten nested structure: provider-id: { service-id: object } -> "provider-id:service-id": object
|
// Flatten nested structure: provider-id: { service-id: object } -> "provider-id:service-id": object
|
||||||
const services: Record<string, ServiceObject> = {}
|
const services: Record<string, ServiceObject> = {}
|
||||||
@@ -145,10 +142,8 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Merge retrieved services into state (acting-user context stays out of the shared cache)
|
// Merge retrieved services into state
|
||||||
if (!user) {
|
_services.value = { ..._services.value, ...services }
|
||||||
_services.value = { ..._services.value, ...services }
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully retrieved', Object.keys(services).length, 'services')
|
console.debug('[Mail Manager][Store] - Successfully retrieved', Object.keys(services).length, 'services')
|
||||||
return services
|
return services
|
||||||
@@ -168,16 +163,14 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
*
|
*
|
||||||
* @returns Promise with service object
|
* @returns Promise with service object
|
||||||
*/
|
*/
|
||||||
async function fetch(provider: string, identifier: string | number, user?: string): Promise<ServiceObject> {
|
async function fetch(provider: string, identifier: string | number): Promise<ServiceObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const service = await serviceService.fetch({ provider, identifier }, user)
|
const service = await serviceService.fetch({ provider, identifier })
|
||||||
|
|
||||||
// Merge fetched service into state (acting-user context stays out of the shared cache)
|
// Merge fetched service into state
|
||||||
const key = identifierKey(service.provider, service.identifier)
|
const key = identifierKey(service.provider, service.identifier)
|
||||||
if (!user) {
|
_services.value[key] = service
|
||||||
_services.value[key] = service
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully fetched service:', key)
|
console.debug('[Mail Manager][Store] - Successfully fetched service:', key)
|
||||||
return service
|
return service
|
||||||
@@ -190,18 +183,18 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve service availability status for the given service identifiers
|
* Retrieve service availability status for a given source selector
|
||||||
*
|
*
|
||||||
* @param targets - array of provider:service identifiers to check availability for
|
* @param sources - source selector to check availability for
|
||||||
*
|
*
|
||||||
* @returns Promise with service availability status
|
* @returns Promise with service availability status
|
||||||
*/
|
*/
|
||||||
async function extant(targets: ServiceIdentifier[], user?: string) {
|
async function extant(sources: SourceSelector) {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const response = await serviceService.extant({ targets }, user)
|
const response = await serviceService.extant({ sources })
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully checked', targets?.length ?? 0, 'services')
|
console.debug('[Mail Manager][Store] - Successfully checked', sources ? Object.keys(sources).length : 0, 'services')
|
||||||
return response
|
return response
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[Mail Manager][Store] - Failed to check services:', error)
|
console.error('[Mail Manager][Store] - Failed to check services:', error)
|
||||||
@@ -219,16 +212,14 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
*
|
*
|
||||||
* @returns Promise with created service object
|
* @returns Promise with created service object
|
||||||
*/
|
*/
|
||||||
async function create(provider: string, data: Partial<ServiceInterface>, user?: string): Promise<ServiceObject> {
|
async function create(provider: string, data: Partial<ServiceInterface>): Promise<ServiceObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
const service = await serviceService.create({ provider, data }, user)
|
const service = await serviceService.create({ provider, data })
|
||||||
|
|
||||||
// Merge created service into state (acting-user context stays out of the shared cache)
|
// Merge created service into state
|
||||||
const key = identifierKey(service.provider, service.identifier)
|
const key = identifierKey(service.provider, service.identifier)
|
||||||
if (!user) {
|
_services.value[key] = service
|
||||||
_services.value[key] = service
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully created service:', key)
|
console.debug('[Mail Manager][Store] - Successfully created service:', key)
|
||||||
return service
|
return service
|
||||||
@@ -250,7 +241,7 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
*
|
*
|
||||||
* @returns Promise with updated service object
|
* @returns Promise with updated service object
|
||||||
*/
|
*/
|
||||||
async function update(provider: string, identifier: string | number, delta: boolean, data: ServiceObject | Partial<ServiceInterface>, user?: string): Promise<ServiceObject> {
|
async function update(provider: string, identifier: string | number, delta: boolean, data: ServiceObject | Partial<ServiceInterface>): Promise<ServiceObject> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
// convert ServiceObject to JSON if needed
|
// convert ServiceObject to JSON if needed
|
||||||
@@ -261,13 +252,11 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
payload = data
|
payload = data
|
||||||
}
|
}
|
||||||
|
|
||||||
const service = await serviceService.update({ provider, identifier, delta, data: payload }, user)
|
const service = await serviceService.update({ provider, identifier, delta, data: payload })
|
||||||
|
|
||||||
// Merge updated service into state (acting-user context stays out of the shared cache)
|
// Merge updated service into state
|
||||||
const key = identifierKey(service.provider, service.identifier)
|
const key = identifierKey(service.provider, service.identifier)
|
||||||
if (!user) {
|
_services.value[key] = service
|
||||||
_services.value[key] = service
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully updated service:', key)
|
console.debug('[Mail Manager][Store] - Successfully updated service:', key)
|
||||||
return service
|
return service
|
||||||
@@ -287,16 +276,14 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
*
|
*
|
||||||
* @returns Promise with deletion result
|
* @returns Promise with deletion result
|
||||||
*/
|
*/
|
||||||
async function remove(provider: string, identifier: string | number, user?: string): Promise<any> {
|
async function remove(provider: string, identifier: string | number): Promise<any> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
await serviceService.delete({ provider, identifier }, user)
|
await serviceService.delete({ provider, identifier })
|
||||||
|
|
||||||
// Remove deleted service from state
|
// Remove deleted service from state
|
||||||
const key = identifierKey(provider, identifier)
|
const key = identifierKey(provider, identifier)
|
||||||
if (!user) {
|
delete _services.value[key]
|
||||||
delete _services.value[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully deleted service:', key)
|
console.debug('[Mail Manager][Store] - Successfully deleted service:', key)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -324,7 +311,6 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
location: string | undefined,
|
location: string | undefined,
|
||||||
provider: string | undefined,
|
provider: string | undefined,
|
||||||
onService?: (service: ServiceObject) => void,
|
onService?: (service: ServiceObject) => void,
|
||||||
user?: string,
|
|
||||||
): Promise<{ total: number }> {
|
): Promise<{ total: number }> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
|
|
||||||
@@ -333,8 +319,7 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
{ identity, secret, location, provider },
|
{ identity, secret, location, provider },
|
||||||
(service: ServiceObject) => {
|
(service: ServiceObject) => {
|
||||||
onService?.(service)
|
onService?.(service)
|
||||||
},
|
}
|
||||||
user
|
|
||||||
)
|
)
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully discovered', result.total, 'services')
|
console.debug('[Mail Manager][Store] - Successfully discovered', result.total, 'services')
|
||||||
@@ -362,7 +347,6 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
identifier?: string | number | null,
|
identifier?: string | number | null,
|
||||||
location?: ServiceLocation | Location | null,
|
location?: ServiceLocation | Location | null,
|
||||||
identity?: ServiceIdentity | Identity | null,
|
identity?: ServiceIdentity | Identity | null,
|
||||||
user?: string,
|
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
transceiving.value = true
|
transceiving.value = true
|
||||||
try {
|
try {
|
||||||
@@ -385,7 +369,7 @@ export const useServicesStore = defineStore('mailServicesStore', () => {
|
|||||||
identity = identity.toJson()
|
identity = identity.toJson()
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await serviceService.test({ provider, identifier, location, identity }, user)
|
const response = await serviceService.test({ provider, identifier, location, identity })
|
||||||
|
|
||||||
console.debug('[Mail Manager][Store] - Successfully tested service:', provider, identifier || location)
|
console.debug('[Mail Manager][Store] - Successfully tested service:', provider, identifier || location)
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Collection type definitions
|
* Collection type definitions
|
||||||
*/
|
*/
|
||||||
import type {
|
import type { CollectionIdentifier, ListFilter, ListSort, ServiceIdentifier, SourceSelector } from './common';
|
||||||
ServiceIdentifier,
|
|
||||||
CollectionIdentifier,
|
|
||||||
ListFilter,
|
|
||||||
ListSort
|
|
||||||
} from './common';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collection information
|
* Collection information
|
||||||
|
|||||||
+27
-4
@@ -85,11 +85,34 @@ export type ApiStreamResponse<T = any> =
|
|||||||
| ApiStreamDataResponse<T>;
|
| ApiStreamDataResponse<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifiers for targeting specific providers, services, collections, or entities in list or extant operations.
|
* Selector for targeting specific providers, services, collections, or entities in list or extant operations.
|
||||||
*
|
*
|
||||||
* Operations accept flat arrays of colon-separated identifier strings, e.g.
|
* Example usage:
|
||||||
* ["imap:account1:INBOX", "imap:account1:Sent:1001"].
|
* {
|
||||||
|
* "provider1": true, // Select all services/collections/entities under provider1
|
||||||
|
* "provider2": {
|
||||||
|
* "serviceA": true, // Select all collections/entities under serviceA of provider2
|
||||||
|
* "serviceB": {
|
||||||
|
* "collectionX": true, // Select all entities under collectionX of serviceB of provider2
|
||||||
|
* "collectionY": [1, 2, 3] // Select entities with identifiers 1, 2, and 3 under collectionY of serviceB of provider2
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
*/
|
*/
|
||||||
|
export type SourceSelector = {
|
||||||
|
[provider: string]: boolean | ServiceSelector;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServiceSelector = {
|
||||||
|
[service: string]: boolean | CollectionSelector;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CollectionSelector = {
|
||||||
|
[collection: string | number]: boolean | EntitySelector;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EntitySelector = (string | number)[];
|
||||||
|
|
||||||
export type ProviderIdentifier = `${string}`;
|
export type ProviderIdentifier = `${string}`;
|
||||||
export type ServiceIdentifier = `${string}:${string}`;
|
export type ServiceIdentifier = `${string}:${string}`;
|
||||||
export type CollectionIdentifier = `${string}:${string}:${string | number}`;
|
export type CollectionIdentifier = `${string}:${string}:${string | number}`;
|
||||||
|
|||||||
+3
-16
@@ -8,10 +8,7 @@ import type {
|
|||||||
ListRange,
|
ListRange,
|
||||||
ListSort,
|
ListSort,
|
||||||
} from './common';
|
} from './common';
|
||||||
import type {
|
import type { MessageInterface, MessageModelInterface } from './message';
|
||||||
MessageInterface,
|
|
||||||
MessageModelInterface
|
|
||||||
} from './message';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entity definition
|
* Entity definition
|
||||||
@@ -95,9 +92,7 @@ export interface EntityExtantResponse {
|
|||||||
* Entity delta
|
* Entity delta
|
||||||
*/
|
*/
|
||||||
export interface EntityDeltaRequest {
|
export interface EntityDeltaRequest {
|
||||||
// Each target is provider:service:collection, or provider:service:collection:signature
|
sources: CollectionIdentifier[];
|
||||||
// to request a delta relative to a known signature (the signature is the entity slot).
|
|
||||||
targets: (CollectionIdentifier | EntityIdentifier)[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityDeltaResponse {
|
export interface EntityDeltaResponse {
|
||||||
@@ -232,17 +227,9 @@ export interface EntityBlobsRequest {
|
|||||||
parts: EntityBlobSelector[];
|
parts: EntityBlobSelector[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EntityBlobsWireResult {
|
export interface EntityBlobsResult {
|
||||||
source: EntityIdentifier;
|
source: EntityIdentifier;
|
||||||
part: EntityBlobSelector;
|
part: EntityBlobSelector;
|
||||||
mime: string;
|
|
||||||
filename: string;
|
|
||||||
bytes: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EntityBlobsWireResponse extends Array<EntityBlobsWireResult> {}
|
|
||||||
|
|
||||||
export interface EntityBlobsResult extends Omit<EntityBlobsWireResult, 'bytes'> {
|
|
||||||
blob: Blob;
|
blob: Blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface MessageAddressInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MessageFlagsInterface {
|
export interface MessageFlagsInterface {
|
||||||
seen?: boolean;
|
read?: boolean;
|
||||||
flagged?: boolean;
|
flagged?: boolean;
|
||||||
answered?: boolean;
|
answered?: boolean;
|
||||||
draft?: boolean;
|
draft?: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Provider type definitions
|
* Provider type definitions
|
||||||
*/
|
*/
|
||||||
import type { ProviderIdentifier } from "./common";
|
import type { SourceSelector } from "./common";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider capabilities
|
* Provider capabilities
|
||||||
@@ -35,18 +35,18 @@ export interface ProviderModelInterface extends Omit<ProviderInterface, '@type'
|
|||||||
* Provider list
|
* Provider list
|
||||||
*/
|
*/
|
||||||
export interface ProviderListRequest {
|
export interface ProviderListRequest {
|
||||||
targets?: ProviderIdentifier[];
|
sources?: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderListResponse {
|
export interface ProviderListResponse {
|
||||||
[identifier: ProviderIdentifier]: ProviderInterface;
|
[identifier: string]: ProviderInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider fetch
|
* Provider fetch
|
||||||
*/
|
*/
|
||||||
export interface ProviderFetchRequest {
|
export interface ProviderFetchRequest {
|
||||||
target: ProviderIdentifier;
|
identifier: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderFetchResponse extends ProviderInterface {}
|
export interface ProviderFetchResponse extends ProviderInterface {}
|
||||||
@@ -55,9 +55,9 @@ export interface ProviderFetchResponse extends ProviderInterface {}
|
|||||||
* Provider extant
|
* Provider extant
|
||||||
*/
|
*/
|
||||||
export interface ProviderExtantRequest {
|
export interface ProviderExtantRequest {
|
||||||
targets: ProviderIdentifier[];
|
sources: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProviderExtantResponse {
|
export interface ProviderExtantResponse {
|
||||||
[identifier: ProviderIdentifier]: boolean;
|
[identifier: string]: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-19
@@ -1,13 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* Service type definitions
|
* Service type definitions
|
||||||
*/
|
*/
|
||||||
import type { ServiceAddressObject } from '@/models/address';
|
|
||||||
import type { Identity } from '@/models/identity';
|
import type { Identity } from '@/models/identity';
|
||||||
import type { Location } from '@/models/location';
|
import type { Location } from '@/models/location';
|
||||||
import type {
|
import type {
|
||||||
ServiceIdentifier,
|
|
||||||
CollectionIdentifier,
|
|
||||||
ListFilterComparisonOperator,
|
ListFilterComparisonOperator,
|
||||||
|
SourceSelector,
|
||||||
} from './common';
|
} from './common';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,15 +40,6 @@ export interface ServiceCapabilitiesInterface {
|
|||||||
[key: string]: boolean | object | string | string[] | undefined;
|
[key: string]: boolean | object | string | string[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Service sender address (primary or secondary/alias)
|
|
||||||
* Mirrors the shared Mail AddressInterface JSON shape
|
|
||||||
*/
|
|
||||||
export interface ServiceAddressInterface {
|
|
||||||
address: string;
|
|
||||||
label?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service information
|
* Service information
|
||||||
*/
|
*/
|
||||||
@@ -64,25 +53,23 @@ export interface ServiceInterface {
|
|||||||
capabilities?: ServiceCapabilitiesInterface;
|
capabilities?: ServiceCapabilitiesInterface;
|
||||||
location?: ServiceLocation | null;
|
location?: ServiceLocation | null;
|
||||||
identity?: ServiceIdentity | null;
|
identity?: ServiceIdentity | null;
|
||||||
primaryAddress?: ServiceAddressInterface | null;
|
primaryAddress?: string | null;
|
||||||
secondaryAddresses?: ServiceAddressInterface[] | null;
|
secondaryAddresses?: string[] | null;
|
||||||
auxiliary?: Record<string, any>; // Provider-specific extension data
|
auxiliary?: Record<string, any>; // Provider-specific extension data
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceModelInterface extends Omit<{
|
export interface ServiceModelInterface extends Omit<{
|
||||||
[K in keyof ServiceInterface]-?: Exclude<ServiceInterface[K], undefined>;
|
[K in keyof ServiceInterface]-?: Exclude<ServiceInterface[K], undefined>;
|
||||||
}, '@type' | 'version' | 'location' | 'identity' | 'primaryAddress' | 'secondaryAddresses'> {
|
}, '@type' | 'version' | 'location' | 'identity'> {
|
||||||
location: Location | null;
|
location: Location | null;
|
||||||
identity: Identity | null;
|
identity: Identity | null;
|
||||||
primaryAddress: ServiceAddressObject | null;
|
|
||||||
secondaryAddresses: ServiceAddressObject[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service list
|
* Service list
|
||||||
*/
|
*/
|
||||||
export interface ServiceListRequest {
|
export interface ServiceListRequest {
|
||||||
targets?: ServiceIdentifier[] | CollectionIdentifier[];
|
sources?: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceListResponse {
|
export interface ServiceListResponse {
|
||||||
@@ -105,7 +92,7 @@ export interface ServiceFetchResponse extends ServiceInterface {}
|
|||||||
* Service extant
|
* Service extant
|
||||||
*/
|
*/
|
||||||
export interface ServiceExtantRequest {
|
export interface ServiceExtantRequest {
|
||||||
targets: ServiceIdentifier[];
|
sources: SourceSelector;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceExtantResponse {
|
export interface ServiceExtantResponse {
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { describe, it, expect } from 'vitest'
|
|
||||||
|
|
||||||
describe('Basic Tests', () => {
|
|
||||||
it('should perform basic assertion', () => {
|
|
||||||
expect(true).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should test array operations', () => {
|
|
||||||
const array = ['foo', 'bar', 'baz']
|
|
||||||
|
|
||||||
expect(array).toHaveLength(3)
|
|
||||||
expect(array).toContain('bar')
|
|
||||||
expect(array[0]).toBe('foo')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should test string operations', () => {
|
|
||||||
const string = 'Hello, World!'
|
|
||||||
|
|
||||||
expect(string).toContain('World')
|
|
||||||
expect(string.length).toBe(13)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should test object operations', () => {
|
|
||||||
const obj = { foo: 'bar', count: 42 }
|
|
||||||
|
|
||||||
expect(obj).toHaveProperty('foo')
|
|
||||||
expect(obj.foo).toBe('bar')
|
|
||||||
expect(obj.count).toBeGreaterThan(40)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { fileURLToPath } from 'node:url'
|
|
||||||
import { defineConfig, configDefaults } from 'vitest/config'
|
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import path from 'path'
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url)
|
|
||||||
const __dirname = path.dirname(__filename)
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [vue()],
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': path.resolve(__dirname, '../../src'),
|
|
||||||
'@KTXC': path.resolve(__dirname, '../../../../core/src'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
test: {
|
|
||||||
environment: 'jsdom',
|
|
||||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
|
||||||
root: fileURLToPath(new URL('../../', import.meta.url)),
|
|
||||||
coverage: {
|
|
||||||
provider: 'v8',
|
|
||||||
reporter: ['text', 'json', 'html'],
|
|
||||||
exclude: [
|
|
||||||
'node_modules/',
|
|
||||||
'tests/',
|
|
||||||
'**/*.d.ts',
|
|
||||||
'**/*.config.*',
|
|
||||||
'**/dist/**',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace KTXT\MailManager\Tests\Integration;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
class BaseTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testBasicAssertion(): void
|
|
||||||
{
|
|
||||||
$this->assertTrue(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testArrayOperations(): void
|
|
||||||
{
|
|
||||||
$array = ['foo' => 'bar'];
|
|
||||||
|
|
||||||
$this->assertArrayHasKey('foo', $array);
|
|
||||||
$this->assertEquals('bar', $array['foo']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testStringOperations(): void
|
|
||||||
{
|
|
||||||
$string = 'Hello, World!';
|
|
||||||
|
|
||||||
$this->assertStringContainsString('World', $string);
|
|
||||||
$this->assertEquals(13, strlen($string));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,6 @@
|
|||||||
|
|
||||||
require dirname(__DIR__, 2).'/lib/vendor/autoload.php';
|
require dirname(__DIR__, 2).'/lib/vendor/autoload.php';
|
||||||
|
|
||||||
// When this module is checked out inside a full server (server/modules/<handle>,
|
|
||||||
// as it is in CI and in this monorepo checkout), also load the server's own
|
|
||||||
// core/shared autoloader so tests can reference framework (KTXC/KTXF) types.
|
|
||||||
// Standalone module checkouts without a server alongside them skip this.
|
|
||||||
define('SERVER_ROOT', dirname(__DIR__, 4));
|
|
||||||
$serverAutoload = SERVER_ROOT . '/vendor/autoload.php';
|
|
||||||
if (is_file($serverAutoload)) {
|
|
||||||
require $serverAutoload;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($_SERVER['APP_DEBUG']) && $_SERVER['APP_DEBUG']) {
|
if (isset($_SERVER['APP_DEBUG']) && $_SERVER['APP_DEBUG']) {
|
||||||
umask(0000);
|
umask(0000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,6 @@
|
|||||||
<testsuite name="Unit Tests">
|
<testsuite name="Unit Tests">
|
||||||
<directory>unit</directory>
|
<directory>unit</directory>
|
||||||
</testsuite>
|
</testsuite>
|
||||||
<testsuite name="Integration Tests">
|
|
||||||
<directory>Integration</directory>
|
|
||||||
</testsuite>
|
|
||||||
</testsuites>
|
</testsuites>
|
||||||
|
|
||||||
<source ignoreSuppressionOfDeprecations="true"
|
<source ignoreSuppressionOfDeprecations="true"
|
||||||
@@ -32,7 +29,8 @@
|
|||||||
restrictWarnings="true"
|
restrictWarnings="true"
|
||||||
>
|
>
|
||||||
<include>
|
<include>
|
||||||
<directory>../../lib</directory>
|
<directory>../../core/lib</directory>
|
||||||
|
<directory>../../shared/lib</directory>
|
||||||
</include>
|
</include>
|
||||||
</source>
|
</source>
|
||||||
|
|
||||||
Reference in New Issue
Block a user