Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
# Email sending (marketing campaigns)
|
||||
|
||||
Tenant email delivery for Descrybe v2 marketing campaigns. **Platform invite SMTP** (`SMTP_*` + `internal/mail`) is separate and must not be used for blasts.
|
||||
|
||||
Schema lives in `011_email_campaigns.sql` (`email_providers`, `email_unsubscribes`, `email_sends`). Pending unsubscribe tokens: `014_email_unsub_pending.sql`.
|
||||
|
||||
## Providers
|
||||
|
||||
| Provider | Company settings | Env fallback |
|
||||
|----------|------------------|--------------|
|
||||
| **Resend** | API key (AES-GCM in `secrets_enc`), `from_email`, domain in `config` | `RESEND_API_KEY` if company key empty |
|
||||
| **SMTP** | host/port/user/password in `secrets_enc`, from | `SMTP_HOST` / `SMTP_USER` / `SMTP_PASSWORD` |
|
||||
|
||||
Configure in UI: **`/integrations/email`** (Account → Email sending).
|
||||
|
||||
## Environment
|
||||
|
||||
Bootstrap encryption and rate limits live in the **root** `.env` (not `apps/api/.env`). Provider API keys belong in **`/integrations/email`**.
|
||||
|
||||
| Variable | Required | Purpose |
|
||||
|----------|----------|---------|
|
||||
| `APP_ENCRYPTION_KEY` | **Prod yes** | AES-256-GCM for email secrets at rest (`enc:v1:…`). 32-byte hex or base64. |
|
||||
| `CREDENTIALS_ENCRYPTION_KEY` | fallback | Used if `APP_ENCRYPTION_KEY` unset. |
|
||||
| `TOKEN_SIGNING_SECRET` | fallback | Last-resort key material (dev only). |
|
||||
| `EMAIL_DRY_RUN` | no | `true` → never deliver; log dry-run / skipped sends. |
|
||||
| `RESEND_API_KEY` | no | Optional platform Resend key fallback (prefer dashboard). |
|
||||
| `EMAIL_SEND_RPM` | no | Per-company sends/minute (default `30`). |
|
||||
| `EMAIL_SEND_RPH` | no | Per-company sends/hour (default `500`). |
|
||||
| `PUBLIC_API_URL` | yes for unsub | Absolute API origin for `List-Unsubscribe` one-click URL. |
|
||||
| `WEB_ORIGIN` | yes | Origin for `/unsubscribe` landing links. |
|
||||
| `SMTP_*` | optional | Platform invite mailer (`internal/mail`); not a substitute for `/integrations/email`. |
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
| Method | Path | Notes |
|
||||
|--------|------|-------|
|
||||
| GET | `/api/integrations/email` | Public config (no secrets) |
|
||||
| PUT/PATCH | `/api/integrations/email` | Upsert; blank secrets keep existing |
|
||||
| POST | `/api/integrations/email/verify` | Resend domain API / SMTP domain match |
|
||||
| POST | `/api/integrations/email/test` | Test send; success → `status=verified` |
|
||||
| POST | `/api/email/send` | `mode=test\|blast`; blast needs `confirm_understood: "I understand"` |
|
||||
| GET/POST | `/api/public/unsubscribe?token=…` | One-click + landing |
|
||||
|
||||
Blast without confirmation → `400`. Unverified blast → `412 email_not_verified`. Free plan / `EMAIL_DRY_RUN` → dry-run only.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Set `APP_ENCRYPTION_KEY`; restart API.
|
||||
2. `make migrate` (includes `011` + `014`).
|
||||
3. Open `/integrations/email` → Resend or SMTP → save from + domain.
|
||||
4. **Verify domain** (Resend: verify domain in Resend dashboard first).
|
||||
5. **Send test** (marks provider verified).
|
||||
6. Campaigns / blasts: `POST /api/email/send` with `confirm_understood: "I understand"`.
|
||||
|
||||
Package: `internal/email` (AES-GCM, Resend/SMTP transports, rate limits, unsubscribe). Shared table with `internal/campaigns`.
|
||||
Reference in New Issue
Block a user