Files
descrybe/docs/email-sending.md
T
greeneclipse 8580c996c3 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.
2026-08-09 22:47:43 +02:00

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

  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.