25-May-2026 — consent and unsubscribe suppression controls for composer recipients #279
build 654 — commit 8c5e2b9
Email composer recipient suppression controls
Gives admins control over who can be emailed from the composer, covering Head Office marketing consent and unsubscribe/block state. Members who should not be emailed are shown clearly in the recipient picker rather than silently disappearing, and the same validation plus an authoritative server-side guard stop them being sent to on every path - picker, manual, and the direct single-member flow.
Mail settings (Brevo, Global Settings)
- Respect Head Office marketing consent (default on): members without
emailMarketingConsentappear in the picker as a disabled "members without Head Office consent" group and cannot be selected. Turn it off to allow selecting them. - Block sending to unsubscribed or blocked members (default off, overridable): when on, unsubscribed/blocked members are disabled in the picker and skipped at send. Off by default because a local unsubscribe is only pushed to Brevo when you run Update Brevo Mailing Lists, so enforcement is opt-in.
- Both settings are defaulted to a concrete value via config migration so the checkbox state always matches the behaviour.

Above: Screenshot of Global Mail Settings
Composer recipient picker
- No-consent, unsubscribed and blocked members are grouped and labelled instead of dropped, so it is obvious they exist and why they cannot be selected.
- Unsubscribed/blocked entries show the date, for example "(unsubscribed 23 May 2026)" or "(blocked 23 May 2026)".
- Auto-fill pre-filters skip disabled members, and any previously saved selection of a now-disabled member is removed.
- Typed search text clears after each member is added.
Recipient selection when "Respect Head Office marketing consent" is true
- See below for a screenshot of when individual members within a mailing list are displayed within the email composer.
- Only the members that have given Head Office consent are selectable. Members without Head Office consent remain disabled and non-selectable.

Recipient selection when "Respect Head Office marketing consent" is false
- See below for a screenshot of when individual members within a mailing list are displayed within the email composer.
- Even though members are grouped according to whether they have given Head Office consent, they can still be included in the member selection.

Reliable unsubscribe detection
- A bare
subscribed: falsecannot distinguish "actively unsubscribed" from "never subscribed", so anunsubscribedAttimestamp records genuine unsubscribe events only. unsubscribedAtis stamped on every true-to-false transition: branded unsubscribe page (per-list and legacy all-lists), Brevo events webhook, Brevo unsubscribes sync, and admin manual un-tick in the member modal (cleared on re-subscribe). Persisted in the Mongo member schema.- Detection requires
subscribed: falsewithunsubscribedAt, so never-subscribed entries no longer produce false positives. - The reference list is the narrowed list when one is chosen, the email type's default list in "Any member" mode, otherwise fully unsubscribed (off every list).
Unsubscribe link targets the right list
- The branded unsubscribe link previously used the email's configured default list (e.g. All Members) even for a member only ever on another list, so the unsubscribe was a no-op on their real subscription. It now resolves to the list the member is actually on when they are not subscribed to the requested list.
Validation and server-side guard on every path
- Sent-status (already-received) dedup is driven by the email type's configured member selection: it applies only to the targeted cohorts (Expired, Recently Added, Missing From Bulk Load) and never to Mailing List or None, so a newsletter goes to the whole list each time.
- The direct single-member flow now uses the same recipients-step validation as the list flow: a chosen member who is blocked, unsubscribed or without consent produces a validation error and disables Next, rather than slipping through.
batch-transactional-sendskips, at send time, any member who is blocked or unsubscribed (when the block toggle is on) or without Head Office consent (when the consent toggle is on), with aSkippedstatus and reason- an authoritative backstop regardless of how the member was chosen. The progress bar shows a "N skipped" badge and per-recipient detail.
Historical data backfill
- Migration
20260524000000-backfill-subscription-unsubscribed-atpopulatesunsubscribedAtfor pre-existing branded unsubscribes from the mail-list-audit history (per-list and legacy), so past unsubscribes are respected too. Runs automatically in the background on server start.
Member modal
- A titled caveat ("Subscriptions do not guarantee delivery") under the subscription checkboxes notes that a ticked list does not guarantee delivery when the member is blocked or has not given Head Office consent.
- The Brevo "View contact details" button sits on the same line as the Subscription audit / Brevo activity toggle, right-aligned; vertical spacing equalised.
Shareable deep links
- Opening a member's edit modal sets a
member-idquery param (membership number, or member id when there is no membership number) and the active modal tab on open, alongside the existing Brevo sub-tab param, so the URL recreates the exact view and is shareable. Closing clears them.
Other composer refinements
- Recipient sections (Send to an email address, Recipients for this send, Pick from previously-saved addresses, include group members) use a consistent collapsible framed layout; Global Settings checkboxes padded.
- Contact action dropdown tooltips placed to the right so they do not overlap the member name.
Also bundled (separate work)
- Brevo contact activity timeline refresh: colour-coded event markers, inline subject, tidier layout and preview toggle.
- Removal of unused Angular imports in a few components.
Member modal refinements (#279 follow-up)
Tab restructure
- Combined the Membership and Ramblers tabs into one Ramblers Membership tab, with framed Membership, Email marketing consent and Walks Manager sections.
- Moved Email marketing consent onto Ramblers Membership, since it is Ramblers/Head Office provided data rather than email-provider state.
- Renamed the provider tab (Brevo or Mailchimp) to "Email & subscriptions" and wired it to the modal-tab query param so it deep-links and reopens like the other tabs.
- Sections across the Email & subscriptions and Ramblers Membership tabs use the shared thumbnail-heading-frame for one consistent framed style.
Consistent, DRY tables
- The Audit tab's member-update-audit table now uses the same shared sortable-table component as the Subscription audit table, with identical styling and sort behaviour. Removed the bespoke table markup and the duplicate client-side sort logic.
Deep-link reliability
- Clicking Edit now reliably sets member-id in the URL: the modal-open navigation is awaited before the modal renders, and the tab navigation only fires on a real tab change, so the two no longer race and strip member-id.
Brevo activity timeline
- Centred the timeline rail so status dots sit on the line rather than being clipped to its left.
Window fit
- Constrained the member modal to the window height so the title bar and the Save / Delete / Copy / Cancel buttons stay pinned and the body scrolls within, rather than the footer dropping below the viewport on tall tabs like Email & subscriptions. The Brevo Recent history keeps its own scroll area.
Jump to consent from the subscription warning
- When a member cannot be emailed because Head Office marketing consent has not been given, the "Subscriptions do not guarantee delivery" note now shows a link that opens the Ramblers Membership tab, where the consent detail now lives.
Tab row, frames and Preferences
- The member modal's tab row no longer wraps to a second line.
- Section frames grow to wrap their own content rather than being held to a fixed height (the earlier fit-to-window flex experiment was dropped); the body scrolls as a whole with the title and footer pinned.
- The standalone Preferences tab is removed
- its profile-settings-confirmed and hide-surname-from-non-logged-in-members settings now sit in a framed section at the bottom of the Contact tab, leaving one fewer tab.