Expand description
§I/O IMAP

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_stdstate machines; no sockets, no async runtime, nostdrequired, drive against any blocking, async, or fuzz harness. - Light standard, blocking client (requires
clientfeature) - Full standard, blocking client with TLS support:
- Rustls with ring crypto (requires
rustls-ringfeature) - Rustls with aws crypto (requires
rustls-awsfeature) - Native TLS (requires
native-tlsfeature)
- Rustls with ring crypto (requires
- SASL mechanisms:
ANONYMOUS,LOGIN,PLAIN,XOAUTH2andOAUTHBEARERbuilt-inSCRAM-SHA-256(requiresscramfeature)
- IMAP extensions:
IDLE,CONDSTORE,QRESYNCetc (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 (
clientfeature): aRead + Writewrapper 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 standardImapYieldisWantsRead(caller reads more bytes and feeds them back; passSome(&[])on EOF) orWantsWrite(Vec<u8>)(caller writes these bytes; next resume usually takesNone). Coroutines that surface domain events (ImapIdle,ImapMailboxWatch) declare their ownYieldenum with an extraEvent(...)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
- Chat on Matrix
- News on Mastodon or RSS
- Mail at pimalaya.org@posteo.net
§Sponsoring
Special thanks to the NLnet foundation and the European Commission that have been financially supporting the project for years:
- 2022 → 2023: NGI Assure
- 2023 → 2024: NGI Zero Entrust
- 2024 → 2026: NGI Zero Core
- 2027 in preparation…
If you appreciate the project, feel free to donate using one of the following providers:
Re-exports§
pub use imap_codec as codec;pub use imap_codec::imap_types as types;
Modules§
- client
client - Blocking IMAP client wrapping a
Read + Writestream with a per-connectionFragmentizerand one method per coroutine. - coroutine
- Generator-shape coroutine driver. Mirrors
core::ops::Coroutine:Yieldfor intermediate progress,Returnfor terminal output,ImapCoroutineStatefor 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.
- rfc7677
scram - 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 borrowedFragmentizer. - watch
- IMAP single-mailbox watcher: IDLE (RFC 2177) for the wake signal, SELECT (QRESYNC) (RFC 7162) for UID-keyed deltas.
Macros§
- imap_
try - Coroutine
?: forwardsYielded(viaInto), short-circuits onErr, evaluates to the innerOkvalue.
