mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 08:15:55 +00:00
Email ids are unique only within their account, so in a view that merges several - the unified mailbox, and since #1038 the tag view - two unrelated messages can share one. Selection was a set of bare ids resolved with `emails.filter(e => selected.has(e.id))`, so ticking one row also ticked its namesake in another account, and batch delete, move, archive or mark-read then acted on a message nobody selected. Reported by @alyyousuf7 on #1013, where `eiaaaabc` exists in two accounts at once. Selection is now keyed by `emailKeyFor` - the same account scoping #1013 introduced for threads - and the actions take the email rather than its id, so a caller cannot reintroduce the collision by passing a bare id. Unstamped emails keep the bare id as their key, so single-account views are unchanged. The batch actions derive the JMAP ids from the selected emails themselves instead of looking each id up again, which is where the namesake used to ride along, and every place that pruned the selection after a delete or a move now prunes by key.
89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { useEmailStore } from '../email-store';
|
|
import type { Email, Mailbox } from '@/lib/jmap/types';
|
|
import type { IJMAPClient } from '@/lib/jmap/client-interface';
|
|
|
|
function makeMailbox(overrides: Partial<Mailbox> = {}): Mailbox {
|
|
return {
|
|
id: overrides.id ?? 'inbox',
|
|
name: overrides.name ?? 'Inbox',
|
|
sortOrder: 0,
|
|
totalEmails: 0,
|
|
unreadEmails: 0,
|
|
totalThreads: 0,
|
|
unreadThreads: 0,
|
|
myRights: {
|
|
mayReadItems: true,
|
|
mayAddItems: true,
|
|
mayRemoveItems: true,
|
|
maySetSeen: true,
|
|
maySetKeywords: true,
|
|
mayCreateChild: true,
|
|
mayRename: true,
|
|
mayDelete: true,
|
|
maySubmit: true,
|
|
},
|
|
isSubscribed: true,
|
|
isShared: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeEmail(id: string, threadId: string, mailboxId = 'inbox'): Email {
|
|
return {
|
|
id,
|
|
threadId,
|
|
mailboxIds: { [mailboxId]: true },
|
|
keywords: {},
|
|
size: 100,
|
|
receivedAt: new Date().toISOString(),
|
|
from: [{ name: 'Test', email: 'test@example.com' }],
|
|
to: [{ name: 'User', email: 'user@example.com' }],
|
|
subject: `Email ${id}`,
|
|
preview: 'preview',
|
|
hasAttachment: false,
|
|
textBody: [],
|
|
htmlBody: [],
|
|
bodyValues: {},
|
|
};
|
|
}
|
|
|
|
describe('email-store archive thread behavior', () => {
|
|
beforeEach(() => {
|
|
const inbox = makeMailbox({ id: 'shared-inbox', role: 'inbox', isShared: true, accountId: 'shared-account', originalId: 'server-inbox' });
|
|
const archive = makeMailbox({ id: 'archive-local', role: 'archive', accountId: 'shared-account', originalId: 'server-archive' });
|
|
const threadEmailA = makeEmail('email-1', 'thread-1', 'shared-inbox');
|
|
const threadEmailB = makeEmail('email-2', 'thread-1', 'shared-inbox');
|
|
const otherEmail = makeEmail('email-3', 'thread-2', 'shared-inbox');
|
|
|
|
useEmailStore.setState({
|
|
emails: [threadEmailA, threadEmailB, otherEmail],
|
|
mailboxes: [inbox, archive],
|
|
selectedMailbox: 'shared-inbox',
|
|
selectedEmail: threadEmailA,
|
|
selectedEmailKeys: new Set(['email-1', 'email-2']),
|
|
expandedThreadIds: new Set(['thread-1']),
|
|
threadEmailsCache: new Map([['thread-1', [threadEmailA, threadEmailB]]]),
|
|
error: null,
|
|
});
|
|
});
|
|
|
|
it('archives the full thread and removes cached thread state', async () => {
|
|
const client = {
|
|
getThread: vi.fn().mockResolvedValue({ id: 'thread-1', emailIds: ['email-1', 'email-2'] }),
|
|
batchMoveEmails: vi.fn().mockResolvedValue(undefined),
|
|
} as unknown as IJMAPClient;
|
|
|
|
await useEmailStore.getState().moveThreadToMailbox(client, 'email-1', 'archive-local');
|
|
|
|
expect(client.getThread).toHaveBeenCalledWith('thread-1', 'shared-account');
|
|
expect(client.batchMoveEmails).toHaveBeenCalledWith(['email-1', 'email-2'], 'server-archive', 'shared-account');
|
|
|
|
const state = useEmailStore.getState();
|
|
expect(state.emails.map(email => email.id)).toEqual(['email-3']);
|
|
expect(state.selectedEmail?.id).toBe('email-3');
|
|
expect(Array.from(state.selectedEmailKeys)).toEqual([]);
|
|
expect(state.expandedThreadIds.has('thread-1')).toBe(false);
|
|
expect(state.threadEmailsCache.has('thread-1')).toBe(false);
|
|
});
|
|
}); |