fix: remember restored sign-ins only when the restoring account is remembered

This commit is contained in:
Linus Rath
2026-10-18 14:39:59 +02:00
parent 1e992e6c7f
commit f3482ce020
3 changed files with 26 additions and 3 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ const FEATURE_GATE_LABELS: Partial<Record<keyof FeatureGates, { label: string; d
crossStarredViewEnabled: { label: 'Unified Mailbox: Starred', description: 'Allow a "Starred" entry in the Unified Mailbox section that lists flagged/starred mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' },
crossAllViewEnabled: { label: 'Unified Mailbox: All Mail', description: 'Allow an "All mail" entry in the Unified Mailbox section that lists all mail across the account and its shared folders (or every account when the cross-account sub-option is on). Honors the user\'s folder selection. Requires the matching per-user toggle in Settings → Appearance.' },
unifiedCrossAccountEnabled: { label: 'Unified Mailbox: Cross-account', description: 'Allow users to expand the Unified Mailbox beyond the active account boundary so its lists merge across every logged-in account. When off, the Unified Mailbox stays within the active account and its shared folders.' },
accountArchiveEnabled: { label: 'Account List Backup', description: 'Let users save their list of accounts (address, server, label, colour) on this server, encrypted in the browser with a password of their choice, and restore it on another device. No mailbox passwords are stored. Archives live in SETTINGS_DATA_DIR/account-archives; requires SESSION_SECRET.' },
accountArchiveEnabled: { label: 'Account List Backup', description: 'Let users save their list of accounts (address, server, label, colour) on this server, encrypted in the browser with a password of their choice, and restore it on another device. Mailbox passwords are stored only when the Passwords option below is on. Archives live in SETTINGS_DATA_DIR/account-archives; requires SESSION_SECRET.' },
accountArchivePasswordsEnabled: { label: 'Account List Backup: Passwords', description: 'With Account List Backup on, offer to include the mailbox passwords of accounts signed in with a password, so a restore signs in to them by itself. They are encrypted in the browser with the backup password together with the list; the server never sees them in clear, but anyone who learns the backup password gets every mailbox in it. Accounts signed in with OAuth or a TOTP code are saved without.' },
customFromEnabled: { label: 'Custom From Address', description: 'Let users type any sender address in the composer instead of picking one of their identities, and let replies to a catch-all address on their domain send from that address. Turn this off when users may only send as their configured identities.' },
archiveActionEnabled: { label: 'Archive Action', description: 'Offer "Archive" on messages: the toolbar button, the context menu and selection bar, the hover and swipe actions, the E shortcut and archiving after an export. Turn this off when mail is archived elsewhere, for example by a mail gateway; existing Archive folders stay as ordinary folders and Bulwark no longer creates one.' },
+17 -1
View File
@@ -59,10 +59,26 @@ describe('restoreArchive', () => {
});
const result = await restoreArchive([entry('a@example.com', 'right'), entry('b@example.com', 'stale'), entry('c@example.com')], true);
expect(auth.signIn).toHaveBeenCalledTimes(2);
expect(auth.signIn).toHaveBeenCalledWith(generateAccountId('a@example.com', server), 'right', true);
expect(auth.signIn).toHaveBeenCalledWith(generateAccountId('a@example.com', server), 'right', false);
expect(result).toMatchObject({ added: 3, signedIn: 1, signInFailed: 1, waiting: 2 });
});
it('remembers the restored sign-ins only when the restoring account is remembered', async () => {
auth.signIn.mockResolvedValue(true);
const owner = add('owner@example.com', 'basic');
useAccountStore.getState().updateAccount(owner, { rememberMe: false });
useAccountStore.setState({ activeAccountId: owner });
await restoreArchive([entry('a@example.com', 'right')], true);
expect(auth.signIn).toHaveBeenLastCalledWith(generateAccountId('a@example.com', server), 'right', false);
useAccountStore.getState().updateAccount(owner, { rememberMe: true });
await restoreArchive([entry('b@example.com', 'right')], true);
expect(auth.signIn).toHaveBeenLastCalledWith(generateAccountId('b@example.com', server), 'right', true);
await restoreArchive([entry('c@example.com', 'right')], false);
expect(auth.signIn).toHaveBeenLastCalledWith(generateAccountId('c@example.com', server), 'right', false);
});
it('does not sign in again to an account already connected here', async () => {
const id = add('a@example.com', 'basic');
auth.clients.set(id, { getBasicPassword: () => 'right' });
+8 -1
View File
@@ -75,8 +75,15 @@ export function restoreAccounts(archived: ArchivedAccount[]): { added: number; u
* Restores the list, then signs in one by one to the accounts saved with a
* password that are not signed in here yet. An account whose password no
* longer works keeps waiting for a sign-in by hand.
*
* A restored sign-in is remembered only when the server allows it and the
* account doing the restore was itself signed in with "Remember me": on a
* shared computer, restoring must not leave long-lived sessions behind.
*/
export async function restoreArchive(archived: ArchivedAccount[], rememberMe: boolean) {
export async function restoreArchive(archived: ArchivedAccount[], rememberMeAllowed: boolean) {
const accounts = useAccountStore.getState();
const active = accounts.accounts.find(a => a.id === accounts.activeAccountId);
const rememberMe = rememberMeAllowed && !!active?.rememberMe;
const result = restoreAccounts(archived);
let signedIn = 0, signInFailed = 0;
for (const entry of archived) {