daaki-imap 0.2.0

An IMAP4rev1/IMAP4rev2 async client library
Documentation

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 architectureImapConnection 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 FETCHuid_fetch_streaming(tx) pushes each FetchResponse through a channel as it arrives, avoiding OOM on large mailboxes.
  • Command pipeliningpipeline() 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<()> {
// Non-blocking drain of all pending events.
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}"),
        _ => {}
    }
}

// Or wait for the next event with a timeout.
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);

// Spawn a task to consume responses as they arrive.
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);

// Watch for new messages and expunges across the personal namespace,
// plus flag changes on the currently selected mailbox.
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,
            ],
        ),
    ],
    /* status = */ 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 { .. } => {
        // RFC 5465 Section 5.8: registration was discarded — re-subscribe.
        conn.notify_set(NotifySetParams::new(vec![], false), timeout).await?;
    }
    _ => {}
}

// Cancel later with `notify_none()`.
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.