fix(security): verify email TLS certs and guard settings-write routes

Two defense-in-depth fixes from an adversarial review of the Email/WebSocket
guided-config surface (#1544). Both harden pre-existing patterns that the new
email probe — the first whose target host is caller-controlled while reusing
stored credentials — made relevant.

Email TLS certificate verification (CERT_NONE -> verified):
- The implicit-SSL paths (IMAP4_SSL/SMTP_SSL) in the connection probe and the
  polling/send adapter used Python's default unverified context (CERT_NONE, no
  hostname check), so the mailbox password was sent to a server whose
  certificate was never checked; an active MITM could harvest it. All four
  call sites now build their context through a shared email_tls_context()
  that verifies the certificate and hostname against the system CA bundle,
  matching the STARTTLS path, which already did.
- New EmailConfig.verify_tls field (default True) is the documented opt-out
  for self-signed/internal-CA servers, surfaced in the guided Email setup with
  a localized label/help in all nine locales.

Settings-write CSRF guard:
- require_settings_write_auth was missing the cross-site browser guard its
  siblings (require_auth, require_event_stream_auth) enforce on unsafe
  methods. POST /channels/email/test merges stored credentials with a caller
  patch, so a cross-site page could steer the stored mailbox password to an
  attacker-chosen host. The guard now runs first on every settings-write
  route; same-origin Web UI calls and non-browser clients (no Origin) are
  unaffected.

Tests: 9 new email TLS regressions (helper + probe/adapter x verify_tls) and
5 new settings-write cross-site guard regressions; the email field-count
(29 -> 30) and i18n parity assertions were updated. Channel sweep 639 passed,
frontend 656 passed, test_readme_counts 108 passed; new/changed files are
black- and ruff-clean (email.py's 9 ruff errors are pre-existing import-block
debt, unchanged 9 -> 9).

Follow-up to #1519 (extends #1520, hardens #1544).

Signed-off-by: shadowinlife <shadowinlife@gmail.com>
This commit is contained in:
shadowinlife committed 2026-09-22 18:28:19 +08:00
1 parent d4861f36e3
commit 2242c412ca
20 files changed
+382 -10

No files matched your search

+21
View File
@@ -161,6 +161,27 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Fixed
- **The Email channel verifies TLS certificates by default** (a follow-up
to #1544). The implicit-SSL paths (`IMAP4_SSL` / `SMTP_SSL`) in both the
connection probe and the polling/send adapter used Python's default
unverified context (`CERT_NONE`, no hostname check), so the mailbox
password travelled to a server whose certificate was never checked — an
active network attacker between the operator and their mail provider could
harvest it. All four call sites now build their context through a shared
`email_tls_context()` that verifies the certificate and hostname against
the system CA bundle, matching the STARTTLS path, which already did. A new
`verify_tls` field (default `true`, surfaced in the guided Email setup in
all nine locales) is the documented opt-out for self-signed or internal-CA
servers.
- **Settings-write routes reject cross-site browser requests** (a follow-up
to #1544). `require_settings_write_auth` did not apply the cross-site guard
that its siblings `require_auth` and `require_event_stream_auth` enforce on
unsafe methods, so a malicious web page could attempt a CSRF write against
any settings route — including the new `POST /channels/email/test`, which
merges stored credentials with a caller-supplied patch and could be steered
to send the stored mailbox password to an attacker-chosen host. The guard
now runs first on every settings-write route; same-origin Web UI calls and
non-browser clients (CLI/curl, no `Origin`) are unaffected.
- **A backtest aborted when a funding debit left cash below zero** (#1542).
CompositeEngine and CryptoEngine subtract crypto funding from capital with
no floor, and the next open then fitted at no scale, not even an empty plan:
+3
View File
@@ -642,6 +642,9 @@ async def require_settings_write_auth(
cred: Optional[HTTPAuthorizationCredentials] = Security(_security),
) -> None:
"""Require explicit authorization before changing credential-routing settings."""
if request.method.upper() not in _SAFE_BROWSER_METHODS:
_reject_cross_site_browser_request(request)
api_key = _configured_api_key()
if api_key:
token = _auth_credential_from_header_or_query(cred, None, allow_query=False)
+1
View File
@@ -133,6 +133,7 @@ def _email_hints() -> list[FieldHint]:
("smtp_password", "password", True, True),
("smtp_use_tls", "bool", False, False),
("smtp_use_ssl", "bool", False, False),
("verify_tls", "bool", False, False),
("from_address", "text", False, False),
("auto_reply_enabled", "bool", False, False),
("poll_interval_seconds", "text", False, False),
+10 -2
View File
@@ -28,7 +28,7 @@ from src.channels.bus.queue import MessageBus
from src.channels.base import BaseChannel
from src.channels.utils import get_media_dir
from pydantic import BaseModel
from src.channels.utils import safe_filename, send_imap_id
from src.channels.utils import email_tls_context, safe_filename, send_imap_id
class EmailConfig(BaseModel):
@@ -50,6 +50,9 @@ class EmailConfig(BaseModel):
smtp_password: str = ""
smtp_use_tls: bool = True
smtp_use_ssl: bool = False
# TLS certificate verification for implicit-SSL (IMAP4_SSL/SMTP_SSL)
# connections; set False only for self-signed / internal-CA mail servers.
verify_tls: bool = True
from_address: str = ""
auto_reply_enabled: bool = True
@@ -355,6 +358,7 @@ class EmailChannel(BaseChannel):
self.config.smtp_host,
self.config.smtp_port,
timeout=timeout,
ssl_context=email_tls_context(self.config.verify_tls),
) as smtp:
smtp.login(self.config.smtp_username, self.config.smtp_password)
smtp.send_message(msg)
@@ -574,7 +578,11 @@ class EmailChannel(BaseChannel):
def _open_imap_client(self, mailbox: str, *, missing_mailbox_ok: bool = False) -> Any | None:
if self.config.imap_use_ssl:
client: Any = imaplib.IMAP4_SSL(self.config.imap_host, self.config.imap_port)
client: Any = imaplib.IMAP4_SSL(
self.config.imap_host,
self.config.imap_port,
ssl_context=email_tls_context(self.config.verify_tls),
)
else:
client = imaplib.IMAP4(self.config.imap_host, self.config.imap_port)
+9 -3
View File
@@ -35,7 +35,7 @@ from collections.abc import Iterable
from typing import TYPE_CHECKING, Any
from src.channels.token_probe import scrub_secrets
from src.channels.utils import send_imap_id
from src.channels.utils import email_tls_context, send_imap_id
if TYPE_CHECKING:
from src.channels.email import EmailConfig
@@ -84,7 +84,10 @@ def _probe_imap(config: EmailConfig) -> tuple[str, str] | None:
try:
if config.imap_use_ssl:
client = imaplib.IMAP4_SSL(
config.imap_host, config.imap_port, timeout=_CONNECT_TIMEOUT_S
config.imap_host,
config.imap_port,
timeout=_CONNECT_TIMEOUT_S,
ssl_context=email_tls_context(config.verify_tls),
)
else:
client = imaplib.IMAP4(
@@ -127,7 +130,10 @@ def _probe_smtp(config: EmailConfig) -> tuple[str, str] | None:
try:
if config.smtp_use_ssl:
client = smtplib.SMTP_SSL(
config.smtp_host, config.smtp_port, timeout=_CONNECT_TIMEOUT_S
config.smtp_host,
config.smtp_port,
timeout=_CONNECT_TIMEOUT_S,
ssl_context=email_tls_context(config.verify_tls),
)
else:
client = smtplib.SMTP(
+20
View File
@@ -6,6 +6,7 @@ import ipaddress
import logging
import re
import socket
import ssl
from pathlib import Path
from typing import Any
from urllib.parse import urlparse
@@ -123,6 +124,25 @@ def send_imap_id(client: Any) -> None:
logger.debug("IMAP ID command failed (non-fatal)", exc_info=True)
def email_tls_context(verify: bool) -> ssl.SSLContext:
"""Return a TLS context for implicit-SSL (IMAP4_SSL/SMTP_SSL) email connections.
``verify=True`` (the default) verifies the server certificate and hostname
against the system CA bundle, so a credential is never sent to an
unverified server. ``verify=False`` disables verification — an explicit,
documented opt-out for self-signed or internal-CA mail servers that trades
MITM protection for connectivity. Mirrors the STARTTLS path, which already
uses :func:`ssl.create_default_context`.
"""
if verify:
return ssl.create_default_context()
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
# check_hostname must be cleared before CERT_NONE or Python raises ValueError.
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
def validate_url_target(url: str, *, allow_loopback: bool = False) -> tuple[bool, str]:
"""Validate a URL is safe to fetch: scheme, hostname, and resolved IPs.
+2 -1
View File
@@ -189,6 +189,7 @@ _EMAIL_HINT_KEYS = (
"smtp_password",
"smtp_use_tls",
"smtp_use_ssl",
"verify_tls",
"from_address",
"auto_reply_enabled",
"poll_interval_seconds",
@@ -233,7 +234,7 @@ def test_email_field_hints_contract() -> None:
keys = tuple(hint["key"] for hint in hints)
assert keys == _EMAIL_HINT_KEYS
assert "enabled" not in keys
assert len(hints) == 29
assert len(hints) == 30
by_key = {hint["key"]: hint for hint in hints}
assert {key for key, hint in by_key.items() if hint["secret"]} == {
+161 -4
View File
@@ -18,13 +18,15 @@ import json
import logging
import smtplib
import socket
import ssl
from email.message import EmailMessage
from typing import Any
import pytest
from src.channels.bus.queue import MessageBus
from src.channels.email import EmailChannel
from src.channels.utils import send_imap_id
from src.channels.utils import email_tls_context, send_imap_id
IMAP_HOST = "imap.example.com"
SMTP_HOST = "smtp.example.com"
@@ -76,6 +78,7 @@ def _install_imap_fakes(
"""Replace ``imaplib.IMAP4``/``IMAP4_SSL`` with recording fakes."""
calls: dict[str, Any] = {
"constructed": [],
"ssl_contexts": [],
"logins": [],
"ids": [],
"selects": [],
@@ -85,8 +88,15 @@ def _install_imap_fakes(
class FakeIMAP4:
kind = "IMAP4"
def __init__(self, host: str, port: int, timeout: float | None = None) -> None:
def __init__(
self,
host: str,
port: int,
timeout: float | None = None,
ssl_context: Any = None,
) -> None:
calls["constructed"].append((self.kind, host, port, timeout))
calls["ssl_contexts"].append(ssl_context)
if connect_error is not None:
raise connect_error
@@ -128,19 +138,38 @@ def _install_smtp_fakes(
"""Replace ``smtplib.SMTP``/``SMTP_SSL`` with recording fakes."""
calls: dict[str, Any] = {
"constructed": [],
"ssl_contexts": [],
"starttls": [],
"logins": [],
"sent": [],
"quits": 0,
}
class FakeSMTP:
kind = "SMTP"
def __init__(self, host: str, port: int, timeout: float | None = None) -> None:
def __init__(
self,
host: str,
port: int,
timeout: float | None = None,
ssl_context: Any = None,
) -> None:
calls["constructed"].append((self.kind, host, port, timeout))
calls["ssl_contexts"].append(ssl_context)
if connect_error is not None:
raise connect_error
def __enter__(self) -> "FakeSMTP":
return self
def __exit__(self, *exc_info: Any) -> None:
return None
def send_message(self, msg: Any) -> dict[Any, Any]:
calls["sent"].append(msg)
return {}
def starttls(self, context: Any = None) -> tuple[int, bytes]:
calls["starttls"].append(context is not None)
if starttls_error is not None:
@@ -416,6 +445,128 @@ def test_smtp_ssl_transport_skips_starttls(monkeypatch: pytest.MonkeyPatch) -> N
assert smtp_calls["starttls"] == []
# --------------------------------------------------------------------------- #
# TLS certificate verification (verify_tls)
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("verify", [True, False])
def test_email_tls_context_verify_modes(verify: bool) -> None:
ctx = email_tls_context(verify)
if verify:
assert ctx.verify_mode == ssl.CERT_REQUIRED
assert ctx.check_hostname is True
else:
assert ctx.verify_mode == ssl.CERT_NONE
assert ctx.check_hostname is False
@pytest.mark.parametrize("verify_tls", [True, False])
def test_probe_imap_ssl_context_follows_verify_tls(
monkeypatch: pytest.MonkeyPatch, verify_tls: bool
) -> None:
"""The probe must never send the IMAP password over an unverified socket
unless the operator explicitly opted out with ``verify_tls=False``."""
imap_calls = _install_imap_fakes(monkeypatch)
_install_smtp_fakes(monkeypatch)
result = _run(_make_channel(verify_tls=verify_tls).test_connection())
assert result["ok"] is True
ctx = imap_calls["ssl_contexts"][0]
assert isinstance(ctx, ssl.SSLContext)
if verify_tls:
assert ctx.verify_mode == ssl.CERT_REQUIRED
assert ctx.check_hostname is True
else:
assert ctx.verify_mode == ssl.CERT_NONE
assert ctx.check_hostname is False
@pytest.mark.parametrize("verify_tls", [True, False])
def test_probe_smtp_ssl_context_follows_verify_tls(
monkeypatch: pytest.MonkeyPatch, verify_tls: bool
) -> None:
imap_calls = _install_imap_fakes(monkeypatch)
smtp_calls = _install_smtp_fakes(monkeypatch)
result = _run(
_make_channel(
smtp_use_ssl=True, smtp_port=465, verify_tls=verify_tls
).test_connection()
)
assert result["ok"] is True
ctx = smtp_calls["ssl_contexts"][0]
assert isinstance(ctx, ssl.SSLContext)
if verify_tls:
assert ctx.verify_mode == ssl.CERT_REQUIRED
assert ctx.check_hostname is True
else:
assert ctx.verify_mode == ssl.CERT_NONE
assert ctx.check_hostname is False
# The plain IMAP4_SSL probe half shares the same verify_tls switch.
assert isinstance(imap_calls["ssl_contexts"][0], ssl.SSLContext)
def test_probe_plain_transports_pass_no_ssl_context(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Non-SSL transports take no context; STARTTLS keeps its own verifying one."""
imap_calls = _install_imap_fakes(monkeypatch)
smtp_calls = _install_smtp_fakes(monkeypatch)
result = _run(
_make_channel(imap_use_ssl=False, smtp_use_ssl=False).test_connection()
)
assert result["ok"] is True
assert imap_calls["ssl_contexts"] == [None]
assert smtp_calls["ssl_contexts"] == [None]
assert smtp_calls["starttls"] == [True]
@pytest.mark.parametrize("verify_tls", [True, False])
def test_adapter_imap_ssl_context_follows_verify_tls(
monkeypatch: pytest.MonkeyPatch, verify_tls: bool
) -> None:
"""The adapter's polling connection uses the same helper as the probe."""
imap_calls = _install_imap_fakes(monkeypatch)
channel = _make_channel(verify_tls=verify_tls)
client = channel._open_imap_client("INBOX")
assert client is not None
ctx = imap_calls["ssl_contexts"][0]
expected = ssl.CERT_REQUIRED if verify_tls else ssl.CERT_NONE
assert ctx.verify_mode == expected
assert ctx.check_hostname is verify_tls
@pytest.mark.parametrize("verify_tls", [True, False])
def test_adapter_smtp_ssl_context_follows_verify_tls(
monkeypatch: pytest.MonkeyPatch, verify_tls: bool
) -> None:
smtp_calls = _install_smtp_fakes(monkeypatch)
channel = _make_channel(smtp_use_ssl=True, smtp_port=465, verify_tls=verify_tls)
channel._smtp_send(EmailMessage())
ctx = smtp_calls["ssl_contexts"][0]
expected = ssl.CERT_REQUIRED if verify_tls else ssl.CERT_NONE
assert ctx.verify_mode == expected
assert ctx.check_hostname is verify_tls
assert len(smtp_calls["sent"]) == 1
def test_verify_tls_defaults_to_true() -> None:
"""Secure by default: certificate verification is on unless opted out."""
channel = _make_channel()
assert channel.config.verify_tls is True
# --------------------------------------------------------------------------- #
# Secret hygiene
# --------------------------------------------------------------------------- #
@@ -576,7 +727,13 @@ def test_probe_sends_imap_id_before_select(monkeypatch: pytest.MonkeyPatch) -> N
order: list[str] = []
class FakeIMAP4_SSL:
def __init__(self, host: str, port: int, timeout: float | None = None) -> None:
def __init__(
self,
host: str,
port: int,
timeout: float | None = None,
ssl_context: Any = None,
) -> None:
order.append("connect")
def login(self, user: str, password: str) -> tuple[str, list[bytes]]:
+115
View File
@@ -308,6 +308,121 @@ def test_local_dev_can_write_llm_settings_when_api_key_unset(
assert "OPENAI_BASE_URL=https://api.openai.com/v1" in env_path.read_text(encoding="utf-8")
# ============================================================================
# PUT /settings/llm: cross-site browser guard on require_settings_write_auth
# ============================================================================
def test_settings_write_rejects_cross_site_sec_fetch_site(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
"""A cross-site browser PUT must not reach the settings-write logic."""
env_path = tmp_path / ".env"
monkeypatch.setattr(api_server, "ENV_PATH", env_path)
response = _local_client().put(
"/settings/llm",
headers={"Host": "127.0.0.1:8899", "Sec-Fetch-Site": "cross-site"},
json=_llm_settings_payload("https://attacker.example/openai-compatible/v1"),
)
assert response.status_code == 403
assert response.json()["detail"] == "Cross-site request denied"
assert not env_path.exists()
def test_settings_write_rejects_cross_site_origin(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
"""An Origin that is neither loopback nor the request host is cross-site."""
env_path = tmp_path / ".env"
monkeypatch.setattr(api_server, "ENV_PATH", env_path)
response = _local_client().put(
"/settings/llm",
headers={"Host": "127.0.0.1:8899", "Origin": "https://attacker.example"},
json=_llm_settings_payload("https://attacker.example/openai-compatible/v1"),
)
assert response.status_code == 403
assert response.json()["detail"] == "Cross-site request denied"
assert not env_path.exists()
def test_settings_write_same_origin_browser_passes_cross_site_guard(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
"""A same-origin browser PUT clears the guard and reaches the write path."""
env_path = tmp_path / ".env"
env_path.write_text("", encoding="utf-8")
monkeypatch.setattr(api_server, "ENV_PATH", env_path)
response = _local_client().put(
"/settings/llm",
headers={
"Host": "127.0.0.1:8899",
"Origin": "http://127.0.0.1:8899",
"Sec-Fetch-Site": "same-origin",
},
json=_llm_settings_payload("https://api.openai.com/v1"),
)
assert response.status_code == 200
assert "OPENAI_BASE_URL=https://api.openai.com/v1" in env_path.read_text(encoding="utf-8")
def test_settings_write_remote_same_origin_passes_guard_and_hits_auth_layer(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
"""A host-matching Origin passes the guard; the later auth layer still decides."""
env_path = tmp_path / ".env"
monkeypatch.setattr(api_server, "ENV_PATH", env_path)
client = TestClient(
api_server.app,
base_url="http://192.168.1.10:8899",
client=("192.168.1.20", 50000),
)
response = client.put(
"/settings/llm",
headers={
"Host": "192.168.1.10:8899",
"Origin": "http://192.168.1.10:8899",
"Sec-Fetch-Site": "same-origin",
},
json=_llm_settings_payload("https://api.openai.com/v1"),
)
assert response.status_code == 403
assert (
response.json()["detail"]
== "Settings writes require API_AUTH_KEY or a local loopback client"
)
assert not env_path.exists()
def test_settings_write_without_browser_headers_passes_guard(
monkeypatch: pytest.MonkeyPatch,
tmp_path,
) -> None:
"""A non-browser client (CLI/curl) sends no Origin and must pass the guard."""
env_path = tmp_path / ".env"
env_path.write_text("", encoding="utf-8")
monkeypatch.setattr(api_server, "ENV_PATH", env_path)
response = _local_client().put(
"/settings/llm",
json=_llm_settings_payload("https://api.openai.com/v1"),
)
assert response.status_code == 200
assert "OPENAI_BASE_URL=https://api.openai.com/v1" in env_path.read_text(encoding="utf-8")
def test_loopback_rejects_rebound_host_before_auth_bypass(
monkeypatch: pytest.MonkeyPatch,
) -> None:
@@ -228,6 +228,7 @@ export function ChannelConfigPanel({
"settings.channels.fields.email.smtp_password": { label: t("settings.channels.fields.email.smtp_password.label"), help: t("settings.channels.fields.email.smtp_password.help") },
"settings.channels.fields.email.smtp_use_tls": { label: t("settings.channels.fields.email.smtp_use_tls.label"), help: t("settings.channels.fields.email.smtp_use_tls.help") },
"settings.channels.fields.email.smtp_use_ssl": { label: t("settings.channels.fields.email.smtp_use_ssl.label"), help: t("settings.channels.fields.email.smtp_use_ssl.help") },
"settings.channels.fields.email.verify_tls": { label: t("settings.channels.fields.email.verify_tls.label"), help: t("settings.channels.fields.email.verify_tls.help") },
"settings.channels.fields.email.from_address": { label: t("settings.channels.fields.email.from_address.label"), help: t("settings.channels.fields.email.from_address.help") },
"settings.channels.fields.email.auto_reply_enabled": { label: t("settings.channels.fields.email.auto_reply_enabled.label"), help: t("settings.channels.fields.email.auto_reply_enabled.help") },
"settings.channels.fields.email.poll_interval_seconds": { label: t("settings.channels.fields.email.poll_interval_seconds.label"), help: t("settings.channels.fields.email.poll_interval_seconds.help") },
@@ -135,6 +135,7 @@ function emailEntry(overrides: Record<string, unknown> = {}) {
{ key: "smtp_password", type: "password", secret: true, required: true, help_key: "settings.channels.fields.email.smtp_password" },
{ key: "smtp_use_tls", type: "bool", secret: false, required: false, help_key: "settings.channels.fields.email.smtp_use_tls" },
{ key: "smtp_use_ssl", type: "bool", secret: false, required: false, help_key: "settings.channels.fields.email.smtp_use_ssl" },
{ key: "verify_tls", type: "bool", secret: false, required: false, help_key: "settings.channels.fields.email.verify_tls" },
{ key: "from_address", type: "text", secret: false, required: false, help_key: "settings.channels.fields.email.from_address" },
{ key: "auto_reply_enabled", type: "bool", secret: false, required: false, help_key: "settings.channels.fields.email.auto_reply_enabled" },
{ key: "poll_interval_seconds", type: "text", secret: false, required: false, help_key: "settings.channels.fields.email.poll_interval_seconds" },
@@ -165,6 +166,7 @@ function emailEntry(overrides: Record<string, unknown> = {}) {
smtp_username: "bot@example.com",
smtp_use_tls: true,
smtp_use_ssl: false,
verify_tls: true,
from_address: "",
auto_reply_enabled: true,
poll_interval_seconds: 30,
@@ -621,6 +623,7 @@ describe("ChannelSettings config panel", () => {
expect(screen.getByText("After processing")).toBeInTheDocument();
expect(screen.getByText("Allowed senders")).toBeInTheDocument();
expect(screen.getByText("Verify DKIM")).toBeInTheDocument();
expect(screen.getByText("Verify TLS certificate")).toBeInTheDocument();
expect(screen.getByText("Allowed attachment types")).toBeInTheDocument();
expect(screen.getByText("Max attachments per email")).toBeInTheDocument();
expect(screen.getByText(/Inbound mail server host, e\.g\. imap\.gmail\.com/)).toBeInTheDocument();
+4
View File
@@ -574,6 +574,10 @@
"label": "SMTP عبر SSL",
"help": "الاتصال بـ SMTP عبر TLS الضمني، مثل المنفذ 465 (معطّل افتراضيًا). لا يُستخدم عمليًا مع STARTTLS في الوقت نفسه."
},
"verify_tls": {
"label": "التحقق من شهادة TLS",
"help": "التحقق من شهادة TLS الخاصة بخادم البريد واسم المضيف (موصى به). لا تُعطّله إلا مع الخوادم ذاتية التوقيع أو التي تستخدم CA داخلي."
},
"from_address": {
"label": "عنوان المرسل",
"help": "العنوان الذي تُرسل منه الردود. إذا تُرك فارغًا يُستخدم اسم مستخدم SMTP."
+4
View File
@@ -579,6 +579,10 @@
"label": "SMTP-SSL",
"help": "SMTP über implizites TLS verbinden, z. B. Port 465 (standardmäßig aus). In der Praxis nicht mit STARTTLS kombinierbar."
},
"verify_tls": {
"label": "TLS-Zertifikat prüfen",
"help": "TLS-Zertifikat und Hostname des Mailservers prüfen (empfohlen). Nur für selbstsignierte oder interne CA-Server deaktivieren."
},
"from_address": {
"label": "Absenderadresse",
"help": "Adresse, von der Antworten versendet werden. Leer übernimmt den SMTP-Benutzernamen."
+4
View File
@@ -579,6 +579,10 @@
"label": "SMTP SSL",
"help": "Connect to SMTP over implicit TLS, e.g. port 465 (off by default). In practice this is mutually exclusive with STARTTLS."
},
"verify_tls": {
"label": "Verify TLS certificate",
"help": "Verify the mail server's TLS certificate and hostname (recommended). Disable only for self-signed or internal-CA servers."
},
"from_address": {
"label": "From address",
"help": "Address replies are sent from. Empty reuses the SMTP username."
+4
View File
@@ -579,6 +579,10 @@
"label": "SSL de SMTP",
"help": "Conectar a SMTP mediante TLS implícito, p. ej. el puerto 465 (desactivado de forma predeterminada). En la práctica es excluyente con STARTTLS."
},
"verify_tls": {
"label": "Verificar el certificado TLS",
"help": "Verificar el certificado TLS y el nombre de host del servidor de correo (recomendado). Desactivar solo para servidores con certificados autofirmados o de CA interna."
},
"from_address": {
"label": "Dirección de remitente",
"help": "Dirección desde la que se envían las respuestas. Si se deja vacía, se usa el usuario SMTP."
+4
View File
@@ -579,6 +579,10 @@
"label": "SSL SMTP",
"help": "Hubungkan ke SMTP melalui TLS implisit, misalnya port 465 (nonaktif secara default). Dalam praktik tidak dipakai bersama STARTTLS."
},
"verify_tls": {
"label": "Verifikasi sertifikat TLS",
"help": "Verifikasi sertifikat TLS dan nama host server email (disarankan). Nonaktifkan hanya untuk server dengan sertifikat self-signed atau CA internal."
},
"from_address": {
"label": "Alamat pengirim",
"help": "Alamat asal balasan dikirim. Jika kosong, memakai nama pengguna SMTP."
+4
View File
@@ -574,6 +574,10 @@
"label": "SMTP SSL",
"help": "暗黙的 TLS で SMTP に接続します(例: ポート 465、既定オフ)。実際には STARTTLS と併用しません。"
},
"verify_tls": {
"label": "TLS 証明書を検証",
"help": "メールサーバーの TLS 証明書とホスト名を検証します(推奨)。自己署名または社内 CA のサーバーの場合のみ無効にしてください。"
},
"from_address": {
"label": "送信元アドレス",
"help": "返信メールの送信元アドレス。空欄の場合は SMTP ユーザー名を使用します。"
+4
View File
@@ -574,6 +574,10 @@
"label": "SMTP SSL",
"help": "암시적 TLS로 SMTP에 연결합니다(예: 포트 465, 기본 꺼짐). 실제로는 STARTTLS와 함께 사용하지 않습니다."
},
"verify_tls": {
"label": "TLS 인증서 검증",
"help": "메일 서버의 TLS 인증서와 호스트 이름을 검증합니다(권장). 자체 서명 또는 내부 CA 서버인 경우에만 끄세요."
},
"from_address": {
"label": "보낸 사람 주소",
"help": "답장을 보낼 주소입니다. 비워 두면 SMTP 사용자 이름을 사용합니다."
+4
View File
@@ -579,6 +579,10 @@
"label": "SSL do SMTP",
"help": "Conectar ao SMTP via TLS implícito, por exemplo na porta 465 (desativado por padrão). Na prática, é exclusivo com o STARTTLS."
},
"verify_tls": {
"label": "Verificar o certificado TLS",
"help": "Verificar o certificado TLS e o nome de host do servidor de e-mail (recomendado). Desative apenas para servidores com certificado autoassinado ou CA interna."
},
"from_address": {
"label": "Endereço do remetente",
"help": "Endereço de onde as respostas são enviadas. Se ficar vazio, usa o usuário SMTP."
+4
View File
@@ -574,6 +574,10 @@
"label": "SMTP SSL",
"help": "通过隐式 TLS 连接 SMTP,例如 465 端口(默认关闭)。实际使用中与 STARTTLS 互斥。"
},
"verify_tls": {
"label": "校验 TLS 证书",
"help": "验证邮件服务器的 TLS 证书与主机名(推荐)。仅在使用自签名或内部 CA 服务器时关闭。"
},
"from_address": {
"label": "发件地址",
"help": "回复邮件的发件地址。留空则沿用 SMTP 用户名。"