daaki-imap
A Rust IMAP client built with tokio & rustls with IMAP4rev1 and IMAP4rev2 support.
Highlights
- IMAP4rev1 and IMAP4rev2 — support for both RFC 3501 and RFC 9051.
- 35+ extensions — IDLE, NOTIFY, CONDSTORE/QRESYNC, MOVE, COMPRESS=DEFLATE, SORT, THREAD, OBJECTID, and many more.
- Driver-task architecture —
ImapConnection is a lightweight handle to a dedicated tokio task that owns the TCP/TLS stream. All methods take &self. Dropping any future mid-flight cannot corrupt the connection.
- Typed event queue — ALERTs, EXISTS/EXPUNGE, NOTIFY data, and other asynchronous server notifications arrive as
TypedEvents via drain_events() / next_event().
- Streaming FETCH —
uid_fetch_streaming(tx) pushes each FetchResponse through a channel as it arrives, avoiding OOM on large mailboxes.
- Command pipelining —
pipeline() returns a typed builder that batches commands into a single write and returns a heterogeneous tuple of per-command results.
- Cancellation-safe — dropping a future returned by the public API leaves the connection in a usable state. The driver task completes the in-flight command; only the result is abandoned.
- Async with explicit timeouts — built on tokio. Every operation takes a
Duration — no hidden infinite waits.
- TLS by default — rustls-based TLS with implicit TLS (port 993) and STARTTLS.
- Typed API — validated newtypes (
SequenceSet, MailboxName, ImapAtom, ObjectId), typed request enum (FetchAttr), and typed response structs/enums (CopyResult, MoveResult, StoreResult, ExpungeResult) catch errors at construction time.
- Zero unsafe code — enforced by
#![deny(unsafe_code)] crate-wide.
- Owned types everywhere — all public API types use
String/Vec<u8>.
- Optional serde — enable the
serde feature for Serialize/Deserialize on all public types.
Quick Start
[dependencies]
daaki-imap = "0.2"
Connect, log in, and fetch the subjects of the first 5 messages in INBOX:
use daaki_imap::{ImapConnection, TlsMode};
use std::time::Duration;
#[tokio::main]
async fn main() -> daaki_imap::Result<()> {
let timeout = Duration::from_secs(30);
let conn = ImapConnection::connect(
"imap.example.com", 993, TlsMode::Implicit, timeout,
).await?;
conn.login("user@example.com", "password", timeout).await?;
let mailbox = conn.select("INBOX", timeout).await?;
println!("INBOX has {} messages", mailbox.exists);
let seq = daaki_imap::types::SequenceSet::new("1:5")?;
let messages = conn.fetch(&seq, &[daaki_imap::types::FetchAttr::Envelope], timeout).await?;
for msg in &messages {
if let Some(env) = &msg.envelope {
println!("Subject: {}", env.subject.as_deref().unwrap_or("(none)"));
}
}
conn.logout().await?;
Ok(())
}
Events
Asynchronous server notifications — ALERTs, EXISTS/EXPUNGE changes, NOTIFY
data — are delivered as TypedEvents. Poll them after any command or wait
for the next one:
# use daaki_imap::{ImapConnection, TypedEvent};
# async fn example(conn: &ImapConnection) -> daaki_imap::Result<()> {
for event in conn.drain_events().await {
match event {
TypedEvent::Alert(text) => eprintln!("ALERT: {text}"),
TypedEvent::Exists(n) => println!("mailbox now has {n} messages"),
TypedEvent::Expunge(seq) => println!("message {seq} removed"),
TypedEvent::Bye { text, .. } => println!("server says goodbye: {text}"),
_ => {}
}
}
let timeout = std::time::Duration::from_secs(60);
if let Some(event) = conn.next_event(timeout).await? {
println!("got event: {event:?}");
}
# Ok(())
# }
Streaming FETCH
For large mailboxes, stream responses through a channel instead of
buffering them all in memory:
# use daaki_imap::{ImapConnection, types::{SequenceSet, FetchAttr, FetchResponse}};
# use std::time::Duration;
# async fn example(conn: &ImapConnection) -> daaki_imap::Result<()> {
let seq = SequenceSet::new("1:*")?;
let (tx, mut rx) = tokio::sync::mpsc::channel::<daaki_imap::Result<FetchResponse>>(64);
let consumer = tokio::spawn(async move {
while let Some(result) = rx.recv().await {
match result {
Ok(msg) => println!("UID {}: {} bytes", msg.uid.unwrap_or(0), msg.rfc822_size.unwrap_or(0)),
Err(e) => eprintln!("fetch error: {e}"),
}
}
});
conn.uid_fetch_streaming(&seq, &[FetchAttr::Uid, FetchAttr::Rfc822Size], tx, Duration::from_secs(120)).await?;
consumer.await.expect("consumer task panicked");
# Ok(())
# }
Pipelining
Batch multiple commands into a single write. The driver routes responses
by tag and returns a typed tuple of per-command results:
# use daaki_imap::{ImapConnection, types::MailboxName};
# use std::time::Duration;
# async fn example(conn: &ImapConnection) -> Result<(), Box<dyn std::error::Error>> {
let inbox = MailboxName::new("INBOX")?;
let drafts = MailboxName::new("Drafts")?;
let (result_a, result_b) = conn
.pipeline()
.status(inbox, "(MESSAGES UNSEEN)".to_string())
.status(drafts, "(MESSAGES)".to_string())
.execute()
.await?;
let status_a = result_a?;
let status_b = result_b?;
println!("INBOX: {status_a:?}, Drafts: {status_b:?}");
# Ok(())
# }
IDLE
Wait for new messages without polling. Pass a cancellation token to break
out from another task:
# use daaki_imap::{IdleEvent, ImapConnection};
# use tokio_util::sync::CancellationToken;
# use std::time::Duration;
# async fn example(conn: &ImapConnection) -> daaki_imap::Result<()> {
let cancel = CancellationToken::new();
match conn.idle(Duration::from_secs(300), cancel.clone()).await? {
IdleEvent::Exists(n) => println!("mailbox now has {n} messages"),
IdleEvent::Expunge(n) => println!("message {n} was deleted"),
IdleEvent::Timeout => println!("idle timed out"),
IdleEvent::Cancelled => println!("cancelled"),
_ => {}
}
# Ok(())
# }
COMPRESS=DEFLATE
Enable wire-level compression — all subsequent traffic is compressed
transparently:
# async fn example(conn: &daaki_imap::ImapConnection) -> daaki_imap::Result<()> {
conn.compress(std::time::Duration::from_secs(10)).await?;
# Ok(())
# }
NOTIFY
Subscribe to events on mailboxes other than the currently selected one
(RFC 5465). Notifications arrive as TypedEvents via drain_events() /
next_event(), or as IdleEvent variants while inside idle():
# use daaki_imap::{
# IdleEvent, ImapConnection, MailboxFilter, NotifyEvent, NotifyEventGroup,
# NotifySetParams,
# };
# use tokio_util::sync::CancellationToken;
# use std::time::Duration;
# async fn example(conn: &ImapConnection) -> daaki_imap::Result<()> {
let timeout = Duration::from_secs(30);
let params = NotifySetParams::new(
vec![
NotifyEventGroup::new(
MailboxFilter::Personal,
vec![NotifyEvent::MessageNew { fetch_attrs: vec![] }, NotifyEvent::MessageExpunge],
),
NotifyEventGroup::new(
MailboxFilter::Selected,
vec![
NotifyEvent::MessageNew { fetch_attrs: vec![] },
NotifyEvent::MessageExpunge,
NotifyEvent::FlagChange,
],
),
],
true,
);
conn.notify_set(params, timeout).await?;
let cancel = CancellationToken::new();
match conn.idle(Duration::from_secs(300), cancel.clone()).await? {
IdleEvent::MailboxStatus { mailbox, items } => {
println!("status changed for {mailbox}: {items:?}");
}
IdleEvent::MailboxEvent(info) => {
println!("mailbox event: {}", info.name);
}
IdleEvent::MetadataChange { mailbox, entries } => {
println!("metadata changed for {mailbox}: {entries:?}");
}
IdleEvent::NotificationOverflow { .. } => {
conn.notify_set(NotifySetParams::new(vec![], false), timeout).await?;
}
_ => {}
}
conn.notify_none(timeout).await?;
# Ok(())
# }
Supported Extensions
| Category |
Extensions |
RFCs |
| Real-time |
IDLE, NOTIFY |
RFC 2177, 5465 |
| Search & sort |
ESEARCH, SEARCHRES, SORT, SORT=DISPLAY, THREAD, WITHIN |
RFC 4731, 5182, 5256, 5957, 5032 |
| Mailbox management |
MOVE, UNSELECT, CHILDREN, SPECIAL-USE, $Important, LIST-EXTENDED, LIST-STATUS, STATUS=SIZE, NAMESPACE |
RFC 6851, 3691, 3348, 6154, 8457, 5258, 5819, 8438, 2342 |
| Message metadata |
CONDSTORE, QRESYNC, OBJECTID, SAVEDATE, PREVIEW |
RFC 7162, 8474, 8514, 8970 |
| Append |
MULTIAPPEND, APPENDLIMIT, UIDPLUS, LITERAL+, LITERAL- |
RFC 3502, 7889, 4315, 7888 |
| Encoding & transport |
COMPRESS=DEFLATE, BINARY, UTF8=ACCEPT, UTF8=ONLY |
RFC 4978, 3516, 6855 |
| Access control |
ACL, QUOTA, METADATA, METADATA-SERVER |
RFC 4314, 2087/9208, 5464 |
| Response codes |
Extended response codes |
RFC 5530 |
| Session & auth |
SASL-IR, ENABLE, ID, UNAUTHENTICATE |
RFC 4959, 5161, 2971, 8437 |
License
The contents of this package are licensed under the terms of the MIT license.