## Purpose

Outbound guest communication — email-only in v1, behind a channel-shaped provider interface so WhatsApp/SMS can be added in v2 without a rewrite. Covers templates, the admin send screen, delivery/bounce tracking, the message log, broadcasts, opt-out, and dry-run.

## ADDED Requirements

### Requirement: Email channel behind a swappable provider interface (FR-6.1 scoped to v1, §8.4, P0)
The system SHALL send all v1 messages via a transactional email provider implemented behind the channel-shaped `MessageProvider` interface (`send` + `parseWebhook`, keyed by channel). The message pipeline, templates, and job schema MUST remain channel-aware (channel enum retained) so WhatsApp/SMS providers can be added in v2 by implementing the interface — no caller changes.

#### Scenario: Email send
- **WHEN** a message job for a guest with an email address is processed
- **THEN** it is delivered through the email provider and the provider message ID is stored

#### Scenario: Vendor swap
- **WHEN** the email vendor is replaced by another implementing the same interface
- **THEN** no calling code outside the provider module changes

### Requirement: Guests without email are visibly unreachable (P0)
Since v1 is email-only, the system SHALL treat guests with no email address as unreachable for automated messaging: their message jobs are marked `skipped` (never `failed`), and admin surfaces (send screen, dashboard, guest table filter) SHALL show how many guests in an audience have no email so the host can reach them another way (printed card, shared link, manual RSVP).

#### Scenario: Send to audience with missing emails
- **WHEN** the admin composes a send to 800 guests of whom 120 have no email
- **THEN** the preview shows 680 deliverable recipients and 120 without email before sending, and the 120 jobs are recorded as skipped

### Requirement: Templates and placeholders (FR-6.3, FR-6.4, P0)
Email templates SHALL exist per purpose (invite, reminder_15, reminder_7, reminder_2, thank_you, cancel_confirm, custom) × locale (bn, en), with subject and body supporting placeholders `{{guest_name}}, {{event_title}}, {{event_date}}, {{event_time}}, {{venue}}, {{days_left}}, {{invite_url}}, {{cancel_url}}, {{map_url}}, {{couple_names}}`. Every outbound message carries the short link `/i/{token}` (under 60 characters). Emails are responsive HTML with a plain-text alternative (§8.3).

#### Scenario: Rendered reminder
- **WHEN** a T-2 reminder renders for a Bangla-preference guest
- **THEN** the bn template is used with all placeholders substituted, a working invite/cancel URL, and a plain-text part

### Requirement: Send screen (FR-4.9, P0)
The admin send flow SHALL be: choose event → choose audience (all / pending only / accepted only / by tag) → preview the rendered email → send now or schedule — showing the recipient count (and the no-email count) before sending.

#### Scenario: Preview before send
- **WHEN** the admin reaches the final send step
- **THEN** the rendered email and recipient counts are displayed and sending requires an explicit confirmation

### Requirement: Message log and retry (FR-4.10, P0)
A message log SHALL show per-message status (`queued, sending, sent, delivered, failed, skipped`), provider message ID, and error reason, with a bulk "retry failed" action.

#### Scenario: Retry failed batch
- **WHEN** the admin triggers retry on failed messages
- **THEN** each failed job is re-queued and its attempt count increments

### Requirement: Delivery and bounce webhooks (FR-6.11, §7.6, §8.3, P0)
Email provider webhooks SHALL update message status (sent → delivered / failed) and process bounces and complaints. Webhook signatures MUST be verified before processing; unverified payloads are rejected. Hard-bounced addresses are marked invalid so future sends skip them instead of wasting attempts.

#### Scenario: Status update
- **WHEN** a valid delivery receipt arrives for a known provider message ID
- **THEN** the corresponding message job status becomes `delivered`

#### Scenario: Hard bounce
- **WHEN** a hard bounce arrives for a guest's address
- **THEN** the address is marked invalid and subsequent jobs to it are skipped with a visible reason

#### Scenario: Forged webhook
- **WHEN** a webhook arrives with an invalid signature
- **THEN** it is rejected and no state changes

### Requirement: Throttling and retry policy (FR-6.10, P0)
Sends SHALL respect the email provider's rate limits with a token-bucket limiter and exponential-backoff retry — 3 attempts at +1min, +10min, +1hr — before marking the job failed. Throughput target: ≥1,000 messages per 10 minutes (§7.5).

#### Scenario: Transient provider error
- **WHEN** a send hits a temporary provider error
- **THEN** it retries on the backoff schedule and only counts as failed after 3 attempts

### Requirement: Broadcasts (FR-6.13, P1)
Admins SHALL send free-text broadcast emails to a filtered audience (e.g. "venue parking has changed").

#### Scenario: Broadcast to accepted guests
- **WHEN** the admin broadcasts to accepted Walima guests
- **THEN** one email job per matching invitation with an email address is queued

### Requirement: Opt-out (FR-6.14 adapted to email, P1)
Every email SHALL include an unsubscribe link; using it marks the guest `do_not_contact`. Opted-out guests keep a working invitation link but receive no further messages.

#### Scenario: Unsubscribe
- **WHEN** a guest clicks the unsubscribe link
- **THEN** future message jobs for that guest are skipped while their invitation link still works

### Requirement: Dry-run mode (FR-6.15, P0)
A dry-run mode SHALL render and log messages without actually sending, for pre-launch testing, controlled by configuration.

#### Scenario: Dry-run send
- **WHEN** dry-run is enabled and a wave is triggered
- **THEN** message jobs are rendered and logged with their would-be content but no provider API is called

### Requirement: Thank-you messages (FR-6.12, P2)
The system SHALL send a thank-you email the day after each event to guests who accepted.

#### Scenario: Post-event thanks
- **WHEN** the day after an event arrives
- **THEN** accepted guests with an email address receive the thank-you template once
