From cfd7b43c4c08323df9c18726c7f6abf7ca42bafd Mon Sep 17 00:00:00 2001 From: Alexander Senier Date: Thu, 13 Aug 2026 19:18:33 +0200 Subject: [PATCH 1/3] fix: Support child mailboxes and isSubscribed in mock JMAP server --- app/api/dev-jmap/[...path]/route.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/api/dev-jmap/[...path]/route.ts b/app/api/dev-jmap/[...path]/route.ts index e5cc03c2..61586a84 100644 --- a/app/api/dev-jmap/[...path]/route.ts +++ b/app/api/dev-jmap/[...path]/route.ts @@ -37,6 +37,8 @@ interface MockMailbox { sortOrder: number; totalEmails: number; unreadEmails: number; + parentId?: string; + isSubscribed: boolean; } interface MockEmail { @@ -64,12 +66,15 @@ function nextState(): string { } const mailboxes: MockMailbox[] = [ - { id: 'mb-inbox', name: 'Inbox', role: 'inbox', sortOrder: 1, totalEmails: 5, unreadEmails: 2 }, - { id: 'mb-drafts', name: 'Drafts', role: 'drafts', sortOrder: 2, totalEmails: 1, unreadEmails: 0 }, - { id: 'mb-sent', name: 'Sent', role: 'sent', sortOrder: 3, totalEmails: 3, unreadEmails: 0 }, - { id: 'mb-junk', name: 'Junk', role: 'junk', sortOrder: 4, totalEmails: 1, unreadEmails: 1 }, - { id: 'mb-trash', name: 'Trash', role: 'trash', sortOrder: 5, totalEmails: 0, unreadEmails: 0 }, - { id: 'mb-archive', name: 'Archive', role: 'archive', sortOrder: 6, totalEmails: 2, unreadEmails: 0 }, + { id: 'mb-inbox', name: 'Inbox', role: 'inbox', sortOrder: 1, totalEmails: 5, unreadEmails: 2, isSubscribed: true }, + { id: 'mb-drafts', name: 'Drafts', role: 'drafts', sortOrder: 2, totalEmails: 1, unreadEmails: 0, isSubscribed: true }, + { id: 'mb-sent', name: 'Sent', role: 'sent', sortOrder: 3, totalEmails: 3, unreadEmails: 0, isSubscribed: true }, + { id: 'mb-junk', name: 'Junk', role: 'junk', sortOrder: 4, totalEmails: 1, unreadEmails: 1, isSubscribed: true }, + { id: 'mb-trash', name: 'Trash', role: 'trash', sortOrder: 5, totalEmails: 0, unreadEmails: 0, isSubscribed: true }, + { id: 'mb-archive', name: 'Archive', role: 'archive', sortOrder: 6, totalEmails: 2, unreadEmails: 0, isSubscribed: true }, + { id: 'mb-invoices', name: 'Invoices', role: null, sortOrder: 7, totalEmails: 8, unreadEmails: 0, isSubscribed: true }, + { id: 'mb-invoices-2025', name: '2025', role: null, sortOrder: 8, totalEmails: 8, unreadEmails: 0, parentId: "mb-invoices", isSubscribed: false }, + { id: 'mb-invoices-2026', name: '2026', role: null, sortOrder: 9, totalEmails: 8, unreadEmails: 0, parentId: "mb-invoices", isSubscribed: true }, ]; function recomputeMailboxCounts(): void { @@ -1544,6 +1549,8 @@ function handleMailboxSet(args: MethodArgs, callId: string): MethodResult { sortOrder: mailboxes.length + 1, totalEmails: 0, unreadEmails: 0, + parentId: typeof data.parentId === 'string' ? (data.parentId as string) : undefined, + isSubscribed: data.isSubscribed !== false, }); created[key] = { id: newId }; } @@ -1556,6 +1563,7 @@ function handleMailboxSet(args: MethodArgs, callId: string): MethodResult { if (mb) { if (changes.name !== undefined) mb.name = changes.name as string; if (changes.sortOrder !== undefined) mb.sortOrder = changes.sortOrder as number; + if (changes.isSubscribed !== undefined) mb.isSubscribed = changes.isSubscribed as boolean; updated[id] = null; } } From 0a1f33f289b19316ff034af4f61b36555359a0a3 Mon Sep 17 00:00:00 2001 From: Alexander Senier Date: Sun, 9 Aug 2026 21:11:57 +0200 Subject: [PATCH 2/3] feat: unconditionally filter out unsubscribed folders --- components/email/email-context-menu.tsx | 2 +- components/layout/sidebar.tsx | 8 ++++---- lib/utils.ts | 4 ++++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/components/email/email-context-menu.tsx b/components/email/email-context-menu.tsx index 3f128b7b..e2d2b7cf 100644 --- a/components/email/email-context-menu.tsx +++ b/components/email/email-context-menu.tsx @@ -172,7 +172,7 @@ export function EmailContextMenu({ const filterTree = (nodes: MailboxNode[]): MailboxNode[] => { return nodes.reduce((acc, node) => { const filteredChildren = filterTree(node.children); - if (moveTargetIds.has(node.id) || filteredChildren.length > 0) { + if (node.isSubscribed && (moveTargetIds.has(node.id) || filteredChildren.length > 0)) { acc.push({ ...node, children: filteredChildren }); } return acc; diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 3700a5d5..dd81d947 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -37,7 +37,7 @@ import { MailOpen, MoreHorizontal, } from "lucide-react"; -import { cn, buildMailboxTree, MailboxNode } from "@/lib/utils"; +import { cn, buildMailboxTree, hasSubscribedChildren, MailboxNode } from "@/lib/utils"; import { localizeMailboxName } from "@/lib/mailbox-label"; import { buildKeywordTree, @@ -495,7 +495,7 @@ function MailboxTreeItem({ }) { const tNotifications = useTranslations('notifications'); const tSidebar = useTranslations('sidebar'); - const hasChildren = node.children.length > 0; + const hasChildren = node.isSubscribed && hasSubscribedChildren(node); const isExpanded = expandedFolders.has(node.id); const Icon = getIconForMailbox(node.role, node.name, hasChildren, isExpanded, node.isShared, node.id); const isVirtualNode = node.id.startsWith('shared-'); @@ -544,7 +544,7 @@ function MailboxTreeItem({ return ( <> - } label={label} testRole={node.role} @@ -573,7 +573,7 @@ function MailboxTreeItem({ isValidDropTarget={isValidDropTarget} isInvalidDropTarget={isInvalidDropTarget} onContextMenu={onContextMenu && !isVirtualNode ? (e) => onContextMenu(e, node) : undefined} - /> + />)} {hasChildren && isExpanded && !isCollapsed && node.children.map((child) => ( n.isSubscribed || hasSubscribedChildren(n)) } \ No newline at end of file From 9dffea9ee74e0d269e3a81864ff184e171c04175 Mon Sep 17 00:00:00 2001 From: Alexander Senier Date: Mon, 10 Aug 2026 22:47:05 +0200 Subject: [PATCH 3/3] feat: Implement subscribing to / unsubscribing from mailboxes --- components/settings/folder-settings.tsx | 27 +++++++++++++++++++++++-- lib/demo/demo-client.ts | 2 +- lib/jmap/client-interface.ts | 2 +- lib/jmap/client.ts | 2 +- locales/de/common.json | 4 +++- locales/en/common.json | 4 +++- stores/email-store.ts | 16 +++++++++++++++ 7 files changed, 50 insertions(+), 7 deletions(-) diff --git a/components/settings/folder-settings.tsx b/components/settings/folder-settings.tsx index 27c0cd67..55e545bc 100644 --- a/components/settings/folder-settings.tsx +++ b/components/settings/folder-settings.tsx @@ -14,6 +14,7 @@ import { Star, Heart, Bookmark, Tag, Flag, Briefcase, Users, Bell, Zap, Globe, Lock, Eye, MessageSquare, Mail, AlertTriangle, NotebookPen, CalendarClock, BellOff, + EyeOff, type LucideIcon, } from 'lucide-react'; import { cn, buildMailboxTree, type MailboxNode } from '@/lib/utils'; @@ -154,7 +155,7 @@ function SortableFolderRow({ id, title, children }: { id: string; title: string; export function FolderSettings() { const t = useTranslations('settings.folders'); const { client } = useAuthStore(); - const { mailboxes, fetchMailboxes, createMailbox, renameMailbox, deleteMailbox, setMailboxRole, reorderMailboxes, moveMailbox } = useEmailStore(); + const { mailboxes, fetchMailboxes, createMailbox, renameMailbox, deleteMailbox, setMailboxRole, setMailboxSubscription, reorderMailboxes, moveMailbox } = useEmailStore(); const sensors = useSensors( // Small activation distance so clicking the row's buttons still works. @@ -388,6 +389,19 @@ export function FolderSettings() { setEditingName(mb.name); }; + const handleFolderSubscription = async (mailboxId: string, subscribe: boolean) => { + if (!client) return; + setIsLoading(true); + try { + await setMailboxSubscription(client, mailboxId, subscribe); + } catch (error) { + console.error('Failed to update folder subscription:', error); + toast.error('Failed to update folder subscription'); + } finally { + setIsLoading(false); + } + }; + const cancelEdit = () => { setEditingId(null); setEditingName(''); @@ -577,7 +591,7 @@ export function FolderSettings() { /> )} - {mb.name} + {mb.name} {mb.role && ( {t(`role_${mb.role}`)} @@ -590,6 +604,15 @@ export function FolderSettings() { )}
+ {mb.role == null && + + } {mb.myRights?.mayCreateChild && (