Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
run: |
version="$(node -p "require('./compatibility.json').cli")"
npx -y "agentmail-cli@$version" inboxes --help | tee /tmp/inboxes-help
npx -y "agentmail-cli@$version" inboxes:messages --help | tee /tmp/messages-help
npx -y "agentmail-cli@$version" inboxes messages --help | tee /tmp/messages-help
grep -q "get" /tmp/inboxes-help
grep -q "get" /tmp/messages-help
! grep -q "retrieve" /tmp/inboxes-help
Expand Down
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

All notable changes to the AgentMail plugin are documented here.

## 0.5.0 - Unreleased
## 0.5.0 - 2026-10-02

- AgentID calls the services an agent creates accounts at "apps" now, not "providers". The `agentid` and `agentmail-mcp` skills use the renamed hosted MCP tools: `list_apps`, `search_apps`, `get_app`, and `connect_app` replace `list_providers`, `search_providers`, `get_provider`, and `connect_provider`, with no aliases. They take `appId` and return `appId` and `appName`. `list_accounts` keeps its name.
- The authorization row for connecting is now "Connect inbox to app".
- The permission `connect_app` needs is now named `app_connect`, not `provider_connect`. The `agentid` skill's 403 `missing_permission` guidance and the README name `app_connect`, the name the AgentMail API and console use.
- The `agentmail` skill's deliverability reference calls `client.metrics.query_events` / `queryEvents`; `metrics.query` is gone from the current SDKs.
- The webhook verification examples verify the signature and then parse the raw body: Svix 2.x `Webhook.verify` returns nothing (`undefined` / `None`), so the old examples dispatched an empty event.
- The `agentmail-cli` skill uses nested subcommands (`agentmail inboxes messages list`); CLI 1.x rejects the colon form (`inboxes:messages`) the skill used before.
- Track AgentMail SDK TypeScript 0.5.35 and Python 2.0.8, CLI 1.8.0, and AgentMail Toolkit TypeScript 0.10.0 and Python 0.3.0.

## 0.4.0 - 2026-09-29

Expand Down
10 changes: 5 additions & 5 deletions compatibility.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"verifiedAt": "2026-07-21",
"verifiedAt": "2026-10-02",
"agentmail": {
"typescript": "0.5.17",
"python": "0.5.8"
"typescript": "0.5.35",
"python": "2.0.8"
},
"cli": "0.7.14",
"cli": "1.8.0",
"toolkit": {
"typescript": "0.5.1",
"typescript": "0.10.0",
"python": "0.3.0"
}
}
34 changes: 17 additions & 17 deletions skills/agentmail-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ npm install -g agentmail-cli
export AGENTMAIL_API_KEY="am_..."
```

Subcommands for a resource nested under another use a colon, e.g. `inboxes:messages`, `inboxes:threads`, `pods:inboxes`, `pods:threads`.
A resource nested under another is a nested subcommand, e.g. `inboxes messages`, `inboxes threads`, `pods inboxes`, `pods threads` (CLI 1.x; the colon form `inboxes:messages` is rejected). Run `agentmail <resource> --help` to list what a resource supports.

## Inboxes

Expand All @@ -28,45 +28,45 @@ Confirm the exact inbox before running the destructive delete command.
## Messages and threads

```bash
agentmail inboxes:messages list --inbox-id <inbox_id>
agentmail inboxes:messages get --inbox-id <inbox_id> --message-id <message_id>
agentmail inboxes messages list --inbox-id <inbox_id>
agentmail inboxes messages get --inbox-id <inbox_id> --message-id <message_id>

agentmail inboxes:messages send --inbox-id <inbox_id> \
agentmail inboxes messages send --inbox-id <inbox_id> \
--to "recipient@example.com" \
--subject "Hello" \
--text "Message body"

# HTML body instead of plain text
agentmail inboxes:messages send --inbox-id <inbox_id> \
agentmail inboxes messages send --inbox-id <inbox_id> \
--to "recipient@example.com" \
--subject "Hello" \
--html "<h1>Hello</h1>"

agentmail inboxes:messages reply --inbox-id <inbox_id> \
agentmail inboxes messages reply --inbox-id <inbox_id> \
--message-id <message_id> \
--text "Reply body"

agentmail inboxes:messages forward --inbox-id <inbox_id> \
agentmail inboxes messages forward --inbox-id <inbox_id> \
--message-id <message_id> \
--to "someone@example.com"

agentmail inboxes:threads list --inbox-id <inbox_id>
agentmail inboxes:threads get --inbox-id <inbox_id> --thread-id <thread_id>
agentmail inboxes threads list --inbox-id <inbox_id>
agentmail inboxes threads get --inbox-id <inbox_id> --thread-id <thread_id>
```

Use a message ID for replies. Fetch the full message before relying on body content.

## Drafts

```bash
agentmail inboxes:drafts create --inbox-id <inbox_id> \
agentmail inboxes drafts create --inbox-id <inbox_id> \
--to "recipient@example.com" \
--subject "Pending approval" \
--text "Draft body"

agentmail inboxes:drafts list --inbox-id <inbox_id>
agentmail inboxes:drafts get --inbox-id <inbox_id> --draft-id <draft_id>
agentmail inboxes:drafts send --inbox-id <inbox_id> --draft-id <draft_id>
agentmail inboxes drafts list --inbox-id <inbox_id>
agentmail inboxes drafts get --inbox-id <inbox_id> --draft-id <draft_id>
agentmail inboxes drafts send --inbox-id <inbox_id> --draft-id <draft_id>
```

## Pods
Expand All @@ -77,11 +77,11 @@ Pods group inboxes together.
agentmail pods create --name "My Pod"
agentmail pods list

agentmail pods:inboxes create --pod-id <pod_id> --display-name "Pod Inbox"
agentmail pods:inboxes list --pod-id <pod_id>
agentmail pods inboxes create --pod-id <pod_id> --display-name "Pod Inbox"
agentmail pods inboxes list --pod-id <pod_id>

agentmail pods:threads list --pod-id <pod_id>
agentmail pods:threads get --pod-id <pod_id> --thread-id <thread_id>
agentmail pods threads list --pod-id <pod_id>
agentmail pods threads get --pod-id <pod_id> --thread-id <thread_id>
```

## Webhooks
Expand Down
2 changes: 1 addition & 1 deletion skills/agentmail/references/deliverability.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Use this when "my agent's email didn't arrive." Find the branch that matches the

- Subscribe to the `message.bounced` event (webhook or WebSocket) — a successful `send()` call only confirms AgentMail accepted the message, not that it was delivered.
- Check whether `feedback_enabled` is set on the sending domain. When enabled, AgentMail routes bounce and complaint notifications to your inboxes; if it was never set, you may be missing that feedback entirely. See [admin.md](admin.md#domains).
- Self-monitor bounce rate with `client.metrics.query(event_types=["message.bounced"], ...)` / `client.metrics.query({ eventTypes: ["message.bounced"], ... })`.
- Self-monitor bounce rate with `client.metrics.query_events(event_types=["message.bounced"], ...)` / `client.metrics.queryEvents({ eventTypes: ["message.bounced"], ... })`.

## Delivered, but landing in spam

Expand Down
16 changes: 10 additions & 6 deletions skills/agentmail/references/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@ if (!secret) throw new Error("AGENTMAIL_WEBHOOK_SECRET is required");
const app = express();
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = new Webhook(secret).verify(
req.body,
req.headers as Record<string, string>,
);
// verify() throws on a bad signature or stale timestamp. Svix 2.x returns nothing, so parse the
// raw body yourself once it passes.
new Webhook(secret).verify(req.body, req.headers as Record<string, string>);
const event = JSON.parse(req.body.toString());
void event; // Enqueue or dispatch the verified event here.
res.status(204).send();
} catch {
Expand All @@ -78,6 +78,7 @@ app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
## Python verification

```python
import json
import os

from flask import Flask, request
Expand All @@ -88,12 +89,15 @@ secret = os.environ["AGENTMAIL_WEBHOOK_SECRET"]

@app.post("/webhooks")
def receive_webhook():
payload = request.get_data()
try:
event = Webhook(secret).verify(request.get_data(), request.headers)
# Raises on a bad signature or stale timestamp. Svix 2.x returns None, so parse the raw body
# yourself once it passes.
Webhook(secret).verify(payload, request.headers)
except WebhookVerificationError:
return "", 400

# Enqueue or dispatch the verified event here.
event = json.loads(payload) # Enqueue or dispatch the verified event here.
return "", 204
```

Expand Down
5 changes: 3 additions & 2 deletions tests/test_webhook_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ def signed_headers(payload: str, timestamp: datetime) -> dict[str, str]:
class WebhookVerificationTests(unittest.TestCase):
def test_valid_signature_verifies(self) -> None:
now = datetime.now(tz=timezone.utc)
event = Webhook(SECRET).verify(PAYLOAD, signed_headers(PAYLOAD, now))
self.assertEqual(event["event_type"], "message.received")
# verify() raises on failure; Svix 2.x returns None, so the event is the parsed raw body.
Webhook(SECRET).verify(PAYLOAD, signed_headers(PAYLOAD, now))
self.assertEqual(json.loads(PAYLOAD)["event_type"], "message.received")

def test_tampered_body_is_rejected(self) -> None:
headers = signed_headers(PAYLOAD, datetime.now(tz=timezone.utc))
Expand Down
6 changes: 2 additions & 4 deletions tests/webhook_typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ if (!secret) throw new Error("AGENTMAIL_WEBHOOK_SECRET is required");
const app = express();
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
try {
const event = new Webhook(secret).verify(
req.body,
req.headers as Record<string, string>,
);
new Webhook(secret).verify(req.body, req.headers as Record<string, string>);
const event = JSON.parse(req.body.toString());
void event;
res.status(204).send();
} catch {
Expand Down
5 changes: 3 additions & 2 deletions tests/webhook_verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ function signedHeaders(body: string, timestamp: Date): Record<string, string> {
};
}

const verified = new Webhook(secret).verify(payload, signedHeaders(payload, new Date()));
assert.deepEqual(verified, JSON.parse(payload));
// verify() throws on failure; Svix 2.x returns undefined, so the event is the parsed raw body.
assert.doesNotThrow(() => new Webhook(secret).verify(payload, signedHeaders(payload, new Date())));
assert.equal(JSON.parse(payload).event_type, "message.received");

assert.throws(() =>
new Webhook(secret).verify(payload.replace("received", "sent"), signedHeaders(payload, new Date())),
Expand Down
Loading