fix(email): explicit TLS mode for non-standard relay ports
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 17s
CI / ui (pull_request) Successful in 1m4s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m54s

The 465-only implicit-TLS heuristic mis-classified Selectel's SSL port (1127),
which the mailer would have dialled with STARTTLS and failed. Add BACKEND_SMTP_TLS
(ssl|starttls) — empty still derives the mode from the port (implicit on 465, else
STARTTLS) — and dial implicit TLS with WithSSL()+WithPort so any port works, not
just 465. Wire SMTP_RELAY_TLS through compose/ci/prod/.env.example and document it
(Selectel: 1127 = SSL, 1126 = STARTTLS). Unit-tested.
This commit is contained in:
Ilia Denisov
2026-07-03 03:44:55 +02:00
parent 01d02fcef6
commit c702f1bdac
10 changed files with 88 additions and 25 deletions
+8 -4
View File
@@ -30,12 +30,16 @@ EXPORT_SIGN_KEY=dev-export-sign-key
# Confirm-code email via the shared Selectel relay (one relay for every contour;
# limit 100 msgs / 5 min). Empty SMTP_RELAY_HOST makes the backend log codes instead
# of sending (dev). Port 465 = implicit TLS, else STARTTLS; no client cert is needed.
# FROM must use the prod domain (Selectel only accepts the verified sender domain), so
# it is the same on every contour. PUBLIC_BASE_URL is the canonical https origin for
# links in the email — the contour's own public URL. Gitea TEST_/PROD_SMTP_RELAY_* +
# TEST_/PROD_PUBLIC_BASE_URL.
# TLS is implicit on port 465 and STARTTLS otherwise, unless SMTP_RELAY_TLS forces it
# (ssl|starttls) — required for Selectel's non-standard ports (1127 = SSL, 1126 =
# STARTTLS). FROM must use the prod domain (Selectel only accepts the verified sender
# domain), so it is the same on every contour; a display name is fine
# (`"Игра Эрудит" <no-reply@erudit-game.ru>`). PUBLIC_BASE_URL is the canonical https
# origin for links in the email — the contour's own public URL. Gitea
# TEST_/PROD_SMTP_RELAY_* + TEST_/PROD_PUBLIC_BASE_URL.
SMTP_RELAY_HOST=
SMTP_RELAY_PORT=465
SMTP_RELAY_TLS= # empty = auto by port; ssl | starttls to force
SMTP_RELAY_USER= # secret
SMTP_RELAY_PASS= # secret
SMTP_RELAY_FROM=no-reply@erudit-game.ru
+3 -2
View File
@@ -97,7 +97,8 @@ without it Docker's resolver handles `otelcol`, `gateway` and `api.telegram.org`
| `VITE_VK_APP_LINK` | variable | _(empty)_ | UI build-arg: the landing "Play on VK" link, the VK Mini App (full URL, `https://vk.com/app<id>`). |
| `VITE_GATEWAY_URL` | variable | _(empty)_ | UI build-arg: gateway origin; empty = same-origin (the usual single-origin deploy). |
| `SMTP_RELAY_HOST` | variable | _(empty)_ | Selectel SMTP relay host for confirm-code email. Empty leaves the backend on the log mailer (email disabled) — the contour still boots. One relay for every contour (limit 100 msgs / 5 min). |
| `SMTP_RELAY_PORT` | variable | `465` | Relay port. `465` selects implicit TLS; any other port uses STARTTLS. No client certificate is needed (the server cert is validated against the system roots). |
| `SMTP_RELAY_PORT` | variable | `465` | Relay port. No client certificate is needed (the server cert is validated against the system roots). |
| `SMTP_RELAY_TLS` | variable | _(empty)_ | Transport security: `ssl` (implicit TLS) or `starttls`. Empty derives it from the port (implicit on `465`, STARTTLS otherwise); required for a relay on a non-standard port (e.g. Selectel's `1127` = SSL, `1126` = STARTTLS). |
| `SMTP_RELAY_USER` | secret | _(empty)_ | Relay SMTP AUTH username. |
| `SMTP_RELAY_PASS` | secret | _(empty)_ | Relay SMTP AUTH password. |
| `SMTP_RELAY_FROM` | variable | `no-reply@localhost` | Sender address. **Must use the prod domain** (`no-reply@erudit-game.ru`) — Selectel only accepts the verified sender domain — so it is the same on every contour. |
@@ -210,7 +211,7 @@ SMTP_RELAY_USER, SMTP_RELAY_PASS}`; variables:
GRAFANA_ROOT_URL, LOG_LEVEL, DICT_VERSION, TELEGRAM_MINIAPP_URL, TELEGRAM_GAME_CHANNEL_ID,
TELEGRAM_CHAT_ID, TELEGRAM_BOT_USERNAME, VITE_TELEGRAM_BOT_ID, VITE_TELEGRAM_LINK,
VITE_TELEGRAM_GAME_CHANNEL_NAME, VITE_VK_APP_LINK,
SMTP_RELAY_HOST, SMTP_RELAY_PORT, SMTP_RELAY_FROM, PUBLIC_BASE_URL}`.
SMTP_RELAY_HOST, SMTP_RELAY_PORT, SMTP_RELAY_TLS, SMTP_RELAY_FROM, PUBLIC_BASE_URL}`.
## Host-side setup (outside this repo)
+8 -5
View File
@@ -137,13 +137,16 @@ services:
# cgroup quota (the runtime otherwise sees all of the host's cores).
GOMAXPROCS: "2"
# Transactional email (confirm-codes) via the shared Selectel relay. An empty
# host makes the backend log codes instead of sending them (dev default). Port
# 465 selects implicit TLS; any other port uses STARTTLS. The From uses the prod
# domain (Selectel only accepts the verified sender domain), so it is the same on
# every contour. PUBLIC_BASE_URL is the canonical origin for links in the email
# (never a request Host header) — required whenever the relay host is set.
# host makes the backend log codes instead of sending them (dev default). TLS is
# implicit on port 465 and STARTTLS otherwise, unless SMTP_RELAY_TLS forces it
# (ssl|starttls) — needed for Selectel's non-standard ports (1127 = SSL, 1126 =
# STARTTLS). The From uses the prod domain (Selectel only accepts the verified
# sender domain), so it is the same on every contour. PUBLIC_BASE_URL is the
# canonical origin for links in the email (never a request Host header) —
# required whenever the relay host is set.
BACKEND_SMTP_HOST: ${SMTP_RELAY_HOST:-}
BACKEND_SMTP_PORT: ${SMTP_RELAY_PORT:-465}
BACKEND_SMTP_TLS: ${SMTP_RELAY_TLS:-}
BACKEND_SMTP_USERNAME: ${SMTP_RELAY_USER:-}
BACKEND_SMTP_PASSWORD: ${SMTP_RELAY_PASS:-}
BACKEND_SMTP_FROM: ${SMTP_RELAY_FROM:-no-reply@localhost}