feat: support message replies and tags - #74
Merged
Conversation
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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)MAILMessagegains requiredmail_version("2.0") andtags, plus optionalreply_to(the replied-to message's id).reply_to/tagsthreaded throughDraftPostRequestandMAILDraft;tagsadded toDraftSendPostRequest. Draft-side fields are default-safe so pre-2.0 persisted drafts stay loadable.MAILMessageInWebhooksurfacesreply_to(in themsg_-prefixed wire form) andtags.Server (
mail-swarms-server)post_draftstoresreply_to/tags;send_draftstampsmail_version="2.0", copiesreply_to, and merges draft + send-time tags as an order-preserving union.reply_to/tags.Client (
mail-swarms-client)replycommand (aliasr):mail reply <message_id> <body> [--subject S] [--tags ...]. Addresses the reply to the original sender, defaults the subject toRe: <original subject>(no doubleRe:), and setsreply_to.--tagsoncompose,send, andreply;reply_to/tagsshown in message output and listed inmail --help.Migration
scripts/migrate_messages_v2.pybackfillsmail_version/tagson persisted message records —--dry-run, automatic backup, deployment/path overrides, idempotent. (reply_toneeds none; it defaults toNone.)Tests / docs
MAILMessageconstruction site; added coverage for tag validators, thereplycommand, draft/tag-merge behavior, webhook payload fields, and the migration script.SPEC.md(§7.8–7.10), regeneratedspec/openapi.yaml, and updated the client CLI reference.Test plan
uv run pytest→ 429 passed, 6 xfail (pre-existing stubs); ruff + mypy clean on touched files.Notes for review
mail_version/tagsare required onMAILMessage(per the schema as specified) — that's why all construction sites needed touching. Easy to give them defaults if preferred.reply_toismsg_-prefixed to matchmessage_id's wire form; swappable to bare UUID if consumers would rather correlate that way.🤖 Generated with Claude Code