Phase 10: contacts, senders, background sending, segments, rate limits #52

Merged
vmanne-poc merged 8 commits from phase-10 into main 2026-10-15 08:51:49 -04:00
Owner

Summary

Phase 10 (v0.11.0). One commit per issue; the backend and frontend for contacts and segments are split into separate commits.

  • #32 Contact pages. Detail with custom data, external ID, campaigns received and an activity timeline; new and edit forms; delete. GET/PATCH/DELETE /contacts/:id. Migration contact_external_id makes the external ID unique per project.
  • #33 Search and paging. Search by email, name or external ID, status filter chips, pages of 25. GET /contacts now returns { items, total, page, pageSize }.
  • #34 CSV import and export. The browser parses the file (papaparse 5.7.0, pinned) and shows a column-mapping preview. Includes a replace-duplicates option, batched upload, a rejected-rows report with download, a template, and instructions. Import matches by external ID, then email, and never changes a contact's status. export.csv defuses cells that look like formulas.
  • #35 Sender setup. Edit pages, reply-to, SMTP security (SSL/TLS, STARTTLS, none), auth, allow self-signed, write-only secrets (blank keeps the stored one), test send to yourself, and 409 when unsent campaigns use the sender. TLS certificates are now verified by default.
  • #36 Background sending. Sending queues the campaign and returns immediately. DeliveryService claims messages, sends over a pooled connection, honours per-sender rate limits, retries 4xx and network failures with back-off, records permanent errors, skips people who unsubscribed while queued, and recovers abandoned claims. The campaign list shows progress; the report shows pending emails and failures. Migration sender_settings_and_delivery_queue.
  • #37 Segment editor. OR groups of AND conditions, a JSON query (editable, for deeper nesting), a live paged contact list, and a CSV download.
  • #38 Rate limiting. @nestjs/throttler 6.7.1 (pinned): 10 requests per minute per client on auth, form submit and test-send routes, 600 per minute elsewhere. nginx now overwrites X-Forwarded-For, only private-network proxies are trusted, and the backend port is bound to 127.0.0.1.

Upgrading: back up the database first. Both migrations are additive. A sender pointing at a server with a self-signed certificate needs "allow self-signed" ticked.

Testing

  • Backend: 62 unit and 46 e2e tests, plus migration paths and the drift check. New tests:
    • contact CRUD, search, paging and validation
    • import: duplicates, per-row errors, status preservation
    • CSV export, including the formula guard
    • segment paging and export
    • sender edit, secrets, validation, test send and delete rules
    • delivery: rate limits, temporary-then-success retries, permanent failures, skipping unsubscribed contacts, stale claims, and two workers never sending twice
    • rate limiting returns 429, then resets
    • transport options and error classification
  • Frontend: 63 tests, covering the contacts list, form, detail and import (with a real CSV file), the segment editor (groups, JSON mode, live list, download state), the sender form and list, and the grouped-rule helpers.
  • npm audit: backend (--omit=dev) 0, frontend 0.
  • Live stack through nginx and Mailpit:
    • test email
    • import with one rejected row
    • search
    • contact and segment CSV
    • background send (request returned in 26 ms; 4 delivered with From, Reply-To and List-Unsubscribe correct; one tracking link per URL)
    • login rate limiting even with a spoofed X-Forwarded-For (429 after 9)
    • test send through a sender saved before this release

Not verified

  • No manual browser click-through; component tests and the bundle cover the UI.
  • Rate limits are in memory, so each backend instance counts separately.

Closes #32
Closes #33
Closes #34
Closes #35
Closes #36
Closes #37
Closes #38

## Summary Phase 10 (v0.11.0). One commit per issue; the backend and frontend for contacts and segments are split into separate commits. - **#32 Contact pages.** Detail with custom data, external ID, campaigns received and an activity timeline; new and edit forms; delete. `GET`/`PATCH`/`DELETE /contacts/:id`. Migration `contact_external_id` makes the external ID unique per project. - **#33 Search and paging.** Search by email, name or external ID, status filter chips, pages of 25. `GET /contacts` now returns `{ items, total, page, pageSize }`. - **#34 CSV import and export.** The browser parses the file (papaparse 5.7.0, pinned) and shows a column-mapping preview. Includes a replace-duplicates option, batched upload, a rejected-rows report with download, a template, and instructions. Import matches by external ID, then email, and never changes a contact's status. `export.csv` defuses cells that look like formulas. - **#35 Sender setup.** Edit pages, reply-to, SMTP security (SSL/TLS, STARTTLS, none), auth, allow self-signed, write-only secrets (blank keeps the stored one), test send to yourself, and 409 when unsent campaigns use the sender. TLS certificates are now verified by default. - **#36 Background sending.** Sending queues the campaign and returns immediately. `DeliveryService` claims messages, sends over a pooled connection, honours per-sender rate limits, retries 4xx and network failures with back-off, records permanent errors, skips people who unsubscribed while queued, and recovers abandoned claims. The campaign list shows progress; the report shows pending emails and failures. Migration `sender_settings_and_delivery_queue`. - **#37 Segment editor.** OR groups of AND conditions, a JSON query (editable, for deeper nesting), a live paged contact list, and a CSV download. - **#38 Rate limiting.** `@nestjs/throttler` 6.7.1 (pinned): 10 requests per minute per client on auth, form submit and test-send routes, 600 per minute elsewhere. nginx now overwrites `X-Forwarded-For`, only private-network proxies are trusted, and the backend port is bound to 127.0.0.1. **Upgrading:** back up the database first. Both migrations are additive. A sender pointing at a server with a self-signed certificate needs "allow self-signed" ticked. ## Testing - Backend: 62 unit and 46 e2e tests, plus migration paths and the drift check. New tests: - contact CRUD, search, paging and validation - import: duplicates, per-row errors, status preservation - CSV export, including the formula guard - segment paging and export - sender edit, secrets, validation, test send and delete rules - delivery: rate limits, temporary-then-success retries, permanent failures, skipping unsubscribed contacts, stale claims, and two workers never sending twice - rate limiting returns 429, then resets - transport options and error classification - Frontend: 63 tests, covering the contacts list, form, detail and import (with a real CSV file), the segment editor (groups, JSON mode, live list, download state), the sender form and list, and the grouped-rule helpers. - `npm audit`: backend (`--omit=dev`) 0, frontend 0. - Live stack through nginx and Mailpit: - test email - import with one rejected row - search - contact and segment CSV - background send (request returned in 26 ms; 4 delivered with From, Reply-To and List-Unsubscribe correct; one tracking link per URL) - login rate limiting even with a spoofed `X-Forwarded-For` (429 after 9) - test send through a sender saved before this release ## Not verified - No manual browser click-through; component tests and the bundle cover the UI. - Rate limits are in memory, so each backend instance counts separately. Closes #32 Closes #33 Closes #34 Closes #35 Closes #36 Closes #37 Closes #38
@nestjs/throttler 6.7.1 (pinned): per-IP limits, strict on auth and public form routes (10/min), general elsewhere (600/min), 429 with Retry-After. Client IP from X-Forwarded-For only from private-network proxies (POSTAK_TRUST_PROXY). SNS webhook and health are exempt.

Closes #38
GET/PATCH sender, validated config (security tls/starttls/none, password auth, allow self-signed), TLS certificates verified by default, write-only secrets kept when left blank, reply-to, per-sender rate limits, test email to the signed-in user, 409 when unsent campaigns use the sender. Address objects for From/Reply-To. New/Edit sender pages. Migration sender_settings_and_delivery_queue.

Closes #35
Send queues one message per recipient in a transaction and returns at once; DeliveryService claims and delivers messages in batches over a pooled transport, honours per-sender limits, retries temporary failures with back-off, records permanent errors, skips contacts who unsubscribed meanwhile, and recovers abandoned claims. One tracking link per URL per campaign. Progress on the campaign list, pending and failures in the report.

Closes #36
GET/PATCH/DELETE contacts/:id, custom data validation, externalId unique per project (migration contact_external_id), list with q/status/page/pageSize/sort, import per-row validation with replaceDuplicates and error report, matching by external ID then email, statuses never changed by import; streamed CSV export with formula-injection guard. Segment preview paging and export.csv.

Refs #32, #33, #34, #37
papaparse 5.7.0 (pinned) parses files in the browser; column mapping preview, replace-duplicates option, batched import with rejected-row report and download, template and instructions; authenticated CSV download helper.

Closes #32

Closes #33

Closes #34
Segments list as cards; editor page with grouped builder, read-only query view and JSON editing for deeper nesting, paged matching contacts refreshed as rules change, download for saved segments.

Closes #37
X-Forwarded-For is overwritten with the real client address so rate limits can't be bypassed; the API port is published on 127.0.0.1 only.

Refs #38
vmanne-poc deleted branch phase-10 2026-10-15 08:51:50 -04:00
Sign in to join this conversation.
No reviewers
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
vmanne-poc/Postak!52
No description provided.