From 272fc5e1f4a1499da574fe88f97d2b7b2a152d82 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:06:53 +0100 Subject: [PATCH] fix: reuse a group account's year and month archive folders instead of creating them again or in the own account --- .../mail-app-source-routing.test.tsx | 60 +++++++++++++++++++ components/mail/mail-app.tsx | 22 ++++--- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/components/mail/__tests__/mail-app-source-routing.test.tsx b/components/mail/__tests__/mail-app-source-routing.test.tsx index 890635547..9348baa37 100644 --- a/components/mail/__tests__/mail-app-source-routing.test.tsx +++ b/components/mail/__tests__/mail-app-source-routing.test.tsx @@ -195,3 +195,63 @@ describe('the open message of a group account in a tag view', () => { expect(moveToMailbox).toHaveBeenCalledWith(client, 'eaaaaab', 'ce:b', 'login-a/ce:eaaaaab'); }); }); + +describe('archiving a group message by month', () => { + // The group's Archive `k` already holds 2026/10, as the list names them: + // store ids and parents namespaced `ce:...`, the server's ids bare. + const year = { ...mailbox('y1', undefined, 'ce', true), name: '2026', parentId: 'ce:k' }; + const month = { ...mailbox('m1', undefined, 'ce', true), name: '10', parentId: 'ce:y1' }; + const withMonth = [...folders, year, month] as Mailbox[]; + + beforeEach(() => { + useSettingsStore.setState({ archiveMode: 'month' } as never); + client.createMailbox = vi.fn(); + }); + + it('reuses the year and month folders in a tag view', async () => { + useEmailStore.setState({ mailboxes: withMonth } as never); + render(); + fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 })); + await waitFor(() => expect(moveThreadToMailbox).toHaveBeenCalled()); + expect(client.createMailbox).not.toHaveBeenCalled(); + expect(moveThreadToMailbox).toHaveBeenCalledWith(client, 'eaaaaab', 'ce:m1', 'login-a/ce:eaaaaab'); + }); + + it('reuses them with the group\'s Inbox open', async () => { + const unstamped = { ...groupMessage, sourceClientAccountId: undefined, sourceAccountId: undefined } as Email; + useEmailStore.setState({ mailboxes: withMonth, selectedMailbox: 'ce:a', selectedKeyword: null, emails: [unstamped], selectedEmail: unstamped } as never); + render(); + fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 })); + await waitFor(() => expect(moveThreadToMailbox).toHaveBeenCalled()); + expect(client.createMailbox).not.toHaveBeenCalled(); + expect(moveThreadToMailbox).toHaveBeenCalledWith(client, 'eaaaaab', 'ce:m1', 'eaaaaab'); + }); + + // The own account has a folder with the server id of the group's Archive, + // with 2026/10 below it (live on 0.16: a's "Shared" folder is `h`, and so + // was the group's Archive). + it('does not take the own account\'s namesake folders', async () => { + const unstamped = { ...groupMessage, sourceClientAccountId: undefined, sourceAccountId: undefined } as Email; + const ownNamesakes = [ + { ...mailbox('k', undefined, 'cf'), name: 'Shared' }, + { ...mailbox('oy', undefined, 'cf'), name: '2026', parentId: 'k' }, + { ...mailbox('om', undefined, 'cf'), name: '10', parentId: 'oy' }, + ]; + useEmailStore.setState({ mailboxes: [...ownNamesakes, ...withMonth], selectedMailbox: 'ce:a', selectedKeyword: null, emails: [unstamped], selectedEmail: unstamped } as never); + render(); + fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 })); + await waitFor(() => expect(moveThreadToMailbox).toHaveBeenCalled()); + expect(moveThreadToMailbox).toHaveBeenCalledWith(client, 'eaaaaab', 'ce:m1', 'eaaaaab'); + }); + + it('creates a missing year folder in the group account with the group\'s Inbox open', async () => { + const unstamped = { ...groupMessage, sourceClientAccountId: undefined, sourceAccountId: undefined } as Email; + useSettingsStore.setState({ archiveMode: 'year' } as never); + (client.createMailbox as ReturnType).mockResolvedValue({ ...mailbox('y2', undefined, 'ce', true), id: 'y2', name: '2026' }); + useEmailStore.setState({ selectedMailbox: 'ce:a', selectedKeyword: null, emails: [unstamped], selectedEmail: unstamped } as never); + render(); + fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 })); + await waitFor(() => expect(client.createMailbox).toHaveBeenCalled()); + expect(client.createMailbox).toHaveBeenCalledWith('2026', 'k', 'ce', undefined); + }); +}); diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx index c366a6cea..2a8d777be 100644 --- a/components/mail/mail-app.tsx +++ b/components/mail/mail-app.tsx @@ -2948,12 +2948,20 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { const year = emailDate.getFullYear().toString(); const month = (emailDate.getMonth() + 1).toString().padStart(2, '0'); const archiveId = archiveMailbox.originalId || archiveMailbox.id; + // By the server's ids: the list names a shared account's folders and + // their parents `account:id`, so a group's year folder was never + // found again. The folders go in the Archive's own account, and only + // that account's are looked at: the own folders have server ids too. + const parentOf = (m: Mailbox) => (m.parentId && m.accountId && m.parentId.startsWith(`${m.accountId}:`) + ? m.parentId.slice(m.accountId.length + 1) + : m.parentId); + const folderAccountId = archiveMailbox.accountId ?? archiveAccountId; + const isFolder = (m: Mailbox, name: string, parentId: string) => + m.name === name && parentOf(m) === parentId && (!folderAccountId || m.accountId === folderAccountId); - let yearMailbox = currentMailboxes.find( - m => m.name === year && m.parentId === archiveId - ) ?? null; + let yearMailbox = currentMailboxes.find(m => isFolder(m, year, archiveId)) ?? null; if (!yearMailbox) { - yearMailbox = await createMailboxWithHooks(archiveClient, year, archiveId, archiveAccountId); + yearMailbox = await createMailboxWithHooks(archiveClient, year, archiveId, folderAccountId); if (!yearMailbox) return; // a plugin refused the folder await refreshMailboxes(); } @@ -2963,11 +2971,9 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { } else { const yearId = yearMailbox.originalId || yearMailbox.id; const afterYear = readMailboxes(); - let monthMailbox = afterYear.find( - m => m.name === month && m.parentId === yearId - ) ?? null; + let monthMailbox = afterYear.find(m => isFolder(m, month, yearId)) ?? null; if (!monthMailbox) { - monthMailbox = await createMailboxWithHooks(archiveClient, month, yearId, archiveAccountId); + monthMailbox = await createMailboxWithHooks(archiveClient, month, yearId, folderAccountId); if (!monthMailbox) return; // a plugin refused the folder await refreshMailboxes(); }