io-imap 0.1.0

IMAP client library
Documentation

I/O IMAP Documentation Matrix Mastodon

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

Module What it covers
2177 IDLE: push notification extension
2971 ID: server/client identification extension
3501 IMAP4rev1: greeting, capability, login/logout, list/lsub/status, create/delete/rename/subscribe/unsubscribe, select/examine/close/check/expunge, fetch/store/search/copy/append, noop, starttls
3691 UNSELECT: discard mailbox state without expunge
4315 UIDPLUS: APPENDUID and COPYUID response codes
5161 ENABLE: capability activation extension
5256 SORT and THREAD: server-side message sorting and threading
6851 MOVE: atomic message move extension
7162 CONDSTORE / 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)
7628 OAUTHBEARER: OAuth 2.0 bearer token SASL mechanism; also XOAUTH2
7677 SCRAM-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 surface domain events (ImapIdle, ImapMailboxWatch) declare their own Yield enum with an extra Event(...) variant.
  • 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