16-Jun-2026 — inbox-aware email routing and Contact Us delivery #278
build 698 — commit 0fa5561
📖 New to this feature? See Setting up a Gmail inbox for committee replies 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).