# 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 ]`. 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: )` 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:** `` and `` 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=` 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.