mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-06 00:15:32 +00:00
fix: name the TLS-only delivery option plainly and point to encryption add-ons
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
||||
import React from 'react';
|
||||
import { EmailComposer } from '../email-composer';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { useSettingsStore } from '@/stores/settings-store';
|
||||
import { usePolicyStore } from '@/stores/policy-store';
|
||||
import { emailHooks } from '@/lib/plugin-hooks';
|
||||
|
||||
// "Require encrypted delivery" read like encryption but only asks the servers
|
||||
// for TLS on the way. It is now named for what it does and is an advanced
|
||||
// option, and without a crypto plugin an "Encrypt…" item says where to get one.
|
||||
|
||||
// ─── Heavy component mocks (mirrors composer-close-guard.test.tsx) ──────────
|
||||
|
||||
vi.mock('@/components/email/rich-text-editor', () => ({
|
||||
RichTextEditor: () => React.createElement('div', { 'data-testid': 'rich-text-editor' }),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
|
||||
vi.mock('@/components/identity/sub-address-helper', () => ({ SubAddressHelper: () => null }));
|
||||
vi.mock('@/components/templates/template-picker', () => ({ TemplatePicker: () => null }));
|
||||
vi.mock('@/components/templates/template-form', () => ({ TemplateForm: () => null }));
|
||||
vi.mock('@/components/files/file-preview-modal', () => ({ FilePreviewModal: () => null }));
|
||||
vi.mock('@/hooks/use-focus-trap', () => ({
|
||||
useFocusTrap: () => ({ current: null }),
|
||||
}));
|
||||
vi.mock('@/hooks/use-pro-multi-account-identities', () => ({
|
||||
useProMultiAccountIdentities: () => ({ enabled: false, groups: [], allIdentities: [] }),
|
||||
stripCrossAccountIdentityPrefix: (id: string) => ({ localAccountId: null, rawId: id }),
|
||||
}));
|
||||
|
||||
const push = vi.fn();
|
||||
vi.mock('@/i18n/navigation', () => ({
|
||||
useRouter: () => ({ push, back: vi.fn(), replace: vi.fn() }),
|
||||
usePathname: () => '/en',
|
||||
}));
|
||||
|
||||
// ─── Store mocks ──────────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock('@/stores/auth-store', () => {
|
||||
const state = {
|
||||
client: null,
|
||||
identities: [],
|
||||
primaryIdentity: null,
|
||||
isAuthenticated: false,
|
||||
isDemoMode: false,
|
||||
activeAccountId: null,
|
||||
connectionLost: false,
|
||||
getClientForAccount: () => undefined,
|
||||
getAllConnectedClients: () => new Map(),
|
||||
syncIdentities: () => {},
|
||||
refreshIdentities: async () => {},
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useAuthStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/identity-store', () => {
|
||||
const state = {
|
||||
identities: [{ id: 'id-me', email: 'me@example.com', name: 'Me' }],
|
||||
defaultIdentityId: 'id-me',
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useIdentityStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/account-store', () => {
|
||||
const state = { accounts: [], getAccountById: () => undefined };
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useAccountStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/email-store', () => {
|
||||
const state = {
|
||||
draftSaveEnabled: false,
|
||||
sendRawEmail: async () => ({ sent: true }),
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useEmailStore: hook };
|
||||
});
|
||||
|
||||
const updateSetting = vi.fn();
|
||||
|
||||
vi.mock('@/stores/settings-store', () => {
|
||||
const state = {
|
||||
timeFormat: '24h',
|
||||
plainTextMode: false,
|
||||
subAddressDelimiter: '+',
|
||||
autoSelectReplyIdentity: true,
|
||||
attachmentReminderEnabled: false,
|
||||
attachmentReminderKeywords: [],
|
||||
emptySubjectWarningEnabled: true,
|
||||
sendDelaySeconds: 0,
|
||||
showAdvancedSettings: false,
|
||||
signaturePosition: 'above_quote',
|
||||
signatureSeparatorEnabled: false,
|
||||
requestReadReceiptDefault: false,
|
||||
addTrustedSender: () => {},
|
||||
trustedSendersAddressBook: null,
|
||||
updateSetting: (...args: unknown[]) => updateSetting(...args),
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useSettingsStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/contact-store', () => {
|
||||
const state = {
|
||||
contacts: [],
|
||||
getAutocomplete: async () => [],
|
||||
addToTrustedSendersBook: async () => {},
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useContactStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/template-store', () => {
|
||||
const state = { templates: [], addTemplate: async () => {} };
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useTemplateStore: hook };
|
||||
});
|
||||
|
||||
// ─── Misc dependency mocks ────────────────────────────────────────────────────
|
||||
|
||||
vi.mock('@/stores/toast-store', () => ({
|
||||
toast: { info: () => {}, error: () => {}, success: () => {} },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/plugin-hooks', () => ({
|
||||
identityHooks: {
|
||||
onSignatureRender: { size: 0, transform: async (value: unknown) => value },
|
||||
onIdentitySelect: { size: 0, emit: async () => {} },
|
||||
},
|
||||
templateHooks: {
|
||||
onTemplateRender: { size: 0, transform: async (value: unknown) => value },
|
||||
onTemplateApply: { size: 0, emit: async () => {} },
|
||||
},
|
||||
emailHooks: {
|
||||
onComposerOpen: { call: async () => [] },
|
||||
onRecipientChange: { call: async () => [] },
|
||||
getRecipientSuggestions: { call: async () => [] },
|
||||
onRecipientChipsChange: { transform: async (chips: unknown) => chips },
|
||||
onDraftChange: { emit: () => {} },
|
||||
onBeforeDraftAutoSave: { transform: async (draft: unknown) => draft },
|
||||
onBeforeEmailSend: { intercept: async () => true, interceptOutcome: async () => 'allowed' },
|
||||
onComposeSend: { size: 0, takeOver: async () => 'default' },
|
||||
onTransformOutgoingEmail: { transform: async (email: unknown) => email },
|
||||
},
|
||||
contactHooks: {
|
||||
search: { call: async () => [] },
|
||||
onProvideRecipientSuggestions: { transform: async (initial: unknown) => initial },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/email-sanitization', () => ({
|
||||
sanitizeSignatureHtml: (v: string) => v,
|
||||
sanitizeEmailHtml: (v: string) => v,
|
||||
sanitizeSignatureHtmlForDisplay: (v: string) => v,
|
||||
sanitizePluginBodyHtml: (v: string) => v,
|
||||
escapeHtml: (v: string) => v,
|
||||
parseHtmlSafely: (html: string) => new DOMParser().parseFromString(html, 'text/html'),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/email-threading', () => ({
|
||||
computeReplyThreadingHeaders: () => ({ inReplyTo: [], references: [] }),
|
||||
}));
|
||||
vi.mock('@/lib/signature-utils', () => ({
|
||||
appendPlainTextSignature: (body: string) => body,
|
||||
getPlainTextSignature: () => '',
|
||||
}));
|
||||
vi.mock('@/lib/sub-addressing', () => ({ generateSubAddress: () => '' }));
|
||||
vi.mock('@/lib/debug', () => ({ debug: { log: () => {}, warn: () => {}, error: () => {} } }));
|
||||
vi.mock('@/components/email/quoted-html', () => ({
|
||||
buildQuotedHtmlBlock: () => '',
|
||||
serializeEditorContent: () => '',
|
||||
}));
|
||||
vi.mock('@/lib/template-utils', () => ({ substitutePlaceholders: (s: string) => s }));
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const DRAFT = {
|
||||
to: 'bob@example.com',
|
||||
cc: '',
|
||||
bcc: '',
|
||||
subject: 'Plans',
|
||||
body: '<p>Hello</p>',
|
||||
showCc: false,
|
||||
showBcc: false,
|
||||
selectedIdentityId: 'id-me',
|
||||
subAddressTag: '',
|
||||
mode: 'compose' as const,
|
||||
draftId: null,
|
||||
};
|
||||
|
||||
const openOptions = () => fireEvent.click(screen.getByTestId('composer-options-menu'));
|
||||
const setComposeSendHandlers = (size: number) => {
|
||||
(emailHooks.onComposeSend as unknown as { size: number }).size = size;
|
||||
};
|
||||
|
||||
describe('composer encryption options', () => {
|
||||
beforeEach(() => {
|
||||
useAuthStore.setState({
|
||||
client: { supportsSubmissionExtension: (name: string) => name === 'REQUIRETLS', hasDelayedSend: () => false, getMaxDelayedSend: () => 0 } as never,
|
||||
});
|
||||
usePolicyStore.setState({ demoFeatureOverrides: { pluginsEnabled: true } } as never);
|
||||
setComposeSendHandlers(0);
|
||||
});
|
||||
afterEach(() => {
|
||||
useAuthStore.setState({ client: null });
|
||||
useSettingsStore.setState({ showAdvancedSettings: false } as never);
|
||||
usePolicyStore.setState({ demoFeatureOverrides: {} } as never);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('names the TLS option for what it does and keeps it to advanced settings', () => {
|
||||
const { unmount } = render(<EmailComposer initialData={DRAFT} />);
|
||||
openOptions();
|
||||
expect(screen.queryByTestId('composer-require-tls-toggle')).toBeNull();
|
||||
unmount();
|
||||
|
||||
useSettingsStore.setState({ showAdvancedSettings: true } as never);
|
||||
render(<EmailComposer initialData={DRAFT} />);
|
||||
openOptions();
|
||||
const item = screen.getByTestId('composer-require-tls-toggle');
|
||||
expect(item).toHaveTextContent('require_tls');
|
||||
expect(item).toHaveTextContent('require_tls_hint');
|
||||
expect(item).not.toHaveTextContent('option_require_tls');
|
||||
});
|
||||
|
||||
it('keeps a ticked TLS option visible so it can be unticked', () => {
|
||||
render(<EmailComposer initialData={{ ...DRAFT, requireTls: true }} />);
|
||||
openOptions();
|
||||
expect(screen.getByTestId('composer-require-tls-toggle')).toHaveAttribute('aria-checked', 'true');
|
||||
});
|
||||
|
||||
it('offers "Encrypt…" without a crypto plugin, opening the add-ons', () => {
|
||||
render(<EmailComposer initialData={DRAFT} />);
|
||||
openOptions();
|
||||
const item = screen.getByTestId('composer-encrypt-setup');
|
||||
expect(item).toHaveTextContent('encrypt');
|
||||
expect(item).toHaveTextContent('encrypt_hint');
|
||||
fireEvent.click(item);
|
||||
expect(push).toHaveBeenCalledWith('/settings/addons');
|
||||
});
|
||||
|
||||
it('leaves "Encrypt…" out once a crypto plugin takes over sending', () => {
|
||||
setComposeSendHandlers(1);
|
||||
render(<EmailComposer initialData={DRAFT} />);
|
||||
openOptions();
|
||||
expect(screen.queryByTestId('composer-encrypt-setup')).toBeNull();
|
||||
});
|
||||
|
||||
it('leaves "Encrypt…" out where add-ons are switched off', () => {
|
||||
usePolicyStore.setState({ demoFeatureOverrides: { pluginsEnabled: false } } as never);
|
||||
render(<EmailComposer initialData={DRAFT} />);
|
||||
openOptions();
|
||||
expect(screen.queryByTestId('composer-encrypt-setup')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -67,21 +67,28 @@ export function ComposerToolbarMenu({ icon: Icon, label, activeCount = 0, testId
|
||||
interface ItemProps {
|
||||
icon?: AppIcon;
|
||||
onSelect: () => void;
|
||||
/** A line under the label, as on the check items. */
|
||||
description?: string;
|
||||
/** Greyed out: the item can't do its job here yet, and says where to set it up. */
|
||||
muted?: boolean;
|
||||
testId?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ComposerToolbarMenuItem({ icon: Icon, onSelect, testId, children }: ItemProps) {
|
||||
export function ComposerToolbarMenuItem({ icon: Icon, onSelect, description, muted, testId, children }: ItemProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
onClick={onSelect}
|
||||
data-testid={testId}
|
||||
className={CONTEXT_MENU_ITEM_CLASS}
|
||||
className={cn(CONTEXT_MENU_ITEM_CLASS, description && "items-start", muted && "text-muted-foreground")}
|
||||
>
|
||||
{Icon ? <Icon className="w-4 h-4 shrink-0" /> : null}
|
||||
<span className="flex-1">{children}</span>
|
||||
{Icon ? <Icon className={cn("w-4 h-4 shrink-0", description && "mt-0.5")} /> : null}
|
||||
<span className="flex-1">
|
||||
<span className="block">{children}</span>
|
||||
{description && <span className="block text-xs text-muted-foreground">{description}</span>}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ import {
|
||||
Reply,
|
||||
Forward,
|
||||
BookUser,
|
||||
Lock,
|
||||
} from "@/components/icons";
|
||||
import { useRouter } from "@/i18n/navigation";
|
||||
import { ComposerPluginMenuItems, ComposerPluginToolbarButtons, ComposerToolbarMenu, ComposerToolbarMenuItem, ComposerToolbarMenuCheckItem } from "@/components/email/composer-toolbar-menu";
|
||||
import { ComposerToolbarMenuRadioItem } from "@/components/email/composer-toolbar-menu";
|
||||
import type { MessagePriority } from "@/lib/message-priority";
|
||||
@@ -603,6 +605,9 @@ export function EmailComposer({
|
||||
const tOffline = useTranslations('offline');
|
||||
const tOfflineNotes = useTranslations('offlineConflicts');
|
||||
const tSendGuards = useTranslations('send_guards');
|
||||
const tWords = useTranslations('compose_words');
|
||||
const router = useRouter();
|
||||
const showAdvancedSettings = useSettingsStore((state) => state.showAdvancedSettings);
|
||||
const timeFormat = useSettingsStore((state) => state.timeFormat);
|
||||
const plainTextModeDefault = useSettingsStore((state) => state.plainTextMode);
|
||||
// Per-message format (#1022). Seeded from the "plain text only" setting -
|
||||
@@ -5632,16 +5637,31 @@ export function EmailComposer({
|
||||
{t('option_dsn')}
|
||||
</ComposerToolbarMenuCheckItem>
|
||||
)}
|
||||
{composerClient?.supportsSubmissionExtension?.('REQUIRETLS') && (
|
||||
{/* REQUIRETLS only secures the way there, so it is an advanced
|
||||
option; one already ticked stays visible to untick. */}
|
||||
{composerClient?.supportsSubmissionExtension?.('REQUIRETLS') && (showAdvancedSettings || requireTls) && (
|
||||
<ComposerToolbarMenuCheckItem
|
||||
checked={requireTls}
|
||||
onToggle={() => setRequireTls(v => !v)}
|
||||
description={t('option_require_tls_hint')}
|
||||
description={tWords('require_tls_hint')}
|
||||
testId="composer-require-tls-toggle"
|
||||
>
|
||||
{t('option_require_tls')}
|
||||
{tWords('require_tls')}
|
||||
</ComposerToolbarMenuCheckItem>
|
||||
)}
|
||||
{/* Encrypting is a crypto plugin's job (S/MIME, PGP). Without
|
||||
one, say where to get it rather than offer nothing. */}
|
||||
{emailHooks.onComposeSend.size === 0 && isFeatureEnabled('pluginsEnabled') && !ownWindow && (
|
||||
<ComposerToolbarMenuItem
|
||||
icon={Lock}
|
||||
muted
|
||||
description={tWords('encrypt_hint')}
|
||||
onSelect={() => { close(); router.push('/settings/addons'); }}
|
||||
testId="composer-encrypt-setup"
|
||||
>
|
||||
{tWords('encrypt')}
|
||||
</ComposerToolbarMenuItem>
|
||||
)}
|
||||
<div role="separator" className="my-1 h-px bg-border" />
|
||||
<div role="group" aria-label={t('option_priority')} data-testid="composer-priority">
|
||||
<div className="px-3 pt-1 pb-0.5 text-xs font-medium text-muted-foreground" aria-hidden="true">{t('option_priority')}</div>
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "الرد من العنوان المستلَم إليه",
|
||||
"description": "الرد من العنوان الذي أُرسلت إليه الرسالة. مفيد إذا كانت لديك عدة عناوين."
|
||||
}
|
||||
},
|
||||
"require_tls": "التسليم عبر اتصال آمن فقط",
|
||||
"require_tls_hint": "الرسالة نفسها غير مشفّرة. تُرَد إذا لم يتمكن خادم من استخدام TLS.",
|
||||
"encrypt": "تشفير…",
|
||||
"encrypt_hint": "يتطلب إضافة S/MIME أو PGP. يفتح الإعدادات."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "المرشحات",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Respon des de l'adreça de recepció",
|
||||
"description": "Respon des de l'adreça a la qual es va enviar el correu. Útil si teniu diverses adreces."
|
||||
}
|
||||
},
|
||||
"require_tls": "Lliura només per una connexió segura",
|
||||
"require_tls_hint": "El missatge en si no està xifrat. Es retorna si un servidor no pot fer servir TLS.",
|
||||
"encrypt": "Xifra…",
|
||||
"encrypt_hint": "Cal un complement S/MIME o PGP. Obre la configuració."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtres",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Automaticky vybírat adresu pro odpověď",
|
||||
"description": "Odpovídat z adresy, na kterou zpráva přišla. Hodí se, pokud máte více adres."
|
||||
}
|
||||
},
|
||||
"require_tls": "Doručit jen přes zabezpečené spojení",
|
||||
"require_tls_hint": "Samotná zpráva šifrovaná není. Vrátí se, pokud server nemůže použít TLS.",
|
||||
"encrypt": "Šifrovat…",
|
||||
"encrypt_hint": "Vyžaduje doplněk S/MIME nebo PGP. Otevře Nastavení."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtry",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Svar fra modtaget adresse",
|
||||
"description": "Svar fra den adresse, mailen blev sendt til. Nyttigt, hvis du har flere adresser."
|
||||
}
|
||||
},
|
||||
"require_tls": "Lever kun via en sikker forbindelse",
|
||||
"require_tls_hint": "Selve beskeden er ikke krypteret. Den sendes retur, hvis en server ikke kan bruge TLS.",
|
||||
"encrypt": "Krypter…",
|
||||
"encrypt_hint": "Kræver en S/MIME- eller PGP-tilføjelse. Åbner Indstillinger."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtre",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Antwortadresse automatisch wählen",
|
||||
"description": "Die Antwort geht von der Adresse aus, an die die E-Mail ging. Nützlich, wenn Sie mehrere Adressen haben."
|
||||
}
|
||||
},
|
||||
"require_tls": "Nur über eine sichere Verbindung zustellen",
|
||||
"require_tls_hint": "Die Nachricht selbst wird nicht verschlüsselt. Sie kommt zurück, wenn ein Server kein TLS nutzen kann.",
|
||||
"encrypt": "Verschlüsseln…",
|
||||
"encrypt_hint": "Dafür brauchen Sie eine S/MIME- oder PGP-Erweiterung. Öffnet die Einstellungen."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filter",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Reply from the address it was sent to",
|
||||
"description": "Reply from the address the mail was sent to. Useful if you have several addresses."
|
||||
}
|
||||
},
|
||||
"require_tls": "Only deliver over a secure connection",
|
||||
"require_tls_hint": "The message itself isn't encrypted. It bounces if a server can't use TLS.",
|
||||
"encrypt": "Encrypt…",
|
||||
"encrypt_hint": "Needs an S/MIME or PGP add-on. Opens Settings."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filters",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Seleccionar dirección de respuesta automáticamente",
|
||||
"description": "Responder desde la dirección a la que se envió el correo. Útil si tiene varias direcciones."
|
||||
}
|
||||
},
|
||||
"require_tls": "Entregar solo por una conexión segura",
|
||||
"require_tls_hint": "El mensaje en sí no se cifra. Se devuelve si un servidor no puede usar TLS.",
|
||||
"encrypt": "Cifrar…",
|
||||
"encrypt_hint": "Necesita un complemento S/MIME o PGP. Abre la Configuración."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtros",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Vasta kirja saanud aadressilt",
|
||||
"description": "Vasta aadressilt, kuhu kiri saadeti. Kasulik, kui sul on mitu aadressi."
|
||||
}
|
||||
},
|
||||
"require_tls": "Toimeta kohale ainult turvalise ühenduse kaudu",
|
||||
"require_tls_hint": "Kiri ise ei ole krüpteeritud. See tuleb tagasi, kui server ei saa TLS-i kasutada.",
|
||||
"encrypt": "Krüpteeri…",
|
||||
"encrypt_hint": "Vajab S/MIME või PGP lisa. Avab seaded."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtrid",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "پاسخ از آدرس دریافتی",
|
||||
"description": "پاسخ از نشانیای که نامه به آن فرستاده شده بود. اگر چند نشانی دارید مفید است."
|
||||
}
|
||||
},
|
||||
"require_tls": "تحویل فقط از راه اتصال امن",
|
||||
"require_tls_hint": "خود پیام رمزگذاری نمیشود. اگر سروری نتواند از TLS استفاده کند، برگشت میخورد.",
|
||||
"encrypt": "رمزگذاری…",
|
||||
"encrypt_hint": "به افزونهٔ S/MIME یا PGP نیاز دارد. تنظیمات را باز میکند."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "فیلترها",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Sélection automatique de l'adresse de réponse",
|
||||
"description": "Répondre depuis l'adresse à laquelle le message a été envoyé. Utile si vous avez plusieurs adresses."
|
||||
}
|
||||
},
|
||||
"require_tls": "Livrer uniquement par une connexion sécurisée",
|
||||
"require_tls_hint": "Le message lui-même n'est pas chiffré. Il est renvoyé si un serveur ne peut pas utiliser TLS.",
|
||||
"encrypt": "Chiffrer…",
|
||||
"encrypt_hint": "Nécessite une extension S/MIME ou PGP. Ouvre les Paramètres."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtres",
|
||||
|
||||
@@ -6393,7 +6393,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "בחירה אוטומטית בכתובת תשובה",
|
||||
"description": "השב מהכתובת שאליה נשלח הדואר. שימושי אם יש לך כמה כתובות."
|
||||
}
|
||||
},
|
||||
"require_tls": "מסירה רק דרך חיבור מאובטח",
|
||||
"require_tls_hint": "ההודעה עצמה אינה מוצפנת. היא תוחזר אם שרת לא יכול להשתמש ב-TLS.",
|
||||
"encrypt": "הצפנה…",
|
||||
"encrypt_hint": "דורש תוסף S/MIME או PGP. פותח את ההגדרות."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "מסננים",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Válasz a fogadott címről",
|
||||
"description": "Válasz arról a címről, amelyre a levél érkezett. Hasznos, ha több címe van."
|
||||
}
|
||||
},
|
||||
"require_tls": "Kézbesítés csak biztonságos kapcsolaton át",
|
||||
"require_tls_hint": "Maga az üzenet nincs titkosítva. Visszapattan, ha egy szerver nem tud TLS-t használni.",
|
||||
"encrypt": "Titkosítás…",
|
||||
"encrypt_hint": "S/MIME vagy PGP bővítmény kell hozzá. Megnyitja a Beállításokat."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Szűrők",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Seleziona automaticamente l'indirizzo di risposta",
|
||||
"description": "Rispondi dall'indirizzo a cui è stata inviata l'email. Utile se hai più indirizzi."
|
||||
}
|
||||
},
|
||||
"require_tls": "Consegna solo tramite una connessione sicura",
|
||||
"require_tls_hint": "Il messaggio in sé non è cifrato. Viene respinto se un server non può usare TLS.",
|
||||
"encrypt": "Cifra…",
|
||||
"encrypt_hint": "Serve un componente aggiuntivo S/MIME o PGP. Apre le Impostazioni."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtri",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "返信元アドレスを自動選択",
|
||||
"description": "メールの宛先になったアドレスから返信します。複数のアドレスがある場合に便利です。"
|
||||
}
|
||||
},
|
||||
"require_tls": "安全な接続でのみ配信",
|
||||
"require_tls_hint": "メッセージ自体は暗号化されません。サーバーが TLS を使えない場合は返送されます。",
|
||||
"encrypt": "暗号化…",
|
||||
"encrypt_hint": "S/MIME または PGP のアドオンが必要です。設定を開きます。"
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "フィルター",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "답장 시 보내는 사람 자동 선택",
|
||||
"description": "메일을 받은 주소로 답장합니다. 주소가 여러 개일 때 유용합니다."
|
||||
}
|
||||
},
|
||||
"require_tls": "보안 연결로만 배달",
|
||||
"require_tls_hint": "메시지 자체는 암호화되지 않습니다. 서버가 TLS를 사용할 수 없으면 반송됩니다.",
|
||||
"encrypt": "암호화…",
|
||||
"encrypt_hint": "S/MIME 또는 PGP 추가 기능이 필요합니다. 설정을 엽니다."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "필터",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Automātiski izvēlēties atbildes adresi",
|
||||
"description": "Atbildēt no adreses, uz kuru vēstule tika nosūtīta. Noder, ja jums ir vairākas adreses."
|
||||
}
|
||||
},
|
||||
"require_tls": "Piegādāt tikai pa drošu savienojumu",
|
||||
"require_tls_hint": "Pats ziņojums nav šifrēts. Tas tiek atgriezts, ja serveris nevar izmantot TLS.",
|
||||
"encrypt": "Šifrēt…",
|
||||
"encrypt_hint": "Nepieciešams S/MIME vai PGP papildinājums. Atver iestatījumus."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtri",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Хүлээн авсан хаягаас хариу бичих",
|
||||
"description": "Захидал илгээгдсэн хаягаас хариу бичнэ. Олон хаягтай бол хэрэгтэй."
|
||||
}
|
||||
},
|
||||
"require_tls": "Зөвхөн аюулгүй холболтоор хүргэх",
|
||||
"require_tls_hint": "Мессеж өөрөө шифрлэгдэхгүй. Сервер TLS ашиглаж чадахгүй бол буцаагдана.",
|
||||
"encrypt": "Шифрлэх…",
|
||||
"encrypt_hint": "S/MIME эсвэл PGP нэмэлт хэрэгтэй. Тохиргоог нээнэ."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Шүүлтүүр",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Svar fra mottatt adresse",
|
||||
"description": "Svar fra adressen e-posten ble sendt til. Nyttig hvis du har flere adresser."
|
||||
}
|
||||
},
|
||||
"require_tls": "Lever bare over en sikker tilkobling",
|
||||
"require_tls_hint": "Selve meldingen er ikke kryptert. Den sendes i retur hvis en server ikke kan bruke TLS.",
|
||||
"encrypt": "Krypter…",
|
||||
"encrypt_hint": "Krever et S/MIME- eller PGP-tillegg. Åpner Innstillinger."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtre",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Antwoordadres automatisch selecteren",
|
||||
"description": "Antwoord vanaf het adres waar de mail naartoe is gestuurd. Handig als je meerdere adressen hebt."
|
||||
}
|
||||
},
|
||||
"require_tls": "Alleen via een beveiligde verbinding bezorgen",
|
||||
"require_tls_hint": "Het bericht zelf is niet versleuteld. Het komt terug als een server geen TLS kan gebruiken.",
|
||||
"encrypt": "Versleutelen…",
|
||||
"encrypt_hint": "Vereist een S/MIME- of PGP-add-on. Opent Instellingen."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filters",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Automatycznie wybieraj adres odpowiedzi",
|
||||
"description": "Odpowiadaj z adresu, na który wysłano wiadomość. Przydatne, jeśli masz kilka adresów."
|
||||
}
|
||||
},
|
||||
"require_tls": "Doręczaj tylko przez bezpieczne połączenie",
|
||||
"require_tls_hint": "Sama wiadomość nie jest szyfrowana. Zostanie zwrócona, jeśli serwer nie może użyć TLS.",
|
||||
"encrypt": "Zaszyfruj…",
|
||||
"encrypt_hint": "Wymaga dodatku S/MIME lub PGP. Otwiera Ustawienia."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtry",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Selecionar automaticamente o endereço de resposta",
|
||||
"description": "Responder do endereço para o qual o e-mail foi enviado. Útil se você tiver vários endereços."
|
||||
}
|
||||
},
|
||||
"require_tls": "Entregar só por uma conexão segura",
|
||||
"require_tls_hint": "A mensagem em si não é cifrada. Ela volta se um servidor não puder usar TLS.",
|
||||
"encrypt": "Cifrar…",
|
||||
"encrypt_hint": "Precisa de um extra S/MIME ou PGP. Abre as Configurações."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtros",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Răspunde de la adresa de la care a fost primit mesajul",
|
||||
"description": "Răspunde de la adresa la care a fost trimis e-mailul. Util dacă ai mai multe adrese."
|
||||
}
|
||||
},
|
||||
"require_tls": "Livrează doar printr-o conexiune sigură",
|
||||
"require_tls_hint": "Mesajul în sine nu este criptat. Este returnat dacă un server nu poate folosi TLS.",
|
||||
"encrypt": "Criptează…",
|
||||
"encrypt_hint": "Necesită un supliment S/MIME sau PGP. Deschide Setările."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtre",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Автоматически выбирать адрес для ответа",
|
||||
"description": "Отвечать с адреса, на который пришло письмо. Удобно, если у вас несколько адресов."
|
||||
}
|
||||
},
|
||||
"require_tls": "Доставлять только по защищённому соединению",
|
||||
"require_tls_hint": "Само письмо не шифруется. Оно вернётся, если сервер не может использовать TLS.",
|
||||
"encrypt": "Зашифровать…",
|
||||
"encrypt_hint": "Нужно дополнение S/MIME или PGP. Открывает настройки."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Фильтры",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Odpovedať z prijatej adresy",
|
||||
"description": "Odpovedať z adresy, na ktorú prišla správa. Hodí sa, ak máte viac adries."
|
||||
}
|
||||
},
|
||||
"require_tls": "Doručiť len cez zabezpečené spojenie",
|
||||
"require_tls_hint": "Samotná správa nie je šifrovaná. Vráti sa, ak server nemôže použiť TLS.",
|
||||
"encrypt": "Šifrovať…",
|
||||
"encrypt_hint": "Vyžaduje doplnok S/MIME alebo PGP. Otvorí Nastavenia."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtre",
|
||||
|
||||
@@ -3313,7 +3313,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "ตอบกลับจากที่อยู่ที่ได้รับ",
|
||||
"description": "ตอบกลับจากที่อยู่ที่อีเมลถูกส่งมา มีประโยชน์หากคุณมีหลายที่อยู่"
|
||||
}
|
||||
},
|
||||
"require_tls": "ส่งผ่านการเชื่อมต่อที่ปลอดภัยเท่านั้น",
|
||||
"require_tls_hint": "ตัวข้อความเองไม่ได้ถูกเข้ารหัส ข้อความจะถูกตีกลับหากเซิร์ฟเวอร์ใช้ TLS ไม่ได้",
|
||||
"encrypt": "เข้ารหัส…",
|
||||
"encrypt_hint": "ต้องใช้ส่วนเสริม S/MIME หรือ PGP จะเปิดการตั้งค่า"
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "ตัวกรอง",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Yanıt Adresini Otomatik Seç",
|
||||
"description": "Postanın gönderildiği adresten yanıtlayın. Birden çok adresiniz varsa kullanışlıdır."
|
||||
}
|
||||
},
|
||||
"require_tls": "Yalnızca güvenli bağlantı üzerinden teslim et",
|
||||
"require_tls_hint": "İletinin kendisi şifrelenmez. Bir sunucu TLS kullanamazsa geri döner.",
|
||||
"encrypt": "Şifrele…",
|
||||
"encrypt_hint": "S/MIME veya PGP eklentisi gerekir. Ayarlar'ı açar."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Filtreler",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Автоматичний вибір адреси для відповіді",
|
||||
"description": "Відповідати з адреси, на яку надійшов лист. Зручно, якщо у вас кілька адрес."
|
||||
}
|
||||
},
|
||||
"require_tls": "Доставляти лише захищеним з'єднанням",
|
||||
"require_tls_hint": "Сам лист не шифрується. Він повернеться, якщо сервер не може використати TLS.",
|
||||
"encrypt": "Зашифрувати…",
|
||||
"encrypt_hint": "Потрібен додаток S/MIME або PGP. Відкриває налаштування."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Фільтри",
|
||||
|
||||
@@ -6404,7 +6404,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "Trả lời bằng địa chỉ nhận thư",
|
||||
"description": "Trả lời từ địa chỉ mà thư đã được gửi đến. Hữu ích nếu bạn có nhiều địa chỉ."
|
||||
}
|
||||
},
|
||||
"require_tls": "Chỉ gửi qua kết nối an toàn",
|
||||
"require_tls_hint": "Bản thân thư không được mã hóa. Thư sẽ bị trả lại nếu máy chủ không dùng được TLS.",
|
||||
"encrypt": "Mã hóa…",
|
||||
"encrypt_hint": "Cần tiện ích S/MIME hoặc PGP. Mở Cài đặt."
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "Bộ lọc",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "使用收件地址回覆",
|
||||
"description": "從郵件寄達的地址回覆。適合擁有多個地址時使用。"
|
||||
}
|
||||
},
|
||||
"require_tls": "僅透過安全連線投遞",
|
||||
"require_tls_hint": "郵件本身不會加密。如果伺服器無法使用 TLS,郵件會被退回。",
|
||||
"encrypt": "加密…",
|
||||
"encrypt_hint": "需要 S/MIME 或 PGP 附加元件。將開啟設定。"
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "篩選",
|
||||
|
||||
@@ -6392,7 +6392,11 @@
|
||||
"reply_from_received": {
|
||||
"label": "自动选择回复地址",
|
||||
"description": "从邮件发往的地址回复。适合拥有多个地址的情况。"
|
||||
}
|
||||
},
|
||||
"require_tls": "仅通过安全连接投递",
|
||||
"require_tls_hint": "邮件本身不会加密。如果服务器无法使用 TLS,邮件会被退回。",
|
||||
"encrypt": "加密…",
|
||||
"encrypt_hint": "需要 S/MIME 或 PGP 附加组件。将打开设置。"
|
||||
},
|
||||
"plugin_list": {
|
||||
"filters": "筛选",
|
||||
|
||||
Reference in New Issue
Block a user