Skip to main content

Crate io_email

Crate io_email 

Source
Expand description

§I/O Email Documentation Matrix Mastodon

Email client library, written in Rust.

This library is composed of 2 feature-gated layers:

  • Low-level I/O-free coroutines: these no_std-compatible state machines wrap the underlying io-imap, io-jmap, io-maildir, io-m2dir and io-smtp coroutines and surface a shared least-common-denominator type on completion
  • Mid-level std client: a standard, blocking unified client that dispatches the shared API across every registered backend

§Table of contents

§Features

  • Shared LCD types: Mailbox, Envelope, Address, Flag that fit IMAP, JMAP, Maildir, m2dir and SMTP.
  • I/O-free coroutines: no_std state machines per (backend, operation), wrapping the underlying io-* coroutine and producing a shared type on completion.
  • Unified std client (client feature): blocking dispatcher that routes shared-API calls to the highest-priority registered backend (Maildir → m2dir → JMAP → IMAP for storage, JMAP → SMTP for send).
  • TLS for the network backends (gated by the same rustls-ring / rustls-aws / native-tls features as the underlying io-* crates).
  • Optional search DSL (search feature) and serde round-trip on every shared type (serde feature).

[!TIP] I/O Email 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.

§Backend coverage

OperationIMAPJMAPMaildirm2dirSMTP
list_mailboxesyesyesyesyes
create_mailboxyesyesyesyes
delete_mailboxyesyesyesyes
diff_mailboxesyes
list_envelopesyesyesyesyes
search_envelopes (feature search)yesyesyesyes
diff_envelopesyesyes
watch_mailboxyesyes
get_messageyesyesyesyes
add_messageyesyesyesyes
copy_messagesyesyesyesyes
move_messagesyesyesyesyes
delete_messageyesyesyesyes
store_flagsyesyesyesyes
send_messageyesyes

§Usage

I/O Email can be consumed two 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 shared-API coroutine implements the backend trait of the protocol it targets (ImapCoroutine, JmapCoroutine, MaildirCoroutine, M2dirCoroutine, SmtpCoroutine). The resume(...) method returns the matching <Backend>CoroutineState<Yield, Return> with two variants:

  • Yielded(Y): intermediate. Y is the backend’s standard <Backend>Yield (WantsRead / WantsWrite for the network backends, WantsDirRead / WantsFileCreate / WantsRename etc. for the filesystem ones), plus a dedicated Event(WatchEvent) variant on watch coroutines.
  • Complete(R): terminal. By convention R = Result<Output, Error> carrying the operation’s final value typed against the shared Mailbox / Envelope / shared payload.

The std client owns the resume loop for you; the I/O-free mode hands it back so you can drive the same coroutine under any blocking, async, or fuzz harness.

§Coroutines

No client feature required: every wrapper lives under <domain>::<protocol>::<op> (for example mailbox::imap::list::ImapMailboxList, message::jmap::add::JmapMessageAdd) and is built straight from the shared inputs. You own the loop and the syscalls; the library only produces operations and consumes their results.

Create a fresh Maildir mailbox against a blocking caller (the same shape works under async or in-memory replay):

use std::fs;

use io_email::mailbox::maildir::create::MaildirMailboxCreate;
use io_maildir::{
    coroutine::*,
    path::{FsPath, MaildirPath},
    store::MaildirStore,
};

let store = MaildirStore { root: FsPath::new("/path/to/root"), maildirpp: false };

let mut coroutine = MaildirMailboxCreate::new(&store, "Archive").unwrap();
let mut arg: Option<MaildirReply> = None;

loop {
    match coroutine.resume(arg.take()) {
        MaildirCoroutineState::Complete(Ok(())) => break,
        MaildirCoroutineState::Complete(Err(err)) => panic!("{err}"),
        MaildirCoroutineState::Yielded(MaildirYield::WantsDirCreate(paths)) => {
            for path in paths {
                fs::create_dir_all(path.as_str()).unwrap();
            }
            arg = Some(MaildirReply::DirCreate);
        }
        MaildirCoroutineState::Yielded(other) => unreachable!("unexpected {other:?}"),
    }
}

println!("created Maildir mailbox Archive");

Network backends follow the same pattern but yield WantsRead / WantsWrite(Vec<u8>) instead; see io-imap, io-jmap and io-smtp for the full TCP / TLS / authentication setup that authenticates the stream before the wrapper coroutine runs.

§Std client

Enable the client feature (pulled in by every backend feature) and at least one backend. EmailClientStd::new() starts empty; with_<protocol>(client) plugs in an already-built per-protocol client, while connect_<protocol>(url, tls, ...) opens the connection through the underlying io-* crate and fills the slot in one shot.

[dependencies]
io-email = "0.1.0"
use io_email::{client::EmailClientStd, maildir::client::MaildirClient};
use pimalaya_stream::{sasl::SaslLogin, tls::Tls};
use secrecy::SecretString;
use url::Url;

let url = Url::parse("imaps://imap.example.com").unwrap();
let tls = Tls::default();
let sasl = SaslLogin {
    username: "alice@example.com".into(),
    password: SecretString::from("hunter2".to_owned()),
};

let mut client = EmailClientStd::new()
    .with_maildir(MaildirClient::new("/home/alice/Maildir"))
    .connect_imap(&url, &tls, false, Some(sasl), None)
    .unwrap();

for mbox in client.list_mailboxes(/* with_counts */ true).unwrap() {
    println!("{}: total={:?} unread={:?}", mbox.name, mbox.total, mbox.unread);
}

Dispatch priority on storage reads walks the registered backends Maildir → m2dir → JMAP → IMAP (local before network, cheap before expensive); send routes JMAP → SMTP. Pick which slots to fill based on the workload (local-first sync vs network-first transactional client).

§Examples

See complete examples at ./examples.

Have also a look at real-world projects built on top of this library:

§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: 06/06/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

Modules§

address
Email address shared across all protocols.
clientclient and (imap or jmap or m2dir or maildir or smtp)
Multi-protocol std-blocking email client.
envelope
flag
imapimap
jmapjmap
m2dirm2dir
mailbox
maildirmaildir
message
searchsearch
smtpsmtp