diff --git a/PLUGIN_COMPOSER_SNAPSHOTS.md b/PLUGIN_COMPOSER_SNAPSHOTS.md deleted file mode 100644 index 7a9e6f109..000000000 --- a/PLUGIN_COMPOSER_SNAPSHOTS.md +++ /dev/null @@ -1,27 +0,0 @@ -# Composer snapshots and conditional edits - -These methods operate on a composer id handed to a plugin by a native composer action or slot. They do not discover other composers or grant permission to send a message. - -| Method | Permission | Result | -| --- | --- | --- | -| `composer.getSnapshot(composerId)` | `compose:read` | Authored text, subject, To/Cc/Bcc, ready attachment metadata, pending state and an opaque revision. | -| `composer.readAttachment(composerId, attachmentId)` | `email:blob-read` | Bytes of an attachment in that composer's snapshot, at most 25 MiB. | -| `composer.replaceBody(composerId, content, { expectedRevision })` | `compose:write` | One native editor transaction, rejected with `composer_changed` when the revision differs. | - -Snapshot text excludes the signature and quoted history. The opaque revision covers authored content, recipients, subject, attachment metadata, pending state, account and identity. Account and identity markers stay host-side. Snapshot arrays are copies. - -Attachment ids are composer-local opaque handles, not server blob ids. Two logins with the same blob id do not collide. The native composer uses the attachment's owning client. Metadata and actual bytes are limited separately, and bytes are discarded if the composer closes or its revision changes during the read. - -`expectedRevision` is optional, preserving existing unconditional edits. When supplied it is checked immediately before the synchronous editor transaction. Existing native Undo, signature, quote and inline-image behavior remains in use. - -```js -const draft = await api.composer.getSnapshot(composerId); -const text = await prepareText(draft.text); -await api.composer.replaceBody(composerId, { text }, { - expectedRevision: draft.revision, -}); -``` - -This change is the composer SDK portion approved in [#1369](https://github.com/bulwarkmail/webmail/issues/1369#issuecomment-6506321971). Quick-reply registration and toolbar actions are a separate change. No product route, companion authentication, service configuration or dependency is introduced. Lite's existing plugin policy is unchanged. - -작성창의 읽기 권한과 발송 권한을 분리하고, 처리 중 변경된 초안을 덮어쓰지 않도록 리비전을 검사합니다. 제품의 모델 호출·설정·인증은 플러그인이 소유합니다. diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index ca050a71a..dbab1a316 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -2977,7 +2977,6 @@ export function EmailComposer({ pluginAttachRef.current = handlePluginAttachmentAdd; const pluginAttachmentIdsRef = useRef(new WeakMap()); // Read live draft data without re-registering the composer on every edit. - // 매번 작성창을 재등록하지 않고 최신 초안 데이터를 읽습니다. const pluginData = { readMetadata: () => { const draft = draftSnapshotRef.current(); diff --git a/lib/plugin-sandbox/composer-registry.ts b/lib/plugin-sandbox/composer-registry.ts index ad856da98..7d80717d7 100644 --- a/lib/plugin-sandbox/composer-registry.ts +++ b/lib/plugin-sandbox/composer-registry.ts @@ -33,10 +33,9 @@ export type ComposerMetadata = { }; export interface ComposerHandle { - /** Read this composer's metadata; account and identity stay host-side. - * 이 작성창의 메타데이터만 읽으며 계정과 identity는 호스트에 남습니다. */ + /** Read this composer's metadata; account and identity stay host-side. */ readMetadata?(): ComposerMetadata; - /** Read a file currently attached to this composer. 현재 첨부된 파일만 읽습니다. */ + /** Read a file currently attached to this composer. */ readAttachment?(id: string): Promise; /** The user's own text, without the signature and the quote. */ readBody(): ComposerContent; @@ -52,8 +51,7 @@ export interface ComposerHandle { const handles = new Map(); const snapshots = new Map(); -/** Bind an opaque revision to the current body, recipients, files and account. - * 현재 본문·수신인·첨부·계정에 불투명 리비전을 결합합니다. */ +/** Bind an opaque revision to the current body, recipients, files and account. */ export function captureComposer(id: string): ComposerSnapshot { const handle = handles.get(id); if (!handle) throw new Error('composer is not open any more'); diff --git a/lib/plugin-sandbox/host-api-composer.ts b/lib/plugin-sandbox/host-api-composer.ts index 9df83e4ab..5bce8b043 100644 --- a/lib/plugin-sandbox/host-api-composer.ts +++ b/lib/plugin-sandbox/host-api-composer.ts @@ -66,8 +66,7 @@ export function doComposerReplaceSelection(plugin: InstalledPlugin, id: unknown, return handle.replaceSelection(html, text, { below }); } -/** Optionally reject a stale revision before the synchronous editor transaction. - * 선택적으로 오래된 리비전을 거부한 뒤 동기 편집 트랜잭션을 실행합니다. */ +/** Optionally reject a stale revision before the synchronous editor transaction. */ export function doComposerReplaceBody(plugin: InstalledPlugin, id: unknown, content: unknown, options?: unknown): boolean { const method = 'composer.replaceBody'; const handle = handleFor(method, id); @@ -81,16 +80,14 @@ export function doComposerReplaceBody(plugin: InstalledPlugin, id: unknown, cont return handle.replaceBody(html, text); } -/** Read a coherent draft snapshot without granting permission to send it. - * 발송 권한 없이 일관된 작성창 스냅샷을 읽습니다. */ +/** Read a coherent draft snapshot without granting permission to send it. */ export function doComposerGetSnapshot(_plugin: InstalledPlugin, id: unknown): ComposerSnapshot { const composerId = readId(id, 'composer.getSnapshot', 'composerId'); handleFor('composer.getSnapshot', composerId); return captureComposer(composerId); } -/** Bound bytes and membership before and after reading a composer's attachment. - * 작성창 첨부를 읽기 전후에 바이트 크기와 소속을 확인합니다. */ +/** Bound bytes and membership before and after reading a composer's attachment. */ export async function doComposerReadAttachment(_plugin: InstalledPlugin, id: unknown, attachmentId: unknown): Promise { const method = 'composer.readAttachment'; const composerId = readId(id, method, 'composerId'); diff --git a/lib/plugin-sandbox/runtime.tsx b/lib/plugin-sandbox/runtime.tsx index 3bc2bc824..daf0ee0db 100644 --- a/lib/plugin-sandbox/runtime.tsx +++ b/lib/plugin-sandbox/runtime.tsx @@ -619,11 +619,9 @@ function buildPluginApi(manifest: PluginManifest) { // The message being written. `composerId` comes from the composer: the // context of a click on a composer action, or a composer slot's props. composer: { - /** Capture own text and metadata for a conditional edit. Needs compose:read. - * 조건부 편집용 본문·메타데이터를 읽습니다. compose:read 권한이 필요합니다. */ + /** Capture own text and metadata for a conditional edit. Needs compose:read. */ getSnapshot: (composerId: string) => callApi('composer.getSnapshot', [composerId]) as Promise, - /** Read a currently attached file. Needs email:blob-read. - * 현재 첨부된 파일을 읽습니다. email:blob-read 권한이 필요합니다. */ + /** Read a currently attached file. Needs email:blob-read. */ readAttachment: (composerId: string, attachmentId: string) => callApi('composer.readAttachment', [composerId, attachmentId]) as Promise, /** The user's own text (no signature, no quote) as { html, text }. Needs compose:read. */ getBody: (composerId: string) => @@ -638,8 +636,7 @@ function buildPluginApi(manifest: PluginManifest) { */ replaceSelection: (composerId: string, content: { text: string } | { html: string }, options?: { below?: boolean }) => callApi('composer.replaceSelection', [composerId, content, options]) as Promise, - /** Replace own text, optionally only at expectedRevision; signature and quote stay. Needs compose:write. - * 선택적으로 expectedRevision을 확인한 뒤 본문만 수정하며 서명·인용은 보존합니다. compose:write 권한이 필요합니다. */ + /** Replace own text, optionally only at expectedRevision; signature and quote stay. Needs compose:write. */ replaceBody: (composerId: string, content: { text: string } | { html: string }, options?: { expectedRevision: string }) => callApi('composer.replaceBody', [composerId, content, options]) as Promise, /** Attach a blob from jmap.uploadBlob. Needs email:blob-write. */ diff --git a/lib/plugin-types.ts b/lib/plugin-types.ts index 6635e8ff9..fa1a74b5d 100644 --- a/lib/plugin-types.ts +++ b/lib/plugin-types.ts @@ -1344,8 +1344,7 @@ export interface PluginAttachmentUpload { size: number; } -/** Own text and metadata of one open composer, with an opaque edit revision. - * 열린 작성창 하나의 본문·메타데이터와 조건부 편집용 불투명 리비전입니다. */ +/** Own text and metadata of one open composer, with an opaque edit revision. */ export type ComposerSnapshot = { revision: string; subject: string; @@ -1357,8 +1356,7 @@ export type ComposerSnapshot = { pending: boolean; }; -/** Bytes of a file still attached to the selected composer. - * 선택한 작성창에 계속 첨부되어 있는 파일의 바이트입니다. */ +/** Bytes of a file still attached to the selected composer. */ export type ComposerAttachmentData = { name: string; type: string;