fix: only drop exact role-folder name duplicates from the folder tree (#771)

This commit is contained in:
Linus Rath
2026-08-29 18:02:37 +02:00
parent c8be383c05
commit 516c9c931a
2 changed files with 49 additions and 7 deletions
+39
View File
@@ -140,3 +140,42 @@ describe('account scoping helpers', () => {
expect(mailboxesForSiblingOf(all, null).map((m) => m.id)).toEqual(['inbox', 'trash']);
});
});
describe('role-folder deduplication (#771)', () => {
it('keeps user folders whose name merely contains a role folder name', () => {
const tree = buildMailboxTree([
own('inbox', 'Inbox', { role: 'inbox' }),
own('sent', 'Sent', { role: 'sent' }),
own('archive', 'Archive', { role: 'archive' }),
own('old-inbox', 'Old Inbox'),
own('arch-2025', '2025 Archive'),
own('sent-acc', 'Sent to Accounting'),
own('in', 'In'),
]);
expect(tree.map((n) => n.id)).toEqual([
'inbox', 'sent', 'archive', 'arch-2025', 'in', 'old-inbox', 'sent-acc',
]);
});
it('drops only an exact (trimmed, case-insensitive) duplicate of a role folder', () => {
const tree = buildMailboxTree([
own('sent', 'Sent', { role: 'sent' }),
own('sent-dup', ' sent '),
own('sent-mail', 'Sent Mail'),
]);
expect(tree.map((n) => n.id)).toEqual(['sent', 'sent-mail']);
});
it('never drops a nested folder or a duplicate that has children', () => {
const tree = buildMailboxTree([
own('inbox', 'Inbox', { role: 'inbox' }),
own('sent', 'Sent', { role: 'sent' }),
own('proj', 'Projects'),
own('proj-sent', 'Sent', { parentId: 'proj' }),
own('inbox-dup', 'Inbox'),
own('inbox-dup-child', 'Child', { parentId: 'inbox-dup' }),
]);
expect(tree.map((n) => n.id)).toEqual(['inbox', 'sent', 'inbox-dup', 'proj']);
expect(tree.find((n) => n.id === 'proj')?.children.map((n) => n.id)).toEqual(['proj-sent']);
});
});
+10 -7
View File
@@ -44,8 +44,12 @@ const ROLE_PRIORITY: Record<string, number> = {
};
// Drop root-level folders whose name collides with an existing role mailbox
// (e.g. "Sent Mail" when a role=sent mailbox already exists). Mirrors the
// webmail's `deduplicateMailboxes`; kept minimal (single account only).
// (e.g. a plain "Sent" folder when a role=sent mailbox already exists).
// Mirrors the webmail's `deduplicateMailboxes`; kept minimal (single account
// only). Only an *exact* (trimmed, case-insensitive) name collision counts:
// substring matching silently hid legitimate user folders whose name merely
// contained a role folder's name — "Old Inbox", "2025 Archive",
// "Sent to Accounting" (GitHub #771).
function deduplicate(mailboxes: Mailbox[]): Mailbox[] {
const roles = mailboxes.filter((m) => m.role);
const referencedParentIds = new Set<string>();
@@ -56,12 +60,11 @@ function deduplicate(mailboxes: Mailbox[]): Mailbox[] {
const result: Mailbox[] = [];
for (const m of mailboxes) {
if (m.role) { result.push(m); continue; }
// Never deduplicate nested folders — removing one would orphan its
// children to the root (GitHub #118).
if (m.parentId) { result.push(m); continue; }
const lower = m.name.toLowerCase();
const dup = roles.find((r) => {
const rn = r.name.toLowerCase();
return lower.includes(rn) || rn.includes(lower);
});
const lower = m.name.trim().toLowerCase();
const dup = roles.find((r) => r.name.trim().toLowerCase() === lower);
if (!dup || referencedParentIds.has(m.id)) result.push(m);
}
return result;