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
182 lines
9.2 KiB
Markdown
182 lines
9.2 KiB
Markdown
# PRD — Inbound Email Rules
|
|
|
|
- Slug: `2026-06-10-inbound-email-rules`
|
|
- Date: `2026-06-10`
|
|
- Status: Accepted — core implementation landed 2026-06-10
|
|
- Design: [../2026-06-10-inbound-email-rules-design.md](../2026-06-10-inbound-email-rules-design.md)
|
|
|
|
## Summary
|
|
|
|
A tenant-wide, ordered list of inbound email rules evaluated inside
|
|
`processInboundEmailInApp()` on the new-ticket path. Rules match on
|
|
sender/subject/body/recipients and perform one action: skip the email, extract a
|
|
client name from the subject/body and assign the matching client, route to a
|
|
destination (`inbound_ticket_defaults` set), or classify with AI (EE add-on).
|
|
Configured through an intuitive builder UI with a regex escape hatch and a live
|
|
tester that runs the production evaluator.
|
|
|
|
## Problem
|
|
|
|
Inbound email can only be attributed to a client by exact sender-contact match or
|
|
sender-domain match. Email *about* a client sent by a third-party service (e.g.
|
|
Huntress alerts with the customer name in the subject) cannot be routed, and noisy
|
|
service email (status updates) always creates tickets. Workflows can do both, but
|
|
executions are metered and authoring is too complex versus competitors' built-in
|
|
configuration.
|
|
|
|
## Goals
|
|
|
|
- Assign tickets to the right client by extracting a name from the subject/body
|
|
(the Huntress case), with safe exact+alias matching.
|
|
- Suppress ticket creation entirely for configured email (status updates).
|
|
- Route matched email to a chosen destination (board/status/priority via
|
|
`inbound_ticket_defaults`).
|
|
- Let AI add-on tenants classify email (skip / assign client) with a natural-language
|
|
instruction instead of patterns; token-meterable later.
|
|
- Intuitive UI: condition builder, friendly extraction templates, regex escape
|
|
hatch, live tester, drag reordering.
|
|
- Zero behavior change for tenants with no rules.
|
|
|
|
## Non-goals
|
|
|
|
- Generic automation framework beyond inbound email.
|
|
- Rules on the reply/threading path (replies always become comments).
|
|
- Fuzzy client-name matching.
|
|
- `any_of` condition groups (JSONB shape accommodates them later).
|
|
- AI token billing integration (usage logged only).
|
|
- Compiling rules to workflows or any workflow-engine involvement.
|
|
|
|
## Users and Primary Flows
|
|
|
|
MSP administrators (email settings RBAC).
|
|
|
|
1. **Huntress routing**: admin creates a rule — conditions `from_address contains
|
|
@huntress.com`; action *extract + assign client*, text between `(` and `)` in
|
|
subject; non-match → fallback destination "Triage". Alerts now land on the right
|
|
client; unknown names land in Triage where the tester's alias quick-add teaches
|
|
the system.
|
|
2. **Status-update suppression**: admin creates a skip rule on subject
|
|
`contains "status update"` scoped to one mailbox. Those emails create nothing and
|
|
are auditable as `skipped` in email diagnostics.
|
|
3. **AI catch-all**: admin with the AI add-on adds a final rule for
|
|
`@huntress.com` email: AI classify with instruction "determine which customer
|
|
this alert concerns". Deterministic rules run first; AI only spends tokens when
|
|
they fail.
|
|
|
|
## UX / UI Notes
|
|
|
|
New **Inbound Rules** section in email settings
|
|
(`packages/integrations/src/components/email/`), same RBAC as provider admin.
|
|
|
|
- Rules list: ordered table, drag-to-reorder (persists `position`), name,
|
|
human-readable summary, mailbox-filter chips, active toggle, edit/delete.
|
|
- Rule editor drawer: name; active; mailbox multi-select (empty = all); condition
|
|
rows (field + operator + value, `matches regex` is an ordinary operator); action
|
|
picker; per-action config; non-match behavior select with fallback destination
|
|
picker. The AI action is always visible; without EE + AI add-on it is disabled
|
|
with an upsell hint.
|
|
- Live tester: paste From/Subject/Body; a server action runs the actual shared
|
|
evaluator on the draft rule and shows per-condition pass/fail, extracted value,
|
|
resolved client, final outcome. When extraction matches but no client resolves,
|
|
offer "Add *<value>* as an alias of…" with a client picker.
|
|
- Client record: "Matching aliases" list next to the inbound email domains UI.
|
|
|
|
## Requirements
|
|
|
|
### Functional Requirements
|
|
|
|
1. **Data**: `inbound_email_rules` and `client_name_aliases` tables per the design
|
|
doc (composite `(tenant, id)` PKs, Citus distribution, `transaction: false`);
|
|
`email_processed_messages.processing_status` check constraint gains `'skipped'`.
|
|
2. **Conditions**: ALL-of array; fields `from_address`, `from_domain`, `to_address`
|
|
(any recipient), `subject`, `body_text`; operators `equals`, `contains`,
|
|
`starts_with`, `ends_with`, `matches_regex`; case-insensitive; regex
|
|
length-capped, compile errors = condition false; body input sliced (~100 KB).
|
|
3. **Evaluation**: new-ticket path only, after thread matching fails, before
|
|
defaults resolution. Active rules ordered by `position`, filtered by
|
|
`provider_ids`. First conditions-match executes the action; resolved action
|
|
stops; non-match honors `on_no_match` (`proceed` continues down the list;
|
|
`skip`; `fallback_destination`). No match → today's pipeline unchanged.
|
|
4. **Skip action**: no ticket/comment/attachments; `email_processed_messages` row
|
|
`processing_status='skipped'` with `{ ruleId, ruleName }` metadata. Works for
|
|
tenants with no inbound defaults.
|
|
5. **Extract + assign**: templates `between`/`after`/`before` (with
|
|
`occurrence` first/last) and raw `regex` (capture group 1) compile to one
|
|
extractor; extracted value normalized (trim, collapse whitespace, lowercase);
|
|
matched against `clients.client_name` then `client_name_aliases`; inactive
|
|
clients excluded. Assigned client wins over sender-based matching; contact =
|
|
sender contact within that client, else primary contact; destination flows
|
|
through `resolveEffectiveInboundTicketDefaults()` with the assigned client.
|
|
6. **Set destination**: applies the referenced `inbound_ticket_defaults` at the top
|
|
of the cascade; sender matching still attributes client/contact.
|
|
7. **AI classify (EE)**: instruction + `allowed_outcomes`; OSS stub returns
|
|
`no_decision` (dynamic loader, `inboundReplyAcknowledgementDecider` pattern); EE
|
|
module returns `{ decision, extracted_client_name }`; `assign_client` resolves
|
|
through the same exact+alias matcher; any failure = non-match; usage logged via
|
|
existing AI usage tracking.
|
|
8. **Audit**: created tickets carry `appliedRuleId` and `clientMatchSource` in
|
|
`email_metadata`; the `INBOUND_EMAIL_RECEIVED` activity-log row includes the
|
|
rule name; one structured log line per evaluated email.
|
|
9. **Management**: CRUD + reorder server actions with payload validation per
|
|
action type; test-rule action (no persistence); alias CRUD; all behind email
|
|
admin RBAC.
|
|
|
|
### Non-functional Requirements
|
|
|
|
- An engine error never blocks email processing — log and fall through to the
|
|
unmodified pipeline.
|
|
- Dangling references (deleted defaults set, deleted/inactive client behind an
|
|
alias) degrade to non-match/proceed with a warning.
|
|
- One indexed query loads a tenant's rules per evaluated email.
|
|
- Rules engine ships CE; only `ai_classify` is EE + AI-add-on gated.
|
|
|
|
## Data / API / Integrations
|
|
|
|
See design doc for full schemas. Key integration points:
|
|
|
|
- `shared/services/email/processInboundEmailInApp.ts` — evaluation hook
|
|
- `shared/workflow/actions/emailWorkflowActions.ts` — client/contact matching,
|
|
defaults cascade
|
|
- `shared/services/email/inboundReplyAcknowledgementDecider.ts` — EE loader pattern
|
|
- `server/migrations/20260213180500_create_client_inbound_email_domains.cjs` —
|
|
migration conventions to mirror
|
|
- `packages/integrations/src/components/email/` — settings UI home
|
|
|
|
## Security / Permissions
|
|
|
|
Same RBAC as email provider administration for all rule/alias CRUD and the tester
|
|
action. Regex inputs are tenant-supplied: length caps and bounded inputs guard
|
|
against ReDoS. AI prompts include only the email excerpt and the tenant's
|
|
instruction — never the tenant client list.
|
|
|
|
## Observability
|
|
|
|
Covered by functional requirement 8 (skipped-email diagnostics, ticket
|
|
`email_metadata` audit fields, structured evaluation log line). Nothing further.
|
|
|
|
## Rollout / Migration
|
|
|
|
Purely additive migrations; no backfill. Feature is inert until a tenant creates a
|
|
rule. No feature flag needed.
|
|
|
|
## Open Questions
|
|
|
|
None — design decisions were settled and approved in the design doc.
|
|
|
|
## Acceptance Criteria (Definition of Done)
|
|
|
|
- A Huntress-style email (`alerts@huntress.com`, subject `Alert (Acme Corp) — ...`)
|
|
creates a ticket assigned to client Acme Corp via a builder-authored rule, with
|
|
`clientMatchSource: 'rule_extraction'` in `email_metadata`.
|
|
- The same rule with an unknown customer name lands the ticket at the configured
|
|
fallback destination (or proceeds/skips, per rule config).
|
|
- A skip rule suppresses ticket creation and the email is visible as `skipped`
|
|
with the rule name in `email_processed_messages`.
|
|
- An AI rule (EE) skips or assigns per its instruction; in OSS the same rule
|
|
degrades to its non-match behavior; AI outages never block ticket creation.
|
|
- Replies threading onto existing tickets are unaffected by any rule.
|
|
- A tenant with no rules has byte-identical pipeline behavior.
|
|
- Rules are manageable (create/edit/reorder/toggle/delete/test) in email settings;
|
|
aliases manageable on the client record and via tester quick-add.
|
|
- Unit + integration tests in `tests.json` pass.
|