de06794e67
6 plans covering full pipeline: PDF parsing foundation (Wave 0), inbox providers + export/SMTP services (Wave 1), pipeline orchestration + frontend pages + settings UI (Wave 2). Includes D-06 MailModule DB-config migration and Nyquist validation strategy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
13 KiB
Markdown
199 lines
13 KiB
Markdown
---
|
|
phase: 07-dkv-fleet-module
|
|
plan: 02
|
|
type: execute
|
|
wave: 1
|
|
depends_on: ["07-01"]
|
|
files_modified:
|
|
- apps/api/src/dkv/providers/inbox-provider.interface.ts
|
|
- apps/api/src/dkv/providers/imap.provider.ts
|
|
- apps/api/src/dkv/providers/exchange-inbox.provider.ts
|
|
- apps/api/src/dkv/dto/dkv-config.dto.ts
|
|
- apps/api/src/dkv/dto/dkv-vehicle.dto.ts
|
|
- apps/api/src/dkv/dto/dkv-history.dto.ts
|
|
autonomous: true
|
|
requirements: [DKV-01]
|
|
user_setup: []
|
|
|
|
must_haves:
|
|
truths:
|
|
- "An IMAP inbox can be polled for PDF attachments filtered by sender"
|
|
- "An Exchange inbox can be polled for PDF attachments via EWS"
|
|
- "Both providers implement the same InboxProvider contract so they are interchangeable"
|
|
- "Invalid module-config input is rejected by class-validator DTOs"
|
|
artifacts:
|
|
- path: "apps/api/src/dkv/providers/inbox-provider.interface.ts"
|
|
provides: "InboxProvider interface contract"
|
|
contains: "fetchPdfAttachments"
|
|
- path: "apps/api/src/dkv/providers/imap.provider.ts"
|
|
provides: "ImapProvider (imapflow implementation)"
|
|
min_lines: 40
|
|
- path: "apps/api/src/dkv/providers/exchange-inbox.provider.ts"
|
|
provides: "ExchangeInboxProvider (ews-javascript-api implementation)"
|
|
min_lines: 40
|
|
- path: "apps/api/src/dkv/dto/dkv-config.dto.ts"
|
|
provides: "DkvConfigDto with class-validator decorators"
|
|
contains: "IsEmail"
|
|
key_links:
|
|
- from: "apps/api/src/dkv/providers/imap.provider.ts"
|
|
to: "imapflow ImapFlow"
|
|
via: "fetchAll then download (never download inside fetch iterator)"
|
|
pattern: "fetchAll"
|
|
- from: "apps/api/src/dkv/providers/exchange-inbox.provider.ts"
|
|
to: "ews-javascript-api"
|
|
via: "WellKnownFolderName.Inbox + FindItems"
|
|
pattern: "WellKnownFolderName.Inbox"
|
|
---
|
|
|
|
<objective>
|
|
Build the interchangeable inbox-access layer: an `InboxProvider` interface and two implementations (IMAP via imapflow, Exchange via ews-javascript-api) that connect to a configured inbox, search for emails from the DKV sender, and download PDF attachments. Also define the class-validator DTOs that the controller (Plan 04) will use to validate module config and vehicle input.
|
|
|
|
Purpose: This is the first half of the DKV-01 "detect invoices from inbox" capability. Defining the provider contract first (interface-first ordering) lets Plan 04's orchestration code be written against a stable contract.
|
|
Output: Inbox interface + IMAP provider + Exchange provider + three DTOs.
|
|
</objective>
|
|
|
|
## Phase Goal
|
|
|
|
**Als** Administrator **möchte ich** DKV-Tankkarten-Rechnungen automatisch aus einem E-Mail-Postfach verarbeiten lassen, **damit** Flotten-Tankdaten ohne manuelle Eingabe als Excel-Datei exportiert und per SMTP zugestellt werden.
|
|
|
|
This plan delivers the inbox-reading capability — the entry point of the pipeline.
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
|
@$HOME/.claude/gsd-core/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/phases/07-dkv-fleet-module/07-CONTEXT.md
|
|
@.planning/phases/07-dkv-fleet-module/07-RESEARCH.md
|
|
@.planning/phases/07-dkv-fleet-module/07-PATTERNS.md
|
|
@apps/api/src/dkv/dkv.types.ts
|
|
@apps/api/src/calendar/providers/exchange.provider.ts
|
|
</context>
|
|
|
|
## Artifacts this phase produces (Plan 02 portion)
|
|
|
|
New symbols (exclude from drift verification):
|
|
- File `apps/api/src/dkv/providers/inbox-provider.interface.ts`: `InboxProvider` interface (`fetchPdfAttachments`, `testConnection`)
|
|
- Class `ImapProvider implements InboxProvider`
|
|
- Class `ExchangeInboxProvider implements InboxProvider`
|
|
- DTO classes: `DkvConfigDto`, `CreateVehicleDto`, `UpdateVehicleDto`, `DkvHistoryQueryDto`
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: InboxProvider interface + config/vehicle/history DTOs</name>
|
|
<files>apps/api/src/dkv/providers/inbox-provider.interface.ts, apps/api/src/dkv/dto/dkv-config.dto.ts, apps/api/src/dkv/dto/dkv-vehicle.dto.ts, apps/api/src/dkv/dto/dkv-history.dto.ts</files>
|
|
<read_first>
|
|
- apps/api/src/dkv/dkv.types.ts — InboxConfig, InboxEmail, InboxAttachment already defined in Plan 01; interface re-uses these
|
|
- apps/api/src/calendar/dto/create-calendar-source.dto.ts — class-validator decorator conventions (IsString, IsInt, IsIn, IsEmail, IsOptional, Min, Max)
|
|
- 07-PATTERNS.md "dkv-config.dto.ts" — exact validator set per field
|
|
- 07-RESEARCH.md Security Domain V5 — senderFilter must use IsEmail(); folder must be constrained
|
|
</read_first>
|
|
<action>
|
|
Create inbox-provider.interface.ts exporting the `InboxProvider` interface with `fetchPdfAttachments(config: InboxConfig): Promise<InboxEmail[]>` and `testConnection(config: InboxConfig): Promise<boolean>`. Import InboxConfig/InboxEmail/InboxAttachment from `../dkv.types` (do not redeclare them).
|
|
Create dkv-config.dto.ts exporting `DkvConfigDto` with these validators: protocol `@IsIn(['imap','exchange'])`; host optional string; port optional `@IsInt() @Min(1) @Max(65535)`; encryption `@IsIn(['none','starttls','ssl-tls'])`; folder optional string; senderFilter optional `@IsEmail()` (mitigates injection per V5); exportRecipient optional `@IsEmail()`; pollIntervalMin optional `@IsInt() @Min(5)` (UI-SPEC min=5); isActive optional `@IsBoolean()`; vehicleFormatString optional string; username optional string; password optional string.
|
|
Create dkv-vehicle.dto.ts exporting `CreateVehicleDto` (kennzeichen, marke, modell, fahrer — all `@IsString() @IsNotEmpty()` except validate kennzeichen non-empty) and `UpdateVehicleDto` (all four optional).
|
|
Create dkv-history.dto.ts exporting `DkvHistoryQueryDto` with `page` and `limit` optional `@IsInt() @Min(1)` (pagination mitigates the history-accumulation DoS, Research Security Domain).
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "IsEmail" apps/api/src/dkv/dto/dkv-config.dto.ts && grep -q "InboxProvider" apps/api/src/dkv/providers/inbox-provider.interface.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `apps/api/src/dkv/providers/inbox-provider.interface.ts` exports `InboxProvider` with both `fetchPdfAttachments` and `testConnection`
|
|
- `DkvConfigDto` decorates `senderFilter` and `exportRecipient` with `@IsEmail()`
|
|
- `DkvConfigDto` decorates `pollIntervalMin` with `@Min(5)`
|
|
- `DkvHistoryQueryDto` exposes `page` and `limit` with `@IsInt()`
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>Provider contract and all input DTOs exist and type-check.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: ImapProvider (imapflow)</name>
|
|
<files>apps/api/src/dkv/providers/imap.provider.ts</files>
|
|
<read_first>
|
|
- 07-RESEARCH.md "Pattern 2: imapflow IMAP Provider" — connect, search by sender, fetchAll then download, streamToBuffer
|
|
- 07-RESEARCH.md "Pitfall 1" — NEVER call download() inside a fetch() async iterator (deadlock); always fetchAll() first, then loop and download()
|
|
- 07-PATTERNS.md "imap.provider.ts" — class shell, `logger: false` to suppress credential logging
|
|
- apps/api/src/dkv/providers/inbox-provider.interface.ts — contract to implement
|
|
- apps/api/src/dkv/dkv.types.ts — InboxEmail / InboxAttachment shapes to return
|
|
</read_first>
|
|
<action>
|
|
Implement `@Injectable() ImapProvider implements InboxProvider` with `new Logger(ImapProvider.name)`. Construct `ImapFlow` with `host`, `port`, `secure: encryption === 'ssl-tls'`, `requireTLS: encryption === 'starttls'`, `auth` only when username present, and `logger: false` (T-07 credential safety). In `fetchPdfAttachments`: connect, acquire mailbox lock on `config.folder`, `client.search({ from: config.senderFilter }, { uid: true })`, then `client.fetchAll(uids, { envelope: true, bodyStructure: true }, { uid: true })`, then loop messages and call `client.download(...)` separately (never inside the fetch iterator). Collect only parts whose content type is application/pdf. Enforce a max attachment size guard (skip/raise on attachments larger than a sane limit, e.g. 25 MB — mitigates the PDF-bomb DoS) before buffering. Always release the lock and `logout()` in finally. `testConnection`: connect + logout, return boolean. Wrap errors in generic log messages (T-05-13) and never include credentials.
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "fetchAll" apps/api/src/dkv/providers/imap.provider.ts && grep -q "logger: false" apps/api/src/dkv/providers/imap.provider.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `ImapProvider` implements `InboxProvider` (type-check enforces the contract)
|
|
- Source contains `fetchAll(` and does NOT call `download(` inside a `for await` / `client.fetch(` iterator
|
|
- `ImapFlow` is constructed with `logger: false`
|
|
- An attachment size guard (numeric byte limit) is present before buffering
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>IMAP provider fetches sender-filtered PDF attachments without the documented deadlock and without logging credentials.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: ExchangeInboxProvider (ews-javascript-api)</name>
|
|
<files>apps/api/src/dkv/providers/exchange-inbox.provider.ts</files>
|
|
<read_first>
|
|
- apps/api/src/calendar/providers/exchange.provider.ts — full EWS pattern: dynamic `await import('ews-javascript-api')`, ExchangeService, WebCredentials, error handling lines 42-48
|
|
- 07-RESEARCH.md "Pitfall 6" — use `WellKnownFolderName.Inbox` + `FindItems` + `EmailMessage.Bind`, NOT FindAppointments/CalendarView
|
|
- 07-PATTERNS.md "exchange-inbox.provider.ts" — class shell mirroring ExchangeProvider
|
|
- apps/api/src/dkv/providers/inbox-provider.interface.ts — contract to implement
|
|
</read_first>
|
|
<action>
|
|
Implement `@Injectable() ExchangeInboxProvider implements InboxProvider` mirroring the existing ExchangeProvider structure. Use the dynamic import `const ews: any = await import('ews-javascript-api')`, construct `ExchangeService`, set `Url` from config.host and `Credentials = new ews.WebCredentials(username, password)`. Differ from the calendar provider per Pitfall 6: bind to `ews.WellKnownFolderName.Inbox`, use `service.FindItems(...)` with an item view, filter by sender (config.senderFilter), then `EmailMessage.Bind` to load each message and read its attachments; download FileAttachment content for application/pdf parts into a Buffer. Apply the same max attachment size guard as the IMAP provider. `testConnection`: attempt a minimal FindItems on the Inbox, return boolean. On error, log a generic message only (T-05-13), never credentials, and return `[]` for fetch (consistent with calendar exchange.provider error path).
|
|
</action>
|
|
<verify>
|
|
<automated>pnpm --filter @tessera/api type-check && grep -q "WellKnownFolderName.Inbox" apps/api/src/dkv/providers/exchange-inbox.provider.ts && grep -q "import('ews-javascript-api')" apps/api/src/dkv/providers/exchange-inbox.provider.ts</automated>
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- `ExchangeInboxProvider` implements `InboxProvider`
|
|
- Source references `WellKnownFolderName.Inbox` and `FindItems` (not `FindAppointments`)
|
|
- Uses dynamic `import('ews-javascript-api')` (matches calendar provider lazy-load convention)
|
|
- Error path logs generic message and does not include credential values
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
</acceptance_criteria>
|
|
<done>Exchange provider reads PDF attachments from the Inbox folder using the email (not calendar) EWS item type.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| external mail server → API | Untrusted IMAP/EWS responses and attachments cross into the server |
|
|
| admin form input → DTO | User-supplied config (host, sender, credentials) enters via DTO |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-07-03 | Information Disclosure | imap.provider.ts / exchange-inbox.provider.ts | mitigate | `logger: false` on ImapFlow; generic error logging (T-05-13); credentials never serialized into log strings |
|
|
| T-07-04 | Tampering | dkv-config.dto.ts | mitigate | senderFilter validated with `@IsEmail()`, port with `@Min(1)@Max(65535)`, encryption with `@IsIn(...)` — prevents injection/malformed config |
|
|
| T-07-05 | Denial of Service | imap.provider.ts / exchange-inbox.provider.ts | mitigate | Max attachment byte-size guard before buffering/parsing (PDF-bomb defense) |
|
|
| T-07-06 | Denial of Service | dkv-history.dto.ts | mitigate | Pagination params (page/limit) on history query |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
- `pnpm --filter @tessera/api type-check` exits 0
|
|
- Both providers structurally implement the InboxProvider contract (type-check enforced)
|
|
- imapflow deadlock pattern avoided (fetchAll before download)
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- InboxProvider contract defined and implemented by both IMAP and Exchange providers
|
|
- DTOs validate all module-config and vehicle inputs with the security constraints from Research V5
|
|
- DKV-01 inbox-access layer ready for orchestration in Plan 04
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/07-dkv-fleet-module/07-02-SUMMARY.md` when done. Record the chosen max attachment size limit and any EWS API adjustments made versus the calendar ExchangeProvider.
|
|
</output>
|