# Outbound communications safety net

**Last reviewed:** 2026-05-03
**Owner:** matt@portnimara.com

This doc enumerates every channel through which the CRM can produce
outbound communication (email, document signing, webhooks) and describes
how each channel respects the `EMAIL_REDIRECT_TO` env var. The goal: a
single environment flip pauses **all** outbound traffic, so a production
data import, dedup migration dry-run, or staging environment can run
against real data without anyone getting paged or spammed.

> **Single env switch:** when `EMAIL_REDIRECT_TO` is set to an address,
> all outbound communication is rerouted there or short-circuited. Unset
> it in production.

---

## Channels

### 1. Direct email (`sendEmail`)

**Path:** `src/lib/email/index.ts` → `sendEmail()` → nodemailer SMTP transport.

**Safety:** YES — covered.

When `EMAIL_REDIRECT_TO` is set, `sendEmail()` rewrites the `to` header
to the redirect address and prefixes the subject with
`[redirected from <orig>]`. The original recipient is logged.

**Call sites** (all flow through `sendEmail`, so all are covered):

- `src/lib/services/portal-auth.service.ts` — portal activation + reset
- `src/lib/services/crm-invite.service.ts` — CRM user invitations
- `src/lib/services/document-templates.ts` — template-generated PDFs sent
  as attachments (the PDF body is generated locally; the email itself
  goes through SMTP)
- `src/lib/services/email-compose.service.ts` — ad-hoc emails composed
  in the in-app UI
- `src/lib/services/gdpr-export.service.ts` — GDPR export delivery

### 2. Documenso e-signature recipients

**Path:** `src/lib/services/documenso-client.ts` → `createDocument()` /
`generateDocumentFromTemplate()` → Documenso REST API.

**Safety:** YES — covered as of 2026-05-03.

Documenso's own server sends the signing-request email on our behalf.
We can't intercept that at the SMTP layer because it's external. The
fix is at the REST-call boundary: when `EMAIL_REDIRECT_TO` is set,
`createDocument` rewrites every recipient's email to the redirect
address and prefixes the recipient name with `(was: <orig email>)` so
the doc is still traceable to its intended recipient.
`generateDocumentFromTemplate` does the same for both shapes the
template-generate endpoint accepts (v1.13 `formValues.*Email` keys and
v2.x `recipients` array).

The redirect happens **before** the API call, so even if Documenso has
its own retry logic the original email never leaves our process.

### 3. Webhooks (outbound to user-configured URLs)

**Path:** `src/lib/queue/workers/webhooks.ts` → BullMQ job → `fetch(webhook.url, ...)`.

**Safety:** YES — covered as of 2026-05-03.

When `EMAIL_REDIRECT_TO` is set, the webhook worker short-circuits
before the HTTP call. The delivery row is marked `dead_letter` with a
human-readable reason so it's still visible in the deliveries listing.
The SSRF guard remains in place independently.

### 4. WhatsApp / phone deep-links

**Path:** `<a href="https://wa.me/...">` and `<a href="tel:...">` in
client / interest detail headers.

**Safety:** N/A — user-initiated only.

These are deep links the user explicitly clicks. No automated dispatch.
A deep link click opens the user's WhatsApp / phone app, which is the
intended interaction. No safety net needed.

### 5. SMS

Not implemented. The `interests.preferredContactMethod` enum includes
`'sms'` as a value but no sending path exists. If/when SMS is added (e.g.
via Twilio), the new send function should respect `EMAIL_REDIRECT_TO`
the same way `sendEmail` does — log the original number, drop the
message, or reroute to a configurable `SMS_REDIRECT_TO` env.

---

## Verification checklist before importing real data

- [ ] `.env` has `EMAIL_REDIRECT_TO=<my-address>` set.
- [ ] Restart dev server (or worker) so the new env is picked up — env
      vars are read at import time in some paths.
- [ ] Send a test email via `pnpm tsx scripts/dev-trigger-portal-invite.ts`
      or similar. Confirm subject is prefixed with `[redirected from ...]`.
- [ ] Trigger an EOI send through the UI (any client). Confirm Documenso
      shows the redirect address as recipient (not the real client email).
- [ ] If any webhooks are configured, trigger an event that fires one and
      confirm the delivery is recorded as `dead_letter` with the
      "EMAIL_REDIRECT_TO is set" reason.
- [ ] Run the NocoDB migration `--dry-run` to count clients/interests; the
      `--apply` step is what creates real records but emails/webhooks are
      still gated by the redirect env.

## Production cutover

When ready to go live:

1. Run a final dry-run of the data migration with `EMAIL_REDIRECT_TO` set
   to a sandbox address.
2. Verify the snapshot looks right (counts, client coverage).
3. Unset `EMAIL_REDIRECT_TO` in the production env.
4. Restart the app + worker.
5. Run the migration with `--apply`. From this point forward, real
   recipients will receive real comms.

If you ever need to re-pause outbound (e.g. handling a security incident,
re-importing on top of existing data), set `EMAIL_REDIRECT_TO` again.
