Skip to content

feat: support message replies and tags - #74

Merged
addisonkline merged 1 commit into
mainfrom
kline/v2-msg-reply
Jun 18, 2026
Merged

feat: support message replies and tags#74
addisonkline merged 1 commit into
mainfrom
kline/v2-msg-reply

Conversation

@addisonkline

Copy link
Copy Markdown
Collaborator

Summary

Adds MAIL 2.0 message-reply and tag support across the protocol, server, and client, with a migration path so existing deployments don't lose persisted messages on upgrade. Server routes are unchanged — the new fields ride on existing draft/send request bodies.

What changed

Protocol (mail-swarms-protocol)

  • MAILMessage gains required mail_version ("2.0") and tags, plus optional reply_to (the replied-to message's id).
  • reply_to/tags threaded through DraftPostRequest and MAILDraft; tags added to DraftSendPostRequest. Draft-side fields are default-safe so pre-2.0 persisted drafts stay loadable.
  • MAILMessageInWebhook surfaces reply_to (in the msg_-prefixed wire form) and tags.

Server (mail-swarms-server)

  • post_draft stores reply_to/tags; send_draft stamps mail_version="2.0", copies reply_to, and merges draft + send-time tags as an order-preserving union.
  • Webhook delivery payloads carry reply_to/tags.

Client (mail-swarms-client)

  • New reply command (alias r): mail reply <message_id> <body> [--subject S] [--tags ...]. Addresses the reply to the original sender, defaults the subject to Re: <original subject> (no double Re:), and sets reply_to.
  • --tags on compose, send, and reply; reply_to/tags shown in message output and listed in mail --help.

Migration

  • scripts/migrate_messages_v2.py backfills mail_version/tags on persisted message records — --dry-run, automatic backup, deployment/path overrides, idempotent. (reply_to needs none; it defaults to None.)

Tests / docs

  • Updated every MAILMessage construction site; added coverage for tag validators, the reply command, draft/tag-merge behavior, webhook payload fields, and the migration script.
  • Documented the new fields in SPEC.md (§7.8–7.10), regenerated spec/openapi.yaml, and updated the client CLI reference.

Test plan

  • uv run pytest429 passed, 6 xfail (pre-existing stubs); ruff + mypy clean on touched files.

Notes for review

  • mail_version/tags are required on MAILMessage (per the schema as specified) — that's why all construction sites needed touching. Easy to give them defaults if preferred.
  • Webhook reply_to is msg_-prefixed to match message_id's wire form; swappable to bare UUID if consumers would rather correlate that way.

🤖 Generated with Claude Code

Add MAIL 2.0 message-reply and tag support across the protocol, server,
and client, plus a migration path for existing deployments.

Protocol:
- MAILMessage gains required `mail_version` ("2.0") and `tags`, plus an
  optional `reply_to` referencing the replied-to message's id.
- Thread `reply_to`/`tags` through DraftPostRequest, MAILDraft, and
  `tags` through DraftSendPostRequest (all default-safe so pre-2.0
  drafts stay loadable).
- Surface `reply_to` (msg_-prefixed) and `tags` in MAILMessageInWebhook.

Server:
- post_draft stores reply_to/tags on the draft; send_draft stamps
  mail_version, copies reply_to, and merges draft + send-time tags as an
  order-preserving union.
- Webhook delivery payloads now carry reply_to/tags.

Client:
- New `reply` command (alias `r`): replies to the original sender,
  defaults the subject to `Re: <subject>`, sets reply_to.
- `--tags` on compose, send, and reply; reply_to/tags shown in message
  output and listed in `mail --help`.

Migration:
- scripts/migrate_messages_v2.py backfills mail_version/tags on persisted
  message records (dry-run, backup, deployment overrides; idempotent).

Tests/docs:
- Update all MAILMessage construction sites; add coverage for tag
  validators, the reply command, draft/tag-merge behavior, webhook
  payload fields, and the migration script.
- Document the new fields in SPEC.md (§7.8-7.10), regenerate
  spec/openapi.yaml, and update the client CLI reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@addisonkline
addisonkline merged commit 44a534a into main Jun 18, 2026
2 checks passed
@addisonkline
addisonkline deleted the kline/v2-msg-reply branch June 18, 2026 18:45
addisonkline pushed a commit that referenced this pull request Jul 1, 2026
…bhook-receiver tutorial

Three new pages plus three index updates, addressing the webhook
documentation gap in the v2 docs branch (the first of the two
docs offers from the dev-list thread on Final Prep for v2).

The conceptual contract. What webhooks are for, the payload shape
(with v2's reply_to and tags from PR #74), the HMAC-SHA256 scheme
(timestamp.body, not body.timestamp — bug-shape worth flagging
explicitly), the headers MAIL sends, the receiver verification
checklist, the retry ladder (6 attempts: immediate, +1s, +30s,
+5min, +1h, +6h — total window ~7h31m), the retry conditions
(timeout / 5xx / 429 retry; 2xx / other 4xx don't), and the
'inbox is source of truth' contract that shapes how receivers
should handle internal failures.

Sources verified against
src/mail/server/src/mail_server/backends/base.py
(_handle_webhook_delivered and _webhook_delivered_post on origin
main; the v2-docs branch is currently behind main on the schema
changes from PR #74). Wrote against the canonical main-branch
shape so the docs match the v2 release contract; the docs branch
needs the rebase before merge.

Operator's guide. Secret generation, POST /admin/webhooks (with
events and url), GET /admin/webhooks for listing, GET
/admin/webhooks/{id} for inspection, PATCH /admin/webhooks/{id}
for URL / secret rotation (event types are immutable in v2),
DELETE /admin/webhooks/{id}. Includes the rotation coordination
note (both sides must update at the same moment to avoid signature
failures in flight).

Implementer's walkthrough. A single-file FastAPI receiver with:

- verify_signature using HMAC over raw bytes (calls out the two
  most common bugs: re-encoded JSON breaking signature; missing
  the timestamp.body prefix).
- is_duplicate / mark_processed for event_id-based dedup with a
  24-hour garbage-collection window.
- is_timestamp_in_window for 5-min skew rejection.
- The full endpoint composing them with the right error codes
  (503 if secret not configured, 408 for skew, 403 for bad
  signature, 200 with status=duplicate for retries).
- Registration command for end-to-end test.
- Diagnostic checklist for the 'nothing arrives' case.

docs/{explanations,howtos,tutorials}/README.md each gain a row
linking to the new page.

- docs/howtos/manage-mailing-lists.md exists as a stub today;
  drafting that one is the second piece of the offer and will
  land as a separate commit on the same branch.
- docs/references/http-api.md is a stub overall (not just for
  webhooks). The webhook-specific endpoints could be sketched
  there in a later pass; deferred so this commit stays focused
  on the conceptual + tutorial layer.

The four facts that are easiest to get wrong (and that I had
ground truth on from the chorus-side webhook receiver):

- HMAC inputs: 'timestamp.raw_body' not 'raw_body.timestamp'.
- X-MAIL-Timestamp value: Unix seconds as a STRING, used in both
  the HMAC and the header so receivers can recompute from the
  header alone.
- Signature header format: 'sha256=<hex>'.
- Body bytes: payload.model_dump_json() (Pydantic's canonical
  JSON), posted as-is — re-encoded JSON has different bytes and
  breaks verification.

All four match what _webhook_delivered_post does in
src/mail/server/src/mail_server/backends/base.py:653.
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