Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
9.0 KiB
PRD — Inbound Email Domain Matching + Client Default Contact
- Slug:
inbound-email-domain-matching-default-contact - Date:
2026-02-13 - Status: Draft
Summary
When processing inbound emails that create new tickets, we currently resolve the ticket client_id and contact_id only via an exact contact email match (and otherwise fall back to inbound ticket defaults). We want to add an explicit, admin-configured mapping from inbound sender email domains to clients, plus an optional per-client default contact.
Key change from the earlier approach: domain matching must be explicitly configured per client and unique across clients; we must not infer domain ownership by scanning existing contacts.
Problem
Inbound email senders are frequently new people at an existing customer. Exact email matching fails, and the system falls back to the inbound ticket defaults’ configured client, causing misfiled tickets and manual triage.
Goals
- New ticket creation from inbound email can resolve
targetClientIdby sender email domain only when the domain is explicitly configured for a client. - A configured inbound email domain must be unique per tenant (cannot be assigned to multiple clients).
- Each client can configure a “Default contact” from its contacts list, and that contact is used as the ticket contact when domain-based client resolution is used.
- Exact email match remains highest precedence for setting both contact and client.
- If no configured domain match exists, behavior falls back to the existing defaults behavior.
Non-goals
- Creating new contacts automatically for unmatched senders.
- Wildcard / suffix / subdomain pattern matching (e.g.
*.acme.com) beyond the exact domain extracted from the sender email. - Inferring domain-to-client matches by scanning contacts’ email addresses.
- Changing reply threading behavior or ticket reply matching behavior (reply-token/thread-header matching remains unchanged).
Users and Primary Flows
Personas
- Helpdesk operator: wants inbound tickets to land on the correct client with minimal manual corrections.
- Admin / dispatcher: configures client inbound email domains and default contact to make domain fallback useful.
Flow A — Existing contact match (current behavior, preserved)
- Inbound email arrives for processing (Google/Microsoft/IMAP pipelines feed into shared processing).
- System normalizes sender email and finds a contact by exact email.
- Ticket is created with
client_id = contact.client_idandcontact_id = contact.contact_id.
Flow B — Domain fallback match (new)
- Inbound email arrives and exact contact lookup returns none.
- System extracts sender domain, and finds a client associated with that domain via an explicit inbound-email-domain mapping.
- Ticket is created with
client_id = matched client. - If that client has a configured default contact, ticket is created with
contact_id = default contact.
Flow C — No domain match configured (existing fallback)
- Inbound email arrives and exact contact lookup returns none.
- Domain lookup returns none.
- Ticket falls back to inbound ticket defaults
client_id;contact_idremains null.
UX / UI Notes
Client details: Inbound email domain configuration
- Add an “Inbound email domains” section on the client details screen.
- Admin can add/remove domain strings (e.g.
acme.com). - On add:
- Validate the domain format.
- Verify uniqueness in the tenant; if the domain is already assigned to another client, block the save and show which client owns it.
- Domains are used for inbound email domain matching only; if a client has no domains configured, the system will not domain-match to that client.
Client details: Default contact configuration
- Add a “Default contact” picker on the client details screen.
- Picker options: contacts belonging to this client.
- Persist selection into
clients.properties.primary_contact_id(and optionallyclients.properties.primary_contact_namefor display). - Provide short helper text: “Used when inbound email sender is not a known contact but matches this client by email domain.”
Requirements
Functional Requirements
-
Precedence rules
- If an exact contact match exists for sender email, use it (sets both client and contact).
- Otherwise, attempt domain-to-client match using the explicit inbound email domain mapping.
- If a configured domain-to-client match exists, set ticket
client_idto the matched client. - If the matched client has a configured default contact, set ticket
contact_idto that contact. - If no configured domain match exists, preserve existing fallback to inbound ticket defaults.
-
Domain extraction
- Use normalized sender email (lowercased, display-name stripped).
- Extract domain via substring after
@. - Treat domain matching as case-insensitive.
-
Explicit domain mapping
- Admin can configure 0..N inbound email domains per client.
- A given domain may belong to at most one client per tenant (uniqueness enforced).
- Domain matching in inbound processing consults only this explicit mapping (no inference from contacts).
-
Default contact resolution
- Default contact is stored on the client as
clients.properties.primary_contact_id(existing JSON field). - When applying it, validate the referenced contact exists, belongs to the client, and is active; otherwise treat as unset.
- Default contact is stored on the client as
-
Location behavior
- If inbound ticket defaults include
location_id, and the resolvedtargetClientIddiffers fromticketDefaults.client_id, do not applylocation_id(set null) to avoid cross-client location mismatch.
- If inbound ticket defaults include
Non-functional Requirements
- Must not materially slow down inbound email processing for the common-case (exact contact match).
- Failure to resolve domain match must not throw; should safely fall back to existing behavior.
Data / API / Integrations
Data model
- Reuse existing
clients.properties.primary_contact_id(andprimary_contact_name) for “default contact”. No migration required for this phase. - Add a tenant-scoped mapping table for inbound email domains, with uniqueness by
(tenant, lower(domain)).- Proposed:
client_inbound_email_domainswith columns:tenant,id,client_id,domain,created_at,updated_at. - Constraints/indexes:
- Unique index on
(tenant, lower(domain)). - Index on
(tenant, client_id)for listing.
- Unique index on
- Proposed:
Touch points (current code paths)
- Inbound email (in-app processing):
shared/services/email/processInboundEmailInApp.ts - Workflow runtime action used by email workflows:
shared/workflow/runtime/actions/registerEmailWorkflowActions.ts(resolve_inbound_ticket_context) - Contact lookup:
shared/workflow/actions/emailWorkflowActions.ts(findContactByEmail) - Client UI:
packages/clients/src/components/clients/ClientDetails.tsx - Client persistence:
packages/clients/src/actions/clientActions.ts(updateClientmergesproperties)
Security / Permissions
- Updating default contact requires existing client update permission (already enforced by
updateClient). - Managing inbound email domains requires existing client update permission (same UX surface as client configuration).
- Inbound email processing runs server-side using admin DB access; ensure tenant scoping is always applied.
Observability
Not in scope for this phase (follow existing logging patterns only).
Rollout / Migration
- Add migration for the inbound email domain mapping table + indexes.
- Existing behavior remains unchanged for tenants/clients without configured inbound email domains (no domain matching will occur).
Open Questions
- Should we allow subdomain matching (e.g.
user@it.acme.commatching configuredacme.com) or require exact match only? - When domain match sets a default contact, should the created comment author be treated as a “contact” or remain “system”? (Today we can keep “system” unless there’s an exact sender contact match.)
- Do we need a one-time helper to suggest/configure domains (e.g. admin UX that proposes domains based on contacts), or keep it fully manual?
Acceptance Criteria (Definition of Done)
- Inbound email new-ticket creation:
- Exact contact match sets ticket client + contact as before.
- If exact contact match fails and sender domain matches a configured inbound email domain, ticket
client_iduses that client. - If the matched client has a valid default contact configured, ticket
contact_idis set to it; otherwise it remains null. - If no inbound email domain is configured for the sender domain, ticket falls back to inbound defaults
client_idand keepscontact_id = null.
- Client screen:
- Admin can add/remove inbound email domains for a client.
- Adding a domain fails with a clear message if the domain is already assigned to another client (uniqueness enforced).
- Admin can set/clear the client “Default contact” via a picker.
- Selection persists and reloads correctly.