diff --git a/src/lib/__tests__/account-id-port.test.ts b/src/lib/__tests__/account-id-port.test.ts index 558f8ce0..18a826a6 100644 --- a/src/lib/__tests__/account-id-port.test.ts +++ b/src/lib/__tests__/account-id-port.test.ts @@ -49,6 +49,45 @@ describe('generateAccountId', () => { }); }); +describe('a new default-port account next to a kept host-only id', () => { + const legacy = { id: 'u@example.org@mail.example.org', username: 'u@example.org', serverUrl: 'https://mail.example.org:8120' }; + + it('gets an id of its own instead of the one the other server kept', () => { + setKnownAccounts([legacy]); + expect(generateAccountId('u@example.org', 'https://mail.example.org')).toBe('u@example.org@mail.example.org:443'); + expect(generateAccountId('u@example.org', 'http://mail.example.org/jmap')).toBe('u@example.org@mail.example.org:80'); + // The kept account itself still resolves to its old id. + expect(generateAccountId('u@example.org', 'https://mail.example.org:8120')).toBe(legacy.id); + }); + + it('keeps that id after the other account is gone', () => { + const own = { id: 'u@example.org@mail.example.org:443', username: 'u@example.org', serverUrl: 'https://mail.example.org' }; + setKnownAccounts([legacy, own]); + expect(generateAccountId('u@example.org', 'https://mail.example.org')).toBe(own.id); + setKnownAccounts([own]); + expect(generateAccountId('u@example.org', 'https://mail.example.org')).toBe(own.id); + }); + + it('leaves an existing default-port account under its plain id', () => { + setKnownAccounts([{ id: 'u@example.org@mail.example.org', username: 'u@example.org', serverUrl: 'https://mail.example.org/' }]); + expect(generateAccountId('u@example.org', 'https://mail.example.org')).toBe('u@example.org@mail.example.org'); + }); + + it('adds it to the registry as a second account', () => { + useAccountStore.setState({ + accounts: [{ ...entry('u@example.org', legacy.serverUrl), id: legacy.id, avatarColor: '#000', isDefault: true }], + activeAccountId: legacy.id, + defaultAccountId: legacy.id, + }); + const id = useAccountStore.getState().addAccount(entry('u@example.org', 'https://mail.example.org')); + expect(id).toBe('u@example.org@mail.example.org:443'); + expect(useAccountStore.getState().accounts.map((x) => [x.id, x.serverUrl])).toEqual([ + [legacy.id, legacy.serverUrl], + ['u@example.org@mail.example.org:443', 'https://mail.example.org'], + ]); + }); +}); + describe('the account registry', () => { it('adds the same address on another port of the same host as a second account', () => { const store = useAccountStore.getState(); diff --git a/src/lib/account-utils.ts b/src/lib/account-utils.ts index 13903bfd..f60eb6e3 100644 --- a/src/lib/account-utils.ts +++ b/src/lib/account-utils.ts @@ -50,6 +50,11 @@ function hostOnlyAccountId(username: string, serverUrl: string): string { return `${username}@${host}`; } +/** The default port of `serverUrl`'s scheme: 80 for http, 443 otherwise. */ +function defaultPortOf(serverUrl: string): string { + return /^http:/i.test(serverUrl.trim()) ? '80' : '443'; +} + /** What the registry tells generateAccountId (structural: no AccountEntry import). */ export interface KnownAccount { id: string; @@ -78,13 +83,22 @@ export function setKnownAccounts(accounts: readonly KnownAccount[]): void { * outbox, push registration and settings: when the registry holds the * host-only id for the same server, that is the answer. A second server on * the same host then gets the id with its port, and the two stay apart. - * Accounts on the default ports have the same id either way. + * Accounts on the default ports have the same id either way, with one + * exception: when the registry holds username@host for a server on another + * port of that host (kept from before ports counted), the default-port + * server gets username@host:443 (or :80 for http), so the two don't share + * credentials and caches. It keeps that id once it has it. */ export function generateAccountId(username: string, serverUrl: string): string { const legacy = hostOnlyAccountId(username, serverUrl); const host = serverHostLabel(serverUrl); const id = `${username}@${host}`; - if (id === legacy) return id; + if (id === legacy) { + const explicit = `${id}:${defaultPortOf(serverUrl)}`; + if (knownAccounts.some((a) => a.id === explicit)) return explicit; + const taken = knownAccounts.some((a) => a.id === id && `${a.username}@${serverHostLabel(a.serverUrl)}` !== id); + return taken ? explicit : id; + } const kept = knownAccounts.find((a) => a.id === legacy && a.username === username && `${a.username}@${serverHostLabel(a.serverUrl)}` === id);