feat: tell plugins when the open message's attachments can't be read from this login

This commit is contained in:
Linus Rath
2026-11-03 00:33:59 +01:00
parent 9ce4d01807
commit d68970126e
5 changed files with 40 additions and 5 deletions
@@ -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<string | symbol, unknown>, {
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(<EmailViewer email={message} isLoading={false} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" onDelete={() => {}} 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(<EmailViewer email={message} isLoading={false} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" onDelete={() => {}} 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 });
+5 -2
View File
@@ -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.
+6 -1
View File
@@ -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<string, string> = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
@@ -50,6 +50,11 @@ export function cidAltTexts(email: Pick<Email, 'htmlBody' | 'bodyValues'>): 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<string, string>): AttachmentExtrasB {
if (!part.cid) return {};
+11
View File
@@ -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 (`<img src="cid:…">`), without angle brackets. */
+2 -2
View File
@@ -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