docs(admin): email is SMTP now, and the Gmail connect flow is gone

Engagement Phase 1's share of this repo (docs/website/ENGAGEMENT.md
§6.0b). Four pages described a delivery path that no longer exists — one
of them under the heading "There is no SMTP option", which is now the
opposite of true.

notifications-and-email.mdx: the Email section is rewritten around the
three postures the org lead settled on (§7.1 Q5), leading with a relay
and naming smtp.gmail.com:587 with an app password as the migration off
OAuth2. Two cautions carry the failures that produce no error at all —
Implicit TLS left on for port 587, which hangs, and an operator-typed
sender the relay will not accept, which is an SPF/DMARC rejection that
looks like nothing. Send test is what proves both.

troubleshooting.mdx gains those two, plus the enabled toggle, which now
gates every message rather than some of them.

configuration.mdx loses the "set up Google first" ordering constraint,
which is gone with the borrowed client. system-architecture.mdx's
encrypted-at-rest list is corrected: the Gmail refresh token is replaced
by the transport credentials, which are write-only like the sidecar
token.

This lands on `edge`, so nothing here is published while `main` still
carries the Gmail flow. platform.json and capabilities.mjs are untouched
— they belong to Phase 12.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-28 21:01:03 -05:00
parent 268e1c98fc
commit 2da014790e
4 changed files with 74 additions and 20 deletions

View File

@@ -59,9 +59,10 @@ per-Team settings:
## Email
Configured on the same screen and covered in
[Notifications and email](/docs/administration/notifications-and-email/): it is Gmail over
OAuth2, it reuses the Google authentication client, and it must be set up on the
[Authentication](/docs/administration/authentication/) page first.
[Notifications and email](/docs/administration/notifications-and-email/): pick a mail
transport, enter its host, port and credentials, and send a test. It depends on nothing
else on the site — a relay is the recommended posture, a mailbox provider over SMTP the
simplest, and your own MTA needs no credentials at all.
<Aside type="note" title="Until email is connected, the contact form is a mailto: link">
That is a deliberate fallback rather than a failure — but it does mean the *Contact email*

View File

@@ -1,6 +1,6 @@
---
title: Notifications and email
description: Email over Gmail OAuth2, the announcement pipeline and its legs, the Discord bot, and opt-in push to the mobile app.
description: Email over SMTP, the announcement pipeline and its legs, the Discord bot, and opt-in push to the mobile app.
---
import { Aside } from '@astrojs/starlight/components';
@@ -11,19 +11,62 @@ it.
## Email
**Admin → Settings → Email delivery.** The site sends contact-form messages (and test
messages) through **Gmail over OAuth2**, delivered to the *Contact email* setting.
**Admin → Settings → Email delivery.** The site sends contact-form messages, invitations,
password resets, team notifications and test messages through **SMTP**. Contact-form mail
goes to the *Contact email* setting.
It reuses the **Google authentication client**, so the order is fixed: configure Google on
the [Authentication](/docs/administration/authentication/) page first, then press **Connect
Gmail** here. Until then the panel reads *Unconfigured* and says exactly that.
You pick a mail transport and fill in the fields it asks for. There is no consent flow and
no redirect to bounce through — it is a form, and the credentials go straight into the
database encrypted at rest, write-only: the panel will tell you a password is *set*, and
will never show it to you again.
The refresh token it stores is encrypted at rest like every other secret.
### Three ways to point it somewhere
<Aside type="note" title="There is no SMTP option">
Gmail over OAuth2 is the only supported delivery path today. Until it is connected, the
contact form falls back to a `mailto:` link to the contact address — which works, and puts
the message in the visitor's own mail client rather than in your logs.
Any SMTP server works. Which one you should use depends on how much mail you expect to send.
**A relay — the recommended one.** Mailgun, SES, Postmark or equivalent: their host, port
`587`, *Implicit TLS* **off**, and your API key as the password. Deliverability is the hard
part of sending mail — reputation, DKIM, bounce handling — and this is the option where
somebody else owns it. Use this for anything with real volume.
**A mailbox provider over SMTP — the simplest.** For example `smtp.gmail.com`, port `587`,
*Implicit TLS* **off**, your address as the username, and an
[app password](https://support.google.com/accounts/answer/185833) — not your account
password, and it requires 2-Step Verification to be on. Fine for a small site; subject to
the provider's daily send caps.
**Your own MTA.** If you already run mail on the same host: its address, port `25`,
*Implicit TLS* **off**, username and password blank. The site treats a username with no
password as incomplete, since that authenticates as nobody.
<Aside type="caution" title="The two fields that cause most failures">
**Implicit TLS** belongs *on* only for port **465**. On port `587` leave it **off** — the
connection still upgrades to TLS, using STARTTLS. Port 587 with it on does not report an
error; it hangs.
**Send from** must be an address the account is allowed to send as. Unlike a username, this
is not verified when you save it — a server that refuses your sender rejects the mail for
SPF/DMARC reasons that look like nothing at all from the outside. **Send test** is what
proves it, and it names this specifically when it happens.
</Aside>
Until a transport is configured, the contact form falls back to a `mailto:` link to the
contact address — which works, and puts the message in the visitor's own mail client rather
than in your logs. Invitations surface a copyable accept link instead, and password resets
still answer normally.
<Aside type="note" title="Upgrading from the Gmail connect flow">
Earlier versions authorised a mailbox with a **Connect Gmail** consent flow that borrowed
the Google authentication client. That flow has been removed.
If your site used it, mail **stops** on upgrade until you enter SMTP credentials — and
nothing errors when it does, because every sender degrades politely. The admin dashboard
warns you while it is true. `smtp.gmail.com` port 587 with an app password is the shortest
route back.
Single sign-on is unaffected: the Google provider exists for SSO in its own right, and email
merely borrowed its credentials. Removing the borrow also removes a trap — rotating the SSO
secret used to break outbound mail silently.
</Aside>
## Announcements

View File

@@ -97,9 +97,18 @@ a Team. See [Teams](/docs/administration/teams/).
## Email and announcements never arrive
- **The contact form opens a mail client.** Email delivery is not connected; that is the
documented fallback. Connect Gmail in **Settings → Email delivery** — after configuring
the Google provider, which it reuses.
- **The contact form opens a mail client.** Email delivery is not configured; that is the
documented fallback. Enter SMTP credentials in **Settings → Email delivery**. If this site
used to send mail and stopped, the Gmail connect flow was removed — the admin dashboard
says so, and [Notifications and email](/docs/administration/notifications-and-email/) has
the migration.
- **Mail is configured but nothing arrives, and there is no error.** Two usual causes, both
invisible without a test send. *Implicit TLS* left on for port 587 hangs rather than
failing; and a **Send from** address the server will not let you send as is rejected for
SPF/DMARC reasons. Press **Send test** — its failure message names both cases.
- **Email was working and the toggle is still on.** *Enable email sending* now gates every
message, not just some of them. If it is off, nothing is sent, including the contact
form.
- **A published post announced nothing.** The Discord bot is a separate container. If the
Discord Bot screen says *bot unreachable*, it is not running.
- **A missed announcement does not come back.** Nothing retries; the post itself is still

View File

@@ -114,9 +114,10 @@ lifecycle](/docs/modules/module-lifecycle/#failure-is-contained-by-construction)
### Secrets are encrypted at rest
OAuth client secrets, the sidecar token and the Gmail refresh token are AES-256-GCM
encrypted, keyed by `SECRET_ENC_KEY`. **The sidecar token is write-only in the API** — it is
never returned to any client.
OAuth client secrets, the sidecar token, the Discord bot token and the mail transport's
credentials are AES-256-GCM encrypted, keyed by `SECRET_ENC_KEY`. **The sidecar token and the
mail credentials are write-only in the API** — neither is ever returned to any client; the
email panel reports only that a password is *set*.
<Aside type="caution" title="Rotating that key orphans every stored secret">
Nothing re-encrypts. What was stored under the old key can no longer be read, and every