Skip to content

feat(inbound): PostmarkInboundParser + InboundEmailController + DI wiring#24

Open
mpge wants to merge 1 commit intofeat/inbound-email-webhookfrom
feat/inbound-email-controller
Open

feat(inbound): PostmarkInboundParser + InboundEmailController + DI wiring#24
mpge wants to merge 1 commit intofeat/inbound-email-webhookfrom
feat/inbound-email-controller

Conversation

@mpge
Copy link
Copy Markdown
Member

@mpge mpge commented Apr 24, 2026

Summary

Closes the core inbound-webhook loop for .NET end-to-end, stacked on #23 (router foundation).

  • PostmarkInboundParser — normalizes Postmark's JSON webhook payload (FromFull / ToFull / Headers / Attachments) into InboundMessage. Extracts In-Reply-To / References / Message-ID from the Headers array. Decodes base64 attachment content inline.

  • InboundEmailControllerPOST /support/webhook/email/inbound

    • Dispatches to the matching IInboundEmailParser by ?adapter=... query param or X-Escalated-Adapter header
    • Signature-guarded by X-Escalated-Inbound-Secret (constant-time compare via CryptographicOperations.FixedTimeEquals). Uses the same EmailOptions.InboundSecret key that signs Reply-To — symmetric
    • Writes an InboundEmail audit row per request with the resolution outcome (pendingmatched | unmatched | failed)
    • Returns 202 Accepted with { inboundId, status, ticketId }
  • AddEscalated() wires InboundEmailRouter + PostmarkInboundParser into DI. Additional providers (Mailgun, SES) register by adding their own IInboundEmailParser implementation.

Dependencies

Test plan

  • 5 parser tests exercise field extraction, threading header parsing, base64 attachment decoding, the minimal-payload path, and the adapter name contract
  • Mailgun + SES parsers, attachment persistence, and reply/ticket-create orchestration land in follow-up PRs
  • CI green (won't trigger against stacked base until rebased)

…ring

Closes the core inbound webhook for .NET end-to-end:

- PostmarkInboundParser: normalizes Postmark's JSON webhook payload
  (FromFull / ToFull / Headers / Attachments fields) into
  InboundMessage. Extracts In-Reply-To / References / Message-ID from
  the Headers array. Decodes base64 attachment content inline.

- InboundEmailController: POST /support/webhook/email/inbound
  - Dispatches to the matching IInboundEmailParser by ?adapter=…
    query param or X-Escalated-Adapter header.
  - Signature-guarded by X-Escalated-Inbound-Secret (constant-time
    compare via CryptographicOperations.FixedTimeEquals). Same
    EmailOptions.InboundSecret key that signs Reply-To — symmetric.
  - Writes an InboundEmail audit row per request with the resolution
    outcome (pending → matched | unmatched | failed).
  - Returns 202 Accepted with { inboundId, status, ticketId }.

- AddEscalated() wires InboundEmailRouter + PostmarkInboundParser
  into DI. Additional providers (Mailgun, SES) can register by
  adding their own IInboundEmailParser implementation.

5 parser tests exercise field extraction, threading header parsing,
base64 attachment decoding, the minimal-payload path, and the
adapter name contract.

Follow-up PRs: Mailgun + SES parsers, attachment persistence, and
reply/ticket-create orchestration based on router outcome.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant