# 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](#supported-extensions).
- **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 `TypedEvent`s 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

```toml
[dependencies]
daaki-imap = "0.2"
```

Connect, log in, and fetch the subjects of the first 5 messages in INBOX:

```rust,no_run
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 `TypedEvent`s. Poll them after any command or wait
for the next one:

```rust,no_run
# 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:

```rust,no_run
# 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:

```rust,no_run
# 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:

```rust,no_run
# 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:

```rust,no_run
# 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 `TypedEvent`s via `drain_events()` /
`next_event()`, or as `IdleEvent` variants while inside `idle()`:

```rust,no_run
# 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.
