mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-30 16:16:19 +00:00
feat: optionally keep mailbox passwords in the account list backup
Part 2 of 3 of the account archive agreed in #1198. When the admin turns on "Account List Backup: Passwords" (off by default), saving the list offers to include the passwords of the accounts signed in here with a plain password. They are encrypted in the browser together with the list. On restore, those accounts are signed in to in the background, one by one, without leaving the active account; a refused password leaves the account waiting for a sign-in by hand, as before. OAuth and TOTP accounts are saved without a password.
This commit is contained in:
@@ -39,6 +39,7 @@ const FEATURE_GATE_LABELS: Partial<Record<keyof FeatureGates, { label: string; d
|
||||
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.' },
|
||||
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.' },
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { decryptArchive, encryptArchive, ARCHIVE_MIN_PASSWORD, type ArchiveOwner } from '@/lib/account-archive';
|
||||
import { collectAccounts, fetchArchive, putArchive, removeArchive, restoreAccounts } from '@/lib/account-archive-client';
|
||||
import { collectAccounts, fetchArchive, putArchive, removeArchive, restoreArchive } from '@/lib/account-archive-client';
|
||||
import { toUnicodeEmail } from '@/lib/idn';
|
||||
import { SettingsSection } from './settings-section';
|
||||
|
||||
@@ -18,10 +20,13 @@ export function AccountArchiveSettings() {
|
||||
const id = useId();
|
||||
const username = useAuthStore(s => s.username);
|
||||
const serverUrl = useAuthStore(s => s.serverUrl);
|
||||
const passwordsAllowed = usePolicyStore(s => s.isFeatureEnabled('accountArchivePasswordsEnabled'));
|
||||
const { rememberMeEnabled } = useConfig();
|
||||
const [saved, setSaved] = useState<boolean | null>(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<string>) => {
|
||||
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 (
|
||||
<SettingsSection title={t('title')} description={t('description')}>
|
||||
<SettingsSection title={t('title')} description={t(passwordsAllowed ? 'description_passwords' : 'description')}>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{saved === null ? '…' : t(saved ? 'status_saved' : 'status_none', { account: toUnicodeEmail(owner.username) })}
|
||||
</p>
|
||||
@@ -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 && (
|
||||
<div className="space-y-1">
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox" checked={includePasswords} aria-describedby={`${id}-passwords-hint`}
|
||||
onChange={e => setIncludePasswords(e.target.checked)}
|
||||
/>
|
||||
{t('include_passwords')}
|
||||
</label>
|
||||
<p id={`${id}-passwords-hint`} className="text-xs text-muted-foreground ps-6">{t('include_passwords_hint')}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const auth = vi.hoisted(() => ({
|
||||
clients: new Map<string, { getBasicPassword: () => 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 });
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<void> {
|
||||
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 };
|
||||
}
|
||||
|
||||
+11
-6
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -93,6 +95,7 @@ export const DEFAULT_FEATURE_GATES: FeatureGates = {
|
||||
crossAllViewEnabled: false,
|
||||
unifiedCrossAccountEnabled: false,
|
||||
accountArchiveEnabled: false,
|
||||
accountArchivePasswordsEnabled: false,
|
||||
customFromEnabled: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -1176,6 +1176,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;
|
||||
}
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1697,6 +1697,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",
|
||||
@@ -1708,9 +1709,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -1735,6 +1735,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",
|
||||
@@ -1746,9 +1747,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.",
|
||||
|
||||
@@ -1732,6 +1732,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",
|
||||
@@ -1743,9 +1744,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.",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -120,6 +120,12 @@ interface AuthState {
|
||||
* session still hold the old one, which the server now refuses.
|
||||
*/
|
||||
updateBasicPassword: (newPassword: string) => Promise<void>;
|
||||
/**
|
||||
* 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<boolean>;
|
||||
refreshIdentities: () => Promise<void>;
|
||||
getClientForAccount: (accountId: string) => JMAPClient | undefined;
|
||||
getAllConnectedClients: () => Map<string, JMAPClient>;
|
||||
@@ -3031,6 +3037,37 @@ export const useAuthStore = create<AuthState>()(
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user