- server/data.js: preserve source/htmlBody/inboundTo/messageId across
server restarts (sanitizeLoadedContactSubmissions was silently
dropping them on reload from disk)
- cloudflare/email-worker.js: rewrite MIME parsing to split on the
actual boundary marker instead of any literal "--", unfold
multi-line headers, and correctly recombine multi-byte UTF-8 in
quoted-printable decoding
- server/routes/inbound-email.js: validate Message-ID against RFC 5322
grammar before storing/using it, and compare the webhook secret with
timingSafeEqual to match the rest of the codebase's auth checks
- server/routes/contact.js: re-validate messageId at the point it's
injected into outgoing In-Reply-To/References headers; move the
allowed reply-from addresses into a shared config constant
- src/AdminPage.tsx: 30s inbox poll now syncs field updates (e.g.
archived) on already-loaded submissions instead of only appending
new ones; consolidate the duplicated from-address list
- .claude/launch.json: add a vite dev server preview config used to
verify these changes
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Reply composer shows a From dropdown (hello@ or nate@), defaulting
to whichever address the inbound email was sent to
- Server validates the chosen address against an allowlist before sending
- Inbox list shows colored address badges: blue for hello@, purple for nate@
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When replying to an inbound email that has a messageId, the outgoing
Resend payload now includes In-Reply-To and References headers so the
reply threads correctly in Gmail and other email clients.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Newsletter nudge — was calling /api/study-account/profile (wrong endpoint, ignored subscription). Now correctly calls /api/study-account/preferences with PATCH.
Security:
Email regex — replaced the permissive [^\s@]+@[^\s@]+ pattern with a proper RFC-compliant regex in contact.js and downloads.js
Avatar magic bytes — server now checks actual PNG/JPEG/GIF/WEBP header bytes, not just the data URL prefix
Certificate rate limit — public /api/public/certificate/:token now has a 30 req/15min limiter
Session absolute TTL — admin sessions now have a 30-day hard cap; a stolen token can no longer be kept alive indefinitely by passive reads
Account lockout — 5 failed logins locks a study account for 1 hour
CSP headers — Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy headers added globally
Data integrity:
Cascade delete — deleting a study account now also removes their certificates, community posts, comments, and progress file
UX / reliability:
Escape key on modals — all 3 modal groups (study index, notes, account) now close on Escape
Display name min-length — empty spaces-only names rejected; if provided, must be ≥2 chars
Note save rate limit — 30 saves/minute per user max
Analytics fetch timeout — 5s AbortController so a hanging server doesn't block the browser indefinitely
Email validation on signup — frontend catches bad email formats before hitting the server
Cleanup:
Deduplicated download forms — StudyDownloadForm and ResourceDownloadForm now share a single DownloadForm base; both are now thin wrappers
---
**🐛 Bugs fixed**
- Study hub/lesson pages turned gold — CSS merge error with `.study-index-page` fixed
- Lesson announcements not saving — `announcement` field missing from `sanitizeColossiansStudySections` whitelist
- 2FA email method blocked with "not configured" — guard checked `totpSecret` instead of `twoFaMethod`
- `STUDY_REMINDERS_FILE` constant never defined — server would crash on first reminder write
- `sanitizeStudyUsers` stripped all 2FA fields — data lost on every restart
- `scheduleStudyReminders` missing from refactored modules
**✨ Features added**
- Commentary supports paragraph formatting (double newline = new paragraph)
- Lesson announcement banner — full-width gold stripe at top of lesson page
- "Take the Quiz" renamed to "Discussion Questions" throughout
- Students must complete checkpoint before unlocking next lesson (lesson list + Next Lesson button both gate on it)
- No checkpoint questions → simple Mark as Completed button as fallback
- Truth For Life syndication widget on homepage
- Newsletter welcome email updated to your new HTML template
**🔧 Maintenance**
- `server.js` refactored from 6,013 lines into 12 focused modules