---
phase: quick-260718-7v8
plan: 01
subsystem: api
tags: [mimecast, blast-radius, phishing, false-positive-fix]

# Dependency graph
requires:
  - phase: 17-mimecast-blast-radius-lookup
    provides: getBlastRadius() fan-out orchestration and MimecastClient
provides:
  - Date-scoped getHeldMessages() (optional start/end threaded into request body)
  - Sender-domain relevance guard on held rows before counting/merging
affects: [17-mimecast-blast-radius-lookup, 22-approval-ui-livelink-addressable-campaign-review-and-approve]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "Sender-relevance guard: filter external-API rows by domain match against campaign input.sender BEFORE they feed counts or a per-recipient merge — mirrors campaign-classifier's exact-or-proper-subdomain comparison without reusing its allowlist-specific function."

key-files:
  created: []
  modified:
    - lib/services/mimecast-client.ts
    - lib/services/mimecast-blast-radius.ts
    - lib/services/mimecast-blast-radius.test.ts
    - lib/services/mimecast-client.test.ts

key-decisions:
  - "getHeldMessages() start/end are pre-formatted strings supplied by the caller (mirrors searchDeliveredMessages), never formatted inside the client method."
  - "domainsMatch() is a new local helper in mimecast-blast-radius.ts, not a reuse of campaign-classifier's domainMatchesAllowlist (that helper is allowlist-specific)."
  - "Held-row counts (held/matched) and the perRecipient merge both use the same relevantHeldRows array so a filtered-out row can never leak into either output."

requirements-completed: [BUG-BLAST-RADIUS-HELD]

# Metrics
duration: 15min
completed: 2026-07-18
---

# Quick Task 260718-7v8: Fix Mimecast Blast-Radius Held-Message False Positives Summary

**Date-scoped `getHeldMessages()` plus a sender-domain relevance guard eliminate unrelated held-message false positives in the Blast Radius panel.**

## Performance

- **Duration:** ~15 min
- **Started:** 2026-07-18T09:35:00Z (approx.)
- **Completed:** 2026-07-18T09:46:50Z
- **Tasks:** 2
- **Files modified:** 4

## Accomplishments
- `MimecastClient.getHeldMessages()` now accepts optional `start`/`end` and threads them into `data[0]` as siblings of `admin`/`searchBy` (backward compatible when omitted; the existing 403 fallback body inherits them automatically via its `{ ...reqBody }` spread).
- `getBlastRadius()` passes the same `startStr`/`endStr` window already computed for `searchDeliveredMessages` into `getHeldMessages()`, so the held-queue lookup is scoped to the reported campaign's window instead of the recipient's entire hold queue.
- Added a `domainsMatch()` sender-relevance guard in `mimecast-blast-radius.ts`: held rows whose sender domain does not match `input.sender`'s domain (exact-or-proper-subdomain, case-insensitive) are filtered out of `relevantHeldRows` before they can contribute to `held`/`matched` counts or the `perRecipient` merge.
- Extended test coverage in both files: date-window threading assertion, unrelated-sender-domain exclusion test, matching-sender-domain override-still-works test, and client-level POST-body-shape tests for `start`/`end` presence/absence.

## Task Commits

Each task was committed atomically:

1. **Task 1: Date-scope getHeldMessages and add sender-relevance guard in getBlastRadius** - `94f7dad` (fix)
2. **Task 2: Extend test coverage for date-scoping and sender-relevance filtering** - `67ee680` (test)

**Plan metadata:** committed separately by the orchestrator after this summary.

## Files Created/Modified
- `lib/services/mimecast-client.ts` - `getHeldMessages()` gains optional `start`/`end` params, threaded into the request body's `data[0]` alongside `admin`/`searchBy`.
- `lib/services/mimecast-blast-radius.ts` - `getBlastRadius()` passes the shared date window into `getHeldMessages()`; new `domainsMatch()` helper filters held rows by sender-domain relevance before they feed `held`/`matched`/`perRecipient`.
- `lib/services/mimecast-blast-radius.test.ts` - new tests: shared-window assertion, unrelated-sender exclusion, matching-sender override still works.
- `lib/services/mimecast-client.test.ts` - new `describe('getHeldMessages date-scoping')` block: asserts `start`/`end` presence/absence in the POST body via a `request` spy.

## Decisions Made
- Followed the plan's interface precedent exactly: caller pre-formats `start`/`end` strings (same pattern as `searchDeliveredMessages`); the client method does no date formatting itself.
- `domainsMatch()` intentionally duplicates (rather than imports) the exact-or-proper-subdomain comparison logic from `campaign-classifier.ts`'s `domainMatchesAllowlist`, per the plan's explicit instruction not to reuse that allowlist-specific function.
- Kept the existing "merges delivered/held/threat-event fixtures" test's held row using `from: BASE_INPUT.sender` (a domain match) so it continues to pass unchanged — confirmed via the test run (25/25 passing).

## Deviations from Plan

None — plan executed exactly as written. No architectural changes, no scope additions beyond the two locked fixes and their specified test coverage.

## Issues Encountered

Two TypeScript errors surfaced in the new client test (`'body' is of type 'unknown'` from destructuring `vi.spyOn(...).mock.calls[0]` directly) — fixed inline by typing the extracted body as `any` off the correct tuple index (`requestSpy.mock.calls[0][2]`) before asserting on `body.data[0]`. This is an in-scope Rule 1 fix (blocking type error in code written as part of this same task), not a deviation from the plan's intent.

## Known Stubs

None.

## Threat Flags

None — no new network endpoints, auth paths, or trust-boundary changes. This plan only tightens an existing data-filtering boundary (held-message relevance) inside an already-reviewed lookup module.

## User Setup Required

None - no external service configuration required.

## Next Phase Readiness

The Blast Radius panel (Phase 17/22 consumers) now returns a correctly date-scoped and sender-relevance-filtered held count for any campaign lookup. No follow-up work is required; this was a standalone bug fix confirmed against two live verification scripts referenced in the plan (date-scoping alone: 15→1 false held rows; sender-relevance guard: 1→0).

---
*Quick task: 260718-7v8*
*Completed: 2026-07-18*

## Self-Check: PASSED

All modified/created files confirmed present on disk; both task commits (94f7dad, 67ee680) confirmed present in git log.
