Files
tessera-ctl/.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-01-PLAN.md
T

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
14-rss-email-alert-ingestion-module-rollout 01 execute 1
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
apps/api/src/dkv/dkv.service.ts
apps/api/src/dkv/dkv.module.ts
apps/api/src/dkv/dkv.types.ts
true
INGEST-05
truths artifacts key_links
DKV's existing fetchPdfAttachments behavior is unchanged after providers move to inbox/ (full API suite + tsc green)
A new fetchMessages(config) method on both providers returns each unread message's subject + html/text body
apps/api/src/inbox/ module exporting ImapProvider, ExchangeInboxProvider, InboxProvider, InboxMessage
apps/api/src/inbox/imap.provider.spec.ts + exchange-inbox.provider.spec.ts (net-new coverage)
dkv.service.ts + dkv.module.ts import the two providers from '../inbox/...' instead of './providers/...'
InboxProvider.fetchMessages is the seam the Phase-14 EmailAlertAdapter (Plan 14-03) consumes
Extract DKV's IMAP/Exchange inbox providers into a shared `apps/api/src/inbox/` module (D-01) and add an additive `fetchMessages()` method (D-02) that returns whole messages with their subject + HTML/text body. This is the one-time prerequisite refactor that lets the Plan 14-03 email-alert adapter reuse DKV's connection mechanics without any DKV behavior change.

Purpose: Share connection code only — not config (D-03). DKV runs in production and its Exchange/NTLM path is known-fragile (project_calendar_ews_fix); regression risk must be zero. Output: inbox/ module (moved files + new fetchMessages), DKV switched to the new import path only, and net-new unit coverage for the new method on both providers.

Phase Goal (MVP user story)

As a tenant admin, I want to have tenders parsed from my portal-alert mailbox, so that I stop missing Unterschwellen-Ausschreibungen that only arrive by email.

This plan is the enabling Wave-1 prerequisite for that story: it creates the shared inbox seam the email-alert slice (14-03) builds on. No user-facing behavior changes here; the user-visible outcome lands in 14-03.

Artifacts this phase produces

  • New module apps/api/src/inbox/ with: inbox.module.ts (provides+exports both providers), inbox.types.ts (InboxConfig, InboxAttachment, InboxEmail, new InboxMessage), inbox-provider.interface.ts (adds fetchMessages), imap.provider.ts, exchange-inbox.provider.ts (both gain fetchMessages).
  • New interface member InboxProvider.fetchMessages(config: InboxConfig): Promise<InboxMessage[]>.
  • New type InboxMessage = { uid: number|string; messageId: string; subject: string; from: string; date: Date; bodyHtml: string | null; bodyText: string }.
  • New spec files apps/api/src/inbox/imap.provider.spec.ts, apps/api/src/inbox/exchange-inbox.provider.spec.ts.
  • DKV re-export shim: dkv.types.ts re-exports the inbox types from ../inbox/inbox.types so existing DKV imports keep compiling.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-CONTEXT.md @.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-RESEARCH.md Task 1: Move inbox providers into apps/api/src/inbox/ (behavior-preserving) 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/dkv/dkv.service.ts, apps/api/src/dkv/dkv.module.ts, apps/api/src/dkv/dkv.types.ts - apps/api/src/dkv/providers/inbox-provider.interface.ts (interface to move) - apps/api/src/dkv/providers/imap.provider.ts (move + relative import fix) - apps/api/src/dkv/providers/exchange-inbox.provider.ts (move + relative import fix) - apps/api/src/dkv/dkv.types.ts (InboxConfig/InboxAttachment/InboxEmail live here; §47/§69/§78) - apps/api/src/dkv/dkv.service.ts (imports at §16-17, injects at §82-83) - apps/api/src/dkv/dkv.module.ts (declares the 2 providers directly) - RESEARCH.md "Runtime State Inventory" + "Recommended Project Structure" (confirms only 2 import-path edits + a re-export shim) Create apps/api/src/inbox/. Move `InboxConfig`, `InboxAttachment`, `InboxEmail` out of dkv.types.ts into a new inbox.types.ts (verbatim). In dkv.types.ts, replace those three interface declarations with a re-export: `export type { InboxConfig, InboxAttachment, InboxEmail } from '../inbox/inbox.types';` so every existing DKV consumer keeps compiling unchanged. Move inbox-provider.interface.ts, imap.provider.ts, exchange-inbox.provider.ts into inbox/ verbatim; update their relative imports to point at './inbox.types' (interface) and './inbox-provider.interface' (providers). Create inbox.module.ts: a NestJS @Module that declares `ImapProvider`, `ExchangeInboxProvider` as providers and lists both in `exports`. In dkv.module.ts, remove the two direct provider declarations from `providers[]`, add `InboxModule` to `imports[]`, and drop the two `./providers/...` import lines in favor of `import { InboxModule } from '../inbox/inbox.module';` (DkvService still injects ImapProvider/ExchangeInboxProvider — now resolved via the imported+exported providers). In dkv.service.ts, change the two provider import paths from `./providers/...` to `../inbox/...`. Delete the now-empty apps/api/src/dkv/providers/ directory. Do NOT change any provider method body, any InboxConfig field, or any DKV logic — this task is a pure move + import-path swap (D-01/D-02: DKV stays byte-behavior-identical). cd apps/api && npx tsc --noEmit -p tsconfig.json && npx vitest run src/dkv - `apps/api/src/dkv/providers/` no longer exists (`test ! -d apps/api/src/dkv/providers`). - `grep -rn "dkv/providers" apps/api/src` returns no matches. - `npx tsc --noEmit` exits 0; the full DKV spec set passes unchanged (regression guard for the fragile Exchange/NTLM path). All three inbox files live under apps/api/src/inbox/, DKV imports them via '../inbox/...' + InboxModule, DKV tests and typecheck are green, and no fetchPdfAttachments/InboxConfig semantics changed. Task 2: Add fetchMessages() to InboxProvider + both implementations (net-new coverage) apps/api/src/inbox/inbox-provider.interface.ts, apps/api/src/inbox/inbox.types.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 - apps/api/src/inbox/imap.provider.ts (collectPdfParts/streamToBuffer patterns to mirror for body-part walking) - apps/api/src/inbox/exchange-inbox.provider.ts (getItemSoap/extractAll/splitMessageBlocks/escapeXml helpers to extend) - apps/api/src/tenders/adapters/cosinex.adapter.spec.ts (fixture/mock spec style to mirror) - RESEARCH.md Pattern 2 "Additive inbox-provider method" (IMAP findBodyParts sketch; EWS `item:Body` FieldURI + BodyType extraction) and Pitfall 4 (EWS body-fetch is untested-fragile — mock-based coverage now, live human-verify is 14-03) - InboxMessage type added to inbox.types.ts: `{ uid, messageId, subject, from, date, bodyHtml: string|null, bodyText: string }`. - IMAP fetchMessages: given a mocked ImapFlow whose bodyStructure has a text/html part and a text/plain part, returns one InboxMessage with bodyHtml from the html part and bodyText from the plain part; marks each processed message \Seen (idempotency for re-polls); honors the same UNSEEN + optional senderFilter search as fetchPdfAttachments; returns [] on connect/search error without throwing. - EWS fetchMessages: given a mocked httpntlm.post returning a GetItem SOAP body with `...`, returns one InboxMessage with bodyHtml populated and bodyText empty; a `BodyType="Text"` response populates bodyText with bodyHtml null; marks the item read (IsRead) like fetchPdfAttachments; returns [] on non-200/error. Add `fetchMessages(config: InboxConfig): Promise` to InboxProvider (inbox-provider.interface.ts) and implement it in both providers WITHOUT touching fetchPdfAttachments (D-02). IMAP: add a `findBodyParts(node)` sibling to collectPdfParts that walks the MIME tree collecting the first `text/html` and first `text/plain` part IDs; reuse the connect/lock/search/fetchAll(envelope+bodyStructure) skeleton, then download the html/text part IDs and streamToBuffer(...).toString('utf8'); mark \Seen after processing each message. EWS: add one `` to a new getItemBodySoap (or extend getItemSoap usage for the messages path), read `BodyType` via extractAttr(block,'t:Body','BodyType') and the body via extractAll(block,'t:Body')[0]; map HTML→bodyHtml, Text→bodyText; reuse markReadSoap for idempotency. Create imap.provider.spec.ts and exchange-inbox.provider.spec.ts mirroring cosinex.adapter.spec.ts's mock style (stub ImapFlow / stub httpntlm.post) — these are the codebase's first inbox-provider tests. Keep all logging credential-free (T-07-03 convention): never log config.username/config.password. cd apps/api && npx vitest run src/inbox/imap.provider.spec.ts src/inbox/exchange-inbox.provider.spec.ts && npx tsc --noEmit -p tsconfig.json - Both new spec files exist and pass; each asserts an InboxMessage with a populated body from a mocked provider response. - `grep -n "fetchMessages" apps/api/src/inbox/inbox-provider.interface.ts apps/api/src/inbox/imap.provider.ts apps/api/src/inbox/exchange-inbox.provider.ts` shows the method on all three. - fetchPdfAttachments is unmodified (`git diff` shows no edits inside its body) and DKV suite still green. Both providers expose a tested fetchMessages returning subject + html/text body per unread message, idempotent via \Seen/IsRead, with fetchPdfAttachments untouched.

<threat_model>

Trust Boundaries

Boundary Description
Tessera API → IMAP/EWS mail server decrypted mailbox credentials + network I/O cross here

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-14-01-01 Information Disclosure fetchMessages logging high mitigate Reuse logger: false (imapflow) / generic-error (EWS) conventions; never log config.username/password (T-07-03)
T-14-01-02 Denial of Service IMAP body download medium mitigate Reuse the existing MAX_ATTACHMENT_BYTES/streamToBuffer size ceiling for body-part downloads
T-14-01-03 Tampering DKV regression via refactor high mitigate Pure move + import-path swap; fetchPdfAttachments body untouched; full DKV suite + tsc gate every task
T-14-01-SC Tampering npm/pip/cargo installs low accept No new packages — imapflow/httpntlm already installed (RESEARCH Package Legitimacy Audit)
</threat_model>
- `cd apps/api && npx vitest run && npx tsc --noEmit -p tsconfig.json` — full API suite green (DKV regression) + new inbox specs pass. - `grep -rn "dkv/providers" apps/api/src` — zero matches (move complete).

<success_criteria>

  • inbox/ module exists and is imported by DKV; DKV behavior unchanged (suite green).
  • fetchMessages tested on both providers; fetchPdfAttachments untouched.
  • No new external dependency added. </success_criteria>
Create `.planning/phases/14-rss-email-alert-ingestion-module-rollout/14-01-SUMMARY.md` when done.