feat: mail composition
Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\Mail;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KTXF\Mail\Object\Address;
|
||||
use KTXF\Mail\Object\AddressInterface;
|
||||
use KTXF\Mail\Object\MessagePart;
|
||||
use KTXF\Mail\Object\MessagePropertiesMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceEntitySubmitInterface;
|
||||
use KTXF\Resource\Identifier\EntityIdentifierInterface;
|
||||
use KTXF\Resource\Identifier\ResourceIdentifier;
|
||||
use KTXF\Utile\UUID;
|
||||
use KTXM\Mail\Stores\CompositionStore;
|
||||
use KTXM\MailManager\Manager as MailManager;
|
||||
|
||||
class CompositionManager {
|
||||
|
||||
public function __construct(
|
||||
private readonly CompositionStore $compositionStore,
|
||||
private readonly MailManager $mailManager,
|
||||
) {}
|
||||
|
||||
public function stage(string $tenantId, string $userId, string $identifier, string $action, array $sender, array $message, ?string $source = null): array {
|
||||
if ($identifier === '') {
|
||||
throw new InvalidArgumentException('Draft identifier is required');
|
||||
}
|
||||
|
||||
$snapshot = [
|
||||
'identifier' => $identifier,
|
||||
'action' => $action,
|
||||
'source' => $source,
|
||||
'revision' => 1,
|
||||
'disposition' => 'staged',
|
||||
'sender' => $sender,
|
||||
'message' => $message,
|
||||
'attachments' => [],
|
||||
];
|
||||
|
||||
$this->compositionStore->compositionSave($tenantId, $userId, $identifier, $snapshot);
|
||||
|
||||
if ($action === 'forward') {
|
||||
$sourceIndentifier = ResourceIdentifier::fromString($source);
|
||||
if ($sourceIndentifier === null) {
|
||||
throw new InvalidArgumentException('Invalid source identifier');
|
||||
}
|
||||
$entities = $this->mailManager->entityFetchBulk($tenantId, $userId, $sourceIndentifier);
|
||||
if (count($entities) === 0) {
|
||||
throw new InvalidArgumentException('Source message not found');
|
||||
}
|
||||
// retrieve the attachments from the source message
|
||||
$attachments = reset($entities)->getProperties()->getAttachments();
|
||||
// For each attachment, we download the content and stage it in the composition store
|
||||
foreach ($attachments as $attachment) {
|
||||
$contentId = $attachment->getContentId();
|
||||
$isInline = $attachment->getDisposition() === 'inline';
|
||||
// target part
|
||||
$targetPart = [
|
||||
'partId' => $attachment->getId(),
|
||||
'blobId' => $attachment->getBlobId(),
|
||||
'cid' => $contentId,
|
||||
'cId' => $contentId,
|
||||
];
|
||||
// retrieve the attachment content as a stream
|
||||
$data = $this->mailManager->entityDownload($tenantId, $userId, $sourceIndentifier, $targetPart);
|
||||
// stage the attachment in the composition store
|
||||
$meta = $this->compositionStore->attachmentStageFromStream(
|
||||
tenantId: $tenantId,
|
||||
userId: $userId,
|
||||
compositionId: $identifier,
|
||||
attachmentId: UUID::v4(),
|
||||
data: $data,
|
||||
);
|
||||
// supplement the attachment metadata with source information
|
||||
$meta['origin'] = 'source';
|
||||
$meta['source'] = $source;
|
||||
$meta['partId'] = $attachment->getId();
|
||||
$meta['blobId'] = $attachment->getBlobId();
|
||||
$meta['cid'] = $contentId;
|
||||
$meta['contentId'] = $contentId;
|
||||
$meta['disposition'] = $attachment->getDisposition();
|
||||
$meta['inline'] = $isInline;
|
||||
$meta['name'] = $attachment->getName() ?? $meta['name'];
|
||||
$meta['type'] = $attachment->getType() ?? $meta['type'];
|
||||
$meta['size'] = $attachment->getSize() ?? $meta['size'];
|
||||
$snapshot['attachments'][$meta['identifier']] = $meta;
|
||||
}
|
||||
// update the snapshot with the staged attachments
|
||||
$this->compositionStore->compositionSave($tenantId, $userId, $identifier, $snapshot);
|
||||
}
|
||||
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
public function patch(string $tenantId, string $userId, string $identifier, array $data): array {
|
||||
$composed = $this->compositionStore->compositionFetch($tenantId, $userId, $identifier);
|
||||
|
||||
if ($composed === null) {
|
||||
$result['disposition'] = 'error';
|
||||
$result['error'] = [
|
||||
'type' => 'composition_not_found',
|
||||
'message' => 'Composition not found',
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (!isset($data['revision']) || !is_int($data['revision']) || $data['revision'] < $composed['revision']) {
|
||||
$result['disposition'] = 'error';
|
||||
$result['error'] = [
|
||||
'type' => 'revision_mismatch',
|
||||
'message' => 'Revision mismatch',
|
||||
'currentRevision' => $composed['revision'],
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Apply the patch data to the composed message
|
||||
$composed['revision'] = $data['revision'];
|
||||
$composed['sender'] = $data['sender'] ?? [];
|
||||
$composed['message'] = $data['message'] ?? [];
|
||||
|
||||
$this->compositionStore->compositionSave($tenantId, $userId, $identifier, $composed);
|
||||
|
||||
$result = [
|
||||
'identifier' => $identifier,
|
||||
'disposition' => 'patched',
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function discard(string $tenantId, string $userId, string $identifier): array {
|
||||
return [
|
||||
'identifier' => $identifier,
|
||||
'disposition' => $this->compositionStore->compositionDiscard($tenantId, $userId, $identifier),
|
||||
];
|
||||
}
|
||||
|
||||
public function send(string $tenantId, string $userId, string $identifier, array $sender, array $message, array $attachments): array {
|
||||
$composed = $this->compositionStore->compositionFetch($tenantId, $userId, $identifier);
|
||||
|
||||
if ($composed === null) {
|
||||
$result['disposition'] = 'error';
|
||||
$result['error'] = [
|
||||
'type' => 'composition_not_found',
|
||||
'message' => 'Composition not found',
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
// store the message and sender information before sending
|
||||
$composed['sender'] = $sender;
|
||||
$composed['message'] = $message;
|
||||
$this->compositionStore->compositionSave($tenantId, $userId, $identifier, $composed);
|
||||
|
||||
// validate the sender address before attempting to send the message
|
||||
if ($sender['address'] === '' || !filter_var($sender['address'], FILTER_VALIDATE_EMAIL)) {
|
||||
$result['disposition'] = 'error';
|
||||
$result['error'] = [
|
||||
'type' => 'invalid_sender',
|
||||
'message' => 'Invalid sender address',
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
$senderAddress = $sender['address'];
|
||||
$senderObject = Address::fromArray($sender);
|
||||
|
||||
// resolve submit-capable service for sender
|
||||
$service = $this->mailManager->serviceFindByAddress($tenantId, $userId, $senderAddress);
|
||||
if ($service === null || $service->getEnabled() === false) {
|
||||
return [
|
||||
'disposition' => 'error',
|
||||
'errorCode' => 'service_not_found',
|
||||
'errorMessage' => "Service not found for sender '$senderAddress' or service is disabled",
|
||||
];
|
||||
}
|
||||
if ($service instanceof ServiceEntitySubmitInterface === false) {
|
||||
return [
|
||||
'disposition' => 'error',
|
||||
'errorCode' => 'service_not_supported',
|
||||
'errorMessage' => "Service '{$service->identifier()}' does not support entity submission",
|
||||
];
|
||||
}
|
||||
|
||||
$source = null;
|
||||
$properties = $this->buildMessageProperties(
|
||||
service: $service,
|
||||
sender: $senderObject,
|
||||
message: $message,
|
||||
attachments: $attachments,
|
||||
composed: $composed,
|
||||
tenantId: $tenantId,
|
||||
userId: $userId,
|
||||
compositionId: $identifier,
|
||||
);
|
||||
|
||||
$sendResult = $service->entitySubmit($senderObject, $source, $properties);
|
||||
|
||||
return $sendResult->jsonSerialize();
|
||||
}
|
||||
|
||||
private function buildMessageProperties(
|
||||
ServiceEntitySubmitInterface $service,
|
||||
AddressInterface $sender,
|
||||
array $message,
|
||||
array $attachments,
|
||||
array $composed,
|
||||
string $tenantId,
|
||||
string $userId,
|
||||
string $compositionId,
|
||||
): MessagePropertiesMutableInterface {
|
||||
$properties = $service->entityFresh()->getProperties();
|
||||
$bodyTextPlain = isset($message['body']['text']) ? (string)$message['body']['text'] : '';
|
||||
$bodyTextHtml = isset($message['body']['html']) ? (string)$message['body']['html'] : '';
|
||||
|
||||
$properties->setFrom($sender);
|
||||
|
||||
$to = $this->mapAddresses($message['to'] ?? []);
|
||||
if ($to !== []) {
|
||||
$properties->setTo(...$to);
|
||||
}
|
||||
|
||||
$cc = $this->mapAddresses($message['cc'] ?? []);
|
||||
if ($cc !== []) {
|
||||
$properties->setCc(...$cc);
|
||||
}
|
||||
|
||||
$bcc = $this->mapAddresses($message['bcc'] ?? []);
|
||||
if ($bcc !== []) {
|
||||
$properties->setBcc(...$bcc);
|
||||
}
|
||||
|
||||
$replyTo = $this->mapAddresses($message['replyTo'] ?? []);
|
||||
if ($replyTo !== []) {
|
||||
$properties->setReplyTo(...$replyTo);
|
||||
}
|
||||
|
||||
$properties->setSubject((string)($message['subject'] ?? ''));
|
||||
$properties->setBodyTextPlain($bodyTextPlain);
|
||||
$properties->setBodyTextHtml($bodyTextHtml);
|
||||
|
||||
if (isset($message['flags']) && is_array($message['flags'])) {
|
||||
$properties->setFlags($message['flags']);
|
||||
}
|
||||
|
||||
$attachmentObjects = [];
|
||||
foreach ($attachments as $attachment) {
|
||||
if (!isset($attachment['identifier']) || !is_string($attachment['identifier']) || $attachment['identifier'] === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$composedAttachment = $composed['attachments'][$attachment['identifier']] ?? null;
|
||||
if (!is_array($composedAttachment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attachmentObjects[] = MessagePart::fromArray([
|
||||
'partId' => $attachment['identifier'],
|
||||
'blobId' => $attachment['blobId'] ?? $composedAttachment['blobId'] ?? null,
|
||||
'size' => $composedAttachment['size'] ?? null,
|
||||
'name' => $composedAttachment['name'] ?? 'unknown.bin',
|
||||
'type' => $composedAttachment['type'] ?? 'application/octet-stream',
|
||||
'disposition' => (($attachment['inline'] ?? $composedAttachment['inline'] ?? false) === true) ? 'inline' : 'attachment',
|
||||
'content' => $this->compositionStore->attachmentFetchData($tenantId, $userId, $compositionId, $attachment['identifier']),
|
||||
'cid' => $attachment['contentId'] ?? $attachment['cid'] ?? $composedAttachment['contentId'] ?? $composedAttachment['cid'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
if ($attachmentObjects !== []) {
|
||||
$properties->setAttachments(...$attachmentObjects);
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array|string> $entries
|
||||
* @return array<int,Address>
|
||||
*/
|
||||
private function mapAddresses(array $entries): array {
|
||||
$addresses = [];
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
if (is_array($entry)) {
|
||||
$address = Address::fromArray($entry);
|
||||
if ($address->getAddress() !== '') {
|
||||
$addresses[] = $address;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($entry) && $entry !== '') {
|
||||
$address = Address::fromString($entry);
|
||||
if ($address->getAddress() !== '') {
|
||||
$addresses[] = $address;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $addresses;
|
||||
}
|
||||
|
||||
public function attachmentAdd(string $tenantId, string $userId, string $composition, array ...$attachments): array {
|
||||
$result = [
|
||||
'composition' => $composition,
|
||||
'attachments' => [],
|
||||
];
|
||||
|
||||
$composed = $this->compositionStore->compositionFetch($tenantId, $userId, $composition);
|
||||
if ($composed === null) {
|
||||
$result['disposition'] = 'error';
|
||||
$result['error'] = [
|
||||
'type' => 'composition_not_found',
|
||||
'message' => 'Composition not found',
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
if (!isset($attachment['identifier']) || !is_string($attachment['identifier']) || $attachment['identifier'] === '') {
|
||||
$identifier = UUID::v4();
|
||||
} else {
|
||||
$identifier = $attachment['identifier'];
|
||||
}
|
||||
|
||||
$meta = $this->compositionStore->attachmentStageFromBase64(
|
||||
tenantId: $tenantId,
|
||||
userId: $userId,
|
||||
compositionId: $composition,
|
||||
attachmentId: $identifier,
|
||||
name: $attachment['name'] ?? 'unknown.bin',
|
||||
type: $attachment['type'] ?? 'application/octet-stream',
|
||||
data: $attachment['data'] ?? '',
|
||||
);
|
||||
$meta['origin'] = 'upload';
|
||||
$composed['attachments'][$meta['identifier']] = $meta;
|
||||
}
|
||||
|
||||
$this->compositionStore->compositionSave($tenantId, $userId, $composition, $composed);
|
||||
|
||||
$result['disposition'] = 'added';
|
||||
$result['attachments'] = $composed['attachments'];
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function attachmentRemove(string $tenantId, string $userId, string $composition, string $identifier): array {
|
||||
$result = [
|
||||
'composition' => $composition,
|
||||
'identifier' => $identifier,
|
||||
];
|
||||
|
||||
$composed = $this->compositionStore->compositionFetch($tenantId, $userId, $composition);
|
||||
if ($composed === null) {
|
||||
$result['disposition'] = 'error';
|
||||
$result['error'] = [
|
||||
'type' => 'composition_not_found',
|
||||
'message' => 'Composition not found',
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
if (!isset($composed['attachments'][$identifier])) {
|
||||
$result['disposition'] = 'error';
|
||||
$result['error'] = [
|
||||
'type' => 'attachment_not_found',
|
||||
'message' => 'Attachment not found',
|
||||
];
|
||||
return $result;
|
||||
}
|
||||
|
||||
unset($composed['attachments'][$identifier]);
|
||||
$this->compositionStore->compositionSave($tenantId, $userId, $composition, $composed);
|
||||
|
||||
$result['disposition'] = 'removed';
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\Mail\Controllers;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
use KTXM\Mail\CompositionManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
final class CompositionController extends ControllerAbstract {
|
||||
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly CompositionManager $compositionManager,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
#[AuthenticatedRoute('/compose/v1', name: 'mail.compose.v1', methods: ['POST'])]
|
||||
public function index(
|
||||
int $version,
|
||||
string $transaction,
|
||||
string|null $operation = null,
|
||||
array|null $data = null,
|
||||
): Response {
|
||||
$tenantId = $this->tenantIdentity->identifier();
|
||||
$userId = $this->userIdentity->identifier();
|
||||
|
||||
try {
|
||||
if ($operation === null) {
|
||||
throw new InvalidArgumentException('Operation must be provided');
|
||||
}
|
||||
|
||||
$result = match ($operation) {
|
||||
'stage' => $this->stage($tenantId, $userId, $data),
|
||||
'patch' => $this->patch($tenantId, $userId, $data),
|
||||
'discard' => $this->discard($tenantId, $userId, $data),
|
||||
'send' => $this->send($tenantId, $userId, $data),
|
||||
'attachment.add' => $this->attachmentAdd($tenantId, $userId, $data),
|
||||
'attachment.remove' => $this->attachmentRemove($tenantId, $userId, $data),
|
||||
default => throw new InvalidArgumentException('Invalid operation: ' . $operation),
|
||||
};
|
||||
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
'operation' => $operation,
|
||||
'status' => 'success',
|
||||
'data' => $result,
|
||||
], JsonResponse::HTTP_OK);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error('Mail draft request failed', ['exception' => $throwable]);
|
||||
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
'operation' => $operation,
|
||||
'status' => 'error',
|
||||
'data' => [
|
||||
'code' => $throwable->getCode(),
|
||||
'message' => $throwable->getMessage(),
|
||||
],
|
||||
], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private function stage(string $tenantId, string $userId, array $data): array {
|
||||
if (!isset($data['identifier']) || !is_string($data['identifier']) || $data['identifier'] === '') {
|
||||
throw new InvalidArgumentException('Invalid parameter: identifier must be a non-empty string');
|
||||
}
|
||||
if (!isset($data['action']) || !is_string($data['action'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: action must be a string');
|
||||
}
|
||||
if (!in_array($data['action'], ['fresh', 'reply', 'forward'], true)) {
|
||||
throw new InvalidArgumentException('Invalid parameter: action must be one of fresh, reply, forward');
|
||||
}
|
||||
if (!isset($data['sender']) || !is_array($data['sender'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: sender must be an array');
|
||||
}
|
||||
if (!isset($data['message']) || !is_array($data['message'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: message must be an array');
|
||||
}
|
||||
if (($data['action'] === 'reply' || $data['action'] === 'forward') && (!isset($data['source']) || !is_string($data['source']) || $data['source'] === '')) {
|
||||
throw new InvalidArgumentException('Invalid parameter: source must be a non-empty string for reply or forward action');
|
||||
}
|
||||
|
||||
return $this->compositionManager->stage($tenantId, $userId, $data['identifier'], $data['action'], $data['sender'], $data['message'], $data['source'] ?? null);
|
||||
}
|
||||
|
||||
private function patch(string $tenantId, string $userId, array $data): array {
|
||||
if (!isset($data['identifier']) || !is_string($data['identifier']) || $data['identifier'] === '') {
|
||||
throw new InvalidArgumentException('Invalid parameter: identifier must be a non-empty string');
|
||||
}
|
||||
if (!isset($data['sender']) || !is_array($data['sender'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: sender must be an array');
|
||||
}
|
||||
if (!isset($data['message']) || !is_array($data['message'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: message must be an array');
|
||||
}
|
||||
|
||||
return $this->compositionManager->patch($tenantId, $userId, $data['identifier'], $data);
|
||||
}
|
||||
private function discard(string $tenantId, string $userId, array $data): array {
|
||||
if (!isset($data['identifier']) || !is_string($data['identifier']) || $data['identifier'] === '') {
|
||||
throw new InvalidArgumentException('Invalid parameter: identifier must be a non-empty string');
|
||||
}
|
||||
|
||||
return $this->compositionManager->discard($tenantId, $userId, $data['identifier']);
|
||||
}
|
||||
|
||||
private function send(string $tenantId, string $userId, array $data): array {
|
||||
if (!isset($data['identifier']) || !is_string($data['identifier']) || $data['identifier'] === '') {
|
||||
throw new InvalidArgumentException('Invalid parameter: identifier must be a non-empty string');
|
||||
}
|
||||
if (!isset($data['sender']) || !is_array($data['sender'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: sender must be an array');
|
||||
}
|
||||
if (!isset($data['message']) || !is_array($data['message'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: message must be an array');
|
||||
}
|
||||
if (!isset($data['attachments']) || !is_array($data['attachments'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: attachments must be an array');
|
||||
}
|
||||
|
||||
return $this->compositionManager->send($tenantId, $userId, $data['identifier'], $data['sender'], $data['message'], $data['attachments']);
|
||||
}
|
||||
|
||||
private function attachmentAdd(string $tenantId, string $userId, array $data): array {
|
||||
if (!isset($data['composition']) || !is_string($data['composition']) || $data['composition'] === '') {
|
||||
throw new InvalidArgumentException('Invalid parameter: composition must be a non-empty string');
|
||||
}
|
||||
if (!isset($data['attachments']) || !is_array($data['attachments'])) {
|
||||
throw new InvalidArgumentException('Invalid parameter: attachments must be an array');
|
||||
}
|
||||
|
||||
return $this->compositionManager->attachmentAdd($tenantId, $userId, $data['composition'], ...$data['attachments']);
|
||||
}
|
||||
|
||||
private function attachmentRemove(string $tenantId, string $userId, array $data): array {
|
||||
if (!isset($data['composition']) || !is_string($data['composition']) || $data['composition'] === '') {
|
||||
throw new InvalidArgumentException('Invalid parameter: composition must be a non-empty string');
|
||||
}
|
||||
if (!isset($data['identifier']) || !is_string($data['identifier']) || $data['identifier'] === '') {
|
||||
throw new InvalidArgumentException('Invalid parameter: identifier must be a non-empty string');
|
||||
}
|
||||
|
||||
return $this->compositionManager->attachmentRemove($tenantId, $userId, $data['composition'], $data['identifier']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\Mail\Stores;
|
||||
|
||||
use DI\Attribute\Inject;
|
||||
use KTXF\Resource\BinaryResource;
|
||||
|
||||
final class CompositionStore {
|
||||
|
||||
private const COMPOSITION_FILENAME = 'composition.json';
|
||||
|
||||
private string $storagePath;
|
||||
|
||||
public function __construct(
|
||||
#[Inject('rootDir')] private readonly string $rootDir,
|
||||
) {
|
||||
$this->storagePath = $this->rootDir . '/var/cache/mail/composer';
|
||||
}
|
||||
|
||||
public function compositionFetch(string $tenantId, string $userId, string $draftId): ?array {
|
||||
$draftDir = $this->draftDir($tenantId, $userId, $draftId);
|
||||
if (!is_dir($draftDir)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$messagePath = $draftDir . '/' . self::COMPOSITION_FILENAME;
|
||||
if (!file_exists($messagePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = json_decode((string)file_get_contents($messagePath), true);
|
||||
return is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
|
||||
public function compositionSave(string $tenantId, string $userId, string $draftId, array $snapshot): array {
|
||||
$draftDir = $this->draftDir($tenantId, $userId, $draftId);
|
||||
if (!is_dir($draftDir)) {
|
||||
mkdir($draftDir, 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($draftDir . '/' . self::COMPOSITION_FILENAME, json_encode($snapshot, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
public function compositionDiscard(string $tenantId, string $userId, string $draftId): bool {
|
||||
$draftDir = $this->draftDir($tenantId, $userId, $draftId);
|
||||
if (!is_dir($draftDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->deleteDir($draftDir);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function attachmentStageFromStream(
|
||||
string $tenantId,
|
||||
string $userId,
|
||||
string $compositionId,
|
||||
string $attachmentId,
|
||||
BinaryResource $data,
|
||||
): array {
|
||||
$attachmentDir = $this->attachmentDir($tenantId, $userId, $compositionId);
|
||||
if (!is_dir($attachmentDir)) {
|
||||
mkdir($attachmentDir, 0755, true);
|
||||
}
|
||||
|
||||
$contentPath = $attachmentDir . '/' . $attachmentId . '.blob';
|
||||
$handle = fopen($contentPath, 'wb');
|
||||
$size = 0;
|
||||
|
||||
foreach ($data->stream() as $chunk) {
|
||||
$chunkString = (string)$chunk;
|
||||
$size += strlen($chunkString);
|
||||
fwrite($handle, $chunkString);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
$meta = [
|
||||
'identifier' => $attachmentId,
|
||||
'composition' => $compositionId,
|
||||
'name' => $data->filename(),
|
||||
'type' => $data->mimeType(),
|
||||
'size' => $size,
|
||||
];
|
||||
|
||||
file_put_contents($attachmentDir . '/' . $attachmentId . '.meta', json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return $meta;
|
||||
}
|
||||
|
||||
public function attachmentStageFromBase64(
|
||||
string $tenantId,
|
||||
string $userId,
|
||||
string $compositionId,
|
||||
string $attachmentId,
|
||||
string $name,
|
||||
string $type,
|
||||
string $data,
|
||||
): array {
|
||||
$decoded = base64_decode($data, true);
|
||||
if ($decoded === false) {
|
||||
throw new \InvalidArgumentException('Attachment payload is not valid base64');
|
||||
}
|
||||
|
||||
$attachmentDir = $this->attachmentDir($tenantId, $userId, $compositionId);
|
||||
if (!is_dir($attachmentDir)) {
|
||||
mkdir($attachmentDir, 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($attachmentDir . '/' . $attachmentId . '.blob', $decoded);
|
||||
|
||||
$metadata = [
|
||||
'identifier' => $attachmentId,
|
||||
'composition' => $compositionId,
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'size' => strlen($decoded),
|
||||
];
|
||||
|
||||
file_put_contents($attachmentDir . '/' . $attachmentId . '.meta', json_encode($metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
public function attachmentFetchData(string $tenantId, string $userId, string $compositionId, string $attachmentId): ?string {
|
||||
$path = $this->attachmentDir($tenantId, $userId, $compositionId) . '/' . $attachmentId . '.blob';
|
||||
if (!file_exists($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return file_get_contents($path) ?: null;
|
||||
}
|
||||
|
||||
public function fetchAttachmentMeta(string $tenantId, string $userId, string $compositionId, string $attachmentId): ?array {
|
||||
$path = $this->attachmentDir($tenantId, $userId, $compositionId) . '/' . $attachmentId . '.meta';
|
||||
if (!file_exists($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$decoded = json_decode((string)file_get_contents($path), true);
|
||||
return is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
|
||||
private function attachmentDir(string $tenantId, string $userId, string $draftId): string {
|
||||
return $this->draftDir($tenantId, $userId, $draftId) . '/attachments';
|
||||
}
|
||||
|
||||
private function draftDir(string $tenantId, string $userId, string $draftId): string {
|
||||
return $this->storagePath . '/' . $tenantId . '/' . $userId . '/' . $draftId;
|
||||
}
|
||||
|
||||
private function deleteDir(string $path): void {
|
||||
if (!is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (scandir($path) ?: [] as $entry) {
|
||||
if ($entry === '.' || $entry === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$child = $path . '/' . $entry;
|
||||
if (is_dir($child)) {
|
||||
$this->deleteDir($child);
|
||||
continue;
|
||||
}
|
||||
|
||||
unlink($child);
|
||||
}
|
||||
|
||||
rmdir($path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user