diff --git a/components/email/__tests__/viewer-list-selection-prop.test.tsx b/components/email/__tests__/viewer-list-selection-prop.test.tsx index 0a29cb08f..319abb65a 100644 --- a/components/email/__tests__/viewer-list-selection-prop.test.tsx +++ b/components/email/__tests__/viewer-list-selection-prop.test.tsx @@ -77,6 +77,22 @@ describe('the reader panel prop listSelection', () => { expect(seen.length).toBeGreaterThan(renders); }); + it('tells the panel when this login can not read the open message\'s parts', () => { + // An open message makes the viewer ask the login more; nothing of it matters here. + const client = new Proxy({ getAccountId: () => 'acc1' } as Record, { + get: (target, key) => (key in target ? target[key] : key === 'then' ? undefined : () => undefined), + }) as unknown as IJMAPClient; + useAuthStore.setState({ client }); + const message = { ...row('m'), textBody: [], htmlBody: [], attachments: [], bodyValues: {} } as unknown as Email; + const view = () => render( {}} blobSource={{ client: otherLogin, accountId: 'acc-2' }} />); + const otherLogin = { getAccountId: () => 'acc-2' } as unknown as IJMAPClient; + view(); + expect((seen.at(-1)?.email as { blobsReadable?: boolean }).blobsReadable).toBe(false); + cleanup(); + render( {}} blobSource={{ client, accountId: undefined }} />); + expect(seen.at(-1)?.email).not.toHaveProperty('blobsReadable'); + }); + it('gives a panel outside the main list (a Pro tab) no ticks', () => { useAuthStore.setState({ client }); useEmailStore.setState({ emails: [row('a'), row('b')], selectedEmailKeys: new Set(['a', 'b']), selectedMailbox: 'inbox', mailboxes }); diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index 173c89c97..2e5440af1 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -136,6 +136,7 @@ import { useAuthStore } from "@/stores/auth-store"; import { useAccountStore } from "@/stores/account-store"; import { useEmailStore, resolveEmailBlobContext, emailWriteSource, type EmailBlobContext } from "@/stores/email-store"; import { useListSelection } from "@/lib/plugin-sandbox/assistant/b.list-selection"; +import { readability } from "@/lib/plugin-sandbox/assistant/b.projection"; import { useThemeStore } from "@/stores/theme-store"; import { EmailIdentityBadge } from "./email-identity-badge"; import { ExternalSenderChip } from "./external-sender-chip"; @@ -1804,9 +1805,11 @@ export function EmailViewer({ // each slot post its props to the plugin again and re-run shouldShow. const wantsPluginEmailView = hasDetailSidebar || hasDetailsSlotOffers || hasBannerOffers || hasHeaderOffers || hasBelowSubjectOffers || hasAboveQuoteOffers; + // A message of another connected login (unified view, Pro tab): the host + // reads parts only through the signed-in login, so plugins hear they can't. const pluginEmailView = useMemo( - () => (email && wantsPluginEmailView ? emailToReadView(email) : null), - [email, wantsPluginEmailView], + () => (email && wantsPluginEmailView ? { ...emailToReadView(email), ...readability(blobClient === client) } : null), + [email, wantsPluginEmailView, blobClient, client], ); // Reader frames learn the layout: the bar a plugin shows on phones has no // place next to a side panel on a desktop. diff --git a/lib/plugin-sandbox/assistant/b.projection.ts b/lib/plugin-sandbox/assistant/b.projection.ts index f4c338e2d..372a3e00e 100644 --- a/lib/plugin-sandbox/assistant/b.projection.ts +++ b/lib/plugin-sandbox/assistant/b.projection.ts @@ -4,7 +4,7 @@ // name a picture without looking at it. import type { Email } from '@/lib/jmap/types'; -import type { AttachmentExtrasB } from './b.types'; +import type { AttachmentExtrasB, EmailReadViewExtrasB } from './b.types'; const ENTITIES: Record = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' }; @@ -50,6 +50,11 @@ export function cidAltTexts(email: Pick): Map< return out; } +/** EmailReadView.blobsReadable: false when the signed-in login can't read the message's parts, else left out. */ +export function readability(readable: boolean): EmailReadViewExtrasB { + return readable ? {} : { blobsReadable: false }; +} + /** `cid` and `alt` of one attachment, for EmailReadView.attachments (empty for a plain attachment). */ export function attachmentExtras(part: { cid?: string | null }, alts: Map): AttachmentExtrasB { if (!part.cid) return {}; diff --git a/lib/plugin-sandbox/assistant/b.types.ts b/lib/plugin-sandbox/assistant/b.types.ts index e476301b7..d55eeefaa 100644 --- a/lib/plugin-sandbox/assistant/b.types.ts +++ b/lib/plugin-sandbox/assistant/b.types.ts @@ -16,6 +16,17 @@ export interface AreaHostFeaturesB { listSelection?: boolean; } +/** What EmailReadView adds for the assistant. */ +export interface EmailReadViewExtrasB { + /** + * false: this login can't read the message's attachments, text or + * conversation through the host (it belongs to another connected login, + * as in a unified view), so attachment, image and thread reads of it + * fail; offer none. Left out when they can be read. + */ + blobsReadable?: boolean; +} + /** What EmailReadView.attachments adds for an image the message shows inline. */ export interface AttachmentExtrasB { /** The Content-ID the message's HTML refers to (``), without angle brackets. */ diff --git a/lib/plugin-types.ts b/lib/plugin-types.ts index 644e5e6aa..37f1fd915 100644 --- a/lib/plugin-types.ts +++ b/lib/plugin-types.ts @@ -1,7 +1,7 @@ import type { CredentialSpec } from './plugin-secrets/credentials'; import type { CatalogSlotName } from './plugin-slot-catalog'; import { ASSISTANT_PERMISSIONS, type AssistantHostFeatures } from './plugin-sandbox/assistant/types'; -import type { AttachmentExtrasB } from './plugin-sandbox/assistant/b.types'; +import type { AttachmentExtrasB, EmailReadViewExtrasB } from './plugin-sandbox/assistant/b.types'; // Plugin & Theme system types @@ -397,7 +397,7 @@ export interface KeyboardShortcut { // ─── Read-Only View Types ──────────────────────────────────── // Projected views exposed to plugins - no direct store references -export interface EmailReadView { +export interface EmailReadView extends EmailReadViewExtrasB { id: string; /** * The JMAP account the message is in. Message ids repeat across accounts