Files
pn-new-crm/docs/operations/outbound-comms-safety.md
Matt Ciaccio 8e4d2fc5b4 feat(safety): EMAIL_REDIRECT_TO now also pauses Documenso + webhooks
Closes a gap exposed by the comms safety audit: the existing
EMAIL_REDIRECT_TO env var only redirected outbound SMTP via the
sendEmail() bottleneck. Two channels still leaked when set:

  1. Documenso e-signature recipients — Documenso's own server emails
     them on our behalf, so SMTP redirect doesn't help. We were sending
     real client emails to the Documenso REST API, which would then
     deliver to the real client.

  2. Outbound webhooks — fire from the BullMQ worker to user-configured
     URLs. SSRF guard blocks internal hosts but doesn't pause production
     endpoints.

Documenso (src/lib/services/documenso-client.ts):
  - createDocument: rewrite every recipient.email to EMAIL_REDIRECT_TO
    and prefix the recipient.name with the original email so the doc
    is traceable.
  - generateDocumentFromTemplate: same treatment for both v1.13
    formValues.*Email keys and v2.x recipients[]. The redirect happens
    BEFORE the API call, so even Documenso's own retry logic can't
    reach the original recipient.
  - Both paths log when they redirect so it's visible in dev.

Webhooks (src/lib/queue/workers/webhooks.ts):
  - When EMAIL_REDIRECT_TO is set, short-circuit the dispatch and write
    a `dead_letter` row with reason "Skipped: EMAIL_REDIRECT_TO is set,
    outbound comms paused." so the attempt is still visible in the
    deliveries listing.

Doc:
  docs/operations/outbound-comms-safety.md catalogs every outbound
  comms channel (email, Documenso, webhooks, WhatsApp/phone deep-links,
  SMS-not-implemented) and explains how each one respects the env flag.
  Includes a verification checklist to run before any production data
  import + cutover steps for going live.

Single env var EMAIL_REDIRECT_TO now reliably pauses ALL automated
outbound comms. Unset for production.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 17:24:41 +02:00

5.0 KiB

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.tssendEmail() → 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.tscreateDocument() / 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.

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.