--- 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" --- 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. @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.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. ## 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 | - `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) - 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 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.