mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 16:14:07 +00:00
feat: ask for the email address first and find the mail server on sign-in
Where people may sign in to a server of their own (the custom endpoint setting and the desktop app's standalone mode), the sign-in form asked for a "JMAP Server" first, refused a host without https:// with the browser's own bubble and talked about CORS. Now the email address comes first and the server is looked up from its domain (JMAP well-known, SRV, then the admin's default) through a new budgeted route. The "Mail server" field opens only when nothing is found, takes a bare host, and keeps the CORS hint behind "Having trouble?" (not in the app). Gmail, Outlook.com and iCloud addresses get a plain note that these offer no JMAP. The app's browser button stays the main one and asks for what it needs under the field it needs. Also on the page: the theme button is an icon named "Light or dark", the heading is the app's name only when the admin gave one (else "Sign in", without the subtitle repeating it), "I have a code from my authenticator app", "Add account", and no 11 px text. login-page-i18n.test.tsx now names the app for the subtitle test and waits for the heading, since the default name no longer shows a subtitle, and finds the theme button by its new name.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// The desktop app with a mail server of one's own: email first, the server found
|
||||
// from it, and "Sign in with your browser" as the main button. When the button
|
||||
// needs something, it says so where that goes.
|
||||
|
||||
// Stubbed, so the flag doesn't stay set for the next file in this worker.
|
||||
vi.hoisted(() => { vi.stubEnv('NEXT_PUBLIC_BULWARK_DESKTOP', '1'); });
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ locale: 'en' }),
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),
|
||||
usePathname: () => '/login',
|
||||
}));
|
||||
vi.mock('@/i18n/navigation', () => ({
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),
|
||||
usePathname: () => '/login',
|
||||
Link: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
vi.mock('@/hooks/use-config', () => ({
|
||||
useConfig: () => ({
|
||||
appName: 'Bulwark',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
jmapServerUrl: '',
|
||||
jmapServers: [],
|
||||
oauthEnabled: false,
|
||||
oauthOnly: false,
|
||||
allowCustomJmapEndpoint: true,
|
||||
desktopBrowserSignIn: true,
|
||||
loginShowHeading: true,
|
||||
}),
|
||||
}));
|
||||
const runDesktopBrowserSignIn = vi.hoisted(() => vi.fn(async (_options: { serverUrl: string }) => ({ kind: 'cancelled' as const })));
|
||||
vi.mock('@/lib/desktop/browser-sign-in', () => ({
|
||||
browserSignInAvailable: () => true,
|
||||
cancelBrowserSignIn: () => {},
|
||||
runDesktopBrowserSignIn,
|
||||
runInstanceBrowserSignIn: vi.fn(),
|
||||
}));
|
||||
|
||||
const { default: LoginPage } = await import('../page');
|
||||
|
||||
let found: Record<string, string | null>;
|
||||
|
||||
beforeEach(() => {
|
||||
found = {};
|
||||
runDesktopBrowserSignIn.mockClear();
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.includes('/api/auth/discover-server')) {
|
||||
const domain = (JSON.parse(String(init?.body)).email as string).split('@')[1];
|
||||
return new Response(JSON.stringify({ serverUrl: found[domain] ?? null }), { status: 200 });
|
||||
}
|
||||
return new Response('{}', { status: 200 });
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
const browserButton = () => screen.findByTestId('desktop-browser-sign-in', {}, { timeout: 10000 });
|
||||
const email = () => document.getElementById('username') as HTMLInputElement;
|
||||
const serverField = () => document.getElementById('jmap-endpoint') as HTMLInputElement | null;
|
||||
|
||||
describe('the desktop app, with a server of one\'s own', () => {
|
||||
it('puts the email address before the browser button', async () => {
|
||||
render(<LoginPage />);
|
||||
const button = await browserButton();
|
||||
expect(email().compareDocumentPosition(button) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(serverField()).toBeNull();
|
||||
// No website to allow in the app: no CORS hint at all.
|
||||
expect(screen.queryByTestId('login-server-trouble')).toBeNull();
|
||||
}, 30_000);
|
||||
|
||||
it('asks for the address under its field when the browser button is pressed first', async () => {
|
||||
render(<LoginPage />);
|
||||
fireEvent.click(await browserButton());
|
||||
expect(await screen.findByTestId('login-email-needed')).toHaveTextContent('email_needed');
|
||||
expect(document.activeElement).toBe(email());
|
||||
expect(runDesktopBrowserSignIn).not.toHaveBeenCalled();
|
||||
}, 30_000);
|
||||
|
||||
it('sends the browser to the server found from the address', async () => {
|
||||
found['example.org'] = 'https://mail.example.org';
|
||||
render(<LoginPage />);
|
||||
const button = await browserButton();
|
||||
fireEvent.change(email(), { target: { value: 'sam@example.org' } });
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => expect(runDesktopBrowserSignIn).toHaveBeenCalled());
|
||||
expect(runDesktopBrowserSignIn.mock.calls[0][0].serverUrl).toBe('https://mail.example.org');
|
||||
}, 30_000);
|
||||
|
||||
it('opens, marks and focuses the server field when nothing was found', async () => {
|
||||
render(<LoginPage />);
|
||||
const button = await browserButton();
|
||||
fireEvent.change(email(), { target: { value: 'sam@example.net' } });
|
||||
fireEvent.click(button);
|
||||
expect(await screen.findByTestId('login-server-error')).toHaveTextContent('server_needed');
|
||||
await waitFor(() => expect(document.activeElement).toBe(serverField()));
|
||||
expect(serverField()).toHaveAttribute('aria-invalid', 'true');
|
||||
expect(screen.queryByTestId('desktop-notice')).toBeNull();
|
||||
expect(runDesktopBrowserSignIn).not.toHaveBeenCalled();
|
||||
fireEvent.change(serverField()!, { target: { value: 'mail.example.net:8443' } });
|
||||
fireEvent.click(button);
|
||||
await waitFor(() => expect(runDesktopBrowserSignIn).toHaveBeenCalled());
|
||||
expect(runDesktopBrowserSignIn.mock.calls[0][0].serverUrl).toBe('https://mail.example.net:8443');
|
||||
}, 30_000);
|
||||
});
|
||||
@@ -41,14 +41,21 @@ const { useAuthStore } = await import('@/stores/auth-store');
|
||||
|
||||
describe('login page translations', () => {
|
||||
it('shows the translated subtitle whatever the app is called', async () => {
|
||||
render(<LoginPage />);
|
||||
expect(await screen.findByText('subtitle', {}, { timeout: 10000 })).toBeInTheDocument();
|
||||
expect(screen.queryByText('Sign in to your account')).toBeNull();
|
||||
// Under an app name the admin gave: with the default name the heading is
|
||||
// "Sign in" itself, and the subtitle would repeat it.
|
||||
configOverrides.value = { appName: 'Acme Mail' };
|
||||
try {
|
||||
render(<LoginPage />);
|
||||
expect(await screen.findByText('subtitle', {}, { timeout: 10000 })).toBeInTheDocument();
|
||||
expect(screen.queryByText('Sign in to your account')).toBeNull();
|
||||
} finally {
|
||||
configOverrides.value = {};
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
it('labels the theme menu from the translations', async () => {
|
||||
render(<LoginPage />);
|
||||
const button = await screen.findByRole('button', { name: 'label: system' }, { timeout: 10000 });
|
||||
const button = await screen.findByRole('button', { name: 'theme_button: system' }, { timeout: 10000 });
|
||||
fireEvent.click(button);
|
||||
expect(screen.getByRole('menu', { name: 'label' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('menuitemradio', { name: 'dark' })).toBeInTheDocument();
|
||||
@@ -87,7 +94,7 @@ describe('login page translations', () => {
|
||||
describe('login page notices', () => {
|
||||
it('asks for the second factor as the next step, not as a failure (#1422)', async () => {
|
||||
render(<LoginPage />);
|
||||
await screen.findByText('subtitle', {}, { timeout: 10000 });
|
||||
await screen.findByRole('heading', { level: 1 }, { timeout: 10000 });
|
||||
// The page clears the error as it mounts; a sign-in attempt sets it after.
|
||||
act(() => { useAuthStore.setState({ error: 'totp_required' }); });
|
||||
const notice = await screen.findByTestId('login-error', {}, { timeout: 10000 });
|
||||
@@ -99,7 +106,7 @@ describe('login page notices', () => {
|
||||
|
||||
it('keeps a wrong password an error', async () => {
|
||||
render(<LoginPage />);
|
||||
await screen.findByText('subtitle', {}, { timeout: 10000 });
|
||||
await screen.findByRole('heading', { level: 1 }, { timeout: 10000 });
|
||||
// The page clears the error as it mounts; a sign-in attempt sets it after.
|
||||
act(() => { useAuthStore.setState({ error: 'invalid_credentials' }); });
|
||||
const notice = await screen.findByTestId('login-error', {}, { timeout: 10000 });
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// A server of one's own (ALLOW_CUSTOM_JMAP_ENDPOINT): the email address comes first,
|
||||
// the server is looked up from it, and the field for it only opens when nothing is
|
||||
// found. A host typed without https:// works.
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ locale: 'en' }),
|
||||
useSearchParams: () => new URLSearchParams(),
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),
|
||||
usePathname: () => '/login',
|
||||
}));
|
||||
vi.mock('@/i18n/navigation', () => ({
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),
|
||||
usePathname: () => '/login',
|
||||
Link: ({ children }: { children: React.ReactNode }) => children,
|
||||
}));
|
||||
|
||||
const configOverrides = vi.hoisted(() => ({ value: {} as Record<string, unknown> }));
|
||||
vi.mock('@/hooks/use-config', () => ({
|
||||
useConfig: () => ({
|
||||
appName: 'Webmail',
|
||||
isLoading: false,
|
||||
error: null,
|
||||
jmapServerUrl: '',
|
||||
jmapServers: [],
|
||||
oauthEnabled: false,
|
||||
oauthOnly: false,
|
||||
allowCustomJmapEndpoint: true,
|
||||
loginShowHeading: true,
|
||||
loginShowSubtitle: true,
|
||||
loginShowTotp: true,
|
||||
...configOverrides.value,
|
||||
}),
|
||||
}));
|
||||
|
||||
const { default: LoginPage } = await import('../page');
|
||||
const { useAuthStore } = await import('@/stores/auth-store');
|
||||
|
||||
let found: Record<string, string | null>;
|
||||
let lookups: string[];
|
||||
const login = vi.fn(async (..._args: unknown[]) => false);
|
||||
|
||||
beforeEach(() => {
|
||||
configOverrides.value = {};
|
||||
found = {};
|
||||
lookups = [];
|
||||
login.mockClear();
|
||||
useAuthStore.setState({ login: login as never, error: null, isLoading: false });
|
||||
vi.stubGlobal('fetch', vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
if (url.includes('/api/auth/discover-server')) {
|
||||
const email = JSON.parse(String(init?.body)).email as string;
|
||||
lookups.push(email);
|
||||
const domain = email.split('@')[1];
|
||||
return new Response(JSON.stringify({ serverUrl: found[domain] ?? null }), { status: 200 });
|
||||
}
|
||||
return new Response('{}', { status: 200 });
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const email = () => document.getElementById('username') as HTMLInputElement;
|
||||
const serverField = () => document.getElementById('jmap-endpoint') as HTMLInputElement | null;
|
||||
const form = () => email().closest('form')!;
|
||||
|
||||
async function typeAddress(address: string) {
|
||||
await waitFor(() => expect(email()).not.toBeNull(), { timeout: 10000 });
|
||||
fireEvent.change(email(), { target: { value: address } });
|
||||
fireEvent.change(document.getElementById('password')!, { target: { value: 'secret' } });
|
||||
}
|
||||
|
||||
describe('sign-in to a server of one\'s own', () => {
|
||||
it('asks for the email address first, with no server field and no CORS line', async () => {
|
||||
render(<LoginPage />);
|
||||
await waitFor(() => expect(email()).not.toBeNull(), { timeout: 10000 });
|
||||
expect(serverField()).toBeNull();
|
||||
expect(screen.queryByText('jmap_endpoint_cors_hint')).toBeNull();
|
||||
expect(document.activeElement).toBe(email());
|
||||
}, 30_000);
|
||||
|
||||
it('finds the server from the address and signs in there', async () => {
|
||||
found['example.org'] = 'https://mail.example.org';
|
||||
render(<LoginPage />);
|
||||
await typeAddress('sam@example.org');
|
||||
expect(await screen.findByTestId('login-server-found', {}, { timeout: 5000 })).toHaveTextContent('server_found');
|
||||
expect(serverField()).toBeNull();
|
||||
fireEvent.submit(form());
|
||||
await waitFor(() => expect(login).toHaveBeenCalled());
|
||||
expect(login.mock.calls[0][0]).toBe('https://mail.example.org');
|
||||
expect(lookups).toEqual(['sam@example.org']);
|
||||
}, 30_000);
|
||||
|
||||
it('opens the field when nothing is found, and takes a host without https://', async () => {
|
||||
render(<LoginPage />);
|
||||
await typeAddress('sam@example.net');
|
||||
await waitFor(() => expect(serverField()).not.toBeNull(), { timeout: 5000 });
|
||||
expect(serverField()!.type).toBe('text');
|
||||
expect(screen.getByTestId('login-server-not-found')).toBeInTheDocument();
|
||||
// The CORS hint waits behind "Having trouble?".
|
||||
expect(screen.getByTestId('login-server-trouble').tagName).toBe('DETAILS');
|
||||
fireEvent.change(serverField()!, { target: { value: 'mail.example.net' } });
|
||||
fireEvent.submit(form());
|
||||
await waitFor(() => expect(login).toHaveBeenCalled());
|
||||
expect(login.mock.calls[0][0]).toBe('https://mail.example.net');
|
||||
}, 30_000);
|
||||
|
||||
it('signing in before the lookup ends waits for it', async () => {
|
||||
found['example.org'] = 'https://mail.example.org';
|
||||
render(<LoginPage />);
|
||||
await typeAddress('sam@example.org');
|
||||
fireEvent.submit(form());
|
||||
await waitFor(() => expect(login).toHaveBeenCalled());
|
||||
expect(login.mock.calls[0][0]).toBe('https://mail.example.org');
|
||||
}, 30_000);
|
||||
|
||||
it('uses the admin\'s server when the lookup finds nothing', async () => {
|
||||
configOverrides.value = { jmapServerUrl: 'https://default.example.com' };
|
||||
render(<LoginPage />);
|
||||
await typeAddress('sam@example.net');
|
||||
expect(await screen.findByTestId('login-server-found', {}, { timeout: 5000 })).toBeInTheDocument();
|
||||
expect(serverField()).toBeNull();
|
||||
fireEvent.submit(form());
|
||||
await waitFor(() => expect(login).toHaveBeenCalled());
|
||||
expect(login.mock.calls[0][0]).toBe('https://default.example.com');
|
||||
}, 30_000);
|
||||
|
||||
it('marks the field and signs in nowhere when the server address is no address', async () => {
|
||||
render(<LoginPage />);
|
||||
await typeAddress('sam@example.net');
|
||||
await waitFor(() => expect(serverField()).not.toBeNull(), { timeout: 5000 });
|
||||
fireEvent.change(serverField()!, { target: { value: 'mail example' } });
|
||||
fireEvent.submit(form());
|
||||
expect(await screen.findByTestId('login-server-error')).toHaveTextContent('server_invalid');
|
||||
expect(serverField()).toHaveAttribute('aria-invalid', 'true');
|
||||
await waitFor(() => expect(document.activeElement).toBe(serverField()));
|
||||
expect(login).not.toHaveBeenCalled();
|
||||
}, 30_000);
|
||||
|
||||
it('says plainly that Gmail can\'t be reached, and asks nothing', async () => {
|
||||
render(<LoginPage />);
|
||||
await typeAddress('sam@gmail.com');
|
||||
expect(await screen.findByTestId('login-provider-unsupported')).toHaveTextContent('provider_unsupported');
|
||||
await act(async () => { await new Promise((r) => setTimeout(r, 800)); });
|
||||
expect(lookups).toEqual([]);
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
describe('the page around the form', () => {
|
||||
it('the theme button is an icon named "Light or dark", with a tooltip', async () => {
|
||||
render(<LoginPage />);
|
||||
const button = await screen.findByTestId('login-theme-button', {}, { timeout: 10000 });
|
||||
expect(button).toHaveAttribute('title', 'theme_button');
|
||||
expect(button.getAttribute('aria-label')).toMatch(/^theme_button: /);
|
||||
expect(button.textContent).toBe('');
|
||||
}, 30_000);
|
||||
|
||||
it('the heading says "Sign in" unless the admin named the app', async () => {
|
||||
render(<LoginPage />);
|
||||
expect(await screen.findByRole('heading', { level: 1 }, { timeout: 10000 })).toHaveTextContent('sign_in');
|
||||
}, 30_000);
|
||||
|
||||
it('the heading is the app\'s name when the admin gave one', async () => {
|
||||
configOverrides.value = { appName: 'Example Mail' };
|
||||
render(<LoginPage />);
|
||||
expect(await screen.findByRole('heading', { level: 1 }, { timeout: 10000 })).toHaveTextContent('Example Mail');
|
||||
}, 30_000);
|
||||
});
|
||||
@@ -46,6 +46,8 @@ import { usePrefetchMailView } from "@/hooks/use-prefetch-mail-view";
|
||||
import { buildMobileHandoffCodeUrl, buildMobileHandoffUrl, desktopHandoffDeclineUrl, desktopHandoffPort, desktopHandoffTarget, mobileCodeChallenge, mobileHandoffTarget, signedInLoginAction } from "@/lib/auth/mobile-handoff";
|
||||
import { checkDesktopSignIn, parkDesktopSignIn, requestMobileHandoffCodeUrl } from "@/lib/auth/mobile-handoff-client";
|
||||
import { MobileHandoffConsent } from "@/components/auth/mobile-handoff-consent";
|
||||
import { LoginServerField, requestServerDiscovery, type ServerFieldError } from "@/components/auth/login-server-field";
|
||||
import { emailDomain, normalizeServerInput, serverHostLabel, unsupportedProvider } from "@/lib/auth/mail-server-address";
|
||||
import { HelpLinks } from "@/components/layout/help-links";
|
||||
import { LoginSkeleton } from "@/components/boot-skeleton/login-skeleton";
|
||||
import { Block } from "@/components/boot-skeleton/kit";
|
||||
@@ -206,6 +208,7 @@ function LoginPageContent() {
|
||||
const tOffline = useTranslations("offline");
|
||||
const tOfflineWipe = useTranslations("offlineWipe");
|
||||
const tTheme = useTranslations("settings.appearance.theme");
|
||||
const tLogin = useTranslations("uxs_login");
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const isAddAccountMode = searchParams.get("mode") === "add-account";
|
||||
@@ -299,6 +302,29 @@ function LoginPageContent() {
|
||||
// with OAuth even while OAuth is off globally: a Gmail bridge next to a
|
||||
// password server.
|
||||
const serverOauthEnabled = oauthEnabled || offersOwnOAuth(selectedServer);
|
||||
// A server of one's own (custom endpoint, the desktop app's standalone mode): the
|
||||
// email address comes first and the server is looked up from its domain (JMAP
|
||||
// well-known, SRV, then the admin's default). The field for it only opens when
|
||||
// nothing was found or the user wants another one. Lite has no server to ask
|
||||
// and keeps the field open.
|
||||
const customServerMode = !hasServerList && allowCustomJmapEndpoint;
|
||||
const canLookUpServer = customServerMode && !IS_LITE;
|
||||
const [serverEditing, setServerEditing] = useState(false);
|
||||
const [serverLookup, setServerLookup] = useState<{ domain: string; status: "looking" | "found" | "failed"; url: string | null } | null>(null);
|
||||
const [serverError, setServerError] = useState<ServerFieldError>(null);
|
||||
const [emailNeeded, setEmailNeeded] = useState(false);
|
||||
const serverInputRef = useRef<HTMLInputElement>(null);
|
||||
const serverLookups = useRef(new Map<string, Promise<string | null>>());
|
||||
const typedDomain = canLookUpServer ? emailDomain(formData.username) : "";
|
||||
const unsupportedMailProvider = typedDomain ? unsupportedProvider(typedDomain) : null;
|
||||
const lookupForAddress = serverLookup && serverLookup.domain === typedDomain ? serverLookup : null;
|
||||
// What the lookup found, else the admin's default once it found nothing (or for
|
||||
// a user name with no domain to look up).
|
||||
const autoServer = lookupForAddress?.status === "found"
|
||||
? lookupForAddress.url
|
||||
: configuredServerUrl && (lookupForAddress?.status === "failed" || (!typedDomain && formData.username.trim() !== ""))
|
||||
? configuredServerUrl
|
||||
: null;
|
||||
const [totpCode, setTotpCode] = useState("");
|
||||
const [showTotpField, setShowTotpField] = useState(false);
|
||||
// Access-token sign-in (Bearer auth) in place of username and password.
|
||||
@@ -306,6 +332,15 @@ function LoginPageContent() {
|
||||
const [tokenMode, setTokenMode] = useState(false);
|
||||
const [accessToken, setAccessToken] = useState("");
|
||||
const signInWithToken = tokenMode && loginShowTokenLogin && !isMobileHandoff;
|
||||
const serverFieldOpen = customServerMode && (
|
||||
!canLookUpServer
|
||||
|| serverEditing
|
||||
|| signInWithToken
|
||||
|| unsupportedMailProvider !== null
|
||||
|| (lookupForAddress?.status === "failed" && !configuredServerUrl)
|
||||
);
|
||||
/** The server a sign-in goes to right now, in custom-server mode; null when there is none yet. */
|
||||
const customServer = serverFieldOpen ? normalizeServerInput(jmapEndpoint) : autoServer;
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
// Lite (no server session): "remember me" needs Stalwart's token login on the
|
||||
// target server. Probe it once per URL and hide the box when it is missing;
|
||||
@@ -384,7 +419,7 @@ function LoginPageContent() {
|
||||
|
||||
const probeTarget = hasServerList
|
||||
? selectedServer?.url ?? ""
|
||||
: (allowCustomJmapEndpoint ? jmapEndpoint : serverUrl) || "";
|
||||
: (allowCustomJmapEndpoint ? normalizeServerInput(jmapEndpoint) : serverUrl) || "";
|
||||
useEffect(() => {
|
||||
if (!IS_LITE || !probeTarget) return;
|
||||
let cancelled = false;
|
||||
@@ -446,10 +481,57 @@ function LoginPageContent() {
|
||||
}, [pageTitle, serverUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (customServerMode) return;
|
||||
if (serverUrl && !jmapEndpoint) {
|
||||
setJmapEndpoint(serverUrl);
|
||||
}
|
||||
}, [serverUrl, jmapEndpoint]);
|
||||
}, [serverUrl, jmapEndpoint, customServerMode]);
|
||||
|
||||
/** One lookup per domain while the page is open. */
|
||||
const lookUpServer = useCallback((domain: string, address: string): Promise<string | null> => {
|
||||
let pending = serverLookups.current.get(domain);
|
||||
if (!pending) {
|
||||
pending = requestServerDiscovery(address);
|
||||
serverLookups.current.set(domain, pending);
|
||||
}
|
||||
return pending;
|
||||
}, []);
|
||||
|
||||
// Look the server up once the address is complete, after a pause in typing.
|
||||
const typedAddress = formData.username.trim();
|
||||
useEffect(() => {
|
||||
if (!typedDomain || unsupportedMailProvider || serverLookup?.domain === typedDomain) return;
|
||||
const timer = setTimeout(() => {
|
||||
setServerLookup({ domain: typedDomain, status: "looking", url: null });
|
||||
lookUpServer(typedDomain, typedAddress).then((url) => {
|
||||
setServerLookup((current) => (current?.domain === typedDomain ? { domain: typedDomain, status: url ? "found" : "failed", url } : current));
|
||||
});
|
||||
}, 600);
|
||||
return () => clearTimeout(timer);
|
||||
}, [typedDomain, typedAddress, unsupportedMailProvider, serverLookup?.domain, lookUpServer]);
|
||||
|
||||
/**
|
||||
* The server to sign in to in custom-server mode, looked up now if need be.
|
||||
* Null after it opened the field and said what is missing there.
|
||||
*/
|
||||
const resolveCustomServer = async (): Promise<string | null> => {
|
||||
setServerError(null);
|
||||
if (!serverFieldOpen) {
|
||||
if (typedDomain) {
|
||||
const url = await lookUpServer(typedDomain, typedAddress);
|
||||
setServerLookup({ domain: typedDomain, status: url ? "found" : "failed", url });
|
||||
if (url) return url;
|
||||
}
|
||||
if (configuredServerUrl && (typedDomain || typedAddress)) return configuredServerUrl;
|
||||
} else {
|
||||
const typed = normalizeServerInput(jmapEndpoint);
|
||||
if (typed) return typed;
|
||||
}
|
||||
setServerEditing(true);
|
||||
setServerError(serverFieldOpen && jmapEndpoint.trim() ? "invalid" : "needed");
|
||||
setTimeout(() => serverInputRef.current?.focus(), 0);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Initialize selected server when the server list arrives. Picks the first
|
||||
// entry; the auto-pick effect below may override based on the email domain.
|
||||
@@ -840,6 +922,7 @@ function LoginPageContent() {
|
||||
|
||||
const handleUsernameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({ ...formData, username: e.target.value });
|
||||
setEmailNeeded(false);
|
||||
};
|
||||
|
||||
const handleUsernameFocus = () => {
|
||||
@@ -962,7 +1045,7 @@ function LoginPageContent() {
|
||||
* whichever server is selected so that "Sign in with Google" is one tap.
|
||||
*/
|
||||
const handleOAuthLogin = async (target?: PublicJmapServerEntry) => {
|
||||
const oauthTarget = (target ?? selectedServer)?.url || (allowCustomJmapEndpoint ? jmapEndpoint : configuredServerUrl);
|
||||
const oauthTarget = (target ?? selectedServer)?.url || (allowCustomJmapEndpoint ? customServer || configuredServerUrl : configuredServerUrl);
|
||||
if (oauthTarget && !jmapServerAllowedByDesktopPolicy(oauthTarget, desktopPolicy)) {
|
||||
setDesktopPolicyNotice(tDesktopPolicy("server_refused"));
|
||||
return;
|
||||
@@ -1011,7 +1094,7 @@ function LoginPageContent() {
|
||||
// Resolve the JMAP URL to send to the callback. Server-list entries win
|
||||
// over the custom-endpoint input, which wins over the global server URL.
|
||||
const oauthServerUrl = server?.url
|
||||
|| (allowCustomJmapEndpoint ? jmapEndpoint : configuredServerUrl);
|
||||
|| (allowCustomJmapEndpoint ? customServer || configuredServerUrl : configuredServerUrl);
|
||||
|
||||
sessionStorage.setItem("oauth_code_verifier", verifier);
|
||||
sessionStorage.setItem("oauth_state", state);
|
||||
@@ -1074,8 +1157,18 @@ function LoginPageContent() {
|
||||
// Spelled out, not IS_DESKTOP_BUILD: only this form keeps the route out of other
|
||||
// builds' bundles, which Lite's check on API strings relies on.
|
||||
if (process.env.NEXT_PUBLIC_BULWARK_DESKTOP === '1') {
|
||||
const target = jmapEndpoint.trim();
|
||||
if (!target) {
|
||||
let target = jmapEndpoint.trim();
|
||||
if (customServerMode) {
|
||||
// The address first: the server is found from it. A server typed by hand needs none.
|
||||
if (!typedAddress && !serverFieldOpen) {
|
||||
setEmailNeeded(true);
|
||||
inputRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
const resolved = await resolveCustomServer();
|
||||
if (!resolved) return;
|
||||
target = resolved;
|
||||
} else if (!target) {
|
||||
setDesktopNotice(tDesktop("server_needed"));
|
||||
return;
|
||||
}
|
||||
@@ -1145,8 +1238,13 @@ function LoginPageContent() {
|
||||
|
||||
// Server-list entries always win - `allowCustomJmapEndpoint` is only honored
|
||||
// when the admin hasn't configured a server list.
|
||||
const effectiveServerUrl = selectedServer?.url
|
||||
let effectiveServerUrl = selectedServer?.url
|
||||
|| (allowCustomJmapEndpoint ? jmapEndpoint : serverUrl);
|
||||
if (customServerMode) {
|
||||
const resolved = await resolveCustomServer();
|
||||
if (!resolved) return;
|
||||
effectiveServerUrl = resolved;
|
||||
}
|
||||
// The desktop app opened this page in the user's browser. Nothing leaves this page until
|
||||
// the user says yes: the password is only checked, and a two-factor code can't be, as it
|
||||
// works once. This browser gets no session of its own out of any of it.
|
||||
@@ -1301,6 +1399,8 @@ function LoginPageContent() {
|
||||
};
|
||||
|
||||
const currentThemeOption = THEME_OPTIONS.find(o => o.value === theme) || THEME_OPTIONS[2];
|
||||
// "Webmail" is the default name, not a brand: the heading then says what the page is for.
|
||||
const brandedAppName = appName && appName !== "Webmail" ? appName : "";
|
||||
const CurrentThemeIcon = currentThemeOption.icon;
|
||||
|
||||
// Server picker (when admin has configured a server list). Rendered by both
|
||||
@@ -1378,7 +1478,7 @@ function LoginPageContent() {
|
||||
))}
|
||||
</select>
|
||||
{domainAutoLocked && (
|
||||
<p className="text-[0.6875rem] text-muted-foreground leading-snug">
|
||||
<p className="text-xs text-muted-foreground leading-snug">
|
||||
{t("jmap_server_auto_picked")}
|
||||
</p>
|
||||
)}
|
||||
@@ -1438,17 +1538,18 @@ function LoginPageContent() {
|
||||
ref={themeButtonRef}
|
||||
onClick={() => setShowThemeMenu(!showThemeMenu)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-xl border text-sm transition-all duration-200",
|
||||
"flex items-center justify-center w-9 h-9 rounded-xl border text-sm transition-all duration-200",
|
||||
showThemeMenu
|
||||
? "bg-secondary border-border text-foreground shadow-md"
|
||||
: "bg-background/60 backdrop-blur-sm border-border/50 text-muted-foreground hover:text-foreground hover:bg-secondary/80 hover:border-border"
|
||||
)}
|
||||
aria-label={`${tTheme("label")}: ${tTheme(currentThemeOption.label)}`}
|
||||
aria-label={`${tLogin("theme_button")}: ${tTheme(currentThemeOption.label)}`}
|
||||
title={tLogin("theme_button")}
|
||||
aria-expanded={showThemeMenu}
|
||||
aria-haspopup="menu"
|
||||
data-testid="login-theme-button"
|
||||
>
|
||||
<CurrentThemeIcon className="w-4 h-4" />
|
||||
<span className="hidden sm:inline" suppressHydrationWarning>{tTheme(currentThemeOption.label)}</span>
|
||||
<CurrentThemeIcon className="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{showThemeMenu && (
|
||||
@@ -1608,17 +1709,18 @@ function LoginPageContent() {
|
||||
ref={themeButtonRef}
|
||||
onClick={() => setShowThemeMenu(!showThemeMenu)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-3 py-2 rounded-xl border text-sm transition-all duration-200",
|
||||
"flex items-center justify-center w-9 h-9 rounded-xl border text-sm transition-all duration-200",
|
||||
showThemeMenu
|
||||
? "bg-secondary border-border text-foreground shadow-md"
|
||||
: "bg-background/60 backdrop-blur-sm border-border/50 text-muted-foreground hover:text-foreground hover:bg-secondary/80 hover:border-border"
|
||||
)}
|
||||
aria-label={`${tTheme("label")}: ${tTheme(currentThemeOption.label)}`}
|
||||
aria-label={`${tLogin("theme_button")}: ${tTheme(currentThemeOption.label)}`}
|
||||
title={tLogin("theme_button")}
|
||||
aria-expanded={showThemeMenu}
|
||||
aria-haspopup="menu"
|
||||
data-testid="login-theme-button"
|
||||
>
|
||||
<CurrentThemeIcon className="w-4 h-4" />
|
||||
<span className="hidden sm:inline" suppressHydrationWarning>{tTheme(currentThemeOption.label)}</span>
|
||||
<CurrentThemeIcon className="w-4 h-4" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{showThemeMenu && (
|
||||
@@ -1671,10 +1773,11 @@ function LoginPageContent() {
|
||||
</div>
|
||||
{loginShowHeading && (
|
||||
<h1 className="text-2xl font-semibold text-foreground tracking-tight">
|
||||
{isAddAccountMode ? t("add_account_title") : appName}
|
||||
{isAddAccountMode ? t("add_account_title") : brandedAppName || t("sign_in")}
|
||||
</h1>
|
||||
)}
|
||||
{loginShowSubtitle && (
|
||||
{/* Under a heading that already says "Sign in" the subtitle would say it twice. */}
|
||||
{loginShowSubtitle && (isAddAccountMode || brandedAppName || !loginShowHeading) && (
|
||||
<p className="text-sm text-muted-foreground mt-1.5">
|
||||
{isAddAccountMode ? t("add_account_subtitle") : t("subtitle")}
|
||||
</p>
|
||||
@@ -1846,28 +1949,8 @@ function LoginPageContent() {
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<fieldset disabled={isLoading} className="space-y-4">
|
||||
{serverPicker}
|
||||
{/* JMAP Endpoint field (only when no server list and custom endpoints are allowed) */}
|
||||
{!hasServerList && allowCustomJmapEndpoint && (
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="jmap-endpoint" className="block text-sm font-medium text-foreground">
|
||||
{t("jmap_endpoint_label")}
|
||||
</label>
|
||||
<Input
|
||||
id="jmap-endpoint"
|
||||
type="url"
|
||||
value={jmapEndpoint}
|
||||
onChange={(e) => setJmapEndpoint(e.target.value)}
|
||||
className="h-11 px-3.5 bg-muted/40 border-border/60 rounded-xl focus:bg-background focus:border-primary/50 transition-all duration-200"
|
||||
placeholder={t("jmap_endpoint_placeholder")}
|
||||
required
|
||||
/>
|
||||
<p className="text-[0.6875rem] text-muted-foreground leading-snug">
|
||||
{t("jmap_endpoint_cors_hint")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{browserSignInOption}
|
||||
{!customServerMode && browserSignInOption}
|
||||
|
||||
{/* Username field. The access token names the account, so
|
||||
token sign-in has none. */}
|
||||
@@ -1947,8 +2030,38 @@ function LoginPageContent() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{emailNeeded && (
|
||||
<p role="alert" data-testid="login-email-needed" className="text-xs text-destructive leading-snug">
|
||||
{tLogin("email_needed")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{customServerMode && (
|
||||
<LoginServerField
|
||||
ref={serverInputRef}
|
||||
lookup={canLookUpServer ? lookupForAddress?.status ?? "idle" : "idle"}
|
||||
server={autoServer}
|
||||
editing={serverFieldOpen}
|
||||
value={jmapEndpoint}
|
||||
onChange={(value) => {
|
||||
setJmapEndpoint(value);
|
||||
setServerEditing(true);
|
||||
setServerError(null);
|
||||
}}
|
||||
onEdit={() => {
|
||||
setJmapEndpoint(autoServer ? serverHostLabel(autoServer) : "");
|
||||
setServerEditing(true);
|
||||
setTimeout(() => serverInputRef.current?.focus(), 0);
|
||||
}}
|
||||
provider={unsupportedMailProvider}
|
||||
error={serverError}
|
||||
showTrouble={!IS_DESKTOP_BUILD}
|
||||
/>
|
||||
)}
|
||||
|
||||
{customServerMode && browserSignInOption}
|
||||
|
||||
{/* Password field. Hidden for an account that signs in at an
|
||||
external provider (Lite on Stalwart): the submit button is
|
||||
the SSO button then. */}
|
||||
@@ -2007,7 +2120,7 @@ function LoginPageContent() {
|
||||
data-lpignore="true"
|
||||
{...errorProps(tokenBlamed)}
|
||||
/>
|
||||
<p className="text-[0.6875rem] text-muted-foreground leading-snug">
|
||||
<p className="text-xs text-muted-foreground leading-snug">
|
||||
{t("token_hint")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { configManager } from '@/lib/admin/config-manager';
|
||||
import { discoverMailServer } from '@/lib/auth/discover-mail-server';
|
||||
import { createFailureBudget } from '@/lib/auth/verify-budget';
|
||||
import { loginClientAddress } from '@/lib/auth/client-address';
|
||||
import { isStoreUnavailable } from '@/lib/auth/store-unavailable';
|
||||
import { rejectCrossOriginRequest } from '@/lib/security/same-origin';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
/**
|
||||
* Finds the mail server of the address typed on the sign-in form, when the
|
||||
* form lets people sign in to a server of their own (allowCustomJmapEndpoint,
|
||||
* and the desktop app's standalone mode). Answers `{ serverUrl }`, null when
|
||||
* nothing was found: the form then asks for the server.
|
||||
*
|
||||
* Each lookup asks a domain the caller chose, so it is budgeted per client:
|
||||
* enough for someone typing, not for using this server to knock on others.
|
||||
*/
|
||||
const discoverBudget = createFailureBudget({ name: 'discover-server', clientMax: 30, globalMax: 600 });
|
||||
|
||||
function answer(serverUrl: string | null, status = 200) {
|
||||
return NextResponse.json({ serverUrl }, { status, headers: { 'Cache-Control': 'no-store' } });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const crossOrigin = rejectCrossOriginRequest(request);
|
||||
if (crossOrigin) return crossOrigin;
|
||||
await configManager.ensureLoaded();
|
||||
if (!configManager.get<boolean>('allowCustomJmapEndpoint', false)) {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
const body = await request.json().catch(() => null);
|
||||
const email = body?.email;
|
||||
if (typeof email !== 'string' || !email || email.length > 320) {
|
||||
return NextResponse.json({ error: 'Missing email' }, { status: 400 });
|
||||
}
|
||||
try {
|
||||
const reserved = await discoverBudget.reserve(loginClientAddress(request));
|
||||
if (!reserved) return answer(null, 429);
|
||||
} catch (error) {
|
||||
if (isStoreUnavailable(error)) return answer(null, 503);
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
return answer(await discoverMailServer(email));
|
||||
} catch (error) {
|
||||
logger.warn('Mail server discovery failed', { error: error instanceof Error ? error.message : 'Unknown error' });
|
||||
return answer(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { forwardRef, useId } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AlertCircle, Loader2 } from "@/components/icons";
|
||||
import { apiFetch } from "@/lib/browser-navigation";
|
||||
import { serverHostLabel } from "@/lib/auth/mail-server-address";
|
||||
|
||||
/** Asks this app's server which mail server an address belongs to. Null when nothing was found. */
|
||||
export async function requestServerDiscovery(email: string): Promise<string | null> {
|
||||
try {
|
||||
const res = await apiFetch("/api/auth/discover-server", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const body = (await res.json().catch(() => null)) as { serverUrl?: unknown } | null;
|
||||
return typeof body?.serverUrl === "string" && body.serverUrl ? body.serverUrl : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type ServerLookup = "idle" | "looking" | "found" | "failed";
|
||||
export type ServerFieldError = "needed" | "invalid" | null;
|
||||
|
||||
interface LoginServerFieldProps {
|
||||
lookup: ServerLookup;
|
||||
/** The server in use when the field is closed: found, or the admin's default. */
|
||||
server: string | null;
|
||||
/** The field is open: nothing was found, or the user chose to change it. */
|
||||
editing: boolean;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onEdit: () => void;
|
||||
/** A provider Bulwark can't reach (Gmail, Outlook.com, iCloud Mail), by name. */
|
||||
provider: string | null;
|
||||
error: ServerFieldError;
|
||||
/** The browser's CORS hint, behind "Having trouble?". Not in the desktop app, which has no website to allow. */
|
||||
showTrouble: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The mail server part of the sign-in form when people may name a server of their
|
||||
* own. It comes after the email address: the server is looked up from it, and the
|
||||
* field only opens when nothing was found or the user wants another one.
|
||||
*/
|
||||
export const LoginServerField = forwardRef<HTMLInputElement, LoginServerFieldProps>(function LoginServerField(
|
||||
{ lookup, server, editing, value, onChange, onEdit, provider, error, showTrouble, disabled },
|
||||
ref,
|
||||
) {
|
||||
const t = useTranslations("uxs_login");
|
||||
const id = useId();
|
||||
const hintId = `${id}-hint`;
|
||||
const errorId = `${id}-error`;
|
||||
|
||||
const providerNote = provider && (
|
||||
<div role="status" data-testid="login-provider-unsupported" className="p-3 rounded-xl border border-warning/30 bg-warning/5 flex items-start gap-2.5 text-sm text-foreground leading-relaxed">
|
||||
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0 text-warning" aria-hidden="true" />
|
||||
<span>{t("provider_unsupported", { provider })}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!editing) {
|
||||
if (lookup === "looking") {
|
||||
return (
|
||||
<p role="status" data-testid="login-server-looking" className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" aria-hidden="true" />
|
||||
{t("server_looking")}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (server) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{providerNote}
|
||||
<p data-testid="login-server-found" className="flex items-center gap-2 text-xs text-muted-foreground min-w-0">
|
||||
<span className="truncate">{t("server_found", { server: serverHostLabel(server) })}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
disabled={disabled}
|
||||
className="underline hover:text-foreground flex-shrink-0"
|
||||
data-testid="login-server-change"
|
||||
>
|
||||
{t("server_change")}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return providerNote || null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{providerNote}
|
||||
<div className="space-y-1.5">
|
||||
<label htmlFor="jmap-endpoint" className="block text-sm font-medium text-foreground">
|
||||
{t("server_label")}
|
||||
</label>
|
||||
<Input
|
||||
ref={ref}
|
||||
id="jmap-endpoint"
|
||||
type="text"
|
||||
inputMode="url"
|
||||
autoComplete="off"
|
||||
autoCapitalize="none"
|
||||
spellCheck={false}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="h-11 px-3.5 bg-muted/40 border-border/60 rounded-xl focus:bg-background focus:border-primary/50 transition-all duration-200"
|
||||
placeholder={t("server_placeholder")}
|
||||
aria-invalid={error ? true : undefined}
|
||||
aria-describedby={[error ? errorId : null, lookup === "failed" && !provider ? hintId : null].filter(Boolean).join(" ") || undefined}
|
||||
/>
|
||||
{lookup === "failed" && !provider && !error && (
|
||||
<p id={hintId} className="text-xs text-muted-foreground leading-snug" data-testid="login-server-not-found">
|
||||
{t("server_not_found")}
|
||||
</p>
|
||||
)}
|
||||
{error && (
|
||||
<p id={errorId} role="alert" className="text-xs text-destructive leading-snug" data-testid="login-server-error">
|
||||
{error === "invalid" ? t("server_invalid") : t("server_needed")}
|
||||
</p>
|
||||
)}
|
||||
{showTrouble && (
|
||||
<details className="text-xs text-muted-foreground" data-testid="login-server-trouble">
|
||||
<summary className="cursor-pointer hover:text-foreground w-fit">{t("trouble")}</summary>
|
||||
<p className="mt-1.5 leading-snug">{t("trouble_cors")}</p>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -226,6 +226,7 @@ const MUTATING_HANDLERS: Array<[string, string]> = [
|
||||
['@/app/api/auth/sso/complete/route', 'POST'],
|
||||
['@/app/api/auth/reauth/sso/complete/route', 'POST'],
|
||||
['@/app/api/auth/verify/route', 'POST'],
|
||||
['@/app/api/auth/discover-server/route', 'POST'],
|
||||
];
|
||||
|
||||
describe('every mutating /api/auth handler rejects cross-site requests', () => {
|
||||
|
||||
@@ -204,6 +204,8 @@ export const BUILT_URL_FILES: readonly { file: string; why: string }[] = [
|
||||
{ file: 'components/email/rich-text-editor.tsx', why: "adds the scheme to a link address the user types in the editor" },
|
||||
{ file: 'lib/admin/branding-asset.ts', why: "scheme checks of admin-configured branding URLs" },
|
||||
{ file: 'lib/auth/pair-bundle.ts', why: "scheme checks of the paired server URL" },
|
||||
{ file: 'lib/auth/mail-server-address.ts', why: 'adds https:// to a server address the user types on the sign-in form' },
|
||||
{ file: 'lib/auth/discover-mail-server.ts', why: "the well-known and SRV addresses of the domain in the address the user typed, fetched through fetchPublicUrl" },
|
||||
{ file: 'lib/calendar-subscription-sync.ts', why: "webcal:// rewritten to https:// for a feed the user subscribed to" },
|
||||
{ file: 'lib/email-sanitization.ts', why: "comment about links in mail" },
|
||||
{ file: 'lib/idn.ts', why: "parses a host name the user typed" },
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// @vitest-environment node
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
// The sign-in form's lookup of a mail server: only where people may name a
|
||||
// server of their own, and budgeted per client.
|
||||
|
||||
vi.mock('@/lib/logger', () => ({ logger: { warn: vi.fn(), error: vi.fn(), debug: vi.fn(), info: vi.fn() } }));
|
||||
const config = vi.hoisted(() => ({ custom: true }));
|
||||
vi.mock('@/lib/admin/config-manager', () => ({
|
||||
configManager: { ensureLoaded: async () => {}, get: (key: string, fallback: unknown) => (key === 'allowCustomJmapEndpoint' ? config.custom : fallback) },
|
||||
}));
|
||||
const discoverMailServer = vi.hoisted(() => vi.fn(async (_email: string) => 'https://mail.example.org' as string | null));
|
||||
vi.mock('@/lib/auth/discover-mail-server', () => ({ discoverMailServer }));
|
||||
|
||||
function post(body: unknown, client = '203.0.113.7') {
|
||||
return new NextRequest('https://webmail.example/api/auth/discover-server', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', 'sec-fetch-site': 'same-origin', 'x-forwarded-for': client },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
async function route() {
|
||||
vi.resetModules();
|
||||
return (await import('@/app/api/auth/discover-server/route')).POST;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
config.custom = true;
|
||||
discoverMailServer.mockClear();
|
||||
});
|
||||
|
||||
describe('POST /api/auth/discover-server', () => {
|
||||
it('answers the server found for the address', async () => {
|
||||
const POST = await route();
|
||||
const res = await POST(post({ email: 'sam@example.org' }));
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual({ serverUrl: 'https://mail.example.org' });
|
||||
expect(discoverMailServer).toHaveBeenCalledWith('sam@example.org');
|
||||
});
|
||||
|
||||
it("isn't there when people can't name a server of their own", async () => {
|
||||
config.custom = false;
|
||||
const POST = await route();
|
||||
const res = await POST(post({ email: 'sam@example.org' }));
|
||||
expect(res.status).toBe(404);
|
||||
expect(discoverMailServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('stops one client after a budget of lookups', async () => {
|
||||
const POST = await route();
|
||||
for (let i = 0; i < 30; i++) expect((await POST(post({ email: `u${i}@example.org` }))).status).toBe(200);
|
||||
const res = await POST(post({ email: 'one-more@example.org' }));
|
||||
expect(res.status).toBe(429);
|
||||
expect(discoverMailServer).toHaveBeenCalledTimes(30);
|
||||
// Another client still gets an answer.
|
||||
expect((await POST(post({ email: 'x@example.org' }, '198.51.100.9'))).status).toBe(200);
|
||||
});
|
||||
|
||||
it('refuses a request from another site', async () => {
|
||||
const POST = await route();
|
||||
const req = new NextRequest('https://webmail.example/api/auth/discover-server', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', 'sec-fetch-site': 'cross-site', origin: 'https://evil.example' },
|
||||
body: JSON.stringify({ email: 'sam@example.org' }),
|
||||
});
|
||||
expect((await POST(req)).status).toBe(403);
|
||||
expect(discoverMailServer).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
// @vitest-environment node
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { emailDomain, normalizeServerInput, serverHostLabel, unsupportedProvider } from '../mail-server-address';
|
||||
import { discoverMailServer, type DiscoveryDeps } from '../discover-mail-server';
|
||||
|
||||
describe('what people type', () => {
|
||||
it('takes the domain of a complete address only', () => {
|
||||
expect(emailDomain('Sam@Example.ORG ')).toBe('example.org');
|
||||
expect(emailDomain('sam@example')).toBe('');
|
||||
expect(emailDomain('sam@')).toBe('');
|
||||
expect(emailDomain('sam')).toBe('');
|
||||
expect(emailDomain('sam@127.0.0.1')).toBe('');
|
||||
expect(emailDomain('a@b@mail.example.org')).toBe('mail.example.org');
|
||||
});
|
||||
|
||||
it('accepts a server with or without https://', () => {
|
||||
expect(normalizeServerInput('mail.example.org')).toBe('https://mail.example.org');
|
||||
expect(normalizeServerInput(' mail.example.org:8443/ ')).toBe('https://mail.example.org:8443');
|
||||
expect(normalizeServerInput('https://mail.example.org/')).toBe('https://mail.example.org');
|
||||
expect(normalizeServerInput('http://192.168.1.5:8080')).toBe('http://192.168.1.5:8080');
|
||||
expect(normalizeServerInput('mail example')).toBeNull();
|
||||
expect(normalizeServerInput('ftp://mail.example.org')).toBeNull();
|
||||
expect(normalizeServerInput('https://user:pw@mail.example.org')).toBeNull();
|
||||
expect(normalizeServerInput('')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows a server without its scheme', () => {
|
||||
expect(serverHostLabel('https://mail.example.org')).toBe('mail.example.org');
|
||||
expect(serverHostLabel('https://mail.example.org:8443')).toBe('mail.example.org:8443');
|
||||
});
|
||||
|
||||
it('names the providers that offer no JMAP', () => {
|
||||
expect(unsupportedProvider('gmail.com')).toBe('Gmail');
|
||||
expect(unsupportedProvider('googlemail.com')).toBe('Gmail');
|
||||
expect(unsupportedProvider('hotmail.de')).toBe('Outlook.com');
|
||||
expect(unsupportedProvider('outlook.com')).toBe('Outlook.com');
|
||||
expect(unsupportedProvider('icloud.com')).toBe('iCloud Mail');
|
||||
expect(unsupportedProvider('me.com')).toBe('iCloud Mail');
|
||||
expect(unsupportedProvider('example.org')).toBeNull();
|
||||
expect(unsupportedProvider('fastmail.com')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
type Answer = { status: number; headers?: Record<string, string>; body?: string };
|
||||
|
||||
function deps(answers: Record<string, Answer>, srv: { name: string; port: number; priority: number; weight: number }[] | Error = []) {
|
||||
const asked: string[] = [];
|
||||
const d: DiscoveryDeps = {
|
||||
fetch: vi.fn(async (url: string) => {
|
||||
asked.push(url);
|
||||
const a = answers[url];
|
||||
if (!a) throw new Error('ENOTFOUND');
|
||||
const headers = new Map(Object.entries(a.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));
|
||||
return { status: a.status, headers: { get: (n: string) => headers.get(n.toLowerCase()) ?? null }, text: async () => a.body ?? '' };
|
||||
}),
|
||||
resolveSrv: vi.fn(async () => {
|
||||
if (srv instanceof Error) throw srv;
|
||||
return srv;
|
||||
}),
|
||||
};
|
||||
return { d, asked };
|
||||
}
|
||||
|
||||
const SESSION = { status: 200, headers: { 'content-type': 'application/json' }, body: '{"capabilities":{"urn:ietf:params:jmap:core":{}}}' };
|
||||
|
||||
describe('finding the server', () => {
|
||||
it('follows the well-known redirect to the server that answers', async () => {
|
||||
const { d } = deps({
|
||||
'https://example.org/.well-known/jmap': { status: 307, headers: { location: 'https://mail.example.org/jmap/session' } },
|
||||
'https://mail.example.org/jmap/session': { status: 401, headers: { 'www-authenticate': 'Basic realm="x"' } },
|
||||
});
|
||||
expect(await discoverMailServer('sam@example.org', d)).toBe('https://mail.example.org');
|
||||
});
|
||||
|
||||
it('takes the anonymous session Stalwart answers with', async () => {
|
||||
const { d } = deps({
|
||||
'https://example.org/.well-known/jmap': { status: 307, headers: { location: '/jmap/session' } },
|
||||
'https://example.org/jmap/session': SESSION,
|
||||
});
|
||||
expect(await discoverMailServer('sam@example.org', d)).toBe('https://example.org');
|
||||
});
|
||||
|
||||
it('falls back to the SRV record, on its port', async () => {
|
||||
const { d, asked } = deps(
|
||||
{ 'https://jmap.example.net:8443/.well-known/jmap': SESSION },
|
||||
[{ name: 'jmap.example.net.', port: 8443, priority: 10, weight: 0 }],
|
||||
);
|
||||
expect(await discoverMailServer('sam@example.org', d)).toBe('https://jmap.example.net:8443');
|
||||
expect(asked[0]).toBe('https://example.org/.well-known/jmap');
|
||||
});
|
||||
|
||||
it("doesn't take a web page or a 404 for a mail server", async () => {
|
||||
const { d } = deps({
|
||||
'https://example.org/.well-known/jmap': { status: 200, headers: { 'content-type': 'text/html' }, body: '<html>' },
|
||||
});
|
||||
expect(await discoverMailServer('sam@example.org', d)).toBeNull();
|
||||
const second = deps({ 'https://example.org/.well-known/jmap': { status: 404 } });
|
||||
expect(await discoverMailServer('sam@example.org', second.d)).toBeNull();
|
||||
const json = deps({ 'https://example.org/.well-known/jmap': { status: 200, headers: { 'content-type': 'application/json' }, body: '{"hello":1}' } });
|
||||
expect(await discoverMailServer('sam@example.org', json.d)).toBeNull();
|
||||
});
|
||||
|
||||
it('never follows a redirect down to plain http', async () => {
|
||||
const { d } = deps({
|
||||
'https://example.org/.well-known/jmap': { status: 302, headers: { location: 'http://mail.example.org/jmap/session' } },
|
||||
'http://mail.example.org/jmap/session': SESSION,
|
||||
}, new Error('ENODATA'));
|
||||
expect(await discoverMailServer('sam@example.org', d)).toBeNull();
|
||||
});
|
||||
|
||||
it('asks nothing for something that is not an address', async () => {
|
||||
const { d, asked } = deps({});
|
||||
expect(await discoverMailServer('sam', d)).toBeNull();
|
||||
expect(await discoverMailServer('sam@localhost', d)).toBeNull();
|
||||
expect(asked).toEqual([]);
|
||||
});
|
||||
|
||||
it('gives up on a redirect loop', async () => {
|
||||
const { d, asked } = deps({
|
||||
'https://example.org/.well-known/jmap': { status: 307, headers: { location: 'https://example.org/.well-known/jmap' } },
|
||||
});
|
||||
expect(await discoverMailServer('sam@example.org', d)).toBeNull();
|
||||
expect(asked.length).toBeLessThanOrEqual(4);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* Finds the JMAP server of an email domain for the sign-in form, server side:
|
||||
* the documents rarely carry CORS headers, and the browser can't ask DNS.
|
||||
*
|
||||
* 1. `https://<domain>/.well-known/jmap` (RFC 8620 2.2), following up to three
|
||||
* redirects. Stalwart answers 307 to /jmap/session, which answers 401.
|
||||
* 2. The SRV record `_jmap._tcp.<domain>` (RFC 8620 2.2), checked the same way
|
||||
* on the host it names.
|
||||
*
|
||||
* Every request goes through fetchPublicUrl, so only public addresses are
|
||||
* asked, and each redirect target is checked again. The answer is the origin
|
||||
* that served the session resource; the form signs in there.
|
||||
*/
|
||||
|
||||
import { resolveSrv } from 'node:dns/promises';
|
||||
import { fetchPublicUrl } from '@/lib/security/url-guard';
|
||||
import { emailDomain } from './mail-server-address';
|
||||
|
||||
const TIMEOUT_MS = 4000;
|
||||
const MAX_REDIRECTS = 3;
|
||||
|
||||
export interface DiscoveryDeps {
|
||||
fetch: (url: string, init: { method: string; headers: Record<string, string>; signal: AbortSignal }) => Promise<{ status: number; headers: { get(name: string): string | null }; text(): Promise<string> }>;
|
||||
resolveSrv: (name: string) => Promise<{ name: string; port: number; priority: number; weight: number }[]>;
|
||||
}
|
||||
|
||||
const defaultDeps: DiscoveryDeps = {
|
||||
fetch: (url, init) => fetchPublicUrl(url, init),
|
||||
resolveSrv: (name) => resolveSrv(name),
|
||||
};
|
||||
|
||||
/** Does a JMAP session resource answer at this base? Returns the origin that answered, or null. */
|
||||
async function probe(base: string, deps: DiscoveryDeps): Promise<string | null> {
|
||||
let url = `${base}/.well-known/jmap`;
|
||||
for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
|
||||
let response;
|
||||
try {
|
||||
response = await deps.fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (response.status >= 300 && response.status < 400) {
|
||||
const location = response.headers.get('location');
|
||||
if (!location) return null;
|
||||
let next: URL;
|
||||
try {
|
||||
next = new URL(location, url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
// Never down to plain http: the password would follow it.
|
||||
if (next.protocol !== 'https:') return null;
|
||||
url = next.toString();
|
||||
continue;
|
||||
}
|
||||
// The session resource usually needs a sign-in: 401 with a challenge. Stalwart
|
||||
// answers 200 with an anonymous session, which names its capabilities.
|
||||
if (response.status === 401 && response.headers.get('www-authenticate') !== null) return new URL(url).origin;
|
||||
if (response.status === 200 && /json/i.test(response.headers.get('content-type') ?? '')) {
|
||||
const body = await response.text().catch(() => '');
|
||||
if (body.length > 256 * 1024) return null;
|
||||
try {
|
||||
const session = JSON.parse(body) as { capabilities?: unknown };
|
||||
if (session && typeof session.capabilities === 'object' && session.capabilities !== null) return new URL(url).origin;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The JMAP server for an email address, or null when nothing answered. */
|
||||
export async function discoverMailServer(address: string, deps: DiscoveryDeps = defaultDeps): Promise<string | null> {
|
||||
const domain = emailDomain(address);
|
||||
if (!domain) return null;
|
||||
|
||||
const direct = await probe(`https://${domain}`, deps);
|
||||
if (direct) return direct;
|
||||
|
||||
let records: Awaited<ReturnType<DiscoveryDeps['resolveSrv']>> = [];
|
||||
try {
|
||||
records = await deps.resolveSrv(`_jmap._tcp.${domain}`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const ordered = [...records]
|
||||
// "." means the service is decidedly not offered (RFC 2782).
|
||||
.filter((r) => r.name && r.name !== '.')
|
||||
.sort((a, b) => a.priority - b.priority || b.weight - a.weight);
|
||||
for (const record of ordered.slice(0, 2)) {
|
||||
const host = record.name.replace(/\.$/, '').toLowerCase();
|
||||
const base = record.port && record.port !== 443 ? `https://${host}:${record.port}` : `https://${host}`;
|
||||
const found = await probe(base, deps);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* What the sign-in form makes of what people type: the domain of an email
|
||||
* address, the big mail providers that don't speak JMAP, and a server address
|
||||
* typed without a scheme. No network here: the browser and the server both use it.
|
||||
*/
|
||||
|
||||
import { toAsciiDomain } from '@/lib/idn';
|
||||
|
||||
/** A host name as it can appear after the @: letters, digits, hyphens, at least one dot. */
|
||||
const DOMAIN = /^(?=.{4,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$/;
|
||||
|
||||
/** The domain of a complete email address, lower case, or "" while it isn't one yet. */
|
||||
export function emailDomain(address: string): string {
|
||||
const trimmed = address.trim().toLowerCase();
|
||||
const at = trimmed.lastIndexOf('@');
|
||||
if (at <= 0) return '';
|
||||
const domain = toAsciiDomain(trimmed.slice(at + 1).replace(/\.$/, '')) ?? '';
|
||||
return DOMAIN.test(domain) ? domain : '';
|
||||
}
|
||||
|
||||
/** Providers people try first. None of them offers JMAP. */
|
||||
const UNSUPPORTED: Record<string, string> = {
|
||||
'gmail.com': 'Gmail',
|
||||
'googlemail.com': 'Gmail',
|
||||
'outlook.com': 'Outlook.com',
|
||||
'hotmail.com': 'Outlook.com',
|
||||
'live.com': 'Outlook.com',
|
||||
'msn.com': 'Outlook.com',
|
||||
'icloud.com': 'iCloud Mail',
|
||||
'me.com': 'iCloud Mail',
|
||||
'mac.com': 'iCloud Mail',
|
||||
};
|
||||
|
||||
/** The provider's name when this domain belongs to one that Bulwark can't reach, else null. */
|
||||
export function unsupportedProvider(domain: string): string | null {
|
||||
const d = domain.toLowerCase();
|
||||
if (UNSUPPORTED[d]) return UNSUPPORTED[d];
|
||||
// Country variants: hotmail.de, outlook.fr, live.co.uk, googlemail.co.uk.
|
||||
const label = d.split('.')[0];
|
||||
if (label === 'hotmail' || label === 'outlook' || label === 'live') return 'Outlook.com';
|
||||
if (label === 'googlemail') return 'Gmail';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A server address as people type it, as a URL: "mail.example.org",
|
||||
* "mail.example.org:8443" and "https://mail.example.org/" all work. http:// stays
|
||||
* http:// when typed (the desktop app asks before it uses one). Null when it
|
||||
* isn't a server address at all.
|
||||
*/
|
||||
export function normalizeServerInput(input: string): string | null {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed || /\s/.test(trimmed)) return null;
|
||||
const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(withScheme);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (url.protocol !== 'https:' && url.protocol !== 'http:') return null;
|
||||
if (!url.hostname || url.username || url.password || url.search || url.hash) return null;
|
||||
const path = url.pathname.replace(/\/+$/, '');
|
||||
return `${url.origin}${path}`;
|
||||
}
|
||||
|
||||
/** The host to show for a server URL: no scheme, no default port. */
|
||||
export function serverHostLabel(serverUrl: string): string {
|
||||
try {
|
||||
const url = new URL(serverUrl);
|
||||
const path = url.pathname.replace(/\/+$/, '');
|
||||
return `${url.protocol === 'http:' ? 'http://' : ''}${url.host}${path}`;
|
||||
} catch {
|
||||
return serverUrl;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,7 @@ export const SERVER_STATE_ALLOWLIST: AllowedState[] = [
|
||||
{ site: 'lib/auth/session-secret.ts#globalThis:SHARED_CACHE', class: 'store-backed', why: 'the generated secret read from the store at boot; it never changes' },
|
||||
{ site: 'lib/auth/stalwart-password-login.ts#defaultClients', class: 'cache', why: 'Stalwart\'s public client id, 10 min; registration is deterministic' },
|
||||
{ site: 'lib/auth/verify-budget.ts#verifyBudget', class: 'store-backed', why: 'createFailureBudget: shared counters in shared mode' },
|
||||
{ site: 'app/api/auth/discover-server/route.ts#discoverBudget', class: 'store-backed', why: 'createFailureBudget: shared counters in shared mode' },
|
||||
{ site: 'lib/auth/verify-budget.ts#loginBudget', class: 'store-backed', why: 'createFailureBudget: shared counters in shared mode' },
|
||||
{ site: 'lib/bimi/lookup.ts#shared', class: 'cache', why: 'BIMI records and logos, 24 h / 6 h / 10 min' },
|
||||
{ site: 'lib/bimi/roots.ts#parsed', class: 'per-process', why: 'pinned root certificates parsed once' },
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "كلمة المرور",
|
||||
"password_placeholder": "أدخل كلمة المرور",
|
||||
"jmap_endpoint_label": "خادم JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "يجب أن يسمح الخادم بطلبات CORS من هذا النطاق.",
|
||||
"jmap_server_label": "الخادم",
|
||||
"jmap_server_auto_picked": "تم اختيار الخادم بناءً على نطاق بريدك الإلكتروني.",
|
||||
"sign_in": "تسجيل الدخول",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "إظهار كلمة المرور",
|
||||
"hide_password": "إخفاء كلمة المرور",
|
||||
"totp_toggle": "لديّ رمز مصادقة ثنائية",
|
||||
"totp_toggle": "لديّ رمز من تطبيق المصادقة",
|
||||
"token_toggle": "تسجيل الدخول باستخدام رمز وصول",
|
||||
"password_toggle": "تسجيل الدخول باستخدام كلمة مرور",
|
||||
"token_label": "رمز الوصول",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "محاولات تسجيل دخول كثيرة جدًا من هذه الشبكة. انتظر بضع دقائق وحاول مرة أخرى.",
|
||||
"page_declined": "لم يتم تسليم أي شيء. يمكنك إغلاق علامة التبويب هذه."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "فاتح أو داكن",
|
||||
"server_label": "خادم البريد",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "جارٍ البحث عن خادم بريدك…",
|
||||
"server_found": "خادم البريد: {server}",
|
||||
"server_change": "تغيير",
|
||||
"server_not_found": "لم نعثر على خادم البريد لهذا العنوان. أدخله هنا، مثل mail.example.com.",
|
||||
"server_needed": "أدخل عنوان خادم بريدك.",
|
||||
"server_invalid": "هذا ليس عنوان خادم. اكتبه بهذا الشكل: mail.example.com.",
|
||||
"email_needed": "أدخل عنوان بريدك الإلكتروني أولاً.",
|
||||
"trouble": "هل تواجه مشكلة؟",
|
||||
"trouble_cors": "يجب أن يسمح خادم بريدك بتسجيل الدخول من هذا الموقع. إذا فشل تسجيل الدخول، فاطلب من مسؤول خادم بريدك السماح بذلك.",
|
||||
"provider_unsupported": "لا يوفّر {provider} بروتوكول JMAP الذي يتواصل به Bulwark مع خوادم البريد، لذلك لا يستطيع Bulwark الاتصال به مباشرة بعد."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "التحديثات",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "usuari@exemple.com",
|
||||
"password_label": "Contrasenya",
|
||||
"password_placeholder": "Introduïu la contrasenya",
|
||||
"jmap_endpoint_label": "Servidor JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "El servidor ha de permetre sol·licituds CORS des d'aquest domini.",
|
||||
"jmap_server_label": "Servidor",
|
||||
"jmap_server_auto_picked": "Servidor seleccionat a partir del domini del vostre correu electrònic.",
|
||||
"sign_in": "Inicia la sessió",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Mostra la contrasenya",
|
||||
"hide_password": "Amaga la contrasenya",
|
||||
"totp_toggle": "Tinc un codi 2FA",
|
||||
"totp_toggle": "Tinc un codi de la meva aplicació d'autenticació",
|
||||
"token_toggle": "Inicia la sessió amb un token d'accés",
|
||||
"password_toggle": "Inicia la sessió amb una contrasenya",
|
||||
"token_label": "Token d'accés",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Massa intents d’inici de sessió des d’aquesta xarxa. Espera uns minuts i torna-ho a provar.",
|
||||
"page_declined": "No s’ha lliurat res. Pots tancar aquesta pestanya."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Clar o fosc",
|
||||
"server_label": "Servidor de correu",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "S'està cercant el teu servidor de correu…",
|
||||
"server_found": "Servidor de correu: {server}",
|
||||
"server_change": "Canvia",
|
||||
"server_not_found": "No hem trobat el servidor de correu d'aquesta adreça. Escriu-lo aquí, per exemple mail.example.com.",
|
||||
"server_needed": "Escriu l'adreça del teu servidor de correu.",
|
||||
"server_invalid": "Això no és l'adreça d'un servidor. Escriu-la com mail.example.com.",
|
||||
"email_needed": "Primer escriu la teva adreça electrònica.",
|
||||
"trouble": "Tens problemes?",
|
||||
"trouble_cors": "El teu servidor de correu ha de permetre iniciar sessió des d'aquest lloc web. Si no funciona, demana a qui gestiona el servidor que ho permeti.",
|
||||
"provider_unsupported": "{provider} no ofereix JMAP, la manera com Bulwark parla amb els servidors de correu, així que Bulwark encara no s'hi pot connectar directament."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Actualitzacions",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "uzivatel@example.com",
|
||||
"password_label": "Heslo",
|
||||
"password_placeholder": "Zadejte heslo",
|
||||
"jmap_endpoint_label": "JMAP server",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Server musí povolovat požadavky CORS z této domény.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Server vybrán podle domény vašeho e-mailu.",
|
||||
"sign_in": "Přihlásit se",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Zobrazit heslo",
|
||||
"hide_password": "Skrýt heslo",
|
||||
"totp_toggle": "Mám 2FA kód",
|
||||
"totp_toggle": "Mám kód z ověřovací aplikace",
|
||||
"token_toggle": "Přihlásit se přístupovým tokenem",
|
||||
"password_toggle": "Přihlásit se heslem",
|
||||
"token_label": "Přístupový token",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Příliš mnoho pokusů o přihlášení z této sítě. Počkejte několik minut a zkuste to znovu.",
|
||||
"page_declined": "Nic nebylo předáno. Tuto kartu můžete zavřít."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Světlý, nebo tmavý",
|
||||
"server_label": "Poštovní server",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Hledáme váš poštovní server…",
|
||||
"server_found": "Poštovní server: {server}",
|
||||
"server_change": "Změnit",
|
||||
"server_not_found": "Pro tuto adresu jsme poštovní server nenašli. Zadejte ho sem, například mail.example.com.",
|
||||
"server_needed": "Zadejte adresu svého poštovního serveru.",
|
||||
"server_invalid": "Tohle není adresa serveru. Napište ji jako mail.example.com.",
|
||||
"email_needed": "Nejdřív zadejte svou e-mailovou adresu.",
|
||||
"trouble": "Nedaří se to?",
|
||||
"trouble_cors": "Váš poštovní server musí povolit přihlašování z tohoto webu. Pokud přihlášení selže, požádejte správce poštovního serveru, aby to povolil.",
|
||||
"provider_unsupported": "{provider} nenabízí JMAP, způsob, jakým Bulwark komunikuje s poštovními servery, takže se k němu Bulwark zatím nemůže připojit přímo."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Aktualizace",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "bruger@example.com",
|
||||
"password_label": "Adgangskode",
|
||||
"password_placeholder": "Indtast din adgangskode",
|
||||
"jmap_endpoint_label": "JMAP-server",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Serveren skal tillade CORS-anmodninger fra dette domæne.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Server valgt ud fra din e-maildomæne.",
|
||||
"sign_in": "Log ind",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Vis adgangskode",
|
||||
"hide_password": "Skjul adgangskode",
|
||||
"totp_toggle": "Jeg har en 2FA-kode",
|
||||
"totp_toggle": "Jeg har en kode fra min godkendelsesapp",
|
||||
"token_toggle": "Log ind med et adgangstoken",
|
||||
"password_toggle": "Log ind med adgangskode",
|
||||
"token_label": "Adgangstoken",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "For mange loginforsøg fra dette netværk. Vent et par minutter, og prøv igen.",
|
||||
"page_declined": "Intet blev overdraget. Du kan lukke denne fane."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Lyst eller mørkt",
|
||||
"server_label": "Mailserver",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Leder efter din mailserver…",
|
||||
"server_found": "Mailserver: {server}",
|
||||
"server_change": "Skift",
|
||||
"server_not_found": "Vi kunne ikke finde mailserveren til denne adresse. Skriv den her, for eksempel mail.example.com.",
|
||||
"server_needed": "Skriv adressen på din mailserver.",
|
||||
"server_invalid": "Det er ikke en serveradresse. Skriv den som mail.example.com.",
|
||||
"email_needed": "Skriv først din e-mailadresse.",
|
||||
"trouble": "Problemer?",
|
||||
"trouble_cors": "Din mailserver skal tillade login fra dette websted. Hvis login mislykkes, så bed den, der driver din mailserver, om at tillade det.",
|
||||
"provider_unsupported": "{provider} tilbyder ikke JMAP, den måde Bulwark taler med mailservere på, så Bulwark kan endnu ikke forbinde direkte til det."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Opdateringer",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "benutzer@beispiel.de",
|
||||
"password_label": "Passwort",
|
||||
"password_placeholder": "Geben Sie Ihr Passwort ein",
|
||||
"jmap_endpoint_label": "JMAP-Server",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Der Server muss CORS-Anfragen von dieser Domain zulassen.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Server anhand Ihrer E-Mail-Domain ausgewählt.",
|
||||
"sign_in": "Anmelden",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Passwort anzeigen",
|
||||
"hide_password": "Passwort verbergen",
|
||||
"totp_toggle": "Ich habe einen 2FA-Code",
|
||||
"totp_toggle": "Ich habe einen Code aus meiner Authenticator-App",
|
||||
"token_toggle": "Mit Zugriffstoken anmelden",
|
||||
"password_toggle": "Mit Passwort anmelden",
|
||||
"token_label": "Zugriffstoken",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Zu viele Anmeldeversuche aus diesem Netzwerk. Warten Sie ein paar Minuten und versuchen Sie es noch einmal.",
|
||||
"page_declined": "Es wurde nichts übergeben. Sie können diesen Tab schließen."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Hell oder dunkel",
|
||||
"server_label": "Mailserver",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Ihr Mailserver wird gesucht…",
|
||||
"server_found": "Mailserver: {server}",
|
||||
"server_change": "Ändern",
|
||||
"server_not_found": "Für diese Adresse wurde kein Mailserver gefunden. Geben Sie ihn hier ein, zum Beispiel mail.example.com.",
|
||||
"server_needed": "Geben Sie die Adresse Ihres Mailservers ein.",
|
||||
"server_invalid": "Das ist keine Serveradresse. Schreiben Sie sie wie mail.example.com.",
|
||||
"email_needed": "Geben Sie zuerst Ihre E-Mail-Adresse ein.",
|
||||
"trouble": "Probleme?",
|
||||
"trouble_cors": "Ihr Mailserver muss Anmeldungen von dieser Website erlauben. Wenn die Anmeldung nicht klappt, bitten Sie die Betreiber Ihres Mailservers, das zu erlauben.",
|
||||
"provider_unsupported": "{provider} bietet kein JMAP an, die Art, wie Bulwark mit Mailservern spricht. Deshalb kann sich Bulwark noch nicht direkt damit verbinden."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Updates",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"login": {
|
||||
"jmap_endpoint_cors_hint": "Der Mailserver muss Anmeldungen von dieser Website erlauben. Ihre Administration kann das einrichten.",
|
||||
"reconnecting": "Die Verbindung ist weg. Wir versuchen, wieder zu verbinden…",
|
||||
"error": {
|
||||
"invalid_credentials": "Die E-Mail-Adresse oder das Passwort ist falsch. Prüfen Sie beides und versuchen Sie es noch einmal.",
|
||||
@@ -14,7 +13,6 @@
|
||||
"oauth_discovery_failed": "Die Anmeldung über Ihre Organisation funktioniert gerade nicht. Versuchen Sie es später noch einmal oder fragen Sie Ihre Administration.",
|
||||
"token_exchange_failed": "Passwort und Code sind richtig, aber der Mailserver hat diese App nicht hereingelassen. Bitten Sie Ihre Administration, die Anmelde-Einstellungen für diese App zu prüfen."
|
||||
},
|
||||
"totp_toggle": "Ich habe einen Code aus meiner Authenticator-App",
|
||||
"token_hint": "Manche Mailanbieter geben Ihnen ein Token statt eines Passworts. Erstellen Sie es in den Einstellungen Ihres Mailanbieters, kopieren Sie es und fügen Sie es hier ein.",
|
||||
"config_error": {
|
||||
"title": "Das Webmail ist nicht richtig eingerichtet",
|
||||
|
||||
+17
-5
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "Password",
|
||||
"password_placeholder": "Enter your password",
|
||||
"jmap_endpoint_label": "JMAP Server",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "The server must allow CORS requests from this domain.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Server selected from your email domain.",
|
||||
"sign_in": "Sign in",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Show password",
|
||||
"hide_password": "Hide password",
|
||||
"totp_toggle": "I have a 2FA code",
|
||||
"totp_toggle": "I have a code from my authenticator app",
|
||||
"token_toggle": "Sign in with an access token",
|
||||
"password_toggle": "Sign in with a password",
|
||||
"token_label": "Access token",
|
||||
@@ -48,7 +45,7 @@
|
||||
"dismiss": "Dismiss",
|
||||
"or": "or",
|
||||
"sign_in_sso": "Sign in with SSO",
|
||||
"add_account_title": "Add Account",
|
||||
"add_account_title": "Add account",
|
||||
"add_account_subtitle": "Sign in with another account",
|
||||
"subtitle": "Sign in to your account",
|
||||
"version": {
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Too many sign-in attempts from this network. Wait a few minutes and try again.",
|
||||
"page_declined": "Nothing was handed over. You can close this tab."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Light or dark",
|
||||
"server_label": "Mail server",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Looking for your mail server…",
|
||||
"server_found": "Mail server: {server}",
|
||||
"server_change": "Change",
|
||||
"server_not_found": "We couldn't find the mail server for this address. Enter it here, for example mail.example.com.",
|
||||
"server_needed": "Enter the address of your mail server.",
|
||||
"server_invalid": "This isn't a server address. Type it like mail.example.com.",
|
||||
"email_needed": "Enter your email address first.",
|
||||
"trouble": "Having trouble?",
|
||||
"trouble_cors": "Your mail server has to allow sign-ins from this website. If signing in fails, ask whoever runs your mail server to allow it.",
|
||||
"provider_unsupported": "{provider} doesn't offer JMAP, the way Bulwark talks to mail servers, so Bulwark can't connect to it directly yet."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Updates",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"login": {
|
||||
"jmap_endpoint_cors_hint": "The mail server must allow sign-ins from this website. Your administrator can set this up.",
|
||||
"reconnecting": "The connection is lost. We are trying to connect again…",
|
||||
"error": {
|
||||
"invalid_credentials": "The email address or password is wrong. Check both and try again.",
|
||||
@@ -14,7 +13,6 @@
|
||||
"oauth_discovery_failed": "Sign-in through your organisation does not work right now. Try again later or ask your administrator.",
|
||||
"token_exchange_failed": "Your password and code are correct, but the mail server did not let this app in. Ask your administrator to check the sign-in settings for this app."
|
||||
},
|
||||
"totp_toggle": "I have a code from my authenticator app",
|
||||
"token_hint": "Some mail providers give you a token instead of a password. Create one in your mail provider's settings, copy it, and paste it here.",
|
||||
"config_error": {
|
||||
"title": "The webmail is not set up correctly",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "usuario@ejemplo.com",
|
||||
"password_label": "Contraseña",
|
||||
"password_placeholder": "Ingrese su contraseña",
|
||||
"jmap_endpoint_label": "Servidor JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "El servidor debe permitir solicitudes CORS desde este dominio.",
|
||||
"jmap_server_label": "Servidor",
|
||||
"jmap_server_auto_picked": "Servidor seleccionado a partir del dominio de tu correo.",
|
||||
"sign_in": "Iniciar sesión",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Mostrar contraseña",
|
||||
"hide_password": "Ocultar contraseña",
|
||||
"totp_toggle": "Tengo un código 2FA",
|
||||
"totp_toggle": "Tengo un código de mi aplicación de autenticación",
|
||||
"token_toggle": "Iniciar sesión con un token de acceso",
|
||||
"password_toggle": "Iniciar sesión con una contraseña",
|
||||
"token_label": "Token de acceso",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Demasiados intentos de inicio de sesión desde esta red. Espera unos minutos e inténtalo de nuevo.",
|
||||
"page_declined": "No se ha entregado nada. Puedes cerrar esta pestaña."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Claro u oscuro",
|
||||
"server_label": "Servidor de correo",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Buscando tu servidor de correo…",
|
||||
"server_found": "Servidor de correo: {server}",
|
||||
"server_change": "Cambiar",
|
||||
"server_not_found": "No encontramos el servidor de correo de esta dirección. Escríbelo aquí, por ejemplo mail.example.com.",
|
||||
"server_needed": "Escribe la dirección de tu servidor de correo.",
|
||||
"server_invalid": "Esto no es la dirección de un servidor. Escríbela como mail.example.com.",
|
||||
"email_needed": "Primero escribe tu dirección de correo.",
|
||||
"trouble": "¿Tienes problemas?",
|
||||
"trouble_cors": "Tu servidor de correo tiene que permitir iniciar sesión desde este sitio web. Si no funciona, pide a quien gestiona tu servidor de correo que lo permita.",
|
||||
"provider_unsupported": "{provider} no ofrece JMAP, la forma en que Bulwark habla con los servidores de correo, así que Bulwark todavía no puede conectarse directamente."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Actualizaciones",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "sinu@email.ee",
|
||||
"password_label": "Parool",
|
||||
"password_placeholder": "Sisesta oma parool",
|
||||
"jmap_endpoint_label": "JMAP server",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Server peab lubama CORS-päringuid sellelt domeenilt.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Server valiti automaatselt sinu e-posti domeeni järgi.",
|
||||
"sign_in": "Logi sisse",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Näita parooli",
|
||||
"hide_password": "Peida parool",
|
||||
"totp_toggle": "Mul on kaheastmelise autentimise kood",
|
||||
"totp_toggle": "Mul on kood autentimisrakendusest",
|
||||
"token_toggle": "Logi sisse juurdepääsuloaga",
|
||||
"password_toggle": "Logi sisse parooliga",
|
||||
"token_label": "Juurdepääsuluba",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Sellest võrgust on liiga palju sisselogimiskatseid. Oota paar minutit ja proovi uuesti.",
|
||||
"page_declined": "Midagi ei antud üle. Võid selle vahelehe sulgeda."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Hele või tume",
|
||||
"server_label": "Meiliserver",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Otsime sinu meiliserverit…",
|
||||
"server_found": "Meiliserver: {server}",
|
||||
"server_change": "Muuda",
|
||||
"server_not_found": "Sellele aadressile ei leitud meiliserverit. Sisesta see siia, näiteks mail.example.com.",
|
||||
"server_needed": "Sisesta oma meiliserveri aadress.",
|
||||
"server_invalid": "See ei ole serveri aadress. Kirjuta see nagu mail.example.com.",
|
||||
"email_needed": "Sisesta kõigepealt oma e-posti aadress.",
|
||||
"trouble": "Probleeme?",
|
||||
"trouble_cors": "Sinu meiliserver peab lubama sisselogimist sellelt veebisaidilt. Kui sisselogimine ei õnnestu, palu meiliserveri haldajal see lubada.",
|
||||
"provider_unsupported": "{provider} ei paku JMAP-i, mille kaudu Bulwark meiliserveritega suhtleb, seega ei saa Bulwark sellega veel otse ühendust luua."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Uuendused",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "رمز عبور",
|
||||
"password_placeholder": "رمز عبور خود را وارد کنید",
|
||||
"jmap_endpoint_label": "سرور JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "سرور باید درخواستهای CORS از این دامنه را مجاز کند.",
|
||||
"jmap_server_label": "سرور",
|
||||
"jmap_server_auto_picked": "سرور از دامنه ایمیل شما انتخاب شد.",
|
||||
"sign_in": "ورود",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "نمایش رمز عبور",
|
||||
"hide_password": "مخفی کردن رمز عبور",
|
||||
"totp_toggle": "کد احراز هویت دو مرحلهای دارم",
|
||||
"totp_toggle": "یک کد از برنامهٔ احراز هویتم دارم",
|
||||
"token_toggle": "ورود با توکن دسترسی",
|
||||
"password_toggle": "ورود با رمز عبور",
|
||||
"token_label": "توکن دسترسی",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "تلاشهای ورود از این شبکه بیش از حد است. چند دقیقه صبر کنید و دوباره تلاش کنید.",
|
||||
"page_declined": "چیزی تحویل داده نشد. میتوانید این زبانه را ببندید."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "روشن یا تیره",
|
||||
"server_label": "سرور ایمیل",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "در حال یافتن سرور ایمیل شما…",
|
||||
"server_found": "سرور ایمیل: {server}",
|
||||
"server_change": "تغییر",
|
||||
"server_not_found": "سرور ایمیل این نشانی پیدا نشد. آن را اینجا وارد کنید، مثلاً mail.example.com.",
|
||||
"server_needed": "نشانی سرور ایمیل خود را وارد کنید.",
|
||||
"server_invalid": "این نشانی سرور نیست. آن را مثل mail.example.com بنویسید.",
|
||||
"email_needed": "اول نشانی ایمیل خود را وارد کنید.",
|
||||
"trouble": "مشکل دارید؟",
|
||||
"trouble_cors": "سرور ایمیل شما باید ورود از این وبسایت را مجاز کند. اگر ورود انجام نشد، از مسئول سرور ایمیل بخواهید آن را مجاز کند.",
|
||||
"provider_unsupported": "{provider} از JMAP، روشی که Bulwark با سرورهای ایمیل صحبت میکند، پشتیبانی نمیکند؛ پس Bulwark هنوز نمیتواند مستقیم به آن وصل شود."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "بهروزرسانیها",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "utilisateur@exemple.com",
|
||||
"password_label": "Mot de passe",
|
||||
"password_placeholder": "Entrez votre mot de passe",
|
||||
"jmap_endpoint_label": "Serveur JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Le serveur doit autoriser les requêtes CORS depuis ce domaine.",
|
||||
"jmap_server_label": "Serveur",
|
||||
"jmap_server_auto_picked": "Serveur sélectionné à partir de votre domaine de messagerie.",
|
||||
"sign_in": "Se connecter",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Afficher le mot de passe",
|
||||
"hide_password": "Masquer le mot de passe",
|
||||
"totp_toggle": "J'ai un code 2FA",
|
||||
"totp_toggle": "J'ai un code de mon application d'authentification",
|
||||
"token_toggle": "Se connecter avec un jeton d'accès",
|
||||
"password_toggle": "Se connecter avec un mot de passe",
|
||||
"token_label": "Jeton d'accès",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Trop de tentatives de connexion depuis ce réseau. Attendez quelques minutes et réessayez.",
|
||||
"page_declined": "Rien n’a été transmis. Vous pouvez fermer cet onglet."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Clair ou sombre",
|
||||
"server_label": "Serveur de messagerie",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Recherche de votre serveur de messagerie…",
|
||||
"server_found": "Serveur de messagerie : {server}",
|
||||
"server_change": "Modifier",
|
||||
"server_not_found": "Nous n'avons pas trouvé le serveur de messagerie de cette adresse. Saisissez-le ici, par exemple mail.example.com.",
|
||||
"server_needed": "Saisissez l'adresse de votre serveur de messagerie.",
|
||||
"server_invalid": "Ce n'est pas une adresse de serveur. Écrivez-la comme mail.example.com.",
|
||||
"email_needed": "Saisissez d'abord votre adresse e-mail.",
|
||||
"trouble": "Un problème ?",
|
||||
"trouble_cors": "Votre serveur de messagerie doit autoriser les connexions depuis ce site. Si la connexion échoue, demandez à la personne qui gère votre serveur de l'autoriser.",
|
||||
"provider_unsupported": "{provider} ne propose pas JMAP, la façon dont Bulwark communique avec les serveurs de messagerie, donc Bulwark ne peut pas encore s'y connecter directement."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Mises à jour",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -5,9 +5,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "סיסמה",
|
||||
"password_placeholder": "הזן את הסיסמה שלך",
|
||||
"jmap_endpoint_label": "שרת JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "השרת חייב לאפשר בקשות CORS מהדומיין הזה.",
|
||||
"sign_in": "היכנס",
|
||||
"signing_in": "נכנס...",
|
||||
"loading": "טוען...",
|
||||
@@ -26,7 +23,7 @@
|
||||
},
|
||||
"show_password": "הצג סיסמה",
|
||||
"hide_password": "הסתר את הסיסמה",
|
||||
"totp_toggle": "יש לי קוד 2FA",
|
||||
"totp_toggle": "יש לי קוד מאפליקציית האימות שלי",
|
||||
"token_toggle": "כניסה באמצעות אסימון גישה",
|
||||
"password_toggle": "כניסה באמצעות סיסמה",
|
||||
"token_label": "אסימון גישה",
|
||||
@@ -6922,6 +6919,21 @@
|
||||
"too_many": "יותר מדי ניסיונות התחברות מהרשת הזו. המתינו כמה דקות ונסו שוב.",
|
||||
"page_declined": "שום דבר לא הועבר. אפשר לסגור את הכרטיסייה."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "בהיר או כהה",
|
||||
"server_label": "שרת דואר",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "מחפשים את שרת הדואר שלך…",
|
||||
"server_found": "שרת דואר: {server}",
|
||||
"server_change": "שינוי",
|
||||
"server_not_found": "לא מצאנו את שרת הדואר של הכתובת הזו. הזינו אותו כאן, למשל mail.example.com.",
|
||||
"server_needed": "הזינו את הכתובת של שרת הדואר שלכם.",
|
||||
"server_invalid": "זו לא כתובת של שרת. כתבו אותה כמו mail.example.com.",
|
||||
"email_needed": "הזינו קודם את כתובת הדוא״ל שלכם.",
|
||||
"trouble": "נתקלתם בבעיה?",
|
||||
"trouble_cors": "שרת הדואר שלכם צריך לאפשר כניסה מהאתר הזה. אם הכניסה נכשלת, בקשו ממי שמפעיל את שרת הדואר לאפשר זאת.",
|
||||
"provider_unsupported": "{provider} לא מציע JMAP, הדרך שבה Bulwark מדבר עם שרתי דואר, ולכן Bulwark עדיין לא יכול להתחבר אליו ישירות."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "עדכונים",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "Jelszó",
|
||||
"password_placeholder": "Add meg a jelszavad",
|
||||
"jmap_endpoint_label": "JMAP Szerver",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "A szervernek engedélyeznie kell a CORS kéréseket erről a tartományról.",
|
||||
"jmap_server_label": "Szerver",
|
||||
"jmap_server_auto_picked": "Szerver kiválasztva az e-mail tartományodból.",
|
||||
"sign_in": "Bejelentkezés",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Jelszó megjelenítése",
|
||||
"hide_password": "Jelszó elrejtése",
|
||||
"totp_toggle": "Van egy 2FA kódom",
|
||||
"totp_toggle": "Van egy kódom a hitelesítő alkalmazásomból",
|
||||
"token_toggle": "Bejelentkezés hozzáférési tokennel",
|
||||
"password_toggle": "Bejelentkezés jelszóval",
|
||||
"token_label": "Hozzáférési token",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Túl sok bejelentkezési kísérlet erről a hálózatról. Várjon néhány percet, és próbálja újra.",
|
||||
"page_declined": "Semmi nem került átadásra. Bezárhatja ezt a lapot."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Világos vagy sötét",
|
||||
"server_label": "Levelezőszerver",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "A levelezőszerver keresése…",
|
||||
"server_found": "Levelezőszerver: {server}",
|
||||
"server_change": "Módosítás",
|
||||
"server_not_found": "Ehhez a címhez nem találtunk levelezőszervert. Írja be ide, például mail.example.com.",
|
||||
"server_needed": "Írja be a levelezőszervere címét.",
|
||||
"server_invalid": "Ez nem szervercím. Így írja: mail.example.com.",
|
||||
"email_needed": "Először írja be az e-mail-címét.",
|
||||
"trouble": "Probléma van?",
|
||||
"trouble_cors": "A levelezőszervernek engedélyeznie kell a bejelentkezést erről a webhelyről. Ha a bejelentkezés nem sikerül, kérje meg a levelezőszerver üzemeltetőjét, hogy engedélyezze.",
|
||||
"provider_unsupported": "A(z) {provider} nem kínál JMAP-ot, azt a módot, ahogyan a Bulwark a levelezőszerverekkel beszél, ezért a Bulwark még nem tud közvetlenül csatlakozni hozzá."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Frissítések",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "utente@esempio.com",
|
||||
"password_label": "Password",
|
||||
"password_placeholder": "Inserisci la tua password",
|
||||
"jmap_endpoint_label": "Server JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Il server deve consentire le richieste CORS da questo dominio.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Server selezionato in base al tuo dominio email.",
|
||||
"sign_in": "Accedi",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Mostra password",
|
||||
"hide_password": "Nascondi password",
|
||||
"totp_toggle": "Ho un codice 2FA",
|
||||
"totp_toggle": "Ho un codice dalla mia app di autenticazione",
|
||||
"token_toggle": "Accedi con un token di accesso",
|
||||
"password_toggle": "Accedi con una password",
|
||||
"token_label": "Token di accesso",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Troppi tentativi di accesso da questa rete. Attendi qualche minuto e riprova.",
|
||||
"page_declined": "Non è stato consegnato nulla. Puoi chiudere questa scheda."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Chiaro o scuro",
|
||||
"server_label": "Server di posta",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Ricerca del tuo server di posta…",
|
||||
"server_found": "Server di posta: {server}",
|
||||
"server_change": "Cambia",
|
||||
"server_not_found": "Non abbiamo trovato il server di posta di questo indirizzo. Inseriscilo qui, per esempio mail.example.com.",
|
||||
"server_needed": "Inserisci l'indirizzo del tuo server di posta.",
|
||||
"server_invalid": "Questo non è l'indirizzo di un server. Scrivilo come mail.example.com.",
|
||||
"email_needed": "Inserisci prima il tuo indirizzo email.",
|
||||
"trouble": "Problemi?",
|
||||
"trouble_cors": "Il tuo server di posta deve consentire l'accesso da questo sito. Se l'accesso non riesce, chiedi a chi gestisce il server di posta di consentirlo.",
|
||||
"provider_unsupported": "{provider} non offre JMAP, il modo in cui Bulwark parla con i server di posta, quindi Bulwark non può ancora collegarsi direttamente."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Aggiornamenti",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "パスワード",
|
||||
"password_placeholder": "パスワードを入力",
|
||||
"jmap_endpoint_label": "JMAPサーバー",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "サーバーはこのドメインからのCORSリクエストを許可する必要があります。",
|
||||
"jmap_server_label": "サーバー",
|
||||
"jmap_server_auto_picked": "メールドメインからサーバーを選択しました。",
|
||||
"sign_in": "サインイン",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "パスワードを表示",
|
||||
"hide_password": "パスワードを隠す",
|
||||
"totp_toggle": "2FAコードを入力",
|
||||
"totp_toggle": "認証アプリのコードがあります",
|
||||
"token_toggle": "アクセストークンでログイン",
|
||||
"password_toggle": "パスワードでログイン",
|
||||
"token_label": "アクセストークン",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "このネットワークからのサインインの試行が多すぎます。数分待ってからもう一度お試しください。",
|
||||
"page_declined": "何も渡していません。このタブを閉じてかまいません。"
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "ライトまたはダーク",
|
||||
"server_label": "メールサーバー",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "メールサーバーを探しています…",
|
||||
"server_found": "メールサーバー: {server}",
|
||||
"server_change": "変更",
|
||||
"server_not_found": "このアドレスのメールサーバーが見つかりませんでした。ここに入力してください(例: mail.example.com)。",
|
||||
"server_needed": "メールサーバーのアドレスを入力してください。",
|
||||
"server_invalid": "サーバーのアドレスではありません。mail.example.com のように入力してください。",
|
||||
"email_needed": "まずメールアドレスを入力してください。",
|
||||
"trouble": "うまくいかない場合",
|
||||
"trouble_cors": "メールサーバーがこのウェブサイトからのサインインを許可している必要があります。サインインできない場合は、メールサーバーの管理者に許可を依頼してください。",
|
||||
"provider_unsupported": "{provider} は Bulwark がメールサーバーとやり取りする方式である JMAP に対応していないため、Bulwark はまだ直接接続できません。"
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "アップデート",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "비밀번호",
|
||||
"password_placeholder": "비밀번호를 입력해 주세요",
|
||||
"jmap_endpoint_label": "JMAP 서버",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "서버에서 이 도메인의 CORS 요청을 허용해야 해요.",
|
||||
"jmap_server_label": "서버",
|
||||
"jmap_server_auto_picked": "이메일 도메인에서 서버를 선택했습니다.",
|
||||
"sign_in": "로그인",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "비밀번호 표시",
|
||||
"hide_password": "비밀번호 숨기기",
|
||||
"totp_toggle": "2단계 인증 코드가 있어요",
|
||||
"totp_toggle": "인증 앱의 코드가 있어요",
|
||||
"token_toggle": "액세스 토큰으로 로그인",
|
||||
"password_toggle": "비밀번호로 로그인",
|
||||
"token_label": "액세스 토큰",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "이 네트워크에서 로그인 시도가 너무 많습니다. 몇 분 기다린 후 다시 시도하세요.",
|
||||
"page_declined": "아무것도 전달되지 않았습니다. 이 탭을 닫아도 됩니다."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "밝게 또는 어둡게",
|
||||
"server_label": "메일 서버",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "메일 서버를 찾고 있어요…",
|
||||
"server_found": "메일 서버: {server}",
|
||||
"server_change": "변경",
|
||||
"server_not_found": "이 주소의 메일 서버를 찾지 못했어요. 여기에 입력하세요. 예: mail.example.com",
|
||||
"server_needed": "메일 서버 주소를 입력하세요.",
|
||||
"server_invalid": "서버 주소가 아니에요. mail.example.com처럼 입력하세요.",
|
||||
"email_needed": "먼저 이메일 주소를 입력하세요.",
|
||||
"trouble": "문제가 있나요?",
|
||||
"trouble_cors": "메일 서버가 이 웹사이트에서의 로그인을 허용해야 해요. 로그인이 안 되면 메일 서버 관리자에게 허용해 달라고 요청하세요.",
|
||||
"provider_unsupported": "{provider}은(는) Bulwark가 메일 서버와 통신하는 방식인 JMAP을 제공하지 않아서, Bulwark가 아직 직접 연결할 수 없어요."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "업데이트",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "lietotajs@example.com",
|
||||
"password_label": "Parole",
|
||||
"password_placeholder": "Ievadiet paroli",
|
||||
"jmap_endpoint_label": "JMAP serveris",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Serverim ir jāatļauj CORS pieprasījumi no šī domēna.",
|
||||
"jmap_server_label": "Serveris",
|
||||
"jmap_server_auto_picked": "Serveris izvēlēts no jūsu e-pasta domēna.",
|
||||
"sign_in": "Pierakstīties",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Rādīt paroli",
|
||||
"hide_password": "Slēpt paroli",
|
||||
"totp_toggle": "Man ir 2FA kods",
|
||||
"totp_toggle": "Man ir kods no autentifikācijas lietotnes",
|
||||
"token_toggle": "Pierakstīties ar piekļuves marķieri",
|
||||
"password_toggle": "Pierakstīties ar paroli",
|
||||
"token_label": "Piekļuves marķieris",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Pārāk daudz pieteikšanās mēģinājumu no šī tīkla. Pagaidiet dažas minūtes un mēģiniet vēlreiz.",
|
||||
"page_declined": "Nekas netika nodots. Varat aizvērt šo cilni."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Gaišs vai tumšs",
|
||||
"server_label": "Pasta serveris",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Meklējam jūsu pasta serveri…",
|
||||
"server_found": "Pasta serveris: {server}",
|
||||
"server_change": "Mainīt",
|
||||
"server_not_found": "Šai adresei pasta serveri neatradām. Ievadiet to šeit, piemēram, mail.example.com.",
|
||||
"server_needed": "Ievadiet sava pasta servera adresi.",
|
||||
"server_invalid": "Tā nav servera adrese. Rakstiet to kā mail.example.com.",
|
||||
"email_needed": "Vispirms ievadiet savu e-pasta adresi.",
|
||||
"trouble": "Neizdodas?",
|
||||
"trouble_cors": "Jūsu pasta serverim jāatļauj pieteikšanās no šīs vietnes. Ja pieteikties neizdodas, palūdziet pasta servera uzturētājam to atļaut.",
|
||||
"provider_unsupported": "{provider} nepiedāvā JMAP, veidu, kā Bulwark sazinās ar pasta serveriem, tāpēc Bulwark tam vēl nevar pieslēgties tieši."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Atjauninājumi",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "Нууц үг",
|
||||
"password_placeholder": "Нууц үгээ оруулна уу",
|
||||
"jmap_endpoint_label": "JMAP сервер",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Сервер энэ домэйноос ирэх CORS хүсэлтийг зөвшөөрсөн байх ёстой.",
|
||||
"jmap_server_label": "Сервер",
|
||||
"jmap_server_auto_picked": "Таны имэйл хаягийн домэйноос серверийг автоматаар сонголоо.",
|
||||
"sign_in": "Нэвтрэх",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Нууц үг харуулах",
|
||||
"hide_password": "Нууц үг нуух",
|
||||
"totp_toggle": "Надад 2FA код байгаа",
|
||||
"totp_toggle": "Надад баталгаажуулах аппын код байгаа",
|
||||
"token_toggle": "Хандалтын токеноор нэвтрэх",
|
||||
"password_toggle": "Нууц үгээр нэвтрэх",
|
||||
"token_label": "Хандалтын токен",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Энэ сүлжээнээс хэт олон удаа нэвтрэхийг оролдсон байна. Хэдэн минут хүлээгээд дахин оролдоно уу.",
|
||||
"page_declined": "Юу ч шилжүүлээгүй. Энэ табыг хааж болно."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Цайвар эсвэл бараан",
|
||||
"server_label": "Шуудангийн сервер",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Таны шуудангийн серверийг хайж байна…",
|
||||
"server_found": "Шуудангийн сервер: {server}",
|
||||
"server_change": "Өөрчлөх",
|
||||
"server_not_found": "Энэ хаягийн шуудангийн сервер олдсонгүй. Энд оруулна уу, жишээ нь mail.example.com.",
|
||||
"server_needed": "Шуудангийн серверийнхээ хаягийг оруулна уу.",
|
||||
"server_invalid": "Энэ серверийн хаяг биш байна. mail.example.com гэх мэтээр бичнэ үү.",
|
||||
"email_needed": "Эхлээд имэйл хаягаа оруулна уу.",
|
||||
"trouble": "Асуудал гарч байна уу?",
|
||||
"trouble_cors": "Таны шуудангийн сервер энэ вэб сайтаас нэвтрэхийг зөвшөөрөх ёстой. Нэвтэрч чадахгүй бол шуудангийн серверээ хариуцдаг хүнээс зөвшөөрөхийг хүснэ үү.",
|
||||
"provider_unsupported": "{provider} нь Bulwark шуудангийн сервертэй харилцдаг JMAP-ыг санал болгодоггүй тул Bulwark одоогоор түүнтэй шууд холбогдож чадахгүй."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Шинэчлэлт",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "Passord",
|
||||
"password_placeholder": "Skriv inn passordet ditt",
|
||||
"jmap_endpoint_label": "JMAP-server",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Serveren må tillate CORS-forespørsler fra dette domenet.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Serveren ble valgt ut fra e-postdomenet ditt.",
|
||||
"sign_in": "Logg på",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Vis passord",
|
||||
"hide_password": "Skjul passord",
|
||||
"totp_toggle": "Jeg har en kode for tofaktorautentisering",
|
||||
"totp_toggle": "Jeg har en kode fra autentiseringsappen min",
|
||||
"token_toggle": "Logg på med et tilgangstoken",
|
||||
"password_toggle": "Logg på med passord",
|
||||
"token_label": "Tilgangstoken",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "For mange innloggingsforsøk fra dette nettverket. Vent noen minutter og prøv igjen.",
|
||||
"page_declined": "Ingenting ble overlevert. Du kan lukke denne fanen."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Lyst eller mørkt",
|
||||
"server_label": "E-postserver",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Leter etter e-postserveren din…",
|
||||
"server_found": "E-postserver: {server}",
|
||||
"server_change": "Endre",
|
||||
"server_not_found": "Vi fant ikke e-postserveren for denne adressen. Skriv den inn her, for eksempel mail.example.com.",
|
||||
"server_needed": "Skriv inn adressen til e-postserveren din.",
|
||||
"server_invalid": "Dette er ikke en serveradresse. Skriv den som mail.example.com.",
|
||||
"email_needed": "Skriv inn e-postadressen din først.",
|
||||
"trouble": "Problemer?",
|
||||
"trouble_cors": "E-postserveren din må tillate pålogging fra dette nettstedet. Hvis påloggingen mislykkes, be den som drifter e-postserveren om å tillate det.",
|
||||
"provider_unsupported": "{provider} tilbyr ikke JMAP, måten Bulwark snakker med e-postservere på, så Bulwark kan ikke koble seg direkte til det ennå."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Oppdateringer",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "gebruiker@voorbeeld.nl",
|
||||
"password_label": "Wachtwoord",
|
||||
"password_placeholder": "Voer je wachtwoord in",
|
||||
"jmap_endpoint_label": "JMAP-server",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "De server moet CORS-verzoeken van dit domein toestaan.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Server geselecteerd op basis van je e-maildomein.",
|
||||
"sign_in": "Aanmelden",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Wachtwoord tonen",
|
||||
"hide_password": "Wachtwoord verbergen",
|
||||
"totp_toggle": "Ik heb een 2FA-code",
|
||||
"totp_toggle": "Ik heb een code uit mijn authenticator-app",
|
||||
"token_toggle": "Aanmelden met een toegangstoken",
|
||||
"password_toggle": "Aanmelden met een wachtwoord",
|
||||
"token_label": "Toegangstoken",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Te veel inlogpogingen vanaf dit netwerk. Wacht een paar minuten en probeer het opnieuw.",
|
||||
"page_declined": "Er is niets overgedragen. Je kunt dit tabblad sluiten."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Licht of donker",
|
||||
"server_label": "Mailserver",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Je mailserver wordt gezocht…",
|
||||
"server_found": "Mailserver: {server}",
|
||||
"server_change": "Wijzigen",
|
||||
"server_not_found": "We hebben de mailserver voor dit adres niet gevonden. Vul hem hier in, bijvoorbeeld mail.example.com.",
|
||||
"server_needed": "Vul het adres van je mailserver in.",
|
||||
"server_invalid": "Dit is geen serveradres. Schrijf het zoals mail.example.com.",
|
||||
"email_needed": "Vul eerst je e-mailadres in.",
|
||||
"trouble": "Lukt het niet?",
|
||||
"trouble_cors": "Je mailserver moet aanmelden vanaf deze website toestaan. Lukt aanmelden niet, vraag dan wie je mailserver beheert om het toe te staan.",
|
||||
"provider_unsupported": "{provider} biedt geen JMAP, de manier waarop Bulwark met mailservers praat, dus Bulwark kan er nog niet rechtstreeks mee verbinden."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Updates",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "Hasło",
|
||||
"password_placeholder": "Wprowadź hasło",
|
||||
"jmap_endpoint_label": "Serwer JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Serwer musi zezwalać na żądania CORS z tej domeny.",
|
||||
"jmap_server_label": "Serwer",
|
||||
"jmap_server_auto_picked": "Serwer wybrany na podstawie domeny Twojej poczty.",
|
||||
"sign_in": "Zaloguj się",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Pokaż hasło",
|
||||
"hide_password": "Ukryj hasło",
|
||||
"totp_toggle": "Mam kod 2FA",
|
||||
"totp_toggle": "Mam kod z aplikacji uwierzytelniającej",
|
||||
"token_toggle": "Zaloguj się tokenem dostępu",
|
||||
"password_toggle": "Zaloguj się hasłem",
|
||||
"token_label": "Token dostępu",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Zbyt wiele prób logowania z tej sieci. Poczekaj kilka minut i spróbuj ponownie.",
|
||||
"page_declined": "Nic nie zostało przekazane. Możesz zamknąć tę kartę."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Jasny czy ciemny",
|
||||
"server_label": "Serwer poczty",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Szukamy Twojego serwera poczty…",
|
||||
"server_found": "Serwer poczty: {server}",
|
||||
"server_change": "Zmień",
|
||||
"server_not_found": "Nie znaleźliśmy serwera poczty dla tego adresu. Wpisz go tutaj, na przykład mail.example.com.",
|
||||
"server_needed": "Wpisz adres swojego serwera poczty.",
|
||||
"server_invalid": "To nie jest adres serwera. Wpisz go jak mail.example.com.",
|
||||
"email_needed": "Najpierw wpisz swój adres e-mail.",
|
||||
"trouble": "Masz problem?",
|
||||
"trouble_cors": "Twój serwer poczty musi zezwalać na logowanie z tej witryny. Jeśli logowanie się nie udaje, poproś osobę, która zarządza serwerem poczty, o zezwolenie.",
|
||||
"provider_unsupported": "{provider} nie oferuje JMAP, czyli sposobu, w jaki Bulwark rozmawia z serwerami poczty, więc Bulwark nie może jeszcze połączyć się z nim bezpośrednio."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Aktualizacje",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "usuario@exemplo.com",
|
||||
"password_label": "Senha",
|
||||
"password_placeholder": "Digite sua senha",
|
||||
"jmap_endpoint_label": "Servidor JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "O servidor deve permitir solicitações CORS deste domínio.",
|
||||
"jmap_server_label": "Servidor",
|
||||
"jmap_server_auto_picked": "Servidor selecionado a partir do domínio do seu e-mail.",
|
||||
"sign_in": "Entrar",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Mostrar senha",
|
||||
"hide_password": "Ocultar senha",
|
||||
"totp_toggle": "Tenho um código 2FA",
|
||||
"totp_toggle": "Tenho um código da minha aplicação de autenticação",
|
||||
"token_toggle": "Entrar com um token de acesso",
|
||||
"password_toggle": "Entrar com uma senha",
|
||||
"token_label": "Token de acesso",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Demasiadas tentativas de entrada a partir desta rede. Aguarde alguns minutos e tente novamente.",
|
||||
"page_declined": "Nada foi entregue. Pode fechar este separador."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Claro ou escuro",
|
||||
"server_label": "Servidor de e-mail",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "A procurar o seu servidor de e-mail…",
|
||||
"server_found": "Servidor de e-mail: {server}",
|
||||
"server_change": "Alterar",
|
||||
"server_not_found": "Não encontrámos o servidor de e-mail deste endereço. Escreva-o aqui, por exemplo mail.example.com.",
|
||||
"server_needed": "Escreva o endereço do seu servidor de e-mail.",
|
||||
"server_invalid": "Isto não é o endereço de um servidor. Escreva-o como mail.example.com.",
|
||||
"email_needed": "Escreva primeiro o seu endereço de e-mail.",
|
||||
"trouble": "Tem problemas?",
|
||||
"trouble_cors": "O seu servidor de e-mail tem de permitir inícios de sessão a partir deste site. Se não conseguir entrar, peça a quem gere o servidor de e-mail que o permita.",
|
||||
"provider_unsupported": "{provider} não oferece JMAP, a forma como o Bulwark fala com os servidores de e-mail, por isso o Bulwark ainda não se consegue ligar diretamente."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Atualizações",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "Parolă",
|
||||
"password_placeholder": "Introduceți parola",
|
||||
"jmap_endpoint_label": "JMAP Server",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Serverul trebuie să permită cererile CORS provenite de la acest domeniu.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Server selectat din domeniul dvs. de e-mail.",
|
||||
"sign_in": "Autentificare",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Afișează parola",
|
||||
"hide_password": "Ascunde parola",
|
||||
"totp_toggle": "Am un cod 2FA",
|
||||
"totp_toggle": "Am un cod din aplicația de autentificare",
|
||||
"token_toggle": "Autentificare cu un token de acces",
|
||||
"password_toggle": "Autentificare cu o parolă",
|
||||
"token_label": "Token de acces",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Prea multe încercări de conectare din această rețea. Așteptați câteva minute și încercați din nou.",
|
||||
"page_declined": "Nu s-a transmis nimic. Puteți închide această filă."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Luminos sau întunecat",
|
||||
"server_label": "Server de e-mail",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Căutăm serverul tău de e-mail…",
|
||||
"server_found": "Server de e-mail: {server}",
|
||||
"server_change": "Schimbă",
|
||||
"server_not_found": "Nu am găsit serverul de e-mail pentru această adresă. Introdu-l aici, de exemplu mail.example.com.",
|
||||
"server_needed": "Introdu adresa serverului tău de e-mail.",
|
||||
"server_invalid": "Aceasta nu este adresa unui server. Scrie-o ca mail.example.com.",
|
||||
"email_needed": "Introdu mai întâi adresa ta de e-mail.",
|
||||
"trouble": "Ai probleme?",
|
||||
"trouble_cors": "Serverul tău de e-mail trebuie să permită autentificarea de pe acest site. Dacă autentificarea nu reușește, cere celui care administrează serverul să o permită.",
|
||||
"provider_unsupported": "{provider} nu oferă JMAP, modul în care Bulwark comunică cu serverele de e-mail, așa că Bulwark nu se poate conecta încă direct la el."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Actualizări",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@пример.рф",
|
||||
"password_label": "Пароль",
|
||||
"password_placeholder": "Введите пароль",
|
||||
"jmap_endpoint_label": "Сервер JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Сервер должен разрешать CORS-запросы с этого домена.",
|
||||
"jmap_server_label": "Сервер",
|
||||
"jmap_server_auto_picked": "Сервер выбран по домену вашей электронной почты.",
|
||||
"sign_in": "Войти",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Показать пароль",
|
||||
"hide_password": "Скрыть пароль",
|
||||
"totp_toggle": "У меня есть код 2FA",
|
||||
"totp_toggle": "У меня есть код из приложения-аутентификатора",
|
||||
"token_toggle": "Войти с токеном доступа",
|
||||
"password_toggle": "Войти с паролем",
|
||||
"token_label": "Токен доступа",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Слишком много попыток входа из этой сети. Подождите несколько минут и попробуйте ещё раз.",
|
||||
"page_declined": "Ничего не передано. Можно закрыть эту вкладку."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Светлая или тёмная",
|
||||
"server_label": "Почтовый сервер",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Ищем ваш почтовый сервер…",
|
||||
"server_found": "Почтовый сервер: {server}",
|
||||
"server_change": "Изменить",
|
||||
"server_not_found": "Мы не нашли почтовый сервер для этого адреса. Введите его здесь, например mail.example.com.",
|
||||
"server_needed": "Введите адрес вашего почтового сервера.",
|
||||
"server_invalid": "Это не адрес сервера. Напишите его как mail.example.com.",
|
||||
"email_needed": "Сначала введите адрес электронной почты.",
|
||||
"trouble": "Не получается?",
|
||||
"trouble_cors": "Ваш почтовый сервер должен разрешать вход с этого сайта. Если войти не удаётся, попросите того, кто обслуживает ваш почтовый сервер, разрешить это.",
|
||||
"provider_unsupported": "{provider} не поддерживает JMAP, способ, которым Bulwark общается с почтовыми серверами, поэтому Bulwark пока не может подключиться к нему напрямую."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Обновления",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "pouzivatel@example.com",
|
||||
"password_label": "Heslo",
|
||||
"password_placeholder": "Zadajte heslo",
|
||||
"jmap_endpoint_label": "JMAP server",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Server musí povoliť požiadavky CORS z tejto domény.",
|
||||
"jmap_server_label": "Server",
|
||||
"jmap_server_auto_picked": "Server bol vybratý podľa domény vášho e-mailu.",
|
||||
"sign_in": "Prihlásiť sa",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Zobraziť heslo",
|
||||
"hide_password": "Skryť heslo",
|
||||
"totp_toggle": "Mám kód 2FA",
|
||||
"totp_toggle": "Mám kód z overovacej aplikácie",
|
||||
"token_toggle": "Prihlásiť sa prístupovým tokenom",
|
||||
"password_toggle": "Prihlásiť sa heslom",
|
||||
"token_label": "Prístupový token",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Príliš veľa pokusov o prihlásenie z tejto siete. Počkajte niekoľko minút a skúste to znova.",
|
||||
"page_declined": "Nič nebolo odovzdané. Túto kartu môžete zavrieť."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Svetlý alebo tmavý",
|
||||
"server_label": "Poštový server",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Hľadáme váš poštový server…",
|
||||
"server_found": "Poštový server: {server}",
|
||||
"server_change": "Zmeniť",
|
||||
"server_not_found": "Pre túto adresu sme poštový server nenašli. Zadajte ho sem, napríklad mail.example.com.",
|
||||
"server_needed": "Zadajte adresu svojho poštového servera.",
|
||||
"server_invalid": "Toto nie je adresa servera. Napíšte ju ako mail.example.com.",
|
||||
"email_needed": "Najprv zadajte svoju e-mailovú adresu.",
|
||||
"trouble": "Nedarí sa?",
|
||||
"trouble_cors": "Váš poštový server musí povoliť prihlasovanie z tejto webovej stránky. Ak prihlásenie zlyhá, požiadajte správcu poštového servera, aby to povolil.",
|
||||
"provider_unsupported": "{provider} neponúka JMAP, spôsob, akým Bulwark komunikuje s poštovými servermi, takže sa k nemu Bulwark zatiaľ nemôže pripojiť priamo."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Aktualizácie",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -136,9 +136,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "รหัสผ่าน",
|
||||
"password_placeholder": "กรอกรหัสผ่านของคุณ",
|
||||
"jmap_endpoint_label": "เซิร์ฟเวอร์ JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "เซิร์ฟเวอร์ต้องอนุญาตคำขอ CORS จากโดเมนนี้",
|
||||
"jmap_server_label": "เซิร์ฟเวอร์",
|
||||
"jmap_server_auto_picked": "เลือกเซิร์ฟเวอร์จากโดเมนอีเมลของคุณโดยอัตโนมัติ",
|
||||
"sign_in": "เข้าสู่ระบบ",
|
||||
@@ -159,7 +156,7 @@
|
||||
},
|
||||
"show_password": "แสดงรหัสผ่าน",
|
||||
"hide_password": "ซ่อนรหัสผ่าน",
|
||||
"totp_toggle": "ฉันมีรหัสยืนยันตัวตน (2FA)",
|
||||
"totp_toggle": "ฉันมีรหัสจากแอปยืนยันตัวตน",
|
||||
"token_toggle": "เข้าสู่ระบบด้วยโทเค็น (Access Token)",
|
||||
"password_toggle": "เข้าสู่ระบบด้วยรหัสผ่าน",
|
||||
"token_label": "โทเค็นการเข้าถึง (Access Token)",
|
||||
@@ -6929,6 +6926,21 @@
|
||||
"too_many": "มีการพยายามลงชื่อเข้าใช้จากเครือข่ายนี้มากเกินไป รอสักครู่แล้วลองอีกครั้ง",
|
||||
"page_declined": "ไม่มีการส่งมอบสิ่งใด คุณปิดแท็บนี้ได้"
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "สว่างหรือมืด",
|
||||
"server_label": "เซิร์ฟเวอร์อีเมล",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "กำลังค้นหาเซิร์ฟเวอร์อีเมลของคุณ…",
|
||||
"server_found": "เซิร์ฟเวอร์อีเมล: {server}",
|
||||
"server_change": "เปลี่ยน",
|
||||
"server_not_found": "ไม่พบเซิร์ฟเวอร์อีเมลของที่อยู่นี้ ป้อนที่นี่ เช่น mail.example.com",
|
||||
"server_needed": "ป้อนที่อยู่เซิร์ฟเวอร์อีเมลของคุณ",
|
||||
"server_invalid": "นี่ไม่ใช่ที่อยู่เซิร์ฟเวอร์ ให้พิมพ์แบบ mail.example.com",
|
||||
"email_needed": "ป้อนที่อยู่อีเมลของคุณก่อน",
|
||||
"trouble": "มีปัญหาใช่ไหม",
|
||||
"trouble_cors": "เซิร์ฟเวอร์อีเมลของคุณต้องอนุญาตให้ลงชื่อเข้าใช้จากเว็บไซต์นี้ หากลงชื่อเข้าใช้ไม่ได้ ให้ขอผู้ดูแลเซิร์ฟเวอร์อีเมลอนุญาต",
|
||||
"provider_unsupported": "{provider} ไม่รองรับ JMAP ซึ่งเป็นวิธีที่ Bulwark ใช้คุยกับเซิร์ฟเวอร์อีเมล Bulwark จึงยังเชื่อมต่อโดยตรงไม่ได้"
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "การอัปเดต",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+17
-5
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "kullanici@example.com",
|
||||
"password_label": "Şifre",
|
||||
"password_placeholder": "Şifrenizi girin",
|
||||
"jmap_endpoint_label": "JMAP Sunucusu",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Sunucu, bu etki alanından gelen CORS isteklerine izin vermelidir.",
|
||||
"jmap_server_label": "Sunucu",
|
||||
"jmap_server_auto_picked": "Sunucu, e-posta etki alanınıza göre seçildi.",
|
||||
"sign_in": "Giriş yap",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Şifreyi göster",
|
||||
"hide_password": "Şifreyi gizle",
|
||||
"totp_toggle": "2FA kodum var",
|
||||
"totp_toggle": "Kimlik doğrulama uygulamamdan bir kodum var",
|
||||
"token_toggle": "Erişim belirteciyle giriş yap",
|
||||
"password_toggle": "Şifreyle giriş yap",
|
||||
"token_label": "Erişim belirteci",
|
||||
@@ -48,7 +45,7 @@
|
||||
"dismiss": "Kapat",
|
||||
"or": "veya",
|
||||
"sign_in_sso": "SSO ile giriş yap",
|
||||
"add_account_title": "Hesap Ekle",
|
||||
"add_account_title": "Hesap ekle",
|
||||
"add_account_subtitle": "Başka bir hesapla giriş yap",
|
||||
"subtitle": "Hesabınızda oturum açın",
|
||||
"version": {
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Bu ağdan çok fazla oturum açma denemesi yapıldı. Birkaç dakika bekleyip yeniden deneyin.",
|
||||
"page_declined": "Hiçbir şey aktarılmadı. Bu sekmeyi kapatabilirsiniz."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Açık veya koyu",
|
||||
"server_label": "Posta sunucusu",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Posta sunucunuz aranıyor…",
|
||||
"server_found": "Posta sunucusu: {server}",
|
||||
"server_change": "Değiştir",
|
||||
"server_not_found": "Bu adresin posta sunucusunu bulamadık. Buraya girin, örneğin mail.example.com.",
|
||||
"server_needed": "Posta sunucunuzun adresini girin.",
|
||||
"server_invalid": "Bu bir sunucu adresi değil. mail.example.com gibi yazın.",
|
||||
"email_needed": "Önce e-posta adresinizi girin.",
|
||||
"trouble": "Sorun mu var?",
|
||||
"trouble_cors": "Posta sunucunuzun bu web sitesinden oturum açmaya izin vermesi gerekir. Oturum açılamazsa posta sunucunuzu yöneten kişiden buna izin vermesini isteyin.",
|
||||
"provider_unsupported": "{provider}, Bulwark'ın posta sunucularıyla konuştuğu yol olan JMAP'i sunmuyor, bu yüzden Bulwark henüz ona doğrudan bağlanamıyor."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Güncellemeler",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "Пароль",
|
||||
"password_placeholder": "Введіть свій пароль",
|
||||
"jmap_endpoint_label": "Сервер JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Сервер має дозволити запити CORS із цього домену.",
|
||||
"jmap_server_label": "Сервер",
|
||||
"jmap_server_auto_picked": "Сервер обрано з домену вашої електронної пошти.",
|
||||
"sign_in": "Увійдіть",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Показати пароль",
|
||||
"hide_password": "Приховати пароль",
|
||||
"totp_toggle": "У мене є код 2FA",
|
||||
"totp_toggle": "У мене є код із застосунку автентифікації",
|
||||
"token_toggle": "Увійти за допомогою токена доступу",
|
||||
"password_toggle": "Увійти за допомогою пароля",
|
||||
"token_label": "Токен доступу",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "Забагато спроб входу з цієї мережі. Зачекайте кілька хвилин і спробуйте ще раз.",
|
||||
"page_declined": "Нічого не передано. Можна закрити цю вкладку."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Світла чи темна",
|
||||
"server_label": "Поштовий сервер",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Шукаємо ваш поштовий сервер…",
|
||||
"server_found": "Поштовий сервер: {server}",
|
||||
"server_change": "Змінити",
|
||||
"server_not_found": "Ми не знайшли поштовий сервер для цієї адреси. Введіть його тут, наприклад mail.example.com.",
|
||||
"server_needed": "Введіть адресу свого поштового сервера.",
|
||||
"server_invalid": "Це не адреса сервера. Напишіть її як mail.example.com.",
|
||||
"email_needed": "Спершу введіть свою адресу електронної пошти.",
|
||||
"trouble": "Не виходить?",
|
||||
"trouble_cors": "Ваш поштовий сервер має дозволяти вхід із цього сайту. Якщо увійти не вдається, попросіть того, хто обслуговує поштовий сервер, дозволити це.",
|
||||
"provider_unsupported": "{provider} не підтримує JMAP, спосіб, у який Bulwark спілкується з поштовими серверами, тому Bulwark поки не може підключитися до нього напряму."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Оновлення",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "Mật khẩu",
|
||||
"password_placeholder": "Nhập mật khẩu của bạn",
|
||||
"jmap_endpoint_label": "Máy chủ JMAP",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "Máy chủ phải cho phép các yêu cầu CORS từ miền này.",
|
||||
"jmap_server_label": "Máy chủ",
|
||||
"jmap_server_auto_picked": "Máy chủ được chọn từ miền email của bạn.",
|
||||
"sign_in": "Đăng nhập",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "Hiển thị mật khẩu",
|
||||
"hide_password": "Ẩn mật khẩu",
|
||||
"totp_toggle": "Tôi có mã 2FA",
|
||||
"totp_toggle": "Tôi có mã từ ứng dụng xác thực",
|
||||
"token_toggle": "Đăng nhập bằng mã truy cập",
|
||||
"password_toggle": "Đăng nhập bằng mật khẩu",
|
||||
"token_label": "Mã truy cập",
|
||||
@@ -6933,6 +6930,21 @@
|
||||
"too_many": "Có quá nhiều lần thử đăng nhập từ mạng này. Hãy đợi vài phút rồi thử lại.",
|
||||
"page_declined": "Không có gì được chuyển giao. Bạn có thể đóng tab này."
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "Sáng hay tối",
|
||||
"server_label": "Máy chủ thư",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "Đang tìm máy chủ thư của bạn…",
|
||||
"server_found": "Máy chủ thư: {server}",
|
||||
"server_change": "Đổi",
|
||||
"server_not_found": "Không tìm thấy máy chủ thư cho địa chỉ này. Nhập vào đây, ví dụ mail.example.com.",
|
||||
"server_needed": "Nhập địa chỉ máy chủ thư của bạn.",
|
||||
"server_invalid": "Đây không phải địa chỉ máy chủ. Hãy viết như mail.example.com.",
|
||||
"email_needed": "Hãy nhập địa chỉ email trước.",
|
||||
"trouble": "Gặp sự cố?",
|
||||
"trouble_cors": "Máy chủ thư của bạn phải cho phép đăng nhập từ trang web này. Nếu đăng nhập không được, hãy nhờ người quản lý máy chủ thư cho phép.",
|
||||
"provider_unsupported": "{provider} không hỗ trợ JMAP, cách Bulwark trao đổi với máy chủ thư, nên Bulwark chưa thể kết nối trực tiếp."
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "Cập nhật",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"login": {
|
||||
"jmap_endpoint_cors_hint": "Máy chủ thư phải cho phép đăng nhập từ trang web này. Quản trị viên có thể thiết lập việc này.",
|
||||
"reconnecting": "Đã mất kết nối. Đang thử kết nối lại…",
|
||||
"error": {
|
||||
"invalid_credentials": "Địa chỉ email hoặc mật khẩu không đúng. Hãy kiểm tra cả hai rồi thử lại.",
|
||||
@@ -14,7 +13,6 @@
|
||||
"oauth_discovery_failed": "Hiện không thể đăng nhập thông qua tổ chức của bạn. Hãy thử lại sau hoặc liên hệ quản trị viên.",
|
||||
"token_exchange_failed": "Mật khẩu và mã của bạn đúng nhưng máy chủ thư không cho phép ứng dụng này truy cập. Hãy nhờ quản trị viên kiểm tra cài đặt đăng nhập cho ứng dụng này."
|
||||
},
|
||||
"totp_toggle": "Tôi có mã từ ứng dụng xác thực",
|
||||
"token_hint": "Một số nhà cung cấp thư cho phép dùng mã truy cập thay cho mật khẩu. Hãy tạo mã trong cài đặt của nhà cung cấp thư, sao chép rồi dán vào đây.",
|
||||
"config_error": {
|
||||
"title": "Ứng dụng thư điện tử trên web chưa được thiết lập đúng",
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "密碼",
|
||||
"password_placeholder": "請輸入密碼",
|
||||
"jmap_endpoint_label": "JMAP 伺服器",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "伺服器必須允許來自此網域的 CORS 請求。",
|
||||
"jmap_server_label": "伺服器",
|
||||
"jmap_server_auto_picked": "已根據您的電子郵件網域選擇伺服器。",
|
||||
"sign_in": "登入",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "顯示密碼",
|
||||
"hide_password": "隱藏密碼",
|
||||
"totp_toggle": "我有 2FA 驗證碼",
|
||||
"totp_toggle": "我有驗證器應用程式的驗證碼",
|
||||
"token_toggle": "使用存取權杖登入",
|
||||
"password_toggle": "使用密碼登入",
|
||||
"token_label": "存取權杖",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "來自此網路的登入嘗試過多。請等幾分鐘後再試一次。",
|
||||
"page_declined": "沒有交出任何內容。你可以關閉此分頁。"
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "淺色或深色",
|
||||
"server_label": "郵件伺服器",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "正在尋找你的郵件伺服器…",
|
||||
"server_found": "郵件伺服器:{server}",
|
||||
"server_change": "變更",
|
||||
"server_not_found": "找不到這個地址的郵件伺服器。請在這裡輸入,例如 mail.example.com。",
|
||||
"server_needed": "請輸入你的郵件伺服器位址。",
|
||||
"server_invalid": "這不是伺服器位址。請依 mail.example.com 的格式輸入。",
|
||||
"email_needed": "請先輸入你的電子郵件地址。",
|
||||
"trouble": "遇到問題?",
|
||||
"trouble_cors": "你的郵件伺服器必須允許從這個網站登入。如果無法登入,請負責郵件伺服器的人允許。",
|
||||
"provider_unsupported": "{provider} 不支援 JMAP(Bulwark 與郵件伺服器溝通的方式),所以 Bulwark 目前還無法直接連線。"
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "更新",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
+16
-4
@@ -6,9 +6,6 @@
|
||||
"username_placeholder": "user@example.com",
|
||||
"password_label": "密码",
|
||||
"password_placeholder": "请输入密码",
|
||||
"jmap_endpoint_label": "JMAP 服务器",
|
||||
"jmap_endpoint_placeholder": "https://mail.example.com",
|
||||
"jmap_endpoint_cors_hint": "服务器必须允许来自此域名的 CORS 请求。",
|
||||
"jmap_server_label": "服务器",
|
||||
"jmap_server_auto_picked": "已根据您的电子邮件域名选择服务器。",
|
||||
"sign_in": "登录",
|
||||
@@ -29,7 +26,7 @@
|
||||
},
|
||||
"show_password": "显示密码",
|
||||
"hide_password": "隐藏密码",
|
||||
"totp_toggle": "我有双重验证代码",
|
||||
"totp_toggle": "我有身份验证器应用中的验证码",
|
||||
"token_toggle": "使用访问令牌登录",
|
||||
"password_toggle": "使用密码登录",
|
||||
"token_label": "访问令牌",
|
||||
@@ -6921,6 +6918,21 @@
|
||||
"too_many": "来自此网络的登录尝试过多。请等几分钟后重试。",
|
||||
"page_declined": "没有交出任何内容。你可以关闭此标签页。"
|
||||
},
|
||||
"uxs_login": {
|
||||
"theme_button": "浅色或深色",
|
||||
"server_label": "邮件服务器",
|
||||
"server_placeholder": "mail.example.com",
|
||||
"server_looking": "正在查找你的邮件服务器…",
|
||||
"server_found": "邮件服务器:{server}",
|
||||
"server_change": "更改",
|
||||
"server_not_found": "没有找到这个地址的邮件服务器。请在这里输入,例如 mail.example.com。",
|
||||
"server_needed": "请输入你的邮件服务器地址。",
|
||||
"server_invalid": "这不是服务器地址。请按 mail.example.com 这样的格式输入。",
|
||||
"email_needed": "请先输入你的邮箱地址。",
|
||||
"trouble": "遇到问题?",
|
||||
"trouble_cors": "你的邮件服务器需要允许从这个网站登录。如果登录失败,请让管理邮件服务器的人允许。",
|
||||
"provider_unsupported": "{provider} 不支持 JMAP(Bulwark 与邮件服务器通信的方式),所以 Bulwark 暂时无法直接连接它。"
|
||||
},
|
||||
"desktop_updates": {
|
||||
"title": "更新",
|
||||
"version_label": "Bulwark {version}",
|
||||
|
||||
@@ -126,6 +126,8 @@ export const LITE_API_STRING_ALLOWLIST = [
|
||||
"/api/auth/totp-token-exchange",
|
||||
"/api/auth/session",
|
||||
"/api/auth/verify",
|
||||
// The sign-in form's mail server lookup: the login page skips it in Lite (IS_LITE).
|
||||
"/api/auth/discover-server",
|
||||
"/api/auth/stalwart-context",
|
||||
"/api/account/stalwart/jmap",
|
||||
// Admin shield probe - skipped in Lite (components/layout/navigation-rail.tsx).
|
||||
|
||||
Reference in New Issue
Block a user