# Client St0r — Roadmap > Living plan. Phases 1–7 + 9 + 10 + 11 + 31 (Vault Access Rules) complete. Update as phases complete. ## Phasing principle Foundations first — engines that *enable* downstream features. Then revenue-relevant features. Then ITIL + ecosystem + polish. Each phase is self-contained, ships incrementally via the Apply flow, and unblocks the next. --- ## Phase 1 — Contract / agreement engine deepening **(M · foundation)** [complete] Mature contract engine — beyond the basics. Required for accurate profitability reporting (Phase 3). - Per-contract **overage rules** — different rate for billable hours past allowance *(1.1 — shipped v3.17.126)* - **Role-based inclusion/exclusion** — e.g. "T1 work included, T3 work billable at $X" *(1.1 — shipped v3.17.126)* - **Prepaid block hours with rollover** — % rollover, expiry dates *(1.1 — shipped v3.17.126)* - **Auto-renewal** — N days before end_date, auto-create next period via `psa_auto_renew_contracts` cron *(1.2 — shipped v3.17.130)* - **Proration** — mid-month start/cancel *(1.1 — shipped v3.17.126)* - **Bundled services** — line items per agreement via `ContractBundleItem` model with dynamic editor *(1.2 — shipped v3.17.130)* - Agreement **profitability snapshot** — revenue vs. cost-of-delivery this period, surfaced on contract detail page *(1.2 — shipped v3.17.130)* ## Phase 2 — Resource management foundation **(M · foundation)** [complete] Required by capacity planning, profitability-by-tech, and scheduling improvements. - `UserSkill`, `UserCertification` models - `WorkingHours` (per user, per weekday + per-org override) - `Holiday` / `LeaveRequest` (PTO booking with approval) - `BillableTarget` (hours/week per tech, used for utilization KPI) - **Capacity report** — forecast vs. scheduled vs. actual hours per week per tech - **Skill matching** on the dispatch board — when assigning, surface techs ranked by skill+availability ## Phase 3 — Financial reporting + BI **(L · keystone)** [complete] Most-requested feature class. Big surface, but builds entirely on Phase 1+2 foundations. - Canonical reporting query layer (`reports/queries.py`) — single source of truth for revenue, hours, costs *(3.1 — shipped v3.17.139)* - **Profitability by**: client *(3.1 — shipped v3.17.139)* / contract / project / tech *(3.2 — shipped v3.17.140)* / agreement / ticket-type / closure-category - **Effective hourly rate** report (revenue ÷ billable hours) *(3.3 — shipped v3.17.141)* - **Revenue-leakage report** (unbilled time ≥ N days old + expired blocks + un-pushed invoices) *(3.3 — shipped v3.17.141)* - **SLA trend report** — breach rate per client, per priority, over time *(3.4 — shipped v3.17.143)* - **Margin analytics** by service line *(3.4 — shipped v3.17.143)* - **Custom dashboards** — drag-and-drop widgets sourced from the canonical query layer *(3.5 — shipped v3.17.142)* - **Scheduled reports** — cron-style, email PDF/CSV *(3.6 wave B — shipped v3.17.147)* - **Wallboard view** — TV-ready big-number display (active tickets, breaches, MTTR, queue depth) *(3.6 wave A — shipped v3.17.146; basic 6-tile fixed layout)* - **Configurable wallboards with widgets** *(shipped v3.17.211)* — multiple named `Wallboard` rows per org via `reports.Wallboard` + `reports.WallboardWidget`, widgets sourced from the v3.17.142 `widget_sources.REGISTRY`, per-wallboard `refresh_seconds`, per-widget refresh override, "rotate through wallboards" mode at `/reports/wallboards//rotate/` using meta-refresh redirects (no JS required — works on any TV browser). Drag-to-reorder UI deferred — admins set the `order` field via the admin interface for now - **Executive scorecard** — single page rolling 30-day MSP KPIs *(3.6 wave A — shipped v3.17.146)* - **Client-health score** — composite of SLA hits, ticket velocity, NPS proxy, billing aging *(3.6 wave B — shipped v3.17.147)* ## Phase 4 — Procurement workflow **(L)** **— shipped** Builds on existing distributor integrations (Ingram/Pax8/Synnex). Adds the workflow above the catalog. - `PurchaseRequisition` → approval → `PurchaseOrder` *(4.1 — shipped v3.17.148)* - POs auto-numbered + branded PDF + email-to-vendor (mirror Quote/Invoice pattern) *(4.1 — shipped v3.17.148)* - **Receiving** — partial receive, back-orders, serial-number capture into Asset records *(4.2 — shipped v3.17.149)* - **Vendor relationship** model — lead times, payment terms, contact preferences *(4.3 — shipped v3.17.150)* - **Stock minimums + auto-replenish** suggestion *(4.3 — shipped v3.17.150)* - **Drop-ship handling** — direct-to-customer flag with shipping address override *(4.1 — shipped v3.17.148)* - **Fulfillment tracking** — link POs to tickets/projects, status pipeline *(4.1 — shipped v3.17.148)* - **One-click PO from accepted quote** — converts quote line items to a draft PO *(4.4 — shipped v3.17.151)* ## Phase 5 — CRM / sales pipeline **(L)** **— complete** A mature PSA covers sales-pipeline-to-invoice. Currently we have quotes; we need everything *before* the quote. - `Lead`, `Opportunity`, `Campaign`, `Commission` models *(5.1 — shipped v3.17.152; 5.2 — shipped v3.17.153)* - Lead scoring + conversion funnel report *(5.2 — shipped v3.17.153)* - Pipeline Kanban view (Discovery → Qualified → Proposal → Closed Won/Lost) *(5.1 — shipped v3.17.152)* - **Quote-to-project automation** — one click on accepted quote spins a Project with tasks pre-populated from quote line items *(5.1 — opportunity → quote shipped v3.17.152; quote → project shipped v3.17.213)* - **Configurable wallboards.** Multi-named per-org boards, widget grid sourced from the v3.17.142 widget registry, per-wallboard refresh + per-widget override, NOC-TV rotation mode *(shipped v3.17.211; Chart.js + nav + screenshots v3.17.212; drag-to-reorder widgets v3.17.215; global / cross-tenant overview boards v3.17.216; selectable category dropdown on widgets v3.17.217; categories on tickets/revenue/at-risk sources v3.17.218; in-form widget add/delete + starter templates v3.17.220; 9 new operations / monitoring widget sources + Monitoring template v3.17.224)* - Sales-activity timeline per org/lead (calls, emails, meetings logged) *(5.3 — shipped v3.17.155)* - Commission rules engine + per-tech commission report *(5.2 — shipped v3.17.153)* - Lead capture from web form / IMAP / API *(5.3 — web + API shipped v3.17.155; IMAP poller stubbed, full implementation deferred to 5.4 follow-up)* ## Phase 6 — ITIL maturity **— complete** Extends existing tickets + approvals; doesn't fork into a separate model layer. - **Change requests** as a `Ticket.ticket_type='change'` extension with required CAB approval before status moves to "Implementing" *(6.1 — shipped v3.17.158)* - **CAB workflow** — multi-approver gate (extends existing single-approval) *(6.1 — shipped v3.17.158)* - **Problem records** — link N related tickets, root-cause analysis field, status pipeline *(6.2 — shipped v3.17.160)* - **Release management** — group changes into release windows, freeze flags, rollback documentation *(6.3 — shipped v3.17.165)* - **Service-catalog governance** — approval gate on catalog item changes *(6.3 — shipped v3.17.165)* - MSP-named sample role templates seeded by `RoleTemplate.get_or_create_system_templates()`: Client, Client Admin, Technician, Tech Manager, Office Manager, Full Admin (in addition to the existing Owner/Administrator/Editor/Help Desk/IT Manager/Documentation Writer/Read-Only) *(shipped v3.17.164)* ## Phase 7 — Outsourcing, integrations, polish **(continuous track)** [complete] Not a single phase — runs alongside 1-6. - **Outsourcing**: subcontractor org type, share-ticket-to-partner endpoint with HMAC, two-way sync of comments + status, optional billing markup *(shipped v3.17.166)* - **Integration SDK**: clean provider plugin interface *(skeleton shipped v3.17.166)*; then steady drops — Datto Backup, ITGlue v2 import, Hudu sync, BackupRadar, ScreenConnect, Acronis, Liongard. Target: 5-10 new providers per quarter. - **Polish backlog** — test coverage gaps, permission edge cases, audit improvements, mobile UI fixes, onboarding docs, import-tool maturity, API stability, third-party trust signals *(continuous track)* - Tenant-isolation security-test suite restored from rotted state; latent `/api/passwords//` audit-log crash fixed *(shipped v3.17.171)* - Removed deprecated `datetime.utcnow()` / `datetime.now()` from core views; bug-report timestamp no longer mislabels server-local time as UTC *(shipped v3.17.173)* - Codebase-wide sweep of deprecated datetime calls; fixes tz-naive query bug in audit-log cleanup + expired-session cleanup on non-UTC servers *(shipped v3.17.174)* - Auto-apply-gunicorn-fix migration silenced on test runners + fresh dev installs (no longer spams "Fix script exited with code 1" banners) *(shipped v3.17.175)* - Defender adapter flagged as a reference stub in label + test-connection message + module docstring so operators don't pick it expecting live alert flow *(shipped v3.17.179)* - Silent `except Exception: pass` swallowers in `core/views.py` audit-log fallback + `core/security_views.py` scan handler tightened to log real exceptions; package-scanner JSON endpoints use a `_staff_or_superuser_api` decorator instead of inline checks *(shipped v3.17.180)* - Vault password mutation audit logging — `password_edit` + `password_delete` now write per-action AuditLog rows on success and on every failure mode (form validation, EncryptionError); 2 new tests in `vault.tests.PasswordMutationAuditTests` *(shipped v3.17.181)* - Phase 9 forms (Auto-Ticket Rule + Vendor Connection) rewritten with proper card-section layout, Bootstrap widget classes, copy-to-clipboard webhook helpers, "ALL must match" semantic hint on rule clauses *(shipped v3.17.182)* - UniFi + M365 connection forms rewritten with the same card-section layout (the only other forms using the sloppy generic field-loop pattern; audit confirmed the rest of the integration forms were already clean) *(shipped v3.17.183)* - Bare `except:` clauses replaced with `except Exception:` across 24 sites in 12 modules — no more accidental `SystemExit`/`KeyboardInterrupt` swallowing *(shipped v3.17.184)* - `core/security_views.py` inline staff-check sweep finished — all 9 endpoints now use `@_staff_or_superuser_view` (HTML) or `@_staff_or_superuser_api` (JSON) decorators; latent silent `except Exception` in `run_python_scan` also logged *(shipped v3.17.185)* - Documentation refresh: 8 new screenshots for Phase 9 + integration forms + the roadmap page; README updated with annotated captions; screenshot script extended with the new pages *(shipped v3.17.186)* - Baseline test coverage for `imports/` app — 34 tests across org-matcher fuzzy logic, ImportJob lifecycle, rollback flow (matched orgs preserved, created orgs deleted), unique-together constraints on mapping models, CSV preview helper *(shipped v3.17.187)* ➡ **Wave 1 closed (v3.17.171 → v3.17.187)** — every item from the original Phase 7 polish survey has been delivered or explicitly deferred (reCAPTCHA needs Google credentials; scheduler email-send is a feature gap not polish; welcome-email on member-add *was* a feature gap, **shipped v3.17.214**). Phase 7 stays `[in progress]` by design (continuous track) — the next wave will be triggered by user-reported issues + the next bug-bash audit. ### Wave 2 — going-from-zero baselines (v3.17.192 → ongoing) The audit punch-list flagged 16 apps with no test coverage. Wave 2 is a sustained pass through them. Each app gets baseline tests (model behavior, view tenant-isolation, key happy-path flows) — not feature-complete, but enough to surface latent crashes and lock down the contract going forward. **The pattern is paying off.** Three of the first six baselines surfaced real production bugs that had been latent for months — usually a wrong kwarg name, a stale field reference, or a `hasattr` check that doesn't catch `None`. Bugs caught: - `psa/tests.py` (5,465 lines / 220+ cases) split into 5 topical shards under `psa/tests/` so CI can run them under the 540s timeout. 271 tests across 5 shards, max shard 147s *(shipped v3.17.192)* - **`api/` baseline (11 tests across 6 classes)** — caught two real bugs: `AssetViewSet.filterset_fields` and `AssetSerializer.Meta.fields` both referenced `is_active` and `location` columns that don't exist on the Asset model. Every `/api/assets/` list and detail request 500'd. Fixed in same release *(shipped v3.17.193)* - **`audit/` baseline (30 tests across 7 classes)** — caught `AuditLoggingMiddleware._is_update()` crashing on POST to unresolved URLs (`hasattr(req, 'resolver_match')` returns True for None values; `None.kwargs` then raised `AttributeError`). Fixed in same release *(shipped v3.17.195)* - `assets/` baseline (16 tests across 6 classes) — model behavior, OrganizationManager filtering, view tenant-isolation, EquipmentModel back-reference. No production bugs surfaced *(shipped v3.17.196)* - `monitoring/` baseline (22 tests across 5 classes) — WebsiteMonitor + Expiration property logic, IPAM `(subnet, ip_address)` unique-together dedupe contract, `for_organization()` filtering. No production bugs surfaced *(shipped v3.17.197)* - `processes/` baseline (19 tests across 4 classes) — workflow engine slug auto-gen, ProcessExecution lifecycle, completion-percentage math (incl. zero-stages safety), `(execution, stage)` unique-together. No production bugs surfaced *(shipped v3.17.199)* - `files/` baseline (9 tests across 2 classes) — `attachment_upload_path()` enforces per-org / per-entity directory structure with UUID filenames (filesystem tenant-isolation contract). No production bugs surfaced *(shipped v3.17.200)* - `scheduling/` baseline (20 tests across 5 classes) — recurrence math for every cadence, `check_completion` any-of vs all-of sign-off, `spawn_next_occurrence` clones assignments + tags. No production bugs surfaced *(shipped v3.17.201)* - `locations/` baseline (20 tests across 3 classes) — HQ-uniqueness invariant, shared-location ACL, `full_address` formatting, WAN bandwidth display. No production bugs surfaced *(shipped v3.17.202)* - `docs/` baseline (13 tests across 5 classes) — Document slug auto-gen, version-snapshot lifecycle (no row on first save, increments on edit), Diagram slug, `is_global` cross-tenant KB. No production bugs surfaced *(shipped v3.17.203)* - `inventory/` baseline (15 tests across 5 classes) — InventoryItem QR-code auto-gen, low-stock boundary, total_value math, transaction signed-quantity formatting. No production bugs surfaced *(shipped v3.17.204)* - `vehicles/` baseline (18 tests across 3 classes) — ServiceVehicle expiry warnings, `has_location` flag, `update_location()` setter, fleet inventory low-stock + needs-restock + total-value. No production bugs surfaced *(shipped v3.17.205)* ➡ **Wave 2 closed (v3.17.192 → v3.17.205)** — every one of the 16 originally-untested apps now has baseline coverage. **Final ratio: 3 of 11 baseline efforts surfaced real production bugs that had been latent for months** (api/, audit/, the v3.17.171 tenant-isolation rebuild before the wave formally started). All bugs caught were stale-attribute / wrong-kwarg / hasattr-vs-None patterns — the same family the next wave should look for first. Wave 2 totals: ~280 new tests across 11 modules in 14 commits. Combined with the v3.17.192 psa-tests shard split, the project's test runtime now fits comfortably under any reasonable CI ceiling: each shard ≤ 147s, smaller apps ≤ 7s. - Installer never installed the background timers — a fresh install had no scheduler, no monitoring and no breach scan, while looking perfectly healthy; `install.sh` now installs all seven with user/path rewriting, enables the operational ones, and leaves `auto-update` and `accounting-sync` off as operator decisions *(shipped v3.17.534)* ✅ **Phase 7 marked complete at v3.17.207.** All three pillars have shipped: outsourcing (v3.17.166), Integration SDK skeleton (v3.17.166) + first reference adapter (v3.17.168), and the polish-backlog test-coverage push (Waves 1 + 2). Ongoing polish continues as routine maintenance — bug fixes, deprecation sweeps, new vendor adapters in the SDK — but it's no longer tracked as an active phase. Future polish lands as standalone releases under whatever phase the change applies to (e.g. a vault-side improvement = Phase 31 polish). ## Phase 9 — Security alert ingestion: EDR / AV / Firewall on the dashboard **(M)** [shipped — v3.17.168] MSPs run a stack of security tools that all alert independently — SentinelOne, CrowdStrike, Defender, Sophos, Bitdefender, Webroot, Fortinet, Palo Alto, Sonicwall, etc. The PSA dashboard should aggregate alerts from all of them, surface critical issues per client, and let techs triage from one screen. ### Sub-phase 9.1 — Connection framework *(shipped v3.17.168)* - Generic `SecurityVendorConnection` model: provider type (edr/av/firewall), org-scoped credentials (encrypted), poll interval, last_sync_at, last_error - Provider type enums covering EDR (CrowdStrike Falcon, SentinelOne Singularity, Microsoft Defender for Endpoint, Sophos Central, Huntress, ThreatLocker), AV (Bitdefender GravityZone, Webroot, Malwarebytes, ESET), Firewall (Fortinet FortiGate, Palo Alto, Sonicwall, Cisco Meraki MX, Sophos XG, pfSense) - Two-way mapping: each connection optionally pinned to a client `Organization` (multi-tenant alert routing) - Reuses existing integration patterns (status pill from v3.17.135) ### Sub-phase 9.2 — Alert model + poller *(shipped v3.17.168)* - `SecurityAlert` model: connection FK, external_id (dedupe key), severity (info/low/medium/high/critical), title, description, asset hint, raw_payload (JSON), seen_at, acknowledged_by, acknowledged_at, status (new/acknowledged/dismissed/resolved), auto_ticket FK (optional) - Per-vendor poller adapters returning normalized alert dicts; one mgmt cmd `poll_security_alerts` runs every 5 min via cron - Idempotent dedupe by (connection_id, external_id) - Alert webhook receiver endpoint `/security/webhook//` for vendors that push (HMAC-verified) ### Sub-phase 9.3 — Dashboard + auto-ticketing *(shipped v3.17.168)* - New "Security alerts" card on the global dashboard + per-org dashboard: count by severity in the last 24h, drill-down to filtered list, color-coded - New page `/security/alerts/` — full triage UI; filter by severity / vendor / client / status - Per-vendor or per-severity rule: auto-create a PSA ticket from an alert (priority mapped from severity, queue configurable, assignee from a default rule). Mirrors workflow rules engine (v3.17.111). - Bulk "ack / dismiss / convert to ticket" actions ### Sub-phase 9.4 — Reporting *(shipped v3.17.168)* - "Mean time to acknowledge" metric per client / per vendor - Suppression rules (don't auto-ticket from this vendor between 22:00–06:00 etc.) - Weekly digest email to client of unresolved alerts (opt-in) *(deferred — pending email subscription mgmt; framework + MTTA shipped)* **Sizing:** **M** — 9.1+9.2 = 2 weeks (one reference adapter + the framework), 9.3 = 2 weeks, 9.4 = 1 week. Each new vendor adapter beyond the first is ~2-4 days. Ship the framework + 1-2 reference adapters first, then add vendors as customer demand arrives. **Dependencies:** none. Can run alongside Phase 2 / 3 / 8. --- # Long-term roadmap — Phases 10-23 The following are **planned / in-progress** items focused on MSP workflow consolidation and operational visibility. None are positioned as fully implemented. Items overlapping with already-shipped phases are noted as "Extends X" so the deltas are explicit. AI-assisted features are clearly marked **OPTIONAL AI**. ## Phase 10 — Advanced Email-to-Ticket Engine **(M)** [complete] **Roadmap item:** Advanced Email Processing & Ticket Intelligence. Extends the basic IMAP poller already shipped (v3.17.83+). Planned capabilities: - Advanced inbound email parsing (HTML + plain-text fallback) *(10.2 — shipped v3.17.177)* - Thread reconstruction across replies + forwards *(10.1 — shipped v3.17.176)* - Reply correlation by Message-ID + In-Reply-To headers (more reliable than current subject-regex match) *(10.1 — shipped v3.17.176)* - Signature stripping *(10.2 — shipped v3.17.177)* - Loop detection (ignore auto-responders) *(10.3 — shipped v3.17.188)* - Spam scoring before ticket creation *(10.3 — shipped v3.17.188)* - Attachment extraction with MIME-type allowlist *(10.2 — shipped v3.17.177)* - Automatic contact association (match sender email → existing contact / membership) *(planned — Phase 10.3.1)* - Per-client parsing rules *(10.3 — shipped v3.17.188 via EmailRoutingRule)* - Ticket categorization (rule-based) *(10.3 — shipped v3.17.188 via routing rule queue/priority overrides)* - Ticket tagging - Email security validation (SPF / DKIM / DMARC inspection) *(10.3 — shipped v3.17.188; opt-in via enforce_dmarc)* - Outbound threading + per-ticket conversation panel *(10.4 — shipped v3.17.189)* - Microsoft 365 mailbox source via Graph API (alternative to IMAP for tenants that can't enable IMAP) *(10.5 — shipped v3.17.506 — `EmailIngestionConfig.source='graph'` reuses an existing M365 connection's app registration (needs `Mail.Read`); the poller fetches each unread message's MIME via Graph and runs the identical threading/quarantine/attachment pipeline, then marks it read — issue #142)* - Microsoft 365 **outbound** via Graph (optional transport; SMTP remains default) *(10.6 — shipped v3.17.507 — `EmailIngestionConfig.outbound_method='graph'` sends staff replies via Graph `sendMail`/`reply`/`replyAll` on the same mailbox (needs `Mail.Send`, scoped via Exchange Online RBAC for Applications). Durable `EmailOutboundJob` with idempotent claim-lock, 202-accepted tracking, Retry-After-aware retries for 429/5xx, fail-fast on auth/validation, and an 'uncertain' state for post-submit timeouts (never auto-resent). Immutable Graph message ids stored on inbound so replies preserve the M365 conversation. Readiness check + labeled test-send in the mailbox form — issue #142)* - Reply-to-requester from the ticket form *(10.7 — shipped v3.17.508 — an opt-in "Email this reply to …" checkbox on the ticket reply box sends the comment to the requester through whichever transport the originating mailbox is configured for (SMTP or Graph), closing the round trip so outbound replies are captured in the conversation panel and, on Graph, in the mailbox's Sent Items. Off by default; internal notes are never emailed; a send failure reports but never discards the saved comment — issue #142)* - Ticket summarization (**OPTIONAL AI**) - Intent detection (**OPTIONAL AI**) ### Sub-phase 10.1 — Threading + Message-ID correlation *(shipped v3.17.176)* - New `EmailMessage` model captures every inbound (and later, outbound) email's Message-ID, In-Reply-To, References, headers, and bodies. Unique per `(organization, message_id)` so cross-tenant Message-ID collisions never thread incorrectly. - New `Ticket.last_inbound_message_id` cache feeds outbound threading in 10.4. - Poller correlation order is now: (a) In-Reply-To against `EmailMessage.message_id` in the same org, (b) walk the References chain right-to-left, (c) subject-regex fallback (legacy tickets keep working), (d) create new ticket. Whichever path matches, the inbound message is persisted as an `EmailMessage` row so the next reply has something to chain against. - Tests cover header-threading, References-chain walking, subject-regex fallback for legacy replies, cross-org isolation, and new-ticket creation. ### Sub-phase 10.2 — Body cleanup + attachment ingestion *(shipped v3.17.177)* - New `psa/email_parsing.py` helper module: `sanitize_html` (bleach with tight allowlist; strips scripts, styles, iframes, inline event handlers, remote images / tracking pixels; rewrites links with `rel="noopener noreferrer" target="_blank"`), `strip_signature` (RFC 3676 `\n-- \n` sentinel + mobile/marketing prefaces), `strip_quoted_reply` (Apple/Gmail "On … wrote:", Outlook `-----Original Message-----` / From-Sent-To-Subject block, trailing `>`-prefix block). - Reply comments to existing tickets get sig + quoted history stripped so only the new content shows. Full original body still preserved on `EmailMessage.body_text` for the Phase 10.4 conversation panel. New tickets keep the full body so context isn't lost. - Attachment ingestion via `_ingest_attachments`: walks parts for `Content-Disposition: attachment`, validates against `PSA_EMAIL_ATTACHMENT_MIME_ALLOWLIST` (default: images, PDF, text, Office, ZIP) + `PSA_EMAIL_ATTACHMENT_MAX_BYTES` (default 25 MB), writes accepted files as `TicketAttachment` rows linked to the new comment. Rejected files logged at WARNING level. Filenames sanitized. - Inbound HTML bodies are sanitized at write time before landing on `EmailMessage.body_html`. ### Sub-phase 10.3 — Routing rules + auto-responder & spam gating *(shipped v3.17.188)* - New `EmailRoutingRule` model with sender-domain glob matching (exact `acme.com`, subdomain `*.acme.com`, full-email `noreply@acme.com`); MSP's generic `help@msp.com` mailbox now fans inbound mail to the right client tenant + queue + priority. First match wins, ordered by `order` (lower fires first). - Auto-responder detection via `detect_auto_responder()` — `Auto-Submitted`, `X-Autoreply`, `Precedence: bulk/list/junk`, NDR `multipart/report`, plus subject heuristics for "Out of Office" / "Vacation Auto-Reply" / "Undeliverable". - DMARC verdict gate — opt-in per-config (`enforce_dmarc`); reads upstream MTA's `Authentication-Results` header; no inline crypto / DNS. - Spam-keyword scorer — opt-in per-config (`spam_keyword_threshold > 0`); conservative pattern list (claim-your-prize, congratulations-winner, nigerian-prince, etc.). - Quarantined inbound persists with `was_quarantined=True` + reason but NEVER creates a ticket; admins triage via Django admin. - 16 new tests across 6 classes covering all four gates plus routing rule end-to-end. ### Sub-phase 10.4 — Outbound threading + conversation panel *(shipped v3.17.189)* - New `psa/email_outbound.py::send_threaded_reply()` helper — generates Message-ID, sets `In-Reply-To` + `References` from `Ticket.last_inbound_message_id`, sends via Django email backend with optional HTML alternative, persists `EmailMessage(direction='out')` row so future replies thread back via 10.1's `_thread_target` (closing the round-trip). - New per-ticket conversation view at `/psa/t//conversation/` — chronological inbound + outbound rows, HTML bodies rendered in `