Phase 4 Email Artifacts
1. Mailgun .env config additions
# .env.example additions (line 21+) — already present; minimal additions needed:
MAILGUN_API_KEY=mg_key-xxxxx
MAILGUN_DOMAIN=mg.fogserv.cloud
MAILGUN_FROM_EMAIL=noreply@fogserv.cloud
MAILGUN_WEBHOOK_SIGNING_KEY=key-xxxxx
MAILGUN_BATCH_SIZE=500
MAILGUN_BATCH_DELAY_MS=1500
Note: rate limit holds at 500/min; batch delay 1500ms avoids 429.
2. Welcome email sequence design
File: kb/phase4-artifacts/welcome-sequence.md (sketch)
- Email 1 (T+0min): Confirm opt-in + intro to fogserv.
- Email 2 (T+2d): Deep link to latest CMS guide (post slug).
- Timing: 0min / 2days; template keys:
welcome_day0,welcome_day2.
3. Newsletter composer UI (admin dashboard section)
Sketch component in kb/phase4-artifacts/NewsletterComposerSketch.tsx (inline).
Fields: subject, preheader (preheader like existing Campaign model), list picker (EmailList[]), body HTML (RichTextEditor reuse), scheduled DateTime, status DRAFT/SCHEDULED.
4. Bulk send with rate limiting
Architectural note (kb/phase4-artifacts/bulk-send.md): chunk subscribers by batchSize (default 500), delay batchDelayMs (1500ms), update CampaignLog per chunk with sent=true. Endpoint snippet (kb/phase4-artifacts/bulk-endpoint-snippet.ts): POST /api/email/campaigns/send → reads segment query, chunks recipient IDs, sends via emailService.gateway.send() iterating batchSize.
5. Subscriber segmentation
Uses existing SubscriberTag model + EmailList relations. Query snippet (kb/phase4-artifacts/segmentation-query.ts): SELECT subscriberId FROM SubscriberTag WHERE tagName IN (...) JOIN Subscriber ON ... WHERE status='ACTIVE'.
6. Unsubscribe link generation
File: kb/phase4-artifacts/unsubscribe-link.md. Signed token (crypto.subtle.sign over subscriberId + timestamp) → endpoint /api/email/unsubscribe?token=...&sub=... validates HMAC, sets Subscriber.status='UNSUBSCRIBED'.
7. Analytics (open/click/bounce aggregation from webhook events)
File: kb/phase4-artifacts/analytics-aggregation.md. Webhook (mailgun events: delivered, opened, clicked, bounced) writes CampaignLog rows; aggregation query sums by campaignId grouping event types. Uses existing Campaign stats (opened, clicked, bounced) and updates from webhook payload.