diff --git a/app/(main)/admin/_tabs/policy.tsx b/app/(main)/admin/_tabs/policy.tsx index 5ba8276b7..4b6e94eb1 100644 --- a/app/(main)/admin/_tabs/policy.tsx +++ b/app/(main)/admin/_tabs/policy.tsx @@ -40,6 +40,7 @@ const FEATURE_GATE_LABELS: Partial s.username); const serverUrl = useAuthStore(s => s.serverUrl); + const passwordsAllowed = usePolicyStore(s => s.isFeatureEnabled('accountArchivePasswordsEnabled')); + const { rememberMeEnabled } = useConfig(); const [saved, setSaved] = useState(null); const [mode, setMode] = useState<'save' | 'restore' | null>(null); const [password, setPassword] = useState(''); const [confirmation, setConfirmation] = useState(''); + const [includePasswords, setIncludePasswords] = useState(false); const [busy, setBusy] = useState(false); const [message, setMessage] = useState<{ error: boolean; text: string } | null>(null); const owner: ArchiveOwner | null = username && serverUrl ? { username, serverUrl } : null; @@ -39,7 +44,7 @@ export function AccountArchiveSettings() { if (!owner) return null; const open = (next: 'save' | 'restore' | null) => { - setMode(next); setPassword(''); setConfirmation(''); setMessage(null); + setMode(next); setPassword(''); setConfirmation(''); setIncludePasswords(false); setMessage(null); }; const run = async (action: () => Promise) => { setBusy(true); setMessage(null); @@ -56,16 +61,22 @@ export function AccountArchiveSettings() { const save = () => run(async () => { if (password !== confirmation) throw new Error('mismatch'); - const accounts = collectAccounts(); + const accounts = collectAccounts(passwordsAllowed && includePasswords); await putArchive(owner, await encryptArchive(owner, accounts, password)); setSaved(true); - return t('saved', { count: accounts.length }); + const withPassword = accounts.filter(a => a.password).length; + return [t('saved', { count: accounts.length }), withPassword ? t('saved_passwords', { count: withPassword }) : ''].join(' ').trim(); }); const restore = () => run(async () => { const envelope = await fetchArchive(owner); if (!envelope) { setSaved(false); throw new Error('storage_failed'); } - const result = restoreAccounts(await decryptArchive(owner, envelope, password)); - return [t('restored', result), result.skipped ? t('skipped', { count: result.skipped }) : ''].join(' ').trim(); + const result = await restoreArchive(await decryptArchive(owner, envelope, password), rememberMeEnabled); + return [ + t(result.waiting ? 'restored' : 'restored_signed_in', result), + result.signedIn ? t('signed_in', { count: result.signedIn }) : '', + result.signInFailed ? t('sign_in_failed', { count: result.signInFailed }) : '', + result.skipped ? t('skipped', { count: result.skipped }) : '', + ].join(' ').trim(); }); const remove = () => { if (!window.confirm(t('delete_confirm'))) return; @@ -73,7 +84,7 @@ export function AccountArchiveSettings() { }; return ( - +

{saved === null ? '…' : t(saved ? 'status_saved' : 'status_none', { account: toUnicodeEmail(owner.username) })}

@@ -105,6 +116,18 @@ export function AccountArchiveSettings() { id={`${id}-confirm`} type="password" value={confirmation} required autoComplete="new-password" onChange={e => setConfirmation(e.target.value)} /> + {passwordsAllowed && ( +
+ +

{t('include_passwords_hint')}

+
+ )} )}
diff --git a/lib/__tests__/account-archive-client.test.ts b/lib/__tests__/account-archive-client.test.ts new file mode 100644 index 000000000..f0f0d87dd --- /dev/null +++ b/lib/__tests__/account-archive-client.test.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const auth = vi.hoisted(() => ({ + clients: new Map string | null }>(), + signIn: vi.fn(), +})); + +vi.mock('@/stores/auth-store', () => ({ + useAuthStore: { + getState: () => ({ + getClientForAccount: (id: string) => auth.clients.get(id), + signInRestoredAccount: auth.signIn, + }), + }, +})); + +import { collectAccounts, restoreArchive } from '@/lib/account-archive-client'; +import { generateAccountId } from '@/lib/account-utils'; +import { useAccountStore } from '@/stores/account-store'; + +const server = 'https://mail.example.com'; +const add = (username: string, authMode: 'basic' | 'oauth') => useAccountStore.getState().addAccount({ + username, serverUrl: server, authMode, rememberMe: true, label: username, displayName: username, email: username, + lastLoginAt: 1, isConnected: true, hasError: false, isDefault: false, +}); + +beforeEach(() => { + useAccountStore.setState({ accounts: [], defaultAccountId: null }); + auth.clients.clear(); + auth.signIn.mockReset(); +}); + +describe('collectAccounts', () => { + it('leaves passwords out unless asked', () => { + const id = add('a@example.com', 'basic'); + auth.clients.set(id, { getBasicPassword: () => 'secret-a' }); + expect(collectAccounts()[0]).not.toHaveProperty('password'); + expect(collectAccounts(true)[0].password).toBe('secret-a'); + }); + + it('takes a password only from a signed-in Basic account', () => { + const basic = add('a@example.com', 'basic'); + const oauth = add('b@example.com', 'oauth'); + add('c@example.com', 'basic'); // not signed in here + auth.clients.set(basic, { getBasicPassword: () => null }); // TOTP or bearer + auth.clients.set(oauth, { getBasicPassword: () => 'never-read' }); + expect(collectAccounts(true).map(a => a.password)).toEqual([undefined, undefined, undefined]); + }); +}); + +describe('restoreArchive', () => { + const entry = (username: string, password?: string) => ({ username, serverUrl: server, label: username, avatarColor: '#2563eb', password }); + + it('signs in to the accounts saved with a password and leaves the others waiting', async () => { + auth.signIn.mockImplementation(async (id: string, password: string) => { + if (password !== 'right') return false; + useAccountStore.getState().updateAccount(id, { awaitingSignIn: false }); + return true; + }); + 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(result).toMatchObject({ added: 3, signedIn: 1, signInFailed: 1, waiting: 2 }); + }); + + 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' }); + const result = await restoreArchive([entry('a@example.com', 'right')], true); + expect(auth.signIn).not.toHaveBeenCalled(); + expect(result).toMatchObject({ updated: 1, signedIn: 0, waiting: 0 }); + }); +}); diff --git a/lib/__tests__/account-archive.test.ts b/lib/__tests__/account-archive.test.ts index 2c5bc6048..c7dba9e78 100644 --- a/lib/__tests__/account-archive.test.ts +++ b/lib/__tests__/account-archive.test.ts @@ -38,12 +38,20 @@ describe('account archive encryption', () => { await expect(encryptArchive(owner, accounts, 'short')).rejects.toThrow('password_length'); }); - it('never carries a password or any other extra field', async () => { - const withSecrets = accounts.map(a => ({ ...a, password: 'mailbox-secret', token: 't' })); + it('carries a mailbox password when given one, and no other extra field', async () => { + const withSecrets = [{ ...accounts[0], password: 'mailbox-secret', token: 't' }, { ...accounts[1], token: 't' }]; const envelope = await encryptArchive(owner, withSecrets, password); + expect(JSON.stringify(envelope)).not.toContain('mailbox-secret'); const restored = await decryptArchive(owner, envelope, password); - expect(JSON.stringify(restored)).not.toContain('mailbox-secret'); - expect(Object.keys(restored[0]).sort()).toEqual(['avatarColor', 'label', 'serverUrl', 'username']); + expect(restored[0].password).toBe('mailbox-secret'); + expect(Object.keys(restored[0]).sort()).toEqual(['avatarColor', 'label', 'password', 'serverUrl', 'username']); + expect(Object.keys(restored[1]).sort()).toEqual(['avatarColor', 'label', 'serverUrl', 'username']); + }); + + it('rejects a password that is empty, too long or not a string', () => { + for (const bad of ['', 'x'.repeat(1025), 42]) { + expect(() => parseArchivedAccounts([{ ...accounts[0], password: bad as string }])).toThrow('invalid_archive'); + } }); it('rejects malformed lists and envelopes', async () => { diff --git a/lib/account-archive-client.ts b/lib/account-archive-client.ts index 2ca00f455..3eb83c826 100644 --- a/lib/account-archive-client.ts +++ b/lib/account-archive-client.ts @@ -2,6 +2,7 @@ import { apiFetch } from '@/lib/browser-navigation'; import { IS_LITE } from '@/lib/lite'; import { generateAccountId, getMaxAccounts } from '@/lib/account-utils'; import { useAccountStore } from '@/stores/account-store'; +import { useAuthStore } from '@/stores/auth-store'; import type { ArchiveEnvelope, ArchiveOwner, ArchivedAccount } from './account-archive'; async function request(owner: ArchiveOwner, init: RequestInit = {}): Promise<{ envelope?: ArchiveEnvelope | null }> { @@ -29,11 +30,19 @@ export async function removeArchive(owner: ArchiveOwner): Promise { await request(owner, { method: 'DELETE' }); } -/** This browser's accounts as the archive keeps them: no credentials of any kind. */ -export function collectAccounts(): ArchivedAccount[] { - return useAccountStore.getState().accounts.map(a => ({ - username: a.username, serverUrl: a.serverUrl, label: a.label, avatarColor: a.avatarColor, - })); +/** + * This browser's accounts as the archive keeps them. With `includePasswords`, + * an account signed in here with a plain password carries it; tokens and + * accounts not signed in carry nothing. + */ +export function collectAccounts(includePasswords = false): ArchivedAccount[] { + const auth = useAuthStore.getState(); + return useAccountStore.getState().accounts.map(a => { + const entry: ArchivedAccount = { username: a.username, serverUrl: a.serverUrl, label: a.label, avatarColor: a.avatarColor }; + const password = includePasswords && a.authMode === 'basic' ? auth.getClientForAccount(a.id)?.getBasicPassword() : null; + if (password) entry.password = password; + return entry; + }); } /** @@ -61,3 +70,24 @@ export function restoreAccounts(archived: ArchivedAccount[]): { added: number; u } return { added, updated, skipped }; } + +/** + * 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. + */ +export async function restoreArchive(archived: ArchivedAccount[], rememberMe: boolean) { + const result = restoreAccounts(archived); + let signedIn = 0, signInFailed = 0; + for (const entry of archived) { + if (!entry.password) continue; + const id = generateAccountId(entry.username, entry.serverUrl); + const account = useAccountStore.getState().getAccountById(id); + if (!account || account.authMode !== 'basic' || useAuthStore.getState().getClientForAccount(id)) continue; + if (await useAuthStore.getState().signInRestoredAccount(id, entry.password, rememberMe)) signedIn++; + else signInFailed++; + } + const waiting = archived.filter(e => useAccountStore.getState() + .getAccountById(generateAccountId(e.username, e.serverUrl))?.awaitingSignIn).length; + return { ...result, signedIn, signInFailed, waiting }; +} diff --git a/lib/account-archive.ts b/lib/account-archive.ts index fa64a505f..7f7fa50c1 100644 --- a/lib/account-archive.ts +++ b/lib/account-archive.ts @@ -3,12 +3,12 @@ * can bring the same accounts back. Encryption happens in the browser with a * password the user chooses; the server only ever sees the envelope. * - * The list holds no mailbox passwords: each restored account is signed in to - * by hand. + * Mailbox passwords are optional: an account saved with its password is + * signed in to on restore, one saved without asks for its own sign-in. */ export interface ArchiveOwner { username: string; serverUrl: string } -export interface ArchivedAccount extends ArchiveOwner { label: string; avatarColor: string } +export interface ArchivedAccount extends ArchiveOwner { label: string; avatarColor: string; password?: string } export interface ArchiveEnvelope { version: 1; iterations: typeof ARCHIVE_ITERATIONS; @@ -55,9 +55,14 @@ export function parseArchivedAccounts(value: unknown): ArchivedAccount[] { return value.map((a: ArchivedAccount) => { archiveIdentity(a); if (typeof a.label !== 'string' || a.label.length > 320 - || typeof a.avatarColor !== 'string' || !/^#[0-9a-f]{6}$/i.test(a.avatarColor)) throw new Error('invalid_archive'); - // Rebuilt field by field, so nothing else (a password, say) survives a round trip. - return { username: a.username.trim(), serverUrl: trimUrl(a.serverUrl), label: a.label, avatarColor: a.avatarColor }; + || typeof a.avatarColor !== 'string' || !/^#[0-9a-f]{6}$/i.test(a.avatarColor) + || (a.password !== undefined && (typeof a.password !== 'string' || !a.password || a.password.length > 1024))) { + throw new Error('invalid_archive'); + } + // Rebuilt field by field, so nothing else (a token, say) survives a round trip. + const entry: ArchivedAccount = { username: a.username.trim(), serverUrl: trimUrl(a.serverUrl), label: a.label, avatarColor: a.avatarColor }; + if (a.password !== undefined) entry.password = a.password; + return entry; }); } diff --git a/lib/admin/types.ts b/lib/admin/types.ts index beefa6730..fef6c081f 100644 --- a/lib/admin/types.ts +++ b/lib/admin/types.ts @@ -63,6 +63,8 @@ export interface FeatureGates { unifiedCrossAccountEnabled: boolean; /** Encrypted account list kept on the server, restorable on another device. */ accountArchiveEnabled: boolean; + /** Lets that list carry mailbox passwords, so a restore signs in by itself. */ + accountArchivePasswordsEnabled: boolean; /** Composer "Custom" From: free-text sender address instead of an identity. */ customFromEnabled: boolean; /** The Archive action on messages (button, menus, hover, swipe, shortcut). */ @@ -95,6 +97,7 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = { crossAllViewEnabled: false, unifiedCrossAccountEnabled: false, accountArchiveEnabled: false, + accountArchivePasswordsEnabled: false, customFromEnabled: true, archiveActionEnabled: true, }; diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index f81daa597..5032231b4 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -1195,6 +1195,15 @@ export class JMAPClient implements IJMAPClient { return this.authHeader; } + /** + * The password of a plain Basic session, or null for a bearer session and + * for one that appends a TOTP code (that code expires; the password alone + * would not sign in). + */ + getBasicPassword(): string | null { + return this.authHeader.startsWith('Basic ') && !this.onTotpRequired ? this.password : null; + } + getServerUrl(): string { return this.serverUrl; } diff --git a/lib/lite-config.ts b/lib/lite-config.ts index ac1a4ea1b..b3f6ed201 100644 --- a/lib/lite-config.ts +++ b/lib/lite-config.ts @@ -125,6 +125,7 @@ export function applyLitePolicy(raw: unknown): SettingsPolicy { sidebarAppsEnabled: false, // Kept by this app's server, which Lite does not have. accountArchiveEnabled: false, + accountArchivePasswordsEnabled: false, }, forceEnabledPlugins: [], approvedPlugins: [], diff --git a/locales/ar/common.json b/locales/ar/common.json index 4089fc848..4aa231a2c 100644 --- a/locales/ar/common.json +++ b/locales/ar/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/ca/common.json b/locales/ca/common.json index b64d5c60d..3f8d316cf 100644 --- a/locales/ca/common.json +++ b/locales/ca/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/cs/common.json b/locales/cs/common.json index a8cf7a0a7..ddc3fd34a 100644 --- a/locales/cs/common.json +++ b/locales/cs/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/da/common.json b/locales/da/common.json index cdf5b2fc7..33c21f084 100644 --- a/locales/da/common.json +++ b/locales/da/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/de/common.json b/locales/de/common.json index e77c5d5b6..eab6d7d39 100644 --- a/locales/de/common.json +++ b/locales/de/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Sicherung der Kontoliste", "description": "Speichert die Liste der Konten in diesem Browser auf dem Server, verschlüsselt mit einem Passwort, das nur Sie kennen, und stellt sie auf einem anderen Gerät wieder her. Postfachpasswörter werden nicht gespeichert: Sie melden sich bei jedem wiederhergestellten Konto selbst an.", + "description_passwords": "Speichert die Liste der Konten in diesem Browser auf dem Server, verschlüsselt mit einem Passwort, das nur Sie kennen, und stellt sie auf einem anderen Gerät wieder her. Sie können die Postfachpasswörter einschließen, dann melden sich die wiederhergestellten Konten selbst an.", "status_none": "Für {account} ist keine Liste gespeichert.", "status_saved": "Für {account} ist eine Liste gespeichert.", "save": "Liste speichern", @@ -1756,9 +1757,15 @@ "password_hint": "Mindestens 10 Zeichen. Es kann nicht wiederhergestellt werden: Wenn Sie es vergessen, speichern Sie die Liste erneut mit einem neuen.", "restore_hint": "Das Passwort, das beim Speichern der Liste gewählt wurde.", "confirm": "Passwort bestätigen", + "include_passwords": "Postfachpasswörter einschließen", + "include_passwords_hint": "Konten, die hier mit einem Passwort angemeldet sind, melden sich nach einer Wiederherstellung selbst an. Wer das Sicherungspasswort kennt, kommt in jedes Postfach der Liste.", "saved": "Liste gespeichert ({count, plural, one {# Konto} other {# Konten}}).", "restored": "Hinzugefügte Konten: {added}. Bereits vorhandene, aktualisierte Konten: {updated}. Öffnen Sie jedes neue Konto in der Kontoliste, um sich anzumelden.", + "restored_signed_in": "Hinzugefügte Konten: {added}. Bereits vorhandene, aktualisierte Konten: {updated}.", "skipped": "{count, plural, one {# Konto wurde} other {# Konten wurden}} nicht hinzugefügt: Die Höchstzahl an Konten ist erreicht.", + "saved_passwords": "Passwörter für {count, plural, one {# Konto} other {# Konten}} eingeschlossen.", + "signed_in": "Bei {count, plural, one {# Konto} other {# Konten}} mit dem gespeicherten Passwort angemeldet.", + "sign_in_failed": "{count, plural, one {# gespeichertes Passwort wurde} other {# gespeicherte Passwörter wurden}} abgelehnt: Melden Sie sich bei diesen Konten über die Kontoliste an.", "deleted": "Gespeicherte Liste gelöscht.", "errors": { "mismatch": "Die Passwörter stimmen nicht überein.", diff --git a/locales/en/common.json b/locales/en/common.json index 5202c8878..8d92a4508 100644 --- a/locales/en/common.json +++ b/locales/en/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/es/common.json b/locales/es/common.json index c119e7947..6ef191e78 100644 --- a/locales/es/common.json +++ b/locales/es/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Copia de la lista de cuentas", "description": "Guarda en el servidor la lista de cuentas de este navegador, cifrada con una contraseña que solo tú conoces, y recupérala en otro dispositivo. Las contraseñas de los buzones no se guardan: inicias sesión tú en cada cuenta restaurada.", + "description_passwords": "Guarda en el servidor la lista de cuentas de este navegador, cifrada con una contraseña que solo tú conoces, y recupérala en otro dispositivo. Puedes incluir las contraseñas de los buzones, así las cuentas restauradas inician sesión solas.", "status_none": "No hay ninguna lista guardada para {account}.", "status_saved": "Hay una lista guardada para {account}.", "save": "Guardar lista", @@ -1756,9 +1757,15 @@ "password_hint": "Al menos 10 caracteres. No se puede recuperar: si la olvidas, vuelve a guardar la lista con una nueva.", "restore_hint": "La contraseña elegida al guardar la lista.", "confirm": "Confirmar contraseña", + "include_passwords": "Incluir las contraseñas de los buzones", + "include_passwords_hint": "Las cuentas iniciadas aquí con contraseña iniciarán sesión solas tras restaurar. Quien conozca la contraseña de la copia entra en todos los buzones de la lista.", "saved": "Lista guardada ({count, plural, one {# cuenta} other {# cuentas}}).", "restored": "Cuentas añadidas: {added}. Cuentas ya presentes y actualizadas: {updated}. Abre cada cuenta nueva desde la lista de cuentas para iniciar sesión.", + "restored_signed_in": "Cuentas añadidas: {added}. Cuentas ya presentes y actualizadas: {updated}.", "skipped": "{count, plural, one {# cuenta no se ha añadido} other {# cuentas no se han añadido}}: se ha alcanzado el límite de cuentas.", + "saved_passwords": "Contraseñas incluidas para {count, plural, one {# cuenta} other {# cuentas}}.", + "signed_in": "Sesión iniciada en {count, plural, one {# cuenta} other {# cuentas}} con la contraseña guardada.", + "sign_in_failed": "{count, plural, one {# contraseña guardada fue rechazada} other {# contraseñas guardadas fueron rechazadas}}: inicia sesión en esas cuentas desde la lista de cuentas.", "deleted": "Lista guardada eliminada.", "errors": { "mismatch": "Las contraseñas no coinciden.", diff --git a/locales/fa/common.json b/locales/fa/common.json index b75c3728f..81345055b 100644 --- a/locales/fa/common.json +++ b/locales/fa/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/fr/common.json b/locales/fr/common.json index 990185589..7a6c69ccd 100644 --- a/locales/fr/common.json +++ b/locales/fr/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Sauvegarde de la liste des comptes", "description": "Conservez sur le serveur la liste des comptes de ce navigateur, chiffrée avec un mot de passe que vous seul connaissez, et retrouvez-la sur un autre appareil. Les mots de passe des boîtes ne sont pas enregistrés : vous vous connectez vous-même à chaque compte restauré.", + "description_passwords": "Conservez sur le serveur la liste des comptes de ce navigateur, chiffrée avec un mot de passe que vous seul connaissez, et retrouvez-la sur un autre appareil. Vous pouvez inclure les mots de passe des boîtes : les comptes restaurés se connectent alors tout seuls.", "status_none": "Aucune liste enregistrée pour {account}.", "status_saved": "Une liste est enregistrée pour {account}.", "save": "Enregistrer la liste", @@ -1756,9 +1757,15 @@ "password_hint": "Au moins 10 caractères. Il ne peut pas être récupéré : si vous l'oubliez, enregistrez à nouveau la liste avec un nouveau mot de passe.", "restore_hint": "Le mot de passe choisi lors de l'enregistrement de la liste.", "confirm": "Confirmer le mot de passe", + "include_passwords": "Inclure les mots de passe des boîtes", + "include_passwords_hint": "Les comptes connectés ici avec un mot de passe se connecteront tout seuls après une restauration. Quiconque connaît le mot de passe de la sauvegarde accède à toutes les boîtes de la liste.", "saved": "Liste enregistrée ({count, plural, one {# compte} other {# comptes}}).", "restored": "Comptes ajoutés : {added}. Comptes déjà présents et mis à jour : {updated}. Ouvrez chaque nouveau compte depuis la liste des comptes pour vous connecter.", + "restored_signed_in": "Comptes ajoutés : {added}. Comptes déjà présents et mis à jour : {updated}.", "skipped": "{count, plural, one {# compte n'a pas été ajouté} other {# comptes n'ont pas été ajoutés}} : la limite de comptes est atteinte.", + "saved_passwords": "Mots de passe inclus pour {count, plural, one {# compte} other {# comptes}}.", + "signed_in": "Connecté à {count, plural, one {# compte} other {# comptes}} avec le mot de passe enregistré.", + "sign_in_failed": "{count, plural, one {# mot de passe enregistré a été refusé} other {# mots de passe enregistrés ont été refusés}} : connectez-vous à ces comptes depuis la liste des comptes.", "deleted": "Liste enregistrée supprimée.", "errors": { "mismatch": "Les mots de passe ne correspondent pas.", diff --git a/locales/he/common.json b/locales/he/common.json index 13e845790..df1b810e1 100644 --- a/locales/he/common.json +++ b/locales/he/common.json @@ -1710,6 +1710,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1721,9 +1722,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/hu/common.json b/locales/hu/common.json index 0c0c176c0..0f48291f9 100644 --- a/locales/hu/common.json +++ b/locales/hu/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/it/common.json b/locales/it/common.json index 788a0df6a..25fb5196d 100644 --- a/locales/it/common.json +++ b/locales/it/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Backup dell'elenco account", "description": "Conserva sul server l'elenco degli account di questo browser, cifrato con una password che conosci solo tu, e ripristinalo su un altro dispositivo. Le password delle caselle non vengono salvate: accedi tu a ogni account ripristinato.", + "description_passwords": "Conserva sul server l'elenco degli account di questo browser, cifrato con una password che conosci solo tu, e ripristinalo su un altro dispositivo. Puoi includere le password delle caselle, così gli account ripristinati accedono da soli.", "status_none": "Nessun elenco salvato per {account}.", "status_saved": "C'è un elenco salvato per {account}.", "save": "Salva elenco", @@ -1756,9 +1757,15 @@ "password_hint": "Almeno 10 caratteri. Non si può recuperare: se la dimentichi, salva di nuovo l'elenco con una nuova password.", "restore_hint": "La password scelta quando l'elenco è stato salvato.", "confirm": "Conferma password", + "include_passwords": "Includi le password delle caselle", + "include_passwords_hint": "Gli account collegati qui con una password accederanno da soli dopo il ripristino. Chi conosce la password del backup entra in tutte le caselle dell'elenco.", "saved": "Elenco salvato ({count, plural, one {# account} other {# account}}).", "restored": "Account aggiunti: {added}. Account già presenti e aggiornati: {updated}. Apri ogni nuovo account dall'elenco account per accedere.", + "restored_signed_in": "Account aggiunti: {added}. Account già presenti e aggiornati: {updated}.", "skipped": "{count, plural, one {# account non è stato aggiunto} other {# account non sono stati aggiunti}}: è stato raggiunto il limite di account.", + "saved_passwords": "Password incluse per {count, plural, one {# account} other {# account}}.", + "signed_in": "Accesso fatto a {count, plural, one {# account} other {# account}} con la password salvata.", + "sign_in_failed": "{count, plural, one {# password salvata è stata rifiutata} other {# password salvate sono state rifiutate}}: accedi a quegli account dall'elenco account.", "deleted": "Elenco salvato eliminato.", "errors": { "mismatch": "Le password non coincidono.", diff --git a/locales/ja/common.json b/locales/ja/common.json index 6c4370fc5..780295405 100644 --- a/locales/ja/common.json +++ b/locales/ja/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/ko/common.json b/locales/ko/common.json index 0aff63e54..fb0d6c372 100644 --- a/locales/ko/common.json +++ b/locales/ko/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/lv/common.json b/locales/lv/common.json index ba600a13a..a53ce9158 100644 --- a/locales/lv/common.json +++ b/locales/lv/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/mn/common.json b/locales/mn/common.json index 57a0740e7..6dc30b6d0 100644 --- a/locales/mn/common.json +++ b/locales/mn/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/nb/common.json b/locales/nb/common.json index 18cf5471e..e7acbd08b 100644 --- a/locales/nb/common.json +++ b/locales/nb/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/nl/common.json b/locales/nl/common.json index a2615a9f0..1eadc7dfa 100644 --- a/locales/nl/common.json +++ b/locales/nl/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/pl/common.json b/locales/pl/common.json index 1c381b5c1..844a511cf 100644 --- a/locales/pl/common.json +++ b/locales/pl/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/pt/common.json b/locales/pt/common.json index e5778af2b..ec8cd9375 100644 --- a/locales/pt/common.json +++ b/locales/pt/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/ro/common.json b/locales/ro/common.json index cc8a2ccb9..2b19fea88 100644 --- a/locales/ro/common.json +++ b/locales/ro/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/ru/common.json b/locales/ru/common.json index 1dcc14dee..85aada5d9 100644 --- a/locales/ru/common.json +++ b/locales/ru/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/sk/common.json b/locales/sk/common.json index c35244b42..ea3ef1f3d 100644 --- a/locales/sk/common.json +++ b/locales/sk/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/tr/common.json b/locales/tr/common.json index 689c4b24f..4cf2a7ffb 100644 --- a/locales/tr/common.json +++ b/locales/tr/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/uk/common.json b/locales/uk/common.json index d5a992996..b4ba9ac92 100644 --- a/locales/uk/common.json +++ b/locales/uk/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/zh-TW/common.json b/locales/zh-TW/common.json index 7da62998d..a2090b0bc 100644 --- a/locales/zh-TW/common.json +++ b/locales/zh-TW/common.json @@ -1748,6 +1748,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1759,9 +1760,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/locales/zh/common.json b/locales/zh/common.json index f0d94539d..62bbd2cce 100644 --- a/locales/zh/common.json +++ b/locales/zh/common.json @@ -1745,6 +1745,7 @@ "backup": { "title": "Account list backup", "description": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. Mailbox passwords are not saved: you sign in to each restored account yourself.", + "description_passwords": "Keep the list of accounts in this browser on the server, encrypted with a password only you know, and bring it back on another device. You can include the mailbox passwords, so the restored accounts sign in by themselves.", "status_none": "No list is saved for {account}.", "status_saved": "A list is saved for {account}.", "save": "Save list", @@ -1756,9 +1757,15 @@ "password_hint": "At least 10 characters. It cannot be recovered: if you forget it, save the list again with a new one.", "restore_hint": "The password chosen when the list was saved.", "confirm": "Confirm password", + "include_passwords": "Include mailbox passwords", + "include_passwords_hint": "Accounts signed in here with a password will sign in by themselves after a restore. Anyone who knows the backup password gets into every mailbox in the list.", "saved": "List saved ({count, plural, one {# account} other {# accounts}}).", "restored": "Accounts added: {added}. Accounts already here and updated: {updated}. Open each new account from the account list to sign in.", + "restored_signed_in": "Accounts added: {added}. Accounts already here and updated: {updated}.", "skipped": "{count, plural, one {# account was} other {# accounts were}} not added: the account limit is reached.", + "saved_passwords": "Passwords included for {count, plural, one {# account} other {# accounts}}.", + "signed_in": "Signed in to {count, plural, one {# account} other {# accounts}} with the saved password.", + "sign_in_failed": "{count, plural, one {# saved password was} other {# saved passwords were}} refused: sign in to those accounts from the account list.", "deleted": "Saved list deleted.", "errors": { "mismatch": "The passwords do not match.", diff --git a/stores/__tests__/auth-store-archive-restore.test.ts b/stores/__tests__/auth-store-archive-restore.test.ts index 47f51b689..5c9511b43 100644 --- a/stores/__tests__/auth-store-archive-restore.test.ts +++ b/stores/__tests__/auth-store-archive-restore.test.ts @@ -8,7 +8,7 @@ vi.mock('@/lib/jmap/client', () => { class JMAPClient { constructor(public serverUrl: string, public username: string, public password: string) {} static withBearer() { throw new Error('not used'); } - async connect() {} + async connect() { if (this.password === 'wrong') throw new Error('Authentication failed'); } getSessionUsername() { return this.username; } getUsername() { return this.username; } async getIdentities() { return [{ id: 'a', email: this.username, name: this.username, mayDelete: false }]; } @@ -119,3 +119,25 @@ it('treats a browser left with only accounts awaiting sign-in as signed out, not expect(useAuthStore.getState().error).not.toBe('connection_failed'); expect(useAccountStore.getState().accounts.map(a => a.id)).toEqual(['alice@x.test@mail.x.test']); }); + +it('signs in to a restored account with its saved password without leaving the active one', async () => { + restoreAccounts(archived); + await useAuthStore.getState().checkAuth(); + const alice = 'alice@x.test@mail.x.test'; + const fetchMock = vi.mocked(fetch); + fetchMock.mockClear(); + const active = useAuthStore.getState().activeAccountId; + + expect(await useAuthStore.getState().signInRestoredAccount(alice, 'wrong', true)).toBe(false); + expect(useAccountStore.getState().getAccountById(alice)?.awaitingSignIn).toBe(true); + + expect(await useAuthStore.getState().signInRestoredAccount(alice, 'alice-pw', true)).toBe(true); + expect(useAuthStore.getState().activeAccountId).toBe(active); + expect(useAuthStore.getState().getClientForAccount(alice)).toBeDefined(); + expect(useAccountStore.getState().getAccountById(alice)).toMatchObject({ isConnected: true, rememberMe: true, awaitingSignIn: false }); + const sessionWrite = fetchMock.mock.calls.find(([url, init]) => String(url).includes('/api/auth/session') && init?.method === 'POST'); + expect(JSON.parse(String(sessionWrite?.[1]?.body))).toMatchObject({ username: 'alice@x.test', password: 'alice-pw' }); + + // Already connected: nothing to do. + expect(await useAuthStore.getState().signInRestoredAccount(alice, 'alice-pw', true)).toBe(false); +}); diff --git a/stores/auth-store.ts b/stores/auth-store.ts index e3a78b048..9d6723174 100644 --- a/stores/auth-store.ts +++ b/stores/auth-store.ts @@ -120,6 +120,12 @@ interface AuthState { * session still hold the old one, which the server now refuses. */ updateBasicPassword: (newPassword: string) => Promise; + /** + * Sign in, in the background, to a Basic account restored from the account + * list backup with its password. The active account stays as it is. + * Resolves false when the server refuses the password or cannot be reached. + */ + signInRestoredAccount: (accountId: string, password: string, rememberMe: boolean) => Promise; refreshIdentities: () => Promise; getClientForAccount: (accountId: string) => JMAPClient | undefined; getAllConnectedClients: () => Map; @@ -3031,6 +3037,37 @@ export const useAuthStore = create()( } }, + signInRestoredAccount: async (accountId, password, rememberMe) => { + const accountStore = useAccountStore.getState(); + const account = accountStore.getAccountById(accountId); + if (!account || account.authMode !== 'basic' || clients.has(accountId)) return false; + const { serverUrl, username, cookieSlot } = account; + if (await precheckCredentials(serverUrl, { username, password })) return false; + const client = new JMAPClient(serverUrl, username, password); + try { + await client.connect(); + } catch (err) { + debug.error(`Failed to sign in to restored account ${accountId}:`, err); + client.disconnect(); + return false; + } + // Without a remembered session the next reload would drop the account + // again, like any sign-in made without "Remember me". + await Promise.all([ + rememberMe || IS_LITE ? persistSession(cookieSlot, { serverUrl, username, password }) : null, + syncStalwartAuthContext(serverUrl, username, client.getAuthHeader(), cookieSlot), + ]); + clients.set(accountId, client); + bindClientStatusHandlers(client, set, get, accountId); + accountStore.updateAccount(accountId, { + awaitingSignIn: false, isConnected: true, hasError: false, errorMessage: undefined, + rememberMe, lastLoginAt: Date.now(), + }); + void syncAccountDisplayName(accountId, client); + set((state) => ({ connectedAccountsRevision: state.connectedAccountsRevision + 1 })); + return true; + }, + syncIdentities: () => { const identityState = useIdentityStore.getState(); const identities = identityState.identities;