Skip to content

feat(inbound): scaffold InboundMessage + InboundEmailParser + InboundEmailRouter#26

Open
mpge wants to merge 1 commit intofeat/email-service-wireupfrom
feat/inbound-email-router
Open

feat(inbound): scaffold InboundMessage + InboundEmailParser + InboundEmailRouter#26
mpge wants to merge 1 commit intofeat/email-service-wireupfrom
feat/inbound-email-router

Conversation

@mpge
Copy link
Copy Markdown
Member

@mpge mpge commented Apr 24, 2026

Summary

Greenfield inbound-email foundation for Spring. Introduces:

  • InboundMessage — transport-agnostic record capturing a normalized inbound email (from, to, subject, bodies, threading headers, attachments).
  • InboundEmailParser — pluggable interface for provider-specific webhook payload normalization (Postmark, Mailgun, SES, IMAP).
  • InboundEmailRouter — the resolution brain that maps an inbound message to an existing ticket via canonical Message-ID parsing + signed Reply-To verification.

Mirrors the NestJS reference and the per-framework inbound-verify PRs (Laravel #70, Rails #45, Django #42, Adonis #50, WordPress #33) + the greenfield .NET router (escalated-dotnet #23).

Resolution order

  1. In-Reply-To parsed via MessageIdUtil.parseTicketIdFromMessageId — cold-start path, no DB lookup required.
  2. References parsed via MessageIdUtil, each id in order.
  3. Signed Reply-To on toEmail (reply+{id}.{hmac8}@...) verified via MessageIdUtil.verifyReplyTo. Forged signatures rejected with the timing-safe HMAC comparison.
  4. Subject line reference tag ([{PREFIX}-...]) — legacy.

Scope

Router + DTO + interface only. Follow-up PRs:

  1. Per-provider parser implementations (Postmark, Mailgun, SES)
  2. InboundEmailControllerPOST /escalated/webhook/email/inbound
  3. InboundEmailService — full process() orchestration with threading / attachments

Dependencies

Test plan

  • 10 Mockito tests verify every branch:
    • Canonical In-Reply-To parse
    • References with mixed unrelated ids
    • Signed Reply-To round-trip
    • Forgery rejection (wrong secret)
    • Secret-blank skip path
    • Subject-tag match
    • No-match empty Optional
    • Null-message safeguard
    • candidateHeaderMessageIds: in-reply-to first, references after
    • candidateHeaderMessageIds: empty headers yield none
  • CI green (won't trigger against stacked base until rebased)

…EmailRouter

Greenfield inbound-email foundation for Spring. Transport-agnostic
DTO (InboundMessage), pluggable provider parser interface
(InboundEmailParser), and the routing brain (InboundEmailRouter)
that resolves an inbound email to an existing ticket via canonical
Message-ID parsing + signed Reply-To verification.

Mirrors the NestJS reference and the per-framework inbound-verify
PRs plus the .NET greenfield router (#23).

Resolution order (first match wins):
  1. In-Reply-To parsed via MessageIdUtil — cold-start path, no DB
     lookup required.
  2. References parsed via MessageIdUtil, each id in order.
  3. Signed Reply-To on toEmail verified via MessageIdUtil. Survives
     clients that strip threading headers; forged signatures are
     rejected with a timing-safe HMAC comparison.
  4. Subject line reference tag [{PREFIX}-...] — legacy.

10 Mockito tests verify every branch + the forged-signature
rejection, the blank-secret skip, the null-message safeguard, and
the candidateHeaderMessageIds helper.

Follow-up PRs:
  - Per-provider parser implementations (Postmark, Mailgun, SES)
  - InboundEmailController (POST /escalated/webhook/email/inbound)
  - InboundEmailService (full process() orchestration)
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