feat: add logic to sync drafts to server
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
+174
-4
@@ -6,13 +6,19 @@ namespace KTXM\Mail;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KTXC\Module\ModuleManager;
|
||||
use KTXF\Event\EventDispatcherInterface;
|
||||
use KTXF\Mail\Collection\CollectionBaseInterface;
|
||||
use KTXF\Mail\Collection\CollectionRoles;
|
||||
use KTXF\Mail\Object\Address;
|
||||
use KTXF\Mail\Object\AddressInterface;
|
||||
use KTXF\Mail\Object\MessagePart;
|
||||
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceEntityMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceBaseInterface;
|
||||
use KTXF\Mail\Service\ServiceEntitySubmitInterface;
|
||||
use KTXF\Mail\Submission\EntitySubmitResult;
|
||||
use KTXF\Resource\BinaryResource;
|
||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||
use KTXF\Resource\Identifier\ResourceIdentifier;
|
||||
use KTXF\Utile\UUID;
|
||||
@@ -20,6 +26,8 @@ use KTXM\Mail\Stores\CompositionStore;
|
||||
use KTXM\MailManager\Manager as MailManager;
|
||||
use KTXM\DocumentsManager\Manager as DocumentsManager;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class CompositionManager {
|
||||
|
||||
@@ -30,6 +38,7 @@ class CompositionManager {
|
||||
private readonly ModuleManager $moduleManager,
|
||||
private readonly CompositionStore $compositionStore,
|
||||
private readonly MailManager $mailManager,
|
||||
private readonly EventDispatcherInterface $events,
|
||||
) {}
|
||||
|
||||
public function stage(string $tenantId, string $userId, string $identifier, string $action, array $sender, array $message, ?string $source = null): array {
|
||||
@@ -115,6 +124,8 @@ class CompositionManager {
|
||||
$identifier,
|
||||
$saveComposition,
|
||||
) ?? $snapshot;
|
||||
$event = new CompositionSavedEvent($tenantId, $userId, $identifier, (int)$snapshot['revision']);
|
||||
$this->events->dispatch($event);
|
||||
|
||||
$response = $snapshot;
|
||||
unset($response['remote']);
|
||||
@@ -157,12 +168,16 @@ class CompositionManager {
|
||||
};
|
||||
|
||||
// Execute the mutation and atomically persist the snapshot it returns.
|
||||
$this->compositionStore->compositionSave(
|
||||
$composed = $this->compositionStore->compositionSave(
|
||||
$tenantId,
|
||||
$userId,
|
||||
$identifier,
|
||||
$patchComposition,
|
||||
);
|
||||
if (($result['disposition'] ?? null) === 'patched' && $composed !== null) {
|
||||
$event = new CompositionSavedEvent($tenantId, $userId, $identifier, (int)($composed['revision'] ?? 0));
|
||||
$this->events->dispatch($event);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -174,6 +189,113 @@ class CompositionManager {
|
||||
];
|
||||
}
|
||||
|
||||
public function synchronize(string $tenantId, string $userId, string $identifier): array {
|
||||
$result = [
|
||||
'identifier' => $identifier,
|
||||
'disposition' => 'staged',
|
||||
];
|
||||
|
||||
// Construct the synchronization operation that will run while the composition is locked.
|
||||
$synchronizeComposition = function (?array $composed) use ($tenantId, $userId, $identifier, &$result): ?array {
|
||||
if ($composed === null) {
|
||||
$result['error'] = 'Composition not found';
|
||||
return null;
|
||||
}
|
||||
|
||||
$remote = isset($composed['remote']) && is_array($composed['remote'])
|
||||
? $composed['remote']
|
||||
: ['status' => 'dirty', 'entity' => null, 'error' => null];
|
||||
|
||||
if (($remote['status'] ?? 'dirty') === 'synced') {
|
||||
$result['disposition'] = 'saved';
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$sender = isset($composed['sender']) && is_array($composed['sender'])
|
||||
? $composed['sender']
|
||||
: [];
|
||||
$senderAddress = (string)($sender['address'] ?? '');
|
||||
if ($senderAddress === '' || !filter_var($senderAddress, FILTER_VALIDATE_EMAIL)) {
|
||||
throw new RuntimeException('Composition sender is invalid');
|
||||
}
|
||||
|
||||
$service = $this->mailManager->serviceFindByAddress($tenantId, $userId, $senderAddress);
|
||||
if ($service === null || $service->getEnabled() === false) {
|
||||
throw new RuntimeException("No enabled mail service handles '$senderAddress'");
|
||||
}
|
||||
if (!$service instanceof ServiceEntityMutableInterface) {
|
||||
throw new RuntimeException("Mail service '{$service->identifier()}' does not support draft mutations");
|
||||
}
|
||||
|
||||
$target = $this->resolveDraftCollection($service);
|
||||
$attachments = isset($composed['attachments']) && is_array($composed['attachments'])
|
||||
? array_values($composed['attachments'])
|
||||
: [];
|
||||
$properties = $this->buildMessageProperties(
|
||||
service: $service,
|
||||
sender: Address::fromArray($sender),
|
||||
message: is_array($composed['message'] ?? null) ? $composed['message'] : [],
|
||||
attachments: $attachments,
|
||||
composed: $composed,
|
||||
tenantId: $tenantId,
|
||||
userId: $userId,
|
||||
compositionId: $identifier,
|
||||
);
|
||||
$properties->setFlag('draft', true);
|
||||
$properties->setFlag('seen', true);
|
||||
|
||||
$remoteEntity = $this->remoteEntityIdentifier($remote['entity'] ?? null);
|
||||
if ($remoteEntity === null) {
|
||||
$entity = $this->mailManager->entityCreate($tenantId, $userId, $target, $properties);
|
||||
} else {
|
||||
if ($remoteEntity->provider() !== $service->provider()
|
||||
|| (string)$remoteEntity->service() !== (string)$service->identifier()) {
|
||||
throw new RuntimeException('Changing the draft service is not supported yet');
|
||||
}
|
||||
$entity = $this->mailManager->entityModify($tenantId, $userId, $remoteEntity, $properties);
|
||||
}
|
||||
|
||||
if ($entity->collection() === null || $entity->identifier() === null) {
|
||||
throw new RuntimeException('Provider returned an incomplete draft identifier');
|
||||
}
|
||||
|
||||
$composed['remote'] = [
|
||||
...$remote,
|
||||
'status' => 'synced',
|
||||
'entity' => (string)new EntityIdentifier(
|
||||
$entity->provider(),
|
||||
(string)$entity->service(),
|
||||
(string)$entity->collection(),
|
||||
(string)$entity->identifier(),
|
||||
),
|
||||
'error' => null,
|
||||
];
|
||||
$result['disposition'] = 'saved';
|
||||
} catch (Throwable $throwable) {
|
||||
$composed['remote'] = [
|
||||
...$remote,
|
||||
'status' => 'failed',
|
||||
'entity' => $remote['entity'] ?? null,
|
||||
'error' => $throwable->getMessage(),
|
||||
];
|
||||
$result['error'] = $throwable->getMessage();
|
||||
}
|
||||
|
||||
return $composed;
|
||||
};
|
||||
|
||||
// Synchronize and atomically persist the resulting remote state.
|
||||
$this->compositionStore->compositionSave(
|
||||
$tenantId,
|
||||
$userId,
|
||||
$identifier,
|
||||
$synchronizeComposition,
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function send(string $tenantId, string $userId, string $identifier, array $sender, array $message, array $attachments): array {
|
||||
// Construct the snapshot mutation that will run while the composition is locked.
|
||||
$saveComposition = function (?array $composed) use ($sender, $message): ?array {
|
||||
@@ -269,7 +391,7 @@ class CompositionManager {
|
||||
}
|
||||
|
||||
private function buildMessageProperties(
|
||||
ServiceEntitySubmitInterface $service,
|
||||
ServiceEntitySubmitInterface|ServiceEntityMutableInterface $service,
|
||||
AddressInterface $sender,
|
||||
array $message,
|
||||
array $attachments,
|
||||
@@ -348,8 +470,9 @@ class CompositionManager {
|
||||
'attachments' => [],
|
||||
];
|
||||
$failed = [];
|
||||
$compositionChanged = false;
|
||||
// Construct the snapshot mutation that will run while the composition is locked.
|
||||
$addAttachments = function (?array $composed) use ($tenantId, $userId, $composition, $attachments, &$result, &$failed): ?array {
|
||||
$addAttachments = function (?array $composed) use ($tenantId, $userId, $composition, $attachments, &$result, &$failed, &$compositionChanged): ?array {
|
||||
if ($composed === null) {
|
||||
$result['disposition'] = 'error';
|
||||
$result['error'] = [
|
||||
@@ -379,6 +502,7 @@ class CompositionManager {
|
||||
$documentAttachmentsChanged = count($failed) < count($documents);
|
||||
if ($deviceAttachmentsChanged || $documentAttachmentsChanged) {
|
||||
$composed = $this->markRemoteDirty($composed);
|
||||
$compositionChanged = true;
|
||||
}
|
||||
|
||||
return $composed;
|
||||
@@ -410,6 +534,10 @@ class CompositionManager {
|
||||
'message' => 'None of the requested attachments could be added',
|
||||
];
|
||||
}
|
||||
if ($compositionChanged) {
|
||||
$event = new CompositionSavedEvent($tenantId, $userId, $composition, (int)($composed['revision'] ?? 0));
|
||||
$this->events->dispatch($event);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -571,12 +699,16 @@ class CompositionManager {
|
||||
};
|
||||
|
||||
// Execute the mutation and atomically persist the snapshot it returns.
|
||||
$this->compositionStore->compositionSave(
|
||||
$composed = $this->compositionStore->compositionSave(
|
||||
$tenantId,
|
||||
$userId,
|
||||
$composition,
|
||||
$removeAttachment,
|
||||
);
|
||||
if (($result['disposition'] ?? null) === 'removed' && $composed !== null) {
|
||||
$event = new CompositionSavedEvent($tenantId, $userId, $composition, (int)($composed['revision'] ?? 0));
|
||||
$this->events->dispatch($event);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
@@ -594,6 +726,44 @@ class CompositionManager {
|
||||
return $composed;
|
||||
}
|
||||
|
||||
private function resolveDraftCollection(ServiceBaseInterface $service): CollectionIdentifier {
|
||||
$configuredTarget = $service->getAuxiliary()['draftTarget'] ?? null;
|
||||
if ($configuredTarget !== null && (string)$configuredTarget !== '') {
|
||||
return new CollectionIdentifier(
|
||||
$service->provider(),
|
||||
(string)$service->identifier(),
|
||||
(string)$configuredTarget,
|
||||
);
|
||||
}
|
||||
|
||||
$filter = $service->collectionListFilter();
|
||||
$filter->condition('role', CollectionRoles::Drafts->value);
|
||||
$collections = $service->collectionList('', $filter);
|
||||
$collection = reset($collections);
|
||||
if (!$collection instanceof CollectionBaseInterface || $collection->identifier() === null) {
|
||||
throw new RuntimeException("Mail service '{$service->identifier()}' has no Drafts collection");
|
||||
}
|
||||
|
||||
return new CollectionIdentifier(
|
||||
$service->provider(),
|
||||
(string)$service->identifier(),
|
||||
(string)$collection->identifier(),
|
||||
);
|
||||
}
|
||||
|
||||
private function remoteEntityIdentifier(mixed $value): ?EntityIdentifier {
|
||||
if (!is_string($value) || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$identifier = ResourceIdentifier::fromString($value);
|
||||
if (!$identifier instanceof EntityIdentifier) {
|
||||
throw new RuntimeException("Invalid remote draft identifier '$value'");
|
||||
}
|
||||
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array|string> $entries
|
||||
* @return array<int,Address>
|
||||
|
||||
Reference in New Issue
Block a user