fix: name the TLS-only delivery option plainly and point to encryption add-ons

This commit is contained in:
Linus Rath
2026-11-04 07:11:03 +01:00
parent 0c5ba654c6
commit 43c9ef9ad1
33 changed files with 464 additions and 37 deletions
@@ -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();
});
});
+11 -4
View File
@@ -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>
);
}
+23 -3
View File
@@ -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>
+5 -1
View File
@@ -6392,7 +6392,11 @@
"reply_from_received": {
"label": "الرد من العنوان المستلَم إليه",
"description": "الرد من العنوان الذي أُرسلت إليه الرسالة. مفيد إذا كانت لديك عدة عناوين."
}
},
"require_tls": "التسليم عبر اتصال آمن فقط",
"require_tls_hint": "الرسالة نفسها غير مشفّرة. تُرَد إذا لم يتمكن خادم من استخدام TLS.",
"encrypt": "تشفير…",
"encrypt_hint": "يتطلب إضافة S/MIME أو PGP. يفتح الإعدادات."
},
"plugin_list": {
"filters": "المرشحات",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -6392,7 +6392,11 @@
"reply_from_received": {
"label": "پاسخ از آدرس دریافتی",
"description": "پاسخ از نشانی‌ای که نامه به آن فرستاده شده بود. اگر چند نشانی دارید مفید است."
}
},
"require_tls": "تحویل فقط از راه اتصال امن",
"require_tls_hint": "خود پیام رمزگذاری نمی‌شود. اگر سروری نتواند از TLS استفاده کند، برگشت می‌خورد.",
"encrypt": "رمزگذاری…",
"encrypt_hint": "به افزونهٔ S/MIME یا PGP نیاز دارد. تنظیمات را باز می‌کند."
},
"plugin_list": {
"filters": "فیلترها",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -6393,7 +6393,11 @@
"reply_from_received": {
"label": "בחירה אוטומטית בכתובת תשובה",
"description": "השב מהכתובת שאליה נשלח הדואר. שימושי אם יש לך כמה כתובות."
}
},
"require_tls": "מסירה רק דרך חיבור מאובטח",
"require_tls_hint": "ההודעה עצמה אינה מוצפנת. היא תוחזר אם שרת לא יכול להשתמש ב-TLS.",
"encrypt": "הצפנה…",
"encrypt_hint": "דורש תוסף S/MIME או PGP. פותח את ההגדרות."
},
"plugin_list": {
"filters": "מסננים",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -6392,7 +6392,11 @@
"reply_from_received": {
"label": "返信元アドレスを自動選択",
"description": "メールの宛先になったアドレスから返信します。複数のアドレスがある場合に便利です。"
}
},
"require_tls": "安全な接続でのみ配信",
"require_tls_hint": "メッセージ自体は暗号化されません。サーバーが TLS を使えない場合は返送されます。",
"encrypt": "暗号化…",
"encrypt_hint": "S/MIME または PGP のアドオンが必要です。設定を開きます。"
},
"plugin_list": {
"filters": "フィルター",
+5 -1
View File
@@ -6392,7 +6392,11 @@
"reply_from_received": {
"label": "답장 시 보내는 사람 자동 선택",
"description": "메일을 받은 주소로 답장합니다. 주소가 여러 개일 때 유용합니다."
}
},
"require_tls": "보안 연결로만 배달",
"require_tls_hint": "메시지 자체는 암호화되지 않습니다. 서버가 TLS를 사용할 수 없으면 반송됩니다.",
"encrypt": "암호화…",
"encrypt_hint": "S/MIME 또는 PGP 추가 기능이 필요합니다. 설정을 엽니다."
},
"plugin_list": {
"filters": "필터",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -6392,7 +6392,11 @@
"reply_from_received": {
"label": "Хүлээн авсан хаягаас хариу бичих",
"description": "Захидал илгээгдсэн хаягаас хариу бичнэ. Олон хаягтай бол хэрэгтэй."
}
},
"require_tls": "Зөвхөн аюулгүй холболтоор хүргэх",
"require_tls_hint": "Мессеж өөрөө шифрлэгдэхгүй. Сервер TLS ашиглаж чадахгүй бол буцаагдана.",
"encrypt": "Шифрлэх…",
"encrypt_hint": "S/MIME эсвэл PGP нэмэлт хэрэгтэй. Тохиргоог нээнэ."
},
"plugin_list": {
"filters": "Шүүлтүүр",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -6392,7 +6392,11 @@
"reply_from_received": {
"label": "Автоматически выбирать адрес для ответа",
"description": "Отвечать с адреса, на который пришло письмо. Удобно, если у вас несколько адресов."
}
},
"require_tls": "Доставлять только по защищённому соединению",
"require_tls_hint": "Само письмо не шифруется. Оно вернётся, если сервер не может использовать TLS.",
"encrypt": "Зашифровать…",
"encrypt_hint": "Нужно дополнение S/MIME или PGP. Открывает настройки."
},
"plugin_list": {
"filters": "Фильтры",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -3313,7 +3313,11 @@
"reply_from_received": {
"label": "ตอบกลับจากที่อยู่ที่ได้รับ",
"description": "ตอบกลับจากที่อยู่ที่อีเมลถูกส่งมา มีประโยชน์หากคุณมีหลายที่อยู่"
}
},
"require_tls": "ส่งผ่านการเชื่อมต่อที่ปลอดภัยเท่านั้น",
"require_tls_hint": "ตัวข้อความเองไม่ได้ถูกเข้ารหัส ข้อความจะถูกตีกลับหากเซิร์ฟเวอร์ใช้ TLS ไม่ได้",
"encrypt": "เข้ารหัส…",
"encrypt_hint": "ต้องใช้ส่วนเสริม S/MIME หรือ PGP จะเปิดการตั้งค่า"
},
"plugin_list": {
"filters": "ตัวกรอง",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -6392,7 +6392,11 @@
"reply_from_received": {
"label": "Автоматичний вибір адреси для відповіді",
"description": "Відповідати з адреси, на яку надійшов лист. Зручно, якщо у вас кілька адрес."
}
},
"require_tls": "Доставляти лише захищеним з'єднанням",
"require_tls_hint": "Сам лист не шифрується. Він повернеться, якщо сервер не може використати TLS.",
"encrypt": "Зашифрувати…",
"encrypt_hint": "Потрібен додаток S/MIME або PGP. Відкриває налаштування."
},
"plugin_list": {
"filters": "Фільтри",
+5 -1
View File
@@ -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",
+5 -1
View File
@@ -6392,7 +6392,11 @@
"reply_from_received": {
"label": "使用收件地址回覆",
"description": "從郵件寄達的地址回覆。適合擁有多個地址時使用。"
}
},
"require_tls": "僅透過安全連線投遞",
"require_tls_hint": "郵件本身不會加密。如果伺服器無法使用 TLS,郵件會被退回。",
"encrypt": "加密…",
"encrypt_hint": "需要 S/MIME 或 PGP 附加元件。將開啟設定。"
},
"plugin_list": {
"filters": "篩選",
+5 -1
View File
@@ -6392,7 +6392,11 @@
"reply_from_received": {
"label": "自动选择回复地址",
"description": "从邮件发往的地址回复。适合拥有多个地址的情况。"
}
},
"require_tls": "仅通过安全连接投递",
"require_tls_hint": "邮件本身不会加密。如果服务器无法使用 TLS,邮件会被退回。",
"encrypt": "加密…",
"encrypt_hint": "需要 S/MIME 或 PGP 附加组件。将打开设置。"
},
"plugin_list": {
"filters": "筛选",