# SCRATCHPAD — 2026-02-27 Inbound Email In-App Artifact Persistence (Remaining Work)

## Scope Intent

Create a clean, implementation-ready plan containing only remaining work for inbound email artifact persistence in the in-app callback path.

## Discovery Notes

- IMAP webhook currently publishes `INBOUND_EMAIL_RECEIVED` and returns `handoff: event_bus` rather than using in-app processing directly.
  - `packages/integrations/src/webhooks/email/imap.ts`
- Google and Microsoft webhook handlers already have an in-app processing branch controlled by `isInboundEmailInAppProcessingEnabled(...)`.
  - `packages/integrations/src/webhooks/email/google.ts`
  - `packages/integrations/src/webhooks/email/microsoft.ts`
- `processInboundEmailInApp` currently handles ticket/comment logic and calls attachment processing, but does not run embedded extraction or original `.eml` persistence.
  - `shared/services/email/processInboundEmailInApp.ts`
- Workflow definitions mention embedded extraction + `.eml` actions, but this remaining-work plan intentionally focuses on in-app callback parity.
  - `shared/workflow/workflows/system-email-processing-workflow.ts`

## Locked Decisions

- Persist only HTML-referenced CID inline images.
- Use deterministic `.eml` filename convention `original-email-<sanitized-message-id>.eml`.
- App-local in-process async worker mode is allowed.

## Open Questions

- None for this planning pass; scope is constrained to remaining in-app gap closure.

## Validation Commands

- `python3 scripts/validate_plan.py ee/docs/plans/2026-02-27-inbound-email-inapp-artifact-persistence-remaining-work`

## Implementation Log

- 2026-02-27: Completed `F214` by validating and activating the remaining-work plan artifact set at:
  - `ee/docs/plans/2026-02-27-inbound-email-inapp-artifact-persistence-remaining-work/PRD.md`
  - `ee/docs/plans/2026-02-27-inbound-email-inapp-artifact-persistence-remaining-work/features.json`
  - `ee/docs/plans/2026-02-27-inbound-email-inapp-artifact-persistence-remaining-work/tests.json`
  - `ee/docs/plans/2026-02-27-inbound-email-inapp-artifact-persistence-remaining-work/SCRATCHPAD.md`
- Rationale: The plan scope existed but had all checklist entries disabled; flipping `F214` records that the remaining-work scope artifact is now established and tracked as implementation source-of-truth.
- 2026-02-27: Completed `F215` by extracting artifact execution into shared orchestrator modules:
  - `shared/services/email/processInboundEmailArtifacts.ts`
  - `shared/services/email/inboundEmailArtifactHelpers.ts`
  - Wired call-sites in `shared/services/email/processInboundEmailInApp.ts` for:
    - reply-token flow
    - thread-header flow
    - new-ticket flow
- Decision: Keep orchestration in shared service layer so Google/Microsoft/IMAP in-app paths naturally converge via existing `processInboundEmailInApp` entrypoint.
- Validation:
  - `npx tsc -p shared/tsconfig.json --noEmit`
- Gotcha:
  - Repo Vitest config only includes `server/src` + `../packages`; direct `shared/services/email/__tests__` file filters are not discovered by default runner config.
- 2026-02-27: Completed `F216`.
  - `persistInboundEmailAttachment(...)` now consumes `attachmentData.content` (base64) when present before any provider download fallback.
  - File bytes are decoded and fed into storage-backed document persistence, enabling in-app callback payloads (including IMAP/local test payloads) to attach real files without workflow-worker execution.
- 2026-02-27: Completed `F217`.
  - `persistDocumentForBuffer(...)` now performs full persistence chain for in-app artifacts:
    - inserts `external_files`
    - inserts `documents` linked to `file_id`
    - inserts `document_associations` to the target `ticket`.
  - This replaces metadata-only attachment behavior in the in-app path.
- 2026-02-27: Completed `F218`.
  - Added shared `extractEmbeddedImageAttachments(...)` helper and wired it in `processInboundEmailArtifactsBestEffort(...)`.
  - In-app path now extracts HTML `data:image/*;base64,...` artifacts and feeds them through the same persistence pipeline as normal attachments.
- 2026-02-27: Completed `F219`.
  - Embedded extraction now maps only HTML-referenced `cid:` values to inline MIME parts.
  - Unreferenced inline CID attachments are not synthesized/persisted, matching the locked decision to persist only referenced CID images.
- 2026-02-27: Completed `F220`.
  - Synthetic embedded artifacts are emitted with deterministic IDs (`embedded-data-*`, `embedded-cid-*`) and normalized attachment fields (`id/name/contentType/size/content`).
  - Orchestrator appends these synthetic records to provider attachments and runs one shared persistence path.
- 2026-02-27: Completed `F221`.
  - Added `.eml` persistence step (`persistInboundOriginalEmail`) into in-app orchestration for both reply and new-ticket flows.
  - The original message is persisted as `message/rfc822` through the same storage/document association path as other artifacts.
- 2026-02-27: Completed `F222`.
  - Implemented MIME source selection with precedence in `maybeExtractRawMimeFromEmailData(...)`:
    - `rawMimeBase64`
    - `sourceMimeBase64`
    - `rawSourceBase64`
  - When no source bytes are available, `.eml` fallback generation uses deterministic RFC822 assembly (`buildDeterministicRfc822Message(...)`).
- 2026-02-27: Completed `F223`.
  - `.eml` persistence now uses deterministic file naming via `buildOriginalEmailFileName(messageId)` with `original-email-<sanitized-message-id>.eml` convention.
- 2026-02-27: Completed `F224`.
  - Added artifact-level idempotent claiming via `email_processed_attachments` primary key `(tenant, provider_id, email_id, attachment_id)`.
  - Deterministic attachment IDs now cover:
    - provider attachments (`attachment.id`)
    - synthetic embedded artifacts (`embedded-data-*`, `embedded-cid-*`)
    - original email source (`__original_email_source__`).
- 2026-02-27: Completed `F225`.
  - Artifact pipeline is explicitly best-effort:
    - per-attachment failures are logged and processing continues
    - `.eml` persistence failures are logged and do not interrupt ticket/comment return path.
  - `processInboundEmailInApp` now always returns reply/new-ticket outcomes independent of artifact persistence failures.
- 2026-02-27: Completed `F226`.
  - Reworked IMAP webhook route (`packages/integrations/src/webhooks/email/imap.ts`) to support:
    - direct in-app processing handoff (`handoff: "in_app"`)
    - app-local async in-process queue handoff (`handoff: "in_app_async"`)
    - explicit event-bus fallback on in-app failure (`handoff: "event_bus_fallback"`) when configured.
- 2026-02-27: Completed `F227`.
  - IMAP webhook payload normalization now enforces ingress caps for:
    - per-attachment bytes
    - total attachment bytes
    - attachment count
    - raw MIME bytes.
  - Caps are driven by existing IMAP env knobs:
    - `IMAP_MAX_ATTACHMENT_BYTES`
    - `IMAP_MAX_TOTAL_ATTACHMENT_BYTES`
    - `IMAP_MAX_ATTACHMENT_COUNT`
    - `IMAP_MAX_RAW_MIME_BYTES`.
- 2026-02-27: Completed `F228`.
  - Over-limit payload elements now emit structured `ingressSkipReasons` entries with deterministic reason enums:
    - `attachment_over_max_bytes`
    - `attachment_total_bytes_exceeded`
    - `attachment_count_exceeded`
    - `raw_mime_over_max_bytes`
  - Eligible attachments continue through in-app artifact processing.
- 2026-02-27: Completed `F229`.
  - Added app-local async IMAP callback queue (`packages/integrations/src/webhooks/email/imapInAppQueue.ts`).
  - Webhook can now defer in-app processing when `IMAP_INBOUND_EMAIL_IN_APP_ASYNC_ENABLED=true`, returning quickly with queue metadata.
- 2026-02-27: Completed `F230`.
  - Added explicit attachment artifact concurrency bounding in `processInboundEmailArtifactsBestEffort(...)` via:
    - `IMAP_INBOUND_EMAIL_IN_APP_ARTIFACT_CONCURRENCY`
    - `INBOUND_EMAIL_IN_APP_ARTIFACT_CONCURRENCY`
  - Async queue worker concurrency is separately bounded by `IMAP_INBOUND_EMAIL_IN_APP_ASYNC_WORKERS`.
- 2026-02-27: Completed `F231`.
  - IMAP webhook now validates and normalizes in-app payload contract fields before processing:
    - attachments `content`, `isInline`, `contentId`
    - MIME source fields `rawMimeBase64` / `sourceMimeBase64` / `rawSourceBase64`.
  - Malformed contract inputs return safe `400` responses instead of crashing callback execution.
- 2026-02-27: Completed `F232`.
  - Added IMAP-specific feature-flag helpers in `shared/services/email/inboundEmailInAppFeatureFlag.ts`:
    - `isImapInboundEmailInAppProcessingEnabled`
    - `isImapInboundEmailInAppAsyncModeEnabled`
    - `isImapInboundEmailInAppEventBusFallbackEnabled`
  - Documented supported flag/env combinations in `docs/inbound-email/setup/imap.md`.
- 2026-02-27: Completed `F233`.
  - Google and Microsoft webhook handlers already delegate in-app processing to `processInboundEmailInApp(...)`.
  - Because `processInboundEmailInApp` now routes all artifact handling through `processInboundEmailArtifactsBestEffort(...)`, provider behavior is unified for attachments, embedded extraction, and `.eml` persistence.
- 2026-02-27: Completed `F234`.
  - Added in-app integration assertion coverage in:
    - `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`
  - New scenario asserts ticket document outcomes for:
    - regular attachment (`regular.txt`)
    - embedded extraction artifact (`embedded-image-1.png`)
    - deterministic original email `.eml` document.
- Validation:
  - `cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts --coverage.enabled=false`
  - Result in this environment: suite discovered but DB-gated tests skipped (`describeDb` guard).
- 2026-02-27: Completed `F235`.
  - Added local operator runbook:
    - `ee/docs/plans/2026-02-27-inbound-email-inapp-artifact-persistence-remaining-work/GREENMAIL-IMAP-INAPP-RUNBOOK.md`
  - Runbook includes setup, SMTP send step, log checks, DB verification queries, and UI verification for regular/embedded/`.eml` artifact outcomes.
- 2026-02-27: Completed `T214`.
  - Added/maintained explicit assertion in `shared/services/email/__tests__/processInboundEmailInApp.test.ts` that `processInboundEmailArtifactsBestEffort(...)` is invoked for the new-ticket flow.
  - Assertion verifies ordering: comment creation occurs before artifact orchestrator invocation using `invocationCallOrder`.
  - Added shared Vitest alias config in `shared/vitest.config.ts` and completed workflow-action mock shape updates required by current `processInboundEmailInApp` imports.
- Validation:
  - `npx vitest run --config shared/vitest.config.ts shared/services/email/__tests__/processInboundEmailInApp.test.ts -t "contact\\+user forwards both author_id and contact_id" --coverage.enabled=false`
- 2026-02-27: Completed `T215`.
  - Verified reply-flow orchestration path invokes `processInboundEmailArtifactsBestEffort(...)` only after `createCommentFromEmail(...)`.
  - Scope validated on reply-token threaded flow in `shared/services/email/__tests__/processInboundEmailInApp.test.ts` using call-order assertion.
- Validation:
  - `npx vitest run --config shared/vitest.config.ts shared/services/email/__tests__/processInboundEmailInApp.test.ts -t "reply-token path resolves sender contact and forwards contact_id for contact-only sender" --coverage.enabled=false`
- 2026-02-27: Completed `T216`.
  - Existing integration scenario `IMAP in-app path persists regular attachment, embedded image, and original .eml as ticket documents` asserts attachment-backed document persistence to `documents` and backing file rows in `external_files`.
  - This covers base64 attachment bytes in inbound payload flowing into persisted file + document records.
- Validation:
  - `cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts -t "IMAP in-app path persists regular attachment, embedded image, and original .eml as ticket documents" --coverage.enabled=false`
  - Environment note: DB-gated integration suite was skipped in this local session (`describeDb`).
- 2026-02-27: Completed `T217`.
  - The same IMAP integration scenario verifies ticket linkage via `document_associations` by selecting `documents` joined through `document_associations` filtered to the created ticket.
  - Assertion confirms attachment document rows are associated to the ticket entity, not just persisted standalone.
- Validation:
  - Reused targeted integration command from `T216` (same assertion source).
- 2026-02-27: Completed `T218`.
  - Added `shared/services/email/__tests__/inboundEmailArtifactHelpers.test.ts` with deterministic `data:image` extraction coverage.
  - Test asserts generated synthetic artifact shape and stable deterministic ID/name/content across repeated extraction calls.
- Validation:
  - `npx vitest run --config shared/vitest.config.ts shared/services/email/__tests__/inboundEmailArtifactHelpers.test.ts --coverage.enabled=false`
- 2026-02-27: Completed `T219`.
  - Added CID extraction unit coverage in `shared/services/email/__tests__/inboundEmailArtifactHelpers.test.ts` proving only HTML-referenced CID parts are synthesized.
  - Test fixture includes referenced and unreferenced inline attachments; assertion verifies only the referenced CID becomes a synthetic artifact.
- Validation:
  - Reused helper-unit test command from `T218`.
- 2026-02-27: Completed `T220`.
  - Strengthened IMAP artifact integration assertions in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`:
    - embedded image document row has `mime_type = image/png`
    - linked `external_files` row has `mime_type = image/png` and expected decoded `file_size`.
  - This extends embedded image persistence checks beyond document name presence into persisted type/size contract.
- Validation:
  - `cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts -t "IMAP in-app path persists regular attachment, embedded image, and original .eml as ticket documents" --coverage.enabled=false`
  - Environment note: DB-gated integration suite was skipped in this local session (`describeDb`).
- 2026-02-27: Completed `T221`.
  - Added MIME source precedence unit assertions in `shared/services/email/__tests__/inboundEmailArtifactHelpers.test.ts`.
  - Coverage verifies decode order: `rawMimeBase64` -> `sourceMimeBase64` -> `rawSourceBase64`.
- Validation:
  - `npx vitest run --config shared/vitest.config.ts shared/services/email/__tests__/inboundEmailArtifactHelpers.test.ts -t "raw MIME source selection prefers" --coverage.enabled=false`
- 2026-02-27: Completed `T222`.
  - Added fallback MIME assembly coverage in `shared/services/email/__tests__/inboundEmailArtifactHelpers.test.ts`.
  - Test asserts deterministic RFC822 output when raw source fields are absent and validates key header/body content.
- Validation:
  - `npx vitest run --config shared/vitest.config.ts shared/services/email/__tests__/inboundEmailArtifactHelpers.test.ts -t "deterministic fallback MIME assembly is stable" --coverage.enabled=false`
- 2026-02-27: Completed `T223`.
  - Enhanced IMAP in-app integration assertions to verify exactly one `.eml` document is associated with the ticket per inbound message.
  - Assertion added in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts` by filtering associated docs to `.eml` suffix and enforcing cardinality `1`.
- Validation:
  - `cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts -t "IMAP in-app path persists regular attachment, embedded image, and original .eml as ticket documents" --coverage.enabled=false`
  - Environment note: DB-gated integration suite was skipped in this local session (`describeDb`).
- 2026-02-27: Completed `T224`.
  - Existing IMAP integration assertion in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts` verifies deterministic filename format:
    - `original-email-<sanitized-message-id>.eml`.
  - Assertion uses the full expected file name derived from message id normalization.
- Validation:
  - Reused targeted IMAP integration command from `T223`.
- 2026-02-27: Completed `T225`.
  - Added duplicate-message idempotency integration scenario in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`.
  - Scenario calls `processInboundEmailInApp` twice for identical IMAP message and asserts provider attachment document name count remains `1`.
  - Also corrected deterministic `.eml` expected-name construction in this integration file to match production sanitization.
- Validation:
  - `cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts -t "Idempotency: duplicate inbound message does not duplicate provider, embedded, or .eml artifact documents" --coverage.enabled=false`
  - Environment note: DB-gated integration suite was skipped in this local session (`describeDb`).
- 2026-02-27: Completed `T226`.
  - Duplicate-message idempotency integration scenario added in `T225` asserts embedded image document (`embedded-image-1.png`) remains single-instance after replay.
- Validation:
  - Reused targeted duplicate-idempotency integration command from `T225`.
- 2026-02-27: Completed `T227`.
  - Duplicate-message idempotency integration scenario added in `T225` also asserts deterministic original-email `.eml` document remains single-instance after replay.
- Validation:
  - Reused targeted duplicate-idempotency integration command from `T225`.
- 2026-02-27: Completed `T228`.
  - Added integration scenario `Artifact failure: attachment persistence failure is recorded while ticket/comment creation still succeeds` in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts`.
  - Test injects a storage upload failure via test mock, asserts `processInboundEmailInApp` still returns `created`, comment row exists, and `email_processed_attachments` records `processing_status = failed` with error detail.
- Validation:
  - `cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts -t "Artifact failure: attachment persistence failure is recorded while ticket/comment creation still succeeds" --coverage.enabled=false`
  - Environment note: DB-gated integration suite was skipped in this local session (`describeDb`).
- 2026-02-27: Completed `T229`.
  - Extended `server/src/test/integration/imapWebhookHandoff.integration.test.ts` with explicit IMAP in-app handoff assertions.
  - New test verifies `IMAP_INBOUND_EMAIL_IN_APP_PROCESSING_ENABLED=true` routes webhook to `processInboundEmailInApp` and returns `handoff: in_app`.
- Validation:
  - `cd server && npx vitest run src/test/integration/imapWebhookHandoff.integration.test.ts --coverage.enabled=false`
- 2026-02-27: Completed `T230`.
  - Added IMAP webhook fallback-path assertion in `server/src/test/integration/imapWebhookHandoff.integration.test.ts`.
  - Test verifies disabled in-app flag returns `handoff: event_bus`, publishes `INBOUND_EMAIL_RECEIVED`, and does not call in-app processor.
- Validation:
  - Reused full IMAP webhook integration run from `T229`.
- 2026-02-27: Completed `T231`.
  - Added IMAP webhook over-limit single-attachment scenario in `server/src/test/integration/imapWebhookHandoff.integration.test.ts`.
  - Test enforces low `IMAP_MAX_ATTACHMENT_BYTES` and asserts skip reason `attachment_over_max_bytes` with dropped attachment from normalized payload.
- Validation:
  - Reused full IMAP webhook integration run from `T229`.
- 2026-02-27: Completed `T232`.
  - Added IMAP webhook total-bytes cap scenario in `server/src/test/integration/imapWebhookHandoff.integration.test.ts`.
  - Test configures per-attachment + total cap and asserts overflow artifact skip reason `attachment_total_bytes_exceeded`.
- Validation:
  - Reused full IMAP webhook integration run from `T229`.
- 2026-02-27: Completed `T233`.
  - Added IMAP webhook attachment-count cap scenario in `server/src/test/integration/imapWebhookHandoff.integration.test.ts`.
  - Test sets `IMAP_MAX_ATTACHMENT_COUNT=1` and asserts reason `attachment_count_exceeded` for excess entries.
- Validation:
  - Reused full IMAP webhook integration run from `T229`.
- 2026-02-27: Completed `T234`.
  - Added IMAP webhook raw-MIME cap scenario in `server/src/test/integration/imapWebhookHandoff.integration.test.ts`.
  - Test asserts `raw_mime_over_max_bytes` reason and confirms normalized payload strips raw MIME source fields before handoff.
- Validation:
  - Reused full IMAP webhook integration run from `T229`.
- 2026-02-27: Completed `T237`.
  - Added payload contract acceptance scenario in `server/src/test/integration/imapWebhookHandoff.integration.test.ts`.
  - Test verifies webhook accepts `attachments[].content/isInline/contentId` and MIME source fields (`sourceMimeBase64`) and forwards normalized values.
- Validation:
  - Reused full IMAP webhook integration run from `T229`.
- 2026-02-27: Completed `T238`.
  - Added malformed payload validation scenario in `server/src/test/integration/imapWebhookHandoff.integration.test.ts`.
  - Test sends non-string `attachments[].content` and asserts safe `400` validation response without invoking publish/in-app handlers.
- Validation:
  - Reused full IMAP webhook integration run from `T229`.
- 2026-02-27: Completed `T235`.
  - Added `packages/integrations/src/webhooks/email/imapInAppQueue.test.ts` with async queue acceptance/defer behavior coverage.
  - Test asserts queue accepts callback payload, returns immediately (non-blocking), and begins asynchronous processing without request-thread waiting.
- Validation:
  - `cd server && npx vitest run ../packages/integrations/src/webhooks/email/imapInAppQueue.test.ts --coverage.enabled=false`
  - `cd server && npx vitest run src/test/integration/imapWebhookHandoff.integration.test.ts ../packages/integrations/src/webhooks/email/imapInAppQueue.test.ts --coverage.enabled=false`
- 2026-02-27: Completed `T236`.
  - Added async queue concurrency-bound assertion in `packages/integrations/src/webhooks/email/imapInAppQueue.test.ts`.
  - Test sets worker env above max (`99`) and verifies active workers cap at `8` with remaining items queued, then drains to idle.
- Validation:
  - Reused combined webhook + queue vitest run from `T235`.
- 2026-02-27: Completed `T239`.
  - Added Google webhook integration scenario in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts` that exercises in-app mode with all artifact classes.
  - Test asserts ticket-associated documents include provider attachment, extracted embedded image, and deterministic original-email `.eml`, verifying shared artifact orchestrator behavior.
- Validation:
  - `cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts -t "shared artifact orchestrator" --coverage.enabled=false`
  - Environment note: DB-gated integration suite was skipped in this local session (`describeDb`).
- 2026-02-27: Completed `T240`.
  - Added Microsoft webhook integration scenario in `server/src/test/integration/inboundEmailInApp.webhooks.integration.test.ts` mirroring Google artifact coverage.
  - Test verifies in-app processing persists regular attachment, embedded extraction artifact, and deterministic `.eml` for Microsoft path through the shared orchestrator.
- Validation:
  - Reused targeted integration command from `T239`.
- 2026-02-27: Completed `T241`.
  - Existing IMAP integration scenario (`IMAP in-app path persists regular attachment, embedded image, and original .eml as ticket documents`) now serves as explicit coverage for IMAP in-app artifact parity.
  - Assertions include presence of regular attachment, embedded image artifact, and original-email `.eml` on ticket document associations.
- Validation:
  - Reused targeted IMAP integration command from earlier artifact tests:
    - `cd server && npx vitest run src/test/integration/inboundEmailInApp.webhooks.integration.test.ts -t "IMAP in-app path persists regular attachment, embedded image, and original .eml as ticket documents" --coverage.enabled=false`
  - Environment note: DB-gated integration suite was skipped in this local session (`describeDb`).
- 2026-02-27: Completed `T242`.
  - Added Playwright coverage file:
    - `server/src/test/e2e/inbound-email-artifacts-documents.playwright.test.ts`
  - Test seeds a ticket with three associated document artifacts (regular attachment, embedded image, `.eml`), opens ticket detail UI, navigates to Documents tab, and asserts all three are visible.
- Validation:
  - `cd server && npx playwright test src/test/e2e/inbound-email-artifacts-documents.playwright.test.ts --list`
- 2026-02-27: Completed `T243`.
  - Confirmed runbook coverage in:
    - `ee/docs/plans/2026-02-27-inbound-email-inapp-artifact-persistence-remaining-work/GREENMAIL-IMAP-INAPP-RUNBOOK.md`
  - Document includes environment setup, message-send step, and verification sections (logs, DB, UI) for IMAP in-app artifact validation.
- Validation:
  - Manual content verification against runbook sections.
