30-Apr-2026 — direct delivery, sender-role lockdown, mail settings selection fix — closes #240 & #250

build 630commit 4cae672


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

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.

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

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: