Compare commits
4 Commits
138b5adcac
...
renovate/v
| Author | SHA1 | Date | |
|---|---|---|---|
| 27ce930a12 | |||
| 0e4bd905a2 | |||
| 935743963f | |||
| 502b18962c |
@@ -13,6 +13,7 @@ use InvalidArgumentException;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Http\Response\StreamedNdJsonResponse;
|
||||
use KTXC\Http\Response\StreamedResponse;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
@@ -31,12 +32,6 @@ use KTXM\MailManager\Manager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Default Controller - Unified Mail API
|
||||
*
|
||||
* Handles all mail operations in JMAP-style API pattern.
|
||||
* Supports both single operations and batches with result references.
|
||||
*/
|
||||
class DefaultController extends ControllerAbstract {
|
||||
|
||||
private const ERR_MISSING_PROVIDER = 'Missing parameter: provider';
|
||||
@@ -168,6 +163,7 @@ class DefaultController extends ControllerAbstract {
|
||||
'entity.move' => $this->entityMove($tenantId, $userId, $data),
|
||||
'entity.copy' => throw new InvalidArgumentException('Operation not implemented: ' . $operation),
|
||||
'entity.transmit' => $this->entityTransmit($tenantId, $userId, $data),
|
||||
'entity.download' => $this->entityDownload($tenantId, $userId, $data),
|
||||
|
||||
default => throw new InvalidArgumentException(self::ERR_INVALID_OPERATION . $operation)
|
||||
};
|
||||
@@ -558,14 +554,14 @@ class DefaultController extends ControllerAbstract {
|
||||
|
||||
if (is_bool($result)) {
|
||||
return [
|
||||
'outcome' => 'deleted'
|
||||
'disposition' => 'deleted'
|
||||
];
|
||||
}
|
||||
|
||||
if ($result instanceof JsonSerializable) {
|
||||
return [
|
||||
'outcome' => 'moved',
|
||||
'data' => $result
|
||||
'disposition' => 'moved',
|
||||
'mutation' => $result
|
||||
];
|
||||
}
|
||||
|
||||
@@ -773,21 +769,21 @@ class DefaultController extends ControllerAbstract {
|
||||
}
|
||||
|
||||
private function entityDelete(string $tenantId, string $userId, array $data): mixed {
|
||||
if (!isset($data['sources'])) {
|
||||
throw new InvalidArgumentException(self::ERR_MISSING_SOURCES);
|
||||
if (!isset($data['targets'])) {
|
||||
throw new InvalidArgumentException(self::ERR_MISSING_TARGETS);
|
||||
}
|
||||
if (!is_array($data['sources'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_SOURCES);
|
||||
if (!is_array($data['targets'])) {
|
||||
throw new InvalidArgumentException(self::ERR_INVALID_TARGETS);
|
||||
}
|
||||
|
||||
$sources = ResourceIdentifiers::fromArray($data['sources']);
|
||||
foreach ($sources as $source) {
|
||||
if (!$source instanceof EntityIdentifier) {
|
||||
throw new InvalidArgumentException('Invalid parameter: sources must contain provider:service:collection:entity identifiers');
|
||||
$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->mailManager->entityDelete($tenantId, $userId, ...$sources->all());
|
||||
return $this->mailManager->entityDelete($tenantId, $userId, ...$targets->all());
|
||||
}
|
||||
|
||||
private function entityPatch(string $tenantId, string $userId, array $data): mixed {
|
||||
@@ -868,4 +864,46 @@ class DefaultController extends ControllerAbstract {
|
||||
return ['jobId' => $jobId];
|
||||
}
|
||||
|
||||
private function entityDownload(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);
|
||||
}
|
||||
|
||||
// 'part' is optional — null means full RFC 822 message download
|
||||
$part = isset($data['part']) && is_array($data['part']) ? $data['part'] : null;
|
||||
|
||||
$target = ResourceIdentifier::fromString($data['target']);
|
||||
$logger = $this->logger;
|
||||
|
||||
$result = $this->mailManager->entityDownload($tenantId, $userId, $target, $part);
|
||||
|
||||
$filename = $result->filename();
|
||||
$asciiFilename = preg_replace('/[^\x20-\x7E]|[\\\\"]/', '_', $filename);
|
||||
$encodedFilename = rawurlencode($filename);
|
||||
$disposition = sprintf(
|
||||
'attachment; filename="%s"; filename*=UTF-8\'\'%s',
|
||||
$asciiFilename,
|
||||
$encodedFilename,
|
||||
);
|
||||
|
||||
$responseGenerator = (static function () use ($result, $logger): \Generator {
|
||||
try {
|
||||
yield from $result->stream();
|
||||
} catch (\Throwable $t) {
|
||||
$logger->error('Error streaming entity download', ['exception' => $t]);
|
||||
// Headers already sent — cannot change status code; stop output cleanly
|
||||
}
|
||||
})();
|
||||
|
||||
return new StreamedResponse($responseGenerator, 200, [
|
||||
'Content-Disposition' => $disposition,
|
||||
'Content-Type' => $result->mimeType(),
|
||||
'Content-Transfer-Encoding' => 'binary',
|
||||
'Cache-Control' => 'no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use KTXF\Mail\Service\ServiceCollectionMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceConfigurableInterface;
|
||||
use KTXF\Mail\Service\ServiceEntityMutableInterface;
|
||||
use KTXF\Mail\Service\ServiceMutableInterface;
|
||||
use KTXF\Resource\BinaryResource;
|
||||
use KTXF\Resource\Filter\IFilter;
|
||||
use KTXF\Resource\Identifier\CollectionIdentifier;
|
||||
use KTXF\Resource\Identifier\EntityIdentifier;
|
||||
@@ -951,6 +952,13 @@ class Manager {
|
||||
}
|
||||
}
|
||||
|
||||
public function entityDownload(string $tenantId, string $userId, EntityIdentifier $targetEntity, array|null $targetPart): BinaryResource {
|
||||
// retrieve service
|
||||
$service = $this->serviceFetch($tenantId, $userId, $targetEntity->provider(), $targetEntity->service());
|
||||
// download entity
|
||||
return $service->entityDownload($targetEntity, $targetPart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if messages exist
|
||||
*
|
||||
@@ -1294,5 +1302,4 @@ class Manager {
|
||||
return $operationOutcome;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
152
package-lock.json
generated
152
package-lock.json
generated
@@ -242,9 +242,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.130.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
|
||||
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
|
||||
"version": "0.133.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
|
||||
"integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -259,9 +259,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
|
||||
"integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -276,9 +276,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-arm64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
|
||||
"integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -293,9 +293,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-darwin-x64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
|
||||
"integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -310,9 +310,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-freebsd-x64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
|
||||
"integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
|
||||
"integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -327,9 +327,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
|
||||
"integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
|
||||
"integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -344,9 +344,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
|
||||
"integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -361,9 +361,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
|
||||
"integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -378,9 +378,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
|
||||
"integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -395,9 +395,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
|
||||
"integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -412,9 +412,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
|
||||
"integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
|
||||
"integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -429,9 +429,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-linux-x64-musl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
|
||||
"integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
|
||||
"integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -446,9 +446,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-openharmony-arm64": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
|
||||
"integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
|
||||
"integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -463,9 +463,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-wasm32-wasi": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
|
||||
"integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
|
||||
"integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
@@ -482,9 +482,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
|
||||
"integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -499,9 +499,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
|
||||
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
|
||||
"integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1930,13 +1930,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
|
||||
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
|
||||
"integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@oxc-project/types": "=0.130.0",
|
||||
"@oxc-project/types": "=0.133.0",
|
||||
"@rolldown/pluginutils": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -1946,21 +1946,21 @@
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rolldown/binding-android-arm64": "1.0.1",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.1",
|
||||
"@rolldown/binding-darwin-x64": "1.0.1",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.1",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.1",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.1",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.1",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.1",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.1",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.1",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.1",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.1",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.1",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.1"
|
||||
"@rolldown/binding-android-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-arm64": "1.0.3",
|
||||
"@rolldown/binding-darwin-x64": "1.0.3",
|
||||
"@rolldown/binding-freebsd-x64": "1.0.3",
|
||||
"@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-arm64-musl": "1.0.3",
|
||||
"@rolldown/binding-linux-ppc64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-s390x-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-gnu": "1.0.3",
|
||||
"@rolldown/binding-linux-x64-musl": "1.0.3",
|
||||
"@rolldown/binding-openharmony-arm64": "1.0.3",
|
||||
"@rolldown/binding-wasm32-wasi": "1.0.3",
|
||||
"@rolldown/binding-win32-arm64-msvc": "1.0.3",
|
||||
"@rolldown/binding-win32-x64-msvc": "1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/scule": {
|
||||
@@ -2083,9 +2083,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
||||
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
@@ -2177,17 +2177,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.0.13",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
|
||||
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
|
||||
"version": "8.0.16",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
|
||||
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lightningcss": "^1.32.0",
|
||||
"picomatch": "^4.0.4",
|
||||
"postcss": "^8.5.14",
|
||||
"rolldown": "1.0.1",
|
||||
"tinyglobby": "^0.2.16"
|
||||
"postcss": "^8.5.15",
|
||||
"rolldown": "1.0.3",
|
||||
"tinyglobby": "^0.2.17"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Entity management service
|
||||
*/
|
||||
|
||||
import { transceivePost, transceiveStream } from './transceive';
|
||||
import { transceivePost, transceiveStream, transceiveDownload } from './transceive';
|
||||
import type {
|
||||
EntityFetchRequest,
|
||||
EntityFetchResponse,
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
EntityListBulkRequest,
|
||||
EntityPatchResponse,
|
||||
EntityPatchRequest,
|
||||
EntityDownloadRequest,
|
||||
} from '../types/entity';
|
||||
import { useIntegrationStore } from '@KTXC/stores/integrationStore';
|
||||
import { EntityObject } from '../models';
|
||||
@@ -110,7 +111,7 @@ export const entityService = {
|
||||
|
||||
// Convert response to EntityObject instances
|
||||
const list: Record<string, EntityObject> = {};
|
||||
Object.entries(response).forEach(([identifier, entity]) => {
|
||||
Object.entries(response).forEach(([, entity]) => {
|
||||
list[entity.identifier] = createEntityObject(entity);
|
||||
});
|
||||
|
||||
@@ -216,6 +217,16 @@ export const entityService = {
|
||||
async transmit(request: EntityTransmitRequest): Promise<EntityTransmitResponse> {
|
||||
return await transceivePost<EntityTransmitRequest, EntityTransmitResponse>('entity.transmit', request);
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit a browser-native attachment download request.
|
||||
*
|
||||
* The backend download endpoint is expected to honor the supplied selector
|
||||
* and respond with an attachment payload rather than JSON.
|
||||
*/
|
||||
download(request: EntityDownloadRequest): { transaction: string } {
|
||||
return transceiveDownload<EntityDownloadRequest>('entity.download', request);
|
||||
},
|
||||
};
|
||||
|
||||
export default entityService;
|
||||
|
||||
@@ -141,3 +141,84 @@ export async function transceiveStream<TRequest, TData>(
|
||||
|
||||
return { total };
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a browser-native file download request via a top-level form POST.
|
||||
*
|
||||
* This avoids buffering the response body in application-managed JavaScript
|
||||
* memory. The backend is expected to accept form fields where `data` contains
|
||||
* the serialized operation payload and to return an attachment response.
|
||||
*/
|
||||
export function transceiveDownload<TRequest>(
|
||||
operation: string,
|
||||
data: TRequest,
|
||||
user?: string,
|
||||
): { transaction: string } {
|
||||
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
||||
throw new Error('Browser window is not available for download submission');
|
||||
}
|
||||
|
||||
const request: ApiRequest<TRequest> = {
|
||||
version: API_VERSION,
|
||||
transaction: generateTransactionId(),
|
||||
operation,
|
||||
data,
|
||||
user,
|
||||
};
|
||||
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = API_URL;
|
||||
form.target = '_blank';
|
||||
form.style.display = 'none';
|
||||
|
||||
appendHiddenField(form, 'version', String(request.version));
|
||||
appendHiddenField(form, 'transaction', request.transaction);
|
||||
appendHiddenField(form, 'operation', request.operation);
|
||||
appendFormValue(form, 'data', request.data);
|
||||
|
||||
if (request.user) {
|
||||
appendHiddenField(form, 'user', request.user);
|
||||
}
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
form.remove();
|
||||
|
||||
return { transaction: request.transaction };
|
||||
}
|
||||
|
||||
function appendHiddenField(form: HTMLFormElement, name: string, value: string): void {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = name;
|
||||
input.value = value;
|
||||
form.appendChild(input);
|
||||
}
|
||||
|
||||
function appendFormValue(form: HTMLFormElement, name: string, value: unknown): void {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
appendHiddenField(form, name, '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => {
|
||||
appendFormValue(form, `${name}[${index}]`, item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
Object.entries(value as Record<string, unknown>).forEach(([key, nestedValue]) => {
|
||||
appendFormValue(form, `${name}[${key}]`, nestedValue);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
appendHiddenField(form, name, String(value));
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import { defineStore } from 'pinia'
|
||||
import { entityService } from '../services'
|
||||
import { EntityObject, MessageObject } from '../models'
|
||||
import type {
|
||||
EntityListStreamRequest,
|
||||
EntityBlobSelector,
|
||||
EntityDownloadRequest,
|
||||
EntityTransmitRequest,
|
||||
EntityTransmitResponse,
|
||||
} from '../types/entity'
|
||||
@@ -18,7 +19,7 @@ import type {
|
||||
ListRange,
|
||||
ListSort,
|
||||
} from '../types/common'
|
||||
import type { MessageInterface } from '@/types/message'
|
||||
import type { MessageInterface, MessagePartInterface } from '@/types/message'
|
||||
|
||||
export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
||||
// State
|
||||
@@ -184,15 +185,15 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
||||
const response = await entityService.delta({ sources })
|
||||
|
||||
// Process delta and update store
|
||||
Object.entries(response).forEach(([provider, providerData]) => {
|
||||
Object.entries(response).forEach(([, providerData]) => {
|
||||
// Skip if no changes for provider
|
||||
if (providerData === false) return
|
||||
|
||||
Object.entries(providerData).forEach(([service, serviceData]) => {
|
||||
Object.entries(providerData).forEach(([, serviceData]) => {
|
||||
// Skip if no changes for service
|
||||
if (serviceData === false) return
|
||||
|
||||
Object.entries(serviceData).forEach(([collection, collectionData]) => {
|
||||
Object.entries(serviceData).forEach(([, collectionData]) => {
|
||||
// Skip if no changes for collection
|
||||
if (collectionData === false) return
|
||||
|
||||
@@ -474,6 +475,39 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function download(target: EntityIdentifier, part?: Partial<MessagePartInterface>) {
|
||||
let targetPart: EntityBlobSelector | undefined = undefined
|
||||
if (part && (part.blobId || part.partId || part.cid)) {
|
||||
targetPart = {
|
||||
blobId: part.blobId ?? undefined,
|
||||
partId: part.partId ?? undefined,
|
||||
cid: part.cid ?? undefined,
|
||||
}
|
||||
}
|
||||
|
||||
let filename: string
|
||||
if (part && part.name && part.name.trim().length > 0) {
|
||||
filename = part.name.trim()
|
||||
} else if (part) {
|
||||
filename = `attachment-${part.partId || part.blobId || part.cid || 'unknown'}`
|
||||
} else {
|
||||
filename = 'message.eml'
|
||||
}
|
||||
|
||||
const request: EntityDownloadRequest = {
|
||||
target,
|
||||
part: targetPart,
|
||||
filename,
|
||||
}
|
||||
|
||||
try {
|
||||
entityService.download(request)
|
||||
} catch (error: any) {
|
||||
console.error('[Mail Manager][Store] - Failed to submit attachment download:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Return public API
|
||||
return {
|
||||
// State (readonly)
|
||||
@@ -495,5 +529,6 @@ export const useEntitiesStore = defineStore('mailEntitiesStore', () => {
|
||||
delta,
|
||||
move,
|
||||
transmit,
|
||||
download,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -205,4 +205,32 @@ export interface EntityTransmitRequest {
|
||||
export interface EntityTransmitResponse {
|
||||
id: string;
|
||||
status: 'queued' | 'sent';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity Blob Fetch
|
||||
*/
|
||||
export interface EntityBlobSelector {
|
||||
blobId?: string;
|
||||
partId?: string;
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
export interface EntityDownloadRequest {
|
||||
target: EntityIdentifier;
|
||||
part?: EntityBlobSelector;
|
||||
filename?: string | null;
|
||||
}
|
||||
|
||||
export interface EntityBlobsRequest {
|
||||
target: EntityIdentifier;
|
||||
parts: EntityBlobSelector[];
|
||||
}
|
||||
|
||||
export interface EntityBlobsResult {
|
||||
source: EntityIdentifier;
|
||||
part: EntityBlobSelector;
|
||||
blob: Blob;
|
||||
}
|
||||
|
||||
export interface EntityBlobsResponse extends Array<EntityBlobsResult> {}
|
||||
Reference in New Issue
Block a user