diff --git a/src/lib/__tests__/mailbox-tree.test.ts b/src/lib/__tests__/mailbox-tree.test.ts index cf41fa3e..1a2d574c 100644 --- a/src/lib/__tests__/mailbox-tree.test.ts +++ b/src/lib/__tests__/mailbox-tree.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect } from 'vitest'; import { buildMailboxTree, + findArchiveMailbox, flattenVisible, mailboxAccountId, mailboxesForSiblingOf, @@ -205,3 +206,28 @@ describe('role-folder deduplication (#771)', () => { expect(tree.find((n) => n.id === 'proj')?.children.map((n) => n.id)).toEqual(['proj-sent']); }); }); + +describe('findArchiveMailbox (#578)', () => { + it('prefers the archive role over a folder named Archive', () => { + const found = findArchiveMailbox([ + own('named', 'Archive'), + own('role', 'Old mail', { role: 'archive' }), + ]); + expect(found?.id).toBe('role'); + }); + + it('falls back to a role-less folder named exactly "Archive", any case', () => { + expect(findArchiveMailbox([own('inbox', 'Inbox', { role: 'inbox' }), own('a', 'ARCHIVE')])?.id) + .toBe('a'); + }); + + it('does not treat "Archives" or folders merely containing "archive" as the Archive', () => { + // The store's archive actions only file into a folder this resolver + // returns; a looser match here offered an archive that did nothing. + expect(findArchiveMailbox([ + own('plural', 'Archives'), + own('past', 'Archived'), + own('proj', 'Project archive 2019'), + ])).toBeUndefined(); + }); +}); diff --git a/src/lib/mailbox-tree.ts b/src/lib/mailbox-tree.ts index da7b89e8..684127f9 100644 --- a/src/lib/mailbox-tree.ts +++ b/src/lib/mailbox-tree.ts @@ -272,15 +272,16 @@ export function findTrashMailbox(mailboxes: Mailbox[]): Mailbox | undefined { }); } +/** + * The Archive folder: the `archive` role, else a role-less folder named + * exactly "Archive" (any case), as the webmail matches it. Whatever offers + * the archive action must resolve through this, like the store's archive + * actions do: a looser list-only match used to offer archiving into e.g. + * "Archives", which the store then silently ignored. (#578) + */ export function findArchiveMailbox(mailboxes: Mailbox[]): Mailbox | undefined { - const roleMatch = mailboxes.find((m) => m.role === 'archive'); - if (roleMatch) return roleMatch; - - const names = ['archive', 'archives', 'archived']; - return mailboxes.find((m) => { - const lower = m.name.toLowerCase(); - return names.includes(lower) || names.some((n) => lower.includes(n)); - }); + return mailboxes.find((m) => m.role === 'archive') + ?? mailboxes.find((m) => m.name.toLowerCase() === 'archive'); } export function findJunkMailbox(mailboxes: Mailbox[]): Mailbox | undefined { diff --git a/src/stores/email-store.ts b/src/stores/email-store.ts index 60ff205f..0e6b4da6 100644 --- a/src/stores/email-store.ts +++ b/src/stores/email-store.ts @@ -34,7 +34,7 @@ import { } from '../api/email'; import { useNetworkStore } from './network-store'; import { JMAPMethodError } from '../api/jmap-result'; -import { mailboxesForSiblingOf, mailboxesOfAccount, findJunkMailbox } from '../lib/mailbox-tree'; +import { mailboxesForSiblingOf, mailboxesOfAccount, findJunkMailbox, findArchiveMailbox } from '../lib/mailbox-tree'; import { toWildcardQuery } from '../lib/search-utils'; import { orderForMailbox, sanitizeSortLevels, type SortLevel } from '../lib/message-list-order'; import { buildListSort, markKeywordSortUnsupported } from '../lib/keyword-sort-polarity'; @@ -1251,9 +1251,7 @@ export const useEmailStore = create()( // Archive into the *same account's* Archive folder — a shared mailbox's // messages can't be filed into the user's own. const scoped = actionMailboxes(state, viewed); - const archiveMailbox = scoped.find( - (m) => m.role === 'archive' || m.name.toLowerCase() === 'archive', - ); + const archiveMailbox = findArchiveMailbox(scoped); if (!archiveMailbox) return; const archive = refFor(state.mailboxes, archiveMailbox.id); if (email.mailboxIds?.[archive.id]) return; @@ -1379,9 +1377,7 @@ export const useEmailStore = create()( archiveEmailsBatch: async (emailIds) => { const state = get(); const scoped = mailboxesForSiblingOf(state.mailboxes, state.currentMailboxId); - const archiveMailbox = scoped.find( - (m) => m.role === 'archive' || m.name.toLowerCase() === 'archive', - ); + const archiveMailbox = findArchiveMailbox(scoped); if (!archiveMailbox) return; const archive = refFor(state.mailboxes, archiveMailbox.id); const targets = state.emails.filter(