## Summary Closes #9. - Campaign composer auto-inserts a tracked homepage link (`utm_source=monica`, `utm_medium` = channel, `utm_campaign` = slug of the name) for email, SMS, and postcard QR — no manual UTM paste. - When `SHORTENER_BASE_URL` + `SHORTENER_API_TOKEN` are set, the app mints that long HTTPS URL via `POST /api/links/` on the shortener **API host** and puts the returned `piha.li` / `beta.piha.li` short URL in SMS, email hrefs, and QR codes. Empty env (local) falls back to the long UTM URL. - Live composer resolves shorts through a portal JSON endpoint (login + CSRF). Browser never calls the shortener. ## Secrets (control node, not git) **monica_site** (`~/Documents/secrets/monica_site/`): ``` # prod SHORTENER_BASE_URL=https://shortener.aimloperations.com SHORTENER_API_TOKEN=monica:<secret> # beta SHORTENER_BASE_URL=https://shortener-beta.aimloperations.com SHORTENER_API_TOKEN=monica:<beta-secret> ``` **url_shortening_service** (same secret, named token): ``` SHORTENER_API_TOKENS=monica:<secret> SHORT_ALLOWED_HOSTS=mkdrealtor.com,aimloperations.com ``` Prod public short host: `piha.li`. Beta: `beta.piha.li`. Generate with `python -c "import secrets; print(secrets.token_urlsafe(32))"`. Template port: westfarn/web_django_template#3 ## Test plan - [ ] `cd site && uv run python manage.py test messaging.tests.CampaignUtmLinkTests` - [ ] Composer: type a campaign name — email gets an HTML link, SMS gets a URL, postcard shows a QR - [ ] With shortener env set: SMS/QR show `piha.li` (or `beta.piha.li`); without it, long UTM URL still works - [ ] Copy/download QR into postcard designer - [ ] Secret files have `SHORTENER_*` on both caller and operator sides before beta/prod deploy Reviewed-on: #10
151 lines
6.1 KiB
Markdown
151 lines
6.1 KiB
Markdown
# Messaging
|
||
|
||
Campaign compose/send, SMTP2GO email + SMS, PCM Integrations postcards, and delivery webhooks.
|
||
|
||
## SMTP2GO webhook setup
|
||
|
||
Campaign report page polls provider events every 10s. Create **one** SMTP2GO
|
||
webhook (email + SMS share a URL — paid plans cap at 10 webhooks).
|
||
|
||
### Auth (`SMTP2GO_WEBHOOK_SECRET`)
|
||
|
||
1. Set `SMTP2GO_WEBHOOK_SECRET` in `.env` / prod env (long random string).
|
||
2. In SMTP2GO, set **Authorization header** to **Bearer** and paste that same secret
|
||
(do not leave it as “None”).
|
||
3. Fallback: `?token=<SMTP2GO_WEBHOOK_SECRET>` on the webhook URL also works.
|
||
|
||
### Unified email + SMS webhook
|
||
|
||
| Field | Value |
|
||
|-------|--------|
|
||
| URL | `https://mkdrealtor.com/portal/messaging/webhooks/smtp2go/` |
|
||
| Authorization header | **Bearer** + `SMTP2GO_WEBHOOK_SECRET` |
|
||
| Output type | JSON |
|
||
| Users | email SMTP user(s) **and** the SMS API key (`SMTP2GO_SMS_API_KEY`) |
|
||
| Email events | processed, bounced, rejected, spam, delivered, unsub/resub, opened, clicked |
|
||
| Email headers | `X-Monica-Message-Id` |
|
||
| SMS events | Submitted, Sending, Delivered, Failed, Rejected (and Opt-out if shown) |
|
||
|
||
The handler classifies each POST from the payload (`sms_*` / `destination_number`
|
||
→ SMS; `rcpt` / `email_id` / `X-Monica-Message-Id` → email; inbound `text=STOP`
|
||
without `event` → SMS opt-out).
|
||
|
||
`X-Monica-Message-Id` is set on every campaign email send and is required so email
|
||
webhook events match the correct recipient row. Invalid / missing header values
|
||
no longer 500 the endpoint (SMTP2GO “Test this webhook” often sends a sample
|
||
non-UUID).
|
||
|
||
## Campaign tracked links (UTM + piha.li)
|
||
|
||
Composer auto-inserts a homepage link:
|
||
|
||
| Param | Value |
|
||
|-------|--------|
|
||
| `utm_source` | `monica` |
|
||
| `utm_medium` | `email` / `sms` / `postcard` |
|
||
| `utm_campaign` | hyphenated campaign name |
|
||
|
||
When `SHORTENER_BASE_URL` + `SHORTENER_API_TOKEN` are set, the app POSTs that
|
||
long HTTPS URL to `url_shortening_service` (`POST /api/links/`, Bearer
|
||
`monica:<secret>`) and puts `short_url` (`https://piha.li/<code>`) in SMS,
|
||
email hrefs, and postcard QR codes. Empty env → long UTM URL (local default).
|
||
|
||
Mint against the **API host**, not `piha.li`. See `.env.example` / `.env.prod.example`.
|
||
|
||
SMS correlation uses `message_id` (SMS id), then `destination_number` phone
|
||
fallback. Do **not** treat webhook `id` as the SMS id.
|
||
|
||
**Opt-out:** SMTP2GO auto-handles replies `STOP` / `UNSUB` / `UNSUBSCRIBE`.
|
||
This endpoint also accepts inbound POSTs without an `event` field
|
||
(`text=STOP`, `from=…`) and opts the contact out of SMS. Later sends to
|
||
opted-out numbers are typically `sms_rejected`.
|
||
|
||
Beta / other hosts: swap the hostname, keep the path.
|
||
|
||
Legacy aliases (same handler): `/portal/messaging/webhooks/email/` and
|
||
`/portal/messaging/webhooks/sms/` — prefer `/smtp2go/` for new configs.
|
||
|
||
## PCM Integrations (postcards)
|
||
|
||
Default postcard provider. Designer embeds PCM’s editor; orders use DirectMail API v3.
|
||
|
||
### Env
|
||
|
||
| Var | Purpose |
|
||
|-----|---------|
|
||
| `PCM_API_KEY` | API key from PCM portal (My Account → API Keys) |
|
||
| `PCM_API_SECRET` | Matching API secret; used with key on `POST /auth/login` |
|
||
| `PCM_CHILD_REF_NBR` | Optional child-app ref for multi-account |
|
||
| `PCM_WEBHOOK_SECRETS` | Comma-separated signature secrets (one per PCM subscription) |
|
||
| `PCM_WEBHOOK_SECRET` | Optional single-secret alias (merged into the list above) |
|
||
| `PCM_RETURN_ADDRESS` | JSON return address on orders |
|
||
| `POSTCARD_PROVIDER` | `pcm` (default) |
|
||
|
||
Auth flow: `POST /auth/login` with `{apiKey, apiSecret}` → short-lived
|
||
`token` used as `Authorization: Bearer …` on design/order calls
|
||
([PCM Logging In](https://docs.pcmintegrations.com/docs/directmail-api/ffef03a112bb0-logging-in)).
|
||
|
||
### Designer
|
||
|
||
Portal → **Postcard design**: create/list designs via API, edit in iframe
|
||
(`POST /design/custom`, `GET /design/{id}/edit?mode=embed`). Save as a
|
||
`MessageTemplate` (stores `design_id`) then pick it when composing a postcard campaign.
|
||
|
||
### Postcard webhook
|
||
|
||
PCM allows **one event per subscription**, and each subscription gets its own
|
||
**signature secret** (copy-only in the UI). Create one subscription per status
|
||
you care about; point them all at the same URL and paste every secret into env.
|
||
|
||
| Field | Value |
|
||
|-------|--------|
|
||
| URL | `https://mkdrealtor.com/portal/messaging/webhooks/postcard/` |
|
||
| Events | One subscription each: Pending, Processing, Processed, Delivered, Undeliverable, Canceled (skip QrCodeScan unless needed) |
|
||
| Environments | Sandbox and/or Production as needed |
|
||
| Secrets | Copy each subscription signature → `PCM_WEBHOOK_SECRETS=sec1,sec2,…` |
|
||
|
||
We accept Bearer, `?token=`, or common signature headers (raw secret or
|
||
HMAC-SHA256 of body) matching **any** listed secret.
|
||
|
||
Correlation: we send `extRefNbr=<Message.uuid>` on each recipient; webhooks should
|
||
echo that (or `orderID`, matched to `Message.provider_message_id`).
|
||
|
||
### Campaign completion email
|
||
|
||
When a campaign reaches **completed** (email, SMS, or postcard), one summary email
|
||
goes to `campaign.created_by.email`, else `CONTACT_EMAIL`. Guarded by
|
||
`Campaign.notify_sent_at` so it only sends once.
|
||
|
||
### Local development
|
||
|
||
SMTP2GO / PCM cannot reach `localhost`. Use a tunnel (Cloudflare Tunnel / ngrok) to `:8000`,
|
||
or test webhooks against beta/prod.
|
||
|
||
For real SMTP delivery locally (not console logs):
|
||
|
||
```bash
|
||
# in .env
|
||
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||
EMAIL_HOST_USER=…
|
||
EMAIL_HOST_PASSWORD=…
|
||
SMTP2GO_WEBHOOK_SECRET=…
|
||
SMTP2GO_SMS_API_KEY=… # SMS sends only
|
||
PCM_API_KEY=…
|
||
PCM_API_SECRET=…
|
||
PCM_WEBHOOK_SECRETS=sec1,sec2,…
|
||
PCM_RETURN_ADDRESS={…}
|
||
```
|
||
|
||
### Endpoints (app)
|
||
|
||
| Path | Purpose |
|
||
|------|---------|
|
||
| `POST /portal/messaging/webhooks/smtp2go/` | Unified SMTP2GO email + SMS (+ inbound STOP) |
|
||
| `POST /portal/messaging/webhooks/email/` | Legacy alias → same as `/smtp2go/` |
|
||
| `POST /portal/messaging/webhooks/sms/` | Legacy alias → same as `/smtp2go/` |
|
||
| `POST /portal/messaging/webhooks/postcard/` | PCM order / mail tracking events |
|
||
| `GET /portal/messaging/campaigns/<id>/status.json` | Live stats for the campaign report UI |
|
||
| `GET /portal/messaging/postcard/` | PCM designer iframe |
|
||
|
||
Code: `webhooks.py`, `providers/postcard/pcm.py`, `views.py`.
|