Expand description
§I/O Maildir

Maildir 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 contain the whole Maildir logic and can be used anywhere - Mid-level std client: a standard, blocking Maildir client built on
std::fs
§Table of contents
§Features
- I/O-free coroutines:
no_stdstate machines; no filesystem calls, no async runtime, nostdrequired, drive against any blocking, async, or fuzz harness. - Standard, blocking client (requires
clientfeature) backed bystd::fs. - Maildir delivery protocol: the entry-store coroutine writes to
/tmpfirst, then atomically renames into/curor/new, producing IDs of the shapesecs.#counter.M<nanos>P<pid>.<host>. - Maildir++ mode: optional dotted folder enumeration (
.Work.Foo) and inbox surfacing, gated by themaildirppstore flag. - Dovecot keywords resolution: read / write the
dovecot-keywordsslot table (a..zletters), gated by thedovecot_keywordsclient option. - Header round-trip for custom keywords: inject and strip
X-Keywords/X-Labelheaders, gated bykeywords_headerandstrip_headers.
[!TIP] I/O Maildir 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.
§Specification coverage
This library implements the Maildir format as I/O-agnostic coroutines.
| Coroutine | What it does |
|---|---|
MaildirCreate | Creates root, cur, new, tmp in lexicographic order |
MaildirDelete | Recursively removes a Maildir |
MaildirRename | Renames a Maildir within its parent directory |
MaildirList | Lists every valid Maildir inside a root directory |
MaildirEntryStore | Writes to /tmp, then atomically renames into /cur or /new with optional flags |
MaildirEntryGet | Locates an entry by ID and reads its contents |
MaildirEntryList | Scans both /new and /cur and returns every confirmed entry |
MaildirEntryCopy | Copies an entry between Maildirs |
MaildirEntryMove | Moves an entry between Maildirs |
MaildirEntryLocate | Finds an entry file by ID across cur, new and tmp |
MaildirFlagsAdd | Adds flags to an entry in /cur (no-op for /new and /tmp) |
MaildirFlagsRemove | Removes flags from an entry in /cur (no-op for /new and /tmp) |
MaildirFlagsSet | Replaces the flags of an entry in /cur (no-op for /new and /tmp) |
DovecotLoad | Reads the per-folder dovecot-keywords slot table |
DovecotStore | Writes a per-folder dovecot-keywords slot table |
§Usage
I/O Maildir 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 coroutine implements the MaildirCoroutine trait. Its resume(arg: Option<MaildirReply>) method returns a MaildirCoroutineState<Yield, Return> with two variants:
Yielded(Y): intermediate.YisMaildirYield, mixing filesystem step requests (WantsDirCreate,WantsDirRead,WantsDirRemove,WantsDirExists,WantsFileCreate,WantsFileRead,WantsFileExists,WantsRename,WantsCopy) with the three environmental inputs used by the delivery protocol to mint entry identifiers (WantsTime,WantsPid,WantsHostname).Complete(R): terminal. By conventionR = Result<Output, Error>carrying the operation’s final value.
The driver answers each Yielded(MaildirYield::Wants*) with the matching MaildirReply variant on the next resume.
§I/O-free coroutines
No features required: works in #![no_std], no filesystem calls, no async runtime. You own the loop and the syscalls; the library only computes the operations to perform and consumes their results.
Create a fresh Maildir against a blocking caller (the same shape works under async or in-memory replay):
use std::fs;
use io_maildir::{
coroutine::*,
maildir::create::MaildirCreate,
path::{FsPath, MaildirPath},
store::MaildirStore,
};
let store = MaildirStore { root: FsPath::new("/path/to/root"), maildirpp: false };
let name = MaildirPath::from("inbox");
let mut coroutine = MaildirCreate::new(&store, name);
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!("MaildirCreate yielded {other:?}"),
}
}Drive a multi-step command (store an entry) the same way:
use std::{
fs, process,
time::{SystemTime, UNIX_EPOCH},
};
use gethostname::gethostname;
use io_maildir::{
coroutine::*,
entry::store::{MaildirEntryStore, MaildirEntryStoreOutput},
flag::types::MaildirFlags,
maildir::types::{Maildir, MaildirSubdir},
path::FsPath,
};
let maildir = Maildir::from_path(FsPath::new("/path/to/root/inbox"));
let contents = b"From: alice@example.com\r\nSubject: Hello\r\n\r\nHello!\r\n".to_vec();
let mut coroutine = MaildirEntryStore::new(maildir, MaildirSubdir::New, MaildirFlags::default(), contents);
let mut arg: Option<MaildirReply> = None;
let MaildirEntryStoreOutput { id, path } = loop {
match coroutine.resume(arg.take()) {
MaildirCoroutineState::Complete(Ok(out)) => break out,
MaildirCoroutineState::Complete(Err(err)) => panic!("{err}"),
MaildirCoroutineState::Yielded(MaildirYield::WantsTime) => {
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
arg = Some(MaildirReply::Time { secs: ts.as_secs(), nanos: ts.subsec_nanos() });
}
MaildirCoroutineState::Yielded(MaildirYield::WantsPid) => {
arg = Some(MaildirReply::Pid(process::id()));
}
MaildirCoroutineState::Yielded(MaildirYield::WantsHostname) => {
arg = Some(MaildirReply::Hostname(gethostname().into_string().unwrap_or_default()));
}
MaildirCoroutineState::Yielded(MaildirYield::WantsFileCreate(files)) => {
for (path, bytes) in files {
fs::write(path.as_str(), &bytes).unwrap();
}
arg = Some(MaildirReply::FileCreate);
}
MaildirCoroutineState::Yielded(MaildirYield::WantsRename(pairs)) => {
for (from, to) in pairs {
fs::rename(from.as_str(), to.as_str()).unwrap();
}
arg = Some(MaildirReply::Rename);
}
MaildirCoroutineState::Yielded(other) => unreachable!("MaildirEntryStore yielded {other:?}"),
}
};
println!("stored {id} at {path}");§Std client
Enable the client feature (on by default). MaildirClient::new(root) wraps a filesystem root and exposes one method per coroutine; the resume loop is driven for you via MaildirClient::run and std::fs.
[dependencies]
io-maildir = "0.1.0" # client is enabled by defaultuse io_maildir::{
client::{MaildirClient, MaildirClientError},
flag::types::MaildirFlags,
maildir::types::MaildirSubdir,
};
let mut client = MaildirClient::new("/path/to/root");
// client.store.maildirpp = true; // opt into Maildir++ if needed
client.create_maildir("inbox")?;
let maildir = client.load_maildir("inbox")?;
let contents = b"From: alice@example.com\r\nSubject: Hello\r\n\r\nHello!\r\n".to_vec();
let (id, path) = client.store(maildir, MaildirSubdir::New, MaildirFlags::default(), contents)?;
println!("stored {id} at {path}");Logical mailbox names (“inbox”, “Archive/2024”) are translated to on-disk paths by client.store according to its maildirpp flag: in fs layout (default) “Archive/2024” becomes <root>/Archive/2024/; in Maildir++ it becomes <root>/.Archive.2024/.
§Examples
See 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
§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 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 spec references. The verification workflow catches most of this; it does not catch all of it. Bug reports are welcome and taken seriously.
-
Last reviewed: 05/06/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:
Modules§
- client
client - Standard blocking Maildir client driving any coroutine against
std::fs. - coroutine
- Generator-shape coroutine driver
- dovecot
- Dovecot-keywords sidecar: I/O-free load/store coroutines plus
pure parser/serialiser helpers under
utils. - entry
- Maildir entries: I/O-free coroutines for the delivery protocol
and entry lifecycle (store, get, list, locate, copy, move), plus
RFC 5322 header helpers under
headers. - flag
- Maildir flags: I/O-free coroutines rewriting the
:2,<flags>suffix on entry filenames, plus the flag set / keyword-header types undertypes. - maildir
- Maildir layout: I/O-free coroutines managing the cur/new/tmp
directory tree (create, delete, list, rename), plus the
Maildirhandle and subdir enum undertypes. - path
- Two path flavours used across the crate: literal filesystem paths
(
FsPath) and logical mailbox-hierarchy paths (MaildirPath). Acrate::store::MaildirStoretranslates between them under its configured layout (fs nested, or Maildir++ flat-dotted). - store
- Layout-aware view of a Maildir tree.
Macros§
- maildir_
try - Coroutine
?: forwardsYielded(viaInto), short-circuits onErr, evaluates to the innerOkvalue.
