Skip to content

pimalaya/io-imap

I/O IMAP Documentation Matrix Mastodon

IMAP client library, written in Rust

Table of contents

Features

  • I/O-free coroutines: every IMAP command is exposed as a resume(arg: Option<&[u8]>) state machine. No sockets, no async runtime, no std required. Drive against any blocking, async, or fuzz harness.
  • Standard, blocking client:
    • Light client (requires client feature): ImapClientStd::new(stream) wraps a connected Read + Write stream and exposes one method per coroutine, with the long-lived ImapContext managed for you. You still own TCP / TLS / STARTTLS.
    • Full std client (requires rustls-ring, rustls-aws, or native-tls feature): ImapClientStd::connect(url, tls, starttls, sasl) opens imap:// / imaps:// URLs via pimalaya/stream, drives the optional STARTTLS upgrade, and runs the chosen SASL mechanism, returning a ready-to-use authenticated client.
  • SASL mechanisms:
    • LOGIN, PLAIN, ANONYMOUS, XOAUTH2 and OAUTHBEARER built-in
    • SCRAM-SHA-256 (requires scram feature)

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.

RFC coverage

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)

Examples

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. Pass Some(&[]) to signal EOF.
  • WantsWrite(Vec<u8>): caller writes these bytes to the socket. The next call typically passes None.
  • Ok { … }: terminal success.
  • Err { … }: terminal failure.

As a no-std coroutine library

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:?}");
}

As a light std client (BYO stream)

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:?}");
}

As a full std client (TCP + TLS)

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 default
use 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.

More examples

Have a look at projects built on top of this library:

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

About

IMAP client library, written in Rust

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors