# Phase 17: Mimecast Blast Radius Lookup - Context

**Gathered:** 2026-07-15
**Status:** Ready for planning

<domain>
## Phase Boundary

Pulse gains a Mimecast "blast radius" lookup abstraction: given a reported
message's identity (Message-ID, sender, recipient/reporter, subject, date
window), the abstraction returns normalized delivery data — matched/
delivered/held/rejected/clicked counts and per-recipient status — when
Mimecast is configured, and a clean `status: unavailable` signal (never a
throw, timeout, or block) when it isn't. This is a pure lookup/abstraction
layer; it does not classify, remediate, or otherwise act on the data. Phase
19's classifier is the consumer and must be able to call it without knowing
whether Mimecast is present.

</domain>

<decisions>
## Implementation Decisions

### Query Strategy
- **D-01 (corrected by research):** `getMessageInfo(messageId)` returns only
  `{ messageId, bodyText, bodyHtml, headers }` — no status, no counts, no
  per-recipient data. It is a supplementary body/header fetch only, never a
  substitute for the counts. The fan-out — `searchDeliveredMessages` +
  `getHeldMessages` + `getThreatEvents` keyed on sender/subject/date-window,
  merged into one normalized blast-radius shape (matched/delivered/held/
  rejected/clicked counts + per-recipient status) — runs **unconditionally**,
  not as a fallback path. `getMessageInfo` may still be called opportunistically
  for body/header evidence but must never gate whether the fan-out runs. This is the
  most complete picture available from the existing `MimecastClient` methods,
  at the cost of more API calls on the fallback path.

### Clicked Count
- **D-02:** Mimecast's currently-wrapped API surface (`lib/services/
  mimecast-client.ts`) has no URL-click-tracking method. Derive `clicked`
  best-effort from `getThreatEvents()` — where Mimecast's TTP URL Protect
  logs a click as a threat-event subtype. If no matching click-type event is
  found, `clicked` is `0` (not `unavailable`) — this is a best-effort signal,
  documented as such in code, not a guaranteed complete count. Do not add a
  new dedicated click-log API method in this phase.

### Persistence
- **D-03:** The blast-radius lookup is ephemeral — a pure function call that
  returns normalized data, nothing more. No new migration, no new table/
  column in this phase. If Phase 19's classifier wants to persist a result,
  it does so via its own `classifications.reasons` JSONB column (already
  defined in migration 097) — that is Phase 19's concern, not Phase 17's.

### Caching
- **D-04:** Repeat lookups for the same message are cached in Redis via the
  existing `lib/services/redis-client.ts`, keyed on message identity
  (Message-ID when known, else a composite of sender+subject+date-window),
  with a **5-minute TTL** — matching the existing cache pattern in
  `lib/services/integration-health.ts`. This protects against Mimecast API
  hammering when Phase 19 re-classifies a campaign multiple times in quick
  succession, while staying short enough that data doesn't go stale across a
  single classification session.

### Tenant Scope
- **D-05 (added post-research):** Research surfaced an existing per-company
  Mimecast multi-tenant setup (`mimecast_tenants` table, migration 062,
  consumed via `getMimecastClientForTenant()` in the existing `/api/mimecast/
  held` and `/api/mimecast/delivered` routes) that this phase's original
  scope did not address. Decision: use **only the single global env-var
  client** (`getMimecastClient()`) for v1 — do not resolve `reports.company_id`
  to a per-tenant client in this phase. This must be documented explicitly in
  code comments as a known limitation: reports from companies with their own
  `mimecast_tenants` row (not covered by the global `MIMECAST_CLIENT_ID`) will
  return `unavailable` even though Mimecast is technically configured for
  that company. Per-tenant resolution is a small, mechanical follow-up later
  (swap `getMimecastClient()` for a tenant lookup + `getMimecastClientForTenant()`,
  same fan-out logic) if it turns out to be needed in practice — not blocking
  for this phase.

### Claude's Discretion (explicitly deferred to research + planner)
- **`isMimecastConfigured()` helper.** `lib/services/mimecast-client.ts`
  currently has `getMimecastClient()` (throws if `MIMECAST_CLIENT_ID`/
  `MIMECAST_CLIENT_SECRET` are missing) but no `is<Name>Configured()` helper
  matching the project's factory convention (see CLAUDE.md). Add one,
  checking the same two env vars `lib/services/integration-health.ts`
  already checks via `checkConfigOnly('mimecast', ...)`. Exact location
  (alongside `getMimecastClient` in `mimecast-client.ts`, vs. a new
  `mimecast-factory.ts` mirroring `veeam-factory.ts`) is planner's call —
  follow whichever existing precedent is cleaner given the file is already
  1000+ lines.
- **New module name/location for the blast-radius abstraction itself**
  (e.g. `lib/services/mimecast-blast-radius.ts`) — orchestration logic that
  composes multiple `MimecastClient` calls into one normalized shape belongs
  in a new file, not bolted onto `mimecast-client.ts` directly, but exact
  naming/exports are planner's call.
- **Exact normalized output TypeScript shape** (field names, per-recipient
  status enum values) — derive from BLAST-01's requirement text
  (matched/delivered/held/rejected/clicked + per-recipient status) plus
  whatever shapes `MimecastHeldMessage`/`MimecastDeliveredMessage`/
  `MimecastThreatEvent` already provide — planner/researcher's call.
- **Date-window sizing** for the fan-out fallback query (e.g. ±48h around
  report creation time) — implementation detail, not a user preference call.
- **Redis cache key exact format** — planner's call, following whatever key
  format `redis-client.ts` callers already use elsewhere.

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Mimecast integration (existing)
- `lib/services/mimecast-client.ts` — existing `MimecastClient` class:
  `getMessageInfo`, `searchDeliveredMessages`, `getHeldMessages`,
  `getThreatEvents`, `getMimecastClient()` (throws if unconfigured, no
  `isMimecastConfigured()` yet). This phase builds on top of it, does not
  replace it.
- `lib/services/mimecast-sync-service.ts` — existing sync service consuming
  `MimecastClient` — shows established call patterns/error handling to
  follow.
- `lib/services/integration-health.ts` (`checkConfigOnly('mimecast', ...)`,
  around line 338) — the exact env vars (`MIMECAST_CLIENT_ID`,
  `MIMECAST_CLIENT_SECRET`) an `isMimecastConfigured()` helper must check,
  and the existing 5-minute health-cache pattern this phase's Redis caching
  should mirror.
- `docs/mimecast-api-guide.md` — Mimecast API reference already in the repo.

### Factory pattern precedent
- `lib/services/veeam-factory.ts` — canonical `is<Name>Configured()` +
  `get<Name>Client()` singleton factory shape to follow if a new
  `mimecast-factory.ts` is created.

### Schema (read-only for this phase)
- `migrations/097_phishing_triage_schema.sql` — `classifications` table
  (`reasons` JSONB) is where Phase 19 would persist a blast-radius result if
  it chooses to; Phase 17 does not touch this schema.

### Prior phase decisions (for consistency)
- `.planning/phases/16-eml-mime-evidence-parser/16-CONTEXT.md` (D-05, D-06,
  D-07) — shows the established pattern of reusing existing service
  infrastructure (B2 client, migration stubs) rather than inventing new
  infrastructure; this phase follows the same instinct by reusing
  `MimecastClient` + `redis-client.ts` rather than building fresh.

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `MimecastClient.getMessageInfo()`, `.searchDeliveredMessages()`,
  `.getHeldMessages()`, `.getThreatEvents()` — the four existing methods this
  phase's abstraction composes; no new Mimecast API surface needed beyond
  possibly deriving clicks from threat events.
- `lib/services/redis-client.ts` — existing Redis client for the caching
  decision (D-04).

### Established Patterns
- Factory convention: `get<Name>Client()` + `is<Name>Configured()`, credentials
  from env vars, throws if missing (see `veeam-factory.ts`). `mimecast-client.ts`
  only has half of this today.
- `checkConfigOnly()` pattern in `integration-health.ts` for config-only
  health checks with a short TTL cache — the caching precedent this phase's
  Redis TTL should match.

### Integration Points
- Phase 19's classifier (not yet built) is the sole consumer of this
  abstraction — it must be callable without the caller knowing whether
  Mimecast is configured.

</code_context>

<specifics>
## Specific Ideas

No specific UI/output-format requirements beyond what's captured in the
Decisions section above — this is a backend abstraction with no UI surface.

</specifics>

<deferred>
## Deferred Ideas

- **Dedicated Mimecast click-log API method** — deferred per D-02. If
  best-effort click derivation from threat events proves insufficient later,
  a real `getUrlClickLogs()`-style method against Mimecast's TTP URL Protect
  logs endpoint would be a future, separate addition (not blocking this
  phase or Phase 19).
- **Persisting blast-radius results to a new table/column** — deferred per
  D-03; left to Phase 19 (or a later phase) if audit/idempotency needs
  surface once the classifier is built.

None — discussion stayed within phase scope otherwise.

</deferred>

---

*Phase: 17-mimecast-blast-radius-lookup*
*Context gathered: 2026-07-15*
