Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
9.2 KiB
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
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_ofcondition 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).
- 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. - Status-update suppression: admin creates a skip rule on subject
contains "status update"scoped to one mailbox. Those emails create nothing and are auditable asskippedin email diagnostics. - AI catch-all: admin with the AI add-on adds a final rule for
@huntress.comemail: 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 regexis 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 as an alias of…" with a client picker.
- Client record: "Matching aliases" list next to the inbound email domains UI.
Requirements
Functional Requirements
- Data:
inbound_email_rulesandclient_name_aliasestables per the design doc (composite(tenant, id)PKs, Citus distribution,transaction: false);email_processed_messages.processing_statuscheck constraint gains'skipped'. - Conditions: ALL-of array; fields
from_address,from_domain,to_address(any recipient),subject,body_text; operatorsequals,contains,starts_with,ends_with,matches_regex; case-insensitive; regex length-capped, compile errors = condition false; body input sliced (~100 KB). - Evaluation: new-ticket path only, after thread matching fails, before
defaults resolution. Active rules ordered by
position, filtered byprovider_ids. First conditions-match executes the action; resolved action stops; non-match honorson_no_match(proceedcontinues down the list;skip;fallback_destination). No match → today's pipeline unchanged. - Skip action: no ticket/comment/attachments;
email_processed_messagesrowprocessing_status='skipped'with{ ruleId, ruleName }metadata. Works for tenants with no inbound defaults. - Extract + assign: templates
between/after/before(withoccurrencefirst/last) and rawregex(capture group 1) compile to one extractor; extracted value normalized (trim, collapse whitespace, lowercase); matched againstclients.client_namethenclient_name_aliases; inactive clients excluded. Assigned client wins over sender-based matching; contact = sender contact within that client, else primary contact; destination flows throughresolveEffectiveInboundTicketDefaults()with the assigned client. - Set destination: applies the referenced
inbound_ticket_defaultsat the top of the cascade; sender matching still attributes client/contact. - AI classify (EE): instruction +
allowed_outcomes; OSS stub returnsno_decision(dynamic loader,inboundReplyAcknowledgementDeciderpattern); EE module returns{ decision, extracted_client_name };assign_clientresolves through the same exact+alias matcher; any failure = non-match; usage logged via existing AI usage tracking. - Audit: created tickets carry
appliedRuleIdandclientMatchSourceinemail_metadata; theINBOUND_EMAIL_RECEIVEDactivity-log row includes the rule name; one structured log line per evaluated email. - 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_classifyis EE + AI-add-on gated.
Data / API / Integrations
See design doc for full schemas. Key integration points:
shared/services/email/processInboundEmailInApp.ts— evaluation hookshared/workflow/actions/emailWorkflowActions.ts— client/contact matching, defaults cascadeshared/services/email/inboundReplyAcknowledgementDecider.ts— EE loader patternserver/migrations/20260213180500_create_client_inbound_email_domains.cjs— migration conventions to mirrorpackages/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, subjectAlert (Acme Corp) — ...) creates a ticket assigned to client Acme Corp via a builder-authored rule, withclientMatchSource: 'rule_extraction'inemail_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
skippedwith the rule name inemail_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.jsonpass.