Skip to main content

Crate io_imap

Crate io_imap 

Source
Expand description

§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

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
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 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

Re-exports§

pub use imap_codec as codec;
pub use imap_codec::imap_types as types;

Modules§

clientclient
Blocking IMAP client wrapping a Read + Write stream with a per-connection Fragmentizer and one method per coroutine.
coroutine
Generator-shape coroutine driver. Mirrors core::ops::Coroutine: Yield for intermediate progress, Return for terminal output, ImapCoroutineState for both.
rfc2177
IMAP4 IDLE extension.
rfc2971
IMAP4 ID extension.
rfc3501
IMAP4rev1 base protocol.
rfc3691
IMAP4 UNSELECT extension.
rfc4315
IMAP UIDPLUS extension.
rfc5161
IMAP ENABLE extension.
rfc5256
IMAP SORT and THREAD extensions.
rfc6851
IMAP MOVE extension.
rfc7628
SASL OAUTHBEARER mechanism.
rfc7677scram
SASL SCRAM-SHA-256 mechanism.
sasl
SASL mechanisms shared across IMAP authentication flows.
send
Base coroutine that all higher-level IMAP coroutines delegate to: serialises a command via imap_codec, drives read/write, and feeds responses back through the borrowed Fragmentizer.
watch
IMAP single-mailbox watcher: IDLE (RFC 2177) for the wake signal, SELECT (QRESYNC) (RFC 7162) for UID-keyed deltas.

Macros§

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