Files
manyangles/docs/email-delivery.md
T

82 lines
4.3 KiB
Markdown

# Email delivery
Manyangles uses the RaceTix-style branded shell for verification, password resets,
account invitations, and gallery-ready messages. HTML has plain-text alternatives,
escaped dynamic content, and system-font fallbacks. People previews the actual
gallery-ready HTML before queueing.
## Production Resend configuration
Set `EMAIL_PROVIDER=resend`, `RESEND_API_KEY`, `EMAIL_FROM` (a verified sender), and
`NEXT_PUBLIC_APP_URL` (the public HTTPS origin). Run the image/email worker with
the same environment as the web process. Configuration changes do not migrate
pending emails between providers. No production test sends are needed.
## Completion outbox
Organizer actions enqueue one immutable payload per event/normalized email
address. A unique database constraint deduplicates concurrent requests. Only
opted-in guests qualify. Before sending, the worker checks consent and gallery
visibility again. Messages whose guest link changed are held for review.
The worker records provider acceptance, attempts and sanitized errors. “Sent”
means accepted by Resend/SMTP, not delivered to an inbox. No automatic event
schedule sends email.
Configure a Resend webhook at `/api/webhooks/resend` and set
`RESEND_WEBHOOK_SECRET` in the web process. Subscribe to `email.sent`,
`email.delivered`, `email.delivery_delayed`, `email.bounced`, `email.complained`,
and `email.failed`. The endpoint verifies the bounded raw body with the Resend
SDK and deduplicates by signed `svix-id`. It stores only message identifiers,
outcomes and timestamps, never raw recipient payloads. Failed database writes
return 503 for provider retry. Callbacks arriving before the worker saves its
provider reference are retained and matched when email history is read.
People shows provider outcomes separately from queue status. Complaints,
bounces and failures take precedence over delivered/delayed/sent regardless of
callback order. Delivered means acceptance by the recipient mail server, not
inbox placement. This tracks gallery-ready outbox messages; auth email outcomes
do not have a management UI. Tracking does not change guest consent or implement
a cross-event suppression list. Mailpit does not produce Resend callbacks.
Resend retries reuse `gallery-ready/<delivery-id>` and the original payload.
Retries back off, stop after five attempts, and stay inside a conservative
23-hour window (Resend retains idempotency keys for 24 hours). Expired/uncertain
deliveries require provider review. The People tab permits safe Resend retries
inside that window. SMTP ambiguity is not automatically retried.
Account verification, password resets and account invitations remain synchronous
transactional sends. They also use stable, recipient-specific idempotency keys.
## Local verification
Keep `EMAIL_PROVIDER=mailpit`, `SMTP_HOST=127.0.0.1`, `SMTP_PORT=1027`.
Open Mailpit at http://localhost:8027. Test scripts reject nonlocal Mailpit settings.
`bun run email:preview` sends all four templates to the local inbox, like RaceTix's
preview workflow. Authentication/invitation links in these samples are deliberately
nonfunctional; the gallery sample links to `/e/demo`.
Docker Compose already supplies Postgres (5439), Mailpit (SMTP 1027 / inbox 8027),
and Garage (3900 / 3903). Use `bun run docker:up`, `bun run docker:logs`, and
`bun run docker:down`. The web app and worker run on the host with Bun, just as
RaceTix's development setup does. Stopping this project's Compose stack does not
stop Colima or disrupt other projects.
```sh
bun run typecheck
bun test
WEBHOOK_INTEGRATION=1 bun --env-file=.env test packages/email/src/webhooks.test.ts
cd apps/web
NOTIFICATIONS_INTEGRATION=1 PUBLISHING_INTEGRATION=1 bun --env-file=../../.env test src/server/guest-notifications.integration.test.ts src/server/publishing.integration.test.ts
```
With the local worker running, add `REAL_UPLOAD_INTEGRATION=1` to the publishing
test command to copy one existing `/e/demo` original through a presigned PUT,
verify a single queued job and smaller generated variants, then remove only the
temporary event and its uploaded objects. The original demo assets are untouched.
Run `bun --env-file=.env packages/database/src/verify-migrations.ts` from the repo
root to migrate a temporary local database; the script removes that database
after verification. It never migrates a remote database.