{"id":"69f30acaf6b905a2876c1e76","title":"2026 04 30","path":"how-to/committee/release-notes/2026-04-30","contentMarkdown":"# 30-Apr-2026 — direct delivery, sender-role lockdown, mail settings selection fix — closes [#240](https://github.com/nbarrett/ngx-ramblers/issues/240) & [#250](https://github.com/nbarrett/ngx-ramblers/issues/250)\n\n## [build 630](https://github.com/nbarrett/ngx-ramblers/actions/runs/25153396578) — [commit 4cae672](https://github.com/nbarrett/ngx-ramblers/commit/4cae672e71c59deb6a2e00751fd8ed4b78e59c39)\n\n_____\n\nThis change finishes the contact-us email-routing story. Group admins\ncan now configure contact-us submissions to deliver straight to an\nexternal address via Brevo without ever needing a Cloudflare email\nrouting rule. The system role that powers contact-us is renamed,\nlocked, and protected from being mistakenly used as both sender and\nrecipient. The Inbound Forwarding admin gets clearer alerts, the\nMail Settings editor stops losing the selected configuration on\nrefresh, and an idempotent migration brings existing sites onto the\nnew shape in one pass.\n\n## Contact-us delivery\n\nCommitteeMember now carries four optional contact-us fields\n(`contactUsLabel`, `contactUsTarget`, `contactUsCustom`,\n`contactUsRecipients`) so the Contact Us tab can be configured\nindependently of Inbound Forwarding. Where unset, contact-us falls\nback to the inbound-forwarding fields, so existing sites need no\ndata changes to keep working.\n\nResolution happens in two places:\n\n- The frontend `MailMessagingService.resolveContactRecipients` reads\nthe new fields with fallback and uses `contactUsLabel || fullName`\nas the display name on the Brevo `to:` header.\n- The contact-us send route (`server/lib/contact-us/resolve-and-send`)\nresolves recipients server-side just before the Brevo call. CUSTOM\nand MULTIPLE rewrite to the configured external address(es).\nMEMBER_EMAIL with a linked member resolves to the member's personal\nemail - the personal email never leaves the server. NONE drops the\nrecipient and the modal rejects the submit with a clear message.\n\nThe contact-us modal title, success/sending notifications, and\nzero-recipient message all use the resolved display name. A\nclick-stage guard in the modal also rejects any URL whose `?role=`\nresolves to the role configured as `notificationConfig.senderRole` -\npreventing the role from being used as both sender and recipient.\nName-agnostic, so it survives any future system-role rename.\n\nThe Contact Us tab on the role editor now binds to the new fields\nvia a dedicated `contactUsTargetControls` template, exposes a\n\"Display name (override)\" input for `contactUsLabel`, and shows a\ndelivery-method preview block that spells out exactly what each\nsetting does at runtime.\n\n## System role rename and lockdown\n\nThe \"Enquiries\" system role is renamed to \"Contact Us\" and given\n`builtInRoleMapping = CONTACT_US`. New sites get this from the\ntemplate; existing sites are migrated in place. On the renamed role\nthe admin UI locks Full Name, Role Description, Role Type, Maps to\nBuilt-in Role, and Link to Committee Member behind a \"system role\"\nnotice, and hides the Contact Us tab entirely (the role is the\nsender, not a target). A backwards-compatible alias in\n`CommitteeReferenceData.committeeMemberForRole` keeps any existing\nmarkdown link `?role=enquiries` working alongside `?role=contact-us`\nduring and after rollout.\n\n\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/0e9ad3c4-43ed-4bea-9055-8ec159d3228e.png)\n\n*Contact Us tab on a committee role - delivery dropdown, Display name override, and the new \"How this delivers\" alert confirming Brevo direct delivery with no Cloudflare rule required.*\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/cc1b6e3c-28ff-43c0-abe5-c1b5caca29a4.png)\n\n*Multiple-recipient delivery on the Contact Us tab - several addresses receive each form submission directly via Brevo, with a generated markdown contact link shown alongside.*\n\n## Inbound Forwarding admin clean-up\n\nThe Inbound Forwarding tab no longer shows duplicate or contradictory\nstatus alerts. Matcher comparisons normalise email casing. The\nstandalone \"Destination Verified\" alert is suppressed when a routing\nalert already conveys the same fact, and `verificationEmail()` falls\nback to the per-role destination for catch-all so the alert never\nshows the global catch-all destination on a per-role view. The\ncatch-all-with-no-direct-rule branch is reframed as \"Direct rule not\nyet configured\" with a warning style, so it reads as action-needed\nrather than success. An earlier defensive elevation of forward-to-\ndestination rules to DIRECT was removed once it became clear it\nconflated unrelated rules sharing the same destination email (e.g.\nNGX Membership and Bolton both forwarding to the same gmail).\n\n\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/b4ad592e-f8c9-4d48-87a1-9b912cd157cc.png)\n\n*Inbound Forwarding tab - the unchanged path for direct emails to the role address (e.g. walks-co-ordinator@your-domain.org.uk), with the Direct Rule Active status banner.*\n\n## Routing decisions extracted into a testable resolver\n\nThe matcher and resolution logic is moved into a pure helper\n(`email-routing-view-resolver.ts`) covering `roleEmailFor`,\n`resolveRouting`, `destinationVerificationStatusFor`, and\n`multiRecipientVerificationDetails`. The component's `refreshStatus`\nnow consumes the resolver. A spec covers the edge cases that surfaced\nduring this ticket: case-insensitive matching, sibling rules with\nthe same forward target not conflating, worker rules, disabled\ncatch-all, all-matcher rules being ignored, and verification status\nedge cases. 22 tests; full project suite 717/717 passing.\n\n## Mail Settings selection fix (closes #250)\n\n`ensureNotificationConfigSelection` was short-circuiting when the\nvalue-comparer matched, but Angular's template `indexOf` relies on\nobject identity. After a fresh emission the new array contained a\nnew object with the same id, the comparer matched, the early return\nkept the stale reference, and the dropdown's ngModel resolved to\nnull - \"Email Configuration 0 of 12:\" with a blank selection. The\nearly return is removed; the editor now always rebinds to the fresh\nreference returned by `selectedConfigFor`, so refreshes preserve the\nselection and Undo Changes works as expected.\n\n## Migration\n\n`20260429220000-rename-enquiries-to-contact-us` is idempotent and\ncombines several rewrites:\n\n- Renames the legacy `enquiries` role to `contact-us` (preserving\nany custom `fullName`), or no-ops the rename if a `contact-us`\nrole already exists but continues with the rest of the migration.\n- Backfills `contactUsTarget` / `contactUsCustom` /\n`contactUsRecipients` from `forwardEmail*` for every role where\n`contactUsTarget` is unset, so the new tab opens pre-populated\nwith the existing inbound-forwarding settings rather than blank.\n- Rewrites `role.email` from `enquiries@<domain>` to\n`contact-us@<domain>` when the local-part is exactly `enquiries`.\nCustom-set emails are left alone.\n- Rewrites `senderRole`, `replyToRole`, `signOffRoles[]`, and\n`bccRoles[]` references on every notification config from\n`enquiries` to `contact-us`.\n- Renames the matching Brevo sender via the SDK (best-effort: looks\nup by either old or new email, updates both `name` and `email`,\nskipped silently if no API key is configured or no sender matches).\nUses `configuredBrevo()` for the API key lookup.\n- Tolerates re-runs from the maintenance UI's per-row retry button -\nevery step is a no-op when already applied.","contentHtml":"<h1>30-Apr-2026 — direct delivery, sender-role lockdown, mail settings selection fix — closes <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/240\">#240</a> &amp; <a href=\"https://github.com/nbarrett/ngx-ramblers/issues/250\">#250</a></h1>\n<h2><a href=\"https://github.com/nbarrett/ngx-ramblers/actions/runs/25153396578\">build 630</a> — <a href=\"https://github.com/nbarrett/ngx-ramblers/commit/4cae672e71c59deb6a2e00751fd8ed4b78e59c39\">commit 4cae672</a></h2>\n<hr>\n<p>This change finishes the contact-us email-routing story. Group admins\ncan now configure contact-us submissions to deliver straight to an\nexternal address via Brevo without ever needing a Cloudflare email\nrouting rule. The system role that powers contact-us is renamed,\nlocked, and protected from being mistakenly used as both sender and\nrecipient. The Inbound Forwarding admin gets clearer alerts, the\nMail Settings editor stops losing the selected configuration on\nrefresh, and an idempotent migration brings existing sites onto the\nnew shape in one pass.</p>\n<h2>Contact-us delivery</h2>\n<p>CommitteeMember now carries four optional contact-us fields\n(<code>contactUsLabel</code>, <code>contactUsTarget</code>, <code>contactUsCustom</code>,\n<code>contactUsRecipients</code>) so the Contact Us tab can be configured\nindependently of Inbound Forwarding. Where unset, contact-us falls\nback to the inbound-forwarding fields, so existing sites need no\ndata changes to keep working.</p>\n<p>Resolution happens in two places:</p>\n<ul>\n<li>The frontend <code>MailMessagingService.resolveContactRecipients</code> reads\nthe new fields with fallback and uses <code>contactUsLabel || fullName</code>\nas the display name on the Brevo <code>to:</code> header.</li>\n<li>The contact-us send route (<code>server/lib/contact-us/resolve-and-send</code>)\nresolves recipients server-side just before the Brevo call. CUSTOM\nand MULTIPLE rewrite to the configured external address(es).\nMEMBER_EMAIL with a linked member resolves to the member&#39;s personal\nemail - the personal email never leaves the server. NONE drops the\nrecipient and the modal rejects the submit with a clear message.</li>\n</ul>\n<p>The contact-us modal title, success/sending notifications, and\nzero-recipient message all use the resolved display name. A\nclick-stage guard in the modal also rejects any URL whose <code>?role=</code>\nresolves to the role configured as <code>notificationConfig.senderRole</code> -\npreventing the role from being used as both sender and recipient.\nName-agnostic, so it survives any future system-role rename.</p>\n<p>The Contact Us tab on the role editor now binds to the new fields\nvia a dedicated <code>contactUsTargetControls</code> template, exposes a\n&quot;Display name (override)&quot; input for <code>contactUsLabel</code>, and shows a\ndelivery-method preview block that spells out exactly what each\nsetting does at runtime.</p>\n<h2>System role rename and lockdown</h2>\n<p>The &quot;Enquiries&quot; system role is renamed to &quot;Contact Us&quot; and given\n<code>builtInRoleMapping = CONTACT_US</code>. New sites get this from the\ntemplate; existing sites are migrated in place. On the renamed role\nthe admin UI locks Full Name, Role Description, Role Type, Maps to\nBuilt-in Role, and Link to Committee Member behind a &quot;system role&quot;\nnotice, and hides the Contact Us tab entirely (the role is the\nsender, not a target). A backwards-compatible alias in\n<code>CommitteeReferenceData.committeeMemberForRole</code> keeps any existing\nmarkdown link <code>?role=enquiries</code> working alongside <code>?role=contact-us</code>\nduring and after rollout.</p>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/0e9ad3c4-43ed-4bea-9055-8ec159d3228e.png\" alt=\"\"></p>\n<p><em>Contact Us tab on a committee role - delivery dropdown, Display name override, and the new &quot;How this delivers&quot; alert confirming Brevo direct delivery with no Cloudflare rule required.</em></p>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/cc1b6e3c-28ff-43c0-abe5-c1b5caca29a4.png\" alt=\"\"></p>\n<p><em>Multiple-recipient delivery on the Contact Us tab - several addresses receive each form submission directly via Brevo, with a generated markdown contact link shown alongside.</em></p>\n<h2>Inbound Forwarding admin clean-up</h2>\n<p>The Inbound Forwarding tab no longer shows duplicate or contradictory\nstatus alerts. Matcher comparisons normalise email casing. The\nstandalone &quot;Destination Verified&quot; alert is suppressed when a routing\nalert already conveys the same fact, and <code>verificationEmail()</code> falls\nback to the per-role destination for catch-all so the alert never\nshows the global catch-all destination on a per-role view. The\ncatch-all-with-no-direct-rule branch is reframed as &quot;Direct rule not\nyet configured&quot; with a warning style, so it reads as action-needed\nrather than success. An earlier defensive elevation of forward-to-\ndestination rules to DIRECT was removed once it became clear it\nconflated unrelated rules sharing the same destination email (e.g.\nNGX Membership and Bolton both forwarding to the same gmail).</p>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/b4ad592e-f8c9-4d48-87a1-9b912cd157cc.png\" alt=\"\"></p>\n<p><em>Inbound Forwarding tab - the unchanged path for direct emails to the role address (e.g. <a href=\"mailto:walks-co-ordinator@your-domain.org.uk\">walks-co-ordinator@your-domain.org.uk</a>), with the Direct Rule Active status banner.</em></p>\n<h2>Routing decisions extracted into a testable resolver</h2>\n<p>The matcher and resolution logic is moved into a pure helper\n(<code>email-routing-view-resolver.ts</code>) covering <code>roleEmailFor</code>,\n<code>resolveRouting</code>, <code>destinationVerificationStatusFor</code>, and\n<code>multiRecipientVerificationDetails</code>. The component&#39;s <code>refreshStatus</code>\nnow consumes the resolver. A spec covers the edge cases that surfaced\nduring this ticket: case-insensitive matching, sibling rules with\nthe same forward target not conflating, worker rules, disabled\ncatch-all, all-matcher rules being ignored, and verification status\nedge cases. 22 tests; full project suite 717/717 passing.</p>\n<h2>Mail Settings selection fix (closes #250)</h2>\n<p><code>ensureNotificationConfigSelection</code> was short-circuiting when the\nvalue-comparer matched, but Angular&#39;s template <code>indexOf</code> relies on\nobject identity. After a fresh emission the new array contained a\nnew object with the same id, the comparer matched, the early return\nkept the stale reference, and the dropdown&#39;s ngModel resolved to\nnull - &quot;Email Configuration 0 of 12:&quot; with a blank selection. The\nearly return is removed; the editor now always rebinds to the fresh\nreference returned by <code>selectedConfigFor</code>, so refreshes preserve the\nselection and Undo Changes works as expected.</p>\n<h2>Migration</h2>\n<p><code>20260429220000-rename-enquiries-to-contact-us</code> is idempotent and\ncombines several rewrites:</p>\n<ul>\n<li>Renames the legacy <code>enquiries</code> role to <code>contact-us</code> (preserving\nany custom <code>fullName</code>), or no-ops the rename if a <code>contact-us</code>\nrole already exists but continues with the rest of the migration.</li>\n<li>Backfills <code>contactUsTarget</code> / <code>contactUsCustom</code> /\n<code>contactUsRecipients</code> from <code>forwardEmail*</code> for every role where\n<code>contactUsTarget</code> is unset, so the new tab opens pre-populated\nwith the existing inbound-forwarding settings rather than blank.</li>\n<li>Rewrites <code>role.email</code> from <code>enquiries@&lt;domain&gt;</code> to\n<code>contact-us@&lt;domain&gt;</code> when the local-part is exactly <code>enquiries</code>.\nCustom-set emails are left alone.</li>\n<li>Rewrites <code>senderRole</code>, <code>replyToRole</code>, <code>signOffRoles[]</code>, and\n<code>bccRoles[]</code> references on every notification config from\n<code>enquiries</code> to <code>contact-us</code>.</li>\n<li>Renames the matching Brevo sender via the SDK (best-effort: looks\nup by either old or new email, updates both <code>name</code> and <code>email</code>,\nskipped silently if no API key is configured or no sender matches).\nUses <code>configuredBrevo()</code> for the API key lookup.</li>\n<li>Tolerates re-runs from the maintenance UI&#39;s per-row retry button -\nevery step is a no-op when already applied.</li>\n</ul>\n"}