From c516ba98d0dcb8cd1c5ad9c13d1ac824ccabad90 Mon Sep 17 00:00:00 2001 From: KazNIISA IT Date: Wed, 12 Aug 2026 18:11:43 +0500 Subject: [PATCH 1/2] fix(email-store): route shared-folder management to the owner account Folder management performed on a shared/group account was sent to the logged-in user's primary JMAP account instead of the mailbox owner's account. Subfolder creation therefore failed with invalidProperties for parentId, root folders could be created silently in the personal account, and rename, delete, role, and reorder targeted the wrong mailbox tree. Shared mailboxes use namespaced store ids to avoid cross-account collisions, while Mailbox/set requires the owner's accountId and the bare server mailbox id. Resolve that mutation context centrally: recover the mailbox originalId and owner account, use a directly connected owner client when one exists, otherwise reach delegated/group accounts through the active client with an explicit accountId. Thread the account target through create, update, and delete JMAP methods and all folder-management actions. Scope role replacement to the same owner account, and expose explicit root-folder creation from shared account headers without making personal root creation depend on the currently selected folder. Add unit coverage for JMAP request scoping, shared store routing, role isolation, direct-client handling, and reorder. Add a Stalwart integration scenario that creates root and nested group folders, renames them, and deletes them through the UI. --- components/layout/mailbox-context-menu.tsx | 28 ++-- components/layout/sidebar.tsx | 11 +- components/mail/mail-app.tsx | 4 +- integration/tests/07-shared-folders.spec.ts | 76 +++++++++- integration/tests/helpers/jmap.ts | 57 ++++--- lib/__tests__/jmap-mailbox-account.test.ts | 103 +++++++++++++ lib/demo/demo-client.ts | 4 +- lib/jmap/client-interface.ts | 4 +- lib/jmap/client.ts | 19 ++- stores/__tests__/folder-management.test.ts | 159 ++++++++++++++++++-- stores/email-store.ts | 99 ++++++++++-- 11 files changed, 493 insertions(+), 71 deletions(-) create mode 100644 lib/__tests__/jmap-mailbox-account.test.ts diff --git a/components/layout/mailbox-context-menu.tsx b/components/layout/mailbox-context-menu.tsx index 5807d0851..557aa83f1 100644 --- a/components/layout/mailbox-context-menu.tsx +++ b/components/layout/mailbox-context-menu.tsx @@ -30,7 +30,7 @@ interface Position { export type MailboxContextTarget = | { kind: "mailbox"; mailbox: Mailbox; hasChildren: boolean } - | { kind: "folders-section" }; + | { kind: "folders-section"; accountId?: string }; const PATH_SEPARATOR = " › "; const MAX_PATH_LENGTH = 40; @@ -82,7 +82,7 @@ interface MailboxContextMenuProps { onMarkAllFoldersRead?: () => void; onEmptyFolder?: (mailboxId: string) => void; onCreateSubfolder?: (parentId: string) => void; - onCreateFolder?: () => void; + onCreateFolder?: (accountId?: string) => void; onRenameFolder?: (mailboxId: string) => void; onDeleteFolder?: (mailboxId: string) => void; onImportEmail?: (mailboxId: string) => void; @@ -119,18 +119,23 @@ export function MailboxContextMenu({ if (target.kind === "folders-section") { return ( - handleAction(onMarkAllFoldersRead!)} - disabled={!onMarkAllFoldersRead} - /> - + {!target.accountId && ( + <> + handleAction(onMarkAllFoldersRead!)} + disabled={!onMarkAllFoldersRead} + /> + + + )} handleAction(onCreateFolder!)} + onClick={() => handleAction(() => onCreateFolder?.(target.accountId))} disabled={!onCreateFolder} + testId="mailbox-new-folder" /> handleAction(() => onCreateSubfolder?.(mailbox.id))} disabled={!onCreateSubfolder || !canCreateChild} + testId="mailbox-new-subfolder" /> handleAction(() => onRenameFolder?.(mailbox.id))} disabled={!onRenameFolder || !canRename} + testId="mailbox-rename" /> @@ -216,6 +223,7 @@ export function MailboxContextMenu({ onClick={() => handleAction(() => onDeleteFolder?.(mailbox.id))} disabled={!onDeleteFolder || !canDelete} destructive + testId="mailbox-delete" /> diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 37fdfa334..bcb85e14b 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -84,7 +84,7 @@ interface SidebarProps { onMarkAllFoldersRead?: () => void; onEmptyFolder?: (mailboxId: string) => void; onCreateSubfolder?: (parentId: string) => void; - onCreateFolder?: () => void; + onCreateFolder?: (accountId?: string) => void; onRenameFolder?: (mailboxId: string) => void; onDeleteFolder?: (mailboxId: string) => void; onImportEmail?: (mailboxId: string) => void; @@ -338,6 +338,7 @@ function SidebarRow({ e.stopPropagation(); onExpandToggle(); }} + data-testid="folder-expand-toggle" className="flex items-center justify-center rounded hover:bg-muted active:bg-accent transition-colors" style={{ width: CHEVRON_SLOT, height: CHEVRON_SLOT }} title={isExpanded ? t('collapse_tooltip') : t('expand_tooltip')} @@ -394,6 +395,7 @@ function SidebarSectionHeader({ icon, sub, testId, + onContextMenu, }: { label: string; expanded: boolean; @@ -405,6 +407,7 @@ function SidebarSectionHeader({ icon?: ReactNode; sub?: boolean; testId?: string; + onContextMenu?: (event: React.MouseEvent) => void; }) { if (isCollapsed) { return first ? null :
; @@ -419,6 +422,7 @@ function SidebarSectionHeader({ return (