Skip to content

feat(email): add MessageIdUtil for RFC 5322 threading + signed Reply-To#24

Open
mpge wants to merge 1 commit intomainfrom
feat/email-message-id
Open

feat(email): add MessageIdUtil for RFC 5322 threading + signed Reply-To#24
mpge wants to merge 1 commit intomainfrom
feat/email-message-id

Conversation

@mpge
Copy link
Copy Markdown
Member

@mpge mpge commented Apr 24, 2026

Summary

Ports the NestJS email/message-id.ts helpers to Spring. Pure utility class, no runtime dependencies beyond the JDK's javax.crypto.

API

  • buildMessageId(ticketId, replyId, domain) — RFC 5322 Message-ID
    • <ticket-{id}@{domain}> for initial ticket emails
    • <ticket-{id}-reply-{replyId}@{domain}> for agent replies
  • parseTicketIdFromMessageId(raw) — extract ticket id from header value (accepts with or without angle brackets)
  • buildReplyTo(ticketId, secret, domain) — signed Reply-To of the form reply+{id}.{hmac8}@{domain}
  • verifyReplyTo(address, secret) — inbound webhook check; returns ticket id on match

Why signed Reply-To

Even when clients strip our Message-ID / In-Reply-To headers, the Reply-To address still carries ticket identity — the 8-char HMAC-SHA256 prefix prevents forgery.

Scope

Utility only. Follow-up PR will:

  1. Wire MessageIdUtil into EmailService.sendEmail (Message-ID + Reply-To headers)
  2. Add an EscalatedProperties.Email config block for domain + inboundSecret
  3. Add an inbound webhook / controller that calls verifyReplyTo

Test plan

  • 13 unit tests covering buildMessageId (initial + reply), parseTicketIdFromMessageId (round-trip + malformed), buildReplyTo (stable, unique per ticket), verifyReplyTo (tamper rejection, wrong secret, local-part-only, case-insensitive hex)
  • CI green: test, checkstyle

Ports the NestJS email/message-id.ts helpers to Spring:

- buildMessageId(ticketId, replyId, domain) — RFC 5322 Message-ID
- parseTicketIdFromMessageId(raw) — inbound lookup
- buildReplyTo(ticketId, secret, domain) — signed Reply-To for threading
- verifyReplyTo(address, secret) — inbound webhook verification

The signed Reply-To carries identity even when clients strip our
Message-ID / In-Reply-To headers, so the inbound provider webhook
can verify before routing a reply to its ticket.

Pure functions, no Spring context — 13 unit tests covering round-trip,
tamper rejection, case-insensitive hex, malformed input, and the
local-part-only acceptance path.

Follow-up PR will wire the util into EmailService so outbound ticket
notifications use the RFC-compliant Message-ID + signed Reply-To,
and add an EscalatedProperties.Email config block for domain+secret.
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