Skip to content

feat(mass-mail): make announcements opt-in, and stoppable at any time - #261

Merged
jouwdan merged 1 commit into
mainfrom
claude/mass-mail-gdpr-compliance-222f5h
Aug 26, 2026
Merged

feat(mass-mail): make announcements opt-in, and stoppable at any time#261
jouwdan merged 1 commit into
mainfrom
claude/mass-mail-gdpr-compliance-222f5h

Conversation

@jouwdan

@jouwdan jouwdan commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Mass mail reached every active, verified member, and there was no opt-out anywhere for it to honour — the admin screen said so in as many words. Under the GDPR that is neither consent nor a way to withdraw it. This makes the audience opt-in, and gives every message a way out.

What it does

Consent is a record, not a default. A new users.mass_mail_opt_in_at column (migration 0058, partial index) stores when consent was given; null means the member never asked. Three places require it, not one:

  • the audience counts on /admin/users/mail,
  • every 500-recipient batch a campaign claims,
  • every individual message the worker sends.

So a member who stops the announcements drops out of a campaign that is already half sent, not merely the next one.

Two ways in. The registration form carries an unticked E-mail me the board's announcements box, and the same switch sits on Notifications → Preferences. Both start off. Registering enrols nobody.

Three ways out. That preferences switch, and the unsubscribe link now in every message's footer — signed on a new mass-mail scope of the token that already carried thread, forum and email, so it needs no sign-in and still goes through the existing POST-confirm page rather than a GET a link scanner could trip.

Admin copy stopped lying. The bullet reading "There is no unsubscribe link and no per-member opt-out" is gone; the audience label reads "Every member who has asked for announcements (N)".

Notes for the reviewer

  • An existing board's audience becomes nought on deploy. That is the point — nobody in it answered the question — but it is a behaviour change an operator should be told about, and the organiser guide now leads with it.
  • The importer does not carry MyBB's allownotices across. It is an opt-out defaulted on, so importing it as consent would record an answer nobody gave. There is a parity entry saying so.
  • No List-Unsubscribe header: OutgoingMail has no headers field, so that means changing the port and every driver. The in-body link satisfies withdrawal; the header is a deliverability nicety worth its own change.
  • SSO sign-ups never pass through the registration form, so they start un-enrolled (fail-closed) and opt in from preferences.
  • The consent date is stored but not surfaced in the admin panel. An organiser answering "I never asked for this" cannot see it on the member's screen yet — small follow-up if wanted.

Verification

  • pnpm verify green; typecheck and 1,927 unit tests green after the last edits.
  • New unit coverage: audience and per-recipient consent checks, consent-date preservation on re-confirmation, the mass-mail token scope, the unsubscribe action, the preferences action, and the registration action.
  • Browser suite with JavaScript off: registration (activation-no-js), opt-in and withdrawal plus registration enrolment (notifications-no-js, two new tests), the announcements notice on /unsubscribe (session-no-js), and the admin mass mail screen including an audience-of-0 assertion (admin-tabs-no-js, 19/19).
  • That browser run earned its keep: the first draft of the "Send to" hint contained the word message, which collided with getByLabel('Message') on the same form. Reworded, re-run green.

🤖 Generated with Claude Code

https://claude.ai/code/session_015ojmw2ioA5aJq89WzMWd6V


Generated by Claude Code

Mass mail reached every active, verified member, and there was no opt-out
anywhere for it to honour. It now reaches only the members who asked for
it, and every message carries a way out.

- users.mass_mail_opt_in_at records when consent was given. The audience
  count, every batch a campaign claims and every message it sends all
  require it, so a member who stops the announcements drops out of a
  campaign that is already half sent.
- Members ask on the registration form or on their notification
  preferences — both unticked — and can withdraw from either.
- Every message carries a signed unsubscribe link on a new mass-mail
  token scope, so stopping the announcements needs no sign-in.

The organiser guide's mass mail and registration sections say all of
this, and the MyBB parity page records that allownotices is an opt-out an
import cannot honestly carry over as consent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ojmw2ioA5aJq89WzMWd6V
@jouwdan
jouwdan merged commit 4bc024b into main Aug 26, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants