# Scratchpad — Inbound Email Rules
- Plan slug: `2026-06-10-inbound-email-rules`
- Created: `2026-06-10`
## What This Is
Rolling log of discoveries and decisions while implementing inbound email rules.
Approved design: `docs/plans/2026-06-10-inbound-email-rules-design.md` (commit fd14d3e445).
## Decisions
- (2026-06-10) Inline rules engine in `processInboundEmailInApp()`, not
workflow-compiled and not a generic automation framework. In-app processing
already replaced workflow-based email handling; generic framework is premature
with one consumer.
- (2026-06-10) Tenant-wide ordered rules with optional per-rule `provider_ids`
filter. Board is an *output* of rules (via destination/defaults), never a scope —
board isn't known until after rules run.
- (2026-06-10) New-ticket path only; replies that thread onto tickets bypass rules
so skip patterns can't eat genuine replies.
- (2026-06-10) Client matching is exact-normalized + per-client aliases
(`client_name_aliases`). Fuzzy matching rejected: wrong match = ticket (and its
contents) on the wrong client.
- (2026-06-10) `on_no_match = proceed` continues down the rules list (not straight
to the normal pipeline) — enables "regex rule first, AI catch-all later" so AI
only burns tokens when deterministic extraction fails.
- (2026-06-10) Rule-assigned client beats sender contact/domain matching — the
sender is a service (e.g. alerts@huntress.com), not the client.
- (2026-06-10) AI never picks a client_id; it returns `extracted_client_name` and
the deterministic matcher resolves it. No client list in the prompt.
- (2026-06-10) Packaging: rules engine CE; only `ai_classify` gated on EE + AI
add-on. AI action is visible-but-disabled (upsell tease) without the add-on —
explicit user request.
- (2026-06-10) Live tester calls the real shared evaluator via a server action; no
parallel test implementation.
## Discoveries / Constraints
- (2026-06-10) Migration conventions: composite `(tenant, id)` PK, best-effort
`create_distributed_table('
', 'tenant')`, `exports.config = { transaction:
false }`, functional unique indexes on `lower(...)` — mirror
`server/migrations/20260213180500_create_client_inbound_email_domains.cjs`.
- (2026-06-10) `email_processed_messages.processing_status` has a CHECK constraint
(`success|failed|partial`) — adding `skipped` requires dropping/re-adding the
constraint (migration `20250130200000` created it).
- (2026-06-10) Unknown senders are currently dropped as
`skipped/missing_defaults` in `processInboundEmailInApp.ts` — running rules
*before* defaults resolution means skip rules work for tenants with no defaults.
- (2026-06-10) EE dynamic-load pattern to copy:
`shared/services/email/inboundReplyAcknowledgementDecider.ts` (OSS stub + EE
module).
- (2026-06-10) Domain-match contact fallback today uses
`findValidClientPrimaryContactId` (clients.properties.primary_contact_id) in
`shared/workflow/actions/emailWorkflowActions.ts` — reuse for rule-assigned
clients.
- (2026-06-10) UI home: `packages/integrations/src/components/email/` (provider
config, `admin/InboundTicketDefaultsManager.tsx`,
`forms/InboundTicketDefaultsForm.tsx`). Alias UI goes next to the client
inbound-domains UI (`packages/clients/src/actions/clientInboundEmailDomainActions.ts`
is the actions-layer sibling).
## Implementation Notes (2026-06-10)
- Implemented in commits `824f5a9178` (engine/pipeline/EE/migrations),
`a3df3eb94e` (server actions), `a2b564237c` (UI).
- Engine lives in `shared/services/email/inboundEmailRules/` with injectable
deps (`loadRules`, `matchClientByName`, `resolveDefaultsById`,
`classifyWithAi`) so the walk semantics are unit-testable without a DB; the
default deps hit the real tables. The tester server action injects only
`loadRules` (the draft rule), so client/alias matching is live.
- **Deviation — reorder**: up/down arrow controls instead of drag-and-drop;
the repo has no dnd dependency and adding one wasn't justified. Positions
persist through `reorderInboundEmailRules` the same way.
- **Deviation — alias quick-add**: `packages/integrations` does not depend on
`packages/clients`, so the tester quick-add uses its own
`addClientNameAliasFromRuleTester` action (system_settings RBAC) writing the
same table the clients-package CRUD uses.
- AI usage logging: there was no pre-existing usage-tracking module; the EE
classifier emits a structured `token usage` log line (tenant/provider/rule/
model/usage) that metering can consume later.
- `email_processed_messages.processing_status='skipped'` is only used for
rule skips; other skip reasons (missing_defaults etc.) keep `partial` to
avoid changing existing diagnostics semantics.
- The rule tester does not exercise provider filtering (draft rule runs with
`provider_ids: null`) — testers have no receiving mailbox.
- Migrations were applied against the worktree's local-test DB
(port 5472) and the resulting schema verified, including the
`lower(regexp_replace(trim(...), '\s+', ' ', 'g'))` matcher expression.
Citus distribution was skipped there (non-Citus dev DB) as designed.
- Pre-existing test failures on this branch (NOT from this work):
`processInboundEmailInApp.test.ts` (2) and
`processInboundEmailInApp.additionalPaths.test.ts` (1) fail at HEAD too.
- Remaining unimplemented tests in `tests.json` need a DB-backed harness
(matcher SQL, unique index, inactive-rule filtering), an EE/AI mock harness
(T058–T065), action-layer tests (T070–T078), and UI tests (T079–T094).
## Commands / Runbooks
- (2026-06-10) Integration tests: see `integration-testing` skill (DB bootstrap,
tenant isolation, transaction cleanup).
- (2026-06-10) Manual smoke: dev stack + MailHog (see `alga-dev-env-manager` /
`alga-manual-smoke-tests` skills); send sample Huntress-style email and a
status-update email.
## Links / References
- Design doc: `docs/plans/2026-06-10-inbound-email-rules-design.md`
- Pipeline: `shared/services/email/processInboundEmailInApp.ts`
- Matching/defaults: `shared/workflow/actions/emailWorkflowActions.ts`
- Reply parsing: `shared/lib/email/replyParser.ts`
- Prior related plans: `ee/docs/plans/2026-02-13-inbound-email-domain-matching-default-contact/`,
`ee/docs/plans/2026-02-25-inbound-email-sender-routing-to-boards/`,
`ee/docs/plans/2026-03-31-inbound-email-reopen-on-reply/`
## Open Questions
- None blocking. Deferred (out of scope per PRD): any_of condition groups,
apply-to-replies flag, fuzzy matching, global AI fallback rule, token billing.