Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
3.0 KiB
3.0 KiB
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. |
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
- Set
APP_ENCRYPTION_KEY; restart API. make migrate(includes011+014).- Open
/integrations/email→ Resend or SMTP → save from + domain. - Verify domain (Resend: verify domain in Resend dashboard first).
- Send test (marks provider verified).
- Campaigns / blasts:
POST /api/email/sendwithconfirm_understood: "I understand".
Package: internal/email (AES-GCM, Resend/SMTP transports, rate limits, unsubscribe). Shared table with internal/campaigns.