Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
7.8 KiB
7.8 KiB
PRD — Inbound Email Sender Routing to Boards
- Slug:
inbound-email-sender-routing-to-boards - Date:
2026-02-25 - Status: Draft
Summary
Revise routing ownership to live on clients and contacts instead of provider-level sender-rule tables.
The system should resolve an effective inbound destination from:
- contact override (exact sender match),
- then client default destination (exact contact client or domain-matched client),
- then provider default inbound settings.
Destination should resolve to an Inbound Ticket Defaults profile (not just board_id) so board/status/priority/category/client/location remain coherent.
Problem
Today, new inbound-email tickets always use one provider-level defaults profile and one board.
Current sender matching logic only influences client_id/contact_id:
- exact contact email match,
- explicit client domain mapping (
client_inbound_email_domains).
Because board/defaults are not resolved from client/contact ownership, tickets from known client domains still land on a generic board and require manual rerouting.
Goals
- Let each client define a default inbound destination profile.
- Let each contact optionally define a destination override that takes precedence over client default.
- Reuse existing client domain mapping so unknown contacts from a known client domain route to that client's destination.
- Preserve deterministic precedence and safe fallback to provider defaults.
- Keep existing reply-thread behavior unchanged.
Non-goals
- Provider-level sender-rule table UI in this phase.
- Wildcard/regex sender matching.
- Routing existing ticket replies to other boards.
- Auto-provisioning client/contact routing from historical mail.
Users and Primary Flows
Personas
- Admin/dispatcher configuring client and contact behavior.
- Helpdesk operators expecting inbound tickets to land on the correct board.
Flow A — Exact contact with contact override
- New email arrives from
invoices@customer.com. - Sender matches a contact by exact email.
- Contact has
inbound_ticket_defaults_idoverride. - Ticket uses contact override defaults profile.
Flow B — Exact contact without override
- New email arrives from
tech@customer.com. - Sender matches a contact by exact email.
- Contact has no override; contact's client has default destination.
- Ticket uses client default destination profile.
Flow C — Domain-matched client (unknown contact)
- New email sender is not an exact contact.
- Domain matches explicit
client_inbound_email_domainsrecord. - Matched client has default destination.
- Ticket uses client default destination profile.
Flow D — No contact/domain client routing match
- New email arrives from unconfigured sender/domain.
- Ticket uses provider default inbound settings.
Flow E — Existing ticket reply
- Email is matched by reply token/thread headers.
- System appends comment to existing ticket.
- No client/contact routing for board/defaults is evaluated.
UX / UI Notes
Client screen (ClientDetails)
- Keep existing Inbound email domains management.
- Add Inbound ticket destination picker for Inbound Ticket Defaults.
- Helper text: "Used for inbound senders that map to this client and have no contact override."
Contact screen
- Add optional Inbound ticket destination override picker.
- Helper text: "If set, this overrides the client destination for this exact sender contact."
Resolution help text
- Display precedence in both screens:
- Contact override -> Client destination -> Provider default.
Requirements
Functional Requirements
- Add client-level inbound destination setting:
clients.inbound_ticket_defaults_id(nullable UUID), or equivalent persisted client property.
- Add contact-level destination override:
contacts.inbound_ticket_defaults_id(nullable UUID), or equivalent persisted contact property.
- Resolution for new-ticket creation must use this precedence:
- exact contact sender + contact override,
- else exact contact sender + contact's client destination,
- else domain-matched client destination,
- else provider default inbound settings.
- Existing ticket reply/comment paths remain unchanged and do not re-evaluate destination.
- Existing exact-contact and domain-to-client matching behavior remains intact.
- Validate resolved destination belongs to tenant and is active; otherwise fallback to provider default.
- Add server actions/API updates to read/write client/contact destination settings with existing permissions.
- Update client and contact UI surfaces to set/clear these values.
- When no client/contact destination is configured, behavior remains identical to current provider-default flow.
Non-functional Requirements
- No degradation of inbound processing throughput.
- No cross-tenant routing leakage.
- No dedupe/idempotency regression.
Data / API / Integrations
Approach Options
- Provider-level sender rule table (previous draft).
- Client/contact-owned destination config (recommended).
- Board-only fields on client/contact.
Recommendation rationale:
- Client and contact screens are the natural ownership point.
- Existing domain mapping already maps senders to client identity.
- Using inbound defaults profile IDs avoids board-only mismatches.
Proposed Data Model
- Reuse:
client_inbound_email_domains(already present). - Add:
clients.inbound_ticket_defaults_idnullable.contacts.inbound_ticket_defaults_idnullable.
- Add indexes for read path:
(tenant, inbound_ticket_defaults_id)on clients.(tenant, inbound_ticket_defaults_id)on contacts.
Resolution Integration Points
shared/services/email/processInboundEmailInApp.ts- resolve effective defaults before ticket creation.
shared/workflow/runtime/actions/registerEmailWorkflowActions.ts- extend
resolve_inbound_ticket_contextto return effective defaults using same precedence.
- extend
shared/workflow/actions/emailWorkflowActions.ts- add shared resolver helper(s) used by both paths.
- UI/actions:
packages/clients/src/components/clients/ClientDetails.tsx- contact edit/create surfaces in
packages/clients/src/components/contacts/* - related actions in
packages/clients/src/actions/*
Security / Permissions
- Client destination updates require existing client update permission.
- Contact override updates require existing contact update permission.
- Resolver validates destination defaults are in the same tenant.
Observability
- Add structured debug logs for destination resolution source:
contact_override,client_default_from_contact,client_default_from_domain,provider_default.
- Warn when configured client/contact destination is invalid or inactive and fallback is applied.
Rollout / Migration
- Add migration(s) for client/contact destination fields (or equivalent persisted schema).
- No backfill required.
- Tenants without new config continue on provider defaults unchanged.
Open Questions
- Should client/contact destination be global, or optionally provider-specific when a tenant has multiple inbound mailboxes?
- Should we block saving client/contact destination when target defaults are inactive, or allow and fallback at runtime?
- Should contact override be shown only for contacts with valid email addresses?
Acceptance Criteria (Definition of Done)
- Exact-sender contact with contact override creates new ticket using contact destination defaults.
- Exact-sender contact without override uses client destination defaults.
- Unknown sender with domain-matched client uses client destination defaults.
- Unmatched sender uses provider default inbound settings.
- Existing-ticket reply flows are unchanged.
- In-app inbound and workflow-runtime paths produce identical destination resolution for the same sender/provider input.