Skip to content

feat(inbound): PostmarkInboundParser + InboundEmailController#27

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

feat(inbound): PostmarkInboundParser + InboundEmailController#27
mpge wants to merge 1 commit intofeat/inbound-email-routerfrom
feat/inbound-email-controller

Conversation

@mpge
Copy link
Copy Markdown
Member

@mpge mpge commented Apr 24, 2026

Summary

Closes the core inbound webhook for Spring end-to-end (stacked on #26's router foundation). Mirrors the .NET counterpart (escalated-dotnet #24).

  • PostmarkInboundParser (@Component) — 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 /escalated/webhook/email/inbound

    • Dispatches to the matching InboundEmailParser by ?adapter=... query param or X-Escalated-Adapter header
    • Signature-guarded by X-Escalated-Inbound-Secret (constant-time compare via MessageDigest.isEqual). Uses the same escalated.email.inbound-secret key that signs Reply-To — symmetric
    • Returns 202 Accepted with { status, ticketId }, 401 on secret mismatch, 400 on unknown adapter / invalid payload

Additional providers (Mailgun, SES) register by implementing InboundEmailParser as @Components — the controller picks them up via DI.

Dependencies

Test plan

  • 5 parser tests exercise field extraction, threading header parsing, base64 attachment decoding, the minimal-payload path, and the adapter name contract
  • CI green (won't trigger against stacked base until rebased)

Closes the core inbound webhook for Spring end-to-end (stacked on
#26's router foundation). Mirrors the .NET counterpart (#24).

- PostmarkInboundParser (@component): 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.

- InboundEmailController: POST /escalated/webhook/email/inbound
  - Dispatches to the matching InboundEmailParser by ?adapter=...
    query param or X-Escalated-Adapter header.
  - Signature-guarded by X-Escalated-Inbound-Secret (constant-time
    compare via MessageDigest.isEqual). Same
    escalated.email.inbound-secret key that signs Reply-To —
    symmetric.
  - Returns 202 Accepted with { status, ticketId }, 401 on secret
    mismatch, 400 on unknown adapter / invalid payload.

Additional providers (Mailgun, SES) register by implementing
InboundEmailParser as @components — the controller picks them up
via DI.

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