mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: allow privileged plugin to fetch only a range of blob
This commit is contained in:
@@ -273,7 +273,7 @@ export interface IJMAPClient {
|
||||
getBlobDownloadUrl(blobId: string, name?: string, type?: string, accountId?: string): string;
|
||||
fetchBlob(blobId: string, name?: string, type?: string, accountId?: string): Promise<Blob>;
|
||||
fetchBlobAsObjectUrl(blobId: string, name?: string, type?: string, accountId?: string): Promise<string>;
|
||||
fetchBlobArrayBuffer(blobId: string, name?: string, type?: string, accountId?: string): Promise<ArrayBuffer>;
|
||||
fetchBlobArrayBuffer(blobId: string, name?: string, type?: string, accountId?: string, rangeHeader?: number): Promise<ArrayBuffer>;
|
||||
downloadBlob(blobId: string, name?: string, type?: string, accountId?: string): Promise<void>;
|
||||
|
||||
// ── Identities ────────────────────────────────────────────────
|
||||
|
||||
+48
-3
@@ -6751,13 +6751,58 @@ export class JMAPClient implements IJMAPClient {
|
||||
// ── S/MIME raw-email helpers ─────────────────────────────────────
|
||||
|
||||
/** Fetch blob content as an ArrayBuffer (for S/MIME byte processing). */
|
||||
async fetchBlobArrayBuffer(blobId: string, name?: string, type?: string, accountId?: string): Promise<ArrayBuffer> {
|
||||
async fetchBlobArrayBuffer(blobId: string, name?: string, type?: string, accountId?: string, rangeHeader?: number): Promise<ArrayBuffer> {
|
||||
const url = this.getBlobDownloadUrl(blobId, name, type, accountId);
|
||||
const response = await this.authenticatedFetch(url, {}, { timeoutMs: JMAPClient.TRANSFER_TIMEOUT_MS });
|
||||
const response = await this.authenticatedFetch(url, { }, { timeoutMs: JMAPClient.TRANSFER_TIMEOUT_MS });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch blob: ${response.status}`);
|
||||
}
|
||||
return response.arrayBuffer();
|
||||
|
||||
const maxBytes = rangeHeader;
|
||||
|
||||
// If no Range header requested or stream is missing, fallback to standard arrayBuffer()
|
||||
if (!maxBytes || !response.body) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
// used to download only the first N bytes of a blob, e.g. for S/MIME /PGP encryption verification
|
||||
// Currently, Stalwart server does not support Range requests for the download endpoint,
|
||||
// so we have to read the stream and cancel it after reading the first N bytes.
|
||||
// Stream reading with early termination via reader.cancel()
|
||||
const reader = response.body.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
let bytesRead = 0;
|
||||
|
||||
try {
|
||||
while (bytesRead < maxBytes) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
chunks.push(value);
|
||||
bytesRead += value.length;
|
||||
}
|
||||
} finally {
|
||||
// Cancel the response body stream to close the network connection early
|
||||
await reader.cancel().catch(() => {});
|
||||
}
|
||||
|
||||
// Slice exact requested length and copy chunks into a contiguous ArrayBuffer
|
||||
const totalLength = Math.min(bytesRead, maxBytes);
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
|
||||
// Iterate over each chunk of binary data (Uint8Array) collected during the stream
|
||||
for (const chunk of chunks) {
|
||||
// Determine how many bytes to copy from the current chunk without exceeding totalLength
|
||||
const bytesToCopy = Math.min(chunk.length, totalLength - offset);
|
||||
// Copy the needed slice from the chunk into the target array starting at the current offset
|
||||
result.set(chunk.subarray(0, bytesToCopy), offset);
|
||||
// Move the offset forward by the number of bytes just written
|
||||
offset += bytesToCopy;
|
||||
// Stop processing further chunks if the target size limit has been reached
|
||||
if (offset >= totalLength) break;
|
||||
}
|
||||
|
||||
return result.buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -421,11 +421,11 @@ async function doHttpFetch(plugin: InstalledPlugin, rawUrl: string, init?: Plugi
|
||||
* exposes the byte-fetch primitive. Returns a Uint8Array (structured-cloneable
|
||||
* across the postMessage boundary).
|
||||
*/
|
||||
async function doJmapFetchBlob(blobId: string, opts?: { name?: string; type?: string }): Promise<Uint8Array> {
|
||||
async function doJmapFetchBlob(blobId: string, opts?: { name?: string; type?: string, rangeHeader?: number }): Promise<Uint8Array> {
|
||||
if (typeof blobId !== 'string' || !blobId) throw new Error('jmap.fetchBlob: blobId required');
|
||||
const { client } = useAuthStore.getState();
|
||||
if (!client) throw new Error('jmap.fetchBlob: no active session');
|
||||
const buf = await client.fetchBlobArrayBuffer(blobId, opts?.name, opts?.type);
|
||||
const buf = await client.fetchBlobArrayBuffer(blobId, opts?.name, opts?.type, undefined, opts?.rangeHeader);
|
||||
return new Uint8Array(buf);
|
||||
}
|
||||
|
||||
@@ -1159,7 +1159,7 @@ export async function dispatchApiCall(
|
||||
case 'http.post': return doHttpPost(plugin, args[0] as string, args[1], args[2] as PluginHttpPostOptions | undefined);
|
||||
case 'http.fetch': return doHttpFetch(plugin, args[0] as string, args[1] as PluginFetchInit | undefined);
|
||||
|
||||
case 'jmap.fetchBlob': return doJmapFetchBlob(args[0] as string, args[1] as { name?: string; type?: string } | undefined);
|
||||
case 'jmap.fetchBlob': return doJmapFetchBlob(args[0] as string, args[1] as { name?: string; type?: string, rangeHeader?: number } | undefined);
|
||||
case 'jmap.uploadBlob': return doJmapUploadBlob(args[0] as Uint8Array, args[1] as string, args[2] as string);
|
||||
case 'jmap.sendRaw': return doJmapSendRaw(
|
||||
args[0] as ArrayBuffer | ArrayBufferView,
|
||||
|
||||
@@ -253,7 +253,7 @@ function buildPluginApi(manifest: PluginManifest) {
|
||||
removeKeyword: (emailId: string, keyword: string, accountId?: string) =>
|
||||
callApi('jmap.removeKeyword', [emailId, keyword, accountId]) as Promise<void>,
|
||||
/** Fetch a blob's raw bytes by id. Resolves to a Uint8Array. */
|
||||
fetchBlob: (blobId: string, opts?: { name?: string; type?: string }) =>
|
||||
fetchBlob: (blobId: string, opts?: { name?: string; type?: string, rangeHeader?: number }) =>
|
||||
callApi('jmap.fetchBlob', [blobId, opts]) as Promise<Uint8Array>,
|
||||
uploadBlob: (content: Uint8Array, name: string, type: string) =>
|
||||
callApi('jmap.uploadBlob', [content, name, type]) as Promise<{ blobId: string; size: number; type: string; }>,
|
||||
|
||||
Reference in New Issue
Block a user