# 16-Jun-2026 — inbox-aware email routing and Contact Us delivery [#278](https://github.com/nbarrett/ngx-ramblers/issues/278)

## [build 698](https://github.com/nbarrett/ngx-ramblers/actions/runs/27590219957) — [commit 0fa5561](https://github.com/nbarrett/ngx-ramblers/commit/0fa55617496e02503b37568e37ac0013994814bf)

_____

> 📖 New to this feature? See [Setting up a Gmail inbox for committee replies](https://ngx-ramblers.org.uk/how-to/technical-articles/2026-05-29-gmail-inbox-setup) for the step-by-step setup guide.

Reworks how committee roles route email - outbound senders, inbound
forwarding, and the Contact Us form - so the admin screens describe what
actually happens for each group's setup: a connected Gmail inbox, a
Cloudflare catch-all, fan-out to several people, or individual personal
addresses. Part of the Gmail inbox work in #278.

## What's new for committee admins

### Contact Us routing, per role

Each role has a dedicated Contact Us tab controlling where contact-us form
submissions for that role are sent, separate from the role's Inbound
Forwarding.

- New option "The role's own address": contact-us is sent to the role
address (e.g. walks@yourdomain.org.uk) and then follows that role's
Inbound Forwarding, so it reaches the same place as the role's normal
mail with no extra setup.
- Automatic handling when the role uses a connected inbox: the tab
collapses to a short read-only note instead of a dropdown, because there
is nothing to configure - contact-us rides the role's forwarding into the
connected inbox and appears grouped under the role.
- The "How this delivers" summary describes the real path for each option
("follows this role's Inbound Forwarding to wherever that's configured")
rather than assuming a connected inbox, so it is correct for groups that
forward to personal addresses or to several recipients.
- The Contact Us help text now lists all routing options and explains the
connected-inbox automatic handling.
- The display name override and the copyable markdown contact link remain
on every role.

### Inbound forwarding clarity

- Connected Gmail inboxes appear as choices in a role's Inbound Forwarding.
- Catch-all routing is described correctly even when the catch-all is a
Worker fan-out, showing "Multiple recipients (Worker)" instead of wrongly
reporting that no catch-all rule exists.
- The "link a member" prompt only appears for the member's-personal-email
target, where linking a member is the right action; Catchall, Custom and
No forwarding no longer show it.
- A forwarding destination awaiting Cloudflare verification is shown as an
amber "Verification needed" notice rather than a red error.

### Contact Us system role

- No Contact Us tab (a role cannot contact itself).
- Inbound Forwarding no longer offers "Member's personal email" (there is
no member to link); it defaults to "No forwarding" and can be set to
Custom, Multiple recipients, or Catchall.
- The "System role" notice now shows the standard warning icon.
- It cannot be deleted: the delete button is disabled with a tooltip
explaining it is the contact-us sender linked across the site. Other
system roles (e.g. Info) are deletable.

### Committee roles table

- Email addresses stay on a single line instead of wrapping mid-address.
- Name, Role and Description columns are sized so descriptions get room and
nothing is cramped; the status columns are sized to their content and the
sort carets no longer wrap onto a second line.

### Email routing status

- The "Active" tooltip shows the role's configured address for multi-role
members, instead of one long address built by joining all of a member's
role names together.
- The "Catch-all" and "Multiple" badge tooltips describe their destinations
in the same style, e.g. "Routed via catch-all -> 4 recipients" and
"info@yourdomain.org.uk -> 4 recipients".

### Gmail inbox setup (External systems -> Mail)

- The Pub/Sub setup now shows only the Google Cloud project ID; the topic
name is hidden and defaulted internally, since the setup creates it.
- The project ID persists when you save the configuration, with a note that
it is not active until you Run Google Cloud setup.
- The setup help text is reworded and no longer references the hidden topic
field; the success message now has a "Google Cloud setup complete" title
and a tick icon.
- The "Clear OAuth client" and per-mailbox "Remove" buttons are grey by
default and red on hover; the "Apply push" button and the help text under
fields have proper spacing.

## Technical details

- New outbound/contact-us target ForwardEmailTarget.ROLE_EMAIL, resolved
server-side to the role's own address without rewriting it
(server/lib/contact-us/resolve-and-send.ts, committee.model.ts).
- Connected-inbox detection roleForwardsToConnectedInbox() drives the
Contact Us collapse and forces contactUsTarget to ROLE_EMAIL so the saved
value matches the displayed behaviour (committee-member.ts).
- catchAllExists()/catchAllSummary() and catchAllTooltipDestination()
recognise Worker catch-alls; the "not yet configured" status is gated to
the member-email target only (committee-member.ts, email-routing-status.ts).
- The Active badge tooltip uses roleEmailFor(role) (configured email,
falling back to type@domain) for consistency with the routing status
logic; removed the unused workerScriptNameFor() method
(committee-settings.ts).
- Roles table switched to natural (auto) layout with a non-wrapping email
cell and content-sized status columns (committee-settings.ts).
- Server Cloudflare email-routing endpoints share a single asyncRoute
wrapper and HttpError type, with a shared pre-flight that creates and
reports unverified forwarding destinations (server/lib/shared/async-route.ts,
server/lib/shared/http-error.ts, cloudflare-email-routing-routes.ts).
- Client error display standardised via extractErrorMessage; verification
-pending responses shown as an amber notice.
- New migration 20260616000000-update-committee-contact-us-help upserts the
refreshed committee-contact-us-help content text so it updates on every
environment at deploy; default-content.ts kept in sync.
- StoredValue keys renamed to match their kebab on-screen values and sorted,
with no behaviour change (ui-actions.ts and consumers).
- Gmail inbox settings: Project ID binds to googleInbox.pubsubProjectId so it
persists on Save; topic-name field removed and defaulted; runSetup reads the
project ID from config; statusTitle drives the success alert title + icon
(system-gmail-inbox-settings.ts).
- New btn-grey-danger style (grey default, red hover) applied to Clear OAuth
client and Remove (buttons.sass and the two external-systems components).
- Committee roles delete restricted to non-Contact-Us roles via
isContactUsSystemRole(); Contact Us shows a disabled delete with tooltip
(committee-settings.ts).