From 44cdd083a35b145171097bb543fa33184ec16db1 Mon Sep 17 00:00:00 2001 From: Stefan Hildebrandt <695494+hildebrandttk@users.noreply.github.com> Date: Mon, 10 Aug 2026 23:18:31 +0200 Subject: [PATCH] fix(identities): namespace all accounts consistently in the Pro composer Follow-up to the calendar/contacts consistent-namespacing fix. The multi-account identity aggregation left the ACTIVE account's identity ids raw while namespacing the others, so an identity id changed form when the active account changed. Now every aggregated account is namespaced (`::`), matching the calendar/contact stores. Safe: the composer already resolves the raw id + owning client via stripCrossAccountIdentityPrefix everywhere it hits JMAP (currentIdentityRawId / composerClient), reply-identity resolves by email, and preferredIdentityIds is applied against the raw identity store (unaffected). No consumer relies on the active account's identity id being raw. Single-account mode (hook disabled) keeps the store's raw ids. Full unit suite + typecheck clean. --- hooks/use-pro-multi-account-identities.ts | 45 +++++++++++------------ 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/hooks/use-pro-multi-account-identities.ts b/hooks/use-pro-multi-account-identities.ts index 786b77960..ea5fee364 100644 --- a/hooks/use-pro-multi-account-identities.ts +++ b/hooks/use-pro-multi-account-identities.ts @@ -16,10 +16,12 @@ interface AccountIdentityGroup { const CROSS_ACCOUNT_IDENTITY_DELIMITER = '::'; -/** Cross-account identity IDs are namespaced to avoid collisions between - * JMAP servers that happen to issue the same opaque ID. The active - * account's IDs are left untouched so existing single-account code paths - * (reply-identity resolution, S/MIME bindings) keep working unchanged. +/** Cross-account identity IDs are namespaced to avoid collisions between JMAP + * servers that happen to issue the same opaque ID. EVERY aggregated account is + * namespaced (including the active one) so an id doesn't change form when the + * active account changes; consumers resolve the raw id via + * stripCrossAccountIdentityPrefix. Single-account mode (hook disabled) uses the + * identity store's raw ids directly. */ export function isCrossAccountIdentityId(id: string): boolean { return id.includes(CROSS_ACCOUNT_IDENTITY_DELIMITER); @@ -94,35 +96,32 @@ export function useProMultiAccountIdentities(): { const groups = useMemo(() => { if (!enabled) return []; + // Namespace EVERY account's identity ids, including the active one, so an id + // stably identifies (account, identity) regardless of which account is + // active. Consumers resolve the raw id via stripCrossAccountIdentityPrefix. + // Mirrors the calendar/contact stores' consistent-namespacing invariant. + const group = (accountId: string, list: Identity[], label: string): AccountIdentityGroup => ({ + localAccountId: accountId, + accountLabel: label, + identities: list.map((id) => ({ + ...id, + id: `${accountId}${CROSS_ACCOUNT_IDENTITY_DELIMITER}${id.id}`, + localAccountId: accountId, + accountName: label, + })), + }); const out: AccountIdentityGroup[] = []; if (activeAccountId) { const active = accounts.find((a) => a.id === activeAccountId); const label = active?.label || active?.email || active?.username || activeAccountId; - out.push({ - localAccountId: activeAccountId, - accountLabel: label, - identities: activeIdentities.map((id) => ({ - ...id, - localAccountId: activeAccountId, - accountName: label, - })), - }); + out.push(group(activeAccountId, activeIdentities, label)); } for (const account of accounts) { if (!account.isConnected || account.id === activeAccountId) continue; const list = remoteIdentities[account.id]; if (!list || list.length === 0) continue; const label = account.label || account.email || account.username; - out.push({ - localAccountId: account.id, - accountLabel: label, - identities: list.map((id) => ({ - ...id, - id: `${account.id}${CROSS_ACCOUNT_IDENTITY_DELIMITER}${id.id}`, - localAccountId: account.id, - accountName: label, - })), - }); + out.push(group(account.id, list, label)); } return out; }, [enabled, accounts, activeAccountId, activeIdentities, remoteIdentities]);