I/O IMAP [](https://docs.rs/io-imap/latest/io_imap) [](https://matrix.to/#/#pimalaya:matrix.org) [](https://fosstodon.org/@pimalaya)

June 11, 2026 · View on GitHub

IMAP 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 IMAP logic and can be used anywhere
  • Mid-level light client: a standard, blocking IMAP 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)
  • SASL mechanisms:
    • ANONYMOUS, LOGIN, PLAIN, XOAUTH2 and OAUTHBEARER built-in
    • SCRAM-SHA-256 (requires scram feature)
  • IMAP extensions: IDLE, CONDSTORE, QRESYNC etc (see RFC coverage)

Tip

I/O IMAP 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
2177IDLE: push notification extension
2971ID: server/client identification extension
3501IMAP4rev1: greeting, capability, login/logout, list/lsub/status, create/delete/rename/subscribe/unsubscribe, select/examine/close/check/expunge, fetch/store/search/copy/append, noop, starttls
3691UNSELECT: discard mailbox state without expunge
4315UIDPLUS: APPENDUID and COPYUID response codes
5161ENABLE: capability activation extension
5256SORT and THREAD: server-side message sorting and threading (SORT also offers a SEARCH + FETCH client-side fallback for servers lacking the extension)
6851MOVE: atomic message move extension
7162CONDSTORE / QRESYNC: CHANGEDSINCE / VANISHED FETCH modifiers and CONDSTORE / QRESYNC SELECT and EXAMINE parameters for fast incremental resync (obsoletes RFC 4551 CONDSTORE and original RFC 5162 QRESYNC)
7628OAUTHBEARER: OAuth 2.0 bearer token SASL mechanism; also XOAUTH2
7677SCRAM-SHA-256: SASL SCRAM-SHA-256 mechanism (feature scram)

Usage

I/O IMAP can be consumed at three layers, depending on how much of the I/O stack you want to own:

  • Coroutines: no_std-friendly state machines. You own the socket and the bytes; the library produces commands and parses responses. Works under any blocking, async, or fuzz harness.
  • Light client (client feature): a Read + Write wrapper exposing one method per IMAP command. You still open the socket and negotiate TLS, then hand over a ready stream.
  • Full client (rustls-ring / rustls-aws / native-tls): TCP, TLS, greeting and SASL handled for you; pass in a URL + SASL config, get back an authenticated session.

Every coroutine implements the ImapCoroutine trait (crate::coroutine). resume(&mut Fragmentizer, Option<&[u8]>) returns ImapCoroutineState<Yield, Return>:

  • Yielded(y): intermediate progress. The standard ImapYield is WantsRead (caller reads more bytes and feeds them back; pass Some(&[]) on EOF) or WantsWrite(Vec<u8>) (caller writes these bytes; next resume usually takes None). Coroutines that need extra variants declare their own Yield enum: ImapIdle and ImapMailboxWatch add Event(...), ImapMessageAppendStream adds WantsStream (caller pumps the declared message octets straight to the socket), and ImapMessageFetchStream adds BodyChunk(Vec<u8>) / WantsStream { len } (caller drains the message body straight to a sink). The streaming variants signal a short transfer by resuming with Some(&[]).
  • Complete(result): terminal payload, Result<Output, Error>.

The three snippets below all connect to a server (HOST / PORT env vars; URL for the full client), read the greeting, and print the CAPABILITY list. They are the verbatim sources of cargo run --example <name>.

Coroutine

No io-imap features required. The same shape works under async or fuzz harnesses, only the I/O glue changes.

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

use io_imap::{
    codec::fragmentizer::Fragmentizer,
    coroutine::{ImapCoroutine, ImapCoroutineState, ImapYield},
    rfc3501::greeting::{ImapGreetingGet, ImapGreetingGetOptions},
};
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use rustls_platform_verifier::ConfigVerifierExt;

fn main() -> Result<(), Box<dyn Error>> {
    env_logger::init();

    let host = env::var("HOST").unwrap();
    let port: u16 = env::var("PORT")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(993);

    rustls::crypto::ring::default_provider()
        .install_default()
        .ok();

    let config = Arc::new(ClientConfig::with_platform_verifier()?);
    let server_name = rustls::pki_types::ServerName::try_from(host.as_str())?.to_owned();
    let tls = ClientConnection::new(config, server_name)?;
    let sock = TcpStream::connect((host.as_str(), port))?;
    let mut stream = StreamOwned::new(tls, sock);

    let mut fragmentizer = Fragmentizer::new(50 * 1024 * 1024);
    let mut buf = [0u8; 4096];

    let opts = ImapGreetingGetOptions {
        ensure_capabilities: true,
    };
    let mut coroutine = ImapGreetingGet::new(opts);
    let mut arg = None;

    let greeting = loop {
        match coroutine.resume(&mut fragmentizer, arg.take()) {
            ImapCoroutineState::Yielded(ImapYield::WantsWrite(bytes)) => {
                stream.write_all(&bytes)?;
            }
            ImapCoroutineState::Yielded(ImapYield::WantsRead) => {
                let n = stream.read(&mut buf)?;
                arg = Some(&buf[..n]);
            }
            ImapCoroutineState::Complete(Ok(greeting)) => break greeting,
            ImapCoroutineState::Complete(Err(err)) => return Err(err.into()),
        }
    };

    for capability in greeting.capability {
        println!("{capability:?}");
    }

    Ok(())
}

[!INFO] See the tokio-based alternative at examples/tokio_coroutine.rs.

Light client

Enable the client feature. ImapClientStd::new(stream) wraps any blocking Read + Write and exposes one method per IMAP command. You still open the TCP socket, negotiate TLS, authenticate; the client takes it from there.

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

use io_imap::client::ImapClientStd;
use rustls::{ClientConfig, ClientConnection, StreamOwned};
use rustls_platform_verifier::ConfigVerifierExt;

fn main() -> Result<(), Box<dyn Error>> {
    env_logger::init();

    let host = env::var("HOST").unwrap();
    let port: u16 = env::var("PORT")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(993);

    rustls::crypto::ring::default_provider()
        .install_default()
        .ok();

    let config = Arc::new(ClientConfig::with_platform_verifier()?);
    let server_name = rustls::pki_types::ServerName::try_from(host.as_str())?.to_owned();
    let tls = ClientConnection::new(config, server_name)?;
    let sock = TcpStream::connect((host.as_str(), port))?;
    let stream = StreamOwned::new(tls, sock);

    let mut client = ImapClientStd::new(stream);
    let capabilities = client.greeting()?;

    for capability in capabilities {
        println!("{capability:?}");
    }

    Ok(())
}

Full client

Enable one of the TLS feature flags: rustls-ring (default), rustls-aws, or native-tls. ImapClientStd::connect(url, tls, starttls, sasl, auto_id) opens imap:// (plain TCP) or imaps:// (implicit TLS) via pimalaya/stream, drives the optional STARTTLS upgrade, reads the greeting + capability list, and runs the chosen SASL mechanism, returning a ready-to-use authenticated client.

[dependencies]
io-imap = { version = "0.1.0", default-features = false, features = ["rustls-ring"] }
use std::{env, error::Error};

use io_imap::client::ImapClientStd;
use pimalaya_stream::{sasl::Sasl, tls::Tls};
use url::Url;

fn main() -> Result<(), Box<dyn Error>> {
    env_logger::init();

    let url = env::var("URL").unwrap();
    let url = Url::parse(&url)?;
    let tls = Tls::default();

    let (_client, capabilities) = ImapClientStd::connect(&url, &tls, false, None::<Sasl>, None)?;

    for capability in capabilities {
        println!("{capability:?}");
    }

    Ok(())
}

The sasl argument is Option<impl Into<Sasl>>, so any of the per-mechanism structs (SaslLogin, SaslPlain, SaslAnonymous, SaslOauthbearer, SaslXoauth2, SaslScramSha256 behind the scram feature) can be passed in Some(...) directly without wrapping in a Sasl variant.

Examples

See the complete examples at ./examples.

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

  • Himalaya CLI: CLI to manage emails
  • Himalaya TUI: TUI to manage emails
  • Neverest: CLI to synchronize emails
  • Mirador: CLI to watch mailbox changes and fire hooks on every event
  • Sirup: CLI to spawn pre-authenticated IMAP/SMTP sessions and expose them via Unix sockets

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: 29/05/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