agentmail-rs 0.4.1

Unofficial typed Rust client for AgentMail (agentmail.to), the email API for agents.
Documentation

agentmail-rs

crates.io docs.rs

Unofficial. This is a community Rust client, not affiliated with or endorsed by AgentMail. AgentMail ships official Python and TypeScript SDKs; this crate fills the Rust gap. Wire shapes track AgentMail's public OpenAPI spec (API v0), which may change; pin a version and read the changelog.

A typed, async client for AgentMail, the email API for agents, with full coverage of the AgentMail REST API at every scope:

  • Inboxes: create / list / search / get / update / delete / authorize
  • Threads: list / filter / search / get / update / delete
  • Messages: send / list / filter / search / get / update / delete, reply, reply-all, forward, raw source, batch get / update, open tracking
  • Drafts: create / list / get / update / delete / send, attachment deltas
  • Attachments: fetch metadata and download bytes (presigned)
  • Webhooks: create / list / get / update / delete, custom delivery headers, plus optional Svix signature verification
  • Domains: create / list / get / update / delete, verify, zone file, provider setup link
  • Calendars: settings, agenda, events (create / get / update / delete), recurring-series instances, invite responses
  • Accounts (human app connections) and apps (directory, search, connect flow)
  • Pods, allow/block lists, metrics (events + usage + rates), inbox events, API keys (bearer and public-key), organization, and agent sign-up / verify / attach-human
  • Realtime (optional websockets feature): the SDKs' WebSocket event stream (connect_realtime), for agents without a public webhook URL
  • Pagination on every list call, list_all_* helpers that drain it, and automatic retries with exponential backoff

Scopes

Resources that AgentMail exposes at more than one scope (threads, webhooks, lists, domains, metrics, API keys, drafts, inboxes) are reached through a typed scope handle so the compiler rejects an operation a scope doesn't support:

# async fn demo(client: agentmail::Client) -> Result<(), agentmail::Error> {
client.org().list_threads(Default::default()).await?;          // all inboxes
client.inbox("ib_1").list_threads(Default::default()).await?;  // one inbox
client.pod("pod_1").list_threads(Default::default()).await?;   // one pod
client.pod("pod_1").create_inbox(Default::default()).await?;   // inbox inside a pod
// client.inbox("ib_1").list_domains(..)  // compile error: inboxes have no domains
# Ok(()) }

Inbox-only resources (messages, drafts, inbox events) live on client.inbox(id); account-global ones (pods, organization, auth, agent) are flat on Client.

Deliberately small: reqwest + serde + thiserror (plus tokio for retry backoff), with permissive deserialization (unknown fields are ignored) so API additions don't break you. Requests carry a 30-second default timeout. TLS is rustls with the ring provider (no OpenSSL, no aws-lc-rs, no C toolchain). The client installs ring as the process default at construction; if your application already installs a crypto provider, that choice is respected.

Features

  • retries (default): automatic retries with backoff. Turn it off with default-features = false to drop the direct tokio dependency and make every request a single attempt; tune it with Client::with_retry_policy.
  • webhook-verify (off by default): verify_webhook_signature for Svix-signed webhook deliveries. Adds ring (already the rustls provider) and base64.

Install

cargo add agentmail-rs

The crate publishes as agentmail-rs (the bare agentmail name was taken) but imports as agentmail. MSRV is Rust 1.86.

Usage

# async fn demo() -> Result<(), agentmail::Error> {
let client = agentmail::Client::from_env()?; // AGENTMAIL_API_KEY

let inbox = client
    .org()
    .create_inbox(agentmail::CreateInbox {
        username: Some("my-agent".into()),
        ..Default::default()
    })
    .await?; // my-agent@agentmail.to

// The common case, in one line:
client
    .inbox(&inbox.inbox_id)
    .send_text("someone@example.com", "Hello", "Sent from an agent's own inbox.")
    .await?;

// Or build the full message for HTML, cc/bcc, attachments, labels:
client
    .inbox(&inbox.inbox_id)
    .send_message(agentmail::SendMessage {
        to: vec!["someone@example.com".into()],
        subject: Some("Hello".into()),
        html: Some("<p>Rich body.</p>".into()),
        ..Default::default()
    })
    .await?;

// Drain every page with a `list_all_*` helper:
for m in client.inbox(&inbox.inbox_id).list_all_messages(Default::default()).await? {
    println!("{:?}: {:?}", m.from, m.subject);
}
# Ok(()) }

Client::from_env() reads AGENTMAIL_API_KEY (and optional AGENTMAIL_BASE_URL for the EU region or a mock server). For explicit config, use Client::new(key, base_url).

Testing

The unit and mock-server tests (cargo test) run offline. For a live smoke test that creates, exercises, and lists real inboxes against the API:

AGENTMAIL_API_KEY=... cargo run --example smoke

Receiving mail? The webhook example verifies a Svix-signed delivery end to end:

cargo run --example webhook --features webhook-verify

Parity

This crate binds the entire AgentMail API the official Python/TypeScript SDKs expose over REST, at all three scopes (organization, inbox, pod) via the typed scope handles above: inboxes, threads, messages, drafts, attachments, webhooks, domains, pods, allow/block lists, metrics, calendars, accounts, apps, API keys, organization, auth, and agent onboarding.

The official SDKs' WebSocket / realtime event stream is also bound here, behind the off-by-default websockets feature: connect_realtime yields typed events (message.received, bounces, opens, calendar activity, ...) without needing a public webhook URL. Extras beyond the SDKs: the Svix webhook signature verification helper (behind webhook-verify) and the list_all_* helpers that drain pagination.

Tracking upstream drift

Wire shapes track AgentMail's OpenAPI spec, which changes often (the official SDKs ship near-daily). The upstream commit this crate was built against is pinned in scripts/upstream_pin.json, and scripts/drift_check.py diffs the live spec against the surface this crate actually binds (operations and model fields). CI runs it weekly (.github/workflows/drift.yml) and opens an issue on drift; locally:

python3 scripts/drift_check.py           # report against the live spec
python3 scripts/drift_check.py --check   # exit 1 on drift

After binding new upstream surface, re-pin scripts/upstream_pin.json to the commit you built against.

Changes land in the changelog.

Contributing

Issues and PRs are welcome. Wire shapes track AgentMail's OpenAPI spec, so when adding or changing an endpoint, match the spec and add a wiremock test under tests/http/ (the suite runs offline: cargo test --all-features). If you need a scope-mirrored endpoint variant that isn't bound yet, open an issue.

License

Licensed under either of MIT or Apache-2.0 at your option.