feat: mail composition

Signed-off-by: Sebastian <krupinski01@gmail.com>
This commit is contained in:
2026-06-16 13:24:54 -04:00
parent 7a3d90d0cd
commit f1cff5441a
22 changed files with 1786 additions and 175 deletions
+176
View File
@@ -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);
}
}