Skip to main content

Crate io_jmap

Crate io_jmap 

Source
Expand description

§I/O JMAP Documentation Matrix Mastodon

JMAP client library, written in Rust.

This library is composed of 3 feature-gated layers:

  • Low-level I/O-free coroutines: these no_std-compatible state machines contain the whole JMAP logic and can be used anywhere
  • Mid-level light client: a standard, blocking JMAP client using a Stream: Read + Write
  • High-level full client: light client + TCP connections and TLS negotiations handled for you

§Table of contents

§Features

  • I/O-free coroutines: no_std state machines; no sockets, no async runtime, no std required, drive against any blocking, async, or fuzz harness.
  • Light standard, blocking client (requires client feature)
  • Full standard, blocking client with TLS support:
    • Rustls with ring crypto (requires rustls-ring feature)
    • Rustls with aws crypto (requires rustls-aws feature)
    • Native TLS (requires native-tls feature)
  • HTTP Auth mechanisms: BASIC, BEARER

[!TIP] I/O JMAP is written in Rust and uses cargo features to gate backend support. The default feature set is declared in Cargo.toml or on docs.rs.

§RFC coverage

ModuleWhat it covers
8620JMAP core: session discovery, API requests, Foo/get, Foo/set, Foo/query, Foo/changes, blobs
8621JMAP for Mail: Mailbox, Email, Thread, Identity, EmailSubmission, VacationResponse

§Usage

I/O JMAP can be consumed three ways, depending on how much of the I/O stack you want to own. Each mode is gated by cargo features.

Whichever mode you pick, every standard-shape coroutine implements the JmapCoroutine trait with two associated types: Yield (intermediate progress) and Return (terminal value, by convention Result<Output, Error>). Its resume(arg: Option<&[u8]>) method returns a JmapCoroutineState<Yield, Return> with two variants:

  • Yielded(Yield): intermediate yield. Most coroutines pick the standard JmapYield with WantsRead / WantsWrite(Vec<u8>). Pass Some(&[]) after WantsRead to signal EOF.
  • Complete(Return): terminal yield, carrying Ok(Output) on success or Err(Error) on failure.

Three coroutines (JmapSessionGet, JmapBlobDownload, JmapBlobUpload) declare their own JmapRedirectYield which extends the standard variants with WantsRedirect { url, keep_alive, same_origin }: the server responded with a 3xx and the caller chooses whether to open a new connection to url and retry, or surface the redirect as an error.

§I/O-free coroutines

No features required: works in #![no_std], no sockets, no async runtime. You own the loop and the bytes; the library only produces request bytes and consumes server responses.

Fetch a JMAP session against a blocking rustls socket:

use std::{io::{Read, Write}, net::TcpStream, sync::Arc};

use io_jmap::{coroutine::*, rfc8620::{coroutine::JmapRedirectYield, session_get::*}};
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use rustls_platform_verifier::ConfigVerifierExt;
use secrecy::SecretString;
use url::Url;

let http_auth = SecretString::from("Bearer your-token-here");
let base_url = Url::parse("https://api.fastmail.com/jmap/session/").unwrap();

let config = ClientConfig::with_platform_verifier().unwrap();
let server_name = base_url.host_str().unwrap().to_string().try_into().unwrap();
let conn = ClientConnection::new(Arc::new(config), server_name).unwrap();
let tcp = TcpStream::connect((base_url.host_str().unwrap(), 443)).unwrap();
let mut stream = StreamOwned::new(conn, tcp);

let mut coroutine = JmapSessionGet::new(&http_auth, &base_url);
let mut arg: Option<&[u8]> = None;
let mut buf = [0u8; 8192];
let mut read_buf = Vec::<u8>::new();

let session = loop {
    match coroutine.resume(arg.take()) {
        JmapCoroutineState::Complete(Ok(JmapSessionGetOutput { session, .. })) => break session,
        JmapCoroutineState::Yielded(JmapRedirectYield::WantsRead) => {
            let n = stream.read(&mut buf).unwrap();
            read_buf.clear();
            read_buf.extend_from_slice(&buf[..n]);
            arg = Some(&read_buf);
        }
        JmapCoroutineState::Yielded(JmapRedirectYield::WantsWrite(bytes)) => {
            stream.write_all(&bytes).unwrap();
        }
        JmapCoroutineState::Yielded(JmapRedirectYield::WantsRedirect { url, .. }) => {
            todo!("reconnect to {url}");
        }
        JmapCoroutineState::Complete(Err(err)) => panic!("{err}"),
    }
};

println!("Logged in as: {}", session.username);
println!("API URL: {}", session.api_url);

§Light client

Enable the client feature. JmapClientStd::new(stream, http_auth) wraps any blocking Read + Write and exposes one method per JMAP coroutine. You still open the TCP socket and run TLS yourself, and hand over a ready-to-talk stream; the client takes it from there.

[dependencies]
io-jmap = { version = "0.1.0", default-features = false, features = ["client"] }
use std::{net::TcpStream, sync::Arc};

use io_jmap::{
    client::JmapClientStd,
    rfc8621::mailbox::query::JmapMailboxQueryOptions,
};
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use rustls_platform_verifier::ConfigVerifierExt;
use secrecy::SecretString;
use url::Url;

let http_auth = SecretString::from("Bearer your-token-here");
let session_url = Url::parse("https://api.fastmail.com/jmap/session/").unwrap();

let config = ClientConfig::with_platform_verifier().unwrap();
let server_name = session_url.host_str().unwrap().to_string().try_into().unwrap();
let conn = ClientConnection::new(Arc::new(config), server_name).unwrap();
let tcp = TcpStream::connect((session_url.host_str().unwrap(), 443)).unwrap();
let stream = StreamOwned::new(conn, tcp);

let mut client = JmapClientStd::new(stream, http_auth);
let session = client.session_get(&session_url).unwrap();
println!("Logged in as: {}", session.username);

let mailboxes = client.mailbox_query(JmapMailboxQueryOptions::default()).unwrap();
for mailbox in &mailboxes.mailboxes {
    println!("{:?}: {:?}", mailbox.role, mailbox.name);
}

§Full client

Enable one of the TLS feature flags: rustls-ring (default), rustls-aws, or native-tls. JmapClientStd::connect(url, tls, http_auth) opens http:// / https:// (or jmap:// / jmaps://) URLs via pimalaya/stream.

[dependencies]
io-jmap = "0.1.0" # rustls-ring is enabled by default
use io_jmap::{
    client::JmapClientStd,
    rfc8621::mailbox::query::JmapMailboxQueryOptions,
};
use pimalaya_stream::tls::Tls;
use secrecy::SecretString;
use url::Url;

let http_auth = SecretString::from("Bearer your-token-here");
let session_url = Url::parse("https://api.fastmail.com/jmap/session/").unwrap();
let tls = Tls::default();

let mut client = JmapClientStd::connect(&session_url, &tls, http_auth).unwrap();
let session = client.session_get(&session_url).unwrap();
println!("Logged in as: {}", session.username);

let mailboxes = client.mailbox_query(JmapMailboxQueryOptions::default()).unwrap();
for mailbox in &mailboxes.mailboxes {
    println!("{:?}: {:?}", mailbox.role, mailbox.name);
}

JMAP typically reuses a single connection for the entire session, so the client wraps one stream. When the apiUrl, uploadUrl or downloadUrl resolves to a different authority than where you first connected, use JmapClientStd::set_stream to swap in a new transport.

§Examples

See complete examples at ./examples.

Have also a look at real-world projects built on top of this library:

§AI disclosure

This project is developed with AI assistance. This section documents how, so users and downstream packagers can make informed decisions.

  • Tools: Claude Code (Anthropic), Opus 4.7, invoked locally with a persistent project-scoped memory and a small set of repo-specific rules.

  • Used for: Refactors, mechanical multi-file edits, boilerplate (feature gates, error enums, derive macros, trait impls), test scaffolding, doc polish, exploratory design conversations.

  • Not used for: Engineering, critical code, git manipulation (commit, merge, rebase…), real-world tests.

  • Verification: Every AI-assisted change is read, compiled, tested, and formatted before commit (nix develop --command cargo check / cargo test / cargo fmt). Behavioural correctness is verified against the relevant RFC or upstream spec, not assumed from the model output. Tests are never adjusted to fit AI-generated code; the code is adjusted to fit correct behaviour.

  • Limitations: AI models occasionally produce code that compiles and passes tests but is subtly wrong: off-by-one errors, missed edge cases, plausible but nonexistent APIs, stale RFC references. The verification workflow catches most of this; it does not catch all of it. Bug reports are welcome and taken seriously.

  • Last reviewed: 05/06/2026

§License

This project is licensed under either of:

at your option.

§Social

§Sponsoring

nlnet

Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:

If you appreciate the project, feel free to donate using one of the following providers:

GitHub Ko-fi Buy Me a Coffee Liberapay thanks.dev PayPal

Modules§

clientclient
Standard, blocking JMAP client.
coroutine
Generator-shape coroutine driver.
rfc8620
RFC 8620: The JSON Meta Application Protocol (JMAP).
rfc8621
RFC 8621: JMAP for Mail.

Macros§

jmap_try
Coroutine ?: forwards Yielded (via Into), short-circuits on Err (via Into), evaluates to the inner Ok value.