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 |
|
true |
|
|
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, newInboxMessage),inbox-provider.interface.ts(addsfetchMessages),imap.provider.ts,exchange-inbox.provider.ts(both gainfetchMessages). - 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.tsre-exports the inbox types from../inbox/inbox.typesso 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> |
<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>