IMAP client library, written in Rust
- I/O-free coroutines: every IMAP command is exposed as a
resume(arg: Option<&[u8]>)state machine. No sockets, no async runtime, nostdrequired. Drive against any blocking, async, or fuzz harness. - Standard, blocking client:
- Light client (requires
clientfeature):ImapClientStd::new(stream)wraps a connectedRead + Writestream and exposes one method per coroutine, with the long-livedImapContextmanaged for you. You still own TCP / TLS / STARTTLS. - Full std client (requires
rustls-ring,rustls-aws, ornative-tlsfeature):ImapClientStd::connect(url, tls, starttls, sasl)opensimap:///imaps://URLs via pimalaya/stream, drives the optional STARTTLS upgrade, and runs the chosen SASL mechanism, returning a ready-to-use authenticated client.
- Light client (requires
- SASL mechanisms:
LOGIN,PLAIN,ANONYMOUS,XOAUTH2andOAUTHBEARERbuilt-inSCRAM-SHA-256(requiresscramfeature)
The io-imap library is written in Rust, and relies on cargo features to enable or disable functionalities. Default features can be found in the features section of the Cargo.toml, or on docs.rs.
This library implements IMAP as I/O-agnostic coroutines: no sockets, no async runtime, no std required.
| Module | What it covers |
|---|---|
| 2177 | IDLE: push notification extension |
| 2971 | ID: server/client identification extension |
| 3501 | IMAP4rev1: greeting, capability, login, logout, select, list, fetch, store, search, copy, append, expunge, 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 |
| 7628 | OAUTHBEARER: OAuth 2.0 bearer token SASL mechanism; also XOAUTH2 |
| 7677 | SCRAM-SHA-256: SASL SCRAM-SHA-256 mechanism (feature scram) |
io-imap 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 coroutine exposes resume(arg: Option<&[u8]>) returning a result enum with four shapes:
WantsRead: caller reads more bytes from the socket and feeds them back on the next call. PassSome(&[])to signal EOF.WantsWrite(Vec<u8>): caller writes these bytes to the socket. The next call typically passesNone.Ok { … }: terminal success.Err { … }: terminal failure.
No features required: works in #![no_std], no sockets, no async runtime. You own the loop and the bytes; the library only produces command bytes and consumes server responses.
Read the IMAP greeting against a blocking TCP socket (the same shape works under async, fuzzing, or in-memory replay):
use std::{io::Read, net::TcpStream};
use io_imap::{context::ImapContext, rfc3501::greeting::*};
let mut stream = TcpStream::connect("imap.example.com:143").unwrap();
let mut buf = [0u8; 16 * 1024];
let mut coroutine = ImapGreetingGet::new(ImapContext::new(), false);
let mut arg: Option<&[u8]> = None;
let context = loop {
match coroutine.resume(arg.take()) {
ImapGreetingGetResult::Ok { context } => break context,
ImapGreetingGetResult::WantsRead => {
let n = stream.read(&mut buf).unwrap();
arg = Some(&buf[..n]);
}
ImapGreetingGetResult::WantsWrite(_) => unreachable!(),
ImapGreetingGetResult::Err { err, .. } => panic!("{err}"),
}
};Drive a multi-step command (LIST) the same way:
use std::{io::{Read, Write}, net::TcpStream};
use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox};
use io_imap::{context::ImapContext, rfc3501::list::*};
# let mut stream = TcpStream::connect("imap.example.com:143").unwrap();
# let mut buf = [0u8; 16 * 1024];
# let context = ImapContext::new();
let reference = Mailbox::try_from("").unwrap();
let pattern = ListMailbox::try_from("*").unwrap();
let mut coroutine = ImapMailboxList::new(context, reference, pattern);
let mut arg: Option<&[u8]> = None;
let mailboxes = loop {
match coroutine.resume(arg.take()) {
ImapMailboxListResult::Ok { mailboxes, .. } => break mailboxes,
ImapMailboxListResult::WantsRead => {
let n = stream.read(&mut buf).unwrap();
arg = Some(&buf[..n]);
}
ImapMailboxListResult::WantsWrite(bytes) => {
stream.write_all(&bytes).unwrap();
arg = None;
}
ImapMailboxListResult::Err { err, .. } => panic!("{err}"),
}
};
for (mailbox, _delimiter, _flags) in mailboxes {
println!("{mailbox:?}");
}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, run TLS / STARTTLS yourself, and hand over a ready-to-talk stream; the client takes it from there.
[dependencies]
io-imap = { version = "0.0.1", default-features = false, features = ["client"] }use std::net::TcpStream;
use io_imap::client::ImapClientStd;
let stream = TcpStream::connect("imap.example.com:143")?;
let mut client = ImapClientStd::new(stream);
let capabilities = client.greeting()?;
println!("server capabilities: {capabilities:?}");
let reference = "".try_into()?;
let pattern = "*".try_into()?;
for (mailbox, _, _) in client.list(reference, pattern)? {
println!("{mailbox:?}");
}Enable one of the TLS feature flags: rustls-ring (default), rustls-aws, or native-tls. ImapClientStd::connect(url, tls, starttls, sasl) 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 = "0.0.1" # rustls-ring is enabled by defaultuse io_imap::client::ImapClientStd;
use pimalaya_stream::{sasl::SaslLogin, tls::Tls};
use secrecy::SecretString;
use url::Url;
let url = Url::parse("imaps://imap.example.com")?;
let tls = Tls::default();
let sasl = SaslLogin {
username: "alice@example.com".into(),
password: SecretString::from("hunter2".to_owned()),
};
let mut client = ImapClientStd::connect(&url, &tls, false, Some(sasl))?;
// session is already authenticated; issue further commands directly
for (mailbox, _, _) in client.list("".try_into()?, "*".try_into()?)? {
println!("{mailbox:?}");
}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.
See complete examples at ./examples.
Have a look at projects built on top of this library:
- himalaya: CLI to manage emails
This project is licensed under either of:
at your option.
- Chat on Matrix
- News on Mastodon or RSS
- Mail at pimalaya.org@posteo.net
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: