Feature: Member Email Broadcasting System #473

Open
opened 2026-04-16 06:55:59 +00:00 by jjalangtry · 0 comments
jjalangtry commented 2026-04-16 06:55:59 +00:00 (Migrated from github.com)

Problem

SSE has no way to reach members via email for official club communications. Elections, constitutional amendments, event announcements, and emergencies require a reliable, officer-controlled broadcast tool with member consent (provided via Quest #2 — issue #474).

Proposed Solution

Add a Member Email Broadcast page to the officer dashboard. Officers compose a rich HTML email, pick a category, preview it, see a live recipient count, and send. A visually distinct emergency broadcast sends to all members regardless of their preferences.


UI — /dashboard/email-broadcast (officer-only)

Standard broadcast:

  • Category dropdown: Elections | Amendments | Events | General Announcements
  • Subject line input
  • Rich text editor (Tiptap or react-quill) — bold, italic, headings, lists, links, images
  • Live badge: "Will reach N members" (updates on category change via GET /api/email/broadcast/count)
  • Preview button → modal showing rendered HTML
  • Send button → confirmation modal ("Send to N members?") → fires on confirm

Emergency broadcast (separate section, red border):

  • Label: "Send to ALL members — ignores email preferences"
  • Subject + rich text editor
  • Requires typing SEND EMERGENCY EMAIL before the Send button enables
  • Fires on confirm, no category filtering

API Routes

Method Route Purpose
GET /api/email/broadcast/count?category= Return recipient count for a given category
POST /api/email/broadcast Send to opted-in members for a category
POST /api/email/broadcast/emergency Send to all members, bypass preferences

Broadcast logic (both routes):

  1. Officer auth check (403 otherwise)
  2. Standard: query Users joined to EmailPreferences where the category flag is true
  3. Emergency: query all Users
  4. Batch send via existing sendEmail() from next/lib/email/index.ts
  5. Every email includes an unsubscribe footer linking to /profile/[id]?tab=quests (where the email consent quest lives)
  6. Write an EmailBroadcastLog entry
  7. Return { sent: N, failed: M }

New Prisma model:

model EmailBroadcastLog {
  id             String   @id @default(cuid())
  sentByUserId   String
  sentBy         User     @relation(fields: [sentByUserId], references: [id])
  category       String   // "elections" | "amendments" | "events" | "general" | "emergency"
  subject        String
  recipientCount Int
  sentAt         DateTime @default(now())
}

EmailPreferences model lives in issue #474 (quests) — this feature depends on it being in place.


Email Template

  • From: "Society of Software Engineers" <no-reply@sse.rit.edu>
  • Footer: SSE name, current semester, unsubscribe link
  • HTML body from editor output; plain-text fallback auto-stripped from HTML

Alternatives Considered

  • Mailchimp / SendGrid — adds cost and a second system to manage; SMTP is sufficient for student org volume
  • Discord announcements only — not every member is in the server; email is needed for official/legal notices (elections, amendments)

Scope

In scope:

  • Officer-only broadcast page at /dashboard/email-broadcast
  • Category selector + rich text editor + preview modal
  • Live recipient count
  • Emergency broadcast with typed confirmation
  • EmailBroadcastLog model + audit trail
  • Unsubscribe link in every email

Out of scope:

  • Scheduled / future-dated sends
  • Open/click tracking
  • Member-to-officer reply threads

Acceptance Criteria

  • Non-officers receive 403 from all broadcast API routes
  • Category dropdown filters recipients by their EmailPreferences flags
  • "Will reach N members" count updates dynamically when category changes
  • Preview modal renders the exact HTML that will be sent
  • Send is blocked until officer confirms in the modal
  • Every sent email contains a working unsubscribe link
  • Emergency broadcast is visually separated, requires typing SEND EMERGENCY EMAIL, and sends to all users
  • Every send (including emergency) writes an EmailBroadcastLog row
  • { sent: N, failed: M } is returned and surfaced to the officer after send

Additional Context

  • Epic: #472
  • Depends on EmailPreferences model from issue #474 (Membership Quests)
  • Existing email lib: next/lib/email/index.ts + next/lib/email/smtp.ts
  • Existing send endpoint for reference: next/app/api/email/send/route.ts
  • Officer auth middleware pattern: next/lib/middlewares/
## Problem SSE has no way to reach members via email for official club communications. Elections, constitutional amendments, event announcements, and emergencies require a reliable, officer-controlled broadcast tool with member consent (provided via Quest #2 — issue #474). ## Proposed Solution Add a **Member Email Broadcast** page to the officer dashboard. Officers compose a rich HTML email, pick a category, preview it, see a live recipient count, and send. A visually distinct emergency broadcast sends to **all** members regardless of their preferences. --- ### UI — `/dashboard/email-broadcast` (officer-only) **Standard broadcast:** - Category dropdown: `Elections` | `Amendments` | `Events` | `General Announcements` - Subject line input - Rich text editor (Tiptap or react-quill) — bold, italic, headings, lists, links, images - Live badge: "Will reach N members" (updates on category change via `GET /api/email/broadcast/count`) - **Preview** button → modal showing rendered HTML - **Send** button → confirmation modal ("Send to N members?") → fires on confirm **Emergency broadcast** (separate section, red border): - Label: "Send to ALL members — ignores email preferences" - Subject + rich text editor - Requires typing `SEND EMERGENCY EMAIL` before the Send button enables - Fires on confirm, no category filtering --- ### API Routes | Method | Route | Purpose | |--------|-------|---------| | `GET` | `/api/email/broadcast/count?category=` | Return recipient count for a given category | | `POST` | `/api/email/broadcast` | Send to opted-in members for a category | | `POST` | `/api/email/broadcast/emergency` | Send to all members, bypass preferences | **Broadcast logic (both routes):** 1. Officer auth check (403 otherwise) 2. Standard: query `User`s joined to `EmailPreferences` where the category flag is `true` 3. Emergency: query all `User`s 4. Batch send via existing `sendEmail()` from `next/lib/email/index.ts` 5. Every email includes an unsubscribe footer linking to `/profile/[id]?tab=quests` (where the email consent quest lives) 6. Write an `EmailBroadcastLog` entry 7. Return `{ sent: N, failed: M }` **New Prisma model:** ```prisma model EmailBroadcastLog { id String @id @default(cuid()) sentByUserId String sentBy User @relation(fields: [sentByUserId], references: [id]) category String // "elections" | "amendments" | "events" | "general" | "emergency" subject String recipientCount Int sentAt DateTime @default(now()) } ``` `EmailPreferences` model lives in issue #474 (quests) — this feature depends on it being in place. --- ### Email Template - From: `"Society of Software Engineers" <no-reply@sse.rit.edu>` - Footer: SSE name, current semester, unsubscribe link - HTML body from editor output; plain-text fallback auto-stripped from HTML --- ## Alternatives Considered - **Mailchimp / SendGrid** — adds cost and a second system to manage; SMTP is sufficient for student org volume - **Discord announcements only** — not every member is in the server; email is needed for official/legal notices (elections, amendments) ## Scope **In scope:** - Officer-only broadcast page at `/dashboard/email-broadcast` - Category selector + rich text editor + preview modal - Live recipient count - Emergency broadcast with typed confirmation - `EmailBroadcastLog` model + audit trail - Unsubscribe link in every email **Out of scope:** - Scheduled / future-dated sends - Open/click tracking - Member-to-officer reply threads ## Acceptance Criteria - [ ] Non-officers receive 403 from all broadcast API routes - [ ] Category dropdown filters recipients by their `EmailPreferences` flags - [ ] "Will reach N members" count updates dynamically when category changes - [ ] Preview modal renders the exact HTML that will be sent - [ ] Send is blocked until officer confirms in the modal - [ ] Every sent email contains a working unsubscribe link - [ ] Emergency broadcast is visually separated, requires typing `SEND EMERGENCY EMAIL`, and sends to all users - [ ] Every send (including emergency) writes an `EmailBroadcastLog` row - [ ] `{ sent: N, failed: M }` is returned and surfaced to the officer after send ## Additional Context - Epic: #472 - Depends on `EmailPreferences` model from issue #474 (Membership Quests) - Existing email lib: `next/lib/email/index.ts` + `next/lib/email/smtp.ts` - Existing send endpoint for reference: `next/app/api/email/send/route.ts` - Officer auth middleware pattern: `next/lib/middlewares/`
Sign in to join this conversation.
No milestone
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
SSE/WebsiteTheSSEquel#473
No description provided.