Hermes 284313f908
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
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

9.0 KiB
Raw Permalink Blame History

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

  1. New ticket creation from inbound email can resolve targetClientId by sender email domain only when the domain is explicitly configured for a client.
  2. A configured inbound email domain must be unique per tenant (cannot be assigned to multiple clients).
  3. 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.
  4. Exact email match remains highest precedence for setting both contact and client.
  5. If no configured domain match exists, behavior falls back to the existing defaults behavior.

Non-goals

  1. Creating new contacts automatically for unmatched senders.
  2. Wildcard / suffix / subdomain pattern matching (e.g. *.acme.com) beyond the exact domain extracted from the sender email.
  3. Inferring domain-to-client matches by scanning contacts email addresses.
  4. 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)

  1. Inbound email arrives for processing (Google/Microsoft/IMAP pipelines feed into shared processing).
  2. System normalizes sender email and finds a contact by exact email.
  3. Ticket is created with client_id = contact.client_id and contact_id = contact.contact_id.

Flow B — Domain fallback match (new)

  1. Inbound email arrives and exact contact lookup returns none.
  2. System extracts sender domain, and finds a client associated with that domain via an explicit inbound-email-domain mapping.
  3. Ticket is created with client_id = matched client.
  4. If that client has a configured default contact, ticket is created with contact_id = default contact.

Flow C — No domain match configured (existing fallback)

  1. Inbound email arrives and exact contact lookup returns none.
  2. Domain lookup returns none.
  3. Ticket falls back to inbound ticket defaults client_id; contact_id remains 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 optionally clients.properties.primary_contact_name for 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

  1. 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_id to the matched client.
    • If the matched client has a configured default contact, set ticket contact_id to that contact.
    • If no configured domain match exists, preserve existing fallback to inbound ticket defaults.
  2. Domain extraction

    • Use normalized sender email (lowercased, display-name stripped).
    • Extract domain via substring after @.
    • Treat domain matching as case-insensitive.
  3. 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).
  4. 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.
  5. Location behavior

    • If inbound ticket defaults include location_id, and the resolved targetClientId differs from ticketDefaults.client_id, do not apply location_id (set null) to avoid cross-client location mismatch.

Non-functional Requirements

  1. Must not materially slow down inbound email processing for the common-case (exact contact match).
  2. 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 (and primary_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_domains with 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.

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 (updateClient merges properties)

Security / Permissions

  1. Updating default contact requires existing client update permission (already enforced by updateClient).
  2. Managing inbound email domains requires existing client update permission (same UX surface as client configuration).
  3. 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

  1. Add migration for the inbound email domain mapping table + indexes.
  2. Existing behavior remains unchanged for tenants/clients without configured inbound email domains (no domain matching will occur).

Open Questions

  1. Should we allow subdomain matching (e.g. user@it.acme.com matching configured acme.com) or require exact match only?
  2. 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 theres an exact sender contact match.)
  3. 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)

  1. 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_id uses that client.
    • If the matched client has a valid default contact configured, ticket contact_id is 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_id and keeps contact_id = null.
  2. 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.