124 lines
5.0 KiB
Markdown
124 lines
5.0 KiB
Markdown
|
|
# 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.
|