Phase 4: Tracking Endpoints
Overview
Open and click events from delivered emails are captured by two unauthenticated endpoints (email clients cannot carry JWTs). Both are secured with HMAC-signed tokens generated at send time — a raw UUID in the URL is not a valid token, so forged or guessed requests are rejected without a DB round-trip. Tracking writes are idempotent (a second open/click on the same message is a no-op). Per-IP rate limiting (120 req/min) prevents analytics inflation and DB write storms from scrapers and mail proxies.
Post-review hardening shipped in this phase:
- Raw UUIDs replaced with HMAC-signed tokens (
sign_message_id/sign_click). - Click endpoint verifies both the message-id signature and the
(message_id, url)signature; non-http(s)schemes are rejected. slowapiper-IP rate limit re-enabled (120/min);limiter.exemptis forbidden on these routes.
Goal
Provide FastAPI endpoints to capture open and click events triggered by the HTML parsing injections.
TDD Acceptance Criteria
pytest tests/api/test_tracking.py::test_track_open_returns_gif_and_updates_dbMUST PASSpytest tests/api/test_tracking.py::test_track_click_returns_302_and_updates_dbMUST PASSpytest tests/api/test_tracking.py::test_tracking_endpoints_have_per_ip_rate_limitMUST PASS
Technical Specifications
GET /api/v1/track/open/{signed_token}.gif
- Does NOT require JWT authentication.
signed_tokenis an HMAC-signed message-id token (sign_message_id); requests with an invalid signature are rejected without a DB read.- Returns a 1x1 transparent GIF with
Content-Type: image/gif. - Header:
Cache-Control: no-cache, max-age=0. - Updates
outbound_messages.opened_atif it is NULL (idempotent).
GET /api/v1/track/click/{signed_token}
- Query parameters:
url(the original destination),sig(HMAC ofmessage_id + url). - Does NOT require JWT authentication.
- Verifies both the
signed_token(message-id HMAC) andsig((message_id, url)HMAC). Rejects non-http/httpsschemes. - Updates
outbound_messages.clicked_atif it is NULL (idempotent). - Returns
302 FoundwithLocationset to the verifiedurl.
Rate Limiting
- Both endpoints apply the standard
slowapiper-IP limit: 120 req/min/IP. limiter.exemptMUST NOT be used on these routes. The per-IP limit is the only abuse control — do not remove it.