Skip to content

feat(api): allow multiple addresses in reply_to - #831

Open
cyj-git-0825 wants to merge 1 commit into
tokencanopy:mainfrom
cyj-git-0825:feat/reply-to-multi
Open

feat(api): allow multiple addresses in reply_to#831
cyj-git-0825 wants to merge 1 commit into
tokencanopy:mainfrom
cyj-git-0825:feat/reply-to-multi

Conversation

@cyj-git-0825

@cyj-git-0825 cyj-git-0825 commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Summary

reply_to on send/reply/forward now accepts either a single RFC 5322 address (unchanged) or an array of up to 5 addresses, so a caller can direct replies to several destinations (e.g. a shared triage inbox and an individual owner) — RFC 5322 Reply-To is an address-list. Closes #813.

The union is normalized at the HTTP edge: each address is validated and the entries are joined verbatim into one canonical address-list string, which the existing outbound pipeline (compose, storage, HITL recompose) already handles. The single-address form is byte-identical to before — no migration, no downstream signature changes. oasdiff confirms the oneOf widening is backward compatible.

Client surface checklist

  • Go handler + integration tests — ReplyToField (string|array) via a Huma SchemaProvider emitting a bounded oneOf; validateReplyTo enforces 1..5 parseable mailboxes and joins them. New handler tests + a schema-bounds drift guard.
  • MigrationN/A: normalized to the existing single-string pipeline; messages.reply_to is already text[].
  • OpenAPI spec + generated types refreshed — clean inline oneOf on all three request bodies.
  • TypeScript SDK — generated base regenerated; the union flows through SendEmailInput/ReplyInput/ForwardInput (no hand-written client change needed). Note: OAG's ObjectSerializer cannot serialize a scalar oneOf, so a documented generate-oag.sh step drops the union from typeMap (raw pass-through is the correct wire form).
  • Python SDK — generated base regenerated; the generated union won't coerce a bare str/list, so the hand-written client wraps reply_to via ForwardRequestReplyTo (_reply_to_union).
  • CLI — --reply-to is now repeatable (max 5), wired in cli/src/bin/e2a.ts.
  • MCP tool — send/reply/forward reply_to schema widened to string | array + client-delegation test in mcp/tests/client.test.ts.
  • Tests at each surface (positive + negative/regression) — Go, TS, Python, CLI, MCP; plus a shared contract scenario reply_to_address_list (array accept, string accept, bad-member 400) proven through the Go runner.

Intentionally skipped:

Web dashboard — no change. The dashboard has no outbound compose/send UI that sets reply_to (all reply_to usage there is inbound display). Verified manually (it's not on this template's checklist).

Operational risk

Low. Additive, backward-compatible request-shape change on a GA field; existing single-string callers are unaffected (byte-identical wire output). No new data storage, no auth/billing/notification changes, no feature flag. Rollback is a plain revert.

Test plan

  • make spec clean; oasdiff compat gate: no breaking changes
  • Go: reply_to handler tests + outbound package pass; contract scenario reply_to_address_list passes end-to-end
  • TS SDK: tsc typecheck + type-tests + client.test.ts (82) pass
  • Python SDK: mypy clean + unit suite (508) pass
  • CLI + MCP: build + tests pass
  • CI green on Linux (one local failure was a Windows-CRLF false-fail in an unrelated docs-table test — passes on LF)

@cyj-git-0825
cyj-git-0825 requested a review from jiashuoz as a code owner August 4, 2026 21:36
Copilot AI lite review requested due to automatic review settings August 4, 2026 21:36

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Expands the /v1 outbound compose surface so reply_to can be provided either as the historical single RFC 5322 address string or as an address list (array, max 5), with normalization/validation at the HTTP edge and parity updates across OpenAPI + SDKs + CLI + MCP + contract tests.

Changes:

  • Updated Go HTTP API request decoding/validation to accept reply_to as string | string[] and normalize it into the existing single-string outbound pipeline.
  • Regenerated OpenAPI + TypeScript/Python SDK types and added targeted serialization tests/guards.
  • Updated CLI and MCP tool schemas/tests to accept and forward multi-address reply_to.

Reviewed changes

Copilot reviewed 14 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/contract/scenarios.yaml Adds a contract scenario covering reply_to scalar vs array and bad-member rejection.
sdks/typescript/test/v1/client.test.ts Adds SDK client serialization tests for replyTo scalar vs array.
sdks/typescript/src/v1/generated/types/PromiseAPI.ts Regenerated TS client types to include the new Reply-To union model.
sdks/typescript/src/v1/generated/types/ObservableAPI.ts Regenerated TS client types to include the new Reply-To union model.
sdks/typescript/src/v1/generated/types/ObjectParamAPI.ts Regenerated TS client types to include the new Reply-To union model.
sdks/typescript/src/v1/generated/models/SendEmailRequest.ts Changes replyTo type to the generated union model.
sdks/typescript/src/v1/generated/models/ReplyRequest.ts Changes replyTo type to the generated union model.
sdks/typescript/src/v1/generated/models/ObjectSerializer.ts Exports the new generated union model (and is post-processed by the regen script).
sdks/typescript/src/v1/generated/models/ForwardRequestReplyTo.ts New generated `string
sdks/typescript/src/v1/generated/models/ForwardRequest.ts Changes replyTo type to the generated union model.
sdks/typescript/src/v1/generated/models/all.ts Exports the new union model.
sdks/typescript/src/v1/generated/.openapi-generator/FILES Tracks the newly generated union model file.
sdks/typescript/scripts/generate-oag.sh Adds a post-gen patch to avoid ObjectSerializer failing on scalar/array oneOf.
sdks/python/tests/test_v1_client.py Adds Python client wire-shape tests for reply_to scalar vs list.
sdks/python/src/e2a/v1/generated/models/send_email_request.py Regenerated model to use the union type and serialize it correctly.
sdks/python/src/e2a/v1/generated/models/reply_request.py Regenerated model to use the union type and serialize it correctly.
sdks/python/src/e2a/v1/generated/models/forward_request.py Regenerated model to use the union type and serialize it correctly.
sdks/python/src/e2a/v1/generated/models/forward_request_reply_to.py New generated oneOf wrapper model for reply_to.
sdks/python/src/e2a/v1/generated/models/init.py Exports the new union model.
sdks/python/src/e2a/v1/generated/init.py Re-exports the new union model at the generated package root.
sdks/python/src/e2a/v1/client.py Adds a handwritten wrapper so reply_to can still be passed as raw `str
mcp/tests/client.test.ts Adds a delegation test ensuring MCP forwards array/scalar replyTo to the SDK.
mcp/src/tools/messages.ts Widens MCP tool Zod schemas for reply_to to `string
mcp/src/client.ts Widens MCP client types to accept scalar or array replyTo.
internal/httpapi/outbound.go Implements ReplyToField (`string
internal/httpapi/outbound_test.go Adds Go handler tests for array propagation, bad members, and over-cap cases.
internal/httpapi/field_bounds_test.go Updates bounds drift-guards to cover the reply_to schema union.
cli/src/commands/send.ts Makes CLI --reply-to repeatable and threads it through to the SDK call.
cli/src/bin/e2a.ts Switches argument parsing to collect repeated --reply-to flags.
cli/src/tests/send.test.ts Adds CLI tests for multi---reply-to and empty-list omission behavior.
api/openapi.yaml Widens reply_to to a bounded oneOf (string vs array) across send/reply/forward request schemas.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/httpapi/outbound.go
Comment thread cli/src/commands/send.ts
Comment thread tests/contract/scenarios.yaml
reply_to on send/reply/forward now accepts EITHER a single RFC 5322
address (unchanged) OR an array of up to 5 addresses, so a caller can
direct replies to several destinations (e.g. a shared triage inbox and an
individual owner) — RFC 5322 Reply-To is an address-list.

The union is normalized at the HTTP edge: each address is validated and
the entries are joined verbatim into one canonical address-list string,
which the rest of the outbound pipeline (compose, storage, HITL
recompose) already handles. The single-address form is byte-identical to
before; no migration and no downstream signature changes. oasdiff
confirms the oneOf widening is backward compatible.

Surfaces:
- Go handler: ReplyToField (string|array) via a Huma SchemaProvider that
  emits a bounded oneOf; validateReplyTo enforces 1..5 parseable
  mailboxes and joins them into the address-list string.
- OpenAPI spec + regenerated TS/Python bases. TS: OAG's ObjectSerializer
  cannot serialize a scalar oneOf, so a documented generate-oag.sh step
  drops the union from typeMap (raw pass-through is the correct wire
  form). Python: the generated union will not coerce a bare str/list, so
  the ergonomic client wraps reply_to via ForwardRequestReplyTo.
- CLI: --reply-to is now repeatable (max 5).
- MCP: send/reply/forward reply_to schema widened to string|array.
- Contract scenario reply_to_address_list (array accept, string accept,
  bad-member 400), proven through the Go runner.
- Web dashboard: no change (it exposes no outbound reply_to input).

Closes tokencanopy#813

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 4, 2026 21:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 31 changed files in this pull request and generated no new comments.

Suppressed comments (1)

internal/httpapi/outbound.go:778

  • ReplyToField.UnmarshalJSON currently treats an explicit JSON null as “field omitted” (sets values=nil). This is a behavior change vs the previous string field (where null would be rejected) and it’s not represented in the emitted OpenAPI oneOf (string|array, not nullable). Prefer rejecting null (or explicitly marking the schema nullable if you want to support it) to keep request-shape validation strict and consistent across clients.
	trimmed := bytes.TrimSpace(data)
	if len(trimmed) == 0 || string(trimmed) == "null" {
		f.values = nil
		return nil
	}

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.

feat(api): allow multiple addresses in reply_to

2 participants