fix(api): 未配置 SMTP 时发码门统一 503 mail_unavailable 且不再把验证码写日志,非本机中继默认强制 STARTTLS(require_tls)

This commit is contained in:
Lemon-miaow committed 2026-09-25 17:25:05 +08:00
1 parent d93c1b6913
commit 7d82402c18
27 files changed
+529 -87

No files matched your search

+5 -6
View File
@@ -116,12 +116,11 @@ worth revisiting.
## Wired since the marker was written
- `internal/api/handlers_email_otp.go:54,223,227` and `internal/api/api.go:84` —
SMTP shipped on
2026-07-20 (`internal/mail`, wired at `cmd/felis/api.go:264`). The nil-`Mailer`
branch that logs the code server-side is a runtime fallback for an install with no
`[smtp]` section, not an unbuilt feature. The comments are accurate; the reading
"Felis cannot send mail" is not.
- `internal/api/handlers_email_otp.go` and `internal/api/api.go` — SMTP shipped on
2026-07-20 (`internal/mail`, wired in `cmd/felis/api.go`). An install with no
`[smtp]` section leaves the `Mailer` nil, and every door that mails a code answers
503 `mail_unavailable`; codes are never logged. The reading "Felis cannot send
mail" is stale.
- `internal/config/config.go:117` — was stale. It described the upload transport as a
deferred integration after both backends had shipped (`LocalContextStore`,
`S3ContextStore`, selected in `cmd/felis/api.go` by the shape of the configured
+24 -2
View File
@@ -192,6 +192,15 @@ components:
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
MailUnavailable:
description: >
This install has no [smtp] relay (code mail_unavailable), so no code was minted
or sent. The public doors answer it before resolving the address, so it is the
same for every address. Sign in with a passkey, or have the operator configure
email with felis setup.
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
RateLimited:
description: >
This client address called the public sign-in doors faster than the per-address
@@ -2162,7 +2171,10 @@ paths:
'200':
description: >-
The login methods available for the address, in a deterministic order
(passkey before email_otp). An empty array means no verified account.
(passkey before email_otp). email_otp is offered only when the install has
a mail relay, passkey only when a verifier is wired and the account has a
credential. An empty array means no verified account, or none of its methods
is available on this install.
content:
application/json:
schema:
@@ -2546,6 +2558,8 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'502':
$ref: '#/components/responses/MailUndeliverable'
'503':
$ref: '#/components/responses/MailUnavailable'
/api/v1/auth/email/verify:
post:
@@ -2673,6 +2687,8 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'502':
$ref: '#/components/responses/MailUndeliverable'
'503':
$ref: '#/components/responses/MailUnavailable'
/api/v1/auth/op-login/status/{id}:
get:
@@ -4113,7 +4129,7 @@ paths:
email: { type: string, format: email }
responses:
'202':
description: Code minted and dispatched (or logged server-side when no mailer is wired).
description: Code minted and mailed.
content:
application/json:
schema:
@@ -4142,6 +4158,8 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'502':
$ref: '#/components/responses/MailUndeliverable'
'503':
$ref: '#/components/responses/MailUnavailable'
/api/v1/account/email/verify:
post:
@@ -4540,6 +4558,8 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'502':
$ref: '#/components/responses/MailUndeliverable'
'503':
$ref: '#/components/responses/MailUnavailable'
/api/v1/account/reauth/email/verify:
post:
@@ -4757,6 +4777,8 @@ paths:
schema: { $ref: '#/components/schemas/Error' }
'502':
$ref: '#/components/responses/MailUndeliverable'
'503':
$ref: '#/components/responses/MailUnavailable'
/api/v1/account/migrate/confirm/otp/verify:
post:
+30 -2
View File
@@ -1947,11 +1947,12 @@ Skips the pre-migration snapshot (`migrate up -no-backup`). The installer warns
loudly when it is set. Use it only when the snapshot cannot work and you have
another backup, e.g. an external database newer than the host's `pg_dump`.
## 17. Sign-in refused with 429, mail budget, account code locks, failed sign-ins
## 17. Sign-in refused: 429 limits, no mail relay, account code locks, failed sign-ins
The public sign-in doors (`/api/v1/auth/*` except logout and the op-login
status poll) have three limits of their own. Each answers 429 with a
`Retry-After` header and a distinct error code.
`Retry-After` header and a distinct error code. The doors that mail a code
also answer 503 `mail_unavailable` on an install with no mail relay.
### `rate_limited`: one address called the doors too often
@@ -1991,6 +1992,33 @@ raise `max_per_hour` to what your relay allows.
(`felis_mail_total{result="failed"}`, 502 `mail_undeliverable` to the caller).
The relay's reason is in the `felis-api` log.
### `mail_unavailable`: no mail relay
With no `[smtp]` section every door that mails a code answers 503
`mail_unavailable` before minting one: email sign-in, op.console sign-in,
email verification, and the email step-up for sensitive changes and
migration. The public doors answer before looking up the address, so every
address gets the same reply. Sign-in is by passkey only, and a verified email
stops counting as a way into the account (it is no longer offered as a
re-verification factor). Codes are never logged: `felis api` says at start
`[smtp] not configured`. Run `felis setup` and configure email to open the
doors.
### Relay refused for lacking TLS
A relay on port 465 is spoken to over TLS from the first byte. On any other
port Felis upgrades with STARTTLS, and when the relay does not offer it the
send fails with `smtp: <host>:<port> does not offer STARTTLS` (502
`mail_undeliverable` to the caller, the full text in the `felis-api` log, and
the same error on the `felis setup` email screen). Without TLS anyone on the
path reads the codes, and anyone who can rewrite the conversation can strip
the STARTTLS offer, so this is the default for every relay except one on this
host (`localhost`, `127.0.0.0/8`, `::1`). Use port 465 or a relay that offers
STARTTLS. For a relay you reach over a link you trust, set
`require_tls = false` under `[smtp]` in `/etc/felis/felis.toml` and
`/etc/felis/felis.pod.toml`; installer re-runs and the setup email screen keep
it. `felis api` warns at start whenever codes may go out without TLS.
### `otp_account_locked`: ten wrong codes in 24 hours
Ten wrong email codes for one account within 24 hours, counted across every