From d3addf54b40c634a179308096a91030ceaf12458 Mon Sep 17 00:00:00 2001 From: Stefan Hildebrandt <695494+hildebrandttk@users.noreply.github.com> Date: Sat, 11 Jul 2026 10:22:48 +0200 Subject: [PATCH] fix(rebase): reconcile viewer blob routing and dedupe settings after main rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebasing feat/unified-mailbox onto main hit deep, divergent conflicts in the mail-view/settings area (main added its own All-Mail + RTL refactor + a username-keyed #507 identity impl). Post-rebase reconciliation: - re-apply the cross-account blob routing to the message viewer (inline images, drag-out, TNEF, embedded messages, thumbnails, bundle download) on main's restructured file — every fetch goes through blobClient/blobAccountId derived from the message's source account; - drop the duplicate `preferredIdentityIds` declaration that both main (username-keyed) and the branch (accountId-keyed) introduced — the branch's account-scoped map is kept, matching the resolved modal/store logic. tsc + eslint clean; unified-mailbox unit tests pass (settings-store all-mail / preferred-identity, unified-mailbox-cross, jmap-client-resilience, migrate-policy). --- components/email/email-viewer.tsx | 56 +++++++++++++++++++------------ stores/settings-store.ts | 9 ----- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index feee8aaf..45c3b51e 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -560,6 +560,8 @@ export function ContactSidebarPanel({ interface DraggableAttachmentChipProps { attachment: EffectiveAttachment; client: IJMAPClient | null; + /** Owner accountId for the blob when it lives in a delegated/shared account. */ + accountId?: string; enabled: boolean; downloadName?: string; children: (dragProps: { @@ -570,14 +572,14 @@ interface DraggableAttachmentChipProps { }) => React.ReactNode; } -function DraggableAttachmentChip({ attachment, client, enabled, downloadName, children }: DraggableAttachmentChipProps) { +function DraggableAttachmentChip({ attachment, client, accountId, enabled, downloadName, children }: DraggableAttachmentChipProps) { const source = useMemo(() => ({ name: downloadName || attachment.name || 'download', type: attachment.type || 'application/octet-stream', getBlobUrl: async () => { if (attachment.blobId && client) { try { - return await client.fetchBlobAsObjectUrl(attachment.blobId, attachment.name || undefined, attachment.type); + return await client.fetchBlobAsObjectUrl(attachment.blobId, attachment.name || undefined, attachment.type, accountId); } catch { return null; } @@ -595,7 +597,7 @@ function DraggableAttachmentChip({ attachment, client, enabled, downloadName, ch } return null; }, - }), [attachment, client, downloadName]); + }), [attachment, client, accountId, downloadName]); const drag = useAttachmentDrag(source, enabled); return <>{children(drag)}; } @@ -714,6 +716,18 @@ export function EmailViewer({ const { tabletListVisible } = useUIStore(); const { identities, client, isDemoMode, activeAccountId } = useAuthStore(); const activeAccount = useAccountStore((s) => s.accounts.find((a) => a.id === activeAccountId)); + // Blobs (inline images, drag-out, TNEF, embedded messages, thumbnails, bundle + // downloads) are account-scoped. In the unified / All-Mail view the open + // message may belong to another login (route to its client) or a delegated + // shared account (same client, owner accountId in the URL). Resolve both from + // the message's source so cross-account blob fetches don't 404 against the + // active account. + const isUnifiedView = useEmailStore((s) => s.isUnifiedView); + const blobClient = useMemo(() => { + const scid = isUnifiedView ? email?.sourceClientAccountId : undefined; + return (scid ? useAuthStore.getState().getClientForAccount(scid) : null) ?? client; + }, [isUnifiedView, email?.sourceClientAccountId, client]); + const blobAccountId = isUnifiedView ? email?.sourceAccountId : undefined; // List-Unsubscribe mailto: send the message ourselves - this is a webmail // client, handing a mailto: URL to the OS mail handler goes nowhere for @@ -1250,7 +1264,7 @@ export function EmailViewer({ async function processTnef() { try { debug.time('TNEF fetch blob', 'email'); - const blobBytes = await client!.fetchBlobArrayBuffer(tnefAtt!.blobId!); + const blobBytes = await blobClient!.fetchBlobArrayBuffer(tnefAtt!.blobId!, undefined, undefined, blobAccountId); debug.timeEnd('TNEF fetch blob', 'email'); debug.log('email', 'TNEF: Fetched blob, size:', blobBytes.byteLength, 'bytes'); @@ -1303,7 +1317,7 @@ export function EmailViewer({ processTnef(); return () => { cancelled = true; }; - }, [email, client]); + }, [email, client, blobClient, blobAccountId]); // Embedded message/rfc822 unwrapping // When Outlook forwards an email as an attachment, the outer email body is @@ -1340,7 +1354,7 @@ export function EmailViewer({ async function unwrapEmbedded() { try { - const blobBytes = await client!.fetchBlobArrayBuffer(rfc822Att!.blobId!); + const blobBytes = await blobClient!.fetchBlobArrayBuffer(rfc822Att!.blobId!, undefined, undefined, blobAccountId); if (cancelled) { debug.groupEnd(); return; } if (blobBytes.byteLength === 0) { debug.warn('email', 'Embedded RFC822: Fetched blob is empty'); @@ -1380,7 +1394,7 @@ export function EmailViewer({ unwrapEmbedded(); return () => { cancelled = true; }; - }, [email, client]); + }, [email, client, blobClient, blobAccountId]); // Fetch inline CID images with authentication to prevent browser auth dialogs useEffect(() => { @@ -1426,7 +1440,7 @@ export function EmailViewer({ await Promise.all(cidAttachments.map(async (att) => { const cidValue = att.cid!.replace(/^<|>$/g, ''); try { - const objectUrl = await client!.fetchBlobAsObjectUrl(att.blobId, att.name || 'inline', att.type); + const objectUrl = await blobClient!.fetchBlobAsObjectUrl(att.blobId, att.name || 'inline', att.type, blobAccountId); if (!cancelled) { urls[cidValue] = objectUrl; objectUrls.push(objectUrl); @@ -1448,7 +1462,7 @@ export function EmailViewer({ cancelled = true; objectUrls.forEach(url => URL.revokeObjectURL(url)); }; - }, [client, email?.id, pluginRenderedAttachments, email?.attachments]); + }, [client, blobClient, blobAccountId, email?.id, pluginRenderedAttachments, email?.attachments]); const effectiveAttachments = useMemo(() => { if (pluginRenderedAttachments.length > 0) { @@ -1926,8 +1940,8 @@ export function EmailViewer({ for (const attachment of effectiveAttachments) { const entryName = uniqueName(getAttachmentDisplayName(attachment.name, attachment.type)); try { - if (attachment.blobId && client) { - const blob = await client.fetchBlob(attachment.blobId, attachment.name || entryName, attachment.type); + if (attachment.blobId && blobClient) { + const blob = await blobClient.fetchBlob(attachment.blobId, attachment.name || entryName, attachment.type, blobAccountId); zip.file(entryName, blob); added++; } else if (attachment.tnefData) { @@ -1959,7 +1973,7 @@ export function EmailViewer({ } finally { setIsDownloadingAll(false); } - }, [isDownloadingAll, effectiveAttachments, client, email]); + }, [isDownloadingAll, effectiveAttachments, blobClient, blobAccountId, email]); // Shared "Download all" chip, shown only when bundling is worthwhile (2+). const downloadAllButton = effectiveAttachments.length > 1 ? ( @@ -2001,8 +2015,8 @@ export function EmailViewer({ await Promise.all(imageAttachments.map(async (att) => { let url: string | undefined; try { - if (att.blobId && client) { - url = await client.fetchBlobAsObjectUrl(att.blobId, att.name || 'thumb', att.type); + if (att.blobId && blobClient) { + url = await blobClient.fetchBlobAsObjectUrl(att.blobId, att.name || 'thumb', att.type, blobAccountId); } else if (att.decryptedAttachment) { const bytes = getAttachmentContentBytes(att.decryptedAttachment); if (!bytes || bytes.byteLength === 0) return; @@ -2033,7 +2047,7 @@ export function EmailViewer({ cancelled = true; createdUrls.forEach((url) => URL.revokeObjectURL(url)); }; - }, [effectiveAttachments, client, attachmentImagePreviewsEnabled]); + }, [effectiveAttachments, client, blobClient, blobAccountId, attachmentImagePreviewsEnabled]); // Iframe for rendering HTML emails true-to-life const iframeRef = useRef(null); @@ -3693,7 +3707,7 @@ export function EmailViewer({ const opensPreview = isPreviewable && mailAttachmentAction === 'preview'; const thumbUrl = imageThumbUrls[attachment.id]; return ( - + {(dragProps) => (
+ {(dragProps) => (
+ {(dragProps) => (
+ {(dragProps) => (
+ {(dragProps) => (
+ {(dragProps) => (
; // Privacy & Security sessionTimeout: number; // minutes (0 = never) @@ -406,8 +400,6 @@ const DEFAULT_SETTINGS = { requestReadReceiptDefault: false, readReceiptResponse: 'ask' as ReadReceiptResponse, - // Identities - preferredIdentityIds: {} as Record, // Privacy & Security sessionTimeout: 0, // Never @@ -619,7 +611,6 @@ export const useSettingsStore = create()( signatureSeparatorEnabled: state.signatureSeparatorEnabled, requestReadReceiptDefault: state.requestReadReceiptDefault, readReceiptResponse: state.readReceiptResponse, - preferredIdentityIds: state.preferredIdentityIds, sessionTimeout: state.sessionTimeout, emailNotificationsEnabled: state.emailNotificationsEnabled, emailNotificationSound: state.emailNotificationSound,