# 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)

## [build 630](https://github.com/nbarrett/ngx-ramblers/actions/runs/25153396578) — [commit 4cae672](https://github.com/nbarrett/ngx-ramblers/commit/4cae672e71c59deb6a2e00751fd8ed4b78e59c39)

_____

This change finishes the contact-us email-routing story. Group admins
can now configure contact-us submissions to deliver straight to an
external address via Brevo without ever needing a Cloudflare email
routing rule. The system role that powers contact-us is renamed,
locked, and protected from being mistakenly used as both sender and
recipient. The Inbound Forwarding admin gets clearer alerts, the
Mail Settings editor stops losing the selected configuration on
refresh, and an idempotent migration brings existing sites onto the
new shape in one pass.

## Contact-us delivery

CommitteeMember now carries four optional contact-us fields
(`contactUsLabel`, `contactUsTarget`, `contactUsCustom`,
`contactUsRecipients`) so the Contact Us tab can be configured
independently of Inbound Forwarding. Where unset, contact-us falls
back to the inbound-forwarding fields, so existing sites need no
data changes to keep working.

Resolution happens in two places:

- The frontend `MailMessagingService.resolveContactRecipients` reads
the new fields with fallback and uses `contactUsLabel || fullName`
as the display name on the Brevo `to:` header.
- The contact-us send route (`server/lib/contact-us/resolve-and-send`)
resolves recipients server-side just before the Brevo call. CUSTOM
and MULTIPLE rewrite to the configured external address(es).
MEMBER_EMAIL with a linked member resolves to the member's personal
email - the personal email never leaves the server. NONE drops the
recipient and the modal rejects the submit with a clear message.

The contact-us modal title, success/sending notifications, and
zero-recipient message all use the resolved display name. A
click-stage guard in the modal also rejects any URL whose `?role=`
resolves to the role configured as `notificationConfig.senderRole` -
preventing the role from being used as both sender and recipient.
Name-agnostic, so it survives any future system-role rename.

The Contact Us tab on the role editor now binds to the new fields
via a dedicated `contactUsTargetControls` template, exposes a
"Display name (override)" input for `contactUsLabel`, and shows a
delivery-method preview block that spells out exactly what each
setting does at runtime.

## System role rename and lockdown

The "Enquiries" system role is renamed to "Contact Us" and given
`builtInRoleMapping = CONTACT_US`. New sites get this from the
template; existing sites are migrated in place. On the renamed role
the admin UI locks Full Name, Role Description, Role Type, Maps to
Built-in Role, and Link to Committee Member behind a "system role"
notice, and hides the Contact Us tab entirely (the role is the
sender, not a target). A backwards-compatible alias in
`CommitteeReferenceData.committeeMemberForRole` keeps any existing
markdown link `?role=enquiries` working alongside `?role=contact-us`
during and after rollout.



![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/0e9ad3c4-43ed-4bea-9055-8ec159d3228e.png)

*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.*

![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/cc1b6e3c-28ff-43c0-abe5-c1b5caca29a4.png)

*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.*

## Inbound Forwarding admin clean-up

The Inbound Forwarding tab no longer shows duplicate or contradictory
status alerts. Matcher comparisons normalise email casing. The
standalone "Destination Verified" alert is suppressed when a routing
alert already conveys the same fact, and `verificationEmail()` falls
back to the per-role destination for catch-all so the alert never
shows the global catch-all destination on a per-role view. The
catch-all-with-no-direct-rule branch is reframed as "Direct rule not
yet configured" with a warning style, so it reads as action-needed
rather than success. An earlier defensive elevation of forward-to-
destination rules to DIRECT was removed once it became clear it
conflated unrelated rules sharing the same destination email (e.g.
NGX Membership and Bolton both forwarding to the same gmail).



![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/b4ad592e-f8c9-4d48-87a1-9b912cd157cc.png)

*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.*

## Routing decisions extracted into a testable resolver

The matcher and resolution logic is moved into a pure helper
(`email-routing-view-resolver.ts`) covering `roleEmailFor`,
`resolveRouting`, `destinationVerificationStatusFor`, and
`multiRecipientVerificationDetails`. The component's `refreshStatus`
now consumes the resolver. A spec covers the edge cases that surfaced
during this ticket: case-insensitive matching, sibling rules with
the same forward target not conflating, worker rules, disabled
catch-all, all-matcher rules being ignored, and verification status
edge cases. 22 tests; full project suite 717/717 passing.

## Mail Settings selection fix (closes #250)

`ensureNotificationConfigSelection` was short-circuiting when the
value-comparer matched, but Angular's template `indexOf` relies on
object identity. After a fresh emission the new array contained a
new object with the same id, the comparer matched, the early return
kept the stale reference, and the dropdown's ngModel resolved to
null - "Email Configuration 0 of 12:" with a blank selection. The
early return is removed; the editor now always rebinds to the fresh
reference returned by `selectedConfigFor`, so refreshes preserve the
selection and Undo Changes works as expected.

## Migration

`20260429220000-rename-enquiries-to-contact-us` is idempotent and
combines several rewrites:

- Renames the legacy `enquiries` role to `contact-us` (preserving
any custom `fullName`), or no-ops the rename if a `contact-us`
role already exists but continues with the rest of the migration.
- Backfills `contactUsTarget` / `contactUsCustom` /
`contactUsRecipients` from `forwardEmail*` for every role where
`contactUsTarget` is unset, so the new tab opens pre-populated
with the existing inbound-forwarding settings rather than blank.
- Rewrites `role.email` from `enquiries@<domain>` to
`contact-us@<domain>` when the local-part is exactly `enquiries`.
Custom-set emails are left alone.
- Rewrites `senderRole`, `replyToRole`, `signOffRoles[]`, and
`bccRoles[]` references on every notification config from
`enquiries` to `contact-us`.
- Renames the matching Brevo sender via the SDK (best-effort: looks
up by either old or new email, updates both `name` and `email`,
skipped silently if no API key is configured or no sender matches).
Uses `configuredBrevo()` for the API key lookup.
- Tolerates re-runs from the maintenance UI's per-row retry button -
every step is a no-op when already applied.