Skip to content

feat(api): forward inbound messages — /messages/{id}/forward across SDKs, CLI, MCP#171

Merged
jiashuoz merged 2 commits into
mainfrom
feat/forward-message-api
May 28, 2026
Merged

feat(api): forward inbound messages — /messages/{id}/forward across SDKs, CLI, MCP#171
jiashuoz merged 2 commits into
mainfrom
feat/forward-message-api

Conversation

@jiashuoz
Copy link
Copy Markdown
Member

Summary

Adds a Forward endpoint mirroring the existing Reply path: POST /api/v1/agents/{email}/messages/{id}/forward.

  • Server compose: caller's optional comment, then a Gmail-style `---------- Forwarded message ---------` header block (From / Date / Subject / To / Cc), then the original body best-effort extracted from the inbound's stored MIME (text/plain, text/html, multipart/alternative, multipart/mixed, quoted-printable, base64).
  • Threading: forwards ship as a new thread — no `In-Reply-To` / `References` headers. Callers can pass `conversation_id` to bind explicitly.
  • HITL fix: `buildSendRequestFromMessage` now only copies `email_message_id` → `ReplyToMessageID` when `type="reply"`, so HITL-approved forwards don't accidentally inherit threading headers and stitch into the original conversation.
  • `InboundContext` review pane: forwards still persist `email_message_id` on the pending row, so reviewers see what's being forwarded.
  • Migration 019 extends the `messages.message_type` CHECK constraint to allow `'forward'` (mirrors the pattern from 008_loopback_method.sql).
  • Compile fix: `internal/e2e/e2e_test.go` had three local-struct declarations of `To` as `string` that should have been `[]string` — pre-existing, unrelated to this change but blocking build. Fixed in this PR.

Surfaces shipped

  • Go handler + 7 integration tests (auth, not-found, wrong-agent, unverified-domain, missing-recipients, SMTP happy-path, HITL hold)
  • Compose helpers + 12 unit tests
  • TypeScript SDK: `api.forwardMessage`, `client.forward()`, `InboundEmail.forward()`
  • Python SDK: regenerated types
  • CLI: `e2a forward --to … [--body …]`
  • MCP: `forward_message` tool + tests

What's NOT in scope (deferred)

  • Auto-forwarding the original's attachments (caller can attach manually; future `include_original_attachments` flag)
  • Conversations / Threads API
  • Labels
  • Drafts

Test plan

  • Go: `make test-unit && make test-integration` for `internal/agent` + `internal/outbound` — all green
  • TS SDK: 85 tests pass
  • CLI: 100 tests pass
  • MCP: 89 tests pass (1 new forward test added)
  • Live e2e against running service: in progress on this branch — register agent, inject inbound via SMTP, call `POST /forward`, verify SMTP receives forwarded message with `Subject: Fwd: …`, divider, original body, no `In-Reply-To`.

Pre-existing failures NOT caused by this PR:

  • `TestInboundDelivered` / `TestReplayProtection` in `internal/e2e` (signature verification failures, reproducible on main with my e2e compile fix applied but backend changes stashed)

🤖 Generated with Claude Code

jiashuoz and others added 2 commits May 27, 2026 20:22
…CLI/MCP

Mirrors the existing reply path: handler validates ownership, applies
HITL/idempotency/rate-limit/domain checks, then composes via new
outbound.BuildForward{Subject,Body,HTMLBody} helpers — best-effort MIME
extraction of the original body, Gmail-style header block, no
In-Reply-To/References (forwards are new threads).

HITL fix: buildSendRequestFromMessage now only copies email_message_id
into ReplyToMessageID when type="reply", so approved forwards don't
accidentally stitch into the original thread on send.

Includes:
- migration 019 extending messages.message_type CHECK with 'forward'
- TS SDK forwardMessage in api/client/inbound-email
- CLI: e2a forward <msg-id> --to … [--body …]
- MCP: forward_message tool
- Compile fix on a pre-existing internal/e2e mismatch where local
  structs declared To as string instead of []string

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ests

Review follow-ups:

1. BuildForwardBody mangled CRLF input. The naive ReplaceAll("\n","\r\n")
   turned existing "\r\n" into "\r\r\n", which renders as a literal CR
   in some clients. Most real inbound bodies arrive CRLF-terminated, so
   the bug would fire on virtually every multi-line forward.
   Fix: normalize "\r\n" → "\n" first, then convert to "\r\n".

2. performSelfSend hardcoded message_type="send" for the loopback row.
   Pre-existing latent bug — self-replies also recorded as "send".
   Fix: thread msgType through; all three callers (send/reply/forward)
   now record their actual intent.

3. Add the 4 integration tests the review called out:
   - TestForwardMessageSelfForwardUsesLoopback (loopback path coverage)
   - TestForwardMessageHTMLAtSMTP (multipart/alternative at the wire)
   - TestForwardMessageWithAttachments (caller-supplied attachment passthrough)
   - TestForwardMessageIdempotentReplay (Idempotency-Key replay shape)

4. Add 2 CRLF regression tests on BuildForwardBody (both CRLF and LF-only
   inputs must emit clean CRLF, never "\r\r\n").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jiashuoz jiashuoz force-pushed the feat/forward-message-api branch from 0dd179c to cd50568 Compare May 28, 2026 03:22
@jiashuoz jiashuoz merged commit 0dbf92c into main May 28, 2026
12 checks passed
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