{"id":"6a1415f4441399f9b098d591","title":"2026 05 25","path":"how-to/committee/release-notes/2026-05-25","contentMarkdown":"# 25-May-2026 — consent and unsubscribe suppression controls for composer recipients [#279](https://github.com/nbarrett/ngx-ramblers/issues/279)\n\n## [build 654](https://github.com/nbarrett/ngx-ramblers/actions/runs/26392751272) — [commit 8c5e2b9](https://github.com/nbarrett/ngx-ramblers/commit/8c5e2b913e28ae4e208a2993d4d5de0c548edc6e)\n\n_____\n\n## Email composer recipient suppression controls\n\nGives 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.\n\n### Mail settings (Brevo, Global Settings)\n\n- **Respect Head Office marketing consent** (default on): members without `emailMarketingConsent` appear in the picker as a disabled \"members without Head Office consent\" group and cannot be selected. Turn it off to allow selecting them.\n- **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.\n- Both settings are defaulted to a concrete value via config migration so the checkbox state always matches the behaviour.\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/beb21959-a590-4b85-9a61-af58df96dd76.jpeg)\n\n_Above: Screenshot of Global Mail Settings_\n\n### Composer recipient picker\n\n- 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.\n- Unsubscribed/blocked entries show the date, for example \"(unsubscribed 23 May 2026)\" or \"(blocked 23 May 2026)\".\n- Auto-fill pre-filters skip disabled members, and any previously saved selection of a now-disabled member is removed.\n- Typed search text clears after each member is added.\n#### Recipient selection when \"Respect Head Office marketing consent\" is true\n- See below for a screenshot of when individual members within a mailing list are displayed within the email composer. \n- Only the members that have given Head Office consent are selectable. Members without Head Office consent remain disabled and non-selectable.\n\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/78355952-bab6-4f23-9e94-59d918d9f2f2.jpeg)\n\n#### Recipient selection when \"Respect Head Office marketing consent\" is false\n- See below for a screenshot of when individual members within a mailing list are displayed within the email composer. \n- Even though members are grouped according to whether they have given Head Office consent,  they can still be included in the member selection.\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/b0d52da3-24d7-463d-8775-29cb9a53139d.jpeg)\n\n\n### Reliable unsubscribe detection\n\n- A bare `subscribed: false` cannot distinguish \"actively unsubscribed\" from \"never subscribed\", so an `unsubscribedAt` timestamp records genuine unsubscribe events only.\n- `unsubscribedAt` is 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.\n- Detection requires `subscribed: false` with `unsubscribedAt`, so never-subscribed entries no longer produce false positives.\n- 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).\n\n### Unsubscribe link targets the right list\n\n- 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.\n\n### Validation and server-side guard on every path\n\n- 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.\n- 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.\n- `batch-transactional-send` skips, 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 a `Skipped` status and reason\n- an authoritative backstop regardless of how the member was chosen. The progress bar shows a \"N skipped\" badge and per-recipient detail.\n\n### Historical data backfill\n\n- Migration `20260524000000-backfill-subscription-unsubscribed-at` populates `unsubscribedAt` for 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.\n\n### Member modal\n\n- 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.\n- The Brevo \"View contact details\" button sits on the same line as the Subscription audit / Brevo activity toggle, right-aligned; vertical spacing equalised.\n\n### Shareable deep links\n\n- Opening a member's edit modal sets a `member-id` query 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.\n\n### Other composer refinements\n\n- 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.\n- Contact action dropdown tooltips placed to the right so they do not overlap the member name.\n\n### Also bundled (separate work)\n\n- Brevo contact activity timeline refresh: colour-coded event markers, inline subject, tidier layout and preview toggle.\n- Removal of unused Angular imports in a few components.\n\n## Member modal refinements (#279 follow-up)\n\n### Tab restructure\n\n- Combined the Membership and Ramblers tabs into one Ramblers Membership tab, with framed Membership, Email marketing consent and Walks Manager sections.\n- Moved Email marketing consent onto Ramblers Membership, since it is Ramblers/Head Office provided data rather than email-provider state.\n- 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.\n- Sections across the Email & subscriptions and Ramblers Membership tabs use the shared thumbnail-heading-frame for one consistent framed style.\n\n### Consistent, DRY tables\n\n- 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.\n\n### Deep-link reliability\n\n- 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.\n\n### Brevo activity timeline\n\n- Centred the timeline rail so status dots sit on the line rather than being clipped to its left.\n\n### Window fit\n\n- 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.\n\n### Jump to consent from the subscription warning\n\n- 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.\n\n### Tab row, frames and Preferences\n\n- The member modal's tab row no longer wraps to a second line.\n- 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.\n- The standalone Preferences tab is removed\n- 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.","contentHtml":"<h1>25-May-2026 — consent and unsubscribe suppression controls for composer recipients <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/279\">#279</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/26392751272\">build 654</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/8c5e2b913e28ae4e208a2993d4d5de0c548edc6e\">commit 8c5e2b9</a></h2>\n<hr>\n<h2>Email composer recipient suppression controls</h2>\n<p>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.</p>\n<h3>Mail settings (Brevo, Global Settings)</h3>\n<ul>\n<li><strong>Respect Head Office marketing consent</strong> (default on): members without <code>emailMarketingConsent</code> appear in the picker as a disabled &quot;members without Head Office consent&quot; group and cannot be selected. Turn it off to allow selecting them.</li>\n<li><strong>Block sending to unsubscribed or blocked members</strong> (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.</li>\n<li>Both settings are defaulted to a concrete value via config migration so the checkbox state always matches the behaviour.</li>\n</ul>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/beb21959-a590-4b85-9a61-af58df96dd76.jpeg\" alt=\"\"></p>\n<p><em>Above: Screenshot of Global Mail Settings</em></p>\n<h3>Composer recipient picker</h3>\n<ul>\n<li>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.</li>\n<li>Unsubscribed/blocked entries show the date, for example &quot;(unsubscribed 23 May 2026)&quot; or &quot;(blocked 23 May 2026)&quot;.</li>\n<li>Auto-fill pre-filters skip disabled members, and any previously saved selection of a now-disabled member is removed.</li>\n<li>Typed search text clears after each member is added.</li>\n</ul>\n<h4>Recipient selection when &quot;Respect Head Office marketing consent&quot; is true</h4>\n<ul>\n<li>See below for a screenshot of when individual members within a mailing list are displayed within the email composer. </li>\n<li>Only the members that have given Head Office consent are selectable. Members without Head Office consent remain disabled and non-selectable.</li>\n</ul>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/78355952-bab6-4f23-9e94-59d918d9f2f2.jpeg\" alt=\"\"></p>\n<h4>Recipient selection when &quot;Respect Head Office marketing consent&quot; is false</h4>\n<ul>\n<li>See below for a screenshot of when individual members within a mailing list are displayed within the email composer. </li>\n<li>Even though members are grouped according to whether they have given Head Office consent,  they can still be included in the member selection.</li>\n</ul>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/b0d52da3-24d7-463d-8775-29cb9a53139d.jpeg\" alt=\"\"></p>\n<h3>Reliable unsubscribe detection</h3>\n<ul>\n<li>A bare <code>subscribed: false</code> cannot distinguish &quot;actively unsubscribed&quot; from &quot;never subscribed&quot;, so an <code>unsubscribedAt</code> timestamp records genuine unsubscribe events only.</li>\n<li><code>unsubscribedAt</code> is 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.</li>\n<li>Detection requires <code>subscribed: false</code> with <code>unsubscribedAt</code>, so never-subscribed entries no longer produce false positives.</li>\n<li>The reference list is the narrowed list when one is chosen, the email type&#39;s default list in &quot;Any member&quot; mode, otherwise fully unsubscribed (off every list).</li>\n</ul>\n<h3>Unsubscribe link targets the right list</h3>\n<ul>\n<li>The branded unsubscribe link previously used the email&#39;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.</li>\n</ul>\n<h3>Validation and server-side guard on every path</h3>\n<ul>\n<li>Sent-status (already-received) dedup is driven by the email type&#39;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.</li>\n<li>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.</li>\n<li><code>batch-transactional-send</code> skips, 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 a <code>Skipped</code> status and reason</li>\n<li>an authoritative backstop regardless of how the member was chosen. The progress bar shows a &quot;N skipped&quot; badge and per-recipient detail.</li>\n</ul>\n<h3>Historical data backfill</h3>\n<ul>\n<li>Migration <code>20260524000000-backfill-subscription-unsubscribed-at</code> populates <code>unsubscribedAt</code> for 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.</li>\n</ul>\n<h3>Member modal</h3>\n<ul>\n<li>A titled caveat (&quot;Subscriptions do not guarantee delivery&quot;) 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.</li>\n<li>The Brevo &quot;View contact details&quot; button sits on the same line as the Subscription audit / Brevo activity toggle, right-aligned; vertical spacing equalised.</li>\n</ul>\n<h3>Shareable deep links</h3>\n<ul>\n<li>Opening a member&#39;s edit modal sets a <code>member-id</code> query 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.</li>\n</ul>\n<h3>Other composer refinements</h3>\n<ul>\n<li>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.</li>\n<li>Contact action dropdown tooltips placed to the right so they do not overlap the member name.</li>\n</ul>\n<h3>Also bundled (separate work)</h3>\n<ul>\n<li>Brevo contact activity timeline refresh: colour-coded event markers, inline subject, tidier layout and preview toggle.</li>\n<li>Removal of unused Angular imports in a few components.</li>\n</ul>\n<h2>Member modal refinements (#279 follow-up)</h2>\n<h3>Tab restructure</h3>\n<ul>\n<li>Combined the Membership and Ramblers tabs into one Ramblers Membership tab, with framed Membership, Email marketing consent and Walks Manager sections.</li>\n<li>Moved Email marketing consent onto Ramblers Membership, since it is Ramblers/Head Office provided data rather than email-provider state.</li>\n<li>Renamed the provider tab (Brevo or Mailchimp) to &quot;Email &amp; subscriptions&quot; and wired it to the modal-tab query param so it deep-links and reopens like the other tabs.</li>\n<li>Sections across the Email &amp; subscriptions and Ramblers Membership tabs use the shared thumbnail-heading-frame for one consistent framed style.</li>\n</ul>\n<h3>Consistent, DRY tables</h3>\n<ul>\n<li>The Audit tab&#39;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.</li>\n</ul>\n<h3>Deep-link reliability</h3>\n<ul>\n<li>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.</li>\n</ul>\n<h3>Brevo activity timeline</h3>\n<ul>\n<li>Centred the timeline rail so status dots sit on the line rather than being clipped to its left.</li>\n</ul>\n<h3>Window fit</h3>\n<ul>\n<li>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 &amp; subscriptions. The Brevo Recent history keeps its own scroll area.</li>\n</ul>\n<h3>Jump to consent from the subscription warning</h3>\n<ul>\n<li>When a member cannot be emailed because Head Office marketing consent has not been given, the &quot;Subscriptions do not guarantee delivery&quot; note now shows a link that opens the Ramblers Membership tab, where the consent detail now lives.</li>\n</ul>\n<h3>Tab row, frames and Preferences</h3>\n<ul>\n<li>The member modal&#39;s tab row no longer wraps to a second line.</li>\n<li>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.</li>\n<li>The standalone Preferences tab is removed</li>\n<li>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.</li>\n</ul>\n"}