diff --git a/lib/plugin-sandbox/host-api.ts b/lib/plugin-sandbox/host-api.ts index 621d3ca93..529d28255 100644 --- a/lib/plugin-sandbox/host-api.ts +++ b/lib/plugin-sandbox/host-api.ts @@ -27,6 +27,7 @@ import { EncryptionAtRestConfig, PublicKeyInfo, PublicKeyInput, useAccountSecuri */ const PRIVILEGED_ONLY_METHODS = new Set([ 'jmap.fetchBlob', + 'jmap.uploadBlob', 'jmap.sendRaw', 'jmap.submitRaw', 'jmap.importRaw', @@ -66,6 +67,7 @@ const PERM_PER_METHOD: Record = { 'http.fetch': 'http:fetch', // jmap (privileged-tier only; see PRIVILEGED_ONLY_METHODS) 'jmap.fetchBlob': 'email:blob-read', + 'jmap.uploadBlob': 'email:blob-write', 'jmap.sendRaw': 'email:raw-send', 'jmap.submitRaw': 'email:raw-send', 'jmap.importRaw': 'email:raw-send', @@ -394,6 +396,13 @@ async function doJmapFetchBlob(blobId: string, opts?: { name?: string; type?: st return new Uint8Array(buf); } +async function doJmapUploadBlob(content: Uint8Array, name: string, type: string): Promise<{ blobId: string; size: number; type: string; }> { + const { client } = useAuthStore.getState(); + if (!client) throw new Error('jmap.uploadBlob: no active session'); + const file = new File([content as BlobPart], name, { type }); + return await client.uploadBlob(file); +} + interface JmapSubmitRawOptions { delayedUntil?: string; envelopeRecipients?: string[]; @@ -860,6 +869,7 @@ export async function dispatchApiCall( 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.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, args[1] as string, diff --git a/lib/plugin-sandbox/protocol.ts b/lib/plugin-sandbox/protocol.ts index 7604b88a6..0ab19f001 100644 --- a/lib/plugin-sandbox/protocol.ts +++ b/lib/plugin-sandbox/protocol.ts @@ -242,7 +242,7 @@ export const API_METHODS = [ 'storage.get', 'storage.set', 'storage.remove', 'storage.keys', 'http.post', 'http.fetch', 'crypto.getOrCreateWebAuthn', 'crypto.getPublicKeys', 'crypto.createPublicKey', 'crypto.removePublicKey', 'crypto.getEncryptionAtRest', 'crypto.setEncryptionAtRest', - 'jmap.fetchBlob', 'jmap.sendRaw', + 'jmap.fetchBlob', 'jmap.uploadBlob', 'jmap.sendRaw', 'upfiles.get', 'upfiles.save', 'contact.get', 'contact.update', 'contact.create', 'contact.search', 'admin.getConfig', 'admin.getAllConfig', 'admin.setConfig', 'admin.deleteConfig', diff --git a/lib/plugin-sandbox/runtime.tsx b/lib/plugin-sandbox/runtime.tsx index a6f52a5bb..432c2d6d1 100644 --- a/lib/plugin-sandbox/runtime.tsx +++ b/lib/plugin-sandbox/runtime.tsx @@ -206,6 +206,8 @@ function buildPluginApi(manifest: PluginManifest) { /** Fetch a blob's raw bytes by id. Resolves to a Uint8Array. */ fetchBlob: (blobId: string, opts?: { name?: string; type?: string }) => callApi('jmap.fetchBlob', [blobId, opts]) as Promise, + uploadBlob: (content: Uint8Array, name: string, type: string) => + callApi('jmap.uploadBlob', [content, name, type]) as Promise<{ blobId: string; size: number; type: string; }>, /** Submit a fully-formed raw RFC822 message (already signed/encrypted). */ sendRaw: ( rawBytes: ArrayBuffer | ArrayBufferView,