--- phase: 14-rss-email-alert-ingestion-module-rollout plan: 01 subsystem: api tags: [nestjs, imapflow, httpntlm, ews, vitest, refactor] # Dependency graph requires: [] provides: - "apps/api/src/inbox/ module: InboxModule, InboxProvider, ImapProvider, ExchangeInboxProvider, InboxConfig/InboxAttachment/InboxEmail/InboxMessage" - "InboxProvider.fetchMessages(config) seam — subject + HTML/text body per unread message, shared by any future module needing mailbox polling" - "DKV switched to the shared inbox/ import path with zero behavior change (regression-gated by full API suite)" affects: [14-03-email-alert-adapter] # Tech tracking tech-stack: added: [] patterns: - "Shared connection-only module extraction: inbox/ shares IMAP/EWS mechanics, each consuming module keeps its own independent config (D-03)" - "require.cache stub for testing raw CommonJS require() dependencies that vi.mock cannot intercept (httpntlm)" key-files: created: - apps/api/src/inbox/inbox.module.ts - apps/api/src/inbox/inbox.types.ts - apps/api/src/inbox/inbox-provider.interface.ts - apps/api/src/inbox/imap.provider.ts - apps/api/src/inbox/exchange-inbox.provider.ts - apps/api/src/inbox/imap.provider.spec.ts - apps/api/src/inbox/exchange-inbox.provider.spec.ts modified: - apps/api/src/dkv/dkv.service.ts - apps/api/src/dkv/dkv.module.ts - apps/api/src/dkv/dkv.types.ts key-decisions: - "Pure move + import-path swap for Task 1 — no logic changes to fetchPdfAttachments, InboxConfig fields, or DKV pipeline (D-01/D-02 regression guard)" - "dkv.types.ts re-exports InboxConfig/InboxAttachment/InboxEmail from ../inbox/inbox.types instead of declaring them, so no DKV consumer import needed to change" - "fetchMessages implemented as a sibling method (findBodyParts for IMAP MIME walk, getItemBodySoap for EWS) — does not touch or call into the existing PDF-attachment code paths" - "httpntlm is loaded via a raw CommonJS require() in exchange-inbox.provider.ts, which vi.mock cannot intercept (verified via repro) — tests instead pre-seed Node's require.cache for the resolved httpntlm path with a stub before dynamically importing the provider in beforeAll" patterns-established: - "Shared inbox/ module is the seam for any future mailbox-polling module (Plan 14-03's EmailAlertAdapter will inject ImapProvider/ExchangeInboxProvider from InboxModule)" requirements-completed: [INGEST-05] coverage: - id: D1 description: "DKV's IMAP/Exchange providers moved into shared apps/api/src/inbox/ module; DKV imports switched to the new path with zero behavior change" requirement: "INGEST-05" verification: - kind: unit ref: "cd apps/api && npx tsc --noEmit -p tsconfig.json && npx vitest run — full 301-test suite green (no DKV spec files exist yet, so this is a compile + no-regression-elsewhere gate)" status: pass human_judgment: false - id: D2 description: "New fetchMessages(config) method on InboxProvider/ImapProvider/ExchangeInboxProvider returns each unread message's subject + HTML/text body, without touching fetchPdfAttachments" requirement: "INGEST-05" verification: - kind: unit ref: "apps/api/src/inbox/imap.provider.spec.ts (7 tests) + apps/api/src/inbox/exchange-inbox.provider.spec.ts (6 tests)" status: pass human_judgment: false duration: 13min completed: 2026-07-23 status: complete --- # Phase 14 Plan 01: Inbox Module Extraction Summary **Extracted DKV's IMAP/Exchange inbox providers into a shared `apps/api/src/inbox/` module and added an additive `fetchMessages()` method returning subject + HTML/text body per unread message — the seam Plan 14-03's EmailAlertAdapter will consume.** ## Performance - **Duration:** ~13 min - **Started:** 2026-07-23T12:59:00+02:00 (approx.) - **Completed:** 2026-07-23T13:09:45+02:00 - **Tasks:** 2/2 completed - **Files modified:** 10 (5 new, 3 modified in Task 1; 2 new specs + 3 further modified in Task 2) ## Accomplishments - Moved `InboxProvider`, `ImapProvider`, `ExchangeInboxProvider`, and `InboxConfig`/`InboxAttachment`/`InboxEmail` verbatim from `apps/api/src/dkv/providers/` into `apps/api/src/inbox/`, with `dkv.types.ts` re-exporting the moved types so no other DKV file's import needed to change - New `InboxModule` (NestJS `@Module`) provides + exports both providers; `DkvModule` now imports `InboxModule` instead of declaring the providers directly - Added `fetchMessages(config): Promise` to the interface and both providers, entirely additive — `fetchPdfAttachments` bodies are byte-identical to before the move (verified via `git diff`) - Net-new unit coverage: 13 tests across `imap.provider.spec.ts` (7) and `exchange-inbox.provider.spec.ts` (6), covering body extraction, idempotent \Seen/IsRead marking, sender filtering, and graceful `[]` returns on connect/search/HTTP errors ## Task Commits Each task was committed atomically: 1. **Task 1: Move inbox providers into apps/api/src/inbox/ (behavior-preserving)** - `eb668fd` (refactor) 2. **Task 2: Add fetchMessages() to InboxProvider + both implementations (net-new coverage)** - `c404954` (feat, TDD RED confirmed via 13 failing "not a function" assertions before implementation) **Plan metadata:** (this commit, to follow) ## Files Created/Modified - `apps/api/src/inbox/inbox.module.ts` - New NestJS module providing/exporting ImapProvider + ExchangeInboxProvider - `apps/api/src/inbox/inbox.types.ts` - InboxConfig, InboxAttachment, InboxEmail (moved) + new InboxMessage type - `apps/api/src/inbox/inbox-provider.interface.ts` - InboxProvider interface, now with fetchMessages - `apps/api/src/inbox/imap.provider.ts` - ImapProvider, moved + fetchMessages/findBodyParts added - `apps/api/src/inbox/exchange-inbox.provider.ts` - ExchangeInboxProvider, moved + fetchMessages/getItemBodySoap/fetchMessagesViaEws added - `apps/api/src/inbox/imap.provider.spec.ts` - New: mocks ImapFlow, covers fetchMessages - `apps/api/src/inbox/exchange-inbox.provider.spec.ts` - New: mocks httpntlm.post via require.cache stub, covers fetchMessages - `apps/api/src/dkv/dkv.service.ts` - Import paths for ImapProvider/ExchangeInboxProvider changed to `../inbox/...` - `apps/api/src/dkv/dkv.module.ts` - Imports InboxModule instead of declaring the two providers directly - `apps/api/src/dkv/dkv.types.ts` - Re-exports InboxConfig/InboxAttachment/InboxEmail from `../inbox/inbox.types` ## Decisions Made - **Pure move, no logic changes (Task 1):** `fetchPdfAttachments`, `testConnection`, and all EWS SOAP builders were copied verbatim; only import paths changed. Verified via `git diff HEAD~1 HEAD` showing zero edits inside the pre-existing method bodies. - **Re-export shim in dkv.types.ts:** rather than updating every DKV file that imports `InboxConfig`/`InboxAttachment`/`InboxEmail`, `dkv.types.ts` now re-exports them from `../inbox/inbox.types`, so only the two files that imported the *providers* (dkv.service.ts, dkv.module.ts) needed edits. - **fetchMessages as a true sibling, not a refactor of fetchPdfAttachments:** IMAP's `findBodyParts` mirrors `collectPdfParts`'s tree-walk shape but is a separate function; EWS's `getItemBodySoap` is a separate SOAP builder from `getItemSoap`. Neither touches the PDF-attachment call path — this was required by D-01/D-02 (DKV must stay behavior-identical) and verified by grepping the diff for edits inside `fetchPdfAttachments`/`fetchViaEws`. - **Testing httpntlm's raw `require()`:** `vi.mock('httpntlm', ...)` was verified (via a throwaway repro test) to have zero effect on `exchange-inbox.provider.ts`'s `const httpntlm = require('httpntlm')` — Vitest's mock interception only covers the ESM module graph, and a literal `require()` call resolves through Node's real module cache regardless. The spec instead pre-seeds `require.cache[require.resolve('httpntlm')]` with a stub `{ post }` in a `beforeAll` (dynamic `import()` of the provider, deferred out of module scope so the file stays compatible with the project's `commonjs` tsconfig which rejects top-level `await`/`import.meta`). ## Deviations from Plan None — plan executed exactly as written. The `require.cache` testing approach for `exchange-inbox.provider.spec.ts` is a test-infrastructure detail within Task 2's stated scope ("mirroring cosinex.adapter.spec.ts's mock style ... stub httpntlm.post") — the plan did not prescribe *how* to intercept a raw CJS `require()`, and the chosen approach achieves the same outcome (httpntlm.post fully mocked, zero real network calls) without touching the provider's production code (which the plan explicitly forbids "improving" or refactoring). ## Issues Encountered - Discovered mid-Task-2 that `vi.mock('httpntlm', ...)` does not intercept `exchange-inbox.provider.ts`'s raw `require('httpntlm')` call (confirmed via a minimal repro: `vi.isMockFunction(require('httpntlm').post)` returned `false`, and all 6 initial test attempts hit a real DNS lookup — `getaddrinfo ENOTFOUND mail.example.com` — with 3 "passing" only by coincidence since their assertions expected `[]`, which a failed network call also produces). Resolved via the `require.cache` stub approach described above; re-verified all 6 exchange-inbox tests genuinely exercise the mocked `httpntlm.post` (assertions on `messages[0]` content, not just `[]`). ## User Setup Required None - no external service configuration required. ## Next Phase Readiness - `apps/api/src/inbox/` is ready for Plan 14-03's `EmailAlertAdapter` to inject `ImapProvider`/`ExchangeInboxProvider` (via `InboxModule`) and call `fetchMessages(config)` for the tender-alert mailbox — with its own independent mailbox config (D-03), not DKV's. - DKV's regression surface (fetchPdfAttachments, the full pipeline) is unchanged and the full API suite (301 tests) is green; no DKV-specific spec file exists yet, so the regression guard for this plan was the compile (`tsc --noEmit`) plus the rest of the suite not regressing — a gap the plan itself flagged as pre-existing (no `dkv/*.spec.ts` files in the repo). --- *Phase: 14-rss-email-alert-ingestion-module-rollout* *Completed: 2026-07-23* ## Self-Check: PASSED All 7 created/modified inbox files verified on disk; both task commits (`eb668fd`, `c404954`) verified in `git log --oneline --all`.