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.
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:
MailMessagingService.resolveContactRecipients reads
the new fields with fallback and uses contactUsLabel || fullName
as the display name on the Brevo to: header.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.
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.

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.

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

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.
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.
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.
20260429220000-rename-enquiries-to-contact-us is idempotent and
combines several rewrites:
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.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.role.email from enquiries@<domain> to
contact-us@<domain> when the local-part is exactly enquiries.
Custom-set emails are left alone.senderRole, replyToRole, signOffRoles[], and
bccRoles[] references on every notification config from
enquiries to contact-us.name and email,
skipped silently if no API key is configured or no sender matches).
Uses configuredBrevo() for the API key lookup.