Files
tessera-ctl/.planning/phases/07-dkv-fleet-module/07-02-PLAN.md
T
schalli de06794e67
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 38s
Tessera CI/CD / Tests (push) Waiting to run
docs(07): create phase 7 execution plans for DKV fleet module
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>
2026-06-26 18:56:59 +02:00

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
07-dkv-fleet-module 02 execute 1
07-01
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
true
DKV-01
truths artifacts key_links
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
path provides contains
apps/api/src/dkv/providers/inbox-provider.interface.ts InboxProvider interface contract fetchPdfAttachments
path provides min_lines
apps/api/src/dkv/providers/imap.provider.ts ImapProvider (imapflow implementation) 40
path provides min_lines
apps/api/src/dkv/providers/exchange-inbox.provider.ts ExchangeInboxProvider (ews-javascript-api implementation) 40
path provides contains
apps/api/src/dkv/dto/dkv-config.dto.ts DkvConfigDto with class-validator decorators IsEmail
from to via pattern
apps/api/src/dkv/providers/imap.provider.ts imapflow ImapFlow fetchAll then download (never download inside fetch iterator) fetchAll
from to via pattern
apps/api/src/dkv/providers/exchange-inbox.provider.ts ews-javascript-api WellKnownFolderName.Inbox + FindItems WellKnownFolderName.Inbox
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.

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>

@.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

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
Task 1: InboxProvider interface + config/vehicle/history DTOs 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 - 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 Create inbox-provider.interface.ts exporting the `InboxProvider` interface with `fetchPdfAttachments(config: InboxConfig): Promise` and `testConnection(config: InboxConfig): Promise`. 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). 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 - `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 Provider contract and all input DTOs exist and type-check. Task 2: ImapProvider (imapflow) apps/api/src/dkv/providers/imap.provider.ts - 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 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. 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 - `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 IMAP provider fetches sender-filtered PDF attachments without the documented deadlock and without logging credentials. Task 3: ExchangeInboxProvider (ews-javascript-api) apps/api/src/dkv/providers/exchange-inbox.provider.ts - 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 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). 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 - `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 Exchange provider reads PDF attachments from the Inbox folder using the email (not calendar) EWS item type.

<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>
- `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)

<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>
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.